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

832 lines
25 KiB
Dart

/// ============================================================
/// 闲言APP — 关于页面
/// 创建时间: 2026-05-06
/// 更新时间: 2026-06-01
/// 作用: 展示应用信息、版本号、用户反馈入口、法律信息、开发者工具
/// 上次更新: 迁移"检查更新"和"开源许可"至软件信息页面
/// ============================================================
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' show Divider;
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:xianyan/core/router/app_nav_extension.dart';
import 'package:xianyan/core/router/app_routes.dart';
import 'package:package_info_plus/package_info_plus.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';
import '../../../../shared/widgets/containers/glass_container.dart';
import '../../../../shared/widgets/adaptive/adaptive_back_button.dart';
import '../../../../shared/widgets/feedback/external_link_dialog.dart';
import '../../../../shared/widgets/feedback/app_toast.dart';
import 'about_shared_widgets.dart';
class AboutPage extends ConsumerWidget {
const AboutPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final ext = AppTheme.ext(context);
final t = ref.watch(translationsProvider);
return CupertinoPageScaffold(
backgroundColor: ext.bgPrimary,
navigationBar: CupertinoNavigationBar(
leading: const AdaptiveBackButton(),
middle: Text(
t.about.aboutTitle,
style: AppTypography.title3.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w600,
),
),
backgroundColor: ext.bgElevated.withValues(alpha: 0.85),
border: null,
),
child: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
child: Column(
children: [
_AppHeader(ext: ext)
.animate(onPlay: (c) => c.forward())
.fadeIn(duration: 400.ms)
.slideY(
begin: 0.06,
end: 0,
duration: 400.ms,
curve: Curves.easeOutCubic,
),
const SizedBox(height: AppSpacing.md),
_BasicInfoSection(ext: ext)
.animate(onPlay: (c) => c.forward())
.fadeIn(duration: 400.ms, delay: 100.ms)
.slideY(
begin: 0.06,
end: 0,
duration: 400.ms,
delay: 100.ms,
curve: Curves.easeOutCubic,
),
const SizedBox(height: AppSpacing.md),
_FeedbackSection(ext: ext)
.animate(onPlay: (c) => c.forward())
.fadeIn(duration: 400.ms, delay: 200.ms)
.slideY(
begin: 0.06,
end: 0,
duration: 400.ms,
delay: 200.ms,
curve: Curves.easeOutCubic,
),
const SizedBox(height: AppSpacing.md),
_LegalSection(ext: ext)
.animate(onPlay: (c) => c.forward())
.fadeIn(duration: 400.ms, delay: 300.ms)
.slideY(
begin: 0.06,
end: 0,
duration: 400.ms,
delay: 300.ms,
curve: Curves.easeOutCubic,
),
const SizedBox(height: AppSpacing.md),
_DeveloperSection(ext: ext)
.animate(onPlay: (c) => c.forward())
.fadeIn(duration: 400.ms, delay: 400.ms)
.slideY(
begin: 0.06,
end: 0,
duration: 400.ms,
delay: 400.ms,
curve: Curves.easeOutCubic,
),
const SizedBox(height: AppSpacing.xl),
_Footer(ext: ext)
.animate(onPlay: (c) => c.forward())
.fadeIn(duration: 400.ms, delay: 500.ms),
const SizedBox(height: AppSpacing.xxl),
],
),
),
),
);
}
}
// ============================================================
// 应用头部 — 渐变卡片
// ============================================================
class _AppHeader extends StatelessWidget {
const _AppHeader({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.appPush('/about/app-info'),
behavior: HitTestBehavior.opaque,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
ext.accent.withValues(alpha: 0.85),
ext.accent,
ext.accentLight.withValues(alpha: 0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: AppRadius.lgBorder,
boxShadow: [
BoxShadow(
color: ext.accent.withValues(alpha: 0.18),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Container(
decoration: BoxDecoration(
borderRadius: AppRadius.mdBorder,
border: Border.all(
color: CupertinoColors.white.withValues(alpha: 0.35),
width: 2,
),
),
child: ClipRRect(
borderRadius: AppRadius.mdBorder,
child: _AppIconImage(ext: ext),
),
),
const SizedBox(width: AppSpacing.lg),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppConstants.appName,
style: AppTypography.title2.copyWith(
fontWeight: FontWeight.bold,
color: CupertinoColors.white,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
AppConstants.appSlogan,
style: AppTypography.subhead.copyWith(
color: CupertinoColors.white.withValues(alpha: 0.85),
),
),
const SizedBox(height: AppSpacing.sm),
_VersionBadge(ext: ext),
],
),
),
Icon(
CupertinoIcons.chevron_right,
size: 16,
color: CupertinoColors.white.withValues(alpha: 0.5),
),
],
),
),
);
}
}
class _AppIconImage extends StatelessWidget {
const _AppIconImage({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return Image.asset(
'assets/templates/resized/icon_80x80.png',
width: 58,
height: 58,
fit: BoxFit.cover,
);
}
}
class _VersionBadge extends StatelessWidget {
const _VersionBadge({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
final version = snapshot.hasData
? snapshot.data!.version
: AppVersion.version;
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: CupertinoColors.white.withValues(alpha: 0.18),
borderRadius: AppRadius.pillBorder,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
CupertinoIcons.tag,
size: 12,
color: CupertinoColors.white.withValues(alpha: 0.9),
),
const SizedBox(width: AppSpacing.xs),
Text(
'Version $version',
style: AppTypography.caption1.copyWith(
fontWeight: FontWeight.w500,
color: CupertinoColors.white.withValues(alpha: 0.95),
),
),
],
),
);
},
);
}
}
// ============================================================
// 基础信息
// ============================================================
class _BasicInfoSection extends ConsumerWidget {
const _BasicInfoSection({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
return _SectionCard(
ext: ext,
titleIcon: CupertinoIcons.info_circle,
title: t.about.basicInfo,
children: [
_ActionTile(
icon: CupertinoIcons.doc_text,
title: t.about.appInfo,
subtitle: t.about.softwareInfoDesc,
ext: ext,
onTap: () => context.appPush('/about/app-info'),
),
_SectionDivider(ext: ext),
_ActionTile(
icon: CupertinoIcons.group_solid,
title: t.about.learnUs,
subtitle: t.about.learnUsMenuDesc,
ext: ext,
onTap: () => context.appPush('/about/learn-us'),
),
_SectionDivider(ext: ext),
_ActionTile(
icon: CupertinoIcons.book,
title: t.about.usageGuide,
subtitle: t.about.usageGuideDesc,
ext: ext,
onTap: () => context.appPush(AppRoutes.onboarding),
),
],
);
}
}
// ============================================================
// 互动反馈
// ============================================================
class _FeedbackSection extends ConsumerWidget {
const _FeedbackSection({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
return _SectionCard(
ext: ext,
titleIcon: CupertinoIcons.chat_bubble_2_fill,
title: t.about.interactionFeedback,
children: [
_ActionTile(
icon: CupertinoIcons.chat_bubble_text,
title: t.about.userFeedback,
subtitle: t.about.userFeedbackDesc,
ext: ext,
onTap: () => context.appPush(AppRoutes.correction),
),
_SectionDivider(ext: ext),
_ActionTile(
icon: CupertinoIcons.star_fill,
title: t.about.rateAppMenu,
subtitle: t.about.rateAppMenuDesc,
ext: ext,
onTap: () => _onRateApp(context, t),
),
_SectionDivider(ext: ext),
_ActionTile(
icon: CupertinoIcons.mail_solid,
title: t.about.contactEmail,
subtitle: t.about.contactEmailMenuDesc,
ext: ext,
onTap: () => _showEmailSheet(context, t),
),
],
);
}
void _onRateApp(BuildContext context, T t) {
AppToast.showInfo('未找到应用商店 🏪');
}
void _showEmailSheet(BuildContext context, T t) {
final emails = [
_ContactEmail(
address: 'gg@0gg.cc',
label: t.about.emailHint1,
icon: CupertinoIcons.globe,
),
_ContactEmail(
address: 'ad@avefs.com',
label: t.about.emailHint1,
icon: CupertinoIcons.link,
),
_ContactEmail(
address: '2821981550@qq.com',
label: t.about.emailHint2,
icon: CupertinoIcons.mail,
),
_ContactEmail(
address: '2572560133@qq.com',
label: t.about.emailHint2,
icon: CupertinoIcons.mail,
),
];
showCupertinoModalPopup<void>(
context: context,
builder: (ctx) => _EmailSheet(ext: ext, emails: emails, t: t),
);
}
}
// ============================================================
// 法律信息
// ============================================================
class _LegalSection extends ConsumerWidget {
const _LegalSection({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
return _SectionCard(
ext: ext,
titleIcon: CupertinoIcons.doc_plaintext,
title: t.about.legalInfo,
children: [
_ActionTile(
icon: CupertinoIcons.doc_text_search,
title: t.about.softwareAgreement,
subtitle: t.about.softwareAgreementDesc,
ext: ext,
onTap: () => context.appPush('/agreements'),
),
_SectionDivider(ext: ext),
_ActionTile(
icon: CupertinoIcons.doc_text_search,
title: t.about.dataCollectionMenu,
subtitle: t.about.dataCollectionMenuDesc,
ext: ext,
onTap: () => context.appPush('/data-collection-info'),
),
_SectionDivider(ext: ext),
_ActionTile(
icon: CupertinoIcons.lock_shield,
title: t.about.softwarePermission,
subtitle: t.about.softwarePermissionDesc,
ext: ext,
onTap: () => context.appPush('/permission-management'),
),
],
);
}
}
// ============================================================
// 开发者
// ============================================================
class _DeveloperSection extends ConsumerWidget {
const _DeveloperSection({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
return _SectionCard(
ext: ext,
titleIcon: CupertinoIcons.hammer_fill,
title: t.about.developer,
children: [
_ActionTile(
icon: CupertinoIcons.chart_bar_alt_fill,
title: t.about.updateLog,
subtitle: t.about.updateLogMenuDesc,
ext: ext,
onTap: () {
// 拼接共享更新日志数据
final logText = AppUpdateLog.entries
.map((entry) {
final items = entry.changes.map((c) => '$c').join('\n');
return '${entry.version} (${entry.date})\n$items';
})
.join('\n\n');
showCupertinoDialog<void>(
context: context,
builder: (ctx) => CupertinoAlertDialog(
title: Text(t.about.updateLog),
content: Text(logText),
actions: [
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text(t.about.okButton),
),
],
),
);
},
),
],
);
}
}
// ============================================================
// 页脚
// ============================================================
class _Footer extends StatelessWidget {
const _Footer({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'© 2026 ${AppConstants.appName}',
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
const SizedBox(height: AppSpacing.xs),
Text(
AppConstants.appSlogan,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
);
}
}
// ============================================================
// 通用组件 — 分组卡片
// ============================================================
class _SectionCard extends StatelessWidget {
const _SectionCard({
required this.ext,
required this.titleIcon,
required this.title,
required this.children,
});
final AppThemeExtension ext;
final IconData titleIcon;
final String title;
final List<Widget> children;
@override
Widget build(BuildContext context) {
return GlassContainer(
depth: GlassDepth.elevated,
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(
AppSpacing.md,
AppSpacing.md,
AppSpacing.md,
AppSpacing.xs,
),
child: Row(
children: [
Icon(titleIcon, size: 14, color: ext.textHint),
const SizedBox(width: AppSpacing.xs),
Text(
title,
style: AppTypography.caption1.copyWith(
fontWeight: FontWeight.w600,
color: ext.textHint,
),
),
],
),
),
...children,
],
),
);
}
}
// ============================================================
// 通用组件 — 列表项
// ============================================================
class _ActionTile extends StatelessWidget {
const _ActionTile({
required this.icon,
required this.title,
required this.subtitle,
required this.ext,
required this.onTap,
});
final IconData icon;
final String title;
final String subtitle;
final AppThemeExtension ext;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.md,
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Icon(icon, size: 20, color: ext.accent),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: AppTypography.body.copyWith(
fontWeight: FontWeight.w500,
color: ext.textPrimary,
),
),
],
),
const SizedBox(height: 2),
Text(
subtitle,
style: AppTypography.caption1.copyWith(
color: ext.textSecondary,
),
),
],
),
),
Icon(CupertinoIcons.chevron_right, size: 16, color: ext.textHint),
],
),
),
);
}
}
// ============================================================
// 通用组件 — 分割线
// ============================================================
class _SectionDivider extends StatelessWidget {
const _SectionDivider({required this.ext});
final AppThemeExtension ext;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: AppSpacing.md + 36 + AppSpacing.md),
child: Divider(
height: 1,
thickness: 0.5,
color: ext.textHint.withValues(alpha: 0.1),
),
);
}
}
// ============================================================
// 邮箱底部面板
// ============================================================
class _EmailSheet extends StatelessWidget {
const _EmailSheet({required this.ext, required this.emails, required this.t});
final AppThemeExtension ext;
final List<_ContactEmail> emails;
final T t;
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
decoration: BoxDecoration(
color: ext.bgElevated,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppRadius.xl),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: AppSpacing.sm),
Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.3),
borderRadius: AppRadius.smBorder,
),
),
const SizedBox(height: AppSpacing.md),
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Row(
children: [
Icon(CupertinoIcons.mail_solid, size: 20, color: ext.accent),
const SizedBox(width: AppSpacing.sm),
Text(
t.about.contactEmail,
style: AppTypography.title3.copyWith(
fontWeight: FontWeight.w700,
color: ext.textPrimary,
),
),
const Spacer(),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: ext.textHint.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: Icon(
CupertinoIcons.xmark,
size: 14,
color: ext.textSecondary,
),
),
),
],
),
),
const SizedBox(height: AppSpacing.sm),
Flexible(
child: ListView.builder(
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
itemCount: emails.length,
itemBuilder: (context, index) {
final email = emails[index];
return _EmailTile(ext: ext, email: email);
},
),
),
],
),
);
}
}
class _EmailTile extends StatelessWidget {
const _EmailTile({required this.ext, required this.email});
final AppThemeExtension ext;
final _ContactEmail email;
@override
Widget build(BuildContext context) {
return CupertinoButton(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
onPressed: () {
Navigator.pop(context);
final uri = Uri.parse('mailto:${email.address}');
ExternalLinkDialog.launchWithConfirm(context, uri: uri);
},
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: ext.accent.withValues(alpha: 0.12),
borderRadius: AppRadius.smBorder,
),
child: Icon(email.icon, size: 16, color: ext.accent),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
email.address,
style: AppTypography.subhead.copyWith(
color: ext.accent,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
email.label,
style: AppTypography.caption1.copyWith(color: ext.textHint),
),
],
),
),
Icon(
CupertinoIcons.arrow_right_circle,
size: 18,
color: ext.textHint,
),
],
),
);
}
}
class _ContactEmail {
const _ContactEmail({
required this.address,
required this.label,
required this.icon,
});
final String address;
final String label;
final IconData icon;
}