diff --git a/lib/constants/regexp.dart b/lib/constants/regexp.dart index 73c8675..605351a 100644 --- a/lib/constants/regexp.dart +++ b/lib/constants/regexp.dart @@ -1,6 +1,8 @@ class Regexps { static final url = RegExp(r'/https?:\/\/(?:www\.)?([-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b)*(\/[\/\d\w\.-]*)*(?:[\?])*(.+)*/gi'); + static final urlWithoutProtocol = + RegExp(r'[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)'); static final ipAddress = RegExp(r'^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$'); static final domain = RegExp(r'^(([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+)$'); } diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 53063f1..049edf4 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: 130 (65 per locale) +/// Strings: 150 (75 per locale) /// -/// Built on 2024-02-23 at 00:40 UTC +/// Built on 2024-02-23 at 14:24 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -166,6 +166,8 @@ class _StringsGenericEn { // Translations String get confirm => 'Confirm'; String get cancel => 'Cancel'; + String get next => 'Next'; + String get save => 'Save'; } // Path: onboarding @@ -207,6 +209,7 @@ class _StringsLinksEn { // Translations String get links => 'Links'; late final _StringsLinksDatesEn dates = _StringsLinksDatesEn._(_root); + late final _StringsLinksAddLinkEn addLink = _StringsLinksAddLinkEn._(_root); } // Path: search @@ -284,6 +287,23 @@ class _StringsLinksDatesEn { String yesterdayAt({required Object time}) => 'Yesterday, ${time}'; } +// Path: links.addLink +class _StringsLinksAddLinkEn { + _StringsLinksAddLinkEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get addLink => 'Add new link'; + String get url => 'URL'; + String get checkingUrl => 'Checking URL...'; + String get title => 'Title'; + String get description => 'Description'; + String get markAsUnread => 'Mark as unread'; + String get notes => 'Notes'; + String get addNotes => 'Add notes'; +} + // Path: settings.customization class _StringsSettingsCustomizationEn { _StringsSettingsCustomizationEn._(this._root); @@ -367,6 +387,8 @@ class _StringsGenericEs implements _StringsGenericEn { // Translations @override String get confirm => 'Confirmar'; @override String get cancel => 'Cancelar'; + @override String get next => 'Siguiente'; + @override String get save => 'Guardar'; } // Path: onboarding @@ -408,6 +430,7 @@ class _StringsLinksEs implements _StringsLinksEn { // Translations @override String get links => 'Enlaces'; @override late final _StringsLinksDatesEs dates = _StringsLinksDatesEs._(_root); + @override late final _StringsLinksAddLinkEs addLink = _StringsLinksAddLinkEs._(_root); } // Path: search @@ -485,6 +508,23 @@ class _StringsLinksDatesEs implements _StringsLinksDatesEn { @override String yesterdayAt({required Object time}) => 'Ayer, ${time}'; } +// Path: links.addLink +class _StringsLinksAddLinkEs implements _StringsLinksAddLinkEn { + _StringsLinksAddLinkEs._(this._root); + + @override final _StringsEs _root; // ignore: unused_field + + // Translations + @override String get addLink => 'Añadir nuevo enlace'; + @override String get url => 'URL'; + @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'; +} + // Path: settings.customization class _StringsSettingsCustomizationEs implements _StringsSettingsCustomizationEn { _StringsSettingsCustomizationEs._(this._root); @@ -534,6 +574,8 @@ extension on Translations { switch (path) { case 'generic.confirm': return 'Confirm'; case 'generic.cancel': return 'Cancel'; + case 'generic.next': return 'Next'; + case 'generic.save': return 'Save'; case 'onboarding.title': return 'Welcome to Linkdy'; case 'onboarding.subtitle': return 'An application to manage your bookmarks.'; case 'onboarding.start': return 'Start'; @@ -558,6 +600,14 @@ extension on Translations { case 'links.links': return 'Links'; 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.url': return 'URL'; + case 'links.addLink.checkingUrl': return 'Checking URL...'; + case 'links.addLink.title': return 'Title'; + case 'links.addLink.description': return 'Description'; + case 'links.addLink.markAsUnread': return 'Mark as unread'; + case 'links.addLink.notes': return 'Notes'; + case 'links.addLink.addNotes': return 'Add notes'; case 'search.search': return 'Search'; case 'settings.settings': return 'Settings'; case 'settings.appSettings': return 'App settings'; @@ -607,6 +657,8 @@ extension on _StringsEs { switch (path) { case 'generic.confirm': return 'Confirmar'; case 'generic.cancel': return 'Cancelar'; + case 'generic.next': return 'Siguiente'; + case 'generic.save': return 'Guardar'; case 'onboarding.title': return 'Bienvenido a Linkdy'; case 'onboarding.subtitle': return 'Una aplicación para gestionar tus enlaces favoritos.'; case 'onboarding.start': return 'Comenzar'; @@ -631,6 +683,14 @@ extension on _StringsEs { case 'links.links': return 'Enlaces'; case 'links.dates.todayAt': return ({required Object time}) => 'Hoy, ${time}'; 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.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 'search.search': return 'Buscar'; case 'settings.settings': return 'Ajustes'; case 'settings.appSettings': return 'Ajustes de la aplicación'; diff --git a/lib/i18n/strings_en.i18n.json b/lib/i18n/strings_en.i18n.json index 85261e5..ea5a14e 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -1,7 +1,9 @@ { "generic": { "confirm": "Confirm", - "cancel": "Cancel" + "cancel": "Cancel", + "next": "Next", + "save": "Save" }, "onboarding": { "title": "Welcome to Linkdy", @@ -31,6 +33,16 @@ "dates": { "todayAt": "Today, $time", "yesterdayAt": "Yesterday, $time" + }, + "addLink": { + "addLink": "Add new link", + "url": "URL", + "checkingUrl": "Checking URL...", + "title": "Title", + "description": "Description", + "markAsUnread": "Mark as unread", + "notes": "Notes", + "addNotes": "Add notes" } }, "search": { diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index 9e99062..e911c4f 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -1,7 +1,9 @@ { "generic": { "confirm": "Confirmar", - "cancel": "Cancelar" + "cancel": "Cancelar", + "next": "Siguiente", + "save": "Guardar" }, "onboarding": { "title": "Bienvenido a Linkdy", @@ -31,6 +33,16 @@ "dates": { "todayAt": "Hoy, $time", "yesterdayAt": "Ayer, $time" + }, + "addLink": { + "addLink": "Añadir nuevo enlace", + "url": "URL", + "checkingUrl": "Comprobando URL...", + "title": "Title", + "description": "Description", + "markAsUnread": "Marcar como no leído", + "notes": "Notas", + "addNotes": "Añadir notas" } }, "search": { diff --git a/lib/models/data/check_bookmark.dart b/lib/models/data/check_bookmark.dart new file mode 100644 index 0000000..8257aa9 --- /dev/null +++ b/lib/models/data/check_bookmark.dart @@ -0,0 +1,43 @@ +class CheckBookmark { + final dynamic bookmark; + final Metadata? metadata; + + CheckBookmark({ + this.bookmark, + this.metadata, + }); + + factory CheckBookmark.fromJson(Map json) => CheckBookmark( + bookmark: json["bookmark"], + metadata: json["metadata"] == null ? null : Metadata.fromJson(json["metadata"]), + ); + + Map toJson() => { + "bookmark": bookmark, + "metadata": metadata?.toJson(), + }; +} + +class Metadata { + final String? url; + final String? title; + final String? description; + + Metadata({ + this.url, + this.title, + this.description, + }); + + factory Metadata.fromJson(Map json) => Metadata( + url: json["url"], + title: json["title"], + description: json["description"], + ); + + Map toJson() => { + "url": url, + "title": title, + "description": description, + }; +} diff --git a/lib/screens/links/model/add_link.model.dart b/lib/screens/links/model/add_link.model.dart new file mode 100644 index 0000000..f34de39 --- /dev/null +++ b/lib/screens/links/model/add_link.model.dart @@ -0,0 +1,29 @@ +import 'package:expandable/expandable.dart'; +import 'package:flutter/material.dart'; + +import 'package:linkdy/constants/enums.dart'; +import 'package:linkdy/models/data/check_bookmark.dart'; + +class AddLinkModel { + final TextEditingController urlController; + String? urlError; + CheckBookmark? checkBookmark; + LoadStatus? checkBookmarkLoadStatus; + final TextEditingController titleController; + final TextEditingController descriptionController; + bool markAsUnread; + final TextEditingController notesController; + final ExpandableController expandableController; + + AddLinkModel({ + required this.urlController, + this.urlError, + this.checkBookmark, + this.checkBookmarkLoadStatus, + required this.titleController, + required this.descriptionController, + this.markAsUnread = false, + 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 new file mode 100644 index 0000000..a88bbeb --- /dev/null +++ b/lib/screens/links/provider/add_link.provider.dart @@ -0,0 +1,66 @@ +import 'package:expandable/expandable.dart'; +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'package:linkdy/screens/links/model/add_link.model.dart'; + +import 'package:linkdy/constants/enums.dart'; +import 'package:linkdy/constants/regexp.dart'; +import 'package:linkdy/models/api_response.dart'; +import 'package:linkdy/models/data/check_bookmark.dart'; +import 'package:linkdy/providers/api_client_provider.dart'; + +part 'add_link.provider.g.dart'; + +@riverpod +Future> checkBookmark(CheckBookmarkRef ref, String url) async { + final result = await ref.watch(apiClientProviderProvider)!.fetchCheckAddBookmark(url: url); + return result; +} + +@riverpod +class AddLink extends _$AddLink { + @override + AddLinkModel build() { + return AddLinkModel( + urlController: TextEditingController(), + titleController: TextEditingController(), + descriptionController: TextEditingController(), + notesController: TextEditingController(), + expandableController: ExpandableController(), + ); + } + + void validateUrl(String value) { + state.checkBookmark = null; + state.checkBookmarkLoadStatus = null; + if (Regexps.urlWithoutProtocol.hasMatch(value)) { + state.urlError = null; + ref.notifyListeners(); + } else { + state.urlError = "Invalid URL"; + ref.notifyListeners(); + } + } + + void checkUrlDetails() async { + if (state.urlError == null && state.urlController.text != "") { + state.checkBookmarkLoadStatus = LoadStatus.loading; + state.checkBookmark = null; + ref.notifyListeners(); + final result = await ref.read(checkBookmarkProvider(state.urlController.text).future); + if (result.successful == true) { + state.checkBookmark = result.content; + state.checkBookmarkLoadStatus = LoadStatus.loaded; + } else { + state.checkBookmarkLoadStatus = LoadStatus.error; + } + ref.notifyListeners(); + } + } + + void updateMarkAsUnread(bool value) { + state.markAsUnread = value; + ref.notifyListeners(); + } +} diff --git a/lib/screens/links/provider/add_link.provider.g.dart b/lib/screens/links/provider/add_link.provider.g.dart new file mode 100644 index 0000000..f6f596b --- /dev/null +++ b/lib/screens/links/provider/add_link.provider.g.dart @@ -0,0 +1,179 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'add_link.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$checkBookmarkHash() => r'a4397a4497e3b7c8dec6027bb927371cc85e526c'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [checkBookmark]. +@ProviderFor(checkBookmark) +const checkBookmarkProvider = CheckBookmarkFamily(); + +/// See also [checkBookmark]. +class CheckBookmarkFamily + extends Family>> { + /// See also [checkBookmark]. + const CheckBookmarkFamily(); + + /// See also [checkBookmark]. + CheckBookmarkProvider call( + String url, + ) { + return CheckBookmarkProvider( + url, + ); + } + + @override + CheckBookmarkProvider getProviderOverride( + covariant CheckBookmarkProvider provider, + ) { + return call( + provider.url, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'checkBookmarkProvider'; +} + +/// See also [checkBookmark]. +class CheckBookmarkProvider + extends AutoDisposeFutureProvider> { + /// See also [checkBookmark]. + CheckBookmarkProvider( + String url, + ) : this._internal( + (ref) => checkBookmark( + ref as CheckBookmarkRef, + url, + ), + from: checkBookmarkProvider, + name: r'checkBookmarkProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$checkBookmarkHash, + dependencies: CheckBookmarkFamily._dependencies, + allTransitiveDependencies: + CheckBookmarkFamily._allTransitiveDependencies, + url: url, + ); + + CheckBookmarkProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.url, + }) : super.internal(); + + final String url; + + @override + Override overrideWith( + FutureOr> Function(CheckBookmarkRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: CheckBookmarkProvider._internal( + (ref) => create(ref as CheckBookmarkRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + url: url, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _CheckBookmarkProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is CheckBookmarkProvider && other.url == url; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, url.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin CheckBookmarkRef + on AutoDisposeFutureProviderRef> { + /// The parameter `url` of this provider. + String get url; +} + +class _CheckBookmarkProviderElement + extends AutoDisposeFutureProviderElement> + with CheckBookmarkRef { + _CheckBookmarkProviderElement(super.provider); + + @override + String get url => (origin as CheckBookmarkProvider).url; +} + +String _$addLinkHash() => r'd971275978e113b16d1bc34a2370d9d3feba60b5'; + +/// See also [AddLink]. +@ProviderFor(AddLink) +final addLinkProvider = + AutoDisposeNotifierProvider.internal( + AddLink.new, + name: r'addLinkProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$addLinkHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AddLink = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/screens/links/ui/add_link_modal.dart b/lib/screens/links/ui/add_link_modal.dart new file mode 100644 index 0000000..dfc5cd7 --- /dev/null +++ b/lib/screens/links/ui/add_link_modal.dart @@ -0,0 +1,261 @@ +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/constants/enums.dart'; +import 'package:linkdy/i18n/strings.g.dart'; + +class AddLinkModal extends ConsumerStatefulWidget { + const AddLinkModal({Key? key}) : 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, + ), + ); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + 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 SizedBox(height: 24), + 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, + ), + ), + ), + 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, + ), + ), + ), + 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: () => Navigator.pop(context), + child: Text(t.generic.confirm), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/links/ui/links.dart b/lib/screens/links/ui/links.dart index 2474a83..ee36c90 100644 --- a/lib/screens/links/ui/links.dart +++ b/lib/screens/links/ui/links.dart @@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:linkdy/screens/links/provider/links.provider.dart'; +import 'package:linkdy/screens/links/ui/add_link_modal.dart'; + import 'package:linkdy/providers/router_provider.dart'; import 'package:linkdy/router/paths.dart'; import 'package:linkdy/i18n/strings.g.dart'; @@ -37,9 +39,18 @@ class Links extends ConsumerWidget { } } - return Material( - color: Colors.transparent, - child: NestedScrollView( + void openAddModal() { + showModalBottomSheet( + useRootNavigator: true, + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => const AddLinkModal(), + ); + } + + return Scaffold( + body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) => [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), @@ -168,6 +179,10 @@ class Links extends ConsumerWidget { ), ), ), + floatingActionButton: FloatingActionButton( + onPressed: openAddModal, + child: const Icon(Icons.add_rounded), + ), ); } } diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart index 0913f67..efe628f 100644 --- a/lib/services/api_client.dart +++ b/lib/services/api_client.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:linkdy/models/api_response.dart'; import 'package:linkdy/models/data/bookmarks.dart'; +import 'package:linkdy/models/data/check_bookmark.dart'; import 'package:linkdy/models/server_instance.dart'; class ApiClient { @@ -40,4 +41,21 @@ class ApiClient { return const ApiResponse(successful: false); } } + + Future> fetchCheckAddBookmark({required String url}) async { + try { + final response = await dioInstance.get( + "/bookmarks/check", + queryParameters: { + "url": url, + }, + ); + return ApiResponse( + successful: true, + content: CheckBookmark.fromJson(response.data), + ); + } catch (e, stackTrace) { + return const ApiResponse(successful: false); + } + } } diff --git a/pubspec.lock b/pubspec.lock index 9116bdf..460ddd3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -289,6 +289,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.9" + expandable: + dependency: "direct main" + description: + name: expandable + sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" + url: "https://pub.dev" + source: hosted + version: "5.0.1" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a9219b7..e8fabc9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: flutter_inappwebview: ^6.0.0 share_plus: ^7.2.2 package_info_plus: ^5.0.1 + expandable: ^5.0.1 dev_dependencies: build_runner: ^2.4.8