✨ Unindexed files
This commit is contained in:
@@ -8,4 +8,6 @@ part 'file_list_item.freezed.dart';
|
|||||||
sealed class FileListItem with _$FileListItem {
|
sealed class FileListItem with _$FileListItem {
|
||||||
const factory FileListItem.file(SnCloudFileIndex fileIndex) = FileItem;
|
const factory FileListItem.file(SnCloudFileIndex fileIndex) = FileItem;
|
||||||
const factory FileListItem.folder(SnCloudFolder folder) = FolderItem;
|
const factory FileListItem.folder(SnCloudFolder folder) = FolderItem;
|
||||||
|
const factory FileListItem.unindexedFile(SnCloudFile file) =
|
||||||
|
UnindexedFileItem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,12 +55,13 @@ extension FileListItemPatterns on FileListItem {
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( FileItem value)? file,TResult Function( FolderItem value)? folder,required TResult orElse(),}){
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( FileItem value)? file,TResult Function( FolderItem value)? folder,TResult Function( UnindexedFileItem value)? unindexedFile,required TResult orElse(),}){
|
||||||
final _that = this;
|
final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case FileItem() when file != null:
|
case FileItem() when file != null:
|
||||||
return file(_that);case FolderItem() when folder != 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();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -78,12 +79,13 @@ return folder(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( FileItem value) file,required TResult Function( FolderItem value) folder,}){
|
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( FileItem value) file,required TResult Function( FolderItem value) folder,required TResult Function( UnindexedFileItem value) unindexedFile,}){
|
||||||
final _that = this;
|
final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case FileItem():
|
case FileItem():
|
||||||
return file(_that);case FolderItem():
|
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`.
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
///
|
///
|
||||||
@@ -97,12 +99,13 @@ return folder(_that);}
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( FileItem value)? file,TResult? Function( FolderItem value)? folder,}){
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( FileItem value)? file,TResult? Function( FolderItem value)? folder,TResult? Function( UnindexedFileItem value)? unindexedFile,}){
|
||||||
final _that = this;
|
final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case FileItem() when file != null:
|
case FileItem() when file != null:
|
||||||
return file(_that);case FolderItem() when folder != 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;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -119,11 +122,12 @@ return folder(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( SnCloudFileIndex fileIndex)? file,TResult Function( SnCloudFolder folder)? folder,required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( SnCloudFileIndex fileIndex)? file,TResult Function( SnCloudFolder folder)? folder,TResult Function( SnCloudFile file)? unindexedFile,required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case FileItem() when file != null:
|
case FileItem() when file != null:
|
||||||
return file(_that.fileIndex);case FolderItem() when folder != 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();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -141,11 +145,12 @@ return folder(_that.folder);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( SnCloudFileIndex fileIndex) file,required TResult Function( SnCloudFolder folder) folder,}) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( SnCloudFileIndex fileIndex) file,required TResult Function( SnCloudFolder folder) folder,required TResult Function( SnCloudFile file) unindexedFile,}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case FileItem():
|
case FileItem():
|
||||||
return file(_that.fileIndex);case FolderItem():
|
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`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@@ -159,11 +164,12 @@ return folder(_that.folder);}
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( SnCloudFileIndex fileIndex)? file,TResult? Function( SnCloudFolder folder)? folder,}) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( SnCloudFileIndex fileIndex)? file,TResult? Function( SnCloudFolder folder)? folder,TResult? Function( SnCloudFile file)? unindexedFile,}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case FileItem() when file != null:
|
case FileItem() when file != null:
|
||||||
return file(_that.fileIndex);case FolderItem() when folder != 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;
|
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<UnindexedFileItem> get copyWith => _$UnindexedFileItemCopyWithImpl<UnindexedFileItem>(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
|
// dart format on
|
||||||
|
|||||||
@@ -58,6 +58,47 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
|
||||||
|
with CursorPagingNotifierMixin<FileListItem> {
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<CursorPagingData<FileListItem>> 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<SnCloudFile> files =
|
||||||
|
(response.data as List)
|
||||||
|
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final List<FileListItem> 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
|
@riverpod
|
||||||
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
Future<Map<String, dynamic>?> billingQuota(Ref ref) async {
|
||||||
final client = ref.read(apiClientProvider);
|
final client = ref.read(apiClientProvider);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final billingQuotaProvider =
|
|||||||
// ignore: unused_element
|
// ignore: unused_element
|
||||||
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
typedef BillingQuotaRef = AutoDisposeFutureProviderRef<Map<String, dynamic>?>;
|
||||||
String _$cloudFileListNotifierHash() =>
|
String _$cloudFileListNotifierHash() =>
|
||||||
r'44c17a8ef959bbef5d07132603a722f76d39b9e9';
|
r'5b919f2212ce64c567b9f31912ed18fc4e4bc87d';
|
||||||
|
|
||||||
/// See also [CloudFileListNotifier].
|
/// See also [CloudFileListNotifier].
|
||||||
@ProviderFor(CloudFileListNotifier)
|
@ProviderFor(CloudFileListNotifier)
|
||||||
@@ -65,5 +65,26 @@ final cloudFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
|||||||
|
|
||||||
typedef _$CloudFileListNotifier =
|
typedef _$CloudFileListNotifier =
|
||||||
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||||
|
String _$unindexedFileListNotifierHash() =>
|
||||||
|
r'48fc92432a50a562190da5fe8ed0920d171b07b6';
|
||||||
|
|
||||||
|
/// See also [UnindexedFileListNotifier].
|
||||||
|
@ProviderFor(UnindexedFileListNotifier)
|
||||||
|
final unindexedFileListNotifierProvider = AutoDisposeAsyncNotifierProvider<
|
||||||
|
UnindexedFileListNotifier,
|
||||||
|
CursorPagingData<FileListItem>
|
||||||
|
>.internal(
|
||||||
|
UnindexedFileListNotifier.new,
|
||||||
|
name: r'unindexedFileListNotifierProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$unindexedFileListNotifierHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$UnindexedFileListNotifier =
|
||||||
|
AutoDisposeAsyncNotifier<CursorPagingData<FileListItem>>;
|
||||||
// ignore_for_file: type=lint
|
// 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
|
// 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
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// Path navigation state
|
// Path navigation state
|
||||||
final currentPath = useState<String>('/');
|
final currentPath = useState<String>('/');
|
||||||
|
final mode = useState<FileListMode>(FileListMode.normal);
|
||||||
|
|
||||||
final usageAsync = ref.watch(billingUsageProvider);
|
final usageAsync = ref.watch(billingUsageProvider);
|
||||||
final quotaAsync = ref.watch(billingQuotaProvider);
|
final quotaAsync = ref.watch(billingQuotaProvider);
|
||||||
@@ -54,6 +55,7 @@ class FileListScreen extends HookConsumerWidget {
|
|||||||
onPickAndUpload:
|
onPickAndUpload:
|
||||||
() => _pickAndUploadFile(ref, currentPath.value),
|
() => _pickAndUploadFile(ref, currentPath.value),
|
||||||
onShowCreateDirectory: _showCreateDirectoryDialog,
|
onShowCreateDirectory: _showCreateDirectoryDialog,
|
||||||
|
mode: mode,
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
error: (e, _) => Center(child: Text('Error loading quota')),
|
error: (e, _) => Center(child: Text('Error loading quota')),
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
|
enum FileListMode { normal, unindexed }
|
||||||
|
|
||||||
class FileListView extends HookConsumerWidget {
|
class FileListView extends HookConsumerWidget {
|
||||||
final Map<String, dynamic>? usage;
|
final Map<String, dynamic>? usage;
|
||||||
final Map<String, dynamic>? quota;
|
final Map<String, dynamic>? quota;
|
||||||
final ValueNotifier<String> currentPath;
|
final ValueNotifier<String> currentPath;
|
||||||
final VoidCallback onPickAndUpload;
|
final VoidCallback onPickAndUpload;
|
||||||
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
|
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
|
||||||
|
final ValueNotifier<FileListMode> mode;
|
||||||
|
|
||||||
const FileListView({
|
const FileListView({
|
||||||
required this.usage,
|
required this.usage,
|
||||||
@@ -27,47 +30,228 @@ class FileListView extends HookConsumerWidget {
|
|||||||
required this.currentPath,
|
required this.currentPath,
|
||||||
required this.onPickAndUpload,
|
required this.onPickAndUpload,
|
||||||
required this.onShowCreateDirectory,
|
required this.onShowCreateDirectory,
|
||||||
|
required this.mode,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final notifier = ref.read(cloudFileListNotifierProvider.notifier);
|
if (mode.value == FileListMode.normal) {
|
||||||
notifier.setPath(currentPath.value);
|
final notifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||||
|
notifier.setPath(currentPath.value);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [currentPath.value]);
|
}, [currentPath.value, mode.value]);
|
||||||
|
|
||||||
if (usage == null) return const SizedBox.shrink();
|
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];
|
final bodyWidget = switch (mode.value) {
|
||||||
return item.map(
|
FileListMode.unindexed => PagingHelperSliverView(
|
||||||
file: (fileItem) {
|
provider: unindexedFileListNotifierProvider,
|
||||||
final file = fileItem.fileIndex.file;
|
futureRefreshable: unindexedFileListNotifierProvider.future,
|
||||||
final itemType =
|
notifierRefreshable: unindexedFileListNotifierProvider.notifier,
|
||||||
file.mimeType?.split('/').firstOrNull;
|
contentBuilder:
|
||||||
return ListTile(
|
(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(
|
leading: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const BorderRadius.all(
|
||||||
Radius.circular(8),
|
Radius.circular(8),
|
||||||
@@ -75,108 +259,51 @@ class FileListView extends HookConsumerWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
width: 48,
|
width: 48,
|
||||||
child: switch (itemType) {
|
child:
|
||||||
'image' => CloudImageWidget(file: file),
|
|
||||||
'audio' =>
|
|
||||||
const Icon(
|
const Icon(
|
||||||
Symbols.audio_file,
|
Symbols.folder,
|
||||||
fill: 1,
|
fill: 1,
|
||||||
).center(),
|
).center(),
|
||||||
'video' =>
|
|
||||||
const Icon(
|
|
||||||
Symbols.video_file,
|
|
||||||
fill: 1,
|
|
||||||
).center(),
|
|
||||||
_ =>
|
|
||||||
const Icon(
|
|
||||||
Symbols.body_system,
|
|
||||||
fill: 1,
|
|
||||||
).center(),
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title:
|
title: Text(
|
||||||
file.name.isEmpty
|
folderItem.folder.name,
|
||||||
? Text('untitled').tr().italic()
|
maxLines: 1,
|
||||||
: Text(
|
overflow: TextOverflow.ellipsis,
|
||||||
file.name,
|
),
|
||||||
maxLines: 1,
|
subtitle: const Text('Folder'),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
subtitle: Text(formatFileSize(file.size)),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.push(
|
// Navigate to folder
|
||||||
'/files/${fileItem.fileIndex.id}',
|
final newPath =
|
||||||
extra: file,
|
currentPath.value == '/'
|
||||||
);
|
? '/${folderItem.folder.name}'
|
||||||
|
: '${currentPath.value}/${folderItem.folder.name}';
|
||||||
|
currentPath.value = newPath;
|
||||||
},
|
},
|
||||||
trailing: IconButton(
|
),
|
||||||
icon: const Icon(Symbols.delete),
|
unindexedFile: (unindexedFileItem) {
|
||||||
onPressed: () async {
|
// This should not happen in normal mode
|
||||||
final confirmed = await showConfirmAlert(
|
return const SizedBox.shrink();
|
||||||
'confirmDeleteFile'.tr(),
|
},
|
||||||
'deleteFile'.tr(),
|
);
|
||||||
);
|
},
|
||||||
if (!confirmed) return;
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
if (context.mounted) {
|
return Column(
|
||||||
showLoadingModal(context);
|
children: [
|
||||||
}
|
const Gap(8),
|
||||||
try {
|
_buildPathNavigation(ref, currentPath),
|
||||||
final client = ref.read(
|
const Gap(8),
|
||||||
apiClientProvider,
|
Expanded(
|
||||||
);
|
child: CustomScrollView(
|
||||||
await client.delete(
|
slivers: [
|
||||||
'/drive/index/remove/${fileItem.fileIndex.id}',
|
bodyWidget,
|
||||||
);
|
if (mode.value == FileListMode.normal && currentPath.value == '/')
|
||||||
ref.invalidate(
|
SliverToBoxAdapter(child: _buildUnindexedFilesEntry(ref)),
|
||||||
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;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -187,7 +314,16 @@ class FileListView extends HookConsumerWidget {
|
|||||||
ValueNotifier<String> currentPath,
|
ValueNotifier<String> currentPath,
|
||||||
) {
|
) {
|
||||||
Widget pathContent;
|
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(
|
pathContent = Text(
|
||||||
'Root Directory',
|
'Root Directory',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
@@ -234,28 +370,87 @@ class FileListView extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Card(
|
return SizedBox(
|
||||||
child: Padding(
|
height: 64,
|
||||||
padding: const EdgeInsets.all(16),
|
child: Card(
|
||||||
child: Row(
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(16),
|
||||||
const Icon(Symbols.folder),
|
child: Row(
|
||||||
const Gap(8),
|
children: [
|
||||||
Expanded(child: pathContent),
|
IconButton(
|
||||||
IconButton(
|
icon: Icon(
|
||||||
icon: const Icon(Symbols.create_new_folder),
|
mode.value == FileListMode.unindexed
|
||||||
onPressed: () => onShowCreateDirectory(ref.context, currentPath),
|
? Symbols.inventory_2
|
||||||
tooltip: 'Create Directory',
|
: Symbols.folder,
|
||||||
),
|
),
|
||||||
IconButton(
|
onPressed: () {
|
||||||
icon: const Icon(Symbols.upload_file),
|
if (mode.value == FileListMode.unindexed) {
|
||||||
onPressed: onPickAndUpload,
|
mode.value = FileListMode.normal;
|
||||||
tooltip: 'Upload File',
|
}
|
||||||
),
|
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(
|
Widget _buildEmptyDirectoryHint(
|
||||||
@@ -263,7 +458,7 @@ class FileListView extends HookConsumerWidget {
|
|||||||
ValueNotifier<String> currentPath,
|
ValueNotifier<String> currentPath,
|
||||||
) {
|
) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
margin: const EdgeInsets.fromLTRB(12, 0, 12, 16),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
|
||||||
child: Column(
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -442,7 +442,9 @@ class _UploadTaskTileState extends State<UploadTaskTile>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
widget.task.fileName,
|
widget.task.fileName.isEmpty
|
||||||
|
? 'untitled'.tr()
|
||||||
|
: widget.task.fileName,
|
||||||
style: Theme.of(
|
style: Theme.of(
|
||||||
context,
|
context,
|
||||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
||||||
|
|||||||
Reference in New Issue
Block a user