feat:参照研听的基础工程
This commit is contained in:
@@ -0,0 +1,476 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user