♻️ Fixes and optimizations in file list

This commit is contained in:
2025-11-15 16:20:05 +08:00
parent 5cf40e27de
commit 5f2f083d72
2 changed files with 88 additions and 171 deletions

View File

@@ -6,18 +6,23 @@ import '../models/file.dart';
import '../widgets/content/cloud_files.dart'; import '../widgets/content/cloud_files.dart';
/// Returns an appropriate icon widget for the given file based on its MIME type /// Returns an appropriate icon widget for the given file based on its MIME type
Widget getFileIcon(SnCloudFile file, {required double size}) { Widget getFileIcon(
SnCloudFile file, {
required double size,
bool tinyPreview = true,
}) {
final itemType = file.mimeType?.split('/').firstOrNull; final itemType = file.mimeType?.split('/').firstOrNull;
final mimeType = file.mimeType ?? ''; final mimeType = file.mimeType ?? '';
final extension = file.name.split('.').lastOrNull?.toLowerCase() ?? ''; final extension = file.name.split('.').lastOrNull?.toLowerCase() ?? '';
// For images, show the actual image thumbnail // For images, show the actual image thumbnail
if (itemType == 'image') { if (itemType == 'image' && tinyPreview) {
return CloudImageWidget(file: file); return CloudImageWidget(file: file);
} }
// Return icon based on MIME type or file extension // Return icon based on MIME type or file extension
final icon = switch ((itemType, mimeType, extension)) { final icon = switch ((itemType, mimeType, extension)) {
('image', _, _) => Symbols.image,
('audio', _, _) => Symbols.audio_file, ('audio', _, _) => Symbols.audio_file,
('video', _, _) => Symbols.video_file, ('video', _, _) => Symbols.video_file,
('application', 'application/pdf', _) => Symbols.picture_as_pdf, ('application', 'application/pdf', _) => Symbols.picture_as_pdf,

View File

@@ -176,18 +176,7 @@ class FileListView extends HookConsumerWidget {
ValueNotifier<String> currentPath, ValueNotifier<String> currentPath,
ValueNotifier<FileListViewMode> currentViewMode, ValueNotifier<FileListViewMode> currentViewMode,
) { ) {
// Check if all files are images return switch (currentViewMode.value) {
final fileItems = items.whereType<FileItem>();
final allFilesAreImages =
fileItems.isNotEmpty &&
fileItems.every(
(fileItem) =>
fileItem.fileIndex.file.mimeType?.startsWith('image/') == true,
);
return switch (allFilesAreImages
? FileListViewMode.waterfall
: currentViewMode.value) {
// Waterfall mode // Waterfall mode
FileListViewMode.waterfall => SliverMasonryGrid( FileListViewMode.waterfall => SliverMasonryGrid(
gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent( gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent(
@@ -539,13 +528,56 @@ class FileListView extends HookConsumerWidget {
WidgetRef ref, WidgetRef ref,
BuildContext context, BuildContext context,
) { ) {
final file = fileItem.fileIndex.file; return _buildWaterfallFileTileBase(
fileItem.fileIndex.file,
() => '/files/${fileItem.fileIndex.id}',
ref,
context,
[
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () async {
final confirmed = await showConfirmAlert(
'confirmDeleteFile'.tr(),
'deleteFile'.tr(),
);
if (!confirmed) return;
if (context.mounted) {
showLoadingModal(context);
}
try {
final client = ref.read(apiClientProvider);
await client.delete(
'/drive/index/remove/${fileItem.fileIndex.id}',
);
ref.invalidate(cloudFileListNotifierProvider);
} catch (e) {
showSnackBar('failedToDeleteFile'.tr());
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
),
],
);
}
Widget _buildWaterfallFileTileBase(
SnCloudFile file,
String Function() getRoutePath,
WidgetRef ref,
BuildContext context,
List<Widget>? actions,
) {
final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {}; final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {};
final ratio = final ratio =
meta['ratio'] is num ? (meta['ratio'] as num).toDouble() : 1.0; meta['ratio'] is num ? (meta['ratio'] as num).toDouble() : 1.0;
final itemType = file.mimeType?.split('/').first; final itemType = file.mimeType?.split('/').first;
final uri = final uri =
'${ref.read(apiClientProvider).options.baseUrl}/drive/files/${fileItem.fileIndex.id}'; '${ref.read(apiClientProvider).options.baseUrl}/drive/files/${file.id}';
Widget previewWidget; Widget previewWidget;
switch (itemType) { switch (itemType) {
@@ -602,7 +634,7 @@ class FileListView extends HookConsumerWidget {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
onTap: () { onTap: () {
context.push('/files/${fileItem.fileIndex.id}', extra: file); context.push(getRoutePath(), extra: file);
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -628,7 +660,7 @@ class FileListView extends HookConsumerWidget {
), ),
Row( Row(
children: [ children: [
getFileIcon(file, size: 24), getFileIcon(file, size: 24, tinyPreview: false),
const Gap(16), const Gap(16),
Expanded( Expanded(
child: Column( child: Column(
@@ -649,33 +681,7 @@ class FileListView extends HookConsumerWidget {
], ],
), ),
), ),
IconButton( if (actions != null) ...actions,
icon: const Icon(Symbols.delete),
onPressed: () async {
final confirmed = await showConfirmAlert(
'confirmDeleteFile'.tr(),
'deleteFile'.tr(),
);
if (!confirmed) return;
if (context.mounted) {
showLoadingModal(context);
}
try {
final client = ref.read(apiClientProvider);
await client.delete(
'/drive/index/remove/${fileItem.fileIndex.id}',
);
ref.invalidate(cloudFileListNotifierProvider);
} catch (e) {
showSnackBar('failedToDeleteFile'.tr());
} finally {
if (context.mounted) {
hideLoadingModal(context);
}
}
},
),
], ],
).padding(horizontal: 16, vertical: 4), ).padding(horizontal: 16, vertical: 4),
], ],
@@ -740,18 +746,7 @@ class FileListView extends HookConsumerWidget {
BuildContext context, BuildContext context,
ValueNotifier<FileListViewMode> currentViewMode, ValueNotifier<FileListViewMode> currentViewMode,
) { ) {
// Check if all unindexed files are images return switch (currentViewMode.value) {
final unindexedFiles = items.whereType<UnindexedFileItem>();
final allFilesAreImages =
unindexedFiles.isNotEmpty &&
unindexedFiles.every(
(unindexedFileItem) =>
unindexedFileItem.file.mimeType?.startsWith('image/') == true,
);
return switch (allFilesAreImages
? FileListViewMode.waterfall
: currentViewMode.value) {
// Waterfall mode // Waterfall mode
FileListViewMode.waterfall => SliverMasonryGrid( FileListViewMode.waterfall => SliverMasonryGrid(
gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent( gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent(
@@ -822,95 +817,14 @@ class FileListView extends HookConsumerWidget {
WidgetRef ref, WidgetRef ref,
BuildContext context, BuildContext context,
) { ) {
final file = unindexedFileItem.file; return _buildWaterfallFileTileBase(
final meta = file.fileMeta is Map ? (file.fileMeta as Map) : const {}; unindexedFileItem.file,
final ratio = () => '/files/${unindexedFileItem.file.id}',
meta['ratio'] is num ? (meta['ratio'] as num).toDouble() : 1.0; ref,
final itemType = file.mimeType?.split('/').first; context,
final tileRatio = itemType == 'image' ? ratio : 1.0; [
final uri = IconButton(
'${ref.read(apiClientProvider).options.baseUrl}/drive/files/${file.id}'; icon: const Icon(Symbols.delete),
Widget previewWidget;
switch (itemType) {
case 'image':
previewWidget = CloudImageWidget(
file: file,
aspectRatio: ratio,
fit: BoxFit.cover,
);
break;
case 'video':
previewWidget = CloudVideoWidget(item: file);
break;
case 'audio':
previewWidget = getFileIcon(file, size: 48);
break;
case 'text':
previewWidget = FutureBuilder<String>(
future: ref
.read(apiClientProvider)
.get(uri)
.then((response) => response.data as String),
builder:
(context, snapshot) =>
snapshot.hasData
? SingleChildScrollView(
child: Text(
snapshot.data!,
style: const TextStyle(
fontSize: 8,
fontFamily: 'monospace',
),
maxLines: 20,
overflow: TextOverflow.ellipsis,
),
)
: const Center(child: CircularProgressIndicator()),
);
break;
case 'application' when file.mimeType == 'application/pdf':
previewWidget = SfPdfViewer.network(
uri,
canShowScrollStatus: false,
canShowScrollHead: false,
enableDoubleTapZooming: false,
pageSpacing: 0,
);
break;
default:
previewWidget = getFileIcon(file, size: 48);
break;
}
return InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {
context.push('/files/${file.id}', extra: file);
},
child: Stack(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
),
),
child: AspectRatio(
aspectRatio: tileRatio,
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Container(color: Colors.white, child: previewWidget),
),
),
),
Positioned(
top: 6,
right: 6,
child: IconButton(
icon: const Icon(Symbols.delete, color: Colors.white),
onPressed: () async { onPressed: () async {
final confirmed = await showConfirmAlert( final confirmed = await showConfirmAlert(
'confirmDeleteFile'.tr(), 'confirmDeleteFile'.tr(),
@@ -923,7 +837,7 @@ class FileListView extends HookConsumerWidget {
} }
try { try {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
await client.delete('/drive/files/${file.id}'); await client.delete('/drive/files/${unindexedFileItem.file.id}');
ref.invalidate(unindexedFileListNotifierProvider); ref.invalidate(unindexedFileListNotifierProvider);
} catch (e) { } catch (e) {
showSnackBar('failedToDeleteFile'.tr()); showSnackBar('failedToDeleteFile'.tr());
@@ -934,9 +848,7 @@ class FileListView extends HookConsumerWidget {
} }
}, },
), ),
),
], ],
),
); );
} }