From d13e52148111e922409fefb2302682c4dcc3bac4 Mon Sep 17 00:00:00 2001 From: Juan Gilsanz Polo Date: Wed, 28 Feb 2024 20:18:54 +0100 Subject: [PATCH] Adapted bookmarks list to tablet mode --- lib/i18n/strings.g.dart | 10 +- lib/i18n/strings_en.i18n.json | 2 +- lib/i18n/strings_es.i18n.json | 2 +- .../bookmarks/model/bookmarks.model.dart | 2 + .../model/search_bookmarks.model.dart | 2 + .../provider/bookmarks.provider.dart | 17 +- .../provider/bookmarks.provider.g.dart | 2 +- .../provider/search_bookmarks.provider.dart | 15 + .../provider/search_bookmarks.provider.g.dart | 2 +- lib/screens/bookmarks/ui/bookmark_item.dart | 518 +++++++++--------- lib/screens/bookmarks/ui/bookmarks.dart | 378 +++++++------ .../bookmarks/ui/search_bookmarks.dart | 262 +++++---- .../model/filtered_bookmarks.model.dart | 2 + .../provider/filtered_bookmarks.provider.dart | 17 +- .../filtered_bookmarks.provider.g.dart | 2 +- .../ui/filtered_bookmarks.dart | 277 ++++++---- lib/screens/webview/ui/webview.dart | 34 +- pubspec.lock | 16 + pubspec.yaml | 2 + 19 files changed, 891 insertions(+), 671 deletions(-) diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index e1eb40c..6fbcb34 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -6,7 +6,7 @@ /// Locales: 2 /// Strings: 350 (175 per locale) /// -/// Built on 2024-02-28 at 16:44 UTC +/// Built on 2024-02-28 at 18:58 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -481,7 +481,7 @@ class _StringsSettingsGeneralSettingsEn { String get disconnectFromServer => 'Disconnect from server'; late final _StringsSettingsGeneralSettingsDisconnectModalEn disconnectModal = _StringsSettingsGeneralSettingsDisconnectModalEn._(_root); String get useInAppBrowser => 'Use in app browser'; - String get useInAppBrowserDescription => 'Opens the bookmarks with the integrated browser instead of with the system browser.'; + String get useInAppBrowserDescription => 'Opens the bookmarks with the integrated browser instead of with the system browser. On tablet mode the internal browser will always be used.'; } // Path: settings.generalSettings.disconnectModal @@ -852,7 +852,7 @@ class _StringsSettingsGeneralSettingsEs implements _StringsSettingsGeneralSettin @override String get disconnectFromServer => 'Desconectar del servidor'; @override late final _StringsSettingsGeneralSettingsDisconnectModalEs disconnectModal = _StringsSettingsGeneralSettingsDisconnectModalEs._(_root); @override String get useInAppBrowser => 'Usar navegador de la aplicación'; - @override String get useInAppBrowserDescription => 'Abre los marcadores con el navegador integrado en vez de con el navegador del sietema.'; + @override String get useInAppBrowserDescription => 'Abre los marcadores con el navegador integrado en vez de con el navegador del sietema. En modo tablet siempre se usará el navegador integrado.'; } // Path: settings.generalSettings.disconnectModal @@ -1028,7 +1028,7 @@ extension on Translations { case 'settings.generalSettings.disconnectModal.title': return 'Disconnect'; case 'settings.generalSettings.disconnectModal.description': return 'Are you sure you want to disconnect from the server?\nYou will be redirected to the initial setup.'; case 'settings.generalSettings.useInAppBrowser': return 'Use in app browser'; - case 'settings.generalSettings.useInAppBrowserDescription': return 'Opens the bookmarks with the integrated browser instead of with the system browser.'; + case 'settings.generalSettings.useInAppBrowserDescription': return 'Opens the bookmarks with the integrated browser instead of with the system browser. On tablet mode the internal browser will always be used.'; case 'webview.goBack': return 'Go back'; case 'webview.goForward': return 'Go forward'; case 'webview.reload': return 'Reload'; @@ -1211,7 +1211,7 @@ extension on _StringsEs { case 'settings.generalSettings.disconnectModal.title': return 'Desconectar'; case 'settings.generalSettings.disconnectModal.description': return '¿Estás seguro que deseas desconectarte del servidor?\nSerás redirigido a la configuración inicial.'; case 'settings.generalSettings.useInAppBrowser': return 'Usar navegador de la aplicación'; - case 'settings.generalSettings.useInAppBrowserDescription': return 'Abre los marcadores con el navegador integrado en vez de con el navegador del sietema.'; + case 'settings.generalSettings.useInAppBrowserDescription': return 'Abre los marcadores con el navegador integrado en vez de con el navegador del sietema. En modo tablet siempre se usará el navegador integrado.'; case 'webview.goBack': return 'Ir atrás'; case 'webview.goForward': return 'Ir adelante'; case 'webview.reload': return 'Recargar'; diff --git a/lib/i18n/strings_en.i18n.json b/lib/i18n/strings_en.i18n.json index 38f3c23..2b8a102 100644 --- a/lib/i18n/strings_en.i18n.json +++ b/lib/i18n/strings_en.i18n.json @@ -183,7 +183,7 @@ "description": "Are you sure you want to disconnect from the server?\nYou will be redirected to the initial setup." }, "useInAppBrowser": "Use in app browser", - "useInAppBrowserDescription": "Opens the bookmarks with the integrated browser instead of with the system browser." + "useInAppBrowserDescription": "Opens the bookmarks with the integrated browser instead of with the system browser. On tablet mode the internal browser will always be used." } }, "webview": { diff --git a/lib/i18n/strings_es.i18n.json b/lib/i18n/strings_es.i18n.json index 9999b53..9804a62 100644 --- a/lib/i18n/strings_es.i18n.json +++ b/lib/i18n/strings_es.i18n.json @@ -183,7 +183,7 @@ "description": "¿Estás seguro que deseas desconectarte del servidor?\nSerás redirigido a la configuración inicial." }, "useInAppBrowser": "Usar navegador de la aplicación", - "useInAppBrowserDescription": "Abre los marcadores con el navegador integrado en vez de con el navegador del sietema." + "useInAppBrowserDescription": "Abre los marcadores con el navegador integrado en vez de con el navegador del sietema. En modo tablet siempre se usará el navegador integrado." } }, "webview": { diff --git a/lib/screens/bookmarks/model/bookmarks.model.dart b/lib/screens/bookmarks/model/bookmarks.model.dart index bb322d9..761389c 100644 --- a/lib/screens/bookmarks/model/bookmarks.model.dart +++ b/lib/screens/bookmarks/model/bookmarks.model.dart @@ -10,6 +10,7 @@ class BookmarksModel { bool loadingMore; int maxNumber; ReadStatus readStatus; + Bookmark? selectedBookmark; BookmarksModel({ this.currentPage = 0, @@ -19,5 +20,6 @@ class BookmarksModel { this.loadingMore = false, this.maxNumber = 0, this.readStatus = ReadStatus.all, + this.selectedBookmark, }); } diff --git a/lib/screens/bookmarks/model/search_bookmarks.model.dart b/lib/screens/bookmarks/model/search_bookmarks.model.dart index ac8d015..490b0b0 100644 --- a/lib/screens/bookmarks/model/search_bookmarks.model.dart +++ b/lib/screens/bookmarks/model/search_bookmarks.model.dart @@ -13,6 +13,7 @@ class SearchBookmarksModel { LoadStatus inialLoadStatus; bool loadingMore; int maxNumber; + Bookmark? selectedBookmark; SearchBookmarksModel({ required this.searchController, @@ -23,5 +24,6 @@ class SearchBookmarksModel { this.inialLoadStatus = LoadStatus.loaded, this.loadingMore = false, this.maxNumber = 0, + this.selectedBookmark, }); } diff --git a/lib/screens/bookmarks/provider/bookmarks.provider.dart b/lib/screens/bookmarks/provider/bookmarks.provider.dart index fece45a..10bc853 100644 --- a/lib/screens/bookmarks/provider/bookmarks.provider.dart +++ b/lib/screens/bookmarks/provider/bookmarks.provider.dart @@ -1,10 +1,15 @@ -import 'package:linkdy/constants/global_keys.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:linkdy/screens/bookmarks/provider/common_functions.dart'; import 'package:linkdy/screens/bookmarks/provider/favicon_loader.provider.dart'; import 'package:linkdy/screens/bookmarks/model/bookmarks.model.dart'; +import 'package:linkdy/config/sizes.dart'; +import 'package:linkdy/constants/global_keys.dart'; +import 'package:linkdy/providers/app_status.provider.dart'; +import 'package:linkdy/providers/router.provider.dart'; +import 'package:linkdy/router/paths.dart'; +import 'package:linkdy/utils/open_url.dart'; import 'package:linkdy/models/data/bookmarks.dart'; import 'package:linkdy/providers/api_client.provider.dart'; import 'package:linkdy/constants/enums.dart'; @@ -93,6 +98,16 @@ class Bookmarks extends _$Bookmarks { ref.notifyListeners(); } + void selectBookmark(Bookmark bookmark, double width) { + if (width <= Sizes.tabletBreakpoint && ref.watch(appStatusProvider).useInAppBrowser == true) { + ref.watch(routerProvider).push(RoutesPaths.webview, extra: bookmark); + } else if (width <= Sizes.tabletBreakpoint && ref.watch(appStatusProvider).useInAppBrowser == true) { + openUrl(bookmark.url!); + } + state.selectedBookmark = bookmark; + ref.notifyListeners(); + } + Future refresh() async { await ref.read(bookmarksRequestProvider(state.readStatus, state.limit).future); } diff --git a/lib/screens/bookmarks/provider/bookmarks.provider.g.dart b/lib/screens/bookmarks/provider/bookmarks.provider.g.dart index aee75fe..c5272b1 100644 --- a/lib/screens/bookmarks/provider/bookmarks.provider.g.dart +++ b/lib/screens/bookmarks/provider/bookmarks.provider.g.dart @@ -189,7 +189,7 @@ final bookmarksRequestLoadMoreProvider = ); typedef BookmarksRequestLoadMoreRef = AutoDisposeFutureProviderRef; -String _$bookmarksHash() => r'6ebfea9e1b2b70e692527de3a3cccd0c85946c12'; +String _$bookmarksHash() => r'80e94e586950b9072f229958f98c88069086d7ab'; /// See also [Bookmarks]. @ProviderFor(Bookmarks) diff --git a/lib/screens/bookmarks/provider/search_bookmarks.provider.dart b/lib/screens/bookmarks/provider/search_bookmarks.provider.dart index 8c47f19..3cbdb16 100644 --- a/lib/screens/bookmarks/provider/search_bookmarks.provider.dart +++ b/lib/screens/bookmarks/provider/search_bookmarks.provider.dart @@ -6,6 +6,11 @@ import 'package:linkdy/screens/bookmarks/provider/bookmarks.provider.dart'; import 'package:linkdy/screens/bookmarks/provider/common_functions.dart'; import 'package:linkdy/screens/bookmarks/model/search_bookmarks.model.dart'; +import 'package:linkdy/config/sizes.dart'; +import 'package:linkdy/providers/app_status.provider.dart'; +import 'package:linkdy/providers/router.provider.dart'; +import 'package:linkdy/router/paths.dart'; +import 'package:linkdy/utils/open_url.dart'; import 'package:linkdy/constants/global_keys.dart'; import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/models/data/bookmarks.dart'; @@ -99,6 +104,16 @@ class SearchBookmarks extends _$SearchBookmarks { ref.notifyListeners(); } + void selectBookmark(Bookmark bookmark, double width) { + if (width <= Sizes.tabletBreakpoint && ref.watch(appStatusProvider).useInAppBrowser == true) { + ref.watch(routerProvider).push(RoutesPaths.webview, extra: bookmark); + } else if (width <= Sizes.tabletBreakpoint && ref.watch(appStatusProvider).useInAppBrowser == true) { + openUrl(bookmark.url!); + } + state.selectedBookmark = bookmark; + ref.notifyListeners(); + } + void deleteBookmark(Bookmark bookmark) async { final result = await BookmarkCommonFunctions.deleteBookmark( ref: ref, diff --git a/lib/screens/bookmarks/provider/search_bookmarks.provider.g.dart b/lib/screens/bookmarks/provider/search_bookmarks.provider.g.dart index 8734592..90fd2f6 100644 --- a/lib/screens/bookmarks/provider/search_bookmarks.provider.g.dart +++ b/lib/screens/bookmarks/provider/search_bookmarks.provider.g.dart @@ -174,7 +174,7 @@ final fetchSearchBookmarksLoadMoreProvider = ); typedef FetchSearchBookmarksLoadMoreRef = AutoDisposeFutureProviderRef; -String _$searchBookmarksHash() => r'bdb0eedf3ebdde4b2cb4795efe61ce42e4c85d00'; +String _$searchBookmarksHash() => r'3daf460b8389e8bcf8498c4bdf3935e88f4ce156'; /// See also [SearchBookmarks]. @ProviderFor(SearchBookmarks) diff --git a/lib/screens/bookmarks/ui/bookmark_item.dart b/lib/screens/bookmarks/ui/bookmark_item.dart index 47175e9..8d5952f 100644 --- a/lib/screens/bookmarks/ui/bookmark_item.dart +++ b/lib/screens/bookmarks/ui/bookmark_item.dart @@ -11,27 +11,30 @@ import 'package:linkdy/config/options.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'; -import 'package:linkdy/router/paths.dart'; import 'package:linkdy/utils/date_to_string.dart'; -import 'package:linkdy/utils/open_url.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; 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, }); @override @@ -46,274 +49,283 @@ class BookmarkItem extends ConsumerWidget { } } - return Slidable( - key: ValueKey(bookmark.id!), - groupTag: ConfigOptions.slidableGroupTag, - startActionPane: ActionPane( - motion: const DrawerMotion(), - extentRatio: 0.75, - children: [ - SlidableAction( - onPressed: (_) => onReadUnread(bookmark), - backgroundColor: Colors.blue, - label: bookmark.unread == true ? t.bookmarks.bookmarkOptions.read : t.bookmarks.bookmarkOptions.unread, - icon: bookmark.unread == true ? Icons.mark_email_read_rounded : Icons.mark_as_unread_rounded, - padding: const EdgeInsets.all(4), - ), - SlidableAction( - onPressed: (ctx) => onArchiveUnarchive(bookmark), - backgroundColor: Colors.grey, - label: bookmark.isArchived == true - ? t.bookmarks.bookmarkOptions.unarchive - : t.bookmarks.bookmarkOptions.archive, - icon: bookmark.isArchived == true ? Icons.unarchive_rounded : Icons.archive_rounded, - padding: const EdgeInsets.all(4), - ), - SlidableAction( - onPressed: (ctx) => showDialog( - context: context, - builder: (ctx) => ShareOptionsModal(bookmark: bookmark, onShareInternally: onShareInternally), - ), - backgroundColor: Colors.orange, - label: t.bookmarks.bookmarkOptions.shareOptions, - icon: Icons.share_rounded, - padding: const EdgeInsets.all(4), - ), - ], - ), - endActionPane: ActionPane( - motion: const DrawerMotion(), - extentRatio: 0.5, - children: [ - SlidableAction( - onPressed: (ctx) => onEdit(bookmark), - backgroundColor: Colors.green, - label: t.bookmarks.bookmarkOptions.edit, - icon: Icons.edit_rounded, - padding: const EdgeInsets.all(4), - ), - SlidableAction( - onPressed: (_) => onDelete(bookmark), - 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, + return Padding( + padding: tabletMode ? const EdgeInsets.symmetric(horizontal: 8) : const EdgeInsets.all(0), + child: ClipRRect( + borderRadius: tabletMode ? BorderRadius.circular(28) : BorderRadius.circular(0), + child: Slidable( + key: ValueKey(bookmark.id!), + groupTag: ConfigOptions.slidableGroupTag, + startActionPane: ActionPane( + motion: const DrawerMotion(), + extentRatio: 0.75, 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, - 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, - ), + SlidableAction( + onPressed: (_) => onReadUnread(bookmark), + backgroundColor: Colors.blue, + label: bookmark.unread == true ? t.bookmarks.bookmarkOptions.read : t.bookmarks.bookmarkOptions.unread, + icon: bookmark.unread == true ? Icons.mark_email_read_rounded : Icons.mark_as_unread_rounded, + padding: const EdgeInsets.all(4), + ), + SlidableAction( + onPressed: (ctx) => onArchiveUnarchive(bookmark), + backgroundColor: Colors.grey, + label: bookmark.isArchived == true + ? t.bookmarks.bookmarkOptions.unarchive + : t.bookmarks.bookmarkOptions.archive, + icon: bookmark.isArchived == true ? Icons.unarchive_rounded : Icons.archive_rounded, + padding: const EdgeInsets.all(4), + ), + SlidableAction( + onPressed: (ctx) => showDialog( + context: context, + builder: (ctx) => ShareOptionsModal(bookmark: bookmark, onShareInternally: onShareInternally), + ), + backgroundColor: Colors.orange, + label: t.bookmarks.bookmarkOptions.shareOptions, + icon: Icons.share_rounded, + padding: const EdgeInsets.all(4), + ), + ], + ), + endActionPane: ActionPane( + motion: const DrawerMotion(), + extentRatio: 0.5, + children: [ + SlidableAction( + onPressed: (ctx) => onEdit(bookmark), + backgroundColor: Colors.green, + label: t.bookmarks.bookmarkOptions.edit, + icon: Icons.edit_rounded, + padding: const EdgeInsets.all(4), + ), + SlidableAction( + onPressed: (_) => onDelete(bookmark), + backgroundColor: Colors.red, + label: t.bookmarks.bookmarkOptions.delete, + icon: Icons.delete_rounded, + padding: const EdgeInsets.all(4), + ), + ], + ), + child: Material( + color: selected && tabletMode ? Theme.of(context).colorScheme.primaryContainer : Colors.transparent, + 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: 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, ), ), - ); - } 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, + ) + : 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, + ), + ), + ); + } + return child; + }, + ); } - 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, ), + ), + ), + ], ), - Expanded( - child: Text( - validateStrings(bookmark.title, bookmark.websiteTitle), - maxLines: 1, + const SizedBox(height: 4), + Text( + validateStrings(bookmark.description, bookmark.websiteDescription), + maxLines: 3, 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: 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, + 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/screens/bookmarks/ui/bookmarks.dart b/lib/screens/bookmarks/ui/bookmarks.dart index bd54a55..14b2d4d 100644 --- a/lib/screens/bookmarks/ui/bookmarks.dart +++ b/lib/screens/bookmarks/ui/bookmarks.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:linkdy/screens/bookmarks/ui/visualization_modal.dart'; +import 'package:linkdy/screens/bookmarks/ui/visualization_modal.dart'; import 'package:linkdy/screens/bookmarks/provider/bookmarks.provider.dart'; import 'package:linkdy/screens/bookmarks/ui/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'; +import 'package:linkdy/screens/webview/ui/webview.dart'; import 'package:linkdy/widgets/circle_page_transition.dart'; import 'package:linkdy/widgets/error_screen.dart'; import 'package:linkdy/widgets/no_data_screen.dart'; +import 'package:linkdy/config/sizes.dart'; import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/providers/router.provider.dart'; import 'package:linkdy/router/paths.dart'; @@ -23,6 +25,46 @@ final GlobalKey _searchButtonKey = GlobalKey(); class BookmarksScreen extends ConsumerWidget { const BookmarksScreen({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + return ScaffoldMessenger( + key: ScaffoldMessengerKeys.bookmarks, + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > Sizes.tabletBreakpoint) { + return Row( + children: [ + const Expanded(flex: 2, child: _List(tabletMode: true)), + if (ref.watch(bookmarksProvider).selectedBookmark != null) + Expanded( + flex: 3, + child: WebViewScreen( + bookmark: ref.watch(bookmarksProvider).selectedBookmark!, + ), + ), + if (ref.watch(bookmarksProvider).selectedBookmark == null) + const Expanded( + flex: 3, + child: SizedBox(), + ), + ], + ); + } else { + return const _List(tabletMode: false); + } + }, + ), + ); + } +} + +class _List extends ConsumerWidget { + final bool tabletMode; + + const _List({ + required this.tabletMode, + }); + @override Widget build(BuildContext context, WidgetRef ref) { final bookmarks = ref.watch(bookmarksProvider); @@ -51,184 +93,184 @@ class BookmarksScreen extends ConsumerWidget { return false; } - return ScaffoldMessenger( - key: ScaffoldMessengerKeys.bookmarks, - child: Scaffold( - body: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) => [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar.large( - pinned: true, - floating: true, - centerTitle: false, - forceElevated: innerBoxIsScrolled, - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text(t.bookmarks.bookmarks), - if (bookmarks.readStatus == ReadStatus.read || bookmarks.readStatus == ReadStatus.unread) - Container( - margin: const EdgeInsets.only(left: 8), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.primaryContainer, - ), - child: Row( - children: [ - Icon( - bookmarks.readStatus == ReadStatus.read - ? Icons.mark_email_read_rounded - : Icons.mark_as_unread_rounded, - size: 14, + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(t.bookmarks.bookmarks), + if (bookmarks.readStatus == ReadStatus.read || bookmarks.readStatus == ReadStatus.unread) + Container( + margin: const EdgeInsets.only(left: 8), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.primaryContainer, + ), + child: Row( + children: [ + Icon( + bookmarks.readStatus == ReadStatus.read + ? Icons.mark_email_read_rounded + : Icons.mark_as_unread_rounded, + size: 14, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + const SizedBox(width: 4), + Text( + bookmarks.readStatus == ReadStatus.read ? t.bookmarks.read : t.bookmarks.unread, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w700, ), - const SizedBox(width: 4), - Text( - bookmarks.readStatus == ReadStatus.read ? t.bookmarks.read : t.bookmarks.unread, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w700, + ), + ], + ), + ), + ], + ), + actions: [ + IconButton( + key: _searchButtonKey, + onPressed: openSearchModal, + icon: const Icon(Icons.search_rounded), + tooltip: t.bookmarks.search.searchBookmarks, + ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () => ref.read(routerProvider).push(RoutesPaths.archivedBookmarks), + child: Row( + children: [ + const Icon(Icons.archive_rounded), + const SizedBox(width: 16), + Text(t.bookmarks.archived), + ], + ), + ), + PopupMenuItem( + onTap: () => ref.read(routerProvider).push(RoutesPaths.sharedBookmarks), + child: Row( + children: [ + const Icon(Icons.share_rounded), + const SizedBox(width: 16), + Text(t.bookmarks.shared), + ], + ), + ), + const PopupMenuDivider(height: 1), + PopupMenuItem( + onTap: () => showModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (context) => const VisualizationModal(), + isScrollControlled: true, + ), + child: Row( + children: [ + const Icon(Icons.sort_rounded), + const SizedBox(width: 16), + Text(t.bookmarks.filterSort), + ], + ), + ), + ], + ), + const SizedBox(width: 8), + ], + ), + ), + ], + body: SafeArea( + top: false, + bottom: false, + child: Builder( + builder: (context) => RefreshIndicator( + displacement: 120, + onRefresh: () => ref.read(bookmarksProvider.notifier).refresh(), + child: NotificationListener( + onNotification: scrollListener, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (bookmarks.inialLoadStatus == LoadStatus.loading) + const SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), + ), + if (bookmarks.inialLoadStatus == LoadStatus.error) + SliverFillRemaining( + child: ErrorScreen( + error: t.bookmarks.cannotLoadBookmarks, + ), + ), + if (bookmarks.bookmarks.isEmpty) + SliverFillRemaining( + child: NoDataScreen( + message: t.bookmarks.noBookmarksAdded, + ), + ), + if (bookmarks.bookmarks.isNotEmpty) + SlidableAutoCloseBehavior( + child: SliverList.builder( + itemCount: bookmarks.bookmarks.length + 1, + itemBuilder: (context, index) { + // index == bookmarks.value!.content!.results!.length -> itemCount + 1 + if (index == bookmarks.bookmarks.length) { + if (bookmarks.loadingMore) { + return const SizedBox( + height: 80, + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + // Bottom gap for FAB + return const SizedBox(height: 80); + } + return BookmarkItem( + bookmark: bookmarks.bookmarks[index], + onSelect: (b) => ref.read(bookmarksProvider.notifier).selectBookmark(b, width), + onReadUnread: ref.read(bookmarksProvider.notifier).markAsReadUnread, + onDelete: (bookmark) => showDialog( + context: context, + builder: (context) => DeleteBookmarkModal( + bookmark: bookmark, + onDelete: ref.read(bookmarksProvider.notifier).deleteBookmark, + ), ), - ), - ], + onArchiveUnarchive: ref.read(bookmarksProvider.notifier).archiveUnarchive, + onShareInternally: ref.read(bookmarksProvider.notifier).shareUnshare, + onEdit: (b) => openBookmarkFormModal(context: context, width: width, bookmark: b), + selected: bookmarks.bookmarks[index] == bookmarks.selectedBookmark, + tabletMode: tabletMode, + ); + }, ), ), ], ), - actions: [ - IconButton( - key: _searchButtonKey, - onPressed: openSearchModal, - icon: const Icon(Icons.search_rounded), - tooltip: t.bookmarks.search.searchBookmarks, - ), - PopupMenuButton( - itemBuilder: (context) => [ - PopupMenuItem( - onTap: () => ref.read(routerProvider).push(RoutesPaths.archivedBookmarks), - child: Row( - children: [ - const Icon(Icons.archive_rounded), - const SizedBox(width: 16), - Text(t.bookmarks.archived), - ], - ), - ), - PopupMenuItem( - onTap: () => ref.read(routerProvider).push(RoutesPaths.sharedBookmarks), - child: Row( - children: [ - const Icon(Icons.share_rounded), - const SizedBox(width: 16), - Text(t.bookmarks.shared), - ], - ), - ), - const PopupMenuDivider(height: 1), - PopupMenuItem( - onTap: () => showModalBottomSheet( - context: context, - useRootNavigator: true, - builder: (context) => const VisualizationModal(), - isScrollControlled: true, - ), - child: Row( - children: [ - const Icon(Icons.sort_rounded), - const SizedBox(width: 16), - Text(t.bookmarks.filterSort), - ], - ), - ), - ], - ), - const SizedBox(width: 8), - ], - ), - ), - ], - body: SafeArea( - top: false, - bottom: false, - child: Builder( - builder: (context) => RefreshIndicator( - displacement: 120, - onRefresh: () => ref.read(bookmarksProvider.notifier).refresh(), - child: NotificationListener( - onNotification: scrollListener, - child: CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - ), - if (bookmarks.inialLoadStatus == LoadStatus.loading) - const SliverFillRemaining( - child: Center(child: CircularProgressIndicator()), - ), - if (bookmarks.inialLoadStatus == LoadStatus.error) - SliverFillRemaining( - child: ErrorScreen( - error: t.bookmarks.cannotLoadBookmarks, - ), - ), - if (bookmarks.bookmarks.isEmpty) - SliverFillRemaining( - child: NoDataScreen( - message: t.bookmarks.noBookmarksAdded, - ), - ), - if (bookmarks.bookmarks.isNotEmpty) - SlidableAutoCloseBehavior( - child: SliverList.builder( - itemCount: bookmarks.bookmarks.length + 1, - itemBuilder: (context, index) { - // index == bookmarks.value!.content!.results!.length -> itemCount + 1 - if (index == bookmarks.bookmarks.length) { - if (bookmarks.loadingMore) { - return const SizedBox( - height: 80, - child: Center( - child: CircularProgressIndicator(), - ), - ); - } - // Bottom gap for FAB - return const SizedBox(height: 80); - } - return BookmarkItem( - bookmark: bookmarks.bookmarks[index], - onReadUnread: ref.read(bookmarksProvider.notifier).markAsReadUnread, - onDelete: (bookmark) => showDialog( - context: context, - builder: (context) => DeleteBookmarkModal( - bookmark: bookmark, - onDelete: ref.read(bookmarksProvider.notifier).deleteBookmark, - ), - ), - onArchiveUnarchive: ref.read(bookmarksProvider.notifier).archiveUnarchive, - onShareInternally: ref.read(bookmarksProvider.notifier).shareUnshare, - onEdit: (b) => openBookmarkFormModal(context: context, width: width, bookmark: b), - ); - }, - ), - ), - ], - ), - ), ), ), ), ), - floatingActionButton: FloatingActionButton( - onPressed: () => openBookmarkFormModal(context: context, width: width), - child: const Icon(Icons.add_rounded), - ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => openBookmarkFormModal(context: context, width: width), + child: const Icon(Icons.add_rounded), ), ); } diff --git a/lib/screens/bookmarks/ui/search_bookmarks.dart b/lib/screens/bookmarks/ui/search_bookmarks.dart index 45af90b..8ff818a 100644 --- a/lib/screens/bookmarks/ui/search_bookmarks.dart +++ b/lib/screens/bookmarks/ui/search_bookmarks.dart @@ -6,15 +6,17 @@ import 'package:linkdy/screens/bookmarks/provider/search_bookmarks.provider.dart import 'package:linkdy/screens/bookmarks/ui/bookmark_form_modal.dart'; import 'package:linkdy/screens/bookmarks/ui/bookmark_item.dart'; import 'package:linkdy/screens/bookmarks/ui/delete_bookmark_modal.dart'; +import 'package:linkdy/screens/webview/ui/webview.dart'; import 'package:linkdy/widgets/enter_search_term_screen.dart'; +import 'package:linkdy/widgets/no_data_screen.dart'; import 'package:linkdy/widgets/error_screen.dart'; import 'package:linkdy/constants/global_keys.dart'; import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/providers/router.provider.dart'; +import 'package:linkdy/config/sizes.dart'; import 'package:linkdy/i18n/strings.g.dart'; -import 'package:linkdy/widgets/no_data_screen.dart'; class SearchBookmarksModal extends ConsumerWidget { final bool fullscreen; @@ -24,6 +26,50 @@ class SearchBookmarksModal extends ConsumerWidget { required this.fullscreen, }); + @override + Widget build(BuildContext context, WidgetRef ref) { + return ScaffoldMessenger( + key: ScaffoldMessengerKeys.search, + child: Dialog.fullscreen( + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > Sizes.tabletBreakpoint) { + return Row( + children: [ + const Expanded(flex: 2, child: _List(tabletMode: true)), + if (ref.watch(searchBookmarksProvider).selectedBookmark != null) + Expanded( + flex: 3, + child: WebViewScreen( + bookmark: ref.watch(searchBookmarksProvider).selectedBookmark!, + ), + ), + if (ref.watch(searchBookmarksProvider).selectedBookmark == null) + Expanded( + flex: 3, + child: Container( + color: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2), + ), + ), + ], + ); + } else { + return const _List(tabletMode: false); + } + }, + ), + ), + ); + } +} + +class _List extends ConsumerWidget { + final bool tabletMode; + + const _List({ + required this.tabletMode, + }); + @override Widget build(BuildContext context, WidgetRef ref) { final provider = ref.watch(searchBookmarksProvider); @@ -40,116 +86,114 @@ class SearchBookmarksModal extends ConsumerWidget { return false; } - return ScaffoldMessenger( - key: ScaffoldMessengerKeys.search, - child: Dialog.fullscreen( - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2), - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0), - toolbarHeight: 68, - leading: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: BackButton( - onPressed: () => ref.watch(routerProvider).pop(), - ), - ), - titleSpacing: 0, - title: Padding( - padding: const EdgeInsets.only(bottom: 8, right: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: TextFormField( - controller: ref.watch(searchBookmarksProvider).searchController, - onChanged: (_) => ref.read(searchBookmarksProvider.notifier).notifyListeners(), - onEditingComplete: () { - FocusManager.instance.primaryFocus?.unfocus(); - ref.read(searchBookmarksProvider.notifier).setSearchTerm(); - ref.read(searchBookmarksProvider.notifier).setInitialLoadStatus(LoadStatus.loading); - ref.read(fetchSearchBookmarksProvider(provider.limit)); - }, - autofocus: true, - decoration: InputDecoration( - hintText: t.bookmarks.search.searchBookmarks, - prefixIcon: const Icon(Icons.search_rounded), - border: InputBorder.none, - filled: true, - fillColor: Colors.grey.withOpacity(0.2), - suffixIcon: ref.watch(searchBookmarksProvider).searchController.text != "" - ? IconButton( - onPressed: ref.read(searchBookmarksProvider.notifier).clearSearch, - icon: const Icon( - Icons.close_rounded, - size: 20, - ), - tooltip: t.bookmarks.search.clearSearch, - ) - : null, - ), - textInputAction: TextInputAction.search, - ), - ), - ), - ), - body: Builder( - builder: (context) { - if (provider.searchTerm == "") { - return EnterSearchTermScreen(message: t.bookmarks.search.inputSearchTerm); - } - - if (provider.inialLoadStatus == LoadStatus.loading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - if (provider.inialLoadStatus == LoadStatus.error) { - return ErrorScreen( - error: t.bookmarks.search.cannotSearchError, - ); - } - - if (provider.bookmarks.isEmpty) { - return NoDataScreen( - message: t.bookmarks.search.inputtedSearchTermNoResults, - ); - } - - return NotificationListener( - onNotification: scrollListener, - child: SlidableAutoCloseBehavior( - child: ListView.builder( - itemCount: provider.loadingMore ? provider.bookmarks.length + 1 : provider.bookmarks.length, - itemBuilder: (context, index) { - if (provider.loadingMore == true && index == provider.bookmarks.length) { - return const Padding( - padding: EdgeInsets.all(16), - child: Center( - child: CircularProgressIndicator(), - ), - ); - } - return BookmarkItem( - bookmark: provider.bookmarks[index], - onReadUnread: ref.read(searchBookmarksProvider.notifier).markAsReadUnread, - onDelete: (bookmark) => showDialog( - context: context, - builder: (context) => DeleteBookmarkModal( - bookmark: bookmark, - onDelete: ref.read(searchBookmarksProvider.notifier).deleteBookmark, - ), - ), - onArchiveUnarchive: ref.read(searchBookmarksProvider.notifier).archiveUnarchive, - onShareInternally: ref.read(searchBookmarksProvider.notifier).shareUnshare, - onEdit: (b) => openBookmarkFormModal(context: context, width: width, bookmark: b), - ); - }, - ), - ), - ); - }, + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0.2), + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.primaryContainer.withOpacity(0), + toolbarHeight: 68, + leading: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: BackButton( + onPressed: () => ref.watch(routerProvider).pop(), ), ), + titleSpacing: 0, + title: Padding( + padding: const EdgeInsets.only(bottom: 8, right: 16), + child: ClipRRect( + borderRadius: BorderRadius.circular(50), + child: TextFormField( + controller: ref.watch(searchBookmarksProvider).searchController, + onChanged: (_) => ref.read(searchBookmarksProvider.notifier).notifyListeners(), + onEditingComplete: () { + FocusManager.instance.primaryFocus?.unfocus(); + ref.read(searchBookmarksProvider.notifier).setSearchTerm(); + ref.read(searchBookmarksProvider.notifier).setInitialLoadStatus(LoadStatus.loading); + ref.read(fetchSearchBookmarksProvider(provider.limit)); + }, + autofocus: true, + decoration: InputDecoration( + hintText: t.bookmarks.search.searchBookmarks, + prefixIcon: const Icon(Icons.search_rounded), + border: InputBorder.none, + filled: true, + fillColor: Colors.grey.withOpacity(0.2), + suffixIcon: ref.watch(searchBookmarksProvider).searchController.text != "" + ? IconButton( + onPressed: ref.read(searchBookmarksProvider.notifier).clearSearch, + icon: const Icon( + Icons.close_rounded, + size: 20, + ), + tooltip: t.bookmarks.search.clearSearch, + ) + : null, + ), + textInputAction: TextInputAction.search, + ), + ), + ), + ), + body: Builder( + builder: (context) { + if (provider.searchTerm == "") { + return EnterSearchTermScreen(message: t.bookmarks.search.inputSearchTerm); + } + + if (provider.inialLoadStatus == LoadStatus.loading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (provider.inialLoadStatus == LoadStatus.error) { + return ErrorScreen( + error: t.bookmarks.search.cannotSearchError, + ); + } + + if (provider.bookmarks.isEmpty) { + return NoDataScreen( + message: t.bookmarks.search.inputtedSearchTermNoResults, + ); + } + + return NotificationListener( + onNotification: scrollListener, + child: SlidableAutoCloseBehavior( + child: ListView.builder( + itemCount: provider.loadingMore ? provider.bookmarks.length + 1 : provider.bookmarks.length, + itemBuilder: (context, index) { + if (provider.loadingMore == true && index == provider.bookmarks.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + return BookmarkItem( + bookmark: provider.bookmarks[index], + onSelect: (b) => ref.read(searchBookmarksProvider.notifier).selectBookmark(b, width), + onReadUnread: ref.read(searchBookmarksProvider.notifier).markAsReadUnread, + onDelete: (bookmark) => showDialog( + context: context, + builder: (context) => DeleteBookmarkModal( + bookmark: bookmark, + onDelete: ref.read(searchBookmarksProvider.notifier).deleteBookmark, + ), + ), + onArchiveUnarchive: ref.read(searchBookmarksProvider.notifier).archiveUnarchive, + onShareInternally: ref.read(searchBookmarksProvider.notifier).shareUnshare, + onEdit: (b) => openBookmarkFormModal(context: context, width: width, bookmark: b), + selected: provider.bookmarks[index] == provider.selectedBookmark, + tabletMode: tabletMode, + ); + }, + ), + ), + ); + }, ), ); } diff --git a/lib/screens/filtered_bookmarks/model/filtered_bookmarks.model.dart b/lib/screens/filtered_bookmarks/model/filtered_bookmarks.model.dart index 19618be..23d7a0a 100644 --- a/lib/screens/filtered_bookmarks/model/filtered_bookmarks.model.dart +++ b/lib/screens/filtered_bookmarks/model/filtered_bookmarks.model.dart @@ -13,6 +13,7 @@ class FilteredBookmarksModel { List bookmarks; int maxNumber; bool loadingMore; + Bookmark? selectedBookmark; FilteredBookmarksModel({ this.filteredBookmarksMode = FilteredBookmarksMode.shared, @@ -24,5 +25,6 @@ class FilteredBookmarksModel { required this.bookmarks, this.maxNumber = 0, this.loadingMore = false, + this.selectedBookmark, }); } diff --git a/lib/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.dart b/lib/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.dart index c3ffa52..e68f601 100644 --- a/lib/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.dart +++ b/lib/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.dart @@ -1,4 +1,3 @@ -import 'package:linkdy/constants/global_keys.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:linkdy/screens/bookmarks/provider/favicon_loader.provider.dart'; @@ -6,6 +5,12 @@ import 'package:linkdy/screens/bookmarks/provider/common_functions.dart'; import 'package:linkdy/screens/bookmarks/provider/bookmarks.provider.dart'; import 'package:linkdy/screens/filtered_bookmarks/model/filtered_bookmarks.model.dart'; +import 'package:linkdy/config/sizes.dart'; +import 'package:linkdy/constants/global_keys.dart'; +import 'package:linkdy/providers/app_status.provider.dart'; +import 'package:linkdy/providers/router.provider.dart'; +import 'package:linkdy/router/paths.dart'; +import 'package:linkdy/utils/open_url.dart'; import 'package:linkdy/models/data/bookmarks.dart'; import 'package:linkdy/constants/enums.dart'; import 'package:linkdy/models/data/tags.dart'; @@ -149,6 +154,16 @@ class FilteredBookmarks extends _$FilteredBookmarks { ref.notifyListeners(); } + void selectBookmark(Bookmark bookmark, double width) { + if (width <= Sizes.tabletBreakpoint && ref.watch(appStatusProvider).useInAppBrowser == true) { + ref.watch(routerProvider).push(RoutesPaths.webview, extra: bookmark); + } else if (width <= Sizes.tabletBreakpoint && ref.watch(appStatusProvider).useInAppBrowser == true) { + openUrl(bookmark.url!); + } + state.selectedBookmark = bookmark; + ref.notifyListeners(); + } + Future refresh() async { if (state.filteredBookmarksMode == FilteredBookmarksMode.tag) { await ref.read( diff --git a/lib/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.g.dart b/lib/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.g.dart index 615d42e..2bc0d1f 100644 --- a/lib/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.g.dart +++ b/lib/screens/filtered_bookmarks/provider/filtered_bookmarks.provider.g.dart @@ -372,7 +372,7 @@ final filteredBookmarksRequestLoadMoreProvider = typedef FilteredBookmarksRequestLoadMoreRef = AutoDisposeFutureProviderRef; -String _$filteredBookmarksHash() => r'fdd67391d76f2bf8fd879b2f64a30d54922c4938'; +String _$filteredBookmarksHash() => r'584c9ee245200f653d43b10d807860dcbac68ff5'; /// See also [FilteredBookmarks]. @ProviderFor(FilteredBookmarks) diff --git a/lib/screens/filtered_bookmarks/ui/filtered_bookmarks.dart b/lib/screens/filtered_bookmarks/ui/filtered_bookmarks.dart index 6475cf9..3539402 100644 --- a/lib/screens/filtered_bookmarks/ui/filtered_bookmarks.dart +++ b/lib/screens/filtered_bookmarks/ui/filtered_bookmarks.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:linkdy/screens/bookmarks/ui/bookmark_form_modal.dart'; import 'package:linkdy/screens/bookmarks/ui/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/screens/webview/ui/webview.dart'; import 'package:linkdy/widgets/error_screen.dart'; import 'package:linkdy/widgets/no_data_screen.dart'; +import 'package:linkdy/config/sizes.dart'; import 'package:linkdy/constants/global_keys.dart'; import 'package:linkdy/models/data/tags.dart'; import 'package:linkdy/constants/enums.dart'; @@ -16,7 +19,7 @@ import 'package:linkdy/providers/router.provider.dart'; import 'package:linkdy/router/paths.dart'; import 'package:linkdy/i18n/strings.g.dart'; -class FilteredBookmarksScreen extends ConsumerStatefulWidget { +class FilteredBookmarksScreen extends HookConsumerWidget { final FilteredBookmarksMode filteredBookmarksMode; final String? tagId; final Tag? tag; @@ -29,44 +32,97 @@ class FilteredBookmarksScreen extends ConsumerStatefulWidget { }); @override - FilteredBookmarksScreenState createState() => FilteredBookmarksScreenState(); -} - -class FilteredBookmarksScreenState extends ConsumerState { - @override - void initState() { - ref.read(filteredBookmarksProvider.notifier).setMode(widget.filteredBookmarksMode); - if (widget.filteredBookmarksMode == FilteredBookmarksMode.tag) { - if (widget.tag == null && widget.tagId == null) { - final router = ref.read(routerProvider); - while (router.canPop() == true) { - router.pop(); + Widget build(BuildContext context, WidgetRef ref) { + useEffect( + () { + ref.read(filteredBookmarksProvider.notifier).setMode(filteredBookmarksMode); + if (filteredBookmarksMode == FilteredBookmarksMode.tag) { + if (tag == null && tagId == null) { + final router = ref.read(routerProvider); + while (router.canPop() == true) { + router.pop(); + } + router.pushReplacement(RoutesPaths.bookmarks); + return; + } + if (tag != null) { + ref.read(filteredBookmarksProvider).tag = tag; + } + if (tagId != null) { + ref.read(filteredBookmarksProvider).tagId = tagId; + } + ref.read(tagBookmarksRequestProvider(tag, tagId, ref.read(filteredBookmarksProvider).limit)); + } else { + ref.read( + filteredBookmarksRequestProvider(filteredBookmarksMode, ref.read(filteredBookmarksProvider).limit), + ); } - router.pushReplacement(RoutesPaths.bookmarks); - return; - } - if (widget.tag != null) { - ref.read(filteredBookmarksProvider).tag = widget.tag; - } - if (widget.tagId != null) { - ref.read(filteredBookmarksProvider).tagId = widget.tagId; - } - ref.read(tagBookmarksRequestProvider(widget.tag, widget.tagId, ref.read(filteredBookmarksProvider).limit)); - } else { - ref.read( - filteredBookmarksRequestProvider(widget.filteredBookmarksMode, ref.read(filteredBookmarksProvider).limit), - ); - } + return null; + }, + [filteredBookmarksMode, tagId, tag], + ); - super.initState(); - } - - @override - Widget build(BuildContext context) { - if (widget.filteredBookmarksMode == FilteredBookmarksMode.tag && widget.tag == null && widget.tagId == null) { + if (filteredBookmarksMode == FilteredBookmarksMode.tag && tag == null && tagId == null) { return const Material(); } + return ScaffoldMessenger( + key: ScaffoldMessengerKeys.filteredBookmarks, + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > Sizes.tabletBreakpoint) { + return Material( + child: Row( + children: [ + Expanded( + flex: 2, + child: _List( + filteredBookmarksMode: filteredBookmarksMode, + tag: tag, + tabletMode: true, + ), + ), + if (ref.watch(filteredBookmarksProvider).selectedBookmark != null) + Expanded( + flex: 3, + child: WebViewScreen( + bookmark: ref.watch(filteredBookmarksProvider).selectedBookmark!, + ), + ), + if (ref.watch(filteredBookmarksProvider).selectedBookmark == null) + const Expanded( + flex: 3, + child: SizedBox(), + ), + ], + ), + ); + } else { + return _List( + filteredBookmarksMode: filteredBookmarksMode, + tag: tag, + tabletMode: false, + ); + } + }, + ), + ); + } +} + +class _List extends ConsumerWidget { + final bool tabletMode; + final FilteredBookmarksMode filteredBookmarksMode; + final Tag? tag; + + const _List({ + required this.filteredBookmarksMode, + this.tag, + required this.tabletMode, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { final provider = ref.watch(filteredBookmarksProvider); final width = MediaQuery.of(context).size.width; @@ -82,10 +138,10 @@ class FilteredBookmarksScreenState extends ConsumerState [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), - sliver: SliverAppBar.large( - pinned: true, - floating: true, - centerTitle: false, - forceElevated: innerBoxIsScrolled, - title: Text(title()), - ), + return Scaffold( + body: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) => [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar.large( + pinned: true, + floating: true, + centerTitle: false, + forceElevated: innerBoxIsScrolled, + title: Text(title()), ), - ], - body: SafeArea( - top: false, - child: Builder( - builder: (context) => RefreshIndicator( - displacement: 120, - onRefresh: () => ref.read(filteredBookmarksProvider.notifier).refresh(), - child: NotificationListener( - onNotification: scrollListener, - child: CustomScrollView( - slivers: [ - SliverOverlapInjector( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + ], + body: SafeArea( + top: false, + child: Builder( + builder: (context) => RefreshIndicator( + displacement: 120, + onRefresh: () => ref.read(filteredBookmarksProvider.notifier).refresh(), + child: NotificationListener( + onNotification: scrollListener, + child: CustomScrollView( + slivers: [ + SliverOverlapInjector( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + ), + if (provider.initialLoadStatus == LoadStatus.loading) + const SliverFillRemaining( + child: Center(child: CircularProgressIndicator()), ), - if (provider.initialLoadStatus == LoadStatus.loading) - const SliverFillRemaining( - child: Center(child: CircularProgressIndicator()), + if (provider.initialLoadStatus == LoadStatus.error) + SliverFillRemaining( + child: ErrorScreen( + error: t.bookmarks.cannotLoadBookmarks, ), - if (provider.initialLoadStatus == LoadStatus.error) - SliverFillRemaining( - child: ErrorScreen( - error: t.bookmarks.cannotLoadBookmarks, - ), + ), + if (provider.bookmarks.isEmpty) + SliverFillRemaining( + child: NoDataScreen( + message: noContent(), ), - if (provider.bookmarks.isEmpty) - SliverFillRemaining( - child: NoDataScreen( - message: noContent(), - ), - ), - if (provider.bookmarks.isNotEmpty) - SlidableAutoCloseBehavior( - child: SliverList.builder( - itemCount: provider.loadingMore == true - ? provider.bookmarks.length + 1 - : provider.bookmarks.length, - itemBuilder: (context, index) { - if (provider.loadingMore == true && index == provider.bookmarks.length) { - return const Padding( - padding: EdgeInsets.all(16), - child: Center( - child: CircularProgressIndicator(), - ), - ); - } - return BookmarkItem( - bookmark: provider.bookmarks[index], - onReadUnread: ref.read(filteredBookmarksProvider.notifier).markAsReadUnread, - onDelete: (bookmark) => showDialog( - context: context, - builder: (context) => DeleteBookmarkModal( - bookmark: bookmark, - onDelete: ref.read(filteredBookmarksProvider.notifier).deleteBookmark, - ), + ), + if (provider.bookmarks.isNotEmpty) + SlidableAutoCloseBehavior( + child: SliverList.builder( + itemCount: + provider.loadingMore == true ? provider.bookmarks.length + 1 : provider.bookmarks.length, + itemBuilder: (context, index) { + if (provider.loadingMore == true && index == provider.bookmarks.length) { + return const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: CircularProgressIndicator(), ), - onArchiveUnarchive: ref.read(filteredBookmarksProvider.notifier).archiveUnarchive, - onShareInternally: ref.read(filteredBookmarksProvider.notifier).shareUnshare, - onEdit: (b) => openBookmarkFormModal(context: context, width: width, bookmark: b), ); - }, - ), + } + return BookmarkItem( + bookmark: provider.bookmarks[index], + onSelect: (b) => ref.read(filteredBookmarksProvider.notifier).selectBookmark(b, width), + onReadUnread: ref.read(filteredBookmarksProvider.notifier).markAsReadUnread, + onDelete: (bookmark) => showDialog( + context: context, + builder: (context) => DeleteBookmarkModal( + bookmark: bookmark, + onDelete: ref.read(filteredBookmarksProvider.notifier).deleteBookmark, + ), + ), + onArchiveUnarchive: ref.read(filteredBookmarksProvider.notifier).archiveUnarchive, + onShareInternally: ref.read(filteredBookmarksProvider.notifier).shareUnshare, + onEdit: (b) => openBookmarkFormModal(context: context, width: width, bookmark: b), + selected: provider.bookmarks[index] == provider.selectedBookmark, + tabletMode: tabletMode, + ); + }, ), - ], - ), + ), + ], ), ), ), diff --git a/lib/screens/webview/ui/webview.dart b/lib/screens/webview/ui/webview.dart index 8dd4ac0..3d19c4a 100644 --- a/lib/screens/webview/ui/webview.dart +++ b/lib/screens/webview/ui/webview.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:share_plus/share_plus.dart'; import 'package:linkdy/screens/webview/provider/webview.provider.dart'; @@ -11,7 +12,7 @@ import 'package:linkdy/i18n/strings.g.dart'; import 'package:linkdy/utils/copy_clipboard.dart'; import 'package:linkdy/utils/open_url.dart'; -class WebViewScreen extends ConsumerStatefulWidget { +class WebViewScreen extends HookConsumerWidget { final Bookmark bookmark; const WebViewScreen({ @@ -20,18 +21,15 @@ class WebViewScreen extends ConsumerStatefulWidget { }); @override - WebViewScreenState createState() => WebViewScreenState(); -} + Widget build(BuildContext context, WidgetRef ref) { + useEffect( + () { + ref.read(webViewProvider).webViewController.loadRequest(Uri.parse(bookmark.url!)); + return null; + }, + [bookmark], + ); -class WebViewScreenState extends ConsumerState { - @override - void initState() { - ref.read(webViewProvider).webViewController.loadRequest(Uri.parse(widget.bookmark.url!)); - super.initState(); - } - - @override - Widget build(BuildContext context) { return ScaffoldMessenger( key: ScaffoldMessengerKeys.webview, child: Scaffold( @@ -41,12 +39,12 @@ class WebViewScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - widget.bookmark.title != "" ? widget.bookmark.title! : widget.bookmark.websiteTitle!, + bookmark.title != "" ? bookmark.title! : bookmark.websiteTitle!, style: const TextStyle(fontSize: 14), ), const SizedBox(height: 4), Text( - widget.bookmark.url!, + bookmark.url!, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -104,7 +102,7 @@ class WebViewScreenState extends ConsumerState { PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( - onTap: () => Share.shareUri(Uri.parse(widget.bookmark.url!)), + onTap: () => Share.shareUri(Uri.parse(bookmark.url!)), child: Row( children: [ const Icon(Icons.share_rounded), @@ -116,7 +114,7 @@ class WebViewScreenState extends ConsumerState { PopupMenuItem( onTap: () => copyToClipboard( key: ScaffoldMessengerKeys.webview, - value: widget.bookmark.url!, + value: bookmark.url!, successMessage: t.webview.linkCopiedClipboard, ), child: Row( @@ -128,7 +126,7 @@ class WebViewScreenState extends ConsumerState { ), ), PopupMenuItem( - onTap: () => openUrl(widget.bookmark.url!), + onTap: () => openUrl(bookmark.url!), child: Row( children: [ const Icon(Icons.open_in_browser_rounded), diff --git a/pubspec.lock b/pubspec.lock index a81de88..b449253 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -390,6 +390,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" flutter_launcher_icons: dependency: "direct dev" description: @@ -493,6 +501,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49" + url: "https://pub.dev" + source: hosted + version: "2.4.10" hotreloader: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 18bf9af..c9dce13 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -57,6 +57,8 @@ dependencies: http: ^1.2.0 flutter_slidable: ^3.0.1 webview_flutter: ^4.7.0 + flutter_hooks: ^0.20.5 + hooks_riverpod: ^2.4.10 dev_dependencies: build_runner: ^2.4.8