Merge branch 'jellyfin:master' into audio-normalization
This commit is contained in:
commit
91210408f0
203 changed files with 2214 additions and 3733 deletions
41
src/App.tsx
41
src/App.tsx
|
@ -1,41 +0,0 @@
|
|||
import { History } from '@remix-run/router';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { HistoryRouter } from './components/HistoryRouter';
|
||||
import { ApiProvider } from './hooks/useApi';
|
||||
import { AppRoutes, ExperimentalAppRoutes } from './routes';
|
||||
|
||||
const App = ({ history }: { history: History }) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
// Initialize the UI components after first render
|
||||
import('./scripts/libraryMenu'),
|
||||
import('./scripts/autoBackdrops')
|
||||
]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ApiProvider>
|
||||
<HistoryRouter history={history}>
|
||||
<div className='backdropContainer' />
|
||||
<div className='backgroundContainer' />
|
||||
|
||||
<div className='mainDrawer hide'>
|
||||
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
||||
</div>
|
||||
<div className='skinHeader focuscontainer-x' />
|
||||
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
{layoutMode === 'experimental' ? <ExperimentalAppRoutes /> : <AppRoutes /> }
|
||||
</div>
|
||||
|
||||
<div className='mainDrawerHandle' />
|
||||
</HistoryRouter>
|
||||
</ApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
30
src/RootApp.tsx
Normal file
30
src/RootApp.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import loadable from '@loadable/component';
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
|
||||
import StableApp from './apps/stable/App';
|
||||
import { HistoryRouter } from './components/router/HistoryRouter';
|
||||
import { ApiProvider } from './hooks/useApi';
|
||||
import { WebConfigProvider } from './hooks/useWebConfig';
|
||||
|
||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||
|
||||
const RootApp = ({ history }: { history: History }) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
|
||||
return (
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<HistoryRouter history={history}>
|
||||
{
|
||||
layoutMode === 'experimental' ?
|
||||
<ExperimentalApp /> :
|
||||
<StableApp />
|
||||
}
|
||||
</HistoryRouter>
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootApp;
|
2
src/apiclient.d.ts
vendored
2
src/apiclient.d.ts
vendored
|
@ -268,7 +268,7 @@ declare module 'jellyfin-apiclient' {
|
|||
sendWebSocketMessage(name: string, data: any): void;
|
||||
serverAddress(val?: string): string;
|
||||
serverId(): string;
|
||||
serverVersion(): string
|
||||
serverVersion(): string;
|
||||
setAuthenticationInfo(accessKey?: string, userId?: string): void;
|
||||
setRequestHeaders(headers: any): void;
|
||||
setSystemInfo(info: SystemInfo): void;
|
||||
|
|
19
src/apps/experimental/App.tsx
Normal file
19
src/apps/experimental/App.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import AppHeader from '../../components/AppHeader';
|
||||
import Backdrop from '../../components/Backdrop';
|
||||
import { ExperimentalAppRoutes } from './routes/AppRoutes';
|
||||
|
||||
const ExperimentalApp = () => (
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader />
|
||||
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
<ExperimentalAppRoutes />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ExperimentalApp;
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import ConnectionRequired from '../../components/ConnectionRequired';
|
||||
import ServerContentPage from '../../components/ServerContentPage';
|
||||
import { toAsyncPageRoute } from '../AsyncRoute';
|
||||
import { toViewManagerPageRoute } from '../LegacyRoute';
|
||||
import ConnectionRequired from '../../../components/ConnectionRequired';
|
||||
import ServerContentPage from '../../../components/ServerContentPage';
|
||||
import { toAsyncPageRoute } from '../../../components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from '../../../components/router/LegacyRoute';
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { AsyncRoute } from '../../AsyncRoute';
|
||||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'usernew.html', page: 'user/usernew' },
|
8
src/apps/experimental/routes/asyncRoutes/user.ts
Normal file
8
src/apps/experimental/routes/asyncRoutes/user.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { AsyncRoute, AsyncRouteType } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'search.html', page: 'search' },
|
||||
{ path: 'userprofile.html', page: 'user/userprofile' },
|
||||
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
|
||||
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental }
|
||||
];
|
|
@ -1,20 +1,20 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import globalize from '../scripts/globalize';
|
||||
import LibraryMenu from '../scripts/libraryMenu';
|
||||
import { clearBackdrop } from '../components/backdrop/backdrop';
|
||||
import layoutManager from '../components/layoutManager';
|
||||
import * as mainTabsManager from '../components/maintabsmanager';
|
||||
import '../elements/emby-tabs/emby-tabs';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import '../elements/emby-scroller/emby-scroller';
|
||||
import Page from '../components/Page';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../scripts/libraryMenu';
|
||||
import { clearBackdrop } from '../../../components/backdrop/backdrop';
|
||||
import layoutManager from '../../../components/layoutManager';
|
||||
import * as mainTabsManager from '../../../components/maintabsmanager';
|
||||
import '../../../elements/emby-tabs/emby-tabs';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-scroller/emby-scroller';
|
||||
import Page from '../../../components/Page';
|
||||
|
||||
type OnResumeOptions = {
|
||||
autoFocus?: boolean;
|
||||
refresh?: boolean
|
||||
}
|
||||
};
|
||||
|
||||
type ControllerProps = {
|
||||
onResume: (
|
||||
|
@ -23,7 +23,7 @@ type ControllerProps = {
|
|||
refreshed: boolean;
|
||||
onPause: () => void;
|
||||
destroy: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
const Home: FunctionComponent = () => {
|
||||
const [ searchParams ] = useSearchParams();
|
||||
|
@ -65,7 +65,7 @@ const Home: FunctionComponent = () => {
|
|||
depends = 'favorites';
|
||||
}
|
||||
|
||||
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => {
|
||||
return import(/* webpackChunkName: "[request]" */ `../../../controllers/${depends}`).then(({ default: controllerFactory }) => {
|
||||
let controller = tabControllers[index];
|
||||
|
||||
if (!controller) {
|
||||
|
@ -101,6 +101,8 @@ const Home: FunctionComponent = () => {
|
|||
|
||||
controller.refreshed = true;
|
||||
tabController.current = controller;
|
||||
}).catch(err => {
|
||||
console.error('[Home] failed to get tab controller', err);
|
||||
});
|
||||
}, [ getTabController ]);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { LegacyRoute } from '../../LegacyRoute';
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
{
|
|
@ -1,4 +1,4 @@
|
|||
import { LegacyRoute } from '../../LegacyRoute';
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
|
||||
{
|
|
@ -1,4 +1,4 @@
|
|||
import { LegacyRoute } from '../../LegacyRoute';
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||
{
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
|
@ -1,9 +1,9 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import loading from '../../components/loading/loading';
|
||||
import GenresItemsContainer from '../../components/common/GenresItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import GenresItemsContainer from '../../../../components/common/GenresItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
|
||||
|
@ -23,6 +23,8 @@ const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
).then((result) => {
|
||||
setItemsResult(result);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[GenresView] failed to fetch genres', err);
|
||||
});
|
||||
}, [topParentId]);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
|
@ -1,13 +1,13 @@
|
|||
import type { BaseItemDto, BaseItemDtoQueryResult, RecommendationDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import layoutManager from '../../components/layoutManager';
|
||||
import loading from '../../components/loading/loading';
|
||||
import dom from '../../scripts/dom';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import RecommendationContainer from '../../components/common/RecommendationContainer';
|
||||
import SectionContainer from '../../components/common/SectionContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import layoutManager from '../../../../components/layoutManager';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import dom from '../../../../scripts/dom';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import RecommendationContainer from '../../../../components/common/RecommendationContainer';
|
||||
import SectionContainer from '../../../../components/common/SectionContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
|
||||
|
@ -28,8 +28,10 @@ const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
}, [enableScrollX]);
|
||||
|
||||
const autoFocus = useCallback((page) => {
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
import('../../../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[SuggestionsView] failed to load data', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -55,6 +57,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
|
||||
loading.hide();
|
||||
autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[SuggestionsView] failed to fetch items', err);
|
||||
});
|
||||
}, [autoFocus]);
|
||||
|
||||
|
@ -72,6 +76,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
setLatestItems(items);
|
||||
|
||||
autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[SuggestionsView] failed to fetch latest items', err);
|
||||
});
|
||||
}, [autoFocus]);
|
||||
|
||||
|
@ -95,6 +101,8 @@ const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
|
|||
setRecommendations(result);
|
||||
|
||||
autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[SuggestionsView] failed to fetch recommendations', err);
|
||||
});
|
||||
}, [autoFocus]);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
import React, { FC, useCallback } from 'react';
|
||||
|
||||
import ViewItemsContainer from '../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../types/interface';
|
||||
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
|
||||
import { LibraryViewProps } from '../../../../types/interface';
|
||||
|
||||
const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
|
||||
const getBasekey = useCallback(() => {
|
|
@ -1,16 +1,16 @@
|
|||
import '../../elements/emby-scroller/emby-scroller';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../../elements/emby-tabs/emby-tabs';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-scroller/emby-scroller';
|
||||
import '../../../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import '../../../../elements/emby-tabs/emby-tabs';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import * as mainTabsManager from '../../components/maintabsmanager';
|
||||
import Page from '../../components/Page';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import * as mainTabsManager from '../../../../components/maintabsmanager';
|
||||
import Page from '../../../../components/Page';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import * as userSettings from '../../../../scripts/settings/userSettings';
|
||||
import CollectionsView from './CollectionsView';
|
||||
import FavoritesView from './FavoritesView';
|
||||
import GenresView from './GenresView';
|
||||
|
@ -114,6 +114,8 @@ const Movies: FC = () => {
|
|||
window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => {
|
||||
page.setAttribute('data-title', item.Name as string);
|
||||
libraryMenu.setTitle(item.Name);
|
||||
}).catch(err => {
|
||||
console.error('[movies] failed to fetch library', err);
|
||||
});
|
||||
} else {
|
||||
page.setAttribute('data-title', globalize.translate('Movies'));
|
19
src/apps/stable/App.tsx
Normal file
19
src/apps/stable/App.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import AppHeader from '../../components/AppHeader';
|
||||
import Backdrop from '../../components/Backdrop';
|
||||
import { AppRoutes } from './routes/AppRoutes';
|
||||
|
||||
const StableApp = () => (
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader />
|
||||
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
<AppRoutes />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
export default StableApp;
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import ConnectionRequired from '../../components/ConnectionRequired';
|
||||
import ServerContentPage from '../../components/ServerContentPage';
|
||||
import { toAsyncPageRoute } from '../AsyncRoute';
|
||||
import { toViewManagerPageRoute } from '../LegacyRoute';
|
||||
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||
import ConnectionRequired from '../../../components/ConnectionRequired';
|
||||
import ServerContentPage from '../../../components/ServerContentPage';
|
||||
import { toAsyncPageRoute } from '../../../components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from '../../../components/router/LegacyRoute';
|
||||
import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||
|
||||
export const AppRoutes = () => (
|
||||
|
@ -19,6 +19,7 @@ export const AppRoutes = () => (
|
|||
|
||||
{/* Admin routes */}
|
||||
<Route path='/' element={<ConnectionRequired isAdminRequired />}>
|
||||
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
||||
|
||||
<Route path='configurationpage' element={
|
10
src/apps/stable/routes/asyncRoutes/admin.ts
Normal file
10
src/apps/stable/routes/asyncRoutes/admin.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'usernew.html', page: 'user/usernew' },
|
||||
{ path: 'userprofiles.html', page: 'user/userprofiles' },
|
||||
{ path: 'useredit.html', page: 'user/useredit' },
|
||||
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess' },
|
||||
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol' },
|
||||
{ path: 'userpassword.html', page: 'user/userpassword' }
|
||||
];
|
2
src/apps/stable/routes/asyncRoutes/index.ts
Normal file
2
src/apps/stable/routes/asyncRoutes/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './admin';
|
||||
export * from './user';
|
6
src/apps/stable/routes/asyncRoutes/user.ts
Normal file
6
src/apps/stable/routes/asyncRoutes/user.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
||||
|
||||
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
|
||||
{ path: 'search.html', page: 'search' },
|
||||
{ path: 'userprofile.html', page: 'user/userprofile' }
|
||||
];
|
|
@ -1,4 +1,4 @@
|
|||
import { LegacyRoute } from '../../LegacyRoute';
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
|
@ -193,41 +193,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
view: 'dashboard/streaming.html',
|
||||
controller: 'dashboard/streaming'
|
||||
}
|
||||
}, {
|
||||
path: 'usernew.html',
|
||||
pageProps: {
|
||||
view: 'dashboard/users/usernew.html',
|
||||
controller: 'dashboard/users/usernew'
|
||||
}
|
||||
}, {
|
||||
path: 'userprofiles.html',
|
||||
pageProps: {
|
||||
view: 'dashboard/users/userprofiles.html',
|
||||
controller: 'dashboard/users/userprofilespage'
|
||||
}
|
||||
}, {
|
||||
path: 'useredit.html',
|
||||
pageProps: {
|
||||
view: 'dashboard/users/useredit.html',
|
||||
controller: 'dashboard/users/useredit'
|
||||
}
|
||||
}, {
|
||||
path: 'userlibraryaccess.html',
|
||||
pageProps: {
|
||||
view: 'dashboard/users/userlibraryaccess.html',
|
||||
controller: 'dashboard/users/userlibraryaccess'
|
||||
}
|
||||
}, {
|
||||
path: 'userparentalcontrol.html',
|
||||
pageProps: {
|
||||
view: 'dashboard/users/userparentalcontrol.html',
|
||||
controller: 'dashboard/users/userparentalcontrol'
|
||||
}
|
||||
}, {
|
||||
path: 'userpassword.html',
|
||||
pageProps: {
|
||||
view: 'dashboard/users/userpassword.html',
|
||||
controller: 'dashboard/users/userpasswordpage'
|
||||
}
|
||||
}
|
||||
];
|
|
@ -1,4 +1,4 @@
|
|||
import { LegacyRoute } from '../../LegacyRoute';
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
|
||||
{
|
|
@ -1,4 +1,4 @@
|
|||
import { LegacyRoute } from '../../LegacyRoute';
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
|
@ -92,12 +92,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
|
|||
isNowPlayingBarEnabled: false,
|
||||
isThemeMediaSupported: true
|
||||
}
|
||||
}, {
|
||||
path: 'userprofile.html',
|
||||
pageProps: {
|
||||
controller: 'user/profile/index',
|
||||
view: 'user/profile/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'home.html',
|
||||
pageProps: {
|
|
@ -1,12 +1,12 @@
|
|||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Page from '../components/Page';
|
||||
import SearchFields from '../components/search/SearchFields';
|
||||
import SearchResults from '../components/search/SearchResults';
|
||||
import SearchSuggestions from '../components/search/SearchSuggestions';
|
||||
import LiveTVSearchResults from '../components/search/LiveTVSearchResults';
|
||||
import globalize from '../scripts/globalize';
|
||||
import Page from '../../../components/Page';
|
||||
import SearchFields from '../../../components/search/SearchFields';
|
||||
import SearchResults from '../../../components/search/SearchResults';
|
||||
import SearchSuggestions from '../../../components/search/SearchSuggestions';
|
||||
import LiveTVSearchResults from '../../../components/search/LiveTVSearchResults';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const Search: FunctionComponent = () => {
|
||||
const [ query, setQuery ] = useState<string>();
|
|
@ -1,28 +1,43 @@
|
|||
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import CheckBoxElement from '../../elements/CheckBoxElement';
|
||||
import InputElement from '../../elements/InputElement';
|
||||
import LinkEditUserPreferences from '../../components/dashboard/users/LinkEditUserPreferences';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../components/loading/loading';
|
||||
import toast from '../../components/toast/toast';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import escapeHTML from 'escape-html';
|
||||
import SelectElement from '../../elements/SelectElement';
|
||||
import Page from '../../components/Page';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import InputElement from '../../../../elements/InputElement';
|
||||
import LinkEditUserPreferences from '../../../../components/dashboard/users/LinkEditUserPreferences';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type ResetProvider = AuthProvider & {
|
||||
checkedAttribute: string
|
||||
}
|
||||
};
|
||||
|
||||
type AuthProvider = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
};
|
||||
|
||||
const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
|
||||
Array.prototype.filter.call(elements, e => e.checked)
|
||||
.map(e => e.getAttribute('data-id'))
|
||||
);
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html')
|
||||
.catch(err => {
|
||||
console.error('[useredit] failed to navigate to user profile', err);
|
||||
});
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const UserEdit: FunctionComponent = () => {
|
||||
|
@ -56,7 +71,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
|
||||
providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide');
|
||||
fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
|
||||
|
||||
setAuthProviders(providers);
|
||||
|
||||
|
@ -73,7 +88,7 @@ const UserEdit: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
|
||||
providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide');
|
||||
fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
|
||||
|
||||
setPasswordResetProviders(providers);
|
||||
|
||||
|
@ -121,6 +136,8 @@ const UserEdit: FunctionComponent = () => {
|
|||
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
|
||||
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
|
||||
triggerChange(chkEnableDeleteAllFolders);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch channels', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
@ -134,18 +151,24 @@ const UserEdit: FunctionComponent = () => {
|
|||
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
|
||||
loadAuthProviders(user, providers);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch auth providers', err);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
|
||||
loadPasswordResetProviders(user, providers);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch password reset providers', err);
|
||||
});
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
})).then(function (folders) {
|
||||
loadDeleteFolders(user, folders.Items);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch media folders', err);
|
||||
});
|
||||
|
||||
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
|
||||
user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide');
|
||||
disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled);
|
||||
|
||||
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
|
||||
txtUserName.disabled = false;
|
||||
|
@ -185,6 +208,8 @@ const UserEdit: FunctionComponent = () => {
|
|||
loading.show();
|
||||
getUser().then(function (user) {
|
||||
loadUser(user);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
|
@ -198,19 +223,9 @@ const UserEdit: FunctionComponent = () => {
|
|||
|
||||
loadData();
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html');
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const saveUser = (user: UserDto) => {
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
}
|
||||
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
|
||||
|
@ -235,18 +250,15 @@ const UserEdit: FunctionComponent = () => {
|
|||
user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
|
||||
user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
|
||||
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
|
||||
}
|
||||
window.ApiClient.updateUser(user).then(function () {
|
||||
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder'));
|
||||
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
|
||||
|
||||
window.ApiClient.updateUser(user).then(() => (
|
||||
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {})
|
||||
)).then(() => {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to update user', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -254,6 +266,8 @@ const UserEdit: FunctionComponent = () => {
|
|||
loading.show();
|
||||
getUser().then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -261,16 +275,13 @@ const UserEdit: FunctionComponent = () => {
|
|||
};
|
||||
|
||||
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
|
||||
if (this.checked) {
|
||||
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide');
|
||||
} else {
|
||||
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide');
|
||||
}
|
||||
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
window.ApiClient.getNamedConfiguration('network').then(function (config) {
|
||||
const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement;
|
||||
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
|
||||
(page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess);
|
||||
}).catch(err => {
|
||||
console.error('[useredit] failed to load network config', err);
|
||||
});
|
||||
|
||||
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
|
|
@ -1,24 +1,24 @@
|
|||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import loading from '../../components/loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import toast from '../../components/toast/toast';
|
||||
import SectionTabs from '../../components/dashboard/users/SectionTabs';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import AccessContainer from '../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../elements/CheckBoxElement';
|
||||
import Page from '../../components/Page';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import libraryMenu from '../../../../scripts/libraryMenu';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string
|
||||
}
|
||||
};
|
||||
|
||||
const UserLibraryAccess: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
|
@ -148,6 +148,8 @@ const UserLibraryAccess: FunctionComponent = () => {
|
|||
const promise4 = window.ApiClient.getJSON(window.ApiClient.getUrl('Devices'));
|
||||
Promise.all([promise1, promise2, promise3, promise4]).then(function (responses) {
|
||||
loadUser(responses[0], responses[1].Items, responses[2].Items, responses[3].Items);
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
|
@ -166,6 +168,8 @@ const UserLibraryAccess: FunctionComponent = () => {
|
|||
const userId = getParameterByName('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -203,6 +207,8 @@ const UserLibraryAccess: FunctionComponent = () => {
|
|||
user.Policy.BlockedMediaFolders = null;
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[userlibraryaccess] failed to update user policy', err);
|
||||
});
|
||||
};
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import loading from '../../components/loading/loading';
|
||||
import toast from '../../components/toast/toast';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import InputElement from '../../elements/InputElement';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import AccessContainer from '../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../elements/CheckBoxElement';
|
||||
import Page from '../../components/Page';
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import InputElement from '../../../../elements/InputElement';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import AccessContainer from '../../../../components/dashboard/users/AccessContainer';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type userInput = {
|
||||
Name?: string;
|
||||
Password?: string;
|
||||
}
|
||||
};
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const UserNew: FunctionComponent = () => {
|
||||
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
|
||||
|
@ -93,6 +93,8 @@ const UserNew: FunctionComponent = () => {
|
|||
loadMediaFolders(responses[0].Items);
|
||||
loadChannels(responses[1].Items);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[usernew] failed to load data', err);
|
||||
});
|
||||
}, [loadChannels, loadMediaFolders]);
|
||||
|
||||
|
@ -111,12 +113,8 @@ const UserNew: FunctionComponent = () => {
|
|||
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
|
||||
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
|
||||
window.ApiClient.createUser(userInput).then(function (user) {
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
}
|
||||
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
|
||||
|
@ -142,7 +140,12 @@ const UserNew: FunctionComponent = () => {
|
|||
}
|
||||
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
Dashboard.navigate('useredit.html?userId=' + user.Id);
|
||||
Dashboard.navigate('useredit.html?userId=' + user.Id)
|
||||
.catch(err => {
|
||||
console.error('[usernew] failed to navigate to edit user page', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[usernew] failed to update user policy', err);
|
||||
});
|
||||
}, function () {
|
||||
toast(globalize.translate('ErrorDefault'));
|
|
@ -1,26 +1,27 @@
|
|||
import type { AccessSchedule, ParentalRating, UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { DynamicDayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/dynamic-day-of-week';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import AccessScheduleList from '../../components/dashboard/users/AccessScheduleList';
|
||||
import BlockedTagList from '../../components/dashboard/users/BlockedTagList';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../components/loading/loading';
|
||||
import toast from '../../components/toast/toast';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import CheckBoxElement from '../../elements/CheckBoxElement';
|
||||
import escapeHTML from 'escape-html';
|
||||
import SelectElement from '../../elements/SelectElement';
|
||||
import Page from '../../components/Page';
|
||||
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import AccessScheduleList from '../../../../components/dashboard/users/AccessScheduleList';
|
||||
import BlockedTagList from '../../../../components/dashboard/users/BlockedTagList';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import CheckBoxElement from '../../../../elements/CheckBoxElement';
|
||||
import SelectElement from '../../../../elements/SelectElement';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type UnratedItem = {
|
||||
name: string;
|
||||
value: string;
|
||||
checkedAttribute: string
|
||||
}
|
||||
};
|
||||
|
||||
const UserParentalControl: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
|
@ -196,6 +197,8 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
const promise2 = window.ApiClient.getParentalRatings();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
loadUser(responses[0], responses[1]);
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load data', err);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
|
@ -215,12 +218,8 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
};
|
||||
|
||||
const saveUser = (user: UserDto) => {
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
}
|
||||
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
if (!user.Id || !user.Policy) {
|
||||
throw new Error('Unexpected null user id or policy');
|
||||
}
|
||||
|
||||
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
|
||||
|
@ -234,12 +233,14 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
user.Policy.BlockedTags = getBlockedTagsFromPage();
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to update user policy', err);
|
||||
});
|
||||
};
|
||||
|
||||
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
|
||||
schedule = schedule || {};
|
||||
import('../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
|
||||
import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
|
||||
accessschedule.show({
|
||||
schedule: schedule
|
||||
}).then(function (updatedSchedule) {
|
||||
|
@ -251,7 +252,11 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
|
||||
schedules[index] = updatedSchedule;
|
||||
renderAccessSchedule(schedules);
|
||||
}).catch(() => {
|
||||
// access schedule closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load access schedule', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -272,7 +277,7 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
};
|
||||
|
||||
const showBlockedTagPopup = () => {
|
||||
import('../../components/prompt/prompt').then(({ default: prompt }) => {
|
||||
import('../../../../components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelTag')
|
||||
}).then(function (value) {
|
||||
|
@ -282,7 +287,11 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
tags.push(value);
|
||||
loadBlockedTags(tags);
|
||||
}
|
||||
}).catch(() => {
|
||||
// prompt closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to load prompt', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -291,6 +300,8 @@ const UserParentalControl: FunctionComponent = () => {
|
|||
const userId = getParameterByName('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
}).catch(err => {
|
||||
console.error('[userparentalcontrol] failed to fetch user', err);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
|
@ -1,10 +1,11 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import SectionTabs from '../../components/dashboard/users/SectionTabs';
|
||||
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import Page from '../../components/Page';
|
||||
import loading from '../../components/loading/loading';
|
||||
|
||||
import SectionTabs from '../../../../components/dashboard/users/SectionTabs';
|
||||
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import Page from '../../../../components/Page';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
|
||||
const UserPassword: FunctionComponent = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
|
@ -18,6 +19,8 @@ const UserPassword: FunctionComponent = () => {
|
|||
}
|
||||
setUserName(user.Name);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[userpassword] failed to fetch user', err);
|
||||
});
|
||||
}, [userId]);
|
||||
useEffect(() => {
|
|
@ -2,17 +2,17 @@ import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
|||
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models/image-type';
|
||||
import React, { FunctionComponent, useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import { appHost } from '../../components/apphost';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import ButtonElement from '../../elements/ButtonElement';
|
||||
import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
|
||||
import loading from '../../components/loading/loading';
|
||||
import toast from '../../components/toast/toast';
|
||||
import { getParameterByName } from '../../utils/url';
|
||||
import Page from '../../components/Page';
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import LibraryMenu from '../../../../scripts/libraryMenu';
|
||||
import { appHost } from '../../../../components/apphost';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
import ButtonElement from '../../../../elements/ButtonElement';
|
||||
import UserPasswordForm from '../../../../components/dashboard/users/UserPasswordForm';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import toast from '../../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../../utils/url';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
const UserProfile: FunctionComponent = () => {
|
||||
const userId = getParameterByName('userId');
|
||||
|
@ -30,12 +30,8 @@ const UserProfile: FunctionComponent = () => {
|
|||
|
||||
loading.show();
|
||||
window.ApiClient.getUser(userId).then(function (user) {
|
||||
if (!user.Name) {
|
||||
throw new Error('Unexpected null user.Name');
|
||||
}
|
||||
|
||||
if (!user.Id) {
|
||||
throw new Error('Unexpected null user.Id');
|
||||
if (!user.Name || !user.Id) {
|
||||
throw new Error('Unexpected null user name or id');
|
||||
}
|
||||
|
||||
setUserName(user.Name);
|
||||
|
@ -63,8 +59,12 @@ const UserProfile: FunctionComponent = () => {
|
|||
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide');
|
||||
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide');
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('[userprofile] failed to get current user', err);
|
||||
});
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[userprofile] failed to load data', err);
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
|
@ -114,6 +114,8 @@ const UserProfile: FunctionComponent = () => {
|
|||
window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () {
|
||||
loading.hide();
|
||||
reloadUser();
|
||||
}).catch(err => {
|
||||
console.error('[userprofile] failed to upload image', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -129,7 +131,11 @@ const UserProfile: FunctionComponent = () => {
|
|||
window.ApiClient.deleteUserImage(userId, ImageType.Primary).then(function () {
|
||||
loading.hide();
|
||||
reloadUser();
|
||||
}).catch(err => {
|
||||
console.error('[userprofile] failed to delete image', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog closed
|
||||
});
|
||||
});
|
||||
|
|
@ -1,24 +1,25 @@
|
|||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useEffect, useState, useRef } from 'react';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import loading from '../../components/loading/loading';
|
||||
import dom from '../../scripts/dom';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import UserCardBox from '../../components/dashboard/users/UserCardBox';
|
||||
import SectionTitleContainer from '../../elements/SectionTitleContainer';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../components/cardbuilder/card.scss';
|
||||
import '../../components/indicators/indicators.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import Page from '../../components/Page';
|
||||
|
||||
import Dashboard from '../../../../utils/dashboard';
|
||||
import globalize from '../../../../scripts/globalize';
|
||||
import loading from '../../../../components/loading/loading';
|
||||
import dom from '../../../../scripts/dom';
|
||||
import confirm from '../../../../components/confirm/confirm';
|
||||
import UserCardBox from '../../../../components/dashboard/users/UserCardBox';
|
||||
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
|
||||
import '../../../../elements/emby-button/emby-button';
|
||||
import '../../../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../../../components/cardbuilder/card.scss';
|
||||
import '../../../../components/indicators/indicators.scss';
|
||||
import '../../../../styles/flexstyles.scss';
|
||||
import Page from '../../../../components/Page';
|
||||
|
||||
type MenuEntry = {
|
||||
name?: string;
|
||||
id?: string;
|
||||
icon?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const UserProfiles: FunctionComponent = () => {
|
||||
const [ users, setUsers ] = useState<UserDto[]>([]);
|
||||
|
@ -30,6 +31,8 @@ const UserProfiles: FunctionComponent = () => {
|
|||
window.ApiClient.getUsers().then(function (result) {
|
||||
setUsers(result);
|
||||
loading.hide();
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to fetch users', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -75,29 +78,42 @@ const UserProfiles: FunctionComponent = () => {
|
|||
icon: 'delete'
|
||||
});
|
||||
|
||||
import('../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
import('../../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: card,
|
||||
callback: function (id: string) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('useredit.html?userId=' + userId);
|
||||
Dashboard.navigate('useredit.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user edit page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'access':
|
||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId);
|
||||
Dashboard.navigate('userlibraryaccess.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to user library page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'parentalcontrol':
|
||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId);
|
||||
Dashboard.navigate('userparentalcontrol.html?userId=' + userId)
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to parental control page', err);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
deleteUser(userId);
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
// action sheet closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to load action sheet', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -113,7 +129,11 @@ const UserProfiles: FunctionComponent = () => {
|
|||
loading.show();
|
||||
window.ApiClient.deleteUser(id).then(function () {
|
||||
loadData();
|
||||
}).catch(err => {
|
||||
console.error('[userprofiles] failed to delete user', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog closed
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -126,7 +146,10 @@ const UserProfiles: FunctionComponent = () => {
|
|||
});
|
||||
|
||||
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
|
||||
Dashboard.navigate('usernew.html');
|
||||
Dashboard.navigate('usernew.html')
|
||||
.catch(err => {
|
||||
console.error('[userprofiles] failed to navigate to new user page', err);
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
20
src/components/AppHeader.tsx
Normal file
20
src/components/AppHeader.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
const AppHeader = () => {
|
||||
useEffect(() => {
|
||||
// Initialize the UI components after first render
|
||||
import('../scripts/libraryMenu');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mainDrawer hide'>
|
||||
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
||||
</div>
|
||||
<div className='skinHeader focuscontainer-x' />
|
||||
<div className='mainDrawerHandle' />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppHeader;
|
|
@ -1,17 +0,0 @@
|
|||
import loadable from '@loadable/component';
|
||||
|
||||
interface AsyncPageProps {
|
||||
/** The relative path to the page component in the routes directory. */
|
||||
page: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Page component that uses the loadable component library to load pages defined in the routes directory asynchronously
|
||||
* with code splitting.
|
||||
*/
|
||||
const AsyncPage = loadable(
|
||||
(props: AsyncPageProps) => import(/* webpackChunkName: "[request]" */ `../routes/${props.page}`),
|
||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||
);
|
||||
|
||||
export default AsyncPage;
|
17
src/components/Backdrop.tsx
Normal file
17
src/components/Backdrop.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React, { useEffect } from 'react';
|
||||
|
||||
const Backdrop = () => {
|
||||
useEffect(() => {
|
||||
// Initialize the UI components after first render
|
||||
import('../scripts/autoBackdrops');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='backdropContainer' />
|
||||
<div className='backgroundContainer' />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Backdrop;
|
|
@ -1,9 +1,9 @@
|
|||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { ConnectResponse } from 'jellyfin-apiclient';
|
||||
|
||||
import alert from './alert';
|
||||
import { appRouter } from './appRouter';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import Loading from './loading/LoadingComponent';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
@ -35,116 +35,134 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
|||
|
||||
const [ isLoading, setIsLoading ] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const bounce = async (connectionResponse: ConnectResponse) => {
|
||||
switch (connectionResponse.State) {
|
||||
case ConnectionState.SignedIn:
|
||||
// Already logged in, bounce to the home page
|
||||
console.debug('[ConnectionRequired] already logged in, redirecting to home');
|
||||
navigate(BounceRoutes.Home);
|
||||
const bounce = useCallback(async (connectionResponse: ConnectResponse) => {
|
||||
switch (connectionResponse.State) {
|
||||
case ConnectionState.SignedIn:
|
||||
// Already logged in, bounce to the home page
|
||||
console.debug('[ConnectionRequired] already logged in, redirecting to home');
|
||||
navigate(BounceRoutes.Home);
|
||||
return;
|
||||
case ConnectionState.ServerSignIn:
|
||||
// Bounce to the login page
|
||||
if (location.pathname === BounceRoutes.Login) {
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
console.debug('[ConnectionRequired] not logged in, redirecting to login page');
|
||||
navigate(`${BounceRoutes.Login}?serverid=${connectionResponse.ApiClient.serverId()}`);
|
||||
}
|
||||
return;
|
||||
case ConnectionState.ServerSelection:
|
||||
// Bounce to select server page
|
||||
console.debug('[ConnectionRequired] redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
case ConnectionState.ServerUpdateNeeded:
|
||||
// Show update needed message and bounce to select server page
|
||||
try {
|
||||
await alert({
|
||||
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
|
||||
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] failed to show alert', ex);
|
||||
}
|
||||
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
const handleIncompleteWizard = useCallback(async (firstConnection: ConnectResponse) => {
|
||||
if (firstConnection.State === ConnectionState.ServerSignIn) {
|
||||
// Verify the wizard is complete
|
||||
try {
|
||||
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Public system info request failed');
|
||||
}
|
||||
const systemInfo = await infoResponse.json();
|
||||
if (!systemInfo?.StartupWizardCompleted) {
|
||||
// Update the current ApiClient
|
||||
// TODO: Is there a better place to handle this?
|
||||
ServerConnections.setLocalApiClient(firstConnection.ApiClient);
|
||||
// Bounce to the wizard
|
||||
console.info('[ConnectionRequired] startup wizard is not complete, redirecting there');
|
||||
navigate(BounceRoutes.StartWizard);
|
||||
return;
|
||||
case ConnectionState.ServerSignIn:
|
||||
// Bounce to the login page
|
||||
if (location.pathname === BounceRoutes.Login) {
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
console.debug('[ConnectionRequired] not logged in, redirecting to login page');
|
||||
navigate(`${BounceRoutes.Login}?serverid=${connectionResponse.ApiClient.serverId()}`);
|
||||
}
|
||||
return;
|
||||
case ConnectionState.ServerSelection:
|
||||
// Bounce to select server page
|
||||
console.debug('[ConnectionRequired] redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
case ConnectionState.ServerUpdateNeeded:
|
||||
// Show update needed message and bounce to select server page
|
||||
try {
|
||||
await alert({
|
||||
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
|
||||
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error('[ConnectionRequired] checking wizard status failed', ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bounce to the correct page in the login flow
|
||||
bounce(firstConnection)
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to bounce', err);
|
||||
});
|
||||
}, [bounce, navigate]);
|
||||
|
||||
const validateUserAccess = useCallback(async () => {
|
||||
const client = ServerConnections.currentApiClient();
|
||||
|
||||
// If this is a user route, ensure a user is logged in
|
||||
if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
|
||||
try {
|
||||
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
|
||||
bounce(await ServerConnections.connect())
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to bounce', err);
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from user route', ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is an admin route, ensure the user has access
|
||||
if (isAdminRequired) {
|
||||
try {
|
||||
const user = await client?.getCurrentUser();
|
||||
if (!user?.Policy?.IsAdministrator) {
|
||||
console.warn('[ConnectionRequired] normal user attempted to access admin route');
|
||||
bounce(await ServerConnections.connect())
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to bounce', err);
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] failed to show alert', ex);
|
||||
}
|
||||
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
|
||||
};
|
||||
|
||||
const validateConnection = async () => {
|
||||
// Check connection status on initial page load
|
||||
const firstConnection = appRouter.firstConnectionResult;
|
||||
appRouter.firstConnectionResult = null;
|
||||
|
||||
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
|
||||
if (firstConnection.State === ConnectionState.ServerSignIn) {
|
||||
// Verify the wizard is complete
|
||||
try {
|
||||
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Public system info request failed');
|
||||
}
|
||||
const systemInfo = await infoResponse.json();
|
||||
if (!systemInfo?.StartupWizardCompleted) {
|
||||
// Update the current ApiClient
|
||||
// TODO: Is there a better place to handle this?
|
||||
ServerConnections.setLocalApiClient(firstConnection.ApiClient);
|
||||
// Bounce to the wizard
|
||||
console.info('[ConnectionRequired] startup wizard is not complete, redirecting there');
|
||||
navigate(BounceRoutes.StartWizard);
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error('[ConnectionRequired] checking wizard status failed', ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bounce to the correct page in the login flow
|
||||
bounce(firstConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route.
|
||||
// This case will need to be handled elsewhere before appRouter can be killed.
|
||||
|
||||
const client = ServerConnections.currentApiClient();
|
||||
|
||||
// If this is a user route, ensure a user is logged in
|
||||
if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
|
||||
try {
|
||||
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
|
||||
bounce(await ServerConnections.connect());
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from user route', ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is an admin route, ensure the user has access
|
||||
if (isAdminRequired) {
|
||||
try {
|
||||
const user = await client?.getCurrentUser();
|
||||
if (!user?.Policy?.IsAdministrator) {
|
||||
console.warn('[ConnectionRequired] normal user attempted to access admin route');
|
||||
bounce(await ServerConnections.connect());
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
setIsLoading(false);
|
||||
}, [bounce, isAdminRequired, isUserRequired]);
|
||||
|
||||
validateConnection();
|
||||
}, [ isAdminRequired, isUserRequired, location.pathname, navigate ]);
|
||||
useEffect(() => {
|
||||
// TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route.
|
||||
// This case will need to be handled elsewhere before appRouter can be killed.
|
||||
|
||||
// Check connection status on initial page load
|
||||
const firstConnection = appRouter.firstConnectionResult;
|
||||
appRouter.firstConnectionResult = null;
|
||||
|
||||
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
|
||||
handleIncompleteWizard(firstConnection)
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to start wizard', err);
|
||||
});
|
||||
} else {
|
||||
validateUserAccess()
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] failed to validate user access', err);
|
||||
});
|
||||
}
|
||||
}, [handleIncompleteWizard, validateUserAccess]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
|
|||
import ServerConnections from './ServerConnections';
|
||||
import viewManager from './viewManager/viewManager';
|
||||
import globalize from '../scripts/globalize';
|
||||
import type { RestoreViewFailResponse } from '../types/viewManager';
|
||||
|
||||
interface ServerContentPageProps {
|
||||
view: string
|
||||
|
@ -29,7 +30,7 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
|
|||
};
|
||||
|
||||
viewManager.tryRestoreView(viewOptions)
|
||||
.catch(async (result?: any) => {
|
||||
.catch(async (result?: RestoreViewFailResponse) => {
|
||||
if (!result || !result.cancelled) {
|
||||
const apiClient = ServerConnections.currentApiClient();
|
||||
|
||||
|
@ -46,12 +47,13 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
|
|||
};
|
||||
|
||||
loadPage();
|
||||
}, [
|
||||
},
|
||||
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
view,
|
||||
location.pathname,
|
||||
location.search
|
||||
// location.state is NOT included as a dependency here since dialogs will update state while the current view
|
||||
// stays the same
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { appRouter } from './appRouter';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import browser from '../scripts/browser';
|
||||
import dialog from './dialog/dialog';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
|
|
@ -114,7 +114,7 @@ button::-moz-focus-inner {
|
|||
}
|
||||
|
||||
.card.show-animation:focus > .cardBox {
|
||||
transform: scale(1.18, 1.18);
|
||||
transform: scale(1.07, 1.07);
|
||||
}
|
||||
|
||||
.cardBox-bottompadded {
|
||||
|
|
|
@ -22,7 +22,7 @@ import './card.scss';
|
|||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../guide/programs.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
|
@ -679,9 +679,8 @@ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout
|
|||
|
||||
let valid = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
for (const text of lines) {
|
||||
let currentCssClass = cssClass;
|
||||
const text = lines[i];
|
||||
|
||||
if (valid > 0 && isOuterFooter) {
|
||||
currentCssClass += ' cardText-secondary';
|
||||
|
@ -862,8 +861,8 @@ function getCardFooterText(item, apiClient, options, footerClass, progressHtml,
|
|||
|
||||
if (options.textLines) {
|
||||
const additionalLines = options.textLines(item);
|
||||
for (let i = 0; i < additionalLines.length; i++) {
|
||||
lines.push(additionalLines[i]);
|
||||
for (const additionalLine of additionalLines) {
|
||||
lines.push(additionalLine);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1118,7 +1117,6 @@ let refreshIndicatorLoaded;
|
|||
function importRefreshIndicator() {
|
||||
if (!refreshIndicatorLoaded) {
|
||||
refreshIndicatorLoaded = true;
|
||||
/* eslint-disable-next-line @babel/no-unused-expressions */
|
||||
import('../../elements/emby-itemrefreshindicator/emby-itemrefreshindicator');
|
||||
}
|
||||
}
|
||||
|
@ -1469,7 +1467,6 @@ function getHoverMenuHtml(item, action) {
|
|||
const userData = item.UserData || {};
|
||||
|
||||
if (itemHelper.canMarkPlayed(item)) {
|
||||
/* eslint-disable-next-line @babel/no-unused-expressions */
|
||||
import('../../elements/emby-playstatebutton/emby-playstatebutton');
|
||||
html += '<button is="emby-playstatebutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
@ -1477,7 +1474,6 @@ function getHoverMenuHtml(item, action) {
|
|||
if (itemHelper.canRate(item)) {
|
||||
const likes = userData.Likes == null ? '' : userData.Likes;
|
||||
|
||||
/* eslint-disable-next-line @babel/no-unused-expressions */
|
||||
import('../../elements/emby-ratingbutton/emby-ratingbutton');
|
||||
html += '<button is="emby-ratingbutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
@ -1724,8 +1720,7 @@ export function onTimerCreated(programId, newTimerId, itemsContainer) {
|
|||
export function onTimerCancelled(timerId, itemsContainer) {
|
||||
const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]');
|
||||
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const cell = cells[i];
|
||||
for (const cell of cells) {
|
||||
const icon = cell.querySelector('.timerIndicator');
|
||||
if (icon) {
|
||||
icon.parentNode.removeChild(icon);
|
||||
|
@ -1742,8 +1737,7 @@ export function onTimerCancelled(timerId, itemsContainer) {
|
|||
export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) {
|
||||
const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]');
|
||||
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const cell = cells[i];
|
||||
for (const cell of cells) {
|
||||
const icon = cell.querySelector('.timerIndicator');
|
||||
if (icon) {
|
||||
icon.parentNode.removeChild(icon);
|
||||
|
|
|
@ -3,7 +3,7 @@ import dom from '../../scripts/dom';
|
|||
import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import loading from '../loading/loading';
|
||||
import layoutManager from '../layoutManager';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
|
|
|
@ -32,7 +32,11 @@ const Filter: FC<FilterProps> = ({
|
|||
serverId: window.ApiClient.serverId(),
|
||||
filterMenuOptions: getFilterMenuOptions(),
|
||||
setfilters: setViewQuerySettings
|
||||
}).catch(() => {
|
||||
// filter menu closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[Filter] failed to load filter menu', err);
|
||||
});
|
||||
}, [viewQuerySettings, getVisibleFilters, topParentId, getItemTypes, getFilterMenuOptions, setViewQuerySettings]);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client'
|
|||
import escapeHTML from 'escape-html';
|
||||
import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||
import layoutManager from '../layoutManager';
|
||||
import lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
|
||||
|
@ -73,6 +73,8 @@ const GenresItemsContainer: FC<GenresItemsContainerProps> = ({
|
|||
centerText: true,
|
||||
showYear: true
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[GenresItemsContainer] failed to fetch items', err);
|
||||
});
|
||||
}, [getPortraitShape, topParentId]);
|
||||
|
||||
|
|
|
@ -12,7 +12,11 @@ const NewCollection: FC = () => {
|
|||
collectionEditor.show({
|
||||
items: [],
|
||||
serverId: serverId
|
||||
}).catch(() => {
|
||||
// closed collection editor
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[NewCollection] failed to load collection editor', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -22,7 +22,11 @@ const SelectView: FC<SelectViewProps> = ({
|
|||
settings: viewQuerySettings,
|
||||
visibleSettings: getVisibleViewSettings(),
|
||||
setviewsettings: setViewQuerySettings
|
||||
}).catch(() => {
|
||||
// view settings closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[SelectView] failed to load view settings', err);
|
||||
});
|
||||
}, [getVisibleViewSettings, viewQuerySettings, setViewQuerySettings]);
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ const Shuffle: FC<ShuffleProps> = ({ itemsResult = {}, topParentId }) => {
|
|||
topParentId as string
|
||||
).then((item) => {
|
||||
playbackManager.shuffle(item);
|
||||
}).catch(err => {
|
||||
console.error('[Shuffle] failed to fetch items', err);
|
||||
});
|
||||
}, [topParentId]);
|
||||
|
||||
|
|
|
@ -25,7 +25,11 @@ const Sort: FC<SortProps> = ({
|
|||
settings: viewQuerySettings,
|
||||
sortOptions: getSortMenuOptions(),
|
||||
setSortValues: setViewQuerySettings
|
||||
}).catch(() => {
|
||||
// sort menu closed
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[Sort] failed to load sort menu', err);
|
||||
});
|
||||
}, [getSortMenuOptions, viewQuerySettings, setViewQuerySettings]);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import loading from '../loading/loading';
|
||||
|
@ -33,6 +33,41 @@ const getDefaultSortBy = () => {
|
|||
return 'SortName';
|
||||
};
|
||||
|
||||
const getFields = (viewQuerySettings: ViewQuerySettings) => {
|
||||
const fields: ItemFields[] = [
|
||||
ItemFields.BasicSyncInfo,
|
||||
ItemFields.MediaSourceCount
|
||||
];
|
||||
|
||||
if (viewQuerySettings.imageType === 'primary') {
|
||||
fields.push(ItemFields.PrimaryImageAspectRatio);
|
||||
}
|
||||
|
||||
return fields.join(',');
|
||||
};
|
||||
|
||||
const getFilters = (viewQuerySettings: ViewQuerySettings) => {
|
||||
const filters: ItemFilter[] = [];
|
||||
|
||||
if (viewQuerySettings.IsPlayed) {
|
||||
filters.push(ItemFilter.IsPlayed);
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsUnplayed) {
|
||||
filters.push(ItemFilter.IsUnplayed);
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsFavorite) {
|
||||
filters.push(ItemFilter.IsFavorite);
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsResumable) {
|
||||
filters.push(ItemFilter.IsResumable);
|
||||
}
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
const getVisibleViewSettings = () => {
|
||||
return [
|
||||
'showTitle',
|
||||
|
@ -228,33 +263,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|||
}, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]);
|
||||
|
||||
const getQuery = useCallback(() => {
|
||||
let fields = 'BasicSyncInfo,MediaSourceCount';
|
||||
|
||||
if (viewQuerySettings.imageType === 'primary') {
|
||||
fields += ',PrimaryImageAspectRatio';
|
||||
}
|
||||
|
||||
if (viewQuerySettings.showYear) {
|
||||
fields += ',ProductionYear';
|
||||
}
|
||||
|
||||
const queryFilters: string[] = [];
|
||||
|
||||
if (viewQuerySettings.IsPlayed) {
|
||||
queryFilters.push('IsPlayed');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsUnplayed) {
|
||||
queryFilters.push('IsUnplayed');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsFavorite) {
|
||||
queryFilters.push('IsFavorite');
|
||||
}
|
||||
|
||||
if (viewQuerySettings.IsResumable) {
|
||||
queryFilters.push('IsResumable');
|
||||
}
|
||||
const queryFilters = getFilters(viewQuerySettings);
|
||||
|
||||
let queryIsHD;
|
||||
|
||||
|
@ -271,7 +280,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|||
SortOrder: viewQuerySettings.SortOrder,
|
||||
IncludeItemTypes: getItemTypes().join(','),
|
||||
Recursive: true,
|
||||
Fields: fields,
|
||||
Fields: getFields(viewQuerySettings),
|
||||
ImageTypeLimit: 1,
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
|
||||
Limit: userSettings.libraryPageSize(undefined) || undefined,
|
||||
|
@ -293,28 +302,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|||
ParentId: topParentId
|
||||
};
|
||||
}, [
|
||||
viewQuerySettings.imageType,
|
||||
viewQuerySettings.showYear,
|
||||
viewQuerySettings.IsPlayed,
|
||||
viewQuerySettings.IsUnplayed,
|
||||
viewQuerySettings.IsFavorite,
|
||||
viewQuerySettings.IsResumable,
|
||||
viewQuerySettings.IsHD,
|
||||
viewQuerySettings.IsSD,
|
||||
viewQuerySettings.SortBy,
|
||||
viewQuerySettings.SortOrder,
|
||||
viewQuerySettings.VideoTypes,
|
||||
viewQuerySettings.GenreIds,
|
||||
viewQuerySettings.Is4K,
|
||||
viewQuerySettings.Is3D,
|
||||
viewQuerySettings.HasSubtitles,
|
||||
viewQuerySettings.HasTrailer,
|
||||
viewQuerySettings.HasSpecialFeature,
|
||||
viewQuerySettings.HasThemeSong,
|
||||
viewQuerySettings.HasThemeVideo,
|
||||
viewQuerySettings.StartIndex,
|
||||
viewQuerySettings.NameLessThan,
|
||||
viewQuerySettings.NameStartsWith,
|
||||
viewQuerySettings,
|
||||
getItemTypes,
|
||||
getBasekey,
|
||||
topParentId
|
||||
|
@ -347,9 +335,13 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
|
|||
|
||||
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[ViewItemsContainer] failed to load autofocuser', err);
|
||||
});
|
||||
loading.hide();
|
||||
setisLoading(true);
|
||||
}).catch(err => {
|
||||
console.error('[ViewItemsContainer] failed to fetch data', err);
|
||||
});
|
||||
}, [fetchData]);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import browser from '../../scripts/browser';
|
||||
import dialog from '../dialog/dialog';
|
||||
import globalize from '../../scripts/globalize';
|
||||
|
|
|
@ -12,7 +12,7 @@ type IProps = {
|
|||
listTitle?: string;
|
||||
description?: string;
|
||||
children?: React.ReactNode
|
||||
}
|
||||
};
|
||||
|
||||
const AccessContainer: FunctionComponent<IProps> = ({ containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
|
||||
return (
|
||||
|
|
|
@ -9,7 +9,7 @@ type AccessScheduleListProps = {
|
|||
DayOfWeek?: string;
|
||||
StartHour?: number ;
|
||||
EndHour?: number;
|
||||
}
|
||||
};
|
||||
|
||||
function getDisplayTime(hours = 0) {
|
||||
let minutes = 0;
|
||||
|
|
|
@ -3,7 +3,7 @@ import IconButtonElement from '../../../elements/IconButtonElement';
|
|||
|
||||
type IProps = {
|
||||
tag?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
|
||||
return (
|
||||
|
|
|
@ -4,7 +4,7 @@ import globalize from '../../../scripts/globalize';
|
|||
type IProps = {
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const createLinkElement = ({ className, title }: IProps) => ({
|
||||
__html: `<a
|
||||
|
|
|
@ -3,7 +3,7 @@ import globalize from '../../../scripts/globalize';
|
|||
|
||||
type IProps = {
|
||||
activeTab: string;
|
||||
}
|
||||
};
|
||||
|
||||
const createLinkElement = (activeTab: string) => ({
|
||||
__html: `<a href="#"
|
||||
|
|
|
@ -19,7 +19,7 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
|
|||
|
||||
type IProps = {
|
||||
user?: UserDto;
|
||||
}
|
||||
};
|
||||
|
||||
const getLastSeenText = (lastActivityDate?: string | null) => {
|
||||
if (lastActivityDate) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
@ -12,12 +11,12 @@ import InputElement from '../../../elements/InputElement';
|
|||
|
||||
type IProps = {
|
||||
userId: string;
|
||||
}
|
||||
};
|
||||
|
||||
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadUser = useCallback(() => {
|
||||
const loadUser = useCallback(async () => {
|
||||
const page = element.current;
|
||||
|
||||
if (!page) {
|
||||
|
@ -25,61 +24,50 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
|||
return;
|
||||
}
|
||||
|
||||
window.ApiClient.getUser(userId).then(function (user) {
|
||||
Dashboard.getCurrentUser().then(function (loggedInUser: UserDto) {
|
||||
if (!user.Policy) {
|
||||
throw new Error('Unexpected null user.Policy');
|
||||
}
|
||||
const user = await window.ApiClient.getUser(userId);
|
||||
const loggedInUser = await Dashboard.getCurrentUser();
|
||||
|
||||
if (!user.Configuration) {
|
||||
throw new Error('Unexpected null user.Configuration');
|
||||
}
|
||||
if (!user.Policy || !user.Configuration) {
|
||||
throw new Error('Unexpected null user policy or configuration');
|
||||
}
|
||||
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
|
||||
let showLocalAccessSection = false;
|
||||
let showLocalAccessSection = false;
|
||||
|
||||
if (user.HasConfiguredPassword) {
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
|
||||
showLocalAccessSection = true;
|
||||
} else {
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
if (user.HasConfiguredPassword) {
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
|
||||
showLocalAccessSection = true;
|
||||
} else {
|
||||
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
|
||||
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
|
||||
if (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess) {
|
||||
(page.querySelector('.passwordSection') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
(page.querySelector('.passwordSection') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
const canChangePassword = loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess;
|
||||
(page.querySelector('.passwordSection') as HTMLDivElement).classList.toggle('hide', !canChangePassword);
|
||||
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.toggle('hide', !(showLocalAccessSection && canChangePassword));
|
||||
|
||||
if (showLocalAccessSection && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
|
||||
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement;
|
||||
txtEasyPassword.value = '';
|
||||
|
||||
const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement;
|
||||
txtEasyPassword.value = '';
|
||||
if (user.HasConfiguredEasyPassword) {
|
||||
txtEasyPassword.placeholder = '******';
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
txtEasyPassword.removeAttribute('placeholder');
|
||||
txtEasyPassword.placeholder = '';
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
|
||||
if (user.HasConfiguredEasyPassword) {
|
||||
txtEasyPassword.placeholder = '******';
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
|
||||
} else {
|
||||
txtEasyPassword.removeAttribute('placeholder');
|
||||
txtEasyPassword.placeholder = '';
|
||||
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
|
||||
}
|
||||
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
|
||||
|
||||
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
|
||||
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
|
||||
|
||||
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
|
||||
|
||||
import('../../autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
});
|
||||
import('../../autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load autofocuser', err);
|
||||
});
|
||||
|
||||
(page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = '';
|
||||
|
@ -95,7 +83,9 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
|||
return;
|
||||
}
|
||||
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
|
||||
const onSubmit = (e: Event) => {
|
||||
if ((page.querySelector('#txtNewPassword') as HTMLInputElement).value != (page.querySelector('#txtNewPasswordConfirm') as HTMLInputElement).value) {
|
||||
|
@ -123,7 +113,9 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
|||
loading.hide();
|
||||
toast(globalize.translate('PasswordSaved'));
|
||||
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
}, function () {
|
||||
loading.hide();
|
||||
Dashboard.alert({
|
||||
|
@ -146,6 +138,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
|||
if (easyPassword) {
|
||||
window.ApiClient.updateEasyPassword(userId, easyPassword).then(function () {
|
||||
onEasyPasswordSaved();
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to update easy password', err);
|
||||
});
|
||||
} else {
|
||||
onEasyPasswordSaved();
|
||||
|
@ -167,8 +161,14 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
|||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to update user configuration', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to fetch user', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -183,8 +183,14 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
|||
message: globalize.translate('PinCodeResetComplete'),
|
||||
title: globalize.translate('HeaderPinCodeReset')
|
||||
});
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to reset easy password', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog was closed
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -198,8 +204,14 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
|||
message: globalize.translate('PasswordResetComplete'),
|
||||
title: globalize.translate('ResetPassword')
|
||||
});
|
||||
loadUser();
|
||||
loadUser().catch(err => {
|
||||
console.error('[UserPasswordForm] failed to load user', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('[UserPasswordForm] failed to reset user password', err);
|
||||
});
|
||||
}).catch(() => {
|
||||
// confirm dialog was closed
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { history } from '../appRouter';
|
||||
import { history } from '../router/appRouter';
|
||||
import focusManager from '../focusManager';
|
||||
import browser from '../../scripts/browser';
|
||||
import layoutManager from '../layoutManager';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import dom from '../scripts/dom';
|
||||
import { appRouter } from './appRouter';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import ServerConnections from './ServerConnections';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import cardBuilder from '../cardbuilder/cardBuilder';
|
|||
import layoutManager from '../layoutManager';
|
||||
import imageLoader from '../images/imageLoader';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import imageHelper from '../../scripts/imagehelper';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
@ -100,10 +100,10 @@ export function loadSections(elem, apiClient, user, userSettings) {
|
|||
|
||||
export function destroySections(elem) {
|
||||
const elems = elem.querySelectorAll('.itemsContainer');
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
elems[i].fetchData = null;
|
||||
elems[i].parentContainer = null;
|
||||
elems[i].getItemsHtml = null;
|
||||
for (const e of elems) {
|
||||
e.fetchData = null;
|
||||
e.parentContainer = null;
|
||||
e.getItemsHtml = null;
|
||||
}
|
||||
|
||||
elem.innerHTML = '';
|
||||
|
@ -111,8 +111,8 @@ export function destroySections(elem) {
|
|||
|
||||
export function pause(elem) {
|
||||
const elems = elem.querySelectorAll('.itemsContainer');
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
elems[i].pause();
|
||||
for (const e of elems) {
|
||||
e.pause();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import browser from '../scripts/browser';
|
||||
import { copy } from '../scripts/clipboard';
|
||||
import dom from '../scripts/dom';
|
||||
import globalize from '../scripts/globalize';
|
||||
import actionsheet from './actionSheet/actionSheet';
|
||||
import { appHost } from './apphost';
|
||||
import { appRouter } from './appRouter';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import itemHelper from './itemHelper';
|
||||
import { playbackManager } from './playback/playbackmanager';
|
||||
import ServerConnections from './ServerConnections';
|
||||
|
@ -98,6 +99,16 @@ export function getCommands(options) {
|
|||
}
|
||||
|
||||
if (!browser.tv) {
|
||||
// Multiselect is currrently only ran on long clicks of card components
|
||||
// This disables Select on any context menu not originating from a card i.e songs
|
||||
if (options.positionTo && (dom.parentWithClass(options.positionTo, 'card') !== null)) {
|
||||
commands.push({
|
||||
name: globalize.translate('Select'),
|
||||
id: 'multiSelect',
|
||||
icon: 'library_add_check'
|
||||
});
|
||||
}
|
||||
|
||||
if (itemHelper.supportsAddingToCollection(item) && options.EnableCollectionManagement) {
|
||||
commands.push({
|
||||
name: globalize.translate('AddToCollection'),
|
||||
|
@ -432,6 +443,12 @@ function executeCommand(item, id, options) {
|
|||
itemMediaInfo.show(itemId, serverId).then(getResolveFunction(resolve, id), getResolveFunction(resolve, id));
|
||||
});
|
||||
break;
|
||||
case 'multiSelect':
|
||||
import('./multiSelect/multiSelect').then(({ startMultiSelect: startMultiSelect }) => {
|
||||
const card = dom.parentWithClass(options.positionTo, 'card');
|
||||
startMultiSelect(card);
|
||||
});
|
||||
break;
|
||||
case 'refresh':
|
||||
refresh(apiClient, item);
|
||||
getResolveFunction(resolve, id)();
|
||||
|
|
|
@ -23,8 +23,7 @@ function populateLanguages(parent) {
|
|||
function populateLanguagesIntoSelect(select, languages) {
|
||||
let html = '';
|
||||
html += "<option value=''></option>";
|
||||
for (let i = 0; i < languages.length; i++) {
|
||||
const culture = languages[i];
|
||||
for (const culture of languages) {
|
||||
html += `<option value='${culture.TwoLetterISOLanguageName}'>${culture.DisplayName}</option>`;
|
||||
}
|
||||
select.innerHTML = html;
|
||||
|
@ -32,8 +31,7 @@ function populateLanguagesIntoSelect(select, languages) {
|
|||
|
||||
function populateLanguagesIntoList(element, languages) {
|
||||
let html = '';
|
||||
for (let i = 0; i < languages.length; i++) {
|
||||
const culture = languages[i];
|
||||
for (const culture of languages) {
|
||||
html += `<label><input type="checkbox" is="emby-checkbox" class="chkSubtitleLanguage" data-lang="${culture.ThreeLetterISOLanguageName.toLowerCase()}" /><span>${culture.DisplayName}</span></label>`;
|
||||
}
|
||||
element.innerHTML = html;
|
||||
|
@ -43,8 +41,7 @@ function populateCountries(select) {
|
|||
return ApiClient.getCountries().then(allCountries => {
|
||||
let html = '';
|
||||
html += "<option value=''></option>";
|
||||
for (let i = 0; i < allCountries.length; i++) {
|
||||
const culture = allCountries[i];
|
||||
for (const culture of allCountries) {
|
||||
html += `<option value='${culture.TwoLetterISORegionName}'>${culture.DisplayName}</option>`;
|
||||
}
|
||||
select.innerHTML = html;
|
||||
|
@ -109,8 +106,7 @@ function renderMetadataSavers(page, metadataSavers) {
|
|||
}
|
||||
html += `<h3 class="checkboxListLabel">${globalize.translate('LabelMetadataSavers')}</h3>`;
|
||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||
for (let i = 0; i < metadataSavers.length; i++) {
|
||||
const plugin = metadataSavers[i];
|
||||
for (const plugin of metadataSavers) {
|
||||
html += `<label><input type="checkbox" data-defaultenabled="${plugin.DefaultEnabled}" is="emby-checkbox" class="chkMetadataSaver" data-pluginname="${escapeHtml(plugin.Name)}" ${false}><span>${escapeHtml(plugin.Name)}</span></label>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
@ -157,8 +153,7 @@ function getMetadataFetchersForTypeHtml(availableTypeOptions, libraryOptionsForT
|
|||
|
||||
function getTypeOptions(allOptions, type) {
|
||||
const allTypeOptions = allOptions.TypeOptions || [];
|
||||
for (let i = 0; i < allTypeOptions.length; i++) {
|
||||
const typeOptions = allTypeOptions[i];
|
||||
for (const typeOptions of allTypeOptions) {
|
||||
if (typeOptions.Type === type) return typeOptions;
|
||||
}
|
||||
return null;
|
||||
|
@ -167,8 +162,7 @@ function getTypeOptions(allOptions, type) {
|
|||
function renderMetadataFetchers(page, availableOptions, libraryOptions) {
|
||||
let html = '';
|
||||
const elem = page.querySelector('.metadataFetchers');
|
||||
for (let i = 0; i < availableOptions.TypeOptions.length; i++) {
|
||||
const availableTypeOptions = availableOptions.TypeOptions[i];
|
||||
for (const availableTypeOptions of availableOptions.TypeOptions) {
|
||||
html += getMetadataFetchersForTypeHtml(availableTypeOptions, getTypeOptions(libraryOptions, availableTypeOptions.Type) || {});
|
||||
}
|
||||
elem.innerHTML = html;
|
||||
|
@ -262,8 +256,7 @@ function getImageFetchersForTypeHtml(availableTypeOptions, libraryOptionsForType
|
|||
function renderImageFetchers(page, availableOptions, libraryOptions) {
|
||||
let html = '';
|
||||
const elem = page.querySelector('.imageFetchers');
|
||||
for (let i = 0; i < availableOptions.TypeOptions.length; i++) {
|
||||
const availableTypeOptions = availableOptions.TypeOptions[i];
|
||||
for (const availableTypeOptions of availableOptions.TypeOptions) {
|
||||
html += getImageFetchersForTypeHtml(availableTypeOptions, getTypeOptions(libraryOptions, availableTypeOptions.Type) || {});
|
||||
}
|
||||
elem.innerHTML = html;
|
||||
|
@ -460,8 +453,7 @@ function setSubtitleFetchersIntoOptions(parent, options) {
|
|||
|
||||
function setMetadataFetchersIntoOptions(parent, options) {
|
||||
const sections = parent.querySelectorAll('.metadataFetcher');
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
for (const section of sections) {
|
||||
const type = section.getAttribute('data-type');
|
||||
let typeOptions = getTypeOptions(options, type);
|
||||
if (!typeOptions) {
|
||||
|
@ -484,8 +476,7 @@ function setMetadataFetchersIntoOptions(parent, options) {
|
|||
|
||||
function setImageFetchersIntoOptions(parent, options) {
|
||||
const sections = parent.querySelectorAll('.imageFetcher');
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
for (const section of sections) {
|
||||
const type = section.getAttribute('data-type');
|
||||
let typeOptions = getTypeOptions(options, type);
|
||||
if (!typeOptions) {
|
||||
|
@ -509,8 +500,7 @@ function setImageFetchersIntoOptions(parent, options) {
|
|||
|
||||
function setImageOptionsIntoOptions(options) {
|
||||
const originalTypeOptions = (currentLibraryOptions || {}).TypeOptions || [];
|
||||
for (let i = 0; i < originalTypeOptions.length; i++) {
|
||||
const originalTypeOption = originalTypeOptions[i];
|
||||
for (const originalTypeOption of originalTypeOptions) {
|
||||
let typeOptions = getTypeOptions(options, originalTypeOption.Type);
|
||||
|
||||
if (!typeOptions) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
import datetime from '../../scripts/datetime';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import itemHelper from '../itemHelper';
|
||||
import indicators from '../indicators/indicators';
|
||||
import 'material-design-icons-iconfont';
|
||||
|
|
|
@ -20,7 +20,7 @@ import '../../styles/flexstyles.scss';
|
|||
import './style.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import template from './metadataEditor.template.html';
|
||||
|
||||
let currentContext;
|
||||
|
@ -955,8 +955,7 @@ function populatePeople(context, people) {
|
|||
|
||||
function getLockedFieldsHtml(fields, currentFields) {
|
||||
let html = '';
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const field = fields[i];
|
||||
for (const field of fields) {
|
||||
const name = field.name;
|
||||
const value = field.value || field.name;
|
||||
const checkedHtml = currentFields.indexOf(value) === -1 ? ' checked' : '';
|
||||
|
|
|
@ -564,3 +564,6 @@ export default function (options) {
|
|||
};
|
||||
}
|
||||
|
||||
export const startMultiSelect = (card) => {
|
||||
showSelections(card);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ import appFooter from '../appFooter/appFooter';
|
|||
import itemShortcuts from '../shortcuts';
|
||||
import './nowPlayingBar.scss';
|
||||
import '../../elements/emby-slider/emby-slider';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
|
||||
let currentPlayer;
|
||||
let currentPlayerSupportedCommands = [];
|
||||
|
|
|
@ -853,11 +853,9 @@ class PlaybackManager {
|
|||
user: user
|
||||
});
|
||||
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
const subTargets = responses[i];
|
||||
|
||||
for (let j = 0; j < subTargets.length; j++) {
|
||||
targets.push(subTargets[j]);
|
||||
for (const subTargets of responses) {
|
||||
for (const subTarget of subTargets) {
|
||||
targets.push(subTarget);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import browser from '../../scripts/browser';
|
|||
import loading from '../loading/loading';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import { pluginManager } from '../pluginManager';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { appHost } from '../apphost';
|
||||
import { enable, isEnabled, supported } from '../../scripts/autocast';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import browser from '../../scripts/browser';
|
||||
import appSettings from '../../scripts/settings/appSettings';
|
||||
import { appHost } from '../apphost';
|
||||
import focusManager from '../focusManager';
|
||||
|
@ -137,15 +136,6 @@ function showHideQualityFields(context, user, apiClient) {
|
|||
});
|
||||
}
|
||||
|
||||
function showOrHideEpisodesField(context) {
|
||||
if (browser.tizen || browser.web0s) {
|
||||
context.querySelector('.fldEpisodeAutoPlay').classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
context.querySelector('.fldEpisodeAutoPlay').classList.remove('hide');
|
||||
}
|
||||
|
||||
function loadForm(context, user, userSettings, apiClient) {
|
||||
const loggedInUserId = apiClient.getCurrentUserId();
|
||||
const userId = user.Id;
|
||||
|
@ -209,8 +199,6 @@ function loadForm(context, user, userSettings, apiClient) {
|
|||
fillSkipLengths(selectSkipBackLength);
|
||||
selectSkipBackLength.value = userSettings.skipBackLength();
|
||||
|
||||
showOrHideEpisodesField(context);
|
||||
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
<div class="fieldDescription checkboxFieldDescription">${CinemaModeConfigurationHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer fldEpisodeAutoPlay hide">
|
||||
<div class="checkboxContainer fldEpisodeAutoPlay">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkEpisodeAutoPlay" />
|
||||
<span>${PlayNextEpisodeAutomatically}</span>
|
||||
|
|
|
@ -6,7 +6,7 @@ import layoutManager from '../layoutManager';
|
|||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import { pluginManager } from '../pluginManager';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import loading from './loading/loading';
|
|||
import appSettings from '../scripts/settings/appSettings';
|
||||
import { playbackManager } from './playback/playbackmanager';
|
||||
import { appHost } from '../components/apphost';
|
||||
import { appRouter } from '../components/appRouter';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import * as inputManager from '../scripts/inputManager';
|
||||
import toast from '../components/toast/toast';
|
||||
import confirm from '../components/confirm/confirm';
|
||||
|
|
|
@ -18,7 +18,7 @@ import './remotecontrol.scss';
|
|||
import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
|
||||
let showMuteButton = true;
|
||||
let showVolumeSlider = true;
|
||||
|
|
44
src/components/router/AsyncRoute.tsx
Normal file
44
src/components/router/AsyncRoute.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import loadable from '@loadable/component';
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
export enum AsyncRouteType {
|
||||
Stable,
|
||||
Experimental
|
||||
}
|
||||
|
||||
export interface AsyncRoute {
|
||||
/** The URL path for this route. */
|
||||
path: string
|
||||
/** The relative path to the page component in the routes directory. */
|
||||
page: string
|
||||
/** The route should use the page component from the experimental app. */
|
||||
type?: AsyncRouteType
|
||||
}
|
||||
|
||||
interface AsyncPageProps {
|
||||
/** The relative path to the page component in the routes directory. */
|
||||
page: string
|
||||
}
|
||||
|
||||
const ExperimentalAsyncPage = loadable(
|
||||
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/experimental/routes/${props.page}`),
|
||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||
);
|
||||
|
||||
const StableAsyncPage = loadable(
|
||||
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/stable/routes/${props.page}`),
|
||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||
);
|
||||
|
||||
export const toAsyncPageRoute = ({ path, page, type = AsyncRouteType.Stable }: AsyncRoute) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
element={(
|
||||
type === AsyncRouteType.Experimental ?
|
||||
<ExperimentalAsyncPage page={page} /> :
|
||||
<StableAsyncPage page={page} />
|
||||
)}
|
||||
/>
|
||||
);
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import ViewManagerPage, { ViewManagerPageProps } from '../components/viewManager/ViewManagerPage';
|
||||
import ViewManagerPage, { ViewManagerPageProps } from '../viewManager/ViewManagerPage';
|
||||
|
||||
export interface LegacyRoute {
|
||||
path: string,
|
|
@ -1,15 +1,15 @@
|
|||
import { Action, createHashHistory } from 'history';
|
||||
|
||||
import { appHost } from './apphost';
|
||||
import { clearBackdrop, setBackdropTransparency } from './backdrop/backdrop';
|
||||
import globalize from '../scripts/globalize';
|
||||
import Events from '../utils/events.ts';
|
||||
import itemHelper from './itemHelper';
|
||||
import loading from './loading/loading';
|
||||
import viewManager from './viewManager/viewManager';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import alert from './alert';
|
||||
import { ConnectionState } from '../utils/jellyfin-apiclient/ConnectionState.ts';
|
||||
import { appHost } from '../apphost';
|
||||
import { clearBackdrop, setBackdropTransparency } from '../backdrop/backdrop';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import Events from '../../utils/events.ts';
|
||||
import itemHelper from '../itemHelper';
|
||||
import loading from '../loading/loading';
|
||||
import viewManager from '../viewManager/viewManager';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import alert from '../alert';
|
||||
import { ConnectionState } from '../../utils/jellyfin-apiclient/ConnectionState.ts';
|
||||
|
||||
export const history = createHashHistory();
|
||||
|
||||
|
@ -247,7 +247,7 @@ class AppRouter {
|
|||
url = apiClient.getUrl(`/web${url}`);
|
||||
promise = apiClient.get(url);
|
||||
} else {
|
||||
promise = import(/* webpackChunkName: "[request]" */ `../controllers/${url}`);
|
||||
promise = import(/* webpackChunkName: "[request]" */ `../../controllers/${url}`);
|
||||
}
|
||||
|
||||
promise.then((html) => {
|
||||
|
@ -267,7 +267,7 @@ class AppRouter {
|
|||
};
|
||||
|
||||
if (route.controller) {
|
||||
import(/* webpackChunkName: "[request]" */ '../controllers/' + route.controller).then(onInitComplete);
|
||||
import(/* webpackChunkName: "[request]" */ '../../controllers/' + route.controller).then(onInitComplete);
|
||||
} else {
|
||||
onInitComplete();
|
||||
}
|
|
@ -39,9 +39,9 @@ try {
|
|||
const elem = document.createElement('div');
|
||||
|
||||
const opts = Object.defineProperty({}, 'behavior', {
|
||||
// eslint-disable-next-line getter-return
|
||||
get: function () {
|
||||
supportsScrollToOptions = true;
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ type LiveTVSearchResultsProps = {
|
|||
parentId?: string | null;
|
||||
collectionType?: string | null;
|
||||
query?: string;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* React component to display search result rows for live tv library search
|
||||
|
@ -79,7 +79,9 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
|||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsMovie: true
|
||||
}).then(result => setMovies(result.Items || []));
|
||||
})
|
||||
.then(result => setMovies(result.Items || []))
|
||||
.catch(() => setMovies([]));
|
||||
// Episodes row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
|
@ -88,22 +90,30 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
|||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
}).then(result => setEpisodes(result.Items || []));
|
||||
})
|
||||
.then(result => setEpisodes(result.Items || []))
|
||||
.catch(() => setEpisodes([]));
|
||||
// Sports row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsSports: true
|
||||
}).then(result => setSports(result.Items || []));
|
||||
})
|
||||
.then(result => setSports(result.Items || []))
|
||||
.catch(() => setSports([]));
|
||||
// Kids row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsKids: true
|
||||
}).then(result => setKids(result.Items || []));
|
||||
})
|
||||
.then(result => setKids(result.Items || []))
|
||||
.catch(() => setKids([]));
|
||||
// News row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
IsNews: true
|
||||
}).then(result => setNews(result.Items || []));
|
||||
})
|
||||
.then(result => setNews(result.Items || []))
|
||||
.catch(() => setNews([]));
|
||||
// Programs row
|
||||
fetchItems(apiClient, {
|
||||
IncludeItemTypes: 'LiveTvProgram',
|
||||
|
@ -112,10 +122,13 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
|||
IsSports: false,
|
||||
IsKids: false,
|
||||
IsNews: false
|
||||
}).then(result => setPrograms(result.Items || []));
|
||||
})
|
||||
.then(result => setPrograms(result.Items || []))
|
||||
.catch(() => setPrograms([]));
|
||||
// Channels row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
|
||||
.then(result => setChannels(result.Items || []));
|
||||
.then(result => setChannels(result.Items || []))
|
||||
.catch(() => setChannels([]));
|
||||
}
|
||||
}, [collectionType, parentId, query, serverId]);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||
import type { ApiClient } from 'jellyfin-apiclient';
|
||||
import classNames from 'classnames';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import globalize from '../../scripts/globalize';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
@ -12,7 +12,18 @@ type SearchResultsProps = {
|
|||
parentId?: string | null;
|
||||
collectionType?: string | null;
|
||||
query?: string;
|
||||
}
|
||||
};
|
||||
|
||||
const ensureNonNullItems = (result: BaseItemDtoQueryResult) => ({
|
||||
...result,
|
||||
Items: result.Items || []
|
||||
});
|
||||
|
||||
const isMovies = (collectionType: string) => collectionType === 'movies';
|
||||
|
||||
const isMusic = (collectionType: string) => collectionType === 'music';
|
||||
|
||||
const isTVShows = (collectionType: string) => collectionType === 'tvshows' || collectionType === 'tv';
|
||||
|
||||
/*
|
||||
* React component to display search result rows for global search and non-live tv library search
|
||||
|
@ -35,55 +46,55 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
|
||||
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getDefaultParameters = () => ({
|
||||
ParentId: parentId,
|
||||
searchTerm: query,
|
||||
Limit: 24,
|
||||
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
|
||||
Recursive: true,
|
||||
EnableTotalRecordCount: false,
|
||||
ImageTypeLimit: 1,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: false,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false
|
||||
});
|
||||
const getDefaultParameters = useCallback(() => ({
|
||||
ParentId: parentId,
|
||||
searchTerm: query,
|
||||
Limit: 24,
|
||||
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
|
||||
Recursive: true,
|
||||
EnableTotalRecordCount: false,
|
||||
ImageTypeLimit: 1,
|
||||
IncludePeople: false,
|
||||
IncludeMedia: false,
|
||||
IncludeGenres: false,
|
||||
IncludeStudios: false,
|
||||
IncludeArtists: false
|
||||
}), [parentId, query]);
|
||||
|
||||
const fetchArtists = (apiClient: ApiClient, params = {}) => apiClient?.getArtists(
|
||||
apiClient?.getCurrentUserId(),
|
||||
const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => (
|
||||
apiClient?.getArtists(
|
||||
apiClient.getCurrentUserId(),
|
||||
{
|
||||
...getDefaultParameters(),
|
||||
IncludeArtists: true,
|
||||
...params
|
||||
}
|
||||
);
|
||||
).then(ensureNonNullItems)
|
||||
), [getDefaultParameters]);
|
||||
|
||||
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
|
||||
apiClient?.getCurrentUserId(),
|
||||
const fetchItems = useCallback((apiClient: ApiClient, params = {}) => (
|
||||
apiClient?.getItems(
|
||||
apiClient.getCurrentUserId(),
|
||||
{
|
||||
...getDefaultParameters(),
|
||||
IncludeMedia: true,
|
||||
...params
|
||||
}
|
||||
);
|
||||
).then(ensureNonNullItems)
|
||||
), [getDefaultParameters]);
|
||||
|
||||
const fetchPeople = (apiClient: ApiClient, params = {}) => apiClient?.getPeople(
|
||||
apiClient?.getCurrentUserId(),
|
||||
const fetchPeople = useCallback((apiClient: ApiClient, params = {}) => (
|
||||
apiClient?.getPeople(
|
||||
apiClient.getCurrentUserId(),
|
||||
{
|
||||
...getDefaultParameters(),
|
||||
IncludePeople: true,
|
||||
...params
|
||||
}
|
||||
);
|
||||
|
||||
const isMovies = () => collectionType === 'movies';
|
||||
|
||||
const isMusic = () => collectionType === 'music';
|
||||
|
||||
const isTVShows = () => collectionType === 'tvshows' || collectionType === 'tv';
|
||||
).then(ensureNonNullItems)
|
||||
), [getDefaultParameters]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset state
|
||||
setMovies([]);
|
||||
setShows([]);
|
||||
|
@ -102,78 +113,99 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||
setPeople([]);
|
||||
setCollections([]);
|
||||
|
||||
if (query) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
// Movie libraries
|
||||
if (!collectionType || isMovies()) {
|
||||
// Movies row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
|
||||
.then(result => setMovies(result.Items || []));
|
||||
}
|
||||
|
||||
// TV Show libraries
|
||||
if (!collectionType || isTVShows()) {
|
||||
// Shows row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
|
||||
.then(result => setShows(result.Items || []));
|
||||
// Episodes row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
|
||||
.then(result => setEpisodes(result.Items || []));
|
||||
}
|
||||
|
||||
// People are included for Movies and TV Shows
|
||||
if (!collectionType || isMovies() || isTVShows()) {
|
||||
// People row
|
||||
fetchPeople(apiClient).then(result => setPeople(result.Items || []));
|
||||
}
|
||||
|
||||
// Music libraries
|
||||
if (!collectionType || isMusic()) {
|
||||
// Playlists row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
|
||||
.then(results => setPlaylists(results.Items || []));
|
||||
// Artists row
|
||||
fetchArtists(apiClient).then(result => setArtists(result.Items || []));
|
||||
// Albums row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
|
||||
.then(result => setAlbums(result.Items || []));
|
||||
// Songs row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
|
||||
.then(result => setSongs(result.Items || []));
|
||||
}
|
||||
|
||||
// Other libraries do not support in-library search currently
|
||||
if (!collectionType) {
|
||||
// Videos row
|
||||
fetchItems(apiClient, {
|
||||
MediaTypes: 'Video',
|
||||
ExcludeItemTypes: 'Movie,Episode,TvChannel'
|
||||
}).then(result => setVideos(result.Items || []));
|
||||
// Programs row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
|
||||
.then(result => setPrograms(result.Items || []));
|
||||
// Channels row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
|
||||
.then(result => setChannels(result.Items || []));
|
||||
// Photo Albums row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
|
||||
.then(results => setPhotoAlbums(results.Items || []));
|
||||
// Photos row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
|
||||
.then(results => setPhotos(results.Items || []));
|
||||
// Audio Books row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
|
||||
.then(results => setAudioBooks(results.Items || []));
|
||||
// Books row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
|
||||
.then(results => setBooks(results.Items || []));
|
||||
// Collections row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
|
||||
.then(result => setCollections(result.Items || []));
|
||||
}
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
}, [collectionType, parentId, query, serverId]);
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
// Movie libraries
|
||||
if (!collectionType || isMovies(collectionType)) {
|
||||
// Movies row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
|
||||
.then(result => setMovies(result.Items))
|
||||
.catch(() => setMovies([]));
|
||||
}
|
||||
|
||||
// TV Show libraries
|
||||
if (!collectionType || isTVShows(collectionType)) {
|
||||
// Shows row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
|
||||
.then(result => setShows(result.Items))
|
||||
.catch(() => setShows([]));
|
||||
// Episodes row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
|
||||
.then(result => setEpisodes(result.Items))
|
||||
.catch(() => setEpisodes([]));
|
||||
}
|
||||
|
||||
// People are included for Movies and TV Shows
|
||||
if (!collectionType || isMovies(collectionType) || isTVShows(collectionType)) {
|
||||
// People row
|
||||
fetchPeople(apiClient)
|
||||
.then(result => setPeople(result.Items))
|
||||
.catch(() => setPeople([]));
|
||||
}
|
||||
|
||||
// Music libraries
|
||||
if (!collectionType || isMusic(collectionType)) {
|
||||
// Playlists row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
|
||||
.then(results => setPlaylists(results.Items))
|
||||
.catch(() => setPlaylists([]));
|
||||
// Artists row
|
||||
fetchArtists(apiClient)
|
||||
.then(result => setArtists(result.Items))
|
||||
.catch(() => setArtists([]));
|
||||
// Albums row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
|
||||
.then(result => setAlbums(result.Items))
|
||||
.catch(() => setAlbums([]));
|
||||
// Songs row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
|
||||
.then(result => setSongs(result.Items))
|
||||
.catch(() => setSongs([]));
|
||||
}
|
||||
|
||||
// Other libraries do not support in-library search currently
|
||||
if (!collectionType) {
|
||||
// Videos row
|
||||
fetchItems(apiClient, {
|
||||
MediaTypes: 'Video',
|
||||
ExcludeItemTypes: 'Movie,Episode,TvChannel'
|
||||
})
|
||||
.then(result => setVideos(result.Items))
|
||||
.catch(() => setVideos([]));
|
||||
// Programs row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
|
||||
.then(result => setPrograms(result.Items))
|
||||
.catch(() => setPrograms([]));
|
||||
// Channels row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
|
||||
.then(result => setChannels(result.Items))
|
||||
.catch(() => setChannels([]));
|
||||
// Photo Albums row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
|
||||
.then(result => setPhotoAlbums(result.Items))
|
||||
.catch(() => setPhotoAlbums([]));
|
||||
// Photos row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
|
||||
.then(result => setPhotos(result.Items))
|
||||
.catch(() => setPhotos([]));
|
||||
// Audio Books row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
|
||||
.then(result => setAudioBooks(result.Items))
|
||||
.catch(() => setAudioBooks([]));
|
||||
// Books row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
|
||||
.then(result => setBooks(result.Items))
|
||||
.catch(() => setBooks([]));
|
||||
// Collections row
|
||||
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
|
||||
.then(result => setCollections(result.Items))
|
||||
.catch(() => setCollections([]));
|
||||
}
|
||||
}, [collectionType, fetchArtists, fetchItems, fetchPeople, query, serverId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -35,13 +35,13 @@ type CardOptions = {
|
|||
showChannelName?: boolean,
|
||||
showTitle?: boolean,
|
||||
showYear?: boolean
|
||||
}
|
||||
};
|
||||
|
||||
type SearchResultsRowProps = {
|
||||
title?: string;
|
||||
items?: BaseItemDto[];
|
||||
cardOptions?: CardOptions;
|
||||
}
|
||||
};
|
||||
|
||||
const SearchResultsRow: FunctionComponent<SearchResultsRowProps> = ({ title, items = [], cardOptions = {} }: SearchResultsRowProps) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
|||
import escapeHtml from 'escape-html';
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
|
||||
import { appRouter } from '../appRouter';
|
||||
import { appRouter } from '../router/appRouter';
|
||||
import { useApi } from '../../hooks/useApi';
|
||||
import globalize from '../../scripts/globalize';
|
||||
|
||||
|
@ -25,7 +25,7 @@ const createSuggestionLink = ({ name, href }: { name: string, href: string }) =>
|
|||
|
||||
type SearchSuggestionsProps = {
|
||||
parentId?: string | null;
|
||||
}
|
||||
};
|
||||
|
||||
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }: SearchSuggestionsProps) => {
|
||||
const [ suggestions, setSuggestions ] = useState<BaseItemDto[]>([]);
|
||||
|
@ -45,7 +45,11 @@ const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId
|
|||
parentId: parentId || undefined,
|
||||
enableTotalRecordCount: false
|
||||
})
|
||||
.then(result => setSuggestions(result.data.Items || []));
|
||||
.then(result => setSuggestions(result.data.Items || []))
|
||||
.catch(err => {
|
||||
console.error('[SearchSuggestions] failed to fetch search suggestions', err);
|
||||
setSuggestions([]);
|
||||
});
|
||||
}
|
||||
}, [ api, parentId, user ]);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { playbackManager } from './playback/playbackmanager';
|
||||
import inputManager from '../scripts/inputManager';
|
||||
import { appRouter } from './appRouter';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import globalize from '../scripts/globalize';
|
||||
import dom from '../scripts/dom';
|
||||
import recordingHelper from './recordingcreator/recordinghelper';
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { FunctionComponent, useEffect } from 'react';
|
|||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import globalize from '../../scripts/globalize';
|
||||
import type { RestoreViewFailResponse } from '../../types/viewManager';
|
||||
import viewManager from './viewManager';
|
||||
|
||||
export interface ViewManagerPageProps {
|
||||
|
@ -45,7 +46,7 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
|
|||
};
|
||||
|
||||
viewManager.tryRestoreView(viewOptions)
|
||||
.catch(async (result?: any) => {
|
||||
.catch(async (result?: RestoreViewFailResponse) => {
|
||||
if (!result || !result.cancelled) {
|
||||
const [ controllerFactory, viewHtml ] = await Promise.all([
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
|
||||
|
@ -63,7 +64,10 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
|
|||
};
|
||||
|
||||
loadPage();
|
||||
}, [
|
||||
},
|
||||
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
controller,
|
||||
view,
|
||||
type,
|
||||
|
@ -73,8 +77,6 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
|
|||
transition,
|
||||
location.pathname,
|
||||
location.search
|
||||
// location.state is NOT included as a dependency here since dialogs will update state while the current view
|
||||
// stays the same
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
|
|
@ -90,11 +90,12 @@ function load(page, devices) {
|
|||
let html = '';
|
||||
html += devices.map(function (device) {
|
||||
let deviceHtml = '';
|
||||
deviceHtml += "<div data-id='" + device.Id + "' class='card backdropCard'>";
|
||||
deviceHtml += "<div data-id='" + escapeHtml(device.Id) + "' class='card backdropCard'>";
|
||||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#/device.html?id=${device.Id}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#!/device.html?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
// audit note: getDeviceIcon returns static text
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
if (iconUrl) {
|
||||
|
@ -113,7 +114,7 @@ function load(page, devices) {
|
|||
deviceHtml += '<div style="text-align:left; float:left;padding-top:5px;">';
|
||||
else
|
||||
deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">';
|
||||
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + device.Id + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + escapeHtml(device.Id) + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
deviceHtml += '</div>';
|
||||
}
|
||||
|
||||
|
|
|
@ -167,6 +167,14 @@
|
|||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="http://ffmpeg.org/ffmpeg-all.html#tonemap_005fopencl" target="_blank">${TonemappingAlgorithmHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingMode" label="${LabelTonemappingMode}">
|
||||
<option value="auto">${Auto}</option>
|
||||
<option value="max">MAX</option>
|
||||
<option value="rgb">RGB</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${TonemappingModeHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingRange" label="${LabelTonemappingRange}">
|
||||
<option value="auto">${Auto}</option>
|
||||
|
@ -179,10 +187,6 @@
|
|||
<input is="emby-input" type="number" id="txtTonemappingDesat" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingDesat}" />
|
||||
<div class="fieldDescription">${LabelTonemappingDesatHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtTonemappingThreshold" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingThreshold}" />
|
||||
<div class="fieldDescription">${LabelTonemappingThresholdHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtTonemappingPeak" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingPeak}" />
|
||||
<div class="fieldDescription">${LabelTonemappingPeakHelp}</div>
|
||||
|
|
|
@ -32,9 +32,9 @@ function loadPage(page, config, systemInfo) {
|
|||
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
||||
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
||||
page.querySelector('#selectTonemappingMode').value = config.TonemappingMode;
|
||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
||||
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
||||
page.querySelector('#txtTonemappingThreshold').value = config.TonemappingThreshold;
|
||||
page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak;
|
||||
page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || '';
|
||||
page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness;
|
||||
|
@ -90,9 +90,9 @@ function onSubmit() {
|
|||
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
||||
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
||||
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
||||
config.TonemappingMode = form.querySelector('#selectTonemappingMode').value;
|
||||
config.TonemappingRange = form.querySelector('#selectTonemappingRange').value;
|
||||
config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value;
|
||||
config.TonemappingThreshold = form.querySelector('#txtTonemappingThreshold').value;
|
||||
config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value;
|
||||
config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0';
|
||||
config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value;
|
||||
|
|
|
@ -28,8 +28,7 @@ function populateVersions(packageInfo, page, installedPlugin) {
|
|||
return b.timestamp < a.timestamp ? -1 : 1;
|
||||
});
|
||||
|
||||
for (let i = 0; i < packageInfo.versions.length; i++) {
|
||||
const version = packageInfo.versions[i];
|
||||
for (const version of packageInfo.versions) {
|
||||
html += '<option value="' + version.version + '">' + globalize.translate('PluginFromRepo', version.version, version.repositoryName) + '</option>';
|
||||
}
|
||||
|
||||
|
|
|
@ -66,8 +66,7 @@ function populateList(options) {
|
|||
let currentCategory = null;
|
||||
let html = '';
|
||||
|
||||
for (let i = 0; i < availablePlugins.length; i++) {
|
||||
const plugin = availablePlugins[i];
|
||||
for (const plugin of availablePlugins) {
|
||||
const category = plugin.categoryDisplayName;
|
||||
if (category != currentCategory) {
|
||||
if (currentCategory) {
|
||||
|
|
|
@ -133,8 +133,7 @@ function updateTaskButton(elem, state) {
|
|||
|
||||
export default function(view) {
|
||||
function updateTasks(tasks) {
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
const task = tasks[i];
|
||||
for (const task of tasks) {
|
||||
view.querySelector('#taskProgress' + task.Id).innerHTML = getTaskProgressHtml(task);
|
||||
updateTaskButton(view.querySelector('#btnTask' + task.Id), task.State);
|
||||
}
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
<div id="editUserPage" data-role="page" class="page type-interior">
|
||||
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle username"></h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-role="controlgroup" data-type="horizontal" class="localnav" id="userProfileNavigation" data-mini="true">
|
||||
<a href="#" is="emby-linkbutton" data-role="button" class="ui-btn-active">${Profile}</a>
|
||||
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
|
||||
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
|
||||
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
|
||||
</div>
|
||||
<p class="lnkEditUserPreferencesContainer">
|
||||
<a class="lnkEditUserPreferences button-link" href="#" is="emby-linkbutton">${ButtonEditOtherUserPreferences}</a>
|
||||
</p>
|
||||
<form class="editUserProfileForm">
|
||||
|
||||
<div class="disabledUserBanner" style="display: none;">
|
||||
<div class="btn btnDarkAccent btnStatic">
|
||||
<div>
|
||||
${HeaderThisUserIsCurrentlyDisabled}
|
||||
</div>
|
||||
<div style="margin-top: 5px;">
|
||||
${MessageReenableUser}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="fldUserName" class="inputContainer">
|
||||
<input is="emby-input" id="txtUserName" required type="text" label="${LabelName}" />
|
||||
</div>
|
||||
|
||||
<div class="selectContainer fldSelectLoginProvider hide">
|
||||
<select class="selectLoginProvider" is="emby-select" label="${LabelAuthProvider}"></select>
|
||||
<div class="fieldDescription">${AuthProviderHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer fldSelectPasswordResetProvider hide">
|
||||
<select class="selectPasswordResetProvider" is="emby-select" label="${LabelPasswordResetProvider}"></select>
|
||||
<div class="fieldDescription">${PasswordResetProviderHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkRemoteAccess" />
|
||||
<span>${AllowRemoteAccess}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowRemoteAccessHelp}</div>
|
||||
</div>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIsAdmin" />
|
||||
<span>${OptionAllowUserToManageServer}</span>
|
||||
</label>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableCollectionManagement" />
|
||||
<span>${AllowCollectionManagement}</span>
|
||||
</label>
|
||||
<div id="featureAccessFields" class="verticalSection">
|
||||
<h2 class="paperListLabel">${HeaderFeatureAccess}</h2>
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableLiveTvAccess" />
|
||||
<span>${OptionAllowBrowsingLiveTv}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkManageLiveTv" />
|
||||
<span>${OptionAllowManageLiveTv}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="paperListLabel">${HeaderPlayback}</h2>
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableMediaPlayback" />
|
||||
<span>${OptionAllowMediaPlayback}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAudioPlaybackTranscoding" />
|
||||
<span>${OptionAllowAudioPlaybackTranscoding}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableVideoPlaybackTranscoding" />
|
||||
<span>${OptionAllowVideoPlaybackTranscoding}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableVideoPlaybackRemuxing" />
|
||||
<span>${OptionAllowVideoPlaybackRemuxing}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkForceRemoteSourceTranscoding" />
|
||||
<span>${OptionForceRemoteSourceTranscoding}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="fieldDescription">${OptionAllowMediaPlaybackTranscodingHelp}</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="verticalSection">
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtRemoteClientBitrateLimit" inputmode="decimal" pattern="[0-9]*(\.[0-9]+)?" min="0" step=".25" label="${LabelRemoteClientBitrateLimit}" />
|
||||
<div class="fieldDescription">${LabelRemoteClientBitrateLimitHelp}</div>
|
||||
<div class="fieldDescription">${LabelUserRemoteClientBitrateLimitHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<div class="selectContainer fldSelectSyncPlayAccess">
|
||||
<select class="selectSyncPlayAccess" is="emby-select" id="selectSyncPlayAccess" label="${LabelSyncPlayAccess}">
|
||||
<option value="CreateAndJoinGroups">${LabelSyncPlayAccessCreateAndJoinGroups}</option>
|
||||
<option value="JoinGroups">${LabelSyncPlayAccessJoinGroups}</option>
|
||||
<option value="None">${LabelSyncPlayAccessNone}</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${SyncPlayAccessHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="checkboxListLabel" style="margin-bottom:1em;">${HeaderAllowMediaDeletionFrom}</h2>
|
||||
<div class="checkboxList paperList checkboxList-paperList">
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableDeleteAllFolders" />
|
||||
<span>${AllLibraries}</span>
|
||||
</label>
|
||||
<div class="deleteAccess">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="checkboxListLabel">${HeaderRemoteControl}</h2>
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableRemoteControlOtherUsers" />
|
||||
<span>${OptionAllowRemoteControlOthers}</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkRemoteControlSharedDevices" />
|
||||
<span>${OptionAllowRemoteSharedDevices}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="fieldDescription">${OptionAllowRemoteSharedDevicesHelp}</div>
|
||||
</div>
|
||||
<h2 class="checkboxListLabel">${Other}</h2>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableDownloading" />
|
||||
<span>${OptionAllowContentDownload}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionAllowContentDownloadHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription" id="fldIsEnabled">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDisabled" />
|
||||
<span>${OptionDisableUser}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionDisableUserHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription" id="fldIsHidden">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIsHidden" />
|
||||
<span>${OptionHideUser}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionHideUserFromLoginHelp}</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class=verticalSection>
|
||||
<div class="inputContainer" id="fldLoginAttemptsBeforeLockout">
|
||||
<input is="emby-input" type="number" id="txtLoginAttemptsBeforeLockout" min="-1" step="1" label="${LabelUserLoginAttemptsBeforeLockout}"/>
|
||||
<div class="fieldDescription">${OptionLoginAttemptsBeforeLockout}</div>
|
||||
<div class="fieldDescription">${OptionLoginAttemptsBeforeLockoutHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class=verticalSection>
|
||||
<div class="inputContainer" id="fldMaxActiveSessions">
|
||||
<input is="emby-input" type="number" id="txtMaxActiveSessions" min="0" step="1" label="${LabelUserMaxActiveSessions}"/>
|
||||
<div class="fieldDescription">${OptionMaxActiveSessions}</div>
|
||||
<div class="fieldDescription">${OptionMaxActiveSessionsHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
|
||||
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
|
||||
<span>${ButtonCancel}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,196 +0,0 @@
|
|||
import 'jquery';
|
||||
import loading from '../../../components/loading/loading';
|
||||
import libraryMenu from '../../../scripts/libraryMenu';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import toast from '../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../utils/url.ts';
|
||||
|
||||
function loadDeleteFolders(page, user, mediaFolders) {
|
||||
ApiClient.getJSON(ApiClient.getUrl('Channels', {
|
||||
SupportsMediaDeletion: true
|
||||
})).then(function (channelsResult) {
|
||||
let isChecked;
|
||||
let checkedAttribute;
|
||||
let html = '';
|
||||
|
||||
for (const folder of mediaFolders) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
for (const folder of channelsResult.Items) {
|
||||
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
|
||||
checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
$('.deleteAccess', page).html(html).trigger('create');
|
||||
$('#chkEnableDeleteAllFolders', page).prop('checked', user.Policy.EnableContentDeletion);
|
||||
});
|
||||
}
|
||||
|
||||
function loadAuthProviders(page, user, providers) {
|
||||
if (providers.length > 1) {
|
||||
page.querySelector('.fldSelectLoginProvider').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSelectLoginProvider').classList.add('hide');
|
||||
}
|
||||
|
||||
const currentProviderId = user.Policy.AuthenticationProviderId;
|
||||
page.querySelector('.selectLoginProvider').innerHTML = providers.map(function (provider) {
|
||||
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
|
||||
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadPasswordResetProviders(page, user, providers) {
|
||||
if (providers.length > 1) {
|
||||
page.querySelector('.fldSelectPasswordResetProvider').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSelectPasswordResetProvider').classList.add('hide');
|
||||
}
|
||||
|
||||
const currentProviderId = user.Policy.PasswordResetProviderId;
|
||||
page.querySelector('.selectPasswordResetProvider').innerHTML = providers.map(function (provider) {
|
||||
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
|
||||
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
|
||||
});
|
||||
}
|
||||
|
||||
function loadUser(page, user) {
|
||||
ApiClient.getJSON(ApiClient.getUrl('Auth/Providers')).then(function (providers) {
|
||||
loadAuthProviders(page, user, providers);
|
||||
});
|
||||
ApiClient.getJSON(ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
|
||||
loadPasswordResetProviders(page, user, providers);
|
||||
});
|
||||
ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
})).then(function (folders) {
|
||||
loadDeleteFolders(page, user, folders.Items);
|
||||
});
|
||||
|
||||
if (user.Policy.IsDisabled) {
|
||||
$('.disabledUserBanner', page).show();
|
||||
} else {
|
||||
$('.disabledUserBanner', page).hide();
|
||||
}
|
||||
|
||||
$('#txtUserName', page).prop('disabled', '').removeAttr('disabled');
|
||||
$('#fldConnectInfo', page).show();
|
||||
$('.lnkEditUserPreferences', page).attr('href', 'mypreferencesmenu.html?userId=' + user.Id);
|
||||
libraryMenu.setTitle(user.Name);
|
||||
page.querySelector('.username').innerHTML = user.Name;
|
||||
$('#txtUserName', page).val(user.Name);
|
||||
$('#chkIsAdmin', page).prop('checked', user.Policy.IsAdministrator);
|
||||
$('#chkDisabled', page).prop('checked', user.Policy.IsDisabled);
|
||||
$('#chkIsHidden', page).prop('checked', user.Policy.IsHidden);
|
||||
$('#chkEnableCollectionManagement', page).prop('checked', user.Policy.chkEnableCollectionManagement);
|
||||
$('#chkRemoteControlSharedDevices', page).prop('checked', user.Policy.EnableSharedDeviceControl);
|
||||
$('#chkEnableRemoteControlOtherUsers', page).prop('checked', user.Policy.EnableRemoteControlOfOtherUsers);
|
||||
$('#chkEnableDownloading', page).prop('checked', user.Policy.EnableContentDownloading);
|
||||
$('#chkManageLiveTv', page).prop('checked', user.Policy.EnableLiveTvManagement);
|
||||
$('#chkEnableLiveTvAccess', page).prop('checked', user.Policy.EnableLiveTvAccess);
|
||||
$('#chkEnableMediaPlayback', page).prop('checked', user.Policy.EnableMediaPlayback);
|
||||
$('#chkEnableAudioPlaybackTranscoding', page).prop('checked', user.Policy.EnableAudioPlaybackTranscoding);
|
||||
$('#chkEnableVideoPlaybackTranscoding', page).prop('checked', user.Policy.EnableVideoPlaybackTranscoding);
|
||||
$('#chkEnableVideoPlaybackRemuxing', page).prop('checked', user.Policy.EnablePlaybackRemuxing);
|
||||
$('#chkForceRemoteSourceTranscoding', page).prop('checked', user.Policy.ForceRemoteSourceTranscoding);
|
||||
$('#chkRemoteAccess', page).prop('checked', user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess);
|
||||
$('#txtRemoteClientBitrateLimit', page).val(user.Policy.RemoteClientBitrateLimit / 1e6 || '');
|
||||
$('#txtLoginAttemptsBeforeLockout', page).val(user.Policy.LoginAttemptsBeforeLockout || '0');
|
||||
$('#txtMaxActiveSessions', page).val(user.Policy.MaxActiveSessions || '0');
|
||||
if (ApiClient.isMinServerVersion('10.6.0')) {
|
||||
$('#selectSyncPlayAccess').val(user.Policy.SyncPlayAccess);
|
||||
}
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html');
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
function saveUser(user, page) {
|
||||
user.Name = $('#txtUserName', page).val();
|
||||
user.Policy.IsAdministrator = $('#chkIsAdmin', page).is(':checked');
|
||||
user.Policy.IsHidden = $('#chkIsHidden', page).is(':checked');
|
||||
user.Policy.IsDisabled = $('#chkDisabled', page).is(':checked');
|
||||
user.Policy.EnableRemoteControlOfOtherUsers = $('#chkEnableRemoteControlOtherUsers', page).is(':checked');
|
||||
user.Policy.EnableLiveTvManagement = $('#chkManageLiveTv', page).is(':checked');
|
||||
user.Policy.EnableLiveTvAccess = $('#chkEnableLiveTvAccess', page).is(':checked');
|
||||
user.Policy.EnableSharedDeviceControl = $('#chkRemoteControlSharedDevices', page).is(':checked');
|
||||
user.Policy.EnableMediaPlayback = $('#chkEnableMediaPlayback', page).is(':checked');
|
||||
user.Policy.EnableAudioPlaybackTranscoding = $('#chkEnableAudioPlaybackTranscoding', page).is(':checked');
|
||||
user.Policy.EnableVideoPlaybackTranscoding = $('#chkEnableVideoPlaybackTranscoding', page).is(':checked');
|
||||
user.Policy.EnablePlaybackRemuxing = $('#chkEnableVideoPlaybackRemuxing', page).is(':checked');
|
||||
user.Policy.EnableCollectionManagement = $('#chkEnableCollectionManagement', page).is(':checked');
|
||||
user.Policy.ForceRemoteSourceTranscoding = $('#chkForceRemoteSourceTranscoding', page).is(':checked');
|
||||
user.Policy.EnableContentDownloading = $('#chkEnableDownloading', page).is(':checked');
|
||||
user.Policy.EnableRemoteAccess = $('#chkRemoteAccess', page).is(':checked');
|
||||
user.Policy.RemoteClientBitrateLimit = parseInt(1e6 * parseFloat($('#txtRemoteClientBitrateLimit', page).val() || '0'), 10);
|
||||
user.Policy.LoginAttemptsBeforeLockout = parseInt($('#txtLoginAttemptsBeforeLockout', page).val() || '0', 10);
|
||||
user.Policy.MaxActiveSessions = parseInt($('#txtMaxActiveSessions', page).val() || '0', 10);
|
||||
user.Policy.AuthenticationProviderId = page.querySelector('.selectLoginProvider').value;
|
||||
user.Policy.PasswordResetProviderId = page.querySelector('.selectPasswordResetProvider').value;
|
||||
user.Policy.EnableContentDeletion = $('#chkEnableDeleteAllFolders', page).is(':checked');
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : $('.chkFolder', page).get().filter(function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
if (ApiClient.isMinServerVersion('10.6.0')) {
|
||||
user.Policy.SyncPlayAccess = page.querySelector('#selectSyncPlayAccess').value;
|
||||
}
|
||||
ApiClient.updateUser(user).then(function () {
|
||||
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const page = $(this).parents('.page')[0];
|
||||
loading.show();
|
||||
getUser().then(function (result) {
|
||||
saveUser(result, page);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function getUser() {
|
||||
const userId = getParameterByName('userId');
|
||||
return ApiClient.getUser(userId);
|
||||
}
|
||||
|
||||
function loadData(page) {
|
||||
loading.show();
|
||||
getUser().then(function (user) {
|
||||
loadUser(page, user);
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#editUserPage', function () {
|
||||
$('.editUserProfileForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
const page = this;
|
||||
$('#chkEnableDeleteAllFolders', this).on('change', function () {
|
||||
if (this.checked) {
|
||||
$('.deleteAccess', page).hide();
|
||||
} else {
|
||||
$('.deleteAccess', page).show();
|
||||
}
|
||||
});
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
if (config.EnableRemoteAccess) {
|
||||
page.querySelector('.fldRemoteAccess').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldRemoteAccess').classList.add('hide');
|
||||
}
|
||||
});
|
||||
}).on('pagebeforeshow', '#editUserPage', function () {
|
||||
loadData(this);
|
||||
});
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
|
||||
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle username"></h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);" class="ui-btn-active">${TabAccess}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
|
||||
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
|
||||
</div>
|
||||
<form class="userLibraryAccessForm">
|
||||
|
||||
<div class="folderAccessContainer">
|
||||
<h2>${HeaderLibraryAccess}</h2>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllFolders" />
|
||||
<span>${OptionEnableAccessToAllLibraries}</span>
|
||||
</label>
|
||||
<div class="folderAccessListContainer">
|
||||
<div class="folderAccess">
|
||||
</div>
|
||||
<div class="fieldDescription">${LibraryAccessHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channelAccessContainer" style="display:none;">
|
||||
<h2>${HeaderChannelAccess}</h2>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllChannels" />
|
||||
<span>${OptionEnableAccessToAllChannels}</span>
|
||||
</label>
|
||||
<div class="channelAccessListContainer">
|
||||
<div class="channelAccess">
|
||||
</div>
|
||||
<div class="fieldDescription">${ChannelAccessHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div class="deviceAccessContainer hide">
|
||||
<h2>${HeaderDeviceAccess}</h2>
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllDevices" />
|
||||
<span>${OptionEnableAccessFromAllDevices}</span>
|
||||
</label>
|
||||
<div class="deviceAccessListContainer">
|
||||
<div class="deviceAccess">
|
||||
</div>
|
||||
<div class="fieldDescription">${DeviceAccessHelp}</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,184 +0,0 @@
|
|||
import 'jquery';
|
||||
import loading from '../../../components/loading/loading';
|
||||
import libraryMenu from '../../../scripts/libraryMenu';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import toast from '../../../components/toast/toast';
|
||||
import { getParameterByName } from '../../../utils/url.ts';
|
||||
|
||||
function triggerChange(select) {
|
||||
const evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('change', false, true);
|
||||
select.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
function loadMediaFolders(page, user, mediaFolders) {
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
|
||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||
|
||||
for (let i = 0, length = mediaFolders.length; i < length; i++) {
|
||||
const folder = mediaFolders[i];
|
||||
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
page.querySelector('.folderAccess').innerHTML = html;
|
||||
const chkEnableAllFolders = page.querySelector('#chkEnableAllFolders');
|
||||
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
|
||||
triggerChange(chkEnableAllFolders);
|
||||
}
|
||||
|
||||
function loadChannels(page, user, channels) {
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
|
||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||
|
||||
for (let i = 0, length = channels.length; i < length; i++) {
|
||||
const folder = channels[i];
|
||||
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
|
||||
const checkedAttribute = isChecked ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('.channelAccess', page).show().html(html);
|
||||
|
||||
if (channels.length) {
|
||||
$('.channelAccessContainer', page).show();
|
||||
} else {
|
||||
$('.channelAccessContainer', page).hide();
|
||||
}
|
||||
|
||||
const chkEnableAllChannels = page.querySelector('#chkEnableAllChannels');
|
||||
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
|
||||
triggerChange(chkEnableAllChannels);
|
||||
}
|
||||
|
||||
function loadDevices(page, user, devices) {
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderDevices') + '</h3>';
|
||||
html += '<div class="checkboxList paperList checkboxList-paperList">';
|
||||
|
||||
for (let i = 0, length = devices.length; i < length; i++) {
|
||||
const device = devices[i];
|
||||
const checkedAttribute = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1 ? ' checked="checked"' : '';
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkDevice" data-id="' + device.Id + '" ' + checkedAttribute + '><span>' + device.Name + ' - ' + device.AppName + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('.deviceAccess', page).show().html(html);
|
||||
const chkEnableAllDevices = page.querySelector('#chkEnableAllDevices');
|
||||
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
|
||||
triggerChange(chkEnableAllDevices);
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
page.querySelector('.deviceAccessContainer').classList.add('hide');
|
||||
} else {
|
||||
page.querySelector('.deviceAccessContainer').classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function loadUser(page, user, loggedInUser, mediaFolders, channels, devices) {
|
||||
page.querySelector('.username').innerHTML = user.Name;
|
||||
libraryMenu.setTitle(user.Name);
|
||||
loadChannels(page, user, channels);
|
||||
loadMediaFolders(page, user, mediaFolders);
|
||||
loadDevices(page, user, devices);
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSaveComplete() {
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
function saveUser(user, page) {
|
||||
user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked');
|
||||
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : $('.chkFolder', page).get().filter(function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked');
|
||||
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : $('.chkChannel', page).get().filter(function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllDevices = $('#chkEnableAllDevices', page).is(':checked');
|
||||
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : $('.chkDevice', page).get().filter(function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.BlockedChannels = null;
|
||||
user.Policy.BlockedMediaFolders = null;
|
||||
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const page = $(this).parents('.page');
|
||||
loading.show();
|
||||
const userId = getParameterByName('userId');
|
||||
ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result, page);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#userLibraryAccessPage', function () {
|
||||
const page = this;
|
||||
$('#chkEnableAllDevices', page).on('change', function () {
|
||||
if (this.checked) {
|
||||
$('.deviceAccessListContainer', page).hide();
|
||||
} else {
|
||||
$('.deviceAccessListContainer', page).show();
|
||||
}
|
||||
});
|
||||
$('#chkEnableAllChannels', page).on('change', function () {
|
||||
if (this.checked) {
|
||||
$('.channelAccessListContainer', page).hide();
|
||||
} else {
|
||||
$('.channelAccessListContainer', page).show();
|
||||
}
|
||||
});
|
||||
page.querySelector('#chkEnableAllFolders').addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
page.querySelector('.folderAccessListContainer').classList.add('hide');
|
||||
} else {
|
||||
page.querySelector('.folderAccessListContainer').classList.remove('hide');
|
||||
}
|
||||
});
|
||||
$('.userLibraryAccessForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#userLibraryAccessPage', function () {
|
||||
const page = this;
|
||||
loading.show();
|
||||
let promise1;
|
||||
const userId = getParameterByName('userId');
|
||||
|
||||
if (userId) {
|
||||
promise1 = ApiClient.getUser(userId);
|
||||
} else {
|
||||
const deferred = $.Deferred();
|
||||
deferred.resolveWith(null, [{
|
||||
Configuration: {}
|
||||
}]);
|
||||
promise1 = deferred.promise();
|
||||
}
|
||||
|
||||
const promise2 = Dashboard.getCurrentUser();
|
||||
const promise4 = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
}));
|
||||
const promise5 = ApiClient.getJSON(ApiClient.getUrl('Channels'));
|
||||
const promise6 = ApiClient.getJSON(ApiClient.getUrl('Devices'));
|
||||
Promise.all([promise1, promise2, promise4, promise5, promise6]).then(function (responses) {
|
||||
loadUser(page, responses[0], responses[1], responses[2].Items, responses[3].Items, responses[4].Items);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<div id="newUserPage" data-role="page" class="page type-interior">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="newUserProfileForm">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${ButtonAddUser}</h2>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="txtUsername" required type="text" label="${LabelName}" />
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="txtPassword" type="password" label="${LabelPassword}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="folderAccessContainer verticalSection">
|
||||
<h2 class="sectionTitle">${HeaderLibraryAccess}</h2>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllFolders" />
|
||||
<span>${OptionEnableAccessToAllLibraries}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LibraryAccessHelp}</div>
|
||||
</div>
|
||||
<div class="folderAccessListContainer">
|
||||
<div class="folderAccess">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="channelAccessContainer verticalSection verticalSection-extrabottompadding" style="display:none;">
|
||||
<h2 class="sectionTitle">${HeaderChannelAccess}</h2>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableAllChannels" />
|
||||
<span>${OptionEnableAccessToAllChannels}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${ChannelAccessHelp}</div>
|
||||
</div>
|
||||
<div class="channelAccessListContainer">
|
||||
<div class="channelAccess">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
|
||||
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
|
||||
<span>${ButtonCancel}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,128 +0,0 @@
|
|||
import 'jquery';
|
||||
import loading from '../../../components/loading/loading';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import '../../../elements/emby-checkbox/emby-checkbox';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import toast from '../../../components/toast/toast';
|
||||
|
||||
function loadMediaFolders(page, mediaFolders) {
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
|
||||
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
|
||||
|
||||
for (let i = 0; i < mediaFolders.length; i++) {
|
||||
const folder = mediaFolders[i];
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('.folderAccess', page).html(html).trigger('create');
|
||||
$('#chkEnableAllFolders', page).prop('checked', false);
|
||||
}
|
||||
|
||||
function loadChannels(page, channels) {
|
||||
let html = '';
|
||||
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
|
||||
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
|
||||
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
const folder = channels[i];
|
||||
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
$('.channelAccess', page).show().html(html).trigger('create');
|
||||
|
||||
if (channels.length) {
|
||||
$('.channelAccessContainer', page).show();
|
||||
} else {
|
||||
$('.channelAccessContainer', page).hide();
|
||||
}
|
||||
|
||||
$('#chkEnableAllChannels', page).prop('checked', false);
|
||||
}
|
||||
|
||||
function loadUser(page) {
|
||||
$('#txtUsername', page).val('');
|
||||
$('#txtPassword', page).val('');
|
||||
loading.show();
|
||||
const promiseFolders = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
|
||||
IsHidden: false
|
||||
}));
|
||||
const promiseChannels = ApiClient.getJSON(ApiClient.getUrl('Channels'));
|
||||
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
|
||||
loadMediaFolders(page, responses[0].Items);
|
||||
loadChannels(page, responses[1].Items);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function saveUser(page) {
|
||||
const _user = {
|
||||
Name: $('#txtUsername', page).val(),
|
||||
Password: $('#txtPassword', page).val()
|
||||
};
|
||||
ApiClient.createUser(_user).then(function (user) {
|
||||
user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked');
|
||||
user.Policy.EnabledFolders = [];
|
||||
|
||||
if (!user.Policy.EnableAllFolders) {
|
||||
user.Policy.EnabledFolders = $('.chkFolder', page).get().filter(function (i) {
|
||||
return i.checked;
|
||||
}).map(function (i) {
|
||||
return i.getAttribute('data-id');
|
||||
});
|
||||
}
|
||||
|
||||
user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked');
|
||||
user.Policy.EnabledChannels = [];
|
||||
|
||||
if (!user.Policy.EnableAllChannels) {
|
||||
user.Policy.EnabledChannels = $('.chkChannel', page).get().filter(function (i) {
|
||||
return i.checked;
|
||||
}).map(function (i) {
|
||||
return i.getAttribute('data-id');
|
||||
});
|
||||
}
|
||||
|
||||
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
Dashboard.navigate('useredit.html?userId=' + user.Id);
|
||||
});
|
||||
}, function () {
|
||||
toast(globalize.translate('ErrorDefault'));
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const page = $(this).parents('.page')[0];
|
||||
loading.show();
|
||||
saveUser(page);
|
||||
return false;
|
||||
}
|
||||
|
||||
function loadData(page) {
|
||||
loadUser(page);
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#newUserPage', function () {
|
||||
const page = this;
|
||||
$('#chkEnableAllChannels', page).on('change', function () {
|
||||
if (this.checked) {
|
||||
$('.channelAccessListContainer', page).hide();
|
||||
} else {
|
||||
$('.channelAccessListContainer', page).show();
|
||||
}
|
||||
});
|
||||
$('#chkEnableAllFolders', page).on('change', function () {
|
||||
if (this.checked) {
|
||||
$('.folderAccessListContainer', page).hide();
|
||||
} else {
|
||||
$('.folderAccessListContainer', page).show();
|
||||
}
|
||||
});
|
||||
$('.newUserProfileForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#newUserPage', function () {
|
||||
loadData(this);
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue