diff --git a/client/lib/libphylum/actions/deserializers.dart b/client/lib/libphylum/actions/deserializers.dart index fafc0e1d..13ca72d1 100644 --- a/client/lib/libphylum/actions/deserializers.dart +++ b/client/lib/libphylum/actions/deserializers.dart @@ -1,7 +1,9 @@ import 'package:phylum/libphylum/actions/resource_mkdir_action.dart'; +import 'package:phylum/libphylum/actions/resource_rename_action.dart'; import 'package:phylum/libphylum/actions/resource_upload_action.dart'; const actionDeserializers = { ResourceMkdirAction.actionName: ResourceMkdirAction.fromMap, ResourceUploadAction.actionName: ResourceUploadAction.fromMap, + ResourceRenameAction.actionName: ResourceRenameAction.fromMap, }; diff --git a/client/lib/libphylum/actions/resource_rename_action.dart b/client/lib/libphylum/actions/resource_rename_action.dart new file mode 100644 index 00000000..fc8e99d4 --- /dev/null +++ b/client/lib/libphylum/actions/resource_rename_action.dart @@ -0,0 +1,70 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:offtheline/offtheline.dart'; +import 'package:phylum/libphylum/db/db.dart'; +import 'package:phylum/libphylum/phylum_account.dart'; + +class ResourceRenameAction extends ApiAction with JsonApiAction { + static const actionName = 'resourceRename'; + @override + String get name => actionName; + + @override + String get method => 'POST'; + + @override + String get endpoint => '/v1/resources/rename/$id'; + + final String id; + final String newName; + final String oldName; + + ResourceRenameAction._({ + required this.id, + required this.newName, + required this.oldName, + }); + + ResourceRenameAction({ + required Resource r, + required String name, + }) : this._(id: r.id, newName: name, oldName: r.name); + + static ResourceRenameAction fromMap(Map map, dynamic data) { + return ResourceRenameAction._( + id: map['id'], + newName: map['newName'], + oldName: map['oldName'], + ); + } + + @override + String generateDescription(PhylumAccount account) { + return "Renaming $oldName to $newName"; + } + + @override + Map? generateRequestBody() { + return { + 'name': newName, + }; + } + + @override + FutureOr applyOptimisticUpdate(PhylumAccount account) { + account.datastore.db.managers.resources.update((o) => o(name: Value(newName))); + } + + @override + FutureOr revertOptimisticUpdate(PhylumAccount account) { + account.datastore.db.managers.resources.update((o) => o(name: Value(oldName))); + } + + @override + Map toMap() => { + 'id': id, + 'newName': newName, + 'oldName': oldName, + }; +} diff --git a/client/lib/ui/folder/folder_contents_view.dart b/client/lib/ui/folder/folder_contents_view.dart index 2f2c9064..37e154ba 100644 --- a/client/lib/ui/folder/folder_contents_view.dart +++ b/client/lib/ui/folder/folder_contents_view.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:phylum/libphylum/actions/resource_rename_action.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; import 'package:phylum/libphylum/requests/resource_detail_request.dart'; +import 'package:phylum/ui/folder/resource_options_dialog.dart'; +import 'package:phylum/util/dialogs.dart'; import 'package:provider/provider.dart'; import 'folder_navigator_stack.dart'; @@ -42,14 +45,31 @@ class _FolderContentsViewState extends State { ListTile( leading: r.getIcon(), title: Text(r.name), - trailing: r.dir ? null : Text(r.getSizeString()), - onTap: () => context.read().push(r.id), + subtitle: r.dir ? const Text('Folder') : Text(r.getSizeString()), + trailing: IconButton(icon: Icon(Icons.adaptive.more), onPressed: () => showResourceOptions(account, r)), + onTap: r.dir ? () => context.read().push(r.id) : null, + onLongPress: () => showResourceOptions(account, r), ), ], )), ), ); } + + void showResourceOptions(PhylumAccount account, Resource r) async { + final context = this.context; + final option = await showModalBottomSheet(context: context, builder: (context) => const ResourceOptionsList()); + if (!context.mounted) { + return; + } + switch (option) { + case ResourceOption.rename: + final name = await showInputDialog(context, title: 'Rename', preset: r.name); + if (name != null) { + account.api.sendRequest(ResourceRenameAction(r: r, name: name)); + } + } + } } extension ResourceViewExtensions on Resource { diff --git a/client/lib/ui/folder/resource_options_dialog.dart b/client/lib/ui/folder/resource_options_dialog.dart new file mode 100644 index 00000000..c82cafc5 --- /dev/null +++ b/client/lib/ui/folder/resource_options_dialog.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +enum ResourceOption { + rename(Icons.edit, 'Rename'), + delete(Icons.delete, 'Delete'); + + const ResourceOption(this.icon, this.text); + + final IconData icon; + final String text; +} + +class ResourceOptionsList extends StatelessWidget { + const ResourceOptionsList({super.key}); + + @override + Widget build(BuildContext context) { + return ListView( + shrinkWrap: true, + children: [ + for (final o in ResourceOption.values) + ListTile( + leading: Icon(o.icon), + title: Text(o.text), + onTap: () => Navigator.of(context).pop(o), + ), + ], + ); + } +}