feat: add system theme option

This commit is contained in:
cogwheel0
2025-09-16 16:07:29 +05:30
parent 157ad79f59
commit f1a10caafb

View File

@@ -21,10 +21,18 @@ class AppCustomizationPage extends ConsumerWidget {
final settings = ref.watch(appSettingsProvider); final settings = ref.watch(appSettingsProvider);
final themeMode = ref.watch(themeModeProvider); final themeMode = ref.watch(themeModeProvider);
final platformBrightness = MediaQuery.platformBrightnessOf(context); final platformBrightness = MediaQuery.platformBrightnessOf(context);
final bool isDarkEffective = final themeDescription = () {
themeMode == ThemeMode.dark || if (themeMode == ThemeMode.system) {
(themeMode == ThemeMode.system && final systemThemeLabel = platformBrightness == Brightness.dark
platformBrightness == Brightness.dark); ? AppLocalizations.of(context)!.themeDark
: AppLocalizations.of(context)!.themeLight;
return AppLocalizations.of(context)!.followingSystem(systemThemeLabel);
}
if (themeMode == ThemeMode.dark) {
return AppLocalizations.of(context)!.currentlyUsingDarkTheme;
}
return AppLocalizations.of(context)!.currentlyUsingLightTheme;
}();
final locale = ref.watch(localeProvider); final locale = ref.watch(localeProvider);
return Scaffold( return Scaffold(
@@ -68,7 +76,7 @@ class AppCustomizationPage extends ConsumerWidget {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: Column( child: Column(
children: [ children: [
// Dark mode toggle // Theme mode dropdown
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding, horizontal: Spacing.listItemPadding,
@@ -77,10 +85,12 @@ class AppCustomizationPage extends ConsumerWidget {
leading: Container( leading: Container(
padding: const EdgeInsets.all(Spacing.sm), padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary color: context.conduitTheme.buttonPrimary.withValues(
.withValues(alpha: Alpha.highlight), alpha: Alpha.highlight,
borderRadius: ),
BorderRadius.circular(AppBorderRadius.small), borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
), ),
child: Icon( child: Icon(
UiUtils.platformIcon( UiUtils.platformIcon(
@@ -99,117 +109,124 @@ class AppCustomizationPage extends ConsumerWidget {
), ),
), ),
subtitle: Text( subtitle: Text(
themeMode == ThemeMode.system themeDescription,
? AppLocalizations.of(context)!.followingSystem(
platformBrightness == Brightness.dark
? AppLocalizations.of(context)!.themeDark
: AppLocalizations.of(context)!.themeLight,
)
: (isDarkEffective
? AppLocalizations.of(context)!
.currentlyUsingDarkTheme
: AppLocalizations.of(context)!
.currentlyUsingLightTheme),
style: context.conduitTheme.bodySmall?.copyWith( style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary, color: context.conduitTheme.textSecondary,
), ),
), ),
trailing: Switch.adaptive( trailing: DropdownButtonHideUnderline(
value: isDarkEffective, child: DropdownButton<ThemeMode>(
onChanged: (value) { value: themeMode,
ref onChanged: (mode) {
.read(themeModeProvider.notifier) if (mode == null) return;
.setTheme(value ? ThemeMode.dark : ThemeMode.light); ref.read(themeModeProvider.notifier).setTheme(mode);
}, },
items: [
DropdownMenuItem(
value: ThemeMode.system,
child: Text(AppLocalizations.of(context)!.system),
),
DropdownMenuItem(
value: ThemeMode.light,
child: Text(
AppLocalizations.of(context)!.themeLight,
),
),
DropdownMenuItem(
value: ThemeMode.dark,
child: Text(
AppLocalizations.of(context)!.themeDark,
),
),
],
),
), ),
onTap: () {
final newValue = !isDarkEffective;
ref
.read(themeModeProvider.notifier)
.setTheme(
newValue ? ThemeMode.dark : ThemeMode.light);
},
), ),
Divider(color: context.conduitTheme.dividerColor, height: 1), Divider(color: context.conduitTheme.dividerColor, height: 1),
// App language selector // App language selector
Builder(builder: (context) { Builder(
final currentCode = locale?.languageCode ?? 'system'; builder: (context) {
final label = () { final currentCode = locale?.languageCode ?? 'system';
switch (currentCode) { final label = () {
case 'en': switch (currentCode) {
return 'English'; case 'en':
case 'de': return 'English';
return 'Deutsch'; case 'de':
case 'fr': return 'Deutsch';
return 'Français'; case 'fr':
case 'it': return 'Français';
return 'Italiano'; case 'it':
default: return 'Italiano';
return 'System'; default:
} return 'System';
}();
return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary
.withValues(alpha: Alpha.highlight),
borderRadius:
BorderRadius.circular(AppBorderRadius.small),
),
child: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.globe,
android: Icons.language,
),
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
title: Text(
AppLocalizations.of(context)!.appLanguage,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
label,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
trailing: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.chevron_right,
android: Icons.chevron_right,
),
color: context.conduitTheme.iconSecondary,
size: IconSize.small,
),
onTap: () async {
final selected = await _showLanguageSelector(
context, currentCode);
if (selected != null) {
if (selected == 'system') {
await ref
.read(localeProvider.notifier)
.setLocale(null);
} else {
await ref
.read(localeProvider.notifier)
.setLocale(Locale(selected));
}
} }
}, }();
);
}), return ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: Spacing.listItemPadding,
vertical: Spacing.sm,
),
leading: Container(
padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary
.withValues(alpha: Alpha.highlight),
borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
),
child: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.globe,
android: Icons.language,
),
color: context.conduitTheme.buttonPrimary,
size: IconSize.medium,
),
),
title: Text(
AppLocalizations.of(context)!.appLanguage,
style: context.conduitTheme.bodyLarge?.copyWith(
color: context.conduitTheme.textPrimary,
fontWeight: FontWeight.w500,
),
),
subtitle: Text(
label,
style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary,
),
),
trailing: Icon(
UiUtils.platformIcon(
ios: CupertinoIcons.chevron_right,
android: Icons.chevron_right,
),
color: context.conduitTheme.iconSecondary,
size: IconSize.small,
),
onTap: () async {
final selected = await _showLanguageSelector(
context,
currentCode,
);
if (selected != null) {
if (selected == 'system') {
await ref
.read(localeProvider.notifier)
.setLocale(null);
} else {
await ref
.read(localeProvider.notifier)
.setLocale(Locale(selected));
}
}
},
);
},
),
Divider(color: context.conduitTheme.dividerColor, height: 1), Divider(color: context.conduitTheme.dividerColor, height: 1),
SwitchListTile.adaptive( SwitchListTile.adaptive(
@@ -227,8 +244,9 @@ class AppCustomizationPage extends ConsumerWidget {
), ),
), ),
subtitle: Text( subtitle: Text(
AppLocalizations.of(context)! AppLocalizations.of(
.hideProviderInModelNamesDescription, context,
)!.hideProviderInModelNamesDescription,
style: context.conduitTheme.bodySmall?.copyWith( style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary, color: context.conduitTheme.textSecondary,
), ),
@@ -241,10 +259,12 @@ class AppCustomizationPage extends ConsumerWidget {
secondary: Container( secondary: Container(
padding: const EdgeInsets.all(Spacing.sm), padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary color: context.conduitTheme.buttonPrimary.withValues(
.withValues(alpha: Alpha.highlight), alpha: Alpha.highlight,
borderRadius: ),
BorderRadius.circular(AppBorderRadius.small), borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
), ),
child: Icon( child: Icon(
Platform.isIOS Platform.isIOS
@@ -284,13 +304,17 @@ class AppCustomizationPage extends ConsumerWidget {
...tools.map((t) => t.id), ...tools.map((t) => t.id),
}; };
// Sanitize persisted selection // Sanitize persisted selection
final selected = final selected = selectedRaw
selectedRaw.where((id) => allowed.contains(id)).take(2).toList(); .where((id) => allowed.contains(id))
.take(2)
.toList();
if (selected.length != selectedRaw.length) { if (selected.length != selectedRaw.length) {
// Persist sanitized list asynchronously // Persist sanitized list asynchronously
Future.microtask(() => ref Future.microtask(
.read(appSettingsProvider.notifier) () => ref
.setQuickPills(selected)); .read(appSettingsProvider.notifier)
.setQuickPills(selected),
);
} }
final int selectedCount = selected.length; final int selectedCount = selected.length;
@@ -302,7 +326,9 @@ class AppCustomizationPage extends ConsumerWidget {
if (current.length >= 2) return; // enforce max 2 if (current.length >= 2) return; // enforce max 2
current.add(id); current.add(id);
} }
await ref.read(appSettingsProvider.notifier).setQuickPills(current); await ref
.read(appSettingsProvider.notifier)
.setQuickPills(current);
} }
// Build dynamic tool chips list once // Build dynamic tool chips list once
@@ -334,7 +360,9 @@ class AppCustomizationPage extends ConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
AppLocalizations.of(context)!.appCustomizationSubtitle, AppLocalizations.of(
context,
)!.appCustomizationSubtitle,
style: context.conduitTheme.bodySmall?.copyWith( style: context.conduitTheme.bodySmall?.copyWith(
color: context.conduitTheme.textSecondary, color: context.conduitTheme.textSecondary,
), ),
@@ -363,7 +391,8 @@ class AppCustomizationPage extends ConsumerWidget {
? CupertinoIcons.search ? CupertinoIcons.search
: Icons.search, : Icons.search,
isSelected: selected.contains('web'), isSelected: selected.contains('web'),
onTap: (selectedCount < 2 || selected.contains('web')) onTap:
(selectedCount < 2 || selected.contains('web'))
? () => toggle('web') ? () => toggle('web')
: null, : null,
), ),
@@ -373,7 +402,9 @@ class AppCustomizationPage extends ConsumerWidget {
? CupertinoIcons.photo ? CupertinoIcons.photo
: Icons.image, : Icons.image,
isSelected: selected.contains('image'), isSelected: selected.contains('image'),
onTap: (selectedCount < 2 || selected.contains('image')) onTap:
(selectedCount < 2 ||
selected.contains('image'))
? () => toggle('image') ? () => toggle('image')
: null, : null,
), ),
@@ -408,9 +439,12 @@ class AppCustomizationPage extends ConsumerWidget {
leading: Container( leading: Container(
padding: const EdgeInsets.all(Spacing.sm), padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary color: context.conduitTheme.buttonPrimary.withValues(
.withValues(alpha: Alpha.highlight), alpha: Alpha.highlight,
borderRadius: BorderRadius.circular(AppBorderRadius.small), ),
borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
), ),
child: Icon( child: Icon(
Platform.isIOS Platform.isIOS
@@ -435,8 +469,9 @@ class AppCustomizationPage extends ConsumerWidget {
), ),
trailing: Switch.adaptive( trailing: Switch.adaptive(
value: settings.sendOnEnter, value: settings.sendOnEnter,
onChanged: (v) => onChanged: (v) => ref
ref.read(appSettingsProvider.notifier).setSendOnEnter(v), .read(appSettingsProvider.notifier)
.setSendOnEnter(v),
), ),
), ),
], ],
@@ -464,10 +499,12 @@ class AppCustomizationPage extends ConsumerWidget {
leading: Container( leading: Container(
padding: const EdgeInsets.all(Spacing.sm), padding: const EdgeInsets.all(Spacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.conduitTheme.buttonPrimary color: context.conduitTheme.buttonPrimary.withValues(
.withValues(alpha: Alpha.highlight), alpha: Alpha.highlight,
borderRadius: ),
BorderRadius.circular(AppBorderRadius.small), borderRadius: BorderRadius.circular(
AppBorderRadius.small,
),
), ),
child: Icon( child: Icon(
Platform.isIOS Platform.isIOS
@@ -509,13 +546,15 @@ class AppCustomizationPage extends ConsumerWidget {
items: [ items: [
DropdownMenuItem( DropdownMenuItem(
value: 'auto', value: 'auto',
child: Text(AppLocalizations.of(context)! child: Text(
.transportModeAuto), AppLocalizations.of(context)!.transportModeAuto,
),
), ),
DropdownMenuItem( DropdownMenuItem(
value: 'ws', value: 'ws',
child: Text(AppLocalizations.of(context)! child: Text(
.transportModeWs), AppLocalizations.of(context)!.transportModeWs,
),
), ),
], ],
decoration: InputDecoration( decoration: InputDecoration(
@@ -538,10 +577,8 @@ class AppCustomizationPage extends ConsumerWidget {
), ),
child: Text( child: Text(
settings.socketTransportMode == 'auto' settings.socketTransportMode == 'auto'
? AppLocalizations.of(context)! ? AppLocalizations.of(context)!.transportModeAutoInfo
.transportModeAutoInfo : AppLocalizations.of(context)!.transportModeWsInfo,
: AppLocalizations.of(context)!
.transportModeWsInfo,
style: context.conduitTheme.caption?.copyWith( style: context.conduitTheme.caption?.copyWith(
color: context.conduitTheme.textSecondary, color: context.conduitTheme.textSecondary,
), ),