feat: 实现起卦、设置与积分系统
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+146
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user