diff --git a/client/lib/libphylum/repositories/bookmark_repository.dart b/client/lib/libphylum/repositories/bookmark_repository.dart index 1704661b..aa400272 100644 --- a/client/lib/libphylum/repositories/bookmark_repository.dart +++ b/client/lib/libphylum/repositories/bookmark_repository.dart @@ -36,7 +36,7 @@ class BookmarkRepository { } Selectable bookmarksCount(Iterable ids) { - return account.db.bookmarks.count(where: (b) => b.resourceId.isIn(ids)); + return account.db.bookmarks.count(where: (b) => b.resourceId.isIn(ids) & b.deleted.equals(false)); } Future refresh() async { diff --git a/client/lib/ui/explorer/explorer_gesture_handler.dart b/client/lib/ui/explorer/explorer_gesture_handler.dart index 6415ee54..cd8db631 100644 --- a/client/lib/ui/explorer/explorer_gesture_handler.dart +++ b/client/lib/ui/explorer/explorer_gesture_handler.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:phylum/app_shortcuts.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/ui/explorer/explorer_view_controller.dart'; -import 'package:phylum/ui/explorer/menu_options.dart'; +import 'package:phylum/ui/menu/menu_option.dart'; +import 'package:phylum/ui/menu/option_groups.dart'; import 'package:provider/provider.dart'; class ExplorerGestureHandler extends StatelessWidget { @@ -25,13 +27,16 @@ class ExplorerGestureHandler extends StatelessWidget { details.globalPosition.dx, details.globalPosition.dy, ); + final resources = [resource]; + final items = await buildPopupMenuItems(context.read(), parentFolderOptions, resources); + if (!context.mounted) return; final selection = await showMenu( context: context, position: position, - items: buildExplorerPopupMenuItems(context, [resource]).toList(growable: false), + items: items.toList(growable: false), ); if (context.mounted && selection != null) { - handleResourceOption(context, [resource], selection); + handleOption(context, resources, selection); } }, child: child, diff --git a/client/lib/ui/explorer/resource_details_row.dart b/client/lib/ui/explorer/resource_details_row.dart index c21efcfd..fd09b3ae 100644 --- a/client/lib/ui/explorer/resource_details_row.dart +++ b/client/lib/ui/explorer/resource_details_row.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/ui/explorer/resource_icon_extension.dart'; +import 'package:phylum/ui/menu/option_groups.dart'; import 'package:provider/provider.dart'; import 'explorer_view_controller.dart'; -import 'menu_options.dart'; +import '../menu/bottom_sheet.dart'; class ResourceDetailsRow extends StatelessWidget { final Resource r; @@ -47,7 +48,7 @@ class ResourceDetailsRow extends StatelessWidget { softWrap: false, overflow: TextOverflow.fade, ), - trailing: ResourceOptionsButton(r: r), + trailing: IconButton(icon: Icon(Icons.adaptive.more), onPressed: () => showMenuOptionsBottomSheet(context, resourceOptions, [r])), shape: RoundedRectangleBorder(side: border, borderRadius: BorderRadius.circular(4.0)), selected: highlight, selectedColor: dim ? theme.colorScheme.primary.withOpacity(0.75) : null, diff --git a/client/lib/ui/explorer/resource_item_gesture_handler.dart b/client/lib/ui/explorer/resource_item_gesture_handler.dart index 6fe1ea38..69136f58 100644 --- a/client/lib/ui/explorer/resource_item_gesture_handler.dart +++ b/client/lib/ui/explorer/resource_item_gesture_handler.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:phylum/app_shortcuts.dart'; import 'package:phylum/libphylum/db/db.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/ui/explorer/explorer_view_controller.dart'; import 'package:phylum/ui/explorer/resource_drop_and_drop.dart'; -import 'package:phylum/ui/explorer/menu_options.dart'; +import 'package:phylum/ui/menu/menu_option.dart'; +import 'package:phylum/ui/menu/option_groups.dart'; import 'package:provider/provider.dart'; class ResourceItemGestureHandler extends StatefulWidget { @@ -78,13 +80,15 @@ class _ResourceItemGestureHandlerState extends State details.globalPosition.dy, ); final resources = state.selected; + final items = await buildPopupMenuItems(context.read(), resourceOptions, resources); + if (!context.mounted) return; final selection = await showMenu( context: context, position: position, - items: buildChildPopupMenuItems(context, resources).toList(growable: false), + items: items.toList(growable: false), ); if (context.mounted && selection != null) { - handleResourceOption(context, resources, selection); + handleOption(context, resources, selection); } }, onDoubleTap: () { diff --git a/client/lib/ui/menu/bottom_sheet.dart b/client/lib/ui/menu/bottom_sheet.dart new file mode 100644 index 00000000..f846fd99 --- /dev/null +++ b/client/lib/ui/menu/bottom_sheet.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:phylum/libphylum/db/db.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/ui/explorer/resource_icon_extension.dart'; +import 'package:phylum/ui/menu/menu_option.dart'; +import 'package:provider/provider.dart'; + +import 'option_groups.dart'; + +void showMenuOptionsBottomSheet(BuildContext context, MenuOptionGroups options, Iterable resources) async { + final items = await buildPopupMenuItems(context.read(), options, resources); + if (!context.mounted) return; + final selected = await showModalBottomSheet( + context: context, + builder: (context) => ListView( + shrinkWrap: true, + children: [ + DecoratedBox( + position: DecorationPosition.foreground, + decoration: BoxDecoration( + border: Border( + bottom: Divider.createBorderSide(context), + ), + ), + child: resources.length == 1 + ? ListTile( + leading: resources.first.getIcon(), + title: Text( + resources.first.name, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + ) + : ListTile( + title: Text('${resources.length} items'), + ), + ), + ...items, + ], + )); + + if (!context.mounted || selected == null) { + return; + } + handleOption(context, resources, selected); +} diff --git a/client/lib/ui/explorer/menu_options.dart b/client/lib/ui/menu/menu_option.dart similarity index 50% rename from client/lib/ui/explorer/menu_options.dart rename to client/lib/ui/menu/menu_option.dart index 78c93fe1..d83b674d 100644 --- a/client/lib/ui/explorer/menu_options.dart +++ b/client/lib/ui/menu/menu_option.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:phylum/app_shortcuts.dart'; import 'package:phylum/integrations/download_manager.dart'; @@ -8,7 +10,6 @@ import 'package:phylum/libphylum/actions/bookmark_remove_action.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/ui/common/responsive_dialog.dart'; -import 'package:phylum/ui/explorer/resource_icon_extension.dart'; import 'package:phylum/ui/explorer/resource_info_view.dart'; import 'package:phylum/util/dialogs.dart'; import 'package:provider/provider.dart'; @@ -16,8 +17,8 @@ import 'package:provider/provider.dart'; enum MenuOption { details(Icons.info_outline, 'Details', isSingle), download(Icons.download, 'Download', isFilesOnly), - bookmarkAdd(Icons.bookmark_add_outlined, 'Add To Bookmarks', all), - bookmarkRemove(Icons.bookmark_remove_outlined, 'Remove From Bookmarks', all), + bookmarkAdd(Icons.bookmark_add_outlined, 'Add To Bookmarks', notAllBookmarked), + bookmarkRemove(Icons.bookmark_remove, 'Remove From Bookmarks', allBookmarked), rename(Icons.drive_file_rename_outline, 'Rename', isSingle), move(Icons.drive_file_move_outlined, 'Move To', all), copy(Icons.copy, 'Copy To', all), @@ -33,79 +34,10 @@ enum MenuOption { final IconData icon; final String text; - final bool Function(PhylumAccount account, Iterable resources) filter; + final FutureOr Function(PhylumAccount account, Iterable resources) filter; } -const resourceOptionsGroups = [ - [ - MenuOption.download, - MenuOption.rename, - MenuOption.move, - MenuOption.copy, - MenuOption.delete, - ], - [ - MenuOption.bookmarkAdd, - MenuOption.bookmarkRemove, - MenuOption.permissions, - MenuOption.publinks, - ], - [ - MenuOption.details, - ], -]; - -final explorerOptionsGroups = [ - [ - MenuOption.newFolder, - MenuOption.uploadFiles, - MenuOption.uploadFolders, - ], - [ - MenuOption.bookmarkAdd, - MenuOption.bookmarkRemove, - MenuOption.permissions, - MenuOption.publinks, - ], - [ - MenuOption.details, - ] -]; - -bool isSingle(PhylumAccount account, Iterable resources) => resources.length == 1; - -bool none(PhylumAccount account, Iterable resources) => false; - -bool all(PhylumAccount accout, Iterable resources) => true; - -bool isFilesOnly(PhylumAccount account, Iterable resources) => resources.every((r) => !r.dir); - -class ResourceOptionsButton extends StatelessWidget { - final Resource r; - - const ResourceOptionsButton({super.key, required this.r}); - - @override - Widget build(BuildContext context) { - final resources = [r]; - return MediaQuery.sizeOf(context).width < 800 - ? IconButton(icon: Icon(Icons.adaptive.more), onPressed: () => showResourceOptions(context, resources)) - : PopupMenuButton( - itemBuilder: (context) => buildChildPopupMenuItems(context, resources).toList(growable: false), - onSelected: (o) => handleResourceOption(context, resources, o), - ); - } -} - -void showResourceOptions(BuildContext context, Iterable resources) async { - final option = await showModalBottomSheet(context: context, builder: (context) => ResourceOptionsList(resources: resources)); - if (!context.mounted || option == null) { - return; - } - handleResourceOption(context, resources, option); -} - -void handleResourceOption(BuildContext context, Iterable resources, MenuOption option) async { +void handleOption(BuildContext context, Iterable resources, MenuOption option) async { final account = context.read(); switch (option) { case MenuOption.rename: @@ -172,73 +104,15 @@ void handleResourceOption(BuildContext context, Iterable resources, Me } } -class ResourceOptionsList extends StatelessWidget { - final Iterable resources; +bool isSingle(PhylumAccount account, Iterable resources) => resources.length == 1; - const ResourceOptionsList({super.key, required this.resources}); +bool none(PhylumAccount account, Iterable resources) => false; - @override - Widget build(BuildContext context) { - return ListView( - shrinkWrap: true, - children: [ - DecoratedBox( - position: DecorationPosition.foreground, - decoration: BoxDecoration( - border: Border( - bottom: Divider.createBorderSide(context), - ), - ), - child: resources.length == 1 - ? ListTile( - leading: resources.first.getIcon(), - title: Text( - resources.first.name, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - ) - : ListTile( - title: Text('${resources.length} items'), - ), - ), - ..._buildItems(context, resourceOptionsGroups, resources) - ], - ); - } -} +bool all(PhylumAccount accout, Iterable resources) => true; -Iterable _buildItems(BuildContext context, Iterable> options, Iterable resources) { - final account = context.read(); - return options - .map((group) => group.map((o) => PopupMenuItem( - value: o, - enabled: o.filter(account, resources), - child: ListTile( - visualDensity: VisualDensity.adaptivePlatformDensity, - leading: Icon(o.icon), - title: Text(o.text), - ), - ))) - .fold( - [], - (acc, e) { - if (e.isEmpty) { - return acc; - } - if (acc.isEmpty) { - return e; - } - return [...acc, PopupMenuDivider(), ...e]; - }, - ); -} +bool isFilesOnly(PhylumAccount account, Iterable resources) => resources.every((r) => !r.dir); -Iterable buildChildPopupMenuItems(BuildContext context, Iterable resources) { - return _buildItems(context, resourceOptionsGroups, resources); -} +Future notAllBookmarked(PhylumAccount account, Iterable resources) => allBookmarked(account, resources).then((b) => !b); -Iterable buildExplorerPopupMenuItems(BuildContext context, Iterable resources) { - return _buildItems(context, explorerOptionsGroups, resources); -} +Future allBookmarked(PhylumAccount account, Iterable resources) => + account.bookmarkRepository.bookmarksCount(resources.map((r) => r.id)).getSingle().then((c) => c == resources.length); diff --git a/client/lib/ui/menu/option_groups.dart b/client/lib/ui/menu/option_groups.dart new file mode 100644 index 00000000..c8629a50 --- /dev/null +++ b/client/lib/ui/menu/option_groups.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:phylum/libphylum/db/db.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; + +import 'menu_option.dart'; + +typedef MenuOptionGroups = Iterable>; + +const resourceOptions = [ + [ + MenuOption.download, + MenuOption.rename, + MenuOption.move, + MenuOption.copy, + MenuOption.delete, + ], + [ + MenuOption.bookmarkAdd, + MenuOption.bookmarkRemove, + MenuOption.permissions, + MenuOption.publinks, + ], + [ + MenuOption.details, + ], +]; + +final parentFolderOptions = [ + [ + MenuOption.newFolder, + MenuOption.uploadFiles, + MenuOption.uploadFolders, + ], + [ + MenuOption.bookmarkAdd, + MenuOption.bookmarkRemove, + MenuOption.permissions, + MenuOption.publinks, + ], + [ + MenuOption.details, + ] +]; + +Future>> buildPopupMenuItems( + PhylumAccount account, + MenuOptionGroups options, + Iterable resources, [ + bool hideDisabled = true, +]) async { + final availableOptions = await Future.wait( + options.map( + (group) async => Future.wait( + group.map((o) async { + final enabled = await o.filter(account, resources); + return hideDisabled && !enabled + ? null + : PopupMenuItem( + value: o, + enabled: enabled, + child: ListTile( + visualDensity: VisualDensity.adaptivePlatformDensity, + leading: Icon(o.icon), + title: Text(o.text), + ), + ); + }), + ).then((items) => items.whereType>()), + ), + ); + + final m = availableOptions.fold( + >[], + (acc, e) { + if (e.isEmpty) { + return acc; + } + if (acc.isEmpty) { + return e.toList(growable: false); + } + return [...acc, PopupMenuDivider(), ...e]; + }, + ); + return m; +}