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
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user