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

68 lines
2.6 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.
/// 引擎生命周期绑定(P3 计划 Task 6 关注点 1,Task 9 接线)。
///
/// 登录会话账号(SessionStore.account)→ [UploadEngine.start] 的 scope:
/// - 账号在场(App 启动恢复会话 / 登录成功)→ start(accountId)——待传
/// 队列尽快恢复出队(A05);
/// - 账号离场(登出 / 401 驱逐)→ stop()——**任务行保留在库内**,账号
/// 切换安全由 TaskStore 的 accountId 硬隔离守护保障;
/// - 会话变更通知点:登录页成功、设置页登出、App 壳 401 回调,均调
/// [SessionAccountNotifier.refresh]。
///
/// [uploadEngineScopeProvider] 由 App 壳(PhotoLiveApp.build)watch 保持
/// 常驻——离开登录态即 stop,绝不遗留旧账号的周期出队。
library;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/api_client.dart';
import 'providers.dart';
import 'upload_engine.dart';
/// 会话账号 Notifier:读 SessionStore;登录/登出/401 后 refresh()。
class SessionAccountNotifier extends AsyncNotifier<String?> {
@override
Future<String?> build() async {
final session = await ref.watch(sessionStoreProvider).read();
return _normalize(session?.account);
}
/// 重读会话(登录成功/登出/401 驱逐后调用)。
Future<void> refresh() async {
final store = ref.read(sessionStoreProvider);
state = const AsyncLoading<String?>();
state = await AsyncValue.guard(() async {
final session = await store.read();
return _normalize(session?.account);
});
}
/// 空串归一为 null(未登录语义;引擎 scope 判空依据)。
static String? _normalize(String? account) =>
(account == null || account.isEmpty) ? null : account;
}
/// 会话账号 Provider(引擎 scope 与队列/连拍 UI 的 accountId 来源)。
final sessionAccountProvider =
AsyncNotifierProvider<SessionAccountNotifier, String?>(
SessionAccountNotifier.new,
);
/// 引擎生命周期绑定:账号在场 → start;离场 → stop(任务保留库内)。
///
/// 返回当前 scope 账号(null=引擎停止);仅被 App 壳 watch 时生效
/// (Provider 惰性求值)。
final uploadEngineScopeProvider = Provider<String?>((ref) {
final account = ref.watch(sessionAccountProvider).value;
final engine = ref.watch(uploadEngineProvider);
if (account != null) {
engine.start(account);
} else {
engine.stop();
}
ref.onDispose(engine.stop);
return account;
});
/// 供文档引用的类型锚点([UploadEngine] 生命周期语义见 upload_engine.dart)。
typedef EngineLifecycleDoc = UploadEngine;