feat: add server=side cfg and default theme

pull/90/head
Gabe Farrell 3 weeks ago
parent b1b8498eab
commit a436bf5c2f

@ -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,17 +68,24 @@ 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);
@ -92,7 +101,6 @@ export function ThemeProvider({
const root = document.documentElement;
root.setAttribute('data-theme', themeName);
localStorage.setItem('theme', themeName);
if (themeName === 'custom') {
applyCustomThemeVars(currentTheme);
@ -102,7 +110,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 />

@ -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…
Cancel
Save