From 776ec9f392c37f8ef31088c8cbf0a72386039834 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Sun, 25 Feb 2024 20:26:08 +0100 Subject: [PATCH] Added bookmark options and delete bookmark --- lib/i18n/strings.g.dart | 68 +++- lib/i18n/strings_en.i18n.json | 13 + lib/i18n/strings_es.i18n.json | 13 + .../provider/bookmarks.provider.dart | 21 ++ lib/screens/bookmarks/ui/bookmark_item.dart | 317 +++++++++++------- .../bookmarks/ui/delete_bookmark_modal.dart | 69 ++++ lib/services/api_client.dart | 10 + pubspec.lock | 8 + pubspec.yaml | 1 + 9 files changed, 389 insertions(+), 131 deletions(-) create mode 100644 lib/screens/bookmarks/ui/delete_bookmark_modal.dart diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 77d04ee..bad4af7 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: 234 (117 per locale) +/// Strings: 256 (128 per locale) /// -/// Built on 2024-02-25 at 16:44 UTC +/// Built on 2024-02-25 at 19:19 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -221,6 +221,7 @@ class _StringsBookmarksEn { late final _StringsBookmarksDatesEn dates = _StringsBookmarksDatesEn._(_root); late final _StringsBookmarksAddBookmarkEn addBookmark = _StringsBookmarksAddBookmarkEn._(_root); late final _StringsBookmarksSearchEn search = _StringsBookmarksSearchEn._(_root); + late final _StringsBookmarksBookmarkOptionsEn bookmarkOptions = _StringsBookmarksBookmarkOptionsEn._(_root); } // Path: tags @@ -348,6 +349,26 @@ class _StringsBookmarksSearchEn { String get inputtedSearchTermNoResults => 'The inputted search term doesn\'t have any results'; } +// Path: bookmarks.bookmarkOptions +class _StringsBookmarksBookmarkOptionsEn { + _StringsBookmarksBookmarkOptionsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + String get edit => 'Edit'; + String get delete => 'Delete'; + String get unread => 'Unread'; + String get read => 'Read'; + String get share => 'Share'; + String get archive => 'Archive'; + String get deletingBookmark => 'Deleting bookmark...'; + String get bookmarkDeleted => 'Bookmark deleted successfully'; + String get bookmarkNotDeleted => 'The bookmark couldn\'t be deleted due to an error.'; + String get deleteBookmark => 'Delete bookmark'; + String get followingBookmarkDeleted => 'The following bookmark will be deleted:'; +} + // Path: tags.createTag class _StringsTagsCreateTagEn { _StringsTagsCreateTagEn._(this._root); @@ -514,6 +535,7 @@ class _StringsBookmarksEs implements _StringsBookmarksEn { @override late final _StringsBookmarksDatesEs dates = _StringsBookmarksDatesEs._(_root); @override late final _StringsBookmarksAddBookmarkEs addBookmark = _StringsBookmarksAddBookmarkEs._(_root); @override late final _StringsBookmarksSearchEs search = _StringsBookmarksSearchEs._(_root); + @override late final _StringsBookmarksBookmarkOptionsEs bookmarkOptions = _StringsBookmarksBookmarkOptionsEs._(_root); } // Path: tags @@ -641,6 +663,26 @@ class _StringsBookmarksSearchEs implements _StringsBookmarksSearchEn { @override String get inputtedSearchTermNoResults => 'El término de búsqueda introducido no tiene resultados'; } +// Path: bookmarks.bookmarkOptions +class _StringsBookmarksBookmarkOptionsEs implements _StringsBookmarksBookmarkOptionsEn { + _StringsBookmarksBookmarkOptionsEs._(this._root); + + @override final _StringsEs _root; // ignore: unused_field + + // Translations + @override String get edit => 'Editar'; + @override String get delete => 'Eliminar'; + @override String get unread => 'No leído'; + @override String get read => 'Leído'; + @override String get share => 'Compartir'; + @override String get archive => 'Archivar'; + @override String get deletingBookmark => 'Eliminando marcador...'; + @override String get bookmarkDeleted => 'Marcador eliminado correctamente'; + @override String get bookmarkNotDeleted => 'El marcador no se ha podido eliminar debido a un error.'; + @override String get deleteBookmark => 'Eliminar marcador'; + @override String get followingBookmarkDeleted => 'El siguiente marcador será eliminado:'; +} + // Path: tags.createTag class _StringsTagsCreateTagEs implements _StringsTagsCreateTagEn { _StringsTagsCreateTagEs._(this._root); @@ -783,6 +825,17 @@ extension on Translations { case 'bookmarks.search.inputSearchTerm': return 'Input a search term'; case 'bookmarks.search.cannotSearchError': return 'Cannot search content due to an error'; case 'bookmarks.search.inputtedSearchTermNoResults': return 'The inputted search term doesn\'t have any results'; + case 'bookmarks.bookmarkOptions.edit': return 'Edit'; + case 'bookmarks.bookmarkOptions.delete': return 'Delete'; + case 'bookmarks.bookmarkOptions.unread': return 'Unread'; + case 'bookmarks.bookmarkOptions.read': return 'Read'; + case 'bookmarks.bookmarkOptions.share': return 'Share'; + case 'bookmarks.bookmarkOptions.archive': return 'Archive'; + case 'bookmarks.bookmarkOptions.deletingBookmark': return 'Deleting bookmark...'; + case 'bookmarks.bookmarkOptions.bookmarkDeleted': return 'Bookmark deleted successfully'; + case 'bookmarks.bookmarkOptions.bookmarkNotDeleted': return 'The bookmark couldn\'t be deleted due to an error.'; + case 'bookmarks.bookmarkOptions.deleteBookmark': return 'Delete bookmark'; + case 'bookmarks.bookmarkOptions.followingBookmarkDeleted': return 'The following bookmark will be deleted:'; case 'tags.tags': return 'Tags'; case 'tags.created': return ({required Object created}) => 'Created: ${created}'; case 'tags.createTag.createTag': return 'Create tag'; @@ -908,6 +961,17 @@ extension on _StringsEs { case 'bookmarks.search.inputSearchTerm': return 'Introduce un término de búsqueda'; case 'bookmarks.search.cannotSearchError': return 'No se puede buscar el contenido por un error'; case 'bookmarks.search.inputtedSearchTermNoResults': return 'El término de búsqueda introducido no tiene resultados'; + case 'bookmarks.bookmarkOptions.edit': return 'Editar'; + case 'bookmarks.bookmarkOptions.delete': return 'Eliminar'; + case 'bookmarks.bookmarkOptions.unread': return 'No leído'; + case 'bookmarks.bookmarkOptions.read': return 'Leído'; + case 'bookmarks.bookmarkOptions.share': return 'Compartir'; + case 'bookmarks.bookmarkOptions.archive': return 'Archivar'; + case 'bookmarks.bookmarkOptions.deletingBookmark': return 'Eliminando marcador...'; + case 'bookmarks.bookmarkOptions.bookmarkDeleted': return 'Marcador eliminado correctamente'; + case 'bookmarks.bookmarkOptions.bookmarkNotDeleted': return 'El marcador no se ha podido eliminar debido a un error.'; + case 'bookmarks.bookmarkOptions.deleteBookmark': return 'Eliminar marcador'; + case 'bookmarks.bookmarkOptions.followingBookmarkDeleted': return 'El siguiente marcador será eliminado:'; case 'tags.tags': return 'Etiquetas'; case 'tags.created': return ({required Object created}) => 'Creado: ${created}'; case 'tags.createTag.createTag': return 'Crear etiqueta'; diff --git a/lib/i18n/strings_en.i18n.json b/lib/i18n/strings_en.i18n.json index a95924f..f8512f7 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -76,6 +76,19 @@ "inputSearchTerm": "Input a search term", "cannotSearchError": "Cannot search content due to an error", "inputtedSearchTermNoResults": "The inputted search term doesn't have any results" + }, + "bookmarkOptions": { + "edit": "Edit", + "delete": "Delete", + "unread": "Unread", + "read": "Read", + "share": "Share", + "archive": "Archive", + "deletingBookmark": "Deleting bookmark...", + "bookmarkDeleted": "Bookmark deleted successfully", + "bookmarkNotDeleted": "The bookmark couldn't be deleted due to an error.", + "deleteBookmark": "Delete bookmark", + "followingBookmarkDeleted": "The following bookmark will be deleted:" } }, "tags": { diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index ea861ec..f9d7b2d 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -76,6 +76,19 @@ "inputSearchTerm": "Introduce un término de búsqueda", "cannotSearchError": "No se puede buscar el contenido por un error", "inputtedSearchTermNoResults": "El término de búsqueda introducido no tiene resultados" + }, + "bookmarkOptions": { + "edit": "Editar", + "delete": "Eliminar", + "unread": "No leído", + "read": "Leído", + "share": "Compartir", + "archive": "Archivar", + "deletingBookmark": "Eliminando marcador...", + "bookmarkDeleted": "Marcador eliminado correctamente", + "bookmarkNotDeleted": "El marcador no se ha podido eliminar debido a un error.", + "deleteBookmark": "Eliminar marcador", + "followingBookmarkDeleted": "El siguiente marcador será eliminado:" } }, "tags": { diff --git a/lib/screens/bookmarks/provider/bookmarks.provider.dart b/lib/screens/bookmarks/provider/bookmarks.provider.dart index ef1b84a..62f1774 100644 --- a/lib/screens/bookmarks/provider/bookmarks.provider.dart +++ b/lib/screens/bookmarks/provider/bookmarks.provider.dart @@ -1,8 +1,13 @@ +import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:linkdy/screens/bookmarks/provider/favicon_loader.provider.dart'; import 'package:linkdy/screens/bookmarks/model/bookmarks.model.dart'; +import 'package:linkdy/utils/process_modal.dart'; +import 'package:linkdy/utils/snackbar.dart'; +import 'package:linkdy/i18n/strings.g.dart'; +import 'package:linkdy/models/data/bookmarks.dart'; import 'package:linkdy/providers/api_client.provider.dart'; import 'package:linkdy/constants/enums.dart'; @@ -80,4 +85,20 @@ class Bookmarks extends _$Bookmarks { Future refresh() async { await ref.read(bookmarksRequestProvider(state.limit).future); } + + void deleteBookmark(Bookmark bookmark) async { + final processModal = ProcessModal(); + processModal.open(t.bookmarks.bookmarkOptions.deletingBookmark); + + final result = await ref.read(apiClientProvider)!.postDeleteBookmark(bookmark.id!); + + processModal.close(); + if (result.successful == true) { + state.bookmarks = state.bookmarks.where((b) => b.id != bookmark.id).toList(); + ref.notifyListeners(); + showSnacbkar(label: t.bookmarks.bookmarkOptions.bookmarkDeleted, color: Colors.green); + } else { + showSnacbkar(label: t.bookmarks.bookmarkOptions.bookmarkNotDeleted, color: Colors.red); + } + } } diff --git a/lib/screens/bookmarks/ui/bookmark_item.dart b/lib/screens/bookmarks/ui/bookmark_item.dart index 2962d25..10023b4 100644 --- a/lib/screens/bookmarks/ui/bookmark_item.dart +++ b/lib/screens/bookmarks/ui/bookmark_item.dart @@ -1,10 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:linkdy/screens/bookmarks/ui/delete_bookmark_modal.dart'; import 'package:linkdy/screens/bookmarks/provider/favicon_loader.provider.dart'; +import 'package:linkdy/i18n/strings.g.dart'; import 'package:linkdy/models/data/bookmarks.dart'; import 'package:linkdy/providers/app_status.provider.dart'; import 'package:linkdy/providers/router.provider.dart'; @@ -32,144 +35,200 @@ class BookmarkItem extends ConsumerWidget { } } - return InkWell( - onTap: ref.watch(appStatusProvider).useInAppBrowser == true - ? () => ref.watch(routerProvider).push(RoutesPaths.webview, extra: bookmark) - : () => openUrl(bookmark.url!), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (ref.watch(appStatusProvider).showFavicon == true) - Padding( - padding: const EdgeInsets.only(right: 8), - child: ref.watch(faviconStoreProvider).loadingFavicons == true - ? ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Skeletonizer( - enabled: true, - ignoreContainers: true, - child: Container( - width: 16, - height: 16, - color: Colors.black, - ), - ), - ) - : Builder( - builder: (context) { - final faviconItem = ref - .watch(faviconStoreProvider) - .favicons - .where((f) => f.url == bookmark.url!) - .toList(); - if (faviconItem.isEmpty) return const SizedBox(); - if (faviconItem[0].isSvg == true) { - return SvgPicture.network( - faviconItem[0].favicon, + void openDeleteBookmarkModal(BuildContext ctx) { + showDialog( + context: context, + builder: (context) => DeleteBookmarkModal(bookmark: bookmark), + ); + } + + return Slidable( + startActionPane: ActionPane( + motion: const DrawerMotion(), + extentRatio: 0.75, + children: [ + SlidableAction( + onPressed: (ctx) => {}, + backgroundColor: Colors.blue, + label: t.bookmarks.bookmarkOptions.unread, + icon: Icons.mark_as_unread_rounded, + padding: const EdgeInsets.all(4), + ), + SlidableAction( + onPressed: (ctx) => {}, + backgroundColor: Colors.grey, + label: t.bookmarks.bookmarkOptions.archive, + icon: Icons.archive_rounded, + padding: const EdgeInsets.all(4), + ), + SlidableAction( + onPressed: (ctx) => {}, + backgroundColor: Colors.orange, + label: t.bookmarks.bookmarkOptions.share, + icon: Icons.share_rounded, + padding: const EdgeInsets.all(4), + ), + ], + ), + endActionPane: ActionPane( + motion: const DrawerMotion(), + extentRatio: 0.5, + children: [ + SlidableAction( + onPressed: (ctx) => {}, + backgroundColor: Colors.green, + label: t.bookmarks.bookmarkOptions.edit, + icon: Icons.edit_rounded, + padding: const EdgeInsets.all(4), + ), + SlidableAction( + onPressed: openDeleteBookmarkModal, + backgroundColor: Colors.red, + label: t.bookmarks.bookmarkOptions.delete, + icon: Icons.delete_rounded, + padding: const EdgeInsets.all(4), + ), + ], + ), + child: InkWell( + onTap: ref.watch(appStatusProvider).useInAppBrowser == true + ? () => ref.watch(routerProvider).push(RoutesPaths.webview, extra: bookmark) + : () => openUrl(bookmark.url!), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (ref.watch(appStatusProvider).showFavicon == true) + Padding( + padding: const EdgeInsets.only(right: 8), + child: ref.watch(faviconStoreProvider).loadingFavicons == true + ? ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Skeletonizer( + enabled: true, + ignoreContainers: true, + child: Container( width: 16, height: 16, - placeholderBuilder: (context) => ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Skeletonizer( - enabled: true, - ignoreContainers: true, - child: Container( - width: 16, - height: 16, - color: Colors.black, + color: Colors.black, + ), + ), + ) + : Builder( + builder: (context) { + final faviconItem = ref + .watch(faviconStoreProvider) + .favicons + .where((f) => f.url == bookmark.url!) + .toList(); + if (faviconItem.isEmpty) return const SizedBox(); + if (faviconItem[0].isSvg == true) { + return SvgPicture.network( + faviconItem[0].favicon, + width: 16, + height: 16, + placeholderBuilder: (context) => ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Skeletonizer( + enabled: true, + ignoreContainers: true, + child: Container( + width: 16, + height: 16, + color: Colors.black, + ), ), ), - ), - ); - } else { - return Image.network( - faviconItem[0].favicon, - width: 16, - height: 16, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress != null) { - return ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Skeletonizer( - enabled: true, - ignoreContainers: true, - child: Container( - width: 16, - height: 16, - color: Colors.black, + ); + } else { + return Image.network( + faviconItem[0].favicon, + width: 16, + height: 16, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress != null) { + return ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Skeletonizer( + enabled: true, + ignoreContainers: true, + child: Container( + width: 16, + height: 16, + color: Colors.black, + ), ), - ), - ); - } - return child; - }, - ); - } - }, - ), - ), - Expanded( - child: Text( - validateStrings(bookmark.title, bookmark.websiteTitle), - maxLines: 1, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, + ); + } + return child; + }, + ); + } + }, + ), ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - validateStrings(bookmark.description, bookmark.websiteDescription), - maxLines: 3, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.left, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - Expanded( - child: Text( - bookmark.tagNames?.map((tag) => "#$tag").join(" ") ?? '', - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - ), - ), - ), - if (bookmark.dateModified != null) ...[ - if (bookmark.tagNames?.isNotEmpty == true) - Container( - width: 1, - height: 12, - margin: const EdgeInsets.symmetric(horizontal: 8), - color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), - ), - Text( - dateToString(bookmark.dateModified!), - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onSurfaceVariant, + Expanded( + child: Text( + validateStrings(bookmark.title, bookmark.websiteTitle), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), ], - ], - ), - ], + ), + const SizedBox(height: 4), + Text( + validateStrings(bookmark.description, bookmark.websiteDescription), + maxLines: 3, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: Text( + bookmark.tagNames?.map((tag) => "#$tag").join(" ") ?? '', + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + if (bookmark.dateModified != null) ...[ + if (bookmark.tagNames?.isNotEmpty == true) + Container( + width: 1, + height: 12, + margin: const EdgeInsets.symmetric(horizontal: 8), + color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), + ), + Text( + dateToString(bookmark.dateModified!), + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ], + ), ), ), ); diff --git a/lib/screens/bookmarks/ui/delete_bookmark_modal.dart b/lib/screens/bookmarks/ui/delete_bookmark_modal.dart new file mode 100644 index 0000000..33d699e --- /dev/null +++ b/lib/screens/bookmarks/ui/delete_bookmark_modal.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:linkdy/screens/bookmarks/provider/bookmarks.provider.dart'; + +import 'package:linkdy/i18n/strings.g.dart'; +import 'package:linkdy/models/data/bookmarks.dart'; + +class DeleteBookmarkModal extends ConsumerWidget { + final Bookmark bookmark; + + const DeleteBookmarkModal({ + Key? key, + required this.bookmark, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AlertDialog( + title: Column( + children: [ + Icon( + Icons.delete_rounded, + size: 24, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(height: 16), + Text(t.bookmarks.bookmarkOptions.deleteBookmark), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(t.bookmarks.bookmarkOptions.followingBookmarkDeleted), + const SizedBox(height: 8), + Center( + child: Text( + bookmark.url!, + textAlign: TextAlign.center, + style: TextStyle( + fontStyle: FontStyle.italic, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(t.generic.cancel), + ), + const SizedBox(width: 8), + TextButton( + onPressed: () { + Navigator.pop(context); + ref.read(bookmarksProvider.notifier).deleteBookmark(bookmark); + }, + child: Text(t.generic.confirm), + ), + ], + ), + ], + ); + } +} diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart index b16f715..c73dc8d 100644 --- a/lib/services/api_client.dart +++ b/lib/services/api_client.dart @@ -134,4 +134,14 @@ class ApiClientService { return const ApiResponse(successful: false); } } + + Future> postDeleteBookmark(int bookmarkId) async { + try { + await dioInstance.delete("/bookmarks/$bookmarkId/"); + return const ApiResponse(successful: true); + } catch (e, stackTrace) { + Sentry.captureException(e, stackTrace: stackTrace); + return const ApiResponse(successful: false); + } + } } diff --git a/pubspec.lock b/pubspec.lock index e9150d2..65b5840 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -483,6 +483,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.10" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c" + url: "https://pub.dev" + source: hosted + version: "3.0.1" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index bfadd24..c77fb32 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: favicon: ^1.1.2 skeletonizer: ^1.0.1 http: ^1.2.0 + flutter_slidable: ^3.0.1 dev_dependencies: build_runner: ^2.4.8