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:
@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../core/api_client.dart';
|
||||
import '../features/uploads/engine_scope.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'albums_tab.dart';
|
||||
import 'queue_tab.dart';
|
||||
|
||||
/// 主壳:底部导航(相册 / 队列 / 设置)。
|
||||
class HomePage extends StatefulWidget {
|
||||
@@ -54,45 +56,6 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传队列(桩)。Task 9:状态机展示、筛选与重试;当前仅提供
|
||||
/// 服务端上传历史入口(A19 跨设备记录)。
|
||||
class QueueTab extends StatelessWidget {
|
||||
const QueueTab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n.queue)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Text(
|
||||
l10n.queueStubHint,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pushNamed('/server-history'),
|
||||
icon: const Icon(Icons.history_outlined),
|
||||
label: Text(l10n.serverHistoryEntry),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置页(P3 Task 3:修改密码入口 + 退出登录;Task 10 补语言切换持久化)。
|
||||
class SettingsTab extends ConsumerWidget {
|
||||
const SettingsTab({super.key});
|
||||
@@ -100,6 +63,8 @@ class SettingsTab extends ConsumerWidget {
|
||||
Future<void> _logout(BuildContext context, WidgetRef ref) async {
|
||||
// api.logout 尽力而为(服务端撤销失败也照常本地清会话——客户端内已处理)。
|
||||
await ref.read(apiClientProvider).logout();
|
||||
// 会话账号失效 → 引擎 stop(任务保留库内,见 engine_scope.dart)。
|
||||
await ref.read(sessionAccountProvider.notifier).refresh();
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user