feat(settings): add a proper modal for switching

This commit is contained in:
0xsysr3ll
2026-02-22 21:33:52 +01:00
parent 3753d67e9a
commit e44d5116e0
3 changed files with 195 additions and 108 deletions

View File

@@ -1,28 +1,50 @@
import ConfirmButton from '@app/components/Common/ConfirmButton';
import Alert from '@app/components/Common/Alert';
import Button from '@app/components/Common/Button';
import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { MediaServerType } from '@server/constants/server';
import axios from 'axios';
import { useState } from 'react';
import Link from 'next/link';
import { Fragment, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
type SwitchTargetServerType = 'jellyfin' | 'emby';
type SwitchTargetServerType = 'jellyfin' | 'emby' | 'plex';
const messages = defineMessages('components.Settings', {
switchMediaServerError:
'Something went wrong while switching media server. Please try again.',
switchMediaServerSuccess:
'Media server switched. All users logged out. Restart the server, then sign in again.',
switchMediaServerStepsPlex:
'1) Have users link Jellyfin or Emby in {profile} → {linkedAccounts}.\n2) Optionally check {users} to see who has linked.\n3) Choose the target below and switch.',
switchMediaServerStepsJellyfinEmby:
'1) Configure Plex in the Plex tab.\n2) Have users link Plex in {profile} → {linkedAccounts}.\n3) Optionally check {users}.\n4) Choose the target below and switch.',
switchMediaServerWarning:
'Everyone will be logged out. You must restart the server after switching.',
switchTargetAfter: 'New media server:',
switchMediaServerButton: 'Switch media server',
checkUsersLink: 'Users',
});
const SwitchMediaServerSection = () => {
const settings = useSettings();
const intl = useIntl();
const { addToast } = useToasts();
const isPlex =
settings.currentSettings.mediaServerType === MediaServerType.PLEX;
const isJellyfin =
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN;
const isEmby =
settings.currentSettings.mediaServerType === MediaServerType.EMBY;
const [isModalOpen, setModalOpen] = useState(false);
const [isSubmitting, setSubmitting] = useState(false);
const [switchTargetServerType, setSwitchTargetServerType] =
useState<SwitchTargetServerType>('jellyfin');
useState<SwitchTargetServerType>(isPlex ? 'jellyfin' : 'plex');
if (
settings.currentSettings.mediaServerType === MediaServerType.NOT_CONFIGURED
@@ -30,113 +52,146 @@ const SwitchMediaServerSection = () => {
return null;
}
const targetPayload = { targetServerType: switchTargetServerType };
const handleSwitch = async () => {
setSubmitting(true);
try {
const { data } = await axios.post<{ message?: string }>(
'/api/v1/settings/switch-media-server',
targetPayload
);
addToast(
data?.message ?? intl.formatMessage(messages.switchMediaServerSuccess),
{ appearance: 'success' }
);
setModalOpen(false);
window.location.reload();
} catch (err: unknown) {
const extracted = axios.isAxiosError(err)
? (err.response?.data?.error ??
err.response?.data?.message ??
err.message)
: err instanceof Error
? err.message
: null;
const message =
extracted != null && String(extracted).trim() !== ''
? String(extracted)
: intl.formatMessage(messages.switchMediaServerError);
addToast(message, { appearance: 'error' });
} finally {
setSubmitting(false);
}
};
const linkValues = {
profile: <strong className="font-semibold text-gray-300">Profile</strong>,
linkedAccounts: (
<strong className="font-semibold text-gray-300">Linked accounts</strong>
),
users: (
<Link
href="/settings/users"
className="font-medium text-indigo-400 hover:text-indigo-300 hover:underline"
>
{intl.formatMessage(messages.checkUsersLink)}
</Link>
),
};
return (
<div className="mt-10 border-t border-gray-700 pt-8">
<h3 className="text-lg font-medium text-red-400">
<FormattedMessage
id="components.Settings.switchMediaServer"
defaultMessage="Switch media server"
/>
</h3>
<p className="mt-1 text-sm text-gray-400">
{settings.currentSettings.mediaServerType === MediaServerType.PLEX ? (
<FormattedMessage
id="components.Settings.switchMediaServerDescriptionPlex"
defaultMessage="Have users link Jellyfin or Emby in {profile} => {linkedAccounts} first, then choose the target below and switch. All users will be logged out. Restart the server after switching."
values={{
profile: (
<strong className="font-semibold text-gray-300">Profile</strong>
),
linkedAccounts: (
<strong className="font-semibold text-gray-300">
Linked accounts
</strong>
),
}}
/>
) : (
<FormattedMessage
id="components.Settings.switchMediaServerDescriptionJellyfinEmby"
defaultMessage="Configure Plex in the Plex tab, then have users link Plex in {profile} => {linkedAccounts}, then switch. This clears the current server. All users will be logged out. Restart the server after switching."
values={{
profile: (
<strong className="font-semibold text-gray-300">Profile</strong>
),
linkedAccounts: (
<strong className="font-semibold text-gray-300">
Linked accounts
</strong>
),
}}
/>
)}
</p>
{settings.currentSettings.mediaServerType === MediaServerType.PLEX && (
<div className="mt-3 flex items-center gap-2">
<label
htmlFor="switch-target-server"
className="text-sm text-gray-400"
>
<FormattedMessage
id="components.Settings.switchTargetServerType"
defaultMessage="After switch, use:"
/>
</label>
<select
id="switch-target-server"
className="rounded-md border border-gray-600 bg-gray-800 px-3 py-1.5 text-sm text-white focus:border-indigo-500 focus:ring-indigo-500"
value={switchTargetServerType}
onChange={(e) =>
setSwitchTargetServerType(
e.target.value as SwitchTargetServerType
)
}
>
<option value="jellyfin">Jellyfin</option>
<option value="emby">Emby</option>
</select>
</div>
)}
<ConfirmButton
className="mt-4"
confirmText={intl.formatMessage(globalMessages.areyousure)}
onClick={async () => {
try {
const { data } = await axios.post<{ message?: string }>(
'/api/v1/settings/switch-media-server',
settings.currentSettings.mediaServerType === MediaServerType.PLEX
? { targetServerType: switchTargetServerType }
: undefined
);
addToast(
data?.message ??
intl.formatMessage(messages.switchMediaServerSuccess),
{
appearance: 'success',
}
);
window.location.reload();
} catch (err: unknown) {
const extracted = axios.isAxiosError(err)
? (err.response?.data?.error ??
err.response?.data?.message ??
err.message)
: err instanceof Error
? err.message
: null;
const message =
extracted != null && String(extracted).trim() !== ''
? String(extracted)
: intl.formatMessage(messages.switchMediaServerError);
addToast(message, { appearance: 'error' });
}
}}
>
<Button buttonType="danger" onClick={() => setModalOpen(true)}>
<FormattedMessage
id="components.Settings.switchMediaServerButton"
defaultMessage="Switch media server"
defaultMessage={messages.switchMediaServerButton.defaultMessage}
/>
</ConfirmButton>
</Button>
<Transition
as={Fragment}
show={isModalOpen}
enter="transition-opacity ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Modal
title={intl.formatMessage(messages.switchMediaServerButton)}
onCancel={() => !isSubmitting && setModalOpen(false)}
onOk={handleSwitch}
okText={intl.formatMessage(messages.switchMediaServerButton)}
okButtonType="danger"
cancelText={intl.formatMessage(globalMessages.cancel)}
loading={isSubmitting}
okDisabled={isSubmitting}
>
<p className="whitespace-pre-line text-gray-300">
{isPlex ? (
<FormattedMessage
id="components.Settings.switchMediaServerStepsPlex"
defaultMessage={
messages.switchMediaServerStepsPlex.defaultMessage
}
values={linkValues}
/>
) : (
<FormattedMessage
id="components.Settings.switchMediaServerStepsJellyfinEmby"
defaultMessage={
messages.switchMediaServerStepsJellyfinEmby.defaultMessage
}
values={linkValues}
/>
)}
</p>
<div className="mt-3">
<Alert
title={intl.formatMessage(messages.switchMediaServerWarning)}
type="warning"
/>
</div>
<div className="mt-3 flex flex-wrap items-center gap-2">
<label
htmlFor="switch-target-server-modal"
className="text-sm text-gray-400"
>
<FormattedMessage
id="components.Settings.switchTargetAfter"
defaultMessage={messages.switchTargetAfter.defaultMessage}
/>
</label>
<select
id="switch-target-server-modal"
className="rounded-md border border-gray-600 bg-gray-800 px-3 py-1.5 text-sm text-white focus:border-indigo-500 focus:ring-indigo-500"
value={switchTargetServerType}
onChange={(e) =>
setSwitchTargetServerType(
e.target.value as SwitchTargetServerType
)
}
disabled={isSubmitting}
>
{isPlex && (
<>
<option value="jellyfin">Jellyfin</option>
<option value="emby">Emby</option>
</>
)}
{(isJellyfin || isEmby) && (
<>
<option value="plex">Plex</option>
{isEmby && <option value="jellyfin">Jellyfin</option>}
{isJellyfin && <option value="emby">Emby</option>}
</>
)}
</select>
</div>
</Modal>
</Transition>
</div>
);
};

View File

@@ -37,7 +37,7 @@ import { Field, Form, Formik } from 'formik';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import useSWR from 'swr';
import validator from 'validator';
import * as Yup from 'yup';
@@ -95,6 +95,8 @@ const messages = defineMessages('components.UserList', {
'The <strong>Enable Local Sign-In</strong> setting is currently disabled.',
linkedToPlex: 'Plex linked',
linkedToJellyfinEmby: 'Jellyfin/Emby linked',
switchMediaServerTip:
'Users with "Plex linked" or "Jellyfin/Emby linked" can sign in after you switch media server in {generalSettings}.',
});
type Sort =
@@ -696,6 +698,29 @@ const UserList = () => {
</div>
</div>
</div>
{(settings.currentSettings.mediaServerType === MediaServerType.PLEX ||
settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN ||
settings.currentSettings.mediaServerType === MediaServerType.EMBY) && (
<p className="mb-4 text-sm text-gray-400">
<FormattedMessage
id="components.UserList.switchMediaServerTip"
defaultMessage={messages.switchMediaServerTip.defaultMessage}
values={{
generalSettings: (
<Link
href="/settings/main"
className="font-medium text-indigo-400 hover:text-indigo-300 hover:underline"
>
<FormattedMessage
id="components.UserList.generalSettingsLinkText"
defaultMessage="General"
/>
</Link>
),
}}
/>
</p>
)}
<Table>
<thead>
<tr>

View File

@@ -1162,6 +1162,7 @@
"components.Settings.blocklistedTagImportTitle": "Import Blocklisted Tag Configuration",
"components.Settings.blocklistedTagsText": "Blocklisted Tags",
"components.Settings.cancelscan": "Cancel Scan",
"components.Settings.checkUsersLink": "Users",
"components.Settings.chooseProvider": "Choose metadata providers for different content types",
"components.Settings.clearBlocklistedTagsConfirm": "Are you sure you want to clear the blocklisted tags?",
"components.Settings.clickTest": "Click on the \"Test\" button to check connectivity with metadata providers",
@@ -1267,8 +1268,13 @@
"components.Settings.ssl": "SSL",
"components.Settings.startscan": "Start Scan",
"components.Settings.starttyping": "Starting typing to search.",
"components.Settings.switchMediaServerButton": "Switch media server",
"components.Settings.switchMediaServerError": "Something went wrong while switching media server. Please try again.",
"components.Settings.switchMediaServerStepsJellyfinEmby": "1) Configure Plex in the Plex tab.\\n2) Have users link Plex in {profile} → {linkedAccounts}.\\n3) Optionally check {users}.\\n4) Choose the target below and switch.",
"components.Settings.switchMediaServerStepsPlex": "1) Have users link Jellyfin or Emby in {profile} → {linkedAccounts}.\\n2) Optionally check {users} to see who has linked.\\n3) Choose the target below and switch.",
"components.Settings.switchMediaServerSuccess": "Media server switched. All users logged out. Restart the server, then sign in again.",
"components.Settings.switchMediaServerWarning": "Everyone will be logged out. You must restart the server after switching.",
"components.Settings.switchTargetAfter": "New media server:",
"components.Settings.syncJellyfin": "Sync Libraries",
"components.Settings.syncing": "Syncing",
"components.Settings.tautulliApiKey": "API Key",
@@ -1425,6 +1431,7 @@
"components.UserList.sortByUser": "Sort by username",
"components.UserList.toggleSortDirection": "Click again to sort {direction}",
"components.UserList.toggleSortDirectionAria": "Toggle sort direction",
"components.UserList.switchMediaServerTip": "Users with \"Plex linked\" or \"Jellyfin/Emby linked\" can sign in after you switch media server in {generalSettings}.",
"components.UserList.totalrequests": "Requests",
"components.UserList.user": "User",
"components.UserList.usercreatedfailed": "Something went wrong while creating the user.",
@@ -1675,4 +1682,4 @@
"pages.returnHome": "Return Home",
"pages.serviceunavailable": "Service Unavailable",
"pages.somethingwentwrong": "Something Went Wrong"
}
}