diff --git a/assets/translations/de-DE.json b/assets/translations/de-DE.json index a57d4cc..e4d9a61 100644 --- a/assets/translations/de-DE.json +++ b/assets/translations/de-DE.json @@ -6,6 +6,53 @@ "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", + "error": { + "missing-information": "TODO Missing information", + "update": "TODO update", + "create": "TODO create" + } + }, + "language-picker": { + "title": "Sprache", + "empty": "Wähle eine Sprache" + }, + "date-picker": { + "title": "Veröffentlichungsdatum", + "empty": "Wähle ein Datum" + }, + "countries": { + "not-available": "Nicht vorhanden", + "gb": "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", @@ -130,7 +177,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 bd51d96..7dff757 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -5,6 +5,53 @@ "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", + "error": { + "missing-information": "TODO Missing information", + "update": "TODO update", + "create": "TODO create" + } + }, + "language-picker": { + "title": "Language", + "empty": "Choose a language" + }, + "date-picker": { + "title": "Publish Date", + "empty": "Choose a date" + }, + "countries": { + "not-available": "Not specified", + "gb": "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/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 @@ 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/providers/app_router.dart b/lib/src/providers/app_router.dart index 72fbd34..efae740 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,18 @@ List _mainRoutes = [ builder: (BuildContext context, GoRouterState state) => const ScanBookPage(), ), + GoRoute( + path: DanteRoute.manualAdd.url, + builder: (BuildContext context, GoRouterState state) => + const ManualAddEditBookPage(), + ), + GoRoute( + path: DanteRoute.editBook.url, + builder: (context, state) { + final bookId = state.pathParameters['bookId'] ?? ''; + return ManualAddEditBookPage(bookId: bookId); + }, + ), GoRoute( path: DanteRoute.bookDetail.url, builder: (context, state) { @@ -199,6 +212,16 @@ enum DanteRoute { mobileUrl: 'scan', navigationUrl: '/scan', ), + manualAdd( + webUrl: '/add-manual', + 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 new file mode 100644 index 0000000..074ddf8 --- /dev/null +++ b/lib/src/ui/add/manual_add_edit_book_page.dart @@ -0,0 +1,414 @@ +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/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/extensions.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 +/// -[ ] Upload image +/// -[ ] Test +/// -[ ] Web +/// -[ ] App +class ManualAddEditBookPage extends ConsumerStatefulWidget { + final String? bookId; + + const ManualAddEditBookPage({ + super.key, + this.bookId, + }); + + @override + ConsumerState createState() => _ManualAddEditBookPageState(); +} + +class _ManualAddEditBookPageState extends ConsumerState { + final TextEditingController _titleController = TextEditingController(); + final TextEditingController _authorController = TextEditingController(); + final TextEditingController _pageController = TextEditingController(); + final TextEditingController _subtitleController = TextEditingController(); + 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 = ''; + Book? _bookEditMode; + + bool get _isInEditMode => widget.bookId != null; + + @override + void initState() { + super.initState(); + + if (widget.bookId != null) { + _loadBookById(); + } + } + + void _loadBookById() { + unawaited( + ref.read(bookRepositoryProvider).getBook(widget.bookId!).first.then(_initializeWithBook), + ); + } + + void _initializeWithBook(Book book) { + _bookEditMode = book; + + _bookCoverController.value = book.thumbnailAddress; + _publishDateController.value = book.publishedDate.parseWithDefaultDateFormat(); + _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.withBackNavigation( + context: context, + onBack: () { + // TODO Handle back navigation + context.pop(); + }, + title: Text(_title ?? ''), + ), + Expanded( + child: Container( + color: Theme.of(context).colorScheme.surface, + 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: _buildActionButtons(), + ), + ], + ); + } + + Widget _buildActionButtons() { + if (_isInEditMode) { + return _buildEditBookActionButtons(); + } else { + return _buildNewBookActionButtons(); + } + } + + Widget _buildEditBookActionButtons() { + 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: _saveExistingBook, + 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), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: _buildRequiredInformationCard(isDesktop: true), + ), + const SizedBox(width: 64), + Expanded( + child: _buildOptionalInformationCard( + context, + isDesktop: true, + ), + ), + ], + ), + ); + } + + Widget _buildMobileLayout(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView( + shrinkWrap: true, + children: [ + _buildRequiredInformationCard(isDesktop: false), + const SizedBox(height: 16), + _buildOptionalInformationCard( + context, + isDesktop: false, + ), + ], + ), + ); + } + + Widget _buildRequiredInformationCard({required bool isDesktop}) { + return Card( + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + if (isDesktop) ...[ + const Spacer(), + BookCoverPickerWidget( + controller: _bookCoverController, + size: 200, + ), + const Spacer(), + ], + Row( + children: [ + if (!isDesktop) ...[ + BookCoverPickerWidget( + controller: _bookCoverController, + size: 64, + ), + const SizedBox(width: 16), + ], + Expanded( + child: DanteTextField( + controller: _titleController, + hint: 'add-manual.title'.tr(), + onChanged: (String? changedTitle) { + setState(() { + _title = changedTitle; + }); + }, + ), + ), + ], + ), + const SizedBox(height: 16), + DanteTextField( + controller: _authorController, + hint: 'add-manual.authors'.tr(), + ), + const SizedBox(height: 16), + DanteTextField( + controller: _pageController, + hint: 'add-manual.page-count'.tr(), + ), + if (isDesktop) const Spacer(), + ], + ), + ), + ); + } + + Widget _buildOptionalInformationCard(BuildContext context, {required bool isDesktop}) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'add-manual.optional-info'.tr(), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 32), + DanteTextField( + controller: _subtitleController, + hint: 'add-manual.subtitle'.tr(), + ), + const SizedBox(height: 16), + DatePickerWidget( + controller: _publishDateController, + ), + const SizedBox(height: 16), + LanguagePickerWidget( + controller: _languageController, + ), + const SizedBox(height: 16), + DanteTextField( + controller: _isbnController, + hint: 'add-manual.isbn'.tr(), + ), + const SizedBox(height: 16), + DanteTextField( + controller: _summaryController, + hint: 'add-manual.summary'.tr(), + maxLines: 5, + ), + ], + ), + ), + ); + } + + Widget _buildBookStateButton(BookState state) { + return SizedBox( + width: 120, + child: OutlinedButton( + onPressed: () async => _saveNewBook(state), + child: Text(_getTitleForBookState(state)), + ), + ); + } + + 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()) { + _showError(errorKey: 'add-manual.error.missing-information'); + } + + // 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() { + 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) { + 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/widgets/date_picker_widget.dart b/lib/src/ui/add/widgets/date_picker_widget.dart new file mode 100644 index 0000000..809871f --- /dev/null +++ b/lib/src/ui/add/widgets/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 StatelessWidget { + final DateController controller; + final Function(DateTime date)? onDateSelected; + + const DatePickerWidget({ + required this.controller, + this.onDateSelected, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'date-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), + height: 48, + 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: InkWell( + 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( + context: context, + firstDate: DateTime(1940), + lastDate: DateTime.now(), + ); + + if (selectedDateTime != null) { + onDateSelected?.call(selectedDateTime); + controller.value = selectedDateTime; + } + }, + ), + ), + ], + ); + } +} + +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/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/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..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, @@ -80,7 +83,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/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; 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), + ); + } } diff --git a/lib/src/util/extensions.dart b/lib/src/util/extensions.dart index c206cf6..bb0698d 100644 --- a/lib/src/util/extensions.dart +++ b/lib/src/util/extensions.dart @@ -15,13 +15,24 @@ extension HexColor on String { } } +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); } + String formatDefault() { + return _dfDefault.format(this); + } String formatWithMonthAndYearShort() { return _dfMonthShort.format(this); } diff --git a/pubspec.lock b/pubspec.lock index 8c8433d..dda0233 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: @@ -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: @@ -293,18 +301,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 +381,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 +453,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 +591,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 +623,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 @@ -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: @@ -829,6 +853,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 +905,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 +969,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 +1390,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 +1462,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 +1507,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 +1555,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..7d90c0f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,48 +12,50 @@ 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 + country_flags: ^2.2.0 dev_dependencies: flutter_test: 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