518 lines
19 KiB
Dart
518 lines
19 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import '../providers/auth_provider.dart';
|
|
import '../providers/settings_provider.dart';
|
|
import '../theme.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
|
|
class SettingsPage extends ConsumerStatefulWidget {
|
|
const SettingsPage({super.key});
|
|
|
|
@override
|
|
ConsumerState<SettingsPage> createState() => _SettingsPageState();
|
|
}
|
|
|
|
class _SettingsPageState extends ConsumerState<SettingsPage> {
|
|
final ScrollController _scrollController = ScrollController();
|
|
bool _scrolled = false;
|
|
|
|
static const double _scrollThreshold = 120;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_scrollController.addListener(_onScroll);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.removeListener(_onScroll);
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onScroll() {
|
|
final shouldBe = _scrollController.offset > _scrollThreshold;
|
|
if (shouldBe != _scrolled) {
|
|
setState(() => _scrolled = shouldBe);
|
|
}
|
|
}
|
|
|
|
void _showThemePicker(BuildContext context) {
|
|
final current = ref.read(settingsProvider).themeMode;
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (ctx) {
|
|
final l10n = AppLocalizations.of(ctx)!;
|
|
return Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
child: SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(width: 40, height: 4, decoration: BoxDecoration(color: AppColors.divider, borderRadius: BorderRadius.circular(2))),
|
|
const SizedBox(height: 20),
|
|
Text(l10n.themeSettings, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary)),
|
|
const SizedBox(height: 20),
|
|
_themeOption(ctx, AppThemeMode.light, l10n.lightMode, Icons.wb_sunny_outlined, current),
|
|
_themeOption(ctx, AppThemeMode.dark, l10n.darkMode, Icons.nights_stay_outlined, current),
|
|
_themeOption(ctx, AppThemeMode.system, l10n.followSystem, Icons.settings_suggest_outlined, current),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _themeOption(BuildContext ctx, AppThemeMode mode, String label, IconData icon, AppThemeMode current) {
|
|
final selected = current == mode;
|
|
return ListTile(
|
|
leading: Icon(icon, color: selected ? AppColors.primary : AppColors.textTertiary),
|
|
title: Text(label, style: TextStyle(fontWeight: selected ? FontWeight.w600 : FontWeight.w400, color: AppColors.textPrimary)),
|
|
trailing: selected ? const Icon(Icons.check_circle, color: AppColors.primary) : null,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
onTap: () {
|
|
ref.read(settingsProvider.notifier).setThemeMode(mode);
|
|
Navigator.of(ctx).pop();
|
|
},
|
|
);
|
|
}
|
|
|
|
void _showLanguagePicker(BuildContext context) {
|
|
final current = ref.read(settingsProvider).locale;
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (ctx) {
|
|
final l10n = AppLocalizations.of(ctx)!;
|
|
return Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
child: SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(width: 40, height: 4, decoration: BoxDecoration(color: AppColors.divider, borderRadius: BorderRadius.circular(2))),
|
|
const SizedBox(height: 20),
|
|
Text(l10n.languageSettings, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.textPrimary)),
|
|
const SizedBox(height: 20),
|
|
_languageOption(ctx, const Locale('zh', 'CN'), l10n.simplifiedChinese, current),
|
|
_languageOption(ctx, const Locale('en', 'US'), l10n.english, current),
|
|
_languageOption(ctx, const Locale('th', 'TH'), l10n.thai, current),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _languageOption(BuildContext ctx, Locale locale, String label, Locale current) {
|
|
final selected = current.languageCode == locale.languageCode;
|
|
return ListTile(
|
|
title: Text(label, style: TextStyle(fontWeight: selected ? FontWeight.w600 : FontWeight.w400, color: AppColors.textPrimary)),
|
|
trailing: selected ? const Icon(Icons.check_circle, color: AppColors.primary) : null,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
onTap: () {
|
|
ref.read(settingsProvider.notifier).setLocale(locale);
|
|
Navigator.of(ctx).pop();
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppLocalizations.of(context)!;
|
|
final authState = ref.watch(authProvider);
|
|
final settings = ref.watch(settingsProvider);
|
|
final user = authState.user;
|
|
final isBoss = user?.isBoss ?? false;
|
|
|
|
final themeLabel = switch (settings.themeMode) {
|
|
AppThemeMode.light => l10n.lightMode,
|
|
AppThemeMode.dark => l10n.darkMode,
|
|
AppThemeMode.system => l10n.followSystem,
|
|
};
|
|
|
|
final localeLabel = switch (settings.locale.languageCode) {
|
|
'zh' => l10n.simplifiedChinese,
|
|
'en' => l10n.english,
|
|
'th' => l10n.thai,
|
|
_ => l10n.simplifiedChinese,
|
|
};
|
|
|
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
|
value: _scrolled
|
|
? (AppColors.isDark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark)
|
|
: SystemUiOverlayStyle.light,
|
|
child: Scaffold(
|
|
backgroundColor: AppColors.background,
|
|
body: Stack(
|
|
children: [
|
|
ListView(
|
|
controller: _scrollController,
|
|
padding: EdgeInsets.zero,
|
|
children: [
|
|
_buildHeader(context, user, l10n, isBoss),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 28, 20, 32),
|
|
child: Column(
|
|
children: [
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: _buildSectionTitle(l10n.appConfiguration),
|
|
),
|
|
const SizedBox(height: 14),
|
|
_buildSettingsCard([
|
|
_SettingItem(
|
|
icon: Icons.notifications_outlined,
|
|
iconColor: const Color(0xFF8B5CF6),
|
|
title: l10n.pushNotification,
|
|
subtitle: settings.pushNotification ? l10n.enabled : l10n.disabled,
|
|
trailing: Switch(
|
|
value: settings.pushNotification,
|
|
onChanged: (v) => ref.read(settingsProvider.notifier).setPushNotification(v),
|
|
activeTrackColor: AppColors.primary,
|
|
),
|
|
onTap: () => ref.read(settingsProvider.notifier).setPushNotification(!settings.pushNotification),
|
|
),
|
|
_SettingItem(
|
|
icon: Icons.volume_up_outlined,
|
|
iconColor: const Color(0xFFEC4899),
|
|
title: l10n.soundNotification,
|
|
subtitle: settings.soundNotification ? l10n.enabled : l10n.disabled,
|
|
trailing: Switch(
|
|
value: settings.soundNotification,
|
|
onChanged: (v) => ref.read(settingsProvider.notifier).setSoundNotification(v),
|
|
activeTrackColor: AppColors.primary,
|
|
),
|
|
onTap: () => ref.read(settingsProvider.notifier).setSoundNotification(!settings.soundNotification),
|
|
),
|
|
_SettingItem(
|
|
icon: Icons.palette_outlined,
|
|
iconColor: const Color(0xFFEC4899),
|
|
title: l10n.themeSettings,
|
|
subtitle: themeLabel,
|
|
onTap: () => _showThemePicker(context),
|
|
),
|
|
_SettingItem(
|
|
icon: Icons.language_outlined,
|
|
iconColor: const Color(0xFF3B82F6),
|
|
title: l10n.languageSettings,
|
|
subtitle: localeLabel,
|
|
onTap: () => _showLanguagePicker(context),
|
|
),
|
|
]),
|
|
const SizedBox(height: 28),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: _buildSectionTitle(l10n.businessExtension),
|
|
),
|
|
const SizedBox(height: 14),
|
|
_buildSettingsCard([
|
|
_SettingItem(
|
|
icon: Icons.people_outline,
|
|
iconColor: const Color(0xFF10B981),
|
|
title: l10n.employeeManagement,
|
|
subtitle: l10n.employeeDesc,
|
|
showBadge: isBoss,
|
|
onTap: () => context.push('/settings/employees'),
|
|
),
|
|
_SettingItem(
|
|
icon: Icons.analytics_outlined,
|
|
iconColor: const Color(0xFFEF4444),
|
|
title: l10n.dataReport,
|
|
subtitle: l10n.reportDesc,
|
|
showBadge: isBoss,
|
|
onTap: () => context.push('/settings/report'),
|
|
),
|
|
_SettingItem(
|
|
icon: Icons.extension_outlined,
|
|
iconColor: const Color(0xFF6366F1),
|
|
title: l10n.appMarket,
|
|
subtitle: l10n.marketDesc,
|
|
onTap: () => context.push('/settings/app-market'),
|
|
),
|
|
]),
|
|
const SizedBox(height: 28),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: _buildSectionTitle(l10n.system),
|
|
),
|
|
const SizedBox(height: 14),
|
|
_buildSettingsCard([
|
|
_SettingItem(
|
|
icon: Icons.help_outline,
|
|
iconColor: const Color(0xFF64748B),
|
|
title: l10n.helpCenter,
|
|
subtitle: l10n.helpDesc,
|
|
onTap: () => context.push('/settings/help'),
|
|
),
|
|
_SettingItem(
|
|
icon: Icons.info_outline,
|
|
iconColor: const Color(0xFF64748B),
|
|
title: l10n.aboutUs,
|
|
subtitle: '${l10n.version} 1.0.0',
|
|
onTap: () => context.push('/settings/about'),
|
|
),
|
|
]),
|
|
const SizedBox(height: 36),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 52,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
ref.read(authProvider.notifier).logout();
|
|
context.go('/login');
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.error.withValues(alpha: 0.1),
|
|
foregroundColor: AppColors.error,
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
),
|
|
child: Text(
|
|
l10n.logout,
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: SafeArea(
|
|
bottom: false,
|
|
child: SizedBox(
|
|
height: 44,
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: IconButton(
|
|
icon: Icon(
|
|
Icons.arrow_back_ios_new,
|
|
color: _scrolled ? AppColors.textPrimary : Colors.white,
|
|
size: 20,
|
|
),
|
|
onPressed: () {
|
|
if (Navigator.of(context).canPop()) {
|
|
Navigator.of(context).pop();
|
|
} else {
|
|
context.go('/home');
|
|
}
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader(BuildContext context, user, AppLocalizations l10n, bool isBoss) {
|
|
final initial = (user?.name is String && (user!.name as String).isNotEmpty)
|
|
? (user.name as String).substring(0, 1)
|
|
: l10n.user.substring(0, 1);
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
gradient: AppGradients.primary,
|
|
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(28)),
|
|
),
|
|
child: SafeArea(
|
|
bottom: false,
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(12, 60, 12, 36),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
width: 96,
|
|
height: 96,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.white.withValues(alpha: 0.16),
|
|
border: Border.all(
|
|
color: Colors.white.withValues(alpha: 0.32),
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
initial,
|
|
style: const TextStyle(
|
|
fontSize: 36,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
Text(
|
|
user?.name ?? l10n.user,
|
|
style: const TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
user?.phone ?? '',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.white.withValues(alpha: 0.82),
|
|
letterSpacing: 1,
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.22),
|
|
borderRadius: BorderRadius.circular(22),
|
|
),
|
|
child: Text(
|
|
isBoss ? l10n.boss : l10n.employee,
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w500,
|
|
letterSpacing: 1.5,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(left: 4),
|
|
child: Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.textSecondary,
|
|
letterSpacing: 1.2,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSettingsCard(List<_SettingItem> items) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: AppColors.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.03),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: items.asMap().entries.map((entry) {
|
|
final item = entry.value;
|
|
final isLast = entry.key == items.length - 1;
|
|
return Column(
|
|
children: [
|
|
ListTile(
|
|
leading: Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: item.iconColor.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Icon(item.icon, color: item.iconColor, size: 20),
|
|
),
|
|
title: Row(
|
|
children: [
|
|
Text(
|
|
item.title,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppColors.textPrimary,
|
|
),
|
|
),
|
|
if (item.showBadge) ...[
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: const BoxDecoration(
|
|
color: AppColors.error,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
subtitle: Text(
|
|
item.subtitle,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: AppColors.textTertiary,
|
|
),
|
|
),
|
|
trailing: item.trailing ?? Icon(Icons.chevron_right, color: AppColors.textTertiary, size: 20),
|
|
onTap: item.onTap,
|
|
),
|
|
if (!isLast)
|
|
const Divider(height: 1, indent: 72),
|
|
],
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SettingItem {
|
|
final IconData icon;
|
|
final Color iconColor;
|
|
final String title;
|
|
final String subtitle;
|
|
final VoidCallback onTap;
|
|
final bool showBadge;
|
|
final Widget? trailing;
|
|
|
|
const _SettingItem({
|
|
required this.icon,
|
|
required this.iconColor,
|
|
required this.title,
|
|
required this.subtitle,
|
|
required this.onTap,
|
|
this.showBadge = false,
|
|
this.trailing,
|
|
});
|
|
}
|