diff --git a/lib/pods/file_list.dart b/lib/pods/file_list.dart index f714b9a7..ce0fba03 100644 --- a/lib/pods/file_list.dart +++ b/lib/pods/file_list.dart @@ -11,12 +11,18 @@ part 'file_list.g.dart'; class CloudFileListNotifier extends _$CloudFileListNotifier with CursorPagingNotifierMixin { String _currentPath = '/'; + String? _poolId; void setPath(String path) { _currentPath = path; ref.invalidateSelf(); } + void setPool(String? poolId) { + _poolId = poolId; + ref.invalidateSelf(); + } + @override Future> build() => fetch(cursor: null); @@ -26,9 +32,15 @@ class CloudFileListNotifier extends _$CloudFileListNotifier }) async { final client = ref.read(apiClientProvider); + final queryParameters = {'path': _currentPath}; + + if (_poolId != null) { + queryParameters['pool'] = _poolId!; + } + final response = await client.get( '/drive/index/browse', - queryParameters: {'path': _currentPath}, + queryParameters: queryParameters, ); final List folders = diff --git a/lib/screens/files/file_list.dart b/lib/screens/files/file_list.dart index 40d6f3e7..0bf9e321 100644 --- a/lib/screens/files/file_list.dart +++ b/lib/screens/files/file_list.dart @@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/file.dart'; +import 'package:island/models/file_pool.dart'; import 'package:island/pods/file_list.dart'; import 'package:island/services/file_uploader.dart'; import 'package:island/widgets/alert.dart'; @@ -24,6 +25,7 @@ class FileListScreen extends HookConsumerWidget { // Path navigation state final currentPath = useState('/'); final mode = useState(FileListMode.normal); + final selectedPool = useState(null); final usageAsync = ref.watch(billingUsageProvider); final quotaAsync = ref.watch(billingQuotaProvider); @@ -56,8 +58,13 @@ class FileListScreen extends HookConsumerWidget { usage: usage, quota: quota, currentPath: currentPath, + selectedPool: selectedPool, onPickAndUpload: - () => _pickAndUploadFile(ref, currentPath.value), + () => _pickAndUploadFile( + ref, + currentPath.value, + selectedPool.value?.id, + ), onShowCreateDirectory: _showCreateDirectoryDialog, mode: mode, viewMode: viewMode, @@ -71,7 +78,11 @@ class FileListScreen extends HookConsumerWidget { ); } - Future _pickAndUploadFile(WidgetRef ref, String currentPath) async { + Future _pickAndUploadFile( + WidgetRef ref, + String currentPath, + String? poolId, + ) async { try { final result = await FilePicker.platform.pickFiles( allowMultiple: true, @@ -93,6 +104,7 @@ class FileListScreen extends HookConsumerWidget { fileData: universalFile, ref: ref, path: currentPath, + poolId: poolId, onProgress: (progress, _) { // Progress is handled by the upload tasks system if (progress != null) { diff --git a/lib/widgets/file_list_view.dart b/lib/widgets/file_list_view.dart index 69330ebe..9477b342 100644 --- a/lib/widgets/file_list_view.dart +++ b/lib/widgets/file_list_view.dart @@ -32,6 +32,7 @@ class FileListView extends HookConsumerWidget { final Map? usage; final Map? quota; final ValueNotifier currentPath; + final ValueNotifier selectedPool; final VoidCallback onPickAndUpload; final Function(BuildContext, ValueNotifier) onShowCreateDirectory; final ValueNotifier mode; @@ -41,6 +42,7 @@ class FileListView extends HookConsumerWidget { required this.usage, required this.quota, required this.currentPath, + required this.selectedPool, required this.onPickAndUpload, required this.onShowCreateDirectory, required this.mode, @@ -115,84 +117,95 @@ class FileListView extends HookConsumerWidget { final unindexedNotifier = ref.read( unindexedFileListNotifierProvider.notifier, ); - final selectedPool = useState(null); + final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier); final recycled = useState(false); final poolsAsync = ref.watch(poolsProvider); - late Widget pathContent; - if (mode.value == FileListMode.unindexed) { - final unindexedItems = poolsAsync.when( - data: - (pools) => [ - const DropdownMenuItem( - value: null, - child: Text('All Pools', style: TextStyle(fontSize: 14)), + useEffect(() { + // Sync pool when mode or selectedPool changes + if (mode.value == FileListMode.unindexed) { + unindexedNotifier.setPool(selectedPool.value?.id); + } else { + cloudNotifier.setPool(selectedPool.value?.id); + } + return null; + }, [selectedPool.value, mode.value]); + + final poolDropdownItems = poolsAsync.when( + data: + (pools) => [ + const DropdownMenuItem( + value: null, + child: Text('All Pools', style: TextStyle(fontSize: 14)), + ), + ...pools.map( + (p) => DropdownMenuItem( + value: p, + child: Text(p.name, style: const TextStyle(fontSize: 14)), ), - ...pools.map( - (p) => DropdownMenuItem( - value: p, - child: Text(p.name, style: const TextStyle(fontSize: 14)), - ), + ), + ], + loading: () => const >[], + error: (err, stack) => const >[], + ); + + final poolDropdown = DropdownButtonHideUnderline( + child: DropdownButton2( + value: selectedPool.value, + items: poolDropdownItems, + onChanged: + isRefreshing + ? null + : (value) { + selectedPool.value = value; + if (mode.value == FileListMode.unindexed) { + unindexedNotifier.setPool(value?.id); + } else { + cloudNotifier.setPool(value?.id); + } + }, + customButton: Container( + height: 28, + width: 200, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(ref.context).colorScheme.outline, + ), + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 6, + children: [ + const Icon(Symbols.pool, size: 16), + Flexible( + child: Text( + selectedPool.value?.name ?? 'All files', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).fontSize(12), ), ], - loading: () => const >[], - error: (err, stack) => const >[], - ); - pathContent = Row( - children: [ - const Text( - 'Unindexed Files', - style: TextStyle(fontWeight: FontWeight.bold), + ).height(24), + ), + buttonStyleData: const ButtonStyleData( + padding: EdgeInsets.zero, + height: 28, + width: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), ), - const Gap(8), - DropdownButtonHideUnderline( - child: DropdownButton2( - value: selectedPool.value, - items: unindexedItems, - onChanged: - isRefreshing - ? null - : (value) { - selectedPool.value = value; - unindexedNotifier.setPool(value?.id); - }, - customButton: Container( - height: 28, - width: 160, - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(ref.context).colorScheme.outline, - ), - borderRadius: const BorderRadius.all(Radius.circular(8)), - ), - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - spacing: 6, - children: [ - const Icon(Symbols.pool, size: 16), - Flexible( - child: Text( - selectedPool.value?.name ?? 'All files', - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).fontSize(12), - ), - ], - ).height(24), - ), - buttonStyleData: const ButtonStyleData( - padding: EdgeInsets.zero, - height: 28, - width: 200, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - ), - dropdownStyleData: const DropdownStyleData(maxHeight: 200), - ), - ), - ], + ), + dropdownStyleData: const DropdownStyleData(maxHeight: 200), + ), + ); + + late Widget pathContent; + if (mode.value == FileListMode.unindexed) { + pathContent = const Text( + 'Unindexed Files', + style: TextStyle(fontWeight: FontWeight.bold), ); } else if (currentPath.value == '/') { pathContent = const Text( @@ -262,6 +275,7 @@ class FileListView extends HookConsumerWidget { fileData: universalFile, ref: ref, path: mode.value == FileListMode.normal ? currentPath.value : null, + poolId: selectedPool.value?.id, onProgress: (progress, _) { // Progress is handled by the upload tasks system if (progress != null) { @@ -293,8 +307,11 @@ class FileListView extends HookConsumerWidget { ? Theme.of(context).primaryColor.withOpacity(0.1) : null, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Gap(8), + const Gap(12), + poolDropdown.padding(horizontal: 16), + const Gap(6), Card( child: Padding( padding: const EdgeInsets.all(16), diff --git a/lib/widgets/upload_overlay.dart b/lib/widgets/upload_overlay.dart index 88a8a4f5..badddb88 100644 --- a/lib/widgets/upload_overlay.dart +++ b/lib/widgets/upload_overlay.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -29,7 +30,11 @@ class UploadOverlay extends HookConsumerWidget { ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first final isVisibleOverride = useState(null); - final isVisible = isVisibleOverride.value ?? activeTasks.isNotEmpty; + final pendingHide = useState(false); + final hideTimer = useState(null); + final isVisible = + (isVisibleOverride.value ?? activeTasks.isNotEmpty) && + !pendingHide.value; final slideController = useAnimationController( duration: const Duration(milliseconds: 300), ); @@ -48,6 +53,24 @@ class UploadOverlay extends HookConsumerWidget { return null; }, [isVisible]); + // Handle hide delay when tasks complete + useEffect(() { + if (activeTasks.isEmpty && (isVisibleOverride.value ?? false) == false) { + // No active tasks and not manually visible (not expanded) + hideTimer.value = Timer(const Duration(seconds: 2), () { + pendingHide.value = true; + }); + } else { + // Cancel any pending hide and reset + hideTimer.value?.cancel(); + hideTimer.value = null; + pendingHide.value = false; + } + return () { + hideTimer.value?.cancel(); + }; + }, [activeTasks.length, isVisibleOverride.value]); + if (!isVisible && slideController.status == AnimationStatus.dismissed) { // If not visible and animation is complete (back to start), don't show anything return const SizedBox.shrink();