💄 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,84 +117,95 @@ 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(() {
if (mode.value == FileListMode.unindexed) { // Sync pool when mode or selectedPool changes
final unindexedItems = poolsAsync.when( if (mode.value == FileListMode.unindexed) {
data: unindexedNotifier.setPool(selectedPool.value?.id);
(pools) => [ } else {
const DropdownMenuItem<SnFilePool>( cloudNotifier.setPool(selectedPool.value?.id);
value: null, }
child: Text('All Pools', style: TextStyle(fontSize: 14)), return null;
}, [selectedPool.value, mode.value]);
final poolDropdownItems = poolsAsync.when(
data:
(pools) => [
const DropdownMenuItem<SnFilePool>(
value: null,
child: Text('All Pools', style: TextStyle(fontSize: 14)),
),
...pools.map(
(p) => DropdownMenuItem<SnFilePool>(
value: p,
child: Text(p.name, style: const TextStyle(fontSize: 14)),
), ),
...pools.map( ),
(p) => DropdownMenuItem<SnFilePool>( ],
value: p, loading: () => const <DropdownMenuItem<SnFilePool>>[],
child: Text(p.name, style: const TextStyle(fontSize: 14)), error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
), );
final poolDropdown = DropdownButtonHideUnderline(
child: DropdownButton2<SnFilePool>(
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 <DropdownMenuItem<SnFilePool>>[], ).height(24),
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[], ),
); buttonStyleData: const ButtonStyleData(
pathContent = Row( padding: EdgeInsets.zero,
children: [ height: 28,
const Text( width: 200,
'Unindexed Files', decoration: BoxDecoration(
style: TextStyle(fontWeight: FontWeight.bold), borderRadius: BorderRadius.all(Radius.circular(8)),
), ),
const Gap(8), ),
DropdownButtonHideUnderline( dropdownStyleData: const DropdownStyleData(maxHeight: 200),
child: DropdownButton2<SnFilePool>( ),
value: selectedPool.value, );
items: unindexedItems,
onChanged: late Widget pathContent;
isRefreshing if (mode.value == FileListMode.unindexed) {
? null pathContent = const Text(
: (value) { 'Unindexed Files',
selectedPool.value = value; style: TextStyle(fontWeight: FontWeight.bold),
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),
),
),
],
); );
} 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();