2026-09-21 17:20:43 +08:00
|
|
|
|
/// 分享二维码对话框(UI-01 §4.5 分层重构,P3 计划 Task 4 基础)。
|
2026-09-19 01:56:38 +08:00
|
|
|
|
///
|
2026-09-21 17:20:43 +08:00
|
|
|
|
/// 分层:相册标题 → 二维码(白底突出,暗色主题保持扫码对比度)→
|
|
|
|
|
|
/// 链接文本(可选中)→ 操作行(复制链接为主操作,「在浏览器中打开」为
|
|
|
|
|
|
/// 次操作,经 url_launcher 外链打开)。viewer 链接构造:
|
|
|
|
|
|
/// `{VIEWER_BASE}/a/{publicToken}`([ApiConfig.viewerAlbumUrl])。
|
2026-09-19 01:56:38 +08:00
|
|
|
|
///
|
|
|
|
|
|
/// 用 [Dialog] 而非 [AlertDialog]:AlertDialog 内容经 IntrinsicWidth
|
|
|
|
|
|
/// 测量,而 QrImageView 内部使用 LayoutBuilder(不支持 intrinsic 计算)
|
2026-09-21 17:20:43 +08:00
|
|
|
|
/// 会抛布局断言,普通 Dialog 无此路径。外链打开经 Provider 注入
|
|
|
|
|
|
/// ([urlLauncherProvider]),widget 测试 override 替身免平台通道。
|
2026-09-19 01:56:38 +08:00
|
|
|
|
library;
|
|
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
|
import 'package:flutter/services.dart';
|
2026-09-21 17:20:43 +08:00
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
2026-09-19 01:56:38 +08:00
|
|
|
|
import 'package:qr_flutter/qr_flutter.dart';
|
2026-09-21 17:20:43 +08:00
|
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2026-09-19 01:56:38 +08:00
|
|
|
|
|
|
|
|
|
|
import '../core/config.dart';
|
|
|
|
|
|
import '../l10n/app_localizations.dart';
|
2026-09-21 17:20:43 +08:00
|
|
|
|
import '../ui/tokens.dart';
|
2026-09-19 01:56:38 +08:00
|
|
|
|
|
2026-09-21 17:20:43 +08:00
|
|
|
|
/// 外链打开注入点(生产=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});
|
2026-09-19 01:56:38 +08:00
|
|
|
|
|
|
|
|
|
|
final String publicToken;
|
|
|
|
|
|
|
2026-09-21 17:20:43 +08:00
|
|
|
|
/// 相册展示名(当前界面语言回退解析后的标题)。
|
|
|
|
|
|
final String? albumTitle;
|
|
|
|
|
|
|
2026-09-19 01:56:38 +08:00
|
|
|
|
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),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 17:20:43 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 01:56:38 +08:00
|
|
|
|
@override
|
2026-09-21 17:20:43 +08:00
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
2026-09-19 01:56:38 +08:00
|
|
|
|
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: [
|
2026-09-21 17:20:43 +08:00
|
|
|
|
// 分层 1:相册标题(回退通用标题)。
|
|
|
|
|
|
Text(
|
|
|
|
|
|
albumTitle ?? l10n.shareQrTitle,
|
|
|
|
|
|
style: theme.textTheme.titleLarge,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
),
|
|
|
|
|
|
const SizedBox(height: kSpace4),
|
2026-09-19 01:56:38 +08:00
|
|
|
|
Text(
|
|
|
|
|
|
l10n.shareQrTitle,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
2026-09-21 17:20:43 +08:00
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
|
|
|
|
),
|
2026-09-19 01:56:38 +08:00
|
|
|
|
),
|
2026-09-21 17:20:43 +08:00
|
|
|
|
const SizedBox(height: kSpace16),
|
|
|
|
|
|
// 分层 2:二维码(白底突出)。
|
2026-09-19 01:56:38 +08:00
|
|
|
|
Center(
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
color: Colors.white,
|
2026-09-21 17:20:43 +08:00
|
|
|
|
padding: const EdgeInsets.all(kSpace8),
|
2026-09-19 01:56:38 +08:00
|
|
|
|
child: QrImageView(
|
|
|
|
|
|
data: url,
|
|
|
|
|
|
version: QrVersions.auto,
|
|
|
|
|
|
size: 216,
|
|
|
|
|
|
backgroundColor: Colors.white,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-09-21 17:20:43 +08:00
|
|
|
|
const SizedBox(height: kSpace12),
|
|
|
|
|
|
// 分层 3:链接文本(可长按选中)。
|
2026-09-19 01:56:38 +08:00
|
|
|
|
SelectableText(
|
|
|
|
|
|
url,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
style: theme.textTheme.bodySmall,
|
|
|
|
|
|
),
|
2026-09-21 17:20:43 +08:00
|
|
|
|
const SizedBox(height: kSpace8),
|
2026-09-19 01:56:38 +08:00
|
|
|
|
Text(
|
|
|
|
|
|
l10n.qrHint,
|
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
|
|
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-09-21 17:20:43 +08:00
|
|
|
|
const SizedBox(height: kSpace12),
|
|
|
|
|
|
// 分层 4:操作行——复制为主操作,浏览器打开为次操作。
|
2026-09-19 01:56:38 +08:00
|
|
|
|
Row(
|
|
|
|
|
|
children: [
|
2026-09-21 17:20:43 +08:00
|
|
|
|
Expanded(
|
|
|
|
|
|
child: FilledButton.icon(
|
|
|
|
|
|
onPressed: () => _copy(context, url),
|
|
|
|
|
|
icon: const Icon(Icons.copy_outlined, size: 18),
|
|
|
|
|
|
label: Text(l10n.copyLink),
|
|
|
|
|
|
),
|
2026-09-19 01:56:38 +08:00
|
|
|
|
),
|
2026-09-21 17:20:43 +08:00
|
|
|
|
const SizedBox(width: kSpace8),
|
2026-09-19 01:56:38 +08:00
|
|
|
|
TextButton(
|
2026-09-21 17:20:43 +08:00
|
|
|
|
onPressed: () => _openInBrowser(context, ref, url),
|
|
|
|
|
|
child: Text(l10n.openInBrowser),
|
2026-09-19 01:56:38 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|