This commit is contained in:
Douwe 2026-01-20 14:31:14 +01:00 committed by GitHub
commit bb184f8ea3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 101 additions and 8 deletions

View File

@ -5,6 +5,7 @@ import copy
import json import json
import logging import logging
import os import os
import re
import traceback import traceback
import urllib import urllib
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -32,6 +33,7 @@ from frigate.config.camera.updater import (
CameraConfigUpdateEnum, CameraConfigUpdateEnum,
CameraConfigUpdateTopic, CameraConfigUpdateTopic,
) )
from frigate.const import THEMES_DIR
from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector
from frigate.jobs.media_sync import ( from frigate.jobs.media_sync import (
get_current_media_sync_job, get_current_media_sync_job,
@ -190,6 +192,28 @@ def config(request: Request):
return JSONResponse(content=config) return JSONResponse(content=config)
@router.get("/config/themes")
def config_themes():
themes_dir = THEMES_DIR
if not os.path.isdir(themes_dir):
return JSONResponse(content=[])
themes: list[str] = []
for name in sorted(os.listdir(themes_dir)):
if not name.lower().endswith(".css"):
continue
if not re.fullmatch(r"[a-zA-Z0-9._-]+\.css", name):
continue
full_path = os.path.join(themes_dir, name)
if os.path.isfile(full_path):
themes.append(name)
return JSONResponse(content=themes)
@router.get("/config/raw_paths", dependencies=[Depends(require_role(["admin"]))]) @router.get("/config/raw_paths", dependencies=[Depends(require_role(["admin"]))])
def config_raw_paths(request: Request): def config_raw_paths(request: Request):
"""Admin-only endpoint that returns camera paths and go2rtc streams without credential masking.""" """Admin-only endpoint that returns camera paths and go2rtc streams without credential masking."""

View File

@ -5,6 +5,7 @@ INSTALL_DIR = "/opt/frigate"
CONFIG_DIR = "/config" CONFIG_DIR = "/config"
DEFAULT_DB_PATH = f"{CONFIG_DIR}/frigate.db" DEFAULT_DB_PATH = f"{CONFIG_DIR}/frigate.db"
MODEL_CACHE_DIR = f"{CONFIG_DIR}/model_cache" MODEL_CACHE_DIR = f"{CONFIG_DIR}/model_cache"
THEMES_DIR = f"{CONFIG_DIR}/themes"
BASE_DIR = "/media/frigate" BASE_DIR = "/media/frigate"
CLIPS_DIR = f"{BASE_DIR}/clips" CLIPS_DIR = f"{BASE_DIR}/clips"
EXPORT_DIR = f"{BASE_DIR}/exports" EXPORT_DIR = f"{BASE_DIR}/exports"

View File

@ -487,11 +487,11 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
{scheme === colorScheme ? ( {scheme === colorScheme ? (
<> <>
<IoColorPalette className="mr-2 size-4 rotate-0 scale-100 transition-all" /> <IoColorPalette className="mr-2 size-4 rotate-0 scale-100 transition-all" />
{t(friendlyColorSchemeName(scheme))} {friendlyColorSchemeName(scheme, t)}
</> </>
) : ( ) : (
<span className="ml-6 mr-2"> <span className="ml-6 mr-2">
{t(friendlyColorSchemeName(scheme))} {friendlyColorSchemeName(scheme, t)}
</span> </span>
)} )}
</MenuItem> </MenuItem>

View File

@ -1,4 +1,5 @@
import { createContext, useContext, useEffect, useMemo, useState } from "react"; import { createContext, useContext, useEffect, useMemo, useState } from "react";
import useSWR from "swr";
type Theme = "dark" | "light" | "system"; type Theme = "dark" | "light" | "system";
type ColorScheme = type ColorScheme =
@ -21,9 +22,22 @@ export const colorSchemes: ColorScheme[] = [
// Helper function to generate friendly color scheme names // Helper function to generate friendly color scheme names
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export const friendlyColorSchemeName = (className: string): string => { export const friendlyColorSchemeName = (
const words = className.split("-").slice(1); // Exclude the first word (e.g., 'theme') className: string,
return "menu.theme." + words.join(""); t?: (key: string, options?: any) => string,
): string => {
const words = className.split("-").slice(1);
const key = "menu.theme." + words.join("");
if (!t) {
return key;
}
const fallback = words
.join(" ")
.replace(/\b\w/g, (char) => char.toUpperCase());
return t(key, { defaultValue: fallback });
}; };
type ThemeProviderProps = { type ThemeProviderProps = {
@ -51,6 +65,9 @@ const initialState: ThemeProviderState = {
const ThemeProviderContext = createContext<ThemeProviderState>(initialState); const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
const fetcher = (url: string) =>
fetch(url).then((res) => (res.ok ? res.json() : []));
export function ThemeProvider({ export function ThemeProvider({
children, children,
defaultTheme = "system", defaultTheme = "system",
@ -92,13 +109,64 @@ export function ThemeProvider({
: "light"; : "light";
}, [theme]); }, [theme]);
const { data: customFiles } = useSWR<string[]>(
"/api/config/themes",
fetcher,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
},
);
const allColorSchemes = useMemo(() => {
const customSchemes =
customFiles
?.filter((f) => /^[a-zA-Z0-9._-]+\.css$/.test(f))
.map((f) => {
const base = f.replace(/\.css$/, "");
return (
base.startsWith("theme-") ? base : `theme-${base}`
) as ColorScheme;
}) ?? [];
return [...colorSchemes, ...customSchemes];
}, [customFiles]);
const [themesReady, setThemesReady] = useState(false);
useEffect(() => {
if (!customFiles) {
setThemesReady(true);
return;
}
const links = customFiles
.filter((f) => /^[a-zA-Z0-9._-]+\.css$/.test(f))
.map((file) => {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = `/config/themes/${file}`;
document.head.appendChild(link);
return new Promise<void>((resolve) => {
link.onload = () => resolve();
link.onerror = () => resolve();
});
});
Promise.all(links).then(() => setThemesReady(true));
}, [customFiles]);
useEffect(() => { useEffect(() => {
//localStorage.removeItem(storageKey); //localStorage.removeItem(storageKey);
//console.log(localStorage.getItem(storageKey)); //console.log(localStorage.getItem(storageKey));
if (!themesReady) {
return;
}
const root = window.document.documentElement; const root = window.document.documentElement;
root.classList.remove("light", "dark", "system", ...colorSchemes); root.classList.remove("light", "dark", "system", ...allColorSchemes);
root.classList.add(theme, colorScheme); root.classList.add(theme, colorScheme);
if (systemTheme) { if (systemTheme) {
@ -107,7 +175,7 @@ export function ThemeProvider({
} }
root.classList.add(theme); root.classList.add(theme);
}, [theme, colorScheme, systemTheme]); }, [theme, colorScheme, systemTheme, themesReady, allColorSchemes]);
const value = { const value = {
theme, theme,