Files
xianyan/lib/features/profile/presentation/app_info_sections.dart
Developer 544f77c0ce chore: 完成v2.4.7版本迭代更新
本次更新包含多项功能优化与兼容性修复:
1. iOS/鸿蒙端添加加密出口合规配置,跳过App Store审核问卷
2. 新增学习计划设置页路由与国际化支持
3. 修复鸿蒙端剪贴板粘贴不工作问题,安装标准剪贴板拦截器
4. 优化收藏功能:兼容复合ID、添加状态同步与触觉反馈
5. 修复鸿蒙端相册保存兼容性,统一使用系统分享降级方案
6. 优化搜索快捷方式跳转逻辑,避免白屏问题
7. 更新本地化资源,新增闲情逸致、学习计划等模块翻译
8. 修复节气日期表排序与跨年边界问题
9. 优化设备信息页面显示,新增系统版本号展示
10. 重构文件传输二维码逻辑,使用纯URL提升兼容性
11. 优化设置项布局,避免文本溢出问题
12. 修复登录页记住账户功能,新增隐私协议守卫
13. 更新macOS依赖库,替换flutter_secure_storage为darwin版本
2026-06-17 08:45:34 +08:00

1195 lines
40 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 软件信息页面(分区组件)
/// 创建时间: 2026-05-29
/// 更新时间: 2026-06-17
/// 作用: 技术栈、构建信息、设备信息、平台兼容、更新日志、备案信息等分区
/// 上次更新: 设备类型卡片增加系统版本号显示
/// ============================================================
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, TargetPlatform, kIsWeb;
import 'package:flutter/services.dart';
import 'dart:io' show Platform;
import '../../../core/theme/app_theme.dart';
import '../../../core/theme/app_spacing.dart';
import '../../../core/theme/app_typography.dart';
import '../../../core/theme/app_radius.dart';
import '../../../core/utils/platform/platform_utils.dart' as pu;
import '../../../core/constants/app_constants.dart';
import '../../../core/utils/platform/platform_utils.dart'
show
isOhos,
isAndroid,
isWindows,
isIOS,
isMacOS,
isLinux,
isWeb,
isMobile,
isDesktop,
platformName;
import '../../../core/network/api_client.dart';
import '../../../core/services/device/device_info_service.dart';
import '../../../shared/widgets/containers/glass_container.dart';
import '../../../l10n/translations.dart';
import 'about_shared_widgets.dart';
import 'app_info_widgets.dart';
import 'learn_us_widgets.dart';
// ============================================================
// 技术栈卡片
// ============================================================
class TechStackSection extends StatelessWidget {
const TechStackSection({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
final items = [
TechItemData(
'Dart',
t.about.techLanguage,
CupertinoIcons.flame,
ext.iconTintYellow,
),
TechItemData(
'Riverpod',
t.about.techState,
CupertinoIcons.arrow_3_trianglepath,
ext.iconTintCyan,
),
TechItemData(
'GoRouter',
t.about.techRouter,
CupertinoIcons.map,
ext.iconTintPurple,
),
TechItemData(
'Dio',
t.about.techNetwork,
CupertinoIcons.wifi,
ext.iconTintBlue,
),
];
return GlassContainer(
depth: GlassDepth.elevated,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AboutSectionTitle(
icon: CupertinoIcons.hammer,
title: t.about.techStack,
ext: ext,
),
Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md,
0,
AppSpacing.md,
AppSpacing.md,
),
child: LayoutBuilder(
builder: (context, constraints) {
final cardWidth = (constraints.maxWidth - AppSpacing.md) / 2;
return Wrap(
spacing: AppSpacing.md,
runSpacing: AppSpacing.md,
children: items
.map(
(item) => SizedBox(
width: cardWidth,
child: TechCard(item: item, ext: ext),
),
)
.toList(),
);
},
),
),
],
),
);
}
}
// ============================================================
// 构建信息卡片StatefulWidget — 支持服务器时间状态管理)
// ============================================================
class BuildInfoSection extends StatefulWidget {
const BuildInfoSection({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
State<BuildInfoSection> createState() => _BuildInfoSectionState();
}
class _BuildInfoSectionState extends State<BuildInfoSection> {
/// 服务器时间显示值,初始为 "--"
String _serverTime = '--';
/// 是否正在加载服务器时间
bool _isLoadingTime = false;
@override
void dispose() {
super.dispose();
}
/// 请求服务器时间
Future<void> _fetchServerTime() async {
if (_isLoadingTime) return;
setState(() => _isLoadingTime = true);
try {
final response = await ApiClient.instance.get<dynamic>(
'/api/statistics/overview',
useCache: false,
forceRefresh: true,
);
final data = response.data;
// 解析响应中的 time 字段Unix 时间戳)
if (data is Map<String, dynamic> && data.containsKey('time')) {
final timeValue = data['time'];
int? timestamp;
if (timeValue is int) {
timestamp = timeValue;
} else if (timeValue is String) {
timestamp = int.tryParse(timeValue);
}
if (timestamp != null) {
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
if (mounted) {
setState(() {
_serverTime =
'${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
});
}
return;
}
}
// 解析失败
if (mounted) {
setState(() => _serverTime = '解析失败');
}
} catch (_) {
if (mounted) {
setState(() => _serverTime = '获取失败');
}
} finally {
if (mounted) {
setState(() => _isLoadingTime = false);
}
}
}
@override
Widget build(BuildContext context) {
final ext = widget.ext;
final t = widget.t;
return GlassContainer(
depth: GlassDepth.elevated,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AboutSectionTitle(
icon: CupertinoIcons.wrench,
title: t.about.buildInfo,
ext: ext,
),
// —— 版本号 + 构建号 合并为一行 ——
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
// 左侧:版本号
Expanded(
child: Row(
children: [
Icon(
CupertinoIcons.star,
size: 18,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.about.version,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
AppVersion.version,
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
),
],
),
),
],
),
),
const SizedBox(width: AppSpacing.md),
// 右侧:构建号
Expanded(
child: Row(
children: [
Icon(
CupertinoIcons.number,
size: 18,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.about.buildNumber,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
AppVersion.buildNumber.toString(),
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
),
],
),
),
],
),
),
],
),
),
AboutDivider(ext: ext),
// —— 服务器 + 服务器时间 ——
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
// 左侧:服务器
Expanded(
child: Row(
children: [
Icon(
CupertinoIcons.cloud,
size: 18,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'服务器',
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'Nginx+PHP',
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
),
],
),
),
],
),
),
const SizedBox(width: AppSpacing.md),
// 右侧:服务器时间(可点击刷新)
Expanded(
child: GestureDetector(
onTap: _fetchServerTime,
behavior: HitTestBehavior.opaque,
child: Row(
children: [
Icon(
CupertinoIcons.clock,
size: 18,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'服务器时间',
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
_isLoadingTime
? SizedBox(
height: 14,
width: 14,
child: CupertinoActivityIndicator(
radius: 7,
color: ext.textHint,
),
)
: Text(
_serverTime,
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
),
],
),
),
if (!_isLoadingTime)
Padding(
padding: const EdgeInsets.only(left: AppSpacing.xs),
child: Icon(
CupertinoIcons.refresh,
size: 14,
color: ext.accent.withValues(alpha: 0.6),
),
),
],
),
),
),
],
),
),
AboutDivider(ext: ext),
// —— 构建时间 + Build SDK ——
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Expanded(
child: Row(
children: [
Icon(
CupertinoIcons.calendar,
size: 18,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.about.buildTime,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
DateTime.now().toString().split(' ').first,
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
),
],
),
),
],
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Row(
children: [
Icon(
CupertinoIcons.cube_box,
size: 18,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Build SDK',
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
_getBuildSdk(),
style: AppTypography.caption1.copyWith(
color: ext.textHint,
),
),
],
),
),
],
),
),
],
),
),
AboutDivider(ext: ext),
LicenseItem(ext: ext, t: t),
AboutDivider(ext: ext),
CheckUpdateItem(ext: ext, t: t),
],
),
);
}
/// 获取当前平台 Build SDK
String _getBuildSdk() {
try {
if (isOhos) return 'Deveco API 23';
if (isAndroid) return 'Android Target 36';
if (isWindows) return 'Win10 SDK';
if (isIOS) return 'iOS 26';
if (isMacOS) return 'macOS 18';
if (isLinux) return 'Linux 20';
if (isWeb) return 'Web SDK';
return 'Unknown';
} catch (_) {
return 'Unknown';
}
}
}
// ============================================================
// 设备信息卡片
// ============================================================
class DeviceInfoSection extends StatefulWidget {
const DeviceInfoSection({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
State<DeviceInfoSection> createState() => _DeviceInfoSectionState();
}
class _DeviceInfoSectionState extends State<DeviceInfoSection> {
/// Impeller 后端缓存,避免频繁反射调用
String? _cachedImpellerBackend;
/// 系统版本缓存
String _systemVersion = '';
@override
void initState() {
super.initState();
_loadSystemVersion();
}
/// 异步加载系统版本
Future<void> _loadSystemVersion() async {
final version = await DeviceInfoService.getSystemVersion();
if (mounted && version.isNotEmpty) {
setState(() => _systemVersion = version);
}
}
@override
Widget build(BuildContext context) {
final ext = widget.ext;
final t = widget.t;
final currentPlatform = platformName;
String deviceType = t.about.deviceUnknown;
if (isWeb) {
deviceType = 'Web';
} else if (isMobile) {
deviceType = t.about.deviceMobile;
} else if (isDesktop) {
deviceType = t.about.deviceDesktop;
}
final size = MediaQuery.of(context).size;
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
return GlassContainer(
depth: GlassDepth.elevated,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AboutSectionTitle(
icon: CupertinoIcons.device_phone_portrait,
title: t.about.deviceInfo,
ext: ext,
),
Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md,
0,
AppSpacing.md,
AppSpacing.md,
),
child: GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
mainAxisSpacing: AppSpacing.md,
crossAxisSpacing: AppSpacing.md,
childAspectRatio: 2.0,
children: [
GridInfoItem(
title: t.about.os,
value: currentPlatform,
icon: CupertinoIcons.device_desktop,
ext: ext,
),
GridInfoItem(
title: t.about.deviceType,
value: _systemVersion.isNotEmpty
? '$deviceType ($_systemVersion)'
: deviceType,
icon: CupertinoIcons.device_phone_portrait,
ext: ext,
),
GridInfoItem(
title: 'Dart',
value: _getDartVersion(),
icon: CupertinoIcons.bolt,
ext: ext,
),
GridInfoItem(
title: t.about.renderEngine,
value: _getRenderingEngine(),
icon: CupertinoIcons.paintbrush,
ext: ext,
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.06),
borderRadius: AppRadius.smBorder,
border: Border.all(color: ext.textHint.withValues(alpha: 0.1)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
CupertinoIcons.info_circle,
size: 14,
color: ext.textHint,
),
const SizedBox(width: AppSpacing.xs),
Text(
t.about.screenDetail,
style: AppTypography.caption1.copyWith(
fontWeight: FontWeight.w500,
color: ext.textSecondary,
),
),
],
),
const SizedBox(height: AppSpacing.xs),
Text(
'${t.about.screenSize}: ${size.width.toStringAsFixed(0)} × ${size.height.toStringAsFixed(0)}',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
const SizedBox(height: AppSpacing.xs),
Text(
'${t.about.pixelRatio}: ${pixelRatio.toStringAsFixed(2)}',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
),
const SizedBox(height: AppSpacing.md),
],
),
);
}
String _getDartVersion() {
try {
final fullVersion = Platform.version;
final match = RegExp(r'^(\d+\.\d+\.\d+)').firstMatch(fullVersion);
if (match != null) return '${match.group(1)} (VM)';
return fullVersion;
} catch (_) {
return 'Unknown';
}
}
String _getRenderingEngine() {
try {
final PlatformDispatcher dispatcher = PlatformDispatcher.instance;
// ignore: avoid_dynamic_calls
final bool? enabled = (dispatcher as dynamic).impellerEnabled as bool?;
if (enabled == true) {
final backend = _getImpellerBackend();
return backend.isNotEmpty ? 'Impeller ($backend)' : 'Impeller';
}
if (enabled == false) return 'Skia';
} catch (_) {}
try {
final FlutterView view = PlatformDispatcher.instance.views.first;
// ignore: avoid_dynamic_calls
final dynamic engine = (view as dynamic).renderingEngine;
if (engine != null) {
// ignore: avoid_dynamic_calls
final String name = engine.name.toString();
return switch (name) {
'skia' => 'Skia',
'impeller' => 'Impeller (${_getImpellerBackend()})',
'canvasKit' => 'CanvasKit',
'html' => 'HTML',
_ => name,
};
}
} catch (_) {}
if (kIsWeb) return 'CanvasKit/HTML';
// 默认推断
if (defaultTargetPlatform == TargetPlatform.iOS) {
return 'Impeller (Metal)';
}
if (defaultTargetPlatform == TargetPlatform.android) {
// Android Impeller 默认 Vulkan但旧 GPU 可能回退 OpenGL
// 运行时才能确定,此处显示推断值
return 'Impeller (Vulkan)';
}
if (defaultTargetPlatform == TargetPlatform.macOS) {
return 'Impeller (Metal)';
}
if (pu.isOhos) return 'Impeller (Vulkan)';
return 'Skia';
}
/// 获取 Impeller 后端类型(带缓存,避免频繁反射)
String _getImpellerBackend() {
if (_cachedImpellerBackend != null) return _cachedImpellerBackend!;
try {
// 尝试通过反射获取 Impeller 后端信息
final dispatcher = PlatformDispatcher.instance;
// ignore: avoid_dynamic_calls
final dynamic impellerData = (dispatcher as dynamic).impeller;
if (impellerData != null) {
// ignore: avoid_dynamic_calls
final String backend = impellerData.backend.toString().toLowerCase();
if (backend.contains('metal')) {
_cachedImpellerBackend = 'Metal';
return 'Metal';
}
if (backend.contains('vulkan')) {
_cachedImpellerBackend = 'Vulkan';
return 'Vulkan';
}
if (backend.contains('opengl') || backend.contains('gles')) {
_cachedImpellerBackend = 'OpenGL';
return 'OpenGL';
}
if (backend.contains('webgpu')) {
_cachedImpellerBackend = 'WebGPU';
return 'WebGPU';
}
// 未知后端,返回原始值
if (backend.isNotEmpty && backend != 'null') {
_cachedImpellerBackend = backend;
return backend;
}
}
} catch (_) {
// 反射不可用,使用平台推断
}
// 根据 Platform 推断
final result = _inferImpellerBackend();
_cachedImpellerBackend = result;
return result;
}
/// 根据平台推断 Impeller 后端
String _inferImpellerBackend() {
if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
return 'Metal';
}
// Android: 新设备 Vulkan旧设备可能 OpenGL
// 无法精确判断,标注推断值
if (defaultTargetPlatform == TargetPlatform.android) {
return 'Vulkan'; // Impeller on Android 默认 Vulkan (Flutter 3.16+)
}
if (pu.isOhos) return 'Vulkan';
return '';
}
}
// ============================================================
// 平台兼容卡片
// ============================================================
class PlatformSection extends StatelessWidget {
const PlatformSection({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
final platforms = [
PlatformItemData('📱 iOS', true, isCurrent: isIOS),
PlatformItemData('🤖 Android', true, isCurrent: isAndroid),
PlatformItemData('🔷 HarmonyOS', true, isCurrent: isOhos),
PlatformItemData('💻 macOS', true, isCurrent: isMacOS),
PlatformItemData('🪟 Windows', true, isCurrent: isWindows),
PlatformItemData('🌐 Web', false, isCurrent: isWeb),
];
return GlassContainer(
depth: GlassDepth.elevated,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AboutSectionTitle(
icon: CupertinoIcons.desktopcomputer,
title: t.about.platformCompat,
ext: ext,
),
Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md,
0,
AppSpacing.md,
AppSpacing.md,
),
child: Wrap(
spacing: AppSpacing.sm,
runSpacing: AppSpacing.sm,
children: platforms.map((p) {
final isCurrent = p.isCurrent;
return GestureDetector(
onTap: () => _showDistributionDialog(context, p.name),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: isCurrent
? ext.accent.withValues(alpha: 0.15)
: p.supported
? ext.successColor.withValues(alpha: 0.08)
: ext.textHint.withValues(alpha: 0.06),
borderRadius: AppRadius.pillBorder,
border: Border.all(
color: isCurrent
? ext.accent.withValues(alpha: 0.5)
: p.supported
? ext.successColor.withValues(alpha: 0.3)
: ext.textHint.withValues(alpha: 0.15),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isCurrent) ...[
Icon(
CupertinoIcons.checkmark_circle_fill,
size: 12,
color: ext.accent,
),
const SizedBox(width: AppSpacing.xs),
],
Text(
p.name,
style: AppTypography.caption1.copyWith(
fontWeight: isCurrent
? FontWeight.w700
: FontWeight.w600,
color: isCurrent
? ext.accent
: p.supported
? ext.successColor
: ext.textHint,
),
),
],
),
),
);
}).toList(),
),
),
],
),
);
}
void _showDistributionDialog(BuildContext context, String platformName) {
final channel = _getDistributionChannel(platformName);
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text(platformName),
content: Padding(
padding: const EdgeInsets.only(top: AppSpacing.sm),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
t.about.distributionChannel,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
channel,
style: AppTypography.subhead.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
],
),
),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.of(ctx).pop(),
child: Text(t.about.okButton),
),
],
),
);
}
String _getDistributionChannel(String platformName) {
if (platformName.contains('Android')) return t.about.distAndroid;
if (platformName.contains('iOS')) return t.about.distIOS;
if (platformName.contains('macOS')) return t.about.distMacOS;
if (platformName.contains('HarmonyOS')) return t.about.distHarmony;
if (platformName.contains('Web')) return t.about.distWeb;
if (platformName.contains('Windows')) return t.about.distWindows;
return '';
}
}
// ============================================================
// 更新日志卡片
// ============================================================
class UpdateLogSection extends StatelessWidget {
const UpdateLogSection({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
return GlassContainer(
depth: GlassDepth.elevated,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AboutSectionTitle(
icon: CupertinoIcons.doc_text,
title: t.about.updateLog,
ext: ext,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Column(
children: AppUpdateLog.entries.map((entry) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.sm),
child: UpdateItem(
version: entry.version,
date: entry.date,
changes: entry.changes,
ext: ext,
),
);
}).toList(),
),
),
const SizedBox(height: AppSpacing.md),
],
),
);
}
}
// ============================================================
// ICP备案信息区域从了解我们页面迁移
// ============================================================
class IcpSection extends StatelessWidget {
const IcpSection({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
const icpNumber = '滇ICP备2022000863号-18A';
const icpUrl = 'https://beian.miit.gov.cn/#/Integrated/index';
final isChinese = Localizations.localeOf(context).languageCode == 'zh';
return GlassContainer(
depth: GlassDepth.elevated,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AboutSectionTitle(
icon: CupertinoIcons.doc_text,
title: t.about.icpInfo,
ext: ext,
trailing: isChinese ? null : _IcpInfoIcon(ext: ext, t: t),
),
GestureDetector(
onTap: () {
Clipboard.setData(const ClipboardData(text: icpNumber));
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
content: Text(t.about.copied),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(ctx),
child: const Text('OK'),
),
],
),
);
},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
icpNumber,
style: AppTypography.body.copyWith(
color: ext.textSecondary,
decoration: TextDecoration.underline,
decorationColor: ext.textHint,
),
),
const SizedBox(height: 2),
Text(
t.about.icpDesc,
style: AppTypography.caption2.copyWith(
color: ext.textHint,
),
),
],
),
),
Icon(
CupertinoIcons.doc_on_clipboard,
size: 16,
color: ext.textHint,
),
],
),
),
),
AboutDivider(ext: ext, leftIndent: 0),
GestureDetector(
onTap: () => _showIcpLaunchDialog(context, icpUrl),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.smBorder,
),
child: Icon(
CupertinoIcons.globe,
size: 18,
color: ext.accent,
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.about.viewIcpDetail,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'beian.miit.gov.cn',
style: AppTypography.caption2.copyWith(
color: ext.accent,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Icon(
CupertinoIcons.arrow_up_right_square,
size: 16,
color: ext.textHint,
),
],
),
),
),
],
),
);
}
void _showIcpLaunchDialog(BuildContext context, String url) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text(t.about.viewIcpDetail),
content: Text(t.about.icpLaunchConfirm),
actions: [
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () => Navigator.of(ctx).pop(),
child: Text(t.common.cancel),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () {
Navigator.of(ctx).pop();
launchAboutUrl(context, url);
},
child: Text(t.common.confirm),
),
],
),
);
}
}
class _IcpInfoIcon extends StatelessWidget {
const _IcpInfoIcon({required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _showIcpInfoHintDialog(context),
child: Icon(CupertinoIcons.info_circle, size: 18, color: ext.textHint),
);
}
void _showIcpInfoHintDialog(BuildContext context) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(CupertinoIcons.info_circle, size: 18, color: ext.accent),
const SizedBox(width: AppSpacing.sm),
Flexible(child: Text(t.about.icpInfo)),
],
),
content: Padding(
padding: const EdgeInsets.only(top: AppSpacing.md),
child: Text(
t.about.icpInfoHint,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
height: 1.5,
),
),
),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.of(ctx).pop(),
child: Text(t.about.okButton),
),
],
),
);
}
}