show upload progress from toolbar popover
This commit is contained in:
@@ -659,12 +659,14 @@ class GuangyaAPI {
|
||||
String? parentID,
|
||||
String contentType = 'application/octet-stream',
|
||||
int chunkSize = 5 * 1024 * 1024,
|
||||
void Function(int sent, int total)? onProgress,
|
||||
}) async {
|
||||
final bytes = await file.readAsBytes();
|
||||
final name = file.uri.pathSegments.isEmpty
|
||||
? file.path.split(Platform.pathSeparator).last
|
||||
: Uri.decodeComponent(file.uri.pathSegments.last);
|
||||
final size = bytes.length;
|
||||
onProgress?.call(0, size);
|
||||
|
||||
Map<String, dynamic> token;
|
||||
if (size < 1024 * 1024) {
|
||||
@@ -676,8 +678,13 @@ class GuangyaAPI {
|
||||
md5: md5Base64,
|
||||
);
|
||||
final taskID = _findStringDeep(token, ['taskId', 'task_id']);
|
||||
if (taskID == null) return token;
|
||||
return uploadInfo(taskID);
|
||||
if (taskID == null) {
|
||||
onProgress?.call(size, size);
|
||||
return token;
|
||||
}
|
||||
final result = await uploadInfo(taskID);
|
||||
onProgress?.call(size, size);
|
||||
return result;
|
||||
}
|
||||
|
||||
token = await uploadToken(name: name, fileSize: size, parentID: parentID);
|
||||
@@ -688,7 +695,9 @@ class GuangyaAPI {
|
||||
final canFlash = await checkCanFlashUpload(taskID, gcid);
|
||||
if (_findBoolDeep(canFlash, ['canFlashUpload', 'can_flash_upload']) ==
|
||||
true) {
|
||||
return uploadInfo(taskID);
|
||||
final result = await uploadInfo(taskID);
|
||||
onProgress?.call(size, size);
|
||||
return result;
|
||||
}
|
||||
|
||||
final tokenData = _findMapDeep(token, ['data']) ?? token;
|
||||
@@ -697,8 +706,11 @@ class GuangyaAPI {
|
||||
tokenData: tokenData,
|
||||
contentType: contentType,
|
||||
chunkSize: chunkSize,
|
||||
onProgress: onProgress,
|
||||
);
|
||||
return uploadInfo(taskID);
|
||||
final result = await uploadInfo(taskID);
|
||||
onProgress?.call(size, size);
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<String> _cdnUpload(
|
||||
@@ -706,6 +718,7 @@ class GuangyaAPI {
|
||||
required Map<String, dynamic> tokenData,
|
||||
required String contentType,
|
||||
required int chunkSize,
|
||||
void Function(int sent, int total)? onProgress,
|
||||
}) async {
|
||||
final creds =
|
||||
_findMapDeep(tokenData, ['creds']) ?? const <String, dynamic>{};
|
||||
@@ -765,6 +778,7 @@ class GuangyaAPI {
|
||||
MapEntry(number, response.headers['etag']?.replaceAll('"', '') ?? ''),
|
||||
);
|
||||
offset = end;
|
||||
onProgress?.call(offset, bytes.length);
|
||||
number += 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||
TextEditingController controller, {
|
||||
required String placeholder,
|
||||
}) => SizedBox(
|
||||
width: 280,
|
||||
width: double.infinity,
|
||||
child: ShadInput(controller: controller, placeholder: Text(placeholder)),
|
||||
);
|
||||
|
||||
@@ -359,7 +359,7 @@ class _SettingsDialogState extends ConsumerState<SettingsDialog> {
|
||||
TextEditingController controller, {
|
||||
String? placeholder,
|
||||
}) => SizedBox(
|
||||
width: 116,
|
||||
width: double.infinity,
|
||||
child: ShadInput(
|
||||
controller: controller,
|
||||
placeholder: placeholder == null ? null : Text(placeholder),
|
||||
@@ -475,37 +475,47 @@ class _SettingsSection extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.11),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 14),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.muted.withValues(alpha: 0.35),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: cs.border),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.11),
|
||||
borderRadius: BorderRadius.circular(7),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: cs.primary),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: cs.primary),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.foreground,
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.foreground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const ShadSeparator.horizontal(),
|
||||
const SizedBox(height: 4),
|
||||
child,
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const ShadSeparator.horizontal(),
|
||||
const SizedBox(height: 4),
|
||||
child,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -532,22 +542,16 @@ class _SettingsRow extends StatelessWidget {
|
||||
Text(label, style: TextStyle(fontSize: 13, color: cs.foreground)),
|
||||
],
|
||||
);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 470) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [labelWidget, const SizedBox(height: 8), child],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(children: [labelWidget, const Spacer(), child]),
|
||||
);
|
||||
},
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
labelWidget,
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(width: double.infinity, child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+163
-66
@@ -599,31 +599,21 @@ class _TopBar extends StatelessWidget {
|
||||
_SegmentButton(
|
||||
icon: Icons.folder_rounded,
|
||||
label: '光鸭云盘',
|
||||
compact: false,
|
||||
compact: true,
|
||||
selected: mode == WorkspaceMode.cloud,
|
||||
onTap: () => onModeChanged(WorkspaceMode.cloud),
|
||||
),
|
||||
_SegmentButton(
|
||||
icon: Icons.movie_rounded,
|
||||
label: '光鸭影视',
|
||||
compact: false,
|
||||
compact: true,
|
||||
selected: mode == WorkspaceMode.media,
|
||||
onTap: () => onModeChanged(WorkspaceMode.media),
|
||||
),
|
||||
ShadTooltip(
|
||||
builder: (_) => const Text('设置'),
|
||||
child: SizedBox(
|
||||
width: 34,
|
||||
height: 32,
|
||||
child: GestureDetector(
|
||||
onTap: onSettings,
|
||||
child: Icon(
|
||||
Icons.settings_rounded,
|
||||
size: 17,
|
||||
color: cs.mutedForeground,
|
||||
),
|
||||
),
|
||||
),
|
||||
_TopBarIconButton(
|
||||
tooltip: '设置',
|
||||
icon: Icons.settings_rounded,
|
||||
onTap: onSettings,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -705,46 +695,54 @@ class _SegmentButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
width: compact ? 38 : 126,
|
||||
height: compact ? 38 : 32,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? cs.secondary : Colors.transparent,
|
||||
return ShadTooltip(
|
||||
builder: (_) => Text(label),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: selected,
|
||||
label: label,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: selected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: cs.border.withValues(alpha: 0.7),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 15,
|
||||
color: selected ? cs.foreground : cs.mutedForeground,
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 160),
|
||||
width: compact ? 38 : 126,
|
||||
height: compact ? 38 : 32,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? cs.secondary : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: selected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: cs.border.withValues(alpha: 0.7),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
if (!compact) ...[
|
||||
const SizedBox(width: 7),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w600,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 15,
|
||||
color: selected ? cs.foreground : cs.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
if (!compact) ...[
|
||||
const SizedBox(width: 7),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w600,
|
||||
color: selected ? cs.foreground : cs.mutedForeground,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1519,12 +1517,20 @@ class _CloudToolbar extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ToolbarButton(
|
||||
icon: Icons.upload_rounded,
|
||||
label: '上传',
|
||||
primary: true,
|
||||
compact: compact,
|
||||
onTap: () => _pickAndUpload(ref),
|
||||
ShadPopover(
|
||||
visible: state.uploadProgress?.isActive == true,
|
||||
closeOnTapOutside: false,
|
||||
popover: (_) =>
|
||||
_UploadProgressPopover(progress: state.uploadProgress),
|
||||
child: _ToolbarButton(
|
||||
icon: Icons.upload_rounded,
|
||||
label: '上传',
|
||||
primary: true,
|
||||
compact: compact,
|
||||
onTap: state.uploadProgress?.isActive == true
|
||||
? null
|
||||
: () => _pickAndUpload(ref),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ToolbarControlGroup(
|
||||
@@ -1677,7 +1683,7 @@ class _FileViewButtons extends StatelessWidget {
|
||||
class _ToolbarButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onTap;
|
||||
final bool primary;
|
||||
final bool grouped;
|
||||
final bool selected;
|
||||
@@ -1696,6 +1702,7 @@ class _ToolbarButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
final disabled = onTap == null;
|
||||
return ShadTooltip(
|
||||
builder: (_) => Text(label),
|
||||
child: Padding(
|
||||
@@ -1713,7 +1720,7 @@ class _ToolbarButton extends StatelessWidget {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: primary
|
||||
? cs.primary
|
||||
? cs.primary.withValues(alpha: disabled ? 0.55 : 1)
|
||||
: selected
|
||||
? cs.primary.withValues(alpha: 0.14)
|
||||
: grouped
|
||||
@@ -1762,6 +1769,99 @@ class _ToolbarButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _UploadProgressPopover extends StatelessWidget {
|
||||
final UploadProgress? progress;
|
||||
|
||||
const _UploadProgressPopover({required this.progress});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = progress;
|
||||
if (value == null) return const SizedBox.shrink();
|
||||
final cs = ShadTheme.of(context).colorScheme;
|
||||
final percentage = (value.fraction * 100).round();
|
||||
return Semantics(
|
||||
label: '上传进度',
|
||||
value: '已完成 ${value.processedFiles} / ${value.totalFiles},$percentage%',
|
||||
child: SizedBox(
|
||||
width: 292,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.upload_rounded, size: 17, color: cs.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'正在上传',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.foreground,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$percentage%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: cs.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ShadProgress(
|
||||
value: value.fraction,
|
||||
minHeight: 8,
|
||||
semanticsLabel: '上传进度',
|
||||
semanticsValue: '$percentage%',
|
||||
),
|
||||
const SizedBox(height: 9),
|
||||
Text(
|
||||
value.currentFileName.isEmpty
|
||||
? '正在整理上传结果'
|
||||
: value.currentFileName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 12, color: cs.foreground),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'已完成 ${value.completedFiles} / ${value.totalFiles} · ${_formatUploadBytes(value.transferredBytes)} / ${_formatUploadBytes(value.totalBytes)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 11, color: cs.mutedForeground),
|
||||
),
|
||||
if (value.failedFiles > 0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${value.failedFiles} 个文件上传失败,队列会继续处理其余文件。',
|
||||
style: TextStyle(fontSize: 11, color: cs.destructive),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatUploadBytes(int bytes) {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
var value = bytes.toDouble();
|
||||
var unit = 0;
|
||||
while (value >= 1024 && unit < units.length - 1) {
|
||||
value /= 1024;
|
||||
unit += 1;
|
||||
}
|
||||
final precision = unit == 0 ? 0 : (value >= 100 ? 0 : 1);
|
||||
return '${value.toStringAsFixed(precision)} ${units[unit]}';
|
||||
}
|
||||
}
|
||||
|
||||
class _ToolbarControlGroup extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
@@ -3513,12 +3613,9 @@ class _SecondaryFilePaneState extends ConsumerState<_SecondaryFilePane> {
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final api = ref.read(authProvider.notifier).api;
|
||||
for (final file in files) {
|
||||
if (await file.exists()) {
|
||||
await api.fileUpload(file, parentID: parentID);
|
||||
}
|
||||
}
|
||||
await ref
|
||||
.read(fileProvider.notifier)
|
||||
.uploadLocalFiles(files, parentID: parentID);
|
||||
await _load(parentID: _currentParentID);
|
||||
} catch (e) {
|
||||
setState(() => _error = e.toString());
|
||||
|
||||
@@ -60,6 +60,36 @@ extension FileSortExt on FileSort {
|
||||
|
||||
enum SortDirection { ascending, descending }
|
||||
|
||||
class UploadProgress {
|
||||
final int totalFiles;
|
||||
final int completedFiles;
|
||||
final int failedFiles;
|
||||
final int totalBytes;
|
||||
final int transferredBytes;
|
||||
final String currentFileName;
|
||||
final bool isActive;
|
||||
|
||||
const UploadProgress({
|
||||
required this.totalFiles,
|
||||
required this.completedFiles,
|
||||
required this.failedFiles,
|
||||
required this.totalBytes,
|
||||
required this.transferredBytes,
|
||||
required this.currentFileName,
|
||||
required this.isActive,
|
||||
});
|
||||
|
||||
int get processedFiles => completedFiles + failedFiles;
|
||||
|
||||
double get fraction {
|
||||
if (totalBytes > 0) {
|
||||
return (transferredBytes / totalBytes).clamp(0.0, 1.0);
|
||||
}
|
||||
if (totalFiles == 0) return 0;
|
||||
return (processedFiles / totalFiles).clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkspaceSection {
|
||||
files('全部', 'folder'),
|
||||
recentViewed('最近查看', 'clock'),
|
||||
@@ -93,6 +123,7 @@ class FileState {
|
||||
final Set<String> selectedIDs;
|
||||
final List<CloudFile>? clipboard;
|
||||
final bool clipboardIsMove;
|
||||
final UploadProgress? uploadProgress;
|
||||
|
||||
const FileState({
|
||||
this.section = WorkspaceSection.files,
|
||||
@@ -109,6 +140,7 @@ class FileState {
|
||||
this.selectedIDs = const {},
|
||||
this.clipboard,
|
||||
this.clipboardIsMove = false,
|
||||
this.uploadProgress,
|
||||
});
|
||||
|
||||
bool get hasSelection => selectedIDs.isNotEmpty;
|
||||
@@ -132,6 +164,8 @@ class FileState {
|
||||
List<CloudFile>? clipboard,
|
||||
bool clearClipboard = false,
|
||||
bool? clipboardIsMove,
|
||||
UploadProgress? uploadProgress,
|
||||
bool clearUploadProgress = false,
|
||||
}) {
|
||||
return FileState(
|
||||
section: section ?? this.section,
|
||||
@@ -148,6 +182,9 @@ class FileState {
|
||||
selectedIDs: selectedIDs ?? this.selectedIDs,
|
||||
clipboard: clearClipboard ? null : (clipboard ?? this.clipboard),
|
||||
clipboardIsMove: clipboardIsMove ?? this.clipboardIsMove,
|
||||
uploadProgress: clearUploadProgress
|
||||
? null
|
||||
: (uploadProgress ?? this.uploadProgress),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1048,19 +1085,99 @@ class FileNotifier extends StateNotifier<FileState> {
|
||||
Future<void> uploadLocalFiles(List<File> files, {String? parentID}) async {
|
||||
if (_api == null || files.isEmpty) return;
|
||||
final targetParentID = parentID ?? _currentParentID;
|
||||
state = state.copyWith(statusMessage: '正在上传 ${files.length} 个文件…');
|
||||
final uploadFiles = <File>[];
|
||||
var totalBytes = 0;
|
||||
for (final file in files) {
|
||||
if (!await file.exists()) continue;
|
||||
uploadFiles.add(file);
|
||||
totalBytes += await file.length();
|
||||
}
|
||||
if (uploadFiles.isEmpty) {
|
||||
state = state.copyWith(errorMessage: '没有可上传的本地文件');
|
||||
return;
|
||||
}
|
||||
|
||||
UploadProgress progress({
|
||||
required int completed,
|
||||
required int failed,
|
||||
required int transferred,
|
||||
required String currentName,
|
||||
required bool active,
|
||||
}) => UploadProgress(
|
||||
totalFiles: uploadFiles.length,
|
||||
completedFiles: completed,
|
||||
failedFiles: failed,
|
||||
totalBytes: totalBytes,
|
||||
transferredBytes: transferred,
|
||||
currentFileName: currentName,
|
||||
isActive: active,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
statusMessage: '正在上传 ${uploadFiles.length} 个文件…',
|
||||
uploadProgress: progress(
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
transferred: 0,
|
||||
currentName: uploadFiles.first.uri.pathSegments.last,
|
||||
active: true,
|
||||
),
|
||||
);
|
||||
var completed = 0;
|
||||
try {
|
||||
for (final file in files) {
|
||||
if (!await file.exists()) continue;
|
||||
state = state.copyWith(
|
||||
statusMessage:
|
||||
'正在上传 ${completed + 1}/${files.length}:${file.uri.pathSegments.last}',
|
||||
var failed = 0;
|
||||
var completedBytes = 0;
|
||||
for (final file in uploadFiles) {
|
||||
final fileSize = await file.length();
|
||||
final fileName = file.uri.pathSegments.last;
|
||||
state = state.copyWith(
|
||||
statusMessage:
|
||||
'正在上传 ${completed + failed + 1}/${uploadFiles.length}:$fileName',
|
||||
uploadProgress: progress(
|
||||
completed: completed,
|
||||
failed: failed,
|
||||
transferred: completedBytes,
|
||||
currentName: fileName,
|
||||
active: true,
|
||||
),
|
||||
);
|
||||
try {
|
||||
await _api!.fileUpload(
|
||||
file,
|
||||
parentID: targetParentID,
|
||||
onProgress: (sent, total) {
|
||||
final currentTransferred =
|
||||
completedBytes + sent.clamp(0, total == 0 ? 0 : total).toInt();
|
||||
state = state.copyWith(
|
||||
uploadProgress: progress(
|
||||
completed: completed,
|
||||
failed: failed,
|
||||
transferred: currentTransferred,
|
||||
currentName: fileName,
|
||||
active: true,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
await _api!.fileUpload(file, parentID: targetParentID);
|
||||
completed += 1;
|
||||
completedBytes += fileSize;
|
||||
} catch (_) {
|
||||
failed += 1;
|
||||
completedBytes += fileSize;
|
||||
}
|
||||
state = state.copyWith(statusMessage: '已上传 $completed 个文件');
|
||||
}
|
||||
state = state.copyWith(
|
||||
statusMessage: failed == 0
|
||||
? '已上传 $completed 个文件'
|
||||
: '已上传 $completed 个文件,失败 $failed 个',
|
||||
uploadProgress: progress(
|
||||
completed: completed,
|
||||
failed: failed,
|
||||
transferred: completedBytes,
|
||||
currentName: '',
|
||||
active: false,
|
||||
),
|
||||
);
|
||||
try {
|
||||
await _invalidateFolderCaches(targetParentID);
|
||||
await loadFiles(parentID: _currentParentID);
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:guangya_flutter/providers/file_provider.dart';
|
||||
|
||||
void main() {
|
||||
test('UploadProgress reports byte-based progress when bytes are known', () {
|
||||
const progress = UploadProgress(
|
||||
totalFiles: 3,
|
||||
completedFiles: 1,
|
||||
failedFiles: 0,
|
||||
totalBytes: 400,
|
||||
transferredBytes: 100,
|
||||
currentFileName: 'movie.mkv',
|
||||
isActive: true,
|
||||
);
|
||||
|
||||
expect(progress.processedFiles, 1);
|
||||
expect(progress.fraction, 0.25);
|
||||
});
|
||||
|
||||
test('UploadProgress falls back to processed files for empty files', () {
|
||||
const progress = UploadProgress(
|
||||
totalFiles: 2,
|
||||
completedFiles: 1,
|
||||
failedFiles: 1,
|
||||
totalBytes: 0,
|
||||
transferredBytes: 0,
|
||||
currentFileName: '',
|
||||
isActive: false,
|
||||
);
|
||||
|
||||
expect(progress.processedFiles, 2);
|
||||
expect(progress.fraction, 1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user