💄 Optimize file list again

This commit is contained in:
2025-11-18 00:20:10 +08:00
parent c2b18ce10b
commit 0303ef4a93
4 changed files with 140 additions and 76 deletions

View File

@@ -11,12 +11,18 @@ part 'file_list.g.dart';
class CloudFileListNotifier extends _$CloudFileListNotifier class CloudFileListNotifier extends _$CloudFileListNotifier
with CursorPagingNotifierMixin<FileListItem> { with CursorPagingNotifierMixin<FileListItem> {
String _currentPath = '/'; String _currentPath = '/';
String? _poolId;
void setPath(String path) { void setPath(String path) {
_currentPath = path; _currentPath = path;
ref.invalidateSelf(); ref.invalidateSelf();
} }
void setPool(String? poolId) {
_poolId = poolId;
ref.invalidateSelf();
}
@override @override
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null); Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
@@ -26,9 +32,15 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
}) async { }) async {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final queryParameters = <String, String>{'path': _currentPath};
if (_poolId != null) {
queryParameters['pool'] = _poolId!;
}
final response = await client.get( final response = await client.get(
'/drive/index/browse', '/drive/index/browse',
queryParameters: {'path': _currentPath}, queryParameters: queryParameters,
); );
final List<String> folders = final List<String> folders =

View File

@@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/file_pool.dart';
import 'package:island/pods/file_list.dart'; import 'package:island/pods/file_list.dart';
import 'package:island/services/file_uploader.dart'; import 'package:island/services/file_uploader.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
@@ -24,6 +25,7 @@ class FileListScreen extends HookConsumerWidget {
// Path navigation state // Path navigation state
final currentPath = useState<String>('/'); final currentPath = useState<String>('/');
final mode = useState<FileListMode>(FileListMode.normal); final mode = useState<FileListMode>(FileListMode.normal);
final selectedPool = useState<SnFilePool?>(null);
final usageAsync = ref.watch(billingUsageProvider); final usageAsync = ref.watch(billingUsageProvider);
final quotaAsync = ref.watch(billingQuotaProvider); final quotaAsync = ref.watch(billingQuotaProvider);
@@ -56,8 +58,13 @@ class FileListScreen extends HookConsumerWidget {
usage: usage, usage: usage,
quota: quota, quota: quota,
currentPath: currentPath, currentPath: currentPath,
selectedPool: selectedPool,
onPickAndUpload: onPickAndUpload:
() => _pickAndUploadFile(ref, currentPath.value), () => _pickAndUploadFile(
ref,
currentPath.value,
selectedPool.value?.id,
),
onShowCreateDirectory: _showCreateDirectoryDialog, onShowCreateDirectory: _showCreateDirectoryDialog,
mode: mode, mode: mode,
viewMode: viewMode, viewMode: viewMode,
@@ -71,7 +78,11 @@ class FileListScreen extends HookConsumerWidget {
); );
} }
Future<void> _pickAndUploadFile(WidgetRef ref, String currentPath) async { Future<void> _pickAndUploadFile(
WidgetRef ref,
String currentPath,
String? poolId,
) async {
try { try {
final result = await FilePicker.platform.pickFiles( final result = await FilePicker.platform.pickFiles(
allowMultiple: true, allowMultiple: true,
@@ -93,6 +104,7 @@ class FileListScreen extends HookConsumerWidget {
fileData: universalFile, fileData: universalFile,
ref: ref, ref: ref,
path: currentPath, path: currentPath,
poolId: poolId,
onProgress: (progress, _) { onProgress: (progress, _) {
// Progress is handled by the upload tasks system // Progress is handled by the upload tasks system
if (progress != null) { if (progress != null) {

View File

@@ -32,6 +32,7 @@ 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 ValueNotifier<SnFilePool?> selectedPool;
final VoidCallback onPickAndUpload; final VoidCallback onPickAndUpload;
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory; final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
final ValueNotifier<FileListMode> mode; final ValueNotifier<FileListMode> mode;
@@ -41,6 +42,7 @@ class FileListView extends HookConsumerWidget {
required this.usage, required this.usage,
required this.quota, required this.quota,
required this.currentPath, required this.currentPath,
required this.selectedPool,
required this.onPickAndUpload, required this.onPickAndUpload,
required this.onShowCreateDirectory, required this.onShowCreateDirectory,
required this.mode, required this.mode,
@@ -115,13 +117,21 @@ class FileListView extends HookConsumerWidget {
final unindexedNotifier = ref.read( final unindexedNotifier = ref.read(
unindexedFileListNotifierProvider.notifier, unindexedFileListNotifierProvider.notifier,
); );
final selectedPool = useState<SnFilePool?>(null); final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
final recycled = useState<bool>(false); final recycled = useState<bool>(false);
final poolsAsync = ref.watch(poolsProvider); final poolsAsync = ref.watch(poolsProvider);
late Widget pathContent; useEffect(() {
// Sync pool when mode or selectedPool changes
if (mode.value == FileListMode.unindexed) { if (mode.value == FileListMode.unindexed) {
final unindexedItems = poolsAsync.when( unindexedNotifier.setPool(selectedPool.value?.id);
} else {
cloudNotifier.setPool(selectedPool.value?.id);
}
return null;
}, [selectedPool.value, mode.value]);
final poolDropdownItems = poolsAsync.when(
data: data:
(pools) => [ (pools) => [
const DropdownMenuItem<SnFilePool>( const DropdownMenuItem<SnFilePool>(
@@ -138,27 +148,25 @@ class FileListView extends HookConsumerWidget {
loading: () => const <DropdownMenuItem<SnFilePool>>[], loading: () => const <DropdownMenuItem<SnFilePool>>[],
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[], error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
); );
pathContent = Row(
children: [ final poolDropdown = DropdownButtonHideUnderline(
const Text(
'Unindexed Files',
style: TextStyle(fontWeight: FontWeight.bold),
),
const Gap(8),
DropdownButtonHideUnderline(
child: DropdownButton2<SnFilePool>( child: DropdownButton2<SnFilePool>(
value: selectedPool.value, value: selectedPool.value,
items: unindexedItems, items: poolDropdownItems,
onChanged: onChanged:
isRefreshing isRefreshing
? null ? null
: (value) { : (value) {
selectedPool.value = value; selectedPool.value = value;
if (mode.value == FileListMode.unindexed) {
unindexedNotifier.setPool(value?.id); unindexedNotifier.setPool(value?.id);
} else {
cloudNotifier.setPool(value?.id);
}
}, },
customButton: Container( customButton: Container(
height: 28, height: 28,
width: 160, width: 200,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Theme.of(ref.context).colorScheme.outline, color: Theme.of(ref.context).colorScheme.outline,
@@ -191,8 +199,13 @@ class FileListView extends HookConsumerWidget {
), ),
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 == '/') { } else if (currentPath.value == '/') {
pathContent = const Text( pathContent = const Text(
@@ -262,6 +275,7 @@ class FileListView extends HookConsumerWidget {
fileData: universalFile, fileData: universalFile,
ref: ref, ref: ref,
path: mode.value == FileListMode.normal ? currentPath.value : null, path: mode.value == FileListMode.normal ? currentPath.value : null,
poolId: selectedPool.value?.id,
onProgress: (progress, _) { onProgress: (progress, _) {
// Progress is handled by the upload tasks system // Progress is handled by the upload tasks system
if (progress != null) { if (progress != null) {
@@ -293,8 +307,11 @@ class FileListView extends HookConsumerWidget {
? Theme.of(context).primaryColor.withOpacity(0.1) ? Theme.of(context).primaryColor.withOpacity(0.1)
: null, : null,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Gap(8), const Gap(12),
poolDropdown.padding(horizontal: 16),
const Gap(6),
Card( Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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 ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
final isVisibleOverride = useState<bool?>(null); final isVisibleOverride = useState<bool?>(null);
final isVisible = isVisibleOverride.value ?? activeTasks.isNotEmpty; final pendingHide = useState(false);
final hideTimer = useState<Timer?>(null);
final isVisible =
(isVisibleOverride.value ?? activeTasks.isNotEmpty) &&
!pendingHide.value;
final slideController = useAnimationController( final slideController = useAnimationController(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
); );
@@ -48,6 +53,24 @@ class UploadOverlay extends HookConsumerWidget {
return null; return null;
}, [isVisible]); }, [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 (!isVisible && slideController.status == AnimationStatus.dismissed) {
// If not visible and animation is complete (back to start), don't show anything // If not visible and animation is complete (back to start), don't show anything
return const SizedBox.shrink(); return const SizedBox.shrink();