diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index a7f5d061b..7c24ed1e8 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -973,12 +973,14 @@ "createFolder": "フォルダを作成", "folderName": "フォルダ名", "fileDownloaded": "ファイルを保存しました", + "moved": "移動しました", "confirmDeleteFile": "このファイルを削除しますか?", "deleted": "削除しました", "editFile": "ファイルを編集", "editImage": "画像を編集", "createNoteFromThisFile": "このファイルからノートを作成", "download": "ダウンロード", + "move": "移動", "changeFolderName": "フォルダ名を変更", "confirmDeleteFolder": "このフォルダを削除しますか?", "noFiles": "ファイルがありません", diff --git a/lib/router/app_router.gr.dart b/lib/router/app_router.gr.dart index d481e002f..42f458bee 100644 --- a/lib/router/app_router.gr.dart +++ b/lib/router/app_router.gr.dart @@ -449,6 +449,8 @@ abstract class _$AppRouter extends RootStackRouter { child: DrivePage( key: args.key, account: args.account, + title: args.title, + floatingActionButtonBuilder: args.floatingActionButtonBuilder, ), ); }, @@ -2050,12 +2052,16 @@ class DriveRoute extends PageRouteInfo { DriveRoute({ Key? key, required Account account, + Widget? title, + Widget Function(BuildContext)? floatingActionButtonBuilder, List? children, }) : super( DriveRoute.name, args: DriveRouteArgs( key: key, account: account, + title: title, + floatingActionButtonBuilder: floatingActionButtonBuilder, ), initialChildren: children, ); @@ -2069,15 +2075,21 @@ class DriveRouteArgs { const DriveRouteArgs({ this.key, required this.account, + this.title, + this.floatingActionButtonBuilder, }); final Key? key; final Account account; + final Widget? title; + + final Widget Function(BuildContext)? floatingActionButtonBuilder; + @override String toString() { - return 'DriveRouteArgs{key: $key, account: $account}'; + return 'DriveRouteArgs{key: $key, account: $account, title: $title, floatingActionButtonBuilder: $floatingActionButtonBuilder}'; } } diff --git a/lib/state_notifier/drive_page/drive_files_notifier.dart b/lib/state_notifier/drive_page/drive_files_notifier.dart index 4380815e6..683bc589c 100644 --- a/lib/state_notifier/drive_page/drive_files_notifier.dart +++ b/lib/state_notifier/drive_page/drive_files_notifier.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:file/file.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:miria/model/pagination_state.dart'; +import 'package:miria/providers.dart'; import 'package:misskey_dart/misskey_dart.dart'; class DriveFilesNotifier extends AutoDisposeFamilyAsyncNotifier< @@ -120,4 +121,33 @@ class DriveFilesNotifier extends AutoDisposeFamilyAsyncNotifier< items: _state.map((file) => file.id == fileId ? response : file).toList(), ); } + + Future move({ + required String fileId, + required String? folderId, + }) async { + if (folderId == _folderId) { + return; + } + // folderIdがnullのときキーが削除されるのを回避 + final response = await _misskey.apiService.post>( + "drive/files/update", + { + "fileId": fileId, + "folderId": folderId, + }, + excludeRemoveNullPredicate: (key, _) => key == "folderId", + ); + final file = DriveFile.fromJson(response); + _state = _state.copyWith( + items: _state.where((file) => file.id != fileId).toList(), + ); + ref + .read(driveFilesNotifierProvider((_misskey, folderId)).notifier) + .add(file); + } + + void add(DriveFile file) { + _state = _state.copyWith(items: [file, ..._state]); + } } diff --git a/lib/state_notifier/drive_page/drive_folders_notifier.dart b/lib/state_notifier/drive_page/drive_folders_notifier.dart index 7bfae5fda..5b2a981b2 100644 --- a/lib/state_notifier/drive_page/drive_folders_notifier.dart +++ b/lib/state_notifier/drive_page/drive_folders_notifier.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:miria/model/pagination_state.dart'; +import 'package:miria/providers.dart'; import 'package:misskey_dart/misskey_dart.dart'; class DriveFoldersNotifier extends AutoDisposeFamilyAsyncNotifier< @@ -95,4 +96,33 @@ class DriveFoldersNotifier extends AutoDisposeFamilyAsyncNotifier< .toList(), ); } + + Future move({ + required String folderId, + required String? parentId, + }) async { + if (parentId == _folderId) { + return; + } + // parentIdがnullのときキーが削除されるのを回避 + final response = await _misskey.apiService.post>( + "drive/folders/update", + { + "folderId": folderId, + "parentId": parentId, + }, + excludeRemoveNullPredicate: (key, _) => key == "parentId", + ); + final folder = DriveFolder.fromJson(response); + _state = _state.copyWith( + items: _state.where((file) => file.id != folderId).toList(), + ); + ref + .read(driveFoldersNotifierProvider((_misskey, parentId)).notifier) + .add(folder); + } + + void add(DriveFolder folder) { + _state = _state.copyWith(items: [folder, ..._state]); + } } diff --git a/lib/view/drive_page/drive_file_modal_sheet.dart b/lib/view/drive_page/drive_file_modal_sheet.dart index a8af08854..672008c60 100644 --- a/lib/view/drive_page/drive_file_modal_sheet.dart +++ b/lib/view/drive_page/drive_file_modal_sheet.dart @@ -13,6 +13,7 @@ import 'package:miria/providers.dart'; import 'package:miria/router/app_router.dart'; import 'package:miria/view/common/error_dialog_handler.dart'; import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; +import 'package:miria/view/drive_page/drive_folder_select_dialog.dart'; import 'package:miria/view/note_create_page/file_settings_dialog.dart'; import 'package:miria/view/note_create_page/thumbnail.dart'; import 'package:misskey_dart/misskey_dart.dart'; @@ -100,6 +101,27 @@ class DriveFileModalSheet extends ConsumerWidget { Navigator.of(context).pop(); } + Future move(WidgetRef ref) async { + final context = ref.context; + final misskey = ref.read(misskeyProvider(account)); + final result = await showDialog<(DriveFolder?,)>( + context: context, + builder: (context) => DriveFolderSelectDialog(account: account), + ); + if (result == null) return; + await ref + .read(driveFilesNotifierProvider((misskey, file.folderId)).notifier) + .move( + fileId: file.id, + folderId: result.$1?.id, + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(S.of(context).moved)), + ); + Navigator.of(context).pop(); + } + Future delete(WidgetRef ref) async { final context = ref.context; final misskey = ref.read(misskeyProvider(account)); @@ -175,6 +197,11 @@ class DriveFileModalSheet extends ConsumerWidget { title: Text(S.of(context).download), onTap: () => download(ref).expectFailure(context), ), + ListTile( + leading: const Icon(Icons.drive_file_move), + title: Text(S.of(context).move), + onTap: () => move(ref).expectFailure(context), + ), ListTile( leading: const Icon(Icons.delete), title: Text(S.of(context).delete), diff --git a/lib/view/drive_page/drive_folder_modal_sheet.dart b/lib/view/drive_page/drive_folder_modal_sheet.dart index e7005c854..836fb7265 100644 --- a/lib/view/drive_page/drive_folder_modal_sheet.dart +++ b/lib/view/drive_page/drive_folder_modal_sheet.dart @@ -7,6 +7,7 @@ import 'package:miria/providers.dart'; import 'package:miria/view/common/error_dialog_handler.dart'; import 'package:miria/view/common/text_form_field_dialog.dart'; import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; +import 'package:miria/view/drive_page/drive_folder_select_dialog.dart'; import 'package:misskey_dart/misskey_dart.dart'; class DriveFolderModalSheet extends ConsumerWidget { @@ -41,6 +42,27 @@ class DriveFolderModalSheet extends ConsumerWidget { } } + Future move(WidgetRef ref) async { + final context = ref.context; + final misskey = ref.read(misskeyProvider(account)); + final result = await showDialog<(DriveFolder?,)>( + context: context, + builder: (context) => DriveFolderSelectDialog(account: account), + ); + if (result == null) return; + await ref + .read(driveFoldersNotifierProvider((misskey, folder.parentId)).notifier) + .move( + folderId: folder.id, + parentId: result.$1?.id, + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(S.of(context).moved)), + ); + Navigator.of(context).pop(); + } + Future delete(WidgetRef ref) async { final context = ref.context; final misskey = ref.read(misskeyProvider(account)); @@ -82,6 +104,11 @@ class DriveFolderModalSheet extends ConsumerWidget { title: Text(S.of(context).changeFolderName), onTap: () => changeName(ref).expectFailure(context), ), + ListTile( + leading: const Icon(Icons.drive_file_move), + title: Text(S.of(context).move), + onTap: () => move(ref).expectFailure(context), + ), ListTile( leading: const Icon(Icons.delete), title: Text(S.of(context).delete), diff --git a/lib/view/drive_page/drive_folder_select_dialog.dart b/lib/view/drive_page/drive_folder_select_dialog.dart new file mode 100644 index 000000000..261b3111d --- /dev/null +++ b/lib/view/drive_page/drive_folder_select_dialog.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:miria/model/account.dart'; +import 'package:miria/providers.dart'; +import 'package:miria/state_notifier/drive_page/drive_page_notifier.dart'; +import 'package:miria/view/drive_page/drive_page.dart'; + +class DriveFolderSelectDialog extends StatelessWidget { + const DriveFolderSelectDialog({ + super.key, + required this.account, + }); + + final Account account; + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + drivePageNotifierProvider.overrideWith(DrivePageNotifier.new), + ], + child: Dialog( + child: DrivePage( + account: account, + title: Text(S.of(context).selectFolder), + floatingActionButtonBuilder: (context) => + const DriveFolderSelectDialogFloatingActionButton(), + ), + ), + ); + } +} + +class DriveFolderSelectDialogFloatingActionButton extends ConsumerWidget { + const DriveFolderSelectDialogFloatingActionButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FloatingActionButton.extended( + onPressed: () { + final folder = + ref.read(drivePageNotifierProvider).breadcrumbs.lastOrNull; + Navigator.of(context).pop((folder,)); + }, + label: Text(S.of(context).select), + icon: const Icon(Icons.check), + ); + } +} diff --git a/lib/view/drive_page/drive_page.dart b/lib/view/drive_page/drive_page.dart index 5528a1fe6..7ea98a8d7 100644 --- a/lib/view/drive_page/drive_page.dart +++ b/lib/view/drive_page/drive_page.dart @@ -18,9 +18,16 @@ import 'package:miria/view/drive_page/drive_folder_modal_sheet.dart'; @RoutePage() class DrivePage extends ConsumerWidget { - const DrivePage({super.key, required this.account}); + const DrivePage({ + super.key, + required this.account, + this.title, + this.floatingActionButtonBuilder, + }); final Account account; + final Widget? title; + final Widget Function(BuildContext context)? floatingActionButtonBuilder; static const itemMaxCrossAxisExtent = 400.0; @@ -57,8 +64,19 @@ class DrivePage extends ConsumerWidget { }, child: Scaffold( appBar: AppBar( - title: Text(S.of(context).drive), + title: title ?? Text(S.of(context).drive), actions: [ + if (floatingActionButtonBuilder != null) + IconButton( + onPressed: () => showModalBottomSheet( + context: context, + builder: (context) => DriveCreateModalSheet( + account: account, + folder: currentFolder, + ), + ), + icon: const Icon(Icons.add), + ), if (currentFolder != null) IconButton( onPressed: () async { @@ -249,16 +267,17 @@ class DrivePage extends ConsumerWidget { ], ), ), - floatingActionButton: FloatingActionButton( - onPressed: () => showModalBottomSheet( - context: context, - builder: (context) => DriveCreateModalSheet( - account: account, - folder: currentFolder, + floatingActionButton: floatingActionButtonBuilder?.call(context) ?? + FloatingActionButton( + onPressed: () => showModalBottomSheet( + context: context, + builder: (context) => DriveCreateModalSheet( + account: account, + folder: currentFolder, + ), + ), + child: const Icon(Icons.add), ), - ), - child: const Icon(Icons.add), - ), ), ); }