@@ 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;
+ }),
+ ),
+ ],
+ ),
+ }
+ ],
),
),
);