diff --git a/src/renderer/themes/use-app-theme.ts b/src/renderer/themes/use-app-theme.ts index 4d3ff4793..2cb073c29 100644 --- a/src/renderer/themes/use-app-theme.ts +++ b/src/renderer/themes/use-app-theme.ts @@ -22,6 +22,7 @@ export const THEME_DATA = [ { label: 'Solarized Light', type: 'light', value: AppTheme.SOLARIZED_LIGHT }, { label: 'GitHub Dark', type: 'dark', value: AppTheme.GITHUB_DARK }, { label: 'GitHub Light', type: 'light', value: AppTheme.GITHUB_LIGHT }, + { label: 'Glassy Dark', type: 'dark', value: AppTheme.GLASSY_DARK }, { label: 'Monokai', type: 'dark', value: AppTheme.MONOKAI }, { label: 'High Contrast Dark', type: 'dark', value: AppTheme.HIGH_CONTRAST_DARK }, { label: 'High Contrast Light', type: 'light', value: AppTheme.HIGH_CONTRAST_LIGHT }, @@ -48,7 +49,7 @@ export const useAppTheme = (overrideTheme?: AppTheme) => { const nativeImageAspect = useNativeAspectRatio(); const { builtIn, custom, system, type } = useFontSettings(); const textStyleRef = useRef(null); - const loadedStylesheetsRef = useRef>(new Set()); + const themeInlineStylesRef = useRef(null); const getCurrentTheme = () => window.matchMedia('(prefers-color-scheme: dark)').matches; const [isDarkTheme, setIsDarkTheme] = useState(getCurrentTheme()); const { followSystemTheme, theme, themeDark, themeLight, useThemeAccentColor } = @@ -58,54 +59,17 @@ 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 applyInlineStylesheets = useCallback((inlineCssStrings: string[] = []) => { + const cssText = inlineCssStrings.filter(Boolean).join('\n'); - 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 (!themeInlineStylesRef.current) { + const styleEl = document.createElement('style'); + styleEl.id = 'theme-inline-styles'; + document.head.appendChild(styleEl); + themeInlineStylesRef.current = styleEl; } - 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); + themeInlineStylesRef.current.textContent = cssText; }, []); const getSelectedTheme = () => { @@ -204,10 +168,8 @@ export const useAppTheme = (overrideTheme?: AppTheme) => { }, [nativeImageAspect]); useEffect(() => { - if (appTheme?.stylesheets) { - loadThemeStylesheets(appTheme.stylesheets); - } - }, [selectedTheme, appTheme?.stylesheets, loadThemeStylesheets]); + applyInlineStylesheets(appTheme?.stylesheets ?? []); + }, [selectedTheme, appTheme?.stylesheets, applyInlineStylesheets]); const themeVars = useMemo(() => { return Object.entries(appTheme?.app ?? {}) diff --git a/src/shared/themes/app-theme-types.ts b/src/shared/themes/app-theme-types.ts index 94e0b67db..912f4cb6d 100644 --- a/src/shared/themes/app-theme-types.ts +++ b/src/shared/themes/app-theme-types.ts @@ -12,6 +12,7 @@ export enum AppTheme { DRACULA = 'dracula', GITHUB_DARK = 'githubDark', GITHUB_LIGHT = 'githubLight', + GLASSY_DARK = 'glassyDark', GRUVBOX_DARK = 'gruvboxDark', GRUVBOX_LIGHT = 'gruvboxLight', HIGH_CONTRAST_DARK = 'highContrastDark', diff --git a/src/shared/themes/app-theme.ts b/src/shared/themes/app-theme.ts index 929da4ebd..1716077b0 100644 --- a/src/shared/themes/app-theme.ts +++ b/src/shared/themes/app-theme.ts @@ -13,6 +13,7 @@ import { defaultLight } from '/@/shared/themes/default-light/default-light'; import { dracula } from '/@/shared/themes/dracula/dracula'; import { githubDark } from '/@/shared/themes/github-dark/github-dark'; import { githubLight } from '/@/shared/themes/github-light/github-light'; +import { glassyDark } from '/@/shared/themes/glassy-dark/glassy-dark'; import { gruvboxDark } from '/@/shared/themes/gruvbox-dark/gruvbox-dark'; import { gruvboxLight } from '/@/shared/themes/gruvbox-light/gruvbox-light'; import { highContrastDark } from '/@/shared/themes/high-contrast-dark/high-contrast-dark'; @@ -43,6 +44,7 @@ export const appTheme: Record = { [AppTheme.DRACULA]: dracula, [AppTheme.GITHUB_DARK]: githubDark, [AppTheme.GITHUB_LIGHT]: githubLight, + [AppTheme.GLASSY_DARK]: glassyDark, [AppTheme.GRUVBOX_DARK]: gruvboxDark, [AppTheme.GRUVBOX_LIGHT]: gruvboxLight, [AppTheme.HIGH_CONTRAST_DARK]: highContrastDark, diff --git a/src/shared/themes/glassy-dark/glassy-dark.ts b/src/shared/themes/glassy-dark/glassy-dark.ts new file mode 100644 index 000000000..82d062ab7 --- /dev/null +++ b/src/shared/themes/glassy-dark/glassy-dark.ts @@ -0,0 +1,30 @@ +import glassyOverridesCss from './glassy_overrides.css?inline'; + +import { AppThemeConfiguration } from '/@/shared/themes/app-theme-types'; + +export const glassyDark: AppThemeConfiguration = { + app: { + 'overlay-header': + 'linear-gradient(transparent 0%, rgb(13 17 23 / 85%) 100%), var(--theme-background-noise)', + 'overlay-subheader': + 'linear-gradient(180deg, rgb(13 17 23 / 5%) 0%, var(--theme-colors-background) 100%), var(--theme-background-noise)', + 'scrollbar-handle-background': 'rgba(88, 166, 255, 20%)', + 'scrollbar-handle-hover-background': 'rgba(88, 166, 255, 40%)', + }, + colors: { + background: 'rgb(2, 2, 6)', + 'background-alternate': 'rgb(0, 0, 0)', + black: 'rgb(0, 0, 0)', + foreground: 'rgb(225, 225, 225)', + 'foreground-muted': 'rgb(150, 150, 150)', + 'state-error': 'rgb(204, 50, 50)', + 'state-info': 'rgb(53, 116, 252)', + 'state-success': 'rgb(50, 204, 50)', + 'state-warning': 'rgb(255, 120, 120)', + surface: 'rgb(4, 4, 9)', + 'surface-foreground': 'rgb(215, 215, 215)', + white: 'rgb(255, 255, 255)', + }, + mode: 'dark', + stylesheets: [glassyOverridesCss], +}; diff --git a/src/shared/themes/glassy-dark/glassy_overrides.css b/src/shared/themes/glassy-dark/glassy_overrides.css new file mode 100644 index 000000000..84a6680d4 --- /dev/null +++ b/src/shared/themes/glassy-dark/glassy_overrides.css @@ -0,0 +1,270 @@ +.fs-player-bar-module-container { + background: rgb(0 0 0 / 40%) !important; + backdrop-filter: blur(2rem); +} + +.fs-poster-card-module-image { + border-radius: 18px !important; +} + +.fs-grid-card-controls-module-grid-card-controls-container { + border-radius: 18px; + backdrop-filter: blur(5px); +} + +.fsplayer-text { + font-size: 45px; + text-align: left; +} + +.fs-full-screen-player-image-module-metadata-container { + width: 100%; +} + +.fs-full-screen-player-queue-module-grid-container::before { + border-radius: 18px !important; +} + +/* stylelint-disable selector-class-pattern */ +.mantine-Modal-overlay { + backdrop-filter: blur(3px); +} +/* stylelint-enable selector-class-pattern */ + +.fs-modal-module-content, +.fs-select-module-dropdown, +.fs-popover-module-dropdown, +.fs-dialog-module-root, +.fs-context-menu-module-content, +.fs-dropdown-menu-module-menu-dropdown, +.fs-accordion-module-panel { + background: rgb(4 4 9 / 50%) !important; + backdrop-filter: blur(2rem); + + button, + input, + .fs-multi-select-module-input, + a { + border-radius: 12px; + } +} + +.fs-context-menu-module-content, +.fs-dropdown-menu-module-menu-dropdown { + border-radius: 18px; +} + +.fs-accordion-module-panel { + border-radius: 18px; + backdrop-filter: none !important; +} + +.fs-accordion-module-control { + background-color: transparent; +} + +.fs-modal-module-header { + background: rgb(4 4 9 / 80%) !important; + border-radius: 18px; +} + +.fs-select-module-dropdown, +.fs-popover-module-dropdown { + border-radius: 18px; +} + +/* stylelint-disable selector-class-pattern */ +.mantine-Center-root img { + border-radius: 18px; +} +/* stylelint-enable selector-class-pattern */ + +.ag-header { + background-color: transparent !important; + border-radius: 8px 8px 0 0; +} + +.fs-left-controls-module-playerbar-image { + border-radius: 8px !important; +} + +/* stylelint-disable selector-class-pattern */ +.favorite_icon .mantine-ActionIcon-icon { + justify-content: left; +} +/* stylelint-enable selector-class-pattern */ + +.fork-header svg { + padding-left: 2px; + margin-left: 5px; +} + +.fs-button-module-root[data-variant='outline'] { + border-radius: 1rem; +} + +.fs-poster-card-module-image-container { + border-radius: 8px !important; +} + +/* stylelint-disable selector-class-pattern */ +.mantine-Table-th { + background-color: transparent !important; +} +/* stylelint-enable selector-class-pattern */ + +table { + border: 0 !important; +} + +.fs-sidebar-module-accordion-content a { + border-radius: 8px; +} + +.fs-main-content-module-main-content-container { + height: 100vh; +} + +/* stylelint-disable selector-class-pattern */ +.mantine-Tabs-root { + input { + border-radius: 18px; + } + + button:not(.mantine-focus-never) { + border-radius: 8px; + } +} +/* stylelint-enable selector-class-pattern */ + +/* stylelint-disable selector-class-pattern */ +.mantine-Slider-track::before { + background-color: var(--theme-colors-surface); +} +/* stylelint-enable selector-class-pattern */ + +/* stylelint-disable selector-not-notation */ +.fs-image-module-image:not(.ag-cell *) +:not(.fs-left-controls-module-image *) +:not(.fs-sidebar-playlist-list-module-row-group *) { + border-radius: 18px !important; +} +/* stylelint-enable selector-not-notation */ + +.fs-left-controls-module-image { + border-radius: 12px; +} + +.fork-server-selector { + /* stylelint-disable selector-class-pattern */ + .mantine-SegmentedControl-indicator, + /* stylelint-enable selector-class-pattern */ + .fs-segmented-control-module-root, + input, + button { + border-radius: 12px; + } +} + +.fs-text-input-module-input, +[cmdk-item][data-selected] { + background: rgb(4 4 9 / 50%) !important; + border: 0; + border-radius: 18px; +} + +.fs-modal-module-content [cmdk-separator] { + display: none; +} + +/* Button fixes */ +.fs-button-module-root[data-variant='filled'] { + border-radius: 12px; +} + +/* stylelint-disable selector-class-pattern */ +.mantine-Accordion-label { + button, + a { + border-radius: 8px; + } +} +/* stylelint-enable selector-class-pattern */ + +/* stylelint-disable selector-class-pattern */ +.mantine-Grid-col button { + border-radius: 8px; +} +/* stylelint-enable selector-class-pattern */ + +/* share dialog */ +.fs-modal-module-body { + .fs-textarea-module-input { + border-radius: 12px; + } + + .fs-accordion-module-panel { + background-color: transparent; + } +} + +.fs-feature-carousel-module-image-column { + align-items: normal !important; +} + +/* stylelint-disable selector-class-pattern */ +.mantine-Badge-root { + background: rgb(1 1 5 / 45%); +} +/* stylelint-enable selector-class-pattern */ + +.fs-sidebar-module-image-container img { + border-radius: 18px; +} + +.fs-expanded-list-item-module-container { + position: relative; + bottom: 90px; + border-radius: 18px; +} + +.fs-sidebar-play-queue-module-lyrics-section { + bottom: 90px; +} + +.fs-page-header-module-container { + background-color: transparent; +} + +.fs-tabs-module-tab { + border-radius: 0 !important; +} + +/* stylelint-disable selector-class-pattern */ +.fs-full-screen-player-module-container .mantine-Group-root button { + border-radius: 100%; +} +/* stylelint-enable selector-class-pattern */ + +.fs-full-screen-player-image-module-image { + border-radius: 18px; +} + +.fs-segmented-control-module-root { + border-radius: 18px; +} + +.fs-segmented-control-module-label[data-active='true'], +/* stylelint-disable selector-class-pattern */ +.mantine-SegmentedControl-control { + border-radius: 8px; +} +/* stylelint-enable selector-class-pattern */ + +.fs-table-config-module-group { + border-radius: 8px; +} + +.fs-server-selector-module-button-group { + border-radius: 18px; +} diff --git a/src/shared/types/css-modules.d.ts b/src/shared/types/css-modules.d.ts index 8b008475b..cd7e4a4c0 100644 --- a/src/shared/types/css-modules.d.ts +++ b/src/shared/types/css-modules.d.ts @@ -2,3 +2,13 @@ declare module '*.module.css' { const classes: { [key: string]: string }; export default classes; } + +declare module '*.css?raw' { + const content: string; + export default content; +} + +declare module '*.css?inline' { + const content: string; + export default content; +} diff --git a/web.vite.config.ts b/web.vite.config.ts index 553435c3f..736490a32 100644 --- a/web.vite.config.ts +++ b/web.vite.config.ts @@ -32,6 +32,7 @@ export default defineConfig({ }, css: { modules: { + generateScopedName: 'fs-[name]-[local]', localsConvention: 'camelCase', }, },