fix(viewer): replace text arrow share icon with SVG share glyph

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-22 13:55:37 +08:00
parent 900ff2a74d
commit b353a3de06
54 changed files with 2130 additions and 14 deletions
+117 -6
View File
@@ -16,6 +16,8 @@
/// 无变更时保存禁用。成功 pop(true) 供调用方刷新列表/详情;失败保留输入。
library;
import 'dart:io' show File;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -25,6 +27,7 @@ import '../core/error_l10n.dart';
import '../features/albums/album_format.dart';
import '../features/albums/album_form.dart';
import '../features/albums/models.dart';
import '../features/uploads/enqueue.dart';
import '../l10n/app_localizations.dart';
import '../ui/components/section_header.dart';
import '../ui/tokens.dart';
@@ -82,6 +85,15 @@ class _AlbumFormPageState extends ConsumerState<AlbumFormPage>
List<AlbumFormError> _errors = const <AlbumFormError>[];
bool _submitting = false;
/// 用户选择的封面本机文件路径(null = 未选)。
/// 创建模式:相册创建成功后上传;编辑模式:保存后上传。
String? _coverFilePath;
/// 编辑模式下的现有封面 URL(来自 AlbumSummary.cover,即时预览)。
String? get _existingCoverUrl => _coverFilePath == null
? (widget.existing?.cover?.url)
: null;
bool get _isCreate => widget.existing == null;
@override
@@ -225,17 +237,37 @@ class _AlbumFormPageState extends ConsumerState<AlbumFormPage>
final messenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
try {
late final String albumId;
if (_isCreate) {
await ref.read(apiClientProvider).createAlbum(draft.toCreateRequest());
final album = await ref
.read(apiClientProvider)
.createAlbum(draft.toCreateRequest());
albumId = album.id;
} else {
final patch = draft.toUpdatePatch(widget.existing!);
if (patch == null) {
if (patch == null && _coverFilePath == null) {
navigator.pop(false);
return;
}
await ref
.read(apiClientProvider)
.updateAlbum(widget.existing!.id, patch);
if (patch != null) {
await ref
.read(apiClientProvider)
.updateAlbum(widget.existing!.id, patch);
}
albumId = widget.existing!.id;
}
// 封面上传:相册保存成功后执行;失败不阻塞表单结果(封面可后补)。
if (_coverFilePath != null) {
try {
await ref
.read(apiClientProvider)
.uploadAlbumCoverFromFile(albumId, _coverFilePath!);
} on Object {
// 封面失败仅提示,不影响相册保存结果。
messenger.showSnackBar(
SnackBar(content: Text(l10n.coverUploadFailed)),
);
}
}
navigator.pop(true);
} on AppException catch (error) {
@@ -359,7 +391,82 @@ class _AlbumFormPageState extends ConsumerState<AlbumFormPage>
);
}
/// Details(= 创建表单):分组字段 + 校验错误汇总。
/// 封面选择器:16:9 可点击区域。编辑模式显示现有封面预览。
Widget _buildCoverPicker(AppLocalizations l10n) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.coverPicker,
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: kSpace8),
InkWell(
onTap: _submitting ? null : _pickCoverImage,
borderRadius: BorderRadius.circular(kRadiusInput),
child: Container(
height: 180,
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(kRadiusInput),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
width: 1,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(kRadiusInput),
child: _coverFilePath != null
? Image.file(
File(_coverFilePath!),
fit: BoxFit.cover,
errorBuilder: (_, _, _) => _coverPlaceholder(l10n),
)
: _existingCoverUrl != null
? Image.network(
_existingCoverUrl!,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => _coverPlaceholder(l10n),
)
: _coverPlaceholder(l10n),
),
),
),
const SizedBox(height: kSpace4),
Text(
_coverFilePath != null
? l10n.coverSelected
: l10n.coverPickerHint,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
}
Widget _coverPlaceholder(AppLocalizations l10n) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_photo_alternate_outlined,
size: 48, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: kSpace8),
Text(
l10n.coverPickerHint,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
);
Future<void> _pickCoverImage() async {
final path = await ref.read(singleImagePickerProvider)();
if (path != null) {
setState(() => _coverFilePath = path);
}
}
/// Details(= 创建表单):封面选择 + 分组字段 + 校验错误汇总。
Widget _detailsForm(AppLocalizations l10n) {
return Form(
child: SingleChildScrollView(
@@ -368,6 +475,10 @@ class _AlbumFormPageState extends ConsumerState<AlbumFormPage>
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── 封面选择(16:9 可点击区域) ─────────────────
_buildCoverPicker(l10n),
const SizedBox(height: kSpace24),
// ── 分组 1:Event details ──────────────────────────
SectionHeader(title: l10n.formEventDetails),
const SizedBox(height: kSpace12),