feat: 实现起卦、设置与积分系统

This commit is contained in:
qzl
2026-04-03 16:56:47 +08:00
parent 31594558eb
commit f245eec5f6
170 changed files with 20728 additions and 328 deletions
@@ -0,0 +1 @@
enum LegalDocumentType { aboutUs, privacyPolicy, termsOfService }
@@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/app_color_palette.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../widgets/settings_section_widgets.dart';
class CoinCenterScreen extends StatelessWidget {
const CoinCenterScreen({super.key, required this.balance});
final int balance;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.settingsCoinCenterTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
Container(
padding: const EdgeInsets.all(AppSpacing.xl),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.xl),
gradient: LinearGradient(
colors: [colors.primary, palette.accentPurple],
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.monetization_on_rounded,
color: colors.onPrimary,
size: 34,
),
const SizedBox(height: AppSpacing.md),
Text(
l10n.settingsCoinBalanceLabel,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: colors.onPrimary),
),
const SizedBox(height: AppSpacing.xs),
Text(
l10n.settingsCoinBalanceValue(balance),
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(color: colors.onPrimary),
),
const SizedBox(height: AppSpacing.sm),
Text(
l10n.settingsCoinCenterDescription,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colors.onPrimary.withValues(alpha: 0.88),
),
),
],
),
),
const SizedBox(height: AppSpacing.xl),
SectionLabel(text: l10n.settingsCoinRechargeSection),
CoinPackageCard(
title: l10n.settingsCoinPackBasic,
price: '\$4.99',
amount: 100,
),
const SizedBox(height: AppSpacing.md),
CoinPackageCard(
title: l10n.settingsCoinPackPopular,
price: '\$7.99',
amount: 210,
badge: l10n.settingsCoinPackPopularBadge,
),
const SizedBox(height: AppSpacing.md),
CoinPackageCard(
title: l10n.settingsCoinPackPremium,
price: '\$12.99',
amount: 415,
),
],
),
);
}
}
@@ -0,0 +1,163 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../data/models/profile_settings.dart';
import 'language_settings_screen.dart';
import 'settings_placeholder_screen.dart';
import '../widgets/settings_section_widgets.dart';
class GeneralSettingsScreen extends StatefulWidget {
const GeneralSettingsScreen({
super.key,
required this.settings,
required this.onInterfaceLanguageChanged,
});
final ProfileSettingsV1 settings;
final Future<void> Function(String languageTag) onInterfaceLanguageChanged;
@override
State<GeneralSettingsScreen> createState() => _GeneralSettingsScreenState();
}
class _GeneralSettingsScreenState extends State<GeneralSettingsScreen> {
late ProfileSettingsV1 _settings;
@override
void initState() {
super.initState();
_settings = widget.settings;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
return PopScope<ProfileSettingsV1>(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (didPop) {
return;
}
Navigator.of(context).pop(_settings);
},
child: Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
leading: IconButton(
onPressed: () => Navigator.of(context).pop(_settings),
icon: const Icon(Icons.arrow_back_ios_new_rounded),
),
title: Text(l10n.settingsGeneralTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
SectionLabel(text: l10n.settingsSectionGeneral),
SettingsGroupCard(
children: [
SettingsMenuTile(
icon: Icons.language_rounded,
title: l10n.language,
subtitle: displayLanguageLabel(
l10n,
_settings.preferences.interfaceLanguage,
),
tint: colors.primary,
background: colors.surfaceContainerHighest,
onTap: _openLanguageSettings,
),
SettingsMenuTile(
icon: Icons.auto_awesome_rounded,
title: l10n.settingsAiLanguage,
subtitle: displayLanguageLabel(
l10n,
_settings.preferences.aiLanguage,
),
tint: colors.secondary,
background: colors.surfaceContainerHighest,
onTap: () => _openPlaceholder(
title: l10n.settingsAiLanguage,
value: displayLanguageLabel(
l10n,
_settings.preferences.aiLanguage,
),
description: l10n.settingsAiLanguageHint,
),
),
SettingsMenuTile(
icon: Icons.public_rounded,
title: l10n.settingsTimezone,
subtitle: _settings.preferences.timezone,
tint: colors.primary,
background: colors.surfaceContainerHighest,
onTap: () => _openPlaceholder(
title: l10n.settingsTimezone,
value: _settings.preferences.timezone,
description: l10n.settingsTimezoneHint,
),
),
SettingsMenuTile(
icon: Icons.flag_outlined,
title: l10n.settingsCountry,
subtitle: _settings.preferences.country,
tint: colors.primary,
background: colors.surfaceContainerHighest,
showDivider: false,
onTap: () => _openPlaceholder(
title: l10n.settingsCountry,
value: _settings.preferences.country,
description: l10n.settingsCountryHint,
),
),
],
),
],
),
),
);
}
Future<void> _openLanguageSettings() async {
final result = await Navigator.of(context).push<String>(
MaterialPageRoute<String>(
builder: (_) => LanguageSettingsScreen(
selectedLanguageTag: _settings.preferences.interfaceLanguage,
),
),
);
if (result == null || result == _settings.preferences.interfaceLanguage) {
return;
}
await widget.onInterfaceLanguageChanged(result);
if (!mounted) {
return;
}
setState(() {
_settings = _settings.copyWith(
preferences: _settings.preferences.copyWith(interfaceLanguage: result),
);
});
}
Future<void> _openPlaceholder({
required String title,
required String value,
required String description,
}) async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => SettingsPlaceholderScreen(
title: title,
value: value,
description: description,
),
),
);
}
}
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../widgets/settings_section_widgets.dart';
class LanguageSettingsScreen extends StatelessWidget {
const LanguageSettingsScreen({super.key, required this.selectedLanguageTag});
final String selectedLanguageTag;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final options = <({String tag, String label})>[
(tag: 'zh-CN', label: l10n.chinese),
(tag: 'en-US', label: l10n.english),
];
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.language),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
SectionLabel(text: l10n.settingsLanguageSection),
SettingsGroupCard(
children: [
for (int i = 0; i < options.length; i++)
SettingsMenuTile(
icon: Icons.language_rounded,
title: options[i].label,
subtitle: options[i].tag,
tint: colors.primary,
background: colors.surfaceContainerHighest,
showDivider: i != options.length - 1,
trailing: selectedLanguageTag == options[i].tag
? Icon(Icons.check_rounded, color: colors.primary)
: Icon(
Icons.chevron_right_rounded,
color: colors.outline,
),
onTap: () => Navigator.of(context).pop(options[i].tag),
),
],
),
],
),
);
}
}
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../models/legal_document_type.dart';
import '../utils/legal_document_assets.dart';
import '../widgets/settings_section_widgets.dart';
import 'legal_document_screen.dart';
class LegalCenterScreen extends StatelessWidget {
const LegalCenterScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final locale = Localizations.localeOf(context);
final documents = [
LegalDocumentType.aboutUs,
LegalDocumentType.privacyPolicy,
LegalDocumentType.termsOfService,
];
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.settingsLegalCenterTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
SectionLabel(text: l10n.settingsSectionAbout),
SettingsGroupCard(
children: [
for (int i = 0; i < documents.length; i++)
SettingsMenuTile(
icon: legalDocumentIcon(documents[i]),
title: legalDocumentTitle(l10n, documents[i]),
subtitle: legalDocumentSubtitle(l10n, documents[i]),
tint: colors.primary,
background: colors.surfaceContainerHighest,
showDivider: i != documents.length - 1,
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => LegalDocumentScreen(
title: legalDocumentTitle(l10n, documents[i]),
assetPath: legalDocumentAssetPath(locale, documents[i]),
),
),
),
),
],
),
],
),
);
}
}
@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
class LegalDocumentScreen extends StatelessWidget {
const LegalDocumentScreen({
super.key,
required this.title,
required this.assetPath,
});
final String title;
final String assetPath;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(title),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: FutureBuilder<String>(
future: rootBundle.loadString(assetPath),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: AppLoadingIndicator(variant: AppLoadingVariant.surface),
);
}
return Markdown(
data: snapshot.data!,
padding: const EdgeInsets.all(AppSpacing.lg),
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context))
.copyWith(
p: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(height: 1.7),
h1: Theme.of(context).textTheme.titleLarge,
h2: Theme.of(context).textTheme.titleMedium,
h3: Theme.of(
context,
).textTheme.titleMedium?.copyWith(color: colors.primary),
blockSpacing: AppSpacing.lg,
),
);
},
),
);
}
}
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../data/models/profile_settings.dart';
import 'settings_placeholder_screen.dart';
import '../widgets/settings_section_widgets.dart';
class PrivacyNotificationSettingsScreen extends StatelessWidget {
const PrivacyNotificationSettingsScreen({super.key, required this.settings});
final ProfileSettingsV1 settings;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.settingsPrivacyAndNotificationTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
SectionLabel(text: l10n.settingsSectionPrivacy),
SettingsGroupCard(
children: [
SettingsMenuTile(
icon: Icons.visibility_outlined,
title: l10n.settingsPrivacyProfileVisibility,
subtitle: l10n.settingsPlaceholderState(
settings.privacy.length,
),
tint: colors.primary,
background: colors.surfaceContainerHighest,
onTap: () => _openPlaceholder(
context,
title: l10n.settingsPrivacyProfileVisibility,
value: l10n.settingsComingSoon,
description: l10n.settingsPrivacyHint,
),
),
SettingsMenuTile(
icon: Icons.psychology_alt_outlined,
title: l10n.settingsPrivacyPersonalization,
subtitle: l10n.settingsComingSoon,
tint: colors.primary,
background: colors.surfaceContainerHighest,
onTap: () => _openPlaceholder(
context,
title: l10n.settingsPrivacyPersonalization,
value: l10n.settingsComingSoon,
description: l10n.settingsPrivacyHint,
),
),
SettingsMenuTile(
icon: Icons.history_toggle_off_rounded,
title: l10n.settingsPrivacyHistoryVisibility,
subtitle: l10n.settingsComingSoon,
tint: colors.primary,
background: colors.surfaceContainerHighest,
showDivider: false,
onTap: () => _openPlaceholder(
context,
title: l10n.settingsPrivacyHistoryVisibility,
value: l10n.settingsComingSoon,
description: l10n.settingsPrivacyHint,
),
),
],
),
const SizedBox(height: AppSpacing.xl),
SectionLabel(text: l10n.settingsSectionNotification),
SettingsGroupCard(
children: [
SettingsMenuTile(
icon: Icons.notifications_outlined,
title: l10n.settingsNotificationSystem,
subtitle: l10n.settingsPlaceholderState(
settings.notification.length,
),
tint: colors.secondary,
background: colors.surfaceContainerHighest,
onTap: () => _openPlaceholder(
context,
title: l10n.settingsNotificationSystem,
value: l10n.settingsComingSoon,
description: l10n.settingsNotificationHint,
),
),
SettingsMenuTile(
icon: Icons.campaign_outlined,
title: l10n.settingsNotificationActivity,
subtitle: l10n.settingsComingSoon,
tint: colors.secondary,
background: colors.surfaceContainerHighest,
onTap: () => _openPlaceholder(
context,
title: l10n.settingsNotificationActivity,
value: l10n.settingsComingSoon,
description: l10n.settingsNotificationHint,
),
),
SettingsMenuTile(
icon: Icons.auto_graph_outlined,
title: l10n.settingsNotificationResult,
subtitle: l10n.settingsComingSoon,
tint: colors.secondary,
background: colors.surfaceContainerHighest,
showDivider: false,
onTap: () => _openPlaceholder(
context,
title: l10n.settingsNotificationResult,
value: l10n.settingsComingSoon,
description: l10n.settingsNotificationHint,
),
),
],
),
],
),
);
}
Future<void> _openPlaceholder(
BuildContext context, {
required String title,
required String value,
required String description,
}) async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => SettingsPlaceholderScreen(
title: title,
value: value,
description: description,
),
),
);
}
}
@@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../widgets/settings_section_widgets.dart';
class SettingsPlaceholderScreen extends StatelessWidget {
const SettingsPlaceholderScreen({
super.key,
required this.title,
required this.value,
required this.description,
});
final String title;
final String value;
final String description;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(title),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
SectionLabel(text: l10n.settingsCurrentValue),
SettingsGroupCard(
children: [
SettingsMenuTile(
icon: Icons.info_outline_rounded,
title: title,
subtitle: value,
tint: colors.primary,
background: colors.surfaceContainerHighest,
showDivider: false,
showChevron: false,
onTap: () {},
),
],
),
const SizedBox(height: AppSpacing.lg),
Card(
margin: EdgeInsets.zero,
elevation: 0,
color: colors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Text(
description,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
],
),
);
}
}
@@ -0,0 +1,234 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/app_color_palette.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../data/models/profile_settings.dart';
import '../widgets/settings_section_widgets.dart';
import 'coin_center_screen.dart';
import 'general_settings_screen.dart';
import 'legal_center_screen.dart';
import 'privacy_notification_settings_screen.dart';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({
super.key,
required this.account,
required this.settings,
required this.coinBalance,
required this.onInterfaceLanguageChanged,
required this.onLogout,
});
final String account;
final ProfileSettingsV1 settings;
final int coinBalance;
final Future<void> Function(String languageTag) onInterfaceLanguageChanged;
final Future<void> Function() onLogout;
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
late ProfileSettingsV1 _settings;
bool _isLoggingOut = false;
@override
void initState() {
super.initState();
_settings = widget.settings;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.settingsTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: ListView(
padding: const EdgeInsets.fromLTRB(
AppSpacing.lg,
AppSpacing.md,
AppSpacing.lg,
AppSpacing.xl,
),
children: [
ProfileHeaderCard(
account: widget.account,
version: _settings.version,
),
const SizedBox(height: AppSpacing.lg),
WalletHeroCard(
balance: widget.coinBalance,
subtitle: l10n.settingsCoinHeroSubtitle,
onTap: _openCoinCenter,
),
const SizedBox(height: AppSpacing.xl),
SectionLabel(text: l10n.settingsSectionQuickAccess),
SettingsGroupCard(
children: [
SettingsMenuTile(
icon: Icons.toll_rounded,
title: l10n.settingsCoinCenterTitle,
subtitle: l10n.settingsCoinCenterSubtitle(widget.coinBalance),
tint: palette.historyGoldText,
background: palette.historyGoldBg,
onTap: _openCoinCenter,
),
SettingsMenuTile(
icon: Icons.tune_rounded,
title: l10n.settingsGeneralTitle,
subtitle: l10n.settingsGeneralSubtitle(
displayLanguageLabel(
l10n,
_settings.preferences.interfaceLanguage,
),
),
tint: colors.primary,
background: colors.surfaceContainerHighest,
onTap: _openGeneralSettings,
),
SettingsMenuTile(
icon: Icons.privacy_tip_outlined,
title: l10n.settingsPrivacyAndNotificationTitle,
subtitle: l10n.settingsPrivacyAndNotificationSubtitle,
tint: palette.warning,
background: palette.warningContainer,
onTap: _openPrivacyAndNotification,
),
SettingsMenuTile(
icon: Icons.description_outlined,
title: l10n.settingsLegalCenterTitle,
subtitle: l10n.settingsLegalCenterSubtitle,
tint: colors.secondary,
background: colors.surfaceContainerHighest,
showDivider: false,
onTap: _openLegalCenter,
),
],
),
const SizedBox(height: AppSpacing.xl),
SectionLabel(text: l10n.settingsSectionAccount),
SettingsGroupCard(
children: [
SettingsMenuTile(
icon: Icons.logout_rounded,
title: l10n.logout,
subtitle: l10n.settingsLogoutSubtitle,
tint: colors.error,
background: colors.error.withValues(alpha: 0.08),
showDivider: false,
onTap: _confirmLogout,
),
],
),
const SizedBox(height: AppSpacing.xl),
FilledButton(
onPressed: _isLoggingOut ? null : _confirmLogout,
style: FilledButton.styleFrom(
elevation: 0,
backgroundColor: colors.error,
foregroundColor: colors.onError,
padding: const EdgeInsets.symmetric(vertical: AppSpacing.lg),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.full),
),
),
child: Text(l10n.logout),
),
],
),
);
}
Future<void> _openCoinCenter() async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => CoinCenterScreen(balance: widget.coinBalance),
),
);
}
Future<void> _openGeneralSettings() async {
final result = await Navigator.of(context).push<ProfileSettingsV1>(
MaterialPageRoute<ProfileSettingsV1>(
builder: (_) => GeneralSettingsScreen(
settings: _settings,
onInterfaceLanguageChanged: widget.onInterfaceLanguageChanged,
),
),
);
if (result == null || !mounted) {
return;
}
setState(() {
_settings = result;
});
}
Future<void> _openPrivacyAndNotification() async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => PrivacyNotificationSettingsScreen(settings: _settings),
),
);
}
Future<void> _openLegalCenter() async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(builder: (_) => const LegalCenterScreen()),
);
}
Future<void> _confirmLogout() async {
final l10n = AppLocalizations.of(context)!;
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) {
return AlertDialog(
title: Text(l10n.settingsLogoutDialogTitle),
content: Text(l10n.settingsLogoutDialogBody),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(l10n.settingsCancel),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(dialogContext).colorScheme.error,
foregroundColor: Theme.of(dialogContext).colorScheme.onError,
),
child: Text(l10n.logout),
),
],
);
},
);
if (confirmed != true) {
return;
}
setState(() {
_isLoggingOut = true;
});
try {
await widget.onLogout();
} finally {
if (mounted) {
setState(() {
_isLoggingOut = false;
});
}
}
}
}
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../models/legal_document_type.dart';
IconData legalDocumentIcon(LegalDocumentType type) {
return switch (type) {
LegalDocumentType.aboutUs => Icons.info_outline_rounded,
LegalDocumentType.privacyPolicy => Icons.security_rounded,
LegalDocumentType.termsOfService => Icons.description_outlined,
};
}
String legalDocumentAssetPath(Locale locale, LegalDocumentType type) {
final localeFolder = locale.languageCode == 'en' ? 'en' : 'zh';
final fileName = switch (type) {
LegalDocumentType.aboutUs => 'about_us.md',
LegalDocumentType.privacyPolicy => 'privacy_policy.md',
LegalDocumentType.termsOfService => 'terms_of_service.md',
};
return 'assets/legal/$localeFolder/$fileName';
}
String legalDocumentTitle(AppLocalizations l10n, LegalDocumentType type) {
return switch (type) {
LegalDocumentType.aboutUs => l10n.aboutUs,
LegalDocumentType.privacyPolicy => l10n.privacyPolicy,
LegalDocumentType.termsOfService => l10n.termsOfService,
};
}
String legalDocumentSubtitle(AppLocalizations l10n, LegalDocumentType type) {
return switch (type) {
LegalDocumentType.aboutUs => l10n.aboutUsSubtitle,
LegalDocumentType.privacyPolicy => l10n.privacyPolicySubtitle,
LegalDocumentType.termsOfService => l10n.termsOfServiceSubtitle,
};
}
@@ -0,0 +1,346 @@
import 'package:flutter/material.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/app_color_palette.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
class SectionLabel extends StatelessWidget {
const SectionLabel({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(
left: AppSpacing.sm,
bottom: AppSpacing.sm,
),
child: Text(
text,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colors.primary,
fontWeight: FontWeight.w700,
),
),
);
}
}
class SettingsGroupCard extends StatelessWidget {
const SettingsGroupCard({super.key, required this.children});
final List<Widget> children;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Card(
margin: EdgeInsets.zero,
elevation: 0,
color: colors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Column(children: children),
);
}
}
class SettingsMenuTile extends StatelessWidget {
const SettingsMenuTile({
super.key,
required this.icon,
required this.title,
required this.subtitle,
required this.tint,
required this.background,
required this.onTap,
this.showDivider = true,
this.showChevron = true,
this.trailing,
});
final IconData icon;
final String title;
final String subtitle;
final Color tint;
final Color background;
final VoidCallback onTap;
final bool showDivider;
final bool showChevron;
final Widget? trailing;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Column(
children: [
ListTile(
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg,
vertical: AppSpacing.sm,
),
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: background,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: Icon(icon, color: tint),
),
title: Text(title),
subtitle: Padding(
padding: const EdgeInsets.only(top: AppSpacing.xs),
child: Text(subtitle),
),
trailing:
trailing ??
(showChevron
? Icon(Icons.chevron_right_rounded, color: colors.outline)
: null),
),
if (showDivider)
Divider(
height: 1,
indent: 72,
endIndent: AppSpacing.lg,
color: colors.outline,
),
],
);
}
}
class ProfileHeaderCard extends StatelessWidget {
const ProfileHeaderCard({
super.key,
required this.account,
required this.version,
});
final String account;
final int version;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
return Card(
margin: EdgeInsets.zero,
elevation: 0,
color: colors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Row(
children: [
CircleAvatar(
radius: 28,
backgroundColor: colors.surfaceContainerHighest,
child: Icon(Icons.person_rounded, color: colors.primary),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(account, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: AppSpacing.xs),
Text(
'${l10n.settingsVersionLabel}: v$version',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
),
);
}
}
class WalletHeroCard extends StatelessWidget {
const WalletHeroCard({
super.key,
required this.balance,
required this.subtitle,
required this.onTap,
});
final int balance;
final String subtitle;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.xl),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppRadius.xl),
gradient: LinearGradient(
colors: [colors.primary, palette.accentPurple],
),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colors.onPrimary.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
child: Icon(
Icons.monetization_on_rounded,
color: colors.onPrimary,
),
),
const Spacer(),
Icon(
Icons.chevron_right_rounded,
color: colors.onPrimary.withValues(alpha: 0.9),
),
],
),
const SizedBox(height: AppSpacing.lg),
Text(
l10n.settingsCoinBalanceValue(balance),
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(color: colors.onPrimary),
),
const SizedBox(height: AppSpacing.xs),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colors.onPrimary.withValues(alpha: 0.92),
),
),
],
),
),
),
);
}
}
class CoinPackageCard extends StatelessWidget {
const CoinPackageCard({
super.key,
required this.title,
required this.price,
required this.amount,
this.badge,
});
final String title;
final String price;
final int amount;
final String? badge;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
final palette = Theme.of(context).extension<AppColorPalette>()!;
return Card(
margin: EdgeInsets.zero,
elevation: 0,
color: colors.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Padding(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: AppSpacing.xs),
Text(
l10n.settingsCoinAmount(amount),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
if (badge != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.sm,
vertical: AppSpacing.xs,
),
decoration: BoxDecoration(
color: palette.historyGoldBg,
borderRadius: BorderRadius.circular(AppRadius.full),
),
child: Text(
badge!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: palette.historyGoldText,
fontWeight: FontWeight.w700,
),
),
),
],
),
const SizedBox(height: AppSpacing.lg),
Row(
children: [
Text(
price,
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(color: colors.primary),
),
const Spacer(),
FilledButton(
onPressed: () {
Toast.show(
context,
l10n.settingsPurchasePending,
type: ToastType.info,
);
},
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.full),
),
),
child: Text(l10n.settingsPurchaseButton),
),
],
),
],
),
),
);
}
}