mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #4777 from thornbill/migrate-quick-connect
Migrate quick connect page to react
This commit is contained in:
commit
f14a61c8c7
17 changed files with 214 additions and 89 deletions
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
6
src/apps/stable/routes/_redirects.ts
Normal file
6
src/apps/stable/routes/_redirects.ts
Normal 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' }
|
||||||
|
];
|
|
@ -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' }
|
||||||
];
|
];
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
116
src/apps/stable/routes/quickConnect/index.tsx
Normal file
116
src/apps/stable/routes/quickConnect/index.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { getQuickConnectApi } from '@jellyfin/sdk/lib/utils/api/quick-connect-api';
|
||||||
|
import React, { FC, FormEvent, useCallback, useMemo, useState } from 'react';
|
||||||
|
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 { 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<string>();
|
||||||
|
const [ success, setSuccess ] = useState(false);
|
||||||
|
|
||||||
|
const onCodeChange = useCallback((value: string) => {
|
||||||
|
setCode(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSubmitCode = useCallback((e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
|
const form = e.currentTarget;
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
setError('QuickConnectInvalidCode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!api) {
|
||||||
|
console.error('[QuickConnect] cannot authorize, missing api instance');
|
||||||
|
setError('UnknownError');
|
||||||
|
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(() => {
|
||||||
|
setSuccess(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError('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 />
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className='quickConnectError'>
|
||||||
|
{globalize.translate(error)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success ? (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<p>
|
||||||
|
{globalize.translate('QuickConnectAuthorizeSuccess')}
|
||||||
|
</p>
|
||||||
|
<Link to='/home.html' className='button-link emby-button'>
|
||||||
|
{globalize.translate('GoHome')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<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;
|
7
src/apps/stable/routes/quickConnect/quickConnect.scss
Normal file
7
src/apps/stable/routes/quickConnect/quickConnect.scss
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.quickConnectError {
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background-color: #160b0b;
|
||||||
|
color: #f4c7c3;
|
||||||
|
padding: 0.7em 0.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
17
src/components/router/Redirect.tsx
Normal file
17
src/components/router/Redirect.tsx
Normal 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} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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');
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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>
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
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 : ''
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -297,6 +297,7 @@
|
||||||
"Genre": "Genre",
|
"Genre": "Genre",
|
||||||
"Genres": "Genres",
|
"Genres": "Genres",
|
||||||
"GetThePlugin": "Get the Plugin",
|
"GetThePlugin": "Get the Plugin",
|
||||||
|
"GoHome": "Go Home",
|
||||||
"GoogleCastUnsupported": "Google Cast Unsupported",
|
"GoogleCastUnsupported": "Google Cast Unsupported",
|
||||||
"GridView": "Grid View",
|
"GridView": "Grid View",
|
||||||
"GroupBySeries": "Group by series",
|
"GroupBySeries": "Group by series",
|
||||||
|
@ -1361,7 +1362,7 @@
|
||||||
"QuickConnectActivationSuccessful": "Successfully activated",
|
"QuickConnectActivationSuccessful": "Successfully activated",
|
||||||
"QuickConnectAuthorizeCode": "Enter code {0} to login",
|
"QuickConnectAuthorizeCode": "Enter code {0} to login",
|
||||||
"QuickConnectAuthorizeFail": "Unknown Quick Connect code",
|
"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",
|
"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.",
|
"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",
|
"QuickConnectInvalidCode": "Invalid Quick Connect code",
|
||||||
|
@ -1557,6 +1558,7 @@
|
||||||
"Typewriter": "Typewriter",
|
"Typewriter": "Typewriter",
|
||||||
"Uniform": "Uniform",
|
"Uniform": "Uniform",
|
||||||
"UninstallPluginConfirmation": "Are you sure you wish to uninstall {0}?",
|
"UninstallPluginConfirmation": "Are you sure you wish to uninstall {0}?",
|
||||||
|
"UnknownError": "An unknown error occurred.",
|
||||||
"Unmute": "Unmute",
|
"Unmute": "Unmute",
|
||||||
"Unplayed": "Unplayed",
|
"Unplayed": "Unplayed",
|
||||||
"Unrated": "Unrated",
|
"Unrated": "Unrated",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue