fix: allow users to override language/region settings

fixes #1013
This commit is contained in:
sct
2021-02-27 12:37:18 +00:00
parent 537850f414
commit 69294a7c4c
6 changed files with 141 additions and 39 deletions

View File

@@ -13,6 +13,7 @@ export interface PublicSettingsResponse {
movie4kEnabled: boolean; movie4kEnabled: boolean;
series4kEnabled: boolean; series4kEnabled: boolean;
region: string; region: string;
originalLanguage: string;
} }
export interface CacheItem { export interface CacheItem {

View File

@@ -85,6 +85,7 @@ interface FullPublicSettings extends PublicSettings {
movie4kEnabled: boolean; movie4kEnabled: boolean;
series4kEnabled: boolean; series4kEnabled: boolean;
region: string; region: string;
originalLanguage: string;
} }
export interface NotificationAgentConfig { export interface NotificationAgentConfig {
@@ -337,6 +338,7 @@ class Settings {
(sonarr) => sonarr.is4k && sonarr.isDefault (sonarr) => sonarr.is4k && sonarr.isDefault
), ),
region: this.data.main.region, region: this.data.main.region,
originalLanguage: this.data.main.originalLanguage,
}; };
} }

View File

@@ -5,16 +5,35 @@ import Media from '../entity/Media';
import { isMovie, isPerson } from '../utils/typeHelpers'; import { isMovie, isPerson } from '../utils/typeHelpers';
import { MediaType } from '../constants/media'; import { MediaType } from '../constants/media';
import { getSettings } from '../lib/settings'; import { getSettings } from '../lib/settings';
import { User } from '../entity/User';
const createTmdbWithRegionLanaguage = (user?: User): TheMovieDb => {
const settings = getSettings();
const region =
user?.settings?.region === 'all'
? ''
: user?.settings?.region
? user?.settings?.region
: settings.main.region;
const originalLanguage =
user?.settings?.originalLanguage === 'all'
? ''
: user?.settings?.originalLanguage
? user?.settings?.originalLanguage
: settings.main.originalLanguage;
return new TheMovieDb({
region,
originalLanguage,
});
};
const discoverRoutes = Router(); const discoverRoutes = Router();
discoverRoutes.get('/movies', async (req, res) => { discoverRoutes.get('/movies', async (req, res) => {
const settings = getSettings(); const tmdb = createTmdbWithRegionLanaguage(req.user);
const tmdb = new TheMovieDb({
region: req.user?.settings?.region ?? settings.main.region,
originalLanguage:
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
});
const data = await tmdb.getDiscoverMovies({ const data = await tmdb.getDiscoverMovies({
page: Number(req.query.page), page: Number(req.query.page),
@@ -41,12 +60,7 @@ discoverRoutes.get('/movies', async (req, res) => {
}); });
discoverRoutes.get('/movies/upcoming', async (req, res) => { discoverRoutes.get('/movies/upcoming', async (req, res) => {
const settings = getSettings(); const tmdb = createTmdbWithRegionLanaguage(req.user);
const tmdb = new TheMovieDb({
region: req.user?.settings?.region ?? settings.main.region,
originalLanguage:
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
});
const now = new Date(); const now = new Date();
const offset = now.getTimezoneOffset(); const offset = now.getTimezoneOffset();
@@ -80,12 +94,7 @@ discoverRoutes.get('/movies/upcoming', async (req, res) => {
}); });
discoverRoutes.get('/tv', async (req, res) => { discoverRoutes.get('/tv', async (req, res) => {
const settings = getSettings(); const tmdb = createTmdbWithRegionLanaguage(req.user);
const tmdb = new TheMovieDb({
region: req.user?.settings?.region ?? settings.main.region,
originalLanguage:
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
});
const data = await tmdb.getDiscoverTv({ const data = await tmdb.getDiscoverTv({
page: Number(req.query.page), page: Number(req.query.page),
@@ -112,12 +121,7 @@ discoverRoutes.get('/tv', async (req, res) => {
}); });
discoverRoutes.get('/tv/upcoming', async (req, res) => { discoverRoutes.get('/tv/upcoming', async (req, res) => {
const settings = getSettings(); const tmdb = createTmdbWithRegionLanaguage(req.user);
const tmdb = new TheMovieDb({
region: req.user?.settings?.region ?? settings.main.region,
originalLanguage:
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
});
const now = new Date(); const now = new Date();
const offset = now.getTimezoneOffset(); const offset = now.getTimezoneOffset();
@@ -151,12 +155,7 @@ discoverRoutes.get('/tv/upcoming', async (req, res) => {
}); });
discoverRoutes.get('/trending', async (req, res) => { discoverRoutes.get('/trending', async (req, res) => {
const settings = getSettings(); const tmdb = createTmdbWithRegionLanaguage(req.user);
const tmdb = new TheMovieDb({
region: req.user?.settings?.region ?? settings.main.region,
originalLanguage:
req.user?.settings?.originalLanguage ?? settings.main.originalLanguage,
});
const data = await tmdb.getAllTrending({ const data = await tmdb.getAllTrending({
page: Number(req.query.page), page: Number(req.query.page),

View File

@@ -1,37 +1,54 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Listbox, Transition } from '@headlessui/react'; import { Listbox, Transition } from '@headlessui/react';
import { countryCodeEmoji } from 'country-code-emoji'; import { countryCodeEmoji } from 'country-code-emoji';
import useSWR from 'swr'; import useSWR from 'swr';
import type { Region } from '../../../server/lib/settings'; import type { Region } from '../../../server/lib/settings';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import useSettings from '../../hooks/useSettings';
const messages = defineMessages({ const messages = defineMessages({
regionDefault: 'All Regions', regionDefault: 'All Regions',
regionServerDefault: '{applicationTitle} Default ({region})',
}); });
interface RegionSelectorProps { interface RegionSelectorProps {
value: string; value: string;
name: string; name: string;
isUserSetting?: boolean;
onChange?: (fieldName: string, region: string) => void; onChange?: (fieldName: string, region: string) => void;
} }
const RegionSelector: React.FC<RegionSelectorProps> = ({ const RegionSelector: React.FC<RegionSelectorProps> = ({
name, name,
value, value,
isUserSetting = false,
onChange, onChange,
}) => { }) => {
const { currentSettings } = useSettings();
const intl = useIntl(); const intl = useIntl();
const { data: regions } = useSWR<Region[]>('/api/v1/regions'); const { data: regions } = useSWR<Region[]>('/api/v1/regions');
const [selectedRegion, setSelectedRegion] = useState<Region | null>(null); const [selectedRegion, setSelectedRegion] = useState<Region | null>(null);
const allRegion: Region = useMemo(
() => ({
iso_3166_1: 'all',
english_name: 'All',
}),
[]
);
useEffect(() => { useEffect(() => {
if (regions && value) { if (regions && value) {
const matchedRegion = regions.find( if (value === 'all') {
(region) => region.iso_3166_1 === value setSelectedRegion(allRegion);
); } else {
setSelectedRegion(matchedRegion ?? null); const matchedRegion = regions.find(
(region) => region.iso_3166_1 === value
);
setSelectedRegion(matchedRegion ?? null);
}
} }
}, [value, regions]); }, [value, regions, allRegion]);
useEffect(() => { useEffect(() => {
if (onChange && regions) { if (onChange && regions) {
@@ -47,15 +64,26 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
<div className="relative"> <div className="relative">
<span className="inline-block w-full rounded-md shadow-sm"> <span className="inline-block w-full rounded-md shadow-sm">
<Listbox.Button className="relative flex items-center w-full py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md cursor-default focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm sm:leading-5"> <Listbox.Button className="relative flex items-center w-full py-2 pl-3 pr-10 text-left text-white transition duration-150 ease-in-out bg-gray-700 border border-gray-500 rounded-md cursor-default focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm sm:leading-5">
{selectedRegion && ( {selectedRegion && selectedRegion.iso_3166_1 !== 'all' && (
<span className="h-4 mr-2 overflow-hidden text-lg leading-4"> <span className="h-4 mr-2 overflow-hidden text-lg leading-4">
{countryCodeEmoji(selectedRegion.iso_3166_1)} {countryCodeEmoji(selectedRegion.iso_3166_1)}
</span> </span>
)} )}
<span className="block truncate"> <span className="block truncate">
{selectedRegion {selectedRegion && selectedRegion.iso_3166_1 !== 'all'
? intl.formatDisplayName(selectedRegion.iso_3166_1, { ? intl.formatDisplayName(selectedRegion.iso_3166_1, {
type: 'region', type: 'region',
fallback: 'none',
}) ?? selectedRegion.english_name
: isUserSetting && selectedRegion?.iso_3166_1 !== 'all'
? intl.formatMessage(messages.regionServerDefault, {
applicationTitle: currentSettings.applicationTitle,
region: currentSettings.region
? intl.formatDisplayName(currentSettings.region, {
type: 'region',
fallback: 'none',
}) ?? currentSettings.region
: intl.formatMessage(messages.regionDefault),
}) })
: intl.formatMessage(messages.regionDefault)} : intl.formatMessage(messages.regionDefault)}
</span> </span>
@@ -89,7 +117,60 @@ const RegionSelector: React.FC<RegionSelectorProps> = ({
static static
className="py-1 overflow-auto text-base leading-6 rounded-md shadow-xs max-h-60 focus:outline-none sm:text-sm sm:leading-5" className="py-1 overflow-auto text-base leading-6 rounded-md shadow-xs max-h-60 focus:outline-none sm:text-sm sm:leading-5"
> >
<Listbox.Option value={null}> {isUserSetting && (
<Listbox.Option value={null}>
{({ selected, active }) => (
<div
className={`${
active
? 'text-white bg-indigo-600'
: 'text-gray-300'
} cursor-default select-none relative py-2 pl-8 pr-4`}
>
<span
className={`${
selected ? 'font-semibold' : 'font-normal'
} block truncate`}
>
{intl.formatMessage(messages.regionServerDefault, {
applicationTitle:
currentSettings.applicationTitle,
region: currentSettings.region
? intl.formatDisplayName(
currentSettings.region,
{
type: 'region',
fallback: 'none',
}
) ?? currentSettings.region
: intl.formatMessage(messages.regionDefault),
})}
</span>
{selected && (
<span
className={`${
active ? 'text-white' : 'text-indigo-600'
} absolute inset-y-0 left-0 flex items-center pl-1.5`}
>
<svg
className="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</div>
)}
</Listbox.Option>
)}
<Listbox.Option value={isUserSetting ? allRegion : null}>
{({ selected, active }) => ( {({ selected, active }) => (
<div <div
className={`${ className={`${

View File

@@ -6,6 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications'; import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr'; import useSWR from 'swr';
import { Language } from '../../../../../server/lib/settings'; import { Language } from '../../../../../server/lib/settings';
import useSettings from '../../../../hooks/useSettings';
import { UserType, useUser } from '../../../../hooks/useUser'; import { UserType, useUser } from '../../../../hooks/useUser';
import Error from '../../../../pages/_error'; import Error from '../../../../pages/_error';
import Badge from '../../../Common/Badge'; import Badge from '../../../Common/Badge';
@@ -29,6 +30,7 @@ const messages = defineMessages({
originallanguageTip: originallanguageTip:
'Filter content by original language (only applies to the "Popular" and "Upcoming" categories)', 'Filter content by original language (only applies to the "Popular" and "Upcoming" categories)',
originalLanguageDefault: 'All Languages', originalLanguageDefault: 'All Languages',
languageServerDefault: '{applicationTitle} Default ({language})',
}); });
const UserGeneralSettings: React.FC = () => { const UserGeneralSettings: React.FC = () => {
@@ -36,6 +38,7 @@ const UserGeneralSettings: React.FC = () => {
const { addToast } = useToasts(); const { addToast } = useToasts();
const router = useRouter(); const router = useRouter();
const { user, mutate } = useUser({ id: Number(router.query.userId) }); const { user, mutate } = useUser({ id: Number(router.query.userId) });
const { currentSettings } = useSettings();
const { data, error, revalidate } = useSWR<{ const { data, error, revalidate } = useSWR<{
username?: string; username?: string;
region?: string; region?: string;
@@ -143,6 +146,7 @@ const UserGeneralSettings: React.FC = () => {
<RegionSelector <RegionSelector
name="region" name="region"
value={values.region ?? ''} value={values.region ?? ''}
isUserSetting
onChange={setFieldValue} onChange={setFieldValue}
/> />
</div> </div>
@@ -162,6 +166,19 @@ const UserGeneralSettings: React.FC = () => {
name="originalLanguage" name="originalLanguage"
> >
<option value=""> <option value="">
{intl.formatMessage(messages.languageServerDefault, {
applicationTitle: currentSettings.applicationTitle,
language:
intl.formatDisplayName(
currentSettings.originalLanguage,
{
type: 'language',
fallback: 'none',
}
) ?? currentSettings.originalLanguage,
})}
</option>
<option value="all">
{intl.formatMessage(messages.originalLanguageDefault)} {intl.formatMessage(messages.originalLanguageDefault)}
</option> </option>
{languages?.map((language) => ( {languages?.map((language) => (

View File

@@ -139,6 +139,7 @@
"components.PlexLoginButton.signingin": "Signing in…", "components.PlexLoginButton.signingin": "Signing in…",
"components.PlexLoginButton.signinwithplex": "Sign In", "components.PlexLoginButton.signinwithplex": "Sign In",
"components.RegionSelector.regionDefault": "All Regions", "components.RegionSelector.regionDefault": "All Regions",
"components.RegionSelector.regionServerDefault": "{applicationTitle} Default ({region})",
"components.RequestBlock.profilechanged": "Quality Profile", "components.RequestBlock.profilechanged": "Quality Profile",
"components.RequestBlock.requestoverrides": "Request Overrides", "components.RequestBlock.requestoverrides": "Request Overrides",
"components.RequestBlock.rootfolder": "Root Folder", "components.RequestBlock.rootfolder": "Root Folder",
@@ -679,6 +680,7 @@
"components.UserProfile.ProfileHeader.settings": "Edit Settings", "components.UserProfile.ProfileHeader.settings": "Edit Settings",
"components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Display Name", "components.UserProfile.UserSettings.UserGeneralSettings.displayName": "Display Name",
"components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "General Settings", "components.UserProfile.UserSettings.UserGeneralSettings.generalsettings": "General Settings",
"components.UserProfile.UserSettings.UserGeneralSettings.languageServerDefault": "{applicationTitle} Default ({language})",
"components.UserProfile.UserSettings.UserGeneralSettings.localuser": "Local User", "components.UserProfile.UserSettings.UserGeneralSettings.localuser": "Local User",
"components.UserProfile.UserSettings.UserGeneralSettings.originalLanguageDefault": "All Languages", "components.UserProfile.UserSettings.UserGeneralSettings.originalLanguageDefault": "All Languages",
"components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "Discover Language", "components.UserProfile.UserSettings.UserGeneralSettings.originallanguage": "Discover Language",