mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-11 00:10:38 -07:00
feat: add server-side configuration with default theme (#90)
* docs: add example for usage of the main listenbrainz instance (#71) * docs: add example for usage of the main listenbrainz instance * Update scrobbler.md --------- Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com> * feat: add server-side cfg and default theme * fix: repair custom theme --------- Co-authored-by: m0d3rnX <jesper@posteo.de>
This commit is contained in:
parent
621ca63b6b
commit
68d922ce55
4 changed files with 254 additions and 206 deletions
|
|
@ -157,6 +157,10 @@ function submitListen(id: string, ts: Date): Promise<Response> {
|
|||
});
|
||||
}
|
||||
|
||||
function getCfg(): Promise<Config> {
|
||||
return fetch(`/apis/web/v1/config`).then(r => r.json() as Promise<Config>)
|
||||
}
|
||||
|
||||
function getApiKeys(): Promise<ApiKey[]> {
|
||||
return fetch(`/apis/web/v1/user/apikeys`).then(
|
||||
(r) => r.json() as Promise<ApiKey[]>
|
||||
|
|
@ -277,6 +281,7 @@ function getNowPlaying(): Promise<NowPlaying> {
|
|||
}
|
||||
|
||||
export {
|
||||
<<<<<<< HEAD
|
||||
getLastListens,
|
||||
getTopTracks,
|
||||
getTopAlbums,
|
||||
|
|
@ -308,6 +313,37 @@ export {
|
|||
submitListen,
|
||||
getNowPlaying,
|
||||
};
|
||||
=======
|
||||
getLastListens,
|
||||
getTopTracks,
|
||||
getTopAlbums,
|
||||
getTopArtists,
|
||||
getActivity,
|
||||
getStats,
|
||||
search,
|
||||
replaceImage,
|
||||
mergeTracks,
|
||||
mergeAlbums,
|
||||
mergeArtists,
|
||||
imageUrl,
|
||||
login,
|
||||
logout,
|
||||
getCfg,
|
||||
deleteItem,
|
||||
updateUser,
|
||||
getAliases,
|
||||
createAlias,
|
||||
deleteAlias,
|
||||
setPrimaryAlias,
|
||||
getApiKeys,
|
||||
createApiKey,
|
||||
deleteApiKey,
|
||||
updateApiKeyLabel,
|
||||
deleteListen,
|
||||
getAlbum,
|
||||
getExport,
|
||||
}
|
||||
>>>>>>> 5d0491a (feat: add server-side configuration with default theme (#90))
|
||||
type Track = {
|
||||
id: number;
|
||||
title: string;
|
||||
|
|
@ -390,6 +426,7 @@ type ApiKey = {
|
|||
created_at: Date;
|
||||
};
|
||||
type ApiError = {
|
||||
<<<<<<< HEAD
|
||||
error: string;
|
||||
};
|
||||
type Config = {
|
||||
|
|
@ -417,3 +454,27 @@ export type {
|
|||
Config,
|
||||
NowPlaying,
|
||||
};
|
||||
=======
|
||||
error: string
|
||||
}
|
||||
type Config = {
|
||||
default_theme: string
|
||||
}
|
||||
|
||||
export type {
|
||||
getItemsArgs,
|
||||
getActivityArgs,
|
||||
Track,
|
||||
Artist,
|
||||
Album,
|
||||
Listen,
|
||||
SearchResponse,
|
||||
PaginatedResponse,
|
||||
ListenActivityItem,
|
||||
User,
|
||||
Alias,
|
||||
ApiKey,
|
||||
ApiError,
|
||||
Config
|
||||
}
|
||||
>>>>>>> 5d0491a (feat: add server-side configuration with default theme (#90))
|
||||
|
|
|
|||
|
|
@ -1,69 +1,78 @@
|
|||
import { useState } from 'react';
|
||||
import { useTheme } from '../../hooks/useTheme';
|
||||
import themes from '~/styles/themes.css';
|
||||
import ThemeOption from './ThemeOption';
|
||||
import { AsyncButton } from '../AsyncButton';
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "../../hooks/useTheme";
|
||||
import themes from "~/styles/themes.css";
|
||||
import ThemeOption from "./ThemeOption";
|
||||
import { AsyncButton } from "../AsyncButton";
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
<<<<<<< HEAD
|
||||
const { setTheme } = useTheme();
|
||||
=======
|
||||
const { theme, themeName, setTheme } = useTheme();
|
||||
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
|
||||
const initialTheme = {
|
||||
bg: "#1e1816",
|
||||
bgSecondary: "#2f2623",
|
||||
bgTertiary: "#453733",
|
||||
fg: "#f8f3ec",
|
||||
fgSecondary: "#d6ccc2",
|
||||
fgTertiary: "#b4a89c",
|
||||
primary: "#f5a97f",
|
||||
primaryDim: "#d88b65",
|
||||
accent: "#f9db6d",
|
||||
accentDim: "#d9bc55",
|
||||
error: "#e26c6a",
|
||||
warning: "#f5b851",
|
||||
success: "#8fc48f",
|
||||
info: "#87b8dd",
|
||||
}
|
||||
|
||||
const { setCustomTheme, getCustomTheme, resetTheme } = useTheme()
|
||||
const [custom, setCustom] = useState(JSON.stringify(getCustomTheme() ?? initialTheme, null, " "))
|
||||
|
||||
const handleCustomTheme = () => {
|
||||
console.log(custom)
|
||||
try {
|
||||
const themeData = JSON.parse(custom)
|
||||
setCustomTheme(themeData)
|
||||
setCustom(JSON.stringify(themeData, null, " "))
|
||||
console.log(themeData)
|
||||
} catch(err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
const { setTheme } = useTheme();
|
||||
const initialTheme = {
|
||||
bg: "#1e1816",
|
||||
bgSecondary: "#2f2623",
|
||||
bgTertiary: "#453733",
|
||||
fg: "#f8f3ec",
|
||||
fgSecondary: "#d6ccc2",
|
||||
fgTertiary: "#b4a89c",
|
||||
primary: "#f5a97f",
|
||||
primaryDim: "#d88b65",
|
||||
accent: "#f9db6d",
|
||||
accentDim: "#d9bc55",
|
||||
error: "#e26c6a",
|
||||
warning: "#f5b851",
|
||||
success: "#8fc48f",
|
||||
info: "#87b8dd",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-10'>
|
||||
<div>
|
||||
<div className='flex items-center gap-3'>
|
||||
<h2>Select Theme</h2>
|
||||
<div className='mb-3'>
|
||||
<AsyncButton onClick={resetTheme}>Reset</AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 items-center gap-2">
|
||||
{Object.entries(themes).map(([name, themeData]) => (
|
||||
<ThemeOption setTheme={setTheme} key={name} theme={themeData} themeName={name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Use Custom Theme</h2>
|
||||
<div className="flex flex-col items-center gap-3 bg-secondary p-5 rounded-lg">
|
||||
<textarea name="custom-theme" onChange={(e) => setCustom(e.target.value)} id="custom-theme-input" className="bg-(--color-bg) h-[450px] w-[300px] p-5 rounded-md" value={custom} />
|
||||
<AsyncButton onClick={handleCustomTheme}>Submit</AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
const { setCustomTheme, getCustomTheme, resetTheme } = useTheme();
|
||||
const [custom, setCustom] = useState(
|
||||
JSON.stringify(getCustomTheme() ?? initialTheme, null, " ")
|
||||
);
|
||||
|
||||
const handleCustomTheme = () => {
|
||||
console.log(custom);
|
||||
try {
|
||||
const themeData = JSON.parse(custom);
|
||||
setCustomTheme(themeData);
|
||||
setCustom(JSON.stringify(themeData, null, " "));
|
||||
console.log(themeData);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h2>Select Theme</h2>
|
||||
<div className="mb-3">
|
||||
<AsyncButton onClick={resetTheme}>Reset</AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className="grid grid-cols-2 items-center gap-2">
|
||||
{Object.entries(themes).map(([name, themeData]) => (
|
||||
<ThemeOption
|
||||
setTheme={setTheme}
|
||||
key={name}
|
||||
theme={themeData}
|
||||
themeName={name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Use Custom Theme</h2>
|
||||
<div className="flex flex-col items-center gap-3 bg-secondary p-5 rounded-lg">
|
||||
<textarea
|
||||
name="custom-theme"
|
||||
onChange={(e) => setCustom(e.target.value)}
|
||||
id="custom-theme-input"
|
||||
className="bg-(--color-bg) h-[450px] w-[300px] p-5 rounded-md"
|
||||
value={custom}
|
||||
/>
|
||||
<AsyncButton onClick={handleCustomTheme}>Submit</AsyncButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,16 +23,19 @@ export const useAppContext = () => {
|
|||
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [user, setUser] = useState<User | null | undefined>(undefined);
|
||||
const [defaultTheme, setDefaultTheme] = useState<string | undefined>(undefined)
|
||||
const [configurableHomeActivity, setConfigurableHomeActivity] = useState<boolean>(false);
|
||||
const [defaultTheme, setDefaultTheme] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [configurableHomeActivity, setConfigurableHomeActivity] =
|
||||
useState<boolean>(false);
|
||||
const [homeItems, setHomeItems] = useState<number>(0);
|
||||
|
||||
const setUsername = (value: string) => {
|
||||
if (!user) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
setUser({...user, username: value})
|
||||
}
|
||||
setUser({ ...user, username: value });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/apis/web/v1/user/me")
|
||||
|
|
@ -45,14 +48,14 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
|||
setConfigurableHomeActivity(true);
|
||||
setHomeItems(12);
|
||||
|
||||
getCfg().then(cfg => {
|
||||
console.log(cfg)
|
||||
if (cfg.default_theme !== '') {
|
||||
setDefaultTheme(cfg.default_theme)
|
||||
getCfg().then((cfg) => {
|
||||
console.log(cfg);
|
||||
if (cfg.default_theme !== "") {
|
||||
setDefaultTheme(cfg.default_theme);
|
||||
} else {
|
||||
setDefaultTheme('yuu')
|
||||
setDefaultTheme("yuu");
|
||||
}
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Block rendering the app until config is loaded
|
||||
|
|
@ -70,5 +73,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
|||
setUsername,
|
||||
};
|
||||
|
||||
return <AppContext.Provider value={contextValue}>{children}</AppContext.Provider>;
|
||||
};
|
||||
return (
|
||||
<AppContext.Provider value={contextValue}>{children}</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,158 +1,131 @@
|
|||
import { createContext, useEffect, useState, useCallback, type ReactNode } from 'react';
|
||||
import { type Theme, themes } from '~/styles/themes.css';
|
||||
import { themeVars } from '~/styles/vars.css';
|
||||
import { useAppContext } from './AppProvider';
|
||||
import {
|
||||
createContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { type Theme, themes } from "~/styles/themes.css";
|
||||
import { themeVars } from "~/styles/vars.css";
|
||||
import { useAppContext } from "./AppProvider";
|
||||
|
||||
interface ThemeContextValue {
|
||||
themeName: string;
|
||||
theme: Theme;
|
||||
setTheme: (theme: string) => void;
|
||||
resetTheme: () => void;
|
||||
setCustomTheme: (theme: Theme) => void;
|
||||
getCustomTheme: () => Theme | undefined;
|
||||
themeName: string;
|
||||
theme: Theme;
|
||||
setTheme: (theme: string) => void;
|
||||
resetTheme: () => void;
|
||||
setCustomTheme: (theme: Theme) => void;
|
||||
getCustomTheme: () => Theme | undefined;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||
|
||||
function toKebabCase(str: string) {
|
||||
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
||||
return str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
||||
}
|
||||
|
||||
function applyCustomThemeVars(theme: Theme) {
|
||||
const root = document.documentElement;
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
if (key === 'name') continue;
|
||||
root.style.setProperty(`--color-${toKebabCase(key)}`, value);
|
||||
}
|
||||
const root = document.documentElement;
|
||||
for (const [key, value] of Object.entries(theme)) {
|
||||
if (key === "name") continue;
|
||||
root.style.setProperty(`--color-${toKebabCase(key)}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
function clearCustomThemeVars() {
|
||||
for (const cssVar of Object.values(themeVars)) {
|
||||
document.documentElement.style.removeProperty(cssVar);
|
||||
}
|
||||
for (const cssVar of Object.values(themeVars)) {
|
||||
document.documentElement.style.removeProperty(cssVar);
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredCustomTheme(): Theme | undefined {
|
||||
const themeStr = localStorage.getItem('custom-theme');
|
||||
if (!themeStr) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(themeStr);
|
||||
const { name, ...theme } = parsed;
|
||||
return theme as Theme;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const themeStr = localStorage.getItem("custom-theme");
|
||||
if (!themeStr) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(themeStr);
|
||||
const { name, ...theme } = parsed;
|
||||
return theme as Theme;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
<<<<<<< HEAD
|
||||
let defaultTheme = useAppContext().defaultTheme
|
||||
let initialTheme = localStorage.getItem("theme") ?? defaultTheme
|
||||
=======
|
||||
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
|
||||
const [themeName, setThemeName] = useState(initialTheme);
|
||||
const [currentTheme, setCurrentTheme] = useState<Theme>(() => {
|
||||
if (initialTheme === 'custom') {
|
||||
const customTheme = getStoredCustomTheme();
|
||||
<<<<<<< HEAD
|
||||
return customTheme || themes[defaultTheme];
|
||||
}
|
||||
return themes[initialTheme] || themes[defaultTheme];
|
||||
=======
|
||||
return customTheme || themes.yuu;
|
||||
}
|
||||
return themes[initialTheme] || themes.yuu;
|
||||
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
|
||||
});
|
||||
|
||||
const setTheme = (newThemeName: string) => {
|
||||
setThemeName(newThemeName);
|
||||
if (newThemeName === 'custom') {
|
||||
const customTheme = getStoredCustomTheme();
|
||||
if (customTheme) {
|
||||
setCurrentTheme(customTheme);
|
||||
} else {
|
||||
// Fallback to default theme if no custom theme found
|
||||
<<<<<<< HEAD
|
||||
setThemeName(defaultTheme);
|
||||
setCurrentTheme(themes[defaultTheme]);
|
||||
=======
|
||||
setThemeName('yuu');
|
||||
setCurrentTheme(themes.yuu);
|
||||
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
|
||||
}
|
||||
} else {
|
||||
const foundTheme = themes[newThemeName];
|
||||
if (foundTheme) {
|
||||
<<<<<<< HEAD
|
||||
localStorage.setItem('theme', newThemeName)
|
||||
setCurrentTheme(foundTheme);
|
||||
}
|
||||
}
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
let defaultTheme = useAppContext().defaultTheme;
|
||||
let initialTheme = localStorage.getItem("theme") ?? defaultTheme;
|
||||
const [themeName, setThemeName] = useState(initialTheme);
|
||||
const [currentTheme, setCurrentTheme] = useState<Theme>(() => {
|
||||
if (initialTheme === "custom") {
|
||||
const customTheme = getStoredCustomTheme();
|
||||
return customTheme || themes[defaultTheme];
|
||||
}
|
||||
return themes[initialTheme] || themes[defaultTheme];
|
||||
});
|
||||
|
||||
const resetTheme = () => {
|
||||
setThemeName(defaultTheme)
|
||||
localStorage.removeItem('theme')
|
||||
setCurrentTheme(themes[defaultTheme])
|
||||
=======
|
||||
setCurrentTheme(foundTheme);
|
||||
}
|
||||
}
|
||||
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
|
||||
}
|
||||
|
||||
const setCustomTheme = useCallback((customTheme: Theme) => {
|
||||
localStorage.setItem('custom-theme', JSON.stringify(customTheme));
|
||||
applyCustomThemeVars(customTheme);
|
||||
setThemeName('custom');
|
||||
<<<<<<< HEAD
|
||||
localStorage.setItem('theme', 'custom')
|
||||
=======
|
||||
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
|
||||
const setTheme = (newThemeName: string) => {
|
||||
setThemeName(newThemeName);
|
||||
if (newThemeName === "custom") {
|
||||
const customTheme = getStoredCustomTheme();
|
||||
if (customTheme) {
|
||||
setCurrentTheme(customTheme);
|
||||
}, []);
|
||||
|
||||
const getCustomTheme = (): Theme | undefined => {
|
||||
return getStoredCustomTheme();
|
||||
} else {
|
||||
// Fallback to default theme if no custom theme found
|
||||
setThemeName(defaultTheme);
|
||||
setCurrentTheme(themes[defaultTheme]);
|
||||
}
|
||||
} else {
|
||||
const foundTheme = themes[newThemeName];
|
||||
if (foundTheme) {
|
||||
localStorage.setItem("theme", newThemeName);
|
||||
setCurrentTheme(foundTheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
const resetTheme = () => {
|
||||
setThemeName(defaultTheme);
|
||||
localStorage.removeItem("theme");
|
||||
setCurrentTheme(themes[defaultTheme]);
|
||||
};
|
||||
|
||||
root.setAttribute('data-theme', themeName);
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
localStorage.setItem('theme', themeName);
|
||||
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
|
||||
const setCustomTheme = useCallback((customTheme: Theme) => {
|
||||
localStorage.setItem("custom-theme", JSON.stringify(customTheme));
|
||||
applyCustomThemeVars(customTheme);
|
||||
setThemeName("custom");
|
||||
localStorage.setItem("theme", "custom");
|
||||
setCurrentTheme(customTheme);
|
||||
}, []);
|
||||
|
||||
if (themeName === 'custom') {
|
||||
applyCustomThemeVars(currentTheme);
|
||||
} else {
|
||||
clearCustomThemeVars();
|
||||
}
|
||||
}, [themeName, currentTheme]);
|
||||
const getCustomTheme = (): Theme | undefined => {
|
||||
return getStoredCustomTheme();
|
||||
};
|
||||
|
||||
return (
|
||||
<<<<<<< HEAD
|
||||
<ThemeContext.Provider value={{
|
||||
themeName,
|
||||
theme: currentTheme,
|
||||
setTheme,
|
||||
resetTheme,
|
||||
setCustomTheme,
|
||||
getCustomTheme
|
||||
}}>
|
||||
=======
|
||||
<ThemeContext.Provider value={{ themeName, theme: currentTheme, setTheme, setCustomTheme, getCustomTheme }}>
|
||||
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
|
||||
root.setAttribute("data-theme", themeName);
|
||||
|
||||
if (themeName === "custom") {
|
||||
applyCustomThemeVars(currentTheme);
|
||||
} else {
|
||||
clearCustomThemeVars();
|
||||
}
|
||||
}, [themeName, currentTheme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
value={{
|
||||
themeName,
|
||||
theme: currentTheme,
|
||||
setTheme,
|
||||
resetTheme,
|
||||
setCustomTheme,
|
||||
getCustomTheme,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { ThemeContext };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue