diff --git a/client/lib/ui/app/app_layout.dart b/client/lib/ui/app/app_layout.dart index f324e5e2..f9d73c26 100644 --- a/client/lib/ui/app/app_layout.dart +++ b/client/lib/ui/app/app_layout.dart @@ -12,9 +12,10 @@ class AppLayout extends StatelessWidget { @override Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; return AppActions( folderId: folderId, - child: MediaQuery.of(context).size.width > 800 ? ExpandedAppLayout(folderId: folderId) : CollapsedAppLayout(folderId: folderId), + child: width > 800 ? ExpandedAppLayout(folderId: folderId, xLarge: width > 1200) : CollapsedAppLayout(folderId: folderId), ); } } @@ -35,7 +36,7 @@ class CollapsedAppLayout extends StatelessWidget { }, child: Icon(Icons.add), ), - body: ExplorerView.create(folderId), + body: ExplorerView.create(folderId, ExplorerLayout.noSidebar), ); } @@ -63,7 +64,9 @@ class CollapsedAppLayout extends StatelessWidget { class ExpandedAppLayout extends StatelessWidget { final String folderId; - const ExpandedAppLayout({super.key, required this.folderId}); + final bool xLarge; + + const ExpandedAppLayout({super.key, required this.folderId, required this.xLarge}); @override Widget build(BuildContext context) { @@ -80,23 +83,14 @@ class ExpandedAppLayout extends StatelessWidget { ), body: Row( children: [ - const SizedBox( - width: 288, + SizedBox( + width: xLarge ? 300 : 240, child: NavList(showFab: true), ), Expanded( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Card( - margin: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - elevation: 0, - child: ExplorerView.create(folderId), - ), - ), - ], + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), + child: ExplorerView.create(folderId, xLarge ? ExplorerLayout.largeSidebar : ExplorerLayout.smallSidebar), ), ), ], diff --git a/client/lib/ui/explorer/explorer_view.dart b/client/lib/ui/explorer/explorer_view.dart index a31f7eee..33cfdb58 100644 --- a/client/lib/ui/explorer/explorer_view.dart +++ b/client/lib/ui/explorer/explorer_view.dart @@ -1,99 +1,48 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_state_notifier/flutter_state_notifier.dart'; -import 'package:phylum/app_shortcuts.dart'; -import 'package:phylum/libphylum/actions/action_resource_copy.dart'; -import 'package:phylum/libphylum/actions/action_resource_move.dart'; -import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; -import 'package:phylum/util/upload_utils.dart'; +import 'package:phylum/ui/explorer/paste_action_handler.dart'; +import 'package:phylum/ui/explorer/resource_info_view.dart'; import 'package:provider/provider.dart'; -import 'package:super_clipboard/super_clipboard.dart'; import 'explorer_view_controller.dart'; import 'folder_empty_view.dart'; import 'folder_list_view.dart'; +enum ExplorerLayout { + noSidebar, + smallSidebar, + largeSidebar, +} + class ExplorerView extends StatelessWidget { const ExplorerView._({super.key}); - static Widget create(String folderId) { - return Builder(builder: (context) { - return ListTileTheme( - data: ListTileThemeData( - mouseCursor: const WidgetStatePropertyAll(SystemMouseCursors.basic), - selectedTileColor: Theme.of(context).colorScheme.primaryContainer, - ), - child: StateNotifierProvider( - create: (context) => ExplorerViewController(account: context.read(), folderId: folderId), - child: Actions( - actions: { - NextFocusIntent: CallbackAction(onInvoke: (i) => null), - PreviousFocusIntent: CallbackAction(onInvoke: (i) => null), - PasteFromClipboardIntent: CallbackAction(onInvoke: (i) async { - // TODO: Move to top-level nav - final account = context.read(); - final openUri = account.apiClient.createUri('/open'); - final clipboard = SystemClipboard.instance; - if (clipboard == null) { - return; - } - final reader = await clipboard.read(); - final paths = []; - final cutResources = []; - final copyResources = []; - for (final item in reader.items) { - final c = Completer(); - item.getValue(Formats.fileUri, (value) => c.complete(value?.toFilePath()), onError: (value) => c.complete(null)); - final filePath = await c.future; - if (filePath != null) { - paths.add(filePath); - } else { - final c = Completer(); - item.getValue(Formats.uri, (value) { - c.complete(value?.uri); - }, onError: (value) => c.complete(null)); - final uri = await c.future; - if (uri != null) { - if (uri.authority == openUri.authority && uri.path == openUri.path && uri.queryParameters.containsKey('id')) { - final resourceId = uri.queryParameters['id']!; - final cut = uri.queryParameters.containsKey('cut'); - final resource = await account.resourceRepository.getResource(resourceId); - if (resource != null) { - (cut ? cutResources : copyResources).add(resource); - } - } - } - } - } - if (!context.mounted) return; - uploadRecursive(context, folderId, paths); - final parent = await account.resourceRepository.getResource(folderId); - if (parent != null) { - for (final r in cutResources) { - account.addAction(ResourceMoveAction(r: r, parent: parent)); - } - for (final r in copyResources) { - account.addAction(ResourceCopyAction( - src: r, - resourceId: generateId(), - resourceName: r.name, - parentId: parent.id, - )); - } - } - - await SystemClipboard.instance?.write([]); - - return null; - }), - }, - child: ExplorerView._(key: ValueKey(folderId)), - ), - ), - ); - }); + static Widget create(String folderId, ExplorerLayout layout) { + final explorer = PasteActionHandler(folderId: folderId, child: ExplorerView._(key: ValueKey(folderId))); + return StateNotifierProvider( + create: (context) => ExplorerViewController(account: context.read(), folderId: folderId), + child: Actions( + actions: { + NextFocusIntent: CallbackAction(onInvoke: (i) => null), + PreviousFocusIntent: CallbackAction(onInvoke: (i) => null), + }, + child: layout == ExplorerLayout.noSidebar + ? explorer + : Row( + children: [ + Expanded(child: Card(child: explorer)), + SizedBox( + width: layout == ExplorerLayout.largeSidebar ? 360 : 300, + child: Card( + margin: EdgeInsets.only(left: 16, bottom: 16), + child: ResourceInfoView(folderId: folderId), + ), + ) + ], + ), + ), + ); } @override diff --git a/client/lib/ui/explorer/folder_list_view.dart b/client/lib/ui/explorer/folder_list_view.dart index 7467b40e..7654ebdd 100644 --- a/client/lib/ui/explorer/folder_list_view.dart +++ b/client/lib/ui/explorer/folder_list_view.dart @@ -23,38 +23,44 @@ class _FolderListViewState extends State { child: Focus( autofocus: true, descendantsAreFocusable: false, - child: ListView.builder( - itemCount: resources.length, - itemBuilder: (context, index) { - final r = resources[index]; - bool deferHandling = false; - return ResourceDetailsRow( - r: r, - onTapDown: (details) { - Focus.maybeOf(context)?.requestFocus(); - final mode = HardwareKeyboard.instance.isControlPressed - ? SelectionMode.toggle - : HardwareKeyboard.instance.isShiftPressed - ? SelectionMode.range - : SelectionMode.single; - if (context.read().isSelected(r.id) && mode == SelectionMode.single) { - deferHandling = true; - context.read().updateSelection((_) => index, SelectionMode.multi, false); - } else { - deferHandling = false; - context.read().updateSelection((_) => index, mode, false); - } - }, - onTap: () { - if (deferHandling) { + child: ListTileTheme( + data: ListTileThemeData( + mouseCursor: const WidgetStatePropertyAll(SystemMouseCursors.basic), + selectedTileColor: Theme.of(context).colorScheme.primaryContainer, + ), + child: ListView.builder( + itemCount: resources.length, + itemBuilder: (context, index) { + final r = resources[index]; + bool deferHandling = false; + return ResourceDetailsRow( + r: r, + onTapDown: (details) { + Focus.maybeOf(context)?.requestFocus(); + final mode = HardwareKeyboard.instance.isControlPressed + ? SelectionMode.toggle + : HardwareKeyboard.instance.isShiftPressed + ? SelectionMode.range + : SelectionMode.single; + if (context.read().isSelected(r.id) && mode == SelectionMode.single) { + deferHandling = true; + context.read().updateSelection((_) => index, SelectionMode.multi, false); + } else { + deferHandling = false; + context.read().updateSelection((_) => index, mode, false); + } + }, + onTap: () { + if (deferHandling) { + context.read().updateSelection((_) => index, SelectionMode.single, false); + } + }, + onDoubleTap: () { context.read().updateSelection((_) => index, SelectionMode.single, false); - } - }, - onDoubleTap: () { - context.read().updateSelection((_) => index, SelectionMode.single, false); - Actions.maybeInvoke(context, const ActivateIntent()); - }); - }, + Actions.maybeInvoke(context, const ActivateIntent()); + }); + }, + ), ), ), ); diff --git a/client/lib/ui/explorer/paste_action_handler.dart b/client/lib/ui/explorer/paste_action_handler.dart new file mode 100644 index 00000000..9cb5557a --- /dev/null +++ b/client/lib/ui/explorer/paste_action_handler.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:phylum/app_shortcuts.dart'; +import 'package:phylum/libphylum/actions/action_resource_copy.dart'; +import 'package:phylum/libphylum/actions/action_resource_move.dart'; +import 'package:phylum/libphylum/db/db.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/util/upload_utils.dart'; +import 'package:provider/provider.dart'; +import 'package:super_clipboard/super_clipboard.dart'; + +class PasteActionHandler extends StatelessWidget { + final String folderId; + final Widget child; + + const PasteActionHandler({super.key, required this.folderId, required this.child}); + + @override + Widget build(BuildContext context) { + return Actions( + actions: { + PasteFromClipboardIntent: CallbackAction(onInvoke: (i) async { + final account = context.read(); + final openUri = account.apiClient.createUri('/open'); + final clipboard = SystemClipboard.instance; + if (clipboard == null) { + return; + } + final reader = await clipboard.read(); + final paths = []; + final cutResources = []; + final copyResources = []; + for (final item in reader.items) { + final c = Completer(); + item.getValue(Formats.fileUri, (value) => c.complete(value?.toFilePath()), onError: (value) => c.complete(null)); + final filePath = await c.future; + if (filePath != null) { + paths.add(filePath); + } else { + final c = Completer(); + item.getValue(Formats.uri, (value) { + c.complete(value?.uri); + }, onError: (value) => c.complete(null)); + final uri = await c.future; + if (uri != null) { + if (uri.authority == openUri.authority && uri.path == openUri.path && uri.queryParameters.containsKey('id')) { + final resourceId = uri.queryParameters['id']!; + final cut = uri.queryParameters.containsKey('cut'); + final resource = await account.resourceRepository.getResource(resourceId); + if (resource != null) { + (cut ? cutResources : copyResources).add(resource); + } + } + } + } + } + if (!context.mounted) return; + uploadRecursive(context, folderId, paths); + final parent = await account.resourceRepository.getResource(folderId); + if (parent != null) { + for (final r in cutResources) { + account.addAction(ResourceMoveAction(r: r, parent: parent)); + } + for (final r in copyResources) { + account.addAction(ResourceCopyAction( + src: r, + resourceId: generateId(), + resourceName: r.name, + parentId: parent.id, + )); + } + } + + await SystemClipboard.instance?.write([]); + + return null; + }), + }, + child: child, + ); + } +} diff --git a/client/lib/ui/explorer/path_view.dart b/client/lib/ui/explorer/path_view.dart index 7d98b714..052cbaf9 100644 --- a/client/lib/ui/explorer/path_view.dart +++ b/client/lib/ui/explorer/path_view.dart @@ -86,7 +86,10 @@ class _PathViewState extends State { }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Text(r.parent == null ? "/" : r.name), + child: Text( + r.parent == null ? "/" : r.name, + style: const TextStyle(fontSize: 16), + ), ), ), ), diff --git a/client/lib/ui/explorer/resource_info_view.dart b/client/lib/ui/explorer/resource_info_view.dart new file mode 100644 index 00000000..a3514b09 --- /dev/null +++ b/client/lib/ui/explorer/resource_info_view.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class ResourceInfoView extends StatefulWidget { + final String folderId; + + const ResourceInfoView({super.key, required this.folderId}); + + @override + State createState() => _ResourceInfoViewState(); +} + +class _ResourceInfoViewState extends State { + @override + Widget build(BuildContext context) { + return ListView( + children: [Text('Loading')], + ); + } +}