💄 Localizable file list

This commit is contained in:
2026-01-17 22:31:51 +08:00
parent 41df5f3907
commit 3d7e7951a2
3 changed files with 125 additions and 78 deletions

View File

@@ -1360,6 +1360,32 @@
"orCreateWith": "Or\ncreate with", "orCreateWith": "Or\ncreate with",
"unindexedFiles": "Unindexed files", "unindexedFiles": "Unindexed files",
"folder": "Folder", "folder": "Folder",
"rootDirectory": "Root Directory",
"pathSeparator": " / ",
"selectAll": "Select All",
"deselectAll": "Deselect All",
"createDirectory": "Create Directory",
"thisDirectoryIsEmpty": "This directory is empty",
"emptyDirectoryHint": "Upload files or create subdirectories to populate this path.\nDirectories are created implicitly when you upload files to them.",
"noUnindexedFiles": "No unindexed files",
"noUnindexedFilesHint": "All files have been assigned to paths.\nFiles without paths will appear here.",
"clearAllRecycledFiles": "Clear All Recycled Files",
"clearRecycledFilesDescription": "Permanently delete all marked recycled files to free up space.",
"allFiles": "All files",
"confirmDeleteSelectedFiles": "Are you sure you want to delete the selected files?",
"deleteSelectedFiles": "Delete Selected Files",
"confirmClearRecycledFiles": "Are you sure you want to clear all recycled files?",
"clearRecycledFiles": "Clear Recycled Files",
"failedToUploadFile": "Failed to upload file: {}",
"deletedFilesCount": "Deleted {} files.",
"failedToDeleteSelectedFiles": "Failed to delete selected files.",
"clearedRecycledFilesCount": "Cleared {} recycled files.",
"failedToClearRecycledFiles": "Failed to clear recycled files.",
"root": "Root",
"searchFiles": "Search files...",
"selectedCount": "{} selected",
"filesSelected": "{} files selected",
"fileSelected": "{} file selected",
"clearCompleted": "Clear Completed", "clearCompleted": "Clear Completed",
"uploadSuccess": "Upload successful!", "uploadSuccess": "Upload successful!",
"wouldYouLikeToViewFile": "Would you like to view the file?", "wouldYouLikeToViewFile": "Would you like to view the file?",

View File

@@ -1,4 +1,5 @@
import 'package:cross_file/cross_file.dart'; import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@@ -41,7 +42,7 @@ class FileListScreen extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
title: SearchBar( title: SearchBar(
constraints: const BoxConstraints(maxWidth: 400, minHeight: 32), constraints: const BoxConstraints(maxWidth: 400, minHeight: 32),
hintText: 'Search files...', hintText: 'searchFiles'.tr(),
hintStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)), hintStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)),
textStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)), textStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)),
onChanged: (value) { onChanged: (value) {
@@ -105,26 +106,26 @@ class FileListScreen extends HookConsumerWidget {
) )
: null, : null,
body: usageAsync.when( body: usageAsync.when(
data: (usage) => quotaAsync.when( data: (usage) => quotaAsync.when(
data: (quota) => FileListView( data: (quota) => FileListView(
usage: usage, usage: usage,
quota: quota, quota: quota,
currentPath: currentPath, currentPath: currentPath,
selectedPool: selectedPool, selectedPool: selectedPool,
onPickAndUpload: () => _pickAndUploadFile( onPickAndUpload: () => _pickAndUploadFile(
ref, ref,
currentPath.value, currentPath.value,
selectedPool.value?.id, selectedPool.value?.id,
),
onShowCreateDirectory: _showCreateDirectoryDialog,
mode: mode,
viewMode: viewMode,
isSelectionMode: isSelectionMode,
query: query,
), ),
loading: () => const Center(child: CircularProgressIndicator()), onShowCreateDirectory: _showCreateDirectoryDialog,
error: (e, _) => Center(child: Text('Error loading quota')), mode: mode,
viewMode: viewMode,
isSelectionMode: isSelectionMode,
query: query,
), ),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error loading quota')),
),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error loading usage')), error: (e, _) => Center(child: Text('Error loading usage')),
), ),

View File

@@ -175,7 +175,7 @@ class FileListView extends HookConsumerWidget {
children: [ children: [
Icon(Symbols.folder), Icon(Symbols.folder),
const Gap(12), const Gap(12),
Text('Root Directory'), Text('rootDirectory').tr(),
], ],
), ),
), ),
@@ -185,7 +185,7 @@ class FileListView extends HookConsumerWidget {
children: [ children: [
Icon(Symbols.inventory_2), Icon(Symbols.inventory_2),
const Gap(12), const Gap(12),
Text('Unindexed Files'), Text('unindexedFiles').tr(),
], ],
), ),
), ),
@@ -202,10 +202,10 @@ class FileListView extends HookConsumerWidget {
children: [ children: [
const Icon(Symbols.inventory_2, size: 20), const Icon(Symbols.inventory_2, size: 20),
const Gap(8), const Gap(8),
const Text( Text(
'Unindexed Files', 'unindexedFiles',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
), ).tr(),
], ],
), ),
); );
@@ -222,7 +222,7 @@ class FileListView extends HookConsumerWidget {
children: [ children: [
Icon(Symbols.inventory_2), Icon(Symbols.inventory_2),
const Gap(12), const Gap(12),
Text('Unindexed Files'), Text('unindexedFiles').tr(),
], ],
), ),
), ),
@@ -232,7 +232,7 @@ class FileListView extends HookConsumerWidget {
children: [ children: [
Icon(Symbols.folder), Icon(Symbols.folder),
const Gap(12), const Gap(12),
Text('Root Directory'), Text('rootDirectory').tr(),
], ],
), ),
), ),
@@ -248,10 +248,10 @@ class FileListView extends HookConsumerWidget {
children: [ children: [
const Icon(Symbols.folder, size: 20), const Icon(Symbols.folder, size: 20),
const Gap(8), const Gap(8),
const Text( Text(
'Root Directory', 'rootDirectory',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
), ).tr(),
], ],
), ),
); );
@@ -286,7 +286,7 @@ class FileListView extends HookConsumerWidget {
currentPathBuilder += '/${pathParts[i]}'; currentPathBuilder += '/${pathParts[i]}';
final path = currentPathBuilder; final path = currentPathBuilder;
breadcrumbs.add(const Text(' / ')); breadcrumbs.add(Text('pathSeparator').tr());
if (i == pathParts.length - 1) { if (i == pathParts.length - 1) {
// Current directory // Current directory
breadcrumbs.add( breadcrumbs.add(
@@ -346,7 +346,7 @@ class FileListView extends HookConsumerWidget {
} }
}) })
.catchError((error) { .catchError((error) {
showSnackBar('Failed to upload file: $error'); showSnackBar('failedToUploadFile'.tr(args: [error]));
}); });
} }
}, },
@@ -378,16 +378,16 @@ class FileListView extends HookConsumerWidget {
), ),
const Gap(12), const Gap(12),
SegmentedButton<FileListViewMode>( SegmentedButton<FileListViewMode>(
segments: const [ segments: [
ButtonSegment<FileListViewMode>( ButtonSegment<FileListViewMode>(
value: FileListViewMode.list, value: FileListViewMode.list,
icon: Icon(Symbols.list), icon: Icon(Symbols.list),
tooltip: 'List View', tooltip: 'listView'.tr(),
), ),
ButtonSegment<FileListViewMode>( ButtonSegment<FileListViewMode>(
value: FileListViewMode.waterfall, value: FileListViewMode.waterfall,
icon: Icon(Symbols.view_module), icon: Icon(Symbols.view_module),
tooltip: 'Waterfall View', tooltip: 'waterfallView'.tr(),
), ),
], ],
selected: {viewMode.value}, selected: {viewMode.value},
@@ -451,7 +451,7 @@ class FileListView extends HookConsumerWidget {
isSelectionMode.value = false; isSelectionMode.value = false;
selectedFileIds.value.clear(); selectedFileIds.value.clear();
}, },
child: const Text('Cancel'), child: Text('cancel').tr(),
), ),
const Gap(12), const Gap(12),
OutlinedButton( OutlinedButton(
@@ -478,7 +478,7 @@ class FileListView extends HookConsumerWidget {
}, },
child: Text( child: Text(
currentVisibleItems.value.isEmpty currentVisibleItems.value.isEmpty
? 'Select All' ? 'selectAll'.tr()
: currentVisibleItems.value : currentVisibleItems.value
.expand( .expand(
(item) => item.maybeMap( (item) => item.maybeMap(
@@ -490,21 +490,29 @@ class FileListView extends HookConsumerWidget {
.toSet() .toSet()
.difference(selectedFileIds.value) .difference(selectedFileIds.value)
.isEmpty .isEmpty
? 'Deselect All' ? 'deselectAll'.tr()
: 'Select All', : 'selectAll'.tr(),
), ),
), ),
const Spacer(), const Spacer(),
Text('${selectedFileIds.value.length} selected'), Text(
selectedFileIds.value.length == 1
? 'fileSelected'.tr(
args: [selectedFileIds.value.length.toString()],
)
: 'filesSelected'.tr(
args: [selectedFileIds.value.length.toString()],
),
),
const Spacer(), const Spacer(),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Symbols.delete), icon: const Icon(Symbols.delete),
label: const Text('Delete'), label: Text('delete').tr(),
onPressed: selectedFileIds.value.isNotEmpty onPressed: selectedFileIds.value.isNotEmpty
? () async { ? () async {
final confirmed = await showConfirmAlert( final confirmed = await showConfirmAlert(
'Are you sure you want to delete the selected files?', 'confirmDeleteSelectedFiles'.tr(),
'Delete Selected Files', 'deleteSelectedFiles'.tr(),
isDanger: true, isDanger: true,
); );
if (!confirmed) return; if (!confirmed) return;
@@ -528,10 +536,14 @@ class FileListView extends HookConsumerWidget {
? indexedCloudFileListProvider ? indexedCloudFileListProvider
: unindexedFileListProvider, : unindexedFileListProvider,
); );
showSnackBar('Deleted $count files.'); showSnackBar(
'deletedFilesCount'.tr(
args: [count.toString()],
),
);
} catch (e) { } catch (e) {
showSnackBar( showSnackBar(
'Failed to delete selected files.', 'failedToDeleteSelectedFiles'.tr(),
); );
} finally { } finally {
if (context.mounted) { if (context.mounted) {
@@ -645,7 +657,7 @@ class FileListView extends HookConsumerWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
subtitle: const Text('folder').tr(), subtitle: Text('folder').tr(),
onTap: () { onTap: () {
final newPath = currentPath.value == '/' final newPath = currentPath.value == '/'
? '/${folderItem.folderName}' ? '/${folderItem.folderName}'
@@ -679,24 +691,23 @@ class FileListView extends HookConsumerWidget {
const Icon(Symbols.folder_off, size: 64, color: Colors.grey), const Icon(Symbols.folder_off, size: 64, color: Colors.grey),
const Gap(16), const Gap(16),
Text( Text(
'This directory is empty', 'thisDirectoryIsEmpty',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(ref.context).textTheme.bodyLarge?.color, color: Theme.of(ref.context).textTheme.bodyLarge?.color,
), ),
), ).tr(),
const Gap(8), const Gap(8),
Text( Text(
'Upload files or create subdirectories to populate this path.\n' 'emptyDirectoryHint',
'Directories are created implicitly when you upload files to them.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Theme.of( color: Theme.of(
ref.context, ref.context,
).textTheme.bodyMedium?.color?.withOpacity(0.7), ).textTheme.bodyMedium?.color?.withOpacity(0.7),
), ),
), ).tr(),
const Gap(16), const Gap(16),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -706,14 +717,14 @@ class FileListView extends HookConsumerWidget {
ElevatedButton.icon( ElevatedButton.icon(
onPressed: onPickAndUpload, onPressed: onPickAndUpload,
icon: const Icon(Symbols.upload_file), icon: const Icon(Symbols.upload_file),
label: const Text('Upload Files'), label: Text('uploadFiles').tr(),
), ),
const Gap(12), const Gap(12),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => onPressed: () =>
onShowCreateDirectory(ref.context, currentPath), onShowCreateDirectory(ref.context, currentPath),
icon: const Icon(Symbols.create_new_folder), icon: const Icon(Symbols.create_new_folder),
label: const Text('Create Directory'), label: Text('createDirectory').tr(),
), ),
], ],
), ),
@@ -1255,24 +1266,23 @@ class FileListView extends HookConsumerWidget {
const Icon(Symbols.inventory_2, size: 64, color: Colors.grey), const Icon(Symbols.inventory_2, size: 64, color: Colors.grey),
const Gap(16), const Gap(16),
Text( Text(
'No unindexed files', 'thisDirectoryIsEmpty',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(ref.context).textTheme.bodyLarge?.color, color: Theme.of(ref.context).textTheme.bodyLarge?.color,
), ),
), ).tr(),
const Gap(8), const Gap(8),
Text( Text(
'All files have been assigned to paths.\n' 'emptyDirectoryHint',
'Files without paths will appear here.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Theme.of( color: Theme.of(
ref.context, ref.context,
).textTheme.bodyMedium?.color?.withOpacity(0.7), ).textTheme.bodyMedium?.color?.withOpacity(0.7),
), ),
), ).tr(),
], ],
), ),
), ),
@@ -1291,20 +1301,18 @@ class FileListView extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text('Clear All Recycled Files').bold(), Text('clearAllRecycledFiles').tr().bold(),
const Text( Text('clearRecycledFilesDescription').tr().fontSize(13),
'Permanently delete all marked recycled files to free up space.',
).fontSize(13),
], ],
), ),
), ),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Symbols.delete_forever), icon: const Icon(Symbols.delete_forever),
label: const Text('Clear'), label: Text('clear').tr(),
onPressed: () async { onPressed: () async {
final confirmed = await showConfirmAlert( final confirmed = await showConfirmAlert(
'Are you sure you want to clear all recycled files?', 'confirmClearRecycledFiles'.tr(),
'Clear Recycled Files', 'clearRecycledFiles'.tr(),
); );
if (!confirmed) return; if (!confirmed) return;
@@ -1317,10 +1325,12 @@ class FileListView extends HookConsumerWidget {
'/drive/files/me/recycle', '/drive/files/me/recycle',
); );
final count = response.data['count'] as int? ?? 0; final count = response.data['count'] as int? ?? 0;
showSnackBar('Cleared $count recycled files.'); showSnackBar(
'clearedRecycledFilesCount'.tr(args: [count.toString()]),
);
ref.invalidate(unindexedFileListProvider); ref.invalidate(unindexedFileListProvider);
} catch (e) { } catch (e) {
showSnackBar('Failed to clear recycled files.'); showSnackBar('failedToClearRecycledFiles'.tr());
} finally { } finally {
if (ref.context.mounted) { if (ref.context.mounted) {
hideLoadingModal(ref.context); hideLoadingModal(ref.context);
@@ -1359,7 +1369,9 @@ class FileListView extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Theme.of(ref.context).colorScheme.outline.withOpacity(0.5), color: Theme.of(
ref.context,
).colorScheme.outline.withOpacity(0.5),
), ),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
@@ -1367,14 +1379,14 @@ class FileListView extends HookConsumerWidget {
child: DropdownButton<SnFilePool>( child: DropdownButton<SnFilePool>(
value: selectedPool.value, value: selectedPool.value,
items: [ items: [
const DropdownMenuItem<SnFilePool>( DropdownMenuItem<SnFilePool>(
value: null, value: null,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Symbols.database, size: 16), Icon(Symbols.database, size: 16),
Gap(6), Gap(6),
Text('All files', style: TextStyle(fontSize: 12)), Text('allFiles', style: TextStyle(fontSize: 12)).tr(),
], ],
), ),
), ),
@@ -1422,7 +1434,9 @@ class FileListView extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Theme.of(ref.context).colorScheme.outline.withOpacity(0.5), color: Theme.of(
ref.context,
).colorScheme.outline.withOpacity(0.5),
), ),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
@@ -1437,10 +1451,12 @@ class FileListView extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Symbols.schedule, size: 16), Icon(Symbols.schedule, size: 16),
Text('Date', style: const TextStyle(fontSize: 12)), Text('date', style: const TextStyle(fontSize: 12)).tr(),
if (order.value == 'date') if (order.value == 'date')
Icon( Icon(
orderDesc.value ? Symbols.arrow_downward : Symbols.arrow_upward, orderDesc.value
? Symbols.arrow_downward
: Symbols.arrow_upward,
size: 14, size: 14,
), ),
], ],
@@ -1459,7 +1475,9 @@ class FileListView extends HookConsumerWidget {
), ),
if (order.value == 'size') if (order.value == 'size')
Icon( Icon(
orderDesc.value ? Symbols.arrow_downward : Symbols.arrow_upward, orderDesc.value
? Symbols.arrow_downward
: Symbols.arrow_upward,
size: 16, size: 16,
), ),
], ],
@@ -1478,7 +1496,9 @@ class FileListView extends HookConsumerWidget {
), ),
if (order.value == 'name') if (order.value == 'name')
Icon( Icon(
orderDesc.value ? Symbols.arrow_downward : Symbols.arrow_upward, orderDesc.value
? Symbols.arrow_downward
: Symbols.arrow_upward,
size: 16, size: 16,
), ),
], ],
@@ -1515,12 +1535,12 @@ class FileListView extends HookConsumerWidget {
// Refresh chip // Refresh chip
FilterChip( FilterChip(
label: const Row( label: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
spacing: 6, spacing: 6,
children: [ children: [
Icon(Symbols.refresh, size: 16), Icon(Symbols.refresh, size: 16),
Text('Refresh', style: TextStyle(fontSize: 12)), Text('refresh', style: TextStyle(fontSize: 12)).tr(),
], ],
), ),
selected: false, selected: false,