Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
actions-user committed Nov 22, 2024
2 parents ea82db8 + a88110f commit b2e3772
Show file tree
Hide file tree
Showing 51 changed files with 526 additions and 181 deletions.
38 changes: 38 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,44 @@ module.exports = {
__WEBPACK_SERVE__: 'readonly'
},
rules: {
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'default',
format: [ 'camelCase', 'PascalCase' ],
leadingUnderscore: 'allow'
},
{
selector: 'variable',
format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble'
},
{
selector: 'typeLike',
format: [ 'PascalCase' ]
},
{
selector: 'enumMember',
format: [ 'PascalCase', 'UPPER_CASE' ]
},
{
selector: [ 'objectLiteralProperty', 'typeProperty' ],
format: [ 'camelCase', 'PascalCase' ],
leadingUnderscore: 'allowSingleOrDouble',
trailingUnderscore: 'allowSingleOrDouble'
},
// Ignore numbers, locale strings (en-us), aria/data attributes, CSS selectors,
// and api_key parameter
{
selector: [ 'objectLiteralProperty', 'typeProperty' ],
format: null,
filter: {
regex: '[ &\\-]|^([0-9]+)$|^api_key$',
match: true
}
}
],
'@typescript-eslint/prefer-string-starts-ends-with': ['error']
}
},
Expand Down
4 changes: 4 additions & 0 deletions src/apps/dashboard/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useLocale } from 'hooks/useLocale';

import AppTabs from './components/AppTabs';
import AppDrawer from './components/drawer/AppDrawer';
import HelpButton from './components/toolbar/HelpButton';
import { DASHBOARD_APP_PATHS } from './routes/routes';

import './AppOverrides.scss';
Expand Down Expand Up @@ -68,6 +69,9 @@ export const Component: FC = () => {
isDrawerAvailable={!isMediumScreen && isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
buttons={
<HelpButton />
}
>
<AppTabs isDrawerOpen={isDrawerOpen} />
</AppToolbar>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dashboard, ExpandLess, ExpandMore, LibraryAdd, People, PlayCircle, Settings } from '@mui/icons-material';
import Palette from '@mui/icons-material/Palette';
import Collapse from '@mui/material/Collapse';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
Expand Down Expand Up @@ -65,6 +66,12 @@ const ServerDrawerSection = () => {
<ListItemText primary={globalize.translate('General')} />
</ListItemLink>
</ListItem>
<ListItemLink to='/dashboard/branding'>
<ListItemIcon>
<Palette />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderBranding')} />
</ListItemLink>
<ListItem disablePadding>
<ListItemLink to='/dashboard/users'>
<ListItemIcon>
Expand Down
36 changes: 36 additions & 0 deletions src/apps/dashboard/components/toolbar/HelpButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import HelpOutline from '@mui/icons-material/HelpOutline';
import IconButton from '@mui/material/IconButton/IconButton';
import Tooltip from '@mui/material/Tooltip/Tooltip';
import React from 'react';
import { Route, Routes } from 'react-router-dom';

import { HelpLinks } from 'apps/dashboard/constants/helpLinks';
import globalize from 'lib/globalize';

const HelpButton = () => (
<Routes>
{
HelpLinks.map(({ paths, url }) => paths.map(path => (
<Route
key={[url, path].join('-')}
path={path}
element={
<Tooltip title={globalize.translate('Help')}>
<IconButton
href={url}
rel='noopener noreferrer'
target='_blank'
size='large'
color='inherit'
>
<HelpOutline />
</IconButton>
</Tooltip>
}
/>
))).flat()
}
</Routes>
);

export default HelpButton;
51 changes: 51 additions & 0 deletions src/apps/dashboard/constants/helpLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export const HelpLinks = [
{
paths: ['/dashboard/devices'],
url: 'https://jellyfin.org/docs/general/server/devices'
}, {
paths: ['/dashboard/libraries'],
url: 'https://jellyfin.org/docs/general/server/libraries'
}, {
paths: [
'/dashboard/livetv',
'/dashboard/livetv/tuner',
'/dashboard/recordings'
],
url: 'https://jellyfin.org/docs/general/server/live-tv/'
}, {
paths: ['/dashboard/livetv/guide'],
url: 'https://jellyfin.org/docs/general/server/live-tv/setup-guide#adding-guide-data'
}, {
paths: ['/dashboard/networking'],
url: 'https://jellyfin.org/docs/general/networking/'
}, {
paths: ['/dashboard/playback/transcoding'],
url: 'https://jellyfin.org/docs/general/server/transcoding'
}, {
paths: [
'/dashboard/plugins',
'/dashboard/plugins/catalog'
],
url: 'https://jellyfin.org/docs/general/server/plugins/'
}, {
paths: ['/dashboard/plugins/repositories'],
url: 'https://jellyfin.org/docs/general/server/plugins/#repositories'
}, {
paths: ['/dashboard/settings'],
url: 'https://jellyfin.org/docs/general/server/settings'
}, {
paths: ['/dashboard/tasks'],
url: 'https://jellyfin.org/docs/general/server/tasks'
}, {
paths: ['/dashboard/users'],
url: 'https://jellyfin.org/docs/general/server/users/adding-managing-users'
}, {
paths: [
'/dashboard/users/access',
'/dashboard/users/parentalcontrol',
'/dashboard/users/password',
'/dashboard/users/profile'
],
url: 'https://jellyfin.org/docs/general/server/users/'
}
];
35 changes: 35 additions & 0 deletions src/apps/dashboard/features/branding/api/useBrandingOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Api } from '@jellyfin/sdk';
import { getBrandingApi } from '@jellyfin/sdk/lib/utils/api/branding-api';
import { queryOptions, useQuery } from '@tanstack/react-query';
import type { AxiosRequestConfig } from 'axios';

import { useApi } from 'hooks/useApi';

export const QUERY_KEY = 'BrandingOptions';

const fetchBrandingOptions = async (
api?: Api,
options?: AxiosRequestConfig
) => {
if (!api) {
console.error('[fetchBrandingOptions] no Api instance provided');
throw new Error('No Api instance provided to fetchBrandingOptions');
}

return getBrandingApi(api)
.getBrandingOptions(options)
.then(({ data }) => data);
};

export const getBrandingOptionsQuery = (
api?: Api
) => queryOptions({
queryKey: [ QUERY_KEY ],
queryFn: ({ signal }) => fetchBrandingOptions(api, { signal }),
enabled: !!api
});

export const useBrandingOptions = () => {
const { api } = useApi();
return useQuery(getBrandingOptionsQuery(api));
};
1 change: 1 addition & 0 deletions src/apps/dashboard/routes/_asyncRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AsyncRouteType, type AsyncRoute } from 'components/router/AsyncRoute';

export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'activity', type: AsyncRouteType.Dashboard },
{ path: 'branding', type: AsyncRouteType.Dashboard },
{ path: 'playback/trickplay', type: AsyncRouteType.Dashboard },
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AsyncRouteType.Dashboard },
{ path: 'users', type: AsyncRouteType.Dashboard },
Expand Down
176 changes: 176 additions & 0 deletions src/apps/dashboard/routes/branding/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type { BrandingOptions } from '@jellyfin/sdk/lib/generated-client/models/branding-options';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControlLabel from '@mui/material/FormControlLabel';
import Stack from '@mui/material/Stack';
import Switch from '@mui/material/Switch';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import React, { useCallback, useEffect, useState } from 'react';
import { type ActionFunctionArgs, Form, useActionData } from 'react-router-dom';

import { getBrandingOptionsQuery, QUERY_KEY, useBrandingOptions } from 'apps/dashboard/features/branding/api/useBrandingOptions';
import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page';
import ServerConnections from 'components/ServerConnections';
import globalize from 'lib/globalize';
import { queryClient } from 'utils/query/queryClient';

interface ActionData {
isSaved: boolean
}

const BRANDING_CONFIG_KEY = 'branding';
const BrandingOption = {
CustomCss: 'CustomCss',
LoginDisclaimer: 'LoginDisclaimer',
SplashscreenEnabled: 'SplashscreenEnabled'
};

export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getCurrentApi();
if (!api) throw new Error('No Api instance available');

const formData = await request.formData();
const data = Object.fromEntries(formData);

const brandingOptions: BrandingOptions = {
CustomCss: data.CustomCss?.toString(),
LoginDisclaimer: data.LoginDisclaimer?.toString(),
SplashscreenEnabled: data.SplashscreenEnabled?.toString() === 'on'
};

await getConfigurationApi(api)
.updateNamedConfiguration({
key: BRANDING_CONFIG_KEY,
body: JSON.stringify(brandingOptions)
});

void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});

return {
isSaved: true
};
};

export const loader = () => {
return queryClient.ensureQueryData(
getBrandingOptionsQuery(ServerConnections.getCurrentApi()));
};

export const Component = () => {
const actionData = useActionData() as ActionData | undefined;
const [ isSubmitting, setIsSubmitting ] = useState(false);

const {
data: defaultBrandingOptions,
isPending
} = useBrandingOptions();
const [ brandingOptions, setBrandingOptions ] = useState(defaultBrandingOptions || {});

useEffect(() => {
setIsSubmitting(false);
}, [ actionData ]);

const onSubmit = useCallback(() => {
setIsSubmitting(true);
}, []);

const setSplashscreenEnabled = useCallback((_: React.ChangeEvent<HTMLInputElement>, isEnabled: boolean) => {
setBrandingOptions({
...brandingOptions,
[BrandingOption.SplashscreenEnabled]: isEnabled
});
}, [ brandingOptions ]);

const setBrandingOption = useCallback((event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
if (Object.keys(BrandingOption).includes(event.target.name)) {
setBrandingOptions({
...brandingOptions,
[event.target.name]: event.target.value
});
}
}, [ brandingOptions ]);

if (isPending) return <Loading />;

return (
<Page
id='brandingPage'
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
<Form
method='POST'
onSubmit={onSubmit}
>
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('HeaderBranding')}
</Typography>

{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}

<FormControlLabel
control={
<Switch
name={BrandingOption.SplashscreenEnabled}
checked={brandingOptions?.SplashscreenEnabled}
onChange={setSplashscreenEnabled}
/>
}
label={globalize.translate('EnableSplashScreen')}
/>

<TextField
fullWidth
multiline
minRows={5}
maxRows={5}
InputProps={{
className: 'textarea-mono'
}}
name={BrandingOption.LoginDisclaimer}
label={globalize.translate('LabelLoginDisclaimer')}
helperText={globalize.translate('LabelLoginDisclaimerHelp')}
value={brandingOptions?.LoginDisclaimer}
onChange={setBrandingOption}
/>

<TextField
fullWidth
multiline
minRows={5}
maxRows={20}
InputProps={{
className: 'textarea-mono'
}}
name={BrandingOption.CustomCss}
label={globalize.translate('LabelCustomCss')}
helperText={globalize.translate('LabelCustomCssHelp')}
value={brandingOptions?.CustomCss}
onChange={setBrandingOption}
/>

<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
</Form>
</Box>
</Page>
);
};

Component.displayName = 'BrandingPage';
1 change: 0 additions & 1 deletion src/apps/dashboard/routes/playback/trickplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ const PlaybackTrickplay: FC = () => {
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('Trickplay')}
isLinkVisible={false}
/>
</div>

Expand Down
Loading

0 comments on commit b2e3772

Please sign in to comment.