feat(app): P0 batch 2+2.5 — local upload dedup with batch dialog, album switch check, file size guard
P0-1 层 1 本地查重:
- checkDuplicates:同账号+同相册+state=completed+filename+bytes 匹配
- EnqueueResult 返回 enqueued+duplicates+skipped
- capture_page + album_detail_page:批量单次弹窗(非逐张)三选一
[跳过重复上传非重复] [全部上传] [取消]
P0-8 相册上传开关预检:album_detail_page 入队前检查 uploadEnabled,
关闭时提示「该相册已关闭上传」+ 不入队
P1-8 文件大小预检:preparer 限单文件 ≤100MB(磁盘满由 on Object 兜底)
P0-7 更正:上传并发限制已有(kMaxParallelUploads=2),v2 分析误报
397/397 全绿、analyze 0
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -285,7 +285,7 @@ class _CapturePageState extends ConsumerState<CapturePage>
|
||||
sourcePaths: [photo.path],
|
||||
);
|
||||
// 入队成功:短暂反馈(轻触 + 200ms 浮层),不逐张弹确认(§4.5)。
|
||||
if (enqueued > 0 && mounted) _flashEnqueued();
|
||||
if (enqueued.enqueued > 0 && mounted) _flashEnqueued();
|
||||
})
|
||||
.catchError((Object _) {
|
||||
// 单张失败吞掉(连拍不中断;失败任务/错误在徽章与队列页可见)。
|
||||
@@ -308,19 +308,93 @@ class _CapturePageState extends ConsumerState<CapturePage>
|
||||
if (paths.isEmpty || !mounted) return;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final count = await enqueueForUpload(
|
||||
|
||||
// P0-1 层 1:入队前查重(同相册已完成的 filename+bytes 匹配)。
|
||||
final duplicates = await checkDuplicates(
|
||||
ref,
|
||||
accountId: widget.target.accountId,
|
||||
albumId: widget.target.albumId,
|
||||
sourcePaths: paths,
|
||||
);
|
||||
|
||||
Set<String> skipPaths;
|
||||
if (duplicates != null && duplicates.isNotEmpty && mounted) {
|
||||
// 批量确认弹窗(单次,非逐张)。
|
||||
final skip = await _showDuplicateDialog(l10n, duplicates, paths);
|
||||
if (skip == null) return; // 取消——不上传。
|
||||
skipPaths = skip;
|
||||
} else {
|
||||
skipPaths = <String>{};
|
||||
}
|
||||
|
||||
final result = await enqueueForUpload(
|
||||
ref,
|
||||
accountId: widget.target.accountId,
|
||||
albumId: widget.target.albumId,
|
||||
categoryId: _categoryId,
|
||||
sourcePaths: paths,
|
||||
skipPaths: skipPaths.toList(),
|
||||
);
|
||||
if (!mounted) return;
|
||||
final uploaded = result.enqueued;
|
||||
final skipped = result.skipped;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.uploadEnqueuedCount(count))),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
skipped > 0
|
||||
? '$uploaded 张已入队,$skipped 张重复已跳过'
|
||||
: l10n.uploadEnqueuedCount(uploaded),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 批量重复确认弹窗:返回 null=取消 / 空 Set=全部上传 / 非空=跳过项路径集。
|
||||
Future<Set<String>?> _showDuplicateDialog(
|
||||
AppLocalizations l10n,
|
||||
List<DuplicateMatch> duplicates,
|
||||
List<String> allPaths,
|
||||
) async {
|
||||
// 找出与 duplicates 匹配的路径(filename 一致即视为重复项)。
|
||||
final dupFilenames = duplicates.map((d) => d.filename).toSet();
|
||||
final dupPaths = allPaths
|
||||
.where((p) => dupFilenames.contains(p.split('/').last))
|
||||
.toSet();
|
||||
final nonDupCount = allPaths.length - dupPaths.length;
|
||||
|
||||
final choice = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('可能已上传过'),
|
||||
content: Text(
|
||||
'${duplicates.length} 张照片可能已上传到该相册'
|
||||
'${nonDupCount > 0 ? '(另 $nonDupCount 张不重复)' : ''}\n\n'
|
||||
'${duplicates.take(3).map((d) => d.filename).join('、')}'
|
||||
'${duplicates.length > 3 ? ' …' : ''}',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop('cancel'),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
if (nonDupCount > 0)
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop('skip'),
|
||||
child: Text('跳过重复,上传 $nonDupCount 张'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop('all'),
|
||||
child: Text('全部上传 ${allPaths.length} 张'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (choice == 'cancel' || choice == null) return null;
|
||||
if (choice == 'skip') return dupPaths;
|
||||
return <String>{}; // 'all' = 不跳过任何
|
||||
}
|
||||
|
||||
void _switchCategory(String? categoryId) {
|
||||
if (categoryId == _categoryId) return;
|
||||
setState(() => _categoryId = categoryId);
|
||||
|
||||
Reference in New Issue
Block a user