Files
photo-live/apps/mobile/lib/features/uploads/queue_controller.dart
T

252 lines
8.2 KiB
Dart
Raw Normal View History

/// 队列 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<UploadTaskState> get states => switch (this) {
QueueStateFilter.all => const <UploadTaskState>{},
QueueStateFilter.preparing => const <UploadTaskState>{
UploadTaskState.preparing,
},
QueueStateFilter.queued => const <UploadTaskState>{
UploadTaskState.queued,
},
QueueStateFilter.uploading => const <UploadTaskState>{
UploadTaskState.uploading,
UploadTaskState.uploadedConfirming,
},
QueueStateFilter.waitingNetwork => const <UploadTaskState>{
UploadTaskState.waitingNetwork,
},
QueueStateFilter.failed => const <UploadTaskState>{
UploadTaskState.preparationFailed,
UploadTaskState.retryableFailed,
},
QueueStateFilter.completed => const <UploadTaskState>{
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<QueueFilter> {
@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, QueueFilter>(QueueFilterNotifier.new);
/// 防抖重查间隔(连拍/引擎迁移写风暴下限流)。
const Duration kQueueRefreshDebounce = Duration(milliseconds: 200);
/// 队列任务列表:筛选参数化查询 + 写通知防抖刷新。
class QueueTasksNotifier extends AsyncNotifier<List<UploadTask>> {
StreamSubscription<void>? _changesSub;
Timer? _debounce;
@override
Future<List<UploadTask>> 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 <UploadTask>[];
return _query(store, account, filter);
}
static Future<List<UploadTask>> _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<List<UploadTask>>(tasks));
});
}
void _dispose() {
_changesSub?.cancel();
_changesSub = null;
_debounce?.cancel();
_debounce = null;
}
}
/// 队列任务列表 Provider。
final queueTasksProvider =
AsyncNotifierProvider<QueueTasksNotifier, List<UploadTask>>(
QueueTasksNotifier.new,
);
/// 账号内各状态计数(连拍页徽章/批量重试条;写通知防抖刷新)。
class TaskCountsNotifier extends Notifier<Map<UploadTaskState, int>> {
StreamSubscription<void>? _changesSub;
Timer? _debounce;
@override
Map<UploadTaskState, int> build() {
final store = ref.watch(taskStoreProvider);
// 会话变化(登录/登出)也触发重建(登出清零)。
ref.watch(sessionAccountProvider);
_changesSub?.cancel();
_debounce?.cancel();
_changesSub = store.changes.listen((_) => _scheduleReload());
ref.onDispose(_dispose);
Future<void>.microtask(() => _reload());
return <UploadTaskState, int>{
for (final state in UploadTaskState.values) state: 0,
};
}
Future<void> _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, Map<UploadTaskState, int>>(
TaskCountsNotifier.new,
);
/// 未完成(待传)计数:准备中/待传/上传中/确认中/等待网络之和。
int pendingCountOf(Map<UploadTaskState, int> counts) =>
counts[UploadTaskState.preparing]! +
counts[UploadTaskState.queued]! +
counts[UploadTaskState.uploading]! +
counts[UploadTaskState.uploadedConfirming]! +
counts[UploadTaskState.waitingNetwork]!;
/// 失败计数:准备失败 + 上传失败。
int failedCountOf(Map<UploadTaskState, int> counts) =>
counts[UploadTaskState.preparationFailed]! +
counts[UploadTaskState.retryableFailed]!;