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

Merge pull request #3733 from grafixeyehero/Migrate-to-react-router

Migrate to react router
This commit is contained in:
Bill Thornton 2022-07-27 00:49:13 -04:00 committed by GitHub
commit f0d29e8175
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 616 additions and 722 deletions

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent, useEffect, useState } from 'react'; import React, { FunctionComponent, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom';
import alert from './alert'; import alert from './alert';
import { appRouter } from './appRouter'; import { appRouter } from './appRouter';
@ -33,7 +33,6 @@ type ConnectionRequiredProps = {
* If a condition fails, this component will navigate to the appropriate page. * If a condition fails, this component will navigate to the appropriate page.
*/ */
const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
children,
isAdminRequired = false, isAdminRequired = false,
isUserRequired = true isUserRequired = true
}) => { }) => {
@ -162,7 +161,9 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
} }
return ( return (
<>{children}</> <div className='skinBody'>
<Outlet />
</div>
); );
}; };

View file

@ -6,6 +6,7 @@ type PageProps = {
id: string, // id is required for libraryMenu id: string, // id is required for libraryMenu
title?: string, title?: string,
isBackButtonEnabled?: boolean, isBackButtonEnabled?: boolean,
isMenuButtonEnabled?: boolean,
isNowPlayingBarEnabled?: boolean, isNowPlayingBarEnabled?: boolean,
isThemeMediaSupported?: boolean isThemeMediaSupported?: boolean
}; };
@ -20,6 +21,7 @@ const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
className = '', className = '',
title, title,
isBackButtonEnabled = true, isBackButtonEnabled = true,
isMenuButtonEnabled = false,
isNowPlayingBarEnabled = true, isNowPlayingBarEnabled = true,
isThemeMediaSupported = false isThemeMediaSupported = false
}) => { }) => {
@ -60,6 +62,7 @@ const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
className={`page ${className}`} className={`page ${className}`}
data-title={title} data-title={title}
data-backbutton={`${isBackButtonEnabled}`} data-backbutton={`${isBackButtonEnabled}`}
data-menubutton={`${isMenuButtonEnabled}`}
> >
{children} {children}
</div> </div>

View file

@ -9,7 +9,6 @@ import loading from './loading/loading';
import viewManager from './viewManager/viewManager'; import viewManager from './viewManager/viewManager';
import ServerConnections from './ServerConnections'; import ServerConnections from './ServerConnections';
import alert from './alert'; import alert from './alert';
import reactControllerFactory from './reactControllerFactory';
export const history = createHashHistory(); export const history = createHashHistory();
@ -264,9 +263,7 @@ class AppRouter {
this.#sendRouteToViewManager(ctx, next, route, controllerFactory); this.#sendRouteToViewManager(ctx, next, route, controllerFactory);
}; };
if (route.pageComponent) { if (route.controller) {
onInitComplete(reactControllerFactory);
} else if (route.controller) {
import('../controllers/' + route.controller).then(onInitComplete); import('../controllers/' + route.controller).then(onInitComplete);
} else { } else {
onInitComplete(); onInitComplete();
@ -293,7 +290,6 @@ class AppRouter {
fullscreen: route.fullscreen, fullscreen: route.fullscreen,
controllerFactory: controllerFactory, controllerFactory: controllerFactory,
options: { options: {
pageComponent: route.pageComponent,
supportsThemeMedia: route.supportsThemeMedia || false, supportsThemeMedia: route.supportsThemeMedia || false,
enableMediaControl: route.enableMediaControl !== false enableMediaControl: route.enableMediaControl !== false
}, },

View file

@ -1,6 +1,6 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
import CheckBoxElement from './CheckBoxElement'; import CheckBoxElement from '../../../elements/CheckBoxElement';
type IProps = { type IProps = {
containerClassName?: string; containerClassName?: string;
@ -18,7 +18,11 @@ const AccessContainer: FunctionComponent<IProps> = ({containerClassName, headerT
return ( return (
<div className={containerClassName}> <div className={containerClassName}>
<h2>{globalize.translate(headerTitle)}</h2> <h2>{globalize.translate(headerTitle)}</h2>
<CheckBoxElement labelClassName='checkboxContainer' type='checkbox' className={checkBoxClassName} title={checkBoxTitle} /> <CheckBoxElement
labelClassName='checkboxContainer'
className={checkBoxClassName}
title={checkBoxTitle}
/>
<div className={listContainerClassName}> <div className={listContainerClassName}>
<div className={accessClassName}> <div className={accessClassName}>
<h3 className='checkboxListLabel'> <h3 className='checkboxListLabel'>

View file

@ -1,17 +1,7 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import datetime from '../../../scripts/datetime'; import datetime from '../../../scripts/datetime';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
import IconButtonElement from '../../../elements/IconButtonElement';
const createButtonElement = (index: number) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
class='btnDelete listItemButton'
data-index='${index}'
>
<span class='material-icons delete' aria-hidden='true' />
</button>`
});
type IProps = { type IProps = {
index: number; index: number;
@ -48,8 +38,12 @@ const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartH
{getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)} {getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)}
</div> </div>
</div> </div>
<div <IconButtonElement
dangerouslySetInnerHTML={createButtonElement(index)} is='paper-icon-button-light'
className='btnDelete listItemButton'
title='Delete'
icon='delete'
dataIndex={index}
/> />
</div> </div>
); );

View file

@ -1,15 +1,5 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import IconButtonElement from '../../../elements/IconButtonElement';
const createButtonElement = (tag?: string) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
class='blockedTag btnDeleteTag listItemButton'
data-tag='${tag}'
>
<span class='material-icons delete' aria-hidden='true' />
</button>`
});
type IProps = { type IProps = {
tag?: string; tag?: string;
@ -24,11 +14,14 @@ const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
{tag} {tag}
</h3> </h3>
</div> </div>
<div <IconButtonElement
dangerouslySetInnerHTML={createButtonElement(tag)} is='paper-icon-button-light'
className='blockedTag btnDeleteTag listItemButton'
title='Delete'
icon='delete'
dataTag={tag}
/> />
</div> </div>
</div> </div>
); );
}; };

View file

@ -1,32 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createButtonElement = ({ type, className, title }: { type?: string, className?: string, title?: string }) => ({
__html: `<button
is="emby-button"
type="${type}"
class="${className}"
>
<span>${title}</span>
</button>`
});
type IProps = {
type?: string;
className?: string;
title?: string
}
const ButtonElement: FunctionComponent<IProps> = ({ type, className, title }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
type: type,
className: className,
title: globalize.translate(title)
})}
/>
);
};
export default ButtonElement;

View file

@ -1,36 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, type, className, title }: { labelClassName?: string, type?: string, className?: string, title?: string }) => ({
__html: `<label class="${labelClassName}">
<input
is="emby-checkbox"
type="${type}"
class="${className}"
/>
<span>${title}</span>
</label>`
});
type IProps = {
labelClassName?: string;
type?: string;
className?: string;
title?: string
}
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, type, className, title }: IProps) => {
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
labelClassName: labelClassName ? labelClassName : '',
type: type,
className: className,
title: globalize.translate(title)
})}
/>
);
};
export default CheckBoxElement;

View file

@ -1,41 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
type IProps = {
className?: string;
Name?: string;
Id?: string;
ItemType?: string;
AppName?: string;
checkedAttribute?: string;
}
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}: {className?: string, Name?: string, dataAttributes?: string, AppName?: string, checkedAttribute?: string}) => ({
__html: `<label>
<input
type="checkbox"
is="emby-checkbox"
class="${className}"
${dataAttributes} ${checkedAttribute}
/>
<span>${escapeHtml(Name || '')} ${AppName}</span>
</label>`
});
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, ItemType, AppName, checkedAttribute}: IProps) => {
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
className: className,
Name: Name,
dataAttributes: ItemType ? `data-itemtype='${ItemType}'` : `data-id='${Id}'`,
AppName: AppName ? `- ${AppName}` : '',
checkedAttribute: checkedAttribute
})}
/>
);
};
export default CheckBoxListItem;

View file

@ -1,34 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
type IProps = {
title: string;
className?: string;
icon: string,
}
const createButtonElement = ({ className, title, icon }: { className?: string, title: string, icon: string }) => ({
__html: `<button
is="emby-button"
type="button"
class="${className}"
style="margin-left:1em;"
title="${title}"
>
<span class="material-icons ${icon}" aria-hidden="true"></span>
</button>`
});
const SectionTitleButtonElement: FunctionComponent<IProps> = ({ className, title, icon }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
className: className,
title: globalize.translate(title),
icon: icon
})}
/>
);
};
export default SectionTitleButtonElement;

View file

@ -1,35 +0,0 @@
import React, { FunctionComponent } from 'react';
import SectionTitleButtonElement from './SectionTitleButtonElement';
import SectionTitleLinkElement from './SectionTitleLinkElement';
type IProps = {
title: string;
isBtnVisible?: boolean;
titleLink?: string;
}
const SectionTitleContainer: FunctionComponent<IProps> = ({title, isBtnVisible = false, titleLink}: IProps) => {
return (
<div className='verticalSection'>
<div className='sectionTitleContainer flex align-items-center'>
<h2 className='sectionTitle'>
{title}
</h2>
{isBtnVisible && <SectionTitleButtonElement
className='fab btnAddUser submit sectionTitleButton'
title='ButtonAddUser'
icon='add'
/>}
<SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url={titleLink}
/>
</div>
</div>
);
};
export default SectionTitleContainer;

View file

@ -1,44 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string[] }) => ({
__html: `<select
class="${className}"
is="emby-select"
label="${label}"
>
${option}
</select>`
});
type ProvidersArr = {
Name?: string;
Id?: string;
}
type IProps = {
className?: string;
label?: string;
currentProviderId: string;
providers: ProvidersArr[]
}
const SelectElement: FunctionComponent<IProps> = ({ className, label, currentProviderId, providers }: IProps) => {
const renderOption = providers.map((provider) => {
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
return '<option value="' + provider.Id + '"' + selected + '>' + escapeHtml(provider.Name) + '</option>';
});
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
label: globalize.translate(label),
option: renderOption
})}
/>
);
};
export default SelectElement;

View file

@ -1,47 +0,0 @@
import escapeHtml from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string }) => ({
__html: `<select
class="${className}"
is="emby-select"
label="${label}"
>
<option value=''></option>
${option}
</select>`
});
type RatingsArr = {
Name: string;
Value: number;
}
type IProps = {
className?: string;
label?: string;
parentalRatings: RatingsArr[];
}
const SelectMaxParentalRating: FunctionComponent<IProps> = ({ className, label, parentalRatings }: IProps) => {
const renderOption = () => {
let content = '';
for (const rating of parentalRatings) {
content += `<option value='${rating.Value}'>${escapeHtml(rating.Name)}</option>`;
}
return content;
};
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
label: globalize.translate(label),
option: renderOption()
})}
/>
);
};
export default SelectMaxParentalRating;

View file

@ -1,35 +0,0 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, id, label }: { className?: string, id?: string, label: string }) => ({
__html: `<select
class="${className}"
is="emby-select"
id="${id}"
label="${label}"
>
<option value='CreateAndJoinGroups'>${globalize.translate('LabelSyncPlayAccessCreateAndJoinGroups')}</option>
<option value='JoinGroups'>${globalize.translate('LabelSyncPlayAccessJoinGroups')}</option>
<option value='None'>${globalize.translate('LabelSyncPlayAccessNone')}</option>
</select>`
});
type IProps = {
className?: string;
id?: string;
label?: string
}
const SelectSyncPlayAccessElement: FunctionComponent<IProps> = ({ className, id, label }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createSelectElement({
className: className,
id: id,
label: globalize.translate(label)
})}
/>
);
};
export default SelectSyncPlayAccessElement;

View file

@ -4,6 +4,8 @@ import { formatDistanceToNow } from 'date-fns';
import { localeWithSuffix } from '../../../scripts/dfnshelper'; import { localeWithSuffix } from '../../../scripts/dfnshelper';
import globalize from '../../../scripts/globalize'; import globalize from '../../../scripts/globalize';
import cardBuilder from '../../cardbuilder/cardBuilder'; import cardBuilder from '../../cardbuilder/cardBuilder';
import IconButtonElement from '../../../elements/IconButtonElement';
import escapeHTML from 'escape-html';
const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
__html: `<a __html: `<a
@ -15,16 +17,6 @@ const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl
</a>` </a>`
}); });
const createButtonElement = () => ({
__html: `<button
is="paper-icon-button-light"
type="button"
class="btnUserMenu flex-shrink-zero"
>
<span class="material-icons more_vert" aria-hidden="true"></span>
</button>`
});
type IProps = { type IProps = {
user?: UserDto; user?: UserDto;
} }
@ -81,16 +73,20 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
/> />
</div> </div>
<div className='cardFooter visualCardBox-cardFooter'> <div className='cardFooter visualCardBox-cardFooter'>
<div className='cardText flex align-items-center'> <div
<div className='flex-grow' style={{overflow: 'hidden', textOverflow: 'ellipsis'}}> style={{textAlign: 'right', float: 'right', paddingTop: '5px'}}
{user.Name} >
</div> <IconButtonElement
<div is='paper-icon-button-light'
dangerouslySetInnerHTML={createButtonElement()} className='btnUserMenu flex-shrink-zero'
icon='more_vert'
/> />
</div> </div>
<div className='cardText'>
<span>{escapeHTML(user.Name)}</span>
</div>
<div className='cardText cardText-secondary'> <div className='cardText cardText-secondary'>
{lastSeen != '' ? lastSeen : ''} <span>{lastSeen != '' ? lastSeen : ''}</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,9 +6,9 @@ import LibraryMenu from '../../../scripts/libraryMenu';
import confirm from '../../confirm/confirm'; import confirm from '../../confirm/confirm';
import loading from '../../loading/loading'; import loading from '../../loading/loading';
import toast from '../../toast/toast'; import toast from '../../toast/toast';
import ButtonElement from './ButtonElement'; import ButtonElement from '../../../elements/ButtonElement';
import CheckBoxElement from './CheckBoxElement'; import CheckBoxElement from '../../../elements/CheckBoxElement';
import InputElement from './InputElement'; import InputElement from '../../../elements/InputElement';
type IProps = { type IProps = {
userId: string; userId: string;
@ -40,11 +40,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
let showLocalAccessSection = false; let showLocalAccessSection = false;
if (user.HasConfiguredPassword) { if (user.HasConfiguredPassword) {
(page.querySelector('.btnResetPassword') as HTMLDivElement).classList.remove('hide'); (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide'); (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
showLocalAccessSection = true; showLocalAccessSection = true;
} else { } else {
(page.querySelector('.btnResetPassword') as HTMLDivElement).classList.add('hide'); (page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide'); (page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
} }
@ -65,11 +65,11 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
if (user.HasConfiguredEasyPassword) { if (user.HasConfiguredEasyPassword) {
txtEasyPassword.placeholder = '******'; txtEasyPassword.placeholder = '******';
(page.querySelector('.btnResetEasyPassword') as HTMLDivElement).classList.remove('hide'); (page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
} else { } else {
txtEasyPassword.removeAttribute('placeholder'); txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = ''; txtEasyPassword.placeholder = '';
(page.querySelector('.btnResetEasyPassword') as HTMLDivElement).classList.add('hide'); (page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
} }
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement; const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
@ -206,8 +206,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
(page.querySelector('.updatePasswordForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('.updatePasswordForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.localAccessForm') as HTMLFormElement).addEventListener('submit', onLocalAccessSubmit); (page.querySelector('.localAccessForm') as HTMLFormElement).addEventListener('submit', onLocalAccessSubmit);
(page.querySelector('.btnResetEasyPassword') as HTMLButtonElement).addEventListener('click', resetEasyPassword); (page.querySelector('#btnResetEasyPassword') as HTMLButtonElement).addEventListener('click', resetEasyPassword);
(page.querySelector('.btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword); (page.querySelector('#btnResetPassword') as HTMLButtonElement).addEventListener('click', resetPassword);
}, [loadUser, userId]); }, [loadUser, userId]);
return ( return (
@ -250,7 +250,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
/> />
<ButtonElement <ButtonElement
type='button' type='button'
className='raised btnResetPassword button-cancel block hide' id='btnResetPassword'
className='raised button-cancel block hide'
title='ResetPassword' title='ResetPassword'
/> />
</div> </div>
@ -281,7 +282,6 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
<br /> <br />
<div className='checkboxContainer checkboxContainer-withDescription'> <div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkEnableLocalEasyPassword' className='chkEnableLocalEasyPassword'
title='LabelInNetworkSignInWithEasyPassword' title='LabelInNetworkSignInWithEasyPassword'
/> />
@ -297,7 +297,8 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
/> />
<ButtonElement <ButtonElement
type='button' type='button'
className='raised btnResetEasyPassword button-cancel block hide' id='btnResetEasyPassword'
className='raised button-cancel block hide'
title='ButtonResetEasyPassword' title='ButtonResetEasyPassword'
/> />
</div> </div>

View file

@ -1,17 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
export default (view, params, { detail }) => {
if (detail.options?.pageComponent) {
// Fetch and render the page component to the view
import(/* webpackChunkName: "[request]" */ `./pages/${detail.options.pageComponent}`)
.then(({ default: component }) => {
ReactDOM.render(React.createElement(component, params), view);
});
// Unmount component when view is destroyed
view.addEventListener('viewdestroy', () => {
ReactDOM.unmountComponentAtNode(view);
});
}
};

View file

@ -21,9 +21,9 @@ viewContainer.setOnBeforeChange(function (newView, isRestored, options) {
newView.initComplete = true; newView.initComplete = true;
if (typeof options.controllerFactory === 'function') { if (typeof options.controllerFactory === 'function') {
new options.controllerFactory(newView, eventDetail.detail.params, eventDetail); new options.controllerFactory(newView, eventDetail.detail.params);
} else if (options.controllerFactory && typeof options.controllerFactory.default === 'function') { } else if (options.controllerFactory && typeof options.controllerFactory.default === 'function') {
new options.controllerFactory.default(newView, eventDetail.detail.params, eventDetail); new options.controllerFactory.default(newView, eventDetail.detail.params);
} }
if (!options.controllerFactory || dispatchPageEvents) { if (!options.controllerFactory || dispatchPageEvents) {

View file

@ -1,3 +0,0 @@
<div id="editUserPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="newUserPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userParentalControlPage" data-role="page" class="page type-interior">
</div>

View file

@ -1,3 +0,0 @@
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
</div>

View file

@ -1,3 +0,0 @@
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
</div>

View file

@ -3,7 +3,7 @@
<div class="readOnlyContent" style="margin: 0 auto;"> <div class="readOnlyContent" style="margin: 0 auto;">
<div class="verticalSection verticalSection-extrabottompadding"> <div class="verticalSection verticalSection-extrabottompadding">
<h2 class="sectionTitle headerUsername" style="padding-left:.25em;"></h2> <h2 class="sectionTitle headerUsername" style="padding-left:.25em;"></h2>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkMyProfile listItem-border"> <a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkUserProfile listItem-border">
<div class="listItem"> <div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent person" aria-hidden="true"></span> <span class="material-icons listItemIcon listItemIcon-transparent person" aria-hidden="true"></span>
<div class="listItemBody"> <div class="listItemBody">

View file

@ -26,7 +26,7 @@ export default function (view, params) {
const userId = params.userId || Dashboard.getCurrentUserId(); const userId = params.userId || Dashboard.getCurrentUserId();
const page = this; const page = this;
page.querySelector('.lnkMyProfile').setAttribute('href', '#/myprofile.html?userId=' + userId); page.querySelector('.lnkUserProfile').setAttribute('href', '#/userprofile.html?userId=' + userId);
page.querySelector('.lnkDisplayPreferences').setAttribute('href', '#/mypreferencesdisplay.html?userId=' + userId); page.querySelector('.lnkDisplayPreferences').setAttribute('href', '#/mypreferencesdisplay.html?userId=' + userId);
page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId); page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome.html?userId=' + userId);
page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId); page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback.html?userId=' + userId);

View file

@ -1,3 +0,0 @@
<div id="userProfilePage" data-role="page" class="page libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage" data-title="${Profile}" data-menubutton="false">
</div>

View file

@ -0,0 +1,41 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createButtonElement = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => ({
__html: `<button
is="emby-button"
type="${type}"
${id}
class="${className}"
>
${leftIcon}
<span>${title}</span>
${rightIcon}
</button>`
});
type IProps = {
type?: string;
id?: string;
className?: string;
title?: string;
leftIcon?: string;
rightIcon?: string;
}
const ButtonElement: FunctionComponent<IProps> = ({ type, id, className, title, leftIcon, rightIcon }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createButtonElement({
type: type,
id: id ? `id="${id}"` : '',
className: className,
title: globalize.translate(title),
leftIcon: leftIcon ? `<span class="material-icons ${leftIcon}" aria-hidden="true"></span>` : '',
rightIcon: rightIcon ? `<span class="material-icons ${rightIcon}" aria-hidden="true"></span>` : ''
})}
/>
);
};
export default ButtonElement;

View file

@ -0,0 +1,57 @@
import escapeHTML from 'escape-html';
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, className, id, dataFilter, dataItemType, dataId, checkedAttribute, renderContent }: { labelClassName?: string, type?: string, className?: string, id?: string, dataFilter?: string, dataItemType?: string, dataId?: string, checkedAttribute?: string, renderContent?: string }) => ({
__html: `<label ${labelClassName}>
<input
is="emby-checkbox"
type="checkbox"
class="${className}"
${id}
${dataFilter}
${dataItemType}
${dataId}
${checkedAttribute}
/>
${renderContent}
</label>`
});
type IProps = {
labelClassName?: string;
className?: string;
elementId?: string;
dataFilter?: string;
itemType?: string;
itemId?: string;
itemAppName?: string;
itemCheckedAttribute?: string;
itemName?: string
title?: string
}
const CheckBoxElement: FunctionComponent<IProps> = ({ labelClassName, className, elementId, dataFilter, itemType, itemId, itemAppName, itemCheckedAttribute, itemName, title }: IProps) => {
const appName = itemAppName ? `- ${itemAppName}` : '';
const renderContent = itemName ?
`<span>${escapeHTML(itemName || '')} ${appName}</span>` :
`<span>${globalize.translate(title)}</span>`;
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
labelClassName: labelClassName ? `class='${labelClassName}'` : '',
className: className,
id: elementId ? `id='${elementId}'` : '',
dataFilter: dataFilter ? `data-filter='${dataFilter}'` : '',
dataItemType: itemType ? `data-itemtype='${itemType}'` : '',
dataId: itemId ? `data-id='${itemId}'` : '',
checkedAttribute: itemCheckedAttribute ? itemCheckedAttribute : '',
renderContent: renderContent
})}
/>
);
};
export default CheckBoxElement;

View file

@ -0,0 +1,47 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
type IProps = {
is?: string;
id?: string;
title?: string;
className?: string;
icon?: string,
dataIndex?: string | number;
dataTag?: string | number;
dataProfileid?: string | number;
}
const createIconButtonElement = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => ({
__html: `<button
is="${is}"
type="button"
${id}
class="${className}"
${title}
${dataIndex}
${dataTag}
${dataProfileid}
>
<span class="material-icons ${icon}" aria-hidden="true"></span>
</button>`
});
const IconButtonElement: FunctionComponent<IProps> = ({ is, id, className, title, icon, dataIndex, dataTag, dataProfileid }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createIconButtonElement({
is: is,
id: id ? `id="${id}"` : '',
className: className,
title: title ? `title="${globalize.translate(title)}"` : '',
icon: icon,
dataIndex: dataIndex ? `data-index="${dataIndex}"` : '',
dataTag: dataTag ? `data-tag="${dataTag}"` : '',
dataProfileid: dataProfileid ? `data-profileid="${dataProfileid}"` : ''
})}
/>
);
};
export default IconButtonElement;

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize'; import globalize from '../scripts/globalize';
const createInputElement = ({ type, id, label, options }: { type?: string, id?: string, label?: string, options?: string }) => ({ const createInputElement = ({ type, id, label, options }: { type?: string, id?: string, label?: string, options?: string }) => ({
__html: `<input __html: `<input

View file

@ -0,0 +1,41 @@
import React, { FunctionComponent } from 'react';
import IconButtonElement from './IconButtonElement';
import SectionTitleLinkElement from './SectionTitleLinkElement';
type IProps = {
SectionClassName?: string;
title?: string;
isBtnVisible?: boolean;
btnId?: string;
btnClassName?: string;
btnTitle?: string;
btnIcon?: string;
isLinkVisible?: boolean;
url?: string;
}
const SectionTitleContainer: FunctionComponent<IProps> = ({SectionClassName, title, isBtnVisible = false, btnId, btnClassName, btnTitle, btnIcon, isLinkVisible = true, url}: IProps) => {
return (
<div className={`${SectionClassName} sectionTitleContainer flex align-items-center`}>
<h2 className='sectionTitle'>
{title}
</h2>
{isBtnVisible && <IconButtonElement
is='emby-button'
id={btnId}
className={btnClassName}
title={btnTitle}
icon={btnIcon}
/>}
{isLinkVisible && <SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url={url}
/>}
</div>
);
};
export default SectionTitleContainer;

View file

@ -1,5 +1,5 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize'; import globalize from '../scripts/globalize';
const createLinkElement = ({ className, title, href }: { className?: string, title?: string, href?: string }) => ({ const createLinkElement = ({ className, title, href }: { className?: string, title?: string, href?: string }) => ({
__html: `<a __html: `<a

View file

@ -0,0 +1,38 @@
import React, { FunctionComponent } from 'react';
import globalize from '../scripts/globalize';
const createSelectElement = ({ name, id, required, label, option }: { name?: string, id?: string, required?: string, label?: string, option?: React.ReactNode }) => ({
__html: `<select
is="emby-select"
${name}
id="${id}"
${required}
label="${label}"
>
${option}
</select>`
});
type IProps = {
name?: string;
id?: string;
required?: string;
label?: string;
children?: React.ReactNode
}
const SelectElement: FunctionComponent<IProps> = ({ name, id, required, label, children }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createSelectElement({
name: name ? `name='${name}'` : '',
id: id,
required: required ? `required='${required}'` : '',
label: globalize.translate(label),
option: children
})}
/>
);
};
export default SelectElement;

View file

@ -2,19 +2,34 @@ import React from 'react';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import ConnectionRequired from '../components/ConnectionRequired'; import ConnectionRequired from '../components/ConnectionRequired';
import SearchPage from './search'; import UserNew from './user/usernew';
import Search from './search';
import UserEdit from './user/useredit';
import UserLibraryAccess from './user/userlibraryaccess';
import UserParentalControl from './user/userparentalcontrol';
import UserPassword from './user/userpassword';
import UserProfile from './user/userprofile';
import UserProfiles from './user/userprofiles';
const AppRoutes = () => ( const AppRoutes = () => (
<Routes> <Routes>
<Route path='/'> <Route path='/'>
<Route {/* User routes */}
path='search.html' <Route path='/' element={<ConnectionRequired />}>
element={ <Route path='search.html' element={<Search />} />
<ConnectionRequired> <Route path='userprofile.html' element={<UserProfile />} />
<SearchPage /> </Route>
</ConnectionRequired>
} {/* Admin routes */}
/> <Route path='/' element={<ConnectionRequired isAdminRequired={true} />}>
<Route path='usernew.html' element={<UserNew />} />
<Route path='userprofiles.html' element={<UserProfiles />} />
<Route path='useredit.html' element={<UserEdit />} />
<Route path='userlibraryaccess.html' element={<UserLibraryAccess />} />
<Route path='userparentalcontrol.html' element={<UserParentalControl />} />
<Route path='userpassword.html' element={<UserPassword />} />
</Route>
{/* Suppress warnings for unhandled routes */} {/* Suppress warnings for unhandled routes */}
<Route path='*' element={null} /> <Route path='*' element={null} />
</Route> </Route>

View file

@ -8,7 +8,7 @@ import SearchSuggestions from '../components/search/SearchSuggestions';
import LiveTVSearchResults from '../components/search/LiveTVSearchResults'; import LiveTVSearchResults from '../components/search/LiveTVSearchResults';
import globalize from '../scripts/globalize'; import globalize from '../scripts/globalize';
const SearchPage: FunctionComponent = () => { const Search: FunctionComponent = () => {
const [ query, setQuery ] = useState<string>(); const [ query, setQuery ] = useState<string>();
const [ searchParams ] = useSearchParams(); const [ searchParams ] = useSearchParams();
@ -41,4 +41,4 @@ const SearchPage: FunctionComponent = () => {
); );
}; };
export default SearchPage; export default Search;

View file

@ -3,30 +3,33 @@ import React, { FunctionComponent, useCallback, useEffect, useState, useRef } fr
import Dashboard from '../../utils/dashboard'; import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu'; import LibraryMenu from '../../scripts/libraryMenu';
import ButtonElement from '../dashboard/users/ButtonElement'; import ButtonElement from '../../elements/ButtonElement';
import CheckBoxElement from '../dashboard/users/CheckBoxElement'; import CheckBoxElement from '../../elements/CheckBoxElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem'; import InputElement from '../../elements/InputElement';
import InputElement from '../dashboard/users/InputElement'; import LinkEditUserPreferences from '../../components/dashboard/users/LinkEditUserPreferences';
import LinkEditUserPreferences from '../dashboard/users/LinkEditUserPreferences'; import SectionTitleContainer from '../../elements/SectionTitleContainer';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer'; import SectionTabs from '../../components/dashboard/users/SectionTabs';
import SelectElement from '../dashboard/users/SelectElement'; import loading from '../../components/loading/loading';
import SelectSyncPlayAccessElement from '../dashboard/users/SelectSyncPlayAccessElement'; import toast from '../../components/toast/toast';
import SectionTabs from '../dashboard/users/SectionTabs';
import loading from '../loading/loading';
import toast from '../toast/toast';
import { getParameterByName } from '../../utils/url'; import { getParameterByName } from '../../utils/url';
import escapeHTML from 'escape-html';
import SelectElement from '../../elements/SelectElement';
import Page from '../../components/Page';
type ItemsArr = { type ResetProvider = AuthProvider & {
Name?: string;
Id?: string;
checkedAttribute: string checkedAttribute: string
} }
const UserEditPage: FunctionComponent = () => { type AuthProvider = {
Name?: string;
Id?: string;
}
const UserEdit: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ItemsArr[]>([]); const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
const [ authProviders, setAuthProviders ] = useState([]); const [ authProviders, setAuthProviders ] = useState<AuthProvider[]>([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState([]); const [ passwordResetProviders, setPasswordResetProviders ] = useState<ResetProvider[]>([]);
const [ authenticationProviderId, setAuthenticationProviderId ] = useState(''); const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState(''); const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
@ -91,7 +94,7 @@ const UserEditPage: FunctionComponent = () => {
})).then(function (channelsResult) { })).then(function (channelsResult) {
let isChecked; let isChecked;
let checkedAttribute; let checkedAttribute;
const itemsArr: ItemsArr[] = []; const itemsArr: ResetProvider[] = [];
for (const folder of mediaFolders) { for (const folder of mediaFolders) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1; isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
@ -172,7 +175,7 @@ const UserEditPage: FunctionComponent = () => {
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0'; (page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0'; (page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
if (window.ApiClient.isMinServerVersion('10.6.0')) { if (window.ApiClient.isMinServerVersion('10.6.0')) {
(page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value = user.Policy.SyncPlayAccess; (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value = user.Policy.SyncPlayAccess;
} }
loading.hide(); loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]); }, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
@ -227,8 +230,8 @@ const UserEditPage: FunctionComponent = () => {
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat((page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value || '0')); 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'); user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0');
user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0'); user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0');
user.Policy.AuthenticationProviderId = (page.querySelector('.selectLoginProvider') as HTMLInputElement).value; user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('.selectPasswordResetProvider') as HTMLInputElement).value; user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked; user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) { user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
return c.checked; return c.checked;
@ -236,7 +239,7 @@ const UserEditPage: FunctionComponent = () => {
return c.getAttribute('data-id'); return c.getAttribute('data-id');
}); });
if (window.ApiClient.isMinServerVersion('10.6.0')) { if (window.ApiClient.isMinServerVersion('10.6.0')) {
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value as SyncPlayUserAccessType; user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
} }
window.ApiClient.updateUser(user).then(function () { window.ApiClient.updateUser(user).then(function () {
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () { window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
@ -270,18 +273,42 @@ const UserEditPage: FunctionComponent = () => {
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() { (page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back(); window.history.back();
}); });
}, [loadData]); }, [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 ( return (
<div ref={element}> <Page
<div className='content-primary'> id='editUserPage'
<SectionTitleContainer className='mainAnimatedPage type-interior'
title={userName} >
titleLink='https://docs.jellyfin.org/general/server/users/' <div ref={element} className='content-primary'>
/> <div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='useredit'/> <SectionTabs activeTab='useredit'/>
<div <div
className='lnkEditUserPreferencesContainer' className='lnkEditUserPreferencesContainer'
@ -313,29 +340,29 @@ const UserEditPage: FunctionComponent = () => {
</div> </div>
<div className='selectContainer fldSelectLoginProvider hide'> <div className='selectContainer fldSelectLoginProvider hide'>
<SelectElement <SelectElement
className= 'selectLoginProvider' id='selectLoginProvider'
label= 'LabelAuthProvider' label='LabelAuthProvider'
currentProviderId={authenticationProviderId} >
providers={authProviders} {optionLoginProvider}
/> </SelectElement>
<div className='fieldDescription'> <div className='fieldDescription'>
{globalize.translate('AuthProviderHelp')} {globalize.translate('AuthProviderHelp')}
</div> </div>
</div> </div>
<div className='selectContainer fldSelectPasswordResetProvider hide'> <div className='selectContainer fldSelectPasswordResetProvider hide'>
<SelectElement <SelectElement
className= 'selectPasswordResetProvider' id='selectPasswordResetProvider'
label= 'LabelPasswordResetProvider' label='LabelPasswordResetProvider'
currentProviderId={passwordResetProviderId} >
providers={passwordResetProviders} {optionPasswordResetProvider}
/> </SelectElement>
<div className='fieldDescription'> <div className='fieldDescription'>
{globalize.translate('PasswordResetProviderHelp')} {globalize.translate('PasswordResetProviderHelp')}
</div> </div>
</div> </div>
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'> <div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkRemoteAccess' className='chkRemoteAccess'
title='AllowRemoteAccess' title='AllowRemoteAccess'
/> />
@ -345,7 +372,6 @@ const UserEditPage: FunctionComponent = () => {
</div> </div>
<CheckBoxElement <CheckBoxElement
labelClassName='checkboxContainer' labelClassName='checkboxContainer'
type='checkbox'
className='chkIsAdmin' className='chkIsAdmin'
title='OptionAllowUserToManageServer' title='OptionAllowUserToManageServer'
/> />
@ -355,12 +381,10 @@ const UserEditPage: FunctionComponent = () => {
</h2> </h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}> <div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkEnableLiveTvAccess' className='chkEnableLiveTvAccess'
title='OptionAllowBrowsingLiveTv' title='OptionAllowBrowsingLiveTv'
/> />
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkManageLiveTv' className='chkManageLiveTv'
title='OptionAllowManageLiveTv' title='OptionAllowManageLiveTv'
/> />
@ -372,27 +396,22 @@ const UserEditPage: FunctionComponent = () => {
</h2> </h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}> <div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkEnableMediaPlayback' className='chkEnableMediaPlayback'
title='OptionAllowMediaPlayback' title='OptionAllowMediaPlayback'
/> />
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkEnableAudioPlaybackTranscoding' className='chkEnableAudioPlaybackTranscoding'
title='OptionAllowAudioPlaybackTranscoding' title='OptionAllowAudioPlaybackTranscoding'
/> />
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkEnableVideoPlaybackTranscoding' className='chkEnableVideoPlaybackTranscoding'
title='OptionAllowVideoPlaybackTranscoding' title='OptionAllowVideoPlaybackTranscoding'
/> />
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkEnableVideoPlaybackRemuxing' className='chkEnableVideoPlaybackRemuxing'
title='OptionAllowVideoPlaybackRemuxing' title='OptionAllowVideoPlaybackRemuxing'
/> />
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkForceRemoteSourceTranscoding' className='chkForceRemoteSourceTranscoding'
title='OptionForceRemoteSourceTranscoding' title='OptionForceRemoteSourceTranscoding'
/> />
@ -420,11 +439,12 @@ const UserEditPage: FunctionComponent = () => {
</div> </div>
<div className='verticalSection'> <div className='verticalSection'>
<div className='selectContainer fldSelectSyncPlayAccess'> <div className='selectContainer fldSelectSyncPlayAccess'>
<SelectSyncPlayAccessElement <SelectElement
className='selectSyncPlayAccess'
id='selectSyncPlayAccess' id='selectSyncPlayAccess'
label='LabelSyncPlayAccess' label='LabelSyncPlayAccess'
/> >
{optionSyncPlayAccess()}
</SelectElement>
<div className='fieldDescription'> <div className='fieldDescription'>
{globalize.translate('SyncPlayAccessHelp')} {globalize.translate('SyncPlayAccessHelp')}
</div> </div>
@ -437,18 +457,17 @@ const UserEditPage: FunctionComponent = () => {
<div className='checkboxList paperList checkboxList-paperList'> <div className='checkboxList paperList checkboxList-paperList'>
<CheckBoxElement <CheckBoxElement
labelClassName='checkboxContainer' labelClassName='checkboxContainer'
type='checkbox'
className='chkEnableDeleteAllFolders' className='chkEnableDeleteAllFolders'
title='AllLibraries' title='AllLibraries'
/> />
<div className='deleteAccess'> <div className='deleteAccess'>
{deleteFoldersAccess.map(Item => ( {deleteFoldersAccess.map(Item => (
<CheckBoxListItem <CheckBoxElement
key={Item.Id} key={Item.Id}
className='chkFolder' className='chkFolder'
Id={Item.Id} itemId={Item.Id}
Name={Item.Name} itemName={Item.Name}
checkedAttribute={Item.checkedAttribute} itemCheckedAttribute={Item.checkedAttribute}
/> />
))} ))}
</div> </div>
@ -460,12 +479,10 @@ const UserEditPage: FunctionComponent = () => {
</h2> </h2>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}> <div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkEnableRemoteControlOtherUsers' className='chkEnableRemoteControlOtherUsers'
title='OptionAllowRemoteControlOthers' title='OptionAllowRemoteControlOthers'
/> />
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkRemoteControlSharedDevices' className='chkRemoteControlSharedDevices'
title='OptionAllowRemoteSharedDevices' title='OptionAllowRemoteSharedDevices'
/> />
@ -479,7 +496,6 @@ const UserEditPage: FunctionComponent = () => {
</h2> </h2>
<div className='checkboxContainer checkboxContainer-withDescription'> <div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkEnableDownloading' className='chkEnableDownloading'
title='OptionAllowContentDownload' title='OptionAllowContentDownload'
/> />
@ -489,7 +505,6 @@ const UserEditPage: FunctionComponent = () => {
</div> </div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'> <div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkDisabled' className='chkDisabled'
title='OptionDisableUser' title='OptionDisableUser'
/> />
@ -499,7 +514,6 @@ const UserEditPage: FunctionComponent = () => {
</div> </div>
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'> <div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
<CheckBoxElement <CheckBoxElement
type='checkbox'
className='chkIsHidden' className='chkIsHidden'
title='OptionHideUser' title='OptionHideUser'
/> />
@ -550,14 +564,16 @@ const UserEditPage: FunctionComponent = () => {
/> />
<ButtonElement <ButtonElement
type='button' type='button'
className='raised button-cancel block btnCancel' id='btnCancel'
className='raised button-cancel block'
title='ButtonCancel' title='ButtonCancel'
/> />
</div> </div>
</form> </form>
</div> </div>
</div> </Page>
); );
}; };
export default UserEditPage; export default UserEdit;

View file

@ -1,16 +1,17 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import loading from '../loading/loading'; import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu'; import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import toast from '../toast/toast'; import toast from '../../components/toast/toast';
import SectionTabs from '../dashboard/users/SectionTabs'; import SectionTabs from '../../components/dashboard/users/SectionTabs';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem'; import ButtonElement from '../../elements/ButtonElement';
import ButtonElement from '../dashboard/users/ButtonElement';
import { getParameterByName } from '../../utils/url'; import { getParameterByName } from '../../utils/url';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer'; import SectionTitleContainer from '../../elements/SectionTitleContainer';
import AccessContainer from '../dashboard/users/AccessContainer'; import AccessContainer from '../../components/dashboard/users/AccessContainer';
import CheckBoxElement from '../../elements/CheckBoxElement';
import Page from '../../components/Page';
type ItemsArr = { type ItemsArr = {
Name?: string; Name?: string;
@ -19,7 +20,7 @@ type ItemsArr = {
checkedAttribute?: string checkedAttribute?: string
} }
const UserLibraryAccessPage: FunctionComponent = () => { const UserLibraryAccess: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]); const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]); const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
@ -226,12 +227,17 @@ const UserLibraryAccessPage: FunctionComponent = () => {
}, [loadData]); }, [loadData]);
return ( return (
<div ref={element}> <Page
<div className='content-primary'> id='userLibraryAccessPage'
<SectionTitleContainer className='mainAnimatedPage type-interior'
title={userName} >
titleLink='https://docs.jellyfin.org/general/server/users/' <div ref={element} className='content-primary'>
/> <div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userlibraryaccess'/> <SectionTabs activeTab='userlibraryaccess'/>
<form className='userLibraryAccessForm'> <form className='userLibraryAccessForm'>
<AccessContainer <AccessContainer
@ -245,12 +251,12 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='LibraryAccessHelp' description='LibraryAccessHelp'
> >
{mediaFoldersItems.map(Item => ( {mediaFoldersItems.map(Item => (
<CheckBoxListItem <CheckBoxElement
key={Item.Id} key={Item.Id}
className='chkFolder' className='chkFolder'
Id={Item.Id} itemId={Item.Id}
Name={Item.Name} itemName={Item.Name}
checkedAttribute={Item.checkedAttribute} itemCheckedAttribute={Item.checkedAttribute}
/> />
))} ))}
</AccessContainer> </AccessContainer>
@ -266,12 +272,12 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='ChannelAccessHelp' description='ChannelAccessHelp'
> >
{channelsItems.map(Item => ( {channelsItems.map(Item => (
<CheckBoxListItem <CheckBoxElement
key={Item.Id} key={Item.Id}
className='chkChannel' className='chkChannel'
Id={Item.Id} itemId={Item.Id}
Name={Item.Name} itemName={Item.Name}
checkedAttribute={Item.checkedAttribute} itemCheckedAttribute={Item.checkedAttribute}
/> />
))} ))}
</AccessContainer> </AccessContainer>
@ -287,13 +293,13 @@ const UserLibraryAccessPage: FunctionComponent = () => {
description='DeviceAccessHelp' description='DeviceAccessHelp'
> >
{devicesItems.map(Item => ( {devicesItems.map(Item => (
<CheckBoxListItem <CheckBoxElement
key={Item.Id} key={Item.Id}
className='chkDevice' className='chkDevice'
Id={Item.Id} itemId={Item.Id}
Name={Item.Name} itemName={Item.Name}
AppName={Item.AppName} itemAppName={Item.AppName}
checkedAttribute={Item.checkedAttribute} itemCheckedAttribute={Item.checkedAttribute}
/> />
))} ))}
</AccessContainer> </AccessContainer>
@ -307,8 +313,9 @@ const UserLibraryAccessPage: FunctionComponent = () => {
</div> </div>
</form> </form>
</div> </div>
</div> </Page>
); );
}; };
export default UserLibraryAccessPage; export default UserLibraryAccess;

View file

@ -2,13 +2,14 @@ import React, { FunctionComponent, useCallback, useEffect, useState, useRef } fr
import Dashboard from '../../utils/dashboard'; import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import loading from '../loading/loading'; import loading from '../../components/loading/loading';
import toast from '../toast/toast'; import toast from '../../components/toast/toast';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer'; import SectionTitleContainer from '../../elements/SectionTitleContainer';
import InputElement from '../dashboard/users/InputElement'; import InputElement from '../../elements/InputElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem'; import ButtonElement from '../../elements/ButtonElement';
import ButtonElement from '../dashboard/users/ButtonElement'; import AccessContainer from '../../components/dashboard/users/AccessContainer';
import AccessContainer from '../dashboard/users/AccessContainer'; import CheckBoxElement from '../../elements/CheckBoxElement';
import Page from '../../components/Page';
type userInput = { type userInput = {
Name?: string; Name?: string;
@ -20,7 +21,7 @@ type ItemsArr = {
Id?: string; Id?: string;
} }
const NewUserPage: FunctionComponent = () => { const UserNew: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]); const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]); const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
@ -169,18 +170,24 @@ const NewUserPage: FunctionComponent = () => {
(page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
(page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() { (page.querySelector('#btnCancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back(); window.history.back();
}); });
}, [loadUser]); }, [loadUser]);
return ( return (
<div ref={element}> <Page
<div className='content-primary'> id='newUserPage'
<SectionTitleContainer className='mainAnimatedPage type-interior'
title={globalize.translate('HeaderAddUser')} >
titleLink='https://docs.jellyfin.org/general/server/users/' <div ref={element} className='content-primary'>
/> <div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('HeaderAddUser')}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<form className='newUserProfileForm'> <form className='newUserProfileForm'>
<div className='inputContainer'> <div className='inputContainer'>
<InputElement <InputElement
@ -208,12 +215,11 @@ const NewUserPage: FunctionComponent = () => {
description='LibraryAccessHelp' description='LibraryAccessHelp'
> >
{mediaFoldersItems.map(Item => ( {mediaFoldersItems.map(Item => (
<CheckBoxListItem <CheckBoxElement
key={Item.Id} key={Item.Id}
className='chkFolder' className='chkFolder'
Id={Item.Id} itemId={Item.Id}
Name={Item.Name} itemName={Item.Name}
checkedAttribute=''
/> />
))} ))}
</AccessContainer> </AccessContainer>
@ -229,12 +235,11 @@ const NewUserPage: FunctionComponent = () => {
description='ChannelAccessHelp' description='ChannelAccessHelp'
> >
{channelsItems.map(Item => ( {channelsItems.map(Item => (
<CheckBoxListItem <CheckBoxElement
key={Item.Id} key={Item.Id}
className='chkChannel' className='chkChannel'
Id={Item.Id} itemId={Item.Id}
Name={Item.Name} itemName={Item.Name}
checkedAttribute=''
/> />
))} ))}
</AccessContainer> </AccessContainer>
@ -246,14 +251,16 @@ const NewUserPage: FunctionComponent = () => {
/> />
<ButtonElement <ButtonElement
type='button' type='button'
className='raised button-cancel block btnCancel' id='btnCancel'
className='raised button-cancel block'
title='ButtonCancel' title='ButtonCancel'
/> />
</div> </div>
</form> </form>
</div> </div>
</div> </Page>
); );
}; };
export default NewUserPage; export default UserNew;

View file

@ -1,25 +1,21 @@
import { AccessSchedule, DynamicDayOfWeek, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client'; import { AccessSchedule, DynamicDayOfWeek, ParentalRating, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu'; import LibraryMenu from '../../scripts/libraryMenu';
import AccessScheduleList from '../dashboard/users/AccessScheduleList'; import AccessScheduleList from '../../components/dashboard/users/AccessScheduleList';
import BlockedTagList from '../dashboard/users/BlockedTagList'; import BlockedTagList from '../../components/dashboard/users/BlockedTagList';
import ButtonElement from '../dashboard/users/ButtonElement'; import ButtonElement from '../../elements/ButtonElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem'; import SectionTitleContainer from '../../elements/SectionTitleContainer';
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement'; import SectionTabs from '../../components/dashboard/users/SectionTabs';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer'; import loading from '../../components/loading/loading';
import SelectMaxParentalRating from '../dashboard/users/SelectMaxParentalRating'; import toast from '../../components/toast/toast';
import SectionTabs from '../dashboard/users/SectionTabs';
import loading from '../loading/loading';
import toast from '../toast/toast';
import { getParameterByName } from '../../utils/url'; 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';
type RatingsArr = { type UnratedItem = {
Name: string;
Value: number;
}
type ItemsArr = {
name: string; name: string;
value: string; value: string;
checkedAttribute: string checkedAttribute: string
@ -27,8 +23,8 @@ type ItemsArr = {
const UserParentalControl: FunctionComponent = () => { const UserParentalControl: FunctionComponent = () => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<RatingsArr[]>([]); const [ parentalRatings, setParentalRatings ] = useState<ParentalRating[]>([]);
const [ unratedItems, setUnratedItems ] = useState<ItemsArr[]>([]); const [ unratedItems, setUnratedItems ] = useState<UnratedItem[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]); const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ blockedTags, setBlockedTags ] = useState([]); const [ blockedTags, setBlockedTags ] = useState([]);
@ -36,7 +32,7 @@ const UserParentalControl: FunctionComponent = () => {
const populateRatings = useCallback((allParentalRatings) => { const populateRatings = useCallback((allParentalRatings) => {
let rating; let rating;
const ratings: RatingsArr[] = []; const ratings: ParentalRating[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) { for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i]; rating = allParentalRatings[i];
@ -90,7 +86,7 @@ const UserParentalControl: FunctionComponent = () => {
value: 'Series' value: 'Series'
}]; }];
const itemsArr: ItemsArr[] = []; const itemsArr: UnratedItem[] = [];
for (const item of items) { for (const item of items) {
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1; const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
@ -181,7 +177,7 @@ const UserParentalControl: FunctionComponent = () => {
} }
} }
(page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value = ratingValue; (page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value = ratingValue;
if (user.Policy.IsAdministrator) { if (user.Policy.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide'); (page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
@ -226,7 +222,7 @@ const UserParentalControl: FunctionComponent = () => {
throw new Error('Unexpected null user.Policy'); throw new Error('Unexpected null user.Policy');
} }
user.Policy.MaxParentalRating = parseInt((page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value || '0', 10) || null; user.Policy.MaxParentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value || '0', 10) || null;
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) { user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
return i.checked; return i.checked;
}).map(function (i) { }).map(function (i) {
@ -299,7 +295,7 @@ const UserParentalControl: FunctionComponent = () => {
return false; return false;
}; };
(page.querySelector('.btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () { (page.querySelector('#btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
showSchedulePopup({ showSchedulePopup({
Id: 0, Id: 0,
UserId: '', UserId: '',
@ -309,28 +305,43 @@ const UserParentalControl: FunctionComponent = () => {
}, -1); }, -1);
}); });
(page.querySelector('.btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () { (page.querySelector('#btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup(); showBlockedTagPopup();
}); });
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit); (page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadBlockedTags, loadData, renderAccessSchedule]); }, [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 ( return (
<div ref={element}> <Page
<div className='content-primary'> id='userParentalControlPage'
<SectionTitleContainer className='mainAnimatedPage type-interior'
title={userName} >
titleLink='https://docs.jellyfin.org/general/server/users/' <div ref={element} className='content-primary'>
/> <div className='verticalSection'>
<SectionTitleContainer
title={userName}
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userparentalcontrol'/> <SectionTabs activeTab='userparentalcontrol'/>
<form className='userParentalControlForm'> <form className='userParentalControlForm'>
<div className='selectContainer'> <div className='selectContainer'>
<SelectMaxParentalRating <SelectElement
className= 'selectMaxParentalRating' id='selectMaxParentalRating'
label= 'LabelMaxParentalRating' label='LabelMaxParentalRating'
parentalRatings={parentalRatings} >
/> {optionMaxParentalRating()}
</SelectElement>
<div className='fieldDescription'> <div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')} {globalize.translate('MaxParentalRatingHelp')}
</div> </div>
@ -342,12 +353,12 @@ const UserParentalControl: FunctionComponent = () => {
</h3> </h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}> <div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => { {unratedItems.map(Item => {
return <CheckBoxListItem return <CheckBoxElement
key={Item.value} key={Item.value}
className='chkUnratedItem' className='chkUnratedItem'
ItemType={Item.value} itemType={Item.value}
Name={Item.name} itemName={Item.name}
checkedAttribute={Item.checkedAttribute} itemCheckedAttribute={Item.checkedAttribute}
/>; />;
})} })}
</div> </div>
@ -355,19 +366,16 @@ const UserParentalControl: FunctionComponent = () => {
</div> </div>
<br /> <br />
<div className='verticalSection' style={{marginBottom: '2em'}}> <div className='verticalSection' style={{marginBottom: '2em'}}>
<div <SectionTitleContainer
className='detailSectionHeader sectionTitleContainer' SectionClassName='detailSectionHeader'
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}} title={globalize.translate('LabelBlockContentWithTags')}
> isBtnVisible={true}
<h2 className='sectionTitle'> btnId='btnAddBlockedTag'
{globalize.translate('LabelBlockContentWithTags')} btnClassName='fab submit sectionTitleButton'
</h2> btnTitle='Add'
<SectionTitleButtonElement btnIcon='add'
className='fab btnAddBlockedTag submit' isLinkVisible={false}
title='Add' />
icon='add'
/>
</div>
<div className='blockedTags' style={{marginTop: '.5em'}}> <div className='blockedTags' style={{marginTop: '.5em'}}>
{blockedTags.map((tag, index) => { {blockedTags.map((tag, index) => {
return <BlockedTagList return <BlockedTagList
@ -378,19 +386,15 @@ const UserParentalControl: FunctionComponent = () => {
</div> </div>
</div> </div>
<div className='accessScheduleSection verticalSection' style={{marginBottom: '2em'}}> <div className='accessScheduleSection verticalSection' style={{marginBottom: '2em'}}>
<div <SectionTitleContainer
className='sectionTitleContainer' title={globalize.translate('HeaderAccessSchedule')}
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}} isBtnVisible={true}
> btnId='btnAddSchedule'
<h2 className='sectionTitle'> btnClassName='fab submit sectionTitleButton'
{globalize.translate('HeaderAccessSchedule')} btnTitle='Add'
</h2> btnIcon='add'
<SectionTitleButtonElement isLinkVisible={false}
className='fab btnAddSchedule submit' />
title='Add'
icon='add'
/>
</div>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p> <p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'> <div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => { {accessSchedules.map((accessSchedule, index) => {
@ -414,7 +418,8 @@ const UserParentalControl: FunctionComponent = () => {
</div> </div>
</form> </form>
</div> </div>
</div> </Page>
); );
}; };

View file

@ -1,19 +1,23 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import SectionTabs from '../dashboard/users/SectionTabs'; import SectionTabs from '../../components/dashboard/users/SectionTabs';
import UserPasswordForm from '../dashboard/users/UserPasswordForm'; import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
import { getParameterByName } from '../../utils/url'; import { getParameterByName } from '../../utils/url';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer'; import SectionTitleContainer from '../../elements/SectionTitleContainer';
import Page from '../../components/Page';
import loading from '../../components/loading/loading';
const UserPasswordPage: FunctionComponent = () => { const UserPassword: FunctionComponent = () => {
const userId = getParameterByName('userId'); const userId = getParameterByName('userId');
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const loadUser = useCallback(() => { const loadUser = useCallback(() => {
loading.show();
window.ApiClient.getUser(userId).then(function (user) { window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) { if (!user.Name) {
throw new Error('Unexpected null user.Name'); throw new Error('Unexpected null user.Name');
} }
setUserName(user.Name); setUserName(user.Name);
loading.hide();
}); });
}, [userId]); }, [userId]);
useEffect(() => { useEffect(() => {
@ -21,12 +25,17 @@ const UserPasswordPage: FunctionComponent = () => {
}, [loadUser]); }, [loadUser]);
return ( return (
<div> <Page
id='userPasswordPage'
className='mainAnimatedPage type-interior userPasswordPage'
>
<div className='content-primary'> <div className='content-primary'>
<SectionTitleContainer <div className='verticalSection'>
title={userName} <SectionTitleContainer
titleLink='https://docs.jellyfin.org/general/server/users/' title={userName}
/> url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
<SectionTabs activeTab='userpassword'/> <SectionTabs activeTab='userpassword'/>
<div className='readOnlyContent'> <div className='readOnlyContent'>
<UserPasswordForm <UserPasswordForm
@ -34,8 +43,9 @@ const UserPasswordPage: FunctionComponent = () => {
/> />
</div> </div>
</div> </div>
</div> </Page>
); );
}; };
export default UserPasswordPage; export default UserPassword;

View file

@ -4,18 +4,17 @@ import React, { FunctionComponent, useEffect, useState, useRef, useCallback } fr
import Dashboard from '../../utils/dashboard'; import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu'; import LibraryMenu from '../../scripts/libraryMenu';
import { appHost } from '../apphost'; import { appHost } from '../../components/apphost';
import confirm from '../confirm/confirm'; import confirm from '../../components/confirm/confirm';
import ButtonElement from '../dashboard/users/ButtonElement'; import ButtonElement from '../../elements/ButtonElement';
import UserPasswordForm from '../dashboard/users/UserPasswordForm'; import UserPasswordForm from '../../components/dashboard/users/UserPasswordForm';
import loading from '../loading/loading'; import loading from '../../components/loading/loading';
import toast from '../toast/toast'; import toast from '../../components/toast/toast';
import { getParameterByName } from '../../utils/url';
import Page from '../../components/Page';
type IProps = { const UserProfile: FunctionComponent = () => {
userId: string; const userId = getParameterByName('userId');
}
const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
const [ userName, setUserName ] = useState(''); const [ userName, setUserName ] = useState('');
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
@ -57,11 +56,11 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
} }
if (user.PrimaryImageTag) { if (user.PrimaryImageTag) {
(page.querySelector('.btnAddImage') as HTMLButtonElement).classList.add('hide'); (page.querySelector('#btnAddImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).classList.remove('hide'); (page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.remove('hide');
} else if (appHost.supports('fileinput') && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) { } else if (appHost.supports('fileinput') && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).classList.add('hide'); (page.querySelector('#btnDeleteImage') as HTMLButtonElement).classList.add('hide');
(page.querySelector('.btnAddImage') as HTMLButtonElement).classList.remove('hide'); (page.querySelector('#btnAddImage') as HTMLButtonElement).classList.remove('hide');
} }
}); });
loading.hide(); loading.hide();
@ -120,7 +119,7 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
reader.readAsDataURL(file); reader.readAsDataURL(file);
}; };
(page.querySelector('.btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () { (page.querySelector('#btnDeleteImage') as HTMLButtonElement).addEventListener('click', function () {
confirm( confirm(
globalize.translate('DeleteImageConfirmation'), globalize.translate('DeleteImageConfirmation'),
globalize.translate('DeleteImage') globalize.translate('DeleteImage')
@ -133,7 +132,7 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
}); });
}); });
(page.querySelector('.btnAddImage') as HTMLButtonElement).addEventListener('click', function () { (page.querySelector('#btnAddImage') as HTMLButtonElement).addEventListener('click', function () {
const uploadImage = page.querySelector('#uploadImage') as HTMLInputElement; const uploadImage = page.querySelector('#uploadImage') as HTMLInputElement;
uploadImage.value = ''; uploadImage.value = '';
uploadImage.click(); uploadImage.click();
@ -145,13 +144,18 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
}, [reloadUser, userId]); }, [reloadUser, userId]);
return ( return (
<div ref={element}> <Page
<div className='padded-left padded-right padded-bottom-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 <div
className='readOnlyContent' className='readOnlyContent'
style={{margin: '0 auto', marginBottom: '1.8em', padding: '0 1em', display: 'flex', flexDirection: 'row', alignItems: 'center'}} style={{margin: '0 auto', marginBottom: '1.8em', padding: '0 1em', display: 'flex', flexDirection: 'row', alignItems: 'center'}}
> >
<div <div
className='imagePlaceHolder'
style={{position: 'relative', display: 'inline-block', maxWidth: 200 }} style={{position: 'relative', display: 'inline-block', maxWidth: 200 }}
> >
<input <input
@ -172,12 +176,14 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
<br /> <br />
<ButtonElement <ButtonElement
type='button' type='button'
className='raised btnAddImage hide' id='btnAddImage'
className='raised button-submit hide'
title='ButtonAddImage' title='ButtonAddImage'
/> />
<ButtonElement <ButtonElement
type='button' type='button'
className='raised btnDeleteImage hide' id='btnDeleteImage'
className='raised hide'
title='DeleteImage' title='DeleteImage'
/> />
</div> </div>
@ -186,8 +192,9 @@ const UserProfilePage: FunctionComponent<IProps> = ({userId}: IProps) => {
userId={userId} userId={userId}
/> />
</div> </div>
</div> </Page>
); );
}; };
export default UserProfilePage; export default UserProfile;

View file

@ -2,16 +2,17 @@ import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, {FunctionComponent, useEffect, useState, useRef} from 'react'; import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import Dashboard from '../../utils/dashboard'; import Dashboard from '../../utils/dashboard';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import loading from '../loading/loading'; import loading from '../../components/loading/loading';
import dom from '../../scripts/dom'; import dom from '../../scripts/dom';
import confirm from '../../components/confirm/confirm'; import confirm from '../../components/confirm/confirm';
import UserCardBox from '../dashboard/users/UserCardBox'; import UserCardBox from '../../components/dashboard/users/UserCardBox';
import SectionTitleContainer from '../dashboard/users/SectionTitleContainer'; import SectionTitleContainer from '../../elements/SectionTitleContainer';
import '../../elements/emby-button/emby-button'; import '../../elements/emby-button/emby-button';
import '../../elements/emby-button/paper-icon-button-light'; import '../../elements/emby-button/paper-icon-button-light';
import '../../components/cardbuilder/card.scss'; import '../../components/cardbuilder/card.scss';
import '../../components/indicators/indicators.scss'; import '../../components/indicators/indicators.scss';
import '../../assets/css/flexstyles.scss'; import '../../assets/css/flexstyles.scss';
import Page from '../../components/Page';
type MenuEntry = { type MenuEntry = {
name?: string; name?: string;
@ -19,7 +20,7 @@ type MenuEntry = {
icon?: string; icon?: string;
} }
const UserProfilesPage: FunctionComponent = () => { const UserProfiles: FunctionComponent = () => {
const [ users, setUsers ] = useState<UserDto[]>([]); const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
@ -124,19 +125,28 @@ const UserProfilesPage: FunctionComponent = () => {
} }
}); });
(page.querySelector('.btnAddUser') as HTMLButtonElement).addEventListener('click', function() { (page.querySelector('#btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('usernew.html'); Dashboard.navigate('usernew.html');
}); });
}, []); }, []);
return ( return (
<div ref={element}> <Page
<div className='content-primary'> id='userProfilesPage'
<SectionTitleContainer className='mainAnimatedPage type-interior userProfilesPage fullWidthContent'
title={globalize.translate('HeaderUsers')} >
isBtnVisible={true} <div ref={element} className='content-primary'>
titleLink='https://docs.jellyfin.org/general/server/users/adding-managing-users.html' <div className='verticalSection'>
/> <SectionTitleContainer
title={globalize.translate('HeaderUsers')}
isBtnVisible={true}
btnId='btnAddUser'
btnClassName='fab submit sectionTitleButton'
btnTitle='ButtonAddUser'
btnIcon='add'
url='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
/>
</div>
<div className='localUsers itemsContainer vertical-wrap'> <div className='localUsers itemsContainer vertical-wrap'>
{users.map(user => { {users.map(user => {
@ -144,8 +154,9 @@ const UserProfilesPage: FunctionComponent = () => {
})} })}
</div> </div>
</div> </div>
</div> </Page>
); );
}; };
export default UserProfilesPage; export default UserProfiles;

View file

@ -355,14 +355,14 @@ import '../assets/css/flexstyles.scss';
} }
} }
function refreshDashboardInfoInDrawer(apiClient) { function refreshDashboardInfoInDrawer(page, apiClient) {
currentDrawerType = 'admin'; currentDrawerType = 'admin';
loadNavDrawer(); loadNavDrawer();
if (navDrawerScrollContainer.querySelector('.adminDrawerLogo')) { if (navDrawerScrollContainer.querySelector('.adminDrawerLogo')) {
updateDashboardMenuSelectedItem(); updateDashboardMenuSelectedItem(page);
} else { } else {
createDashboardMenu(apiClient); createDashboardMenu(page, apiClient);
} }
} }
@ -370,9 +370,9 @@ import '../assets/css/flexstyles.scss';
return window.location.href.toString().toLowerCase().indexOf(url.toLowerCase()) !== -1; return window.location.href.toString().toLowerCase().indexOf(url.toLowerCase()) !== -1;
} }
function updateDashboardMenuSelectedItem() { function updateDashboardMenuSelectedItem(page) {
const links = navDrawerScrollContainer.querySelectorAll('.navMenuOption'); const links = navDrawerScrollContainer.querySelectorAll('.navMenuOption');
const currentViewId = viewManager.currentView().id; const currentViewId = page.id;
for (let i = 0, length = links.length; i < length; i++) { for (let i = 0, length = links.length; i < length; i++) {
let link = links[i]; let link = links[i];
@ -590,7 +590,7 @@ import '../assets/css/flexstyles.scss';
}); });
} }
function createDashboardMenu(apiClient) { function createDashboardMenu(page, apiClient) {
return getToolsMenuHtml(apiClient).then(function (toolsMenuHtml) { return getToolsMenuHtml(apiClient).then(function (toolsMenuHtml) {
let html = ''; let html = '';
html += '<a class="adminDrawerLogo clearLink" is="emby-linkbutton" href="#/home.html">'; html += '<a class="adminDrawerLogo clearLink" is="emby-linkbutton" href="#/home.html">';
@ -598,7 +598,7 @@ import '../assets/css/flexstyles.scss';
html += '</a>'; html += '</a>';
html += toolsMenuHtml; html += toolsMenuHtml;
navDrawerScrollContainer.innerHTML = html; navDrawerScrollContainer.innerHTML = html;
updateDashboardMenuSelectedItem(); updateDashboardMenuSelectedItem(page);
}); });
} }
@ -1017,7 +1017,7 @@ import '../assets/css/flexstyles.scss';
mainDrawerButton.classList.remove('hide'); mainDrawerButton.classList.remove('hide');
} }
refreshDashboardInfoInDrawer(apiClient); refreshDashboardInfoInDrawer(page, apiClient);
} else { } else {
if (mainDrawerButton) { if (mainDrawerButton) {
if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) { if (enableLibraryNavDrawer || (isHomePage && enableLibraryNavDrawerHome)) {

View file

@ -77,13 +77,6 @@ import { appRouter } from '../components/appRouter';
controller: 'user/menu/index' controller: 'user/menu/index'
}); });
defineRoute({
alias: '/myprofile.html',
path: 'user/profile/index.html',
autoFocus: false,
pageComponent: 'UserProfilePage'
});
defineRoute({ defineRoute({
alias: '/mypreferencescontrols.html', alias: '/mypreferencescontrols.html',
path: 'user/controls/index.html', path: 'user/controls/index.html',
@ -429,53 +422,6 @@ import { appRouter } from '../components/appRouter';
controller: 'shows/tvrecommended' controller: 'shows/tvrecommended'
}); });
defineRoute({
alias: '/useredit.html',
path: 'dashboard/users/useredit.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserEditPage'
});
defineRoute({
alias: '/userlibraryaccess.html',
path: 'dashboard/users/userlibraryaccess.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserLibraryAccessPage'
});
defineRoute({
alias: '/usernew.html',
path: 'dashboard/users/usernew.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'NewUserPage'
});
defineRoute({
alias: '/userparentalcontrol.html',
path: 'dashboard/users/userparentalcontrol.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserParentalControl'
});
defineRoute({
alias: '/userpassword.html',
path: 'dashboard/users/userpassword.html',
autoFocus: false,
pageComponent: 'UserPasswordPage'
});
defineRoute({
alias: '/userprofiles.html',
path: 'dashboard/users/userprofiles.html',
autoFocus: false,
roles: 'admin',
pageComponent: 'UserProfilesPage'
});
defineRoute({ defineRoute({
alias: '/wizardremoteaccess.html', alias: '/wizardremoteaccess.html',
path: 'wizard/remote/index.html', path: 'wizard/remote/index.html',