Files
xianyan/lib/features/mine/profile/presentation/app_info_sections.dart
2026-06-06 06:54:22 +08:00

860 lines
28 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-01
/// 作用: 技术栈、构建信息、设备信息、平台兼容、更新日志、备案信息等分区
/// 上次更新: IcpSection标题改为"APP ICP核准备案号"; 非中文时标题右侧增加info icon弹窗说明
/// ============================================================
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 '../../../../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(),
);
},
),
),
],
),
);
}
}
// ============================================================
// 构建信息卡片
// ============================================================
class BuildInfoSection extends StatelessWidget {
const BuildInfoSection({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.wrench,
title: t.about.buildInfo,
ext: ext,
),
CopyableItem(
icon: CupertinoIcons.star,
title: t.about.version,
value: AppVersion.version,
ext: ext,
),
AboutDivider(ext: ext),
CopyableItem(
icon: CupertinoIcons.number,
title: t.about.buildNumber,
value: AppVersion.buildNumber.toString(),
ext: ext,
),
AboutDivider(ext: ext),
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),
],
),
);
}
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 StatelessWidget {
const DeviceInfoSection({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
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: 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 dynamic dispatcher = PlatformDispatcher.instance;
final bool? enabled = dispatcher.impellerEnabled as bool?;
if (enabled == true) return 'Impeller';
if (enabled == false) return 'Skia';
} catch (_) {}
try {
final dynamic view = PlatformDispatcher.instance.views.first;
final dynamic engine = view.renderingEngine;
if (engine != null) {
final String name = engine.name.toString();
return switch (name) {
'skia' => 'Skia',
'impeller' => 'Impeller',
'canvasKit' => 'CanvasKit',
'html' => 'HTML',
_ => name,
};
}
} catch (_) {}
if (kIsWeb) return 'CanvasKit/HTML';
if (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.macOS) {
return 'Impeller';
}
if (defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.linux ||
defaultTargetPlatform == TargetPlatform.fuchsia) {
return 'Skia';
}
if (pu.isOhos) return 'Impeller';
return 'Skia';
}
}
// ============================================================
// 平台兼容卡片
// ============================================================
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),
),
],
),
);
}
}