mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Migrate libraries metadata to React
This commit is contained in:
parent
1693618589
commit
c8d2ce4142
9 changed files with 222 additions and 145 deletions
|
@ -1,94 +0,0 @@
|
||||||
import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution';
|
|
||||||
|
|
||||||
import 'jquery';
|
|
||||||
|
|
||||||
import loading from 'components/loading/loading';
|
|
||||||
import globalize from 'lib/globalize';
|
|
||||||
import Dashboard from 'utils/dashboard';
|
|
||||||
|
|
||||||
import 'components/listview/listview.scss';
|
|
||||||
|
|
||||||
function populateImageResolutionOptions(select) {
|
|
||||||
let html = '';
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: globalize.translate('ResolutionMatchSource'),
|
|
||||||
value: ImageResolution.MatchSource
|
|
||||||
},
|
|
||||||
{ name: '2160p', value: ImageResolution.P2160 },
|
|
||||||
{ name: '1440p', value: ImageResolution.P1440 },
|
|
||||||
{ name: '1080p', value: ImageResolution.P1080 },
|
|
||||||
{ name: '720p', value: ImageResolution.P720 },
|
|
||||||
{ name: '480p', value: ImageResolution.P480 },
|
|
||||||
{ name: '360p', value: ImageResolution.P360 },
|
|
||||||
{ name: '240p', value: ImageResolution.P240 },
|
|
||||||
{ name: '144p', value: ImageResolution.P144 }
|
|
||||||
].forEach(({ value, name }) => {
|
|
||||||
html += `<option value="${value}">${name}</option>`;
|
|
||||||
});
|
|
||||||
select.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateLanguages(select) {
|
|
||||||
return ApiClient.getCultures().then(function(languages) {
|
|
||||||
let html = '';
|
|
||||||
html += "<option value=''></option>";
|
|
||||||
for (let i = 0, length = languages.length; i < length; i++) {
|
|
||||||
const culture = languages[i];
|
|
||||||
html += "<option value='" + culture.TwoLetterISOLanguageName + "'>" + culture.DisplayName + '</option>';
|
|
||||||
}
|
|
||||||
select.innerHTML = html;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateCountries(select) {
|
|
||||||
return ApiClient.getCountries().then(function(allCountries) {
|
|
||||||
let html = '';
|
|
||||||
html += "<option value=''></option>";
|
|
||||||
for (let i = 0, length = allCountries.length; i < length; i++) {
|
|
||||||
const culture = allCountries[i];
|
|
||||||
html += "<option value='" + culture.TwoLetterISORegionName + "'>" + culture.DisplayName + '</option>';
|
|
||||||
}
|
|
||||||
select.innerHTML = html;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadPage(page) {
|
|
||||||
const promises = [
|
|
||||||
ApiClient.getServerConfiguration(),
|
|
||||||
populateLanguages(page.querySelector('#selectLanguage')),
|
|
||||||
populateCountries(page.querySelector('#selectCountry'))
|
|
||||||
];
|
|
||||||
|
|
||||||
populateImageResolutionOptions(page.querySelector('#txtChapterImageResolution'));
|
|
||||||
|
|
||||||
Promise.all(promises).then(function(responses) {
|
|
||||||
const config = responses[0];
|
|
||||||
page.querySelector('#selectLanguage').value = config.PreferredMetadataLanguage || '';
|
|
||||||
page.querySelector('#selectCountry').value = config.MetadataCountryCode || '';
|
|
||||||
page.querySelector('#valDummyChapterDuration').value = config.DummyChapterDuration || '0';
|
|
||||||
page.querySelector('#txtChapterImageResolution').value = config.ChapterImageResolution || '';
|
|
||||||
loading.hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSubmit() {
|
|
||||||
const form = this;
|
|
||||||
loading.show();
|
|
||||||
ApiClient.getServerConfiguration().then(function(config) {
|
|
||||||
config.PreferredMetadataLanguage = form.querySelector('#selectLanguage').value;
|
|
||||||
config.MetadataCountryCode = form.querySelector('#selectCountry').value;
|
|
||||||
config.DummyChapterDuration = form.querySelector('#valDummyChapterDuration').value;
|
|
||||||
config.ChapterImageResolution = form.querySelector('#txtChapterImageResolution').value;
|
|
||||||
ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).on('pageinit', '#metadataImagesConfigurationPage', function() {
|
|
||||||
$('.metadataImagesConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
|
|
||||||
}).on('pageshow', '#metadataImagesConfigurationPage', function() {
|
|
||||||
loading.show();
|
|
||||||
loadPage(this);
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
<div id="metadataImagesConfigurationPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${LabelMetadata}">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<div class="content-primary">
|
|
||||||
|
|
||||||
<form class="metadataImagesConfigurationForm">
|
|
||||||
<div class="verticalSection">
|
|
||||||
<h2 style="margin-top:0;">${HeaderPreferredMetadataLanguage}</h2>
|
|
||||||
|
|
||||||
<p style="margin:1.5em 0;">${DefaultMetadataLangaugeDescription}</p>
|
|
||||||
|
|
||||||
<div class="selectContainer">
|
|
||||||
<select is="emby-select" id="selectLanguage" required="required" label="${LabelLanguage}"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="selectContainer">
|
|
||||||
<select is="emby-select" id="selectCountry" required="required" label="${LabelCountry}"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="verticalSection">
|
|
||||||
<h2>${HeaderDummyChapter}</h2>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" type="number" id="valDummyChapterDuration" label="${LabelDummyChapterDuration}" min="0"></input>
|
|
||||||
<div class="fieldDescription">${LabelDummyChapterDurationHelp}</div>
|
|
||||||
</div>
|
|
||||||
<div class="selectContainer">
|
|
||||||
<select is="emby-select" id="txtChapterImageResolution" label="${LabelChapterImageResolution}"></select>
|
|
||||||
<div class="fieldDescription">
|
|
||||||
<div>${LabelChapterImageResolutionHelp}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
21
src/apps/dashboard/features/libraries/api/useCountries.ts
Normal file
21
src/apps/dashboard/features/libraries/api/useCountries.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Api } from '@jellyfin/sdk';
|
||||||
|
import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
const fetchCountries = async (api: Api, options?: AxiosRequestConfig) => {
|
||||||
|
const response = await getLocalizationApi(api).getCountries(options);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCountries = () => {
|
||||||
|
const { api } = useApi();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [ 'Countries' ],
|
||||||
|
queryFn: ({ signal }) => fetchCountries(api!, { signal }),
|
||||||
|
enabled: !!api
|
||||||
|
});
|
||||||
|
};
|
21
src/apps/dashboard/features/libraries/api/useCultures.ts
Normal file
21
src/apps/dashboard/features/libraries/api/useCultures.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Api } from '@jellyfin/sdk';
|
||||||
|
import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
const fetchCultures = async (api: Api, options?: AxiosRequestConfig) => {
|
||||||
|
const response = await getLocalizationApi(api).getCultures(options);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCultures = () => {
|
||||||
|
const { api } = useApi();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [ 'Cultures' ],
|
||||||
|
queryFn: ({ signal }) => fetchCultures(api!, { signal }),
|
||||||
|
enabled: !!api
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution';
|
||||||
|
import globalize from 'lib/globalize';
|
||||||
|
|
||||||
|
export function getImageResolutionOptions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: globalize.translate('ResolutionMatchSource'),
|
||||||
|
value: ImageResolution.MatchSource
|
||||||
|
},
|
||||||
|
{ name: '2160p', value: ImageResolution.P2160 },
|
||||||
|
{ name: '1440p', value: ImageResolution.P1440 },
|
||||||
|
{ name: '1080p', value: ImageResolution.P1080 },
|
||||||
|
{ name: '720p', value: ImageResolution.P720 },
|
||||||
|
{ name: '480p', value: ImageResolution.P480 },
|
||||||
|
{ name: '360p', value: ImageResolution.P360 },
|
||||||
|
{ name: '240p', value: ImageResolution.P240 },
|
||||||
|
{ name: '144p', value: ImageResolution.P144 }
|
||||||
|
];
|
||||||
|
};
|
|
@ -7,6 +7,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||||
{ path: 'devices', type: AppType.Dashboard },
|
{ path: 'devices', type: AppType.Dashboard },
|
||||||
{ path: 'keys', type: AppType.Dashboard },
|
{ path: 'keys', type: AppType.Dashboard },
|
||||||
{ path: 'libraries/display', type: AppType.Dashboard },
|
{ path: 'libraries/display', type: AppType.Dashboard },
|
||||||
|
{ path: 'libraries/metadata', type: AppType.Dashboard },
|
||||||
{ path: 'logs', type: AppType.Dashboard },
|
{ path: 'logs', type: AppType.Dashboard },
|
||||||
{ path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard },
|
{ path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard },
|
||||||
{ path: 'playback/resume', type: AppType.Dashboard },
|
{ path: 'playback/resume', type: AppType.Dashboard },
|
||||||
|
|
|
@ -37,13 +37,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'encodingsettings',
|
controller: 'encodingsettings',
|
||||||
view: 'encodingsettings.html'
|
view: 'encodingsettings.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'libraries/metadata',
|
|
||||||
pageProps: {
|
|
||||||
appType: AppType.Dashboard,
|
|
||||||
controller: 'metadataImages',
|
|
||||||
view: 'metadataimages.html'
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
path: 'libraries/nfo',
|
path: 'libraries/nfo',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
|
|
159
src/apps/dashboard/routes/libraries/metadata.tsx
Normal file
159
src/apps/dashboard/routes/libraries/metadata.tsx
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution';
|
||||||
|
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 MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { useCountries } from 'apps/dashboard/features/libraries/api/useCountries';
|
||||||
|
import { useCultures } from 'apps/dashboard/features/libraries/api/useCultures';
|
||||||
|
import { getImageResolutionOptions } from 'apps/dashboard/features/libraries/utils/metadataOptions';
|
||||||
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
import Page from 'components/Page';
|
||||||
|
import ServerConnections from 'components/ServerConnections';
|
||||||
|
import { useConfiguration } from 'hooks/useConfiguration';
|
||||||
|
import globalize from 'lib/globalize';
|
||||||
|
import React from 'react';
|
||||||
|
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
|
||||||
|
import { ActionData } from 'types/actionData';
|
||||||
|
|
||||||
|
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 { data: config } = await getConfigurationApi(api).getConfiguration();
|
||||||
|
|
||||||
|
config.PreferredMetadataLanguage = data.Language.toString();
|
||||||
|
config.MetadataCountryCode = data.Country.toString();
|
||||||
|
config.DummyChapterDuration = parseInt(data.DummyChapterDuration.toString(), 10);
|
||||||
|
config.ChapterImageResolution = data.ChapterImageResolution.toString() as ImageResolution;
|
||||||
|
|
||||||
|
await getConfigurationApi(api)
|
||||||
|
.updateConfiguration({ serverConfiguration: config });
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSaved: true
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Component = () => {
|
||||||
|
const {
|
||||||
|
data: config,
|
||||||
|
isPending: isConfigPending,
|
||||||
|
isError: isConfigError
|
||||||
|
} = useConfiguration();
|
||||||
|
const {
|
||||||
|
data: cultures,
|
||||||
|
isPending: isCulturesPending,
|
||||||
|
isError: isCulturesError
|
||||||
|
} = useCultures();
|
||||||
|
const {
|
||||||
|
data: countries,
|
||||||
|
isPending: isCountriesPending,
|
||||||
|
isError: isCountriesError
|
||||||
|
} = useCountries();
|
||||||
|
|
||||||
|
const navigation = useNavigation();
|
||||||
|
const actionData = useActionData() as ActionData | undefined;
|
||||||
|
const isSubmitting = navigation.state === 'submitting';
|
||||||
|
|
||||||
|
const imageResolutions = getImageResolutionOptions();
|
||||||
|
|
||||||
|
if (isConfigPending || isCulturesPending || isCountriesPending) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
id='metadataImagesConfigurationPage'
|
||||||
|
title={globalize.translate('LabelMetadata')}
|
||||||
|
className='type-interior mainAnimatedPage'
|
||||||
|
>
|
||||||
|
<Box className='content-primary'>
|
||||||
|
{isConfigError || isCulturesError || isCountriesError ? (
|
||||||
|
<Alert severity='error'>{globalize.translate('MetadataImagesLoadError')}</Alert>
|
||||||
|
) : (
|
||||||
|
<Form method='POST'>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
{!isSubmitting && actionData?.isSaved && (
|
||||||
|
<Alert severity='success'>
|
||||||
|
{globalize.translate('SettingsSaved')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Typography variant='h2'>{globalize.translate('HeaderPreferredMetadataLanguage')}</Typography>
|
||||||
|
<Typography>{globalize.translate('DefaultMetadataLangaugeDescription')}</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name={'Language'}
|
||||||
|
label={globalize.translate('LabelLanguage')}
|
||||||
|
defaultValue={config?.PreferredMetadataLanguage}
|
||||||
|
select
|
||||||
|
>
|
||||||
|
{cultures.map(culture => {
|
||||||
|
return <MenuItem
|
||||||
|
key={culture.TwoLetterISOLanguageName}
|
||||||
|
value={culture.TwoLetterISOLanguageName}
|
||||||
|
>{culture.DisplayName}</MenuItem>;
|
||||||
|
})}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name={'Country'}
|
||||||
|
label={globalize.translate('LabelCountry')}
|
||||||
|
defaultValue={config?.MetadataCountryCode}
|
||||||
|
select
|
||||||
|
>
|
||||||
|
{countries.map(country => {
|
||||||
|
return <MenuItem
|
||||||
|
key={country.DisplayName}
|
||||||
|
value={country.TwoLetterISORegionName || ''}
|
||||||
|
>{country.DisplayName}</MenuItem>;
|
||||||
|
})}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<Typography variant='h2'>{globalize.translate('HeaderDummyChapter')}</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name={'DummyChapterDuration'}
|
||||||
|
defaultValue={config?.DummyChapterDuration}
|
||||||
|
type='number'
|
||||||
|
inputProps={{
|
||||||
|
min: 0,
|
||||||
|
required: true
|
||||||
|
}}
|
||||||
|
label={globalize.translate('LabelDummyChapterDuration')}
|
||||||
|
helperText={globalize.translate('LabelDummyChapterDurationHelp')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name={'ChapterImageResolution'}
|
||||||
|
select
|
||||||
|
defaultValue={config?.ChapterImageResolution}
|
||||||
|
label={globalize.translate('LabelChapterImageResolution')}
|
||||||
|
helperText={globalize.translate('LabelChapterImageResolutionHelp')}
|
||||||
|
>
|
||||||
|
{imageResolutions.map(resolution => {
|
||||||
|
return <MenuItem key={resolution.name} value={resolution.value}>{resolution.name}</MenuItem>;
|
||||||
|
})}
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
size='large'
|
||||||
|
>
|
||||||
|
{globalize.translate('Save')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Component.displayName = 'MetadataImagesPage';
|
|
@ -1172,6 +1172,7 @@
|
||||||
"MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.",
|
"MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.",
|
||||||
"MessageUnauthorizedUser": "You are not authorized to access the server at this time. Please contact your server administrator for more information.",
|
"MessageUnauthorizedUser": "You are not authorized to access the server at this time. Please contact your server administrator for more information.",
|
||||||
"MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.",
|
"MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.",
|
||||||
|
"MetadataImagesLoadError": "Failed to load metadata settings",
|
||||||
"MetadataManager": "Metadata Manager",
|
"MetadataManager": "Metadata Manager",
|
||||||
"MetadataSettingChangeHelp": "Changing metadata settings will affect new content added going forward. To refresh existing content, open the detail screen and click the 'Refresh' button, or do bulk refreshes using the 'Metadata Manager'.",
|
"MetadataSettingChangeHelp": "Changing metadata settings will affect new content added going forward. To refresh existing content, open the detail screen and click the 'Refresh' button, or do bulk refreshes using the 'Metadata Manager'.",
|
||||||
"MillisecondsUnit": "ms",
|
"MillisecondsUnit": "ms",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue