custom accent color + port over desktop cover art color

This commit is contained in:
Boof2015
2026-07-28 18:35:36 -04:00
parent 0b709725d2
commit d2bda52c86
18 changed files with 1402 additions and 51 deletions
+105 -12
View File
@@ -2,7 +2,14 @@ import { Appearance } from 'react-native';
import { create } from 'zustand';
import { AstraSystemColors, type SystemPalette } from '../../modules/astra-system-colors';
import { getNativeSetting, setNativeSetting } from '@/db/nativeSettings';
import { parseAccentId, DEFAULT_ACCENT, type AccentId } from '@/theme/accents';
import {
DEFAULT_ACCENT_PREFERENCE,
parseAccentPreference,
serializeAccentPreference,
type AccentId,
type AccentPreference,
} from '@/theme/accents';
import { normalizeHexColor } from '@/theme/colorUtils';
import {
parseBaseTheme,
parsePreferredDark,
@@ -21,8 +28,12 @@ import {
const BASE_THEME_KEY = 'theme_base';
const PREFERRED_DARK_KEY = 'theme_preferred_dark';
const ACCENT_KEY = 'theme_accent';
const NOW_PLAYING_ACCENT_SOURCE_KEY = 'now_playing_accent_source';
const COVER_ART_ACCENT_METHOD_KEY = 'now_playing_cover_art_method';
type SystemScheme = 'light' | 'dark';
export type NowPlayingAccentSource = 'app' | 'cover-art';
export type CoverArtAccentMethod = 'dominant' | 'vibrant' | 'average';
function currentSystemScheme(): SystemScheme {
return Appearance.getColorScheme() === 'light' ? 'light' : 'dark';
@@ -35,7 +46,7 @@ let materialYouRamps: SystemPalette | null = null;
interface ResolutionInputs {
baseTheme: BaseThemeId;
preferredDark: PreferredDark;
accentId: AccentId;
accentPreference: AccentPreference;
systemScheme: SystemScheme;
}
@@ -45,12 +56,19 @@ function recompute(inputs: ResolutionInputs): AppTheme {
interface ThemeStore extends ResolutionInputs {
materialYouAvailable: boolean;
accentPreviewHex: string | null;
nowPlayingAccentSource: NowPlayingAccentSource;
coverArtAccentMethod: CoverArtAccentMethod;
theme: AppTheme;
loaded: boolean;
load: () => Promise<void>;
setBaseTheme: (id: BaseThemeId) => Promise<void>;
setPreferredDark: (id: PreferredDark) => Promise<void>;
setAccent: (id: AccentId) => Promise<void>;
setCustomAccent: (hex: string) => Promise<void>;
previewCustomAccent: (hex: string | null) => void;
setNowPlayingAccentSource: (source: NowPlayingAccentSource) => Promise<void>;
setCoverArtAccentMethod: (method: CoverArtAccentMethod) => Promise<void>;
/** Re-reads OS scheme + monet ramps; no-op set when nothing changed. */
refreshSystemInputs: () => void;
}
@@ -58,22 +76,46 @@ interface ThemeStore extends ResolutionInputs {
const DEFAULT_INPUTS: ResolutionInputs = {
baseTheme: 'midnight',
preferredDark: 'midnight',
accentId: DEFAULT_ACCENT,
accentPreference: DEFAULT_ACCENT_PREFERENCE,
systemScheme: currentSystemScheme(),
};
function parseNowPlayingAccentSource(value: string | null): NowPlayingAccentSource {
return value === 'cover-art' ? 'cover-art' : 'app';
}
function parseCoverArtAccentMethod(value: string | null): CoverArtAccentMethod {
return value === 'average' || value === 'vibrant' ? value : 'dominant';
}
function effectiveInputs(state: ThemeStore): ResolutionInputs {
return {
baseTheme: state.baseTheme,
preferredDark: state.preferredDark,
accentPreference: state.accentPreviewHex
? { kind: 'custom', hex: state.accentPreviewHex }
: state.accentPreference,
systemScheme: state.systemScheme,
};
}
export const useThemeStore = create<ThemeStore>((set, get) => ({
...DEFAULT_INPUTS,
materialYouAvailable: AstraSystemColors.isAvailable(),
accentPreviewHex: null,
nowPlayingAccentSource: 'app',
coverArtAccentMethod: 'dominant',
theme: recompute(DEFAULT_INPUTS),
loaded: false,
load: async () => {
if (get().loaded) return;
const [base, dark, accent] = await Promise.all([
const [base, dark, accent, nowPlayingAccentSource, coverArtAccentMethod] = await Promise.all([
getNativeSetting(BASE_THEME_KEY),
getNativeSetting(PREFERRED_DARK_KEY),
getNativeSetting(ACCENT_KEY),
getNativeSetting(NOW_PLAYING_ACCENT_SOURCE_KEY),
getNativeSetting(COVER_ART_ACCENT_METHOD_KEY),
]);
if (get().materialYouAvailable) {
materialYouRamps = AstraSystemColors.getSystemPalette();
@@ -81,40 +123,91 @@ export const useThemeStore = create<ThemeStore>((set, get) => ({
const inputs: ResolutionInputs = {
baseTheme: parseBaseTheme(base),
preferredDark: parsePreferredDark(dark),
accentId: parseAccentId(accent),
accentPreference: parseAccentPreference(accent),
systemScheme: currentSystemScheme(),
};
set({ ...inputs, theme: recompute(inputs), loaded: true });
set({
...inputs,
nowPlayingAccentSource: parseNowPlayingAccentSource(nowPlayingAccentSource),
coverArtAccentMethod: parseCoverArtAccentMethod(coverArtAccentMethod),
theme: recompute(inputs),
loaded: true,
});
},
setBaseTheme: async (id) => {
if (get().baseTheme === id) return;
const inputs: ResolutionInputs = { ...get(), baseTheme: id };
const inputs: ResolutionInputs = { ...effectiveInputs(get()), baseTheme: id };
set({ baseTheme: id, theme: recompute(inputs) });
await setNativeSetting(BASE_THEME_KEY, id);
},
setPreferredDark: async (id) => {
if (get().preferredDark === id) return;
const inputs: ResolutionInputs = { ...get(), preferredDark: id };
const inputs: ResolutionInputs = { ...effectiveInputs(get()), preferredDark: id };
set({ preferredDark: id, theme: recompute(inputs) });
await setNativeSetting(PREFERRED_DARK_KEY, id);
},
setAccent: async (id) => {
if (get().accentId === id) return;
const inputs: ResolutionInputs = { ...get(), accentId: id };
set({ accentId: id, theme: recompute(inputs) });
const preference: AccentPreference = { kind: 'preset', id };
const current = get();
if (
current.accentPreviewHex === null &&
current.accentPreference.kind === 'preset' &&
current.accentPreference.id === id
) return;
const inputs: ResolutionInputs = {
...effectiveInputs(get()),
accentPreference: preference,
};
set({ accentPreference: preference, accentPreviewHex: null, theme: recompute(inputs) });
await setNativeSetting(ACCENT_KEY, id);
},
setCustomAccent: async (hex) => {
const normalized = normalizeHexColor(hex);
if (!normalized) return;
const preference: AccentPreference = { kind: 'custom', hex: normalized };
const inputs: ResolutionInputs = {
...effectiveInputs(get()),
accentPreference: preference,
};
set({ accentPreference: preference, accentPreviewHex: null, theme: recompute(inputs) });
await setNativeSetting(ACCENT_KEY, serializeAccentPreference(preference));
},
previewCustomAccent: (hex) => {
const normalized = hex === null ? null : normalizeHexColor(hex);
if (get().accentPreviewHex === normalized) return;
const inputs: ResolutionInputs = {
...effectiveInputs(get()),
accentPreference: normalized
? { kind: 'custom', hex: normalized }
: get().accentPreference,
};
set({ accentPreviewHex: normalized, theme: recompute(inputs) });
},
setNowPlayingAccentSource: async (source) => {
if (get().nowPlayingAccentSource === source) return;
set({ nowPlayingAccentSource: source });
await setNativeSetting(NOW_PLAYING_ACCENT_SOURCE_KEY, source);
},
setCoverArtAccentMethod: async (method) => {
if (get().coverArtAccentMethod === method) return;
set({ coverArtAccentMethod: method });
await setNativeSetting(COVER_ART_ACCENT_METHOD_KEY, method);
},
refreshSystemInputs: () => {
const scheme = currentSystemScheme();
const ramps = get().materialYouAvailable ? AstraSystemColors.getSystemPalette() : null;
const rampsChanged = JSON.stringify(ramps) !== JSON.stringify(materialYouRamps);
if (scheme === get().systemScheme && !rampsChanged) return;
materialYouRamps = ramps;
const inputs: ResolutionInputs = { ...get(), systemScheme: scheme };
const inputs: ResolutionInputs = { ...effectiveInputs(get()), systemScheme: scheme };
set({ systemScheme: scheme, theme: recompute(inputs) });
},
}));