💄 Optimize file list again
This commit is contained in:
@@ -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 =
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user