~andreafeletto/flussimetro

0d2d130ce709838f10e9699b4ff29dd08e4d95f6 — Andrea Feletto 1 year, 1 month ago 0582f23 main
rework add subscription page
6 files changed, 363 insertions(+), 160 deletions(-)

M lib/app.dart
M lib/models.dart
M lib/models.g.dart
M lib/page/subscription_add.dart
M pubspec.lock
M pubspec.yaml
M lib/app.dart => lib/app.dart +1 -7
@@ 26,13 26,7 @@ class App extends StatelessWidget {
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flussimetro',
      theme: ThemeData(
        brightness: Brightness.light,
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        brightness: Brightness.dark,
      ),
      theme: ThemeData.dark(),
      debugShowCheckedModeBanner: false,
      routerConfig: _router,
    );

M lib/models.dart => lib/models.dart +4 -1
@@ 62,8 62,11 @@ enum Currency {
@HiveType(typeId: 2)
enum Periodicity {
  @HiveField(0)
  yearly,
  weekly,

  @HiveField(1)
  monthly,

  @HiveField(2)
  yearly,
}

M lib/models.g.dart => lib/models.g.dart +8 -3
@@ 104,23 104,28 @@ class PeriodicityAdapter extends TypeAdapter<Periodicity> {
  Periodicity read(BinaryReader reader) {
    switch (reader.readByte()) {
      case 0:
        return Periodicity.yearly;
        return Periodicity.weekly;
      case 1:
        return Periodicity.monthly;
      default:
      case 2:
        return Periodicity.yearly;
      default:
        return Periodicity.weekly;
    }
  }

  @override
  void write(BinaryWriter writer, Periodicity obj) {
    switch (obj) {
      case Periodicity.yearly:
      case Periodicity.weekly:
        writer.writeByte(0);
        break;
      case Periodicity.monthly:
        writer.writeByte(1);
        break;
      case Periodicity.yearly:
        writer.writeByte(2);
        break;
    }
  }


M lib/page/subscription_add.dart => lib/page/subscription_add.dart +268 -149
@@ 1,11 1,22 @@
import 'package:favicon/favicon.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';
import 'package:transparent_image/transparent_image.dart';

import '../models.dart';

const dayNames = [
  'Monday',
  'Tuesday',
  'Wednesday',
  'Thursday',
  'Friday',
  'Saturday',
  'Sunday',
];

const monthNames = [
  'January',
  'February',


@@ 30,185 41,293 @@ class SubscriptionAddPage extends StatefulWidget {
}

class _SubscriptionAddPageState extends State<SubscriptionAddPage> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  final nameController = TextEditingController();
  final urlController = TextEditingController();
  final amountController = TextEditingController();

  bool formIsValid = false;
  Currency currency = Currency.eur;
  final currencyController = TextEditingController();
  Periodicity periodicity = Periodicity.monthly;
  int? startDay;
  int? startMonth;
  int? day;
  int? month;

  Favicon? favicon;

  @override
  void initState() {
    super.initState();

    urlController.addListener(_refreshFavicon);
  }

  @override
  void dispose() {
    nameController.dispose();
    urlController.dispose();
    amountController.dispose();

    super.dispose();
  }

  Future<void> _refreshFavicon() async {
    if (urlController.text.isEmpty) {
      setState(() {
        favicon = null;
      });
      return;
    }
    try {
      final uri = Uri.parse(urlController.text);
      final fav = await FaviconFinder.getBest(uri.toString());
      if (fav != null) {
        setState(() {
          favicon = fav;
        });
      }
    } catch (e) {
      return;
    }
  }

  @override
  Widget build(BuildContext context) {
    final startDayDropdown = DropdownButtonFormField(
      decoration: const InputDecoration(
        labelText: 'Day of the month',
      ),
      items: List.generate(
        31,
        (i) => DropdownMenuItem(
          value: i,
          child: Text((i + 1).toString()),
        ),
      ),
      onChanged: (int? value) => setState(() {
        startDay = value;
      }),
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('New Flow'),
        title: const Text('New Subscription'),
      ),
      floatingActionButton: formIsValid
          ? FloatingActionButton(
              tooltip: 'Confirm',
              onPressed: () {
                final box = Hive.box('flows');
                final flow = FlowEntry.create(
                  name: nameController.text,
                  url: urlController.text,
                  amount: amountController.text,
                  currency: currency,
                  periodicity: periodicity,
                  day: startDay!,
                  month: startMonth,
                );
                box.add(flow);
                context.go('/');
              },
              child: const Icon(Icons.check),
            )
          : null,
      body: Form(
        key: _formKey,
        onChanged: () => setState(() {
          formIsValid = _formKey.currentState!.validate();
        }),
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            children: [
              TextFormField(
                controller: nameController,
                decoration: const InputDecoration(
                  labelText: 'Subsctiption name',
      floatingActionButton: FloatingActionButton(
        tooltip: 'Confirm',
        onPressed: () {
          final box = Hive.box('flows');
          final flow = FlowEntry.create(
            name: nameController.text,
            url: urlController.text,
            amount: amountController.text,
            currency: Currency.eur,
            periodicity: periodicity,
            day: 0,
            month: 0,
          );
          box.add(flow);
          context.go('/');
        },
        child: const Icon(Icons.check),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Wrap(
          runSpacing: 24,
          children: [
            Row(
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Container(
                  width: 54,
                  height: 54,
                  decoration: BoxDecoration(
                    border: Border.all(
                      width: 1,
                      color: Theme.of(context).colorScheme.outline,
                    ),
                    borderRadius: const BorderRadius.all(Radius.circular(8)),
                  ),
                  child: Stack(
                    children: [
                      Center(
                        child: Text(
                          nameController.text.isNotEmpty
                              ? nameController.text[0]
                              : '',
                          textAlign: TextAlign.center,
                          style: const TextStyle(
                            fontSize: 12,
                          ),
                        ),
                      ),
                      if (favicon != null)
                        Center(
                          child: CircularProgressIndicator(
                            backgroundColor:
                                Theme.of(context).colorScheme.background,
                          ),
                        ),
                      if (favicon != null)
                        FadeInImage.memoryNetwork(
                          placeholder: kTransparentImage,
                          image: favicon!.url,
                        ),
                    ],
                  ),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a subscription name';
                  }
                  return null;
                },
              ),
              TextFormField(
                controller: urlController,
                decoration: const InputDecoration(
                  labelText: 'Website URL',
                const SizedBox(width: 16),
                Flexible(
                  child: TextField(
                    controller: nameController,
                    decoration: const InputDecoration(
                      labelText: 'Name',
                      floatingLabelBehavior: FloatingLabelBehavior.always,
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter a website URL';
                  }
                  return null;
                },
              ],
            ),
            TextField(
              controller: urlController,
              keyboardType: TextInputType.url,
              decoration: const InputDecoration(
                labelText: 'URL',
                floatingLabelBehavior: FloatingLabelBehavior.always,
                border: OutlineInputBorder(),
                isDense: true,
              ),
              TextFormField(
                controller: amountController,
                decoration: const InputDecoration(
                  labelText: 'Amount',
                ),
                keyboardType: const TextInputType.numberWithOptions(
                  signed: false,
                  decimal: true,
            ),
            Row(
              children: [
                Flexible(
                  child: TextField(
                    controller: amountController,
                    keyboardType: const TextInputType.numberWithOptions(
                      signed: false,
                      decimal: true,
                    ),
                    decoration: const InputDecoration(
                      labelText: 'Amount',
                      floatingLabelBehavior: FloatingLabelBehavior.always,
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                    inputFormatters: [
                      FilteringTextInputFormatter.allow(
                        RegExp(r'^\d+(\.\d{0,2})?'),
                      ),
                    ],
                  ),
                ),
                inputFormatters: [
                  FilteringTextInputFormatter.allow(
                    RegExp(r'^\d+(\.\d{0,2})?'),
                const SizedBox(width: 16),
                SizedBox(
                  width: 128,
                  child: TextField(
                    controller: currencyController,
                    decoration: const InputDecoration(
                      labelText: 'Currency',
                      floatingLabelBehavior: FloatingLabelBehavior.always,
                      border: OutlineInputBorder(),
                      isDense: true,
                    ),
                  ),
                ],
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter an amount';
                  }
                  return null;
                },
                ),
              ],
            ),
            InputDecorator(
              decoration: const InputDecoration(
                labelText: 'Periodicity',
                floatingLabelBehavior: FloatingLabelBehavior.always,
                border: OutlineInputBorder(),
                isDense: true,
              ),
              DropdownButtonFormField(
                value: currency,
                items: Currency.values
                    .map(
                      (value) => DropdownMenuItem(
                        value: value,
                        child: Text(
                          value.name.toUpperCase(),
                        ),
                      ),
                    )
              child: Column(
                children: Periodicity.values
                    .map((field) => ListTile(
                          title: Text(
                            field.name[0].toUpperCase() +
                                field.name.substring(1),
                          ),
                          leading: Radio(
                            value: field,
                            groupValue: periodicity,
                            onChanged: (value) => setState(() {
                              day = null;
                              month = null;
                              periodicity = value!;
                            }),
                          ),
                        ))
                    .toList(),
                onChanged: (Currency? value) => setState(() {
                  currency = value!;
                }),
              ),
              DropdownButtonFormField(
                decoration: const InputDecoration(
                  labelText: 'Periodicity',
            ),
            switch (periodicity) {
              Periodicity.weekly => DropdownButtonFormField(
                  decoration: const InputDecoration(
                    labelText: 'Day of the week',
                    floatingLabelBehavior: FloatingLabelBehavior.always,
                    border: OutlineInputBorder(),
                    isDense: true,
                  ),
                  items: List.generate(
                    dayNames.length,
                    (i) => DropdownMenuItem(
                      value: i,
                      child: Text(dayNames[i]),
                    ),
                  ),
                  value: day,
                  onChanged: (int? value) => setState(() {
                    day = value;
                  }),
                ),
              Periodicity.monthly => DropdownButtonFormField(
                  decoration: const InputDecoration(
                    labelText: 'Day of the month',
                    floatingLabelBehavior: FloatingLabelBehavior.always,
                    border: OutlineInputBorder(),
                    isDense: true,
                  ),
                  items: List.generate(
                    31,
                    (i) => DropdownMenuItem(
                      value: i,
                      child: Text(i.toString()),
                    ),
                  ),
                  value: day,
                  onChanged: (int? value) => setState(() {
                    day = value;
                  }),
                ),
                items: Periodicity.values
                    .map(
                      (value) => DropdownMenuItem(
                        value: value,
                        child: Text(toBeginningOfSentenceCase(value.name)!),
              Periodicity.yearly => Wrap(
                  runSpacing: 24,
                  children: [
                    DropdownButtonFormField(
                      decoration: const InputDecoration(
                        labelText: 'Day of the month',
                        floatingLabelBehavior: FloatingLabelBehavior.always,
                        border: OutlineInputBorder(),
                        isDense: true,
                      ),
                    )
                    .toList(),
                value: periodicity,
                onChanged: (Periodicity? value) => setState(() {
                  periodicity = value!;
                }),
                validator: (value) {
                  if (value == null) {
                    return 'Please select a periodicity';
                  }
                  return null;
                },
              ),
              switch (periodicity) {
                Periodicity.monthly => startDayDropdown,
                Periodicity.yearly => Column(
                    children: [
                      startDayDropdown,
                      DropdownButtonFormField(
                        decoration: const InputDecoration(
                          labelText: 'Month',
                      items: List.generate(
                        31,
                        (i) => DropdownMenuItem(
                          value: i,
                          child: Text(i.toString()),
                        ),
                        items: List.generate(
                          monthNames.length,
                          (i) => DropdownMenuItem(
                            value: i,
                            child: Text(monthNames[i]),
                          ),
                      ),
                      value: day,
                      onChanged: (int? value) => setState(() {
                        day = value;
                      }),
                    ),
                    DropdownButtonFormField(
                      decoration: const InputDecoration(
                        labelText: 'Month',
                        floatingLabelBehavior: FloatingLabelBehavior.always,
                        border: OutlineInputBorder(),
                        isDense: true,
                      ),
                      items: List.generate(
                        monthNames.length,
                        (i) => DropdownMenuItem(
                          value: i,
                          child: Text(monthNames[i]),
                        ),
                        onChanged: (int? value) => setState(() {
                          startMonth = value;
                        }),
                      ),
                    ],
                  ),
              }
            ],
          ),
                      value: month,
                      onChanged: (int? value) => setState(() {
                        month = value;
                      }),
                    ),
                  ],
                ),
            }
          ],
        ),
      ),
    );

M pubspec.lock => pubspec.lock +80 -0
@@ 17,6 17,14 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "5.13.0"
  archive:
    dependency: transitive
    description:
      name: archive
      sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
      url: "https://pub.dev"
    source: hosted
    version: "3.3.7"
  args:
    dependency: transitive
    description:


@@ 161,6 169,14 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "3.0.3"
  csslib:
    dependency: transitive
    description:
      name: csslib
      sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
      url: "https://pub.dev"
    source: hosted
    version: "1.0.0"
  cupertino_icons:
    dependency: "direct main"
    description:


@@ 185,6 201,14 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "1.3.1"
  favicon:
    dependency: "direct main"
    description:
      name: favicon
      sha256: "627fa31a9de0e791501a214a4418cd03681e1a259e159a75c8a6c28b83817b02"
      url: "https://pub.dev"
    source: hosted
    version: "1.1.1"
  ffi:
    dependency: transitive
    description:


@@ 293,6 317,22 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "2.0.0"
  html:
    dependency: transitive
    description:
      name: html
      sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
      url: "https://pub.dev"
    source: hosted
    version: "0.15.4"
  http:
    dependency: transitive
    description:
      name: http
      sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
      url: "https://pub.dev"
    source: hosted
    version: "0.13.6"
  http_multi_server:
    dependency: transitive
    description:


@@ 309,6 349,14 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "4.0.2"
  image:
    dependency: transitive
    description:
      name: image
      sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf
      url: "https://pub.dev"
    source: hosted
    version: "4.0.17"
  intl:
    dependency: "direct main"
    description:


@@ 453,6 501,14 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "2.1.7"
  petitparser:
    dependency: transitive
    description:
      name: petitparser
      sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
      url: "https://pub.dev"
    source: hosted
    version: "5.4.0"
  platform:
    dependency: transitive
    description:


@@ 469,6 525,14 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "2.1.4"
  pointycastle:
    dependency: transitive
    description:
      name: pointycastle
      sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
      url: "https://pub.dev"
    source: hosted
    version: "3.7.3"
  pool:
    dependency: transitive
    description:


@@ 650,6 714,14 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "1.0.1"
  transparent_image:
    dependency: "direct main"
    description:
      name: transparent_image
      sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f
      url: "https://pub.dev"
    source: hosted
    version: "2.0.1"
  typed_data:
    dependency: transitive
    description:


@@ 698,6 770,14 @@ packages:
      url: "https://pub.dev"
    source: hosted
    version: "1.0.1"
  xml:
    dependency: transitive
    description:
      name: xml
      sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
      url: "https://pub.dev"
    source: hosted
    version: "6.3.0"
  yaml:
    dependency: transitive
    description:

M pubspec.yaml => pubspec.yaml +2 -0
@@ 18,6 18,8 @@ dependencies:
  intl: ^0.18.0
  shared_preferences: ^2.2.0
  go_router: ^10.0.0
  favicon: ^1.1.1
  transparent_image: ^2.0.1

dev_dependencies:
  flutter_test: