diff --git a/app/lib/dialogs/packs/dialog.dart b/app/lib/dialogs/packs/dialog.dart index 1a2331129a21..a8dee3cc7270 100644 --- a/app/lib/dialogs/packs/dialog.dart +++ b/app/lib/dialogs/packs/dialog.dart @@ -109,77 +109,77 @@ class _PacksDialogState extends State background: Container( color: Colors.red, ), - child: ListTile( - title: Text(metadata.name), - subtitle: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (metadata.author.isNotEmpty) - Text(AppLocalizations.of(context) - .byAuthor(metadata.author)), - if (metadata.description.isNotEmpty) - Text(metadata.description), - ], - ), - onTap: () async { - final bloc = context.read(); - Navigator.of(context).pop(); - final newPack = await showDialog( - context: context, - builder: (context) => - BlocProvider.value( - value: bloc, - child: PackDialog(pack: pack))); - if (newPack == null) return; - bloc.add( - PackUpdated(metadata.name, newPack)); - }, - trailing: MenuAnchor( - builder: defaultMenuButton( - tooltip: - AppLocalizations.of(context).actions, + child: ContextRegion( + menuChildren: [ + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.appWindow), + child: Text( + AppLocalizations.of(context).local), + onPressed: () async { + await _addPack(pack!, true); + if (mounted) { + setState(() {}); + } + }, ), - menuChildren: [ - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.appWindow), - child: Text( - AppLocalizations.of(context).local), - onPressed: () async { - await _addPack(pack!, true); - if (mounted) { - setState(() {}); - } - }, - ), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.download), - child: Text(AppLocalizations.of(context) - .export), - onPressed: () async { - await _exportPack(pack!); - if (mounted) { - setState(() {}); - } - }, - ), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.trash), - child: Text(AppLocalizations.of(context) - .delete), - onPressed: () { - context - .read() - .add(PackRemoved(metadata.name)); - if (mounted) { - setState(() {}); - } - }, - ), - ], + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.download), + child: Text( + AppLocalizations.of(context).export), + onPressed: () async { + await _exportPack(pack!); + if (mounted) { + setState(() {}); + } + }, + ), + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.trash), + child: Text( + AppLocalizations.of(context).delete), + onPressed: () { + context + .read() + .add(PackRemoved(metadata.name)); + if (mounted) { + setState(() {}); + } + }, + ), + ], + builder: (context, button, controller) => + ListTile( + title: Text(metadata.name), + subtitle: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (metadata.author.isNotEmpty) + Text(AppLocalizations.of(context) + .byAuthor(metadata.author)), + if (metadata.description.isNotEmpty) + Text(metadata.description), + ], + ), + onTap: () async { + final bloc = context.read(); + Navigator.of(context).pop(); + final newPack = + await showDialog( + context: context, + builder: (context) => + BlocProvider.value( + value: bloc, + child: PackDialog( + pack: pack))); + if (newPack == null) return; + bloc.add( + PackUpdated(metadata.name, newPack)); + }, + trailing: button, ), ), ); @@ -221,90 +221,86 @@ class _PacksDialogState extends State background: Container( color: Colors.red, ), - child: ListTile( - title: Text(metadata.name), - subtitle: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - if (metadata.author.isNotEmpty) - Text(AppLocalizations.of(context) - .byAuthor(metadata.author)), - if (metadata.description.isNotEmpty) - Text(metadata.description), - ], - ), - leading: widget.globalOnly - ? null - : IconButton.outlined( - icon: const PhosphorIcon( - PhosphorIconsLight.plus), - onPressed: () async { - await _addPack(pack, false); - _controller.animateTo(0); - if (mounted) { - setState(() {}); - } - }, - tooltip: - AppLocalizations.of(context) - .install, - ), - onTap: () async { - final bloc = context.read(); - final newPack = - await showDialog( - context: context, - builder: (context) => - BlocProvider.value( - value: bloc, - child: - PackDialog(pack: pack), - )); - if (newPack == null) return; - final name = newPack.name ?? ''; - if (pack.name != name) { - await _packSystem - .deleteFile(metadata.name); - } - await _packSystem.updateFile( - name, newPack); - setState(() {}); - }, - trailing: MenuAnchor( - builder: defaultMenuButton( - tooltip: - AppLocalizations.of(context).copy, + child: ContextRegion( + menuChildren: [ + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.download), + child: Text(AppLocalizations.of(context) + .export), + onPressed: () async { + await _exportPack(pack); + if (mounted) { + setState(() {}); + } + }, ), - menuChildren: [ - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.download), - child: Text( - AppLocalizations.of(context) - .export), - onPressed: () async { - await _exportPack(pack); - if (mounted) { - setState(() {}); - } - }, - ), - MenuItemButton( - leadingIcon: const PhosphorIcon( - PhosphorIconsLight.trash), - child: Text( - AppLocalizations.of(context) - .delete), - onPressed: () async { - await _packSystem - .deleteFile(metadata.name); - if (mounted) { - setState(() {}); - } - }, - ), - ], + MenuItemButton( + leadingIcon: const PhosphorIcon( + PhosphorIconsLight.trash), + child: Text(AppLocalizations.of(context) + .delete), + onPressed: () async { + await _packSystem + .deleteFile(metadata.name); + if (mounted) { + setState(() {}); + } + }, + ), + ], + builder: (context, button, controller) => + ListTile( + title: Text(metadata.name), + subtitle: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (metadata.author.isNotEmpty) + Text(AppLocalizations.of(context) + .byAuthor(metadata.author)), + if (metadata.description.isNotEmpty) + Text(metadata.description), + ], + ), + leading: widget.globalOnly + ? null + : IconButton.outlined( + icon: const PhosphorIcon( + PhosphorIconsLight.plus), + onPressed: () async { + await _addPack(pack, false); + _controller.animateTo(0); + if (mounted) { + setState(() {}); + } + }, + tooltip: + AppLocalizations.of(context) + .install, + ), + onTap: () async { + final bloc = + context.read(); + final newPack = await showDialog< + NoteData>( + context: context, + builder: (context) => + BlocProvider.value( + value: bloc, + child: PackDialog(pack: pack), + )); + if (newPack == null) return; + final name = newPack.name ?? ''; + if (pack.name != name) { + await _packSystem + .deleteFile(metadata.name); + } + await _packSystem.updateFile( + name, newPack); + setState(() {}); + }, + trailing: button, ), ), ); diff --git a/app/lib/services/sync.dart b/app/lib/services/sync.dart index 8cdf019cccdb..7c7f71f5f1f6 100644 --- a/app/lib/services/sync.dart +++ b/app/lib/services/sync.dart @@ -83,11 +83,7 @@ class SyncService { List get syncs => List.unmodifiable(_syncs); } -enum SyncStatus { - synced, - syncing, - error, -} +enum SyncStatus { synced, syncing, error } class RemoteSync { final BuildContext context; @@ -109,6 +105,18 @@ class RemoteSync { _filesSubject.onListen = _onListen; } + FileSyncStatus getFileStatus(AssetLocation location) { + final files = this.files; + return files + ?.where((file) => + file.location.remote == location.remote && + location.path.startsWith(file.location.path)) + .fold(FileSyncStatus.offline, (value, element) { + return value.combine(element.status); + }) ?? + FileSyncStatus.offline; + } + RemoteDirectoryFileSystem? buildRemoteSystem() => fileSystem.buildDocumentSystem(remoteStorage).remoteSystem; diff --git a/app/lib/views/files/entity.dart b/app/lib/views/files/entity.dart index d682ec7dbe2d..1d16b3e9729a 100644 --- a/app/lib/views/files/entity.dart +++ b/app/lib/views/files/entity.dart @@ -1,16 +1,22 @@ import 'dart:typed_data'; import 'package:butterfly/api/file_system.dart'; +import 'package:butterfly/api/save.dart'; import 'package:butterfly/cubits/settings.dart'; +import 'package:butterfly/dialogs/file_system/move.dart'; +import 'package:butterfly/services/sync.dart'; import 'package:butterfly/views/files/grid.dart'; import 'package:butterfly/views/files/list.dart'; import 'package:butterfly/visualizer/asset.dart'; +import 'package:butterfly/visualizer/connection.dart'; import 'package:butterfly_api/butterfly_api.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; import 'package:lw_file_system/lw_file_system.dart'; +import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; import 'package:popover/popover.dart'; @@ -159,41 +165,56 @@ class _FileEntityItemState extends State { ), ), ), - child: widget.gridView - ? FileEntityGridItem( - modifiedText: modifiedText, - createdText: createdText, - icon: icon, - onTap: widget.onTap, - onDelete: onDelete, - onReload: widget.onReload, - onEdit: onEdit, - entity: widget.entity, - nameController: _nameController, - collapsed: widget.collapsed, - editable: _editable, - active: widget.active, - selected: widget.selected, - onSelectedChanged: widget.onSelected, - thumbnail: thumbnail, - ) - : FileEntityListTile( - modifiedText: modifiedText, - createdText: createdText, - icon: icon, - onTap: widget.onTap, - onDelete: onDelete, - onReload: widget.onReload, - onEdit: onEdit, - entity: widget.entity, - nameController: _nameController, - collapsed: widget.collapsed, - editable: _editable, - active: widget.active, - selected: widget.selected, - onSelectedChanged: widget.onSelected, - thumbnail: thumbnail, - ), + child: ContextFileRegion( + remote: remote, + entity: entity, + settingsCubit: fileSystem.settingsCubit, + editable: _editable, + onEdit: onEdit, + nameController: _nameController, + onDelete: onDelete, + onReload: widget.onReload, + documentSystem: documentSystem, + onSelect: + widget.selected == null ? () => widget.onSelected(true) : null, + builder: (context, button, controller) => widget.gridView + ? FileEntityGridItem( + modifiedText: modifiedText, + createdText: createdText, + icon: icon, + onTap: widget.onTap, + onDelete: onDelete, + onReload: widget.onReload, + onEdit: onEdit, + entity: widget.entity, + nameController: _nameController, + collapsed: widget.collapsed, + editable: _editable, + active: widget.active, + selected: widget.selected, + onSelectedChanged: widget.onSelected, + thumbnail: thumbnail, + actionButton: button, + ) + : FileEntityListTile( + modifiedText: modifiedText, + createdText: createdText, + icon: icon, + onTap: widget.onTap, + onDelete: onDelete, + onReload: widget.onReload, + onEdit: onEdit, + entity: widget.entity, + nameController: _nameController, + collapsed: widget.collapsed, + editable: _editable, + active: widget.active, + selected: widget.selected, + onSelectedChanged: widget.onSelected, + thumbnail: thumbnail, + actionButton: button, + ), + ), ); if (widget.entity is FileSystemDirectory) { return DragTarget( @@ -213,3 +234,127 @@ class _FileEntityItemState extends State { return draggable; } } + +class ContextFileRegion extends StatelessWidget { + final ExternalStorage? remote; + final DocumentFileSystem documentSystem; + final FileSystemEntity entity; + final SettingsCubit settingsCubit; + final bool editable; + final ValueChanged onEdit; + final VoidCallback onReload, onDelete; + final VoidCallback? onSelect; + final TextEditingController nameController; + final ContextRegionChildBuilder builder; + + const ContextFileRegion({ + super.key, + required this.remote, + required this.entity, + required this.settingsCubit, + required this.editable, + required this.onEdit, + required this.nameController, + required this.onDelete, + required this.documentSystem, + required this.onReload, + required this.builder, + this.onSelect, + }); + + @override + Widget build(BuildContext context) { + final syncService = context.read(); + return ContextRegion( + menuChildren: [ + if (remote is RemoteStorage) + StreamBuilder>( + stream: syncService.getSync(remote!.identifier)?.filesStream, + builder: (context, snapshot) { + final currentStatus = snapshot.data + ?.lastWhereOrNull((element) => + entity.location.path.startsWith(element.location.path)) + ?.status; + return MenuItemButton( + leadingIcon: PhosphorIcon(currentStatus.getIcon(), + color: + currentStatus.getColor(Theme.of(context).colorScheme)), + child: Text(currentStatus.getLocalizedName(context)), + onPressed: () { + syncService.getSync(remote!.identifier)?.sync(); + }, + ); + }, + ), + BlocBuilder( + builder: (context, state) { + final starred = state.isStarred(entity.location); + return MenuItemButton( + onPressed: () { + settingsCubit.toggleStarred(entity.location); + }, + leadingIcon: starred + ? const PhosphorIcon(PhosphorIconsFill.star) + : const PhosphorIcon(PhosphorIconsLight.star), + child: Text(starred + ? AppLocalizations.of(context).unstar + : AppLocalizations.of(context).star), + ); + }), + if (onSelect != null) + MenuItemButton( + onPressed: onSelect, + leadingIcon: const PhosphorIcon(PhosphorIconsLight.check), + child: Text(AppLocalizations.of(context).select), + ), + MenuItemButton( + onPressed: () => showDialog( + context: context, + builder: (context) => FileSystemAssetMoveDialog( + assets: [entity.location], + fileSystem: documentSystem, + ), + ).then((value) { + if (value != null) onReload(); + }), + leadingIcon: const PhosphorIcon(PhosphorIconsLight.arrowsDownUp), + child: Text(AppLocalizations.of(context).move), + ), + if (!editable) + MenuItemButton( + onPressed: () { + onEdit(true); + nameController.text = entity.fileName; + }, + leadingIcon: const PhosphorIcon(PhosphorIconsLight.pencil), + child: Text(AppLocalizations.of(context).rename), + ), + if (entity is FileSystemFile) + MenuItemButton( + onPressed: () { + try { + final data = (entity as FileSystemFile).data; + exportData(context, data?.exportAsBytes() ?? []); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of(context).error, + ), + ), + ); + } + }, + leadingIcon: const PhosphorIcon(PhosphorIconsLight.paperPlaneRight), + child: Text(AppLocalizations.of(context).export), + ), + MenuItemButton( + onPressed: onDelete, + leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), + child: Text(AppLocalizations.of(context).delete), + ), + ], + builder: builder, + ); + } +} diff --git a/app/lib/views/files/grid.dart b/app/lib/views/files/grid.dart index dce06f7c6cce..1cae22ff1100 100644 --- a/app/lib/views/files/grid.dart +++ b/app/lib/views/files/grid.dart @@ -1,24 +1,18 @@ import 'dart:typed_data'; import 'package:butterfly/api/file_system.dart'; -import 'package:butterfly/api/save.dart'; -import 'package:butterfly/cubits/settings.dart'; -import 'package:butterfly/dialogs/file_system/move.dart'; -import 'package:butterfly/services/sync.dart'; -import 'package:butterfly/visualizer/connection.dart'; import 'package:butterfly_api/butterfly_api.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:lw_file_system/lw_file_system.dart'; -import 'package:material_leap/material_leap.dart'; import 'package:phosphor_flutter/phosphor_flutter.dart'; class FileEntityGridItem extends StatelessWidget { final String? modifiedText, createdText; final bool active, editable, collapsed; final bool? selected; + final Widget actionButton; final PhosphorIconData icon; final VoidCallback onTap, onDelete, onReload; final ValueChanged onEdit, onSelectedChanged; @@ -34,6 +28,7 @@ class FileEntityGridItem extends StatelessWidget { this.active = false, this.editable = false, this.collapsed = false, + required this.actionButton, required this.icon, required this.onTap, required this.onDelete, @@ -49,8 +44,8 @@ class FileEntityGridItem extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final fileSystem = context.read(); - final syncService = context.read(); - final remote = fileSystem.settingsCubit.getRemote(entity.location.remote); + final settingsCubit = fileSystem.settingsCubit; + final remote = settingsCubit.getRemote(entity.location.remote); final documentSystem = fileSystem.buildDocumentSystem(remote); final leading = PhosphorIcon( icon, @@ -58,333 +53,197 @@ class FileEntityGridItem extends StatelessWidget { size: 48, ); return Card( - elevation: 5, - margin: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 0, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: active - ? BorderSide( - color: colorScheme.primaryContainer, - width: 1, - ) - : BorderSide.none, - ), - surfaceTintColor: active - ? colorScheme.primaryContainer - : colorScheme.secondaryContainer, - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: onTap, - highlightColor: active ? colorScheme.primaryContainer : null, - child: SizedBox( - width: 160, - height: 192, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Stack( - children: [ - Center( - child: Container( - height: 96, - width: 96, - padding: const EdgeInsets.only(top: 8), - child: thumbnail != null - ? Image.memory( - thumbnail!, - fit: BoxFit.contain, - cacheWidth: 96, - cacheHeight: 96, - errorBuilder: (context, error, stackTrace) => - leading, - ) - : leading, - ), - ), - Align( - alignment: Alignment.topRight, - child: FilesActionMenu( - remote: remote, - syncService: syncService, - entity: entity, - settingsCubit: fileSystem.settingsCubit, - editable: editable, - onEdit: onEdit, - nameController: nameController, - onDelete: onDelete, - onReload: onReload, - documentSystem: documentSystem, - onSelect: selected == null - ? () => onSelectedChanged(true) - : null, + elevation: 5, + margin: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 0, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: active + ? BorderSide( + color: colorScheme.primaryContainer, + width: 1, + ) + : BorderSide.none, + ), + surfaceTintColor: active + ? colorScheme.primaryContainer + : colorScheme.secondaryContainer, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: onTap, + highlightColor: active ? colorScheme.primaryContainer : null, + child: SizedBox( + width: 160, + height: 192, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Stack( + children: [ + Center( + child: Container( + height: 96, + width: 96, + padding: const EdgeInsets.only(top: 8), + child: thumbnail != null + ? Image.memory( + thumbnail!, + fit: BoxFit.contain, + cacheWidth: 96, + cacheHeight: 96, + errorBuilder: (context, error, stackTrace) => + leading, + ) + : leading, + ), ), - ), - if (selected != null) Align( - alignment: Alignment.topLeft, - child: Checkbox( - value: selected, - onChanged: (value) => onSelectedChanged(value ?? false), - ), + alignment: Alignment.topRight, + child: actionButton, ), - ], - ), - const SizedBox(height: 8), - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox( - height: 16, - child: modifiedText != null - ? Tooltip( - message: AppLocalizations.of(context).modified, - child: Row( - children: [ - PhosphorIcon( - PhosphorIconsLight.clockCounterClockwise, - size: 12, - color: colorScheme.outline, - ), - const SizedBox(width: 8), - Text( - modifiedText!, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - color: colorScheme.outline, - ), - ), - ], - ), - ) - : null, + if (selected != null) + Align( + alignment: Alignment.topLeft, + child: Checkbox( + value: selected, + onChanged: (value) => + onSelectedChanged(value ?? false), + ), ), - SizedBox( - height: 16, - child: createdText != null - ? Tooltip( - message: AppLocalizations.of(context).created, - child: Row( - children: [ - PhosphorIcon( - PhosphorIconsLight.plus, - size: 12, - color: colorScheme.outline, - ), - const SizedBox(width: 8), - Text( - createdText!, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - color: colorScheme.outline, - ), + ], + ), + const SizedBox(height: 8), + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox( + height: 16, + child: modifiedText != null + ? Tooltip( + message: + AppLocalizations.of(context).modified, + child: Row( + children: [ + PhosphorIcon( + PhosphorIconsLight + .clockCounterClockwise, + size: 12, + color: colorScheme.outline, + ), + const SizedBox(width: 8), + Text( + modifiedText!, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: colorScheme.outline, + ), + ), + ], + ), + ) + : null, + ), + SizedBox( + height: 16, + child: createdText != null + ? Tooltip( + message: AppLocalizations.of(context).created, + child: Row( + children: [ + PhosphorIcon( + PhosphorIconsLight.plus, + size: 12, + color: colorScheme.outline, + ), + const SizedBox(width: 8), + Text( + createdText!, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: colorScheme.outline, + ), + ), + ], + ), + ) + : null, + ), + const SizedBox(height: 8), + SizedBox( + height: 32, + child: editable + ? ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 200, + ), + child: TextField( + controller: nameController, + autofocus: true, + style: + Theme.of(context).textTheme.labelLarge, + onSubmitted: (value) async { + await documentSystem.renameAsset( + entity.location.path, value); + onEdit(false); + onReload(); + }, + decoration: InputDecoration( + filled: true, + hintText: AppLocalizations.of(context) + .enterText, + suffix: IconButton( + onPressed: () async { + await documentSystem.renameAsset( + entity.location.path, + nameController.text); + onEdit(false); + onReload(); + }, + icon: const PhosphorIcon( + PhosphorIconsLight.check), + tooltip: + AppLocalizations.of(context).save, + ), ), - ], - ), - ) - : null, - ), - const SizedBox(height: 8), - SizedBox( - height: 32, - child: editable - ? ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 200, - ), - child: TextField( - controller: nameController, - autofocus: true, - style: Theme.of(context).textTheme.labelLarge, - onSubmitted: (value) async { - await documentSystem.renameAsset( - entity.location.path, value); - onEdit(false); - onReload(); - }, - decoration: InputDecoration( - filled: true, - hintText: - AppLocalizations.of(context).enterText, - suffix: IconButton( - onPressed: () async { - await documentSystem.renameAsset( - entity.location.path, - nameController.text); - onEdit(false); - onReload(); + )) + : Align( + alignment: Alignment.centerLeft, + child: Tooltip( + message: entity.fileName, + child: GestureDetector( + child: Text( + entity.fileName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .labelLarge, + ), + onDoubleTap: () { + onEdit(true); + nameController.text = entity.fileName; }, - icon: const PhosphorIcon( - PhosphorIconsLight.check), - tooltip: - AppLocalizations.of(context).save, - ), - ), - )) - : Align( - alignment: Alignment.centerLeft, - child: Tooltip( - message: entity.fileName, - child: GestureDetector( - child: Text( - entity.fileName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .labelLarge, ), - onDoubleTap: () { - onEdit(true); - nameController.text = entity.fileName; - }, ), ), - ), - ), - ], + ), + ], + ), ), ), - ), - ], - ), - ), - ), - ); - } -} - -class FilesActionMenu extends StatelessWidget { - const FilesActionMenu({ - super.key, - required this.remote, - required this.syncService, - required this.entity, - required this.settingsCubit, - required this.editable, - required this.onEdit, - required this.nameController, - required this.onDelete, - required this.documentSystem, - required this.onReload, - this.onSelect, - }); - - final ExternalStorage? remote; - final SyncService syncService; - final DocumentFileSystem documentSystem; - final FileSystemEntity entity; - final SettingsCubit settingsCubit; - final bool editable; - final ValueChanged onEdit; - final VoidCallback onReload, onDelete; - final VoidCallback? onSelect; - final TextEditingController nameController; - - @override - Widget build(BuildContext context) { - return MenuAnchor( - builder: defaultMenuButton( - tooltip: AppLocalizations.of(context).actions, - ), - menuChildren: [ - if (remote is RemoteStorage) - StreamBuilder>( - stream: syncService.getSync(remote!.identifier)?.filesStream, - builder: (context, snapshot) { - final currentStatus = snapshot.data - ?.lastWhereOrNull((element) => - entity.location.path.startsWith(element.location.path)) - ?.status; - return MenuItemButton( - leadingIcon: PhosphorIcon(currentStatus.getIcon(), - color: - currentStatus.getColor(Theme.of(context).colorScheme)), - child: Text(currentStatus.getLocalizedName(context)), - onPressed: () { - syncService.getSync(remote!.identifier)?.sync(); - }, - ); - }, - ), - BlocBuilder( - builder: (context, state) { - final starred = state.isStarred(entity.location); - return MenuItemButton( - onPressed: () { - settingsCubit.toggleStarred(entity.location); - }, - leadingIcon: starred - ? const PhosphorIcon(PhosphorIconsFill.star) - : const PhosphorIcon(PhosphorIconsLight.star), - child: Text(starred - ? AppLocalizations.of(context).unstar - : AppLocalizations.of(context).star), - ); - }), - if (onSelect != null) - MenuItemButton( - onPressed: onSelect, - leadingIcon: const PhosphorIcon(PhosphorIconsLight.check), - child: Text(AppLocalizations.of(context).select), - ), - MenuItemButton( - onPressed: () => showDialog( - context: context, - builder: (context) => FileSystemAssetMoveDialog( - assets: [entity.location], - fileSystem: documentSystem, + ], ), - ).then((value) { - if (value != null) onReload(); - }), - leadingIcon: const PhosphorIcon(PhosphorIconsLight.arrowsDownUp), - child: Text(AppLocalizations.of(context).move), - ), - if (!editable) - MenuItemButton( - onPressed: () { - onEdit(true); - nameController.text = entity.fileName; - }, - leadingIcon: const PhosphorIcon(PhosphorIconsLight.pencil), - child: Text(AppLocalizations.of(context).rename), - ), - if (entity is FileSystemFile) - MenuItemButton( - onPressed: () { - try { - final data = (entity as FileSystemFile).data; - exportData(context, data?.exportAsBytes() ?? []); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - AppLocalizations.of(context).error, - ), - ), - ); - } - }, - leadingIcon: const PhosphorIcon(PhosphorIconsLight.paperPlaneRight), - child: Text(AppLocalizations.of(context).export), ), - MenuItemButton( - onPressed: onDelete, - leadingIcon: const PhosphorIcon(PhosphorIconsLight.trash), - child: Text(AppLocalizations.of(context).delete), - ), - ], - ); + )); } } diff --git a/app/lib/views/files/list.dart b/app/lib/views/files/list.dart index d725916341a3..f2c2ee8bf645 100644 --- a/app/lib/views/files/list.dart +++ b/app/lib/views/files/list.dart @@ -5,7 +5,6 @@ import 'package:butterfly/api/save.dart'; import 'package:butterfly/cubits/settings.dart'; import 'package:butterfly/dialogs/file_system/move.dart'; import 'package:butterfly/services/sync.dart'; -import 'package:butterfly/views/files/grid.dart'; import 'package:butterfly/visualizer/connection.dart'; import 'package:butterfly_api/butterfly_api.dart'; import 'package:collection/collection.dart'; @@ -26,6 +25,7 @@ class FileEntityListTile extends StatelessWidget { final Uint8List? thumbnail; final FileSystemEntity entity; final TextEditingController nameController; + final Widget actionButton; const FileEntityListTile({ super.key, @@ -44,6 +44,7 @@ class FileEntityListTile extends StatelessWidget { this.thumbnail, required this.entity, required this.nameController, + required this.actionButton, }); @override @@ -301,21 +302,6 @@ class FileEntityListTile extends StatelessWidget { ), ], ); - final actionMenu = FilesActionMenu( - remote: remote, - syncService: syncService, - entity: entity, - settingsCubit: fileSystem.settingsCubit, - editable: editable, - onEdit: onEdit, - nameController: nameController, - onDelete: onDelete, - documentSystem: documentSystem, - onReload: onReload, - onSelect: selected == null - ? () => onSelectedChanged(true) - : null, - ); final selectionCheckbox = Checkbox( value: selected ?? false, onChanged: (value) => onSelectedChanged(value ?? false), @@ -343,7 +329,7 @@ class FileEntityListTile extends StatelessWidget { const SizedBox(width: 32), actions, ] else if (collapsed) - actionMenu, + actionButton, ], ); } else if (isTablet) { @@ -367,7 +353,7 @@ class FileEntityListTile extends StatelessWidget { const SizedBox(width: 8), edit, ])), - if (!collapsed) actions else actionMenu, + if (!collapsed) actions else actionButton, ], ); } else { @@ -387,7 +373,7 @@ class FileEntityListTile extends StatelessWidget { ], ), ), - actionMenu, + actionButton, ], ); } diff --git a/app/lib/views/files/view.dart b/app/lib/views/files/view.dart index f63e6a8257c6..543d33b93160 100644 --- a/app/lib/views/files/view.dart +++ b/app/lib/views/files/view.dart @@ -212,6 +212,7 @@ class FilesViewState extends State { onSelected: (value) => _setRemote( value == null ? null : state.getRemote(value)), ), + const SizedBox(width: 2), state.connections.any((e) => e is RemoteStorage) ? const SyncButton() : const SizedBox.shrink(), diff --git a/app/lib/widgets/editable_list_tile.dart b/app/lib/widgets/editable_list_tile.dart index d77e4eb67243..87261b31ad12 100644 --- a/app/lib/widgets/editable_list_tile.dart +++ b/app/lib/widgets/editable_list_tile.dart @@ -64,23 +64,40 @@ class _EditableListTileState extends State { }); } - @override - Widget build(BuildContext context) { - void onSaved([String? value]) { - widget.onSaved?.call(value ?? _controller.text); + void _onSaved([String? value]) { + widget.onSaved?.call(value ?? _controller.text); + setState(() { + _isEditing = false; + }); + } + + void _edit() { + if (widget.onSaved != null) { setState(() { - _isEditing = false; + _isEditing = true; }); } + } - void edit() { - if (widget.onSaved != null) { - setState(() { - _isEditing = true; - }); - } + @override + Widget build(BuildContext context) { + if (widget.actions != null) { + return ContextRegion( + builder: (context, button, controller) => _buildWidget(context, button), + menuChildren: [ + MenuItemButton( + leadingIcon: const PhosphorIcon(PhosphorIconsLight.textT), + onPressed: _edit, + child: Text(AppLocalizations.of(context).rename), + ), + ...widget.actions!, + ], + ); } + return _buildWidget(context, null); + } + Widget _buildWidget(BuildContext context, Widget? actionButton) { return ListTile( onTap: widget.onTap, selected: widget.selected, @@ -95,10 +112,10 @@ class _EditableListTileState extends State { builder: (context) => TextFormField( controller: _controller, onChanged: widget.onChanged, - onSaved: onSaved, + onSaved: _onSaved, autofocus: true, - onFieldSubmitted: onSaved, - onTapOutside: (_) => onSaved(), + onFieldSubmitted: _onSaved, + onTapOutside: (_) => _onSaved(), style: DefaultTextStyle.of(context).style, decoration: InputDecoration( filled: true, @@ -111,7 +128,7 @@ class _EditableListTileState extends State { ), )) : GestureDetector( - onDoubleTap: edit, + onDoubleTap: _edit, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8, @@ -128,27 +145,14 @@ class _EditableListTileState extends State { ), ), ), - trailing: widget.actions != null - ? MenuAnchor( - builder: defaultMenuButton( - tooltip: AppLocalizations.of(context).actions, - ), - menuChildren: [ - MenuItemButton( - leadingIcon: const PhosphorIcon(PhosphorIconsLight.textT), - onPressed: edit, - child: Text(AppLocalizations.of(context).rename), - ), - ...widget.actions!, - ], - ) - : widget.onSaved == null + trailing: actionButton ?? + (widget.onSaved == null ? null : IconButton( icon: const PhosphorIcon(PhosphorIconsLight.pencil), tooltip: AppLocalizations.of(context).rename, - onPressed: edit, - ), + onPressed: _edit, + )), ); } } diff --git a/app/pubspec.lock b/app/pubspec.lock index 791cccbc00d9..afa1c65c3351 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -881,8 +881,8 @@ packages: dependency: "direct main" description: path: "packages/material_leap" - ref: "1771b732aaad508053729cd2a14e10a4e426e397" - resolved-ref: "1771b732aaad508053729cd2a14e10a4e426e397" + ref: "18cd4cd6cb9e8663036eb59bb431f8ac65827788" + resolved-ref: "18cd4cd6cb9e8663036eb59bb431f8ac65827788" url: "https://github.com/LinwoodDev/dart_pkgs.git" source: git version: "0.0.1" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 0166dccea46f..a72c69855ab1 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -69,7 +69,7 @@ dependencies: material_leap: git: url: https://github.com/LinwoodDev/dart_pkgs.git - ref: 1771b732aaad508053729cd2a14e10a4e426e397 + ref: 18cd4cd6cb9e8663036eb59bb431f8ac65827788 path: packages/material_leap lw_sysapi: git: diff --git a/metadata/en-US/changelogs/120.txt b/metadata/en-US/changelogs/120.txt index f97bd2c8d083..bb3cc19e7233 100644 --- a/metadata/en-US/changelogs/120.txt +++ b/metadata/en-US/changelogs/120.txt @@ -1,4 +1,5 @@ * Add layer merging +* Add option to open context menus with right click on desktop or long press on mobile * Remove asking for name on layer creation * Fix layer rendering * Fix pin icon in property view