Files
photo-live/apps/mobile/lib/pages/share_qr_dialog.dart
T
2026-09-21 17:20:43 +08:00

143 lines
5.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 分享二维码对话框(UI-01 §4.5 分层重构,P3 计划 Task 4 基础)。
///
/// 分层:相册标题 → 二维码(白底突出,暗色主题保持扫码对比度)→
/// 链接文本(可选中)→ 操作行(复制链接为主操作,「在浏览器中打开」为
/// 次操作,经 url_launcher 外链打开)。viewer 链接构造:
/// `{VIEWER_BASE}/a/{publicToken}`([ApiConfig.viewerAlbumUrl])。
///
/// 用 [Dialog] 而非 [AlertDialog]:AlertDialog 内容经 IntrinsicWidth
/// 测量,而 QrImageView 内部使用 LayoutBuilder(不支持 intrinsic 计算)
/// 会抛布局断言,普通 Dialog 无此路径。外链打开经 Provider 注入
/// ([urlLauncherProvider]),widget 测试 override 替身免平台通道。
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 '../l10n/app_localizations.dart';
import '../ui/tokens.dart';
/// 外链打开注入点(生产=url_launcher;测试 override)。
typedef UrlLauncher = Future<bool> Function(String url);
/// Provider:外部浏览器打开 URL(LaunchMode.externalApplication——浏览器
/// 才能承载观众页的全部交互,内置 WebView 不适用)。
final urlLauncherProvider = Provider<UrlLauncher>(
(ref) => (url) =>
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication),
);
/// 观众分享二维码对话框([publicToken] 来自相册详情;[albumTitle] 可选
/// 分层标题,缺省回退通用标题)。
class ShareQrDialog extends ConsumerWidget {
const ShareQrDialog({required this.publicToken, super.key, this.albumTitle});
final String publicToken;
/// 相册展示名(当前界面语言回退解析后的标题)。
final String? albumTitle;
Future<void> _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<void> _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 url = ApiConfig.viewerAlbumUrl(publicToken);
return Dialog(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 分层 1:相册标题(回退通用标题)。
Text(
albumTitle ?? l10n.shareQrTitle,
style: theme.textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: kSpace4),
Text(
l10n.shareQrTitle,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: kSpace16),
// 分层 2:二维码(白底突出)。
Center(
child: Container(
color: Colors.white,
padding: const EdgeInsets.all(kSpace8),
child: QrImageView(
data: url,
version: QrVersions.auto,
size: 216,
backgroundColor: Colors.white,
),
),
),
const SizedBox(height: kSpace12),
// 分层 3:链接文本(可长按选中)。
SelectableText(
url,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
const SizedBox(height: kSpace8),
Text(
l10n.qrHint,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: kSpace12),
// 分层 4:操作行——复制为主操作,浏览器打开为次操作。
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: () => _copy(context, url),
icon: const Icon(Icons.copy_outlined, size: 18),
label: Text(l10n.copyLink),
),
),
const SizedBox(width: kSpace8),
TextButton(
onPressed: () => _openInBrowser(context, ref, url),
child: Text(l10n.openInBrowser),
),
],
),
],
),
),
);
}
}