Skip to content
10 changes: 7 additions & 3 deletions mobile/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@ class App extends HookConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeProvider);
final accentIndex = ref.watch(accentProvider);
final schemeName = ref.watch(schemeProvider);
final communityTheme = ref.watch(communityThemeProvider);
final themeMode = communityTheme.mode;
final accentIndex = effectiveAccentIndex(
communityTheme.theme,
communityTheme.accent,
);
final schemeName = communityTheme.theme;
final authState = ref.watch(authProvider);

final resolved = resolveSchemes(schemeName, themeMode);
Expand Down
7 changes: 5 additions & 2 deletions mobile/lib/features/settings/accent_picker_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ class AccentPickerPage extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final selected = ref.watch(accentProvider);
final selected =
accentIndexForWireValue(ref.watch(communityThemeProvider).accent) ??
defaultAccentIndex;
final colorScheme = context.colors;

return FrostedScaffold(
Expand All @@ -31,7 +33,8 @@ class AccentPickerPage extends ConsumerWidget {
color: accentColorForScheme(colorScheme, i),
label: accentColors[i].name,
selected: selected == i,
onTap: () => ref.read(accentProvider.notifier).setAccent(i),
onTap: () =>
ref.read(communityThemeProvider.notifier).setAccent(i),
),
],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ class _AppearanceSection extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final mode = ref.watch(themeProvider);
final schemeName = ref.watch(schemeProvider);
final accentIndex = ref.watch(accentProvider);
final preference = ref.watch(communityThemeProvider);
final mode = preference.mode;
final schemeName = preference.theme;
final accentIndex = effectiveAccentIndex(
preference.theme,
preference.accent,
);

return AppListCard(
label: 'Style',
label: 'Style · This community',
children: [
AppListRow(
icon: LucideIcons.sunMoon,
Expand All @@ -38,16 +42,17 @@ class _AppearanceSection extends ConsumerWidget {
MaterialPageRoute<void>(builder: (_) => const ThemePickerPage()),
),
),
AppListRow(
icon: LucideIcons.droplet,
title: 'Accent color',
// The swatch *is* the value — naming the color as well would say the
// same thing twice, so it takes the chevron's place.
trailing: _AccentSwatch(accentIndex: accentIndex),
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const AccentPickerPage()),
if (!isBuzzTheme(effectiveTheme(schemeName, mode)?.name ?? schemeName))
AppListRow(
icon: LucideIcons.droplet,
title: 'Accent color',
// The swatch *is* the value — naming the color as well would say the
// same thing twice, so it takes the chevron's place.
trailing: _AccentSwatch(accentIndex: accentIndex),
onTap: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const AccentPickerPage()),
),
),
),
],
);
}
Expand All @@ -68,7 +73,7 @@ class _AppearanceModeSheet extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final mode = ref.watch(themeProvider);
final mode = ref.watch(communityThemeProvider).mode;

return SafeArea(
child: Column(
Expand Down Expand Up @@ -96,15 +101,7 @@ class _AppearanceModeSheet extends ConsumerWidget {
)
: null,
onTap: () {
final schemeName = ref.read(schemeProvider);
final compatibleScheme = schemeForAppearanceMode(
schemeName,
option.mode,
);
if (compatibleScheme != schemeName) {
ref.read(schemeProvider.notifier).setScheme(compatibleScheme);
}
ref.read(themeProvider.notifier).setMode(option.mode);
ref.read(communityThemeProvider.notifier).setMode(option.mode);
Navigator.of(context).pop();
},
),
Expand Down
9 changes: 5 additions & 4 deletions mobile/lib/features/settings/theme_picker_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ class ThemePickerPage extends HookConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final mode = ref.watch(themeProvider);
final selectedScheme = ref.watch(schemeProvider);
final preference = ref.watch(communityThemeProvider);
final mode = preference.mode;
final selectedScheme = preference.theme;
final searchQuery = useState('');
final searchController = useTextEditingController();
final scrollController = useScrollController();
Expand Down Expand Up @@ -112,8 +113,8 @@ class ThemePickerPage extends HookConsumerWidget {
label: labelFor(theme),
selected: isSelected(theme),
onTap: () => ref
.read(schemeProvider.notifier)
.setScheme(theme.name),
.read(communityThemeProvider.notifier)
.setTheme(theme.name),
);
},
),
Expand Down
80 changes: 68 additions & 12 deletions mobile/lib/shared/theme/accent_colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,78 @@ class AccentColor {
final Color light;
final Color dark;
final bool useThemeForegroundInDark;
final String wireValue;

const AccentColor({
required this.name,
required this.light,
required this.dark,
this.useThemeForegroundInDark = false,
required this.wireValue,
});
}

const accentColors = [
AccentColor(name: 'Blue', light: Color(0xFF3B82F6), dark: Color(0xFF60A5FA)),
AccentColor(name: 'Cyan', light: Color(0xFF06B6D4), dark: Color(0xFF22D3EE)),
AccentColor(name: 'Green', light: Color(0xFF22C55E), dark: Color(0xFF4ADE80)),
AccentColor(
name: 'Neutral',
light: Color(0xFF000000),
dark: Color(0xFFE1E4E8),
wireValue: 'neutral',
useThemeForegroundInDark: true,
),
AccentColor(
name: 'Blue',
light: Color(0xFF3B82F6),
dark: Color(0xFF60A5FA),
wireValue: '#3b82f6',
),
AccentColor(
name: 'Cyan',
light: Color(0xFF06B6D4),
dark: Color(0xFF22D3EE),
wireValue: '#06b6d4',
),
AccentColor(
name: 'Green',
light: Color(0xFF22C55E),
dark: Color(0xFF4ADE80),
wireValue: '#22c55e',
),
AccentColor(
name: 'Orange',
light: Color(0xFFF97316),
dark: Color(0xFFFB923C),
wireValue: '#f97316',
),
AccentColor(
name: 'Red',
light: Color(0xFFEF4444),
dark: Color(0xFFF87171),
wireValue: '#ef4444',
),
AccentColor(
name: 'Pink',
light: Color(0xFFEC4899),
dark: Color(0xFFF472B6),
wireValue: '#ec4899',
),
AccentColor(
name: 'Lilac',
light: Color(0xFFC0A2F1),
dark: Color(0xFFC0A2F1),
wireValue: '#c0a2f1',
),
AccentColor(name: 'Red', light: Color(0xFFEF4444), dark: Color(0xFFF87171)),
AccentColor(name: 'Pink', light: Color(0xFFEC4899), dark: Color(0xFFF472B6)),
AccentColor(
name: 'Purple',
light: Color(0xFFA855F7),
dark: Color(0xFFC084FC),
wireValue: '#a855f7',
),
AccentColor(
name: 'Indigo',
light: Color(0xFF6366F1),
dark: Color(0xFF818CF8),
),
AccentColor(
name: 'Black',
light: Color(0xFF000000),
dark: Color(0xFFFFFFFF),
useThemeForegroundInDark: true,
wireValue: '#6366f1',
),
];

Expand All @@ -59,7 +96,26 @@ Color accentColorForScheme(ColorScheme scheme, int accentIndex) {
///
/// Keep this at the end of [accentColors] so existing saved accent indexes keep
/// pointing at the same colors.
const defaultAccentIndex = 8;
const neutralAccentIndex = 0;
const defaultAccentIndex = neutralAccentIndex;

/// Legacy default: Catppuccin Mauve/the base theme primary.
const legacyDefaultAccentIndex = -1;

int? accentIndexForWireValue(String value) {
final index = accentColors.indexWhere((accent) => accent.wireValue == value);
return index < 0 ? null : index;
}

String legacyAccentWireValue(int? index) {
// Legacy mobile indexes 0...7 match desktop Blue...Indigo except Lilac,
// which did not exist. Black (8) has no desktop wire value, so migrate it
// deterministically to Neutral rather than publishing a mobile-only value.
if (index == 8) return 'neutral';
if (index != null && index >= 0 && index <= 5) {
return accentColors[index + 1].wireValue;
}
if (index == 6) return '#a855f7';
if (index == 7) return '#6366f1';
return '#3b82f6';
}
9 changes: 9 additions & 0 deletions mobile/lib/shared/theme/buzz_theme.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';

import 'accent_colors.dart';

/// Name of the first-party Buzz theme. Buzz reuses the GitHub Light palette for
/// every base color; the one thing that sets it apart is a branded gradient
/// painted across the app's top section. Mirrors desktop, where the same
Expand All @@ -17,6 +19,13 @@ const buzzDarkThemeName = 'buzz-dark';
bool isBuzzTheme(String themeName) =>
themeName == buzzThemeName || themeName == buzzDarkThemeName;

/// Buzz renders with its fixed neutral foreground while preserving the stored
/// wire accent so the user's choice returns on another theme.
int effectiveAccentIndex(String themeName, String storedAccent) {
if (isBuzzTheme(themeName)) return neutralAccentIndex;
return accentIndexForWireValue(storedAccent) ?? defaultAccentIndex;
}

/// Gradient stops, matching desktop's `--buzz-gradient-*` custom properties.
const _lightTop = Color(0xFFE6E6B6);
const _lightBottom = Color(0xFFC4D0DA);
Expand Down
Loading
Loading