Task 8: close the gap between server-side duplicate detection (90dcc58)
and the app.
- ApiClient.mapDioException: normalize 409 wire body reusing
VALIDATION_FAILED with message=PHOTO_DUPLICATE to
ErrorCodes.photoDuplicate (message-based; auto-compat if the contract
later adds a dedicated code)
- l10n zh/en errorPhotoDuplicate; error_l10n maps the code (queue page
inherits via fallback)
- UploadEngine._classifyFailure: PHOTO_DUPLICATE joins the no-auto-retry
bucket (retrying the same init body would 409 again); force to skip
dedup travels only through the enqueue path
- UploadTasks drift table +force column (schema v2, addColumn migration;
legacy rows default false); UploadTaskDraft/UploadTask/prepare carry it
- enqueueForUpload +forcePaths; duplicate dialog "全部上传" (capture page
+ album detail page) now returns DuplicateDialogChoice so confirmed
duplicates enqueue with force=true (server adjudicates owner/admin)
- engine init body carries force only when true (absent otherwise)
Co-Authored-By: Claude Code <noreply@anthropic.com>
Parallelize the enqueueForUpload collect loop (preparer's internal
bounded semaphore from the previous task keeps IO in check). Also two
test hardenings from that task's code review: pin that failed and
thrown prepare operations release their concurrency slot (cap=1,
subsequent prepare still completes), and pin the maxConcurrentPrepares
<1 -> 1 clamp (peak 1, no deadlock).
Co-Authored-By: Claude Code <noreply@anthropic.com>
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>