feat(mobile): UI-01 theme system, shared components and navigation
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import 'dart:io';
|
||||
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';
|
||||
@@ -31,7 +32,8 @@ class QueueTab extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final counts = ref.watch(taskCountsProvider);
|
||||
final autoRetryable = (counts[UploadTaskState.retryableFailed] ?? 0) +
|
||||
final autoRetryable =
|
||||
(counts[UploadTaskState.retryableFailed] ?? 0) +
|
||||
(counts[UploadTaskState.waitingNetwork] ?? 0);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -40,8 +42,7 @@ class QueueTab extends ConsumerWidget {
|
||||
IconButton(
|
||||
tooltip: l10n.serverHistoryEntry,
|
||||
icon: const Icon(Icons.history_outlined),
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pushNamed('/server-history'),
|
||||
onPressed: () => Navigator.of(context).pushNamed('/server-history'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -77,10 +78,15 @@ class _FilterBar extends ConsumerWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.filter_list,
|
||||
size: 18, color: theme.colorScheme.onSurfaceVariant),
|
||||
Icon(
|
||||
Icons.filter_list,
|
||||
size: 18,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: _AlbumDropdown(filter: filter, albums: albums)),
|
||||
Expanded(
|
||||
child: _AlbumDropdown(filter: filter, albums: albums),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -93,9 +99,8 @@ class _FilterBar extends ConsumerWidget {
|
||||
ChoiceChip(
|
||||
label: Text(_timeLabel(l10n, time)),
|
||||
selected: filter.time == time,
|
||||
onSelected: (_) => ref
|
||||
.read(queueFilterProvider.notifier)
|
||||
.setTime(time),
|
||||
onSelected: (_) =>
|
||||
ref.read(queueFilterProvider.notifier).setTime(time),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
@@ -159,10 +164,9 @@ class _AlbumDropdown extends ConsumerWidget {
|
||||
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;
|
||||
final value = filter.albumId != null && knownIds.contains(filter.albumId)
|
||||
? filter.albumId
|
||||
: null;
|
||||
return DropdownButton<String?>(
|
||||
value: value,
|
||||
isExpanded: true,
|
||||
@@ -224,8 +228,7 @@ class _TaskListView extends ConsumerWidget {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) =>
|
||||
_TaskTile(task: tasks[index]),
|
||||
itemBuilder: (context, index) => _TaskTile(task: tasks[index]),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -250,7 +253,8 @@ class _TaskTile extends ConsumerWidget {
|
||||
final albumName = album != null
|
||||
? albumDisplayTitle(album, l10n.localeName)
|
||||
: task.albumId;
|
||||
final failedLike = task.state == UploadTaskState.preparationFailed ||
|
||||
final failedLike =
|
||||
task.state == UploadTaskState.preparationFailed ||
|
||||
task.state == UploadTaskState.retryableFailed ||
|
||||
task.state == UploadTaskState.waitingNetwork;
|
||||
|
||||
@@ -258,11 +262,7 @@ class _TaskTile extends ConsumerWidget {
|
||||
leading: _Thumb(
|
||||
path: task.cleanedAt == null ? task.sourceFilePath : null,
|
||||
),
|
||||
title: Text(
|
||||
task.filename,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
title: Text(task.filename, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -276,8 +276,9 @@ class _TaskTile extends ConsumerWidget {
|
||||
albumName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -287,12 +288,13 @@ class _TaskTile extends ConsumerWidget {
|
||||
Text(
|
||||
task.attempt > 0
|
||||
? '${l10n.messageForTaskError(task.lastError!)} · '
|
||||
'${l10n.queueAttemptCount(task.attempt)}'
|
||||
'${l10n.queueAttemptCount(task.attempt)}'
|
||||
: l10n.messageForTaskError(task.lastError!),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: theme.colorScheme.error),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (task.state == UploadTaskState.uploading ||
|
||||
@@ -318,18 +320,18 @@ class _Thumb extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
Widget placeholder() => Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.image_outlined,
|
||||
size: 24,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.image_outlined,
|
||||
size: 24,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
if (path == null) return placeholder();
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
@@ -358,35 +360,33 @@ class _StateChip extends StatelessWidget {
|
||||
final theme = Theme.of(context);
|
||||
final (color, onColor) = switch (state) {
|
||||
UploadTaskState.preparing => (
|
||||
theme.colorScheme.surfaceContainerHighest,
|
||||
theme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
UploadTaskState.preparationFailed ||
|
||||
UploadTaskState.retryableFailed => (
|
||||
theme.colorScheme.errorContainer,
|
||||
theme.colorScheme.onErrorContainer
|
||||
),
|
||||
theme.colorScheme.surfaceContainerHighest,
|
||||
theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
UploadTaskState.preparationFailed || UploadTaskState.retryableFailed => (
|
||||
theme.colorScheme.errorContainer,
|
||||
theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
UploadTaskState.queued => (
|
||||
theme.colorScheme.secondaryContainer,
|
||||
theme.colorScheme.onSecondaryContainer
|
||||
),
|
||||
UploadTaskState.uploading ||
|
||||
UploadTaskState.uploadedConfirming => (
|
||||
theme.colorScheme.primaryContainer,
|
||||
theme.colorScheme.onPrimaryContainer
|
||||
),
|
||||
theme.colorScheme.secondaryContainer,
|
||||
theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
UploadTaskState.uploading || UploadTaskState.uploadedConfirming => (
|
||||
theme.colorScheme.primaryContainer,
|
||||
theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
UploadTaskState.completed => (
|
||||
theme.colorScheme.tertiaryContainer,
|
||||
theme.colorScheme.onTertiaryContainer
|
||||
),
|
||||
theme.colorScheme.tertiaryContainer,
|
||||
theme.colorScheme.onTertiaryContainer,
|
||||
),
|
||||
UploadTaskState.waitingNetwork => (
|
||||
theme.colorScheme.tertiaryContainer,
|
||||
theme.colorScheme.onTertiaryContainer
|
||||
),
|
||||
theme.colorScheme.tertiaryContainer,
|
||||
theme.colorScheme.onTertiaryContainer,
|
||||
),
|
||||
UploadTaskState.cancelled => (
|
||||
theme.colorScheme.surfaceContainerHighest,
|
||||
theme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
theme.colorScheme.surfaceContainerHighest,
|
||||
theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
};
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
@@ -458,9 +458,7 @@ class _TaskActions extends ConsumerWidget {
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: account == null
|
||||
? null
|
||||
: () => ref
|
||||
.read(uploadEngineProvider)
|
||||
.retryNow(account, task.id),
|
||||
: () => ref.read(uploadEngineProvider).retryNow(account, task.id),
|
||||
);
|
||||
case UploadTaskState.queued:
|
||||
case UploadTaskState.preparing:
|
||||
@@ -495,15 +493,11 @@ class _TaskActions extends ConsumerWidget {
|
||||
if (sourcePath == null || !context.mounted) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
await ref.read(uploadPreparerProvider).retry(
|
||||
accountId: account,
|
||||
taskId: task.id,
|
||||
sourcePath: sourcePath,
|
||||
);
|
||||
await ref
|
||||
.read(uploadPreparerProvider)
|
||||
.retry(accountId: account, taskId: task.id, sourcePath: sourcePath);
|
||||
} on Object {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.errorGeneric)),
|
||||
);
|
||||
messenger.showSnackBar(SnackBar(content: Text(l10n.errorGeneric)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,13 +535,17 @@ class _BatchRetryBar extends ConsumerWidget {
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: theme.colorScheme.onErrorContainer),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${l10n.queueStateFailed} · $failed',
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(color: theme.colorScheme.onErrorContainer),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
FilledButton.icon(
|
||||
@@ -572,7 +570,9 @@ class _BatchRetryBar extends ConsumerWidget {
|
||||
final account = ref.read(sessionAccountProvider).value;
|
||||
if (account == null) return;
|
||||
final engine = ref.read(uploadEngineProvider);
|
||||
final tasks = await ref.read(taskStoreProvider).listByAccount(
|
||||
final tasks = await ref
|
||||
.read(taskStoreProvider)
|
||||
.listByAccount(
|
||||
account,
|
||||
states: const {
|
||||
UploadTaskState.retryableFailed,
|
||||
|
||||
Reference in New Issue
Block a user