1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Migrate quick connect page to react

This commit is contained in:
Bill Thornton 2023-09-15 10:31:48 -04:00
parent 9435e3172d
commit d24b030962
15 changed files with 179 additions and 88 deletions

View file

@ -9,6 +9,8 @@ import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import AppLayout from './AppLayout'; import AppLayout from './AppLayout';
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; 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 = () => { const ExperimentalApp = () => {
return ( return (
@ -38,7 +40,7 @@ const ExperimentalApp = () => {
</Route> </Route>
{/* Redirects for old paths */} {/* Redirects for old paths */}
<Route path='serveractivity.html' element={<Navigate replace to='/dashboard/activity' />} /> {REDIRECTS.map(toRedirectRoute)}
</Route> </Route>
</Routes> </Routes>
); );

View file

@ -140,7 +140,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
<Divider /> <Divider />
<MenuItem <MenuItem
component={Link} component={Link}
to='/mypreferencesquickconnect.html' to='/quickconnect'
onClick={onMenuClose} onClick={onMenuClose}
> >
<ListItemIcon> <ListItemIcon>

View file

@ -1,6 +1,7 @@
import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute'; import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute';
export const ASYNC_USER_ROUTES: AsyncRoute[] = [ export const ASYNC_USER_ROUTES: AsyncRoute[] = [
{ path: 'quickconnect', page: 'quickConnect' },
{ path: 'search.html', page: 'search' }, { path: 'search.html', page: 'search' },
{ path: 'userprofile.html', page: 'user/userprofile' }, { path: 'userprofile.html', page: 'user/userprofile' },
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental }, { path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },

View file

@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
controller: 'user/home/index', controller: 'user/home/index',
view: 'user/home/index.html' view: 'user/home/index.html'
} }
}, {
path: 'mypreferencesquickconnect.html',
pageProps: {
controller: 'user/quickConnect/index',
view: 'user/quickConnect/index.html'
}
}, { }, {
path: 'mypreferencesplayback.html', path: 'mypreferencesplayback.html',
pageProps: { pageProps: {

View file

@ -10,6 +10,8 @@ import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes'; import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './routes/asyncRoutes';
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes'; 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 = () => ( const Layout = () => (
<> <>
@ -53,7 +55,7 @@ const StableApp = () => (
<Route path='*' element={null} /> <Route path='*' element={null} />
{/* Redirects for old paths */} {/* Redirects for old paths */}
<Route path='/serveractivity.html' element={<Navigate replace to='/dashboard/activity' />} /> {REDIRECTS.map(toRedirectRoute)}
</Route> </Route>
</Routes> </Routes>
); );

View file

@ -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' }
];

View file

@ -1,6 +1,7 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute'; import { AsyncRoute } from '../../../../components/router/AsyncRoute';
export const ASYNC_USER_ROUTES: AsyncRoute[] = [ export const ASYNC_USER_ROUTES: AsyncRoute[] = [
{ path: 'quickconnect', page: 'quickConnect' },
{ path: 'search.html', page: 'search' }, { path: 'search.html', page: 'search' },
{ path: 'userprofile.html', page: 'user/userprofile' } { path: 'userprofile.html', page: 'user/userprofile' }
]; ];

View file

@ -49,12 +49,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
controller: 'user/home/index', controller: 'user/home/index',
view: 'user/home/index.html' view: 'user/home/index.html'
} }
}, {
path: 'mypreferencesquickconnect.html',
pageProps: {
controller: 'user/quickConnect/index',
view: 'user/quickConnect/index.html'
}
}, { }, {
path: 'mypreferencesplayback.html', path: 'mypreferencesplayback.html',
pageProps: { pageProps: {

View file

@ -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<HTMLFormElement>) => {
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 (
<Page
id='quickConnectPreferencesPage'
title={globalize.translate('QuickConnect')}
className='mainAnimatedPage libraryPage userPreferencesPage noSecondaryNavPage'
>
<div className='padded-left padded-right padded-bottom-page'>
<form onSubmit={onSubmitCode}>
<div className='verticalSection'>
<h2 className='sectionTitle'>
{globalize.translate('QuickConnect')}
</h2>
<div>
{globalize.translate('QuickConnectDescription')}
</div>
<br />
<InputElement
containerClassName='inputContainer'
initialValue={initialValue}
onChange={onCodeChange}
id='txtQuickConnectCode'
label='LabelQuickConnectCode'
type='text'
options="inputmode='numeric' pattern='[0-9\s]*' minlength='6' required autocomplete='off'"
/>
<ButtonElement
type='submit'
className='raised button-submit block'
title={globalize.translate('Authorize')}
/>
</div>
</form>
</div>
</Page>
);
};
export default QuickConnectPage;

View file

@ -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 (
<Route
key={from}
path={from}
element={<Navigate replace to={to} />}
/>
);
}

View file

@ -31,7 +31,7 @@ export default function (view, params) {
page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId); page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId);
page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId); page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId);
page.querySelector('.lnkSubtitlePreferences').setAttribute('href', '#/mypreferencessubtitles.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); page.querySelector('.lnkControlsPreferences').setAttribute('href', '#/mypreferencescontrols.html?userId=' + userId);
const supportsClientSettings = appHost.supports('clientsettings'); const supportsClientSettings = appHost.supports('clientsettings');

View file

@ -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;
};

View file

@ -1,15 +0,0 @@
<div id="quickConnectPreferencesPage" data-role="page" class="page libraryPage userPreferencesPage noSecondaryNavPage" data-title="${QuickConnect}" data-backbutton="true" style="margin: 0 auto; max-width: 54em">
<form class="quickConnectSettingsContainer">
<div class="verticalSection">
<h2 class="sectionTitle">${QuickConnect}</h2>
<div>${QuickConnectDescription}</div>
<br />
<div class="inputContainer">
<input is="emby-input" type="text" inputmode="numeric" pattern="[0-9\s]*" minlength="6" required id="txtQuickConnectCode" label="${LabelQuickConnectCode}" autocomplete="off" />
</div>
<button id="btnQuickConnectAuthorize" is="emby-button" type="submit" class="raised button-submit block">
<span>${Authorize}</span>
</button>
</div>
</form>
</div>

View file

@ -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);
});
});
}

View file

@ -1,32 +1,72 @@
import React, { FunctionComponent } from 'react'; import React, { type FC, useCallback, useEffect, useMemo, useRef } from 'react';
import globalize from '../scripts/globalize'; 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: `<input __html: `<input
is="emby-input" is="emby-input"
type="${type}" type="${type}"
id="${id}" id="${id}"
label="${label}" label="${label}"
value="${initialValue}"
${options} ${options}
/>` />`
}); });
type IProps = { type InputElementProps = {
type?: string; containerClassName?: string
id?: string; onChange?: (value: string) => void
label?: string; } & CreateInputElementParams;
options?: string
}; const InputElement: FC<InputElementProps> = ({
containerClassName,
initialValue: initialValue,
onChange = () => { /* no-op */ },
type,
id,
label,
options = ''
}) => {
const container = useRef<HTMLDivElement>(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<HTMLInputElement>('input');
inputElement?.addEventListener('input', onInput);
return () => {
inputElement?.removeEventListener('input', onInput);
};
}, [ container, onInput ]);
const InputElement: FunctionComponent<IProps> = ({ type, id, label, options }: IProps) => {
return ( return (
<div <div
dangerouslySetInnerHTML={createInputElement({ ref={container}
type: type, className={containerClassName}
id: id, dangerouslySetInnerHTML={inputHtml}
label: globalize.translate(label),
options: options ? options : ''
})}
/> />
); );
}; };