update theme selection

This commit is contained in:
Phuoc Nguyen
2025-12-01 11:31:26 +07:00
parent 4ecb236532
commit 250c453413
18 changed files with 1351 additions and 304 deletions

View File

@@ -166,6 +166,14 @@ class AccountPage extends ConsumerWidget {
_showComingSoon(context);
},
),
AccountMenuItem(
icon: FontAwesomeIcons.palette,
title: 'Giao diện',
subtitle: 'Màu sắc và chế độ hiển thị',
onTap: () {
context.push(RouteNames.themeSettings);
},
),
],
),
);

View File

@@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/theme/theme_provider.dart';
/// Theme Settings Page
///
/// Allows user to customize app theme:
/// - Select seed color from predefined options
/// - Toggle light/dark mode
class ThemeSettingsPage extends ConsumerWidget {
const ThemeSettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(themeSettingsProvider);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Giao diện'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.md),
children: [
// Color Selection Section
_buildSectionTitle('Màu chủ đề'),
const SizedBox(height: AppSpacing.sm),
_buildColorGrid(context, ref, settings),
const SizedBox(height: AppSpacing.lg),
// Theme Mode Section
_buildSectionTitle('Chế độ hiển thị'),
const SizedBox(height: AppSpacing.sm),
_buildThemeModeSelector(context, ref, settings, colorScheme),
],
),
);
}
Widget _buildSectionTitle(String title) {
return Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
);
}
Widget _buildColorGrid(
BuildContext context,
WidgetRef ref,
ThemeSettings settings,
) {
const options = AppColors.seedColorOptions;
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: Column(
children: [
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: AppSpacing.md,
crossAxisSpacing: AppSpacing.md,
childAspectRatio: 1,
),
itemCount: options.length,
itemBuilder: (context, index) {
final option = options[index];
final isSelected = option.id == settings.seedColorId;
return _ColorOption(
option: option,
isSelected: isSelected,
onTap: () {
ref.read(themeSettingsProvider.notifier).setSeedColor(option.id);
},
);
},
),
const SizedBox(height: AppSpacing.md),
// Current color name
Text(
settings.seedColorOption.name,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
Widget _buildThemeModeSelector(
BuildContext context,
WidgetRef ref,
ThemeSettings settings,
ColorScheme colorScheme,
) {
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
children: [
_ThemeModeOption(
icon: FontAwesomeIcons.mobile,
title: 'Theo hệ thống',
subtitle: 'Tự động theo cài đặt thiết bị',
isSelected: settings.themeMode == ThemeMode.system,
onTap: () {
ref.read(themeSettingsProvider.notifier).setThemeMode(ThemeMode.system);
},
),
Divider(height: 1, color: colorScheme.outlineVariant),
_ThemeModeOption(
icon: FontAwesomeIcons.sun,
title: 'Sáng',
subtitle: 'Luôn sử dụng giao diện sáng',
isSelected: settings.themeMode == ThemeMode.light,
onTap: () {
ref.read(themeSettingsProvider.notifier).setThemeMode(ThemeMode.light);
},
),
Divider(height: 1, color: colorScheme.outlineVariant),
_ThemeModeOption(
icon: FontAwesomeIcons.moon,
title: 'Tối',
subtitle: 'Luôn sử dụng giao diện tối',
isSelected: settings.themeMode == ThemeMode.dark,
onTap: () {
ref.read(themeSettingsProvider.notifier).setThemeMode(ThemeMode.dark);
},
),
],
),
);
}
}
/// Color option widget
class _ColorOption extends StatelessWidget {
const _ColorOption({
required this.option,
required this.isSelected,
required this.onTap,
});
final SeedColorOption option;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: option.color,
shape: BoxShape.circle,
border: isSelected
? Border.all(
color: Theme.of(context).colorScheme.onSurface,
width: 3,
)
: null,
boxShadow: isSelected
? [
BoxShadow(
color: option.color.withValues(alpha: 0.4),
blurRadius: 8,
spreadRadius: 2,
),
]
: null,
),
child: isSelected
? const Center(
child: Icon(
Icons.check,
color: Colors.white,
size: 24,
),
)
: null,
),
);
}
}
/// Theme mode option widget
class _ThemeModeOption extends StatelessWidget {
const _ThemeModeOption({
required this.icon,
required this.title,
required this.subtitle,
required this.isSelected,
required this.onTap,
});
final IconData icon;
final String title;
final String subtitle;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Row(
children: [
FaIcon(
icon,
size: 20,
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: colorScheme.onSurface,
),
),
Text(
subtitle,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
if (isSelected)
Icon(
Icons.check_circle,
color: colorScheme.primary,
size: 24,
),
],
),
),
);
}
}

View File

@@ -9,7 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/utils/extensions.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
import 'package:worker/features/home/presentation/providers/member_card_provider.dart';
import 'package:worker/features/home/presentation/providers/promotions_provider.dart';
@@ -59,8 +59,10 @@ class _HomePageState extends ConsumerState<HomePage> {
// Watch cart item count
final cartItemCount = ref.watch(cartItemCountProvider);
final colorScheme = context.colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8), // --background-gray from CSS
backgroundColor: colorScheme.surfaceContainerLowest,
body: CustomScrollView(
slivers: [
// Add top padding for status bar
@@ -76,7 +78,7 @@ class _HomePageState extends ConsumerState<HomePage> {
margin: const EdgeInsets.all(16),
height: 200,
decoration: BoxDecoration(
color: AppColors.grey100,
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
),
child: const Center(child: CircularProgressIndicator()),
@@ -85,30 +87,30 @@ class _HomePageState extends ConsumerState<HomePage> {
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.danger.withValues(alpha: 0.1),
color: colorScheme.errorContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.circleExclamation,
color: AppColors.danger,
color: colorScheme.error,
size: 48,
),
const SizedBox(height: 8),
Text(
l10n.error,
style: const TextStyle(
color: AppColors.danger,
style: TextStyle(
color: colorScheme.error,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
error.toString(),
style: const TextStyle(
color: AppColors.grey500,
style: TextStyle(
color: colorScheme.onSurfaceVariant,
fontSize: 12,
),
textAlign: TextAlign.center,

View File

@@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/home/domain/entities/promotion.dart';
/// Promotion Slider Widget
@@ -35,19 +34,21 @@ class PromotionSlider extends StatelessWidget {
return const SizedBox.shrink();
}
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Chương trình ưu đãi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Color(0xFF212121), // --text-dark
color: colorScheme.onSurface,
),
),
),
@@ -90,13 +91,15 @@ class _PromotionCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
width: 280,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
@@ -122,16 +125,16 @@ class _PromotionCard extends StatelessWidget {
fit: BoxFit.cover,
placeholder: (context, url) => Container(
height: 140,
color: AppColors.grey100,
color: colorScheme.surfaceContainerHighest,
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
height: 140,
color: AppColors.grey100,
child: const FaIcon(
color: colorScheme.surfaceContainerHighest,
child: FaIcon(
FontAwesomeIcons.image,
size: 48,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
),
@@ -140,9 +143,9 @@ class _PromotionCard extends StatelessWidget {
// Promotion Info
Container(
padding: const EdgeInsets.all(12),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(12),
),
),
@@ -151,10 +154,10 @@ class _PromotionCard extends StatelessWidget {
children: [
Text(
promotion.title,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF212121),
color: colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -162,9 +165,9 @@ class _PromotionCard extends StatelessWidget {
const SizedBox(height: 2),
Text(
promotion.description,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: Color(0xFF666666), // --text-muted
color: colorScheme.onSurfaceVariant,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,

View File

@@ -5,7 +5,6 @@
library;
import 'package:flutter/material.dart';
import 'package:worker/core/theme/colors.dart';
/// Quick Action Item Widget
///
@@ -34,15 +33,17 @@ class QuickActionItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Material(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
@@ -63,15 +64,15 @@ class QuickActionItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Icon
Icon(icon, size: 32, color: AppColors.primaryBlue),
Icon(icon, size: 32, color: colorScheme.primary),
const SizedBox(height: 8),
// Label
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF212121), // --text-dark
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
maxLines: 1,
@@ -90,14 +91,14 @@ class QuickActionItem extends StatelessWidget {
vertical: 4,
),
decoration: BoxDecoration(
color: AppColors.danger,
color: colorScheme.error,
borderRadius: BorderRadius.circular(12),
),
constraints: const BoxConstraints(minWidth: 20),
child: Text(
badge!,
style: const TextStyle(
color: Colors.white,
style: TextStyle(
color: colorScheme.onError,
fontSize: 11,
fontWeight: FontWeight.w700,
),

View File

@@ -41,10 +41,12 @@ class QuickActionSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
@@ -63,10 +65,10 @@ class QuickActionSection extends StatelessWidget {
// Section Title
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Color(0xFF212121), // --text-dark
color: colorScheme.onSurface,
height: 1.0, // Reduce line height to minimize spacing
),
),

View File

@@ -8,14 +8,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/account/presentation/pages/account_page.dart';
import 'package:worker/features/home/presentation/pages/home_page.dart';
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
import 'package:worker/features/main/presentation/providers/current_page_provider.dart';
import 'package:worker/features/news/presentation/pages/news_list_page.dart';
import 'package:worker/features/notifications/presentation/pages/notifications_page.dart';
import 'package:worker/features/promotions/presentation/pages/promotions_page.dart';
/// Main Scaffold Page
///
@@ -31,6 +29,7 @@ class MainScaffold extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentIndex = ref.watch(currentPageIndexProvider);
final colorScheme = Theme.of(context).colorScheme;
// Define pages
final pages = [
@@ -48,11 +47,11 @@ class MainScaffold extends ConsumerWidget {
padding: const EdgeInsets.only(bottom: 20),
child: FloatingActionButton(
onPressed: () => context.push(RouteNames.chat),
backgroundColor: const Color(0xFF35C6F4), // Accent cyan color
backgroundColor: colorScheme.tertiary,
elevation: 4,
child: const Icon(
child: Icon(
Icons.chat_bubble,
color: AppColors.white,
color: colorScheme.onTertiary,
size: 28,
),
),
@@ -60,7 +59,7 @@ class MainScaffold extends ConsumerWidget {
: null,
bottomNavigationBar: Container(
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
@@ -74,9 +73,9 @@ class MainScaffold extends ConsumerWidget {
height: 70,
child: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: Colors.white,
selectedItemColor: AppColors.primaryBlue,
unselectedItemColor: const Color(0xFF666666),
backgroundColor: colorScheme.surface,
selectedItemColor: colorScheme.primary,
unselectedItemColor: colorScheme.onSurfaceVariant,
selectedFontSize: 11,
unselectedFontSize: 11,
iconSize: 24,
@@ -109,17 +108,17 @@ class MainScaffold extends ConsumerWidget {
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.danger,
color: colorScheme.error,
borderRadius: BorderRadius.circular(12),
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: const Text(
child: Text(
'5',
style: TextStyle(
color: Colors.white,
color: colorScheme.onError,
fontSize: 11,
fontWeight: FontWeight.w700,
),