Implement Glassy Dark theme (#1388)

* implement theme

* refactor theme stylesheets to load inline to simplify vite bundling

* add missing css module scope name for web build

---------

Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
Pyx
2026-02-06 05:01:32 -05:00
committed by GitHub
parent f52c4f7900
commit 82b50a60bc
7 changed files with 326 additions and 50 deletions
+12 -50
View File
@@ -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<HTMLStyleElement | null>(null);
const loadedStylesheetsRef = useRef<Set<string>>(new Set());
const themeInlineStylesRef = useRef<HTMLStyleElement | null>(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<void> => {
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 ?? {})
+1
View File
@@ -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',
+2
View File
@@ -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, AppThemeConfiguration> = {
[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,
@@ -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],
};
@@ -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;
}
+10
View File
@@ -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;
}