diff --git a/lib/constants/shared_preferences_keys.dart b/lib/constants/shared_preferences_keys.dart index 323f7d4..22ba365 100644 --- a/lib/constants/shared_preferences_keys.dart +++ b/lib/constants/shared_preferences_keys.dart @@ -8,4 +8,5 @@ class SharedPreferencesKeys { static const openLinksBrowser = "openLinksBrowser"; static const defaultTags = "defaultTags"; static const markAsUnreadDefault = "markAsUnreadDefault"; + static const enableSlideOptions = "enableSlideOptions"; } diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 3aa09ec..db79b71 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 3 -/// Strings: 630 (210 per locale) +/// Strings: 636 (212 per locale) /// -/// Built on 2026-03-18 at 19:43 UTC +/// Built on 2026-03-22 at 19:27 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index d2b3549..8a8ebf9 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -814,6 +814,12 @@ class TranslationsSettingsGeneralSettingsEn { /// en: 'Set the mark as unread option by default when creating a new bookmark.' String get setMarkAsUnreadDefaultDescription => 'Set the mark as unread option by default when creating a new bookmark.'; + + /// en: 'Enable slide options' + String get enableSlideOptions => 'Enable slide options'; + + /// en: 'Enable slide options on the bookmark list' + String get enableSlideOptionsDescription => 'Enable slide options on the bookmark list'; } // Path: settings.generalSettings.disconnectModal @@ -1057,6 +1063,8 @@ extension on Translations { 'settings.generalSettings.others' => 'Others', 'settings.generalSettings.setMarkAsUnreadDefault' => 'Set mark as unread by default', 'settings.generalSettings.setMarkAsUnreadDefaultDescription' => 'Set the mark as unread option by default when creating a new bookmark.', + 'settings.generalSettings.enableSlideOptions' => 'Enable slide options', + 'settings.generalSettings.enableSlideOptionsDescription' => 'Enable slide options on the bookmark list', 'webview.goBack' => 'Go back', 'webview.goForward' => 'Go forward', 'webview.reload' => 'Reload', diff --git a/lib/i18n/strings_en.i18n.json b/lib/i18n/strings_en.i18n.json index c8b216f..ffb1dd6 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -219,7 +219,9 @@ }, "others": "Others", "setMarkAsUnreadDefault": "Set mark as unread by default", - "setMarkAsUnreadDefaultDescription": "Set the mark as unread option by default when creating a new bookmark." + "setMarkAsUnreadDefaultDescription": "Set the mark as unread option by default when creating a new bookmark.", + "enableSlideOptions": "Enable slide options", + "enableSlideOptionsDescription": "Enable slide options on the bookmark list" } }, "webview": { diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 1423491..aadd9bb 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -401,6 +401,8 @@ class _TranslationsSettingsGeneralSettingsEs implements TranslationsSettingsGene @override String get others => 'Otros'; @override String get setMarkAsUnreadDefault => 'Marcar como no leído por defecto'; @override String get setMarkAsUnreadDefaultDescription => 'Habilitar por defecto la opción de marcar como leído al crear un marcador.'; + @override String get enableSlideOptions => 'Habilitar opciones al deslizar'; + @override String get enableSlideOptionsDescription => 'Habilitar opciones al deslizar en la lista de marcadores'; } // Path: settings.generalSettings.disconnectModal @@ -628,6 +630,8 @@ extension on TranslationsEs { 'settings.generalSettings.others' => 'Otros', 'settings.generalSettings.setMarkAsUnreadDefault' => 'Marcar como no leído por defecto', 'settings.generalSettings.setMarkAsUnreadDefaultDescription' => 'Habilitar por defecto la opción de marcar como leído al crear un marcador.', + 'settings.generalSettings.enableSlideOptions' => 'Habilitar opciones al deslizar', + 'settings.generalSettings.enableSlideOptionsDescription' => 'Habilitar opciones al deslizar en la lista de marcadores', 'webview.goBack' => 'Ir atrás', 'webview.goForward' => 'Ir adelante', 'webview.reload' => 'Recargar', diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index 400a3be..f8e5167 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -219,7 +219,9 @@ }, "others": "Otros", "setMarkAsUnreadDefault": "Marcar como no leído por defecto", - "setMarkAsUnreadDefaultDescription": "Habilitar por defecto la opción de marcar como leído al crear un marcador." + "setMarkAsUnreadDefaultDescription": "Habilitar por defecto la opción de marcar como leído al crear un marcador.", + "enableSlideOptions": "Habilitar opciones al deslizar", + "enableSlideOptionsDescription": "Habilitar opciones al deslizar en la lista de marcadores" } }, "webview": { diff --git a/lib/i18n/strings_tr.g.dart b/lib/i18n/strings_tr.g.dart index 21ba132..7d53af9 100644 --- a/lib/i18n/strings_tr.g.dart +++ b/lib/i18n/strings_tr.g.dart @@ -401,6 +401,8 @@ class _TranslationsSettingsGeneralSettingsTr implements TranslationsSettingsGene @override String get others => 'Others'; @override String get setMarkAsUnreadDefault => 'Set mark as unread by default'; @override String get setMarkAsUnreadDefaultDescription => 'Set the mark as unread option by default when creating a new bookmark.'; + @override String get enableSlideOptions => 'Enable slide options'; + @override String get enableSlideOptionsDescription => 'Enable slide options on the bookmark list'; } // Path: settings.generalSettings.disconnectModal @@ -628,6 +630,8 @@ extension on TranslationsTr { 'settings.generalSettings.others' => 'Others', 'settings.generalSettings.setMarkAsUnreadDefault' => 'Set mark as unread by default', 'settings.generalSettings.setMarkAsUnreadDefaultDescription' => 'Set the mark as unread option by default when creating a new bookmark.', + 'settings.generalSettings.enableSlideOptions' => 'Enable slide options', + 'settings.generalSettings.enableSlideOptionsDescription' => 'Enable slide options on the bookmark list', 'webview.goBack' => 'Geri dön', 'webview.goForward' => 'İleri git', 'webview.reload' => 'Yenile', diff --git a/lib/i18n/strings_tr.i18n.json b/lib/i18n/strings_tr.i18n.json index bc82578..c475fa4 100644 --- a/lib/i18n/strings_tr.i18n.json +++ b/lib/i18n/strings_tr.i18n.json @@ -219,7 +219,9 @@ }, "others": "Others", "setMarkAsUnreadDefault": "Set mark as unread by default", - "setMarkAsUnreadDefaultDescription": "Set the mark as unread option by default when creating a new bookmark." + "setMarkAsUnreadDefaultDescription": "Set the mark as unread option by default when creating a new bookmark.", + "enableSlideOptions": "Enable slide options", + "enableSlideOptionsDescription": "Enable slide options on the bookmark list" } }, "webview": { diff --git a/lib/models/providers_models/app_status.model.dart b/lib/models/providers_models/app_status.model.dart index d80af08..b01ada8 100644 --- a/lib/models/providers_models/app_status.model.dart +++ b/lib/models/providers_models/app_status.model.dart @@ -9,6 +9,7 @@ class AppStatusModel { bool showFavicon; List defaultTags; bool markAsUnreadDefault; + bool enableSlideOptions; AppStatusModel({ this.selectedTheme = SelectedTheme.system, @@ -19,5 +20,6 @@ class AppStatusModel { this.showFavicon = true, required this.defaultTags, this.markAsUnreadDefault = false, + this.enableSlideOptions = true, }); } diff --git a/lib/providers/app_status.provider.dart b/lib/providers/app_status.provider.dart index ca18945..dc34b22 100644 --- a/lib/providers/app_status.provider.dart +++ b/lib/providers/app_status.provider.dart @@ -24,6 +24,7 @@ class AppStatus extends _$AppStatus { showFavicon: sharedPreferences.getBool(SharedPreferencesKeys.showFavicon) ?? true, defaultTags: sharedPreferences.getStringList(SharedPreferencesKeys.defaultTags) ?? [], markAsUnreadDefault: sharedPreferences.getBool(SharedPreferencesKeys.markAsUnreadDefault) ?? false, + enableSlideOptions: sharedPreferences.getBool(SharedPreferencesKeys.enableSlideOptions) ?? true, ); } @@ -81,6 +82,12 @@ class AppStatus extends _$AppStatus { ref.read(sharedPreferencesProvider).setBool(SharedPreferencesKeys.markAsUnreadDefault, value); ref.notifyListeners(); } + + void setEnableSlideOptions(bool value) { + state.enableSlideOptions = value; + ref.read(sharedPreferencesProvider).setBool(SharedPreferencesKeys.enableSlideOptions, value); + ref.notifyListeners(); + } } @riverpod diff --git a/lib/screens/bookmarks/ui/bookmarks.dart b/lib/screens/bookmarks/ui/bookmarks.dart index 20eaf1f..09c8b49 100644 --- a/lib/screens/bookmarks/ui/bookmarks.dart +++ b/lib/screens/bookmarks/ui/bookmarks.dart @@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:linkdy/screens/bookmarks/ui/visualization_modal.dart'; import 'package:linkdy/screens/bookmarks/provider/bookmarks.provider.dart'; -import 'package:linkdy/widgets/bookmark_item.dart'; +import 'package:linkdy/widgets/bookmark_item/bookmark_item.dart'; import 'package:linkdy/screens/bookmarks/ui/bookmark_form_modal.dart'; import 'package:linkdy/screens/bookmarks/ui/delete_bookmark_modal.dart'; import 'package:linkdy/screens/bookmarks/ui/search_bookmarks.dart'; diff --git a/lib/screens/bookmarks/ui/search_bookmarks.dart b/lib/screens/bookmarks/ui/search_bookmarks.dart index 4d7ff4e..e493cd0 100644 --- a/lib/screens/bookmarks/ui/search_bookmarks.dart +++ b/lib/screens/bookmarks/ui/search_bookmarks.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:linkdy/screens/bookmarks/provider/search_bookmarks.provider.dart'; import 'package:linkdy/screens/bookmarks/ui/bookmark_form_modal.dart'; -import 'package:linkdy/widgets/bookmark_item.dart'; +import 'package:linkdy/widgets/bookmark_item/bookmark_item.dart'; import 'package:linkdy/screens/bookmarks/ui/delete_bookmark_modal.dart'; import 'package:linkdy/widgets/enter_search_term_screen.dart'; import 'package:linkdy/widgets/no_data_screen.dart'; diff --git a/lib/screens/filtered_bookmarks/ui/filtered_bookmarks.dart b/lib/screens/filtered_bookmarks/ui/filtered_bookmarks.dart index 0f75feb..d26112c 100644 --- a/lib/screens/filtered_bookmarks/ui/filtered_bookmarks.dart +++ b/lib/screens/filtered_bookmarks/ui/filtered_bookmarks.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:linkdy/screens/bookmarks/ui/bookmark_form_modal.dart'; -import 'package:linkdy/widgets/bookmark_item.dart'; +import 'package:linkdy/widgets/bookmark_item/bookmark_item.dart'; import 'package:linkdy/screens/bookmarks/ui/delete_bookmark_modal.dart'; import 'package:linkdy/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.dart'; import 'package:linkdy/widgets/error_screen.dart'; diff --git a/lib/screens/settings/ui/general_settings/general_settings.dart b/lib/screens/settings/ui/general_settings/general_settings.dart index 861dddf..b71d3a8 100644 --- a/lib/screens/settings/ui/general_settings/general_settings.dart +++ b/lib/screens/settings/ui/general_settings/general_settings.dart @@ -71,6 +71,12 @@ class GeneralSettings extends ConsumerWidget { subtitle: Text(browserLabel()), onTap: openBrowserSelectionSheet, ), + SwitchListTile( + title: Text(t.settings.generalSettings.enableSlideOptions), + subtitle: Text(t.settings.generalSettings.enableSlideOptionsDescription), + value: ref.watch(appStatusProvider).enableSlideOptions, + onChanged: ref.read(appStatusProvider.notifier).setEnableSlideOptions, + ), SectionLabel(label: t.settings.generalSettings.tags), ListTile( title: Text(t.settings.generalSettings.defaultTags.defaultTags), diff --git a/lib/widgets/bookmark_item.dart b/lib/widgets/bookmark_item.dart deleted file mode 100644 index e2d2cdd..0000000 --- a/lib/widgets/bookmark_item.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:linkdy/constants/urls.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:skeletonizer/skeletonizer.dart'; -import 'package:super_context_menu/super_context_menu.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/utils/date_to_string.dart'; - -// Wait until the context menu close animation animation finishes -Duration _durationTime = const Duration(milliseconds: 150); - -class BookmarkItem extends ConsumerWidget { - final Bookmark bookmark; - final void Function(Bookmark bookmark) onSelect; - final void Function(Bookmark bookmark) onDelete; - final void Function(Bookmark bookmark) onReadUnread; - final void Function(Bookmark bookmark) onArchiveUnarchive; - final void Function(Bookmark bookmark) onEdit; - final void Function(Bookmark bookmark) onShareInternally; - final bool selected; - final bool tabletMode; - final Color? tileBackgroundColor; - - const BookmarkItem({ - super.key, - required this.bookmark, - required this.onSelect, - required this.onDelete, - required this.onReadUnread, - required this.onArchiveUnarchive, - required this.onEdit, - required this.onShareInternally, - required this.selected, - required this.tabletMode, - this.tileBackgroundColor, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - String validateStrings(String? string1, String? string2, String? string3) { - if (string1 != null && string1.isNotEmpty) { - return string1; - } else if (string2 != null && string2.isNotEmpty) { - return string2; - } else if (string3 != null && string3.isNotEmpty) { - return string3; - } else { - return ""; - } - } - - return Padding( - padding: tabletMode ? const EdgeInsets.symmetric(horizontal: 8) : const EdgeInsets.all(0), - child: ClipRRect( - borderRadius: tabletMode ? BorderRadius.circular(28) : BorderRadius.circular(0), - child: Builder( - builder: (ctx) { - return ContextMenuWidget( - menuProvider: (request) => Menu( - children: [ - MenuAction( - callback: () => Future.delayed(_durationTime).then((value) => onReadUnread(bookmark)), - title: bookmark.unread == true - ? t.bookmarks.bookmarkOptions.markAsRead - : t.bookmarks.bookmarkOptions.markAsUnread, - image: MenuImage.icon( - bookmark.unread == true ? Icons.mark_email_read_rounded : Icons.mark_as_unread_rounded, - ), - ), - MenuAction( - callback: () => Future.delayed(_durationTime).then((value) => onArchiveUnarchive(bookmark)), - title: bookmark.isArchived == true - ? t.bookmarks.bookmarkOptions.unarchive - : t.bookmarks.bookmarkOptions.archive, - image: MenuImage.icon( - bookmark.isArchived == true ? Icons.unarchive_rounded : Icons.archive_rounded, - ), - ), - Menu( - title: t.bookmarks.bookmarkOptions.shareOptions, - image: MenuImage.icon(Icons.share_rounded), - children: [ - MenuAction( - callback: () => Future.delayed(_durationTime).then((value) => onShareInternally(bookmark)), - title: bookmark.shared == true - ? t.bookmarks.bookmarkOptions.unshareInternally - : t.bookmarks.bookmarkOptions.shareInternally, - image: MenuImage.icon(Icons.input_rounded), - ), - MenuAction( - callback: () { - final box = ctx.findRenderObject() as RenderBox?; - Future.delayed(_durationTime).then( - (value) => Share.share( - bookmark.url!, - sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ), - ); - }, - title: t.bookmarks.bookmarkOptions.shareThirdPartyApp, - image: MenuImage.icon(Icons.output_rounded), - ), - ], - ), - MenuSeparator(), - MenuAction( - callback: () => Future.delayed(_durationTime).then((value) => onEdit(bookmark)), - title: t.bookmarks.bookmarkOptions.edit, - image: MenuImage.icon(Icons.edit_rounded), - ), - MenuAction( - callback: () => Future.delayed(_durationTime).then((value) => onDelete(bookmark)), - title: t.bookmarks.bookmarkOptions.delete, - image: MenuImage.icon(Icons.delete_rounded), - attributes: const MenuActionAttributes(destructive: true), - ), - ], - ), - child: Material( - color: selected && tabletMode - ? Theme.of(context).colorScheme.primaryContainer - : tileBackgroundColor ?? Theme.of(context).colorScheme.surface, - child: InkWell( - onTap: () => onSelect(bookmark), - 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: Image.network( - Urls.gstatic(bookmark.url), - 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, bookmark.url), - 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, bookmark.url), - 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: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - if (bookmark.shared == true) ...[ - 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), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).colorScheme.primaryContainer, - ), - child: Row( - children: [ - Icon( - Icons.share_rounded, - size: 12, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - const SizedBox(width: 4), - Text( - t.bookmarks.bookmarkOptions.shared, - style: TextStyle( - fontSize: 10, - color: Theme.of(context).colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), - ], - if (bookmark.unread == true) ...[ - if (bookmark.tagNames?.isNotEmpty == true || bookmark.shared == true) - Container( - width: 1, - height: 12, - margin: const EdgeInsets.symmetric(horizontal: 8), - color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).colorScheme.primaryContainer, - ), - child: Row( - children: [ - Icon( - Icons.mark_as_unread_rounded, - size: 12, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - const SizedBox(width: 4), - Text( - t.bookmarks.bookmarkOptions.unread, - style: TextStyle( - fontSize: 10, - color: Theme.of(context).colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), - ], - if (bookmark.dateModified != null) ...[ - if (bookmark.unread == true || - bookmark.shared == true || - 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/widgets/bookmark_item/bookmark_context_menu.dart b/lib/widgets/bookmark_item/bookmark_context_menu.dart new file mode 100644 index 0000000..42d1149 --- /dev/null +++ b/lib/widgets/bookmark_item/bookmark_context_menu.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:super_context_menu/super_context_menu.dart'; + +import 'package:linkdy/models/data/bookmarks.dart'; +import 'package:linkdy/i18n/strings.g.dart'; + +// Wait until the context menu close animation animation finishes +Duration _durationTime = const Duration(milliseconds: 150); + +class BookmarkContextMenu extends ConsumerWidget { + final Widget child; + final Bookmark bookmark; + final void Function(Bookmark bookmark) onSelect; + final void Function(Bookmark bookmark) onDelete; + final void Function(Bookmark bookmark) onReadUnread; + final void Function(Bookmark bookmark) onArchiveUnarchive; + final void Function(Bookmark bookmark) onEdit; + final void Function(Bookmark bookmark) onShareInternally; + + const BookmarkContextMenu({ + super.key, + required this.bookmark, + required this.child, + required this.onSelect, + required this.onDelete, + required this.onReadUnread, + required this.onArchiveUnarchive, + required this.onEdit, + required this.onShareInternally, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ContextMenuWidget( + menuProvider: (request) => Menu( + children: [ + MenuAction( + callback: () => Future.delayed(_durationTime).then((value) => onReadUnread(bookmark)), + title: bookmark.unread == true + ? t.bookmarks.bookmarkOptions.markAsRead + : t.bookmarks.bookmarkOptions.markAsUnread, + image: MenuImage.icon( + bookmark.unread == true ? Icons.mark_email_read_rounded : Icons.mark_as_unread_rounded, + ), + ), + MenuAction( + callback: () => Future.delayed(_durationTime).then((value) => onArchiveUnarchive(bookmark)), + title: bookmark.isArchived == true + ? t.bookmarks.bookmarkOptions.unarchive + : t.bookmarks.bookmarkOptions.archive, + image: MenuImage.icon( + bookmark.isArchived == true ? Icons.unarchive_rounded : Icons.archive_rounded, + ), + ), + Menu( + title: t.bookmarks.bookmarkOptions.shareOptions, + image: MenuImage.icon(Icons.share_rounded), + children: [ + MenuAction( + callback: () => Future.delayed(_durationTime).then((value) => onShareInternally(bookmark)), + title: bookmark.shared == true + ? t.bookmarks.bookmarkOptions.unshareInternally + : t.bookmarks.bookmarkOptions.shareInternally, + image: MenuImage.icon(Icons.input_rounded), + ), + MenuAction( + callback: () { + final box = context.findRenderObject() as RenderBox?; + Future.delayed(_durationTime).then( + (value) => Share.share( + bookmark.url!, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ), + ); + }, + title: t.bookmarks.bookmarkOptions.shareThirdPartyApp, + image: MenuImage.icon(Icons.output_rounded), + ), + ], + ), + MenuSeparator(), + MenuAction( + callback: () => Future.delayed(_durationTime).then((value) => onEdit(bookmark)), + title: t.bookmarks.bookmarkOptions.edit, + image: MenuImage.icon(Icons.edit_rounded), + ), + MenuAction( + callback: () => Future.delayed(_durationTime).then((value) => onDelete(bookmark)), + title: t.bookmarks.bookmarkOptions.delete, + image: MenuImage.icon(Icons.delete_rounded), + attributes: const MenuActionAttributes(destructive: true), + ), + ], + ), + child: child, + ); + } +} diff --git a/lib/widgets/bookmark_item/bookmark_item.dart b/lib/widgets/bookmark_item/bookmark_item.dart new file mode 100644 index 0000000..206df32 --- /dev/null +++ b/lib/widgets/bookmark_item/bookmark_item.dart @@ -0,0 +1,281 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:linkdy/constants/urls.dart'; +import 'package:linkdy/widgets/bookmark_item/bookmark_context_menu.dart'; +import 'package:linkdy/widgets/bookmark_item/bookmark_slide_menu.dart'; +import 'package:skeletonizer/skeletonizer.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/utils/date_to_string.dart'; + +class BookmarkItem extends ConsumerWidget { + final Bookmark bookmark; + final void Function(Bookmark bookmark) onSelect; + final void Function(Bookmark bookmark) onDelete; + final void Function(Bookmark bookmark) onReadUnread; + final void Function(Bookmark bookmark) onArchiveUnarchive; + final void Function(Bookmark bookmark) onEdit; + final void Function(Bookmark bookmark) onShareInternally; + final bool selected; + final bool tabletMode; + final Color? tileBackgroundColor; + + const BookmarkItem({ + super.key, + required this.bookmark, + required this.onSelect, + required this.onDelete, + required this.onReadUnread, + required this.onArchiveUnarchive, + required this.onEdit, + required this.onShareInternally, + required this.selected, + required this.tabletMode, + this.tileBackgroundColor, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: tabletMode ? const EdgeInsets.symmetric(horizontal: 8) : const EdgeInsets.all(0), + child: ClipRRect( + borderRadius: tabletMode ? BorderRadius.circular(28) : BorderRadius.circular(0), + child: BookmarkSlideMenu( + bookmark: bookmark, + onSelect: onSelect, + onDelete: onDelete, + onReadUnread: onReadUnread, + onArchiveUnarchive: onArchiveUnarchive, + onEdit: onEdit, + onShareInternally: onShareInternally, + child: BookmarkContextMenu( + bookmark: bookmark, + onSelect: onSelect, + onDelete: onDelete, + onReadUnread: onReadUnread, + onArchiveUnarchive: onArchiveUnarchive, + onEdit: onEdit, + onShareInternally: onShareInternally, + child: _BookmarkContent( + bookmark: bookmark, + onSelect: onSelect, + selected: selected, + tabletMode: tabletMode, + tileBackgroundColor: tileBackgroundColor, + ), + ), + ), + ), + ); + } +} + +class _BookmarkContent extends ConsumerWidget { + final Bookmark bookmark; + final void Function(Bookmark bookmark) onSelect; + final bool selected; + final bool tabletMode; + final Color? tileBackgroundColor; + + const _BookmarkContent({ + required this.bookmark, + required this.onSelect, + required this.selected, + required this.tabletMode, + this.tileBackgroundColor, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + String validateStrings(String? string1, String? string2, String? string3) { + if (string1 != null && string1.isNotEmpty) { + return string1; + } else if (string2 != null && string2.isNotEmpty) { + return string2; + } else if (string3 != null && string3.isNotEmpty) { + return string3; + } else { + return ""; + } + } + + return Material( + color: selected && tabletMode + ? Theme.of(context).colorScheme.primaryContainer + : tileBackgroundColor ?? Theme.of(context).colorScheme.surface, + child: InkWell( + onTap: () => onSelect(bookmark), + 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: Image.network( + Urls.gstatic(bookmark.url), + 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, bookmark.url), + 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, bookmark.url), + maxLines: 3, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.left, + style: TextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 3), + Row( + children: [ + Expanded( + child: Text( + bookmark.tagNames?.map((tag) => "#$tag").join(" ") ?? '', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + if (bookmark.shared == true) ...[ + 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), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.primaryContainer, + ), + child: Row( + children: [ + Icon( + Icons.share_rounded, + size: 12, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + const SizedBox(width: 4), + Text( + t.bookmarks.bookmarkOptions.shared, + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + if (bookmark.unread == true) ...[ + if (bookmark.tagNames?.isNotEmpty == true || bookmark.shared == true) + Container( + width: 1, + height: 12, + margin: const EdgeInsets.symmetric(horizontal: 8), + color: Theme.of(context).colorScheme.tertiary.withOpacity(0.3), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.primaryContainer, + ), + child: Row( + children: [ + Icon( + Icons.mark_as_unread_rounded, + size: 12, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + const SizedBox(width: 4), + Text( + t.bookmarks.bookmarkOptions.unread, + style: TextStyle( + fontSize: 10, + color: Theme.of(context).colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + if (bookmark.dateModified != null) ...[ + if (bookmark.unread == true || bookmark.shared == true || 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/widgets/bookmark_item/bookmark_slide_menu.dart b/lib/widgets/bookmark_item/bookmark_slide_menu.dart new file mode 100644 index 0000000..d0a791e --- /dev/null +++ b/lib/widgets/bookmark_item/bookmark_slide_menu.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:linkdy/providers/app_status.provider.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:slideable/slideable.dart'; + +import 'package:linkdy/models/data/bookmarks.dart'; +import 'package:linkdy/i18n/strings.g.dart'; + +class BookmarkSlideMenu extends ConsumerWidget { + final Widget child; + final Bookmark bookmark; + final void Function(Bookmark bookmark) onSelect; + final void Function(Bookmark bookmark) onDelete; + final void Function(Bookmark bookmark) onReadUnread; + final void Function(Bookmark bookmark) onArchiveUnarchive; + final void Function(Bookmark bookmark) onEdit; + final void Function(Bookmark bookmark) onShareInternally; + + const BookmarkSlideMenu({ + super.key, + required this.bookmark, + required this.child, + required this.onSelect, + required this.onDelete, + required this.onReadUnread, + required this.onArchiveUnarchive, + required this.onEdit, + required this.onShareInternally, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (ref.watch(appStatusProvider).enableSlideOptions == true) { + return Slideable( + resetSlide: false, + items: [ + ActionItems( + icon: _ActionItemContent( + icon: Icon( + bookmark.unread == true ? Icons.mark_email_read_rounded : Icons.mark_as_unread_rounded, + color: Colors.white, + ), + text: bookmark.unread == true + ? t.bookmarks.bookmarkOptions.markAsRead + : t.bookmarks.bookmarkOptions.markAsUnread, + ), + onPress: () => onReadUnread(bookmark), + backgroudColor: Colors.green, + ), + ActionItems( + icon: _ActionItemContent( + icon: Icon( + bookmark.isArchived == true ? Icons.unarchive_rounded : Icons.archive_rounded, + color: Colors.white, + ), + text: bookmark.isArchived == true + ? t.bookmarks.bookmarkOptions.unarchive + : t.bookmarks.bookmarkOptions.archive, + ), + onPress: () => onArchiveUnarchive(bookmark), + backgroudColor: Colors.blueGrey, + ), + ActionItems( + icon: _ActionItemContent( + icon: Icon( + bookmark.shared == true ? Icons.person_remove_rounded : Icons.person_add_rounded, + color: Colors.white, + ), + text: bookmark.shared == true + ? t.bookmarks.bookmarkOptions.unshareInternally + : t.bookmarks.bookmarkOptions.shareInternally, + ), + onPress: () => onShareInternally(bookmark), + backgroudColor: Colors.teal, + ), + ActionItems( + icon: _ActionItemContent( + icon: Icon( + Icons.output_rounded, + color: Colors.white, + ), + text: t.bookmarks.bookmarkOptions.shareThirdPartyApp, + ), + onPress: () { + final box = context.findRenderObject() as RenderBox?; + Share.share( + bookmark.url!, + sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, + ); + }, + backgroudColor: Colors.purple, + ), + ActionItems( + icon: _ActionItemContent( + icon: Icon( + Icons.edit_rounded, + color: Colors.white, + ), + text: t.bookmarks.bookmarkOptions.edit, + ), + onPress: () => onEdit(bookmark), + backgroudColor: Colors.blue, + ), + ActionItems( + icon: _ActionItemContent( + icon: Icon( + Icons.delete_rounded, + color: Colors.white, + ), + text: t.bookmarks.bookmarkOptions.delete, + ), + onPress: () => onDelete(bookmark), + backgroudColor: Colors.red, + ), + ], + child: child, + ); + } else { + return child; + } + } +} + +class _ActionItemContent extends StatelessWidget { + final Icon icon; + final String text; + + const _ActionItemContent({ + required this.icon, + required this.text, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + icon, + SizedBox(height: 6), + Text( + text, + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ea200b1..d8a4d02 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1099,6 +1099,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.14.0" + slideable: + dependency: "direct main" + description: + name: slideable + sha256: "6ab04552c1fba1767e91f09d6e3bc6acf69d87c0fadb8c19656f621ffd93fdf6" + url: "https://pub.dev" + source: hosted + version: "0.0.3" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 71d88bb..567ea1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: ref: master-alt super_context_menu: ^0.9.1 listen_sharing_intent: ^1.9.2 + slideable: ^0.0.3 dev_dependencies: build_runner: ^2.5.1