import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:video_player/video_player.dart'; import '../../../common/config/app_h5_urls.dart'; import '../../../common/services/device_header_service.dart'; import '../../../common/theme/jinzhi_theme.dart'; import '../../../common/widgets/adaptive.dart'; import '../../../common/router/app_router.dart'; class PrivacyConsentPage extends StatefulWidget { const PrivacyConsentPage({super.key}); @override State createState() => _PrivacyConsentPageState(); } class _PrivacyConsentPageState extends State { bool _dialogShown = false; late final VideoPlayerController _video; bool _videoReady = false; @override void initState() { super.initState(); _video = VideoPlayerController.asset('assets/launch/launch-bg.mp4') ..setLooping(true) ..setVolume(0) ..initialize().then((_) { if (!mounted) return; _video.setPlaybackSpeed(0.5); _video.play(); setState(() => _videoReady = true); }); } @override void dispose() { _video.dispose(); super.dispose(); } Future _agree() async { await DeviceHeaderService.to.markPrivacyAgreed(); await DeviceHeaderService.to.ensureInitialized(); if (mounted) context.go(AppRoutes.assets); } Future _enter() async { await DeviceHeaderService.to.markIosStartupSeen(); if (mounted) context.go(AppRoutes.assets); } @override Widget build(BuildContext context) { const cs = jinzhiDarkScheme; final showAndroidConsent = DeviceHeaderService.to.privacyConsentRequired; if (showAndroidConsent && !_dialogShown) { _dialogShown = true; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; showModalBottomSheet( context: context, isScrollControlled: true, isDismissible: false, enableDrag: false, backgroundColor: Colors.transparent, barrierColor: Colors.black.withValues(alpha: 0.55), builder: (_) => _PrivacyConsentSheet(onAgree: _agree), ); }); } return AnnotatedRegion( value: SystemUiOverlayStyle.light, child: Scaffold( backgroundColor: cs.background, body: Stack( fit: StackFit.expand, children: [ if (_videoReady) FittedBox( fit: BoxFit.cover, clipBehavior: Clip.hardEdge, child: SizedBox( width: _video.value.size.width, height: _video.value.size.height, child: VideoPlayer(_video), ), ) else Image.asset('assets/launch/launch-poster.jpg', fit: BoxFit.cover), DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [0.0, 0.46, 0.88], colors: [ cs.background.withValues(alpha: 0.66), cs.background.withValues(alpha: 0.82), cs.background, ], ), ), ), SafeArea( child: Column( children: [ Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const _LaunchMark(size: 88), const SizedBox(height: 26), Text( '金值', style: TextStyle( fontSize: 42, fontWeight: FontWeight.w800, color: cs.foreground, height: 1.1, ), ), const SizedBox(height: 12), Text( '读懂全球研报', style: TextStyle( fontSize: 16, color: cs.foreground.withValues(alpha: 0.82), letterSpacing: 2, ), ), ], ), ), if (!showAndroidConsent) Padding( padding: const EdgeInsets.fromLTRB(22, 0, 22, 20), child: Center( child: ConstrainedBox( constraints: BoxConstraints( maxWidth: Adaptive.isTablet(context) ? 400 : double.infinity, ), child: GestureDetector( onTap: _enter, behavior: HitTestBehavior.opaque, child: Container( width: double.infinity, height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: cs.primary, borderRadius: BorderRadius.circular(9999), ), child: Text( '立即进入', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: cs.primaryForeground, ), ), ), ), ), ), ), Padding( padding: const EdgeInsets.only(bottom: 14), child: Text( '内容为公开研报的结构化解读,不构成投资建议', style: TextStyle( fontSize: 11.5, color: cs.mutedForeground.withValues(alpha: 0.75), ), ), ), ], ), ), ], ), ), ); } } class _LaunchMark extends StatefulWidget { const _LaunchMark({this.size = 88}); final double size; @override State<_LaunchMark> createState() => _LaunchMarkState(); } class _LaunchMarkState extends State<_LaunchMark> with SingleTickerProviderStateMixin { late final AnimationController _spin = AnimationController( vsync: this, duration: const Duration(seconds: 8), )..repeat(); @override void dispose() { _spin.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final m = widget.size; const cxf = 0.6758, cyf = 0.3242, sideF = 0.4121; final side = m * sideF; return SizedBox( width: m, height: m, child: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: Image.asset('assets/launch/mark-grid-lime.png'), ), Positioned( left: cxf * m - side / 2, top: cyf * m - side / 2, width: side, height: side, child: RotationTransition( turns: _spin, child: Image.asset('assets/launch/mark-star-lime.png'), ), ), ], ), ); } } class _PrivacyConsentSheet extends StatefulWidget { const _PrivacyConsentSheet({required this.onAgree}); final Future Function() onAgree; @override State<_PrivacyConsentSheet> createState() => _PrivacyConsentSheetState(); } class _PrivacyConsentSheetState extends State<_PrivacyConsentSheet> { bool _verify = false; @override Widget build(BuildContext context) { const cs = jinzhiDarkScheme; final privacyUrl = AppH5UrlsHelper.buildUrlWithNight( AppH5Urls.privacyUrl, false, ); final protocolUrl = AppH5UrlsHelper.buildUrlWithNight( AppH5Urls.userProtocolUrl, false, ); return SafeArea( top: false, child: Container( width: double.infinity, decoration: BoxDecoration( color: cs.card, borderRadius: const BorderRadius.vertical(top: Radius.circular(11.2)), border: Border.all(color: cs.border), ), padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: _verify ? _verifyView(cs, protocolUrl, privacyUrl) : _consentView(cs, protocolUrl, privacyUrl), ), ); } Widget _grip(ShadColorScheme cs) => Center( child: Container( width: 38, height: 4, margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cs.border, borderRadius: BorderRadius.circular(9999), ), ), ); Widget _consentView( ShadColorScheme cs, String protocolUrl, String privacyUrl, ) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _grip(cs), Text( '欢迎使用金值', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 12), _policyBody(cs, protocolUrl: protocolUrl, privacyUrl: privacyUrl), const SizedBox(height: 20), Row( children: [ Expanded( child: _Btn( label: '不同意', primary: false, cs: cs, onTap: () => setState(() => _verify = true), ), ), const SizedBox(width: 12), Expanded( child: _Btn( label: '同意', primary: true, cs: cs, onTap: () async { Navigator.of(context).pop(); await widget.onAgree(); }, ), ), ], ), ], ); } Widget _verifyView( ShadColorScheme cs, String protocolUrl, String privacyUrl, ) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ _grip(cs), Text( '确认放弃使用?', style: TextStyle( fontSize: 19, fontWeight: FontWeight.w700, color: cs.foreground, ), ), const SizedBox(height: 12), _policyBody(cs, protocolUrl: protocolUrl, privacyUrl: privacyUrl), const SizedBox(height: 20), Row( children: [ Expanded( child: _Btn( label: '不同意并退出', primary: false, cs: cs, onTap: () => SystemNavigator.pop(), ), ), const SizedBox(width: 12), Expanded( child: _Btn( label: '同意', primary: true, cs: cs, onTap: () async { Navigator.of(context).pop(); await widget.onAgree(); }, ), ), ], ), ], ); } Widget _policyBody( ShadColorScheme cs, { required String protocolUrl, required String privacyUrl, }) { return Text.rich( TextSpan( style: TextStyle(fontSize: 13, height: 1.7, color: cs.mutedForeground), children: [ const TextSpan( text: '尊敬的用户:\n\n我们非常重视您的个人信息和隐私保护,为了更好的保障您的个人权益,请您在使用我们的产品前,仔细阅读并充分理解 ', ), _link('《用户协议》', protocolUrl, cs), const TextSpan(text: ' 和 '), _link('《隐私政策》', privacyUrl, cs), const TextSpan( text: '内容。我们将按照该协议内容收集、使用和共享您的个人信息。\n为了保证业务安全风控,在您使用我们基本功能的过程中,我们会收集您的手机号码,以及硬件序列号或唯一设备识别码(如 AndroidID/MAC/OAID/IMEI/WIFI 的 BSSID)等信息。\n如您同意,请点击“同意”开始接受我们的服务。', ), ], ), ); } TextSpan _link(String text, String url, ShadColorScheme cs) => TextSpan( text: text, style: TextStyle( color: cs.accentForeground, fontWeight: FontWeight.w500, decoration: TextDecoration.underline, decorationColor: cs.accentForeground, ), recognizer: TapGestureRecognizer() ..onTap = () => launchUrlString( AppH5UrlsHelper.withCacheBuster(url), mode: LaunchMode.inAppBrowserView, ), ); } class _Btn extends StatelessWidget { const _Btn({ required this.label, required this.primary, required this.cs, required this.onTap, }); final String label; final bool primary; final ShadColorScheme cs; final VoidCallback onTap; @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( height: 52, alignment: Alignment.center, decoration: BoxDecoration( color: primary ? cs.primary : cs.secondary, borderRadius: BorderRadius.circular(12), border: primary ? null : Border.all(color: cs.border), ), child: Text( label, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: primary ? cs.primaryForeground : cs.foreground, ), ), ), ); } }