import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:share_plus/share_plus.dart'; import '../../../core/i18n/app_locale.dart'; import '../../../core/theme/tokens.dart'; import '../../../core/ui/app_icons.dart'; import '../../campaign/campaign_share.dart' show shareHashtags, shareLink, shareBaseUrl; import '../home_data.dart'; import '../../../core/ui/clipboard_copy.dart'; /// 브리핑 공유 — 「홈_브리핑 공유」 `266:90445` · 인스타 `272:240423` · 틱톡 `272:240430`. /// /// 별첨 1 ③-5 「카드 열람 → SNS 공유 카드 정상 노출」. 명세 `523:18300` — AI 브리핑 /// 최대 3건 · `523:18306` 인스타그램 · 틱톡 · X · 링크 복사. /// /// SNS 셋은 **카드 이미지 + 문구 + 링크**를 OS 공유 시트로 보낸다(인스타 · 틱톡은 이미지가 /// 있어야 올라간다). 링크 복사는 문구와 링크를 클립보드에 둔다. /// 그날 팬덤 브리핑으로 돌아오는 링크. 식별자는 팬덤과 KST 날짜다(상세정의 v1.6.1 「공유 /// 링크에는 원래 문맥을 복원할 수 있는 식별자를 포함」 · B-47 브리핑은 팬덤 · 당일 단위). String? briefingShareLink(String fandomId, String dayKey, {String base = shareBaseUrl}) => shareLink(['b', fandomId, dayKey], base: base); /// 공유 문구 — 제목 한 줄 · 헤드라인(최대 3) · 링크 · 해시태그(B-78 동적 구성). String briefingShareText({ required List items, String? fandomName, String? link, }) => [ 'AI DAILY BRIEFING', for (final (i, item) in items.take(3).indexed) '${i + 1}. ${item.headline}', ?link, shareHashtags(fandomName: fandomName, topic: 'AIBriefing').join(' '), ].join('\n'); /// 공유 오버레이. 캠페인 공유(`480:14594`)와 같은 틀이다. class BriefingShareDialog extends StatefulWidget { const BriefingShareDialog({ super.key, required this.items, required this.fandomName, this.link, this.share, }); final List items; final String fandomName; final String? link; /// 공유 시트 호출. 테스트가 대신 받는다 — 위젯 테스트에서 OS 공유 시트를 띄울 수 없다. final Future Function(ShareParams params)? share; static Future show( BuildContext context, { required List items, required String fandomName, String? link, Future Function(ShareParams params)? share, }) => showGeneralDialog( context: context, barrierDismissible: true, barrierLabel: context.l10n.commonClose, barrierColor: AppColors.cosmicVoid, pageBuilder: (_, _, _) => BriefingShareDialog( items: items, fandomName: fandomName, link: link, share: share, ), ); @override State createState() => _BriefingShareDialogState(); } class _BriefingShareDialogState extends State { final _card = GlobalKey(); static const _icons = [ 'assets/figma/share-instagram.png', 'assets/figma/share-tiktok.png', 'assets/figma/share-x.png', 'assets/figma/share-link.png', ]; String get _text => briefingShareText( items: widget.items, fandomName: widget.fandomName, link: widget.link, ); /// 카드 영역을 PNG 로 굽는다. 실패하면 null — 문구만이라도 보낸다. Future _capture() async { final boundary = _card.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) return null; final image = await boundary.toImage(pixelRatio: 3); final bytes = await image.toByteData(format: ui.ImageByteFormat.png); image.dispose(); if (bytes == null) return null; return XFile.fromData( bytes.buffer.asUint8List(), mimeType: 'image/png', name: 'fanuzu-briefing.png', ); } /// 공유 중. 연타로 공유 시트가 겹쳐 뜨지 않게 한다. bool _busy = false; /// [button] 은 누른 아이콘의 문맥 — iPad 공유 팝오버가 그 자리에서 뜬다. Future _onTap(int index, BuildContext button) async { if (_busy) return; final box = button.findRenderObject() as RenderBox?; final origin = box == null ? null : box.localToGlobal(Offset.zero) & box.size; final l10n = context.l10n; final messenger = ScaffoldMessenger.maybeOf(context); void toast(String message) => messenger ?..hideCurrentSnackBar() ..showSnackBar( SnackBar(content: Text(message), behavior: SnackBarBehavior.floating), ); setState(() => _busy = true); try { if (index == _icons.length - 1) { var announce = false; await copyToClipboard(_text, () => announce = true); if (!mounted) return; // 오버레이를 닫고 알린다 — 열린 채로 띄우면 스낵바가 장막 뒤에 가린다. Navigator.of(context).pop(); if (announce) { toast(widget.link == null ? l10n.briefingShareTextCopied : l10n.briefingShareCopied); } return; } final file = await _capture(); await (widget.share ?? SharePlus.instance.share)(ShareParams( text: _text, subject: 'AI DAILY BRIEFING', files: file == null ? null : [file], sharePositionOrigin: origin, )); } catch (_) { toast(l10n.commonRequestFailed); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final labels = [ context.l10n.shareInstagram, context.l10n.shareTiktok, 'X', // 웹 브릿지 주소가 없으면 링크가 없으니 「문구 복사」다. widget.link == null ? context.l10n.shareCopyText : context.l10n.shareCopyLink, ]; return Padding( padding: const EdgeInsets.fromLTRB(20, 23, 20, 22), child: Center( child: Material( color: const Color(0xFF101010), borderRadius: BorderRadius.circular(24), clipBehavior: Clip.antiAlias, child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 20, 16, 28), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Align( alignment: Alignment.centerRight, child: IconButton( tooltip: context.l10n.commonClose, onPressed: () => Navigator.of(context).maybePop(), icon: const ImageIcon( AppIcons.x, size: 24, color: AppColors.onCosmic, ), ), ), // 공유 이미지로 굽는 영역 — 제목 · 행성 · 헤드라인. RepaintBoundary( key: _card, child: ColoredBox( color: const Color(0xFF101010), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( 'AI DAILY BRIEFING', textAlign: TextAlign.center, style: TextStyle( fontFamily: AppFonts.body, fontSize: 25, fontWeight: FontWeight.w600, height: 1.4, color: AppColors.onCosmic, ), ), const SizedBox(height: 24), Center( child: Image.asset( 'assets/figma/planet-home.png', width: 120, height: 120, ), ), const SizedBox(height: 24), Container( padding: const EdgeInsets.all(AppSpace.s4), decoration: BoxDecoration( color: AppColors.cosmicSurfaceContainer, borderRadius: BorderRadius.circular(AppRadius.md), ), child: Column( children: [ for (final (i, item) in widget.items.take(3).indexed) Padding( padding: const EdgeInsets.symmetric( vertical: AppSpace.s2, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 32, child: Text('${i + 1}', style: _row), ), Expanded( child: Text(item.headline, style: _row), ), ], ), ), ], ), ), ], ), ), ), const SizedBox(height: 36), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ for (final (i, asset) in _icons.indexed) ...[ if (i > 0) const SizedBox(width: 16), Tooltip( message: labels[i], child: Builder( builder: (button) => InkResponse( onTap: () => _onTap(i, button), radius: 26, child: Image.asset(asset, width: 36, height: 36), ), ), ), ], ], ), ], ), ), ), ), ); } static const _row = TextStyle( fontFamily: AppFonts.body, fontSize: 16, fontWeight: FontWeight.w500, height: 1.5, color: AppColors.onCosmicVariant, ); }