Files
xianyan/lib/features/profile/presentation/app_info_widgets.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

659 lines
20 KiB
Dart

/// ============================================================
/// 闲言APP — 软件信息页面(通用组件)
/// 创建时间: 2026-05-29
/// 更新时间: 2026-06-01
/// 作用: 软件信息页面的通用小组件和数据类
/// 上次更新: LicenseItem增加"查看完整许可"按钮; 新增CheckUpdateItem
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' show showLicensePage;
import 'package:flutter/services.dart';
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/constants/app_constants.dart';
import '../../../l10n/translations.dart';
// ============================================================
// 通用组件 — 信息行
// ============================================================
class InfoItem extends StatelessWidget {
const InfoItem({
super.key,
required this.icon,
required this.title,
required this.value,
required this.ext,
});
final IconData icon;
final String title;
final String value;
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Icon(icon, size: 18, color: ext.textHint),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
value,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
],
),
);
}
}
// ============================================================
// 通用组件 — 可复制信息行
// ============================================================
class CopyableItem extends StatelessWidget {
const CopyableItem({
super.key,
required this.icon,
required this.title,
required this.value,
required this.ext,
});
final IconData icon;
final String title;
final String value;
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: value));
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
content: Text(value),
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: [
Icon(icon, size: 18, color: ext.textHint),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
value,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
Icon(
CupertinoIcons.doc_on_doc,
size: 14,
color: ext.accent.withValues(alpha: 0.6),
),
],
),
),
);
}
}
// ============================================================
// 通用组件 — 网格信息项
// ============================================================
class GridInfoItem extends StatelessWidget {
const GridInfoItem({
super.key,
required this.title,
required this.value,
required this.icon,
required this.ext,
});
final String title;
final String value;
final IconData icon;
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.06),
borderRadius: AppRadius.mdBorder,
border: Border.all(color: ext.textHint.withValues(alpha: 0.1)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 14, color: ext.accent),
const SizedBox(width: AppSpacing.xs),
Expanded(
child: Text(
title,
style: AppTypography.caption2.copyWith(
fontWeight: FontWeight.w500,
color: ext.textHint,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: AppSpacing.xs),
Text(
value,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
);
}
}
// ============================================================
// 通用组件 — 技术栈卡片
// ============================================================
class TechCard extends StatelessWidget {
const TechCard({super.key, required this.item, required this.ext});
final TechItemData item;
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: item.color.withValues(alpha: 0.08),
borderRadius: AppRadius.mdBorder,
border: Border.all(color: item.color.withValues(alpha: 0.18)),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(
color: item.color.withValues(alpha: 0.18),
borderRadius: AppRadius.smBorder,
),
child: Icon(item.icon, color: item.color, size: 20),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item.title,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w600,
color: ext.textPrimary,
),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
item.subtitle,
style: AppTypography.caption2.copyWith(color: ext.textHint),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
}
// ============================================================
// 通用组件 — 开源许可项
// ============================================================
class LicenseItem extends StatelessWidget {
const LicenseItem({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => _showLicenseDialog(context),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Icon(CupertinoIcons.doc_plaintext, size: 18, color: ext.accent),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.about.openSource,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
'Flutter SDK',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
Icon(CupertinoIcons.chevron_right, size: 14, color: ext.textHint),
],
),
),
);
}
void _showLicenseDialog(BuildContext context) {
final licenses = [
{'name': 'Flutter SDK', 'license': 'BSD 3-Clause'},
{'name': 'OpenHarmony SDK', 'license': 'Apache 2.0'},
{'name': 'Cupertino Icons', 'license': 'MIT'},
{'name': 'Riverpod', 'license': 'MIT'},
{'name': 'GoRouter', 'license': 'BSD 3-Clause'},
{'name': 'Dio', 'license': 'MIT'},
{'name': 'Drift', 'license': 'MIT'},
{'name': 'Hive CE', 'license': 'Apache 2.0'},
{'name': 'url_launcher', 'license': 'BSD 3-Clause'},
{'name': 'package_info_plus', 'license': 'BSD 3-Clause'},
{'name': 'device_info_plus', 'license': 'BSD 3-Clause'},
{'name': 'shared_preferences', 'license': 'BSD 3-Clause'},
];
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Row(
children: [
Icon(CupertinoIcons.doc_plaintext, color: ext.accent, size: 18),
const SizedBox(width: AppSpacing.sm),
Text(t.about.openSource),
],
),
content: Padding(
padding: const EdgeInsets.only(top: AppSpacing.md),
child: SizedBox(
width: double.maxFinite,
height: 260,
child: ListView(
shrinkWrap: true,
children: licenses.map((item) {
return Container(
margin: const EdgeInsets.only(bottom: AppSpacing.sm),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: ext.bgPrimary,
borderRadius: AppRadius.smBorder,
border: Border.all(
color: ext.textHint.withValues(alpha: 0.1),
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(AppSpacing.xs),
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.1),
borderRadius: AppRadius.smBorder,
),
child: Icon(
CupertinoIcons.cube_box,
size: 14,
color: ext.accent,
),
),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['name']!,
style: AppTypography.caption1.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 1),
Text(
item['license']!,
style: AppTypography.caption2.copyWith(
color: ext.textHint,
),
),
],
),
),
],
),
);
}).toList(),
),
),
),
actions: [
CupertinoDialogAction(
onPressed: () {
Navigator.of(ctx).pop();
showLicensePage(
context: context,
applicationName: AppConstants.appName,
applicationVersion: AppVersion.version,
applicationIcon: Image.asset(
'assets/templates/resized/icon_80x80.png',
width: 48,
height: 48,
),
);
},
child: const Text('查看完整许可'),
),
CupertinoDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('OK'),
),
],
),
);
}
}
// ============================================================
// 通用组件 — 更新日志项
// ============================================================
class UpdateItem extends StatelessWidget {
const UpdateItem({
super.key,
required this.version,
required this.date,
required this.changes,
required this.ext,
});
final String version;
final String date;
final List<String> changes;
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return Container(
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(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
version,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.bold,
color: ext.textPrimary,
),
),
Text(
date,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
const SizedBox(height: AppSpacing.xs),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: changes.map((change) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: AppSpacing.sm),
Text(
'',
style: AppTypography.caption1.copyWith(color: ext.accent),
),
Expanded(
child: Text(
change,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
),
],
),
);
}).toList(),
),
],
),
);
}
}
// ============================================================
// 数据类
// ============================================================
class TechItemData {
const TechItemData(this.title, this.subtitle, this.icon, this.color);
final String title;
final String subtitle;
final IconData icon;
final Color color;
}
class PlatformItemData {
const PlatformItemData(this.name, this.supported, {this.isCurrent = false});
final String name;
final bool supported;
final bool isCurrent;
}
// ============================================================
// 通用组件 — 检查更新项
// ============================================================
class CheckUpdateItem extends StatelessWidget {
const CheckUpdateItem({super.key, required this.ext, required this.t});
final AppThemeExtension ext;
final T t;
@override
Widget build(BuildContext context) {
// 判断运行模式
const isRelease = bool.fromEnvironment('dart.vm.product');
const modeLabel = isRelease ? 'Release' : 'Debug';
const modeColor = isRelease
? CupertinoColors.systemGreen
: CupertinoColors.systemOrange;
return GestureDetector(
onTap: () => _showCheckUpdateDialog(context),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Icon(
CupertinoIcons.checkmark_circle_fill,
size: 18,
color: ext.accent,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
t.about.checkUpdate,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
const SizedBox(height: 2),
Text(
t.about.alreadyLatest,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
// 运行模式标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: modeColor.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
border: Border.all(
color: modeColor.withValues(alpha: 0.3),
width: 0.5,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
isRelease
? CupertinoIcons.shield_fill
: CupertinoIcons.lab_flask_solid,
size: 11,
color: modeColor,
),
const SizedBox(width: 4),
Text(
modeLabel,
style: AppTypography.caption2.copyWith(
color: modeColor,
fontWeight: FontWeight.w600,
fontSize: 11,
),
),
],
),
),
const SizedBox(width: AppSpacing.xs),
Icon(CupertinoIcons.chevron_right, size: 14, color: ext.textHint),
],
),
),
);
}
void _showCheckUpdateDialog(BuildContext context) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
CupertinoIcons.checkmark_circle_fill,
size: 20,
color: CupertinoColors.systemGreen,
),
const SizedBox(width: 8),
Text(t.about.checkUpdate),
],
),
content: Text(t.about.alreadyLatestDesc),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text(t.about.okButton),
),
],
),
);
}
}