From 7d9b1bf80ec8b1275f46359769af71a0ec6f4c8a Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Sat, 24 Feb 2024 07:51:21 +0100 Subject: [PATCH 1/8] Update dependencies for new Flutter 3.19 / Web release --- pubspec.lock | 144 ++++++++++++++++++++++++++++++--------------------- pubspec.yaml | 49 +++++++++--------- 2 files changed, 109 insertions(+), 84 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 8c8433d..ea3f28d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "1a52f1afae8ab7ac4741425114713bdbba802f1ce1e0648e167ffcc6e05e96cf" + sha256: "737321f9be522620ed3794937298fb0027a48a402624fa2500f7532f94aea810" url: "https://pub.dev" source: hosted - version: "1.3.21" + version: "1.3.22" analyzer: dependency: transitive description: @@ -293,18 +293,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" easy_localization: dependency: "direct main" description: name: easy_localization - sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 + sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" easy_logger: dependency: transitive description: @@ -373,50 +373,50 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: edb9f9eaecf0e6431e5c12b7fabdb68be3e85ce51f941ccbfa6cb71327e8b535 + sha256: "54681f6a8c35ec782c86680919953edbae66517a718fe7606a7ba52cfa1b36ca" url: "https://pub.dev" source: hosted - version: "10.8.5" + version: "10.8.6" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: de4a54353cf58412c6da6b660a0dbad8efacb33b345c0286bc3a2edb869124d8 + sha256: cf7378f407c26fd1f1cc808c0d791f2a15dd4f1fe0626bb2ce6a96663c4d8470 url: "https://pub.dev" source: hosted - version: "3.9.5" + version: "3.9.6" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "77e4c02ffd0204ccc7856221193265c807b7e056fa62855f973a7f77435b5d41" + sha256: f62c7a2514771f6e122c5b55227c0c7fdaa253cc2fe6efce768897c0fb68a5ab url: "https://pub.dev" source: hosted - version: "0.5.5+17" + version: "0.5.5+18" firebase_auth: dependency: "direct main" description: name: firebase_auth - sha256: "549f8ceb8cfc1920f85dea0ab73fb7dc209ee8182916b252eda342786c33369d" + sha256: "06863a0031b5f1b11ecac2c7d93c026df039a1379224a6671c59f528397d19e1" url: "https://pub.dev" source: hosted - version: "4.17.4" + version: "4.17.5" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "83bfc14649f673db17ad0bffaa0222019f99f3ddf499bcc8b46e1eb3443d3e08" + sha256: dba259dee045b706112c3d7619b01c2ad338d86cc492df52dd2073584a64de66 url: "https://pub.dev" source: hosted - version: "7.1.4" + version: "7.1.5" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: d2266452698dd5f6e522408dacfa06bb7f9703b5bdd11498fce2812ded50805b + sha256: ea47c590d0da836ae7253fadb95546b4595fbc59a5c50583e0fb5a1c5c476aff url: "https://pub.dev" source: hosted - version: "5.9.4" + version: "5.9.5" firebase_core: dependency: "direct main" description: @@ -445,66 +445,66 @@ packages: dependency: "direct main" description: name: firebase_crashlytics - sha256: efd096e4c3d2c568e128505b6e4ce5f5d5a1629f700a4d6fee6bd25b85937dde + sha256: "4c754db28a7daabe03c4cbf1079dbe81e6f0681284fed6d07e0e640b7f1027c4" url: "https://pub.dev" source: hosted - version: "3.4.14" + version: "3.4.15" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "225a54d834a118be262c1f1096d407515e35b99d9b474c987abdcff7663f2b81" + sha256: "08c5d7b5f93dbad7306d26702935abd8b579313ea256eb27006562a1867df249" url: "https://pub.dev" source: hosted - version: "3.6.21" + version: "3.6.22" firebase_database: dependency: "direct main" description: name: firebase_database - sha256: "3beb37b5869e68dba67fc648a9ee2398bb5284d9e94d513e84bb85b1b010048e" + sha256: d8f58548ab06b0b4dc79bb01e6018e31436c0f44c54c6ad821eec6f6b90d19cb url: "https://pub.dev" source: hosted - version: "10.4.5" + version: "10.4.6" firebase_database_platform_interface: dependency: transitive description: name: firebase_database_platform_interface - sha256: "9d5f25225a17200d59470da1d5c0646c75aaa73c5df6f920e36bae1c60e0b83d" + sha256: "783fa084f57a231366121ad9b66ae6efd643b10f50ac6a53200eeeeeb88a2879" url: "https://pub.dev" source: hosted - version: "0.2.5+21" + version: "0.2.5+22" firebase_database_web: dependency: transitive description: name: firebase_database_web - sha256: "751c047a362fad3d2fea60b1d97bbb2f4515097b641a5b38ab5aa5ba0e1afd26" + sha256: c3308b70ab02d58a7f4fdc6dfe2b25ccee374758fa7d2c0e509e88ba2a846332 url: "https://pub.dev" source: hosted - version: "0.2.3+21" + version: "0.2.3+22" firebase_storage: dependency: "direct main" description: name: firebase_storage - sha256: b87029b506972987a827feaf296c21cd0fe1bb69c2595be1672253ba5205573e + sha256: a07433d3ffe948f7a10086e4b0bd3a940be4ed873cffd24746eed1896b568369 url: "https://pub.dev" source: hosted - version: "11.6.5" + version: "11.6.6" firebase_storage_platform_interface: dependency: transitive description: name: firebase_storage_platform_interface - sha256: "180822103b164d0d597131f2fb658cd1c438148abafc6f2256b565227303ba35" + sha256: af99d9fcf058d6f3a591cea899cad054ded7adb21fb1788dfe65c3e223196b15 url: "https://pub.dev" source: hosted - version: "5.1.8" + version: "5.1.9" firebase_storage_web: dependency: transitive description: name: firebase_storage_web - sha256: "9523c455521b0497ee436be8614aab52f719309d16147a5b11091e44e4c5aa0a" + sha256: "06a684016de9609928865798dece0f7d34782a15a76f5ac08bd3a8780035b0b2" url: "https://pub.dev" source: hosted - version: "3.6.22" + version: "3.7.0" fixnum: dependency: transitive description: @@ -583,10 +583,10 @@ packages: dependency: "direct main" description: name: flutter_platform_widgets - sha256: "4970c211af1dad0a161e6379d04de2cace80283da0439f2f87d31a541f9b2b84" + sha256: c483c0591d845d2adb84e341a1cfb746f1a8a7aff4c72a5957772446020601f4 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -615,10 +615,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -829,6 +829,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -857,34 +881,34 @@ packages: dependency: "direct main" description: name: lottie - sha256: "1f0ce68112072d66ea271a9841994fa8d16442e23d8cf8996c9fa74174e58b4e" + sha256: ce2bb2605753915080e4ee47f036a64228c88dc7f56f7bc1dbe912d75b55b1e2 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -921,10 +945,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -1342,10 +1366,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.2.5" url_launcher_android: dependency: transitive description: @@ -1414,26 +1438,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1459,13 +1483,13 @@ packages: source: hosted version: "1.1.0" web: - dependency: transitive + dependency: "direct main" description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.2" web_socket_channel: dependency: transitive description: @@ -1507,5 +1531,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.3.0 <4.0.0" flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8a8497c..d34b838 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,48 +12,49 @@ dependencies: flutter: sdk: flutter - shared_preferences: ^2.2.1 - flutter_platform_widgets: ^6.0.2 - flutter_svg: ^2.0.7 - lottie: ^3.0.0 + shared_preferences: ^2.2.2 + flutter_platform_widgets: ^6.1.0 + flutter_svg: ^2.0.10+1 + lottie: ^3.1.0 google_fonts: ^6.1.0 # Firebase - firebase_crashlytics: ^3.3.6 - firebase_database: ^10.2.6 - firebase_analytics: ^10.5.0 - firebase_auth: ^4.10.0 - firebase_core: ^2.16.0 - firebase_storage: ^11.2.7 + firebase_crashlytics: ^3.4.15 + firebase_database: ^10.4.6 + firebase_analytics: ^10.8.6 + firebase_auth: ^4.17.5 + firebase_core: ^2.25.4 + firebase_storage: ^11.6.6 - url_launcher: ^6.1.14 - cached_network_image: ^3.3.0 - uuid: ^4.1.0 - dio: ^5.3.3 + url_launcher: ^6.2.5 + cached_network_image: ^3.3.1 + uuid: ^4.3.3 + dio: ^5.4.1 pretty_dio_logger: ^1.3.1 percent_indicator: ^4.2.3 email_validator: ^2.1.17 - flutter_riverpod: ^2.4.0 - riverpod_annotation: ^2.1.5 - collection: ^1.17.2 - go_router: ^13.1.0 + flutter_riverpod: ^2.4.10 + riverpod_annotation: ^2.3.4 + collection: ^1.18.0 + go_router: ^13.2.0 flutter_barcode_scanner: ^2.0.0 settings_ui: ^2.0.2 logger: ^2.0.2+1 - easy_localization: ^3.0.3 + easy_localization: ^3.0.4 rxdart: ^0.27.7 freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 intl: ^0.18.1 carousel_slider: ^4.2.1 - flex_color_picker: ^3.3.0 + flex_color_picker: ^3.3.1 timeline_tile: ^2.0.0 expandable: ^5.0.1 googleapis: ^12.0.0 - google_sign_in: ^6.1.6 - extension_google_sign_in_as_googleapis_auth: ^2.0.11 - fl_chart: ^0.66.1 + google_sign_in: ^6.2.1 + extension_google_sign_in_as_googleapis_auth: ^2.0.12 + fl_chart: ^0.66.2 flutter_staggered_grid_view: ^0.7.0 - share_plus: ^7.2.1 + share_plus: ^7.2.2 + web: ^0.4.2 dev_dependencies: flutter_test: From be64b1e3c614597d02ba2501264c95a3af498f68 Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Sat, 24 Feb 2024 12:30:31 +0100 Subject: [PATCH 2/8] Basic layout for manually adding books --- lib/src/providers/app_router.dart | 11 ++ lib/src/ui/add/manual_add_edit_book_page.dart | 182 ++++++++++++++++++ lib/src/ui/core/dante_app_bar.dart | 4 +- lib/src/ui/core/dante_components.dart | 2 +- web/index.html | 2 +- 5 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 lib/src/ui/add/manual_add_edit_book_page.dart diff --git a/lib/src/providers/app_router.dart b/lib/src/providers/app_router.dart index 72fbd34..7819447 100644 --- a/lib/src/providers/app_router.dart +++ b/lib/src/providers/app_router.dart @@ -1,4 +1,5 @@ import 'package:dantex/src/providers/authentication.dart'; +import 'package:dantex/src/ui/add/manual_add_edit_book_page.dart'; import 'package:dantex/src/ui/add/scan_book_page.dart'; import 'package:dantex/src/ui/book/book_detail_page.dart'; import 'package:dantex/src/ui/book/book_notes_page.dart'; @@ -157,6 +158,11 @@ List _mainRoutes = [ builder: (BuildContext context, GoRouterState state) => const ScanBookPage(), ), + GoRoute( + path: DanteRoute.manualAdd.url, + builder: (BuildContext context, GoRouterState state) => + ManualAddEditBookPage(), + ), GoRoute( path: DanteRoute.bookDetail.url, builder: (context, state) { @@ -199,6 +205,11 @@ enum DanteRoute { mobileUrl: 'scan', navigationUrl: '/scan', ), + manualAdd( + webUrl: '/add-manual', + mobileUrl: 'add-manual', + navigationUrl: '/add-manual', + ), settings( webUrl: '/settings', mobileUrl: 'settings', diff --git a/lib/src/ui/add/manual_add_edit_book_page.dart b/lib/src/ui/add/manual_add_edit_book_page.dart new file mode 100644 index 0000000..be9c066 --- /dev/null +++ b/lib/src/ui/add/manual_add_edit_book_page.dart @@ -0,0 +1,182 @@ +import 'package:dantex/src/data/book/entity/book_state.dart'; +import 'package:dantex/src/ui/core/dante_components.dart'; +import 'package:dantex/src/ui/core/themed_app_bar.dart'; +import 'package:dantex/src/util/layout_utils.dart'; +import 'package:flutter/material.dart'; + +/// TODO +/// 2. Manual Add book +/// -[ ] Web +/// -[ ] Translations +/// -[ ] App +/// -[ ] Dark Mode +/// 3. Upload image +/// 4. Entry points for edit book: +/// - Overflow +/// - Detail Page +/// 4. Edit existing book +/// 5. Test +/// - Web +/// - App +class ManualAddEditBookPage extends StatelessWidget { + final TextEditingController _titleController = TextEditingController(); + final TextEditingController _authorController = TextEditingController(); + final TextEditingController _pageController = TextEditingController(); + final TextEditingController _subtitleController = TextEditingController(); + final TextEditingController _isbnController = TextEditingController(); + final TextEditingController _summaryController = TextEditingController(); + + ManualAddEditBookPage({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ThemedAppBar( + // TODO + title: Text('Book Title goes here'), + ), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return switch (getDeviceFormFactor(constraints)) { + DeviceFormFactor.desktop => _buildDesktopLayout(context), + _ => _buildMobileLayout(context), + }; + }, + ), + ), + Container( + color: Theme.of(context).colorScheme.tertiaryContainer, + height: 100, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildStateButton(BookState.readLater), + _buildStateButton(BookState.reading), + _buildStateButton(BookState.read), + ], + ), + ), + ], + ); + } + + Widget _buildDesktopLayout(BuildContext context) { + return Container( + padding: const EdgeInsets.all(64), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: _buildRequiredInformationCard(isDesktop: true), + ), + const SizedBox(width: 64), + Expanded( + child: _buildOptionalInformationCard(isDesktop: true), + ), + ], + ), + ); + } + + // TODO + Widget _buildMobileLayout(BuildContext context) { + return ListView( + shrinkWrap: true, + children: [ + _buildRequiredInformationCard(isDesktop: false), + _buildOptionalInformationCard(isDesktop: false), + ], + ); + } + + Widget _buildRequiredInformationCard({ required bool isDesktop }) { + return Card( + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + if (isDesktop)...[ + const Spacer(), + Text('Image'), + const Spacer(), + ], + Row( + children: [ + if (!isDesktop)...[ + Text('Image'), + const SizedBox(width: 16), + ], + Expanded( + child: DanteTextField( + controller: _titleController, + hint: 'Title', + ), + ), + ], + ), + const SizedBox(height: 16), + DanteTextField( + controller: _authorController, + hint: 'Authors', + ), + const SizedBox(height: 16), + DanteTextField( + controller: _pageController, + hint: 'Page Count', + ), + if (isDesktop) + const Spacer(), + ], + ), + ), + ); + } + + Widget _buildOptionalInformationCard({required bool isDesktop}) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Optional information'), + const SizedBox(height: 32), + DanteTextField( + controller: _subtitleController, + hint: 'Subtitle', + ), + const SizedBox(height: 16), + Text('DatePicker PublishDate'), + const SizedBox(height: 16), + Text('Language'), + const SizedBox(height: 16), + DanteTextField( + controller: _isbnController, + hint: 'ISBN', + ), + const SizedBox(height: 16), + DanteTextField( + controller: _summaryController, + hint: 'Summary', + maxLines: 5, + ), + ], + ) + ), + ); + } + + Widget _buildStateButton(BookState state) { + return SizedBox( + width: 140, + child: OutlinedButton( + onPressed: () {}, + // icon: state.icon, + child: Text(state.name), + ), + ); + } +} diff --git a/lib/src/ui/core/dante_app_bar.dart b/lib/src/ui/core/dante_app_bar.dart index 60854fa..efe6b80 100644 --- a/lib/src/ui/core/dante_app_bar.dart +++ b/lib/src/ui/core/dante_app_bar.dart @@ -73,7 +73,7 @@ class DanteAppBar extends ConsumerWidget implements PreferredSizeWidget { const SizedBox(width: 16), TextButton( onPressed: () { - // TODO Support manually adding books + context.go(DanteRoute.manualAdd.navigationUrl); }, child: _AddActionItem( text: 'add_book.manual'.tr(), @@ -148,7 +148,7 @@ class DanteAppBar extends ConsumerWidget implements PreferredSizeWidget { await _handleQueryAction(context); break; case AddBookAction.manual: - // TODO Support manually adding books + context.go(DanteRoute.manualAdd.navigationUrl); break; } } diff --git a/lib/src/ui/core/dante_components.dart b/lib/src/ui/core/dante_components.dart index aedb1a5..1c31e4e 100644 --- a/lib/src/ui/core/dante_components.dart +++ b/lib/src/ui/core/dante_components.dart @@ -80,7 +80,7 @@ class DanteTextField extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(5.0)), borderSide: BorderSide( color: Theme.of(context).colorScheme.secondary, - width: 2, + width: 1, ), ), errorBorder: OutlineInputBorder( diff --git a/web/index.html b/web/index.html index 102b719..be0ee6d 100644 --- a/web/index.html +++ b/web/index.html @@ -30,7 +30,7 @@ - Dante + Dante - Book Tracker From 4f2a70316cb5083ad2f661b995c48d19e9018337 Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Sat, 24 Feb 2024 13:21:33 +0100 Subject: [PATCH 3/8] Implement LanguagePicker --- assets/translations/de-DE.json | 38 ++++++ assets/translations/en-US.json | 38 ++++++ lib/src/data/core/language.dart | 98 ++++++++++++++++ lib/src/ui/add/language_picker.dart | 111 ++++++++++++++++++ lib/src/ui/add/manual_add_edit_book_page.dart | 84 ++++++++----- pubspec.lock | 24 ++++ pubspec.yaml | 1 + 7 files changed, 367 insertions(+), 27 deletions(-) create mode 100644 lib/src/data/core/language.dart create mode 100644 lib/src/ui/add/language_picker.dart diff --git a/assets/translations/de-DE.json b/assets/translations/de-DE.json index a57d4cc..9292b92 100644 --- a/assets/translations/de-DE.json +++ b/assets/translations/de-DE.json @@ -6,6 +6,44 @@ "query": "Titel suche", "scan": "Buchode scannen" }, + "add-manual": { + "title": "Buchtitel", + "authors": "Autor(en)", + "page-count": "Seitenzahl", + "optional-info": "Optionale Informationen", + "subtitle": "Untertitel", + "isbn": "ISBN Buchnummer", + "summary": "Zusammenfassung" + }, + "language-picker": { + "title": "Sprache", + "empty": "Wähle eine Sprache" + }, + "countries": { + "not-available": "Nicht vorhanden", + "us": "Englisch", + "de": "Deutsch", + "it": "Italienisch", + "fr": "Französisch", + "es": "Spanisch", + "pt": "Portugiesisch", + "nl": "Holländisch", + "se": "Swedisch", + "dk": "Dänisch", + "no": "Norwegisch", + "fi": "Finnisch", + "tr": "Türkisch", + "ch": "Chinesisch", + "ua": "Ukrainisch", + "ru": "Russisch", + "pl": "Polisch", + "ro": "Rumänisch", + "bg": "Bulgarisch", + "hr": "Kroatisch", + "hu": "Ungarisch", + "id": "Indonesisch", + "th": "Thai" + }, "add_label_bottom_sheet": { "choose_a_color": "Wähle eine Farbe", "create_new_label": "Neues Label", diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index bd51d96..84fac4b 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -5,6 +5,44 @@ "query": "Title search", "scan": "Scan book" }, + "add-manual": { + "title": "Book Title", + "authors": "Author(s)", + "page-count": "Page Count", + "optional-info": "Optional Information", + "subtitle": "Subtitle", + "isbn": "ISBN number", + "summary": "Summary" + }, + "language-picker": { + "title": "Language", + "empty": "Choose a language" + }, + "countries": { + "not-available": "Not specified", + "us": "English", + "de": "German", + "it": "Italian", + "fr": "French", + "es": "Spanish", + "pt": "Portuguese", + "nl": "Dutch", + "se": "Swedish", + "dk": "Danish", + "no": "Norwegian", + "fi": "Finnish", + "tr": "Turkish", + "ch": "Chinese", + "ua": "Ukrainian", + "ru": "Russian", + "pl": "Polish", + "ro": "Romanian", + "bg": "Bulgarian", + "hr": "Croatian", + "hu": "Hungarian", + "id": "Indonesian", + "th": "Thai" + }, "add_label_bottom_sheet": { "choose_a_color": "Choose a color", "create_new_label": "Create new label", diff --git a/lib/src/data/core/language.dart b/lib/src/data/core/language.dart new file mode 100644 index 0000000..88e3a5e --- /dev/null +++ b/lib/src/data/core/language.dart @@ -0,0 +1,98 @@ +enum Language { + na( + countryCode: null, + translationKey: 'countries.not-available', + ), + english( + countryCode: 'US', + translationKey: 'countries.us', + ), + german( + countryCode: 'DE', + translationKey: 'countries.de', + ), + italian( + countryCode: 'IT', + translationKey: 'countries.it', + ), + french( + countryCode: 'FR', + translationKey: 'countries.fr', + ), + spanish( + countryCode: 'ES', + translationKey: 'countries.es', + ), + portuguese( + countryCode: 'PT', + translationKey: 'countries.pt', + ), + dutch( + countryCode: 'NL', + translationKey: 'countries.nl', + ), + chinese( + countryCode: 'CH', + translationKey: 'countries.ch', + ), + ukrainian( + countryCode: 'UA', + translationKey: 'countries.ua', + ), + swedish( + countryCode: 'SE', + translationKey: 'countries.se', + ), + danish( + countryCode: 'DK', + translationKey: 'countries.dk', + ), + norwegian( + countryCode: 'NO', + translationKey: 'countries.no', + ), + polish( + countryCode: 'PL', + translationKey: 'countries.pl', + ), + rumanian( + countryCode: 'RO', + translationKey: 'countries.ro', + ), + bulgarian( + countryCode: 'BG', + translationKey: 'countries.bg', + ), + croatian( + countryCode: 'HR', + translationKey: 'countries.hr', + ), + hungarian( + countryCode: 'HU', + translationKey: 'countries.hu', + ), + turkish( + countryCode: 'TR', + translationKey: 'countries.tr', + ), + russian( + countryCode: 'RU', + translationKey: 'countries.ru', + ), + indonesian( + countryCode: 'ID', + translationKey: 'countries.id', + ), + thai( + countryCode: 'TH', + translationKey: 'countries.th', + ); + + final String? countryCode; + final String translationKey; + + const Language({ + required this.countryCode, + required this.translationKey, + }); +} diff --git a/lib/src/ui/add/language_picker.dart b/lib/src/ui/add/language_picker.dart new file mode 100644 index 0000000..2b44370 --- /dev/null +++ b/lib/src/ui/add/language_picker.dart @@ -0,0 +1,111 @@ +import 'package:country_flags/country_flags.dart'; +import 'package:dantex/src/data/core/language.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class LanguagePicker extends StatefulWidget { + final Language preSelectedLanguage; + final Function(Language language) onLanguageSelected; + + const LanguagePicker({ + required this.onLanguageSelected, + this.preSelectedLanguage = Language.na, + super.key, + }); + + @override + State createState() => _LanguagePickerState(); +} + +class _LanguagePickerState extends State { + final List _supportedLanguages = Language.values; + Language _selectedLanguage = Language.na; + + @override + void initState() { + _selectedLanguage = widget.preSelectedLanguage; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'language-picker.title'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + Container( + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + border: Border.all(width: 0.7), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + hint: Text( + 'language-picker.empty'.tr(), + textAlign: TextAlign.center, + ), + icon: Icon( + Icons.arrow_drop_down, + color: Theme.of(context).colorScheme.onSurface, + ), + iconSize: 30, + value: _selectedLanguage, + items: _supportedLanguages.map( + (Language language) { + return DropdownMenuItem( + value: language, + child: Row( + children: [ + const SizedBox(width: 12), + _buildCountryFlag(language), + const SizedBox(width: 16), + Text( + language.translationKey.tr(), + ), + ], + ), + ); + }, + ).toList(), + onChanged: (Language? language) { + if (language != null) { + widget.onLanguageSelected(language); + + setState(() { + _selectedLanguage = language; + }); + } + }, + ), + ), + ), + ], + ); + } + + Widget _buildCountryFlag(Language language) { + if (language.countryCode == null) { + return const SizedBox( + width: 48, + child: Center( + child: Text('NA'), + ), + ); + } + + return CountryFlag.fromCountryCode( + language.countryCode!, + height: 48, + width: 48, + borderRadius: 12, + ); + } +} diff --git a/lib/src/ui/add/manual_add_edit_book_page.dart b/lib/src/ui/add/manual_add_edit_book_page.dart index be9c066..ec0864f 100644 --- a/lib/src/ui/add/manual_add_edit_book_page.dart +++ b/lib/src/ui/add/manual_add_edit_book_page.dart @@ -1,7 +1,10 @@ import 'package:dantex/src/data/book/entity/book_state.dart'; +import 'package:dantex/src/data/core/language.dart'; +import 'package:dantex/src/ui/add/language_picker.dart'; import 'package:dantex/src/ui/core/dante_components.dart'; import 'package:dantex/src/ui/core/themed_app_bar.dart'; import 'package:dantex/src/util/layout_utils.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; /// TODO @@ -18,7 +21,14 @@ import 'package:flutter/material.dart'; /// 5. Test /// - Web /// - App -class ManualAddEditBookPage extends StatelessWidget { +class ManualAddEditBookPage extends StatefulWidget { + const ManualAddEditBookPage({super.key}); + + @override + State createState() => _ManualAddEditBookPageState(); +} + +class _ManualAddEditBookPageState extends State { final TextEditingController _titleController = TextEditingController(); final TextEditingController _authorController = TextEditingController(); final TextEditingController _pageController = TextEditingController(); @@ -26,15 +36,14 @@ class ManualAddEditBookPage extends StatelessWidget { final TextEditingController _isbnController = TextEditingController(); final TextEditingController _summaryController = TextEditingController(); - ManualAddEditBookPage({super.key}); + String? _title = ''; @override Widget build(BuildContext context) { return Column( children: [ ThemedAppBar( - // TODO - title: Text('Book Title goes here'), + title: Text(_title ?? ''), ), Expanded( child: LayoutBuilder( @@ -74,7 +83,10 @@ class ManualAddEditBookPage extends StatelessWidget { ), const SizedBox(width: 64), Expanded( - child: _buildOptionalInformationCard(isDesktop: true), + child: _buildOptionalInformationCard( + context, + isDesktop: true, + ), ), ], ), @@ -83,36 +95,48 @@ class ManualAddEditBookPage extends StatelessWidget { // TODO Widget _buildMobileLayout(BuildContext context) { - return ListView( - shrinkWrap: true, - children: [ - _buildRequiredInformationCard(isDesktop: false), - _buildOptionalInformationCard(isDesktop: false), - ], + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView( + shrinkWrap: true, + itemExtent: 16, + children: [ + _buildRequiredInformationCard(isDesktop: false), + _buildOptionalInformationCard( + context, + isDesktop: false, + ), + ], + ), ); } - Widget _buildRequiredInformationCard({ required bool isDesktop }) { + Widget _buildRequiredInformationCard({required bool isDesktop}) { return Card( child: Container( padding: const EdgeInsets.all(16), child: Column( children: [ - if (isDesktop)...[ + if (isDesktop) ...[ const Spacer(), Text('Image'), const Spacer(), ], Row( children: [ - if (!isDesktop)...[ + if (!isDesktop) ...[ Text('Image'), const SizedBox(width: 16), ], Expanded( child: DanteTextField( controller: _titleController, - hint: 'Title', + hint: 'add-manual.title'.tr(), + onChanged: (String? changedTitle) { + setState(() { + _title = changedTitle; + }); + }, ), ), ], @@ -120,51 +144,57 @@ class ManualAddEditBookPage extends StatelessWidget { const SizedBox(height: 16), DanteTextField( controller: _authorController, - hint: 'Authors', + hint: 'add-manual.authors'.tr(), ), const SizedBox(height: 16), DanteTextField( controller: _pageController, - hint: 'Page Count', + hint: 'add-manual.page-count'.tr(), ), - if (isDesktop) - const Spacer(), + if (isDesktop) const Spacer(), ], ), ), ); } - Widget _buildOptionalInformationCard({required bool isDesktop}) { + Widget _buildOptionalInformationCard(BuildContext context, {required bool isDesktop}) { return Card( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text('Optional information'), + Text( + 'add-manual.optional-info'.tr(), + style: Theme.of(context).textTheme.titleLarge, + ), const SizedBox(height: 32), DanteTextField( controller: _subtitleController, - hint: 'Subtitle', + hint: 'add-manual.subtitle'.tr(), ), const SizedBox(height: 16), - Text('DatePicker PublishDate'), + Text('TODO DatePicker PublishDate'), const SizedBox(height: 16), - Text('Language'), + LanguagePicker( + onLanguageSelected: (Language language) { + print(language); + }, + ), const SizedBox(height: 16), DanteTextField( controller: _isbnController, - hint: 'ISBN', + hint: 'add-manual.isbn'.tr(), ), const SizedBox(height: 16), DanteTextField( controller: _summaryController, - hint: 'Summary', + hint: 'add-manual.summary'.tr(), maxLines: 5, ), ], - ) + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index ea3f28d..dda0233 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -233,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + country_flags: + dependency: "direct main" + description: + name: country_flags + sha256: dbc4f76e7c801619b2d841023e0327191ba00663f1f1b4770394e9bc6632c444 + url: "https://pub.dev" + source: hosted + version: "2.2.0" cross_file: dependency: transitive description: @@ -805,6 +813,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + jovial_misc: + dependency: transitive + description: + name: jovial_misc + sha256: f6e64f789ee311025bb367be9c9afe9759f76dd8209070b7f38e735b5f529eb1 + url: "https://pub.dev" + source: hosted + version: "0.8.5" + jovial_svg: + dependency: transitive + description: + name: jovial_svg + sha256: "7162d9fce53e284782f6178e175c9e07dde695779392eaa2542c1f795658eb3d" + url: "https://pub.dev" + source: hosted + version: "1.1.20" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d34b838..7d90c0f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: flutter_staggered_grid_view: ^0.7.0 share_plus: ^7.2.2 web: ^0.4.2 + country_flags: ^2.2.0 dev_dependencies: flutter_test: From 8014d9e25884ec546073299ea070ad62e2cba119 Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Sat, 24 Feb 2024 13:41:54 +0100 Subject: [PATCH 4/8] Finalize Add manual book layout for web --- assets/translations/de-DE.json | 4 + assets/translations/en-US.json | 4 + lib/src/ui/add/date_picker_widget.dart | 80 +++++++++++++++++++ ...icker.dart => language_picker_widget.dart} | 12 +-- lib/src/ui/add/manual_add_edit_book_page.dart | 24 +++--- lib/src/util/extensions.dart | 4 + 6 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 lib/src/ui/add/date_picker_widget.dart rename lib/src/ui/add/{language_picker.dart => language_picker_widget.dart} (89%) diff --git a/assets/translations/de-DE.json b/assets/translations/de-DE.json index 9292b92..b0d476f 100644 --- a/assets/translations/de-DE.json +++ b/assets/translations/de-DE.json @@ -19,6 +19,10 @@ "title": "Sprache", "empty": "Wähle eine Sprache" }, + "date-picker": { + "title": "Veröffentlichungsdatum", + "empty": "Wähle ein Datum" + }, "countries": { "not-available": "Nicht vorhanden", "us": "Englisch", diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 84fac4b..75b6549 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -18,6 +18,10 @@ "title": "Language", "empty": "Choose a language" }, + "date-picker": { + "title": "Publish Date", + "empty": "Choose a date" + }, "countries": { "not-available": "Not specified", "us": "English", diff --git a/lib/src/ui/add/date_picker_widget.dart b/lib/src/ui/add/date_picker_widget.dart new file mode 100644 index 0000000..e8ce278 --- /dev/null +++ b/lib/src/ui/add/date_picker_widget.dart @@ -0,0 +1,80 @@ +import 'package:dantex/src/util/extensions.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class DatePickerWidget extends StatefulWidget { + final DateTime? preSelectedDate; + final Function(DateTime date) onDateSelected; + + const DatePickerWidget({ + required this.onDateSelected, + this.preSelectedDate, + super.key, + }); + + @override + State createState() => _DatePickerWidgetState(); +} + +class _DatePickerWidgetState extends State { + DateTime? _selectedDateTime; + + @override + void initState() { + _selectedDateTime = widget.preSelectedDate; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'date-picker.title'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + Container( + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width), + height: 48, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + border: Border.all(width: 0.7), + ), + child: InkWell( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(width: 16), + Center( + child: Text( + _selectedDateTime?.formatDefault() ?? 'date-picker.empty'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ], + ), + onTap: () async { + final DateTime? selectedDateTime = await showDatePicker( + context: context, + firstDate: DateTime(1940), + lastDate: DateTime.now(), + ); + + if (selectedDateTime != null) { + widget.onDateSelected(selectedDateTime); + + setState(() { + _selectedDateTime = selectedDateTime; + }); + } + }, + ), + ), + ], + ); + } +} diff --git a/lib/src/ui/add/language_picker.dart b/lib/src/ui/add/language_picker_widget.dart similarity index 89% rename from lib/src/ui/add/language_picker.dart rename to lib/src/ui/add/language_picker_widget.dart index 2b44370..0c12625 100644 --- a/lib/src/ui/add/language_picker.dart +++ b/lib/src/ui/add/language_picker_widget.dart @@ -3,21 +3,21 @@ import 'package:dantex/src/data/core/language.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -class LanguagePicker extends StatefulWidget { +class LanguagePickerWidget extends StatefulWidget { final Language preSelectedLanguage; final Function(Language language) onLanguageSelected; - const LanguagePicker({ + const LanguagePickerWidget({ required this.onLanguageSelected, this.preSelectedLanguage = Language.na, super.key, }); @override - State createState() => _LanguagePickerState(); + State createState() => _LanguagePickerWidgetState(); } -class _LanguagePickerState extends State { +class _LanguagePickerWidgetState extends State { final List _supportedLanguages = Language.values; Language _selectedLanguage = Language.na; @@ -37,7 +37,9 @@ class _LanguagePickerState extends State { style: Theme.of(context).textTheme.titleMedium, ), Container( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, + ), alignment: Alignment.center, decoration: BoxDecoration( borderRadius: const BorderRadius.all( diff --git a/lib/src/ui/add/manual_add_edit_book_page.dart b/lib/src/ui/add/manual_add_edit_book_page.dart index ec0864f..3dbb5d9 100644 --- a/lib/src/ui/add/manual_add_edit_book_page.dart +++ b/lib/src/ui/add/manual_add_edit_book_page.dart @@ -1,6 +1,7 @@ import 'package:dantex/src/data/book/entity/book_state.dart'; import 'package:dantex/src/data/core/language.dart'; -import 'package:dantex/src/ui/add/language_picker.dart'; +import 'package:dantex/src/ui/add/date_picker_widget.dart'; +import 'package:dantex/src/ui/add/language_picker_widget.dart'; import 'package:dantex/src/ui/core/dante_components.dart'; import 'package:dantex/src/ui/core/themed_app_bar.dart'; import 'package:dantex/src/util/layout_utils.dart'; @@ -8,17 +9,18 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; /// TODO -/// 2. Manual Add book -/// -[ ] Web -/// -[ ] Translations +/// 2. Manual Add book UI +/// -[x] Web +/// -[x] Translations /// -[ ] App /// -[ ] Dark Mode /// 3. Upload image -/// 4. Entry points for edit book: +/// 4. Save manual book +/// 5. Entry points for edit book: /// - Overflow /// - Detail Page -/// 4. Edit existing book -/// 5. Test +/// 6. Edit existing book +/// 7. Test /// - Web /// - App class ManualAddEditBookPage extends StatefulWidget { @@ -175,9 +177,13 @@ class _ManualAddEditBookPageState extends State { hint: 'add-manual.subtitle'.tr(), ), const SizedBox(height: 16), - Text('TODO DatePicker PublishDate'), + DatePickerWidget( + onDateSelected: (DateTime date) { + print(date); + }, + ), const SizedBox(height: 16), - LanguagePicker( + LanguagePickerWidget( onLanguageSelected: (Language language) { print(language); }, diff --git a/lib/src/util/extensions.dart b/lib/src/util/extensions.dart index c206cf6..1e67ff2 100644 --- a/lib/src/util/extensions.dart +++ b/lib/src/util/extensions.dart @@ -15,6 +15,7 @@ extension HexColor on String { } } +final DateFormat _dfDefault = DateFormat('dd.MM.yyyy'); final DateFormat _dfMonth = DateFormat('MMMM yyyy'); final DateFormat _dfMonthShort = DateFormat('MMM yy'); @@ -22,6 +23,9 @@ extension DateTimeX on DateTime { String formatWithMonthAndYear() { return _dfMonth.format(this); } + String formatDefault() { + return _dfDefault.format(this); + } String formatWithMonthAndYearShort() { return _dfMonthShort.format(this); } From 67279b43f9d23f1e7148917de928e2681d189425 Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Sat, 24 Feb 2024 13:54:10 +0100 Subject: [PATCH 5/8] Finalize UI for manually adding a book --- lib/src/ui/add/book_cover_picker_widget.dart | 18 +++++++++ lib/src/ui/add/date_picker_widget.dart | 13 +++++-- lib/src/ui/add/language_picker_widget.dart | 28 ++++++++++--- lib/src/ui/add/manual_add_edit_book_page.dart | 39 ++++++++++--------- lib/src/ui/core/dante_components.dart | 3 ++ 5 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 lib/src/ui/add/book_cover_picker_widget.dart diff --git a/lib/src/ui/add/book_cover_picker_widget.dart b/lib/src/ui/add/book_cover_picker_widget.dart new file mode 100644 index 0000000..4c7723e --- /dev/null +++ b/lib/src/ui/add/book_cover_picker_widget.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class BookCoverPickerWidget extends StatelessWidget { + const BookCoverPickerWidget({super.key}); + + @override + Widget build(BuildContext context) { + // TODO Handle book cover picks + return Container( + width: 100, + height: 100, + color: Colors.white, + child: const Center( + child: Text('Pick Image'), + ), + ); + } +} diff --git a/lib/src/ui/add/date_picker_widget.dart b/lib/src/ui/add/date_picker_widget.dart index e8ce278..94662a4 100644 --- a/lib/src/ui/add/date_picker_widget.dart +++ b/lib/src/ui/add/date_picker_widget.dart @@ -32,7 +32,9 @@ class _DatePickerWidgetState extends State { children: [ Text( 'date-picker.title'.tr(), - style: Theme.of(context).textTheme.titleMedium, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ), Container( constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width), @@ -42,7 +44,10 @@ class _DatePickerWidgetState extends State { borderRadius: const BorderRadius.all( Radius.circular(4), ), - border: Border.all(width: 0.7), + border: Border.all( + width: 0.7, + color: Theme.of(context).colorScheme.onSurface, + ), ), child: InkWell( child: Row( @@ -52,7 +57,9 @@ class _DatePickerWidgetState extends State { Center( child: Text( _selectedDateTime?.formatDefault() ?? 'date-picker.empty'.tr(), - style: Theme.of(context).textTheme.titleMedium, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ), ), ], diff --git a/lib/src/ui/add/language_picker_widget.dart b/lib/src/ui/add/language_picker_widget.dart index 0c12625..46f19fc 100644 --- a/lib/src/ui/add/language_picker_widget.dart +++ b/lib/src/ui/add/language_picker_widget.dart @@ -34,7 +34,9 @@ class _LanguagePickerWidgetState extends State { children: [ Text( 'language-picker.title'.tr(), - style: Theme.of(context).textTheme.titleMedium, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ), Container( constraints: BoxConstraints( @@ -45,7 +47,10 @@ class _LanguagePickerWidgetState extends State { borderRadius: const BorderRadius.all( Radius.circular(4), ), - border: Border.all(width: 0.7), + border: Border.all( + width: 0.7, + color: Theme.of(context).colorScheme.onSurface, + ), ), child: DropdownButtonHideUnderline( child: DropdownButton( @@ -53,6 +58,9 @@ class _LanguagePickerWidgetState extends State { hint: Text( 'language-picker.empty'.tr(), textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), ), icon: Icon( Icons.arrow_drop_down, @@ -67,10 +75,13 @@ class _LanguagePickerWidgetState extends State { child: Row( children: [ const SizedBox(width: 12), - _buildCountryFlag(language), + _buildCountryFlag(context, language), const SizedBox(width: 16), Text( language.translationKey.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), ), ], ), @@ -93,12 +104,17 @@ class _LanguagePickerWidgetState extends State { ); } - Widget _buildCountryFlag(Language language) { + Widget _buildCountryFlag(BuildContext context, Language language) { if (language.countryCode == null) { - return const SizedBox( + return SizedBox( width: 48, child: Center( - child: Text('NA'), + child: Text( + 'NA', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), ), ); } diff --git a/lib/src/ui/add/manual_add_edit_book_page.dart b/lib/src/ui/add/manual_add_edit_book_page.dart index 3dbb5d9..9d2eef9 100644 --- a/lib/src/ui/add/manual_add_edit_book_page.dart +++ b/lib/src/ui/add/manual_add_edit_book_page.dart @@ -1,5 +1,6 @@ import 'package:dantex/src/data/book/entity/book_state.dart'; import 'package:dantex/src/data/core/language.dart'; +import 'package:dantex/src/ui/add/book_cover_picker_widget.dart'; import 'package:dantex/src/ui/add/date_picker_widget.dart'; import 'package:dantex/src/ui/add/language_picker_widget.dart'; import 'package:dantex/src/ui/core/dante_components.dart'; @@ -9,13 +10,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; /// TODO -/// 2. Manual Add book UI -/// -[x] Web -/// -[x] Translations -/// -[ ] App -/// -[ ] Dark Mode -/// 3. Upload image -/// 4. Save manual book +/// 3. Save manual book +/// 4. Upload image /// 5. Entry points for edit book: /// - Overflow /// - Detail Page @@ -48,13 +44,16 @@ class _ManualAddEditBookPageState extends State { title: Text(_title ?? ''), ), Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return switch (getDeviceFormFactor(constraints)) { - DeviceFormFactor.desktop => _buildDesktopLayout(context), - _ => _buildMobileLayout(context), - }; - }, + child: Container( + color: Theme.of(context).colorScheme.surface, + child: LayoutBuilder( + builder: (context, constraints) { + return switch (getDeviceFormFactor(constraints)) { + DeviceFormFactor.desktop => _buildDesktopLayout(context), + _ => _buildMobileLayout(context), + }; + }, + ), ), ), Container( @@ -101,9 +100,9 @@ class _ManualAddEditBookPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), child: ListView( shrinkWrap: true, - itemExtent: 16, children: [ _buildRequiredInformationCard(isDesktop: false), + const SizedBox(height: 16), _buildOptionalInformationCard( context, isDesktop: false, @@ -121,13 +120,13 @@ class _ManualAddEditBookPageState extends State { children: [ if (isDesktop) ...[ const Spacer(), - Text('Image'), + BookCoverPickerWidget(), const Spacer(), ], Row( children: [ if (!isDesktop) ...[ - Text('Image'), + BookCoverPickerWidget(), const SizedBox(width: 16), ], Expanded( @@ -169,7 +168,9 @@ class _ManualAddEditBookPageState extends State { children: [ Text( 'add-manual.optional-info'.tr(), - style: Theme.of(context).textTheme.titleLarge, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), ), const SizedBox(height: 32), DanteTextField( @@ -207,7 +208,7 @@ class _ManualAddEditBookPageState extends State { Widget _buildStateButton(BookState state) { return SizedBox( - width: 140, + width: 120, child: OutlinedButton( onPressed: () {}, // icon: state.icon, diff --git a/lib/src/ui/core/dante_components.dart b/lib/src/ui/core/dante_components.dart index 1c31e4e..47e947d 100644 --- a/lib/src/ui/core/dante_components.dart +++ b/lib/src/ui/core/dante_components.dart @@ -63,6 +63,9 @@ class DanteTextField extends StatelessWidget { maxLines: maxLines, onChanged: onChanged, inputFormatters: [if (formatter != null) formatter!], + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), decoration: InputDecoration( label: label, errorText: errorText, From 268e2b414d84ba6270a2cbb43c67570e0bcdc055 Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Sat, 24 Feb 2024 20:16:32 +0100 Subject: [PATCH 6/8] Open book edit page --- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/src/providers/app_router.dart | 14 ++++++- lib/src/ui/add/manual_add_edit_book_page.dart | 29 ++++++++++---- lib/src/ui/book/book_item_widget.dart | 4 ++ lib/src/ui/book/desktop_book_action_menu.dart | 4 +- lib/src/ui/book/mobile_book_action_menu.dart | 6 +-- lib/src/ui/main/book_state_page.dart | 39 +++++++++++++++---- 8 files changed, 78 insertions(+), 22 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 36afeec..ceecec0 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -163,7 +163,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826d..5e31d3d 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ _mainRoutes = [ GoRoute( path: DanteRoute.manualAdd.url, builder: (BuildContext context, GoRouterState state) => - ManualAddEditBookPage(), + const ManualAddEditBookPage(), + ), + GoRoute( + path: DanteRoute.editBook.url, + builder: (context, state) { + final bookId = state.pathParameters['bookId'] ?? ''; + return ManualAddEditBookPage(bookId: bookId); + }, ), GoRoute( path: DanteRoute.bookDetail.url, @@ -210,6 +217,11 @@ enum DanteRoute { mobileUrl: 'add-manual', navigationUrl: '/add-manual', ), + editBook( + webUrl: '/edit/:bookId', + mobileUrl: 'edit/:bookId', + navigationUrl: '/edit/:bookId', + ), settings( webUrl: '/settings', mobileUrl: 'settings', diff --git a/lib/src/ui/add/manual_add_edit_book_page.dart b/lib/src/ui/add/manual_add_edit_book_page.dart index 9d2eef9..df071ae 100644 --- a/lib/src/ui/add/manual_add_edit_book_page.dart +++ b/lib/src/ui/add/manual_add_edit_book_page.dart @@ -12,15 +12,18 @@ import 'package:flutter/material.dart'; /// TODO /// 3. Save manual book /// 4. Upload image -/// 5. Entry points for edit book: -/// - Overflow -/// - Detail Page -/// 6. Edit existing book -/// 7. Test +/// 5. Edit existing book +/// 6. Test /// - Web /// - App class ManualAddEditBookPage extends StatefulWidget { - const ManualAddEditBookPage({super.key}); + + final String? bookId; + + const ManualAddEditBookPage({ + super.key, + this.bookId, + }); @override State createState() => _ManualAddEditBookPageState(); @@ -36,6 +39,16 @@ class _ManualAddEditBookPageState extends State { String? _title = ''; + @override + void initState() { + // TODO + + print('EDIT Book'); + print(widget.bookId); + + super.initState(); + } + @override Widget build(BuildContext context) { return Column( @@ -169,8 +182,8 @@ class _ManualAddEditBookPageState extends State { Text( 'add-manual.optional-info'.tr(), style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), + color: Theme.of(context).colorScheme.onSurface, + ), ), const SizedBox(height: 32), DanteTextField( diff --git a/lib/src/ui/book/book_item_widget.dart b/lib/src/ui/book/book_item_widget.dart index bb2baa3..b4c23a8 100644 --- a/lib/src/ui/book/book_item_widget.dart +++ b/lib/src/ui/book/book_item_widget.dart @@ -20,12 +20,14 @@ class BookItemWidget extends StatelessWidget { final Function(Book book, BookState updatedState) onBookStateChanged; final Function(Book book) onBookDeleted; + final Function(Book book) onBookEditClicked; BookItemWidget( this._book, { required this.useMobileLayout, required this.onBookStateChanged, required this.onBookDeleted, + required this.onBookEditClicked, super.key, }); @@ -69,6 +71,7 @@ class BookItemWidget extends StatelessWidget { _book, onBookDeleted: onBookDeleted, onBookStateChanged: onBookStateChanged, + onBookEditClicked: onBookEditClicked, ), controller: _controller, ); @@ -145,6 +148,7 @@ class BookItemWidget extends StatelessWidget { _book, onBookDeleted: onBookDeleted, onBookStateChanged: onBookStateChanged, + onBookEditClicked: onBookEditClicked, ); } } diff --git a/lib/src/ui/book/desktop_book_action_menu.dart b/lib/src/ui/book/desktop_book_action_menu.dart index c5b5b58..f1c6482 100644 --- a/lib/src/ui/book/desktop_book_action_menu.dart +++ b/lib/src/ui/book/desktop_book_action_menu.dart @@ -9,11 +9,13 @@ class DesktopBookActionMenu extends StatelessWidget { final Function(Book book, BookState updatedState) onBookStateChanged; final Function(Book book) onBookDeleted; + final Function(Book book) onBookEditClicked; const DesktopBookActionMenu( this._book, { required this.onBookStateChanged, required this.onBookDeleted, + required this.onBookEditClicked, super.key, }); @@ -99,7 +101,7 @@ class DesktopBookActionMenu extends StatelessWidget { // TODO: Handle this case. break; case BookAction.edit: - // TODO: Handle this case. + onBookEditClicked(_book); break; case BookAction.delete: onBookDeleted(_book); diff --git a/lib/src/ui/book/mobile_book_action_menu.dart b/lib/src/ui/book/mobile_book_action_menu.dart index 0677cac..3b52500 100644 --- a/lib/src/ui/book/mobile_book_action_menu.dart +++ b/lib/src/ui/book/mobile_book_action_menu.dart @@ -10,11 +10,13 @@ class MobileBookActionMenu extends StatelessWidget { final Function(Book book, BookState updatedState) onBookStateChanged; final Function(Book book) onBookDeleted; + final Function(Book book) onBookEditClicked; const MobileBookActionMenu( this._book, { required this.onBookStateChanged, required this.onBookDeleted, + required this.onBookEditClicked, super.key, }); @@ -69,9 +71,7 @@ class MobileBookActionMenu extends StatelessWidget { title: 'book-actions.edit'.tr(), icon: Icons.edit_outlined, color: Theme.of(context).colorScheme.secondary, - onClick: () { - // TODO Edit book in ticket: TODO - }, + onClick: () => onBookEditClicked(_book), ), _buildBookAction( title: 'book-actions.delete'.tr(), diff --git a/lib/src/ui/main/book_state_page.dart b/lib/src/ui/main/book_state_page.dart index b20b585..b85d33f 100644 --- a/lib/src/ui/main/book_state_page.dart +++ b/lib/src/ui/main/book_state_page.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:dantex/src/data/book/book_repository.dart'; import 'package:dantex/src/data/book/entity/book.dart'; import 'package:dantex/src/data/book/entity/book_state.dart'; +import 'package:dantex/src/providers/app_router.dart'; import 'package:dantex/src/providers/book.dart'; import 'package:dantex/src/providers/repository.dart'; import 'package:dantex/src/ui/book/book_item_widget.dart'; @@ -11,6 +12,7 @@ import 'package:dantex/src/ui/main/empty_state_view.dart'; import 'package:dantex/src/util/layout_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; class BookStatePage extends ConsumerWidget { final BookState _state; @@ -48,11 +50,18 @@ class _BooksScreen extends ConsumerWidget { final DeviceFormFactor formFactor = getDeviceFormFactor(constraints); return switch (formFactor) { - DeviceFormFactor.desktop => - _buildLargeLayout(bookRepository, columns: 3), - DeviceFormFactor.tablet => - _buildLargeLayout(bookRepository, columns: 2), + DeviceFormFactor.desktop => _buildLargeLayout( + context, + bookRepository, + columns: 3, + ), + DeviceFormFactor.tablet => _buildLargeLayout( + context, + bookRepository, + columns: 2, + ), DeviceFormFactor.phone => _buildPhoneLayout( + context, bookRepository, ), }; @@ -60,22 +69,26 @@ class _BooksScreen extends ConsumerWidget { ); } - Widget _buildPhoneLayout(BookRepository bookRepository) { + Widget _buildPhoneLayout( + BuildContext context, + BookRepository bookRepository, + ) { return ListView.separated( padding: const EdgeInsets.all(16.0), physics: const BouncingScrollPhysics(), itemCount: books.length, itemBuilder: (context, index) => _buildItem( + context, books[index], bookRepository, useMobileLayout: true, ), - separatorBuilder: (BuildContext context, int index) => - const SizedBox(height: 16), + separatorBuilder: (BuildContext context, int index) => const SizedBox(height: 16), ); } Widget _buildLargeLayout( + BuildContext context, BookRepository bookRepository, { required int columns, }) { @@ -88,6 +101,7 @@ class _BooksScreen extends ConsumerWidget { childAspectRatio: 4, ), itemBuilder: (context, index) => _buildItem( + context, books[index], bookRepository, useMobileLayout: false, @@ -97,6 +111,7 @@ class _BooksScreen extends ConsumerWidget { } Widget _buildItem( + BuildContext context, Book book, BookRepository bookRepository, { required bool useMobileLayout, @@ -110,6 +125,7 @@ class _BooksScreen extends ConsumerWidget { book, state, ), + onBookEditClicked: (Book book) => _handleBookEditClicked(context, book), ); } @@ -131,4 +147,13 @@ class _BooksScreen extends ConsumerWidget { repository.delete(book.id), ); } + + void _handleBookEditClicked( + BuildContext context, + Book book, + ) { + context.go( + DanteRoute.editBook.navigationUrl.replaceAll(':bookId', book.id), + ); + } } From 32d2e3cb9f6d9e117cd03983541d948e6a02bfcb Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Sun, 25 Feb 2024 12:57:28 +0100 Subject: [PATCH 7/8] Load existing book for editing a book --- assets/translations/de-DE.json | 4 +- assets/translations/en-US.json | 2 +- lib/src/data/core/language.dart | 23 ++- lib/src/ui/add/book_cover_picker_widget.dart | 18 -- lib/src/ui/add/language_picker_widget.dart | 129 ------------ lib/src/ui/add/manual_add_edit_book_page.dart | 189 ++++++++++++++---- .../add/widgets/book_cover_picker_widget.dart | 63 ++++++ .../add/{ => widgets}/date_picker_widget.dart | 63 +++--- .../add/widgets/language_picker_widget.dart | 123 ++++++++++++ lib/src/ui/core/themed_app_bar.dart | 15 ++ lib/src/ui/login/login_page.dart | 11 +- 11 files changed, 414 insertions(+), 226 deletions(-) delete mode 100644 lib/src/ui/add/book_cover_picker_widget.dart delete mode 100644 lib/src/ui/add/language_picker_widget.dart create mode 100644 lib/src/ui/add/widgets/book_cover_picker_widget.dart rename lib/src/ui/add/{ => widgets}/date_picker_widget.dart (55%) create mode 100644 lib/src/ui/add/widgets/language_picker_widget.dart diff --git a/assets/translations/de-DE.json b/assets/translations/de-DE.json index b0d476f..abc4a8d 100644 --- a/assets/translations/de-DE.json +++ b/assets/translations/de-DE.json @@ -25,7 +25,7 @@ }, "countries": { "not-available": "Nicht vorhanden", - "us": "Englisch", + "gb": "Englisch", "de": "Deutsch", "it": "Italienisch", "fr": "Französisch", @@ -172,7 +172,7 @@ "reset": "Zurücksetzen", "reset_password": "Password zurücksetzen", "reset_password_text": "Wir versenden ein Email an deine registrierte Adresse, wo du weitere Instruktionen zum Zurücksetzen findest.", - "save": "", + "save": "Speichern", "search": { "empty": { "action": "Online suchen", diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 75b6549..613ba36 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -24,7 +24,7 @@ }, "countries": { "not-available": "Not specified", - "us": "English", + "gb": "English", "de": "German", "it": "Italian", "fr": "French", diff --git a/lib/src/data/core/language.dart b/lib/src/data/core/language.dart index 88e3a5e..42f8432 100644 --- a/lib/src/data/core/language.dart +++ b/lib/src/data/core/language.dart @@ -4,8 +4,11 @@ enum Language { translationKey: 'countries.not-available', ), english( - countryCode: 'US', - translationKey: 'countries.us', + countryCode: 'GB', + translationKey: 'countries.gb', + alternativeCountryCodes: [ + 'en', + ], ), german( countryCode: 'DE', @@ -90,9 +93,25 @@ enum Language { final String? countryCode; final String translationKey; + final List alternativeCountryCodes; + + List get countryCodes { + return [ + countryCode?.toLowerCase(), + ...alternativeCountryCodes.map((a) => a.toLowerCase()), + ]; + } const Language({ required this.countryCode, required this.translationKey, + this.alternativeCountryCodes = const [], }); + + static Language fromCountryCode(String countryCode) { + return Language.values.firstWhere( + (element) => element.countryCodes.contains(countryCode.toLowerCase()), + orElse: () => Language.na, + ); + } } diff --git a/lib/src/ui/add/book_cover_picker_widget.dart b/lib/src/ui/add/book_cover_picker_widget.dart deleted file mode 100644 index 4c7723e..0000000 --- a/lib/src/ui/add/book_cover_picker_widget.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; - -class BookCoverPickerWidget extends StatelessWidget { - const BookCoverPickerWidget({super.key}); - - @override - Widget build(BuildContext context) { - // TODO Handle book cover picks - return Container( - width: 100, - height: 100, - color: Colors.white, - child: const Center( - child: Text('Pick Image'), - ), - ); - } -} diff --git a/lib/src/ui/add/language_picker_widget.dart b/lib/src/ui/add/language_picker_widget.dart deleted file mode 100644 index 46f19fc..0000000 --- a/lib/src/ui/add/language_picker_widget.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:country_flags/country_flags.dart'; -import 'package:dantex/src/data/core/language.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class LanguagePickerWidget extends StatefulWidget { - final Language preSelectedLanguage; - final Function(Language language) onLanguageSelected; - - const LanguagePickerWidget({ - required this.onLanguageSelected, - this.preSelectedLanguage = Language.na, - super.key, - }); - - @override - State createState() => _LanguagePickerWidgetState(); -} - -class _LanguagePickerWidgetState extends State { - final List _supportedLanguages = Language.values; - Language _selectedLanguage = Language.na; - - @override - void initState() { - _selectedLanguage = widget.preSelectedLanguage; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'language-picker.title'.tr(), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width, - ), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(4), - ), - border: Border.all( - width: 0.7, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - hint: Text( - 'language-picker.empty'.tr(), - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - icon: Icon( - Icons.arrow_drop_down, - color: Theme.of(context).colorScheme.onSurface, - ), - iconSize: 30, - value: _selectedLanguage, - items: _supportedLanguages.map( - (Language language) { - return DropdownMenuItem( - value: language, - child: Row( - children: [ - const SizedBox(width: 12), - _buildCountryFlag(context, language), - const SizedBox(width: 16), - Text( - language.translationKey.tr(), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ), - ); - }, - ).toList(), - onChanged: (Language? language) { - if (language != null) { - widget.onLanguageSelected(language); - - setState(() { - _selectedLanguage = language; - }); - } - }, - ), - ), - ), - ], - ); - } - - Widget _buildCountryFlag(BuildContext context, Language language) { - if (language.countryCode == null) { - return SizedBox( - width: 48, - child: Center( - child: Text( - 'NA', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ); - } - - return CountryFlag.fromCountryCode( - language.countryCode!, - height: 48, - width: 48, - borderRadius: 12, - ); - } -} diff --git a/lib/src/ui/add/manual_add_edit_book_page.dart b/lib/src/ui/add/manual_add_edit_book_page.dart index df071ae..29f48be 100644 --- a/lib/src/ui/add/manual_add_edit_book_page.dart +++ b/lib/src/ui/add/manual_add_edit_book_page.dart @@ -1,23 +1,29 @@ +import 'dart:async'; + +import 'package:dantex/src/data/book/entity/book.dart'; import 'package:dantex/src/data/book/entity/book_state.dart'; import 'package:dantex/src/data/core/language.dart'; -import 'package:dantex/src/ui/add/book_cover_picker_widget.dart'; -import 'package:dantex/src/ui/add/date_picker_widget.dart'; -import 'package:dantex/src/ui/add/language_picker_widget.dart'; +import 'package:dantex/src/providers/repository.dart'; +import 'package:dantex/src/ui/add/widgets/book_cover_picker_widget.dart'; +import 'package:dantex/src/ui/add/widgets/date_picker_widget.dart'; +import 'package:dantex/src/ui/add/widgets/language_picker_widget.dart'; import 'package:dantex/src/ui/core/dante_components.dart'; import 'package:dantex/src/ui/core/themed_app_bar.dart'; import 'package:dantex/src/util/layout_utils.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; /// TODO -/// 3. Save manual book -/// 4. Upload image -/// 5. Edit existing book -/// 6. Test -/// - Web -/// - App -class ManualAddEditBookPage extends StatefulWidget { - +/// -[ ] Upload image +/// -[ ] Save/Update book +/// -[ ] Edit existing book +/// -[ ] Test +/// -[ ] Web +/// -[ ] App +/// -[ ] Back Navigation +class ManualAddEditBookPage extends ConsumerStatefulWidget { final String? bookId; const ManualAddEditBookPage({ @@ -26,10 +32,10 @@ class ManualAddEditBookPage extends StatefulWidget { }); @override - State createState() => _ManualAddEditBookPageState(); + ConsumerState createState() => _ManualAddEditBookPageState(); } -class _ManualAddEditBookPageState extends State { +class _ManualAddEditBookPageState extends ConsumerState { final TextEditingController _titleController = TextEditingController(); final TextEditingController _authorController = TextEditingController(); final TextEditingController _pageController = TextEditingController(); @@ -37,23 +43,56 @@ class _ManualAddEditBookPageState extends State { final TextEditingController _isbnController = TextEditingController(); final TextEditingController _summaryController = TextEditingController(); + final LanguageController _languageController = LanguageController(Language.na); + final BookCoverController _bookCoverController = BookCoverController(null); + final DateController _publishDateController = DateController(null); + String? _title = ''; + bool get _isInEditMode => widget.bookId != null; + @override void initState() { - // TODO + super.initState(); - print('EDIT Book'); - print(widget.bookId); + if (widget.bookId != null) { + _loadBookById(); + } + } - super.initState(); + void _loadBookById() { + unawaited( + ref.read(bookRepositoryProvider).getBook(widget.bookId!).first.then(_initializeWithBook), + ); + } + + void _initializeWithBook(Book book) { + _bookCoverController.value = book.thumbnailAddress; + _publishDateController.value = DateTime.parse(book.publishedDate); + _languageController.value = Language.fromCountryCode(book.language); + + _titleController.text = book.title; + _authorController.text = book.author; + _pageController.text = book.pageCount.toString(); + _subtitleController.text = book.subTitle; + _isbnController.text = book.isbn; + _summaryController.text = book.summary ?? ''; + + setState(() { + _title = book.title; + }); } @override Widget build(BuildContext context) { return Column( children: [ - ThemedAppBar( + ThemedAppBar.withBackNavigation( + context: context, + onBack: () { + // TODO Handle back navigation + context.pop(); + }, title: Text(_title ?? ''), ), Expanded( @@ -72,19 +111,69 @@ class _ManualAddEditBookPageState extends State { Container( color: Theme.of(context).colorScheme.tertiaryContainer, height: 100, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildStateButton(BookState.readLater), - _buildStateButton(BookState.reading), - _buildStateButton(BookState.read), - ], + child: _buildActionButtons(context), + ), + ], + ); + } + + Widget _buildActionButtons(BuildContext context) { + if (_isInEditMode) { + return _buildEditBookActionButtons(context); + } else { + return _buildNewBookActionButtons(); + } + } + + Widget _buildEditBookActionButtons(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton.icon( + onPressed: () { + context.pop(); + }, + icon: Icon( + Icons.close, + color: Theme.of(context).colorScheme.error, + ), + label: Text( + 'dismiss'.tr(), + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + TextButton.icon( + onPressed: () { + // TODO Save + }, + icon: Icon( + Icons.check_circle_outline_outlined, + color: Theme.of(context).colorScheme.onTertiaryContainer, + ), + label: Text( + 'save'.tr(), + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onTertiaryContainer, + ), ), ), ], ); } + Widget _buildNewBookActionButtons() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildBookStateButton(BookState.readLater), + _buildBookStateButton(BookState.reading), + _buildBookStateButton(BookState.read), + ], + ); + } + Widget _buildDesktopLayout(BuildContext context) { return Container( padding: const EdgeInsets.all(64), @@ -107,7 +196,6 @@ class _ManualAddEditBookPageState extends State { ); } - // TODO Widget _buildMobileLayout(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -133,13 +221,19 @@ class _ManualAddEditBookPageState extends State { children: [ if (isDesktop) ...[ const Spacer(), - BookCoverPickerWidget(), + BookCoverPickerWidget( + controller: _bookCoverController, + size: 200, + ), const Spacer(), ], Row( children: [ if (!isDesktop) ...[ - BookCoverPickerWidget(), + BookCoverPickerWidget( + controller: _bookCoverController, + size: 64, + ), const SizedBox(width: 16), ], Expanded( @@ -192,15 +286,11 @@ class _ManualAddEditBookPageState extends State { ), const SizedBox(height: 16), DatePickerWidget( - onDateSelected: (DateTime date) { - print(date); - }, + controller: _publishDateController, ), const SizedBox(height: 16), LanguagePickerWidget( - onLanguageSelected: (Language language) { - print(language); - }, + controller: _languageController, ), const SizedBox(height: 16), DanteTextField( @@ -219,14 +309,37 @@ class _ManualAddEditBookPageState extends State { ); } - Widget _buildStateButton(BookState state) { + Widget _buildBookStateButton(BookState state) { return SizedBox( width: 120, child: OutlinedButton( - onPressed: () {}, - // icon: state.icon, - child: Text(state.name), + onPressed: _saveNewBook, + child: Text(_getTitleForBookState(state)), ), ); } + + void _saveNewBook() { + if (!_hasBookRequiredInformation()) { + // TODO Show error information + } + + // TODO Save new book + // ref.read(bookRepositoryProvider).create(book); + // TODO Navigate back + } + + bool _hasBookRequiredInformation() { + // TODO + return false; + } + + String _getTitleForBookState(BookState state) { + return switch (state) { + BookState.readLater => 'tabs.for_later'.tr(), + BookState.reading => 'tabs.reading'.tr(), + BookState.read => 'tabs.read'.tr(), + BookState.wishlist => 'tabs.wishlist'.tr(), + }; + } } diff --git a/lib/src/ui/add/widgets/book_cover_picker_widget.dart b/lib/src/ui/add/widgets/book_cover_picker_widget.dart new file mode 100644 index 0000000..5156206 --- /dev/null +++ b/lib/src/ui/add/widgets/book_cover_picker_widget.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class BookCoverPickerWidget extends StatelessWidget { + final BookCoverController controller; + final double size; + final Function(String uploadedUrl)? onImageUploaded; + + const BookCoverPickerWidget({ + required this.controller, + required this.size, + this.onImageUploaded, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + border: Border.all( + width: 0.7, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + child: InkWell( + child: ValueListenableBuilder( + valueListenable: controller, + builder: (context, imageUrl, _) { + return _buildBookCover(imageUrl); + }, + ), + onTap: () { + // TODO Pick image + }, + ), + ); + } + + Widget _buildBookCover(String? imageUrl) { + if (imageUrl != null) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: Image.network( + imageUrl, + width: size, + ), + ); + } else { + return Icon( + Icons.image_outlined, + size: size * 0.66, + ); + } + } +} + +class BookCoverController extends ValueNotifier { + BookCoverController(super.value); +} \ No newline at end of file diff --git a/lib/src/ui/add/date_picker_widget.dart b/lib/src/ui/add/widgets/date_picker_widget.dart similarity index 55% rename from lib/src/ui/add/date_picker_widget.dart rename to lib/src/ui/add/widgets/date_picker_widget.dart index 94662a4..809871f 100644 --- a/lib/src/ui/add/date_picker_widget.dart +++ b/lib/src/ui/add/widgets/date_picker_widget.dart @@ -2,29 +2,16 @@ import 'package:dantex/src/util/extensions.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -class DatePickerWidget extends StatefulWidget { - final DateTime? preSelectedDate; - final Function(DateTime date) onDateSelected; +class DatePickerWidget extends StatelessWidget { + final DateController controller; + final Function(DateTime date)? onDateSelected; const DatePickerWidget({ - required this.onDateSelected, - this.preSelectedDate, + required this.controller, + this.onDateSelected, super.key, }); - @override - State createState() => _DatePickerWidgetState(); -} - -class _DatePickerWidgetState extends State { - DateTime? _selectedDateTime; - - @override - void initState() { - _selectedDateTime = widget.preSelectedDate; - super.initState(); - } - @override Widget build(BuildContext context) { return Column( @@ -50,19 +37,24 @@ class _DatePickerWidgetState extends State { ), ), child: InkWell( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(width: 16), - Center( - child: Text( - _selectedDateTime?.formatDefault() ?? 'date-picker.empty'.tr(), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface, + child: ValueListenableBuilder( + valueListenable: controller, + builder: (context, selectedDateTime, _) { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(width: 16), + Center( + child: Text( + selectedDateTime?.formatDefault() ?? 'date-picker.empty'.tr(), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), ), - ), - ), - ], + ], + ); + }, ), onTap: () async { final DateTime? selectedDateTime = await showDatePicker( @@ -72,11 +64,8 @@ class _DatePickerWidgetState extends State { ); if (selectedDateTime != null) { - widget.onDateSelected(selectedDateTime); - - setState(() { - _selectedDateTime = selectedDateTime; - }); + onDateSelected?.call(selectedDateTime); + controller.value = selectedDateTime; } }, ), @@ -85,3 +74,7 @@ class _DatePickerWidgetState extends State { ); } } + +class DateController extends ValueNotifier { + DateController(super.value); +} diff --git a/lib/src/ui/add/widgets/language_picker_widget.dart b/lib/src/ui/add/widgets/language_picker_widget.dart new file mode 100644 index 0000000..45cab68 --- /dev/null +++ b/lib/src/ui/add/widgets/language_picker_widget.dart @@ -0,0 +1,123 @@ +import 'package:country_flags/country_flags.dart'; +import 'package:dantex/src/data/core/language.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class LanguagePickerWidget extends StatelessWidget { + final LanguageController controller; + final Function(Language language)? onLanguageSelected; + + const LanguagePickerWidget({ + required this.controller, + this.onLanguageSelected, + super.key, + }); + + final List _supportedLanguages = Language.values; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'language-picker.title'.tr(), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ValueListenableBuilder( + valueListenable: controller, + builder: (context, language, _) { + return Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width, + ), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + border: Border.all( + width: 0.7, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + hint: Text( + 'language-picker.empty'.tr(), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + icon: Icon( + Icons.arrow_drop_down, + color: Theme.of(context).colorScheme.onSurface, + ), + iconSize: 30, + value: language, + items: _supportedLanguages.map( + (Language language) { + return DropdownMenuItem( + value: language, + child: Row( + children: [ + const SizedBox(width: 12), + _buildCountryFlag(context, language), + const SizedBox(width: 16), + Text( + language.translationKey.tr(), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + ); + }, + ).toList(), + onChanged: (Language? language) { + if (language != null) { + controller.value = language; + onLanguageSelected?.call(language); + } + }, + ), + ), + ); + }, + ), + ], + ); + } + + Widget _buildCountryFlag(BuildContext context, Language language) { + if (language.countryCode == null) { + return SizedBox( + width: 48, + child: Center( + child: Text( + 'NA', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ); + } + + return CountryFlag.fromCountryCode( + language.countryCode!, + height: 48, + width: 48, + borderRadius: 12, + ); + } +} + +class LanguageController extends ValueNotifier { + LanguageController(super.value); +} diff --git a/lib/src/ui/core/themed_app_bar.dart b/lib/src/ui/core/themed_app_bar.dart index 1ad40b4..1e687d1 100644 --- a/lib/src/ui/core/themed_app_bar.dart +++ b/lib/src/ui/core/themed_app_bar.dart @@ -14,6 +14,21 @@ class ThemedAppBar extends StatelessWidget implements PreferredSizeWidget { this.bottom, }); + ThemedAppBar.withBackNavigation({ + required this.title, + required BuildContext context, + required VoidCallback onBack, + super.key, + this.actions, + this.bottom, + }) : leading = IconButton( + onPressed: onBack, + icon: Icon( + Icons.arrow_back, + color: Theme.of(context).colorScheme.onSurface, + ), + ); + @override Size get preferredSize { // If we have a bottom widget, for example a tab bar as part of the app bar diff --git a/lib/src/ui/login/login_page.dart b/lib/src/ui/login/login_page.dart index 79ad5bf..ca51ad1 100644 --- a/lib/src/ui/login/login_page.dart +++ b/lib/src/ui/login/login_page.dart @@ -31,7 +31,7 @@ class LoginPageState extends ConsumerState { child: _isLoading ? const CircularProgressIndicator.adaptive() : Container( - width: MediaQuery.of(context).size.width * 0.6, + width: _getContainerWidth(), padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16.0), @@ -154,6 +154,15 @@ class LoginPageState extends ConsumerState { ); } + double _getContainerWidth() { + final double multiplier = _isDesktop() ? 0.3 : 0.6; + return MediaQuery.of(context).size.width * multiplier; + } + + bool _isDesktop() { + return MediaQuery.of(context).size.width > 1200; + } + void _loginErrorReceived(Exception exception, StackTrace stackTrace) { setState(() { _isLoading = false; From 39ead9091063d5f9bdce0f83a8c1db2299c0f842 Mon Sep 17 00:00:00 2001 From: Martin Macheiner Date: Fri, 1 Mar 2024 20:48:54 +0100 Subject: [PATCH 8/8] Save/update book in manual book screen --- assets/translations/de-DE.json | 7 +- assets/translations/en-US.json | 7 +- lib/src/ui/add/manual_add_edit_book_page.dart | 111 ++++++++++++++---- lib/src/util/extensions.dart | 7 ++ 4 files changed, 109 insertions(+), 23 deletions(-) diff --git a/assets/translations/de-DE.json b/assets/translations/de-DE.json index abc4a8d..e4d9a61 100644 --- a/assets/translations/de-DE.json +++ b/assets/translations/de-DE.json @@ -13,7 +13,12 @@ "optional-info": "Optionale Informationen", "subtitle": "Untertitel", "isbn": "ISBN Buchnummer", - "summary": "Zusammenfassung" + "summary": "Zusammenfassung", + "error": { + "missing-information": "TODO Missing information", + "update": "TODO update", + "create": "TODO create" + } }, "language-picker": { "title": "Sprache", diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 613ba36..7dff757 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -12,7 +12,12 @@ "optional-info": "Optional Information", "subtitle": "Subtitle", "isbn": "ISBN number", - "summary": "Summary" + "summary": "Summary", + "error": { + "missing-information": "TODO Missing information", + "update": "TODO update", + "create": "TODO create" + } }, "language-picker": { "title": "Language", diff --git a/lib/src/ui/add/manual_add_edit_book_page.dart b/lib/src/ui/add/manual_add_edit_book_page.dart index 29f48be..074ddf8 100644 --- a/lib/src/ui/add/manual_add_edit_book_page.dart +++ b/lib/src/ui/add/manual_add_edit_book_page.dart @@ -9,6 +9,7 @@ import 'package:dantex/src/ui/add/widgets/date_picker_widget.dart'; import 'package:dantex/src/ui/add/widgets/language_picker_widget.dart'; import 'package:dantex/src/ui/core/dante_components.dart'; import 'package:dantex/src/ui/core/themed_app_bar.dart'; +import 'package:dantex/src/util/extensions.dart'; import 'package:dantex/src/util/layout_utils.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -17,12 +18,9 @@ import 'package:go_router/go_router.dart'; /// TODO /// -[ ] Upload image -/// -[ ] Save/Update book -/// -[ ] Edit existing book /// -[ ] Test /// -[ ] Web /// -[ ] App -/// -[ ] Back Navigation class ManualAddEditBookPage extends ConsumerStatefulWidget { final String? bookId; @@ -48,6 +46,7 @@ class _ManualAddEditBookPageState extends ConsumerState { final DateController _publishDateController = DateController(null); String? _title = ''; + Book? _bookEditMode; bool get _isInEditMode => widget.bookId != null; @@ -67,8 +66,10 @@ class _ManualAddEditBookPageState extends ConsumerState { } void _initializeWithBook(Book book) { + _bookEditMode = book; + _bookCoverController.value = book.thumbnailAddress; - _publishDateController.value = DateTime.parse(book.publishedDate); + _publishDateController.value = book.publishedDate.parseWithDefaultDateFormat(); _languageController.value = Language.fromCountryCode(book.language); _titleController.text = book.title; @@ -111,21 +112,21 @@ class _ManualAddEditBookPageState extends ConsumerState { Container( color: Theme.of(context).colorScheme.tertiaryContainer, height: 100, - child: _buildActionButtons(context), + child: _buildActionButtons(), ), ], ); } - Widget _buildActionButtons(BuildContext context) { + Widget _buildActionButtons() { if (_isInEditMode) { - return _buildEditBookActionButtons(context); + return _buildEditBookActionButtons(); } else { return _buildNewBookActionButtons(); } } - Widget _buildEditBookActionButtons(BuildContext context) { + Widget _buildEditBookActionButtons() { return Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -145,9 +146,7 @@ class _ManualAddEditBookPageState extends ConsumerState { ), ), TextButton.icon( - onPressed: () { - // TODO Save - }, + onPressed: _saveExistingBook, icon: Icon( Icons.check_circle_outline_outlined, color: Theme.of(context).colorScheme.onTertiaryContainer, @@ -155,8 +154,8 @@ class _ManualAddEditBookPageState extends ConsumerState { label: Text( 'save'.tr(), style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onTertiaryContainer, - ), + color: Theme.of(context).colorScheme.onTertiaryContainer, + ), ), ), ], @@ -313,25 +312,95 @@ class _ManualAddEditBookPageState extends ConsumerState { return SizedBox( width: 120, child: OutlinedButton( - onPressed: _saveNewBook, + onPressed: () async => _saveNewBook(state), child: Text(_getTitleForBookState(state)), ), ); } - void _saveNewBook() { + Future _saveExistingBook() async { + if (!_hasBookRequiredInformation()) { + _showError(errorKey: 'add-manual.error.missing-information'); + } + + // Save new book and navigate back + try { + await ref + .read(bookRepositoryProvider) + .update( + _bookEditMode!.copyWith( + title: _titleController.text, + author: _authorController.text, + pageCount: int.tryParse(_pageController.text) ?? 0, + subTitle: _subtitleController.text, + publishedDate: _publishDateController.value?.formatDefault() ?? _bookEditMode!.publishedDate, + isbn: _isbnController.text, + thumbnailAddress: _bookCoverController.value, + language: _languageController.value.countryCode ?? _bookEditMode!.language, + summary: _summaryController.text, + ), + ) + .then( + (value) => context.pop(), + ); + } catch (e) { + _showError(errorKey: 'add-manual.error.update'); + } + } + + Future _saveNewBook(BookState bookState) async { if (!_hasBookRequiredInformation()) { - // TODO Show error information + _showError(errorKey: 'add-manual.error.missing-information'); } - // TODO Save new book - // ref.read(bookRepositoryProvider).create(book); - // TODO Navigate back + // Save new book and navigate back + try { + await ref + .read(bookRepositoryProvider) + .create( + Book( + id: '', + title: _titleController.text, + subTitle: _subtitleController.text, + author: _authorController.text, + state: bookState, + pageCount: int.tryParse(_pageController.text) ?? 0, + currentPage: 0, + publishedDate: _publishDateController.value?.formatDefault() ?? '', + position: 0, + isbn: _isbnController.text, + thumbnailAddress: _bookCoverController.value, + startDate: (bookState == BookState.reading) ? DateTime.now() : null, + endDate: (bookState == BookState.read) ? DateTime.now() : null, + forLaterDate: (bookState == BookState.readLater) ? DateTime.now() : null, + language: _languageController.value.countryCode ?? 'na', + rating: 0, + notes: '', + summary: _summaryController.text, + ), + ) + .then( + (value) => context.pop(), + ); + } catch (e) { + print(e); + _showError(errorKey: 'add-manual.error.create'); + } } bool _hasBookRequiredInformation() { - // TODO - return false; + return _hasValue(_titleController, (v) => v.text.isNotEmpty) && + _hasValue(_authorController, (v) => v.text.isNotEmpty) && + _hasValue(_pageController, (v) => int.tryParse(v.text) != null); + } + + bool _hasValue(ValueNotifier vn, bool Function(T value) probe) { + return vn.value != null && probe(vn.value as T); + } + + void _showError({required String errorKey}) { + // TODO Show error information + print('Missing data: ${errorKey.tr()}'); } String _getTitleForBookState(BookState state) { diff --git a/lib/src/util/extensions.dart b/lib/src/util/extensions.dart index 1e67ff2..bb0698d 100644 --- a/lib/src/util/extensions.dart +++ b/lib/src/util/extensions.dart @@ -19,6 +19,13 @@ final DateFormat _dfDefault = DateFormat('dd.MM.yyyy'); final DateFormat _dfMonth = DateFormat('MMMM yyyy'); final DateFormat _dfMonthShort = DateFormat('MMM yy'); +extension StringX on String { + + DateTime parseWithDefaultDateFormat() { + return _dfDefault.parse(this); + } +} + extension DateTimeX on DateTime { String formatWithMonthAndYear() { return _dfMonth.format(this);