/// 相册分享页(UI-03 §4 页面 19 重设计;原 UI-01 分享对话框升级为全屏 /// 分享页,文件名保留 share_qr_dialog.dart 兼容既有引用)。 /// /// 手机全屏页(平板内容居中收窄为卡片,同一布局代码两条宽度分支): /// 真实封面(可缺省占位)→ 名称/日期地点 → 现场生成二维码(白底突出, /// data 为观众链接 `{VIEWER_BASE}/a/{token}?lang=`)→ 扫码说明 → Copy /// link(主操作,复制同一链接)→ Open in browser(次操作,经 url_launcher /// 外链打开)。相册未公开时以琥珀提示明确「该链接目前无法公开浏览」, /// 不写「任何人可查看」;URL 文本可长按选中复制(复制失败的选择文本 /// 回退),但公开 token 不作主视觉编号。 library; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; import '../core/config.dart'; import '../features/albums/album_format.dart'; import '../features/albums/models.dart'; import '../l10n/app_localizations.dart'; import '../ui/components/photo_thumbnail.dart'; import '../ui/tokens.dart'; /// 外链打开注入点(生产=url_launcher;测试 override)。 typedef UrlLauncher = Future Function(String url); /// 观众分享链接语言参数(viewer i18n 仅认 zh-CN/en;App locale 前缀映射)。 String shareLangParam(String localeName) => localeName.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en'; /// 分享链接(二维码与复制同一 URL):`{VIEWER_BASE}/a/{token}?lang=`。 String shareAlbumUrl(Album album, String localeName) => ApiConfig.viewerAlbumUrl( album.publicToken, lang: shareLangParam(localeName), ); /// Provider:外部浏览器打开 URL(LaunchMode.externalApplication——浏览器 /// 才能承载观众页的全部交互,内置 WebView 不适用)。 final urlLauncherProvider = Provider( (ref) => (url) => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication), ); /// 观众分享页([album] 来自相册详情;手机全屏 / 宽屏内容居中卡片)。 class ShareQrPage extends ConsumerWidget { const ShareQrPage({required this.album, super.key}); final Album album; Future _copy(BuildContext context, String url) async { await Clipboard.setData(ClipboardData(text: url)); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context).copiedToClipboard), ), ); } Future _openInBrowser( BuildContext context, WidgetRef ref, String url, ) async { final launched = await ref.read(urlLauncherProvider)(url); if (!launched || !context.mounted) return; Navigator.of(context).pop(); } @override Widget build(BuildContext context, WidgetRef ref) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final localeName = l10n.localeName; final url = shareAlbumUrl(album, localeName); final venue = albumDisplayVenue(album, localeName); final start = formatAlbumDateTime(album.startAt, album.timezone, localeName); final end = album.endAt == null ? null : formatAlbumDateTime(album.endAt!, album.timezone, localeName); final dateRange = end == null ? start : '$start ~ $end'; final meta = venue == null ? dateRange : '$dateRange · $venue'; return Scaffold( appBar: AppBar(title: Text(l10n.shareAlbum)), body: SingleChildScrollView( child: Center( // 宽屏(平板)内容收窄居中成卡片;手机全宽卡片。 child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: Card( margin: const EdgeInsets.all(kSpace16), child: Padding( padding: const EdgeInsets.all(kSpace16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 真实封面(无封面同尺寸中性占位)。 ClipRRect( borderRadius: BorderRadius.circular(kRadiusInput), child: PhotoThumbnail( url: album.cover?.url, aspectRatio: 16 / 9, ), ), const SizedBox(height: kSpace16), Text( albumDisplayTitle(album, localeName), textAlign: TextAlign.center, style: theme.textTheme.titleLarge, ), const SizedBox(height: kSpace4), Text( meta, textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: kSpace16), // 二维码(白底突出,暗色主题保持扫码对比度)。 Center( child: Container( color: Colors.white, padding: const EdgeInsets.all(kSpace12), child: QrImageView( data: url, version: QrVersions.auto, size: 216, backgroundColor: Colors.white, ), ), ), const SizedBox(height: kSpace12), Text( l10n.shareScanHint, textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: kSpace8), // 链接文本(可长按选中复制;弱化呈现,不作主视觉)。 SelectableText( url, textAlign: TextAlign.center, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), if (!album.publicEnabled) ...[ const SizedBox(height: kSpace12), Container( padding: const EdgeInsets.all(kSpace12), decoration: BoxDecoration( color: theme.brightness == Brightness.light ? kColorWaiting.withValues(alpha: 0.12) : kColorWaitingDark.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(kRadiusBadge), ), child: Row( children: [ Icon( Icons.warning_amber_rounded, size: 18, color: theme.brightness == Brightness.light ? kColorWaiting : kColorWaitingDark, ), const SizedBox(width: kSpace8), Expanded( child: Text( l10n.shareNotPublic, style: theme.textTheme.bodySmall?.copyWith( color: theme.brightness == Brightness.light ? kColorWaiting : kColorWaitingDark, ), ), ), ], ), ), ], const SizedBox(height: kSpace16), // Copy link 主操作;浏览器打开为次级入口。 FilledButton.icon( onPressed: () => _copy(context, url), icon: const Icon(Icons.copy_outlined, size: 18), label: Text(l10n.copyLink), ), const SizedBox(height: kSpace8), OutlinedButton.icon( onPressed: () => _openInBrowser(context, ref, url), icon: const Icon(Icons.open_in_new, size: 18), label: Text(l10n.openInBrowser), ), ], ), ), ), ), ), ), ); } }