Files
photo-live/apps/mobile/lib/features/uploads/providers.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

88 lines
2.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 上传域 Provider 装配(P3 计划 Task 6)。
///
/// 生产装配:TaskStore.app()(documents/upload_tasks.sqlite)+ ApiClient
/// (Bearer 自动注入)+ DeviceId(shared_preferences)。引擎生命周期
/// (start(accountId)/stop,随登录态切换)在 engine_scope.dart 绑定
/// (Task 9 接线)。
library;
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api_client.dart';
import '../../core/device_id.dart';
import 'preparer.dart';
import 'task_store.dart';
import 'upload_engine.dart';
/// 任务库(生产:app documents dir 持久库;测试 override 为 memory)。
final taskStoreProvider = Provider<TaskStore>((ref) {
final store = TaskStore.app();
ref.onDispose(store.close);
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 常量;进度经 [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;
});
/// 准备器(入队回调唤醒引擎立即出队)。
final uploadPreparerProvider = Provider<UploadPreparer>((ref) {
final engine = ref.watch(uploadEngineProvider);
return UploadPreparer(
ref.watch(taskStoreProvider),
onEnqueued: engine.notifyWorkAvailable,
);
});