mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 12:18:35 -05:00
feat(frontend): allow selecting multiple original languages
This commit is contained in:
194
src/components/LanguageSelector/index.tsx
Normal file
194
src/components/LanguageSelector/index.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import type { OptionsType, OptionTypeBase } from 'react-select';
|
||||
import { Language } from '../../../server/lib/settings';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
|
||||
const messages = defineMessages({
|
||||
originalLanguageDefault: 'All Languages',
|
||||
languageServerDefault: 'Default ({language})',
|
||||
});
|
||||
|
||||
const Select = dynamic(() => import('react-select'), { ssr: false });
|
||||
|
||||
type OptionType = {
|
||||
value: string;
|
||||
label: string;
|
||||
isFixed?: boolean;
|
||||
};
|
||||
|
||||
const selectStyles = {
|
||||
multiValueLabel: (base: any, state: { data: { isFixed?: boolean } }) => {
|
||||
return state.data.isFixed ? { ...base, paddingRight: 6 } : base;
|
||||
},
|
||||
multiValueRemove: (base: any, state: { data: { isFixed?: boolean } }) => {
|
||||
return state.data.isFixed ? { ...base, display: 'none' } : base;
|
||||
},
|
||||
};
|
||||
|
||||
interface LanguageSelectorProps {
|
||||
languages: Language[];
|
||||
value?: string;
|
||||
setFieldValue: (property: string, value: string) => void;
|
||||
serverValue?: string;
|
||||
isUserSettings?: boolean;
|
||||
}
|
||||
|
||||
const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
||||
languages,
|
||||
value,
|
||||
setFieldValue,
|
||||
serverValue,
|
||||
isUserSettings = false,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const defaultLanguageNameFallback = serverValue
|
||||
? languages.find((language) => language.iso_639_1 === serverValue)
|
||||
?.english_name ?? serverValue
|
||||
: undefined;
|
||||
|
||||
const options: OptionType[] =
|
||||
languages.map((language) => ({
|
||||
label:
|
||||
intl.formatDisplayName(language.iso_639_1, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ?? language.english_name,
|
||||
value: language.iso_639_1,
|
||||
})) ?? [];
|
||||
|
||||
if (isUserSettings) {
|
||||
options.unshift({
|
||||
value: 'server',
|
||||
label: intl.formatMessage(messages.languageServerDefault, {
|
||||
language: serverValue
|
||||
? serverValue
|
||||
.split('|')
|
||||
.map(
|
||||
(value) =>
|
||||
intl.formatDisplayName(value, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ?? defaultLanguageNameFallback
|
||||
)
|
||||
.reduce((prev, curr) =>
|
||||
intl.formatMessage(globalMessages.delimitedlist, {
|
||||
a: prev,
|
||||
b: curr,
|
||||
})
|
||||
)
|
||||
: intl.formatMessage(messages.originalLanguageDefault),
|
||||
}),
|
||||
isFixed: true,
|
||||
});
|
||||
}
|
||||
|
||||
options.unshift({
|
||||
value: 'all',
|
||||
label: intl.formatMessage(messages.originalLanguageDefault),
|
||||
isFixed: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
isMulti
|
||||
className="react-select-container"
|
||||
classNamePrefix="react-select"
|
||||
value={
|
||||
(isUserSettings && value === 'all') || (!isUserSettings && !value)
|
||||
? {
|
||||
value: 'all',
|
||||
label: intl.formatMessage(messages.originalLanguageDefault),
|
||||
isFixed: true,
|
||||
}
|
||||
: (value === '' || !value || value === 'server') && isUserSettings
|
||||
? {
|
||||
value: 'server',
|
||||
label: intl.formatMessage(messages.languageServerDefault, {
|
||||
language: serverValue
|
||||
? serverValue
|
||||
.split('|')
|
||||
.map(
|
||||
(value) =>
|
||||
intl.formatDisplayName(value, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ?? defaultLanguageNameFallback
|
||||
)
|
||||
.reduce((prev, curr) =>
|
||||
intl.formatMessage(globalMessages.delimitedlist, {
|
||||
a: prev,
|
||||
b: curr,
|
||||
})
|
||||
)
|
||||
: intl.formatMessage(messages.originalLanguageDefault),
|
||||
}),
|
||||
isFixed: true,
|
||||
}
|
||||
: value?.split('|').map((code) => {
|
||||
const matchedLanguage = languages.find(
|
||||
(lang) => lang.iso_639_1 === code
|
||||
);
|
||||
|
||||
if (!matchedLanguage) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
label:
|
||||
intl.formatDisplayName(matchedLanguage.iso_639_1, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ?? matchedLanguage.english_name,
|
||||
value: matchedLanguage.iso_639_1,
|
||||
};
|
||||
}) ?? undefined
|
||||
}
|
||||
onChange={(
|
||||
value: OptionTypeBase | OptionsType<OptionType> | null,
|
||||
options
|
||||
) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(options &&
|
||||
options.action === 'select-option' &&
|
||||
options.option?.value === 'server') ||
|
||||
value?.every(
|
||||
(v: { value: string; label: string }) => v.value === 'server'
|
||||
)
|
||||
) {
|
||||
return setFieldValue('originalLanguage', '');
|
||||
}
|
||||
|
||||
if (
|
||||
(options &&
|
||||
options.action === 'select-option' &&
|
||||
options.option?.value === 'all') ||
|
||||
value?.every(
|
||||
(v: { value: string; label: string }) => v.value === 'all'
|
||||
)
|
||||
) {
|
||||
return setFieldValue('originalLanguage', isUserSettings ? 'all' : '');
|
||||
}
|
||||
|
||||
setFieldValue(
|
||||
'originalLanguage',
|
||||
value
|
||||
?.map((lang) => lang.value)
|
||||
.filter((v) => v !== 'all')
|
||||
.join('|')
|
||||
);
|
||||
}}
|
||||
styles={selectStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelector;
|
||||
@@ -12,6 +12,7 @@ import Badge from '../Common/Badge';
|
||||
import Button from '../Common/Button';
|
||||
import LoadingSpinner from '../Common/LoadingSpinner';
|
||||
import PageTitle from '../Common/PageTitle';
|
||||
import LanguageSelector from '../LanguageSelector';
|
||||
import RegionSelector from '../RegionSelector';
|
||||
import CopyButton from './CopyButton';
|
||||
|
||||
@@ -46,7 +47,6 @@ const messages = defineMessages({
|
||||
validationApplicationTitle: 'You must provide an application title',
|
||||
validationApplicationUrl: 'You must provide a valid URL',
|
||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||
originalLanguageDefault: 'All Languages',
|
||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||
});
|
||||
|
||||
@@ -347,26 +347,11 @@ const SettingsMain: React.FC = () => {
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
as="select"
|
||||
id="originalLanguage"
|
||||
name="originalLanguage"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.originalLanguageDefault)}
|
||||
</option>
|
||||
{sortedLanguages?.map((language) => (
|
||||
<option
|
||||
key={`language-key-${language.iso_639_1}`}
|
||||
value={language.iso_639_1}
|
||||
>
|
||||
{intl.formatDisplayName(language.iso_639_1, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ?? language.english_name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
<LanguageSelector
|
||||
languages={sortedLanguages ?? []}
|
||||
setFieldValue={setFieldValue}
|
||||
value={values.originalLanguage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import Badge from '../../../Common/Badge';
|
||||
import Button from '../../../Common/Button';
|
||||
import LoadingSpinner from '../../../Common/LoadingSpinner';
|
||||
import PageTitle from '../../../Common/PageTitle';
|
||||
import LanguageSelector from '../../../LanguageSelector';
|
||||
import QuotaSelector from '../../../QuotaSelector';
|
||||
import RegionSelector from '../../../RegionSelector';
|
||||
|
||||
@@ -101,11 +102,6 @@ const UserGeneralSettings: React.FC = () => {
|
||||
return <Error statusCode={500} />;
|
||||
}
|
||||
|
||||
const defaultLanguageNameFallback =
|
||||
languages.find(
|
||||
(language) => language.iso_639_1 === currentSettings.originalLanguage
|
||||
)?.english_name ?? currentSettings.originalLanguage;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
@@ -237,41 +233,13 @@ const UserGeneralSettings: React.FC = () => {
|
||||
</label>
|
||||
<div className="form-input">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
as="select"
|
||||
id="originalLanguage"
|
||||
name="originalLanguage"
|
||||
>
|
||||
<option value="">
|
||||
{intl.formatMessage(messages.languageServerDefault, {
|
||||
language: currentSettings.originalLanguage
|
||||
? intl.formatDisplayName(
|
||||
currentSettings.originalLanguage,
|
||||
{
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}
|
||||
) ?? defaultLanguageNameFallback
|
||||
: intl.formatMessage(
|
||||
messages.originalLanguageDefault
|
||||
),
|
||||
})}
|
||||
</option>
|
||||
<option value="all">
|
||||
{intl.formatMessage(messages.originalLanguageDefault)}
|
||||
</option>
|
||||
{sortedLanguages?.map((language) => (
|
||||
<option
|
||||
key={`language-key-${language.iso_639_1}`}
|
||||
value={language.iso_639_1}
|
||||
>
|
||||
{intl.formatDisplayName(language.iso_639_1, {
|
||||
type: 'language',
|
||||
fallback: 'none',
|
||||
}) ?? language.english_name}
|
||||
</option>
|
||||
))}
|
||||
</Field>
|
||||
<LanguageSelector
|
||||
languages={sortedLanguages ?? []}
|
||||
setFieldValue={setFieldValue}
|
||||
serverValue={currentSettings.originalLanguage}
|
||||
value={values.originalLanguage}
|
||||
isUserSettings
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user