bharat_erp/lib/modules/settings/presentation/screens/appearance_settings_screen.dart
2026-07-20 09:47:39 +05:30

634 lines
20 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/constants/enums.dart';
import '../../../../core/theme/branding_config.dart';
import '../../../../core/theme/theme_provider.dart';
import '../../../../shared/widgets/app_button.dart';
import '../providers/settings_provider.dart';
import '../widgets/settings_widgets.dart';
import '../../../../shared/widgets/app_toast.dart';
class _BrandThemePreset {
const _BrandThemePreset({
required this.name,
required this.primary,
required this.secondary,
});
final String name;
final int primary;
final int secondary;
bool matches(BrandingConfig branding) =>
branding.primaryColorValue == primary &&
branding.secondaryColorValue == secondary;
}
const _brandThemePresets = <_BrandThemePreset>[
_BrandThemePreset(
name: 'Ocean Blue',
primary: 0xFF2563EB,
secondary: 0xFF0891B2,
),
_BrandThemePreset(
name: 'Royal Purple',
primary: 0xFF6D28D9,
secondary: 0xFF0F766E,
),
_BrandThemePreset(
name: 'Emerald Green',
primary: 0xFF15803D,
secondary: 0xFF0F766E,
),
_BrandThemePreset(
name: 'Sunset Orange',
primary: 0xFFEA580C,
secondary: 0xFFD97706,
),
_BrandThemePreset(
name: 'Crimson Red',
primary: 0xFFDC2626,
secondary: 0xFFB45309,
),
_BrandThemePreset(
name: 'Indigo Sky',
primary: 0xFF4F46E5,
secondary: 0xFF0284C7,
),
_BrandThemePreset(
name: 'Slate Blue',
primary: 0xFF334155,
secondary: 0xFF2563EB,
),
_BrandThemePreset(
name: 'Teal Mint',
primary: 0xFF0F766E,
secondary: 0xFF059669,
),
_BrandThemePreset(
name: 'Rose Pink',
primary: 0xFFDB2777,
secondary: 0xFF7C3AED,
),
_BrandThemePreset(
name: 'Charcoal Gold',
primary: 0xFF374151,
secondary: 0xFFD97706,
),
];
class AppearanceSettingsScreen extends ConsumerWidget {
const AppearanceSettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
final branding = ref.watch(brandingProvider);
final uiPrefs = ref.watch(appSettingsProvider).uiPreferences;
final selectedPreset = _brandThemePresets
.where((preset) => preset.matches(branding))
.firstOrNull;
return SettingsPageLayout(
title: 'Appearance',
subtitle: 'Theme, branding, and UI preferences',
child: Column(
children: [
SettingsFormCard(
title: 'Theme',
children: [
...ThemeModeOption.values.map(
(mode) => RadioListTile<ThemeModeOption>(
title: Text(_themeLabel(mode)),
value: mode,
groupValue: themeMode,
onChanged: (v) {
if (v != null) {
ref.read(themeModeProvider.notifier).setThemeMode(v);
}
},
),
),
],
),
const SizedBox(height: 16),
SettingsFormCard(
title: 'Branding',
subtitle:
'Professionally curated primary and secondary color pairs',
children: [
_BrandingPreviewCard(
primary: branding.primaryColor,
secondary: branding.secondaryColor,
themeName: selectedPreset?.name ?? 'Custom',
primaryHex: _hex(branding.primaryColorValue),
secondaryHex: _hex(branding.secondaryColorValue),
),
const SizedBox(height: 20),
Text(
'Color themes',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
'Choose a cohesive pair optimized for light and dark modes.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final crossAxisCount = width >= 720
? 3
: width >= 480
? 2
: 1;
final spacing = 12.0;
final itemWidth =
(width - spacing * (crossAxisCount - 1)) / crossAxisCount;
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: [
for (final preset in _brandThemePresets)
SizedBox(
width: itemWidth,
child: _ThemePresetCard(
preset: preset,
selected: preset.matches(branding),
onTap: () {
ref.read(brandingProvider.notifier).updateBranding(
branding.copyWith(
primaryColorValue: preset.primary,
secondaryColorValue: preset.secondary,
),
);
},
),
),
],
);
},
),
],
),
const SizedBox(height: 16),
SettingsFormCard(
title: 'Navigation Layout',
subtitle: 'Choose how the main menu is displayed',
children: [
...NavigationLayout.values.map(
(layout) => RadioListTile<NavigationLayout>(
title: Text(layout.label),
subtitle: Text(
layout == NavigationLayout.sidebar
? 'Vertical sidebar on the left (default)'
: 'Horizontal menu bar at the top',
),
value: layout,
groupValue:
NavigationLayout.fromValue(uiPrefs.navigationLayout),
onChanged: (v) {
if (v != null) {
ref
.read(appSettingsProvider.notifier)
.updateUiPreferences(
uiPrefs.copyWith(navigationLayout: v.value),
);
}
},
),
),
],
),
const SizedBox(height: 16),
SettingsFormCard(
title: 'UI Preferences',
children: [
SettingsSwitchTile(
title: 'Sidebar Expanded By Default',
subtitle: 'Show full sidebar labels on login',
value: uiPrefs.sidebarExpanded,
onChanged: (v) => ref
.read(appSettingsProvider.notifier)
.updateUiPreferences(uiPrefs.copyWith(sidebarExpanded: v)),
),
SettingsDropdownField<String>(
label: 'Dashboard Layout',
value: uiPrefs.dashboardLayout,
items: const ['default', 'compact', 'analytics'],
itemLabel: (v) => v[0].toUpperCase() + v.substring(1),
onChanged: (v) {
if (v != null) {
ref.read(appSettingsProvider.notifier).updateUiPreferences(
uiPrefs.copyWith(dashboardLayout: v),
);
}
},
),
const SizedBox(height: 16),
SettingsDropdownField<String>(
label: 'Table Density',
value: uiPrefs.tableDensity,
items: const ['compact', 'comfortable', 'spacious'],
itemLabel: (v) => v[0].toUpperCase() + v.substring(1),
onChanged: (v) {
if (v != null) {
ref.read(appSettingsProvider.notifier).updateUiPreferences(
uiPrefs.copyWith(tableDensity: v),
);
}
},
),
const SizedBox(height: 16),
SettingsDropdownField<int>(
label: 'Pagination Size',
value: uiPrefs.paginationSize,
items: const [10, 20, 50, 100],
itemLabel: (v) => '$v rows',
onChanged: (v) {
if (v != null) {
ref.read(appSettingsProvider.notifier).updateUiPreferences(
uiPrefs.copyWith(paginationSize: v),
);
}
},
),
],
),
const SizedBox(height: 24),
AppButton(
label: 'Settings Auto-Saved',
onPressed: () {
showAppToastFromSnackBar(
context,
const SnackBar(
content: Text('Appearance settings are saved automatically'),
),
);
},
),
],
),
);
}
String _themeLabel(ThemeModeOption mode) => switch (mode) {
ThemeModeOption.light => 'Light Theme',
ThemeModeOption.dark => 'Dark Theme',
ThemeModeOption.system => 'System Theme',
};
static String _hex(int value) =>
'#${value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}';
}
class _BrandingPreviewCard extends StatelessWidget {
const _BrandingPreviewCard({
required this.primary,
required this.secondary,
required this.themeName,
required this.primaryHex,
required this.secondaryHex,
});
final Color primary;
final Color secondary;
final String themeName;
final String primaryHex;
final String secondaryHex;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final onPrimary =
ThemeData.estimateBrightnessForColor(primary) == Brightness.dark
? Colors.white
: Colors.black87;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.2),
),
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.35),
),
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
color: primary,
child: Row(
children: [
Icon(Icons.dashboard_outlined, color: onPrimary, size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
'Bharat ERP · $themeName',
style: theme.textTheme.labelLarge?.copyWith(
color: onPrimary,
fontWeight: FontWeight.w700,
),
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: secondary,
borderRadius: BorderRadius.circular(999),
),
child: Text(
'Accent',
style: theme.textTheme.labelSmall?.copyWith(
color: ThemeData.estimateBrightnessForColor(secondary) ==
Brightness.dark
? Colors.white
: Colors.black87,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Live preview',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'How headers, primary actions, and secondary accents will look.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: () {},
style: FilledButton.styleFrom(
backgroundColor: primary,
foregroundColor: onPrimary,
),
icon: const Icon(Icons.add, size: 18),
label: const Text('Add'),
),
),
const SizedBox(width: 10),
Expanded(
child: OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: secondary,
side: BorderSide(
color: secondary.withValues(alpha: 0.55),
),
),
child: const Text('Export'),
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
_ColorSwatchLabel(
color: primary,
label: 'Primary',
hex: primaryHex,
),
const SizedBox(width: 16),
_ColorSwatchLabel(
color: secondary,
label: 'Secondary',
hex: secondaryHex,
),
],
),
],
),
),
],
),
);
}
}
class _ColorSwatchLabel extends StatelessWidget {
const _ColorSwatchLabel({
required this.color,
required this.label,
required this.hex,
});
final Color color;
final String label;
final String hex;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.25),
),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.28),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
Text(
hex,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
],
);
}
}
class _ThemePresetCard extends StatelessWidget {
const _ThemePresetCard({
required this.preset,
required this.selected,
required this.onTap,
});
final _BrandThemePreset preset;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final primary = Color(preset.primary);
final secondary = Color(preset.secondary);
final borderColor = selected
? primary
: theme.colorScheme.outline.withValues(alpha: 0.28);
return Material(
color: selected
? primary.withValues(alpha: 0.06)
: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOut,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: borderColor,
width: selected ? 2 : 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_StackedColorCircles(
primary: primary,
secondary: secondary,
),
const Spacer(),
if (selected)
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
color: primary,
shape: BoxShape.circle,
),
child: Icon(
Icons.check,
size: 14,
color: ThemeData.estimateBrightnessForColor(primary) ==
Brightness.dark
? Colors.white
: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
preset.name,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
'${AppearanceSettingsScreen._hex(preset.primary)} · '
'${AppearanceSettingsScreen._hex(preset.secondary)}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
);
}
}
class _StackedColorCircles extends StatelessWidget {
const _StackedColorCircles({
required this.primary,
required this.secondary,
});
final Color primary;
final Color secondary;
@override
Widget build(BuildContext context) {
final outline = Theme.of(context)
.colorScheme
.outline
.withValues(alpha: 0.2);
return SizedBox(
width: 56,
height: 32,
child: Stack(
children: [
Positioned(
left: 0,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: primary,
shape: BoxShape.circle,
border: Border.all(color: outline, width: 2),
),
),
),
Positioned(
left: 22,
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: secondary,
shape: BoxShape.circle,
border: Border.all(color: outline, width: 2),
),
),
),
],
),
);
}
}