Files
photo-live/apps/mobile/lib/features/uploads/engine_scope.dart
T
浅念 33f347cdea feat(app): P0 batch 3 — offline detection with engine pause/resume + UI banner
P0-4 离线感知:
  - connectivity_plus → NetworkAvailability(online/offline/unknown)
  - engine_scope 三重绑定:有账号+有网 → start;任一失去 → stop(在途不中断)
  - 网络恢复 → start + onNetworkRestored(waitingNetwork 即时重试)
  - HomePage 顶部离线黄条(wifi_off + i18n 双语)

P0-5 更正:EXIF 方向已由服务端 sharp .rotate() 自动转正——v2 分析误报

397/397 全绿、analyze 0

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:23:51 +08:00

84 lines
3.4 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 '../../core/network_status.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(任务保留库内)。
///
/// P0-4 离线感知:**离线 → stop(暂停再出队,在途不受影响)**;
/// **恢复 → start + onNetworkRestored(立即重试 waitingNetwork)**。
/// 引擎与账号/网络三重绑定:有账号且有网才出队,任一失去即停。
///
/// 返回当前 scope 账号(null=引擎停止);仅被 App 壳 watch 时生效
/// (Provider 惰性求值)。
final uploadEngineScopeProvider = Provider<String?>((ref) {
final account = ref.watch(sessionAccountProvider).value;
final engine = ref.watch(uploadEngineProvider);
// P0-4:网络状态(offline 时不出队;unknown 视为有网)。
final network = ref.watch(networkStatusProvider).value;
final online = isNetworkOnline(network);
if (account != null && online) {
// 恢复在线且已有账号 → start(幂等:已 start 则只刷新 timer)。
engine.start(account);
// 网络从 offline → online 的跃变触发即时重试。
if (network == NetworkAvailability.online) {
engine.onNetworkRestored();
}
} else {
// 离线(或无账号)→ stop(不中断在途;恢复时上面的 start 恢复出队)。
engine.stop();
}
ref.onDispose(engine.stop);
return account;
});
/// 供文档引用的类型锚点([UploadEngine] 生命周期语义见 upload_engine.dart)。
typedef EngineLifecycleDoc = UploadEngine;