From ea122f5a4f75ac33cd66b92bf9d03de0b8b2774f Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 6 Sep 2025 03:02:58 -0700 Subject: [PATCH] add theme stylesheet loader --- src/renderer/themes/use-app-theme.ts | 59 +++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/renderer/themes/use-app-theme.ts b/src/renderer/themes/use-app-theme.ts index f9db76bb..9dfdbdcb 100644 --- a/src/renderer/themes/use-app-theme.ts +++ b/src/renderer/themes/use-app-theme.ts @@ -1,5 +1,5 @@ import { useMantineColorScheme } from '@mantine/core'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSettingsStore } from '/@/renderer/store/settings.store'; import { createMantineTheme } from '/@/renderer/themes/mantine-theme'; @@ -17,6 +17,7 @@ export const useAppTheme = (overrideTheme?: AppTheme) => { const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio); const { builtIn, custom, system, type } = useSettingsStore((state) => state.font); const textStyleRef = useRef(null); + const loadedStylesheetsRef = useRef>(new Set()); const getCurrentTheme = () => window.matchMedia('(prefers-color-scheme: dark)').matches; const [isDarkTheme, setIsDarkTheme] = useState(getCurrentTheme()); const { followSystemTheme, theme, themeDark, themeLight } = useSettingsStore( @@ -27,6 +28,56 @@ export const useAppTheme = (overrideTheme?: AppTheme) => { setIsDarkTheme(e.matches); }; + const loadStylesheet = (href: string): Promise => { + return new Promise((resolve, reject) => { + if (loadedStylesheetsRef.current.has(href)) { + resolve(); + return; + } + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + link.onload = () => { + loadedStylesheetsRef.current.add(href); + resolve(); + }; + link.onerror = () => { + console.warn(`Failed to load stylesheet: ${href}`); + reject(new Error(`Failed to load stylesheet: ${href}`)); + }; + + document.head.appendChild(link); + }); + }; + + const unloadStylesheet = (href: string) => { + const existingLink = document.querySelector(`link[href="${href}"]`); + if (existingLink) { + existingLink.remove(); + loadedStylesheetsRef.current.delete(href); + } + }; + + const loadThemeStylesheets = useCallback(async (stylesheets: string[] = []) => { + if (loadedStylesheetsRef.current.size > 0) { + loadedStylesheetsRef.current.forEach((href) => unloadStylesheet(href)); + loadedStylesheetsRef.current.clear(); + } + + if (stylesheets.length === 0) { + return; + } + + const loadPromises = stylesheets.map((href) => + loadStylesheet(href).catch((error) => { + console.warn(`Error loading stylesheet ${href}:`, error); + }), + ); + + await Promise.all(loadPromises); + }, []); + const getSelectedTheme = () => { if (overrideTheme) { return overrideTheme; @@ -113,6 +164,12 @@ export const useAppTheme = (overrideTheme?: AppTheme) => { root.style.setProperty('--theme-image-fit', nativeImageAspect ? 'contain' : 'cover'); }, [nativeImageAspect]); + useEffect(() => { + if (appTheme?.stylesheets) { + loadThemeStylesheets(appTheme.stylesheets); + } + }, [selectedTheme, appTheme?.stylesheets, loadThemeStylesheets]); + const themeVars = useMemo(() => { return Object.entries(appTheme?.app ?? {}) .map(([key, value]) => {