1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Move theme and custom css to react components

This commit is contained in:
Bill Thornton 2025-03-24 01:07:51 -04:00
parent 27f4b8a7e5
commit 88b247596a
8 changed files with 159 additions and 143 deletions

View file

@ -11,6 +11,7 @@ import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import ThemeCss from 'components/ThemeCss';
import { useApi } from 'hooks/useApi';
import { useLocale } from 'hooks/useLocale';
@ -101,6 +102,7 @@ export const Component: FC = () => {
</AppBody>
</Box>
</Box>
<ThemeCss dashboard />
</LocalizationProvider>
);
};

View file

@ -6,8 +6,10 @@ import useMediaQuery from '@mui/material/useMediaQuery';
import { Outlet, useLocation } from 'react-router-dom';
import AppBody from 'components/AppBody';
import CustomCss from 'components/CustomCss';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import ThemeCss from 'components/ThemeCss';
import { useApi } from 'hooks/useApi';
import AppToolbar from './components/AppToolbar';
@ -29,52 +31,56 @@ export const Component = () => {
}, [ isDrawerActive, setIsDrawerActive ]);
return (
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
<StrictMode>
<ElevationScroll elevate={false}>
<AppBar
position='fixed'
sx={{
width: {
xs: '100%',
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
},
ml: {
xs: 0,
md: isDrawerAvailable ? DRAWER_WIDTH : 0
}
}}
>
<AppToolbar
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>
<>
<Box sx={{ position: 'relative', display: 'flex', height: '100%' }}>
<StrictMode>
<ElevationScroll elevate={false}>
<AppBar
position='fixed'
sx={{
width: {
xs: '100%',
md: isDrawerAvailable ? `calc(100% - ${DRAWER_WIDTH}px)` : '100%'
},
ml: {
xs: 0,
md: isDrawerAvailable ? DRAWER_WIDTH : 0
}
}}
>
<AppToolbar
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>
{
isDrawerAvailable && (
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
)
}
</StrictMode>
{
isDrawerAvailable && (
<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>
)
}
</StrictMode>
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1
}}
>
<AppBody>
<Outlet />
</AppBody>
<Box
component='main'
sx={{
width: '100%',
flexGrow: 1
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</Box>
</Box>
<ThemeCss />
<CustomCss />
</>
);
};

View file

@ -2,11 +2,17 @@ import React from 'react';
import { Outlet } from 'react-router-dom';
import AppBody from 'components/AppBody';
import ThemeCss from 'components/ThemeCss';
import CustomCss from 'components/CustomCss';
export default function AppLayout() {
return (
<AppBody>
<Outlet />
</AppBody>
<>
<AppBody>
<Outlet />
</AppBody>
<ThemeCss />
<CustomCss />
</>
);
}

View file

@ -0,0 +1,37 @@
import React, { FC, useEffect, useState } from 'react';
import { useApi } from 'hooks/useApi';
import { useUserSettings } from 'hooks/useUserSettings';
const CustomCss: FC = () => {
const { api } = useApi();
const { customCss: userCustomCss, disableCustomCss } = useUserSettings();
const [ brandingCssUrl, setBrandingCssUrl ] = useState<string>();
useEffect(() => {
if (!api) return;
setBrandingCssUrl(api.getUri('/Branding/Css.css'));
}, [ api ]);
if (!api) return null;
return (
<>
{!disableCustomCss && brandingCssUrl && (
<link
rel='stylesheet'
type='text/css'
href={brandingCssUrl}
/>
)}
{userCustomCss && (
<style>
{userCustomCss}
</style>
)}
</>
);
};
export default CustomCss;

View file

@ -0,0 +1,34 @@
import React, { FC, useEffect, useState } from 'react';
import { useUserTheme } from 'hooks/useUserTheme';
import { getDefaultTheme } from 'scripts/settings/webSettings';
interface ThemeCssProps {
dashboard?: boolean
}
const getThemeUrl = (id: string) => `themes/${id}/theme.css`;;
const DEFAULT_THEME_URL = getThemeUrl(getDefaultTheme().id);
const ThemeCss: FC<ThemeCssProps> = ({
dashboard = false
}) => {
const { theme, dashboardTheme } = useUserTheme();
const [ themeUrl, setThemeUrl ] = useState(DEFAULT_THEME_URL);
useEffect(() => {
const id = dashboard ? dashboardTheme : theme;
if (id) setThemeUrl(getThemeUrl(id));
}, [dashboard, dashboardTheme, theme]);
return (
<link
rel='stylesheet'
type='text/css'
href={themeUrl}
/>
);
};
export default ThemeCss;

View file

@ -7,6 +7,8 @@ import Events, { type Event } from 'utils/events';
import { useApi } from './useApi';
interface UserSettings {
customCss?: string
disableCustomCss: boolean
theme?: string
dashboardTheme?: string
dateTimeLocale?: string
@ -15,6 +17,9 @@ interface UserSettings {
// NOTE: This is an incomplete list of only the settings that are currently being used
const UserSettingField = {
// Custom CSS
CustomCss: 'customCss',
DisableCustomCss: 'disableCustomCss',
// Theme settings
Theme: 'appTheme',
DashboardTheme: 'dashboardTheme',
@ -23,11 +28,15 @@ const UserSettingField = {
Language: 'language'
};
const UserSettingsContext = createContext<UserSettings>({});
const UserSettingsContext = createContext<UserSettings>({
disableCustomCss: false
});
export const useUserSettings = () => useContext(UserSettingsContext);
export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children }) => {
const [ customCss, setCustomCss ] = useState<string>();
const [ disableCustomCss, setDisableCustomCss ] = useState(false);
const [ theme, setTheme ] = useState<string>();
const [ dashboardTheme, setDashboardTheme ] = useState<string>();
const [ dateTimeLocale, setDateTimeLocale ] = useState<string>();
@ -36,14 +45,25 @@ export const UserSettingsProvider: FC<PropsWithChildren<unknown>> = ({ children
const { user } = useApi();
const context = useMemo<UserSettings>(() => ({
customCss,
disableCustomCss,
theme,
dashboardTheme,
dateTimeLocale,
locale: language
}), [ theme, dashboardTheme, dateTimeLocale, language ]);
}), [
customCss,
disableCustomCss,
theme,
dashboardTheme,
dateTimeLocale,
language
]);
// Update the values of the user settings
const updateUserSettings = useCallback(() => {
setCustomCss(userSettings.customCss());
setDisableCustomCss(userSettings.disableCustomCss());
setTheme(userSettings.theme());
setDashboardTheme(userSettings.dashboardTheme());
setDateTimeLocale(userSettings.dateTimeLocale());

View file

@ -17,7 +17,6 @@ import { loadCoreDictionary } from 'lib/globalize/loader';
import { initialize as initializeAutoCast } from 'scripts/autocast';
import browser from './scripts/browser';
import keyboardNavigation from './scripts/keyboardNavigation';
import { currentSettings } from './scripts/settings/userSettings';
import { getPlugins } from './scripts/settings/webSettings';
import taskButton from './scripts/taskbutton';
import { pageClassOn, serverAddress } from './utils/dashboard';
@ -116,9 +115,6 @@ build: ${__JF_BUILD_VERSION__}`);
// Load platform specific features
loadPlatformFeatures();
// Load custom CSS styles
loadCustomCss();
// Enable navigation controls
keyboardNavigation.enable();
autoFocuser.enable();
@ -191,54 +187,7 @@ function loadPlatformFeatures() {
}
}
function loadCustomCss() {
// Apply custom CSS
const apiClient = ServerConnections.currentApiClient();
if (apiClient) {
const brandingCss = fetch(apiClient.getUrl('Branding/Css'))
.then(function(response) {
if (!response.ok) {
throw new Error(response.status + ' ' + response.statusText);
}
return response.text();
})
.catch(function(err) {
console.warn('Error applying custom css', err);
});
const handleStyleChange = async () => {
let style = document.querySelector('#cssBranding');
if (!style) {
// Inject the branding css as a dom element in body so it will take
// precedence over other stylesheets
style = document.createElement('style');
style.id = 'cssBranding';
document.body.appendChild(style);
}
const css = [];
// Only add branding CSS when enabled
if (!currentSettings.disableCustomCss()) css.push(await brandingCss);
// Always add user CSS
css.push(currentSettings.customCss());
style.textContent = css.join('\n');
};
Events.on(ServerConnections, 'localusersignedin', handleStyleChange);
Events.on(ServerConnections, 'localusersignedout', handleStyleChange);
Events.on(currentSettings, 'change', (e, prop) => {
if (prop == 'disableCustomCss' || prop == 'customCss') {
handleStyleChange();
}
});
handleStyleChange();
}
}
function registerServiceWorker() {
/* eslint-disable compat/compat */
if (navigator.serviceWorker && window.appMode !== 'cordova' && window.appMode !== 'android') {
navigator.serviceWorker.register('serviceworker.js').then(() =>
console.log('serviceWorker registered')
@ -248,7 +197,6 @@ function registerServiceWorker() {
} else {
console.warn('serviceWorker unsupported');
}
/* eslint-enable compat/compat */
}
async function renderApp() {

View file

@ -1,16 +1,7 @@
import { getDefaultTheme, getThemes as getConfiguredThemes } from './settings/webSettings';
let themeStyleElement = document.querySelector('#cssTheme');
let currentThemeId;
function unloadTheme() {
const elem = themeStyleElement;
if (elem) {
elem.removeAttribute('href');
currentThemeId = null;
}
}
function getThemes() {
return getConfiguredThemes();
}
@ -29,11 +20,7 @@ function getThemeStylesheetInfo(id) {
theme = getDefaultTheme();
}
return {
stylesheetPath: 'themes/' + theme.id + '/theme.css',
themeId: theme.id,
color: theme.color
};
return theme;
});
}
@ -45,36 +32,12 @@ function setTheme(id) {
}
getThemeStylesheetInfo(id).then(function (info) {
if (currentThemeId && currentThemeId === info.themeId) {
if (currentThemeId && currentThemeId === info.id) {
resolve();
return;
}
const linkUrl = info.stylesheetPath;
unloadTheme();
let link = themeStyleElement;
if (!link) {
// Inject the theme css as a dom element in body so it will take
// precedence over other stylesheets
link = document.createElement('link');
link.id = 'cssTheme';
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
document.body.appendChild(link);
}
const onLoad = function (e) {
e.target.removeEventListener('load', onLoad);
resolve();
};
link.addEventListener('load', onLoad);
link.setAttribute('href', linkUrl);
themeStyleElement = link;
currentThemeId = info.themeId;
currentThemeId = info.id;
document.getElementById('themeColor').content = info.color;
});
@ -82,6 +45,6 @@ function setTheme(id) {
}
export default {
getThemes: getThemes,
setTheme: setTheme
getThemes,
setTheme
};