Compare commits
2 Commits
c8ad791ff3
...
5e61805db7
| Author | SHA1 | Date | |
|---|---|---|---|
|
5e61805db7
|
|||
|
35b96b0bd2
|
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user