From d24b030962e2de2e9beccca8816422eb6aed4ef7 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 15 Sep 2023 10:31:48 -0400 Subject: [PATCH 1/3] Migrate quick connect page to react --- src/apps/experimental/App.tsx | 4 +- .../AppToolbar/menus/AppUserMenu.tsx | 2 +- .../experimental/routes/asyncRoutes/user.ts | 1 + .../experimental/routes/legacyRoutes/user.ts | 6 -- src/apps/stable/App.tsx | 4 +- src/apps/stable/routes/_redirects.ts | 6 ++ src/apps/stable/routes/asyncRoutes/user.ts | 1 + src/apps/stable/routes/legacyRoutes/user.ts | 6 -- src/apps/stable/routes/quickConnect/index.tsx | 91 +++++++++++++++++++ src/components/router/Redirect.tsx | 17 ++++ src/controllers/user/menu/index.js | 2 +- src/controllers/user/quickConnect/helper.js | 17 ---- src/controllers/user/quickConnect/index.html | 15 --- src/controllers/user/quickConnect/index.js | 25 ----- src/elements/InputElement.tsx | 70 +++++++++++--- 15 files changed, 179 insertions(+), 88 deletions(-) create mode 100644 src/apps/stable/routes/_redirects.ts create mode 100644 src/apps/stable/routes/quickConnect/index.tsx create mode 100644 src/components/router/Redirect.tsx delete mode 100644 src/controllers/user/quickConnect/helper.js delete mode 100644 src/controllers/user/quickConnect/index.html delete mode 100644 src/controllers/user/quickConnect/index.js diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx index 0c0778b74e..57ad22eae7 100644 --- a/src/apps/experimental/App.tsx +++ b/src/apps/experimental/App.tsx @@ -9,6 +9,8 @@ import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; import AppLayout from './AppLayout'; import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; +import { REDIRECTS } from 'apps/stable/routes/_redirects'; +import { toRedirectRoute } from 'components/router/Redirect'; const ExperimentalApp = () => { return ( @@ -38,7 +40,7 @@ const ExperimentalApp = () => { {/* Redirects for old paths */} - } /> + {REDIRECTS.map(toRedirectRoute)} ); diff --git a/src/apps/experimental/components/AppToolbar/menus/AppUserMenu.tsx b/src/apps/experimental/components/AppToolbar/menus/AppUserMenu.tsx index 54fdd3ae62..7119a4f504 100644 --- a/src/apps/experimental/components/AppToolbar/menus/AppUserMenu.tsx +++ b/src/apps/experimental/components/AppToolbar/menus/AppUserMenu.tsx @@ -140,7 +140,7 @@ const AppUserMenu: FC = ({ diff --git a/src/apps/experimental/routes/asyncRoutes/user.ts b/src/apps/experimental/routes/asyncRoutes/user.ts index d7ea0dd7d4..023e292edb 100644 --- a/src/apps/experimental/routes/asyncRoutes/user.ts +++ b/src/apps/experimental/routes/asyncRoutes/user.ts @@ -1,6 +1,7 @@ import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute'; export const ASYNC_USER_ROUTES: AsyncRoute[] = [ + { path: 'quickconnect', page: 'quickConnect' }, { path: 'search.html', page: 'search' }, { path: 'userprofile.html', page: 'user/userprofile' }, { path: 'home.html', page: 'home', type: AsyncRouteType.Experimental }, diff --git a/src/apps/experimental/routes/legacyRoutes/user.ts b/src/apps/experimental/routes/legacyRoutes/user.ts index 49afc715f1..965767d915 100644 --- a/src/apps/experimental/routes/legacyRoutes/user.ts +++ b/src/apps/experimental/routes/legacyRoutes/user.ts @@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [ controller: 'user/home/index', view: 'user/home/index.html' } - }, { - path: 'mypreferencesquickconnect.html', - pageProps: { - controller: 'user/quickConnect/index', - view: 'user/quickConnect/index.html' - } }, { path: 'mypreferencesplayback.html', pageProps: { diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx index 73c73f5881..3ad72bcb84 100644 --- a/src/apps/stable/App.tsx +++ b/src/apps/stable/App.tsx @@ -10,6 +10,8 @@ import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; +import { REDIRECTS } from './routes/_redirects'; +import { toRedirectRoute } from 'components/router/Redirect'; const Layout = () => ( <> @@ -53,7 +55,7 @@ const StableApp = () => ( {/* Redirects for old paths */} - } /> + {REDIRECTS.map(toRedirectRoute)} ); diff --git a/src/apps/stable/routes/_redirects.ts b/src/apps/stable/routes/_redirects.ts new file mode 100644 index 0000000000..fb24865d84 --- /dev/null +++ b/src/apps/stable/routes/_redirects.ts @@ -0,0 +1,6 @@ +import type { Redirect } from 'components/router/Redirect'; + +export const REDIRECTS: Redirect[] = [ + { from: 'mypreferencesquickconnect.html', to: '/quickconnect' }, + { from: 'serveractivity.html', to: '/dashboard/activity' } +]; diff --git a/src/apps/stable/routes/asyncRoutes/user.ts b/src/apps/stable/routes/asyncRoutes/user.ts index 6a683323bc..153e310b35 100644 --- a/src/apps/stable/routes/asyncRoutes/user.ts +++ b/src/apps/stable/routes/asyncRoutes/user.ts @@ -1,6 +1,7 @@ import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_USER_ROUTES: AsyncRoute[] = [ + { path: 'quickconnect', page: 'quickConnect' }, { path: 'search.html', page: 'search' }, { path: 'userprofile.html', page: 'user/userprofile' } ]; diff --git a/src/apps/stable/routes/legacyRoutes/user.ts b/src/apps/stable/routes/legacyRoutes/user.ts index f87bd4383f..fa1fa43ad2 100644 --- a/src/apps/stable/routes/legacyRoutes/user.ts +++ b/src/apps/stable/routes/legacyRoutes/user.ts @@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [ controller: 'user/home/index', view: 'user/home/index.html' } - }, { - path: 'mypreferencesquickconnect.html', - pageProps: { - controller: 'user/quickConnect/index', - view: 'user/quickConnect/index.html' - } }, { path: 'mypreferencesplayback.html', pageProps: { diff --git a/src/apps/stable/routes/quickConnect/index.tsx b/src/apps/stable/routes/quickConnect/index.tsx new file mode 100644 index 0000000000..1887b90a1b --- /dev/null +++ b/src/apps/stable/routes/quickConnect/index.tsx @@ -0,0 +1,91 @@ +import { getQuickConnectApi } from '@jellyfin/sdk/lib/utils/api/quick-connect-api'; +import React, { FC, FormEvent, useCallback, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import Page from 'components/Page'; +import globalize from 'scripts/globalize'; +import InputElement from 'elements/InputElement'; +import ButtonElement from 'elements/ButtonElement'; +import toast from 'components/toast/toast'; +import { useApi } from 'hooks/useApi'; + +const QuickConnectPage: FC = () => { + const { api, user } = useApi(); + const [ searchParams ] = useSearchParams(); + // eslint-disable-next-line react-hooks/exhaustive-deps + const initialValue = useMemo(() => searchParams.get('code') ?? '', []); + const [ code, setCode ] = useState(initialValue); + + const onCodeChange = useCallback((value: string) => { + setCode(value); + }, []); + + const onSubmitCode = useCallback((e: FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + + if (!form.checkValidity()) { + toast(globalize.translate('QuickConnectInvalidCode')); + return; + } + + if (!api) { + console.error('[QuickConnect] cannot authorize, missing api instance'); + return; + } + + const userId = searchParams.get('userId') ?? user?.Id; + const normalizedCode = code.replace(/\s/g, ''); + console.log('[QuickConnect] authorizing code %s as user %s', normalizedCode, userId); + + getQuickConnectApi(api) + .authorizeQuickConnect({ + code: normalizedCode, + userId + }) + .then(() => { + toast(globalize.translate('QuickConnectAuthorizeSuccess')); + }) + .catch(() => { + toast(globalize.translate('QuickConnectAuthorizeFail')); + }); + }, [api, code, searchParams, user?.Id]); + + return ( + +
+
+
+

+ {globalize.translate('QuickConnect')} +

+
+ {globalize.translate('QuickConnectDescription')} +
+
+ + +
+
+
+
+ ); +}; + +export default QuickConnectPage; diff --git a/src/components/router/Redirect.tsx b/src/components/router/Redirect.tsx new file mode 100644 index 0000000000..44fd57bbfb --- /dev/null +++ b/src/components/router/Redirect.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Navigate, Route } from 'react-router-dom'; + +export interface Redirect { + from: string + to: string +} + +export function toRedirectRoute({ from, to }: Redirect) { + return ( + } + /> + ); +} diff --git a/src/controllers/user/menu/index.js b/src/controllers/user/menu/index.js index 7f8ae7ecb4..035d07492b 100644 --- a/src/controllers/user/menu/index.js +++ b/src/controllers/user/menu/index.js @@ -31,7 +31,7 @@ export default function (view, params) { page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId); page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId); page.querySelector('.lnkSubtitlePreferences').setAttribute('href', '#/mypreferencessubtitles.html?userId=' + userId); - page.querySelector('.lnkQuickConnectPreferences').setAttribute('href', '#/mypreferencesquickconnect.html?userId=' + userId); + page.querySelector('.lnkQuickConnectPreferences').setAttribute('href', '#/quickconnect?userId=' + userId); page.querySelector('.lnkControlsPreferences').setAttribute('href', '#/mypreferencescontrols.html?userId=' + userId); const supportsClientSettings = appHost.supports('clientsettings'); diff --git a/src/controllers/user/quickConnect/helper.js b/src/controllers/user/quickConnect/helper.js deleted file mode 100644 index 3b306a9f80..0000000000 --- a/src/controllers/user/quickConnect/helper.js +++ /dev/null @@ -1,17 +0,0 @@ -import globalize from '../../../scripts/globalize'; -import toast from '../../../components/toast/toast'; - -export const authorize = (code, userId) => { - const url = ApiClient.getUrl('/QuickConnect/Authorize?Code=' + code + '&UserId=' + userId); - ApiClient.ajax({ - type: 'POST', - url: url - }, true).then(() => { - toast(globalize.translate('QuickConnectAuthorizeSuccess')); - }).catch(() => { - toast(globalize.translate('QuickConnectAuthorizeFail')); - }); - - // prevent bubbling - return false; -}; diff --git a/src/controllers/user/quickConnect/index.html b/src/controllers/user/quickConnect/index.html deleted file mode 100644 index 6e0e0cbf6f..0000000000 --- a/src/controllers/user/quickConnect/index.html +++ /dev/null @@ -1,15 +0,0 @@ -
-
-
-

${QuickConnect}

-
${QuickConnectDescription}
-
-
- -
- -
-
-
diff --git a/src/controllers/user/quickConnect/index.js b/src/controllers/user/quickConnect/index.js deleted file mode 100644 index 2af89cafcb..0000000000 --- a/src/controllers/user/quickConnect/index.js +++ /dev/null @@ -1,25 +0,0 @@ -import { authorize } from './helper'; -import globalize from '../../../scripts/globalize'; -import toast from '../../../components/toast/toast'; - -export default function (view, params) { - const userId = params.userId || ApiClient.getCurrentUserId(); - - view.addEventListener('viewshow', function () { - const codeElement = view.querySelector('#txtQuickConnectCode'); - - view.querySelector('.quickConnectSettingsContainer').addEventListener('submit', (e) => { - e.preventDefault(); - - if (!codeElement.validity.valid) { - toast(globalize.translate('QuickConnectInvalidCode')); - - return; - } - - // Remove spaces from code - const normalizedCode = codeElement.value.replace(/\s/g, ''); - authorize(normalizedCode, userId); - }); - }); -} diff --git a/src/elements/InputElement.tsx b/src/elements/InputElement.tsx index 99b2e8a087..8c82a6a34e 100644 --- a/src/elements/InputElement.tsx +++ b/src/elements/InputElement.tsx @@ -1,32 +1,72 @@ -import React, { FunctionComponent } from 'react'; +import React, { type FC, useCallback, useEffect, useMemo, useRef } from 'react'; + import globalize from '../scripts/globalize'; -const createInputElement = ({ type, id, label, options }: { type?: string, id?: string, label?: string, options?: string }) => ({ +interface CreateInputElementParams { + type?: string + id?: string + label?: string + initialValue?: string + options?: string +} + +const createInputElement = ({ type, id, label, initialValue, options }: CreateInputElementParams) => ({ __html: `` }); -type IProps = { - type?: string; - id?: string; - label?: string; - options?: string -}; +type InputElementProps = { + containerClassName?: string + onChange?: (value: string) => void +} & CreateInputElementParams; + +const InputElement: FC = ({ + containerClassName, + initialValue: initialValue, + onChange = () => { /* no-op */ }, + type, + id, + label, + options = '' +}) => { + const container = useRef(null); + + // NOTE: We need to memoize the input html because any re-render will break the webcomponent + const inputHtml = useMemo(() => ( + createInputElement({ + type, + id, + label: globalize.translate(label), + initialValue, + options + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + ), []); + + const onInput = useCallback((e: Event) => { + onChange((e.target as HTMLInputElement).value); + }, [ onChange ]); + + useEffect(() => { + const inputElement = container?.current?.querySelector('input'); + inputElement?.addEventListener('input', onInput); + + return () => { + inputElement?.removeEventListener('input', onInput); + }; + }, [ container, onInput ]); -const InputElement: FunctionComponent = ({ type, id, label, options }: IProps) => { return (
); }; From a405577519506b3de6b4f5595169e6c5a44e87f5 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 15 Sep 2023 11:08:31 -0400 Subject: [PATCH 2/3] Fix code smell --- src/elements/InputElement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/elements/InputElement.tsx b/src/elements/InputElement.tsx index 8c82a6a34e..c1cb4863d5 100644 --- a/src/elements/InputElement.tsx +++ b/src/elements/InputElement.tsx @@ -28,7 +28,7 @@ type InputElementProps = { const InputElement: FC = ({ containerClassName, - initialValue: initialValue, + initialValue, onChange = () => { /* no-op */ }, type, id, From 8553807c0d41c107ce5c55821f329ef7d6edfd16 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sat, 16 Sep 2023 03:02:55 -0400 Subject: [PATCH 3/3] Update quick connect success/error ui --- src/apps/stable/routes/quickConnect/index.tsx | 65 +++++++++++++------ .../routes/quickConnect/quickConnect.scss | 7 ++ src/strings/en-us.json | 4 +- 3 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 src/apps/stable/routes/quickConnect/quickConnect.scss diff --git a/src/apps/stable/routes/quickConnect/index.tsx b/src/apps/stable/routes/quickConnect/index.tsx index 1887b90a1b..8bff06c7ae 100644 --- a/src/apps/stable/routes/quickConnect/index.tsx +++ b/src/apps/stable/routes/quickConnect/index.tsx @@ -1,20 +1,23 @@ import { getQuickConnectApi } from '@jellyfin/sdk/lib/utils/api/quick-connect-api'; import React, { FC, FormEvent, useCallback, useMemo, useState } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import Page from 'components/Page'; import globalize from 'scripts/globalize'; import InputElement from 'elements/InputElement'; import ButtonElement from 'elements/ButtonElement'; -import toast from 'components/toast/toast'; import { useApi } from 'hooks/useApi'; +import './quickConnect.scss'; + const QuickConnectPage: FC = () => { const { api, user } = useApi(); const [ searchParams ] = useSearchParams(); // eslint-disable-next-line react-hooks/exhaustive-deps const initialValue = useMemo(() => searchParams.get('code') ?? '', []); const [ code, setCode ] = useState(initialValue); + const [ error, setError ] = useState(); + const [ success, setSuccess ] = useState(false); const onCodeChange = useCallback((value: string) => { setCode(value); @@ -22,15 +25,17 @@ const QuickConnectPage: FC = () => { const onSubmitCode = useCallback((e: FormEvent) => { e.preventDefault(); - const form = e.currentTarget; + setError(undefined); + const form = e.currentTarget; if (!form.checkValidity()) { - toast(globalize.translate('QuickConnectInvalidCode')); + setError('QuickConnectInvalidCode'); return; } if (!api) { console.error('[QuickConnect] cannot authorize, missing api instance'); + setError('UnknownError'); return; } @@ -44,10 +49,10 @@ const QuickConnectPage: FC = () => { userId }) .then(() => { - toast(globalize.translate('QuickConnectAuthorizeSuccess')); + setSuccess(true); }) .catch(() => { - toast(globalize.translate('QuickConnectAuthorizeFail')); + setError('QuickConnectAuthorizeFail'); }); }, [api, code, searchParams, user?.Id]); @@ -67,20 +72,40 @@ const QuickConnectPage: FC = () => { {globalize.translate('QuickConnectDescription')}

- - + + {error && ( +
+ {globalize.translate(error)} +
+ )} + + {success ? ( +
+

+ {globalize.translate('QuickConnectAuthorizeSuccess')} +

+ + {globalize.translate('GoHome')} + +
+ ) : ( + <> + + + + )} diff --git a/src/apps/stable/routes/quickConnect/quickConnect.scss b/src/apps/stable/routes/quickConnect/quickConnect.scss new file mode 100644 index 0000000000..d81e715613 --- /dev/null +++ b/src/apps/stable/routes/quickConnect/quickConnect.scss @@ -0,0 +1,7 @@ +.quickConnectError { + border-radius: 0.2em; + background-color: #160b0b; + color: #f4c7c3; + padding: 0.7em 0.5em; + margin-bottom: 1em; +} diff --git a/src/strings/en-us.json b/src/strings/en-us.json index bc6dc0da08..551026c255 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -297,6 +297,7 @@ "Genre": "Genre", "Genres": "Genres", "GetThePlugin": "Get the Plugin", + "GoHome": "Go Home", "GoogleCastUnsupported": "Google Cast Unsupported", "GroupBySeries": "Group by series", "GroupVersions": "Group versions", @@ -1358,7 +1359,7 @@ "QuickConnectActivationSuccessful": "Successfully activated", "QuickConnectAuthorizeCode": "Enter code {0} to login", "QuickConnectAuthorizeFail": "Unknown Quick Connect code", - "QuickConnectAuthorizeSuccess": "Request authorized", + "QuickConnectAuthorizeSuccess": "You have successfully authenticated your device!", "QuickConnectDeactivated": "Quick Connect was deactivated before the login request could be approved", "QuickConnectDescription": "To sign in with Quick Connect, select the 'Quick Connect' button on the device you are logging in from and enter the displayed code below.", "QuickConnectInvalidCode": "Invalid Quick Connect code", @@ -1554,6 +1555,7 @@ "Typewriter": "Typewriter", "Uniform": "Uniform", "UninstallPluginConfirmation": "Are you sure you wish to uninstall {0}?", + "UnknownError": "An unknown error occurred.", "Unmute": "Unmute", "Unplayed": "Unplayed", "Unrated": "Unrated",