mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -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>
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
"components.Discover.upcoming": "Upcoming Movies",
|
||||
"components.Discover.upcomingmovies": "Upcoming Movies",
|
||||
"components.Discover.upcomingtv": "Upcoming Series",
|
||||
"components.LanguageSelector.languageServerDefault": "Default ({language})",
|
||||
"components.LanguageSelector.originalLanguageDefault": "All Languages",
|
||||
"components.Layout.LanguagePicker.changelanguage": "Change Language",
|
||||
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
|
||||
"components.Layout.Sidebar.dashboard": "Discover",
|
||||
@@ -524,7 +526,6 @@
|
||||
"components.Settings.notificationsettingsfailed": "Notification settings failed to save.",
|
||||
"components.Settings.notificationsettingssaved": "Notification settings saved successfully!",
|
||||
"components.Settings.notrunning": "Not Running",
|
||||
"components.Settings.originalLanguageDefault": "All Languages",
|
||||
"components.Settings.originallanguage": "Discover Language",
|
||||
"components.Settings.originallanguageTip": "Filter content by original language",
|
||||
"components.Settings.partialRequestsEnabled": "Allow Partial Series Requests",
|
||||
|
||||
@@ -329,3 +329,56 @@ code {
|
||||
input[type='search']::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.react-select-container {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__control {
|
||||
@apply text-white bg-gray-700 border border-gray-500 rounded-md hover:border-gray-500;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__control--is-focused {
|
||||
@apply text-white bg-gray-700 border border-gray-500 rounded-md shadow;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__menu {
|
||||
@apply text-gray-300 bg-gray-700;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__option--is-focused {
|
||||
@apply text-white bg-gray-600;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__indicator-separator {
|
||||
@apply bg-gray-500;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__indicator {
|
||||
@apply text-gray-500;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__placeholder {
|
||||
@apply text-gray-400;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__multi-value {
|
||||
@apply bg-gray-800 border border-gray-500 rounded-md;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__multi-value__label {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__multi-value__remove {
|
||||
@apply cursor-pointer rounded-r-md hover:bg-red-700 hover:text-red-100;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__input {
|
||||
@apply text-base text-white border-none shadow-sm;
|
||||
}
|
||||
|
||||
.react-select-container .react-select__input input:focus {
|
||||
@apply text-white border-none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
8
src/types/custom.d.ts
vendored
8
src/types/custom.d.ts
vendored
@@ -22,3 +22,11 @@ declare module '*.png' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
interface IClassNames {
|
||||
[className: string]: string;
|
||||
}
|
||||
const classNames: IClassNames;
|
||||
export = classNames;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user