1
0
Fork 0
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:
viown 2025-02-25 15:47:09 +03:00
parent 1693618589
commit c8d2ce4142
9 changed files with 222 additions and 145 deletions

View file

@ -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);
});

View file

@ -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>

View 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
});
};

View 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
});
};

View file

@ -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 }
];
};

View file

@ -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 },

View file

@ -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: {

View 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';

View file

@ -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",