Files
R3ap3r 3178b8f4f7 Aurora SpotiRip desktop and web app
- Spotify playlist, library, album and artist browsing with in-app playback
- Song, playlist and album downloads via spotdl with YouTube and Bandcamp fallbacks
- Electron desktop app (castLabs Widevine) with a permanent sidebar layout
- Linux and Windows packaging scripts
2026-09-25 23:20:57 -05:00

118 lines
2.7 KiB
TypeScript

import React, {
createContext,
useContext,
useEffect,
useState,
useMemo,
ReactNode,
} from "react";
export type ThemeMode = "light" | "dark" | "auto";
type ThemeChangeListener = (mode: ThemeMode) => void;
const listeners = new Set<ThemeChangeListener>();
function notifyThemeChange() {
const mode = getCurrentThemeMode();
listeners.forEach((listener) => listener(mode));
}
function subscribeToThemeChange(listener: ThemeChangeListener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
function updateTheme(darkPreferred: boolean): void {
if (darkPreferred) {
document.body.classList.add("dark");
} else {
document.body.classList.remove("dark");
}
}
let currentMediaQuery: MediaQueryList | null = null;
export function switchToDarkMode(): void {
if (currentMediaQuery) {
currentMediaQuery.onchange = null;
currentMediaQuery = null;
}
document.body.classList.add("dark");
notifyThemeChange();
}
export function switchToLightMode(): void {
if (currentMediaQuery) {
currentMediaQuery.onchange = null;
currentMediaQuery = null;
}
document.body.classList.remove("dark");
notifyThemeChange();
}
export function switchToAutoMode(): void {
if (currentMediaQuery) {
currentMediaQuery.onchange = null;
}
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.onchange = (e: MediaQueryListEvent) => {
updateTheme(e.matches);
};
currentMediaQuery = mediaQuery;
updateTheme(mediaQuery.matches);
notifyThemeChange();
}
export function getCurrentThemeMode(): ThemeMode {
if (currentMediaQuery) {
return "auto";
}
return document.body.classList.contains("dark") ? "dark" : "light";
}
interface ThemeModeContextValue {
mode: ThemeMode;
switchToDarkMode: () => void;
switchToLightMode: () => void;
switchToAutoMode: () => void;
}
const ThemeModeContext = createContext<ThemeModeContextValue | null>(null);
export function ThemeModeProvider({ children }: { children: ReactNode }) {
const [mode, setMode] = useState<ThemeMode>(() => getCurrentThemeMode());
useEffect(() => {
const unsubscribe = subscribeToThemeChange((newMode) => {
setMode(newMode);
});
return unsubscribe;
}, []);
const value = useMemo(
() => ({
mode,
switchToDarkMode,
switchToLightMode,
switchToAutoMode,
}),
[mode],
);
return (
<ThemeModeContext.Provider value={value}>
{children}
</ThemeModeContext.Provider>
);
}
export function useThemeMode(): ThemeModeContextValue {
const context = useContext(ThemeModeContext);
if (!context) {
throw new Error("useThemeMode must be used within a ThemeModeProvider");
}
return context;
}