955f9b13c1
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>
65 lines
2.5 KiB
Dart
65 lines
2.5 KiB
Dart
/// 选图注入点 + 批量入队动作(P3 计划 Task 9)。
|
|
///
|
|
/// image_picker 的平台调用经 Provider 注入([singleImagePickerProvider]/
|
|
/// [multiImagePickerProvider]),widget 测试 override 为替身——不经平台
|
|
/// 通道即可驱动入队流;[enqueueForUpload] 为连拍页与相册详情 FAB 的共用
|
|
/// 入队路径:首次动作触发 POST_NOTIFICATIONS 请求(静默降级)+ 逐张经
|
|
/// 准备器入队(准备器内部串行复制,防连拍 IO 风暴)。
|
|
library;
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
|
|
import '../../core/notification_permission.dart';
|
|
import 'providers.dart';
|
|
import 'task_state.dart';
|
|
|
|
/// 单图选择(准备失败重试的重新选图路径);返回源文件路径或 null(取消)。
|
|
typedef SingleImagePicker = Future<String?> Function();
|
|
|
|
/// 多图选择(相册多选入队);返回源文件路径列表(空=取消)。
|
|
typedef MultiImagePicker = Future<List<String>> Function();
|
|
|
|
/// 单图选择 Provider(生产=image_picker;测试 override)。
|
|
final singleImagePickerProvider = Provider<SingleImagePicker>(
|
|
(ref) => () async {
|
|
final file = await ImagePicker().pickImage(source: ImageSource.gallery);
|
|
return file?.path;
|
|
},
|
|
);
|
|
|
|
/// 多图选择 Provider(生产=image_picker;测试 override)。
|
|
final multiImagePickerProvider = Provider<MultiImagePicker>(
|
|
(ref) => () async {
|
|
final files = await ImagePicker().pickMultiImage();
|
|
return files.map((file) => file.path).toList();
|
|
},
|
|
);
|
|
|
|
/// 批量入队:通知权限单次请求 + 逐张准备入队。
|
|
///
|
|
/// 返回成功入队(state=queued)的张数;准备失败的任务已落库为
|
|
/// preparationFailed(连拍页失败徽章/队列页可见,不在此抛出)。
|
|
Future<int> enqueueForUpload(
|
|
WidgetRef ref, {
|
|
required String accountId,
|
|
required String albumId,
|
|
String? categoryId,
|
|
required List<String> sourcePaths,
|
|
}) async {
|
|
// 首次入队动作触发(幂等;拒绝/非 Android 静默降级,见实现注释)。
|
|
await ref.read(notificationPermissionProvider).ensureRequested();
|
|
final preparer = ref.read(uploadPreparerProvider);
|
|
var enqueued = 0;
|
|
for (final path in sourcePaths) {
|
|
final task = await preparer.prepare(
|
|
accountId: accountId,
|
|
albumId: albumId,
|
|
categoryId: categoryId,
|
|
sourcePath: path,
|
|
);
|
|
if (task.state == UploadTaskState.queued) enqueued++;
|
|
}
|
|
return enqueued;
|
|
}
|