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