diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index e36730d..5410312 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 162 (81 per locale) +/// Strings: 182 (91 per locale) /// -/// Built on 2024-02-23 at 17:01 UTC +/// Built on 2024-02-23 at 18:23 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -170,6 +170,7 @@ class _StringsGenericEn { String get save => 'Save'; String get close => 'Close'; String get error => 'Error'; + String get optional => 'Optional'; } // Path: onboarding @@ -297,6 +298,13 @@ class _StringsLinksAddLinkEn { // Translations String get addLink => 'Add new link'; + String get bookmarkUrl => 'Bookmark URL'; + String get bookmarkDetails => 'Bookmark details'; + String get validateUrl => 'Validate URL'; + String get urlValid => 'The URL is valid'; + String get urlNotValid => 'The URL is not valid'; + String get cannotCheckUrl => 'The URL couldn\'t be checked'; + String get invalidUrl => 'Invalid URL'; String get url => 'URL'; String get checkingUrl => 'Checking URL...'; String get title => 'Title'; @@ -304,6 +312,8 @@ class _StringsLinksAddLinkEn { String get markAsUnread => 'Mark as unread'; String get notes => 'Notes'; String get addNotes => 'Add notes'; + String get tags => 'Tags'; + String get others => 'Others'; String get savingLink => 'Saving link...'; String get errorSavingLink => 'An error occured when saving the new link.'; String get leaveEmptyUseWebsiteTitle => 'Leave empty to use the website\'s title.'; @@ -397,6 +407,7 @@ class _StringsGenericEs implements _StringsGenericEn { @override String get save => 'Guardar'; @override String get close => 'Cerrar'; @override String get error => 'Error'; + @override String get optional => 'Opcional'; } // Path: onboarding @@ -525,12 +536,21 @@ class _StringsLinksAddLinkEs implements _StringsLinksAddLinkEn { // Translations @override String get addLink => 'Añadir nuevo enlace'; @override String get url => 'URL'; + @override String get bookmarkUrl => 'URL del marcador'; + @override String get bookmarkDetails => 'Detalles del marcador'; + @override String get validateUrl => 'Validar URL'; + @override String get urlValid => 'La URL es válida'; + @override String get urlNotValid => 'La URL no es válida'; + @override String get cannotCheckUrl => 'No se pudo comprobar la URL'; + @override String get invalidUrl => 'URL no válida'; @override String get checkingUrl => 'Comprobando URL...'; @override String get title => 'Title'; @override String get description => 'Description'; @override String get markAsUnread => 'Marcar como no leído'; @override String get notes => 'Notas'; @override String get addNotes => 'Añadir notas'; + @override String get tags => 'Etiquetas'; + @override String get others => 'Otros'; @override String get savingLink => 'Guardando enlace...'; @override String get errorSavingLink => 'Ocurrió un error al guardar el nuevo enlace.'; @override String get leaveEmptyUseWebsiteTitle => 'Dejar vacío para usar el título del sitio web.'; @@ -590,6 +610,7 @@ extension on Translations { case 'generic.save': return 'Save'; case 'generic.close': return 'Close'; case 'generic.error': return 'Error'; + case 'generic.optional': return 'Optional'; case 'onboarding.title': return 'Welcome to Linkdy'; case 'onboarding.subtitle': return 'An application to manage your bookmarks.'; case 'onboarding.start': return 'Start'; @@ -615,6 +636,13 @@ extension on Translations { case 'links.dates.todayAt': return ({required Object time}) => 'Today, ${time}'; case 'links.dates.yesterdayAt': return ({required Object time}) => 'Yesterday, ${time}'; case 'links.addLink.addLink': return 'Add new link'; + case 'links.addLink.bookmarkUrl': return 'Bookmark URL'; + case 'links.addLink.bookmarkDetails': return 'Bookmark details'; + case 'links.addLink.validateUrl': return 'Validate URL'; + case 'links.addLink.urlValid': return 'The URL is valid'; + case 'links.addLink.urlNotValid': return 'The URL is not valid'; + case 'links.addLink.cannotCheckUrl': return 'The URL couldn\'t be checked'; + case 'links.addLink.invalidUrl': return 'Invalid URL'; case 'links.addLink.url': return 'URL'; case 'links.addLink.checkingUrl': return 'Checking URL...'; case 'links.addLink.title': return 'Title'; @@ -622,6 +650,8 @@ extension on Translations { case 'links.addLink.markAsUnread': return 'Mark as unread'; case 'links.addLink.notes': return 'Notes'; case 'links.addLink.addNotes': return 'Add notes'; + case 'links.addLink.tags': return 'Tags'; + case 'links.addLink.others': return 'Others'; case 'links.addLink.savingLink': return 'Saving link...'; case 'links.addLink.errorSavingLink': return 'An error occured when saving the new link.'; case 'links.addLink.leaveEmptyUseWebsiteTitle': return 'Leave empty to use the website\'s title.'; @@ -679,6 +709,7 @@ extension on _StringsEs { case 'generic.save': return 'Guardar'; case 'generic.close': return 'Cerrar'; case 'generic.error': return 'Error'; + case 'generic.optional': return 'Opcional'; case 'onboarding.title': return 'Bienvenido a Linkdy'; case 'onboarding.subtitle': return 'Una aplicación para gestionar tus enlaces favoritos.'; case 'onboarding.start': return 'Comenzar'; @@ -705,12 +736,21 @@ extension on _StringsEs { case 'links.dates.yesterdayAt': return ({required Object time}) => 'Ayer, ${time}'; case 'links.addLink.addLink': return 'Añadir nuevo enlace'; case 'links.addLink.url': return 'URL'; + case 'links.addLink.bookmarkUrl': return 'URL del marcador'; + case 'links.addLink.bookmarkDetails': return 'Detalles del marcador'; + case 'links.addLink.validateUrl': return 'Validar URL'; + case 'links.addLink.urlValid': return 'La URL es válida'; + case 'links.addLink.urlNotValid': return 'La URL no es válida'; + case 'links.addLink.cannotCheckUrl': return 'No se pudo comprobar la URL'; + case 'links.addLink.invalidUrl': return 'URL no válida'; case 'links.addLink.checkingUrl': return 'Comprobando URL...'; case 'links.addLink.title': return 'Title'; case 'links.addLink.description': return 'Description'; case 'links.addLink.markAsUnread': return 'Marcar como no leído'; case 'links.addLink.notes': return 'Notas'; case 'links.addLink.addNotes': return 'Añadir notas'; + case 'links.addLink.tags': return 'Etiquetas'; + case 'links.addLink.others': return 'Otros'; case 'links.addLink.savingLink': return 'Guardando enlace...'; case 'links.addLink.errorSavingLink': return 'Ocurrió un error al guardar el nuevo enlace.'; case 'links.addLink.leaveEmptyUseWebsiteTitle': return 'Dejar vacío para usar el título del sitio web.'; diff --git a/lib/i18n/strings_en.i18n.json b/lib/i18n/strings_en.i18n.json index e332de1..c948380 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -5,7 +5,8 @@ "next": "Next", "save": "Save", "close": "Close", - "error": "Error" + "error": "Error", + "optional": "Optional" }, "onboarding": { "title": "Welcome to Linkdy", @@ -38,6 +39,13 @@ }, "addLink": { "addLink": "Add new link", + "bookmarkUrl": "Bookmark URL", + "bookmarkDetails": "Bookmark details", + "validateUrl": "Validate URL", + "urlValid": "The URL is valid", + "urlNotValid": "The URL is not valid", + "cannotCheckUrl": "The URL couldn't be checked", + "invalidUrl": "Invalid URL", "url": "URL", "checkingUrl": "Checking URL...", "title": "Title", @@ -45,6 +53,8 @@ "markAsUnread": "Mark as unread", "notes": "Notes", "addNotes": "Add notes", + "tags": "Tags", + "others": "Others", "savingLink": "Saving link...", "errorSavingLink": "An error occured when saving the new link.", "leaveEmptyUseWebsiteTitle": "Leave empty to use the website's title.", diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index 73206f6..ba713cd 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -5,7 +5,8 @@ "next": "Siguiente", "save": "Guardar", "close": "Cerrar", - "error": "Error" + "error": "Error", + "optional": "Opcional" }, "onboarding": { "title": "Bienvenido a Linkdy", @@ -39,12 +40,21 @@ "addLink": { "addLink": "Añadir nuevo enlace", "url": "URL", + "bookmarkUrl": "URL del marcador", + "bookmarkDetails": "Detalles del marcador", + "validateUrl": "Validar URL", + "urlValid": "La URL es válida", + "urlNotValid": "La URL no es válida", + "cannotCheckUrl": "No se pudo comprobar la URL", + "invalidUrl": "URL no válida", "checkingUrl": "Comprobando URL...", "title": "Title", "description": "Description", "markAsUnread": "Marcar como no leído", "notes": "Notas", "addNotes": "Añadir notas", + "tags": "Etiquetas", + "others": "Otros", "savingLink": "Guardando enlace...", "errorSavingLink": "Ocurrió un error al guardar el nuevo enlace.", "leaveEmptyUseWebsiteTitle": "Dejar vacío para usar el título del sitio web.", diff --git a/lib/screens/links/model/add_link.model.dart b/lib/screens/links/model/add_link.model.dart index f34de39..5ff4cb4 100644 --- a/lib/screens/links/model/add_link.model.dart +++ b/lib/screens/links/model/add_link.model.dart @@ -1,4 +1,3 @@ -import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:linkdy/constants/enums.dart'; @@ -12,8 +11,8 @@ class AddLinkModel { final TextEditingController titleController; final TextEditingController descriptionController; bool markAsUnread; + final List tags; final TextEditingController notesController; - final ExpandableController expandableController; AddLinkModel({ required this.urlController, @@ -23,7 +22,7 @@ class AddLinkModel { required this.titleController, required this.descriptionController, this.markAsUnread = false, + required this.tags, required this.notesController, - required this.expandableController, }); } diff --git a/lib/screens/links/provider/add_link.provider.dart b/lib/screens/links/provider/add_link.provider.dart index 62f2226..0167d44 100644 --- a/lib/screens/links/provider/add_link.provider.dart +++ b/lib/screens/links/provider/add_link.provider.dart @@ -1,13 +1,11 @@ -import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; +import 'package:linkdy/utils/snackbar.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:linkdy/screens/links/ui/error_save_link.dart'; import 'package:linkdy/screens/links/provider/links.provider.dart'; import 'package:linkdy/screens/links/model/add_link.model.dart'; import 'package:linkdy/i18n/strings.g.dart'; -import 'package:linkdy/router/routes.dart'; import 'package:linkdy/utils/process_modal.dart'; import 'package:linkdy/models/data/post_bookmark.dart'; import 'package:linkdy/providers/router_provider.dart'; @@ -40,8 +38,8 @@ class AddLink extends _$AddLink { urlController: TextEditingController(), titleController: TextEditingController(), descriptionController: TextEditingController(), + tags: [], notesController: TextEditingController(), - expandableController: ExpandableController(), ); } @@ -52,7 +50,7 @@ class AddLink extends _$AddLink { state.urlError = null; ref.notifyListeners(); } else { - state.urlError = "Invalid URL"; + state.urlError = t.links.addLink.invalidUrl; ref.notifyListeners(); } } @@ -102,9 +100,9 @@ class AddLink extends _$AddLink { ref.invalidate(linksRequestProvider); ref.watch(routerProvider).pop(); } else { - showDialog( - context: rootNavigatorKey.currentContext!, - builder: (context) => const ErrorSaveLink(), + showSnacbkar( + label: t.links.addLink.errorSavingLink, + color: Colors.red, ); } } diff --git a/lib/screens/links/ui/add_link_modal.dart b/lib/screens/links/ui/add_link_modal.dart index a8d7d02..b4142ba 100644 --- a/lib/screens/links/ui/add_link_modal.dart +++ b/lib/screens/links/ui/add_link_modal.dart @@ -1,266 +1,231 @@ -import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:linkdy/screens/links/provider/add_link.provider.dart'; +import 'package:linkdy/widgets/autocomplete_text_field.dart'; +import 'package:linkdy/widgets/section_label.dart'; -import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/i18n/strings.g.dart'; +import 'package:linkdy/constants/enums.dart'; -class AddLinkModal extends ConsumerStatefulWidget { - const AddLinkModal({Key? key}) : super(key: key); +class AddLinkModal extends ConsumerWidget { + final bool fullscreen; + + const AddLinkModal({ + Key? key, + required this.fullscreen, + }) : super(key: key); @override - AddLinkModalState createState() => AddLinkModalState(); -} - -class AddLinkModalState extends ConsumerState with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _animation; - - @override - void initState() { - _animationController = AnimationController( - duration: const Duration(milliseconds: 200), - vsync: this, - ); - _animation = Tween( - begin: 0.0, - end: 0.5, - ).animate( - CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, + Widget build(BuildContext context, WidgetRef ref) { + return Dialog.fullscreen( + child: Scaffold( + appBar: AppBar( + leading: CloseButton( + onPressed: () => Navigator.pop(context), + ), + title: Text(t.links.addLink.addLink), + actions: [ + IconButton( + onPressed: ref.watch(addLinkProvider).checkBookmark != null + ? () => ref.read(addLinkProvider.notifier).addBookmark() + : null, + icon: const Icon(Icons.save_rounded), + tooltip: t.generic.save, + ), + const SizedBox(width: 8), + ], + ), + body: const _ModalContent(), ), ); - - super.initState(); } +} + +class _ModalContent extends ConsumerWidget { + const _ModalContent({Key? key}) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final provider = ref.watch(addLinkProvider); - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only(topLeft: Radius.circular(28), topRight: Radius.circular(28)), - color: Theme.of(context).dialogBackgroundColor, - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 24), - Icon( - Icons.add_rounded, - color: Theme.of(context).colorScheme.secondary, - size: 24, - ), - const SizedBox(height: 16), - Text( - t.links.addLink.addLink, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 24), - ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: provider.urlController, - onChanged: ref.read(addLinkProvider.notifier).validateUrl, - enabled: provider.checkBookmarkLoadStatus != LoadStatus.loading, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.link_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - errorText: provider.urlError, - labelText: t.links.addLink.url, - ), - ), - ), - if (provider.checkBookmarkLoadStatus == LoadStatus.loading) - Padding( - padding: const EdgeInsets.only(top: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox( - height: 12, - width: 12, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ), - const SizedBox(width: 8), - Text(t.links.addLink.checkingUrl), - ], - ), - ), - if (provider.checkBookmark != null) ...[ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Divider(), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: provider.titleController, - // onChanged: ref.read(addLinkProvider.notifier).validateUrl, - // enabled: provider.checkBookmarkLoadStatus != LoadStatus.loading, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.title_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - errorText: provider.urlError, - labelText: t.links.addLink.title, - hintText: provider.checkBookmark?.metadata?.title, - floatingLabelBehavior: FloatingLabelBehavior.always, - helperText: t.links.addLink.leaveEmptyUseWebsiteTitle, - ), - ), - ), - const SizedBox(height: 24), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: TextFormField( - controller: provider.descriptionController, - // onChanged: ref.read(addLinkProvider.notifier).validateUrl, - // enabled: provider.checkBookmarkLoadStatus != LoadStatus.loading, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.description_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - errorText: provider.urlError, - labelText: t.links.addLink.description, - hintText: provider.checkBookmark?.metadata?.description, - floatingLabelBehavior: FloatingLabelBehavior.always, - helperText: t.links.addLink.leaveEmptyUseWebsiteDescription, - ), - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: ExpandablePanel( - controller: provider.expandableController, - collapsed: ListTile( - onTap: () { - _animationController.animateTo(provider.expandableController.expanded == true ? 0 : 1); - provider.expandableController.toggle(); - }, - title: Text(t.links.addLink.addNotes), - trailing: RotationTransition( - turns: _animation, - child: Icon( - Icons.expand_more_rounded, - size: 26, - color: Theme.of(context).colorScheme.secondary, - ), - ), - contentPadding: const EdgeInsets.all(0), - ), - expanded: Column( - children: [ - ListTile( - onTap: () { - _animationController.animateTo( - provider.expandableController.expanded == true ? 0 : 1, - ); - provider.expandableController.toggle(); - }, - title: Text(t.links.addLink.addNotes), - trailing: RotationTransition( - turns: _animation, - child: Icon( - Icons.expand_more_rounded, - size: 26, - color: Theme.of(context).colorScheme.secondary, - ), - ), - contentPadding: const EdgeInsets.all(0), - ), - const SizedBox(height: 8), - SizedBox( - height: 130, - child: TextFormField( - controller: provider.notesController, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.note_rounded), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( - Radius.circular(10), - ), - ), - labelText: t.links.addLink.notes, - ), - autocorrect: false, - expands: true, - minLines: null, - maxLines: null, - textAlignVertical: TextAlignVertical.top, - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ), - SwitchListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 0), - title: Text(t.links.addLink.markAsUnread), - value: provider.markAsUnread, - onChanged: ref.read(addLinkProvider.notifier).updateMarkAsUnread, - ), - ], - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(24), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(t.generic.cancel), - ), - const SizedBox(width: 8), - if (provider.checkBookmark == null) - TextButton( - onPressed: provider.checkBookmarkLoadStatus != LoadStatus.loading - ? ref.read(addLinkProvider.notifier).checkUrlDetails - : null, - child: Text(t.generic.next), - ), - if (provider.checkBookmark != null) - TextButton( - onPressed: ref.read(addLinkProvider.notifier).addBookmark, - child: Text(t.generic.confirm), - ), - ], - ), - ), - ], + return ListView( + children: [ + const SizedBox(height: 16), + SectionLabel( + label: t.links.addLink.bookmarkUrl, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 0, ), ), - ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextFormField( + controller: provider.urlController, + onChanged: ref.read(addLinkProvider.notifier).validateUrl, + enabled: provider.checkBookmarkLoadStatus != LoadStatus.loading, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.link_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + errorText: provider.urlError, + labelText: t.links.addLink.url, + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: ElevatedButton( + onPressed: provider.checkBookmarkLoadStatus == null && + provider.urlError == null && + provider.urlController.text != "" + ? () => ref.read(addLinkProvider.notifier).checkUrlDetails() + : null, + style: ButtonStyle( + foregroundColor: provider.checkBookmarkLoadStatus == LoadStatus.loaded + ? const MaterialStatePropertyAll(Colors.green) + : provider.checkBookmarkLoadStatus == LoadStatus.loaded + ? const MaterialStatePropertyAll(Colors.red) + : null, + backgroundColor: provider.checkBookmarkLoadStatus == LoadStatus.loaded + ? MaterialStatePropertyAll(Colors.green.withOpacity(0.15)) + : provider.checkBookmarkLoadStatus == LoadStatus.loaded + ? MaterialStatePropertyAll(Colors.red.withOpacity(0.15)) + : null, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (provider.checkBookmarkLoadStatus == null) Text(t.links.addLink.validateUrl), + if (provider.checkBookmarkLoadStatus == LoadStatus.loading) ...[ + const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + const SizedBox(width: 8), + Text(t.links.addLink.checkingUrl), + ], + if (provider.checkBookmarkLoadStatus == LoadStatus.loaded) ...[ + const Icon(Icons.check_circle_rounded), + const SizedBox(width: 8), + Text(t.links.addLink.urlValid), + ], + if (provider.checkBookmarkLoadStatus == LoadStatus.error) ...[ + const Icon(Icons.error_rounded), + const SizedBox(width: 8), + Text(t.links.addLink.cannotCheckUrl), + ], + ], + ), + ), + ), + ), + SectionLabel( + label: t.links.addLink.bookmarkDetails, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 24, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextFormField( + controller: provider.titleController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.title_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + labelText: t.links.addLink.title, + hintText: provider.checkBookmark?.metadata?.title, + floatingLabelBehavior: + provider.checkBookmark != null ? FloatingLabelBehavior.always : FloatingLabelBehavior.auto, + helperText: t.links.addLink.leaveEmptyUseWebsiteTitle, + enabled: provider.checkBookmark != null, + ), + ), + ), + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextFormField( + controller: provider.descriptionController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.description_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + labelText: t.links.addLink.description, + hintText: provider.checkBookmark?.metadata?.description, + floatingLabelBehavior: + provider.checkBookmark != null ? FloatingLabelBehavior.always : FloatingLabelBehavior.auto, + helperText: t.links.addLink.leaveEmptyUseWebsiteDescription, + enabled: provider.checkBookmark != null, + ), + ), + ), + SectionLabel( + label: t.links.addLink.tags, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 24, + ), + ), + SectionLabel( + label: t.links.addLink.others, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 24, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: 150, + child: TextFormField( + controller: provider.notesController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.note_rounded), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + labelText: t.links.addLink.notes, + helperText: t.generic.optional, + ), + autocorrect: false, + expands: true, + minLines: null, + maxLines: null, + textAlignVertical: TextAlignVertical.top, + enabled: provider.checkBookmark != null, + ), + ), + ), + const SizedBox(height: 12), + SwitchListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + title: Text(t.links.addLink.markAsUnread), + value: provider.markAsUnread, + onChanged: + provider.checkBookmark != null ? (v) => ref.read(addLinkProvider.notifier).updateMarkAsUnread(v) : null, + ), + ], ); } } diff --git a/lib/screens/links/ui/links.dart b/lib/screens/links/ui/links.dart index ac637f3..fa4ad4a 100644 --- a/lib/screens/links/ui/links.dart +++ b/lib/screens/links/ui/links.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -16,6 +18,8 @@ class Links extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final bookmarks = ref.watch(linksRequestProvider); + final width = MediaQuery.of(context).size.width; + String validateStrings(String? string1, String? string2) { if (string1 != null && string1.isNotEmpty) { return string1; @@ -44,12 +48,18 @@ class Links extends ConsumerWidget { } void openAddModal() { - showModalBottomSheet( - useRootNavigator: true, + showGeneralDialog( context: context, - isScrollControlled: true, - useSafeArea: true, - builder: (context) => const AddLinkModal(), + barrierColor: !(width > 700 || !(Platform.isAndroid || Platform.isIOS)) ? Colors.transparent : Colors.black54, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween(begin: const Offset(0, 1), end: const Offset(0, 0)).animate( + CurvedAnimation(parent: anim1, curve: Curves.easeInOutCubicEmphasized), + ), + child: child, + ); + }, + pageBuilder: (context, animation, secondaryAnimation) => AddLinkModal(fullscreen: width <= 700), ); } diff --git a/lib/widgets/autocomplete_text_field.dart b/lib/widgets/autocomplete_text_field.dart new file mode 100644 index 0000000..255466e --- /dev/null +++ b/lib/widgets/autocomplete_text_field.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class AutocompleteTextField extends StatefulWidget { + final IconData leadingIcon; + final String label; + + const AutocompleteTextField({ + Key? key, + required this.leadingIcon, + required this.label, + }) : super(key: key); + + @override + State createState() => _AutocompleteTextFieldState(); +} + +class _AutocompleteTextFieldState extends State { + final _controller = TextEditingController(); + final _overlayController = OverlayPortalController(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + TextFormField( + controller: _controller, + onChanged: (value) { + if (value.isNotEmpty) { + _overlayController.show(); + } else { + _overlayController.hide(); + } + }, + decoration: InputDecoration( + prefixIcon: Icon(widget.leadingIcon), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(10), + ), + ), + labelText: widget.label, + ), + ), + OverlayPortal( + controller: _overlayController, + overlayChildBuilder: (context) => Positioned( + top: 0, + left: 0, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.grey, + ), + ), + ), + ), + ], + ); + } +}