From 74fa2215a69a23026db3b53ce7bb24d0fc25a384 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 15 Nov 2025 02:59:20 +0800 Subject: [PATCH] :sparkles: Unindexed files --- lib/models/file_list_item.dart | 2 + lib/models/file_list_item.freezed.dart | 105 ++++- lib/pods/file_list.dart | 41 ++ lib/pods/file_list.g.dart | 23 +- lib/screens/files/file_list.dart | 2 + lib/widgets/file_list_view.dart | 524 ++++++++++++++++++------- lib/widgets/upload_overlay.dart | 4 +- 7 files changed, 540 insertions(+), 161 deletions(-) diff --git a/lib/models/file_list_item.dart b/lib/models/file_list_item.dart index 9772c66e..b78ce947 100644 --- a/lib/models/file_list_item.dart +++ b/lib/models/file_list_item.dart @@ -8,4 +8,6 @@ part 'file_list_item.freezed.dart'; sealed class FileListItem with _$FileListItem { const factory FileListItem.file(SnCloudFileIndex fileIndex) = FileItem; const factory FileListItem.folder(SnCloudFolder folder) = FolderItem; + const factory FileListItem.unindexedFile(SnCloudFile file) = + UnindexedFileItem; } diff --git a/lib/models/file_list_item.freezed.dart b/lib/models/file_list_item.freezed.dart index 555bc4f5..1b9ddf3b 100644 --- a/lib/models/file_list_item.freezed.dart +++ b/lib/models/file_list_item.freezed.dart @@ -55,12 +55,13 @@ extension FileListItemPatterns on FileListItem { /// } /// ``` -@optionalTypeArgs TResult maybeMap({TResult Function( FileItem value)? file,TResult Function( FolderItem value)? folder,required TResult orElse(),}){ +@optionalTypeArgs TResult maybeMap({TResult Function( FileItem value)? file,TResult Function( FolderItem value)? folder,TResult Function( UnindexedFileItem value)? unindexedFile,required TResult orElse(),}){ final _that = this; switch (_that) { case FileItem() when file != null: return file(_that);case FolderItem() when folder != null: -return folder(_that);case _: +return folder(_that);case UnindexedFileItem() when unindexedFile != null: +return unindexedFile(_that);case _: return orElse(); } @@ -78,12 +79,13 @@ return folder(_that);case _: /// } /// ``` -@optionalTypeArgs TResult map({required TResult Function( FileItem value) file,required TResult Function( FolderItem value) folder,}){ +@optionalTypeArgs TResult map({required TResult Function( FileItem value) file,required TResult Function( FolderItem value) folder,required TResult Function( UnindexedFileItem value) unindexedFile,}){ final _that = this; switch (_that) { case FileItem(): return file(_that);case FolderItem(): -return folder(_that);} +return folder(_that);case UnindexedFileItem(): +return unindexedFile(_that);} } /// A variant of `map` that fallback to returning `null`. /// @@ -97,12 +99,13 @@ return folder(_that);} /// } /// ``` -@optionalTypeArgs TResult? mapOrNull({TResult? Function( FileItem value)? file,TResult? Function( FolderItem value)? folder,}){ +@optionalTypeArgs TResult? mapOrNull({TResult? Function( FileItem value)? file,TResult? Function( FolderItem value)? folder,TResult? Function( UnindexedFileItem value)? unindexedFile,}){ final _that = this; switch (_that) { case FileItem() when file != null: return file(_that);case FolderItem() when folder != null: -return folder(_that);case _: +return folder(_that);case UnindexedFileItem() when unindexedFile != null: +return unindexedFile(_that);case _: return null; } @@ -119,11 +122,12 @@ return folder(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function( SnCloudFileIndex fileIndex)? file,TResult Function( SnCloudFolder folder)? folder,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function( SnCloudFileIndex fileIndex)? file,TResult Function( SnCloudFolder folder)? folder,TResult Function( SnCloudFile file)? unindexedFile,required TResult orElse(),}) {final _that = this; switch (_that) { case FileItem() when file != null: return file(_that.fileIndex);case FolderItem() when folder != null: -return folder(_that.folder);case _: +return folder(_that.folder);case UnindexedFileItem() when unindexedFile != null: +return unindexedFile(_that.file);case _: return orElse(); } @@ -141,11 +145,12 @@ return folder(_that.folder);case _: /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function( SnCloudFileIndex fileIndex) file,required TResult Function( SnCloudFolder folder) folder,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function( SnCloudFileIndex fileIndex) file,required TResult Function( SnCloudFolder folder) folder,required TResult Function( SnCloudFile file) unindexedFile,}) {final _that = this; switch (_that) { case FileItem(): return file(_that.fileIndex);case FolderItem(): -return folder(_that.folder);} +return folder(_that.folder);case UnindexedFileItem(): +return unindexedFile(_that.file);} } /// A variant of `when` that fallback to returning `null` /// @@ -159,11 +164,12 @@ return folder(_that.folder);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function( SnCloudFileIndex fileIndex)? file,TResult? Function( SnCloudFolder folder)? folder,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function( SnCloudFileIndex fileIndex)? file,TResult? Function( SnCloudFolder folder)? folder,TResult? Function( SnCloudFile file)? unindexedFile,}) {final _that = this; switch (_that) { case FileItem() when file != null: return file(_that.fileIndex);case FolderItem() when folder != null: -return folder(_that.folder);case _: +return folder(_that.folder);case UnindexedFileItem() when unindexedFile != null: +return unindexedFile(_that.file);case _: return null; } @@ -321,4 +327,79 @@ $SnCloudFolderCopyWith<$Res> get folder { } } +/// @nodoc + + +class UnindexedFileItem implements FileListItem { + const UnindexedFileItem(this.file); + + + final SnCloudFile file; + +/// Create a copy of FileListItem +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$UnindexedFileItemCopyWith get copyWith => _$UnindexedFileItemCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is UnindexedFileItem&&(identical(other.file, file) || other.file == file)); +} + + +@override +int get hashCode => Object.hash(runtimeType,file); + +@override +String toString() { + return 'FileListItem.unindexedFile(file: $file)'; +} + + +} + +/// @nodoc +abstract mixin class $UnindexedFileItemCopyWith<$Res> implements $FileListItemCopyWith<$Res> { + factory $UnindexedFileItemCopyWith(UnindexedFileItem value, $Res Function(UnindexedFileItem) _then) = _$UnindexedFileItemCopyWithImpl; +@useResult +$Res call({ + SnCloudFile file +}); + + +$SnCloudFileCopyWith<$Res> get file; + +} +/// @nodoc +class _$UnindexedFileItemCopyWithImpl<$Res> + implements $UnindexedFileItemCopyWith<$Res> { + _$UnindexedFileItemCopyWithImpl(this._self, this._then); + + final UnindexedFileItem _self; + final $Res Function(UnindexedFileItem) _then; + +/// Create a copy of FileListItem +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') $Res call({Object? file = null,}) { + return _then(UnindexedFileItem( +null == file ? _self.file : file // ignore: cast_nullable_to_non_nullable +as SnCloudFile, + )); +} + +/// Create a copy of FileListItem +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SnCloudFileCopyWith<$Res> get file { + + return $SnCloudFileCopyWith<$Res>(_self.file, (value) { + return _then(_self.copyWith(file: value)); + }); +} +} + // dart format on diff --git a/lib/pods/file_list.dart b/lib/pods/file_list.dart index cbfb2bdd..a9a172c8 100644 --- a/lib/pods/file_list.dart +++ b/lib/pods/file_list.dart @@ -58,6 +58,47 @@ Future?> billingUsage(Ref ref) async { return response.data; } +@riverpod +class UnindexedFileListNotifier extends _$UnindexedFileListNotifier + with CursorPagingNotifierMixin { + @override + Future> build() => fetch(cursor: null); + + @override + Future> fetch({ + required String? cursor, + }) async { + final client = ref.read(apiClientProvider); + + final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0; + const take = 50; // Default page size + + final response = await client.get( + '/drive/index/unindexed', + queryParameters: {'take': take.toString(), 'offset': offset.toString()}, + ); + + final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0; + + final List files = + (response.data as List) + .map((e) => SnCloudFile.fromJson(e as Map)) + .toList(); + + final List items = + files.map((file) => FileListItem.unindexedFile(file)).toList(); + + final hasMore = offset + take < total; + final nextCursor = hasMore ? (offset + take).toString() : null; + + return CursorPagingData( + items: items, + hasMore: hasMore, + nextCursor: nextCursor, + ); + } +} + @riverpod Future?> billingQuota(Ref ref) async { final client = ref.read(apiClientProvider); diff --git a/lib/pods/file_list.g.dart b/lib/pods/file_list.g.dart index 1b752c4e..95e96b46 100644 --- a/lib/pods/file_list.g.dart +++ b/lib/pods/file_list.g.dart @@ -45,7 +45,7 @@ final billingQuotaProvider = // ignore: unused_element typedef BillingQuotaRef = AutoDisposeFutureProviderRef?>; String _$cloudFileListNotifierHash() => - r'44c17a8ef959bbef5d07132603a722f76d39b9e9'; + r'5b919f2212ce64c567b9f31912ed18fc4e4bc87d'; /// See also [CloudFileListNotifier]. @ProviderFor(CloudFileListNotifier) @@ -65,5 +65,26 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider< typedef _$CloudFileListNotifier = AutoDisposeAsyncNotifier>; +String _$unindexedFileListNotifierHash() => + r'48fc92432a50a562190da5fe8ed0920d171b07b6'; + +/// See also [UnindexedFileListNotifier]. +@ProviderFor(UnindexedFileListNotifier) +final unindexedFileListNotifierProvider = AutoDisposeAsyncNotifierProvider< + UnindexedFileListNotifier, + CursorPagingData +>.internal( + UnindexedFileListNotifier.new, + name: r'unindexedFileListNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$unindexedFileListNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$UnindexedFileListNotifier = + AutoDisposeAsyncNotifier>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/screens/files/file_list.dart b/lib/screens/files/file_list.dart index ce4c1e5a..297137a7 100644 --- a/lib/screens/files/file_list.dart +++ b/lib/screens/files/file_list.dart @@ -21,6 +21,7 @@ class FileListScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // Path navigation state final currentPath = useState('/'); + final mode = useState(FileListMode.normal); final usageAsync = ref.watch(billingUsageProvider); final quotaAsync = ref.watch(billingQuotaProvider); @@ -54,6 +55,7 @@ class FileListScreen extends HookConsumerWidget { onPickAndUpload: () => _pickAndUploadFile(ref, currentPath.value), onShowCreateDirectory: _showCreateDirectoryDialog, + mode: mode, ), loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Error loading quota')), diff --git a/lib/widgets/file_list_view.dart b/lib/widgets/file_list_view.dart index 9ab6c17e..b4587441 100644 --- a/lib/widgets/file_list_view.dart +++ b/lib/widgets/file_list_view.dart @@ -14,12 +14,15 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; import 'package:styled_widget/styled_widget.dart'; +enum FileListMode { normal, unindexed } + class FileListView extends HookConsumerWidget { final Map? usage; final Map? quota; final ValueNotifier currentPath; final VoidCallback onPickAndUpload; final Function(BuildContext, ValueNotifier) onShowCreateDirectory; + final ValueNotifier mode; const FileListView({ required this.usage, @@ -27,47 +30,228 @@ class FileListView extends HookConsumerWidget { required this.currentPath, required this.onPickAndUpload, required this.onShowCreateDirectory, + required this.mode, super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { useEffect(() { - final notifier = ref.read(cloudFileListNotifierProvider.notifier); - notifier.setPath(currentPath.value); + if (mode.value == FileListMode.normal) { + final notifier = ref.read(cloudFileListNotifierProvider.notifier); + notifier.setPath(currentPath.value); + } return null; - }, [currentPath.value]); + }, [currentPath.value, mode.value]); if (usage == null) return const SizedBox.shrink(); - return CustomScrollView( - slivers: [ - const SliverGap(8), - SliverToBoxAdapter(child: _buildPathNavigation(ref, currentPath)), - const SliverGap(8), - PagingHelperSliverView( - provider: cloudFileListNotifierProvider, - futureRefreshable: cloudFileListNotifierProvider.future, - notifierRefreshable: cloudFileListNotifierProvider.notifier, - contentBuilder: - (data, widgetCount, endItemView) => - data.items.isEmpty - ? SliverToBoxAdapter( - child: _buildEmptyDirectoryHint(ref, currentPath), - ) - : SliverList.builder( - itemCount: widgetCount, - itemBuilder: (context, index) { - if (index == widgetCount - 1) { - return endItemView; - } - final item = data.items[index]; - return item.map( - file: (fileItem) { - final file = fileItem.fileIndex.file; - final itemType = - file.mimeType?.split('/').firstOrNull; - return ListTile( + final bodyWidget = switch (mode.value) { + FileListMode.unindexed => PagingHelperSliverView( + provider: unindexedFileListNotifierProvider, + futureRefreshable: unindexedFileListNotifierProvider.future, + notifierRefreshable: unindexedFileListNotifierProvider.notifier, + contentBuilder: + (data, widgetCount, endItemView) => + data.items.isEmpty + ? SliverToBoxAdapter( + child: _buildEmptyUnindexedFilesHint(ref), + ) + : SliverList.builder( + itemCount: widgetCount, + itemBuilder: (context, index) { + if (index == widgetCount - 1) { + return endItemView; + } + + final item = data.items[index]; + return item.map( + file: (fileItem) { + // This should not happen in unindexed mode + return const SizedBox.shrink(); + }, + folder: (folderItem) { + // This should not happen in unindexed mode + return const SizedBox.shrink(); + }, + unindexedFile: (unindexedFileItem) { + final file = unindexedFileItem.file; + final itemType = + file.mimeType?.split('/').firstOrNull; + return ListTile( + leading: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + child: SizedBox( + height: 48, + width: 48, + child: switch (itemType) { + 'image' => CloudImageWidget(file: file), + 'audio' => + const Icon( + Symbols.audio_file, + fill: 1, + ).center(), + 'video' => + const Icon( + Symbols.video_file, + fill: 1, + ).center(), + _ => + const Icon( + Symbols.body_system, + fill: 1, + ).center(), + }, + ), + ), + title: + file.name.isEmpty + ? Text('untitled').tr().italic() + : Text( + file.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(formatFileSize(file.size)), + onTap: () { + context.push('/files/${file.id}', extra: file); + }, + trailing: 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/files/${file.id}', + ); + ref.invalidate( + unindexedFileListNotifierProvider, + ); + } catch (e) { + showSnackBar('failedToDeleteFile'.tr()); + } finally { + if (context.mounted) { + hideLoadingModal(context); + } + } + }, + ), + ); + }, + ); + }, + ), + ), + _ => PagingHelperSliverView( + provider: cloudFileListNotifierProvider, + futureRefreshable: cloudFileListNotifierProvider.future, + notifierRefreshable: cloudFileListNotifierProvider.notifier, + contentBuilder: + (data, widgetCount, endItemView) => + data.items.isEmpty + ? SliverToBoxAdapter( + child: _buildEmptyDirectoryHint(ref, currentPath), + ) + : SliverList.builder( + itemCount: widgetCount, + itemBuilder: (context, index) { + if (index == widgetCount - 1) { + return endItemView; + } + + final item = data.items[index]; + return item.map( + file: (fileItem) { + final file = fileItem.fileIndex.file; + final itemType = + file.mimeType?.split('/').firstOrNull; + return ListTile( + leading: ClipRRect( + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + child: SizedBox( + height: 48, + width: 48, + child: switch (itemType) { + 'image' => CloudImageWidget(file: file), + 'audio' => + const Icon( + Symbols.audio_file, + fill: 1, + ).center(), + 'video' => + const Icon( + Symbols.video_file, + fill: 1, + ).center(), + _ => + const Icon( + Symbols.body_system, + fill: 1, + ).center(), + }, + ), + ), + title: + file.name.isEmpty + ? Text('untitled').tr().italic() + : Text( + file.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text(formatFileSize(file.size)), + onTap: () { + context.push( + '/files/${fileItem.fileIndex.id}', + extra: file, + ); + }, + trailing: 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); + } + } + }, + ), + ); + }, + folder: + (folderItem) => ListTile( leading: ClipRRect( borderRadius: const BorderRadius.all( Radius.circular(8), @@ -75,108 +259,51 @@ class FileListView extends HookConsumerWidget { child: SizedBox( height: 48, width: 48, - child: switch (itemType) { - 'image' => CloudImageWidget(file: file), - 'audio' => + child: const Icon( - Symbols.audio_file, + Symbols.folder, fill: 1, ).center(), - 'video' => - const Icon( - Symbols.video_file, - fill: 1, - ).center(), - _ => - const Icon( - Symbols.body_system, - fill: 1, - ).center(), - }, ), ), - title: - file.name.isEmpty - ? Text('untitled').tr().italic() - : Text( - file.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text(formatFileSize(file.size)), + title: Text( + folderItem.folder.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: const Text('Folder'), onTap: () { - context.push( - '/files/${fileItem.fileIndex.id}', - extra: file, - ); + // Navigate to folder + final newPath = + currentPath.value == '/' + ? '/${folderItem.folder.name}' + : '${currentPath.value}/${folderItem.folder.name}'; + currentPath.value = newPath; }, - trailing: IconButton( - icon: const Icon(Symbols.delete), - onPressed: () async { - final confirmed = await showConfirmAlert( - 'confirmDeleteFile'.tr(), - 'deleteFile'.tr(), - ); - if (!confirmed) return; + ), + unindexedFile: (unindexedFileItem) { + // This should not happen in normal mode + return const SizedBox.shrink(); + }, + ); + }, + ), + ), + }; - 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); - } - } - }, - ), - ); - }, - folder: - (folderItem) => ListTile( - leading: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - child: SizedBox( - height: 48, - width: 48, - child: - const Icon( - Symbols.folder, - fill: 1, - ).center(), - ), - ), - title: Text( - folderItem.folder.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: const Text('Folder'), - onTap: () { - // Navigate to folder - final newPath = - currentPath.value == '/' - ? '/${folderItem.folder.name}' - : '${currentPath.value}/${folderItem.folder.name}'; - currentPath.value = newPath; - }, - ), - ); - }, - ), + return Column( + children: [ + const Gap(8), + _buildPathNavigation(ref, currentPath), + const Gap(8), + Expanded( + child: CustomScrollView( + slivers: [ + bodyWidget, + if (mode.value == FileListMode.normal && currentPath.value == '/') + SliverToBoxAdapter(child: _buildUnindexedFilesEntry(ref)), + ], + ), ), ], ); @@ -187,7 +314,16 @@ class FileListView extends HookConsumerWidget { ValueNotifier currentPath, ) { Widget pathContent; - if (currentPath.value == '/') { + if (mode.value == FileListMode.unindexed) { + pathContent = Row( + children: [ + Text( + 'Unindexed Files', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ); + } else if (currentPath.value == '/') { pathContent = Text( 'Root Directory', style: TextStyle(fontWeight: FontWeight.bold), @@ -234,28 +370,87 @@ class FileListView extends HookConsumerWidget { ); } - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - const Icon(Symbols.folder), - const Gap(8), - Expanded(child: pathContent), - IconButton( - icon: const Icon(Symbols.create_new_folder), - onPressed: () => onShowCreateDirectory(ref.context, currentPath), - tooltip: 'Create Directory', - ), - IconButton( - icon: const Icon(Symbols.upload_file), - onPressed: onPickAndUpload, - tooltip: 'Upload File', - ), - ], + return SizedBox( + height: 64, + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + IconButton( + icon: Icon( + mode.value == FileListMode.unindexed + ? Symbols.inventory_2 + : Symbols.folder, + ), + onPressed: () { + if (mode.value == FileListMode.unindexed) { + mode.value = FileListMode.normal; + } + currentPath.value = '/'; + }, + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + const Gap(8), + Expanded(child: pathContent), + if (mode.value == FileListMode.normal) ...[ + IconButton( + icon: const Icon(Symbols.create_new_folder), + onPressed: + () => onShowCreateDirectory(ref.context, currentPath), + tooltip: 'Create Directory', + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + IconButton( + icon: const Icon(Symbols.upload_file), + onPressed: onPickAndUpload, + tooltip: 'Upload File', + visualDensity: const VisualDensity( + horizontal: -4, + vertical: -4, + ), + ), + ], + ], + ), ), + ).padding(horizontal: 8), + ); + } + + Widget _buildUnindexedFilesEntry(WidgetRef ref) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(ref.context).colorScheme.outline), + borderRadius: const BorderRadius.all(Radius.circular(8)), ), - ).padding(horizontal: 8); + margin: const EdgeInsets.symmetric(horizontal: 12), + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + const Icon(Symbols.inventory_2).padding(horizontal: 8), + const Gap(8), + const Text('Unindexed Files').bold(), + const Spacer(), + const Icon(Symbols.chevron_right).padding(horizontal: 8), + ], + ), + ), + onTap: () { + mode.value = FileListMode.unindexed; + currentPath.value = '/'; + }, + ), + ); } Widget _buildEmptyDirectoryHint( @@ -263,7 +458,7 @@ class FileListView extends HookConsumerWidget { ValueNotifier currentPath, ) { return Card( - margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), + margin: const EdgeInsets.fromLTRB(12, 0, 12, 16), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), child: Column( @@ -313,4 +508,39 @@ class FileListView extends HookConsumerWidget { ), ); } + + Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) { + return Card( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 0), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Symbols.inventory_2, size: 64, color: Colors.grey), + const Gap(16), + Text( + 'No unindexed files', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Theme.of(ref.context).textTheme.bodyLarge?.color, + ), + ), + const Gap(8), + Text( + 'All files have been assigned to paths.\n' + 'Files without paths will appear here.', + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of( + ref.context, + ).textTheme.bodyMedium?.color?.withOpacity(0.7), + ), + ), + ], + ), + ), + ); + } } diff --git a/lib/widgets/upload_overlay.dart b/lib/widgets/upload_overlay.dart index 7a8b0006..90dd21c3 100644 --- a/lib/widgets/upload_overlay.dart +++ b/lib/widgets/upload_overlay.dart @@ -442,7 +442,9 @@ class _UploadTaskTileState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - widget.task.fileName, + widget.task.fileName.isEmpty + ? 'untitled'.tr() + : widget.task.fileName, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),