import 'dart:io'; import 'dart:ui' as ui; import 'package:app_settings/app_settings.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import '../../../core/utils/top_toast.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:gal/gal.dart'; import '../../../core/l10n/app_localizations.dart'; import '../../../core/network/dio_client.dart'; import '../../../core/theme/app_colors.dart'; import '../../../data/services/auth_service.dart'; import '../../../providers/app_provider.dart'; /// 邀请好友页面 class InviteFriendsScreen extends ConsumerStatefulWidget { const InviteFriendsScreen({super.key}); @override ConsumerState createState() => _InviteFriendsScreenState(); } class _InviteFriendsScreenState extends ConsumerState { String? _inviteCode; String? _inviteUrl; bool _loaded = false; int _currentPage = 0; final _pageController = PageController(); // 每张海报单独一个 key 用于截图 final _repaintKeys = [GlobalKey(), GlobalKey(), GlobalKey()]; bool _saving = false; static const _posterCount = 3; /// 各域名/语种素材缺失时的统一兜底(英文海报 1) static const String invitePosterUniversalEnFallback = 'assets/images/invite_poster_1_EN.png'; @override void initState() { super.initState(); _loadInviteData(); } @override void dispose() { _pageController.dispose(); super.dispose(); } Future _loadInviteData() async { try { final dio = ref.read(dioClientProvider); final data = await AuthService(dio).getMyInfo(); final prefix = data['promotionPrefix']?.toString() ?? ''; final code = data['promotionCode']?.toString() ?? ''; if (mounted) { setState(() { _inviteCode = code.isNotEmpty ? code : null; _inviteUrl = prefix.isNotEmpty || code.isNotEmpty ? '$prefix$code' : null; _loaded = true; }); } } catch (_) { if (mounted) setState(() => _loaded = true); } } void _copy(String text, String label) { Clipboard.setData(ClipboardData(text: text)); showTopToast(context, message: AppLocalizations.of(context)!.labelCopied(label), backgroundColor: Colors.black87); } Future _ensurePhotoPermission() async { if (await Gal.hasAccess(toAlbum: true)) return true; final granted = await Gal.requestAccess(toAlbum: true); if (granted) return true; if (!mounted) return false; final l10n = AppLocalizations.of(context)!; await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(l10n.photoPermissionTitle, style: const TextStyle(color: AppColors.brand)), content: Text(l10n.photoPermissionContent), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(), child: Text(l10n.cancel), ), TextButton( onPressed: () { Navigator.of(ctx).pop(); AppSettings.openAppSettings(); }, child: Text(l10n.goToSettings), ), ], ), ); return false; } Future _saveCurrentPoster() async { if (_saving) return; setState(() => _saving = true); try { if (!await _ensurePhotoPermission()) { return; } // 渲染海报为 PNG await WidgetsBinding.instance.endOfFrame; final key = _repaintKeys[_currentPage]; final boundary = key.currentContext!.findRenderObject() as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 3.0); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); final bytes = byteData!.buffer.asUint8List(); // 写临时文件后存入相册 final tempDir = await getTemporaryDirectory(); final file = File( '${tempDir.path}/invite_poster_${DateTime.now().millisecondsSinceEpoch}.png'); await file.writeAsBytes(bytes); await Gal.putImage(file.path); if (mounted) { showTopToast(context, message: AppLocalizations.of(context)!.saveSuccess, backgroundColor: Colors.black87); } } catch (e) { if (mounted) { showTopToast(context, message: AppLocalizations.of(context)!.saveFailed, backgroundColor: Colors.black87); } } finally { if (mounted) setState(() => _saving = false); } } String _posterSuffix() { final locale = ref.read(localeProvider); if (locale.languageCode == 'zh' && locale.countryCode == 'TW') return 'HK'; if (locale.languageCode == 'zh') return 'CN'; if (locale.languageCode == 'ja') return 'jp'; if (locale.languageCode == 'ko') return 'KR'; return 'EN'; } @override Widget build(BuildContext context) { ref.watch(localeProvider); // 语言切换时触发 rebuild final safeBottom = MediaQuery.of(context).padding.bottom; final cs = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( elevation: 0, leading: IconButton( icon: Icon(Icons.chevron_left, color: cs.onSurface, size: 28), onPressed: () => context.pop(), ), title: Text(AppLocalizations.of(context)!.inviteFriends, style: TextStyle(color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.w600)), centerTitle: true, ), body: Column( children: [ // ── 海报轮播 ────────────────────────────────── Expanded( child: PageView.builder( controller: _pageController, itemCount: _posterCount, onPageChanged: (i) => setState(() => _currentPage = i), itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: RepaintBoundary( key: _repaintKeys[index], child: _PosterPage( posterAsset: 'assets/images/invite_poster_${index + 1}_${_posterSuffix()}.png', posterAssetEn: 'assets/images/invite_poster_${index + 1}_EN.png', posterAssetUniversalEn: invitePosterUniversalEnFallback, inviteUrl: _inviteUrl, inviteCode: _inviteCode, loaded: _loaded, ), ), ); }, ), ), const SizedBox(height: 16), // ── 圆点指示器 ──────────────────────────────── Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(_posterCount, (i) { final active = i == _currentPage; return AnimatedContainer( duration: const Duration(milliseconds: 250), margin: const EdgeInsets.symmetric(horizontal: 4), width: active ? 20 : 8, height: 8, decoration: BoxDecoration( color: active ? AppColors.brand : cs.onSurface.withAlpha(60), borderRadius: BorderRadius.circular(4), ), ); }), ), const SizedBox(height: 24), // ── 操作按钮 ────────────────────────────────── Padding( padding: EdgeInsets.fromLTRB(24, 0, 24, safeBottom + 16), child: Row( children: [ Expanded( child: _ActionButton( icon: Icons.save_alt_outlined, label: AppLocalizations.of(context)!.savePoster, loading: _saving, onTap: _inviteUrl != null ? _saveCurrentPoster : null, ), ), const SizedBox(width: 12), Expanded( child: _ActionButton( icon: Icons.copy_outlined, label: AppLocalizations.of(context)!.copyInviteCode, onTap: _inviteCode != null ? () => _copy(_inviteCode!, AppLocalizations.of(context)!.inviteCode) : null, ), ), const SizedBox(width: 12), Expanded( child: _ActionButton( icon: Icons.link_outlined, label: AppLocalizations.of(context)!.copyLink, onTap: _inviteUrl != null ? () => _copy(_inviteUrl!, AppLocalizations.of(context)!.inviteLink) : null, ), ), ], ), ), ], ), ); } } // ── 单张海报页面 ─────────────────────────────────────────────── // 海报原始尺寸 1443×2778,QR 占位白框坐标(原图像素): // x: 981-1286, y: 2291-2596,约 305×305 // 转为比例: // fromRight = (1443-1286)/1443 ≈ 0.1088 // fromBottom = (2778-2596)/2778 ≈ 0.0655 // qrSize/W = 305/1443 ≈ 0.2114 class _PosterPage extends StatelessWidget { const _PosterPage({ required this.posterAsset, required this.posterAssetEn, required this.posterAssetUniversalEn, required this.inviteUrl, required this.inviteCode, required this.loaded, }); final String posterAsset; final String posterAssetEn; final String posterAssetUniversalEn; final String? inviteUrl; final String? inviteCode; final bool loaded; @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(16), child: LayoutBuilder( builder: (context, constraints) { final w = constraints.maxWidth; final h = constraints.maxHeight; // QR 码尺寸和位置按原图比例换算(海报 1443×2778,QR 占位白框 305×305) final qrSize = w * 0.2114; final right = w * 0.1088; final bottom = h * 0.0655; return Stack( children: [ // 海报背景:当前语种/域名图 → 同序号 EN → 统一 invite_poster_1_EN Image.asset( posterAsset, width: w, height: h, fit: BoxFit.fill, errorBuilder: (_, __, ___) => Image.asset( posterAssetEn, width: w, height: h, fit: BoxFit.fill, errorBuilder: (_, __, ___) => Image.asset( posterAssetUniversalEn, width: w, height: h, fit: BoxFit.fill, errorBuilder: (_, __, ___) => Container( color: const Color(0xFF111111), child: const Center( child: Icon(Icons.image_outlined, color: Colors.white30, size: 48), ), ), ), ), ), // 精准覆盖海报右下角 QR 占位框 Positioned( right: right, bottom: bottom, width: qrSize, height: qrSize, child: ClipRRect( borderRadius: BorderRadius.circular(8), child: inviteUrl != null ? QrImageView( data: inviteUrl!, version: QrVersions.auto, backgroundColor: Colors.white, padding: const EdgeInsets.all(6), ) : loaded ? const SizedBox.shrink() : const ColoredBox( color: Colors.white, child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ), ), ), ), ], ); }, ), ); } } // ── 底部操作按钮 ────────────────────────────────────────────── class _ActionButton extends StatelessWidget { const _ActionButton({ required this.icon, required this.label, this.onTap, this.loading = false, }); final IconData icon; final String label; final VoidCallback? onTap; final bool loading; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final enabled = onTap != null; final iconColor = enabled ? cs.onSurface : cs.onSurface.withAlpha(80); return GestureDetector( onTap: onTap, child: Container( height: 52, decoration: BoxDecoration( color: enabled ? cs.surfaceContainerHighest : cs.surfaceContainerHighest.withAlpha(120), borderRadius: BorderRadius.circular(12), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (loading) SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: cs.onSurface.withAlpha(140)), ) else Icon(icon, color: iconColor, size: 20), const SizedBox(height: 3), Text( label, style: TextStyle( color: enabled ? cs.onSurface.withAlpha(180) : cs.onSurface.withAlpha(80), fontSize: 11, ), ), ], ), ), ); } }