784 lines
25 KiB
Dart
784 lines
25 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import '../../../data/models/user_model.dart';
|
|
import '../../../data/models/user_setting_model.dart';
|
|
import '../../../services/update_service.dart';
|
|
import '../../../services/user_setting_service.dart';
|
|
import '../../providers/auth_provider.dart';
|
|
import '../../providers/user_setting_provider.dart';
|
|
import '../../widgets/user_avatar.dart';
|
|
import '../../widgets/toast_helper.dart';
|
|
import '../../widgets/desktop_constrained.dart';
|
|
import 'profile_edit_page.dart';
|
|
import 'security_settings_page.dart';
|
|
import 'file_preferences_page.dart';
|
|
import 'app_settings_page.dart';
|
|
import 'credit_history_page.dart';
|
|
import 'quick_access_settings_page.dart';
|
|
import 'network_test_page.dart';
|
|
import '../../../router/app_router.dart';
|
|
|
|
/// 设置主页
|
|
class SettingsPage extends StatefulWidget {
|
|
const SettingsPage({super.key});
|
|
|
|
@override
|
|
State<SettingsPage> createState() => _SettingsPageState();
|
|
}
|
|
|
|
class _SettingsPageState extends State<SettingsPage> {
|
|
String _appVersion = '';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadAppVersion();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
context.read<UserSettingProvider>().loadAll();
|
|
});
|
|
}
|
|
|
|
Future<void> _loadAppVersion() async {
|
|
try {
|
|
final info = await PackageInfo.fromPlatform();
|
|
if (mounted) setState(() => _appVersion = info.version);
|
|
} catch (_) {}
|
|
}
|
|
|
|
Future<void> _refresh() async {
|
|
await context.read<UserSettingProvider>().loadAll();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final auth = context.watch<AuthProvider>();
|
|
final user = auth.user;
|
|
final settingProvider = context.watch<UserSettingProvider>();
|
|
final settings = settingProvider.settings;
|
|
final capacity = settingProvider.capacity;
|
|
final isLoading = settingProvider.isLoading;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('设置'),
|
|
actions: [
|
|
if (isLoading)
|
|
const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.only(right: 16),
|
|
child: SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
),
|
|
)
|
|
else
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: _refresh,
|
|
tooltip: '刷新',
|
|
),
|
|
],
|
|
),
|
|
body: DesktopConstrained(
|
|
child: ListView(
|
|
children: [
|
|
_buildProfileCard(context, user, settings, capacity),
|
|
const SizedBox(height: 8),
|
|
_buildSection(
|
|
title: '账户与安全',
|
|
children: [
|
|
_SettingsTile(
|
|
icon: Icons.person_outline,
|
|
title: '个人资料',
|
|
subtitle: '修改昵称、头像',
|
|
onTap: () => _navigateTo(context, const ProfileEditPage()),
|
|
),
|
|
_SettingsTile(
|
|
icon: Icons.security_outlined,
|
|
title: '安全设置',
|
|
subtitle: _securitySubtitle(settings),
|
|
onTap: () =>
|
|
_navigateTo(context, const SecuritySettingsPage()),
|
|
),
|
|
],
|
|
),
|
|
_buildSection(
|
|
title: '偏好',
|
|
children: [
|
|
_SettingsTile(
|
|
icon: Icons.sync_outlined,
|
|
title: '文件同步',
|
|
subtitle: '本地与云端文件自动同步',
|
|
onTap: () =>
|
|
Navigator.of(context).pushNamed(RouteNames.syncSettings),
|
|
),
|
|
_SettingsTile(
|
|
icon: Icons.apps_outlined,
|
|
title: '快捷入口',
|
|
subtitle: '自定义概览页快捷目录',
|
|
onTap: () =>
|
|
_navigateTo(context, const QuickAccessSettingsPage()),
|
|
),
|
|
_SettingsTile(
|
|
icon: Icons.folder_outlined,
|
|
title: '文件偏好',
|
|
subtitle: '版本保留、视图同步、分享可见性',
|
|
onTap: () =>
|
|
_navigateTo(context, const FilePreferencesPage()),
|
|
),
|
|
_SettingsTile(
|
|
icon: Icons.tune,
|
|
title: '应用设置',
|
|
subtitle: '缓存、主题、语言',
|
|
onTap: () => _navigateTo(context, const AppSettingsPage()),
|
|
),
|
|
_SettingsTile(
|
|
icon: Icons.network_check,
|
|
title: '网络测试',
|
|
subtitle: '服务连通性测试与自定义线路(会员)',
|
|
onTap: () => _openNetworkTest(context),
|
|
),
|
|
],
|
|
),
|
|
// 财务区域(有数据时才显示)
|
|
if (settings != null) ..._buildProSections(context, settings),
|
|
_buildSection(
|
|
title: '关于',
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.info_outline),
|
|
title: const Text('应用名称'),
|
|
subtitle: const Text('公云存储'),
|
|
),
|
|
_buildVersionTile(),
|
|
ListTile(
|
|
leading: const Icon(Icons.copyright),
|
|
title: const Text('License'),
|
|
subtitle: const Text('AGPL-3.0'),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.code),
|
|
title: const Text('二次开发'),
|
|
subtitle: const Text('gongyun_app'),
|
|
trailing: const Icon(Icons.open_in_new, size: 16),
|
|
onTap: () {
|
|
launchUrl(
|
|
Uri.parse('https://git.saont.net/gongyun/app'),
|
|
mode: LaunchMode.externalApplication,
|
|
);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.code),
|
|
title: const Text('基于'),
|
|
subtitle: const Text('cloudreve4_flutter'),
|
|
trailing: const Icon(Icons.open_in_new, size: 16),
|
|
onTap: () {
|
|
launchUrl(
|
|
Uri.parse(
|
|
'https://github.com/LimoYuan/cloudreve4_flutter',
|
|
),
|
|
mode: LaunchMode.externalApplication,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
_buildLogoutButton(context, auth),
|
|
const SizedBox(height: 32),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 财务区域(存储包、积分、会员)
|
|
List<Widget> _buildProSections(
|
|
BuildContext context,
|
|
UserSettingModel settings,
|
|
) {
|
|
final sections = <Widget>[];
|
|
final hasStoragePacks = settings.storagePacks.isNotEmpty;
|
|
final hasCredit = settings.credit > 0;
|
|
final hasMembership = settings.groupExpires != null;
|
|
|
|
if (hasStoragePacks || hasCredit || hasMembership) {
|
|
final children = <Widget>[];
|
|
if (hasMembership) {
|
|
children.add(
|
|
ListTile(
|
|
leading: const Icon(Icons.workspace_premium_outlined),
|
|
title: const Text('会员'),
|
|
subtitle: Text('到期: ${_formatDate(settings.groupExpires!)}'),
|
|
trailing: TextButton(
|
|
onPressed: () => _cancelMembership(context),
|
|
child: const Text('取消会员', style: TextStyle(color: Colors.red)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
if (hasStoragePacks) {
|
|
children.add(
|
|
ListTile(
|
|
leading: const Icon(Icons.inventory_2_outlined),
|
|
title: Text('存储包 (${settings.storagePacks.length})'),
|
|
subtitle: Text(_storagePackSummary(settings.storagePacks)),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: () => _showStoragePacks(context, settings.storagePacks),
|
|
),
|
|
);
|
|
}
|
|
if (hasCredit) {
|
|
children.add(
|
|
ListTile(
|
|
leading: const Icon(Icons.account_balance_wallet_outlined),
|
|
title: const Text('积分'),
|
|
subtitle: Text('${settings.credit} 积分'),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: () => _navigateTo(
|
|
context,
|
|
CreditHistoryPage(currentCredit: settings.credit),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
sections.add(_buildSection(title: '财务', children: children));
|
|
}
|
|
return sections;
|
|
}
|
|
|
|
Widget _buildProfileCard(
|
|
BuildContext context,
|
|
UserModel? user,
|
|
UserSettingModel? settings,
|
|
UserCapacityModel? capacity,
|
|
) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: InkWell(
|
|
onTap: () => _navigateTo(context, const ProfileEditPage()),
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
_buildAvatar(context, user, 56),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
user?.nickname ?? '未登录',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
user?.email ?? '',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
if (user?.group != null) ...[
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primaryContainer,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
user!.group!.name,
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
color: colorScheme.onPrimaryContainer,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const Icon(Icons.chevron_right),
|
|
],
|
|
),
|
|
if (capacity != null) ...[
|
|
const SizedBox(height: 16),
|
|
_buildStorageBar(context, capacity),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAvatar(BuildContext context, UserModel? user, double size) {
|
|
return UserAvatar(
|
|
userId: user?.id ?? '',
|
|
email: user?.email,
|
|
displayName: user?.nickname ?? '用户',
|
|
radius: size / 2,
|
|
);
|
|
}
|
|
|
|
Widget _buildStorageBar(BuildContext context, UserCapacityModel capacity) {
|
|
final theme = Theme.of(context);
|
|
final colorScheme = theme.colorScheme;
|
|
final usedText = _formatBytes(capacity.used);
|
|
final totalText = _formatBytes(capacity.total);
|
|
final percent = capacity.usagePercentage;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text('存储空间', style: theme.textTheme.bodySmall),
|
|
Text(
|
|
'$usedText / $totalText',
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: LinearProgressIndicator(
|
|
value: (percent / 100).clamp(0.0, 1.0),
|
|
minHeight: 6,
|
|
backgroundColor: colorScheme.surfaceContainerHighest,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSection({
|
|
required String title,
|
|
required List<Widget> children,
|
|
}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
),
|
|
),
|
|
Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Column(children: children),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildVersionTile() {
|
|
return AnimatedBuilder(
|
|
animation: UpdateService.instance,
|
|
builder: (context, _) {
|
|
return ListTile(
|
|
leading: const Icon(Icons.tag),
|
|
title: const Text('版本号'),
|
|
subtitle: Text(_appVersion.isEmpty ? '加载中...' : _appVersion),
|
|
trailing: UpdateService.instance.hasUpdate
|
|
? const _BlinkingUpdateDot()
|
|
: null,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildLogoutButton(BuildContext context, AuthProvider auth) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: OutlinedButton.icon(
|
|
onPressed: () => _confirmLogout(context, auth),
|
|
icon: Icon(Icons.logout, color: Theme.of(context).colorScheme.error),
|
|
label: Text(
|
|
'退出登录',
|
|
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
|
),
|
|
style: OutlinedButton.styleFrom(
|
|
minimumSize: const Size(double.infinity, 48),
|
|
side: BorderSide(
|
|
color: Theme.of(context).colorScheme.error.withValues(alpha: 0.3),
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _securitySubtitle(UserSettingModel? settings) {
|
|
if (settings == null) return '';
|
|
final items = <String>[];
|
|
if (settings.twoFaEnabled) items.add('2FA已启用');
|
|
if (settings.passwordless) items.add('无密码登录');
|
|
return items.isEmpty ? '密码、2FA' : items.join('、');
|
|
}
|
|
|
|
// ---- 存储包 ----
|
|
String _storagePackSummary(List<StoragePack> packs) {
|
|
final total = packs.fold<int>(0, (sum, p) => sum + p.size);
|
|
return '共 ${_formatBytes(total)} · ${packs.length} 个存储包';
|
|
}
|
|
|
|
void _showStoragePacks(BuildContext context, List<StoragePack> packs) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (ctx) => DraggableScrollableSheet(
|
|
initialChildSize: 0.5,
|
|
minChildSize: 0.3,
|
|
maxChildSize: 0.8,
|
|
expand: false,
|
|
builder: (ctx, controller) => ListView(
|
|
controller: controller,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
child: Text('存储包', style: Theme.of(ctx).textTheme.titleMedium),
|
|
),
|
|
...packs.map(
|
|
(pack) => Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
pack.name,
|
|
style: Theme.of(ctx).textTheme.titleSmall,
|
|
),
|
|
Text(
|
|
_formatBytes(pack.size),
|
|
style: Theme.of(ctx).textTheme.labelLarge,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'激活: ${_formatDate(pack.activeSince)}'
|
|
'${pack.expireAt != null ? " · 到期: ${_formatDate(pack.expireAt!)}" : " · 永久"}',
|
|
style: Theme.of(ctx).textTheme.bodySmall,
|
|
),
|
|
if (pack.isExpired) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'已过期',
|
|
style: TextStyle(
|
|
color: Theme.of(ctx).colorScheme.error,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ---- 取消会员 ----
|
|
Future<void> _cancelMembership(BuildContext context) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('取消会员'),
|
|
content: const Text('确定要取消当前会员吗?取消后将失去会员权益。'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: const Text('取消'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: Theme.of(ctx).colorScheme.error,
|
|
),
|
|
child: const Text('确认取消'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed != true || !mounted) return;
|
|
|
|
try {
|
|
await UserSettingService.instance.updateUserSetting(groupExpires: true);
|
|
if (!context.mounted) return;
|
|
await context.read<UserSettingProvider>().loadSettings();
|
|
if (mounted) ToastHelper.success('会员已取消');
|
|
} catch (e) {
|
|
if (mounted) ToastHelper.failure('取消会员失败: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _navigateTo(BuildContext context, Widget page) async {
|
|
await Navigator.of(context).push(MaterialPageRoute(builder: (_) => page));
|
|
if (!context.mounted) return;
|
|
if (mounted) {
|
|
context.read<UserSettingProvider>().loadAll();
|
|
}
|
|
}
|
|
|
|
Future<void> _openNetworkTest(BuildContext context) async {
|
|
final agreed = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: const Text('会话信息授权'),
|
|
content: _NetworkTestConsentText(
|
|
onOpenPrivacy: () => launchUrl(
|
|
Uri.parse('https://www.gongyun.org/policy/privacy.html'),
|
|
mode: LaunchMode.externalApplication,
|
|
),
|
|
onOpenTerms: () => launchUrl(
|
|
Uri.parse('https://www.gongyun.org/policy/terms.html'),
|
|
mode: LaunchMode.externalApplication,
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(false),
|
|
child: const Text('不同意'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
child: const Text('同意'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (agreed == true && context.mounted) {
|
|
await Navigator.of(
|
|
context,
|
|
).push(MaterialPageRoute(builder: (_) => const NetworkTestPage()));
|
|
}
|
|
}
|
|
|
|
Future<void> _confirmLogout(BuildContext context, AuthProvider auth) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('退出登录'),
|
|
content: const Text('确定要退出当前账号吗?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: const Text('取消'),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: Theme.of(context).colorScheme.error,
|
|
),
|
|
child: const Text('退出'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmed == true && mounted) {
|
|
await auth.logout();
|
|
if (!context.mounted) return;
|
|
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
|
|
}
|
|
}
|
|
|
|
String _formatBytes(int bytes) {
|
|
if (bytes < 1024) return '$bytes B';
|
|
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
|
if (bytes < 1024 * 1024 * 1024) {
|
|
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
}
|
|
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
|
|
}
|
|
|
|
String _formatDate(DateTime date) {
|
|
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
|
}
|
|
}
|
|
|
|
class _BlinkingUpdateDot extends StatefulWidget {
|
|
const _BlinkingUpdateDot();
|
|
|
|
@override
|
|
State<_BlinkingUpdateDot> createState() => _BlinkingUpdateDotState();
|
|
}
|
|
|
|
class _BlinkingUpdateDotState extends State<_BlinkingUpdateDot>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _controller;
|
|
late final Animation<double> _opacity;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 900),
|
|
)..repeat(reverse: true);
|
|
_opacity = Tween<double>(
|
|
begin: 0.3,
|
|
end: 1,
|
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FadeTransition(
|
|
opacity: _opacity,
|
|
child: Container(
|
|
width: 10,
|
|
height: 10,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.red,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NetworkTestConsentText extends StatefulWidget {
|
|
final VoidCallback onOpenPrivacy;
|
|
final VoidCallback onOpenTerms;
|
|
|
|
const _NetworkTestConsentText({
|
|
required this.onOpenPrivacy,
|
|
required this.onOpenTerms,
|
|
});
|
|
|
|
@override
|
|
State<_NetworkTestConsentText> createState() =>
|
|
_NetworkTestConsentTextState();
|
|
}
|
|
|
|
class _NetworkTestConsentTextState extends State<_NetworkTestConsentText> {
|
|
late final TapGestureRecognizer _privacyRecognizer;
|
|
late final TapGestureRecognizer _termsRecognizer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_privacyRecognizer = TapGestureRecognizer()..onTap = widget.onOpenPrivacy;
|
|
_termsRecognizer = TapGestureRecognizer()..onTap = widget.onOpenTerms;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant _NetworkTestConsentText oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
_privacyRecognizer.onTap = widget.onOpenPrivacy;
|
|
_termsRecognizer.onTap = widget.onOpenTerms;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_privacyRecognizer.dispose();
|
|
_termsRecognizer.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final linkStyle = TextStyle(
|
|
color: Theme.of(context).colorScheme.primary,
|
|
fontWeight: FontWeight.w700,
|
|
);
|
|
|
|
return RichText(
|
|
text: TextSpan(
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
children: [
|
|
const TextSpan(text: '您需同意'),
|
|
TextSpan(
|
|
text: '隐私政策',
|
|
style: linkStyle,
|
|
recognizer: _privacyRecognizer,
|
|
),
|
|
const TextSpan(text: '与'),
|
|
TextSpan(
|
|
text: '用户协议',
|
|
style: linkStyle,
|
|
recognizer: _termsRecognizer,
|
|
),
|
|
const TextSpan(text: '方可进入网络测试页。'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SettingsTile extends StatelessWidget {
|
|
final IconData icon;
|
|
final String title;
|
|
final String subtitle;
|
|
final VoidCallback onTap;
|
|
|
|
const _SettingsTile({
|
|
required this.icon,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListTile(
|
|
leading: Icon(icon),
|
|
title: Text(title),
|
|
subtitle: Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
}
|