feat(mobile): UI-01 upload center, capture and auxiliary pages

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-21 17:20:43 +08:00
parent f6929ae18f
commit 1f67b7c818
26 changed files with 2144 additions and 771 deletions
@@ -20,31 +20,29 @@ import 'providers.dart';
import 'task_state.dart';
import 'task_store.dart';
/// 状态筛选分组(chip → 状态集合;「全部」= 不过滤)。
/// 状态筛选分组(UI-01 §4.4:chip → 状态集合;「全部」= 不过滤)。
enum QueueStateFilter {
all,
preparing,
queued,
uploading,
waitingNetwork,
failed,
inProgress,
needsAttention,
completed;
/// 分组对应的状态集合(空 = 不过滤)。
///
/// - 进行中 = 引擎正在推进的活任务(准备/排队/传输/确认 + 等待网络
/// ——网络等待自动恢复,属系统仍在推进的范畴,归入进行中而非需关注);
/// - 需关注 = 需要用户动作的失败(准备失败需重新选图、上传失败需重试);
/// - 已完成 = 服务端已受理的终态(cancelled 仅在「全部」可见)。
Set<UploadTaskState> get states => switch (this) {
QueueStateFilter.all => const <UploadTaskState>{},
QueueStateFilter.preparing => const <UploadTaskState>{
QueueStateFilter.inProgress => const <UploadTaskState>{
UploadTaskState.preparing,
},
QueueStateFilter.queued => const <UploadTaskState>{UploadTaskState.queued},
QueueStateFilter.uploading => const <UploadTaskState>{
UploadTaskState.queued,
UploadTaskState.uploading,
UploadTaskState.uploadedConfirming,
},
QueueStateFilter.waitingNetwork => const <UploadTaskState>{
UploadTaskState.waitingNetwork,
},
QueueStateFilter.failed => const <UploadTaskState>{
QueueStateFilter.needsAttention => const <UploadTaskState>{
UploadTaskState.preparationFailed,
UploadTaskState.retryableFailed,
},
@@ -237,6 +235,60 @@ final taskCountsProvider =
TaskCountsNotifier.new,
);
/// 摘要计数(UI-01 §4.4):**当前筛选范围内**(相册 × 时间)的任务数聚合
/// ——与列表同口径,范围标注由队列页摘要卡展示;仍走 SQL 聚合
/// ([TaskStore.countsByState]),不整表载入内存。写通知防抖刷新。
class QueueSummaryNotifier extends Notifier<Map<UploadTaskState, int>> {
StreamSubscription<void>? _changesSub;
Timer? _debounce;
@override
Map<UploadTaskState, int> build() {
final store = ref.watch(taskStoreProvider);
// 会话与筛选变化(相册/时间维度改变摘要口径)都触发重建。
ref.watch(sessionAccountProvider);
ref.watch(queueFilterProvider);
_changesSub?.cancel();
_debounce?.cancel();
_changesSub = store.changes.listen((_) => _scheduleReload());
ref.onDispose(_dispose);
Future<void>.microtask(_reload);
return <UploadTaskState, int>{
for (final state in UploadTaskState.values) state: 0,
};
}
Future<void> _reload() async {
final account = ref.read(sessionAccountProvider).value;
if (account == null) return;
final filter = ref.read(queueFilterProvider);
final counts = await ref.read(taskStoreProvider).countsByState(
account,
albumId: filter.albumId,
since: filter.time.since(DateTime.now()),
);
if (ref.mounted) state = counts;
}
void _scheduleReload() {
_debounce?.cancel();
_debounce = Timer(kQueueRefreshDebounce, _reload);
}
void _dispose() {
_changesSub?.cancel();
_changesSub = null;
_debounce?.cancel();
_debounce = null;
}
}
/// 摘要计数 Provider(当前筛选范围口径;键覆盖全部状态,无任务计 0)。
final queueSummaryProvider =
NotifierProvider<QueueSummaryNotifier, Map<UploadTaskState, int>>(
QueueSummaryNotifier.new,
);
/// 未完成(待传)计数:准备中/待传/上传中/确认中/等待网络之和。
int pendingCountOf(Map<UploadTaskState, int> counts) =>
counts[UploadTaskState.preparing]! +
@@ -250,6 +302,23 @@ int failedCountOf(Map<UploadTaskState, int> counts) =>
counts[UploadTaskState.preparationFailed]! +
counts[UploadTaskState.retryableFailed]!;
// ── 摘要行计数(UI-01 §4.4:按任务数统计,不与字节进度混口径)──────────
/// 摘要「已完成」:服务端已受理的任务数(终态记录,不含已取消)。
int completedSummaryOf(Map<UploadTaskState, int> counts) =>
counts[UploadTaskState.completed]!;
/// 摘要「上传中」:正在传输/确认的任务数。
int uploadingSummaryOf(Map<UploadTaskState, int> counts) =>
counts[UploadTaskState.uploading]! +
counts[UploadTaskState.uploadedConfirming]!;
/// 摘要「等待中」:准备/排队/等待网络的任务数(等待调度,非用户错误)。
int waitingSummaryOf(Map<UploadTaskState, int> counts) =>
counts[UploadTaskState.preparing]! +
counts[UploadTaskState.queued]! +
counts[UploadTaskState.waitingNetwork]!;
/// 「未结束」计数(底部导航「上传」角标,UI-01 §4.1):待传(
/// [pendingCountOf])+ 可重试失败——均为引擎待推进或用户需关注的活任务;
/// 准备失败/取消/完成不计入。账号隔离由 TaskStore.countsByState 硬守护。