feat(app): 海报弹层内中/English 切换——二维码海报支持英文版

- 弹层顶部 SegmentTabs(light) 两档,默认跟 App 语言前缀;只影响海报内容
  与二维码 ?lang=,不动 App 语言设置
- 提示文案经 lookupAppLocalizations 按海报语言取(arb 中英双全,零硬编码)
- 标题/场地/地址复用相册多语言回退链按海报语言取
- QrImageView 补 semanticsLabel(读屏原本无名,兼作测试断言口)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-29 11:04:13 +08:00
parent c68e7115e4
commit a02d7a5e98
2 changed files with 67 additions and 7 deletions
+30 -6
View File
@@ -27,6 +27,7 @@ import '../features/albums/album_format.dart';
import '../features/albums/models.dart';
import '../l10n/app_localizations.dart';
import '../ui/components/photo_thumbnail.dart';
import '../ui/components/segment_tabs.dart';
import '../ui/tokens.dart';
/// 外链打开注入点(生产=url_launcher;测试 override)。
@@ -325,6 +326,9 @@ class ShareQrPage extends ConsumerWidget {
/// 二维码海报弹层:宣传图(banner→cover 兜底)+ 标题 + 时间 + 地址 + 二维码;
/// 「保存海报图」= RepaintBoundary 3x 渲染 PNG → 相册(gal)。
///
/// 弹层内「中文/English」切换海报语言(默认跟 App 语言):内容文案/相册
/// 字段/二维码 `?lang=` 随切换,App 语言设置与弹层 chrome 不受影响。
class _PosterSheet extends ConsumerStatefulWidget {
const _PosterSheet({required this.album});
@@ -339,6 +343,12 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> {
String? _bannerUrl;
bool _saving = false;
/// 海报语言('zh'|'en';打开时刻快照 App 语言前缀,切换只影响海报)。
late String _posterLang =
AppLocalizations.of(context).localeName.toLowerCase().startsWith('zh')
? 'zh'
: 'en';
@override
void initState() {
super.initState();
@@ -377,9 +387,10 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
final localeName = l10n.localeName;
final url = shareAlbumUrl(widget.album, localeName);
final title = albumDisplayTitle(widget.album, localeName);
// 海报内容按海报语言渲染(弹层 chrome 仍跟 App 语言)。
final posterL10n = lookupAppLocalizations(Locale(_posterLang));
final url = shareAlbumUrl(widget.album, _posterLang);
final title = albumDisplayTitle(widget.album, _posterLang);
// 海报日期:日期粒度(UI06 选择器已去时间;展示同口径)。
String posterDate(DateTime utc) {
final wall = tryToAlbumZone(utc, widget.album.timezone) ?? utc.toUtc();
@@ -391,9 +402,9 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> {
? null
: posterDate(widget.album.endAt!);
final dateRange = end == null ? start : '$start ~ $end';
final venue = albumDisplayVenue(widget.album, localeName);
final venue = albumDisplayVenue(widget.album, _posterLang);
String? addressForPoster(AlbumSummary album) {
for (final t in albumDisplayTranslations(album, localeName)) {
for (final t in albumDisplayTranslations(album, _posterLang)) {
final a = t.address?.trim();
if (a != null && a.isNotEmpty) return a;
}
@@ -426,6 +437,18 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> {
),
],
),
const SizedBox(height: kSpace8),
// ── 海报语言切换:内容/二维码随之,App 语言不动 ──
SegmentTabs<String>(
value: _posterLang,
onChanged: (lang) => setState(() => _posterLang = lang),
style: SegmentTabsStyle.light,
segments: [
('zh', l10n.localeChinese),
('en', l10n.localeEnglish),
],
),
const SizedBox(height: kSpace12),
// ── 海报(整块 RepaintBoundary;下载即渲染此区域)──
RepaintBoundary(
key: _posterKey,
@@ -520,12 +543,13 @@ class _PosterSheetState extends ConsumerState<_PosterSheet> {
version: QrVersions.auto,
size: 200,
backgroundColor: Colors.white,
semanticsLabel: url,
),
),
const SizedBox(height: kSpace12),
Center(
child: Text(
l10n.shareScanHint,
posterL10n.shareScanHint,
style: theme.textTheme.bodySmall?.copyWith(
color: const Color(0xFF69778E),
),
+37 -1
View File
@@ -4,7 +4,8 @@
/// 可选中链接)、链接携带正确相册 token 与 `?lang=` 语言参数、Copy link
/// 主操作写剪贴板 + 已复制提示、Open in browser 次操作经注入替身启动器
/// 打开并退出页面、未公开相册显示「该链接目前无法公开浏览」琥珀提示
/// (公开相册不显示)。
/// (公开相册不显示)、海报弹层内「中文/English」切换(默认跟 App 语言,
/// 提示文案/相册标题/二维码 lang 随切换,弹层 chrome 仍跟 App 语言)。
library;
import 'package:flutter/material.dart';
@@ -85,6 +86,41 @@ void main() {
expect(find.text('2026/9/18'), findsOneWidget);
});
testWidgets('海报语言切换:默认跟 App 语言,切中文后提示/标题/二维码随之', (
tester,
) async {
await pumpPage(tester);
await tester.tap(find.text('Generate QR poster'));
await tester.pumpAndSettle();
// 默认 English 档(App 语言 en):英文提示 + lang=en 的二维码
// (qr_flutter 4.1 无公开 data getter,经 semanticsLabel 断言编码内容)。
expect(find.text('Scan to view photos'), findsOneWidget);
expect(
tester.widget<QrImageView>(find.byType(QrImageView)).semanticsLabel,
'http://10.0.2.2:5175/a/$token?lang=en',
reason: '二维码 lang 跟海报语言(默认=App 语言前缀)',
);
// 两档切换(语言名以自身语言书写,与语言选择面板同约定)。
expect(find.text('中文'), findsOneWidget);
expect(find.text('English'), findsOneWidget);
// 英文档标题取英文翻译(中文翻译不出现在海报上)。
expect(find.text('开幕晚宴'), findsNothing);
// 切「中文」:提示/标题换中文、二维码 lang=zh-CN。
await tester.tap(find.text('中文'));
await tester.pumpAndSettle();
expect(find.text('扫码查看照片'), findsOneWidget);
expect(find.text('Scan to view photos'), findsNothing);
expect(find.text('开幕晚宴'), findsOneWidget);
expect(
tester.widget<QrImageView>(find.byType(QrImageView)).semanticsLabel,
'http://10.0.2.2:5175/a/$token?lang=zh-CN',
);
// 弹层 chrome(标题/下载按钮)不随海报语言切换,仍跟 App 语言。
expect(find.text('Download poster image'), findsOneWidget);
});
testWidgets('Copy link(主操作)→ 剪贴板收到带 lang 的 URL + 已复制提示',
(tester) async {
final clipboardCalls = <MethodCall>[];