832 lines
25 KiB
Dart
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;
|
|
}
|