From e44d5116e0d8c8eaac4d83cc8111979ecf93fbc3 Mon Sep 17 00:00:00 2001 From: 0xsysr3ll <0xsysr3ll@pm.me> Date: Sun, 22 Feb 2026 21:33:52 +0100 Subject: [PATCH] feat(settings): add a proper modal for switching --- .../Settings/SwitchMediaServerSection.tsx | 267 +++++++++++------- src/components/UserList/index.tsx | 27 +- src/i18n/locale/en.json | 9 +- 3 files changed, 195 insertions(+), 108 deletions(-) diff --git a/src/components/Settings/SwitchMediaServerSection.tsx b/src/components/Settings/SwitchMediaServerSection.tsx index 6fb8ee271..5c409aeee 100644 --- a/src/components/Settings/SwitchMediaServerSection.tsx +++ b/src/components/Settings/SwitchMediaServerSection.tsx @@ -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('jellyfin'); + useState(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: Profile, + linkedAccounts: ( + Linked accounts + ), + users: ( + + {intl.formatMessage(messages.checkUsersLink)} + + ), + }; + return (
-

- -

-

- {settings.currentSettings.mediaServerType === MediaServerType.PLEX ? ( - Profile - ), - linkedAccounts: ( - - Linked accounts - - ), - }} - /> - ) : ( - Profile - ), - linkedAccounts: ( - - Linked accounts - - ), - }} - /> - )} -

- {settings.currentSettings.mediaServerType === MediaServerType.PLEX && ( -
- - -
- )} - { - 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' }); - } - }} - > + + + + !isSubmitting && setModalOpen(false)} + onOk={handleSwitch} + okText={intl.formatMessage(messages.switchMediaServerButton)} + okButtonType="danger" + cancelText={intl.formatMessage(globalMessages.cancel)} + loading={isSubmitting} + okDisabled={isSubmitting} + > +

+ {isPlex ? ( + + ) : ( + + )} +

+
+ +
+
+ + +
+
+
); }; diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 2ae48e88d..bb04f8af5 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -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 Enable Local Sign-In 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 = () => { + {(settings.currentSettings.mediaServerType === MediaServerType.PLEX || + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN || + settings.currentSettings.mediaServerType === MediaServerType.EMBY) && ( +

+ + + + ), + }} + /> +

+ )} diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index b3f5fd211..507662f58 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -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" -} +} \ No newline at end of file