Files
photo-live/apps/mobile/lib/features/uploads/queue_l10n.dart
T
浅念 955f9b13c1 feat(mobile): upload queue ui and continuous capture page
Task 9 (P3 plan): queue tab replaces the stub, continuous capture page
enqueues per shot, album detail gains an upload FAB, and the upload
engine lifecycle is now bound to the login session.

- Engine scope: session account drives UploadEngine start/stop; wired
  from app boot, login success, logout, and 401 eviction. Tasks stay in
  the store across logout (accountId hard isolation).
- Queue tab: album x state x time filter bar, task rows with thumbnail,
  state chip, localized error copy, attempt count, per-row progress via
  a broadcast progress hub, state-dependent actions (re-pick retry,
  retryNow, cancel, delete), batch retry bar, 200ms-debounced live
  refresh from drift table updates.
- Capture page: camera preview via injected CaptureSource, one enqueue
  per shutter press, persistent album/category/badges overlay with a
  switch-only-affects-new-shots hint, gallery multi-pick, exit confirm
  while uploads are pending, permission-denied and init-failure views.
- POST_NOTIFICATIONS runtime permission via a small MethodChannel
  (photolive/permissions) + PermissionPlugin.kt: single runtime
  permission need, avoids permission_handler dependency; requested once
  on first enqueue, silent degradation when denied or unavailable.
- Album detail FAB: capture-and-upload / pick-from-gallery sheet.
- l10n: ~50 new keys in zh/en arbs; tests: +45 (filter matrix, engine
  scope lifecycle, permission gate, queue tab, capture page, FAB entry);
  existing page tests updated to inject session/task-store fakes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 04:47:31 +08:00

76 lines
2.9 KiB
Dart

/// 任务状态/上传域错误码 → 本地化文案(P3 计划 Task 9)。
///
/// 队列/连拍页专用扩展:契约错误码复用 core/error_l10n 的映射,此处
/// 补齐上传域客户端伪码(准备阶段 [PrepErrorCodes]、引擎源缺失伪码)
/// 与队列高频契约码(ALBUM_UPLOAD_DISABLED 等)。
library;
import '../../core/error_codes.dart';
import '../../core/error_l10n.dart';
import '../../l10n/app_localizations.dart';
import '../albums/models.dart';
import 'preparer.dart';
import 'task_state.dart';
import 'upload_engine.dart';
extension AppLocalizationsUploadX on AppLocalizations {
/// 任务状态标签(chip/文案)。
String labelForTaskState(UploadTaskState state) => switch (state) {
UploadTaskState.preparing => taskStatePreparing,
UploadTaskState.preparationFailed => taskStatePreparationFailed,
UploadTaskState.queued => taskStateQueued,
UploadTaskState.uploading => taskStateUploading,
UploadTaskState.uploadedConfirming => taskStateUploadedConfirming,
UploadTaskState.completed => taskStateCompleted,
UploadTaskState.waitingNetwork => taskStateWaitingNetwork,
UploadTaskState.retryableFailed => taskStateRetryableFailed,
UploadTaskState.cancelled => taskStateCancelled,
};
/// 任务 lastError → 展示文案(上传域伪码 + 契约码;未映射回退
/// error_l10n 的通用解析)。
String messageForTaskError(String code) {
switch (code) {
case PrepErrorCodes.sourceUnreadable:
return errorPrepSourceUnreadable;
case PrepErrorCodes.unsupportedType:
return errorPrepUnsupportedType;
case PrepErrorCodes.tooLarge:
return errorPrepTooLarge;
case PrepErrorCodes.copyFailed:
return errorPrepCopyFailed;
case UploadEngine.sourceMissingCode:
return errorUploadSourceMissing;
case ErrorCodes.albumUploadDisabled:
return errorAlbumUploadDisabled;
case ErrorCodes.uploadSessionConflict:
return errorUploadSessionConflict;
case ErrorCodes.uploadVerifyFailed:
return errorUploadVerifyFailed;
default:
return messageForErrorCode(code);
}
}
}
/// 相册展示标题解析:当前 locale → 相册默认语言 → 首条 → 相册 ID。
///
/// locale 匹配按语言前缀(App locale `zh`/`en` vs 契约 `zh-CN`/`en`)。
String albumDisplayTitle(AlbumSummary album, String localeName) {
final lang = localeName.split('-').first.toLowerCase();
AlbumTranslation? match;
for (final t in album.translations) {
if (t.locale.toLowerCase() == localeName.toLowerCase() ||
t.locale.toLowerCase().split('-').first == lang) {
match = t;
break;
}
}
if (match != null) return match.title;
for (final t in album.translations) {
if (t.locale == album.defaultLocale) return t.title;
}
if (album.translations.isNotEmpty) return album.translations.first.title;
return album.id;
}