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>
This commit is contained in:
@@ -14,10 +14,12 @@ import '../core/error_l10n.dart';
|
||||
import '../features/albums/album_format.dart';
|
||||
import '../features/albums/models.dart';
|
||||
import '../features/albums/providers.dart';
|
||||
import '../features/uploads/enqueue.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'album_form_page.dart';
|
||||
import 'album_members_page.dart';
|
||||
import 'album_photos_page.dart';
|
||||
import 'capture_page.dart';
|
||||
import 'share_qr_dialog.dart';
|
||||
|
||||
/// 相册详情([albumId] 路由参数;进入后 GET /albums/{id})。
|
||||
@@ -127,6 +129,87 @@ class _AlbumDetailPageState extends ConsumerState<AlbumDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 上传入口(Task 9 FAB:拍照上传 / 从相册选择) ──────────────────────
|
||||
|
||||
/// 当前登录账号(TaskStore 隔离键;详情页处于登录壳内,null 视为异常)。
|
||||
Future<String?> _currentAccountId() async {
|
||||
final session = await ref.read(sessionStoreProvider).read();
|
||||
return session?.account;
|
||||
}
|
||||
|
||||
Future<void> _openUploadSheet() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_camera_outlined),
|
||||
title: Text(l10n.uploadActionCapture),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
_openCapture();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.photo_library_outlined),
|
||||
title: Text(l10n.uploadActionPick),
|
||||
onTap: () {
|
||||
Navigator.of(sheetContext).pop();
|
||||
_pickFromGallery();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 拍照上传:进入连拍页(目标相册 + 分类选定一次)。
|
||||
Future<void> _openCapture() async {
|
||||
final album = _album;
|
||||
if (album == null) return;
|
||||
final account = await _currentAccountId();
|
||||
if (!mounted) return;
|
||||
if (account == null) return;
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => CapturePage(
|
||||
target: CaptureTarget(
|
||||
accountId: account,
|
||||
albumId: album.id,
|
||||
albumTitle: _titleOf(album),
|
||||
categories: album.categories,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 从相册选择:多选 → 逐张入队(默认分类)→ 入队数提示。
|
||||
Future<void> _pickFromGallery() async {
|
||||
final album = _album;
|
||||
if (album == null) return;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final account = await _currentAccountId();
|
||||
if (account == null) return;
|
||||
final paths = await ref.read(multiImagePickerProvider)();
|
||||
if (paths.isEmpty || !mounted) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final count = await enqueueForUpload(
|
||||
ref,
|
||||
accountId: account,
|
||||
albumId: album.id,
|
||||
sourcePaths: paths,
|
||||
);
|
||||
if (!mounted) return;
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.uploadEnqueuedCount(count))),
|
||||
);
|
||||
}
|
||||
|
||||
void _scrollToCategories() {
|
||||
final ctx = _categoriesKey.currentContext;
|
||||
if (ctx != null) {
|
||||
@@ -192,6 +275,14 @@ class _AlbumDetailPageState extends ConsumerState<AlbumDetailPage> {
|
||||
final title = album == null ? l10n.albumDetail : _titleOf(album);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(title)),
|
||||
// 上传入口(Task 9):拍照上传(连拍页)/ 从相册选择(多选入队)。
|
||||
floatingActionButton: album == null
|
||||
? null
|
||||
: FloatingActionButton.extended(
|
||||
onPressed: _openUploadSheet,
|
||||
icon: const Icon(Icons.photo_camera_outlined),
|
||||
label: Text(l10n.uploadAction),
|
||||
),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: album == null
|
||||
|
||||
Reference in New Issue
Block a user