From 463b2dc4e03629e1539a92cc9839fe828b87af08 Mon Sep 17 00:00:00 2001 From: Lockie Richter Date: Mon, 6 Nov 2023 16:51:15 +1030 Subject: [PATCH] Implement book label logic --- assets/translations/de-DE.json | 39 +-- assets/translations/en-US.json | 18 +- build.yaml | 7 + lib/src/data/book/book_label_repository.dart | 2 + lib/src/data/book/book_repository.dart | 2 +- lib/src/data/book/entity/book.dart | 12 +- lib/src/data/book/entity/book_label.dart | 2 +- .../book/firebase_book_label_repository.dart | 37 ++- .../data/book/firebase_book_repository.dart | 83 +++--- .../data/book/in_memory_book_repository.dart | 2 +- lib/src/providers/book.dart | 2 +- lib/src/providers/labels.dart | 9 + lib/src/ui/book/add_label_bottom_sheet.dart | 256 ++++++++++++++++++ lib/src/ui/book/book_detail_page.dart | 72 ++++- lib/src/ui/core/dante_components.dart | 4 + lib/src/ui/core/platform_components.dart | 4 +- lib/src/ui/login/email_login_page.dart | 6 +- lib/src/ui/login/login_page.dart | 2 +- lib/src/ui/profile/email_bottom_sheet.dart | 4 +- pubspec.lock | 24 ++ pubspec.yaml | 2 + .../book/firebase_book_repository_test.dart | 1 + test/ui/book_detail_page_test.dart | 6 +- 23 files changed, 503 insertions(+), 93 deletions(-) create mode 100644 build.yaml create mode 100644 lib/src/providers/labels.dart create mode 100644 lib/src/ui/book/add_label_bottom_sheet.dart diff --git a/assets/translations/de-DE.json b/assets/translations/de-DE.json index e6c06b5..62ba41a 100644 --- a/assets/translations/de-DE.json +++ b/assets/translations/de-DE.json @@ -1,21 +1,19 @@ { - "add": "Hinzufügen", - "navigation": { - "library": "Meine Bücher", - "stats": "Statistiken", - "timeline": "Timeline", - "wishlist": "Wunschliste", - "recommendations": "Vorschläge", - "book-keeping": "Verwaltung", - "settings": "Einstellungen" - }, - "anonymous-user": "Anonymer Bücherwurm", "account_creation_failed": "Dein Konto konnte nicht angelegt werden.", + "add": "Hinzufügen", "add_book": { "manual": "Manuell eingeben", "query": "Titel suche", "scan": "Buchode scannen" }, + "add_label_bottom_sheet": { + "choose_a_color": "TODO", + "create_new_label": "TODO", + "name": "TODO", + "no_labels_available": "TODO", + "pick_a_label": "TODO" + }, + "anonymous-user": "Anonymer Bücherwurm", "anonymous_login": { "description": "Deine Daten sind gesichert, solange du eingeloggt bist. Wenn du dich ausloggst, werden all deine Daten gelöscht.\n\nDu kannst dein Konto später noch anlegen.", "title": "Anonymer Login" @@ -51,6 +49,15 @@ "login_with_email": "Weiter mit Mail", "login_with_google": "Weiter mit Google", "logout": "Logout", + "navigation": { + "book-keeping": "Verwaltung", + "library": "Meine Bücher", + "recommendations": "Vorschläge", + "settings": "Einstellungen", + "stats": "Statistiken", + "timeline": "Timeline", + "wishlist": "Wunschliste" + }, "no_thanks": "TODO", "not_my_book": "Nicht mein Buch", "password": "Passwort", @@ -65,13 +72,13 @@ "reset_password": "TODO", "reset_password_text": "TODO", "search": { - "search": "Suchen", + "empty": { + "action": "Online suchen", + "description": "Nichts gefunden. Sollen wir online suchen?" + }, "hint": "Durchsuche deine Bibliothek...", "page-hint": "Tippe etwas um deine Bibliothek zu durchsuchen!", - "empty": { - "description": "Nichts gefunden. Sollen wir online suchen?", - "action": "Online suchen" - } + "search": "Suchen" }, "select": "Wählen", "settings": { diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 35776c3..26936c4 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -11,11 +11,19 @@ }, "anonymous-user": "Anonymous Bookworm", "account_creation_failed": "Failed to create account", + "add": "Add", "add_book": { "manual": "Add manual", "query": "Title search", "scan": "Scan book" }, + "add_label_bottom_sheet": { + "choose_a_color": "Choose a color", + "create_new_label": "Create new label", + "name": "Name", + "no_labels_available": "No labels available.\nCreate a new label first.", + "pick_a_label": "Pick a label" + }, "anonymous_login": { "description": "Your data will be stored as long as you stay signed in. When you sign out, your data will be erased.\n\nYou can always upgrade to a regular account later on.", "title": "Anonymous Login" @@ -65,13 +73,13 @@ "reset_password": "Reset Password", "reset_password_text": "A link will be sent to your registered email address with instructions on how to reset your password.", "search": { - "search": "Search", + "empty": { + "action": "Search online", + "description": "Nothing found. Should we search online?" + }, "hint": "Search your library...", "page-hint": "Start typing to search your library!", - "empty": { - "description": "Nothing found. Should we search online?", - "action": "Search online" - } + "search": "Search" }, "select": "Select", "settings": { diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..49f8214 --- /dev/null +++ b/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + json_serializable: + options: + any_map: true + explicit_to_json: true diff --git a/lib/src/data/book/book_label_repository.dart b/lib/src/data/book/book_label_repository.dart index 54e9065..c82d9ef 100644 --- a/lib/src/data/book/book_label_repository.dart +++ b/lib/src/data/book/book_label_repository.dart @@ -6,4 +6,6 @@ abstract class BookLabelRepository { Future createBookLabel(BookLabel label); Future deleteBookLabel(String id); + + Future fetch(String id); } diff --git a/lib/src/data/book/book_repository.dart b/lib/src/data/book/book_repository.dart index 6bedd78..e44e8e9 100644 --- a/lib/src/data/book/book_repository.dart +++ b/lib/src/data/book/book_repository.dart @@ -8,7 +8,7 @@ abstract class BookRepository { Stream> getAllBooks(); - Future getBook(String id); + Stream getBook(String id); Future create(Book book); diff --git a/lib/src/data/book/entity/book.dart b/lib/src/data/book/entity/book.dart index 99884cd..f603526 100644 --- a/lib/src/data/book/entity/book.dart +++ b/lib/src/data/book/entity/book.dart @@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'book.freezed.dart'; part 'book.g.dart'; -@freezed +@Freezed(makeCollectionsUnmodifiable: false) class Book with _$Book { const Book._(); @@ -32,19 +32,11 @@ class Book with _$Book { required int rating, required String? notes, required String? summary, - @Default([]) List labels, + @Default([]) List labels, }) = _Book; factory Book.fromJson(Map json) => _$BookFromJson(json); int get progressPercentage => pageCount != 0 ? ((currentPage / pageCount) * 100).toInt() : 0; - - void addLabel(BookLabel label) { - labels.add(label); - } - - void removeLabel(String labelId) { - labels.removeWhere((label) => label.id == labelId); - } } diff --git a/lib/src/data/book/entity/book_label.dart b/lib/src/data/book/entity/book_label.dart index dd5e69f..f6a8c53 100644 --- a/lib/src/data/book/entity/book_label.dart +++ b/lib/src/data/book/entity/book_label.dart @@ -3,7 +3,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'book_label.freezed.dart'; part 'book_label.g.dart'; -@freezed +@Freezed(makeCollectionsUnmodifiable: false) class BookLabel with _$BookLabel { const factory BookLabel({ required String id, diff --git a/lib/src/data/book/firebase_book_label_repository.dart b/lib/src/data/book/firebase_book_label_repository.dart index c734b66..98195e2 100644 --- a/lib/src/data/book/firebase_book_label_repository.dart +++ b/lib/src/data/book/firebase_book_label_repository.dart @@ -11,26 +11,49 @@ class FirebaseBookLabelRepository implements BookLabelRepository { @override Future createBookLabel(BookLabel label) { - // TODO: implement createBookLabel - throw UnimplementedError(); + final newRef = _labelsRef().push(); + + final data = label.copyWith(id: newRef.key!).toJson(); + + return newRef.set(data); } @override Future deleteBookLabel(String id) { - // TODO: implement deleteBookLabel - throw UnimplementedError(); + return _labelsRef().child(id).remove(); } @override Stream> getBookLabels() { - // TODO: implement getBookLabels + return _labelsRef().onValue.map( + (DatabaseEvent event) { + final Map? data = event.snapshot.toMap(); + + if (data == null) { + return []; + } + + return data.values.map( + (value) { + final Map bookMap = + (value as Map).cast(); + return BookLabel.fromJson(bookMap); + }, + ).toList(); + }, + ); + } + + @override + Future fetch(String id) { + // TODO: implement fetch throw UnimplementedError(); } - DatabaseReference _rootRef() { + DatabaseReference _labelsRef() { // At this point we can assume that the customer is already logged in, even as anonymous user final user = _fbAuth.currentUser!.uid; - return _fbDb.ref('users/$user/labels'); + return _fbDb.ref('users/$user/labels/'); } } diff --git a/lib/src/data/book/firebase_book_repository.dart b/lib/src/data/book/firebase_book_repository.dart index 6d7ccbc..0fbe66b 100644 --- a/lib/src/data/book/firebase_book_repository.dart +++ b/lib/src/data/book/firebase_book_repository.dart @@ -14,7 +14,7 @@ class FirebaseBookRepository implements BookRepository { @override Future create(Book book) { - final newRef = _rootRef().push(); + final newRef = _booksRef().push(); final data = book.copyWith(id: newRef.key!).toJson(); @@ -23,12 +23,12 @@ class FirebaseBookRepository implements BookRepository { @override Future delete(String id) { - return _rootRef().child(id).remove(); + return _booksRef().child(id).remove(); } @override Stream> getAllBooks() { - return _rootRef().onValue.map( + return _booksRef().onValue.map( (DatabaseEvent event) { final Map? data = event.snapshot.toMap(); @@ -36,31 +36,37 @@ class FirebaseBookRepository implements BookRepository { return []; } - return data - .map( - (key, value) { - final Map bookMap = - (value as Map).cast(); - final Book bookValue = Book.fromJson(bookMap); - return MapEntry(key, bookValue); - }, - ) - .values - .toList(); + return data.values.map( + (value) { + final Map bookMap = + (value as Map).cast(); + return Book.fromJson(bookMap); + }, + ).toList(); }, ); } @override - Future getBook(String id) async { - final bookSnapshot = await _rootRef().child(id).get(); - final bookMap = bookSnapshot.child(id).toMap(); + Stream getBook(String id) { + // final bookSnapshot = await _booksRef().child(id).get(); + // final bookMap = bookSnapshot.child(id).toMap(); - if (bookMap == null) { - throw Exception('Cannot read book with id $id as it does not exist!'); - } + // if (bookMap == null) { + // throw Exception('Cannot read book with id $id as it does not exist!'); + // } + + // return Book.fromJson(bookMap); + + return _booksRef().child(id).onValue.map((DatabaseEvent event) { + final Map? data = event.snapshot.toMap(); - return Book.fromJson(bookMap); + if (data == null) { + throw Exception('Cannot read book with id $id as it does not exist!'); + } + + return Book.fromJson(data); + }); } @override @@ -83,16 +89,23 @@ class FirebaseBookRepository implements BookRepository { @override Future update(Book book) { - return _rootRef().child(book.id).set(book); + return _booksRef().child(book.id).set(book.toJson()); } @override Future updateCurrentPage(String bookId, int currentPage) async { - final Book currentBook = await getBook(bookId); + final bookSnapshot = await _booksRef().child(bookId).get(); + final bookMap = bookSnapshot.child(bookId).toMap(); + + if (bookMap == null) { + throw Exception('Cannot read book with id $bookId as it does not exist!'); + } + final currentBook = Book.fromJson(bookMap); + return update(currentBook.copyWith(currentPage: currentPage)); } - DatabaseReference _rootRef() { + DatabaseReference _booksRef() { // At this point we can assume that the customer is already logged in, even as anonymous user final user = _fbAuth.currentUser!.uid; return _fbDb.ref('users/$user/books'); @@ -124,17 +137,25 @@ class FirebaseBookRepository implements BookRepository { } @override - Future addLabelToBook(String bookId, BookLabel label) async { - final book = await getBook(bookId); - book.addLabel(label); - return update(book); + Future addLabelToBook(String bookId, BookLabel label) { + // TODO: implement addLabelToBook + throw UnimplementedError(); } @override Future removeLabelFromBook(String bookId, String labelId) async { - final book = await getBook(bookId); - book.removeLabel(labelId); - return update(book); + final bookSnapshot = await _booksRef().child(bookId).get(); + final bookMap = bookSnapshot.child(bookId).toMap(); + + if (bookMap == null) { + throw Exception('Cannot read book with id $bookId as it does not exist!'); + } + final book = Book.fromJson(bookMap); + return update( + book.copyWith( + labels: book.labels..removeWhere((label) => label.id == labelId), + ), + ); } } diff --git a/lib/src/data/book/in_memory_book_repository.dart b/lib/src/data/book/in_memory_book_repository.dart index b6d7f9d..cd19b5e 100644 --- a/lib/src/data/book/in_memory_book_repository.dart +++ b/lib/src/data/book/in_memory_book_repository.dart @@ -39,7 +39,7 @@ class InMemoryBookRepository implements BookRepository { } @override - Future getBook(id) { + Stream getBook(id) { throw UnimplementedError('Not required'); } diff --git a/lib/src/providers/book.dart b/lib/src/providers/book.dart index 277eff0..1f4ae27 100644 --- a/lib/src/providers/book.dart +++ b/lib/src/providers/book.dart @@ -6,7 +6,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'book.g.dart'; @riverpod -Future book(BookRef ref, String id) => +Stream book(BookRef ref, String id) => ref.watch(bookRepositoryProvider).getBook(id); @riverpod diff --git a/lib/src/providers/labels.dart b/lib/src/providers/labels.dart new file mode 100644 index 0000000..80a052c --- /dev/null +++ b/lib/src/providers/labels.dart @@ -0,0 +1,9 @@ +import 'package:dantex/src/data/book/entity/book_label.dart'; +import 'package:dantex/src/providers/repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'labels.g.dart'; + +@riverpod +Stream> getBookLabels(GetBookLabelsRef ref) => + ref.watch(bookLabelRepositoryProvider).getBookLabels(); diff --git a/lib/src/ui/book/add_label_bottom_sheet.dart b/lib/src/ui/book/add_label_bottom_sheet.dart new file mode 100644 index 0000000..7c9baeb --- /dev/null +++ b/lib/src/ui/book/add_label_bottom_sheet.dart @@ -0,0 +1,256 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:dantex/src/data/book/entity/book.dart'; +import 'package:dantex/src/data/book/entity/book_label.dart'; +import 'package:dantex/src/providers/labels.dart'; +import 'package:dantex/src/providers/repository.dart'; +import 'package:dantex/src/ui/core/dante_components.dart'; +import 'package:dantex/src/ui/core/generic_error_widget.dart'; +import 'package:dantex/src/ui/core/platform_components.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flex_color_picker/flex_color_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AddLabelBottomSheet extends ConsumerWidget { + final Book book; + + const AddLabelBottomSheet({ + required this.book, + }) : super(key: const ValueKey('add-label-bottom-sheet')); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final allBookLabels = ref.watch(getBookLabelsProvider); + return BottomSheet( + onClosing: () {}, + showDragHandle: true, + builder: (context) => SafeArea( + child: allBookLabels.when( + data: (allLabels) { + final availableLabels = allLabels.where( + (label) => !book.labels.contains(label), + ); + return Column( + children: [ + Text( + 'add_label_bottom_sheet.pick_a_label'.tr(), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Visibility( + visible: availableLabels.isNotEmpty, + replacement: Text( + key: const ValueKey('no-labels-available'), + 'add_label_bottom_sheet.no_labels_available'.tr(), + textAlign: TextAlign.center, + ), + child: CarouselSlider( + options: CarouselOptions( + aspectRatio: 21 / 9, + viewportFraction: 0.4, + enlargeCenterPage: true, + enableInfiniteScroll: false, + ), + items: availableLabels + .map( + (bookLabel) => _LabelCard( + book: book, + bookLabel: bookLabel, + ), + ) + .toList(), + ), + ), + DanteOutlinedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.add), + Text('add_label_bottom_sheet.create_new_label'.tr()), + ], + ), + onPressed: () async => showDanteDialog( + context, + title: 'add_label_bottom_sheet.create_new_label'.tr(), + content: const _CreateLabelDialog(), + ), + ), + ], + ); + }, + error: (error, stackTrace) { + return GenericErrorWidget(error); + }, + loading: () { + return const Center( + child: CircularProgressIndicator.adaptive( + key: ValueKey('add-labels-loading'), + ), + ); + }, + ), + ), + ); + } +} + +class _LabelCard extends ConsumerWidget { + final Book book; + final BookLabel bookLabel; + + const _LabelCard({ + required this.book, + required this.bookLabel, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: ListView( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + PopupMenuButton( + onSelected: (item) async { + await ref.read(bookLabelRepositoryProvider).deleteBookLabel( + bookLabel.id, + ); + + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + value: 1, + child: Row( + children: [ + const Icon( + Icons.delete_outlined, + color: Colors.red, + ), + Text('delete'.tr()), + ], + ), + ), + ], + ), + ], + ), + GestureDetector( + key: ValueKey('label-card-${bookLabel.title}'), + onTap: () async { + Navigator.of(context).pop(); + final labels = [...book.labels, bookLabel]; + final updatedBook = book.copyWith( + labels: labels, + ); + await ref.read(bookRepositoryProvider).update( + updatedBook, + ); + }, + child: Icon( + Icons.sell_rounded, + color: bookLabel.hexColor.toColor, + size: 80, + ), + ), + Text( + bookLabel.title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} + +class _CreateLabelDialog extends ConsumerStatefulWidget { + const _CreateLabelDialog() + : super(key: const ValueKey('create-label-dialog')); + + @override + createState() => _CreateLabelDialogState(); +} + +class _CreateLabelDialogState extends ConsumerState<_CreateLabelDialog> { + Color screenPickerColor = const Color(0xFF7D20FF); + final textController = TextEditingController(); + + @override + Widget build(BuildContext context) { + final Map, String> customSwatches = + , String>{ + ColorTools.createPrimarySwatch(const Color(0xFF7D20FF)): 'Purple', + ColorTools.createPrimarySwatch(const Color(0xFFFFA500)): 'Orange', + ColorTools.createPrimarySwatch(const Color(0xFFFF00FF)): 'Pink', + ColorTools.createPrimarySwatch(const Color(0xFFCD853F)): 'Brown', + ColorTools.createPrimarySwatch(const Color(0xFF00E5EE)): 'Cyan', + ColorTools.createPrimarySwatch(const Color(0xFF00FF00)): 'Lime Green', + ColorTools.createPrimarySwatch(const Color(0xFF0000FF)): 'Blue', + ColorTools.createPrimarySwatch(const Color(0xFFFF0000)): 'Red', + ColorTools.createPrimarySwatch(const Color(0xFF006400)): 'Dark Green', + ColorTools.createPrimarySwatch(const Color(0xFFEE82EE)): 'Lavender', + }; + + return Material( + child: Column( + children: [ + DanteTextField( + controller: textController, + hint: 'add_label_bottom_sheet.name'.tr(), + formatter: LengthLimitingTextInputFormatter(16), + ), + ColorPicker( + color: screenPickerColor, + enableShadesSelection: false, + pickersEnabled: const { + ColorPickerType.primary: false, + ColorPickerType.accent: false, + ColorPickerType.bw: false, + ColorPickerType.custom: true, + ColorPickerType.wheel: false, + }, + onColorChanged: (Color color) => + setState(() => screenPickerColor = color), + customColorSwatchesAndNames: customSwatches, + width: 36, + height: 36, + borderRadius: 22, + title: Text( + 'add_label_bottom_sheet.choose_a_color'.tr(), + style: Theme.of(context).textTheme.labelMedium, + ), + ), + DanteOutlinedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.add), + Text('add_label_bottom_sheet.create_new_label'.tr()), + ], + ), + onPressed: () async { + Navigator.of(context).pop(); + + await ref.read(bookLabelRepositoryProvider).createBookLabel( + BookLabel( + id: '', + title: textController.text, + hexColor: screenPickerColor.value.toRadixString(16), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/ui/book/book_detail_page.dart b/lib/src/ui/book/book_detail_page.dart index b3f63d3..4b7086a 100644 --- a/lib/src/ui/book/book_detail_page.dart +++ b/lib/src/ui/book/book_detail_page.dart @@ -1,6 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:dantex/src/data/book/entity/book.dart'; +import 'package:dantex/src/data/book/entity/book_label.dart'; import 'package:dantex/src/providers/book.dart'; +import 'package:dantex/src/providers/repository.dart'; +import 'package:dantex/src/ui/book/add_label_bottom_sheet.dart'; import 'package:dantex/src/ui/core/generic_error_widget.dart'; import 'package:dantex/src/util/extensions.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -206,19 +209,21 @@ class _BookActions extends StatelessWidget { } } -class _BookLabels extends StatelessWidget { +class _BookLabels extends ConsumerWidget { final Book book; const _BookLabels({required this.book}) : super(key: const ValueKey('book-detail-labels')); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Row( children: [ OutlinedButton( key: const ValueKey('book-detail-add-label'), - onPressed: () {}, + onPressed: () async { + await _showAddLabelBottomSheet(context, book.labels); + }, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -227,16 +232,65 @@ class _BookLabels extends StatelessWidget { ], ), ), - ...book.labels.map( - (bookLabel) => Chip( - key: ValueKey('book-label-chip-${bookLabel.title}'), - color: MaterialStateProperty.all(bookLabel.hexColor.toColor()), - label: Text(bookLabel.title), - ), + Row( + children: [ + ...book.labels.map( + (bookLabel) => Padding( + padding: const EdgeInsets.only(left: 8), + child: Chip( + key: ValueKey('book-label-chip-${bookLabel.title}'), + deleteIcon: Icon( + Icons.cancel, + key: ValueKey('book-label-chip-${bookLabel.title}-delete'), + ), + deleteIconColor: bookLabel.hexColor.toColor(), + padding: const EdgeInsets.symmetric(), + onDeleted: () async { + await ref + .read(bookRepositoryProvider) + .removeLabelFromBook(book.id, bookLabel.id); + }, + label: Text( + bookLabel.title, + style: TextStyle( + fontSize: 12, + color: bookLabel.hexColor.toColor(), + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: Colors.transparent, + shape: StadiumBorder( + side: BorderSide(color: bookLabel.hexColor.toColor()), + ), + ), + ), + ), + ], ), ], ); } + + Future _showAddLabelBottomSheet( + BuildContext context, + List bookLabels, + ) async { + await showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: AddLabelBottomSheet( + book: book, + ), + ), + ); + }, + ); + } } class _BookSaveState extends StatelessWidget { diff --git a/lib/src/ui/core/dante_components.dart b/lib/src/ui/core/dante_components.dart index c3fbd89..63976f6 100644 --- a/lib/src/ui/core/dante_components.dart +++ b/lib/src/ui/core/dante_components.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class DanteDivider extends StatelessWidget { final double width; @@ -25,6 +26,7 @@ class DanteTextField extends StatelessWidget { final void Function(String)? onChanged; final int maxLines; final String? errorText; + final TextInputFormatter? formatter; const DanteTextField({ required this.controller, @@ -39,6 +41,7 @@ class DanteTextField extends StatelessWidget { this.enabled, this.onChanged, this.errorText, + this.formatter, }); @override @@ -56,6 +59,7 @@ class DanteTextField extends StatelessWidget { textInputAction: textInputAction, maxLines: maxLines, onChanged: onChanged, + inputFormatters: [if (formatter != null) formatter!], decoration: InputDecoration( errorText: errorText, hintText: hint, diff --git a/lib/src/ui/core/platform_components.dart b/lib/src/ui/core/platform_components.dart index 0b49696..eac35b9 100644 --- a/lib/src/ui/core/platform_components.dart +++ b/lib/src/ui/core/platform_components.dart @@ -16,7 +16,7 @@ class DanteDialogAction { Future showDanteDialog( BuildContext context, { required String title, - required String content, + required Widget content, Widget? leading, List actions = const [], }) { @@ -24,7 +24,7 @@ Future showDanteDialog( context: context, builder: (_) => PlatformAlertDialog( title: _buildDialogTitle(title, leading), - content: Text(content), + content: content, actions: actions .map( (action) => PlatformDialogAction( diff --git a/lib/src/ui/login/email_login_page.dart b/lib/src/ui/login/email_login_page.dart index 7197230..9b73250 100644 --- a/lib/src/ui/login/email_login_page.dart +++ b/lib/src/ui/login/email_login_page.dart @@ -117,8 +117,8 @@ class EmailLoginPageState extends ConsumerState { duration: const Duration(milliseconds: 500), child: DanteOutlinedButton( key: ValueKey(_phase), - child: Text(_getButtonText()), onPressed: _getButtonAction(), + child: Text(_getButtonText()), ), ), ], @@ -224,7 +224,7 @@ class EmailLoginPageState extends ConsumerState { return showDanteDialog( context, title: 'reset_password'.tr(), - content: 'reset_password_text'.tr(), + content: Text('reset_password_text'.tr()), actions: [ DanteDialogAction( action: (_) { @@ -260,7 +260,7 @@ class EmailLoginPageState extends ConsumerState { Icons.g_mobiledata, color: Colors.red, ), - content: 'email_in_use_description'.tr(), + content: Text('email_in_use_description'.tr()), actions: [ DanteDialogAction( action: (_) { diff --git a/lib/src/ui/login/login_page.dart b/lib/src/ui/login/login_page.dart index ecd5afa..3fadf33 100644 --- a/lib/src/ui/login/login_page.dart +++ b/lib/src/ui/login/login_page.dart @@ -163,7 +163,7 @@ class LoginPageState extends ConsumerState { await showDanteDialog( context, title: 'anonymous_login.title'.tr(), - content: 'anonymous_login.description'.tr(), + content: Text('anonymous_login.description'.tr()), actions: [ DanteDialogAction( action: (_) { diff --git a/lib/src/ui/profile/email_bottom_sheet.dart b/lib/src/ui/profile/email_bottom_sheet.dart index b029205..532df8b 100644 --- a/lib/src/ui/profile/email_bottom_sheet.dart +++ b/lib/src/ui/profile/email_bottom_sheet.dart @@ -83,8 +83,8 @@ class EmailBottomSheetState extends ConsumerState { duration: const Duration(milliseconds: 500), child: DanteOutlinedButton( key: ValueKey(_phase), - child: Text(_getButtonText()), onPressed: _getButtonAction(), + child: Text(_getButtonText()), ), ), ], @@ -167,7 +167,7 @@ class EmailBottomSheetState extends ConsumerState { Icons.g_mobiledata, color: Colors.red, ), - content: 'email_in_use_description'.tr(), + content: Text('email_in_use_description'.tr()), actions: [ DanteDialogAction( action: (_) => Navigator.of(context).pop(), diff --git a/pubspec.lock b/pubspec.lock index aea3055..995756c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42" + url: "https://pub.dev" + source: hosted + version: "4.2.1" characters: dependency: transitive description: @@ -465,6 +473,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + flex_color_picker: + dependency: "direct main" + description: + name: flex_color_picker + sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c + url: "https://pub.dev" + source: hosted + version: "3.3.0" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + url: "https://pub.dev" + source: hosted + version: "1.4.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 44b3693..9861254 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: rxdart: ^0.27.7 freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 + carousel_slider: ^4.2.1 + flex_color_picker: ^3.3.0 dev_dependencies: flutter_test: diff --git a/test/data/book/firebase_book_repository_test.dart b/test/data/book/firebase_book_repository_test.dart index f15c588..2af4348 100644 --- a/test/data/book/firebase_book_repository_test.dart +++ b/test/data/book/firebase_book_repository_test.dart @@ -43,6 +43,7 @@ void main() { rating: 1, notes: 'notes', summary: 'summary', + labels: [], ); when(user.uid).thenReturn('userId'); diff --git a/test/ui/book_detail_page_test.dart b/test/ui/book_detail_page_test.dart index 73dae2c..7e91d3c 100644 --- a/test/ui/book_detail_page_test.dart +++ b/test/ui/book_detail_page_test.dart @@ -15,7 +15,7 @@ void main() { ProviderScope( overrides: [ bookProvider('book-id').overrideWith( - (provider) => Future.value(book), + (ref) => Stream.value(book), ), ], child: const MaterialApp( @@ -71,7 +71,7 @@ void main() { ProviderScope( overrides: [ bookProvider('book-id').overrideWith( - (provider) => book, + (ref) => Stream.value(book), ), ], child: const MaterialApp( @@ -91,7 +91,7 @@ void main() { ProviderScope( overrides: [ bookProvider('book-id').overrideWith( - (provider) => Future.error('Error fetching book'), + (ref) => Stream.error('Error fetching book'), ), ], child: const MaterialApp(