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>
dev
Gabe Farrell 3 weeks ago committed by Gabe Farrell
parent 621ca63b6b
commit 68d922ce55

@ -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[]> { function getApiKeys(): Promise<ApiKey[]> {
return fetch(`/apis/web/v1/user/apikeys`).then( return fetch(`/apis/web/v1/user/apikeys`).then(
(r) => r.json() as Promise<ApiKey[]> (r) => r.json() as Promise<ApiKey[]>
@ -277,6 +281,7 @@ function getNowPlaying(): Promise<NowPlaying> {
} }
export { export {
<<<<<<< HEAD
getLastListens, getLastListens,
getTopTracks, getTopTracks,
getTopAlbums, getTopAlbums,
@ -308,6 +313,37 @@ export {
submitListen, submitListen,
getNowPlaying, 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 = { type Track = {
id: number; id: number;
title: string; title: string;
@ -390,6 +426,7 @@ type ApiKey = {
created_at: Date; created_at: Date;
}; };
type ApiError = { type ApiError = {
<<<<<<< HEAD
error: string; error: string;
}; };
type Config = { type Config = {
@ -417,3 +454,27 @@ export type {
Config, Config,
NowPlaying, 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,15 +1,11 @@
import { useState } from 'react'; import { useState } from "react";
import { useTheme } from '../../hooks/useTheme'; import { useTheme } from "../../hooks/useTheme";
import themes from '~/styles/themes.css'; import themes from "~/styles/themes.css";
import ThemeOption from './ThemeOption'; import ThemeOption from "./ThemeOption";
import { AsyncButton } from '../AsyncButton'; import { AsyncButton } from "../AsyncButton";
export function ThemeSwitcher() { export function ThemeSwitcher() {
<<<<<<< HEAD
const { setTheme } = useTheme(); 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 = { const initialTheme = {
bg: "#1e1816", bg: "#1e1816",
bgSecondary: "#2f2623", bgSecondary: "#2f2623",
@ -25,42 +21,55 @@ export function ThemeSwitcher() {
warning: "#f5b851", warning: "#f5b851",
success: "#8fc48f", success: "#8fc48f",
info: "#87b8dd", info: "#87b8dd",
} };
const { setCustomTheme, getCustomTheme, resetTheme } = useTheme() const { setCustomTheme, getCustomTheme, resetTheme } = useTheme();
const [custom, setCustom] = useState(JSON.stringify(getCustomTheme() ?? initialTheme, null, " ")) const [custom, setCustom] = useState(
JSON.stringify(getCustomTheme() ?? initialTheme, null, " ")
);
const handleCustomTheme = () => { const handleCustomTheme = () => {
console.log(custom) console.log(custom);
try { try {
const themeData = JSON.parse(custom) const themeData = JSON.parse(custom);
setCustomTheme(themeData) setCustomTheme(themeData);
setCustom(JSON.stringify(themeData, null, " ")) setCustom(JSON.stringify(themeData, null, " "));
console.log(themeData) console.log(themeData);
} catch (err) { } catch (err) {
console.log(err) console.log(err);
}
} }
};
return ( return (
<div className='flex flex-col gap-10'> <div className="flex flex-col gap-10">
<div> <div>
<div className='flex items-center gap-3'> <div className="flex items-center gap-3">
<h2>Select Theme</h2> <h2>Select Theme</h2>
<div className='mb-3'> <div className="mb-3">
<AsyncButton onClick={resetTheme}>Reset</AsyncButton> <AsyncButton onClick={resetTheme}>Reset</AsyncButton>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 items-center gap-2"> <div className="grid grid-cols-2 items-center gap-2">
{Object.entries(themes).map(([name, themeData]) => ( {Object.entries(themes).map(([name, themeData]) => (
<ThemeOption setTheme={setTheme} key={name} theme={themeData} themeName={name} /> <ThemeOption
setTheme={setTheme}
key={name}
theme={themeData}
themeName={name}
/>
))} ))}
</div> </div>
</div> </div>
<div> <div>
<h2>Use Custom Theme</h2> <h2>Use Custom Theme</h2>
<div className="flex flex-col items-center gap-3 bg-secondary p-5 rounded-lg"> <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} /> <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> <AsyncButton onClick={handleCustomTheme}>Submit</AsyncButton>
</div> </div>
</div> </div>

@ -23,16 +23,19 @@ export const useAppContext = () => {
export const AppProvider = ({ children }: { children: React.ReactNode }) => { export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<User | null | undefined>(undefined); const [user, setUser] = useState<User | null | undefined>(undefined);
const [defaultTheme, setDefaultTheme] = useState<string | undefined>(undefined) const [defaultTheme, setDefaultTheme] = useState<string | undefined>(
const [configurableHomeActivity, setConfigurableHomeActivity] = useState<boolean>(false); undefined
);
const [configurableHomeActivity, setConfigurableHomeActivity] =
useState<boolean>(false);
const [homeItems, setHomeItems] = useState<number>(0); const [homeItems, setHomeItems] = useState<number>(0);
const setUsername = (value: string) => { const setUsername = (value: string) => {
if (!user) { if (!user) {
return return;
}
setUser({...user, username: value})
} }
setUser({ ...user, username: value });
};
useEffect(() => { useEffect(() => {
fetch("/apis/web/v1/user/me") fetch("/apis/web/v1/user/me")
@ -45,14 +48,14 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
setConfigurableHomeActivity(true); setConfigurableHomeActivity(true);
setHomeItems(12); setHomeItems(12);
getCfg().then(cfg => { getCfg().then((cfg) => {
console.log(cfg) console.log(cfg);
if (cfg.default_theme !== '') { if (cfg.default_theme !== "") {
setDefaultTheme(cfg.default_theme) setDefaultTheme(cfg.default_theme);
} else { } else {
setDefaultTheme('yuu') setDefaultTheme("yuu");
} }
}) });
}, []); }, []);
// Block rendering the app until config is loaded // Block rendering the app until config is loaded
@ -70,5 +73,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
setUsername, setUsername,
}; };
return <AppContext.Provider value={contextValue}>{children}</AppContext.Provider>; return (
<AppContext.Provider value={contextValue}>{children}</AppContext.Provider>
);
}; };

@ -1,7 +1,13 @@
import { createContext, useEffect, useState, useCallback, type ReactNode } from 'react'; import {
import { type Theme, themes } from '~/styles/themes.css'; createContext,
import { themeVars } from '~/styles/vars.css'; useEffect,
import { useAppContext } from './AppProvider'; 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 { interface ThemeContextValue {
themeName: string; themeName: string;
@ -15,13 +21,13 @@ interface ThemeContextValue {
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined); const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function toKebabCase(str: string) { 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) { function applyCustomThemeVars(theme: Theme) {
const root = document.documentElement; const root = document.documentElement;
for (const [key, value] of Object.entries(theme)) { for (const [key, value] of Object.entries(theme)) {
if (key === 'name') continue; if (key === "name") continue;
root.style.setProperty(`--color-${toKebabCase(key)}`, value); root.style.setProperty(`--color-${toKebabCase(key)}`, value);
} }
} }
@ -33,7 +39,7 @@ function clearCustomThemeVars() {
} }
function getStoredCustomTheme(): Theme | undefined { function getStoredCustomTheme(): Theme | undefined {
const themeStr = localStorage.getItem('custom-theme'); const themeStr = localStorage.getItem("custom-theme");
if (!themeStr) return undefined; if (!themeStr) return undefined;
try { try {
const parsed = JSON.parse(themeStr); const parsed = JSON.parse(themeStr);
@ -44,93 +50,62 @@ function getStoredCustomTheme(): Theme | undefined {
} }
} }
export function ThemeProvider({ export function ThemeProvider({ children }: { children: ReactNode }) {
children, let defaultTheme = useAppContext().defaultTheme;
}: { let initialTheme = localStorage.getItem("theme") ?? defaultTheme;
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 [themeName, setThemeName] = useState(initialTheme);
const [currentTheme, setCurrentTheme] = useState<Theme>(() => { const [currentTheme, setCurrentTheme] = useState<Theme>(() => {
if (initialTheme === 'custom') { if (initialTheme === "custom") {
const customTheme = getStoredCustomTheme(); const customTheme = getStoredCustomTheme();
<<<<<<< HEAD
return customTheme || themes[defaultTheme]; return customTheme || themes[defaultTheme];
} }
return themes[initialTheme] || 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) => { const setTheme = (newThemeName: string) => {
setThemeName(newThemeName); setThemeName(newThemeName);
if (newThemeName === 'custom') { if (newThemeName === "custom") {
const customTheme = getStoredCustomTheme(); const customTheme = getStoredCustomTheme();
if (customTheme) { if (customTheme) {
setCurrentTheme(customTheme); setCurrentTheme(customTheme);
} else { } else {
// Fallback to default theme if no custom theme found // Fallback to default theme if no custom theme found
<<<<<<< HEAD
setThemeName(defaultTheme); setThemeName(defaultTheme);
setCurrentTheme(themes[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 { } else {
const foundTheme = themes[newThemeName]; const foundTheme = themes[newThemeName];
if (foundTheme) { if (foundTheme) {
<<<<<<< HEAD localStorage.setItem("theme", newThemeName);
localStorage.setItem('theme', newThemeName)
setCurrentTheme(foundTheme); setCurrentTheme(foundTheme);
} }
} }
} };
const resetTheme = () => { const resetTheme = () => {
setThemeName(defaultTheme) setThemeName(defaultTheme);
localStorage.removeItem('theme') localStorage.removeItem("theme");
setCurrentTheme(themes[defaultTheme]) 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) => { const setCustomTheme = useCallback((customTheme: Theme) => {
localStorage.setItem('custom-theme', JSON.stringify(customTheme)); localStorage.setItem("custom-theme", JSON.stringify(customTheme));
applyCustomThemeVars(customTheme); applyCustomThemeVars(customTheme);
setThemeName('custom'); setThemeName("custom");
<<<<<<< HEAD localStorage.setItem("theme", "custom");
localStorage.setItem('theme', 'custom')
=======
>>>>>>> fbaa6a6 (Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name)
setCurrentTheme(customTheme); setCurrentTheme(customTheme);
}, []); }, []);
const getCustomTheme = (): Theme | undefined => { const getCustomTheme = (): Theme | undefined => {
return getStoredCustomTheme(); return getStoredCustomTheme();
} };
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
root.setAttribute('data-theme', themeName); 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)
if (themeName === 'custom') { if (themeName === "custom") {
applyCustomThemeVars(currentTheme); applyCustomThemeVars(currentTheme);
} else { } else {
clearCustomThemeVars(); clearCustomThemeVars();
@ -138,18 +113,16 @@ export function ThemeProvider({
}, [themeName, currentTheme]); }, [themeName, currentTheme]);
return ( return (
<<<<<<< HEAD <ThemeContext.Provider
<ThemeContext.Provider value={{ value={{
themeName, themeName,
theme: currentTheme, theme: currentTheme,
setTheme, setTheme,
resetTheme, resetTheme,
setCustomTheme, setCustomTheme,
getCustomTheme 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} {children}
</ThemeContext.Provider> </ThemeContext.Provider>
); );

Loading…
Cancel
Save