mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-09 07:28:55 -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
70f5198781
commit
1aeb6408aa
9 changed files with 86 additions and 26 deletions
|
|
@ -101,6 +101,10 @@ function logout(): 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[]>)
|
||||
}
|
||||
|
|
@ -214,6 +218,7 @@ export {
|
|||
imageUrl,
|
||||
login,
|
||||
logout,
|
||||
getCfg,
|
||||
deleteItem,
|
||||
updateUser,
|
||||
getAliases,
|
||||
|
|
@ -309,6 +314,9 @@ type ApiKey = {
|
|||
type ApiError = {
|
||||
error: string
|
||||
}
|
||||
type Config = {
|
||||
default_theme: string
|
||||
}
|
||||
|
||||
export type {
|
||||
getItemsArgs,
|
||||
|
|
@ -323,5 +331,6 @@ export type {
|
|||
User,
|
||||
Alias,
|
||||
ApiKey,
|
||||
ApiError
|
||||
ApiError,
|
||||
Config
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
// ThemeSwitcher.tsx
|
||||
import { useEffect, useState } from 'react';
|
||||
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() {
|
||||
const { theme, themeName, setTheme } = useTheme();
|
||||
const { setTheme } = useTheme();
|
||||
const initialTheme = {
|
||||
bg: "#1e1816",
|
||||
bgSecondary: "#2f2623",
|
||||
|
|
@ -24,7 +23,7 @@ export function ThemeSwitcher() {
|
|||
info: "#87b8dd",
|
||||
}
|
||||
|
||||
const { setCustomTheme, getCustomTheme } = useTheme()
|
||||
const { setCustomTheme, getCustomTheme, resetTheme } = useTheme()
|
||||
const [custom, setCustom] = useState(JSON.stringify(getCustomTheme() ?? initialTheme, null, " "))
|
||||
|
||||
const handleCustomTheme = () => {
|
||||
|
|
@ -42,7 +41,12 @@ export function ThemeSwitcher() {
|
|||
return (
|
||||
<div className='flex flex-col gap-10'>
|
||||
<div>
|
||||
<h2>Select Theme</h2>
|
||||
<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} />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import type { User } from "api/api";
|
||||
import { getCfg, type User } from "api/api";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
interface AppContextType {
|
||||
user: User | null | undefined;
|
||||
configurableHomeActivity: boolean;
|
||||
homeItems: number;
|
||||
defaultTheme: string;
|
||||
setConfigurableHomeActivity: (value: boolean) => void;
|
||||
setHomeItems: (value: number) => void;
|
||||
setUsername: (value: string) => void;
|
||||
|
|
@ -22,6 +23,7 @@ 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 [homeItems, setHomeItems] = useState<number>(0);
|
||||
|
||||
|
|
@ -42,9 +44,15 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
|||
|
||||
setConfigurableHomeActivity(true);
|
||||
setHomeItems(12);
|
||||
|
||||
getCfg().then(cfg => {
|
||||
console.log(cfg)
|
||||
setDefaultTheme(cfg.default_theme)
|
||||
})
|
||||
}, []);
|
||||
|
||||
if (user === undefined) {
|
||||
// Block rendering the app until config is loaded
|
||||
if (user === undefined || defaultTheme === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +60,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
|||
user,
|
||||
configurableHomeActivity,
|
||||
homeItems,
|
||||
defaultTheme,
|
||||
setConfigurableHomeActivity,
|
||||
setHomeItems,
|
||||
setUsername,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -43,19 +45,19 @@ function getStoredCustomTheme(): Theme | undefined {
|
|||
}
|
||||
|
||||
export function ThemeProvider({
|
||||
theme: initialTheme,
|
||||
children,
|
||||
}: {
|
||||
theme: string;
|
||||
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.yuu;
|
||||
return customTheme || themes[defaultTheme];
|
||||
}
|
||||
return themes[initialTheme] || themes.yuu;
|
||||
return themes[initialTheme] || themes[defaultTheme];
|
||||
});
|
||||
|
||||
const setTheme = (newThemeName: string) => {
|
||||
|
|
@ -66,21 +68,29 @@ export function ThemeProvider({
|
|||
setCurrentTheme(customTheme);
|
||||
} else {
|
||||
// Fallback to default theme if no custom theme found
|
||||
setThemeName('yuu');
|
||||
setCurrentTheme(themes.yuu);
|
||||
setThemeName(defaultTheme);
|
||||
setCurrentTheme(themes[defaultTheme]);
|
||||
}
|
||||
} else {
|
||||
const foundTheme = themes[newThemeName];
|
||||
if (foundTheme) {
|
||||
localStorage.setItem('theme', newThemeName)
|
||||
setCurrentTheme(foundTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetTheme = () => {
|
||||
setThemeName(defaultTheme)
|
||||
localStorage.removeItem('theme')
|
||||
setCurrentTheme(themes[defaultTheme])
|
||||
}
|
||||
|
||||
const setCustomTheme = useCallback((customTheme: Theme) => {
|
||||
localStorage.setItem('custom-theme', JSON.stringify(customTheme));
|
||||
applyCustomThemeVars(customTheme);
|
||||
setThemeName('custom');
|
||||
localStorage.setItem('theme', 'custom')
|
||||
setCurrentTheme(customTheme);
|
||||
}, []);
|
||||
|
||||
|
|
@ -92,7 +102,6 @@ export function ThemeProvider({
|
|||
const root = document.documentElement;
|
||||
|
||||
root.setAttribute('data-theme', themeName);
|
||||
localStorage.setItem('theme', themeName);
|
||||
|
||||
if (themeName === 'custom') {
|
||||
applyCustomThemeVars(currentTheme);
|
||||
|
|
@ -102,7 +111,14 @@ export function ThemeProvider({
|
|||
}, [themeName, currentTheme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ themeName, theme: currentTheme, setTheme, setCustomTheme, getCustomTheme }}>
|
||||
<ThemeContext.Provider value={{
|
||||
themeName,
|
||||
theme: currentTheme,
|
||||
setTheme,
|
||||
resetTheme,
|
||||
setCustomTheme,
|
||||
getCustomTheme
|
||||
}}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,12 +58,10 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
|
||||
export default function App() {
|
||||
let theme = localStorage.getItem('theme') ?? 'yuu'
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="flex-col flex sm:flex-row">
|
||||
<Sidebar />
|
||||
|
|
@ -99,18 +97,12 @@ export function ErrorBoundary() {
|
|||
stack = error.stack;
|
||||
}
|
||||
|
||||
let theme = 'yuu'
|
||||
try {
|
||||
theme = localStorage.getItem('theme') ?? theme
|
||||
} catch(err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
const title = `${message} - Koito`
|
||||
|
||||
return (
|
||||
<AppProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider>
|
||||
<title>{title}</title>
|
||||
<div className="flex">
|
||||
<Sidebar />
|
||||
|
|
|
|||
|
|
@ -32,4 +32,5 @@ Once the relay is configured, Koito will automatically forward any requests it r
|
|||
|
||||
:::note
|
||||
Be sure to include the full path to the ListenBrainz endpoint of the server you are relaying to in the `KOITO_LBZ_RELAY_URL`.
|
||||
For example, to relay to the main ListenBrainz instance, you would set `KOITO_ENABLE_LBZ_RELAY` to `https://api.listenbrainz.org/1`.
|
||||
:::
|
||||
|
|
|
|||
18
engine/handlers/server_cfg.go
Normal file
18
engine/handlers/server_cfg.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gabehf/koito/internal/cfg"
|
||||
"github.com/gabehf/koito/internal/utils"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
DefaultTheme string `json:"default_theme"`
|
||||
}
|
||||
|
||||
func GetCfgHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
utils.WriteJSON(w, http.StatusOK, ServerConfig{DefaultTheme: cfg.DefaultTheme()})
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ func bindRoutes(
|
|||
Get("/images/{size}/{filename}", handlers.ImageHandler(db))
|
||||
|
||||
r.Route("/apis/web/v1", func(r chi.Router) {
|
||||
r.Get("/config", handlers.GetCfgHandler())
|
||||
r.Get("/artist", handlers.GetArtistHandler(db))
|
||||
r.Get("/artists", handlers.GetArtistsForItemHandler(db))
|
||||
r.Get("/album", handlers.GetAlbumHandler(db))
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const (
|
|||
CONFIG_DIR_ENV = "KOITO_CONFIG_DIR"
|
||||
DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME"
|
||||
DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD"
|
||||
DEFAULT_THEME_ENV = "KOITO_DEFAULT_THEME"
|
||||
DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER"
|
||||
DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE"
|
||||
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
|
||||
|
|
@ -60,6 +61,7 @@ type config struct {
|
|||
lbzRelayToken string
|
||||
defaultPw string
|
||||
defaultUsername string
|
||||
defaultTheme string
|
||||
disableDeezer bool
|
||||
disableCAA bool
|
||||
disableMusicBrainz bool
|
||||
|
|
@ -162,6 +164,8 @@ func loadConfig(getenv func(string) string, version string) (*config, error) {
|
|||
cfg.defaultPw = getenv(DEFAULT_PASSWORD_ENV)
|
||||
}
|
||||
|
||||
cfg.defaultTheme = getenv(DEFAULT_THEME_ENV)
|
||||
|
||||
cfg.configDir = getenv(CONFIG_DIR_ENV)
|
||||
if cfg.configDir == "" {
|
||||
cfg.configDir = "/etc/koito"
|
||||
|
|
@ -277,6 +281,12 @@ func DefaultUsername() string {
|
|||
return globalConfig.defaultUsername
|
||||
}
|
||||
|
||||
func DefaultTheme() string {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
return globalConfig.defaultTheme
|
||||
}
|
||||
|
||||
func FullImageCacheEnabled() bool {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue