Compare commits

...

2 Commits

Author SHA1 Message Date
5e61805db7 💄 Upload overlay auto hide 2025-11-18 22:38:27 +08:00
35b96b0bd2 💄 Optimize downloading and files 2025-11-18 22:21:23 +08:00
5 changed files with 114 additions and 88 deletions

View File

@@ -1338,5 +1338,6 @@
"enterNumberOfSplits": "Enter Splits Amount", "enterNumberOfSplits": "Enter Splits Amount",
"orCreateWith": "Or\ncreate with", "orCreateWith": "Or\ncreate with",
"unindexedFiles": "Unindexed files", "unindexedFiles": "Unindexed files",
"folder": "Folder" "folder": "Folder",
"clearCompleted": "Clear Completed"
} }

View File

@@ -293,6 +293,10 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
.toList(); .toList();
} }
void clearAllTasks() {
state = [];
}
DriveTask? getTask(String taskId) { DriveTask? getTask(String taskId) {
return state.where((task) => task.taskId == taskId).firstOrNull; return state.where((task) => task.taskId == taskId).firstOrNull;
} }

View File

@@ -233,9 +233,8 @@ class FileDetailScreen extends HookConsumerWidget {
} }
Future<void> _downloadFile(WidgetRef ref) async { Future<void> _downloadFile(WidgetRef ref) async {
final taskId = ref final taskNotifier = ref.read(uploadTasksProvider.notifier);
.read(uploadTasksProvider.notifier) final taskId = taskNotifier.addLocalDownloadTask(item);
.addLocalDownloadTask(item);
try { try {
showSnackBar('Downloading file...'); showSnackBar('Downloading file...');
@@ -253,12 +252,8 @@ class FileDetailScreen extends HookConsumerWidget {
queryParameters: {'original': true}, queryParameters: {'original': true},
onReceiveProgress: (count, total) { onReceiveProgress: (count, total) {
if (total > 0) { if (total > 0) {
ref taskNotifier.updateDownloadProgress(taskId, count, total);
.read(uploadTasksProvider.notifier) taskNotifier.updateTransmissionProgress(taskId, count / total);
.updateDownloadProgress(taskId, count, total);
ref
.read(uploadTasksProvider.notifier)
.updateTransmissionProgress(taskId, count / total);
} }
}, },
); );
@@ -267,18 +262,14 @@ class FileDetailScreen extends HookConsumerWidget {
name: item.name.isEmpty ? '${item.id}.$extName' : item.name, name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
file: File(filePath), file: File(filePath),
); );
ref taskNotifier.updateTaskStatus(taskId, DriveTaskStatus.completed);
.read(uploadTasksProvider.notifier)
.updateTaskStatus(taskId, DriveTaskStatus.completed);
showSnackBar('File saved to downloads'); showSnackBar('File saved to downloads');
} catch (e) { } catch (e) {
ref taskNotifier.updateTaskStatus(
.read(uploadTasksProvider.notifier) taskId,
.updateTaskStatus( DriveTaskStatus.failed,
taskId, errorMessage: e.toString(),
DriveTaskStatus.failed, );
errorMessage: e.toString(),
);
showErrorAlert(e); showErrorAlert(e);
} }
} }

View File

@@ -1,8 +1,5 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@@ -10,16 +7,11 @@ import 'package:go_router/go_router.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/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/time.dart'; import 'package:island/services/time.dart';
import 'package:island/utils/format.dart'; import 'package:island/utils/format.dart';
import 'package:island/widgets/alert.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:path/path.dart' show extension;
import 'package:path_provider/path_provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/data_saving_gate.dart'; import 'package:island/widgets/data_saving_gate.dart';
import 'package:island/widgets/content/file_info_sheet.dart';
import 'file_viewer_contents.dart'; import 'file_viewer_contents.dart';
import 'image.dart'; import 'image.dart';
@@ -281,41 +273,13 @@ class CloudFileWidget extends HookConsumerWidget {
'audio' => AudioFileContent(item: item, uri: uri), 'audio' => AudioFileContent(item: item, uri: uri),
_ => Builder( _ => Builder(
builder: (context) { builder: (context) {
Future<void> downloadFile() async {
try {
showSnackBar('Downloading file...');
final client = ref.read(apiClientProvider);
final tempDir = await getTemporaryDirectory();
var extName = extension(item.name).trim();
if (extName.isEmpty) {
extName = item.mimeType?.split('/').lastOrNull ?? 'bin';
}
final filePath = '${tempDir.path}/${item.id}.$extName';
await client.download(
'/drive/files/${item.id}',
filePath,
queryParameters: {'original': true},
);
await FileSaver.instance.saveFile(
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
file: File(filePath),
);
showSnackBar('File saved to downloads');
} catch (e) {
showErrorAlert(e);
}
}
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
width: 1, width: 1,
), ),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -347,19 +311,12 @@ class CloudFileWidget extends HookConsumerWidget {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextButton.icon(
onPressed: downloadFile,
icon: const Icon(Symbols.download),
label: Text('download').tr(),
),
const Gap(8),
TextButton.icon( TextButton.icon(
onPressed: () { onPressed: () {
showModalBottomSheet( context.pushNamed(
useRootNavigator: true, 'fileDetail',
context: context, pathParameters: {'id': item.id},
isScrollControlled: true, extra: item,
builder: (context) => FileInfoSheet(item: item),
); );
}, },
icon: const Icon(Symbols.info), icon: const Icon(Symbols.info),

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';
@@ -6,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/drive_task.dart'; import 'package:island/models/drive_task.dart';
import 'package:island/pods/upload_tasks.dart'; import 'package:island/pods/upload_tasks.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:island/talker.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -30,6 +32,44 @@ class UploadOverlay extends HookConsumerWidget {
final isVisibleOverride = useState<bool?>(null); final isVisibleOverride = useState<bool?>(null);
final pendingHide = useState(false); final pendingHide = useState(false);
final isExpandedLocal = useState(false);
final autoHideTimer = useState<Timer?>(null);
final allFinished = activeTasks.every(
(task) =>
task.status == DriveTaskStatus.completed ||
task.status == DriveTaskStatus.failed ||
task.status == DriveTaskStatus.cancelled ||
task.status == DriveTaskStatus.expired,
);
// Auto-hide timer effect
useEffect(
() {
autoHideTimer.value?.cancel();
if (allFinished &&
activeTasks.isNotEmpty &&
!isExpandedLocal.value &&
!pendingHide.value) {
talker.info('[UploadOverlay] Setting auto hide timer...');
autoHideTimer.value = Timer(const Duration(seconds: 3), () {
talker.info('[UploadOverlay] Ready to hide!');
pendingHide.value = true;
});
} else {
talker.info('[UploadOverlay] Remove auto hide timer...');
autoHideTimer.value?.cancel();
autoHideTimer.value = null;
}
return null;
},
[
allFinished,
activeTasks.length,
isExpandedLocal.value,
pendingHide.value,
],
);
final isVisible = final isVisible =
(isVisibleOverride.value ?? activeTasks.isNotEmpty) && (isVisibleOverride.value ?? activeTasks.isNotEmpty) &&
!pendingHide.value; !pendingHide.value;
@@ -66,6 +106,8 @@ class UploadOverlay extends HookConsumerWidget {
position: slideAnimation, position: slideAnimation,
child: _UploadOverlayContent( child: _UploadOverlayContent(
activeTasks: activeTasks, activeTasks: activeTasks,
isExpanded: isExpandedLocal.value,
onExpansionChanged: (expanded) => isExpandedLocal.value = expanded,
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom), ).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
), ),
); );
@@ -74,12 +116,17 @@ class UploadOverlay extends HookConsumerWidget {
class _UploadOverlayContent extends HookConsumerWidget { class _UploadOverlayContent extends HookConsumerWidget {
final List<DriveTask> activeTasks; final List<DriveTask> activeTasks;
final bool isExpanded;
final Function(bool)? onExpansionChanged;
const _UploadOverlayContent({required this.activeTasks}); const _UploadOverlayContent({
required this.activeTasks,
required this.isExpanded,
this.onExpansionChanged,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isExpanded = useState(false);
final animationController = useAnimationController( final animationController = useAnimationController(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
initialValue: 0.0, initialValue: 0.0,
@@ -94,15 +141,17 @@ class _UploadOverlayContent extends HookConsumerWidget {
); );
useEffect(() { useEffect(() {
if (isExpanded.value) { if (isExpanded) {
animationController.forward(); animationController.forward();
} else { } else {
animationController.reverse(); animationController.reverse();
} }
return null; return null;
}, [isExpanded.value]); }, [isExpanded]);
final isMobile = MediaQuery.of(context).size.width < 600; final isMobile = !isWideScreen(context);
final taskNotifier = ref.read(uploadTasksProvider.notifier);
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
@@ -111,7 +160,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
right: isMobile ? 16 : 24, right: isMobile ? 16 : 24,
), ),
child: GestureDetector( child: GestureDetector(
onTap: () => isExpanded.value = !isExpanded.value, onTap: () => onExpansionChanged?.call(!isExpanded),
child: AnimatedBuilder( child: AnimatedBuilder(
animation: animationController, animation: animationController,
builder: (context, child) { builder: (context, child) {
@@ -145,8 +194,8 @@ class _UploadOverlayContent extends HookConsumerWidget {
); );
}, },
child: Icon( child: Icon(
key: ValueKey(isExpanded.value), key: ValueKey(isExpanded),
isExpanded.value isExpanded
? Symbols.list_rounded ? Symbols.list_rounded
: _getOverallStatusIcon(activeTasks), : _getOverallStatusIcon(activeTasks),
size: 24, size: 24,
@@ -162,7 +211,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
isExpanded.value isExpanded
? 'uploadTasks'.tr() ? 'uploadTasks'.tr()
: _getOverallStatusText(activeTasks), : _getOverallStatusText(activeTasks),
style: Theme.of(context) style: Theme.of(context)
@@ -172,8 +221,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
if (!isExpanded.value && if (!isExpanded && activeTasks.isNotEmpty)
activeTasks.isNotEmpty)
Text( Text(
_getOverallProgressText(activeTasks), _getOverallProgressText(activeTasks),
style: Theme.of( style: Theme.of(
@@ -190,7 +238,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
), ),
// Progress indicator (collapsed) // Progress indicator (collapsed)
if (!isExpanded.value) if (!isExpanded)
SizedBox( SizedBox(
width: 32, width: 32,
height: 32, height: 32,
@@ -210,14 +258,14 @@ class _UploadOverlayContent extends HookConsumerWidget {
turns: opacityAnimation * 0.5, turns: opacityAnimation * 0.5,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: Icon( child: Icon(
isExpanded.value isExpanded
? Symbols.expand_more ? Symbols.expand_more
: Symbols.chevron_right, : Symbols.chevron_right,
size: 20, size: 20,
), ),
), ),
onPressed: onPressed:
() => isExpanded.value = !isExpanded.value, () => onExpansionChanged?.call(!isExpanded),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
), ),
@@ -226,7 +274,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
), ),
// Expanded content // Expanded content
if (isExpanded.value) if (isExpanded)
Expanded( Expanded(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -246,7 +294,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
SliverToBoxAdapter( SliverToBoxAdapter(
child: ListTile( child: ListTile(
dense: true, dense: true,
title: const Text('Clear Completed'), title: const Text('clearCompleted').tr(),
leading: Icon( leading: Icon(
Symbols.clear_all, Symbols.clear_all,
size: 18, size: 18,
@@ -256,10 +304,35 @@ class _UploadOverlayContent extends HookConsumerWidget {
).colorScheme.onSurfaceVariant, ).colorScheme.onSurfaceVariant,
), ),
onTap: () { onTap: () {
ref taskNotifier.clearCompletedTasks();
.read(uploadTasksProvider.notifier) onExpansionChanged?.call(false);
.clearCompletedTasks(); },
isExpanded.value = false;
tileColor:
Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
),
// Clear all tasks button
if (activeTasks.any(
(task) =>
task.status != DriveTaskStatus.completed,
))
SliverToBoxAdapter(
child: ListTile(
dense: true,
title: const Text('Clear All'),
leading: Icon(
Symbols.clear_all,
size: 18,
color:
Theme.of(context).colorScheme.error,
),
onTap: () {
taskNotifier.clearAllTasks();
onExpansionChanged?.call(false);
}, },
tileColor: tileColor:
Theme.of( Theme.of(