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

Merge branch 'master' of https://github.com/jellyfin/jellyfin-web into jellyfin-master

This commit is contained in:
oledfish 2022-01-16 20:52:54 -03:00
commit 8f55658c91
219 changed files with 21450 additions and 12796 deletions

View file

@ -1,4 +1,4 @@
import { appRouter } from './appRouter';
import browser from '../scripts/browser';
import dialog from './dialog/dialog';
import globalize from '../scripts/globalize';
@ -10,7 +10,16 @@ import globalize from '../scripts/globalize';
return originalString.replace(reg, strWith);
}
export default function (text, title) {
function useNativeAlert() {
// webOS seems to block modals
// Tizen 2.x seems to block modals
return !browser.web0s
&& !(browser.tizenVersion && browser.tizenVersion < 3)
&& browser.tv
&& window.alert;
}
export default async function (text, title) {
let options;
if (typeof text === 'string') {
options = {
@ -21,8 +30,11 @@ import globalize from '../scripts/globalize';
options = text;
}
if (browser.tv && window.alert) {
await appRouter.ready();
if (useNativeAlert()) {
alert(replaceAll(options.text || '', '<br/>', '\n'));
return Promise.resolve();
} else {
const items = [];
@ -35,8 +47,6 @@ import globalize from '../scripts/globalize';
options.buttons = items;
return dialog.show(options);
}
return Promise.resolve();
}
/* eslint-enable indent */

View file

@ -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';

View file

@ -24,6 +24,7 @@ class AppRouter {
isDummyBackToHome;
msgTimeout;
popstateOccurred = false;
promiseShow;
resolveOnNextShow;
previousRoute = {};
/**
@ -44,13 +45,7 @@ class AppRouter {
}, 0);
});
document.addEventListener('viewshow', () => {
const resolve = this.resolveOnNextShow;
if (resolve) {
this.resolveOnNextShow = null;
resolve();
}
});
document.addEventListener('viewshow', () => this.onViewShow());
this.baseRoute = window.location.href.split('?')[0].replace(this.getRequestFile(), '');
// support hashbang
@ -128,11 +123,24 @@ class AppRouter {
}
}
back() {
page.back();
ready() {
return this.promiseShow || Promise.resolve();
}
show(path, options) {
async back() {
if (this.promiseShow) await this.promiseShow;
this.promiseShow = new Promise((resolve) => {
this.resolveOnNextShow = resolve;
page.back();
});
return this.promiseShow;
}
async show(path, options) {
if (this.promiseShow) await this.promiseShow;
// ensure the path does not start with '#!' since the router adds this
if (path.startsWith('#!')) {
path = path.substring(2);
@ -152,17 +160,25 @@ class AppRouter {
}
}
return new Promise((resolve) => {
this.promiseShow = new Promise((resolve) => {
this.resolveOnNextShow = resolve;
page.show(path, options);
// Schedule a call to return the promise
setTimeout(() => page.show(path, options), 0);
});
return this.promiseShow;
}
showDirect(path) {
return new Promise(function(resolve) {
async showDirect(path) {
if (this.promiseShow) await this.promiseShow;
this.promiseShow = new Promise((resolve) => {
this.resolveOnNextShow = resolve;
page.show(this.baseUrl() + path);
// Schedule a call to return the promise
setTimeout(() => page.show(this.baseUrl() + path), 0);
});
return this.promiseShow;
}
start(options) {
@ -417,6 +433,15 @@ class AppRouter {
});
}
onViewShow() {
const resolve = this.resolveOnNextShow;
if (resolve) {
this.promiseShow = null;
this.resolveOnNextShow = null;
resolve();
}
}
onForcedLogoutMessageTimeout() {
const msg = this.forcedLogoutMsg;
this.forcedLogoutMsg = null;
@ -638,7 +663,11 @@ class AppRouter {
const ignore = route.dummyRoute === true || this.previousRoute.dummyRoute === true;
this.previousRoute = route;
if (ignore) return;
if (ignore) {
// Resolve 'show' promise
this.onViewShow();
return;
}
this.handleRoute(ctx, next, route);
};
@ -768,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;
}

View file

@ -150,11 +150,14 @@ button::-moz-focus-inner {
left: 0.3em;
text-align: center;
vertical-align: middle;
width: 1.6em;
height: 1.6em;
font-size: 88%;
font-weight: 500;
width: 2em;
height: 2em;
border-radius: 50%;
color: #fff;
background: rgb(51, 136, 204);
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
}
.cardImageContainer {
@ -330,6 +333,7 @@ button::-moz-focus-inner {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
}
.innerCardFooter > .cardText {
@ -352,7 +356,8 @@ button::-moz-focus-inner {
background-position: center center;
}
.cardTextCentered {
.cardTextCentered,
.cardTextCentered > .textActionButton {
text-align: center;
}

View file

@ -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) {

View file

@ -1,3 +1,4 @@
import { appRouter } from '../appRouter';
import browser from '../../scripts/browser';
import dialog from '../dialog/dialog';
import globalize from '../../scripts/globalize';
@ -6,7 +7,16 @@ function replaceAll(str, find, replace) {
return str.split(find).join(replace);
}
function nativeConfirm(options) {
function useNativeConfirm() {
// webOS seems to block modals
// Tizen 2.x seems to block modals
return !browser.web0s
&& !(browser.tizenVersion && browser.tizenVersion < 3)
&& browser.tv
&& window.confirm;
}
async function nativeConfirm(options) {
if (typeof options === 'string') {
options = {
title: '',
@ -15,6 +25,7 @@ function nativeConfirm(options) {
}
const text = replaceAll(options.text || '', '<br/>', '\n');
await appRouter.ready();
const result = window.confirm(text);
if (result) {
@ -24,7 +35,7 @@ function nativeConfirm(options) {
}
}
function customConfirm(text, title) {
async function customConfirm(text, title) {
let options;
if (typeof text === 'string') {
options = {
@ -51,6 +62,8 @@ function customConfirm(text, title) {
options.buttons = items;
await appRouter.ready();
return dialog.show(options).then(result => {
if (result === 'ok') {
return Promise.resolve();
@ -60,6 +73,6 @@ function customConfirm(text, title) {
});
}
const confirm = browser.tv && window.confirm ? nativeConfirm : customConfirm;
const confirm = useNativeConfirm() ? nativeConfirm : customConfirm;
export default confirm;

View file

@ -0,0 +1,32 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createButtonElement = ({ type, className, title }) => ({
__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

@ -0,0 +1,36 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, type, className, title }) => ({
__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

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

View file

@ -0,0 +1,34 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createInputElement = ({ type, id, label, options }) => ({
__html: `<input
is="emby-input"
type="${type}"
id="${id}"
label="${label}"
${options}
/>`
});
type IProps = {
type?: string;
id?: string;
label?: string;
options?: string
}
const InputElement: FunctionComponent<IProps> = ({ type, id, label, options }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createInputElement({
type: type,
id: id,
label: globalize.translate(label),
options: options ? options : ''
})}
/>
);
};
export default InputElement;

View 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;

View 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;

View file

@ -0,0 +1,33 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createButtonElement = ({ className, title, icon }) => ({
__html: `<button
is="emby-button"
type="button"
class="${className}"
style="margin-left:1em;"
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
dangerouslySetInnerHTML={createButtonElement({
className: className,
title: globalize.translate(title),
icon: icon
})}
/>
);
};
export default SectionTitleButtonElement;

View file

@ -0,0 +1,34 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createLinkElement = ({ className, title, href }) => ({
__html: `<a
is="emby-linkbutton"
rel="noopener noreferrer"
class="${className}"
target="_blank"
href="${href}"
>
${title}
</a>`
});
type IProps = {
title?: string;
className?: string;
url?: string
}
const SectionTitleLinkElement: FunctionComponent<IProps> = ({ className, title, url }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createLinkElement({
className: className,
title: globalize.translate(title),
href: url
})}
/>
);
};
export default SectionTitleLinkElement;

View 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;

View file

@ -0,0 +1,35 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, id, label }) => ({
__html: `<select
className="${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

@ -0,0 +1,100 @@
import React, { FunctionComponent } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { localeWithSuffix } from '../../../scripts/dfnshelper';
import globalize from '../../../scripts/globalize';
import cardBuilder from '../../cardbuilder/cardBuilder';
const createLinkElement = ({ user, renderImgUrl }) => ({
__html: `<a
is="emby-linkbutton"
class="cardContent"
href="#!/useredit.html?userId=${user.Id}"
>
${renderImgUrl}
</a>`
});
const createButtonElement = () => ({
__html: `<button
is="paper-icon-button-light"
type="button"
class="btnUserMenu flex-shrink-zero"
>
<span class="material-icons more_vert"></span>
</button>`
});
type IProps = {
user?: Record<string, any>;
}
const getLastSeenText = (lastActivityDate) => {
if (lastActivityDate) {
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
}
return '';
};
const UserCardBox: FunctionComponent<IProps> = ({ user = [] }: IProps) => {
let cssClass = 'card squareCard scalableCard squareCard-scalable';
if (user.Policy.IsDisabled) {
cssClass += ' grayscale';
}
let imgUrl;
if (user.PrimaryImageTag) {
imgUrl = window.ApiClient.getUserImageUrl(user.Id, {
width: 300,
tag: user.PrimaryImageTag,
type: 'Primary'
});
}
let imageClass = 'cardImage';
if (user.Policy.IsDisabled) {
imageClass += ' disabledUser';
}
const lastSeen = getLastSeenText(user.LastActivityDate);
const renderImgUrl = imgUrl ?
`<div class='${imageClass}' style='background-image:url(${imgUrl})'></div>` :
`<div class='${imageClass} ${cardBuilder.getDefaultBackgroundClass(user.Name)} flex align-items-center justify-content-center'>
<span class='material-icons cardImageIcon person'></span>
</div>`;
return (
<div data-userid={user.Id} className={cssClass}>
<div className='cardBox visualCardBox'>
<div className='cardScalable visualCardBox-cardScalable'>
<div className='cardPadder cardPadder-square'></div>
<div
dangerouslySetInnerHTML={createLinkElement({
user: user,
renderImgUrl: renderImgUrl
})}
/>
</div>
<div className='cardFooter visualCardBox-cardFooter'>
<div className='cardText flex align-items-center'>
<div className='flex-grow' style={{overflow: 'hidden', textOverflow: 'ellipsis'}}>
{user.Name}
</div>
<div
dangerouslySetInnerHTML={createButtonElement()}
/>
</div>
<div className='cardText cardText-secondary'>
{lastSeen != '' ? lastSeen : ''}
</div>
</div>
</div>
</div>
);
};
export default UserCardBox;

View file

@ -122,6 +122,8 @@
right: 0 !important;
margin: 0 !important;
box-shadow: none;
width: auto !important;
height: auto !important;
}
}

View file

@ -10,303 +10,291 @@ import '../formdialog.scss';
import '../../elements/emby-button/emby-button';
import alert from '../alert';
/* eslint-disable indent */
function getSystemInfo() {
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
info => {
systemInfo = info;
return info;
}
);
}
function onDialogClosed() {
loading.hide();
}
function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
if (path && typeof path !== 'string') {
throw new Error('invalid path');
function getSystemInfo() {
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
info => {
systemInfo = info;
return info;
}
);
}
loading.show();
function onDialogClosed() {
loading.hide();
}
const promises = [];
function refreshDirectoryBrowser(page, path, fileOptions, updatePathOnError) {
if (path && typeof path !== 'string') {
throw new Error('invalid path');
}
loading.show();
const promises = [];
if (path) {
promises.push(ApiClient.getDirectoryContents(path, fileOptions));
promises.push(ApiClient.getParentPath(path));
} else {
promises.push(ApiClient.getDrives());
}
Promise.all(promises).then(
responses => {
const folders = responses[0];
const parentPath = responses[1] || '';
let html = '';
page.querySelector('.results').scrollTop = 0;
page.querySelector('#txtDirectoryPickerPath').value = path || '';
if (path === 'Network') {
promises.push(ApiClient.getNetworkDevices());
} else {
if (path) {
promises.push(ApiClient.getDirectoryContents(path, fileOptions));
promises.push(ApiClient.getParentPath(path));
} else {
promises.push(ApiClient.getDrives());
html += getItem('lnkPath lnkDirectory', '', parentPath, '...');
}
for (let i = 0, length = folders.length; i < length; i++) {
const folder = folders[i];
const cssClass = folder.Type === 'File' ? 'lnkPath lnkFile' : 'lnkPath lnkDirectory';
html += getItem(cssClass, folder.Type, folder.Path, folder.Name);
}
page.querySelector('.results').innerHTML = html;
loading.hide();
}, () => {
if (updatePathOnError) {
page.querySelector('#txtDirectoryPickerPath').value = '';
page.querySelector('.results').innerHTML = '';
loading.hide();
}
}
);
}
Promise.all(promises).then(
responses => {
const folders = responses[0];
const parentPath = responses[1] || '';
let html = '';
function getItem(cssClass, type, path, name) {
let html = '';
html += `<div class="listItem listItem-border ${cssClass}" data-type="${type}" data-path="${path}">`;
html += '<div class="listItemBody" style="padding-left:0;padding-top:.5em;padding-bottom:.5em;">';
html += '<div class="listItemBodyText">';
html += name;
html += '</div>';
html += '</div>';
html += '<span class="material-icons arrow_forward" style="font-size:inherit;"></span>';
html += '</div>';
return html;
}
page.querySelector('.results').scrollTop = 0;
page.querySelector('#txtDirectoryPickerPath').value = path || '';
function getEditorHtml(options, systemInfo) {
let html = '';
html += '<div class="formDialogContent scrollY">';
html += '<div class="dialogContentInner dialog-content-centered" style="padding-top:2em;">';
if (!options.pathReadOnly) {
const instruction = options.instruction ? `${options.instruction}<br/><br/>` : '';
html += '<div class="infoBanner" style="margin-bottom:1.5em;">';
html += instruction;
if (systemInfo.OperatingSystem.toLowerCase() === 'bsd') {
html += '<br/>';
html += '<br/>';
html += globalize.translate('MessageDirectoryPickerBSDInstruction');
html += '<br/>';
} else if (systemInfo.OperatingSystem.toLowerCase() === 'linux') {
html += '<br/>';
html += '<br/>';
html += globalize.translate('MessageDirectoryPickerLinuxInstruction');
html += '<br/>';
}
html += '</div>';
}
html += '<form style="margin:auto;">';
html += '<div class="inputContainer" style="display: flex; align-items: center;">';
html += '<div style="flex-grow:1;">';
let labelKey;
if (options.includeFiles !== true) {
labelKey = 'LabelFolder';
} else {
labelKey = 'LabelPath';
}
const readOnlyAttribute = options.pathReadOnly ? ' readonly' : '';
html += `<input is="emby-input" id="txtDirectoryPickerPath" type="text" required="required" ${readOnlyAttribute} label="${globalize.translate(labelKey)}"/>`;
html += '</div>';
if (!readOnlyAttribute) {
html += `<button type="button" is="paper-icon-button-light" class="btnRefreshDirectories emby-input-iconbutton" title="${globalize.translate('Refresh')}"><span class="material-icons search"></span></button>`;
}
html += '</div>';
if (!readOnlyAttribute) {
html += '<div class="results paperList" style="max-height: 200px; overflow-y: auto;"></div>';
}
if (options.enableNetworkSharePath) {
html += '<div class="inputContainer" style="margin-top:2em;">';
html += `<input is="emby-input" id="txtNetworkPath" type="text" label="${globalize.translate('LabelOptionalNetworkPath')}"/>`;
html += '<div class="fieldDescription">';
html += globalize.translate('LabelOptionalNetworkPathHelp', '<b>\\\\server</b>', '<b>\\\\192.168.1.101</b>');
html += '</div>';
html += '</div>';
}
html += '<div class="formDialogFooter">';
html += `<button is="emby-button" type="submit" class="raised button-submit block formDialogFooterItem">${globalize.translate('ButtonOk')}</button>`;
html += '</div>';
html += '</form>';
html += '</div>';
html += '</div>';
html += '</div>';
if (path) {
html += getItem('lnkPath lnkDirectory', '', parentPath, '...');
}
for (let i = 0, length = folders.length; i < length; i++) {
const folder = folders[i];
const cssClass = folder.Type === 'File' ? 'lnkPath lnkFile' : 'lnkPath lnkDirectory';
html += getItem(cssClass, folder.Type, folder.Path, folder.Name);
return html;
}
function alertText(text) {
alertTextWithOptions({
text: text
});
}
function alertTextWithOptions(options) {
alert(options);
}
function validatePath(path, validateWriteable, apiClient) {
return apiClient.ajax({
type: 'POST',
url: apiClient.getUrl('Environment/ValidatePath'),
data: JSON.stringify({
ValidateWriteable: validateWriteable,
Path: path
}),
contentType: 'application/json'
}).catch(response => {
if (response) {
if (response.status === 404) {
alertText(globalize.translate('PathNotFound'));
return Promise.reject();
}
if (response.status === 500) {
if (validateWriteable) {
alertText(globalize.translate('WriteAccessRequired'));
} else {
alertText(globalize.translate('PathNotFound'));
}
return Promise.reject();
}
}
return Promise.resolve();
});
}
if (!path) {
html += getItem('lnkPath lnkDirectory', '', 'Network', globalize.translate('ButtonNetwork'));
}
function initEditor(content, options, fileOptions) {
content.addEventListener('click', e => {
const lnkPath = dom.parentWithClass(e.target, 'lnkPath');
if (lnkPath) {
const path = lnkPath.getAttribute('data-path');
if (lnkPath.classList.contains('lnkFile')) {
content.querySelector('#txtDirectoryPickerPath').value = path;
} else {
refreshDirectoryBrowser(content, path, fileOptions, true);
}
}
});
page.querySelector('.results').innerHTML = html;
loading.hide();
content.addEventListener('click', e => {
if (dom.parentWithClass(e.target, 'btnRefreshDirectories')) {
const path = content.querySelector('#txtDirectoryPickerPath').value;
refreshDirectoryBrowser(content, path, fileOptions);
}
});
content.addEventListener('change', e => {
const txtDirectoryPickerPath = dom.parentWithTag(e.target, 'INPUT');
if (txtDirectoryPickerPath && txtDirectoryPickerPath.id === 'txtDirectoryPickerPath') {
refreshDirectoryBrowser(content, txtDirectoryPickerPath.value, fileOptions);
}
});
content.querySelector('form').addEventListener('submit', function(e) {
if (options.callback) {
let networkSharePath = this.querySelector('#txtNetworkPath');
networkSharePath = networkSharePath ? networkSharePath.value : null;
const path = this.querySelector('#txtDirectoryPickerPath').value;
validatePath(path, options.validateWriteable, ApiClient).then(options.callback(path, networkSharePath));
}
e.preventDefault();
e.stopPropagation();
return false;
});
}
function getDefaultPath(options) {
if (options.path) {
return Promise.resolve(options.path);
} else {
return ApiClient.getJSON(ApiClient.getUrl('Environment/DefaultDirectoryBrowser')).then(
result => {
return result.Path || '';
}, () => {
if (updatePathOnError) {
page.querySelector('#txtDirectoryPickerPath').value = '';
page.querySelector('.results').innerHTML = '';
loading.hide();
}
return '';
}
);
}
}
function getItem(cssClass, type, path, name) {
let html = '';
html += `<div class="listItem listItem-border ${cssClass}" data-type="${type}" data-path="${path}">`;
html += '<div class="listItemBody" style="padding-left:0;padding-top:.5em;padding-bottom:.5em;">';
html += '<div class="listItemBodyText">';
html += name;
html += '</div>';
html += '</div>';
html += '<span class="material-icons arrow_forward" style="font-size:inherit;"></span>';
html += '</div>';
return html;
}
let systemInfo;
class DirectoryBrowser {
currentDialog;
function getEditorHtml(options, systemInfo) {
let html = '';
html += '<div class="formDialogContent scrollY">';
html += '<div class="dialogContentInner dialog-content-centered" style="padding-top:2em;">';
if (!options.pathReadOnly) {
const instruction = options.instruction ? `${options.instruction}<br/><br/>` : '';
html += '<div class="infoBanner" style="margin-bottom:1.5em;">';
html += instruction;
if (systemInfo.OperatingSystem.toLowerCase() === 'bsd') {
html += '<br/>';
html += '<br/>';
html += globalize.translate('MessageDirectoryPickerBSDInstruction');
html += '<br/>';
} else if (systemInfo.OperatingSystem.toLowerCase() === 'linux') {
html += '<br/>';
html += '<br/>';
html += globalize.translate('MessageDirectoryPickerLinuxInstruction');
html += '<br/>';
show = options => {
options = options || {};
const fileOptions = {
includeDirectories: true
};
if (options.includeDirectories != null) {
fileOptions.includeDirectories = options.includeDirectories;
}
html += '</div>';
}
html += '<form style="margin:auto;">';
html += '<div class="inputContainer" style="display: flex; align-items: center;">';
html += '<div style="flex-grow:1;">';
let labelKey;
if (options.includeFiles !== true) {
labelKey = 'LabelFolder';
} else {
labelKey = 'LabelPath';
}
const readOnlyAttribute = options.pathReadOnly ? ' readonly' : '';
html += `<input is="emby-input" id="txtDirectoryPickerPath" type="text" required="required" ${readOnlyAttribute} label="${globalize.translate(labelKey)}"/>`;
html += '</div>';
if (!readOnlyAttribute) {
html += `<button type="button" is="paper-icon-button-light" class="btnRefreshDirectories emby-input-iconbutton" title="${globalize.translate('Refresh')}"><span class="material-icons search"></span></button>`;
}
html += '</div>';
if (!readOnlyAttribute) {
html += '<div class="results paperList" style="max-height: 200px; overflow-y: auto;"></div>';
}
if (options.enableNetworkSharePath) {
html += '<div class="inputContainer" style="margin-top:2em;">';
html += `<input is="emby-input" id="txtNetworkPath" type="text" label="${globalize.translate('LabelOptionalNetworkPath')}"/>`;
html += '<div class="fieldDescription">';
html += globalize.translate('LabelOptionalNetworkPathHelp', '<b>\\\\server</b>', '<b>\\\\192.168.1.101</b>');
html += '</div>';
html += '</div>';
}
html += '<div class="formDialogFooter">';
html += `<button is="emby-button" type="submit" class="raised button-submit block formDialogFooterItem">${globalize.translate('ButtonOk')}</button>`;
html += '</div>';
html += '</form>';
html += '</div>';
html += '</div>';
html += '</div>';
if (options.includeFiles != null) {
fileOptions.includeFiles = options.includeFiles;
}
Promise.all([getSystemInfo(), getDefaultPath(options)]).then(
responses => {
const systemInfo = responses[0];
const initialPath = responses[1];
const dlg = dialogHelper.createDialog({
size: 'small',
removeOnClose: true,
scrollY: false
});
dlg.classList.add('ui-body-a');
dlg.classList.add('background-theme-a');
dlg.classList.add('directoryPicker');
dlg.classList.add('formDialog');
return html;
}
function alertText(text) {
alertTextWithOptions({
text: text
});
}
function alertTextWithOptions(options) {
alert(options);
}
function validatePath(path, validateWriteable, apiClient) {
return apiClient.ajax({
type: 'POST',
url: apiClient.getUrl('Environment/ValidatePath'),
data: JSON.stringify({
ValidateWriteable: validateWriteable,
Path: path
}),
contentType: 'application/json'
}).catch(response => {
if (response) {
if (response.status === 404) {
alertText(globalize.translate('PathNotFound'));
return Promise.reject();
}
if (response.status === 500) {
if (validateWriteable) {
alertText(globalize.translate('WriteAccessRequired'));
} else {
alertText(globalize.translate('PathNotFound'));
let html = '';
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCloseDialog autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>';
html += '<h3 class="formDialogHeaderTitle">';
html += options.header || globalize.translate('HeaderSelectPath');
html += '</h3>';
html += '</div>';
html += getEditorHtml(options, systemInfo);
dlg.innerHTML = html;
initEditor(dlg, options, fileOptions);
dlg.addEventListener('close', onDialogClosed);
dialogHelper.open(dlg);
dlg.querySelector('.btnCloseDialog').addEventListener('click', () => {
dialogHelper.close(dlg);
});
this.currentDialog = dlg;
dlg.querySelector('#txtDirectoryPickerPath').value = initialPath;
const txtNetworkPath = dlg.querySelector('#txtNetworkPath');
if (txtNetworkPath) {
txtNetworkPath.value = options.networkSharePath || '';
}
if (!options.pathReadOnly) {
refreshDirectoryBrowser(dlg, initialPath, fileOptions, true);
}
return Promise.reject();
}
}
return Promise.resolve();
});
}
function initEditor(content, options, fileOptions) {
content.addEventListener('click', e => {
const lnkPath = dom.parentWithClass(e.target, 'lnkPath');
if (lnkPath) {
const path = lnkPath.getAttribute('data-path');
if (lnkPath.classList.contains('lnkFile')) {
content.querySelector('#txtDirectoryPickerPath').value = path;
} else {
refreshDirectoryBrowser(content, path, fileOptions, true);
}
}
});
content.addEventListener('click', e => {
if (dom.parentWithClass(e.target, 'btnRefreshDirectories')) {
const path = content.querySelector('#txtDirectoryPickerPath').value;
refreshDirectoryBrowser(content, path, fileOptions);
}
});
content.addEventListener('change', e => {
const txtDirectoryPickerPath = dom.parentWithTag(e.target, 'INPUT');
if (txtDirectoryPickerPath && txtDirectoryPickerPath.id === 'txtDirectoryPickerPath') {
refreshDirectoryBrowser(content, txtDirectoryPickerPath.value, fileOptions);
}
});
content.querySelector('form').addEventListener('submit', function(e) {
if (options.callback) {
let networkSharePath = this.querySelector('#txtNetworkPath');
networkSharePath = networkSharePath ? networkSharePath.value : null;
const path = this.querySelector('#txtDirectoryPickerPath').value;
validatePath(path, options.validateWriteable, ApiClient).then(options.callback(path, networkSharePath));
}
e.preventDefault();
e.stopPropagation();
return false;
});
}
function getDefaultPath(options) {
if (options.path) {
return Promise.resolve(options.path);
} else {
return ApiClient.getJSON(ApiClient.getUrl('Environment/DefaultDirectoryBrowser')).then(
result => {
return result.Path || '';
}, () => {
return '';
}
);
}
}
};
class directoryBrowser {
constructor() {
let currentDialog;
this.show = options => {
options = options || {};
const fileOptions = {
includeDirectories: true
};
if (options.includeDirectories != null) {
fileOptions.includeDirectories = options.includeDirectories;
}
if (options.includeFiles != null) {
fileOptions.includeFiles = options.includeFiles;
}
Promise.all([getSystemInfo(), getDefaultPath(options)]).then(
responses => {
const systemInfo = responses[0];
const initialPath = responses[1];
const dlg = dialogHelper.createDialog({
size: 'small',
removeOnClose: true,
scrollY: false
});
dlg.classList.add('ui-body-a');
dlg.classList.add('background-theme-a');
dlg.classList.add('directoryPicker');
dlg.classList.add('formDialog');
close = () => {
if (this.currentDialog) {
dialogHelper.close(this.currentDialog);
}
};
}
let html = '';
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCloseDialog autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>';
html += '<h3 class="formDialogHeaderTitle">';
html += options.header || globalize.translate('HeaderSelectPath');
html += '</h3>';
html += '</div>';
html += getEditorHtml(options, systemInfo);
dlg.innerHTML = html;
initEditor(dlg, options, fileOptions);
dlg.addEventListener('close', onDialogClosed);
dialogHelper.open(dlg);
dlg.querySelector('.btnCloseDialog').addEventListener('click', () => {
dialogHelper.close(dlg);
});
currentDialog = dlg;
dlg.querySelector('#txtDirectoryPickerPath').value = initialPath;
const txtNetworkPath = dlg.querySelector('#txtNetworkPath');
if (txtNetworkPath) {
txtNetworkPath.value = options.networkSharePath || '';
}
if (!options.pathReadOnly) {
refreshDirectoryBrowser(dlg, initialPath, fileOptions, true);
}
}
);
};
this.close = () => {
if (currentDialog) {
dialogHelper.close(currentDialog);
}
};
}
}
let systemInfo;
/* eslint-enable indent */
export default directoryBrowser;
export default DirectoryBrowser;

View file

@ -132,6 +132,7 @@ import template from './displaySettings.template.html';
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
context.querySelector('#txtMaxDaysForNextUp').value = userSettings.maxDaysForNextUp();
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
@ -156,6 +157,7 @@ import template from './displaySettings.template.html';
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
userSettingsInstance.maxDaysForNextUp(context.querySelector('#txtMaxDaysForNextUp').value);
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);

View file

@ -7,65 +7,75 @@
<select id="selectLanguage" is="emby-select" label="${LabelDisplayLanguage}">
<option value="">${Auto}</option>
<option value="af">Afrikaans</option>
<option value="sq">Albanian</option>
<option value="ar">Arabic</option>
<option value="be-BY">Belarusian</option>
<option value="bn_BD">Bengali (Bangladesh)</option>
<option value="bg-BG">Bulgarian</option>
<option value="ca">Catalan</option>
<option value="zh-HK">Chinese (Hong Kong)</option>
<option value="zh-CN">Chinese (Simplified)</option>
<option value="zh-TW">Chinese (Traditional)</option>
<option value="hr">Croatian</option>
<option value="cs">Czech</option>
<option value="da">Danish</option>
<option value="nl">Dutch</option>
<option value="en-US">English</option>
<option value="ar">العربية</option>
<option value="be-BY">Беларуская</option>
<option value="bg-BG">Български</option>
<option value="bn_BD">বাংলা (বাংলাদেশ)</option>
<option value="ca">Català</option>
<option value="cs">Čeština</option>
<option value="cy">Cymraeg</option>
<option value="da">Dansk</option>
<option value="de">Deutsch</option>
<option value="el">Ελληνικά</option>
<option value="en-GB">English (United Kingdom)</option>
<option value="en-US">English</option>
<option value="eo">Esperanto</option>
<option value="es">Español</option>
<option value="es_419">Español americano</option>
<option value="es-AR">Español (Argentina)</option>
<option value="es_DO">Español (Dominicana)</option>
<option value="es-MX">Español (México)</option>
<option value="et">Eesti</option>
<option value="fa">فارسی</option>
<option value="fi">Suomi</option>
<option value="fil">Filipino</option>
<option value="fi">Finnish</option>
<option value="fr">French</option>
<option value="fr-CA">French (Canada)</option>
<option value="gl">Galician</option>
<option value="de">German</option>
<option value="gsw">German (Swiss)</option>
<option value="el">Greek</option>
<option value="he">Hebrew</option>
<option value="hi-IN">Hindi</option>
<option value="hu">Hungarian</option>
<option value="is">Icelandic</option>
<option value="id">Indonesian</option>
<option value="it">Italian</option>
<option value="ja">Japanese</option>
<option value="kk">Kazakh</option>
<option value="ko">Korean</option>
<option value="lt-LT">Lithuanian</option>
<option value="ms">Malay</option>
<option value="mr">Marathi</option>
<option value="nb">Norwegian Bokmål</option>
<option value="fa">Persian</option>
<option value="fr">Français</option>
<option value="fr-CA">Français (Canada)</option>
<option value="gl">Galego</option>
<option value="gsw">Schwiizerdütsch</option>
<option value="he">עִבְרִית</option>
<option value="hi-IN">हिन्दी</option>
<option value="hr">Hrvatski </option>
<option value="hu">Magyar</option>
<option value="id">Bahasa Indonesia</option>
<option value="is-IS">Íslenska</option>
<option value="it">Italiano</option>
<option value="ja">日本語</option>
<option value="kk">Qazaqşa</option>
<option value="ko">한국어</option>
<option value="lt-LT">Lietuvių</option>
<option value="lv">Latviešu</option>
<option value="mk">Македонски</option>
<option value="ml">മലയാളം</option>
<option value="mr">मराठी</option>
<option value="ms">Bahasa Melayu</option>
<option value="nb">Norsk bokmål</option>
<option value="ne">नेपाली</option>
<option value="nl">Nederlands</option>
<option value="nn">Norsk nynorsk</option>
<option value="pa">ਪੰਜਾਬੀ</option>
<option value="pl">Polski</option>
<option value="pr">Pirate</option>
<option value="pl">Polish</option>
<option value="pt">Portuguese</option>
<option value="pt-BR">Portuguese (Brazil)</option>
<option value="pt-PT">Portuguese (Portugal)</option>
<option value="ro">Romanian</option>
<option value="ru">Russian</option>
<option value="sk">Slovak</option>
<option value="sl-SI">Slovenian (Slovenia)</option>
<option value="es">Spanish</option>
<option value="es_AR">Spanish (Argentina)</option>
<option value="es_DO">Spanish (Dominican Republic)</option>
<option value="es-419">Spanish (Latin America)</option>
<option value="es-MX">Spanish (Mexico)</option>
<option value="sv">Swedish</option>
<option value="ta">Tamil</option>
<option value="th">Thai</option>
<option value="tr">Turkish</option>
<option value="uk">Ukrainian</option>
<option value="ur_PK">Urdu (Pakistan)</option>
<option value="vi">Vietnamese</option>
<option value="pt">Português</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="pt-PT">Português (Portugal)</option>
<option value="ro">Românește</option>
<option value="ru">Русский</option>
<option value="sk">Slovenčina</option>
<option value="sl-SI">Slovenščina</option>
<option value="sq">Shqip</option>
<option value="sr">Српски</option>
<option value="sv">Svenska</option>
<option value="ta">தமிழ்</option>
<option value="te">తెలుగు</option>
<option value="th">ภาษาไทย</option>
<option value="tr">Türkçe</option>
<option value="uk">Українська</option>
<option value="ur_PK">اُردُو</option>
<option value="vi">Tiếng Việt</option>
<option value="zh-CN">汉语 (简化字)</option>
<option value="zh-TW">漢語 (繁体字)</option>
<option value="zh-HK">廣東話 (香港)</option>
</select>
<div class="fieldDescription">
<div>${LabelDisplayLanguageHelp}</div>
@ -79,65 +89,75 @@
<select is="emby-select" class="selectDateTimeLocale" label="${LabelDateTimeLocale}">
<option value="">${Auto}</option>
<option value="af">Afrikaans</option>
<option value="sq">Albanian</option>
<option value="ar">Arabic</option>
<option value="be-BY">Belarusian</option>
<option value="bn_BD">Bengali (Bangladesh)</option>
<option value="bg-BG">Bulgarian</option>
<option value="ca">Catalan</option>
<option value="zh-HK">Chinese (Hong Kong)</option>
<option value="zh-CN">Chinese (Simplified)</option>
<option value="zh-TW">Chinese (Traditional)</option>
<option value="hr">Croatian</option>
<option value="cs">Czech</option>
<option value="da">Danish</option>
<option value="nl">Dutch</option>
<option value="en-US">English</option>
<option value="ar">العربية</option>
<option value="be-BY">Беларуская</option>
<option value="bg-BG">Български</option>
<option value="bn_BD">বাংলা (বাংলাদেশ)</option>
<option value="ca">Català</option>
<option value="cs">Čeština</option>
<option value="cy">Cymraeg</option>
<option value="da">Dansk</option>
<option value="de">Deutsch</option>
<option value="el">Ελληνικά</option>
<option value="en-GB">English (United Kingdom)</option>
<option value="en-US">English</option>
<option value="eo">Esperanto</option>
<option value="es">Español</option>
<option value="es_419">Español americano</option>
<option value="es-AR">Español (Argentina)</option>
<option value="es_DO">Español (Dominicana)</option>
<option value="es-MX">Español (México)</option>
<option value="et">Eesti</option>
<option value="fa">فارسی</option>
<option value="fi">Suomi</option>
<option value="fil">Filipino</option>
<option value="fi">Finnish</option>
<option value="fr">French</option>
<option value="fr-CA">French (Canada)</option>
<option value="gl">Galician</option>
<option value="de">German</option>
<option value="gsw">German (Swiss)</option>
<option value="el">Greek</option>
<option value="he">Hebrew</option>
<option value="hi-IN">Hindi</option>
<option value="hu">Hungarian</option>
<option value="is">Icelandic</option>
<option value="id">Indonesian</option>
<option value="it">Italian</option>
<option value="ja">Japanese</option>
<option value="kk">Kazakh</option>
<option value="ko">Korean</option>
<option value="lt-LT">Lithuanian</option>
<option value="ms">Malay</option>
<option value="mr">Marathi</option>
<option value="nb">Norwegian Bokmål</option>
<option value="fa">Persian</option>
<option value="fr">Français</option>
<option value="fr-CA">Français (Canada)</option>
<option value="gl">Galego</option>
<option value="gsw">Schwiizerdütsch</option>
<option value="he">עִבְרִית</option>
<option value="hi-IN">हिन्दी</option>
<option value="hr">Hrvatski </option>
<option value="hu">Magyar</option>
<option value="id">Bahasa Indonesia</option>
<option value="is-IS">Íslenska</option>
<option value="it">Italiano</option>
<option value="ja">日本語</option>
<option value="kk">Qazaqşa</option>
<option value="ko">한국어</option>
<option value="lt-LT">Lietuvių</option>
<option value="lv">Latviešu</option>
<option value="mk">Македонски</option>
<option value="ml">മലയാളം</option>
<option value="mr">मराठी</option>
<option value="ms">Bahasa Melayu</option>
<option value="nb">Norsk bokmål</option>
<option value="ne">नेपाली</option>
<option value="nl">Nederlands</option>
<option value="nn">Norsk nynorsk</option>
<option value="pa">ਪੰਜਾਬੀ</option>
<option value="pl">Polski</option>
<option value="pr">Pirate</option>
<option value="pl">Polish</option>
<option value="pt">Portuguese</option>
<option value="pt-BR">Portuguese (Brazil)</option>
<option value="pt-PT">Portuguese (Portugal)</option>
<option value="ro">Romanian</option>
<option value="ru">Russian</option>
<option value="sk">Slovak</option>
<option value="sl-SI">Slovenian (Slovenia)</option>
<option value="es">Spanish</option>
<option value="es_AR">Spanish (Argentina)</option>
<option value="es_DO">Spanish (Dominican Republic)</option>
<option value="es-419">Spanish (Latin America)</option>
<option value="es-MX">Spanish (Mexico)</option>
<option value="sv">Swedish</option>
<option value="ta">Tamil</option>
<option value="th">Thai</option>
<option value="tr">Turkish</option>
<option value="uk">Ukrainian</option>
<option value="ur_PK">Urdu (Pakistan)</option>
<option value="vi">Vietnamese</option>
<option value="pt">Português</option>
<option value="pt-BR">Português (Brasil)</option>
<option value="pt-PT">Português (Portugal)</option>
<option value="ro">Românește</option>
<option value="ru">Русский</option>
<option value="sk">Slovenčina</option>
<option value="sl-SI">Slovenščina</option>
<option value="sq">Shqip</option>
<option value="sr">Српски</option>
<option value="sv">Svenska</option>
<option value="ta">தமிழ்</option>
<option value="te">తెలుగు</option>
<option value="th">ภาษาไทย</option>
<option value="tr">Türkçe</option>
<option value="uk">Українська</option>
<option value="ur_PK">اُردُو</option>
<option value="vi">Tiếng Việt</option>
<option value="zh-CN">汉语 (简化字)</option>
<option value="zh-TW">漢語 (繁体字)</option>
<option value="zh-HK">廣東話 (香港)</option>
</select>
</div>
@ -182,6 +202,11 @@
<div class="fieldDescription">${LabelLibraryPageSizeHelp}</div>
</div>
<div class="inputContainer inputContainer-withDescription">
<input is="emby-input" type="number" id="txtMaxDaysForNextUp" pattern="[0-9]*" required="required" min="0" max="1000" step="1" label="${LabelMaxDaysForNextUp}" />
<div class="fieldDescription">${LabelMaxDaysForNextUpHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkFadein" />

View file

@ -84,11 +84,6 @@
flex-basis: 12em;
}
.layout-tv .formDialogFooterItem {
flex-grow: 1;
flex-basis: 0;
}
.formDialogFooterItem-vertical {
max-width: none !important;
width: 100%;

View file

@ -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>';
@ -595,9 +600,11 @@ import ServerConnections from '../ServerConnections';
});
}
function getNextUpFetchFn(serverId) {
function getNextUpFetchFn(serverId, userSettings) {
return function () {
const apiClient = ServerConnections.getApiClient(serverId);
const oldestDateForNextUp = new Date();
oldestDateForNextUp.setDate(oldestDateForNextUp.getDate() - userSettings.maxDaysForNextUp());
return apiClient.getNextUpEpisodes({
Limit: enableScrollX() ? 24 : 15,
Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path',
@ -605,7 +612,8 @@ import ServerConnections from '../ServerConnections';
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false,
DisableFirstEpisode: true
DisableFirstEpisode: false,
NextUpDateCutoff: oldestDateForNextUp.toISOString()
});
};
}
@ -665,7 +673,7 @@ import ServerConnections from '../ServerConnections';
elem.innerHTML = html;
const itemsContainer = elem.querySelector('.itemsContainer');
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId());
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings);
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume());
itemsContainer.parentContainer = elem;
}

View file

@ -185,6 +185,12 @@ import { Events } from 'jellyfin-apiclient';
return Promise.resolve();
}
export function resetSrc(elem) {
elem.src = '';
elem.innerHTML = '';
elem.removeAttribute('src');
}
function onSuccessfulPlay(elem, onErrorFn) {
elem.addEventListener('error', onErrorFn);
}
@ -344,9 +350,7 @@ import { Events } from 'jellyfin-apiclient';
export function onEndedInternal(instance, elem, onErrorFn) {
elem.removeEventListener('error', onErrorFn);
elem.src = '';
elem.innerHTML = '';
elem.removeAttribute('src');
resetSrc(elem);
destroyHlsPlayer(instance);
destroyFlvPlayer(instance);

View 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 */

View file

@ -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,23 +122,23 @@ 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) {
elem.removeEventListener('animationend', onAnimationEnd);
const canvas = elem.previousSibling;
if (canvas && canvas.tagName === 'CANVAS') {
canvas.classList.remove('lazy-hidden');
}
let url;
if (elem.tagName !== 'IMG') {
@ -122,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) {

View file

@ -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 {

View file

@ -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;

View file

@ -72,7 +72,7 @@ import template from './itemMediaInfo.template.html';
html += `<h2 class="mediaInfoStreamType">${displayType}</h2>`;
const attributes = [];
if (stream.DisplayTitle) {
attributes.push(createAttribute('Title', stream.DisplayTitle));
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
}
if (stream.Language && stream.Type !== 'Video') {
attributes.push(createAttribute(globalize.translate('MediaInfoLanguage'), stream.Language));

View file

@ -13,7 +13,10 @@
callback(entry);
});
},
{rootMargin: '25%'});
{
rootMargin: '50%',
threshold: 0
});
this.observer = observer;
}

View file

@ -118,7 +118,7 @@ import template from './libraryoptionseditor.template.html';
if (!plugins.length) return html;
html += '<div class="metadataFetcher" data-type="' + availableTypeOptions.Type + '">';
html += '<h3 class="checkboxListLabel">' + globalize.translate('LabelTypeMetadataDownloaders', globalize.translate(availableTypeOptions.Type)) + '</h3>';
html += '<h3 class="checkboxListLabel">' + globalize.translate('LabelTypeMetadataDownloaders', globalize.translate('TypeOptionPlural' + availableTypeOptions.Type)) + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
plugins.forEach((plugin, index) => {
@ -218,7 +218,7 @@ import template from './libraryoptionseditor.template.html';
html += '<div class="imageFetcher" data-type="' + availableTypeOptions.Type + '">';
html += '<div class="flex align-items-center" style="margin:1.5em 0 .5em;">';
html += '<h3 class="checkboxListLabel" style="margin:0;">' + globalize.translate('HeaderTypeImageFetchers', availableTypeOptions.Type) + '</h3>';
html += '<h3 class="checkboxListLabel" style="margin:0;">' + globalize.translate('HeaderTypeImageFetchers', globalize.translate('TypeOptionPlural' + availableTypeOptions.Type)) + '</h3>';
const supportedImageTypes = availableTypeOptions.SupportedImageTypes || [];
if (supportedImageTypes.length > 1 || supportedImageTypes.length === 1 && supportedImageTypes[0] !== 'Primary') {
html += '<button is="emby-button" class="raised btnImageOptionsForType" type="button" style="margin-left:1.5em;font-size:90%;"><span>' + globalize.translate('HeaderFetcherSettings') + '</span></button>';
@ -411,6 +411,8 @@ import template from './libraryoptionseditor.template.html';
parent.querySelector('.chkEnableEmbeddedEpisodeInfosContainer').classList.add('hide');
}
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies');
return populateMetadataSettings(parent, contentType);
}
@ -511,6 +513,7 @@ import template from './libraryoptionseditor.template.html';
SkipSubtitlesIfAudioTrackMatches: parent.querySelector('#chkSkipIfAudioTrackPresent').checked,
SaveSubtitlesWithMedia: parent.querySelector('#chkSaveSubtitlesLocally').checked,
RequirePerfectSubtitleMatch: parent.querySelector('#chkRequirePerfectMatch').checked,
AutomaticallyAddToCollection: parent.querySelector('#chkAutomaticallyAddToCollection').checked,
MetadataSavers: Array.prototype.map.call(Array.prototype.filter.call(parent.querySelectorAll('.chkMetadataSaver'), elem => {
return elem.checked;
}), elem => {
@ -562,6 +565,7 @@ import template from './libraryoptionseditor.template.html';
parent.querySelector('#chkSaveSubtitlesLocally').checked = options.SaveSubtitlesWithMedia;
parent.querySelector('#chkSkipIfAudioTrackPresent').checked = options.SkipSubtitlesIfAudioTrackMatches;
parent.querySelector('#chkRequirePerfectMatch').checked = options.RequirePerfectSubtitleMatch;
parent.querySelector('#chkAutomaticallyAddToCollection').checked = options.AutomaticallyAddToCollection;
Array.prototype.forEach.call(parent.querySelectorAll('.chkMetadataSaver'), elem => {
elem.checked = options.MetadataSavers ? options.MetadataSavers.includes(elem.getAttribute('data-pluginname')) : elem.getAttribute('data-defaultenabled') === 'true';
});

View file

@ -39,6 +39,14 @@
<div class="fieldDescription checkboxFieldDescription">${LabelEnableRealtimeMonitorHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription chkAutomaticallyAddToCollectionContainer hide advanced">
<label>
<input is="emby-checkbox" type="checkbox" id="chkAutomaticallyAddToCollection" checked />
<span>${LabelAutomaticallyAddToCollection}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LabelAutomaticallyAddToCollectionHelp}</div>
</div>
<div class="metadataReaders hide advanced" style="margin-bottom: 2em;">
</div>

View file

@ -133,21 +133,28 @@ import ServerConnections from '../ServerConnections';
continue;
}
let elem;
if (i === 0) {
if (isLargeStyle) {
html += `<${largeTitleTagName} class="listItemBodyText">`;
elem = document.createElement(largeTitleTagName);
} else {
html += '<div class="listItemBodyText">';
elem = document.createElement('div');
}
} else {
html += '<div class="secondary listItemBodyText">';
elem = document.createElement('div');
elem.classList.add('secondary');
}
html += (textlines[i] || '&nbsp;');
if (i === 0 && isLargeStyle) {
html += `</${largeTitleTagName}>`;
elem.classList.add('listItemBodyText');
if (textlines[i]) {
elem.innerText = textlines[i];
} else {
html += '</div>';
elem.innerHTML = '&nbsp;';
}
html += elem.outerHTML;
}
return html;

View file

@ -33,7 +33,7 @@
}
.mdl-spinner__layer-1 {
border-color: rgb(66, 165, 245);
border-color: #00a4dc;
}
.mdl-spinner__layer-1-active {
@ -42,7 +42,7 @@
}
.mdl-spinner__layer-2 {
border-color: rgb(244, 67, 54);
border-color: #00a4dc;
}
.mdl-spinner__layer-2-active {
@ -51,7 +51,7 @@
}
.mdl-spinner__layer-3 {
border-color: rgb(253, 216, 53);
border-color: #00a4dc;
}
.mdl-spinner__layer-3-active {
@ -60,7 +60,7 @@
}
.mdl-spinner__layer-4 {
border-color: rgb(76, 175, 80);
border-color: #00a4dc;
}
.mdl-spinner__layer-4-active {

View file

@ -102,8 +102,8 @@ import template from './mediaLibraryCreator.template.html';
function onAddButtonClick() {
const page = dom.parentWithClass(this, 'dlg-librarycreator');
import('../directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
const picker = new directoryBrowser();
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
const picker = new DirectoryBrowser();
picker.show({
enableNetworkSharePath: true,
callback: function (path, networkSharePath) {

View file

@ -162,8 +162,8 @@ import template from './mediaLibraryEditor.template.html';
}
function showDirectoryBrowser(context, originalPath, networkPath) {
import('../directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
const picker = new directoryBrowser();
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
const picker = new DirectoryBrowser();
picker.show({
enableNetworkSharePath: true,
pathReadOnly: originalPath != null,

View file

@ -106,10 +106,9 @@ import '../../elements/emby-button/emby-button';
const miscInfo = [];
let text;
let date;
let minutes;
let count;
const showFolderRuntime = item.Type === 'MusicAlbum' || item.MediaType === 'MusicArtist' || item.MediaType === 'Playlist' || item.MediaType === 'MusicGenre';
const showFolderRuntime = item.Type === 'MusicAlbum' || item.MediaType === 'MusicArtist' || item.Type === 'Playlist' || item.MediaType === 'Playlist' || item.MediaType === 'MusicGenre';
if (showFolderRuntime) {
count = item.SongCount || item.ChildCount;
@ -119,7 +118,7 @@ import '../../elements/emby-button/emby-button';
}
if (item.RunTimeTicks) {
miscInfo.push(datetime.getDisplayRunningTime(item.RunTimeTicks));
miscInfo.push(datetime.getDisplayDuration(item.RunTimeTicks));
}
} else if (item.Type === 'PhotoAlbum' || item.Type === 'BoxSet') {
count = item.ChildCount;
@ -132,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);
@ -257,11 +257,7 @@ import '../../elements/emby-button/emby-button';
if (item.Type === 'Audio') {
miscInfo.push(datetime.getDisplayRunningTime(item.RunTimeTicks));
} else {
minutes = item.RunTimeTicks / 600000000;
minutes = minutes || 1;
miscInfo.push(`${Math.round(minutes)} mins`);
miscInfo.push(datetime.getDisplayDuration(item.RunTimeTicks));
}
}

View file

@ -520,7 +520,7 @@ import template from './metadataEditor.template.html';
hideElement('#fldPath', context);
}
if (item.Type === 'Series' || item.Type === 'Movie' || item.Type === 'Trailer') {
if (item.Type === 'Series' || item.Type === 'Movie' || item.Type === 'Trailer' || item.Type === 'Person') {
showElement('#fldOriginalName', context);
} else {
hideElement('#fldOriginalName', context);
@ -637,7 +637,9 @@ import template from './metadataEditor.template.html';
}
if (item.Type === 'Person') {
//todo
context.querySelector('#txtName').label(globalize.translate('LabelName'));
context.querySelector('#txtSortName').label(globalize.translate('LabelSortName'));
context.querySelector('#txtOriginalName').label(globalize.translate('LabelOriginalName'));
context.querySelector('#txtProductionYear').label(globalize.translate('LabelBirthYear'));
context.querySelector('#txtPremiereDate').label(globalize.translate('LabelBirthDate'));
context.querySelector('#txtEndDate').label(globalize.translate('LabelDeathDate'));

View file

@ -252,7 +252,7 @@
<br />
<div class="formDialogFooter">
<button is="emby-button" type="button" class="raised button-cancel block btnCancel formDialogFooterItem">
<span>${Cancel}</span>
<span>${ButtonCancel}</span>
</button>
<button is="emby-button" type="submit" class="raised button-submit block btnSave formDialogFooterItem">
<span>${SaveChanges}</span>

View file

@ -8,6 +8,7 @@ import ServerConnections from '../ServerConnections';
import alert from '../alert';
import playlistEditor from '../playlisteditor/playlisteditor';
import confirm from '../confirm/confirm';
import itemHelper from '../itemHelper';
/* eslint-disable indent */
@ -170,129 +171,155 @@ import confirm from '../confirm/confirm';
const apiClient = ServerConnections.currentApiClient();
apiClient.getCurrentUser().then(user => {
const menuItems = [];
// get first selected item to perform metadata refresh permission check
apiClient.getItem(apiClient.getCurrentUserId(), selectedItems[0]).then(firstItem => {
const menuItems = [];
menuItems.push({
name: globalize.translate('AddToCollection'),
id: 'addtocollection',
icon: 'add'
});
menuItems.push({
name: globalize.translate('AddToPlaylist'),
id: 'playlist',
icon: 'playlist_add'
});
// TODO: Be more dynamic based on what is selected
if (user.Policy.EnableContentDeletion) {
menuItems.push({
name: globalize.translate('Delete'),
id: 'delete',
icon: 'delete'
name: globalize.translate('SelectAll'),
id: 'selectall',
icon: 'select_all'
});
}
if (user.Policy.EnableContentDownloading && appHost.supports('filedownload')) {
// Disabled because there is no callback for this item
/*
menuItems.push({
name: globalize.translate('Download'),
id: 'download',
icon: 'file_download'
name: globalize.translate('AddToCollection'),
id: 'addtocollection',
icon: 'add'
});
*/
}
if (user.Policy.IsAdministrator) {
menuItems.push({
name: globalize.translate('GroupVersions'),
id: 'groupvideos',
icon: 'call_merge'
name: globalize.translate('AddToPlaylist'),
id: 'playlist',
icon: 'playlist_add'
});
}
menuItems.push({
name: globalize.translate('MarkPlayed'),
id: 'markplayed',
icon: 'check_box'
});
// TODO: Be more dynamic based on what is selected
if (user.Policy.EnableContentDeletion) {
menuItems.push({
name: globalize.translate('Delete'),
id: 'delete',
icon: 'delete'
});
}
menuItems.push({
name: globalize.translate('MarkUnplayed'),
id: 'markunplayed',
icon: 'check_box_outline_blank'
});
if (user.Policy.EnableContentDownloading && appHost.supports('filedownload')) {
// Disabled because there is no callback for this item
/*
menuItems.push({
name: globalize.translate('Download'),
id: 'download',
icon: 'file_download'
});
*/
}
menuItems.push({
name: globalize.translate('RefreshMetadata'),
id: 'refresh',
icon: 'refresh'
});
if (user.Policy.IsAdministrator) {
menuItems.push({
name: globalize.translate('GroupVersions'),
id: 'groupvideos',
icon: 'call_merge'
});
}
import('../actionSheet/actionSheet').then((actionsheet) => {
actionsheet.show({
items: menuItems,
positionTo: e.target,
callback: function (id) {
const items = selectedItems.slice(0);
const serverId = apiClient.serverInfo().Id;
menuItems.push({
name: globalize.translate('MarkPlayed'),
id: 'markplayed',
icon: 'check_box'
});
switch (id) {
case 'addtocollection':
import('../collectionEditor/collectionEditor').then(({default: collectionEditor}) => {
new collectionEditor({
menuItems.push({
name: globalize.translate('MarkUnplayed'),
id: 'markunplayed',
icon: 'check_box_outline_blank'
});
// this assues that if the user can refresh metadata for the first item
// they can refresh metadata for all items
if (itemHelper.canRefreshMetadata(firstItem, user)) {
menuItems.push({
name: globalize.translate('RefreshMetadata'),
id: 'refresh',
icon: 'refresh'
});
}
import('../actionSheet/actionSheet').then((actionsheet) => {
actionsheet.show({
items: menuItems,
positionTo: e.target,
callback: function (id) {
const items = selectedItems.slice(0);
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({
items: items,
serverId: serverId
});
});
hideSelections();
dispatchNeedsRefresh();
break;
case 'playlist':
new playlistEditor({
items: items,
serverId: serverId
});
});
hideSelections();
dispatchNeedsRefresh();
break;
case 'playlist':
new playlistEditor({
items: items,
serverId: serverId
});
hideSelections();
dispatchNeedsRefresh();
break;
case 'delete':
deleteItems(apiClient, items).then(dispatchNeedsRefresh);
hideSelections();
dispatchNeedsRefresh();
break;
case 'groupvideos':
combineVersions(apiClient, items);
break;
case 'markplayed':
items.forEach(itemId => {
apiClient.markPlayed(apiClient.getCurrentUserId(), itemId);
});
hideSelections();
dispatchNeedsRefresh();
break;
case 'markunplayed':
items.forEach(itemId => {
apiClient.markUnplayed(apiClient.getCurrentUserId(), itemId);
});
hideSelections();
dispatchNeedsRefresh();
break;
case 'refresh':
import('../refreshdialog/refreshdialog').then(({default: refreshDialog}) => {
new refreshDialog({
itemIds: items,
serverId: serverId
}).show();
});
hideSelections();
dispatchNeedsRefresh();
break;
default:
break;
hideSelections();
dispatchNeedsRefresh();
break;
case 'delete':
deleteItems(apiClient, items).then(dispatchNeedsRefresh);
hideSelections();
dispatchNeedsRefresh();
break;
case 'groupvideos':
combineVersions(apiClient, items);
break;
case 'markplayed':
items.forEach(itemId => {
apiClient.markPlayed(apiClient.getCurrentUserId(), itemId);
});
hideSelections();
dispatchNeedsRefresh();
break;
case 'markunplayed':
items.forEach(itemId => {
apiClient.markUnplayed(apiClient.getCurrentUserId(), itemId);
});
hideSelections();
dispatchNeedsRefresh();
break;
case 'refresh':
import('../refreshdialog/refreshdialog').then(({default: refreshDialog}) => {
new refreshDialog({
itemIds: items,
serverId: serverId
}).show();
});
hideSelections();
dispatchNeedsRefresh();
break;
default:
break;
}
}
}
});
});
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

@ -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
}
];

View file

@ -0,0 +1,250 @@
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../scripts/clientUtils';
import globalize from '../../scripts/globalize';
import loading from '../loading/loading';
import toast from '../toast/toast';
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
import InputElement from '../dashboard/users/InputElement';
import CheckBoxElement from '../dashboard/users/CheckBoxElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import ButtonElement from '../dashboard/users/ButtonElement';
type userInput = {
Name?: string;
Password?: string;
}
type ItemsArr = {
Name?: string;
Id?: string;
}
const NewUserPage: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState([]);
const element = useRef(null);
const getItemsResult = (items: ItemsArr[]) => {
return items.map(item =>
({
Id: item.Id,
Name: item.Name
})
);
};
const loadMediaFolders = useCallback((result) => {
const mediaFolders = getItemsResult(result);
setMediaFoldersItems(mediaFolders);
const folderAccess = element?.current?.querySelector('.folderAccess');
folderAccess.dispatchEvent(new CustomEvent('create'));
element.current.querySelector('.chkEnableAllFolders').checked = false;
}, []);
const loadChannels = useCallback((result) => {
const channels = getItemsResult(result);
setChannelsItems(channels);
const channelAccess = element?.current?.querySelector('.channelAccess');
channelAccess.dispatchEvent(new CustomEvent('create'));
const channelAccessContainer = element?.current?.querySelector('.channelAccessContainer');
channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide');
element.current.querySelector('.chkEnableAllChannels').checked = false;
}, []);
const loadUser = useCallback(() => {
element.current.querySelector('#txtUsername').value = '';
element.current.querySelector('#txtPassword').value = '';
loading.show();
const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promiseChannels = window.ApiClient.getJSON(window.ApiClient.getUrl('Channels'));
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
loadMediaFolders(responses[0].Items);
loadChannels(responses[1].Items);
loading.hide();
});
}, [loadChannels, loadMediaFolders]);
useEffect(() => {
loadUser();
const saveUser = () => {
const userInput: userInput = {};
userInput.Name = element?.current?.querySelector('#txtUsername').value;
userInput.Password = element?.current?.querySelector('#txtPassword').value;
window.ApiClient.createUser(userInput).then(function (user) {
user.Policy.EnableAllFolders = element?.current?.querySelector('.chkEnableAllFolders').checked;
user.Policy.EnabledFolders = [];
if (!user.Policy.EnableAllFolders) {
user.Policy.EnabledFolders = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked;
user.Policy.EnabledChannels = [];
if (!user.Policy.EnableAllChannels) {
user.Policy.EnabledChannels = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
Dashboard.navigate('useredit.html?userId=' + user.Id);
});
}, function () {
toast(globalize.translate('ErrorDefault'));
loading.hide();
});
};
const onSubmit = (e) => {
loading.show();
saveUser();
e.preventDefault();
e.stopPropagation();
return false;
};
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) {
const channelAccessListContainer = element?.current?.querySelector('.channelAccessListContainer');
this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide');
});
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) {
const folderAccessListContainer = element?.current?.querySelector('.folderAccessListContainer');
this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide');
});
element?.current?.querySelector('.newUserProfileForm').addEventListener('submit', onSubmit);
element?.current?.querySelector('.button-cancel').addEventListener('click', function() {
window.history.back();
});
}, [loadUser]);
return (
<div ref={element}>
<div className='content-primary'>
<div className='verticalSection'>
<div className='sectionTitleContainer flex align-items-center'>
<h2 className='sectionTitle'>
{globalize.translate('ButtonAddUser')}
</h2>
<SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
</div>
<form className='newUserProfileForm'>
<div className='inputContainer'>
<InputElement
type='text'
id='txtUsername'
label='LabelName'
options={'required'}
/>
</div>
<div className='inputContainer'>
<InputElement
type='password'
id='txtPassword'
label='LabelPassword'
/>
</div>
<div className='folderAccessContainer'>
<h2>{globalize.translate('HeaderLibraryAccess')}</h2>
<CheckBoxElement
type='checkbox'
className='chkEnableAllFolders'
title='OptionEnableAccessToAllLibraries'
/>
<div className='folderAccessListContainer'>
<div className='folderAccess'>
<h3 className='checkboxListLabel'>
{globalize.translate('HeaderLibraries')}
</h3>
<div className='checkboxList paperList' style={{padding: '.5em 1em'}}>
{mediaFoldersItems.map(Item => (
<CheckBoxListItem
key={Item.Id}
className='chkFolder'
Id={Item.Id}
Name={Item.Name}
checkedAttribute=''
/>
))}
</div>
</div>
<div className='fieldDescription'>
{globalize.translate('LibraryAccessHelp')}
</div>
</div>
</div>
<div className='channelAccessContainer verticalSection-extrabottompadding hide'>
<h2>{globalize.translate('HeaderChannelAccess')}</h2>
<CheckBoxElement
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=''
/>
))}
</div>
</div>
<div className='fieldDescription'>
{globalize.translate('ChannelAccessHelp')}
</div>
</div>
</div>
<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 NewUserPage;

View 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;

View 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;

View file

@ -0,0 +1,154 @@
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import Dashboard from '../../scripts/clientUtils';
import globalize from '../../scripts/globalize';
import loading from '../loading/loading';
import dom from '../../scripts/dom';
import confirm from '../../components/confirm/confirm';
import UserCardBox from '../dashboard/users/UserCardBox';
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement';
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
import '../../elements/emby-button/emby-button';
import '../../elements/emby-button/paper-icon-button-light';
import '../../components/cardbuilder/card.scss';
import '../../components/indicators/indicators.scss';
import '../../assets/css/flexstyles.scss';
type MenuEntry = {
name?: string;
id?: string;
icon?: string;
}
const UserProfilesPage: FunctionComponent = () => {
const [ users, setUsers ] = useState([]);
const element = useRef(null);
const loadData = () => {
loading.show();
window.ApiClient.getUsers().then(function (result) {
setUsers(result);
loading.hide();
});
};
useEffect(() => {
loadData();
const showUserMenu = (elem) => {
const card = dom.parentWithClass(elem, 'card');
const userId = card.getAttribute('data-userid');
const menuItems: MenuEntry[] = [];
menuItems.push({
name: globalize.translate('ButtonOpen'),
id: 'open',
icon: 'mode_edit'
});
menuItems.push({
name: globalize.translate('ButtonLibraryAccess'),
id: 'access',
icon: 'lock'
});
menuItems.push({
name: globalize.translate('ButtonParentalControl'),
id: 'parentalcontrol',
icon: 'person'
});
menuItems.push({
name: globalize.translate('Delete'),
id: 'delete',
icon: 'delete'
});
import('../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
actionsheet.show({
items: menuItems,
positionTo: card,
callback: function (id) {
switch (id) {
case 'open':
Dashboard.navigate('useredit.html?userId=' + userId);
break;
case 'access':
Dashboard.navigate('userlibraryaccess.html?userId=' + userId);
break;
case 'parentalcontrol':
Dashboard.navigate('userparentalcontrol.html?userId=' + userId);
break;
case 'delete':
deleteUser(userId);
}
}
});
});
};
const deleteUser = (id) => {
const msg = globalize.translate('DeleteUserConfirmation');
confirm({
title: globalize.translate('DeleteUser'),
text: msg,
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(function () {
loading.show();
window.ApiClient.deleteUser(id).then(function () {
loadData();
});
});
};
element?.current?.addEventListener('click', function (e) {
const btnUserMenu = dom.parentWithClass(e.target, 'btnUserMenu');
if (btnUserMenu) {
showUserMenu(btnUserMenu);
}
});
element?.current?.querySelector('.btnAddUser').addEventListener('click', function() {
Dashboard.navigate('usernew.html');
});
}, []);
return (
<div ref={element}>
<div className='content-primary'>
<div className='verticalSection verticalSection-extrabottompadding'>
<div
className='sectionTitleContainer sectionTitleContainer-cards'
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
>
<h2 className='sectionTitle sectionTitle-cards'>
{globalize.translate('HeaderUsers')}
</h2>
<SectionTitleButtonElement
className='fab btnAddUser submit sectionTitleButton'
title='ButtonAddUser'
icon='add'
/>
<SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url='https://docs.jellyfin.org/general/server/users/adding-managing-users.html'
/>
</div>
<div className='localUsers itemsContainer vertical-wrap'>
{users.map(user => {
return <UserCardBox key={user.Id} user={user} />;
})}
</div>
</div>
</div>
</div>
);
};
export default UserProfilesPage;

View file

@ -8,7 +8,7 @@ import * as userSettings from '../../scripts/settings/userSettings';
import globalize from '../../scripts/globalize';
import loading from '../loading/loading';
import { appHost } from '../apphost';
import * as Screenfull from 'screenfull';
import Screenfull from 'screenfull';
import ServerConnections from '../ServerConnections';
import alert from '../alert';
@ -618,21 +618,6 @@ function supportsDirectPlay(apiClient, item, mediaSource) {
} else {
return isHostReachable(mediaSource, apiClient);
}
} else if (mediaSource.Protocol === 'File') {
return new Promise(function (resolve) {
// Determine if the file can be accessed directly
import('../../scripts/filesystem').then((filesystem) => {
const method = isFolderRip ?
'directoryExists' :
'fileExists';
filesystem[method](mediaSource.Path).then(function () {
resolve(true);
}, function () {
resolve(false);
});
});
});
}
}
@ -1812,7 +1797,8 @@ class PlaybackManager {
// Setting this to true may cause some incorrect sorting
Recursive: false,
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Photo,Video',
// Only include Photos because we do not handle mixed queues currently
MediaTypes: 'Photo',
Limit: 1000
});
} else if (firstItem.Type === 'MusicGenre') {
@ -1823,6 +1809,16 @@ class PlaybackManager {
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Audio'
});
} else if (firstItem.IsFolder && firstItem.CollectionType === 'homevideos') {
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
ParentId: firstItem.Id,
Filters: 'IsNotFolder',
Recursive: true,
SortBy: options.shuffle ? 'Random' : 'SortName',
// Only include Photos because we do not handle mixed queues currently
MediaTypes: 'Photo',
Limit: 1000
}, queryOptions));
} else if (firstItem.IsFolder) {
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
ParentId: firstItem.Id,
@ -2481,7 +2477,7 @@ class PlaybackManager {
// Only used for audio
playMethod = 'Transcode';
mediaUrl = mediaSource.StreamUrl;
} else if (mediaSource.SupportsDirectStream) {
} else if (mediaSource.SupportsDirectPlay || mediaSource.SupportsDirectStream) {
directOptions = {
Static: true,
mediaSourceId: mediaSource.Id,
@ -2500,7 +2496,7 @@ class PlaybackManager {
const prefix = type === 'Video' ? 'Videos' : 'Audio';
mediaUrl = apiClient.getUrl(prefix + '/' + item.Id + '/stream.' + mediaSourceContainer, directOptions);
playMethod = 'DirectStream';
playMethod = mediaSource.SupportsDirectPlay ? 'DirectPlay' : 'DirectStream';
} else if (mediaSource.SupportsTranscoding) {
mediaUrl = apiClient.getUrl(mediaSource.TranscodingUrl);
@ -3068,7 +3064,9 @@ class PlaybackManager {
const data = getPlayerData(player);
const streamInfo = data.streamInfo;
const nextItem = self._playNextAfterEnded ? self._playQueueManager.getNextItemInfo() : null;
const errorOccurred = displayErrorCode && typeof (displayErrorCode) === 'string';
const nextItem = self._playNextAfterEnded && !errorOccurred ? self._playQueueManager.getNextItemInfo() : null;
const nextMediaType = (nextItem ? nextItem.item.MediaType : null);
@ -3105,17 +3103,15 @@ class PlaybackManager {
const newPlayer = nextItem ? getPlayer(nextItem.item, nextItemPlayOptions) : null;
if (newPlayer !== player) {
data.streamInfo = null;
destroyPlayer(player);
removeCurrentPlayer(player);
}
if (displayErrorCode && typeof (displayErrorCode) === 'string') {
if (errorOccurred) {
showPlaybackInfoErrorMessage(self, 'PlaybackError' + displayErrorCode);
} else if (nextItem) {
self.nextTrack();
} else {
// Nothing more to play - clear data
data.streamInfo = null;
}
}
@ -3503,7 +3499,7 @@ class PlaybackManager {
this.seek(ticks, player);
}
playTrailers(item) {
async playTrailers(item) {
const player = this._currentPlayer;
if (player && player.playTrailers) {
@ -3512,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) {
@ -3606,6 +3600,9 @@ class PlaybackManager {
setPlaybackRate(value, player = this._currentPlayer) {
if (player && player.setPlaybackRate) {
player.setPlaybackRate(value);
// Save the new playback rate in the browser session, to restore when playing a new video.
sessionStorage.setItem('playbackRateSpeed', value);
}
}

View file

@ -157,15 +157,6 @@ import template from './playbackSettings.template.html';
context.querySelector('.chkEpisodeAutoPlay').checked = user.Configuration.EnableNextEpisodeAutoPlay || false;
});
// hide cinema mode options if disabled at server level
apiClient.getNamedConfiguration('cinemamode').then(cinemaConfig => {
if (cinemaConfig.EnableIntrosForMovies || cinemaConfig.EnableIntrosForEpisodes) {
context.querySelector('.cinemaModeOptions').classList.remove('hide');
} else {
context.querySelector('.cinemaModeOptions').classList.add('hide');
}
});
if (appHost.supports('externalplayerintent') && userId === loggedInUserId) {
context.querySelector('.fldExternalPlayer').classList.remove('hide');
} else {

View file

@ -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);

View file

@ -24,9 +24,9 @@ function getEditorHtml() {
html += '<div class="fldSelectPlaylist selectContainer">';
html += '<select is="emby-select" id="selectMetadataRefreshMode" label="' + globalize.translate('LabelRefreshMode') + '">';
html += '<option value="scan">' + globalize.translate('ScanForNewAndUpdatedFiles') + '</option>';
html += '<option value="scan" selected>' + globalize.translate('ScanForNewAndUpdatedFiles') + '</option>';
html += '<option value="missing">' + globalize.translate('SearchForMissingMetadata') + '</option>';
html += '<option value="all" selected>' + globalize.translate('ReplaceAllMetadata') + '</option>';
html += '<option value="all">' + globalize.translate('ReplaceAllMetadata') + '</option>';
html += '</select>';
html += '</div>';

View file

@ -718,11 +718,9 @@ export default function () {
btnCommand[i].addEventListener('click', onBtnCommandClick);
}
context.querySelector('.btnToggleFullscreen').addEventListener('click', function (e) {
context.querySelector('.btnToggleFullscreen').addEventListener('click', function () {
if (currentPlayer) {
playbackManager.sendCommand({
Name: e.target.getAttribute('data-command')
}, currentPlayer);
playbackManager.toggleFullscreen(currentPlayer);
}
});
context.querySelector('.btnAudioTracks').addEventListener('click', function (e) {

View file

@ -172,6 +172,8 @@ export default function (options) {
html += '<div class="topActionButtons">';
if (actionButtonsOnTop) {
html += getIcon('play_arrow', 'btnSlideshowPause slideshowButton', true);
if (appHost.supports('filedownload') && options.user && options.user.Policy.EnableContentDownloading) {
html += getIcon('file_download', 'btnDownload slideshowButton', true);
}
@ -347,7 +349,7 @@ export default function (options) {
minRatio: 1,
toggle: true
},
autoplay: !options.interactive,
autoplay: !options.interactive || !!options.autoplay,
keyboard: {
enabled: true
},
@ -376,6 +378,8 @@ export default function (options) {
if (useFakeZoomImage) {
swiperInstance.on('zoomChange', onZoomChange);
}
if (swiperInstance.autoplay?.running) onAutoplayStart();
}
/**

View file

@ -78,6 +78,7 @@
<option value="typewriter">${Typewriter}</option>
<option value="print">${Print}</option>
<option value="console">${Console}</option>
<option value="cursive">${Cursive}</option>
<option value="casual">${Casual}</option>
<option value="smallcaps">${SmallCaps}</option>
</select>

View file

@ -193,10 +193,19 @@ class Manager {
this.queueCore.updatePlayQueue(apiClient, cmd.Data);
break;
case 'UserJoined':
toast(globalize.translate('MessageSyncPlayUserJoined', cmd.Data));
if (!this.groupInfo.Participants) {
this.groupInfo.Participants = [cmd.Data];
} else {
this.groupInfo.Participants.push(cmd.Data);
}
break;
case 'UserLeft':
toast(globalize.translate('MessageSyncPlayUserLeft', cmd.Data));
if (this.groupInfo.Participants) {
this.groupInfo.Participants = this.groupInfo.Participants.filter((user) => user !== cmd.Data);
}
break;
case 'GroupJoined':
cmd.Data.LastUpdatedAt = new Date(cmd.Data.LastUpdatedAt);

View file

@ -2,8 +2,9 @@
* Module that manages the playback of SyncPlay.
* @module components/syncPlay/core/PlaybackCore
*/
import { Events } from 'jellyfin-apiclient';
import browser from '../../../scripts/browser';
import { toBoolean, toFloat } from '../../../scripts/stringUtils';
import * as Helper from './Helper';
import { getSetting } from './Settings';
@ -20,7 +21,6 @@ class PlaybackCore {
this.playbackDiffMillis = 0; // Used for stats and remote time sync.
this.syncAttempts = 0;
this.lastSyncTime = new Date();
this.enableSyncCorrection = true; // User setting to disable sync during playback.
this.playerIsBuffering = false;
@ -67,7 +67,7 @@ class PlaybackCore {
this.useSkipToSync = toBoolean(getSetting('useSkipToSync'), true);
// Whether sync correction during playback is active.
this.enableSyncCorrection = toBoolean(getSetting('enableSyncCorrection'), true);
this.enableSyncCorrection = toBoolean(getSetting('enableSyncCorrection'), !(browser.mobile || browser.iOS));
}
/**

View file

@ -48,7 +48,7 @@ class TimeSyncCore {
Events.trigger(this, 'time-sync-server-update', [timeOffset, ping]);
});
Events.on(appSettings, 'change', function (e, name) {
Events.on(appSettings, 'change', (e, name) => {
if (name === 'extraTimeOffset') {
this.extraTimeOffset = toFloat(getSetting('extraTimeOffset'), 0.0);
}

View file

@ -7,6 +7,7 @@ import actionsheet from '../../actionSheet/actionSheet';
import globalize from '../../../scripts/globalize';
import playbackPermissionManager from './playbackPermissionManager';
import ServerConnections from '../../ServerConnections';
import './groupSelectionMenu.scss';
/**
* Class that manages the SyncPlay group selection menu.
@ -63,7 +64,8 @@ class GroupSelectionMenu {
title: globalize.translate('HeaderSyncPlaySelectGroup'),
items: menuItems,
positionTo: button,
border: true
border: true,
dialogClass: 'syncPlayGroupMenu'
};
actionsheet.show(menuOptions).then(function (id) {
@ -139,6 +141,8 @@ class GroupSelectionMenu {
const menuOptions = {
title: groupInfo.GroupName,
text: groupInfo.Participants.join(', '),
dialogClass: 'syncPlayGroupMenu',
items: menuItems,
positionTo: button,
border: true

View file

@ -0,0 +1,4 @@
.syncPlayGroupMenu .actionSheetText {
margin-left: 0.6em; /* to line up with the title */
margin-top: 0;
}

View file

@ -5,13 +5,12 @@
import { Events } from 'jellyfin-apiclient';
import SyncPlay from '../../core';
import { getSetting, setSetting } from '../../core/Settings';
import { setSetting } from '../../core/Settings';
import dialogHelper from '../../../dialogHelper/dialogHelper';
import layoutManager from '../../../layoutManager';
import loading from '../../../loading/loading';
import toast from '../../../toast/toast';
import globalize from '../../../../scripts/globalize';
import { toBoolean, toFloat } from '../../../../scripts/stringUtils';
import 'material-design-icons-iconfont';
import '../../../../elements/emby-input/emby-input';
@ -96,14 +95,14 @@ class SettingsEditor {
async initEditor() {
const { context } = this;
context.querySelector('#txtExtraTimeOffset').value = toFloat(getSetting('extraTimeOffset'), 0.0);
context.querySelector('#chkSyncCorrection').checked = toBoolean(getSetting('enableSyncCorrection'), true);
context.querySelector('#txtMinDelaySpeedToSync').value = toFloat(getSetting('minDelaySpeedToSync'), 60.0);
context.querySelector('#txtMaxDelaySpeedToSync').value = toFloat(getSetting('maxDelaySpeedToSync'), 3000.0);
context.querySelector('#txtSpeedToSyncDuration').value = toFloat(getSetting('speedToSyncDuration'), 1000.0);
context.querySelector('#txtMinDelaySkipToSync').value = toFloat(getSetting('minDelaySkipToSync'), 400.0);
context.querySelector('#chkSpeedToSync').checked = toBoolean(getSetting('useSpeedToSync'), true);
context.querySelector('#chkSkipToSync').checked = toBoolean(getSetting('useSkipToSync'), true);
context.querySelector('#txtExtraTimeOffset').value = SyncPlay.Manager.timeSyncCore.extraTimeOffset;
context.querySelector('#chkSyncCorrection').checked = SyncPlay.Manager.playbackCore.enableSyncCorrection;
context.querySelector('#txtMinDelaySpeedToSync').value = SyncPlay.Manager.playbackCore.minDelaySpeedToSync;
context.querySelector('#txtMaxDelaySpeedToSync').value = SyncPlay.Manager.playbackCore.maxDelaySpeedToSync;
context.querySelector('#txtSpeedToSyncDuration').value = SyncPlay.Manager.playbackCore.speedToSyncDuration;
context.querySelector('#txtMinDelaySkipToSync').value = SyncPlay.Manager.playbackCore.minDelaySkipToSync;
context.querySelector('#chkSpeedToSync').checked = SyncPlay.Manager.playbackCore.useSpeedToSync;
context.querySelector('#chkSkipToSync').checked = SyncPlay.Manager.playbackCore.useSkipToSync;
}
onSubmit() {

View file

@ -145,8 +145,8 @@ export default function (page, providerId, options) {
function onSelectPathClick(e) {
const page = $(e.target).parents('.xmltvForm')[0];
import('../directorybrowser/directorybrowser').then(({default: directoryBrowser}) => {
const picker = new directoryBrowser();
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
const picker = new DirectoryBrowser();
picker.show({
includeFiles: true,
callback: function (path) {