Compare commits

...

3 Commits

Author SHA1 Message Date
f92cfafda4 Downloading file tasks 2025-11-18 01:45:15 +08:00
fa208b44d7 🐛 Fix publisher account name shows wrong 2025-11-18 01:31:15 +08:00
94adecafbb 💄 Optimize file detail view styling 2025-11-18 00:32:26 +08:00
6 changed files with 170 additions and 130 deletions

View File

@@ -258,6 +258,24 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
}).toList();
}
void updateDownloadProgress(
String taskId,
int downloadedBytes,
int totalBytes,
) {
state =
state.map((task) {
if (task.taskId == taskId) {
return task.copyWith(
fileSize: totalBytes,
uploadedBytes: downloadedBytes,
updatedAt: DateTime.now(),
);
}
return task;
}).toList();
}
void removeTask(String taskId) {
state = state.where((task) => task.taskId != taskId).toList();
}
@@ -291,6 +309,27 @@ class UploadTasksNotifier extends StateNotifier<List<DriveTask>> {
.toList();
}
String addLocalDownloadTask(SnCloudFile item) {
final taskId =
'download-${item.id}-${DateTime.now().millisecondsSinceEpoch}';
final task = DriveTask(
id: taskId,
taskId: taskId,
fileName: item.name,
contentType: item.mimeType ?? '',
fileSize: 0,
uploadedBytes: 0,
totalChunks: 1,
uploadedChunks: 0,
status: DriveTaskStatus.inProgress,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
type: 'FileDownload',
);
state = [...state, task];
return taskId;
}
@override
void dispose() {
_websocketSubscription?.cancel();

View File

@@ -10,6 +10,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/upload_tasks.dart';
import 'package:island/models/drive_task.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
@@ -76,7 +78,7 @@ class FileDetailScreen extends HookConsumerWidget {
}, [animationController]);
return AppScaffold(
isNoBackground: true,
isNoBackground: false,
appBar: AppBar(
elevation: 0,
leading: IconButton(
@@ -86,26 +88,47 @@ class FileDetailScreen extends HookConsumerWidget {
title: Text(item.name.isEmpty ? 'File Details' : item.name),
actions: _buildAppBarActions(context, ref, showInfoSheet),
),
body: AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Row(
children: [
// Main content area
Expanded(child: _buildContent(context, ref, serverUrl)),
// Animated drawer panel
if (isWide)
SizedBox(
height: double.infinity,
width: animation.value * 400, // Max width of 400px
child: Container(
child:
animation.value > 0.1
? FileInfoSheet(item: item, onClose: showInfoSheet)
: const SizedBox.shrink(),
body: LayoutBuilder(
builder: (context, constraints) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Stack(
children: [
// Main content area - resizes with animation
Positioned(
left: 0,
top: 0,
bottom: 0,
width: constraints.maxWidth - animation.value * 400,
child: _buildContent(context, ref, serverUrl),
),
),
],
// Animated drawer panel - overlays
if (isWide)
Positioned(
right: 0,
top: 0,
bottom: 0,
width: 400,
child: Transform.translate(
offset: Offset((1 - animation.value) * 400, 0),
child: SizedBox(
width: 400,
child: Material(
color:
Theme.of(context).colorScheme.surfaceContainer,
elevation: 8,
child: FileInfoSheet(
item: item,
onClose: showInfoSheet,
),
),
),
),
),
],
);
},
);
},
),
@@ -187,6 +210,9 @@ class FileDetailScreen extends HookConsumerWidget {
}
Future<void> _downloadFile(WidgetRef ref) async {
final taskId = ref
.read(uploadTasksProvider.notifier)
.addLocalDownloadTask(item);
try {
showSnackBar('Downloading file...');
@@ -202,14 +228,34 @@ class FileDetailScreen extends HookConsumerWidget {
'/drive/files/${item.id}',
filePath,
queryParameters: {'original': true},
onReceiveProgress: (count, total) {
if (total > 0) {
ref
.read(uploadTasksProvider.notifier)
.updateDownloadProgress(taskId, count, total);
ref
.read(uploadTasksProvider.notifier)
.updateTransmissionProgress(taskId, count / total);
}
},
);
await FileSaver.instance.saveFile(
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
file: File(filePath),
);
ref
.read(uploadTasksProvider.notifier)
.updateTaskStatus(taskId, DriveTaskStatus.completed);
showSnackBar('File saved to downloads');
} catch (e) {
ref
.read(uploadTasksProvider.notifier)
.updateTaskStatus(
taskId,
DriveTaskStatus.failed,
errorMessage: e.toString(),
);
showErrorAlert(e);
}
}

View File

@@ -226,7 +226,7 @@ class AccountName extends StatelessWidget {
children: [
Flexible(
child: Text(
account.nick,
textOverride ?? account.nick,
style: nameStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,

View File

@@ -1,7 +1,7 @@
import 'dart:io';
import 'dart:math' as math;
import 'dart:ui';
import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -266,68 +266,57 @@ class GenericFileContent extends HookConsumerWidget {
}
return Center(
child: Container(
margin: const EdgeInsets.all(32),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.insert_drive_file,
size: 64,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Symbols.insert_drive_file,
size: 64,
const Gap(16),
Text(
item.name,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const Gap(8),
Text(
formatFileSize(item.size),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const Gap(16),
Text(
item.name,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
const Gap(24),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton.icon(
onPressed: downloadFile,
icon: const Icon(Symbols.download),
label: Text('download').tr(),
),
textAlign: TextAlign.center,
),
const Gap(8),
Text(
formatFileSize(item.size),
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
const Gap(16),
OutlinedButton.icon(
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder: (context) => FileInfoSheet(item: item),
);
},
icon: const Icon(Symbols.info),
label: Text('info').tr(),
),
),
const Gap(24),
Row(
mainAxisSize: MainAxisSize.min,
children: [
FilledButton.icon(
onPressed: downloadFile,
icon: const Icon(Symbols.download),
label: Text('download'),
),
const Gap(16),
OutlinedButton.icon(
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder: (context) => FileInfoSheet(item: item),
);
},
icon: const Icon(Symbols.info),
label: Text('info'),
),
],
),
],
),
],
),
],
),
);
}

View File

@@ -1,10 +1,7 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/pods/network.dart';
import 'package:island/talker.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
@@ -28,28 +25,12 @@ class _UniversalVideoState extends ConsumerState<UniversalVideo> {
VideoController? _videoController;
void _openVideo() async {
final url = widget.uri;
MediaKit.ensureInitialized();
_player = Player();
_videoController = VideoController(_player!);
String? uri;
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) {
talker.info('[MediaPlayer] Miss cache: $url');
final token = ref.watch(tokenProvider)?.token;
DefaultCacheManager().downloadFile(
url,
authHeaders: {'Authorization': 'AtField $token'},
);
uri = url;
} else {
uri = inCacheInfo.file.path;
talker.info('[MediaPlayer] Hit cache: $url');
}
_player!.open(Media(uri), play: widget.autoplay);
_player!.open(Media(widget.uri), play: widget.autoplay);
}
@override

View File

@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -31,7 +30,6 @@ class UploadOverlay extends HookConsumerWidget {
final isVisibleOverride = useState<bool?>(null);
final pendingHide = useState(false);
final hideTimer = useState<Timer?>(null);
final isVisible =
(isVisibleOverride.value ?? activeTasks.isNotEmpty) &&
!pendingHide.value;
@@ -53,24 +51,6 @@ 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();
@@ -86,7 +66,6 @@ class UploadOverlay extends HookConsumerWidget {
position: slideAnimation,
child: _UploadOverlayContent(
activeTasks: activeTasks,
onVisibilityChanged: (bool? v) => isVisibleOverride.value = v,
).padding(bottom: 16 + MediaQuery.of(context).padding.bottom),
),
);
@@ -95,15 +74,12 @@ class UploadOverlay extends HookConsumerWidget {
class _UploadOverlayContent extends HookConsumerWidget {
final List<DriveTask> activeTasks;
final void Function(bool?) onVisibilityChanged;
const _UploadOverlayContent({
required this.activeTasks,
required this.onVisibilityChanged,
});
const _UploadOverlayContent({required this.activeTasks});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isExpanded = useState(false);
final animationController = useAnimationController(
duration: const Duration(milliseconds: 200),
initialValue: 0.0,
@@ -117,15 +93,12 @@ 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]);
@@ -349,6 +322,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
IconData _getOverallStatusIcon(List<DriveTask> tasks) {
if (tasks.isEmpty) return Symbols.upload;
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
final hasInProgress = tasks.any(
(task) => task.status == DriveTaskStatus.inProgress,
);
@@ -370,6 +344,9 @@ class _UploadOverlayContent extends HookConsumerWidget {
// Priority order: in progress > pending > paused > failed > completed
if (hasInProgress) {
if (hasDownload) {
return Symbols.download;
}
return Symbols.upload;
} else if (hasPending) {
return Symbols.schedule;
@@ -387,6 +364,7 @@ class _UploadOverlayContent extends HookConsumerWidget {
String _getOverallStatusText(List<DriveTask> tasks) {
if (tasks.isEmpty) return '0 tasks';
final hasDownload = tasks.any((task) => task.type == 'FileDownload');
final hasInProgress = tasks.any(
(task) => task.status == DriveTaskStatus.inProgress,
);
@@ -408,7 +386,11 @@ class _UploadOverlayContent extends HookConsumerWidget {
// Priority order: in progress > pending > paused > failed > completed
if (hasInProgress) {
return '${tasks.length} ${'uploading'.tr()}';
if (hasDownload) {
return '${tasks.length} ${'downloading'.tr()}';
} else {
return '${tasks.length} ${'uploading'.tr()}';
}
} else if (hasPending) {
return '${tasks.length} ${'pending'.tr()}';
} else if (hasPaused) {
@@ -550,7 +532,10 @@ class _UploadTaskTileState extends State<UploadTaskTile>
color = Theme.of(context).colorScheme.secondary;
break;
case DriveTaskStatus.inProgress:
icon = Symbols.upload;
icon =
widget.task.type == 'FileDownload'
? Symbols.download
: Symbols.upload;
color = Theme.of(context).colorScheme.primary;
break;
case DriveTaskStatus.paused: