477 lines
14 KiB
Dart
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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|