feat(mobile): UI-03 capture and upload pages (13-16)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
/// 上传中心(UI-01 §4.4,P3 计划 Task 9 基础上重构)。
|
||||
/// 上传中心(UI-03 页面 14/15:上传中心 + 异常任务视图)。
|
||||
///
|
||||
/// 顶部摘要卡(筛选范围标注「相册·时间·本设备」+ 四组任务数:已完成/
|
||||
/// 上传中/等待中/需关注——按任务数统计,单任务百分比才走字节进度)→
|
||||
/// 筛选栏(相册下拉 × 时间快捷 × 状态分组 chips:全部/进行中/需关注/
|
||||
/// 已完成)→ 任务行 = 缩略图 + 文件名 + 状态徽章(StateBadge 图标+文案+
|
||||
/// 页头「Uploads」(24sp)+ 右上「History」入口(→ 服务端记录页,与
|
||||
/// 本机队列数据源区分)。摘要卡(范围标注「相册·时间·本设备」+ 四组
|
||||
/// 任务数:已完成(放大突出)/上传中/等待中/需关注——按任务数统计,
|
||||
/// 单任务百分比才走字节进度;**不随状态筛选变口径**,异常视图下摘要
|
||||
/// 仍显示全量计数)。主筛选 = 大号三段(All / Active / Needs attention);
|
||||
/// 次级筛选(相册 × 时间 × 已完成)收进筛选图标菜单(BottomSheet)。
|
||||
/// 任务行 = 缩略图 + 文件名 + 相册 + 状态徽章(StateBadge 图标+文案+
|
||||
/// 颜色三重表达)+ 状态专属文案/操作(§4.4 表格:确认中不显示已公开、
|
||||
/// 准备失败走重新选择而非普通重试、终态走「移除记录」并明确不删云端
|
||||
/// 照片)+ 传输进度(进度总线行级订阅)。存在可重试失败时底部
|
||||
/// 「一键重试」条(仅作用于可重试任务,文案说明范围)。
|
||||
/// 照片)+ 传输进度(进度总线行级订阅,不整页刷新)。存在可重试失败时
|
||||
/// 底部「重试可恢复项」条(仅作用于可重试任务,文案说明范围)。
|
||||
///
|
||||
/// 列表经 TaskStore 写通知 + 200ms 防抖实时刷新(连拍风暴不逐条重建)。
|
||||
library;
|
||||
@@ -18,7 +21,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../features/albums/album_format.dart';
|
||||
import '../features/albums/models.dart';
|
||||
import '../features/albums/providers.dart';
|
||||
import '../features/uploads/engine_scope.dart';
|
||||
import '../features/uploads/enqueue.dart';
|
||||
@@ -46,19 +48,25 @@ class QueueTab extends ConsumerWidget {
|
||||
final retryable = counts[UploadTaskState.retryableFailed] ?? 0;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// 24sp 主标题(theme titleLarge)+ 右上「History」链接(§4 页面14)。
|
||||
title: Text(l10n.queue),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: l10n.serverHistoryEntry,
|
||||
icon: const Icon(Icons.history_outlined),
|
||||
onPressed: () => Navigator.of(context).pushNamed('/server-history'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: kSpace8),
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pushNamed('/server-history'),
|
||||
icon: const Icon(Icons.history_outlined, size: 18),
|
||||
label: Text(l10n.uploadsHistory),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: const Column(
|
||||
children: [
|
||||
_SummaryCard(),
|
||||
_FilterBar(),
|
||||
_PrimaryFilterBar(),
|
||||
_NeedsAttentionNote(),
|
||||
Expanded(child: _TaskListView()),
|
||||
],
|
||||
),
|
||||
@@ -69,7 +77,8 @@ class QueueTab extends ConsumerWidget {
|
||||
|
||||
// ── 摘要卡 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 顶部摘要:筛选范围标注 + 四组任务数(SQL 聚合,与列表同口径)。
|
||||
/// 顶部摘要:筛选范围标注 + 四组任务数(SQL 聚合,与列表同口径;**不受
|
||||
/// 状态筛选影响**——页面 15 异常视图下仍显示全量计数,配说明行)。
|
||||
class _SummaryCard extends ConsumerWidget {
|
||||
const _SummaryCard();
|
||||
|
||||
@@ -88,8 +97,8 @@ class _SummaryCard extends ConsumerWidget {
|
||||
: albums.where((a) => a.id == filter.albumId).firstOrNull;
|
||||
final scope = [
|
||||
if (album != null) albumDisplayTitle(album, l10n.localeName),
|
||||
_FilterBar._timeLabel(l10n, filter.time),
|
||||
l10n.thisDevice,
|
||||
_queueTimeLabel(l10n, filter.time),
|
||||
l10n.uploadsThisDevice,
|
||||
].join(' · ');
|
||||
|
||||
final light = theme.brightness == Brightness.light;
|
||||
@@ -119,23 +128,26 @@ class _SummaryCard extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: kSpace8),
|
||||
// 已完成突出(§4 页面 14:摘要突出已完成数)。
|
||||
_SummaryCount(
|
||||
value: completedSummaryOf(counts),
|
||||
label: l10n.uploadsCompleted,
|
||||
color: light ? kColorSuccess : kColorSuccessDark,
|
||||
emphasized: true,
|
||||
),
|
||||
const SizedBox(height: kSpace4),
|
||||
Wrap(
|
||||
spacing: kSpace16,
|
||||
runSpacing: kSpace4,
|
||||
children: [
|
||||
_SummaryCount(
|
||||
value: completedSummaryOf(counts),
|
||||
label: l10n.queueSummaryCompleted,
|
||||
color: light ? kColorSuccess : kColorSuccessDark,
|
||||
),
|
||||
_SummaryCount(
|
||||
value: uploadingSummaryOf(counts),
|
||||
label: l10n.queueSummaryUploading,
|
||||
label: l10n.uploadsUploading,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
_SummaryCount(
|
||||
value: waitingSummaryOf(counts),
|
||||
label: l10n.queueSummaryWaiting,
|
||||
label: l10n.uploadsWaiting,
|
||||
color: light ? kColorWaiting : kColorWaitingDark,
|
||||
),
|
||||
_SummaryCount(
|
||||
@@ -152,17 +164,20 @@ class _SummaryCard extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 摘要单项:数字(主文字色、表格数字)+ 标签(语义色)。
|
||||
/// 摘要单项:数字(主文字色、表格数字)+ 标签(语义色);[emphasized]
|
||||
/// 档用于突出的「已完成」(titleLarge)。
|
||||
class _SummaryCount extends StatelessWidget {
|
||||
const _SummaryCount({
|
||||
required this.value,
|
||||
required this.label,
|
||||
required this.color,
|
||||
this.emphasized = false,
|
||||
});
|
||||
|
||||
final int value;
|
||||
final String label;
|
||||
final Color color;
|
||||
final bool emphasized;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -174,9 +189,12 @@ class _SummaryCount extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'$value',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
style: (emphasized
|
||||
? theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
)
|
||||
: theme.textTheme.titleMedium)
|
||||
?.copyWith(fontFeatures: const [FontFeature.tabularFigures()]),
|
||||
),
|
||||
const SizedBox(width: kSpace4),
|
||||
Text(label, style: theme.textTheme.bodySmall?.copyWith(color: color)),
|
||||
@@ -187,9 +205,86 @@ class _SummaryCount extends StatelessWidget {
|
||||
|
||||
// ── 筛选栏 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 筛选栏:相册下拉 + 时间快捷 chips + 状态分组 chips(§4.4 四组)。
|
||||
class _FilterBar extends ConsumerWidget {
|
||||
const _FilterBar();
|
||||
/// 时间快捷标签(「今天」与摘要口径共用一词,§4 页面 14 摘要「Today」)。
|
||||
String _queueTimeLabel(AppLocalizations l10n, QueueTimeFilter time) =>
|
||||
switch (time) {
|
||||
QueueTimeFilter.all => l10n.queueTimeAll,
|
||||
QueueTimeFilter.today => l10n.uploadsToday,
|
||||
QueueTimeFilter.last7Days => l10n.queueTimeLast7Days,
|
||||
QueueTimeFilter.last30Days => l10n.queueTimeLast30Days,
|
||||
};
|
||||
|
||||
/// 主筛选(§4 页面 14):大号三段 All / Active / Needs attention;次级
|
||||
/// 筛选(相册 × 时间 × 已完成)收进筛选图标的 BottomSheet。
|
||||
class _PrimaryFilterBar extends ConsumerWidget {
|
||||
const _PrimaryFilterBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final filter = ref.watch(queueFilterProvider);
|
||||
|
||||
return Material(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(kSpace16, kSpace8, kSpace8, kSpace8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SegmentedButton<QueueStateFilter>(
|
||||
// 已完成经筛选菜单选择时三段不亮(属次级筛选,非主段)。
|
||||
selected: filter.state == QueueStateFilter.completed
|
||||
? const <QueueStateFilter>{}
|
||||
: <QueueStateFilter>{filter.state},
|
||||
emptySelectionAllowed: true,
|
||||
showSelectedIcon: false,
|
||||
style: SegmentedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(44),
|
||||
padding: const EdgeInsets.symmetric(horizontal: kSpace12),
|
||||
),
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: QueueStateFilter.all,
|
||||
label: Text(l10n.uploadsAll),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: QueueStateFilter.inProgress,
|
||||
label: Text(l10n.uploadsActive),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: QueueStateFilter.needsAttention,
|
||||
label: Text(l10n.uploadsNeedsAttention),
|
||||
),
|
||||
],
|
||||
onSelectionChanged: (selection) => ref
|
||||
.read(queueFilterProvider.notifier)
|
||||
.setStateGroup(selection.first),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.uploadsFilters,
|
||||
icon: const Icon(Icons.tune),
|
||||
onPressed: () => _openFilterSheet(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openFilterSheet(BuildContext context) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (_) => const _FilterSheet(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 次级筛选面板(相册 × 时间快捷 × 已完成):单选即选即生效并收起。
|
||||
class _FilterSheet extends ConsumerWidget {
|
||||
const _FilterSheet();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -198,125 +293,131 @@ class _FilterBar extends ConsumerWidget {
|
||||
final filter = ref.watch(queueFilterProvider);
|
||||
final albums = ref.watch(albumListProvider).items;
|
||||
|
||||
return Material(
|
||||
color: theme.colorScheme.surfaceContainerLow,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
void close() => Navigator.of(context).pop();
|
||||
// 相册组值:'' = 全部相册(选中相册不在列表时回退全部)。
|
||||
final albumValue =
|
||||
albums.any((a) => a.id == filter.albumId) ? (filter.albumId ?? '') : '';
|
||||
return SafeArea(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: kSpace16),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: kSpace16),
|
||||
child: Text(
|
||||
l10n.uploadsFilters,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
// 相册(单选即选即生效并收起)。
|
||||
RadioGroup<String>(
|
||||
groupValue: albumValue,
|
||||
onChanged: (albumId) {
|
||||
ref
|
||||
.read(queueFilterProvider.notifier)
|
||||
.setAlbum(albumId == '' ? null : albumId);
|
||||
close();
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.filter_list,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: _AlbumDropdown(filter: filter, albums: albums),
|
||||
RadioListTile<String>(
|
||||
value: '',
|
||||
title: Text(l10n.queueAlbumFilterAll),
|
||||
),
|
||||
for (final album in albums)
|
||||
RadioListTile<String>(
|
||||
value: album.id,
|
||||
title: Text(
|
||||
albumDisplayTitle(album, l10n.localeName),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final (i, time) in QueueTimeFilter.values.indexed) ...[
|
||||
if (i > 0) const SizedBox(width: 8),
|
||||
ChoiceChip(
|
||||
label: Text(_timeLabel(l10n, time)),
|
||||
selected: filter.time == time,
|
||||
onSelected: (_) =>
|
||||
ref.read(queueFilterProvider.notifier).setTime(time),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(indent: kSpace16, endIndent: kSpace16),
|
||||
// 时间快捷(今天为摘要默认口径,§4 页面 14)。
|
||||
RadioGroup<QueueTimeFilter>(
|
||||
groupValue: filter.time,
|
||||
onChanged: (time) {
|
||||
if (time != null) {
|
||||
ref.read(queueFilterProvider.notifier).setTime(time);
|
||||
}
|
||||
close();
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
for (final time in QueueTimeFilter.values)
|
||||
RadioListTile<QueueTimeFilter>(
|
||||
value: time,
|
||||
title: Text(_queueTimeLabel(l10n, time)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final (i, state) in QueueStateFilter.values.indexed) ...[
|
||||
if (i > 0) const SizedBox(width: 8),
|
||||
ChoiceChip(
|
||||
label: Text(_stateLabel(l10n, state)),
|
||||
selected: filter.state == state,
|
||||
onSelected: (_) => ref
|
||||
.read(queueFilterProvider.notifier)
|
||||
.setStateGroup(state),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(indent: kSpace16, endIndent: kSpace16),
|
||||
// 已完成(主三段之外的状态组,收进筛选菜单——§4 页面 14)。
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
filter.state == QueueStateFilter.completed
|
||||
? Icons.check_circle
|
||||
: Icons.check_circle_outline,
|
||||
color: filter.state == QueueStateFilter.completed
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
title: Text(l10n.queueStateCompleted),
|
||||
onTap: () {
|
||||
ref.read(queueFilterProvider.notifier).setStateGroup(
|
||||
filter.state == QueueStateFilter.completed
|
||||
? QueueStateFilter.all
|
||||
: QueueStateFilter.completed,
|
||||
);
|
||||
close();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static String _stateLabel(AppLocalizations l10n, QueueStateFilter state) =>
|
||||
switch (state) {
|
||||
QueueStateFilter.all => l10n.queueStateAll,
|
||||
QueueStateFilter.inProgress => l10n.queueStateInProgress,
|
||||
QueueStateFilter.needsAttention => l10n.queueStateNeedsAttention,
|
||||
QueueStateFilter.completed => l10n.queueStateCompleted,
|
||||
};
|
||||
|
||||
static String _timeLabel(AppLocalizations l10n, QueueTimeFilter time) =>
|
||||
switch (time) {
|
||||
QueueTimeFilter.all => l10n.queueTimeAll,
|
||||
QueueTimeFilter.today => l10n.queueTimeToday,
|
||||
QueueTimeFilter.last7Days => l10n.queueTimeLast7Days,
|
||||
QueueTimeFilter.last30Days => l10n.queueTimeLast30Days,
|
||||
};
|
||||
}
|
||||
|
||||
/// 相册下拉(全部相册 + 相册列表缓存;选中相册不在列表时退回全部)。
|
||||
class _AlbumDropdown extends ConsumerWidget {
|
||||
const _AlbumDropdown({required this.filter, required this.albums});
|
||||
|
||||
final QueueFilter filter;
|
||||
final List<AlbumSummary> albums;
|
||||
/// 异常视图说明行(页面 15):Needs attention 主段选中时常驻,说明列表
|
||||
/// 已过滤而摘要仍是全量口径。
|
||||
class _NeedsAttentionNote extends ConsumerWidget {
|
||||
const _NeedsAttentionNote();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final knownIds = albums.map((a) => a.id).toSet();
|
||||
final value = filter.albumId != null && knownIds.contains(filter.albumId)
|
||||
? filter.albumId
|
||||
: null;
|
||||
return DropdownButton<String?>(
|
||||
value: value,
|
||||
isExpanded: true,
|
||||
underline: const SizedBox.shrink(),
|
||||
hint: Text(l10n.queueAlbumFilterAll),
|
||||
items: [
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text(
|
||||
l10n.queueAlbumFilterAll,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
final theme = Theme.of(context);
|
||||
final state = ref.watch(
|
||||
queueFilterProvider.select((filter) => filter.state),
|
||||
);
|
||||
if (state != QueueStateFilter.needsAttention) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(kSpace16, kSpace8, kSpace16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.filter_alt_outlined,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
for (final album in albums)
|
||||
DropdownMenuItem<String?>(
|
||||
value: album.id,
|
||||
const SizedBox(width: kSpace8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
albumDisplayTitle(album, l10n.localeName),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
l10n.uploadsShowingNeedsAttention,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (albumId) =>
|
||||
ref.read(queueFilterProvider.notifier).setAlbum(albumId),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -329,6 +430,9 @@ class _TaskListView extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final stateFilter = ref.watch(
|
||||
queueFilterProvider.select((filter) => filter.state),
|
||||
);
|
||||
final tasksAsync = ref.watch(queueTasksProvider);
|
||||
return tasksAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
@@ -339,9 +443,14 @@ class _TaskListView extends ConsumerWidget {
|
||||
),
|
||||
data: (tasks) {
|
||||
if (tasks.isEmpty) {
|
||||
// 页面 15:异常视图空态专用文案(区别于整体队列空态)。
|
||||
return EmptyState(
|
||||
icon: Icons.cloud_upload_outlined,
|
||||
title: l10n.queueEmpty,
|
||||
icon: stateFilter == QueueStateFilter.needsAttention
|
||||
? Icons.task_alt_outlined
|
||||
: Icons.cloud_upload_outlined,
|
||||
title: stateFilter == QueueStateFilter.needsAttention
|
||||
? l10n.uploadsNoNeedsAttention
|
||||
: l10n.queueEmpty,
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
@@ -354,7 +463,7 @@ class _TaskListView extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 单条任务行(§4.4 状态表逐态落地)。
|
||||
/// 单条任务行(§4 页面 14 状态表逐态落地)。
|
||||
class _TaskTile extends ConsumerWidget {
|
||||
const _TaskTile({required this.task});
|
||||
|
||||
@@ -466,7 +575,8 @@ class _TaskTile extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 徽章语义变体(§4.4:进行=主色、等待=琥珀、失败=红、完成=绿、取消=灰)。
|
||||
/// 徽章语义变体(§4 页面 14:进行=主色、等待=琥珀、失败=红、完成=绿、
|
||||
/// 取消=灰)。
|
||||
static BadgeVariant _badgeVariant(UploadTaskState state) =>
|
||||
switch (state) {
|
||||
UploadTaskState.preparing ||
|
||||
@@ -484,12 +594,12 @@ class _TaskTile extends ConsumerWidget {
|
||||
static String _badgeLabel(AppLocalizations l10n, UploadTaskState state) =>
|
||||
switch (state) {
|
||||
UploadTaskState.preparing => l10n.queueRowPreparing,
|
||||
UploadTaskState.preparationFailed => l10n.queueRowFileUnavailable,
|
||||
UploadTaskState.queued => l10n.queueRowWaitingUpload,
|
||||
UploadTaskState.preparationFailed => l10n.uploadsFileUnavailable,
|
||||
UploadTaskState.queued => l10n.uploadsWaitingUpload,
|
||||
UploadTaskState.uploading => l10n.taskStateUploading,
|
||||
UploadTaskState.uploadedConfirming => l10n.queueRowConfirming,
|
||||
UploadTaskState.uploadedConfirming => l10n.uploadsConfirming,
|
||||
UploadTaskState.completed => l10n.taskStateCompleted,
|
||||
UploadTaskState.waitingNetwork => l10n.taskStateWaitingNetwork,
|
||||
UploadTaskState.waitingNetwork => l10n.uploadsWaitingNetwork,
|
||||
UploadTaskState.retryableFailed => l10n.taskStateRetryableFailed,
|
||||
UploadTaskState.cancelled => l10n.taskStateCancelled,
|
||||
};
|
||||
@@ -592,7 +702,7 @@ class _TaskActions extends ConsumerWidget {
|
||||
case UploadTaskState.preparationFailed:
|
||||
return _RowAction(
|
||||
icon: Icons.image_search_outlined,
|
||||
label: l10n.queueActionSelectAgain,
|
||||
label: l10n.uploadsSelectAgain,
|
||||
onPressed: account == null
|
||||
? null
|
||||
: () => _retryPreparation(context, ref, account),
|
||||
@@ -628,7 +738,7 @@ class _TaskActions extends ConsumerWidget {
|
||||
case UploadTaskState.cancelled:
|
||||
return _RowAction(
|
||||
icon: Icons.delete_outline,
|
||||
label: l10n.queueActionRemoveRecord,
|
||||
label: l10n.uploadsRemoveRecord,
|
||||
foreground: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
onPressed: account == null
|
||||
? null
|
||||
@@ -678,7 +788,7 @@ class _TaskActions extends ConsumerWidget {
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: Text(l10n.queueRemoveRecordConfirm),
|
||||
child: Text(l10n.uploadsRemoveRecordConfirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -731,7 +841,8 @@ class _RowAction extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 底部批量重试条(存在可重试失败任务时出现;仅作用于 retryableFailed)。
|
||||
/// 底部批量重试条(存在可重试失败任务时出现;仅作用于 retryableFailed,
|
||||
/// 按钮文案「重试可恢复项」明确范围——§4 页面 15)。
|
||||
class _BatchRetryBar extends ConsumerWidget {
|
||||
const _BatchRetryBar();
|
||||
|
||||
@@ -770,7 +881,7 @@ class _BatchRetryBar extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
l10n.queueRetryAllScope,
|
||||
l10n.uploadsRetryEligibleScope,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
@@ -784,7 +895,7 @@ class _BatchRetryBar extends ConsumerWidget {
|
||||
foregroundColor: theme.colorScheme.onError,
|
||||
),
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: Text(l10n.queueRetryAllFailed),
|
||||
label: Text(l10n.uploadsRetryEligible),
|
||||
onPressed: () => _retryAll(ref),
|
||||
),
|
||||
],
|
||||
@@ -801,9 +912,9 @@ class _BatchRetryBar extends ConsumerWidget {
|
||||
if (account == null) return;
|
||||
final engine = ref.read(uploadEngineProvider);
|
||||
final tasks = await ref.read(taskStoreProvider).listByAccount(
|
||||
account,
|
||||
states: const {UploadTaskState.retryableFailed},
|
||||
);
|
||||
account,
|
||||
states: const {UploadTaskState.retryableFailed},
|
||||
);
|
||||
for (final task in tasks) {
|
||||
await engine.retryNow(account, task.id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user