mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into disable-embedded-subs
This commit is contained in:
commit
7912daf0c6
158 changed files with 8225 additions and 7117 deletions
|
@ -280,6 +280,16 @@ import 'material-design-icons-iconfont';
|
|||
element.removeEventListener(name, fn);
|
||||
}
|
||||
|
||||
updateControls(query) {
|
||||
if (query.NameLessThan) {
|
||||
this.value('#');
|
||||
} else {
|
||||
this.value(query.NameStartsWith);
|
||||
}
|
||||
|
||||
this.visible(query.SortBy.indexOf('SortName') === 0);
|
||||
}
|
||||
|
||||
visible(visible) {
|
||||
const element = this.options.element;
|
||||
element.style.visibility = visible ? 'visible' : 'hidden';
|
||||
|
|
|
@ -797,6 +797,10 @@ class AppRouter {
|
|||
return '#!/list.html?type=Programs&IsAiring=true&serverId=' + options.serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'channels') {
|
||||
return '#!/livetv.html?tab=2&serverId=' + options.serverId;
|
||||
}
|
||||
|
||||
if (options.section === 'dvrschedule') {
|
||||
return '#!/livetv.html?tab=4&serverId=' + options.serverId;
|
||||
}
|
||||
|
|
|
@ -199,7 +199,6 @@ const supportedFeatures = function () {
|
|||
if (browser.operaTv || browser.tizen || browser.orsay || browser.web0s) {
|
||||
features.push('exit');
|
||||
} else {
|
||||
features.push('exitmenu');
|
||||
features.push('plugins');
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,9 @@ import layoutManager from './layoutManager';
|
|||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
} else if (activeElement.classList.contains('btnSelectView')) {
|
||||
candidates.push(container.querySelector('.btnSelectView'));
|
||||
} else if (activeElement.classList.contains('btnPlay')) {
|
||||
// Resume has priority over Play
|
||||
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnResume')));
|
||||
}
|
||||
|
||||
candidates.push(activeElement);
|
||||
|
|
|
@ -333,6 +333,7 @@ button::-moz-focus-inner {
|
|||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.innerCardFooter > .cardText {
|
||||
|
@ -355,7 +356,8 @@ button::-moz-focus-inner {
|
|||
background-position: center center;
|
||||
}
|
||||
|
||||
.cardTextCentered {
|
||||
.cardTextCentered,
|
||||
.cardTextCentered > .textActionButton {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
|
@ -771,6 +771,7 @@ import ServerConnections from '../ServerConnections';
|
|||
* @returns {string} HTML markup of the card's footer text element.
|
||||
*/
|
||||
function getCardFooterText(item, apiClient, options, showTitle, forceName, overlayText, imgUrl, footerClass, progressHtml, logoUrl, isOuterFooter) {
|
||||
item = item.ProgramInfo || item;
|
||||
let html = '';
|
||||
|
||||
if (logoUrl) {
|
||||
|
@ -855,6 +856,10 @@ import ServerConnections from '../ServerConnections';
|
|||
}
|
||||
}
|
||||
|
||||
if (item.ExtraType && item.ExtraType !== 'Unknown') {
|
||||
lines.push(globalize.translate(item.ExtraType));
|
||||
}
|
||||
|
||||
if (options.showItemCounts) {
|
||||
lines.push(getItemCountsHtml(options, item));
|
||||
}
|
||||
|
|
60
src/components/dashboard/users/AccessScheduleList.tsx
Normal file
60
src/components/dashboard/users/AccessScheduleList.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import datetime from '../../../scripts/datetime';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createButtonElement = ({index}) => ({
|
||||
__html: `<button
|
||||
type='button'
|
||||
is='paper-icon-button-light'
|
||||
class='btnDelete listItemButton'
|
||||
data-index='${index}'
|
||||
>
|
||||
<span class='material-icons delete' />
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
index: number;
|
||||
Id: number;
|
||||
DayOfWeek?: string;
|
||||
StartHour?: number ;
|
||||
EndHour?: number;
|
||||
}
|
||||
|
||||
function getDisplayTime(hours) {
|
||||
let minutes = 0;
|
||||
const pct = hours % 1;
|
||||
|
||||
if (pct) {
|
||||
minutes = Math.floor(60 * pct);
|
||||
}
|
||||
|
||||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
}
|
||||
|
||||
const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartHour, EndHour}: IProps) => {
|
||||
return (
|
||||
<div
|
||||
className='liSchedule listItem'
|
||||
data-day={ DayOfWeek}
|
||||
data-start={ StartHour}
|
||||
data-end={ EndHour}
|
||||
>
|
||||
<div className='listItemBody two-line'>
|
||||
<h3 className='listItemBodyText'>
|
||||
{globalize.translate(DayOfWeek)}
|
||||
</h3>
|
||||
<div className='listItemBodyText secondary'>
|
||||
{getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement({
|
||||
index: index
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessScheduleList;
|
38
src/components/dashboard/users/BlockedTagList.tsx
Normal file
38
src/components/dashboard/users/BlockedTagList.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
|
||||
const createButtonElement = ({tag}) => ({
|
||||
__html: `<button
|
||||
type='button'
|
||||
is='paper-icon-button-light'
|
||||
class='blockedTag btnDeleteTag listItemButton'
|
||||
data-tag='${tag}'
|
||||
>
|
||||
<span class='material-icons delete' />
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
|
||||
return (
|
||||
<div className='paperList'>
|
||||
<div className='listItem'>
|
||||
<div className='listItemBody'>
|
||||
<h3 className='listItemBodyText'>
|
||||
{tag}
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
dangerouslySetInnerHTML={createButtonElement({
|
||||
tag: tag
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlockedTagList;
|
|
@ -1,8 +1,8 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createCheckBoxElement = ({ type, className, title }) => ({
|
||||
__html: `<label>
|
||||
const createCheckBoxElement = ({ labelClassName, type, className, title }) => ({
|
||||
__html: `<label class="${labelClassName}">
|
||||
<input
|
||||
is="emby-checkbox"
|
||||
type="${type}"
|
||||
|
@ -13,15 +13,18 @@ const createCheckBoxElement = ({ type, className, title }) => ({
|
|||
});
|
||||
|
||||
type IProps = {
|
||||
labelClassName?: string;
|
||||
type?: string;
|
||||
className?: string;
|
||||
title?: string
|
||||
}
|
||||
|
||||
const CheckBoxElement: FunctionComponent<IProps> = ({ type, className, title }: IProps) => {
|
||||
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)
|
||||
|
|
|
@ -4,27 +4,33 @@ type IProps = {
|
|||
className?: string;
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
ItemType?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string;
|
||||
}
|
||||
|
||||
const createCheckBoxElement = ({className, Name, Id}) => ({
|
||||
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}) => ({
|
||||
__html: `<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
is="emby-checkbox"
|
||||
class="${className}"
|
||||
data-id="${Id}"
|
||||
${dataAttributes} ${checkedAttribute}
|
||||
/>
|
||||
<span>${Name}</span>
|
||||
<span>${Name} ${AppName}</span>
|
||||
</label>`
|
||||
});
|
||||
|
||||
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id}: IProps) => {
|
||||
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, ItemType, AppName, checkedAttribute}: IProps) => {
|
||||
return (
|
||||
<div
|
||||
className='sectioncheckbox'
|
||||
dangerouslySetInnerHTML={createCheckBoxElement({
|
||||
className: className,
|
||||
Name: Name,
|
||||
Id: Id
|
||||
dataAttributes: ItemType ? `data-itemtype='${ItemType}'` : `data-id='${Id}'`,
|
||||
AppName: AppName ? `- ${AppName}` : '',
|
||||
checkedAttribute: checkedAttribute
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
|
30
src/components/dashboard/users/LinkEditUserPreferences.tsx
Normal file
30
src/components/dashboard/users/LinkEditUserPreferences.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
type IProps = {
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const createLinkElement = ({ className, title }) => ({
|
||||
__html: `<a
|
||||
is="emby-linkbutton"
|
||||
class="${className}"
|
||||
href='#'
|
||||
>
|
||||
${title}
|
||||
</a>`
|
||||
});
|
||||
|
||||
const LinkEditUserPreferences: FunctionComponent<IProps> = ({ className, title }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createLinkElement({
|
||||
className: className,
|
||||
title: globalize.translate(title)
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkEditUserPreferences;
|
52
src/components/dashboard/users/SectionTabs.tsx
Normal file
52
src/components/dashboard/users/SectionTabs.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
type IProps = {
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
const createLinkElement = ({ activeTab }) => ({
|
||||
__html: `<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'useredit' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('useredit.html', true);">
|
||||
${globalize.translate('Profile')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userlibraryaccess' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userlibraryaccess.html', true);">
|
||||
${globalize.translate('TabAccess')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userparentalcontrol' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userparentalcontrol.html', true);">
|
||||
${globalize.translate('TabParentalControl')}
|
||||
</a>
|
||||
<a href="#"
|
||||
is="emby-linkbutton"
|
||||
data-role="button"
|
||||
class="${activeTab === 'userpassword' ? 'ui-btn-active' : ''}"
|
||||
onclick="Dashboard.navigate('userpassword.html', true);">
|
||||
${globalize.translate('HeaderPassword')}
|
||||
</a>`
|
||||
});
|
||||
|
||||
const SectionTabs: FunctionComponent<IProps> = ({activeTab}: IProps) => {
|
||||
return (
|
||||
<div
|
||||
data-role='controlgroup'
|
||||
data-type='horizontal'
|
||||
className='localnav'
|
||||
dangerouslySetInnerHTML={createLinkElement({
|
||||
activeTab: activeTab
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionTabs;
|
|
@ -1,23 +1,24 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
type IProps = {
|
||||
title: string;
|
||||
className?: string;
|
||||
icon: string,
|
||||
}
|
||||
|
||||
const createButtonElement = ({ className, title, icon }) => ({
|
||||
__html: `<button
|
||||
is="emby-button"
|
||||
type="button"
|
||||
class="${className}"
|
||||
style="margin-left:1em;"
|
||||
title="${title}">
|
||||
title="${title}"
|
||||
>
|
||||
<span class="material-icons ${icon}"></span>
|
||||
</button>`
|
||||
});
|
||||
|
||||
type IProps = {
|
||||
title?: string;
|
||||
className?: string;
|
||||
icon?: string,
|
||||
}
|
||||
|
||||
const SectionTitleButtonElement: FunctionComponent<IProps> = ({ className, title, icon }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
|
|
43
src/components/dashboard/users/SelectElement.tsx
Normal file
43
src/components/dashboard/users/SelectElement.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, label, option }) => ({
|
||||
__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 + '>' + provider.Name + '</option>';
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
className: className,
|
||||
label: globalize.translate(label),
|
||||
option: renderOption
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectElement;
|
46
src/components/dashboard/users/SelectMaxParentalRating.tsx
Normal file
46
src/components/dashboard/users/SelectMaxParentalRating.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, label, option }) => ({
|
||||
__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}'>${rating.Name}</option>`;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={createSelectElement({
|
||||
className: className,
|
||||
label: globalize.translate(label),
|
||||
option: renderOption()
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectMaxParentalRating;
|
|
@ -0,0 +1,35 @@
|
|||
import React, { FunctionComponent } from 'react';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
|
||||
const createSelectElement = ({ className, id, label }) => ({
|
||||
__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;
|
|
@ -32,15 +32,11 @@ function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
|
|||
|
||||
const promises = [];
|
||||
|
||||
if (path === 'Network') {
|
||||
promises.push(ApiClient.getNetworkDevices());
|
||||
if (path) {
|
||||
promises.push(ApiClient.getDirectoryContents(path, fileOptions));
|
||||
promises.push(ApiClient.getParentPath(path));
|
||||
} else {
|
||||
if (path) {
|
||||
promises.push(ApiClient.getDirectoryContents(path, fileOptions));
|
||||
promises.push(ApiClient.getParentPath(path));
|
||||
} else {
|
||||
promises.push(ApiClient.getDrives());
|
||||
}
|
||||
promises.push(ApiClient.getDrives());
|
||||
}
|
||||
|
||||
Promise.all(promises).then(
|
||||
|
@ -61,10 +57,6 @@ function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
|
|||
html += getItem(cssClass, folder.Type, folder.Path, folder.Name);
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
html += getItem('lnkPath lnkDirectory', '', 'Network', globalize.translate('ButtonNetwork'));
|
||||
}
|
||||
|
||||
page.querySelector('.results').innerHTML = html;
|
||||
loading.hide();
|
||||
}, () => {
|
||||
|
|
|
@ -532,6 +532,11 @@ import ServerConnections from '../ServerConnections';
|
|||
section: 'guide'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Guide') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('livetv', {
|
||||
serverId: apiClient.serverId(),
|
||||
section: 'channels'
|
||||
}) + '" class="raised"><span>' + globalize.translate('Channels') + '</span></a>';
|
||||
|
||||
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('recordedtv', {
|
||||
serverId: apiClient.serverId()
|
||||
}) + '" class="raised"><span>' + globalize.translate('Recordings') + '</span></a>';
|
||||
|
|
16
src/components/images/blurhash.worker.ts
Normal file
16
src/components/images/blurhash.worker.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
self.onmessage = ({ data: { hash, width, height } }): void => {
|
||||
try {
|
||||
self.postMessage({
|
||||
pixels: decode(hash, width, height),
|
||||
hsh: hash,
|
||||
width: width,
|
||||
height: height
|
||||
});
|
||||
} catch {
|
||||
throw new TypeError(`Blurhash ${hash} is not valid`);
|
||||
}
|
||||
};
|
||||
/* eslint-enable no-restricted-globals */
|
|
@ -1,7 +1,22 @@
|
|||
import Worker from './blurhash.worker.ts'; // eslint-disable-line import/default
|
||||
import * as lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import { decode, isBlurhashValid } from 'blurhash';
|
||||
import './style.scss';
|
||||
// eslint-disable-next-line compat/compat
|
||||
const worker = new Worker();
|
||||
const targetDic = {};
|
||||
worker.addEventListener(
|
||||
'message',
|
||||
({ data: { pixels, hsh, width, height } }) => {
|
||||
const elems = targetDic[hsh];
|
||||
if (elems && elems.length) {
|
||||
for (const elem of elems) {
|
||||
drawBlurhash(elem, pixels, width, height);
|
||||
}
|
||||
delete targetDic[hsh];
|
||||
}
|
||||
}
|
||||
);
|
||||
/* eslint-disable indent */
|
||||
|
||||
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
|
||||
|
@ -12,42 +27,45 @@ import './style.scss';
|
|||
fillImageElement(elem, source);
|
||||
}
|
||||
|
||||
function itemBlurhashing(target, blurhashstr) {
|
||||
if (isBlurhashValid(blurhashstr)) {
|
||||
// Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 seems to be the sweet spot for us,
|
||||
function drawBlurhash(target, pixels, width, height) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imgData = ctx.createImageData(width, height);
|
||||
|
||||
imgData.data.set(pixels);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// This class is just an utility class, so users can customize the canvas using their own CSS.
|
||||
canvas.classList.add('blurhash-canvas');
|
||||
|
||||
target.parentNode.insertBefore(canvas, target);
|
||||
target.classList.add('blurhashed');
|
||||
target.removeAttribute('data-blurhash');
|
||||
});
|
||||
}
|
||||
|
||||
function itemBlurhashing(target, hash) {
|
||||
try {
|
||||
// Although the default values recommended by Blurhash developers is 32x32, a size of 20x20 seems to be the sweet spot for us,
|
||||
// improving the performance and reducing the memory usage, while retaining almost full blur quality.
|
||||
// Lower values had more visible pixelation
|
||||
const width = 18;
|
||||
const height = 18;
|
||||
let pixels;
|
||||
try {
|
||||
pixels = decode(blurhashstr, width, height);
|
||||
} catch (err) {
|
||||
console.error('Blurhash decode error: ', err);
|
||||
target.classList.add('non-blurhashable');
|
||||
return;
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imgData = ctx.createImageData(width, height);
|
||||
const width = 20;
|
||||
const height = 20;
|
||||
targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target);
|
||||
targetDic[hash].push(target);
|
||||
|
||||
imgData.data.set(pixels);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
canvas.classList.add('blurhash-canvas');
|
||||
if (userSettings.enableFastFadein()) {
|
||||
canvas.classList.add('lazy-blurhash-fadein-fast');
|
||||
} else {
|
||||
canvas.classList.add('lazy-blurhash-fadein');
|
||||
}
|
||||
|
||||
target.parentNode.insertBefore(canvas, target);
|
||||
target.classList.add('blurhashed');
|
||||
target.removeAttribute('data-blurhash');
|
||||
worker.postMessage({
|
||||
hash,
|
||||
width,
|
||||
height
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
target.classList.add('non-blurhashable');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,14 +83,25 @@ import './style.scss';
|
|||
}
|
||||
|
||||
if (entry.intersectionRatio > 0) {
|
||||
if (source) fillImageElement(target, source);
|
||||
if (source) {
|
||||
fillImageElement(target, source);
|
||||
}
|
||||
} else if (!source) {
|
||||
requestAnimationFrame(() => {
|
||||
emptyImageElement(target);
|
||||
});
|
||||
emptyImageElement(target);
|
||||
}
|
||||
}
|
||||
|
||||
function onAnimationEnd(event) {
|
||||
const elem = event.target;
|
||||
requestAnimationFrame(() => {
|
||||
const canvas = elem.previousSibling;
|
||||
if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') {
|
||||
canvas.classList.add('lazy-hidden');
|
||||
}
|
||||
});
|
||||
elem.removeEventListener('animationend', onAnimationEnd);
|
||||
}
|
||||
|
||||
function fillImageElement(elem, url) {
|
||||
if (url === undefined) {
|
||||
throw new TypeError('url cannot be undefined');
|
||||
|
@ -82,6 +111,7 @@ import './style.scss';
|
|||
preloaderImg.src = url;
|
||||
|
||||
elem.classList.add('lazy-hidden');
|
||||
elem.addEventListener('animationend', onAnimationEnd);
|
||||
|
||||
preloaderImg.addEventListener('load', () => {
|
||||
requestAnimationFrame(() => {
|
||||
|
@ -92,25 +122,22 @@ import './style.scss';
|
|||
}
|
||||
elem.removeAttribute('data-src');
|
||||
|
||||
elem.classList.remove('lazy-hidden');
|
||||
if (userSettings.enableFastFadein()) {
|
||||
elem.classList.add('lazy-image-fadein-fast');
|
||||
} else {
|
||||
elem.classList.add('lazy-image-fadein');
|
||||
}
|
||||
|
||||
const canvas = elem.previousSibling;
|
||||
if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') {
|
||||
canvas.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
|
||||
canvas.classList.add('lazy-hidden');
|
||||
}
|
||||
elem.classList.remove('lazy-hidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function emptyImageElement(elem) {
|
||||
// block repeated call - requestAnimationFrame twice for one image
|
||||
if (elem.getAttribute('data-src')) return;
|
||||
elem.removeEventListener('animationend', onAnimationEnd);
|
||||
const canvas = elem.previousSibling;
|
||||
if (canvas && canvas.tagName === 'CANVAS') {
|
||||
canvas.classList.remove('lazy-hidden');
|
||||
}
|
||||
|
||||
let url;
|
||||
|
||||
|
@ -125,16 +152,6 @@ import './style.scss';
|
|||
|
||||
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
|
||||
elem.classList.add('lazy-hidden');
|
||||
|
||||
const canvas = elem.previousSibling;
|
||||
if (canvas && canvas.tagName === 'CANVAS') {
|
||||
canvas.classList.remove('lazy-hidden');
|
||||
if (userSettings.enableFastFadein()) {
|
||||
canvas.classList.add('lazy-image-fadein-fast');
|
||||
} else {
|
||||
canvas.classList.add('lazy-image-fadein');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function lazyChildren(elem) {
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
.lazy-image-fadein {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.lazy-image-fadein-fast {
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
|
||||
.lazy-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
@ -22,12 +8,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.lazy-blurhash-fadein-fast {
|
||||
.lazy-image-fadein {
|
||||
opacity: 1;
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
|
||||
.lazy-image-fadein-fast {
|
||||
opacity: 1;
|
||||
animation: fadein 0.1s;
|
||||
}
|
||||
|
||||
.lazy-blurhash-fadein {
|
||||
animation: fadein 0.4s;
|
||||
.lazy-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.blurhash-canvas {
|
||||
|
|
|
@ -20,7 +20,7 @@ export function getDisplayName(item, options = {}) {
|
|||
}
|
||||
if (item.Type === 'Episode' && item.ParentIndexNumber === 0) {
|
||||
name = globalize.translate('ValueSpecialEpisodeName', name);
|
||||
} else if ((item.Type === 'Episode' || item.Type === 'Program') && item.IndexNumber != null && item.ParentIndexNumber != null && options.includeIndexNumber !== false) {
|
||||
} else if ((item.Type === 'Episode' || item.Type === 'Program' || item.Type === 'Recording') && item.IndexNumber != null && item.ParentIndexNumber != null && options.includeIndexNumber !== false) {
|
||||
let displayIndexNumber = item.IndexNumber;
|
||||
|
||||
let number = displayIndexNumber;
|
||||
|
|
|
@ -143,10 +143,8 @@ import template from './itemMediaInfo.template.html';
|
|||
if (stream.NalLengthSize) {
|
||||
attributes.push(createAttribute('NAL', stream.NalLengthSize));
|
||||
}
|
||||
if (stream.Type !== 'Video') {
|
||||
if (stream.Type === 'Subtitle' || stream.Type === 'Audio') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No')));
|
||||
}
|
||||
if (stream.Type === 'Subtitle') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No')));
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No')));
|
||||
}
|
||||
|
|
|
@ -13,7 +13,10 @@
|
|||
callback(entry);
|
||||
});
|
||||
},
|
||||
{rootMargin: '25%'});
|
||||
{
|
||||
rootMargin: '50%',
|
||||
threshold: 0
|
||||
});
|
||||
|
||||
this.observer = observer;
|
||||
}
|
||||
|
|
|
@ -416,8 +416,8 @@ import template from './libraryoptionseditor.template.html';
|
|||
} else {
|
||||
parent.querySelector('.fldAllowEmbeddedSubtitlesContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies');
|
||||
|
||||
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies' && contentType !== 'mixed');
|
||||
|
||||
return populateMetadataSettings(parent, contentType);
|
||||
}
|
||||
|
|
|
@ -164,6 +164,10 @@
|
|||
padding: 0.2em;
|
||||
}
|
||||
|
||||
.listItemImage .cardImageIcon {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
@media all and (max-width: 64em) {
|
||||
.listItemImage-large {
|
||||
width: 22vw;
|
||||
|
|
|
@ -73,7 +73,7 @@ import template from './mediaLibraryCreator.template.html';
|
|||
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
|
||||
const value = this.value;
|
||||
const dlg = $(this).parents('.dialog')[0];
|
||||
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value == 'mixed' ? '' : value);
|
||||
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
|
||||
|
||||
if (value) {
|
||||
dlg.querySelector('.libraryOptions').classList.remove('hide');
|
||||
|
|
|
@ -131,7 +131,8 @@ import '../../elements/emby-button/emby-button';
|
|||
if ((item.Type === 'Episode' || item.MediaType === 'Photo') && options.originalAirDate !== false) {
|
||||
if (item.PremiereDate) {
|
||||
try {
|
||||
date = datetime.parseISO8601Date(item.PremiereDate);
|
||||
//don't modify date to locale if episode. Only Dates (not times) are stored, or editable in the edit metadata dialog
|
||||
date = datetime.parseISO8601Date(item.PremiereDate, item.Type !== 'Episode');
|
||||
|
||||
text = datetime.toLocaleDateString(date);
|
||||
miscInfo.push(text);
|
||||
|
|
|
@ -175,6 +175,12 @@ import itemHelper from '../itemHelper';
|
|||
apiClient.getItem(apiClient.getCurrentUserId(), selectedItems[0]).then(firstItem => {
|
||||
const menuItems = [];
|
||||
|
||||
menuItems.push({
|
||||
name: globalize.translate('SelectAll'),
|
||||
id: 'selectall',
|
||||
icon: 'select_all'
|
||||
});
|
||||
|
||||
menuItems.push({
|
||||
name: globalize.translate('AddToCollection'),
|
||||
id: 'addtocollection',
|
||||
|
@ -246,6 +252,19 @@ import itemHelper from '../itemHelper';
|
|||
const serverId = apiClient.serverInfo().Id;
|
||||
|
||||
switch (id) {
|
||||
case 'selectall':
|
||||
{
|
||||
const elems = document.querySelectorAll('.itemSelectionPanel');
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const chkItemSelect = elems[i].querySelector('.chkItemSelect');
|
||||
|
||||
if (chkItemSelect && !chkItemSelect.classList.contains('checkedInitial') && !chkItemSelect.checked && chkItemSelect.getBoundingClientRect().width != 0) {
|
||||
chkItemSelect.checked = true;
|
||||
updateItemSelection(chkItemSelect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'addtocollection':
|
||||
import('../collectionEditor/collectionEditor').then(({default: collectionEditor}) => {
|
||||
new collectionEditor({
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB |
|
@ -3,6 +3,8 @@ import { playbackManager } from '../playback/playbackmanager';
|
|||
import { Events } from 'jellyfin-apiclient';
|
||||
import globalize from '../../scripts/globalize';
|
||||
|
||||
import NotificationIcon from './notificationicon.png';
|
||||
|
||||
function onOneDocumentClick() {
|
||||
document.removeEventListener('click', onOneDocumentClick);
|
||||
document.removeEventListener('keydown', onOneDocumentClick);
|
||||
|
@ -71,8 +73,8 @@ function showNotification(options, timeoutMs, apiClient) {
|
|||
|
||||
options.data = options.data || {};
|
||||
options.data.serverId = apiClient.serverInfo().Id;
|
||||
options.icon = options.icon || getIconUrl();
|
||||
options.badge = options.badge || getIconUrl('badge.png');
|
||||
options.icon = options.icon || NotificationIcon;
|
||||
options.badge = options.badge || NotificationIcon;
|
||||
|
||||
resetRegistration();
|
||||
|
||||
|
@ -148,11 +150,6 @@ function onLibraryChanged(data, apiClient) {
|
|||
});
|
||||
}
|
||||
|
||||
function getIconUrl(name) {
|
||||
name = name || 'notificationicon.png';
|
||||
return './components/notifications/' + name;
|
||||
}
|
||||
|
||||
function showPackageInstallNotification(apiClient, installation, status) {
|
||||
apiClient.getCurrentUser().then(function (user) {
|
||||
if (!user.Policy.IsAdministrator) {
|
||||
|
@ -180,7 +177,7 @@ function showPackageInstallNotification(apiClient, installation, status) {
|
|||
{
|
||||
action: 'cancel-install',
|
||||
title: globalize.translate('ButtonCancel'),
|
||||
icon: getIconUrl()
|
||||
icon: NotificationIcon
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -249,7 +246,7 @@ Events.on(serverNotifications, 'RestartRequired', function (e, apiClient) {
|
|||
{
|
||||
action: 'restart',
|
||||
title: globalize.translate('Restart'),
|
||||
icon: getIconUrl()
|
||||
icon: NotificationIcon
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -68,7 +68,6 @@ const NewUserPage: FunctionComponent = () => {
|
|||
IsHidden: false
|
||||
}));
|
||||
const promiseChannels = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
|
||||
// eslint-disable-next-line compat/compat
|
||||
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
|
||||
loadMediaFolders(responses[0].Items);
|
||||
loadChannels(responses[1].Items);
|
||||
|
@ -191,6 +190,7 @@ const NewUserPage: FunctionComponent = () => {
|
|||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute=''
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -219,6 +219,7 @@ const NewUserPage: FunctionComponent = () => {
|
|||
className='chkChannel'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute=''
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
526
src/components/pages/UserEditPage.tsx
Normal file
526
src/components/pages/UserEditPage.tsx
Normal file
|
@ -0,0 +1,526 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import Dashboard from '../../scripts/clientUtils';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import { appRouter } from '../appRouter';
|
||||
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
|
||||
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||
import InputElement from '../dashboard/users/InputElement';
|
||||
import LinkEditUserPreferences from '../dashboard/users/LinkEditUserPreferences';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SelectElement from '../dashboard/users/SelectElement';
|
||||
import SelectSyncPlayAccessElement from '../dashboard/users/SelectSyncPlayAccessElement';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import loading from '../loading/loading';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
checkedAttribute: string
|
||||
}
|
||||
|
||||
const UserEditPage: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]);
|
||||
const [ authProviders, setAuthProviders ] = useState([]);
|
||||
const [ passwordResetProviders, setPasswordResetProviders ] = useState([]);
|
||||
|
||||
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
|
||||
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
|
||||
|
||||
const element = useRef(null);
|
||||
|
||||
const triggerChange = (select) => {
|
||||
const evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('change', false, true);
|
||||
select.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
const getUser = () => {
|
||||
const userId = appRouter.param('userId');
|
||||
return window.ApiClient.getUser(userId);
|
||||
};
|
||||
|
||||
const loadAuthProviders = useCallback((user, providers) => {
|
||||
const fldSelectLoginProvider = element?.current?.querySelector('.fldSelectLoginProvider');
|
||||
providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide');
|
||||
|
||||
setAuthProviders(providers);
|
||||
|
||||
const currentProviderId = user.Policy.AuthenticationProviderId;
|
||||
setAuthenticationProviderId(currentProviderId);
|
||||
}, []);
|
||||
|
||||
const loadPasswordResetProviders = useCallback((user, providers) => {
|
||||
const fldSelectPasswordResetProvider = element?.current?.querySelector('.fldSelectPasswordResetProvider');
|
||||
providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide');
|
||||
|
||||
setPasswordResetProviders(providers);
|
||||
|
||||
const currentProviderId = user.Policy.PasswordResetProviderId;
|
||||
setPasswordResetProviderId(currentProviderId);
|
||||
}, []);
|
||||
|
||||
const loadDeleteFolders = useCallback((user, mediaFolders) => {
|
||||
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
|
||||
SupportsMediaDeletion: true
|
||||
})).then(function (channelsResult) {
|
||||
let isChecked;
|
||||
let checkedAttribute;
|
||||
const itemsArr: ItemsArr[] = [];
|
||||
|
||||
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 = element.current.querySelector('.chkEnableDeleteAllFolders');
|
||||
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
|
||||
triggerChange(chkEnableDeleteAllFolders);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user) => {
|
||||
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 = element?.current?.querySelector('.disabledUserBanner');
|
||||
user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide');
|
||||
|
||||
const txtUserName = element?.current?.querySelector('#txtUserName');
|
||||
txtUserName.disabled = '';
|
||||
txtUserName.removeAttribute('disabled');
|
||||
|
||||
const lnkEditUserPreferences = element?.current?.querySelector('.lnkEditUserPreferences');
|
||||
lnkEditUserPreferences.setAttribute('href', 'mypreferencesmenu.html?userId=' + user.Id);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
setUserName(user.Name);
|
||||
element.current.querySelector('#txtUserName').value = user.Name;
|
||||
element.current.querySelector('.chkIsAdmin').checked = user.Policy.IsAdministrator;
|
||||
element.current.querySelector('.chkDisabled').checked = user.Policy.IsDisabled;
|
||||
element.current.querySelector('.chkIsHidden').checked = user.Policy.IsHidden;
|
||||
element.current.querySelector('.chkRemoteControlSharedDevices').checked = user.Policy.EnableSharedDeviceControl;
|
||||
element.current.querySelector('.chkEnableRemoteControlOtherUsers').checked = user.Policy.EnableRemoteControlOfOtherUsers;
|
||||
element.current.querySelector('.chkEnableDownloading').checked = user.Policy.EnableContentDownloading;
|
||||
element.current.querySelector('.chkManageLiveTv').checked = user.Policy.EnableLiveTvManagement;
|
||||
element.current.querySelector('.chkEnableLiveTvAccess').checked = user.Policy.EnableLiveTvAccess;
|
||||
element.current.querySelector('.chkEnableMediaPlayback').checked = user.Policy.EnableMediaPlayback;
|
||||
element.current.querySelector('.chkEnableAudioPlaybackTranscoding').checked = user.Policy.EnableAudioPlaybackTranscoding;
|
||||
element.current.querySelector('.chkEnableVideoPlaybackTranscoding').checked = user.Policy.EnableVideoPlaybackTranscoding;
|
||||
element.current.querySelector('.chkEnableVideoPlaybackRemuxing').checked = user.Policy.EnablePlaybackRemuxing;
|
||||
element.current.querySelector('.chkForceRemoteSourceTranscoding').checked = user.Policy.ForceRemoteSourceTranscoding;
|
||||
element.current.querySelector('.chkRemoteAccess').checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
|
||||
element.current.querySelector('#txtRemoteClientBitrateLimit').value = user.Policy.RemoteClientBitrateLimit / 1e6 || '';
|
||||
element.current.querySelector('#txtLoginAttemptsBeforeLockout').value = user.Policy.LoginAttemptsBeforeLockout || '0';
|
||||
element.current.querySelector('#txtMaxActiveSessions').value = user.Policy.MaxActiveSessions || '0';
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
element.current.querySelector('#selectSyncPlayAccess').value = user.Policy.SyncPlayAccess;
|
||||
}
|
||||
loading.hide();
|
||||
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
getUser().then(function (user) {
|
||||
loadUser(user);
|
||||
});
|
||||
}, [loadUser]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
|
||||
function onSaveComplete() {
|
||||
Dashboard.navigate('userprofiles.html');
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
const saveUser = (user) => {
|
||||
user.Name = element?.current?.querySelector('#txtUserName').value;
|
||||
user.Policy.IsAdministrator = element?.current?.querySelector('.chkIsAdmin').checked;
|
||||
user.Policy.IsHidden = element?.current?.querySelector('.chkIsHidden').checked;
|
||||
user.Policy.IsDisabled = element?.current?.querySelector('.chkDisabled').checked;
|
||||
user.Policy.EnableRemoteControlOfOtherUsers = element?.current?.querySelector('.chkEnableRemoteControlOtherUsers').checked;
|
||||
user.Policy.EnableLiveTvManagement = element?.current?.querySelector('.chkManageLiveTv').checked;
|
||||
user.Policy.EnableLiveTvAccess = element?.current?.querySelector('.chkEnableLiveTvAccess').checked;
|
||||
user.Policy.EnableSharedDeviceControl = element?.current?.querySelector('.chkRemoteControlSharedDevices').checked;
|
||||
user.Policy.EnableMediaPlayback = element?.current?.querySelector('.chkEnableMediaPlayback').checked;
|
||||
user.Policy.EnableAudioPlaybackTranscoding = element?.current?.querySelector('.chkEnableAudioPlaybackTranscoding').checked;
|
||||
user.Policy.EnableVideoPlaybackTranscoding = element?.current?.querySelector('.chkEnableVideoPlaybackTranscoding').checked;
|
||||
user.Policy.EnablePlaybackRemuxing = element?.current?.querySelector('.chkEnableVideoPlaybackRemuxing').checked;
|
||||
user.Policy.ForceRemoteSourceTranscoding = element?.current?.querySelector('.chkForceRemoteSourceTranscoding').checked;
|
||||
user.Policy.EnableContentDownloading = element?.current?.querySelector('.chkEnableDownloading').checked;
|
||||
user.Policy.EnableRemoteAccess = element?.current?.querySelector('.chkRemoteAccess').checked;
|
||||
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat(element?.current?.querySelector('#txtRemoteClientBitrateLimit').value || '0'));
|
||||
user.Policy.LoginAttemptsBeforeLockout = parseInt(element?.current?.querySelector('#txtLoginAttemptsBeforeLockout').value || '0');
|
||||
user.Policy.MaxActiveSessions = parseInt(element?.current?.querySelector('#txtMaxActiveSessions').value || '0');
|
||||
user.Policy.AuthenticationProviderId = element?.current?.querySelector('.selectLoginProvider').value;
|
||||
user.Policy.PasswordResetProviderId = element?.current?.querySelector('.selectPasswordResetProvider').value;
|
||||
user.Policy.EnableContentDeletion = element?.current?.querySelector('.chkEnableDeleteAllFolders').checked;
|
||||
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
if (window.ApiClient.isMinServerVersion('10.6.0')) {
|
||||
user.Policy.SyncPlayAccess = element?.current?.querySelector('#selectSyncPlayAccess').value;
|
||||
}
|
||||
window.ApiClient.updateUser(user).then(function () {
|
||||
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
|
||||
onSaveComplete();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = (e) => {
|
||||
loading.show();
|
||||
getUser().then(function (result) {
|
||||
saveUser(result);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
element?.current?.querySelector('.chkEnableDeleteAllFolders').addEventListener('change', function (this: HTMLInputElement) {
|
||||
if (this.checked) {
|
||||
element?.current?.querySelector('.deleteAccess').classList.add('hide');
|
||||
} else {
|
||||
element?.current?.querySelector('.deleteAccess').classList.remove('hide');
|
||||
}
|
||||
});
|
||||
|
||||
window.ApiClient.getServerConfiguration().then(function (config) {
|
||||
const fldRemoteAccess = element?.current?.querySelector('.fldRemoteAccess');
|
||||
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.editUserProfileForm').addEventListener('submit', onSubmit);
|
||||
|
||||
element?.current?.querySelector('.button-cancel').addEventListener('click', function() {
|
||||
window.history.back();
|
||||
});
|
||||
}, [loadData]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</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
|
||||
className= 'selectLoginProvider'
|
||||
label= 'LabelAuthProvider'
|
||||
currentProviderId={authenticationProviderId}
|
||||
providers={authProviders}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('AuthProviderHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='selectContainer fldSelectPasswordResetProvider hide'>
|
||||
<SelectElement
|
||||
className= 'selectPasswordResetProvider'
|
||||
label= 'LabelPasswordResetProvider'
|
||||
currentProviderId={passwordResetProviderId}
|
||||
providers={passwordResetProviders}
|
||||
/>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('PasswordResetProviderHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide'>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkRemoteAccess'
|
||||
title='AllowRemoteAccess'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('AllowRemoteAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkIsAdmin'
|
||||
title='OptionAllowUserToManageServer'
|
||||
/>
|
||||
<div id='featureAccessFields' className='verticalSection'>
|
||||
<h2 className='paperListLabel'>
|
||||
{globalize.translate('HeaderFeatureAccess')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableLiveTvAccess'
|
||||
title='OptionAllowBrowsingLiveTv'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
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
|
||||
type='checkbox'
|
||||
className='chkEnableMediaPlayback'
|
||||
title='OptionAllowMediaPlayback'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableAudioPlaybackTranscoding'
|
||||
title='OptionAllowAudioPlaybackTranscoding'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableVideoPlaybackTranscoding'
|
||||
title='OptionAllowVideoPlaybackTranscoding'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableVideoPlaybackRemuxing'
|
||||
title='OptionAllowVideoPlaybackRemuxing'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
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'>
|
||||
<SelectSyncPlayAccessElement
|
||||
className='selectSyncPlayAccess'
|
||||
id='selectSyncPlayAccess'
|
||||
label='LabelSyncPlayAccess'
|
||||
/>
|
||||
<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'
|
||||
type='checkbox'
|
||||
className='chkEnableDeleteAllFolders'
|
||||
title='AllLibraries'
|
||||
/>
|
||||
<div className='deleteAccess'>
|
||||
{deleteFoldersAccess.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='verticalSection'>
|
||||
<h2 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderRemoteControl')}
|
||||
</h2>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkEnableRemoteControlOtherUsers'
|
||||
title='OptionAllowRemoteControlOthers'
|
||||
/>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
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
|
||||
type='checkbox'
|
||||
className='chkEnableDownloading'
|
||||
title='OptionAllowContentDownload'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('OptionAllowContentDownloadHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsEnabled'>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
className='chkDisabled'
|
||||
title='OptionDisableUser'
|
||||
/>
|
||||
<div className='fieldDescription checkboxFieldDescription'>
|
||||
{globalize.translate('OptionDisableUserHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='checkboxContainer checkboxContainer-withDescription' id='fldIsHidden'>
|
||||
<CheckBoxElement
|
||||
type='checkbox'
|
||||
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'
|
||||
className='raised button-cancel block btnCancel'
|
||||
title='ButtonCancel'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserEditPage;
|
317
src/components/pages/UserLibraryAccessPage.tsx
Normal file
317
src/components/pages/UserLibraryAccessPage.tsx
Normal file
|
@ -0,0 +1,317 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
|
||||
import loading from '../loading/loading';
|
||||
import libraryMenu from '../../scripts/libraryMenu';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import toast from '../toast/toast';
|
||||
import { appRouter } from '../appRouter';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
|
||||
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||
|
||||
type ItemsArr = {
|
||||
Name?: string;
|
||||
Id?: string;
|
||||
AppName?: string;
|
||||
checkedAttribute?: string
|
||||
}
|
||||
|
||||
const UserLibraryAccessPage: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [channelsItems, setChannelsItems] = useState([]);
|
||||
const [mediaFoldersItems, setMediaFoldersItems] = useState([]);
|
||||
const [devicesItems, setDevicesItems] = useState([]);
|
||||
|
||||
const element = useRef(null);
|
||||
|
||||
const triggerChange = (select) => {
|
||||
const evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent('change', false, true);
|
||||
select.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
const loadMediaFolders = useCallback((user, mediaFolders) => {
|
||||
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 = element.current.querySelector('.chkEnableAllFolders');
|
||||
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
|
||||
triggerChange(chkEnableAllFolders);
|
||||
}, []);
|
||||
|
||||
const loadChannels = useCallback((user, channels) => {
|
||||
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) {
|
||||
element?.current?.querySelector('.channelAccessContainer').classList.remove('hide');
|
||||
} else {
|
||||
element?.current?.querySelector('.channelAccessContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
const chkEnableAllChannels = element.current.querySelector('.chkEnableAllChannels');
|
||||
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
|
||||
triggerChange(chkEnableAllChannels);
|
||||
}, []);
|
||||
|
||||
const loadDevices = useCallback((user, devices) => {
|
||||
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 = element.current.querySelector('.chkEnableAllDevices');
|
||||
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
|
||||
triggerChange(chkEnableAllDevices);
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
element?.current?.querySelector('.deviceAccessContainer').classList.add('hide');
|
||||
} else {
|
||||
element?.current?.querySelector('.deviceAccessContainer').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 = appRouter.param('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(() => {
|
||||
loadData();
|
||||
|
||||
const onSubmit = (e) => {
|
||||
loading.show();
|
||||
const userId = appRouter.param('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
const saveUser = (user) => {
|
||||
user.Policy.EnableAllFolders = element?.current?.querySelector('.chkEnableAllFolders').checked;
|
||||
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked;
|
||||
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (c) {
|
||||
return c.checked;
|
||||
}).map(function (c) {
|
||||
return c.getAttribute('data-id');
|
||||
});
|
||||
user.Policy.EnableAllDevices = element?.current?.querySelector('.chkEnableAllDevices').checked;
|
||||
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(element?.current?.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'));
|
||||
};
|
||||
|
||||
element?.current?.querySelector('.chkEnableAllDevices').addEventListener('change', function (this: HTMLInputElement) {
|
||||
element?.current?.querySelector('.deviceAccessListContainer').classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) {
|
||||
element?.current?.querySelector('.channelAccessListContainer').classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) {
|
||||
element?.current?.querySelector('.folderAccessListContainer').classList.toggle('hide', this.checked);
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.userLibraryAccessForm').addEventListener('submit', onSubmit);
|
||||
}, [loadData]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTabs activeTab='userlibraryaccess'/>
|
||||
<form className='userLibraryAccessForm'>
|
||||
<div className='folderAccessContainer'>
|
||||
<h2>{globalize.translate('HeaderLibraryAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllFolders'
|
||||
title='OptionEnableAccessToAllLibraries'
|
||||
/>
|
||||
<div className='folderAccessListContainer'>
|
||||
<div className='folderAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderLibraries')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList checkboxList-paperList'>
|
||||
{mediaFoldersItems.map(Item => {
|
||||
return (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkFolder'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('LibraryAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='channelAccessContainer hide'>
|
||||
<h2>{globalize.translate('HeaderChannelAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllChannels'
|
||||
title='OptionEnableAccessToAllChannels'
|
||||
/>
|
||||
<div className='channelAccessListContainer'>
|
||||
<div className='channelAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('Channels')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
{channelsItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkChannel'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('ChannelAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='deviceAccessContainer hide'>
|
||||
<h2>{globalize.translate('HeaderDeviceAccess')}</h2>
|
||||
<CheckBoxElement
|
||||
labelClassName='checkboxContainer'
|
||||
type='checkbox'
|
||||
className='chkEnableAllDevices'
|
||||
title='OptionEnableAccessFromAllDevices'
|
||||
/>
|
||||
<div className='deviceAccessListContainer'>
|
||||
<div className='deviceAccess'>
|
||||
<h3 className='checkboxListLabel'>
|
||||
{globalize.translate('HeaderDevices')}
|
||||
</h3>
|
||||
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
|
||||
{devicesItems.map(Item => (
|
||||
<CheckBoxListItem
|
||||
key={Item.Id}
|
||||
className='chkDevice'
|
||||
Id={Item.Id}
|
||||
Name={Item.Name}
|
||||
AppName={Item.AppName}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='fieldDescription'>
|
||||
{globalize.translate('DeviceAccessHelp')}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<ButtonElement
|
||||
type='submit'
|
||||
className='raised button-submit block'
|
||||
title='Save'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserLibraryAccessPage;
|
378
src/components/pages/UserParentalControl.tsx
Normal file
378
src/components/pages/UserParentalControl.tsx
Normal file
|
@ -0,0 +1,378 @@
|
|||
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
import { appRouter } from '../appRouter';
|
||||
import AccessScheduleList from '../dashboard/users/AccessScheduleList';
|
||||
import BlockedTagList from '../dashboard/users/BlockedTagList';
|
||||
import ButtonElement from '../dashboard/users/ButtonElement';
|
||||
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
|
||||
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement';
|
||||
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
|
||||
import SelectMaxParentalRating from '../dashboard/users/SelectMaxParentalRating';
|
||||
import SectionTabs from '../dashboard/users/SectionTabs';
|
||||
import loading from '../loading/loading';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
type RatingsArr = {
|
||||
Name: string;
|
||||
Value: number;
|
||||
}
|
||||
|
||||
type ItemsArr = {
|
||||
name: string;
|
||||
value: string;
|
||||
checkedAttribute: string
|
||||
}
|
||||
|
||||
const UserParentalControl: FunctionComponent = () => {
|
||||
const [ userName, setUserName ] = useState('');
|
||||
const [ parentalRatings, setParentalRatings ] = useState([]);
|
||||
const [ unratedItems, setUnratedItems ] = useState([]);
|
||||
const [ accessSchedules, setAccessSchedules ] = useState([]);
|
||||
const [ blockedTags, setBlockedTags ] = useState([]);
|
||||
|
||||
const element = useRef(null);
|
||||
|
||||
const populateRatings = useCallback((allParentalRatings) => {
|
||||
let rating;
|
||||
const ratings: RatingsArr[] = [];
|
||||
|
||||
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 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: ItemsArr[] = [];
|
||||
|
||||
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 = element?.current?.querySelector('.blockUnratedItems');
|
||||
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
|
||||
}, []);
|
||||
|
||||
const loadBlockedTags = useCallback((tags) => {
|
||||
setBlockedTags(tags);
|
||||
|
||||
const blockedTagsElem = element?.current?.querySelector('.blockedTags');
|
||||
|
||||
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
|
||||
btnDeleteTag.addEventListener('click', function () {
|
||||
const tag = btnDeleteTag.getAttribute('data-tag');
|
||||
const newTags = tags.filter(function (t) {
|
||||
return t != tag;
|
||||
});
|
||||
loadBlockedTags(newTags);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderAccessSchedule = useCallback((schedules) => {
|
||||
setAccessSchedules(schedules);
|
||||
|
||||
const accessScheduleList = element?.current?.querySelector('.accessScheduleList');
|
||||
|
||||
for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) {
|
||||
btnDelete.addEventListener('click', function () {
|
||||
const index = parseInt(btnDelete.getAttribute('data-index'));
|
||||
schedules.splice(index, 1);
|
||||
const newindex = schedules.filter(function (i) {
|
||||
return i != index;
|
||||
});
|
||||
renderAccessSchedule(newindex);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadUser = useCallback((user, allParentalRatings) => {
|
||||
setUserName(user.Name);
|
||||
LibraryMenu.setTitle(user.Name);
|
||||
loadUnratedItems(user);
|
||||
|
||||
loadBlockedTags(user.Policy.BlockedTags);
|
||||
populateRatings(allParentalRatings);
|
||||
let ratingValue = '';
|
||||
|
||||
if (user.Policy.MaxParentalRating) {
|
||||
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
|
||||
const rating = allParentalRatings[i];
|
||||
|
||||
if (user.Policy.MaxParentalRating >= rating.Value) {
|
||||
ratingValue = rating.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
element.current.querySelector('.selectMaxParentalRating').value = ratingValue;
|
||||
|
||||
if (user.Policy.IsAdministrator) {
|
||||
element?.current?.querySelector('.accessScheduleSection').classList.add('hide');
|
||||
} else {
|
||||
element?.current?.querySelector('.accessScheduleSection').classList.remove('hide');
|
||||
}
|
||||
renderAccessSchedule(user.Policy.AccessSchedules || []);
|
||||
loading.hide();
|
||||
}, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
loading.show();
|
||||
const userId = appRouter.param('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(() => {
|
||||
loadData();
|
||||
|
||||
const onSaveComplete = () => {
|
||||
loading.hide();
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
};
|
||||
|
||||
const saveUser = (user) => {
|
||||
user.Policy.MaxParentalRating = element?.current?.querySelector('.selectMaxParentalRating').value || null;
|
||||
user.Policy.BlockUnratedItems = Array.prototype.filter.call(element?.current?.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, index) => {
|
||||
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(element?.current?.querySelectorAll('.liSchedule'), function (elem) {
|
||||
return {
|
||||
DayOfWeek: elem.getAttribute('data-day'),
|
||||
StartHour: elem.getAttribute('data-start'),
|
||||
EndHour: elem.getAttribute('data-end')
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getBlockedTagsFromPage = () => {
|
||||
return Array.prototype.map.call(element?.current?.querySelectorAll('.blockedTag'), function (elem) {
|
||||
return elem.getAttribute('data-tag');
|
||||
});
|
||||
};
|
||||
|
||||
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) => {
|
||||
loading.show();
|
||||
const userId = appRouter.param('userId');
|
||||
window.ApiClient.getUser(userId).then(function (result) {
|
||||
saveUser(result);
|
||||
});
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
};
|
||||
|
||||
element?.current?.querySelector('.btnAddSchedule').addEventListener('click', function () {
|
||||
showSchedulePopup({}, -1);
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.btnAddBlockedTag').addEventListener('click', function () {
|
||||
showBlockedTagPopup();
|
||||
});
|
||||
|
||||
element?.current?.querySelector('.userParentalControlForm').addEventListener('submit', onSubmit);
|
||||
}, [loadBlockedTags, loadData, renderAccessSchedule]);
|
||||
|
||||
return (
|
||||
<div ref={element}>
|
||||
<div className='content-primary'>
|
||||
<div className='verticalSection'>
|
||||
<div className='sectionTitleContainer flex align-items-center'>
|
||||
<h2 className='sectionTitle username'>
|
||||
{userName}
|
||||
</h2>
|
||||
<SectionTitleLinkElement
|
||||
className='raised button-alt headerHelpButton'
|
||||
title='Help'
|
||||
url='https://docs.jellyfin.org/general/server/users/'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionTabs activeTab='userparentalcontrol'/>
|
||||
<form className='userParentalControlForm'>
|
||||
<div className='selectContainer'>
|
||||
<SelectMaxParentalRating
|
||||
className= 'selectMaxParentalRating'
|
||||
label= 'LabelMaxParentalRating'
|
||||
parentalRatings={parentalRatings}
|
||||
/>
|
||||
<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 <CheckBoxListItem
|
||||
key={Item.value}
|
||||
className='chkUnratedItem'
|
||||
ItemType={Item.value}
|
||||
Name={Item.name}
|
||||
checkedAttribute={Item.checkedAttribute}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div className='verticalSection' style={{marginBottom: '2em'}}>
|
||||
<div
|
||||
className='detailSectionHeader sectionTitleContainer'
|
||||
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
|
||||
>
|
||||
<h2 className='sectionTitle'>
|
||||
{globalize.translate('LabelBlockContentWithTags')}
|
||||
</h2>
|
||||
<SectionTitleButtonElement
|
||||
className='fab btnAddBlockedTag submit'
|
||||
title='Add'
|
||||
icon='add'
|
||||
/>
|
||||
</div>
|
||||
<div className='blockedTags' style={{marginTop: '.5em'}}>
|
||||
{blockedTags.map((tag, index) => {
|
||||
return <BlockedTagList
|
||||
key={index}
|
||||
tag={tag}
|
||||
/>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='accessScheduleSection verticalSection' style={{marginBottom: '2em'}}>
|
||||
<div
|
||||
className='sectionTitleContainer'
|
||||
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
|
||||
>
|
||||
<h2 className='sectionTitle'>
|
||||
{globalize.translate('HeaderAccessSchedule')}
|
||||
</h2>
|
||||
<SectionTitleButtonElement
|
||||
className='fab btnAddSchedule submit'
|
||||
title='Add'
|
||||
icon='add'
|
||||
/>
|
||||
</div>
|
||||
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
|
||||
<div className='accessScheduleList paperList'>
|
||||
{accessSchedules.map((accessSchedule, index) => {
|
||||
return <AccessScheduleList
|
||||
key={index}
|
||||
index={index}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserParentalControl;
|
|
@ -3499,7 +3499,7 @@ class PlaybackManager {
|
|||
this.seek(ticks, player);
|
||||
}
|
||||
|
||||
playTrailers(item) {
|
||||
async playTrailers(item) {
|
||||
const player = this._currentPlayer;
|
||||
|
||||
if (player && player.playTrailers) {
|
||||
|
@ -3508,33 +3508,31 @@ class PlaybackManager {
|
|||
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
const instance = this;
|
||||
let items;
|
||||
|
||||
if (item.LocalTrailerCount) {
|
||||
return apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(function (result) {
|
||||
return instance.play({
|
||||
items: result
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const remoteTrailers = item.RemoteTrailers || [];
|
||||
items = await apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id);
|
||||
}
|
||||
|
||||
if (!remoteTrailers.length) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return this.play({
|
||||
items: remoteTrailers.map(function (t) {
|
||||
return {
|
||||
Name: t.Name || (item.Name + ' Trailer'),
|
||||
Url: t.Url,
|
||||
MediaType: 'Video',
|
||||
Type: 'Trailer',
|
||||
ServerId: apiClient.serverId()
|
||||
};
|
||||
})
|
||||
if (!items || !items.length) {
|
||||
items = (item.RemoteTrailers || []).map((t) => {
|
||||
return {
|
||||
Name: t.Name || (item.Name + ' Trailer'),
|
||||
Url: t.Url,
|
||||
MediaType: 'Video',
|
||||
Type: 'Trailer',
|
||||
ServerId: apiClient.serverId()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length) {
|
||||
return this.play({
|
||||
items
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
getSubtitleUrl(textStream, serverId) {
|
||||
|
|
|
@ -20,6 +20,8 @@ import ServerConnections from '../ServerConnections';
|
|||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import template from './recordingcreator.template.html';
|
||||
|
||||
import PlaceholderImage from './empty.png';
|
||||
|
||||
let currentDialog;
|
||||
let closeAction;
|
||||
let currentRecordingFields;
|
||||
|
@ -70,7 +72,7 @@ function renderRecording(context, defaultTimer, program, apiClient, refreshRecor
|
|||
const imageContainer = context.querySelector('.recordingDialog-imageContainer');
|
||||
|
||||
if (imgUrl) {
|
||||
imageContainer.innerHTML = '<img src="./empty.png" data-src="' + imgUrl + '" class="recordingDialog-img lazy" />';
|
||||
imageContainer.innerHTML = `<img src="${PlaceholderImage}" data-src="${imgUrl}" class="recordingDialog-img lazy" />`;
|
||||
imageContainer.classList.remove('hide');
|
||||
|
||||
imageLoader.lazyChildren(imageContainer);
|
||||
|
|
|
@ -203,6 +203,7 @@
|
|||
.layout-desktop .playlistSectionButton,
|
||||
.layout-tv .playlistSectionButton {
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.layout-desktop .nowPlayingPlaylist,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue