Move routes to the correct apps

This commit is contained in:
Bill Thornton 2023-05-01 16:50:41 -04:00
parent f96166657d
commit 1224ba7ec4
20 changed files with 170 additions and 158 deletions

View file

@ -1,10 +1,10 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
import { AsyncRoute, AsyncRouteType } 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' }
{ path: 'usernew.html', page: 'user/usernew', type: AsyncRouteType.Experimental },
{ path: 'userprofiles.html', page: 'user/userprofiles', type: AsyncRouteType.Experimental },
{ path: 'useredit.html', page: 'user/useredit', type: AsyncRouteType.Experimental },
{ path: 'userlibraryaccess.html', page: 'user/userlibraryaccess', type: AsyncRouteType.Experimental },
{ path: 'userparentalcontrol.html', page: 'user/userparentalcontrol', type: AsyncRouteType.Experimental },
{ path: 'userpassword.html', page: 'user/userpassword', type: AsyncRouteType.Experimental }
];

View file

@ -1,8 +1,8 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
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' },
{ path: 'movies.html', page: 'movies' }
{ path: 'userprofile.html', page: 'user/userprofile', type: AsyncRouteType.Experimental },
{ path: 'home.html', page: 'home', type: AsyncRouteType.Experimental },
{ path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental }
];

View file

@ -0,0 +1,169 @@
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';
type OnResumeOptions = {
autoFocus?: boolean;
refresh?: boolean
};
type ControllerProps = {
onResume: (
options: OnResumeOptions
) => void;
refreshed: boolean;
onPause: () => void;
destroy: () => void;
};
const Home: FunctionComponent = () => {
const [ searchParams ] = useSearchParams();
const initialTabIndex = parseInt(searchParams.get('tab') || '0', 10);
const tabController = useRef<ControllerProps | null>();
const tabControllers = useMemo<ControllerProps[]>(() => [], []);
const element = useRef<HTMLDivElement>(null);
const setTitle = () => {
LibraryMenu.setTitle(null);
};
const getTabs = () => {
return [{
name: globalize.translate('Home')
}, {
name: globalize.translate('Favorites')
}];
};
const getTabContainers = () => {
return element.current?.querySelectorAll('.tabContent');
};
const getTabController = useCallback((index: number) => {
if (index == null) {
throw new Error('index cannot be null');
}
let depends = '';
switch (index) {
case 0:
depends = 'hometab';
break;
case 1:
depends = 'favorites';
}
return import(/* webpackChunkName: "[request]" */ `../../../controllers/${depends}`).then(({ default: controllerFactory }) => {
let controller = tabControllers[index];
if (!controller) {
const tabContent = element.current?.querySelector(".tabContent[data-index='" + index + "']");
controller = new controllerFactory(tabContent, null);
tabControllers[index] = controller;
}
return controller;
});
}, [ tabControllers ]);
const onViewDestroy = useCallback(() => {
if (tabControllers) {
tabControllers.forEach(function (t) {
if (t.destroy) {
t.destroy();
}
});
}
tabController.current = null;
}, [ tabControllers ]);
const loadTab = useCallback((index: number, previousIndex: number | null) => {
getTabController(index).then((controller) => {
const refresh = !controller.refreshed;
controller.onResume({
autoFocus: previousIndex == null && layoutManager.tv,
refresh: refresh
});
controller.refreshed = true;
tabController.current = controller;
});
}, [ getTabController ]);
const onTabChange = useCallback((e: { detail: { selectedTabIndex: string; previousIndex: number | null }; }) => {
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
const previousIndex = e.detail.previousIndex;
const previousTabController = previousIndex == null ? null : tabControllers[previousIndex];
if (previousTabController && previousTabController.onPause) {
previousTabController.onPause();
}
loadTab(newIndex, previousIndex);
}, [ loadTab, tabControllers ]);
const onResume = useCallback(() => {
setTitle();
clearBackdrop();
const currentTabController = tabController.current;
if (!currentTabController) {
mainTabsManager.selectedTabIndex(initialTabIndex);
} else if (currentTabController && currentTabController.onResume) {
currentTabController.onResume({});
}
(document.querySelector('.skinHeader') as HTMLDivElement).classList.add('noHomeButtonHeader');
}, [ initialTabIndex ]);
const onPause = useCallback(() => {
const currentTabController = tabController.current;
if (currentTabController && currentTabController.onPause) {
currentTabController.onPause();
}
(document.querySelector('.skinHeader') as HTMLDivElement).classList.remove('noHomeButtonHeader');
}, []);
useEffect(() => {
mainTabsManager.setTabs(element.current, initialTabIndex, getTabs, getTabContainers, null, onTabChange, false);
onResume();
return () => {
onPause();
};
}, [ initialTabIndex, onPause, onResume, onTabChange, onViewDestroy ]);
return (
<div ref={element}>
<Page
id='indexPage'
className='mainAnimatedPage homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs'
isBackButtonEnabled={false}
backDropType='movie,series,book'
>
<div className='tabContent pageTabContent' id='homeTab' data-index='0'>
<div className='sections'></div>
</div>
<div className='tabContent pageTabContent' id='favoritesTab' data-index='1'>
<div className='sections'></div>
</div>
</Page>
</div>
);
};
export default Home;

View file

@ -0,0 +1,32 @@
import React, { FC, useCallback } from 'react';
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
import { LibraryViewProps } from '../../../../types/interface';
const CollectionsView: FC<LibraryViewProps> = ({ topParentId }) => {
const getBasekey = useCallback(() => {
return 'collections';
}, []);
const getItemTypes = useCallback(() => {
return ['BoxSet'];
}, []);
const getNoItemsMessage = useCallback(() => {
return 'MessageNoCollectionsAvailable';
}, []);
return (
<ViewItemsContainer
topParentId={topParentId}
isBtnFilterEnabled={false}
isBtnNewCollectionEnabled={true}
isAlphaPickerEnabled={false}
getBasekey={getBasekey}
getItemTypes={getItemTypes}
getNoItemsMessage={getNoItemsMessage}
/>
);
};
export default CollectionsView;

View file

@ -0,0 +1,29 @@
import React, { FC, useCallback } from 'react';
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
import { LibraryViewProps } from '../../../../types/interface';
const FavoritesView: FC<LibraryViewProps> = ({ topParentId }) => {
const getBasekey = useCallback(() => {
return 'favorites';
}, []);
const getItemTypes = useCallback(() => {
return ['Movie'];
}, []);
const getNoItemsMessage = useCallback(() => {
return 'MessageNoFavoritesAvailable';
}, []);
return (
<ViewItemsContainer
topParentId={topParentId}
getBasekey={getBasekey}
getItemTypes={getItemTypes}
getNoItemsMessage={getNoItemsMessage}
/>
);
};
export default FavoritesView;

View file

@ -0,0 +1,41 @@
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';
const GenresView: FC<LibraryViewProps> = ({ topParentId }) => {
const [ itemsResult, setItemsResult ] = useState<BaseItemDtoQueryResult>({});
const reloadItems = useCallback(() => {
loading.show();
window.ApiClient.getGenres(
window.ApiClient.getCurrentUserId(),
{
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Movie',
Recursive: true,
EnableTotalRecordCount: false,
ParentId: topParentId
}
).then((result) => {
setItemsResult(result);
loading.hide();
});
}, [topParentId]);
useEffect(() => {
reloadItems();
}, [reloadItems]);
return (
<GenresItemsContainer
topParentId={topParentId}
itemsResult={itemsResult}
/>
);
};
export default GenresView;

View file

@ -0,0 +1,30 @@
import React, { FC, useCallback } from 'react';
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
import { LibraryViewProps } from '../../../../types/interface';
const MoviesView: FC<LibraryViewProps> = ({ topParentId }) => {
const getBasekey = useCallback(() => {
return 'movies';
}, []);
const getItemTypes = useCallback(() => {
return ['Movie'];
}, []);
const getNoItemsMessage = useCallback(() => {
return 'MessageNoItemsAvailable';
}, []);
return (
<ViewItemsContainer
topParentId={topParentId}
isBtnShuffleEnabled={true}
getBasekey={getBasekey}
getItemTypes={getItemTypes}
getNoItemsMessage={getNoItemsMessage}
/>
);
};
export default MoviesView;

View file

@ -0,0 +1,153 @@
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';
const SuggestionsView: FC<LibraryViewProps> = ({ topParentId }) => {
const [ latestItems, setLatestItems ] = useState<BaseItemDto[]>([]);
const [ resumeResult, setResumeResult ] = useState<BaseItemDtoQueryResult>({});
const [ recommendations, setRecommendations ] = useState<RecommendationDto[]>([]);
const element = useRef<HTMLDivElement>(null);
const enableScrollX = useCallback(() => {
return !layoutManager.desktop;
}, []);
const getPortraitShape = useCallback(() => {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}, [enableScrollX]);
const getThumbShape = useCallback(() => {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}, [enableScrollX]);
const autoFocus = useCallback((page) => {
import('../../../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
}, []);
const loadResume = useCallback((page, userId, parentId) => {
loading.show();
const screenWidth = dom.getWindowSize().innerWidth;
const options = {
SortBy: 'DatePlayed',
SortOrder: 'Descending',
IncludeItemTypes: 'Movie',
Filters: 'IsResumable',
Limit: screenWidth >= 1600 ? 5 : 3,
Recursive: true,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
CollapseBoxSetItems: false,
ParentId: parentId,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false
};
window.ApiClient.getItems(userId, options).then(result => {
setResumeResult(result);
loading.hide();
autoFocus(page);
});
}, [autoFocus]);
const loadLatest = useCallback((page: HTMLDivElement, userId: string, parentId: string | null) => {
const options = {
IncludeItemTypes: 'Movie',
Limit: 18,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ParentId: parentId,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false
};
window.ApiClient.getJSON(window.ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(items => {
setLatestItems(items);
autoFocus(page);
});
}, [autoFocus]);
const loadSuggestions = useCallback((page, userId) => {
const screenWidth = dom.getWindowSize().innerWidth;
let itemLimit = 5;
if (screenWidth >= 1600) {
itemLimit = 8;
} else if (screenWidth >= 1200) {
itemLimit = 6;
}
const url = window.ApiClient.getUrl('Movies/Recommendations', {
userId: userId,
categoryLimit: 6,
ItemLimit: itemLimit,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
});
window.ApiClient.getJSON(url).then(result => {
setRecommendations(result);
autoFocus(page);
});
}, [autoFocus]);
const loadSuggestionsTab = useCallback((view) => {
const parentId = topParentId;
const userId = window.ApiClient.getCurrentUserId();
loadResume(view, userId, parentId);
loadLatest(view, userId, parentId);
loadSuggestions(view, userId);
}, [loadLatest, loadResume, loadSuggestions, topParentId]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadSuggestionsTab(page);
}, [loadSuggestionsTab]);
return (
<div ref={element}>
<SectionContainer
sectionTitle={globalize.translate('HeaderContinueWatching')}
enableScrollX={enableScrollX}
items={resumeResult.Items || []}
cardOptions={{
preferThumb: true,
shape: getThumbShape(),
showYear: true
}}
/>
<SectionContainer
sectionTitle={globalize.translate('HeaderLatestMovies')}
enableScrollX={enableScrollX}
items={latestItems}
cardOptions={{
shape: getPortraitShape(),
showYear: true
}}
/>
{!recommendations.length ? <div className='noItemsMessage centerMessage'>
<h1>{globalize.translate('MessageNothingHere')}</h1>
<p>{globalize.translate('MessageNoMovieSuggestionsAvailable')}</p>
</div> : recommendations.map(recommendation => {
return <RecommendationContainer key={recommendation.CategoryId} getPortraitShape={getPortraitShape} enableScrollX={enableScrollX} recommendation={recommendation} />;
})}
</div>
);
};
export default SuggestionsView;

View file

@ -0,0 +1,30 @@
import React, { FC, useCallback } from 'react';
import ViewItemsContainer from '../../../../components/common/ViewItemsContainer';
import { LibraryViewProps } from '../../../../types/interface';
const TrailersView: FC<LibraryViewProps> = ({ topParentId }) => {
const getBasekey = useCallback(() => {
return 'trailers';
}, []);
const getItemTypes = useCallback(() => {
return ['Trailer'];
}, []);
const getNoItemsMessage = useCallback(() => {
return 'MessageNoTrailersFound';
}, []);
return (
<ViewItemsContainer
topParentId={topParentId}
getBasekey={getBasekey}
getItemTypes={getItemTypes}
getNoItemsMessage={getNoItemsMessage}
/>
);
};
export default TrailersView;

View file

@ -0,0 +1,139 @@
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 CollectionsView from './CollectionsView';
import FavoritesView from './FavoritesView';
import GenresView from './GenresView';
import MoviesView from './MoviesView';
import SuggestionsView from './SuggestionsView';
import TrailersView from './TrailersView';
const getDefaultTabIndex = (folderId: string | null) => {
switch (userSettings.get('landing-' + folderId, false)) {
case 'suggestions':
return 1;
case 'favorites':
return 3;
case 'collections':
return 4;
case 'genres':
return 5;
default:
return 0;
}
};
const getTabs = () => {
return [{
name: globalize.translate('Movies')
}, {
name: globalize.translate('Suggestions')
}, {
name: globalize.translate('Trailers')
}, {
name: globalize.translate('Favorites')
}, {
name: globalize.translate('Collections')
}, {
name: globalize.translate('Genres')
}];
};
const Movies: FC = () => {
const [ searchParams ] = useSearchParams();
const currentTabIndex = parseInt(searchParams.get('tab') || getDefaultTabIndex(searchParams.get('topParentId')).toString(), 10);
const [ selectedIndex, setSelectedIndex ] = useState(currentTabIndex);
const element = useRef<HTMLDivElement>(null);
const getTabComponent = (index: number) => {
if (index == null) {
throw new Error('index cannot be null');
}
let component;
switch (index) {
case 0:
component = <MoviesView topParentId={searchParams.get('topParentId')} />;
break;
case 1:
component = <SuggestionsView topParentId={searchParams.get('topParentId')} />;
break;
case 2:
component = <TrailersView topParentId={searchParams.get('topParentId')} />;
break;
case 3:
component = <FavoritesView topParentId={searchParams.get('topParentId')} />;
break;
case 4:
component = <CollectionsView topParentId={searchParams.get('topParentId')} />;
break;
case 5:
component = <GenresView topParentId={searchParams.get('topParentId')} />;
break;
}
return component;
};
const onTabChange = useCallback((e: { detail: { selectedTabIndex: string; }; }) => {
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
setSelectedIndex(newIndex);
}, []);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
mainTabsManager.setTabs(page, selectedIndex, getTabs, undefined, undefined, onTabChange);
if (!page.getAttribute('data-title')) {
const parentId = searchParams.get('topParentId');
if (parentId) {
window.ApiClient.getItem(window.ApiClient.getCurrentUserId(), parentId).then((item) => {
page.setAttribute('data-title', item.Name as string);
libraryMenu.setTitle(item.Name);
});
} else {
page.setAttribute('data-title', globalize.translate('Movies'));
libraryMenu.setTitle(globalize.translate('Movies'));
}
}
}, [onTabChange, searchParams, selectedIndex]);
return (
<div ref={element}>
<Page
id='moviesPage'
className='mainAnimatedPage libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs'
backDropType='movie'
>
{getTabComponent(selectedIndex)}
</Page>
</div>
);
};
export default Movies;

View file

@ -0,0 +1,579 @@
import type { SyncPlayUserAccessType, UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import escapeHTML from 'escape-html';
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');
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const UserEdit: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState<ResetProvider[]>([]);
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
const element = useRef<HTMLDivElement>(null);
const triggerChange = (select: HTMLInputElement) => {
const evt = document.createEvent('HTMLEvents');
evt.initEvent('change', false, true);
select.dispatchEvent(evt);
};
const getUser = () => {
const userId = getParameterByName('userId');
return window.ApiClient.getUser(userId);
};
const loadAuthProviders = useCallback((user, providers) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
setAuthProviders(providers);
const currentProviderId = user.Policy.AuthenticationProviderId;
setAuthenticationProviderId(currentProviderId);
}, []);
const loadPasswordResetProviders = useCallback((user, providers) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
setPasswordResetProviders(providers);
const currentProviderId = user.Policy.PasswordResetProviderId;
setPasswordResetProviderId(currentProviderId);
}, []);
const loadDeleteFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
SupportsMediaDeletion: true
})).then(function (channelsResult) {
let isChecked;
let checkedAttribute;
const itemsArr: ResetProvider[] = [];
for (const folder of mediaFolders) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
Id: folder.Id,
Name: folder.Name,
checkedAttribute: checkedAttribute
});
}
for (const folder of channelsResult.Items) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
Id: folder.Id,
Name: folder.Name,
checkedAttribute: checkedAttribute
});
}
setDeleteFoldersAccess(itemsArr);
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
triggerChange(chkEnableDeleteAllFolders);
});
}, []);
const loadUser = useCallback((user) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
loadAuthProviders(user, providers);
});
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
loadPasswordResetProviders(user, providers);
});
window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
})).then(function (folders) {
loadDeleteFolders(user, folders.Items);
});
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled);
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
txtUserName.disabled = false;
txtUserName.removeAttribute('disabled');
const lnkEditUserPreferences = page.querySelector('.lnkEditUserPreferences') as HTMLDivElement;
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
LibraryMenu.setTitle(user.Name);
setUserName(user.Name);
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name;
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = user.Policy.IsAdministrator;
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
(page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked = user.Policy.EnableCollectionManagement;
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
(page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = user.Policy.EnableLiveTvManagement;
(page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = user.Policy.EnableLiveTvAccess;
(page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = user.Policy.EnableMediaPlayback;
(page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableAudioPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableVideoPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = user.Policy.EnablePlaybackRemuxing;
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = user.Policy.ForceRemoteSourceTranscoding;
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy.RemoteClientBitrateLimit > 0 ?
(user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 }) : '';
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
if (window.ApiClient.isMinServerVersion('10.6.0')) {
(page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = user.Policy.SyncPlayAccess;
}
loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
const loadData = useCallback(() => {
loading.show();
getUser().then(function (user) {
loadUser(user);
});
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const saveUser = (user: UserDto) => {
if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user id or policy');
}
user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
user.Policy.IsAdministrator = (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked;
user.Policy.IsHidden = (page.querySelector('.chkIsHidden') as HTMLInputElement).checked;
user.Policy.IsDisabled = (page.querySelector('.chkDisabled') as HTMLInputElement).checked;
user.Policy.EnableRemoteControlOfOtherUsers = (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked;
user.Policy.EnableLiveTvManagement = (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked;
user.Policy.EnableLiveTvAccess = (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked;
user.Policy.EnableSharedDeviceControl = (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked;
user.Policy.EnableMediaPlayback = (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked;
user.Policy.EnableAudioPlaybackTranscoding = (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked;
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
user.Policy.EnableCollectionManagement = (page.querySelector('.chkEnableCollectionManagement') as HTMLInputElement).checked;
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat((page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value || '0'));
user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0', 10);
user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0', 10);
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 ? [] : 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();
});
};
const onSubmit = (e: Event) => {
loading.show();
getUser().then(function (result) {
saveUser(result);
});
e.preventDefault();
e.stopPropagation();
return false;
};
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked);
});
window.ApiClient.getNamedConfiguration('network').then(function (config) {
(page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess);
});
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadData]);
const optionLoginProvider = authProviders.map((provider) => {
const selected = provider.Id === authenticationProviderId || authProviders.length < 2 ? ' selected' : '';
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
});
const optionPasswordResetProvider = passwordResetProviders.map((provider) => {
const selected = provider.Id === passwordResetProviderId || passwordResetProviders.length < 2 ? ' selected' : '';
return `<option value="${provider.Id}"${selected}>${escapeHTML(provider.Name)}</option>`;
});
const optionSyncPlayAccess = () => {
let content = '';
content += `<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>`;
content += `<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>`;
content += `<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>`;
return content;
};
return (
<Page
id='editUserPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://jellyfin.org/docs/general/server/users/'
/>
</div>
<SectionTabs activeTab='useredit'/>
<div
className='lnkEditUserPreferencesContainer'
style={{ paddingBottom: '1em' }}
>
<LinkEditUserPreferences
className= 'lnkEditUserPreferences button-link'
title= 'ButtonEditOtherUserPreferences'
/>
</div>
<form className='editUserProfileForm'>
<div className='disabledUserBanner hide'>
<div className='btn btnDarkAccent btnStatic'>
<div>
{globalize.translate('HeaderThisUserIsCurrentlyDisabled')}
</div>
<div style={{ marginTop: 5 }}>
{globalize.translate('MessageReenableUser')}
</div>
</div>
</div>
<div id='fldUserName' className='inputContainer'>
<InputElement
type='text'
id='txtUserName'
label='LabelName'
options={'required'}
/>
</div>
<div className='selectContainer fldSelectLoginProvider hide'>
<SelectElement
id='selectLoginProvider'
label='LabelAuthProvider'
>
{optionLoginProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('AuthProviderHelp')}
</div>
</div>
<div className='selectContainer fldSelectPasswordResetProvider hide'>
<SelectElement
id='selectPasswordResetProvider'
label='LabelPasswordResetProvider'
>
{optionPasswordResetProvider}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('PasswordResetProviderHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
<CheckBoxElement
className='chkRemoteAccess'
title='AllowRemoteAccess'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('AllowRemoteAccessHelp')}
</div>
</div>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkIsAdmin'
title='OptionAllowUserToManageServer'
/>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkEnableCollectionManagement'
title='AllowCollectionManagement'
/>
<div id='featureAccessFields' className='verticalSection'>
<h2 className='paperListLabel'>
{globalize.translate('HeaderFeatureAccess')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableLiveTvAccess'
title='OptionAllowBrowsingLiveTv'
/>
<CheckBoxElement
className='chkManageLiveTv'
title='OptionAllowManageLiveTv'
/>
</div>
</div>
<div className='verticalSection'>
<h2 className='paperListLabel'>
{globalize.translate('HeaderPlayback')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableMediaPlayback'
title='OptionAllowMediaPlayback'
/>
<CheckBoxElement
className='chkEnableAudioPlaybackTranscoding'
title='OptionAllowAudioPlaybackTranscoding'
/>
<CheckBoxElement
className='chkEnableVideoPlaybackTranscoding'
title='OptionAllowVideoPlaybackTranscoding'
/>
<CheckBoxElement
className='chkEnableVideoPlaybackRemuxing'
title='OptionAllowVideoPlaybackRemuxing'
/>
<CheckBoxElement
className='chkForceRemoteSourceTranscoding'
title='OptionForceRemoteSourceTranscoding'
/>
</div>
<div className='fieldDescription'>
{globalize.translate('OptionAllowMediaPlaybackTranscodingHelp')}
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtRemoteClientBitrateLimit'
label='LabelRemoteClientBitrateLimit'
options={'inputMode="decimal" pattern="[0-9]*(.[0-9]+)?" min="{0}" step=".25"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelRemoteClientBitrateLimitHelp')}
</div>
<div className='fieldDescription'>
{globalize.translate('LabelUserRemoteClientBitrateLimitHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectSyncPlayAccess'>
<SelectElement
id='selectSyncPlayAccess'
label='LabelSyncPlayAccess'
>
{optionSyncPlayAccess()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('SyncPlayAccessHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<h2 className='checkboxListLabel' style={{ marginBottom: '1em' }}>
{globalize.translate('HeaderAllowMediaDeletionFrom')}
</h2>
<div className='checkboxList paperList checkboxList-paperList'>
<CheckBoxElement
labelClassName='checkboxContainer'
className='chkEnableDeleteAllFolders'
title='AllLibraries'
/>
<div className='deleteAccess'>
{deleteFoldersAccess.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkFolder'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</div>
</div>
</div>
<div className='verticalSection'>
<h2 className='checkboxListLabel'>
{globalize.translate('HeaderRemoteControl')}
</h2>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
<CheckBoxElement
className='chkEnableRemoteControlOtherUsers'
title='OptionAllowRemoteControlOthers'
/>
<CheckBoxElement
className='chkRemoteControlSharedDevices'
title='OptionAllowRemoteSharedDevices'
/>
</div>
<div className='fieldDescription'>
{globalize.translate('OptionAllowRemoteSharedDevicesHelp')}
</div>
</div>
<h2 className='checkboxListLabel'>
{globalize.translate('Other')}
</h2>
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
className='chkEnableDownloading'
title='OptionAllowContentDownload'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionAllowContentDownloadHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
<CheckBoxElement
className='chkDisabled'
title='OptionDisableUser'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionDisableUserHelp')}
</div>
</div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
<CheckBoxElement
className='chkIsHidden'
title='OptionHideUser'
/>
<div className='fieldDescription checkboxFieldDescription'>
{globalize.translate('OptionHideUserFromLoginHelp')}
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer' id='fldLoginAttemptsBeforeLockout'>
<InputElement
type='number'
id='txtLoginAttemptsBeforeLockout'
label='LabelUserLoginAttemptsBeforeLockout'
options={'min={-1} step={1}'}
/>
<div className='fieldDescription'>
{globalize.translate('OptionLoginAttemptsBeforeLockout')}
</div>
<div className='fieldDescription'>
{globalize.translate('OptionLoginAttemptsBeforeLockoutHelp')}
</div>
</div>
</div>
<br />
<div className='verticalSection'>
<div className='inputContainer' id='fldMaxActiveSessions'>
<InputElement
type='number'
id='txtMaxActiveSessions'
label='LabelUserMaxActiveSessions'
options={'min={0} step={1}'}
/>
<div className='fieldDescription'>
{globalize.translate('OptionMaxActiveSessions')}
</div>
<div className='fieldDescription'>
{globalize.translate('OptionMaxActiveSessionsHelp')}
</div>
</div>
</div>
<br />
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
<ButtonElement
type='button'
id='btnCancel'
className='raised button-cancel block'
title='ButtonCancel'
/>
</div>
</form>
</div>
</Page>
);
};
export default UserEdit;

View file

@ -0,0 +1,321 @@
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';
type ItemsArr = {
Name?: string;
Id?: string;
AppName?: string;
checkedAttribute?: string
};
const UserLibraryAccess: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
const [devicesItems, setDevicesItems] = useState<ItemsArr[]>([]);
const element = useRef<HTMLDivElement>(null);
const triggerChange = (select: HTMLInputElement) => {
const evt = document.createEvent('HTMLEvents');
evt.initEvent('change', false, true);
select.dispatchEvent(evt);
};
const loadMediaFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const folder of mediaFolders) {
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
Id: folder.Id,
Name: folder.Name,
checkedAttribute: checkedAttribute
});
}
setMediaFoldersItems(itemsArr);
const chkEnableAllFolders = page.querySelector('.chkEnableAllFolders') as HTMLInputElement;
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
triggerChange(chkEnableAllFolders);
}, []);
const loadChannels = useCallback((user, channels) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const folder of channels) {
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
Id: folder.Id,
Name: folder.Name,
checkedAttribute: checkedAttribute
});
}
setChannelsItems(itemsArr);
if (channels.length) {
(page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.remove('hide');
} else {
(page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.add('hide');
}
const chkEnableAllChannels = page.querySelector('.chkEnableAllChannels') as HTMLInputElement;
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
triggerChange(chkEnableAllChannels);
}, []);
const loadDevices = useCallback((user, devices) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const device of devices) {
const isChecked = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
Id: device.Id,
Name: device.Name,
AppName: device.AppName,
checkedAttribute: checkedAttribute
});
}
setDevicesItems(itemsArr);
const chkEnableAllDevices = page.querySelector('.chkEnableAllDevices') as HTMLInputElement;
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
triggerChange(chkEnableAllDevices);
if (user.Policy.IsAdministrator) {
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.remove('hide');
}
}, []);
const loadUser = useCallback((user, mediaFolders, channels, devices) => {
setUserName(user.Name);
libraryMenu.setTitle(user.Name);
loadChannels(user, channels);
loadMediaFolders(user, mediaFolders);
loadDevices(user, devices);
loading.hide();
}, [loadChannels, loadDevices, loadMediaFolders]);
const loadData = useCallback(() => {
loading.show();
const userId = getParameterByName('userId');
const promise1 = userId ? window.ApiClient.getUser(userId) : Promise.resolve({ Configuration: {} });
const promise2 = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promise3 = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
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);
});
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const onSubmit = (e: Event) => {
loading.show();
const userId = getParameterByName('userId');
window.ApiClient.getUser(userId).then(function (result) {
saveUser(result);
});
e.preventDefault();
e.stopPropagation();
return false;
};
const saveUser = (user: UserDto) => {
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllDevices = (page.querySelector('.chkEnableAllDevices') as HTMLInputElement).checked;
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkDevice'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.BlockedChannels = null;
user.Policy.BlockedMediaFolders = null;
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
};
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
};
(page.querySelector('.chkEnableAllDevices') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.deviceAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
});
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.channelAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
});
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.folderAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
});
(page.querySelector('.userLibraryAccessForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadData]);
return (
<Page
id='userLibraryAccessPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://jellyfin.org/docs/general/server/users/'
/>
</div>
<SectionTabs activeTab='userlibraryaccess'/>
<form className='userLibraryAccessForm'>
<AccessContainer
containerClassName='folderAccessContainer'
headerTitle='HeaderLibraryAccess'
checkBoxClassName='chkEnableAllFolders'
checkBoxTitle='OptionEnableAccessToAllLibraries'
listContainerClassName='folderAccessListContainer'
accessClassName='folderAccess'
listTitle='HeaderLibraries'
description='LibraryAccessHelp'
>
{mediaFoldersItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkFolder'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<AccessContainer
containerClassName='channelAccessContainer hide'
headerTitle='HeaderChannelAccess'
checkBoxClassName='chkEnableAllChannels'
checkBoxTitle='OptionEnableAccessToAllChannels'
listContainerClassName='channelAccessListContainer'
accessClassName='channelAccess'
listTitle='Channels'
description='ChannelAccessHelp'
>
{channelsItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkChannel'
itemId={Item.Id}
itemName={Item.Name}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<AccessContainer
containerClassName='deviceAccessContainer hide'
headerTitle='HeaderDeviceAccess'
checkBoxClassName='chkEnableAllDevices'
checkBoxTitle='OptionEnableAccessFromAllDevices'
listContainerClassName='deviceAccessListContainer'
accessClassName='deviceAccess'
listTitle='HeaderDevices'
description='DeviceAccessHelp'
>
{devicesItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkDevice'
itemId={Item.Id}
itemName={Item.Name}
itemAppName={Item.AppName}
itemCheckedAttribute={Item.checkedAttribute}
/>
))}
</AccessContainer>
<br />
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
</div>
</form>
</div>
</Page>
);
};
export default UserLibraryAccess;

View file

@ -0,0 +1,262 @@
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';
type userInput = {
Name?: string;
Password?: string;
};
type ItemsArr = {
Name?: string;
Id?: string;
};
const UserNew: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const element = useRef<HTMLDivElement>(null);
const getItemsResult = (items: ItemsArr[]) => {
return items.map(item =>
({
Id: item.Id,
Name: item.Name
})
);
};
const loadMediaFolders = useCallback((result) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const mediaFolders = getItemsResult(result);
setMediaFoldersItems(mediaFolders);
const folderAccess = page.querySelector('.folderAccess') as HTMLDivElement;
folderAccess.dispatchEvent(new CustomEvent('create'));
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked = false;
}, []);
const loadChannels = useCallback((result) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const channels = getItemsResult(result);
setChannelsItems(channels);
const channelAccess = page.querySelector('.channelAccess') as HTMLDivElement;
channelAccess.dispatchEvent(new CustomEvent('create'));
const channelAccessContainer = page.querySelector('.channelAccessContainer') as HTMLDivElement;
channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide');
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked = false;
}, []);
const loadUser = useCallback(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
(page.querySelector('#txtUsername') as HTMLInputElement).value = '';
(page.querySelector('#txtPassword') as HTMLInputElement).value = '';
loading.show();
const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promiseChannels = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
loadMediaFolders(responses[0].Items);
loadChannels(responses[1].Items);
loading.hide();
});
}, [loadChannels, loadMediaFolders]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadUser();
const saveUser = () => {
const userInput: userInput = {};
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 || !user.Policy) {
throw new Error('Unexpected null user id or policy');
}
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
user.Policy.EnabledFolders = [];
if (!user.Policy.EnableAllFolders) {
user.Policy.EnabledFolders = Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = [];
if (!user.Policy.EnableAllChannels) {
user.Policy.EnabledChannels = Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
Dashboard.navigate('useredit.html?userId=' + user.Id);
});
}, function () {
toast(globalize.translate('ErrorDefault'));
loading.hide();
});
};
const onSubmit = (e: Event) => {
loading.show();
saveUser();
e.preventDefault();
e.stopPropagation();
return false;
};
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const channelAccessListContainer = page.querySelector('.channelAccessListContainer') as HTMLDivElement;
this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide');
});
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const folderAccessListContainer = page.querySelector('.folderAccessListContainer') as HTMLDivElement;
this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide');
});
(page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadUser]);
return (
<Page
id='newUserPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('HeaderAddUser')}
url='https://jellyfin.org/docs/general/server/users/'
/>
</div>
<form className='newUserProfileForm'>
<div className='inputContainer'>
<InputElement
type='text'
id='txtUsername'
label='LabelName'
options={'required'}
/>
</div>
<div className='inputContainer'>
<InputElement
type='password'
id='txtPassword'
label='LabelPassword'
/>
</div>
<AccessContainer
containerClassName='folderAccessContainer'
headerTitle='HeaderLibraryAccess'
checkBoxClassName='chkEnableAllFolders'
checkBoxTitle='OptionEnableAccessToAllLibraries'
listContainerClassName='folderAccessListContainer'
accessClassName='folderAccess'
listTitle='HeaderLibraries'
description='LibraryAccessHelp'
>
{mediaFoldersItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkFolder'
itemId={Item.Id}
itemName={Item.Name}
/>
))}
</AccessContainer>
<AccessContainer
containerClassName='channelAccessContainer verticalSection-extrabottompadding hide'
headerTitle='HeaderChannelAccess'
checkBoxClassName='chkEnableAllChannels'
checkBoxTitle='OptionEnableAccessToAllChannels'
listContainerClassName='channelAccessListContainer'
accessClassName='channelAccess'
listTitle='Channels'
description='ChannelAccessHelp'
>
{channelsItems.map(Item => (
<CheckBoxElement
key={Item.Id}
className='chkChannel'
itemId={Item.Id}
itemName={Item.Name}
/>
))}
</AccessContainer>
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
<ButtonElement
type='button'
id='btnCancel'
className='raised button-cancel block'
title='ButtonCancel'
/>
</div>
</form>
</div>
</Page>
);
};
export default UserNew;

View file

@ -0,0 +1,425 @@
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 escapeHTML from 'escape-html';
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('');
const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ blockedTags, setBlockedTags ] = useState([]);
const element = useRef<HTMLDivElement>(null);
const populateRatings = useCallback((allParentalRatings) => {
let rating;
const ratings: ParentalRating[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
if (ratings.length) {
const lastRating = ratings[ratings.length - 1];
if (lastRating.Value === rating.Value) {
lastRating.Name += '/' + rating.Name;
continue;
}
}
ratings.push({
Name: rating.Name,
Value: rating.Value
});
}
setParentalRatings(ratings);
}, []);
const loadUnratedItems = useCallback((user) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const items = [{
name: globalize.translate('Books'),
value: 'Book'
}, {
name: globalize.translate('Channels'),
value: 'ChannelContent'
}, {
name: globalize.translate('LiveTV'),
value: 'LiveTvChannel'
}, {
name: globalize.translate('Movies'),
value: 'Movie'
}, {
name: globalize.translate('Music'),
value: 'Music'
}, {
name: globalize.translate('Trailers'),
value: 'Trailer'
}, {
name: globalize.translate('Shows'),
value: 'Series'
}];
const itemsArr: UnratedItem[] = [];
for (const item of items) {
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
value: item.value,
name: item.name,
checkedAttribute: checkedAttribute
});
}
setUnratedItems(itemsArr);
const blockUnratedItems = page.querySelector('.blockUnratedItems') as HTMLDivElement;
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
}, []);
const loadBlockedTags = useCallback((tags) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setBlockedTags(tags);
const blockedTagsElem = page.querySelector('.blockedTags') as HTMLDivElement;
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
btnDeleteTag.addEventListener('click', function () {
const tag = btnDeleteTag.getAttribute('data-tag');
const newTags = tags.filter(function (t: string) {
return t != tag;
});
loadBlockedTags(newTags);
});
}
}, []);
const renderAccessSchedule = useCallback((schedules) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setAccessSchedules(schedules);
const accessScheduleList = page.querySelector('.accessScheduleList') as HTMLDivElement;
for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) {
btnDelete.addEventListener('click', function () {
const index = parseInt(btnDelete.getAttribute('data-index') || '0', 10);
schedules.splice(index, 1);
const newindex = schedules.filter(function (i: number) {
return i != index;
});
renderAccessSchedule(newindex);
});
}
}, []);
const loadUser = useCallback((user, allParentalRatings) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setUserName(user.Name);
LibraryMenu.setTitle(user.Name);
loadUnratedItems(user);
loadBlockedTags(user.Policy.BlockedTags);
populateRatings(allParentalRatings);
let ratingValue = '';
if (user.Policy.MaxParentalRating != null) {
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
const rating = allParentalRatings[i];
if (user.Policy.MaxParentalRating >= rating.Value) {
ratingValue = rating.Value;
}
}
}
(page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
if (user.Policy.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
}
renderAccessSchedule(user.Policy.AccessSchedules || []);
loading.hide();
}, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
const loadData = useCallback(() => {
loading.show();
const userId = getParameterByName('userId');
const promise1 = window.ApiClient.getUser(userId);
const promise2 = window.ApiClient.getParentalRatings();
Promise.all([promise1, promise2]).then(function (responses) {
loadUser(responses[0], responses[1]);
});
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
};
const saveUser = (user: UserDto) => {
if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user id or policy');
}
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);
user.Policy.MaxParentalRating = Number.isNaN(parentalRating) ? null : parentalRating;
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-itemtype');
});
user.Policy.AccessSchedules = getSchedulesFromPage();
user.Policy.BlockedTags = getBlockedTagsFromPage();
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
};
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
schedule = schedule || {};
import('../../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
accessschedule.show({
schedule: schedule
}).then(function (updatedSchedule) {
const schedules = getSchedulesFromPage();
if (index == -1) {
index = schedules.length;
}
schedules[index] = updatedSchedule;
renderAccessSchedule(schedules);
});
});
};
const getSchedulesFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.liSchedule'), function (elem) {
return {
DayOfWeek: elem.getAttribute('data-day'),
StartHour: elem.getAttribute('data-start'),
EndHour: elem.getAttribute('data-end')
};
}) as AccessSchedule[];
};
const getBlockedTagsFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
return elem.getAttribute('data-tag');
}) as string[];
};
const showBlockedTagPopup = () => {
import('../../../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({
label: globalize.translate('LabelTag')
}).then(function (value) {
const tags = getBlockedTagsFromPage();
if (tags.indexOf(value) == -1) {
tags.push(value);
loadBlockedTags(tags);
}
});
});
};
const onSubmit = (e: Event) => {
loading.show();
const userId = getParameterByName('userId');
window.ApiClient.getUser(userId).then(function (result) {
saveUser(result);
});
e.preventDefault();
e.stopPropagation();
return false;
};
(page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
showSchedulePopup({
Id: 0,
UserId: '',
DayOfWeek: DynamicDayOfWeek.Sunday,
StartHour: 0,
EndHour: 0
}, -1);
});
(page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup();
});
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadBlockedTags, loadData, renderAccessSchedule]);
const optionMaxParentalRating = () => {
let content = '';
content += '<option value=\'\'></option>';
for (const rating of parentalRatings) {
content += `<option value='${rating.Value}'>${escapeHTML(rating.Name)}</option>`;
}
return content;
};
return (
<Page
id='userParentalControlPage'
className='mainAnimatedPage type-interior'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://jellyfin.org/docs/general/server/users/'
/>
</div>
<SectionTabs activeTab='userparentalcontrol'/>
<form className='userParentalControlForm'>
<div className='selectContainer'>
<SelectElement
id='selectMaxParentalRating'
label='LabelMaxParentalRating'
>
{optionMaxParentalRating()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')}
</div>
</div>
<div>
<div className='blockUnratedItems'>
<h3 className='checkboxListLabel'>
{globalize.translate('HeaderBlockItemsWithNoRating')}
</h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => {
return <CheckBoxElement
key={Item.value}
className='chkUnratedItem'
itemType={Item.value}
itemName={Item.name}
itemCheckedAttribute={Item.checkedAttribute}
/>;
})}
</div>
</div>
</div>
<br />
<div className='verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
SectionClassName='detailSectionHeader'
title={globalize.translate('LabelBlockContentWithTags')}
isBtnVisible={true}
btnId='btnAddBlockedTag'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<div className='blockedTags' style={{ marginTop: '.5em' }}>
{blockedTags.map(tag => {
return <BlockedTagList
key={tag}
tag={tag}
/>;
})}
</div>
</div>
<div className='accessScheduleSection verticalSection' style={{ marginBottom: '2em' }}>
<SectionTitleContainer
title={globalize.translate('HeaderAccessSchedule')}
isBtnVisible={true}
btnId='btnAddSchedule'
btnClassName='fab submit sectionTitleButton'
btnTitle='Add'
btnIcon='add'
isLinkVisible={false}
/>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => {
return <AccessScheduleList
key={accessSchedule.Id}
index={index}
Id={accessSchedule.Id}
DayOfWeek={accessSchedule.DayOfWeek}
StartHour={accessSchedule.StartHour}
EndHour={accessSchedule.EndHour}
/>;
})}
</div>
</div>
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
</div>
</form>
</div>
</Page>
);
};
export default UserParentalControl;

View file

@ -0,0 +1,52 @@
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';
const UserPassword: FunctionComponent = () => {
const userId = getParameterByName('userId');
const [ userName, setUserName ] = useState('');
const loadUser = useCallback(() => {
loading.show();
window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) {
throw new Error('Unexpected null user.Name');
}
setUserName(user.Name);
loading.hide();
});
}, [userId]);
useEffect(() => {
loadUser();
}, [loadUser]);
return (
<Page
id='userPasswordPage'
className='mainAnimatedPage type-interior userPasswordPage'
>
<div className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://jellyfin.org/docs/general/server/users/'
/>
</div>
<SectionTabs activeTab='userpassword'/>
<div className='readOnlyContent'>
<UserPasswordForm
userId={userId}
/>
</div>
</div>
</Page>
);
};
export default UserPassword;

View file

@ -0,0 +1,197 @@
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';
const UserProfile: FunctionComponent = () => {
const userId = getParameterByName('userId');
const [ userName, setUserName ] = useState('');
const element = useRef<HTMLDivElement>(null);
const reloadUser = useCallback(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loading.show();
window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name || !user.Id) {
throw new Error('Unexpected null user name or id');
}
setUserName(user.Name);
LibraryMenu.setTitle(user.Name);
let imageUrl = 'assets/img/avatar.png';
if (user.PrimaryImageTag) {
imageUrl = window.ApiClient.getUserImageUrl(user.Id, {
tag: user.PrimaryImageTag,
type: 'Primary'
});
}
const userImage = (page.querySelector('#image') as HTMLDivElement);
userImage.style.backgroundImage = 'url(' + imageUrl + ')';
Dashboard.getCurrentUser().then(function (loggedInUser: UserDto) {
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
if (user.PrimaryImageTag) {
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.remove('hide');
} else if (appHost.supports('fileinput') && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide');
}
});
loading.hide();
});
}, [userId]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
reloadUser();
const onFileReaderError = (evt: ProgressEvent<FileReader>) => {
loading.hide();
switch (evt.target?.error?.code) {
case DOMException.NOT_FOUND_ERR:
toast(globalize.translate('FileNotFound'));
break;
case DOMException.ABORT_ERR:
onFileReaderAbort();
break;
default:
toast(globalize.translate('FileReadError'));
}
};
const onFileReaderAbort = () => {
loading.hide();
toast(globalize.translate('FileReadCancelled'));
};
const setFiles = (evt: Event) => {
const userImage = (page.querySelector('#image') as HTMLDivElement);
const target = evt.target as HTMLInputElement;
const file = (target.files as FileList)[0];
if (!file || !file.type.match('image.*')) {
return false;
}
const reader: FileReader = new FileReader();
reader.onerror = onFileReaderError;
reader.onabort = onFileReaderAbort;
reader.onload = () => {
userImage.style.backgroundImage = 'url(' + reader.result + ')';
window.ApiClient.uploadUserImage(userId, ImageType.Primary, file).then(function () {
loading.hide();
reloadUser();
});
};
reader.readAsDataURL(file);
};
(page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () {
confirm(
globalize.translate('DeleteImageConfirmation'),
globalize.translate('DeleteImage')
).then(function () {
loading.show();
window.ApiClient.deleteUserImage(userId, ImageType.Primary).then(function () {
loading.hide();
reloadUser();
});
});
});
(page.querySelector('#btnAddImage') as HTMLButtonElement).addEventListener('click', function () {
const uploadImage = page.querySelector('#uploadImage') as HTMLInputElement;
uploadImage.value = '';
uploadImage.click();
});
(page.querySelector('#uploadImage') as HTMLInputElement).addEventListener('change', function (evt: Event) {
setFiles(evt);
});
}, [reloadUser, userId]);
return (
<Page
id='userProfilePage'
title={globalize.translate('Profile')}
className='mainAnimatedPage libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage'
>
<div ref={element} className='padded-left padded-right padded-bottom-page'>
<div
className='readOnlyContent'
style={{ margin: '0 auto', marginBottom: '1.8em', padding: '0 1em', display: 'flex', flexDirection: 'row', alignItems: 'center' }}
>
<div
className='imagePlaceHolder'
style={{ position: 'relative', display: 'inline-block', maxWidth: 200 }}
>
<input
id='uploadImage'
type='file'
accept='image/*'
style={{ position: 'absolute', right: 0, width: '100%', height: '100%', opacity: 0, cursor: 'pointer' }}
/>
<div
id='image'
style={{ width: 200, height: 200, backgroundRepeat: 'no-repeat', backgroundPosition: 'center', borderRadius: '100%', backgroundSize: 'cover' }}
/>
</div>
<div style={{ verticalAlign: 'top', margin: '1em 2em', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<h2 className='username' style={{ margin: 0, fontSize: 'xx-large' }}>
{userName}
</h2>
<br />
<ButtonElement
type='button'
id='btnAddImage'
className='raised button-submit hide'
title='ButtonAddImage'
/>
<ButtonElement
type='button'
id='btnDeleteImage'
className='raised hide'
title='DeleteImage'
/>
</div>
</div>
<UserPasswordForm
userId={userId}
/>
</div>
</Page>
);
};
export default UserProfile;

View file

@ -0,0 +1,163 @@
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';
type MenuEntry = {
name?: string;
id?: string;
icon?: string;
};
const UserProfiles: FunctionComponent = () => {
const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef<HTMLDivElement>(null);
const loadData = () => {
loading.show();
window.ApiClient.getUsers().then(function (result) {
setUsers(result);
loading.hide();
});
};
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const showUserMenu = (elem: HTMLElement) => {
const card = dom.parentWithClass(elem, 'card');
const userId = card.getAttribute('data-userid');
if (!userId) {
console.error('Unexpected null user id');
return;
}
const menuItems: MenuEntry[] = [];
menuItems.push({
name: globalize.translate('ButtonOpen'),
id: 'open',
icon: 'mode_edit'
});
menuItems.push({
name: globalize.translate('ButtonLibraryAccess'),
id: 'access',
icon: 'lock'
});
menuItems.push({
name: globalize.translate('ButtonParentalControl'),
id: 'parentalcontrol',
icon: 'person'
});
menuItems.push({
name: globalize.translate('Delete'),
id: 'delete',
icon: 'delete'
});
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);
break;
case 'access':
Dashboard.navigate('userlibraryaccess.html?userId=' + userId);
break;
case 'parentalcontrol':
Dashboard.navigate('userparentalcontrol.html?userId=' + userId);
break;
case 'delete':
deleteUser(userId);
}
}
});
});
};
const deleteUser = (id: string) => {
const msg = globalize.translate('DeleteUserConfirmation');
confirm({
title: globalize.translate('DeleteUser'),
text: msg,
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(function () {
loading.show();
window.ApiClient.deleteUser(id).then(function () {
loadData();
});
});
};
page.addEventListener('click', function (e) {
const btnUserMenu = dom.parentWithClass(e.target as HTMLElement, 'btnUserMenu');
if (btnUserMenu) {
showUserMenu(btnUserMenu);
}
});
(page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('usernew.html');
});
}, []);
return (
<Page
id='userProfilesPage'
className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('HeaderUsers')}
isBtnVisible={true}
btnId='btnAddUser'
btnClassName='fab submit sectionTitleButton'
btnTitle='ButtonAddUser'
btnIcon='add'
url='https://jellyfin.org/docs/general/server/users/adding-managing-users'
/>
</div>
<div className='localUsers itemsContainer vertical-wrap'>
{users.map(user => {
return <UserCardBox key={user.Id} user={user} />;
})}
</div>
</div>
</Page>
);
};
export default UserProfiles;

View file

@ -0,0 +1,43 @@
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';
const Search: FunctionComponent = () => {
const [ query, setQuery ] = useState<string>();
const [ searchParams ] = useSearchParams();
return (
<Page
id='searchPage'
title={globalize.translate('Search')}
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
>
<SearchFields onSearch={setQuery} />
{!query
&& <SearchSuggestions
parentId={searchParams.get('parentId')}
/>
}
<SearchResults
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
parentId={searchParams.get('parentId')}
collectionType={searchParams.get('collectionType')}
query={query}
/>
<LiveTVSearchResults
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
parentId={searchParams.get('parentId')}
collectionType={searchParams.get('collectionType')}
query={query}
/>
</Page>
);
};
export default Search;