mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
## Summary - reorganize mobile settings around profile, appearance, and connection cards - add System/Light/Dark theme pairing, accent selection, and the Buzz gradient theme - align avatar badges, status editing, and supporting mobile chrome ## Test plan - `just mobile-check` - `just mobile-test`
116 lines
3.7 KiB
Dart
116 lines
3.7 KiB
Dart
import 'theme_catalog.dart';
|
|
|
|
/// Light → dark theme counterparts, ported from the desktop app's `THEME_PAIRS`
|
|
/// (`desktop/src/shared/theme/theme-loader.ts`) so both clients offer the same
|
|
/// System-mode pairings.
|
|
///
|
|
/// Buzz leads the map the way it leads desktop's, so the first-party pair sorts
|
|
/// ahead of the borrowed syntax themes wherever insertion order is preserved.
|
|
const themePairs = <String, String>{
|
|
'buzz': 'buzz-dark',
|
|
'catppuccin-latte': 'catppuccin-mocha',
|
|
'everforest-light': 'everforest-dark',
|
|
'github-light': 'github-dark',
|
|
'github-light-default': 'github-dark-default',
|
|
'github-light-high-contrast': 'github-dark-high-contrast',
|
|
'gruvbox-light-hard': 'gruvbox-dark-hard',
|
|
'gruvbox-light-medium': 'gruvbox-dark-medium',
|
|
'gruvbox-light-soft': 'gruvbox-dark-soft',
|
|
'kanagawa-lotus': 'kanagawa-wave',
|
|
'light-plus': 'dark-plus',
|
|
'material-theme-lighter': 'material-theme',
|
|
'min-light': 'min-dark',
|
|
'one-light': 'one-dark-pro',
|
|
'rose-pine-dawn': 'rose-pine',
|
|
'slack-ochin': 'slack-dark',
|
|
'solarized-light': 'solarized-dark',
|
|
'vitesse-light': 'vitesse-dark',
|
|
};
|
|
|
|
final Map<String, String> _darkToLight = {
|
|
for (final entry in themePairs.entries) entry.value: entry.key,
|
|
};
|
|
|
|
/// The counterpart of [name] in the opposite brightness, or null when the theme
|
|
/// has no pair. Resolves in both directions.
|
|
String? themePairFor(String name) => themePairs[name] ?? _darkToLight[name];
|
|
|
|
/// Whether [name] participates in a light/dark pair, and so can follow the OS.
|
|
bool isPairedTheme(String name) => themePairFor(name) != null;
|
|
|
|
/// [themeCatalog] partitioned the way the appearance picker offers it.
|
|
class ThemeGroups {
|
|
/// Light members of light/dark pairs — the System-mode options. Each stands
|
|
/// in for both halves of its pair.
|
|
final List<ThemeColors> paired;
|
|
|
|
/// Every light theme, paired or not — the Light-mode options.
|
|
final List<ThemeColors> light;
|
|
|
|
/// Every dark theme, paired or not — the Dark-mode options.
|
|
final List<ThemeColors> dark;
|
|
|
|
const ThemeGroups({
|
|
required this.paired,
|
|
required this.light,
|
|
required this.dark,
|
|
});
|
|
}
|
|
|
|
ThemeGroups? _groups;
|
|
|
|
/// [themeCatalog] grouped for the appearance picker, each list sorted by
|
|
/// display name. Computed once — the catalog is a compile-time constant.
|
|
ThemeGroups themeGroups() {
|
|
final cached = _groups;
|
|
if (cached != null) return cached;
|
|
|
|
final paired = <ThemeColors>[];
|
|
final light = <ThemeColors>[];
|
|
final dark = <ThemeColors>[];
|
|
|
|
for (final theme in themeCatalog) {
|
|
if (theme.isDark) {
|
|
dark.add(theme);
|
|
continue;
|
|
}
|
|
light.add(theme);
|
|
if (themePairs.containsKey(theme.name)) paired.add(theme);
|
|
}
|
|
|
|
int byDisplayName(ThemeColors a, ThemeColors b) =>
|
|
a.displayName.compareTo(b.displayName);
|
|
paired.sort(byDisplayName);
|
|
light.sort(byDisplayName);
|
|
dark.sort(byDisplayName);
|
|
|
|
return _groups = ThemeGroups(paired: paired, light: light, dark: dark);
|
|
}
|
|
|
|
/// Tokens that only describe a theme's brightness, stripped from paired labels.
|
|
const _modeTokens = <String>{
|
|
'light',
|
|
'latte',
|
|
'dawn',
|
|
'lotus',
|
|
'ochin',
|
|
'lighter',
|
|
'plus',
|
|
};
|
|
|
|
/// Display label for a pair, derived from its light member: strips the
|
|
/// mode-specific tokens so `github-light` reads as "Github" and stands for both
|
|
/// halves. Mirrors desktop's `pairedThemeLabel`.
|
|
String pairedThemeLabel(String lightName) {
|
|
final stripped = lightName
|
|
.split('-')
|
|
.where((token) => !_modeTokens.contains(token))
|
|
.toList();
|
|
// Stripping can remove everything (e.g. "light-plus") — fall back to the raw
|
|
// name so the row is never blank.
|
|
final parts = stripped.isEmpty ? lightName.split('-') : stripped;
|
|
return parts
|
|
.map((w) => w.isEmpty ? w : '${w[0].toUpperCase()}${w.substring(1)}')
|
|
.join(' ');
|
|
}
|