diff --git a/src/RootAppRouter.tsx b/src/RootAppRouter.tsx index 4ce26864d..9c2205d33 100644 --- a/src/RootAppRouter.tsx +++ b/src/RootAppRouter.tsx @@ -16,6 +16,7 @@ import Backdrop from 'components/Backdrop'; import BangRedirect from 'components/router/BangRedirect'; import { createRouterHistory } from 'components/router/routerHistory'; import appTheme from 'themes/themes'; +import { ThemeStorageManager } from 'themes/themeStorageManager'; const layoutMode = localStorage.getItem('layout'); const isExperimentalLayout = layoutMode === 'experimental'; @@ -54,8 +55,7 @@ function RootAppLayout() { diff --git a/src/apps/dashboard/components/AppTabs.tsx b/src/apps/dashboard/components/AppTabs.tsx index f56d4df4a..3feea3bbd 100644 --- a/src/apps/dashboard/components/AppTabs.tsx +++ b/src/apps/dashboard/components/AppTabs.tsx @@ -7,7 +7,7 @@ import isEqual from 'lodash-es/isEqual'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { Link } from 'react-router-dom'; -import { EventType } from 'types/eventType'; +import { EventType } from 'constants/eventType'; import Events, { type Event } from 'utils/events'; interface AppTabsParams { diff --git a/src/apps/dashboard/components/table/TablePage.tsx b/src/apps/dashboard/components/table/TablePage.tsx index 3b68ab851..a29061624 100644 --- a/src/apps/dashboard/components/table/TablePage.tsx +++ b/src/apps/dashboard/components/table/TablePage.tsx @@ -1,7 +1,8 @@ import Box from '@mui/material/Box/Box'; import Stack from '@mui/material/Stack/Stack'; +import type {} from '@mui/material/themeCssVarsAugmentation'; import Typography from '@mui/material/Typography/Typography'; -import { type MRT_RowData, type MRT_TableInstance, MaterialReactTable } from 'material-react-table'; +import { type MRT_RowData, type MRT_TableInstance, type MRT_TableOptions, MaterialReactTable } from 'material-react-table'; import React from 'react'; import Page, { type PageProps } from 'components/Page'; @@ -12,7 +13,7 @@ interface TablePageProps extends PageProps { table: MRT_TableInstance } -export const DEFAULT_TABLE_OPTIONS = { +export const DEFAULT_TABLE_OPTIONS: Partial> = { // Enable custom features enableColumnPinning: true, enableColumnResizing: true, diff --git a/src/apps/dashboard/routes/activity/index.tsx b/src/apps/dashboard/routes/activity/index.tsx index 09547af0b..fa11a45bd 100644 --- a/src/apps/dashboard/routes/activity/index.tsx +++ b/src/apps/dashboard/routes/activity/index.tsx @@ -2,9 +2,10 @@ import parseISO from 'date-fns/parseISO'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry'; import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level'; +import { useTheme } from '@mui/material/styles'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import { type MRT_ColumnDef, useMaterialReactTable } from 'material-react-table'; +import { type MRT_ColumnDef, type MRT_Theme, useMaterialReactTable } from 'material-react-table'; import { useSearchParams } from 'react-router-dom'; import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell'; @@ -53,6 +54,8 @@ export const Component = () => { const { usersById: users, names: userNames, isLoading: isUsersLoading } = useUsersDetails(); + const theme = useTheme(); + const UserCell = getUserCell(users); const activityParams = useMemo(() => ({ @@ -156,8 +159,15 @@ export const Component = () => { } }, [ activityView, searchParams, setSearchParams ]); + // NOTE: We need to provide a custom theme due to a MRT bug causing the initial theme to always be used + // https://github.com/KevinVandy/material-react-table/issues/1429 + const mrtTheme = useMemo>(() => ({ + baseBackgroundColor: theme.palette.background.paper + }), [ theme ]); + const table = useMaterialReactTable({ ...DEFAULT_TABLE_OPTIONS, + mrtTheme, columns, data: logEntries, diff --git a/src/apps/dashboard/routes/devices/index.tsx b/src/apps/dashboard/routes/devices/index.tsx index e7961ccdb..65bd54e18 100644 --- a/src/apps/dashboard/routes/devices/index.tsx +++ b/src/apps/dashboard/routes/devices/index.tsx @@ -4,9 +4,10 @@ import Edit from '@mui/icons-material/Edit'; import Box from '@mui/material/Box/Box'; import Button from '@mui/material/Button/Button'; import IconButton from '@mui/material/IconButton'; +import { useTheme } from '@mui/material/styles'; import Tooltip from '@mui/material/Tooltip/Tooltip'; import parseISO from 'date-fns/parseISO'; -import { type MRT_ColumnDef, useMaterialReactTable } from 'material-react-table'; +import { type MRT_ColumnDef, type MRT_Theme, useMaterialReactTable } from 'material-react-table'; import React, { useCallback, useMemo, useState } from 'react'; import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell'; @@ -41,6 +42,7 @@ export const Component = () => { data?.Items || [] ), [ data ]); const { usersById: users, names: userNames, isLoading: isUsersLoading } = useUsersDetails(); + const theme = useTheme(); const [ isDeleteConfirmOpen, setIsDeleteConfirmOpen ] = useState(false); const [ isDeleteAllConfirmOpen, setIsDeleteAllConfirmOpen ] = useState(false); @@ -137,8 +139,15 @@ export const Component = () => { } ], [ UserCell, userNames ]); + // NOTE: We need to provide a custom theme due to a MRT bug causing the initial theme to always be used + // https://github.com/KevinVandy/material-react-table/issues/1429 + const mrtTheme = useMemo>(() => ({ + baseBackgroundColor: theme.palette.background.paper + }), [ theme ]); + const mrTable = useMaterialReactTable({ ...DEFAULT_TABLE_OPTIONS, + mrtTheme, columns, data: devices, @@ -190,10 +199,18 @@ export const Component = () => { renderRowActions: ({ row, table }) => { const isDeletable = api && row.original.Id && api.deviceInfo.id === row.original.Id; return ( - + table.setEditingRow(row)} > diff --git a/src/apps/dashboard/routes/keys/index.tsx b/src/apps/dashboard/routes/keys/index.tsx index 1abb66884..1036c13eb 100644 --- a/src/apps/dashboard/routes/keys/index.tsx +++ b/src/apps/dashboard/routes/keys/index.tsx @@ -2,11 +2,12 @@ import type { AuthenticationInfo } from '@jellyfin/sdk/lib/generated-client/mode import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import IconButton from '@mui/material/IconButton'; +import { useTheme } from '@mui/material/styles'; import Tooltip from '@mui/material/Tooltip'; import AddIcon from '@mui/icons-material/Add'; import DeleteIcon from '@mui/icons-material/Delete'; import parseISO from 'date-fns/parseISO'; -import { type MRT_ColumnDef, useMaterialReactTable } from 'material-react-table'; +import { type MRT_ColumnDef, type MRT_Theme, useMaterialReactTable } from 'material-react-table'; import React, { useCallback, useMemo } from 'react'; import DateTimeCell from 'apps/dashboard/components/table/DateTimeCell'; @@ -27,6 +28,7 @@ export const Component = () => { ), [ data ]); const revokeKey = useRevokeKey(); const createKey = useCreateKey(); + const theme = useTheme(); const columns = useMemo[]>(() => [ { @@ -49,8 +51,15 @@ export const Component = () => { } ], []); + // NOTE: We need to provide a custom theme due to a MRT bug causing the initial theme to always be used + // https://github.com/KevinVandy/material-react-table/issues/1429 + const mrtTheme = useMemo>(() => ({ + baseBackgroundColor: theme.palette.background.paper + }), [ theme ]); + const table = useMaterialReactTable({ ...DEFAULT_TABLE_OPTIONS, + mrtTheme, columns, data: keys, diff --git a/src/apps/dashboard/routes/tasks/task.tsx b/src/apps/dashboard/routes/tasks/task.tsx index 2aa00f886..ecceff6d1 100644 --- a/src/apps/dashboard/routes/tasks/task.tsx +++ b/src/apps/dashboard/routes/tasks/task.tsx @@ -7,10 +7,11 @@ import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import AddIcon from '@mui/icons-material/Add'; import IconButton from '@mui/material/IconButton'; +import { useTheme } from '@mui/material/styles'; import Tooltip from '@mui/material/Tooltip'; import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'; import Loading from 'components/loading/LoadingComponent'; -import { MRT_ColumnDef, MRT_Table, useMaterialReactTable } from 'material-react-table'; +import { type MRT_ColumnDef, MRT_Table, type MRT_Theme, useMaterialReactTable } from 'material-react-table'; import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info'; import globalize from '../../../../lib/globalize'; import { useTask } from 'apps/dashboard/features/tasks/api/useTask'; @@ -26,6 +27,7 @@ export const Component = () => { const [ isAddTriggerDialogOpen, setIsAddTriggerDialogOpen ] = useState(false); const [ isRemoveConfirmOpen, setIsRemoveConfirmOpen ] = useState(false); const [ pendingDeleteTrigger, setPendingDeleteTrigger ] = useState(null); + const theme = useTheme(); const onCloseRemoveConfirmDialog = useCallback(() => { setPendingDeleteTrigger(null); @@ -80,7 +82,15 @@ export const Component = () => { } ], []); + // NOTE: We need to provide a custom theme due to a MRT bug causing the initial theme to always be used + // https://github.com/KevinVandy/material-react-table/issues/1429 + const mrtTheme = useMemo>(() => ({ + baseBackgroundColor: theme.palette.background.paper + }), [ theme ]); + const table = useMaterialReactTable({ + mrtTheme, + columns, data: task?.Triggers || [], diff --git a/src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx b/src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx index aa1ac14f8..8c921e89b 100644 --- a/src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx +++ b/src/apps/experimental/components/AppToolbar/RemotePlayButton.tsx @@ -4,7 +4,7 @@ import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Cast from '@mui/icons-material/Cast'; import IconButton from '@mui/material/IconButton'; -import { useTheme } from '@mui/material/styles'; +import type {} from '@mui/material/themeCssVarsAugmentation'; import Tooltip from '@mui/material/Tooltip'; import { playbackManager } from 'components/playback/playbackmanager'; @@ -15,7 +15,6 @@ import RemotePlayMenu, { ID } from './menus/RemotePlayMenu'; import RemotePlayActiveMenu, { ID as ACTIVE_ID } from './menus/RemotePlayActiveMenu'; const RemotePlayButton = () => { - const theme = useTheme(); const [ playerInfo, setPlayerInfo ] = useState(playbackManager.getPlayerInfo()); const updatePlayerInfo = useCallback(() => { @@ -70,9 +69,10 @@ const RemotePlayButton = () => { aria-haspopup='true' onClick={onRemotePlayActiveButtonClick} color='inherit' - sx={{ - color: theme.palette.primary.main - }} + // eslint-disable-next-line react/jsx-no-bind + sx={(theme) => ({ + color: theme.vars.palette.primary.main + })} > {playerInfo.deviceName || playerInfo.name} diff --git a/src/apps/experimental/routes/home.tsx b/src/apps/experimental/routes/home.tsx index 773eee5d4..6dfda1b49 100644 --- a/src/apps/experimental/routes/home.tsx +++ b/src/apps/experimental/routes/home.tsx @@ -5,7 +5,7 @@ import globalize from '../../../lib/globalize'; import { clearBackdrop } from '../../../components/backdrop/backdrop'; import layoutManager from '../../../components/layoutManager'; import Page from '../../../components/Page'; -import { EventType } from 'types/eventType'; +import { EventType } from 'constants/eventType'; import Events from 'utils/events'; import '../../../elements/emby-tabs/emby-tabs'; diff --git a/src/apps/experimental/routes/video/index.tsx b/src/apps/experimental/routes/video/index.tsx index edb4f0871..8b91abb9f 100644 --- a/src/apps/experimental/routes/video/index.tsx +++ b/src/apps/experimental/routes/video/index.tsx @@ -6,7 +6,7 @@ import RemotePlayButton from 'apps/experimental/components/AppToolbar/RemotePlay import SyncPlayButton from 'apps/experimental/components/AppToolbar/SyncPlayButton'; import AppToolbar from 'components/toolbar/AppToolbar'; import ViewManagerPage from 'components/viewManager/ViewManagerPage'; -import { EventType } from 'types/eventType'; +import { EventType } from 'constants/eventType'; import Events, { type Event } from 'utils/events'; /** diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx index be4e4c672..ba020628f 100644 --- a/src/components/UserAvatar.tsx +++ b/src/components/UserAvatar.tsx @@ -1,7 +1,7 @@ -import React, { FC } from 'react'; +import React, { type FC } from 'react'; import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; import Avatar from '@mui/material/Avatar'; -import { useTheme } from '@mui/material/styles'; +import type {} from '@mui/material/themeCssVarsAugmentation'; import { useApi } from 'hooks/useApi'; @@ -11,7 +11,6 @@ interface UserAvatarProps { const UserAvatar: FC = ({ user }) => { const { api } = useApi(); - const theme = useTheme(); return user ? ( = ({ user }) => { `${api.basePath}/Users/${user.Id}/Images/Primary?tag=${user.PrimaryImageTag}` : undefined } - sx={{ + // eslint-disable-next-line react/jsx-no-bind + sx={(theme) => ({ bgcolor: api && user.Id && user.PrimaryImageTag ? - theme.palette.background.paper : - theme.palette.primary.dark, + theme.vars.palette.background.paper : + theme.vars.palette.primary.dark, color: 'inherit' - }} + })} /> ) : null; }; diff --git a/src/components/mediainfo/StarIcons.tsx b/src/components/mediainfo/StarIcons.tsx index ca2a1859d..922ccddae 100644 --- a/src/components/mediainfo/StarIcons.tsx +++ b/src/components/mediainfo/StarIcons.tsx @@ -2,7 +2,7 @@ import React, { type FC } from 'react'; import classNames from 'classnames'; import StarIcon from '@mui/icons-material/Star'; import Box from '@mui/material/Box'; -import { useTheme } from '@mui/material/styles'; +import type {} from '@mui/material/themeCssVarsAugmentation'; interface StarIconsProps { className?: string; @@ -10,7 +10,6 @@ interface StarIconsProps { } const StarIcons: FC = ({ className, communityRating }) => { - const theme = useTheme(); const cssClass = classNames( 'mediaInfoItem', 'starRatingContainer', @@ -21,9 +20,10 @@ const StarIcons: FC = ({ className, communityRating }) => { ({ + color: theme.vars.palette.starIcon.main + })} /> {communityRating.toFixed(1)} diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts index 3cbfa0c11..eb49c470d 100644 --- a/src/components/playback/skipsegment.ts +++ b/src/components/playback/skipsegment.ts @@ -4,7 +4,7 @@ import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/ import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber'; import { isInSegment } from 'apps/stable/features/playback/utils/mediaSegments'; import Events, { type Event } from 'utils/events'; -import { EventType } from 'types/eventType'; +import { EventType } from 'constants/eventType'; import './skipbutton.scss'; import dom from 'utils/dom'; import globalize from 'lib/globalize'; diff --git a/src/types/eventType.ts b/src/constants/eventType.ts similarity index 63% rename from src/types/eventType.ts rename to src/constants/eventType.ts index 06487e982..c9edaed8a 100644 --- a/src/types/eventType.ts +++ b/src/constants/eventType.ts @@ -4,5 +4,6 @@ export enum EventType { HEADER_RENDERED = 'HEADER_RENDERED', SET_TABS = 'SET_TABS', - SHOW_VIDEO_OSD = 'SHOW_VIDEO_OSD' + SHOW_VIDEO_OSD = 'SHOW_VIDEO_OSD', + THEME_CHANGE = 'THEME_CHANGE' } diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index bd0dc8f6b..e3f3d3733 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -3,7 +3,7 @@ import escapeHtml from 'escape-html'; import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent'; import { AppFeature } from 'constants/appFeature'; import { TICKS_PER_MINUTE, TICKS_PER_SECOND } from 'constants/time'; -import { EventType } from 'types/eventType'; +import { EventType } from 'constants/eventType'; import { playbackManager } from '../../../components/playback/playbackmanager'; import browser from '../../../scripts/browser'; diff --git a/src/elements/emby-progressbar/AutoTimeProgressBar.tsx b/src/elements/emby-progressbar/AutoTimeProgressBar.tsx index b6e5910cb..efba05bd8 100644 --- a/src/elements/emby-progressbar/AutoTimeProgressBar.tsx +++ b/src/elements/emby-progressbar/AutoTimeProgressBar.tsx @@ -1,7 +1,8 @@ import React, { type FC, useCallback, useEffect, useRef, useState } from 'react'; import classNames from 'classnames'; import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'; -import { useTheme } from '@mui/material/styles'; +import type {} from '@mui/material/themeCssVarsAugmentation'; + import type { ProgressOptions } from 'types/progressOptions'; interface AutoTimeProgressBarProps { @@ -23,7 +24,6 @@ const AutoTimeProgressBar: FC = ({ }) => { const [progress, setProgress] = useState(pct); const timerRef = useRef | null>(null); - const theme = useTheme(); const onAutoTimeProgress = useCallback(() => { const start = parseInt(starTtime.toString(), 10); @@ -66,12 +66,15 @@ const AutoTimeProgressBar: FC = ({ className={progressBarClass} variant='determinate' value={progress} - sx={{ + // eslint-disable-next-line react/jsx-no-bind + sx={(theme) => ({ [`& .${linearProgressClasses.bar}`]: { borderRadius: 5, - backgroundColor: isRecording ? theme.palette.error.main : theme.palette.primary.main + backgroundColor: isRecording ? + theme.vars.palette.error.main : + theme.vars.palette.primary.main } - }} + })} /> ); }; diff --git a/src/scripts/libraryMenu.js b/src/scripts/libraryMenu.js index 0ffa8aad1..86fa3bab3 100644 --- a/src/scripts/libraryMenu.js +++ b/src/scripts/libraryMenu.js @@ -8,7 +8,7 @@ import { AppFeature } from 'constants/appFeature'; import { getUserViewsQuery } from 'hooks/useUserViews'; import globalize from 'lib/globalize'; import { ServerConnections } from 'lib/jellyfin-apiclient'; -import { EventType } from 'types/eventType'; +import { EventType } from 'constants/eventType'; import { toApi } from 'utils/jellyfin-apiclient/compat'; import { queryClient } from 'utils/query/queryClient'; diff --git a/src/scripts/themeManager.js b/src/scripts/themeManager.js index ecf153967..aca9b6f0d 100644 --- a/src/scripts/themeManager.js +++ b/src/scripts/themeManager.js @@ -1,3 +1,6 @@ +import Events from 'utils/events'; +import { EventType } from 'constants/eventType'; + import { getDefaultTheme, getThemes as getConfiguredThemes } from './settings/webSettings'; let currentThemeId; @@ -44,6 +47,8 @@ function setTheme(id) { // set the meta theme color document.getElementById('themeColor').content = info.color; + + Events.trigger(document, EventType.THEME_CHANGE, [ info.id ]); }); }); } diff --git a/src/themes/themeStorageManager.ts b/src/themes/themeStorageManager.ts new file mode 100644 index 000000000..2a968c169 --- /dev/null +++ b/src/themes/themeStorageManager.ts @@ -0,0 +1,21 @@ +import { StorageManager } from '@mui/material/styles'; + +import Events, { type Event } from 'utils/events'; +import { EventType } from 'constants/eventType'; + +/** + * A custom MUI StorageManager. + * + * Since we switch the theme based on the current page, we handle getting/setting the current theme via autoTheme + + * themeManager. We need to implement `subscribe` so MUI is aware of theme changes though otherwise the `useTheme` hook + * will always return the default theme. + */ +export const ThemeStorageManager: StorageManager = () => ({ + get: defaultValue => defaultValue, + set: () => { /* no-op */ }, + subscribe: handler => { + const wrappedHandler = (_e: Event, value: string) => handler(value); + Events.on(document, EventType.THEME_CHANGE, wrappedHandler); + return () => Events.off(document, EventType.THEME_CHANGE, wrappedHandler); + } +});