/// 队列 UI 状态层(P3 计划 Task 9)。 /// /// - [QueueFilter]:需求 4.3 三维筛选(相册 × 状态 × 时间)的**纯逻辑** /// (单测直接覆盖;与 [TaskStore.listByAccount] 的参数化查询语义一致: /// 状态分组 → states 集合、时间快捷 → since 下限); /// - [QueueFilterNotifier]:筛选状态(队列页 filter bar 消费); /// - [QueueTasksNotifier]:筛选后的任务列表——**实时刷新 = TaskStore 写 /// 通知(drift tableUpdates)+ 200ms 防抖重查**(连拍入队/引擎迁移风暴 /// 下不逐条重建 UI); /// - [TaskCountsNotifier]:账号内各状态计数(连拍页徽章 + 批量重试条, /// 同一防抖策略)。 library; import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'engine_scope.dart'; import 'providers.dart'; import 'task_state.dart'; import 'task_store.dart'; /// 状态筛选分组(chip → 状态集合;「全部」= 不过滤)。 enum QueueStateFilter { all, preparing, queued, uploading, waitingNetwork, failed, completed; /// 分组对应的状态集合(空 = 不过滤)。 Set get states => switch (this) { QueueStateFilter.all => const {}, QueueStateFilter.preparing => const { UploadTaskState.preparing, }, QueueStateFilter.queued => const { UploadTaskState.queued, }, QueueStateFilter.uploading => const { UploadTaskState.uploading, UploadTaskState.uploadedConfirming, }, QueueStateFilter.waitingNetwork => const { UploadTaskState.waitingNetwork, }, QueueStateFilter.failed => const { UploadTaskState.preparationFailed, UploadTaskState.retryableFailed, }, QueueStateFilter.completed => const { UploadTaskState.completed, }, }; } /// 时间筛选快捷(今天/近7天/近30天/全部)。 enum QueueTimeFilter { all, today, last7Days, last30Days; /// 创建时间下限(null = 不过滤;本地时刻,matches 内与 UTC 落库值 /// 比较前归一)。 DateTime? since(DateTime now) => switch (this) { QueueTimeFilter.all => null, QueueTimeFilter.today => DateTime(now.year, now.month, now.day), QueueTimeFilter.last7Days => now.subtract(const Duration(days: 7)), QueueTimeFilter.last30Days => now.subtract(const Duration(days: 30)), }; } /// 三维筛选条件(不可变值对象;纯匹配逻辑单测覆盖)。 class QueueFilter { const QueueFilter({ this.albumId, this.state = QueueStateFilter.all, this.time = QueueTimeFilter.all, }); /// 相册过滤(null = 全部相册)。 final String? albumId; /// 状态分组过滤。 final QueueStateFilter state; /// 时间快捷过滤。 final QueueTimeFilter time; /// 纯匹配(与 TaskStore.listByAccount(albumId/states/since) 查询语义 /// 一致;createdAt 为 UTC 落库值,比较前归一时区)。 bool matches(UploadTask task, {required DateTime now}) { if (albumId != null && task.albumId != albumId) return false; final states = state.states; if (states.isNotEmpty && !states.contains(task.state)) return false; final cutoff = time.since(now); if (cutoff != null && task.createdAt.isBefore(cutoff.toUtc())) return false; return true; } QueueFilter copyWith({ String? albumId, bool clearAlbum = false, QueueStateFilter? state, QueueTimeFilter? time, }) => QueueFilter( albumId: clearAlbum ? null : (albumId ?? this.albumId), state: state ?? this.state, time: time ?? this.time, ); } /// 筛选状态 Notifier。 class QueueFilterNotifier extends Notifier { @override QueueFilter build() => const QueueFilter(); void setAlbum(String? albumId) => state = albumId == null ? state.copyWith(clearAlbum: true) : state.copyWith(albumId: albumId); void setStateGroup(QueueStateFilter group) => state = state.copyWith(state: group); void setTime(QueueTimeFilter time) => state = state.copyWith(time: time); } /// 筛选状态 Provider。 final queueFilterProvider = NotifierProvider(QueueFilterNotifier.new); /// 防抖重查间隔(连拍/引擎迁移写风暴下限流)。 const Duration kQueueRefreshDebounce = Duration(milliseconds: 200); /// 队列任务列表:筛选参数化查询 + 写通知防抖刷新。 class QueueTasksNotifier extends AsyncNotifier> { StreamSubscription? _changesSub; Timer? _debounce; @override Future> build() async { final filter = ref.watch(queueFilterProvider); final account = await ref.watch(sessionAccountProvider.future); final store = ref.watch(taskStoreProvider); _changesSub?.cancel(); _debounce?.cancel(); _changesSub = store.changes.listen((_) => _scheduleReload()); ref.onDispose(_dispose); if (account == null) return const []; return _query(store, account, filter); } static Future> _query( TaskStore store, String account, QueueFilter filter, ) => store.listByAccount( account, albumId: filter.albumId, states: filter.state.states.isEmpty ? null : filter.state.states, since: filter.time.since(DateTime.now()), ); /// 写通知 → 200ms 防抖后重查(同一账号/筛选的最新快照)。 void _scheduleReload() { _debounce?.cancel(); _debounce = Timer(kQueueRefreshDebounce, () { final account = ref.read(sessionAccountProvider).value; if (account == null) return; _query(ref.read(taskStoreProvider), account, ref.read(queueFilterProvider)) .then((tasks) => state = AsyncData>(tasks)); }); } void _dispose() { _changesSub?.cancel(); _changesSub = null; _debounce?.cancel(); _debounce = null; } } /// 队列任务列表 Provider。 final queueTasksProvider = AsyncNotifierProvider>( QueueTasksNotifier.new, ); /// 账号内各状态计数(连拍页徽章/批量重试条;写通知防抖刷新)。 class TaskCountsNotifier extends Notifier> { StreamSubscription? _changesSub; Timer? _debounce; @override Map build() { final store = ref.watch(taskStoreProvider); // 会话变化(登录/登出)也触发重建(登出清零)。 ref.watch(sessionAccountProvider); _changesSub?.cancel(); _debounce?.cancel(); _changesSub = store.changes.listen((_) => _scheduleReload()); ref.onDispose(_dispose); Future.microtask(() => _reload()); return { for (final state in UploadTaskState.values) state: 0, }; } Future _reload() async { final account = ref.read(sessionAccountProvider).value; if (account == null) return; final counts = await ref.read(taskStoreProvider).countsByState(account); if (ref.mounted) state = counts; } void _scheduleReload() { _debounce?.cancel(); _debounce = Timer(kQueueRefreshDebounce, _reload); } void _dispose() { _changesSub?.cancel(); _changesSub = null; _debounce?.cancel(); _debounce = null; } } /// 各状态计数 Provider(键覆盖全部状态,无任务计 0)。 final taskCountsProvider = NotifierProvider>( TaskCountsNotifier.new, ); /// 未完成(待传)计数:准备中/待传/上传中/确认中/等待网络之和。 int pendingCountOf(Map counts) => counts[UploadTaskState.preparing]! + counts[UploadTaskState.queued]! + counts[UploadTaskState.uploading]! + counts[UploadTaskState.uploadedConfirming]! + counts[UploadTaskState.waitingNetwork]!; /// 失败计数:准备失败 + 上传失败。 int failedCountOf(Map counts) => counts[UploadTaskState.preparationFailed]! + counts[UploadTaskState.retryableFailed]!;