feat: implement dynamic theme palette selection

- Introduced a new feature allowing users to select from multiple accent color palettes for buttons, cards, and chat bubbles.
- Added `AppThemePalette` provider to manage the current theme palette and persist user selections.
- Updated the `AppTheme` class to utilize the selected palette for light and dark themes, enhancing visual customization.
- Enhanced the `AppCustomizationPage` to include a palette selector, improving user experience and personalization options.
- Updated localization files to support new palette selection UI elements in multiple languages.
This commit is contained in:
cogwheel0
2025-10-02 01:58:12 +05:30
parent 5eb23b01de
commit 1fa8412e0a
23 changed files with 1011 additions and 577 deletions

View File

@@ -6,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/services/settings_service.dart';
import '../../../shared/theme/theme_extensions.dart';
import '../../../shared/theme/color_palettes.dart';
import '../../tools/providers/tools_providers.dart';
import '../../../core/models/tool.dart';
import '../../../shared/widgets/conduit_components.dart';
@@ -36,6 +37,7 @@ class AppCustomizationPage extends ConsumerWidget {
final locale = ref.watch(appLocaleProvider);
final currentLanguageCode = locale?.languageCode ?? 'system';
final languageLabel = _resolveLanguageLabel(context, currentLanguageCode);
final activePalette = ref.watch(appThemePaletteProvider);
return Scaffold(
backgroundColor: context.conduitTheme.surfaceBackground,
@@ -58,6 +60,7 @@ class AppCustomizationPage extends ConsumerWidget {
currentLanguageCode,
languageLabel,
settings,
activePalette,
),
const SizedBox(height: Spacing.sectionGap),
_buildQuickPillsSection(context, ref, settings),
@@ -110,6 +113,7 @@ class AppCustomizationPage extends ConsumerWidget {
String currentLanguageCode,
String languageLabel,
AppSettings settings,
AppColorPalette palette,
) {
final theme = context.conduitTheme;
@@ -125,6 +129,8 @@ class AppCustomizationPage extends ConsumerWidget {
const SizedBox(height: Spacing.sm),
_buildThemeSelector(context, ref, themeMode, themeDescription),
const SizedBox(height: Spacing.md),
_buildPaletteSelector(context, ref, palette),
const SizedBox(height: Spacing.md),
_CustomizationTile(
leading: _buildIconBadge(
context,
@@ -277,6 +283,53 @@ class AppCustomizationPage extends ConsumerWidget {
);
}
Widget _buildPaletteSelector(
BuildContext context,
WidgetRef ref,
AppColorPalette activePalette,
) {
final theme = context.conduitTheme;
final palettes = AppColorPalettes.all;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context)!.themePalette,
style:
theme.bodyLarge?.copyWith(
color: theme.textPrimary,
fontWeight: FontWeight.w600,
) ??
TextStyle(color: theme.textPrimary, fontWeight: FontWeight.w600),
),
const SizedBox(height: Spacing.xs),
Text(
AppLocalizations.of(context)!.themePaletteDescription,
style:
theme.bodySmall?.copyWith(color: theme.textSecondary) ??
TextStyle(color: theme.textSecondary),
),
const SizedBox(height: Spacing.sm),
ConduitCard(
padding: const EdgeInsets.all(Spacing.cardPadding),
child: Column(
children: [
for (final palette in palettes)
_PaletteOption(
palette: palette,
activeId: activePalette.id,
onSelect: () => ref
.read(appThemePaletteProvider.notifier)
.setPalette(palette.id),
),
],
),
),
],
);
}
Widget _buildThemeChip(
BuildContext context,
WidgetRef ref, {
@@ -551,6 +604,128 @@ class AppCustomizationPage extends ConsumerWidget {
}
}
class _PaletteOption extends StatelessWidget {
const _PaletteOption({
required this.palette,
required this.activeId,
required this.onSelect,
});
final AppColorPalette palette;
final String activeId;
final VoidCallback onSelect;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
final isSelected = palette.id == activeId;
final previewColors =
palette.preview ??
<Color>[
palette.light.primary,
palette.light.secondary,
palette.dark.primary,
];
return InkWell(
onTap: onSelect,
borderRadius: BorderRadius.circular(AppBorderRadius.md),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: Spacing.sm),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
isSelected ? Icons.radio_button_checked : Icons.radio_button_off,
color: isSelected ? theme.buttonPrimary : theme.iconSecondary,
size: IconSize.md,
),
const SizedBox(width: Spacing.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
palette.label,
style:
theme.bodyLarge?.copyWith(
color: theme.textPrimary,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w500,
) ??
TextStyle(
color: theme.textPrimary,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
if (isSelected)
Padding(
padding: const EdgeInsets.only(left: Spacing.xs),
child: Icon(
Icons.check_circle,
color: theme.buttonPrimary,
size: IconSize.sm,
),
),
],
),
const SizedBox(height: Spacing.xxs),
Text(
palette.description,
style:
theme.bodySmall?.copyWith(color: theme.textSecondary) ??
TextStyle(color: theme.textSecondary),
),
const SizedBox(height: Spacing.xs),
Row(
children: [
for (final color in previewColors)
_PaletteColorDot(color: color),
],
),
],
),
),
],
),
),
);
}
}
class _PaletteColorDot extends StatelessWidget {
const _PaletteColorDot({required this.color});
final Color color;
@override
Widget build(BuildContext context) {
final theme = context.conduitTheme;
return Container(
margin: const EdgeInsets.only(right: Spacing.xs),
width: 20,
height: 20,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: theme.dividerColor.withValues(alpha: 0.4),
width: 1.2,
),
),
);
}
}
class _CustomizationTile extends StatelessWidget {
const _CustomizationTile({
required this.leading,