💄 Optimize file list again
This commit is contained in:
@@ -11,12 +11,18 @@ part 'file_list.g.dart';
|
||||
class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
with CursorPagingNotifierMixin<FileListItem> {
|
||||
String _currentPath = '/';
|
||||
String? _poolId;
|
||||
|
||||
void setPath(String path) {
|
||||
_currentPath = path;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
void setPool(String? poolId) {
|
||||
_poolId = poolId;
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
|
||||
|
||||
@@ -26,9 +32,15 @@ class CloudFileListNotifier extends _$CloudFileListNotifier
|
||||
}) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final queryParameters = <String, String>{'path': _currentPath};
|
||||
|
||||
if (_poolId != null) {
|
||||
queryParameters['pool'] = _poolId!;
|
||||
}
|
||||
|
||||
final response = await client.get(
|
||||
'/drive/index/browse',
|
||||
queryParameters: {'path': _currentPath},
|
||||
queryParameters: queryParameters,
|
||||
);
|
||||
|
||||
final List<String> folders =
|
||||
|
||||
@@ -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<String>('/');
|
||||
final mode = useState<FileListMode>(FileListMode.normal);
|
||||
final selectedPool = useState<SnFilePool?>(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<void> _pickAndUploadFile(WidgetRef ref, String currentPath) async {
|
||||
Future<void> _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) {
|
||||
|
||||
@@ -32,6 +32,7 @@ class FileListView extends HookConsumerWidget {
|
||||
final Map<String, dynamic>? usage;
|
||||
final Map<String, dynamic>? quota;
|
||||
final ValueNotifier<String> currentPath;
|
||||
final ValueNotifier<SnFilePool?> selectedPool;
|
||||
final VoidCallback onPickAndUpload;
|
||||
final Function(BuildContext, ValueNotifier<String>) onShowCreateDirectory;
|
||||
final ValueNotifier<FileListMode> 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<SnFilePool?>(null);
|
||||
final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
|
||||
final recycled = useState<bool>(false);
|
||||
final poolsAsync = ref.watch(poolsProvider);
|
||||
|
||||
late Widget pathContent;
|
||||
if (mode.value == FileListMode.unindexed) {
|
||||
final unindexedItems = poolsAsync.when(
|
||||
data:
|
||||
(pools) => [
|
||||
const DropdownMenuItem<SnFilePool>(
|
||||
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<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,
|
||||
child: Text(p.name, style: const TextStyle(fontSize: 14)),
|
||||
),
|
||||
),
|
||||
],
|
||||
loading: () => const <DropdownMenuItem<SnFilePool>>[],
|
||||
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>>[],
|
||||
error: (err, stack) => const <DropdownMenuItem<SnFilePool>>[],
|
||||
);
|
||||
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<SnFilePool>(
|
||||
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),
|
||||
|
||||
@@ -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<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(
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user