Compare commits

...

8 Commits

Author SHA1 Message Date
0303ef4a93 💄 Optimize file list again 2025-11-18 00:20:10 +08:00
c2b18ce10b 🐛 Fix file list 2025-11-18 00:04:07 +08:00
0767bb53ce Put clean up recycled files back 2025-11-17 23:53:11 +08:00
b233f9a410 💄 File list loading indicator 2025-11-17 23:10:13 +08:00
256024fb46 💄 Adjust upload overlay auto show and hide logic 2025-11-17 22:57:53 +08:00
4a80aaf24d Unindexed files filter 2025-11-17 22:57:42 +08:00
aafd160c44 🐛 Fix waterfall styling issue 2025-11-17 22:00:51 +08:00
4a800725e3 Zoom image via mosue scroll 2025-11-17 22:00:35 +08:00
5 changed files with 471 additions and 175 deletions

View File

@@ -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 =
@@ -58,6 +70,19 @@ Future<Map<String, dynamic>?> billingUsage(Ref ref) async {
@riverpod
class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
with CursorPagingNotifierMixin<FileListItem> {
String? _poolId;
bool _recycled = false;
void setPool(String? poolId) {
_poolId = poolId;
ref.invalidateSelf();
}
void setRecycled(bool recycled) {
_recycled = recycled;
ref.invalidateSelf();
}
@override
Future<CursorPagingData<FileListItem>> build() => fetch(cursor: null);
@@ -70,9 +95,22 @@ class UnindexedFileListNotifier extends _$UnindexedFileListNotifier
final offset = cursor != null ? int.tryParse(cursor) ?? 0 : 0;
const take = 50; // Default page size
final queryParameters = <String, String>{
'take': take.toString(),
'offset': offset.toString(),
};
if (_poolId != null) {
queryParameters['pool'] = _poolId!;
}
if (_recycled) {
queryParameters['recycled'] = _recycled.toString();
}
final response = await client.get(
'/drive/index/unindexed',
queryParameters: {'take': take.toString(), 'offset': offset.toString()},
queryParameters: queryParameters,
);
final total = int.tryParse(response.headers.value('x-total') ?? '0') ?? 0;

View File

@@ -1,10 +1,12 @@
import 'package:cross_file/cross_file.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
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';
@@ -23,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);
@@ -32,7 +35,7 @@ class FileListScreen extends HookConsumerWidget {
return AppScaffold(
isNoBackground: false,
appBar: AppBar(
title: Text('Files'),
title: Text('files').tr(),
leading: const PageBackButton(),
actions: [
IconButton(
@@ -55,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,
@@ -70,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,
@@ -92,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) {

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'dart:math' as math;
import 'dart:ui';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/material.dart';
@@ -89,19 +90,39 @@ class ImageFileContent extends HookConsumerWidget {
return Stack(
children: [
Positioned.fill(
child: PhotoView(
backgroundDecoration: BoxDecoration(
color: Colors.black.withOpacity(0.9),
child: Listener(
onPointerSignal: (pointerSignal) {
try {
// Handle mouse wheel zoom - cast to dynamic to access scrollDelta
final delta =
(pointerSignal as dynamic).scrollDelta.dy as double?;
if (delta != null && delta != 0) {
final currentScale = photoViewController.scale ?? 1.0;
// Adjust scale based on scroll direction (invert for natural zoom)
final newScale =
delta > 0 ? currentScale * 0.9 : currentScale * 1.1;
// Clamp scale to reasonable bounds
final clampedScale = newScale.clamp(0.1, 10.0);
photoViewController.scale = clampedScale;
}
} catch (e) {
// Ignore non-scroll events
}
},
child: PhotoView(
backgroundDecoration: BoxDecoration(
color: Colors.black.withOpacity(0.9),
),
controller: photoViewController,
imageProvider: CloudImageWidget.provider(
fileId: item.id,
serverUrl: ref.watch(serverUrlProvider),
original: showOriginal.value,
),
customSize: MediaQuery.of(context).size,
basePosition: Alignment.center,
filterQuality: FilterQuality.high,
),
controller: photoViewController,
imageProvider: CloudImageWidget.provider(
fileId: item.id,
serverUrl: ref.watch(serverUrlProvider),
original: showOriginal.value,
),
customSize: MediaQuery.of(context).size,
basePosition: Alignment.center,
filterQuality: FilterQuality.high,
),
),
// Controls overlay

View File

@@ -1,4 +1,5 @@
import 'package:desktop_drop/desktop_drop.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -8,7 +9,9 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file_list_item.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_pool.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file_uploader.dart';
import 'package:island/services/responsive.dart';
@@ -29,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;
@@ -38,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,
@@ -59,6 +64,14 @@ class FileListView extends HookConsumerWidget {
if (usage == null) return const SizedBox.shrink();
final isRefreshing = ref.watch(
mode.value == FileListMode.normal
? cloudFileListNotifierProvider.select((value) => value.isLoading)
: unindexedFileListNotifierProvider.select(
(value) => value.isLoading,
),
);
final bodyWidget = switch (mode.value) {
FileListMode.unindexed => PagingHelperSliverView(
provider: unindexedFileListNotifierProvider,
@@ -101,6 +114,152 @@ class FileListView extends HookConsumerWidget {
),
};
final unindexedNotifier = ref.read(
unindexedFileListNotifierProvider.notifier,
);
final cloudNotifier = ref.read(cloudFileListNotifierProvider.notifier);
final recycled = useState<bool>(false);
final poolsAsync = ref.watch(poolsProvider);
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)),
),
),
],
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),
),
],
).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),
),
);
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(
'Root Directory',
style: TextStyle(fontWeight: FontWeight.bold),
);
} else {
final pathParts =
currentPath.value
.split('/')
.where((part) => part.isNotEmpty)
.toList();
final breadcrumbs = <Widget>[];
// Add root
breadcrumbs.add(
InkWell(
onTap: () => currentPath.value = '/',
child: const Text('Root'),
),
);
// Add path parts
String currentPathBuilder = '';
for (int i = 0; i < pathParts.length; i++) {
currentPathBuilder += '/${pathParts[i]}';
final path = currentPathBuilder;
breadcrumbs.add(const Text(' / '));
if (i == pathParts.length - 1) {
// Current directory
breadcrumbs.add(
Text(
pathParts[i],
style: const TextStyle(fontWeight: FontWeight.bold),
),
);
} else {
// Clickable parent directory
breadcrumbs.add(
InkWell(
onTap: () => currentPath.value = path,
child: Text(pathParts[i]),
),
);
}
}
pathContent = Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: breadcrumbs,
);
}
return DropTarget(
onDragDone: (details) async {
dragging.value = false;
@@ -115,7 +274,8 @@ class FileListView extends HookConsumerWidget {
final completer = FileUploader.createCloudFile(
fileData: universalFile,
ref: ref,
path: currentPath.value,
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) {
@@ -147,9 +307,130 @@ class FileListView extends HookConsumerWidget {
? Theme.of(context).primaryColor.withOpacity(0.1)
: null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Gap(8),
_buildPathNavigation(ref, currentPath),
const Gap(12),
poolDropdown.padding(horizontal: 16),
const Gap(6),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
IconButton(
icon: Icon(
mode.value == FileListMode.unindexed
? Symbols.inventory_2
: currentPath.value != '/'
? Symbols.arrow_back
: Symbols.folder,
),
onPressed:
isRefreshing
? null
: () {
if (mode.value == FileListMode.unindexed) {
mode.value = FileListMode.normal;
currentPath.value = '/';
} else {
final pathParts =
currentPath.value
.split('/')
.where((part) => part.isNotEmpty)
.toList();
if (pathParts.isNotEmpty) {
pathParts.removeLast();
currentPath.value =
pathParts.isEmpty
? '/'
: '/${pathParts.join('/')}';
}
}
},
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
const Gap(8),
Expanded(
child: AbsorbPointer(
absorbing: isRefreshing,
child: pathContent,
),
),
IconButton(
icon: Icon(
viewMode.value == FileListViewMode.list
? Symbols.view_module
: Symbols.list,
),
onPressed:
() =>
viewMode.value =
viewMode.value == FileListViewMode.list
? FileListViewMode.waterfall
: FileListViewMode.list,
tooltip:
viewMode.value == FileListViewMode.list
? 'Switch to Waterfall View'
: 'Switch to List View',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
if (mode.value == FileListMode.normal)
IconButton(
icon: const Icon(Symbols.create_new_folder),
onPressed:
() =>
onShowCreateDirectory(ref.context, currentPath),
tooltip: 'Create Directory',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
if (mode.value == FileListMode.unindexed)
IconButton(
icon: Icon(
recycled.value
? Symbols.delete_forever
: Symbols.restore_from_trash,
),
onPressed: () {
recycled.value = !recycled.value;
unindexedNotifier.setRecycled(recycled.value);
},
tooltip:
recycled.value
? 'Show Active Files'
: 'Show Recycle Bin',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
IconButton(
icon: const Icon(Symbols.upload_file),
onPressed: onPickAndUpload,
tooltip: 'Upload File',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
],
),
),
).padding(horizontal: 8),
if (mode.value == FileListMode.unindexed && recycled.value)
_buildClearRecycledButton(ref).padding(horizontal: 8),
if (isRefreshing)
const LinearProgressIndicator(
minHeight: 4,
).padding(horizontal: 16, top: 6, bottom: 4),
const Gap(8),
if (mode.value == FileListMode.normal && currentPath.value == '/')
_buildUnindexedFilesEntry(ref).padding(bottom: 12),
@@ -302,155 +583,6 @@ class FileListView extends HookConsumerWidget {
};
}
Widget _buildPathNavigation(
WidgetRef ref,
ValueNotifier<String> currentPath,
) {
Widget pathContent;
if (mode.value == FileListMode.unindexed) {
pathContent = Row(
children: [
Text(
'Unindexed Files',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
);
} else if (currentPath.value == '/') {
pathContent = Text(
'Root Directory',
style: TextStyle(fontWeight: FontWeight.bold),
);
} else {
final pathParts =
currentPath.value
.split('/')
.where((part) => part.isNotEmpty)
.toList();
final breadcrumbs = <Widget>[];
// Add root
breadcrumbs.add(
InkWell(onTap: () => currentPath.value = '/', child: Text('Root')),
);
// Add path parts
String currentPathBuilder = '';
for (int i = 0; i < pathParts.length; i++) {
currentPathBuilder += '/${pathParts[i]}';
final path = currentPathBuilder;
breadcrumbs.add(const Text(' / '));
if (i == pathParts.length - 1) {
// Current directory
breadcrumbs.add(
Text(pathParts[i], style: TextStyle(fontWeight: FontWeight.bold)),
);
} else {
// Clickable parent directory
breadcrumbs.add(
InkWell(
onTap: () => currentPath.value = path,
child: Text(pathParts[i]),
),
);
}
}
pathContent = Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: breadcrumbs,
);
}
return SizedBox(
height: 64,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
IconButton(
icon: Icon(
mode.value == FileListMode.unindexed
? Symbols.inventory_2
: currentPath.value != '/'
? Symbols.arrow_back
: Symbols.folder,
),
onPressed: () {
if (mode.value == FileListMode.unindexed) {
mode.value = FileListMode.normal;
currentPath.value = '/';
} else {
final pathParts =
currentPath.value
.split('/')
.where((part) => part.isNotEmpty)
.toList();
if (pathParts.isNotEmpty) {
pathParts.removeLast();
currentPath.value =
pathParts.isEmpty ? '/' : '/${pathParts.join('/')}';
}
}
},
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
const Gap(8),
Expanded(child: pathContent),
IconButton(
icon: Icon(
viewMode.value == FileListViewMode.list
? Symbols.view_module
: Symbols.list,
),
onPressed:
() =>
viewMode.value =
viewMode.value == FileListViewMode.list
? FileListViewMode.waterfall
: FileListViewMode.list,
tooltip:
viewMode.value == FileListViewMode.list
? 'Switch to Waterfall View'
: 'Switch to List View',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
if (mode.value == FileListMode.normal) ...[
IconButton(
icon: const Icon(Symbols.create_new_folder),
onPressed:
() => onShowCreateDirectory(ref.context, currentPath),
tooltip: 'Create Directory',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
IconButton(
icon: const Icon(Symbols.upload_file),
onPressed: onPickAndUpload,
tooltip: 'Upload File',
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
],
],
),
),
).padding(horizontal: 8),
);
}
Widget _buildUnindexedFilesEntry(WidgetRef ref) {
return Container(
decoration: BoxDecoration(
@@ -485,7 +617,10 @@ class FileListView extends HookConsumerWidget {
ValueNotifier<String> currentPath,
) {
return Card(
margin: const EdgeInsets.fromLTRB(12, 0, 12, 16),
margin:
viewMode.value == FileListViewMode.waterfall
? const EdgeInsets.fromLTRB(0, 0, 0, 16)
: const EdgeInsets.fromLTRB(12, 0, 12, 16),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
child: Column(
@@ -922,7 +1057,10 @@ class FileListView extends HookConsumerWidget {
Widget _buildEmptyUnindexedFilesHint(WidgetRef ref) {
return Card(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 0),
margin:
viewMode.value == FileListViewMode.waterfall
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(12, 0, 12, 0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 48),
child: Column(
@@ -954,4 +1092,59 @@ class FileListView extends HookConsumerWidget {
),
);
}
Widget _buildClearRecycledButton(WidgetRef ref) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(
spacing: 16,
children: [
const Icon(Symbols.recycling).padding(horizontal: 2),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Clear All Recycled Files').bold(),
const Text(
'Permanently delete all marked recycled files to free up space.',
).fontSize(13),
],
),
),
ElevatedButton.icon(
icon: const Icon(Symbols.delete_forever),
label: const Text('Clear'),
onPressed: () async {
final confirmed = await showConfirmAlert(
'Are you sure you want to clear all recycled files?',
'Clear Recycled Files',
);
if (!confirmed) return;
if (ref.context.mounted) {
showLoadingModal(ref.context);
}
try {
final client = ref.read(apiClientProvider);
final response = await client.delete(
'/drive/files/me/recycle',
);
final count = response.data['count'] as int? ?? 0;
showSnackBar('Cleared $count recycled files.');
ref.invalidate(unindexedFileListNotifierProvider);
} catch (e) {
showSnackBar('Failed to clear recycled files.');
} finally {
if (ref.context.mounted) {
hideLoadingModal(ref.context);
}
}
},
),
],
),
),
);
}
}

View File

@@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
@@ -29,7 +29,12 @@ class UploadOverlay extends HookConsumerWidget {
.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt)); // Newest first
final isVisible = activeTasks.isNotEmpty;
final isVisibleOverride = useState<bool?>(null);
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();
@@ -63,6 +86,7 @@ class UploadOverlay extends HookConsumerWidget {
position: slideAnimation,
child: _UploadOverlayContent(
activeTasks: activeTasks,
onVisibilityChanged: (bool? v) => isVisibleOverride.value = v,
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
),
);
@@ -71,12 +95,15 @@ class UploadOverlay extends HookConsumerWidget {
class _UploadOverlayContent extends HookConsumerWidget {
final List<DriveTask> activeTasks;
final void Function(bool?) onVisibilityChanged;
const _UploadOverlayContent({required this.activeTasks});
const _UploadOverlayContent({
required this.activeTasks,
required this.onVisibilityChanged,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isExpanded = useState(false);
final animationController = useAnimationController(
duration: const Duration(milliseconds: 200),
initialValue: 0.0,
@@ -90,12 +117,15 @@ class _UploadOverlayContent extends HookConsumerWidget {
CurvedAnimation(parent: animationController, curve: Curves.easeInOut),
);
final isExpanded = useState(false);
useEffect(() {
if (isExpanded.value) {
animationController.forward();
} else {
animationController.reverse();
}
onVisibilityChanged.call(isExpanded.value);
return null;
}, [isExpanded.value]);
@@ -256,6 +286,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
ref
.read(uploadTasksProvider.notifier)
.clearCompletedTasks();
isExpanded.value = false;
},
tileColor:
Theme.of(