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>
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
///
|
||||
/// 生产装配:TaskStore.app()(documents/upload_tasks.sqlite)+ ApiClient
|
||||
/// (Bearer 自动注入)+ DeviceId(shared_preferences)。引擎生命周期
|
||||
/// (start(accountId)/stop,随登录态切换)由 App 壳/队列 UI 驱动——
|
||||
/// Task 9 队列 UI 落地时接线;本文件保证依赖图就绪。
|
||||
/// (start(accountId)/stop,随登录态切换)在 engine_scope.dart 绑定
|
||||
/// (Task 9 接线)。
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/api_client.dart';
|
||||
@@ -21,13 +23,55 @@ final taskStoreProvider = Provider<TaskStore>((ref) {
|
||||
return store;
|
||||
});
|
||||
|
||||
/// 单任务传输进度事件(队列 UI 行级进度条消费)。
|
||||
class UploadTaskProgress {
|
||||
const UploadTaskProgress({
|
||||
required this.taskId,
|
||||
required this.sent,
|
||||
required this.total,
|
||||
});
|
||||
|
||||
final String taskId;
|
||||
final int sent;
|
||||
final int total;
|
||||
|
||||
/// 0~1 进度分数;total 未知(0)返回 null(UI 转不确定态进度条)。
|
||||
double? get fraction =>
|
||||
total > 0 ? (sent / total).clamp(0.0, 1.0).toDouble() : null;
|
||||
}
|
||||
|
||||
/// 进度事件总线:引擎 onProgress 回调 → 广播流(行级 StreamBuilder 按
|
||||
/// taskId 过滤消费,避免整表重建风暴)。
|
||||
class UploadProgressHub {
|
||||
final StreamController<UploadTaskProgress> _controller =
|
||||
StreamController<UploadTaskProgress>.broadcast();
|
||||
|
||||
/// 进度事件流(广播;行级按 taskId 过滤)。
|
||||
Stream<UploadTaskProgress> get stream => _controller.stream;
|
||||
|
||||
/// 引擎回调挂接点(uploadEngineProvider 内接线)。
|
||||
void emit(String taskId, int sent, int total) =>
|
||||
_controller.add(UploadTaskProgress(taskId: taskId, sent: sent, total: total));
|
||||
|
||||
void dispose() => _controller.close();
|
||||
}
|
||||
|
||||
/// 进度总线 Provider。
|
||||
final uploadProgressHubProvider = Provider<UploadProgressHub>((ref) {
|
||||
final hub = UploadProgressHub();
|
||||
ref.onDispose(hub.dispose);
|
||||
return hub;
|
||||
});
|
||||
|
||||
/// 传输引擎(裸 Dio 直传 presign;并行/保留期配置见 upload_engine.dart
|
||||
/// 的 dart-define 常量)。
|
||||
/// 的 dart-define 常量;进度经 [UploadProgressHub] 广播)。
|
||||
final uploadEngineProvider = Provider<UploadEngine>((ref) {
|
||||
final hub = ref.watch(uploadProgressHubProvider);
|
||||
final engine = UploadEngine(
|
||||
ref.watch(taskStoreProvider),
|
||||
ref.watch(apiClientProvider),
|
||||
ref.watch(deviceIdProvider),
|
||||
onProgress: (taskId, sent, total) => hub.emit(taskId, sent, total),
|
||||
);
|
||||
ref.onDispose(engine.dispose);
|
||||
return engine;
|
||||
|
||||
Reference in New Issue
Block a user