From 15cb949f1f2e617853f90ae7bb8ae5d6622f610e Mon Sep 17 00:00:00 2001 From: Fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Wed, 21 Aug 2024 02:35:47 +0500 Subject: [PATCH] feat: Jellyfin/Emby server type setup (#685) * feat: add Media Server Selection to Setup Page Introduce the ability to select the media server type on the setup page. Users can now choose their preferred media server (e.g., Plex through the Plex sign-in or Emby/Jellyfin sign-in to select either Emby or Jellyfin). The selected media server type is then reflected in the application settings. This enhancement provides users with increased flexibility and customization options during the initial setup process, eliminating the need to rely on environment variables (which cannot be set if using platforms like snaps). Existing Emby users, who use the environment variable, should log out and log back in after updating to set their mediaServerType to Emby. BREAKING CHANGE: This commit deprecates the JELLYFIN_TYPE variable to identify Emby media server and instead rely on the mediaServerType that is set in the `settings.json`. Existing environment variable users can log out and log back in to set the mediaServerType to `3` (Emby). * feat(api): add severType to the api BREAKING CHANGE: This adds a serverType to the `/auth/jellyfin` which requires a serverType to be set (`jellyfin`/`emby`) * refactor: use enums for serverType and rename selectedservice to serverType * refactor(auth): jellyfin/emby authentication to set MediaServerType * fix: issue page formatMessage for 4k media * refactor: cleaner way of handling serverType change using MediaServerType instead of strings instead of using strings now it will use MediaServerType enums for serverType * revert: removed conditional render of the auto-request permission reverts the conditional render toshow the auto-request permission if the mediaServerType was set to Plex as this should be handled in a different PR and Cypress tests should be modified accordingly(currently cypress test would fail if this conditional check is there) * feat: add server type step to setup * feat: migrate existing emby setups to use emby mediaServerType * fix: scan jobs not running when media server type is emby * fix: emby media server type migration * refactor: change emby logo to full logo * style: decrease emby logo size in setup screen * refactor: use title case for servertype i18n message * refactor(i18n): fix a typo * refactor: use enums instead of numbers * fix: remove old references to JELLYFIN_TYPE environment variable * fix: go back to the last step when refresh the setup page * fix: move "scanning in background" tip next to the scanning section * fix: redirect the setup page when Jellyseerr is already setup --------- Co-authored-by: Gauthier --- next.config.js | 4 - overseerr-api.yml | 2 + server/constants/server.ts | 5 + server/constants/user.ts | 1 + server/entity/Media.ts | 5 +- server/lib/scanners/jellyfin/index.ts | 5 +- .../migrations/0002_emby_media_server_type.ts | 17 ++ server/routes/auth.ts | 92 +++++---- src/assets/services/emby-icon-only.svg | 47 +++++ src/assets/services/emby.svg | 177 +++++++++++++----- src/components/ExternalLinkBlock/index.tsx | 5 +- src/components/IssueDetails/index.tsx | 14 +- src/components/Login/JellyfinLogin.tsx | 65 ++++--- src/components/Login/index.tsx | 21 ++- src/components/ManageSlideOver/index.tsx | 5 +- src/components/MovieDetails/index.tsx | 8 +- src/components/Settings/SettingsJellyfin.tsx | 122 ++++++------ .../Settings/SettingsJobsCache/index.tsx | 17 +- src/components/Settings/SettingsLayout.tsx | 8 +- .../Settings/SettingsUsers/index.tsx | 51 +++-- src/components/Setup/SetupLogin.tsx | 128 ++++++------- src/components/Setup/index.tsx | 134 ++++++++++--- src/components/StatusBadge/index.tsx | 4 +- src/components/TvDetails/index.tsx | 10 +- .../UserList/JellyfinImportModal.tsx | 22 ++- src/components/UserList/index.tsx | 7 +- .../UserGeneralSettings/index.tsx | 4 +- src/i18n/locale/en.json | 14 +- src/pages/settings/jellyfin.tsx | 2 +- src/styles/globals.css | 10 + 30 files changed, 663 insertions(+), 343 deletions(-) create mode 100644 server/lib/settings/migrations/0002_emby_media_server_type.ts create mode 100644 src/assets/services/emby-icon-only.svg diff --git a/next.config.js b/next.config.js index 380eb897c..35a316c63 100644 --- a/next.config.js +++ b/next.config.js @@ -6,10 +6,6 @@ module.exports = { commitTag: process.env.COMMIT_TAG || 'local', forceIpv4First: process.env.FORCE_IPV4_FIRST === 'true' ? 'true' : 'false', }, - publicRuntimeConfig: { - // Will be available on both server and client - JELLYFIN_TYPE: process.env.JELLYFIN_TYPE, - }, images: { remotePatterns: [ { hostname: 'gravatar.com' }, diff --git a/overseerr-api.yml b/overseerr-api.yml index 8f916708c..d24035380 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -3586,6 +3586,8 @@ paths: type: string email: type: string + serverType: + type: number required: - username - password diff --git a/server/constants/server.ts b/server/constants/server.ts index 7b2f9f1ff..eed1939f6 100644 --- a/server/constants/server.ts +++ b/server/constants/server.ts @@ -4,3 +4,8 @@ export enum MediaServerType { EMBY, NOT_CONFIGURED, } + +export enum ServerType { + JELLYFIN = 'Jellyfin', + EMBY = 'Emby', +} diff --git a/server/constants/user.ts b/server/constants/user.ts index 5a0a4bd56..90b33dc8d 100644 --- a/server/constants/user.ts +++ b/server/constants/user.ts @@ -2,4 +2,5 @@ export enum UserType { PLEX = 1, LOCAL = 2, JELLYFIN = 3, + EMBY = 4, } diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 102185be1..723eb213d 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -211,9 +211,10 @@ class Media { } } else { const pageName = - process.env.JELLYFIN_TYPE === 'emby' ? 'item' : 'details'; + getSettings().main.mediaServerType == MediaServerType.EMBY + ? 'item' + : 'details'; const { serverId, externalHostname } = getSettings().jellyfin; - const jellyfinHost = externalHostname && externalHostname.length > 0 ? externalHostname diff --git a/server/lib/scanners/jellyfin/index.ts b/server/lib/scanners/jellyfin/index.ts index 4ccf54850..f48de70ef 100644 --- a/server/lib/scanners/jellyfin/index.ts +++ b/server/lib/scanners/jellyfin/index.ts @@ -567,7 +567,10 @@ class JellyfinScanner { public async run(): Promise { const settings = getSettings(); - if (settings.main.mediaServerType != MediaServerType.JELLYFIN) { + if ( + settings.main.mediaServerType != MediaServerType.JELLYFIN && + settings.main.mediaServerType != MediaServerType.EMBY + ) { return; } diff --git a/server/lib/settings/migrations/0002_emby_media_server_type.ts b/server/lib/settings/migrations/0002_emby_media_server_type.ts new file mode 100644 index 000000000..2bfd2cda9 --- /dev/null +++ b/server/lib/settings/migrations/0002_emby_media_server_type.ts @@ -0,0 +1,17 @@ +import { MediaServerType } from '@server/constants/server'; +import type { AllSettings } from '@server/lib/settings'; + +const migrateHostname = (settings: any): AllSettings => { + const oldMediaServerType = settings.main.mediaServerType; + console.log('Migrating media server type', oldMediaServerType); + if ( + oldMediaServerType === MediaServerType.JELLYFIN && + process.env.JELLYFIN_TYPE === 'emby' + ) { + settings.main.mediaServerType = MediaServerType.EMBY; + } + + return settings; +}; + +export default migrateHostname; diff --git a/server/routes/auth.ts b/server/routes/auth.ts index 6f01135de..cd931c254 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -1,7 +1,7 @@ import JellyfinAPI from '@server/api/jellyfin'; import PlexTvAPI from '@server/api/plextv'; import { ApiErrorCode } from '@server/constants/error'; -import { MediaServerType } from '@server/constants/server'; +import { MediaServerType, ServerType } from '@server/constants/server'; import { UserType } from '@server/constants/user'; import { getRepository } from '@server/datasource'; import { User } from '@server/entity/User'; @@ -227,15 +227,20 @@ authRoutes.post('/jellyfin', async (req, res, next) => { urlBase?: string; useSsl?: boolean; email?: string; + serverType?: number; }; - //Make sure jellyfin login is enabled, but only if jellyfin is not already configured + //Make sure jellyfin login is enabled, but only if jellyfin && Emby is not already configured if ( settings.main.mediaServerType !== MediaServerType.JELLYFIN && - settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED + settings.main.mediaServerType !== MediaServerType.EMBY && + settings.main.mediaServerType != MediaServerType.NOT_CONFIGURED && + settings.jellyfin.ip !== '' ) { return res.status(500).json({ error: 'Jellyfin login is disabled' }); - } else if (!body.username) { + } + + if (!body.username) { return res.status(500).json({ error: 'You must provide an username' }); } else if (settings.jellyfin.ip !== '' && body.hostname) { return res @@ -273,7 +278,8 @@ authRoutes.post('/jellyfin', async (req, res, next) => { } // First we need to attempt to log the user in to jellyfin - const jellyfinserver = new JellyfinAPI(hostname, undefined, deviceId); + const jellyfinserver = new JellyfinAPI(hostname ?? '', undefined, deviceId); + const jellyfinHost = externalHostname && externalHostname.length > 0 ? externalHostname @@ -317,22 +323,47 @@ authRoutes.post('/jellyfin', async (req, res, next) => { ); // User doesn't exist, and there are no users in the database, we'll create the user - // with admin permission - settings.main.mediaServerType = MediaServerType.JELLYFIN; - user = new User({ - email: body.email || account.User.Name, - jellyfinUsername: account.User.Name, - jellyfinUserId: account.User.Id, - jellyfinDeviceId: deviceId, - permissions: Permission.ADMIN, - avatar: account.User.PrimaryImageTag - ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` - : gravatarUrl(body.email || account.User.Name, { - default: 'mm', - size: 200, - }), - userType: UserType.JELLYFIN, - }); + // with admin permissions + switch (body.serverType) { + case MediaServerType.EMBY: + settings.main.mediaServerType = MediaServerType.EMBY; + user = new User({ + email: body.email || account.User.Name, + jellyfinUsername: account.User.Name, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, + jellyfinAuthToken: account.AccessToken, + permissions: Permission.ADMIN, + avatar: account.User.PrimaryImageTag + ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + : gravatarUrl(body.email || account.User.Name, { + default: 'mm', + size: 200, + }), + userType: UserType.EMBY, + }); + break; + case MediaServerType.JELLYFIN: + settings.main.mediaServerType = MediaServerType.JELLYFIN; + user = new User({ + email: body.email || account.User.Name, + jellyfinUsername: account.User.Name, + jellyfinUserId: account.User.Id, + jellyfinDeviceId: deviceId, + jellyfinAuthToken: account.AccessToken, + permissions: Permission.ADMIN, + avatar: account.User.PrimaryImageTag + ? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90` + : gravatarUrl(body.email || account.User.Name, { + default: 'mm', + size: 200, + }), + userType: UserType.JELLYFIN, + }); + break; + default: + throw new Error('select_server_type'); + } // Create an API key on Jellyfin from this admin user const jellyfinClient = new JellyfinAPI( @@ -361,12 +392,12 @@ authRoutes.post('/jellyfin', async (req, res, next) => { logger.info( `Found matching ${ settings.main.mediaServerType === MediaServerType.JELLYFIN - ? 'Jellyfin' - : 'Emby' + ? ServerType.JELLYFIN + : ServerType.EMBY } user; updating user with ${ settings.main.mediaServerType === MediaServerType.JELLYFIN - ? 'Jellyfin' - : 'Emby' + ? ServerType.JELLYFIN + : ServerType.EMBY }`, { label: 'API', @@ -389,12 +420,6 @@ authRoutes.post('/jellyfin', async (req, res, next) => { user.username = ''; } - // TODO: If JELLYFIN_TYPE is set to 'emby' then set mediaServerType to EMBY - // if (process.env.JELLYFIN_TYPE === 'emby') { - // settings.main.mediaServerType = MediaServerType.EMBY; - // settings.save(); - // } - await userRepository.save(user); } else if (!settings.main.newPlexLogin) { logger.warn( @@ -432,7 +457,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => { default: 'mm', size: 200, }), - userType: UserType.JELLYFIN, + userType: + settings.main.mediaServerType === MediaServerType.JELLYFIN + ? UserType.JELLYFIN + : UserType.EMBY, }); //initialize Jellyfin/Emby users with local login const passedExplicitPassword = body.password && body.password.length > 0; diff --git a/src/assets/services/emby-icon-only.svg b/src/assets/services/emby-icon-only.svg new file mode 100644 index 000000000..e2f2cf2e4 --- /dev/null +++ b/src/assets/services/emby-icon-only.svg @@ -0,0 +1,47 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/assets/services/emby.svg b/src/assets/services/emby.svg index eddc540cd..2aac8662e 100644 --- a/src/assets/services/emby.svg +++ b/src/assets/services/emby.svg @@ -1,46 +1,131 @@ - - - - - - - image/svg+xml - - - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/src/components/ExternalLinkBlock/index.tsx b/src/components/ExternalLinkBlock/index.tsx index 9782d186c..9199da7d0 100644 --- a/src/components/ExternalLinkBlock/index.tsx +++ b/src/components/ExternalLinkBlock/index.tsx @@ -11,7 +11,6 @@ import useLocale from '@app/hooks/useLocale'; import useSettings from '@app/hooks/useSettings'; import { MediaType } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; -import getConfig from 'next/config'; interface ExternalLinkBlockProps { mediaType: 'movie' | 'tv'; @@ -31,7 +30,6 @@ const ExternalLinkBlock = ({ mediaUrl, }: ExternalLinkBlockProps) => { const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { locale } = useLocale(); return ( @@ -45,7 +43,8 @@ const ExternalLinkBlock = ({ > {settings.currentSettings.mediaServerType === MediaServerType.PLEX ? ( - ) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( + ) : settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? ( ) : ( diff --git a/src/components/IssueDetails/index.tsx b/src/components/IssueDetails/index.tsx index a5ec6391a..ca0337e59 100644 --- a/src/components/IssueDetails/index.tsx +++ b/src/components/IssueDetails/index.tsx @@ -28,7 +28,6 @@ import type Issue from '@server/entity/Issue'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -108,7 +107,6 @@ const IssueDetails = () => { (opt) => opt.issueType === issueData?.issueType ); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); if (!data && !error) { return ; @@ -390,7 +388,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Emby', }) @@ -437,7 +436,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.play4konplex, { mediaServerName: 'Emby', }) @@ -662,7 +662,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.playonplex, { mediaServerName: 'Emby', }) @@ -708,7 +709,8 @@ const IssueDetails = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.play4konplex, { mediaServerName: 'Emby', }) diff --git a/src/components/Login/JellyfinLogin.tsx b/src/components/Login/JellyfinLogin.tsx index dd08e53d3..ba59d11b1 100644 --- a/src/components/Login/JellyfinLogin.tsx +++ b/src/components/Login/JellyfinLogin.tsx @@ -4,9 +4,9 @@ import useSettings from '@app/hooks/useSettings'; import defineMessages from '@app/utils/defineMessages'; import { InformationCircleIcon } from '@heroicons/react/24/solid'; import { ApiErrorCode } from '@server/constants/error'; +import { MediaServerType, ServerType } from '@server/constants/server'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; -import { useIntl } from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import * as Yup from 'yup'; @@ -26,6 +26,7 @@ const messages = defineMessages('components.Login', { validationemailformat: 'Valid email required', validationusernamerequired: 'Username required', validationpasswordrequired: 'Password required', + validationservertyperequired: 'Please select a server type', validationHostnameRequired: 'You must provide a valid hostname or IP address', validationPortRequired: 'You must provide a valid port number', validationUrlTrailingSlash: 'URL must not end in a trailing slash', @@ -40,42 +41,51 @@ const messages = defineMessages('components.Login', { initialsigningin: 'Connecting…', initialsignin: 'Connect', forgotpassword: 'Forgot Password?', + servertype: 'Server Type', + back: 'Go back', }); interface JellyfinLoginProps { revalidate: () => void; initial?: boolean; + serverType?: MediaServerType; + onCancel?: () => void; } const JellyfinLogin: React.FC = ({ revalidate, initial, + serverType, + onCancel, }) => { const toasts = useToasts(); const intl = useIntl(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); + + const mediaServerFormatValues = { + mediaServerName: + serverType === MediaServerType.JELLYFIN + ? ServerType.JELLYFIN + : serverType === MediaServerType.EMBY + ? ServerType.EMBY + : 'Media Server', + }; if (initial) { const LoginSchema = Yup.object().shape({ hostname: Yup.string().required( - intl.formatMessage(messages.validationhostrequired, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', - }) + intl.formatMessage( + messages.validationhostrequired, + mediaServerFormatValues + ) ), port: Yup.number().required( intl.formatMessage(messages.validationPortRequired) ), - urlBase: Yup.string() - .matches( - /^(\/[^/].*[^/]$)/, - intl.formatMessage(messages.validationUrlBaseLeadingSlash) - ) - .matches( - /^(.*[^/])$/, - intl.formatMessage(messages.validationUrlBaseTrailingSlash) - ), + urlBase: Yup.string().matches( + /^(.*[^/])$/, + intl.formatMessage(messages.validationUrlBaseTrailingSlash) + ), email: Yup.string() .email(intl.formatMessage(messages.validationemailformat)) .required(intl.formatMessage(messages.validationemailrequired)), @@ -85,11 +95,6 @@ const JellyfinLogin: React.FC = ({ password: Yup.string(), }); - const mediaServerFormatValues = { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', - }; - return ( = ({ validationSchema={LoginSchema} onSubmit={async (values) => { try { + // Check if serverType is either 'Jellyfin' or 'Emby' + // if (serverType !== 'Jellyfin' && serverType !== 'Emby') { + // throw new Error('Invalid serverType'); // You can customize the error message + // } + const res = await fetch('/api/v1/auth/jellyfin', { method: 'POST', headers: { @@ -117,6 +127,7 @@ const JellyfinLogin: React.FC = ({ useSsl: values.useSsl, urlBase: values.urlBase, email: values.email, + serverType: serverType, }), }); if (!res.ok) throw new Error(res.statusText, { cause: res }); @@ -312,7 +323,7 @@ const JellyfinLogin: React.FC = ({
-
+
+ {onCancel && ( + + + + )}
@@ -429,7 +447,8 @@ const JellyfinLogin: React.FC = ({ jellyfinForgotPasswordUrl ? `${jellyfinForgotPasswordUrl}` : `${baseUrl}/web/index.html#!/${ - process.env.JELLYFIN_TYPE === 'emby' + settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? 'startup/' : '' }forgotpassword.html` diff --git a/src/components/Login/index.tsx b/src/components/Login/index.tsx index eca7b6acf..7b95b9fcd 100644 --- a/src/components/Login/index.tsx +++ b/src/components/Login/index.tsx @@ -10,7 +10,6 @@ import defineMessages from '@app/utils/defineMessages'; import { Transition } from '@headlessui/react'; import { XCircleIcon } from '@heroicons/react/24/solid'; import { MediaServerType } from '@server/constants/server'; -import getConfig from 'next/config'; import { useRouter } from 'next/dist/client/router'; import Image from 'next/image'; import { useEffect, useState } from 'react'; @@ -34,7 +33,6 @@ const Login = () => { const { user, revalidate } = useUser(); const router = useRouter(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); // Effect that is triggered when the `authToken` comes back from the Plex OAuth // We take the token and attempt to sign in. If we get a success message, we will @@ -88,6 +86,15 @@ const Login = () => { revalidateOnFocus: false, }); + const mediaServerFormatValues = { + mediaServerName: + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN + ? 'Jellyfin' + : settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : undefined, + }; + return (
@@ -154,12 +161,10 @@ const Login = () => { {settings.currentSettings.mediaServerType == MediaServerType.PLEX ? intl.formatMessage(messages.signinwithplex) - : intl.formatMessage(messages.signinwithjellyfin, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - })} + : intl.formatMessage( + messages.signinwithjellyfin, + mediaServerFormatValues + )}
diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index 35c8bc1c9..b669ebb43 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -26,7 +26,6 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa import type { RadarrSettings, SonarrSettings } from '@server/lib/settings'; import type { MovieDetails } from '@server/models/Movie'; import type { TvDetails } from '@server/models/Tv'; -import getConfig from 'next/config'; import Image from 'next/image'; import Link from 'next/link'; import { useIntl } from 'react-intl'; @@ -95,7 +94,6 @@ const ManageSlideOver = ({ const { user: currentUser, hasPermission } = useUser(); const intl = useIntl(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { data: watchData } = useSWR( settings.currentSettings.mediaServerType === MediaServerType.PLEX && data.mediaInfo && @@ -661,7 +659,8 @@ const ManageSlideOver = ({ mediaType === 'movie' ? messages.movie : messages.tvshow ), mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? 'Emby' : settings.currentSettings.mediaServerType === MediaServerType.PLEX diff --git a/src/components/MovieDetails/index.tsx b/src/components/MovieDetails/index.tsx index b18d506ce..e4bc991ef 100644 --- a/src/components/MovieDetails/index.tsx +++ b/src/components/MovieDetails/index.tsx @@ -53,7 +53,6 @@ import type { MovieDetails as MovieDetailsType } from '@server/models/Movie'; import { countries } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; import { uniqBy } from 'lodash'; -import getConfig from 'next/config'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; @@ -126,7 +125,6 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { const [toggleWatchlist, setToggleWatchlist] = useState( !movie?.onUserWatchlist ); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { @@ -279,7 +277,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { ?.flatrate ?? []; function getAvalaibleMediaServerName() { - if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } @@ -291,8 +289,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => { } function getAvalaible4kMediaServerName() { - if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { - return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' }); + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { + return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) { diff --git a/src/components/Settings/SettingsJellyfin.tsx b/src/components/Settings/SettingsJellyfin.tsx index a627f6d31..316dc48ef 100644 --- a/src/components/Settings/SettingsJellyfin.tsx +++ b/src/components/Settings/SettingsJellyfin.tsx @@ -3,13 +3,14 @@ import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import SensitiveInput from '@app/components/Common/SensitiveInput'; import LibraryItem from '@app/components/Settings/LibraryItem'; +import useSettings from '@app/hooks/useSettings'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ApiErrorCode } from '@server/constants/error'; +import { MediaServerType } from '@server/constants/server'; import type { JellyfinSettings } from '@server/lib/settings'; import { Field, Formik } from 'formik'; -import getConfig from 'next/config'; import { useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; @@ -61,6 +62,9 @@ const messages = defineMessages('components.Settings', { validationUrlTrailingSlash: 'URL must not end in a trailing slash', validationUrlBaseLeadingSlash: 'URL base must have a leading slash', validationUrlBaseTrailingSlash: 'URL base must not end in a trailing slash', + tip: 'Tip', + scanbackground: + 'Scanning will run in the background. You can continue the setup process in the meantime.', }); interface Library { @@ -78,13 +82,13 @@ interface SyncStatus { } interface SettingsJellyfinProps { - showAdvancedSettings?: boolean; + isSetupSettings?: boolean; onComplete?: () => void; } const SettingsJellyfin: React.FC = ({ onComplete, - showAdvancedSettings, + isSetupSettings, }) => { const [isSyncing, setIsSyncing] = useState(false); const toasts = useToasts(); @@ -102,7 +106,7 @@ const SettingsJellyfin: React.FC = ({ ); const intl = useIntl(); const { addToast } = useToasts(); - const { publicRuntimeConfig } = getConfig(); + const settings = useSettings(); const JellyfinSettingsSchema = Yup.object().shape({ hostname: Yup.string() @@ -284,26 +288,29 @@ const SettingsJellyfin: React.FC = ({ return ; } + const mediaServerFormatValues = { + mediaServerName: + settings.currentSettings.mediaServerType === MediaServerType.JELLYFIN + ? 'Jellyfin' + : settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : undefined, + }; + return ( <>

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.jellyfinlibraries, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.jellyfinlibraries, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.jellyfinlibraries, + mediaServerFormatValues + )}

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.jellyfinlibrariesDescription, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.jellyfinlibrariesDescription, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.jellyfinlibrariesDescription, + mediaServerFormatValues + )}

@@ -340,13 +347,10 @@ const SettingsJellyfin: React.FC = ({

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.manualscanDescriptionJellyfin, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.manualscanDescriptionJellyfin, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.manualscanDescriptionJellyfin, + mediaServerFormatValues + )}

@@ -446,24 +450,26 @@ const SettingsJellyfin: React.FC = ({
+ {isSetupSettings && ( +
+ + {intl.formatMessage(messages.tip)} + + {intl.formatMessage(messages.scanbackground)} +
+ )}

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.jellyfinSettings, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.jellyfinSettings, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.jellyfinSettings, + mediaServerFormatValues + )}

- {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? intl.formatMessage(messages.jellyfinSettingsDescription, { - mediaServerName: 'Emby', - }) - : intl.formatMessage(messages.jellyfinSettingsDescription, { - mediaServerName: 'Jellyfin', - })} + {intl.formatMessage( + messages.jellyfinSettingsDescription, + mediaServerFormatValues + )}

= ({ if (!res.ok) throw new Error(res.statusText, { cause: res }); addToast( - intl.formatMessage(messages.jellyfinSettingsSuccess, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), + intl.formatMessage( + messages.jellyfinSettingsSuccess, + mediaServerFormatValues + ), { autoDismiss: true, appearance: 'success', @@ -518,12 +522,10 @@ const SettingsJellyfin: React.FC = ({ } if (errorData?.message === ApiErrorCode.InvalidUrl) { addToast( - intl.formatMessage(messages.invalidurlerror, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), + intl.formatMessage( + messages.invalidurlerror, + mediaServerFormatValues + ), { autoDismiss: true, appearance: 'error', @@ -531,12 +533,10 @@ const SettingsJellyfin: React.FC = ({ ); } else { addToast( - intl.formatMessage(messages.jellyfinSettingsFailure, { - mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' - ? 'Emby' - : 'Jellyfin', - }), + intl.formatMessage( + messages.jellyfinSettingsFailure, + mediaServerFormatValues + ), { autoDismiss: true, appearance: 'error', @@ -559,7 +559,7 @@ const SettingsJellyfin: React.FC = ({ }) => { return (
- {showAdvancedSettings && ( + {!isSetupSettings && ( <>
- {showAdvancedSettings && ( + {!isSetupSettings && ( <>
-
+
- -
- { - setMediaServerType(MediaServerType.PLEX); - setAuthToken(authToken); - }} - /> -
-
-
- - -
- -
-
-
- + {serverType === MediaServerType.JELLYFIN ? ( + + ) : serverType === MediaServerType.EMBY ? ( + + ) : ( + )} - +
+ {serverType === MediaServerType.PLEX && ( + <> +
+ { + setMediaServerType(MediaServerType.PLEX); + setAuthToken(authToken); + }} + /> +
+
+ +
+ + )} + {serverType === MediaServerType.JELLYFIN && ( + + )} + {serverType === MediaServerType.EMBY && ( + + )}
); }; diff --git a/src/components/Setup/index.tsx b/src/components/Setup/index.tsx index 6ec227c95..936114671 100644 --- a/src/components/Setup/index.tsx +++ b/src/components/Setup/index.tsx @@ -1,5 +1,7 @@ +import EmbyLogo from '@app/assets/services/emby.svg'; +import JellyfinLogo from '@app/assets/services/jellyfin.svg'; +import PlexLogo from '@app/assets/services/plex.svg'; import AppDataWarning from '@app/components/AppDataWarning'; -import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import ImageFader from '@app/components/Common/ImageFader'; import PageTitle from '@app/components/Common/PageTitle'; @@ -9,26 +11,30 @@ import SettingsPlex from '@app/components/Settings/SettingsPlex'; import SettingsServices from '@app/components/Settings/SettingsServices'; import SetupSteps from '@app/components/Setup/SetupSteps'; import useLocale from '@app/hooks/useLocale'; +import useSettings from '@app/hooks/useSettings'; import defineMessages from '@app/utils/defineMessages'; import { MediaServerType } from '@server/constants/server'; import Image from 'next/image'; import { useRouter } from 'next/router'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import useSWR, { mutate } from 'swr'; import SetupLogin from './SetupLogin'; const messages = defineMessages('components.Setup', { + welcome: 'Welcome to Jellyseerr', + subtitle: 'Get started by choosing your media server', + configjellyfin: 'Configure Jellyfin', + configplex: 'Configure Plex', + configemby: 'Configure Emby', setup: 'Setup', finish: 'Finish Setup', finishing: 'Finishing…', continue: 'Continue', + servertype: 'Choose Server Type', signin: 'Sign In', configuremediaserver: 'Configure Media Server', configureservices: 'Configure Services', - tip: 'Tip', - scanbackground: - 'Scanning will run in the background. You can continue the setup process in the meantime.', }); const Setup = () => { @@ -42,6 +48,7 @@ const Setup = () => { ); const router = useRouter(); const { locale } = useLocale(); + const settings = useSettings(); const finishSetup = async () => { setIsUpdating(true); @@ -76,6 +83,23 @@ const Setup = () => { revalidateOnFocus: false, }); + useEffect(() => { + if (settings.currentSettings.initialized) { + router.push('/'); + } + if ( + settings.currentSettings.mediaServerType !== + MediaServerType.NOT_CONFIGURED + ) { + setCurrentStep(3); + setMediaServerType(settings.currentSettings.mediaServerType); + } + }, [ + settings.currentSettings.mediaServerType, + settings.currentSettings.initialized, + router, + ]); + return (
@@ -101,58 +125,120 @@ const Setup = () => { > 1} /> 2} /> 3} + /> +
{currentStep === 1 && ( - { - setMediaServerType(mServerType); - setCurrentStep(2); - }} - /> +
+
+ {intl.formatMessage(messages.welcome)} +
+
+ {intl.formatMessage(messages.subtitle)} +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
)} {currentStep === 2 && ( -
+ { + setMediaServerType(MediaServerType.NOT_CONFIGURED); + setCurrentStep(1); + }} + onComplete={() => setCurrentStep(3)} + /> + )} + {currentStep === 3 && ( +
{mediaServerType === MediaServerType.PLEX ? ( setMediaServerSettingsComplete(true)} /> ) : ( setMediaServerSettingsComplete(true)} /> )} -
- - {intl.formatMessage(messages.tip)} - - {intl.formatMessage(messages.scanbackground)} -
@@ -161,7 +247,7 @@ const Setup = () => {
)} - {currentStep === 3 && ( + {currentStep === 4 && (
diff --git a/src/components/StatusBadge/index.tsx b/src/components/StatusBadge/index.tsx index 0061a903a..1d280d289 100644 --- a/src/components/StatusBadge/index.tsx +++ b/src/components/StatusBadge/index.tsx @@ -9,7 +9,6 @@ import defineMessages from '@app/utils/defineMessages'; import { MediaStatus } from '@server/constants/media'; import { MediaServerType } from '@server/constants/server'; import type { DownloadingItem } from '@server/lib/downloadtracker'; -import getConfig from 'next/config'; import { useIntl } from 'react-intl'; const messages = defineMessages('components.StatusBadge', { @@ -48,7 +47,6 @@ const StatusBadge = ({ const intl = useIntl(); const { hasPermission } = useUser(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); let mediaLink: string | undefined; let mediaLinkDescription: string | undefined; @@ -86,7 +84,7 @@ const StatusBadge = ({ mediaLink = plexUrl; mediaLinkDescription = intl.formatMessage(messages.playonplex, { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + settings.currentSettings.mediaServerType === MediaServerType.EMBY ? 'Emby' : settings.currentSettings.mediaServerType === MediaServerType.PLEX ? 'Plex' diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index af253f589..634c72d05 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -59,7 +59,6 @@ import type { Crew } from '@server/models/common'; import type { TvDetails as TvDetailsType } from '@server/models/Tv'; import { countries } from 'country-flag-icons'; import 'country-flag-icons/3x2/flags.css'; -import getConfig from 'next/config'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; @@ -126,7 +125,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => { const [toggleWatchlist, setToggleWatchlist] = useState( !tv?.onUserWatchlist ); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { @@ -300,7 +298,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { ?.flatrate ?? []; function getAvalaibleMediaServerName() { - if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } @@ -312,15 +310,15 @@ const TvDetails = ({ tv }: TvDetailsProps) => { } function getAvalaible4kMediaServerName() { - if (publicRuntimeConfig.JELLYFIN_TYPE === 'emby') { - return intl.formatMessage(messages.play4k, { mediaServerName: 'Emby' }); + if (settings.currentSettings.mediaServerType === MediaServerType.EMBY) { + return intl.formatMessage(messages.play, { mediaServerName: 'Emby' }); } if (settings.currentSettings.mediaServerType === MediaServerType.PLEX) { return intl.formatMessage(messages.play4k, { mediaServerName: 'Plex' }); } - return intl.formatMessage(messages.play4k, { mediaServerName: 'Jellyfin' }); + return intl.formatMessage(messages.play, { mediaServerName: 'Jellyfin' }); } const onClickWatchlistBtn = async (): Promise => { diff --git a/src/components/UserList/JellyfinImportModal.tsx b/src/components/UserList/JellyfinImportModal.tsx index 64ca18616..36dbe0aaa 100644 --- a/src/components/UserList/JellyfinImportModal.tsx +++ b/src/components/UserList/JellyfinImportModal.tsx @@ -3,8 +3,8 @@ 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 { MediaServerType } from '@server/constants/server'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; -import getConfig from 'next/config'; import Image from 'next/image'; import { useState } from 'react'; import { useIntl } from 'react-intl'; @@ -36,7 +36,6 @@ const JellyfinImportModal: React.FC = ({ }) => { const intl = useIntl(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const [isImporting, setImporting] = useState(false); const [selectedUsers, setSelectedUsers] = useState([]); @@ -81,7 +80,9 @@ const JellyfinImportModal: React.FC = ({ userCount: createdUsers.length, strong: (msg: React.ReactNode) => {msg}, mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : 'Jellyfin', }), { autoDismiss: true, @@ -96,7 +97,9 @@ const JellyfinImportModal: React.FC = ({ addToast( intl.formatMessage(messages.importfromJellyfinerror, { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : 'Jellyfin', }), { autoDismiss: true, @@ -134,7 +137,9 @@ const JellyfinImportModal: React.FC = ({ loading={!data && !error} title={intl.formatMessage(messages.importfromJellyfin, { mediaServerName: - publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? 'Emby' : 'Jellyfin', + settings.currentSettings.mediaServerType === MediaServerType.EMBY + ? 'Emby' + : 'Jellyfin', })} onOk={() => { importUsers(); @@ -151,7 +156,8 @@ const JellyfinImportModal: React.FC = ({ ( @@ -277,7 +283,9 @@ const JellyfinImportModal: React.FC = ({ diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index 66df469be..7a91a1036 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -28,7 +28,6 @@ import { MediaServerType } from '@server/constants/server'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; import { hasPermission } from '@server/lib/permissions'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import Image from 'next/image'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -90,7 +89,6 @@ const UserList = () => { const intl = useIntl(); const router = useRouter(); const settings = useSettings(); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { user: currentUser, hasPermission: currentHasPermission } = useUser(); const [currentSort, setCurrentSort] = useState('displayname'); @@ -535,7 +533,8 @@ const UserList = () => { > - {publicRuntimeConfig.JELLYFIN_TYPE == 'emby' + {settings.currentSettings.mediaServerType === + MediaServerType.EMBY ? intl.formatMessage(messages.importfrommediaserver, { mediaServerName: 'Emby', }) @@ -690,7 +689,7 @@ const UserList = () => { {intl.formatMessage(messages.localuser)} - ) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( + ) : user.userType === UserType.EMBY ? ( {intl.formatMessage(messages.mediaServerUser, { mediaServerName: 'Emby', diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index 3bcf1a049..15d960714 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -16,7 +16,6 @@ import defineMessages from '@app/utils/defineMessages'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; import { Field, Form, Formik } from 'formik'; -import getConfig from 'next/config'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -69,7 +68,6 @@ const messages = defineMessages( const UserGeneralSettings = () => { const intl = useIntl(); - const { publicRuntimeConfig } = getConfig(); const { addToast } = useToasts(); const { locale, setLocale } = useLocale(); const [movieQuotaEnabled, setMovieQuotaEnabled] = useState(false); @@ -229,7 +227,7 @@ const UserGeneralSettings = () => { {intl.formatMessage(messages.localuser)} - ) : publicRuntimeConfig.JELLYFIN_TYPE == 'emby' ? ( + ) : user?.userType === UserType.EMBY ? ( {intl.formatMessage(messages.mediaServerUser, { mediaServerName: 'Emby', diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 0c07003f0..cf66b67e9 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -221,6 +221,7 @@ "components.Layout.VersionStatus.streamdevelop": "Jellyseerr Develop", "components.Layout.VersionStatus.streamstable": "Jellyseerr Stable", "components.Login.adminerror": "You must use an admin account to sign in.", + "components.Login.back": "Go back", "components.Login.credentialerror": "The username or password is incorrect.", "components.Login.description": "Since this is your first time logging into {applicationName}, you are required to add a valid email address.", "components.Login.email": "Email Address", @@ -236,6 +237,7 @@ "components.Login.port": "Port", "components.Login.save": "Add", "components.Login.saving": "Adding…", + "components.Login.servertype": "Server Type", "components.Login.signin": "Sign In", "components.Login.signingin": "Signing In…", "components.Login.signinheader": "Sign in to continue", @@ -257,6 +259,7 @@ "components.Login.validationhostformat": "Valid URL required", "components.Login.validationhostrequired": "{mediaServerName} URL required", "components.Login.validationpasswordrequired": "You must provide a password", + "components.Login.validationservertyperequired": "Please select a server type", "components.Login.validationusernamerequired": "Username required", "components.ManageSlideOver.alltime": "All Time", "components.ManageSlideOver.downloadstatus": "Downloads", @@ -1047,17 +1050,24 @@ "components.Settings.webAppUrlTip": "Optionally direct users to the web app on your server instead of the \"hosted\" web app", "components.Settings.webhook": "Webhook", "components.Settings.webpush": "Web Push", + "components.Setup.back": "Go back", + "components.Setup.configemby": "Configure Emby", + "components.Setup.configjellyfin": "Configure Jellyfin", + "components.Setup.configplex": "Configure Plex", "components.Setup.configuremediaserver": "Configure Media Server", "components.Setup.configureservices": "Configure Services", "components.Setup.continue": "Continue", "components.Setup.finish": "Finish Setup", "components.Setup.finishing": "Finishing…", "components.Setup.scanbackground": "Scanning will run in the background. You can continue the setup process in the meantime.", + "components.Setup.servertype": "Choose Server Type", "components.Setup.setup": "Setup", "components.Setup.signin": "Sign In", "components.Setup.signinMessage": "Get started by signing in", - "components.Setup.signinWithJellyfin": "Use your {mediaServerName} account", - "components.Setup.signinWithPlex": "Use your Plex account", + "components.Setup.signinWithEmby": "Enter your Emby details", + "components.Setup.signinWithJellyfin": "Enter your Jellyfin details", + "components.Setup.signinWithPlex": "Enter your Plex details", + "components.Setup.subtitle": "Get started by choosing your media server", "components.Setup.tip": "Tip", "components.Setup.welcome": "Welcome to Jellyseerr", "components.StatusBadge.managemedia": "Manage {mediaType}", diff --git a/src/pages/settings/jellyfin.tsx b/src/pages/settings/jellyfin.tsx index 8f1377daf..2490c8635 100644 --- a/src/pages/settings/jellyfin.tsx +++ b/src/pages/settings/jellyfin.tsx @@ -8,7 +8,7 @@ const JellyfinSettingsPage: NextPage = () => { useRouteGuard(Permission.MANAGE_SETTINGS); return ( - + ); }; diff --git a/src/styles/globals.css b/src/styles/globals.css index 66c023d9a..1e99d53df 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -83,6 +83,16 @@ background: #f19a30; } + .server-type-button { + @apply rounded-md border border-gray-500 bg-gray-700 px-4 py-2 text-white transition duration-150 ease-in-out hover:bg-gray-500; + } + .jellyfin-server svg { + @apply h-6 w-6; + } + .emby-server svg { + @apply h-7 w-7; + } + ul.cards-vertical, ul.cards-horizontal { @apply grid gap-4;