Files
jinzhi/lib/features/settings/presentation/privacy_consent_page.dart
T
2026-06-18 15:02:28 +08:00

477 lines
14 KiB
Dart

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<PrivacyConsentPage> createState() => _PrivacyConsentPageState();
}
class _PrivacyConsentPageState extends State<PrivacyConsentPage> {
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<void> _agree() async {
await DeviceHeaderService.to.markPrivacyAgreed();
await DeviceHeaderService.to.ensureInitialized();
if (mounted) context.go(AppRoutes.assets);
}
Future<void> _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<void>(
context: context,
isScrollControlled: true,
isDismissible: false,
enableDrag: false,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withValues(alpha: 0.55),
builder: (_) => _PrivacyConsentSheet(onAgree: _agree),
);
});
}
return AnnotatedRegion<SystemUiOverlayStyle>(
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<void> 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,
),
),
),
);
}
}