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

Merge branch 'master' into additional-episode-orders

This commit is contained in:
oledfish 2022-03-03 12:01:58 -03:00 committed by GitHub
commit 0e606ed69d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
275 changed files with 6941 additions and 4435 deletions

View file

@ -142,7 +142,7 @@ export function show(options) {
if (layoutManager.tv) {
html += `<button is="paper-icon-button-light" class="btnCloseActionSheet hide-mouse-idle-tv" tabindex="-1">
<span class="material-icons arrow_back"></span>
<span class="material-icons arrow_back" aria-hidden="true"></span>
</button>`;
}
@ -204,9 +204,9 @@ export function show(options) {
itemIcon = icons[i];
if (itemIcon) {
html += `<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons ${itemIcon}"></span>`;
html += `<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons ${itemIcon}" aria-hidden="true"></span>`;
} else if (renderIcon && !center) {
html += '<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons check" style="visibility:hidden;"></span>';
html += '<span class="actionsheetMenuItemIcon listItemIcon listItemIcon-transparent material-icons check" aria-hidden="true" style="visibility:hidden;"></span>';
}
html += '<div class="listItemBody actionsheetListItemBody">';

View file

@ -90,12 +90,13 @@
.actionSheetTitle {
margin: 0.6em 0 0.7em !important;
padding: 0 0.9em;
padding: 0 0.75rem;
flex-grow: 0;
}
.actionSheetText {
padding: 0 1em;
margin-top: 0;
padding: 0 0.75rem;
flex-grow: 0;
}

View file

@ -23,12 +23,12 @@ import alert from './alert';
}
if (entry.UserId && entry.UserPrimaryImageTag) {
html += '<span class="listItemIcon material-icons dvr" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
type: 'Primary',
tag: entry.UserPrimaryImageTag
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
} else {
html += '<span class="listItemIcon material-icons ' + icon + '" style="background-color:' + color + '"></span>';
html += '<span class="listItemIcon material-icons ' + icon + '" aria-hidden="true" style="background-color:' + color + '"></span>';
}
html += '<div class="listItemBody three-line">';
@ -45,7 +45,7 @@ import alert from './alert';
if (entry.Overview) {
html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">
<span class="material-icons info"></span>
<span class="material-icons info" aria-hidden="true"></span>
</button>`;
}

View file

@ -3,14 +3,14 @@ import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
import AlphaPicker from './alphaPicker';
type AlphaPickerProps = {
onAlphaPicked?: () => void
onAlphaPicked?: (e: Event) => void
};
// React compatibility wrapper component for alphaPicker.js
// eslint-disable-next-line @typescript-eslint/no-empty-function
const AlphaPickerComponent: FunctionComponent<AlphaPickerProps> = ({ onAlphaPicked = () => {} }: AlphaPickerProps) => {
const [ alphaPicker, setAlphaPicker ] = useState(null);
const element = useRef(null);
const [ alphaPicker, setAlphaPicker ] = useState<AlphaPicker>();
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
setAlphaPicker(new AlphaPicker({

View file

@ -75,7 +75,7 @@ import 'material-design-icons-iconfont';
html += `<div class="${rowClassName}">`;
if (options.mode === 'keyboard') {
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}"><span class="material-icons alphaPickerButtonIcon space_bar"></span></button>`;
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}"><span class="material-icons alphaPickerButtonIcon space_bar" aria-hidden="true"></span></button>`;
} else {
letters = ['#'];
html += mapLetters(letters, vertical).join('');
@ -85,7 +85,7 @@ import 'material-design-icons-iconfont';
html += mapLetters(letters, vertical).join('');
if (options.mode === 'keyboard') {
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}"><span class="material-icons alphaPickerButtonIcon backspace"></span></button>`;
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}"><span class="material-icons alphaPickerButtonIcon backspace" aria-hidden="true"></span></button>`;
html += '</div>';
letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
@ -287,7 +287,7 @@ import 'material-design-icons-iconfont';
this.value(query.NameStartsWith);
}
this.visible(query.SortBy.indexOf('SortName') === 0);
this.visible(query.SortBy.indexOf('SortName') !== -1);
}
visible(visible) {

View file

@ -34,15 +34,20 @@ class AppRouter {
constructor() {
// WebKit fires a popstate event on document load
// Skip it using timeout
// Skip it using boolean
// For Tizen 2.x
// https://stackoverflow.com/a/12214354
window.addEventListener('load', () => {
setTimeout(() => {
window.addEventListener('popstate', () => {
this.popstateOccurred = true;
});
}, 0);
// See `page` node module
let loaded = document.readyState === 'complete';
if (!loaded) {
window.addEventListener('load', () => {
setTimeout(() => {
loaded = true;
}, 0);
});
}
window.addEventListener('popstate', () => {
if (!loaded) return;
this.popstateOccurred = true;
});
document.addEventListener('viewshow', () => this.onViewShow());
@ -752,7 +757,13 @@ class AppRouter {
}
if (item === 'nextup') {
return '#!/list.html?type=nextup&serverId=' + options.serverId;
url = '#!/list.html?type=nextup&serverId=' + options.serverId;
if (options.rewatching) {
url += '&rewatching=' + options.rewatching;
}
return url;
}
if (item === 'list') {

View file

@ -199,7 +199,6 @@ const supportedFeatures = function () {
if (browser.operaTv || browser.tizen || browser.orsay || browser.web0s) {
features.push('exit');
} else {
features.push('exitmenu');
features.push('plugins');
}

View file

@ -64,7 +64,6 @@ import layoutManager from './layoutManager';
candidates.push(activeElement);
}
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnResume')));
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay')));
let focusedElement;

View file

@ -227,6 +227,10 @@ button::-moz-focus-inner {
background-color: transparent;
}
.cardPadder {
position: relative; // For centering the cardImageIcon
}
.cardBox:not(.visualCardBox) .cardPadder {
border-radius: 0.2em;
background-color: #242424;
@ -377,6 +381,18 @@ button::-moz-focus-inner {
color: inherit;
}
.cardPadder .cardImageIcon {
color: #111;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.cardImageContainer .cardImageIcon {
margin: auto; /* 'justify-content: center' doesn't work in Safari 10 */
}
.cardIndicators {
right: 0.225em;
top: 0.225em;

View file

@ -145,7 +145,7 @@ import ServerConnections from '../ServerConnections';
return 100 / 14.2857142857;
}
if (screenWidth >= 1200) {
return 100 / 16.666666666666666666;
return 100 / 16.66666667;
}
if (screenWidth >= 1000) {
return 5;
@ -481,12 +481,20 @@ import ServerConnections from '../ServerConnections';
return null;
}
/**
* @typedef {Object} CardImageUrl
* @property {string} imgUrl - Image URL.
* @property {string} blurhash - Image blurhash.
* @property {boolean} forceName - Force name.
* @property {boolean} coverImage - Use cover style.
*/
/** Get the URL of the card's image.
* @param {Object} item - Item for which to generate a card.
* @param {Object} apiClient - API client object.
* @param {Object} options - Options of the card.
* @param {string} shape - Shape of the desired image.
* @returns {Object} Object representing the URL of the card's image.
* @returns {CardImageUrl} Object representing the URL of the card's image.
*/
function getCardImageUrl(item, apiClient, options, shape) {
item = item.ProgramInfo || item;
@ -639,7 +647,7 @@ import ServerConnections from '../ServerConnections';
/**
* Generates an index used to select the default color of a card based on a string.
* @param {string} str - String to use for generating the index.
* @param {?string} [str] - String to use for generating the index.
* @returns {number} Index of the color.
*/
function getDefaultColorIndex(str) {
@ -726,8 +734,8 @@ import ServerConnections from '../ServerConnections';
/**
* Returns the air time text for the item based on the given times.
* @param {object} item - Item used to generate the air time text.
* @param {string} showAirDateTime - ISO8601 date for the start of the show.
* @param {string} showAirEndTime - ISO8601 date for the end of the show.
* @param {boolean} showAirDateTime - ISO8601 date for the start of the show.
* @param {boolean} showAirEndTime - ISO8601 date for the end of the show.
* @returns {string} The air time text for the item based on the given dates.
*/
function getAirTimeText(item, showAirDateTime, showAirEndTime) {
@ -782,7 +790,7 @@ import ServerConnections from '../ServerConnections';
if (isOuterFooter && options.cardLayout && layoutManager.mobile) {
if (options.cardFooterAside !== 'none') {
html += '<button is="paper-icon-button-light" class="itemAction btnCardOptions cardText-secondary" data-action="menu"><span class="material-icons more_vert"></span></button>';
html += '<button is="paper-icon-button-light" class="itemAction btnCardOptions cardText-secondary" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
}
}
@ -856,6 +864,10 @@ import ServerConnections from '../ServerConnections';
}
}
if (item.ExtraType && item.ExtraType !== 'Unknown') {
lines.push(globalize.translate(item.ExtraType));
}
if (options.showItemCounts) {
lines.push(getItemCountsHtml(options, item));
}
@ -1125,7 +1137,7 @@ import ServerConnections from '../ServerConnections';
/**
* Returns the default background class for a card based on a string.
* @param {string} str - Text used to generate the background class.
* @param {?string} [str] - Text used to generate the background class.
* @returns {string} CSS classes for default card backgrounds.
*/
export function getDefaultBackgroundClass(str) {
@ -1297,15 +1309,15 @@ import ServerConnections from '../ServerConnections';
const btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction';
if (options.centerPlayButton) {
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayButton-centered" data-action="play"><span class="material-icons cardOverlayButtonIcon play_arrow"></span></button>';
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayButton-centered" data-action="play"><span class="material-icons cardOverlayButtonIcon play_arrow" aria-hidden="true"></span></button>';
}
if (overlayPlayButton && !item.IsPlaceHolder && (item.LocationType !== 'Virtual' || !item.MediaType || item.Type === 'Program') && item.Type !== 'Person') {
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="play"><span class="material-icons cardOverlayButtonIcon play_arrow"></span></button>';
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="play"><span class="material-icons cardOverlayButtonIcon play_arrow" aria-hidden="true"></span></button>';
}
if (options.overlayMoreButton) {
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="menu"><span class="material-icons cardOverlayButtonIcon more_vert"></span></button>';
overlayButtons += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="menu"><span class="material-icons cardOverlayButtonIcon more_vert" aria-hidden="true"></span></button>';
}
}
@ -1340,7 +1352,18 @@ import ServerConnections from '../ServerConnections';
const cardScalableClass = 'cardScalable';
cardImageContainerOpen = '<div class="' + cardBoxClass + '"><div class="' + cardScalableClass + '"><div class="cardPadder cardPadder-' + shape + '"></div>' + cardImageContainerOpen;
let cardPadderIcon = '';
// TV Channel logos are transparent so skip the placeholder to avoid overlapping
if (imgUrl && item.Type !== 'TvChannel') {
cardPadderIcon = getDefaultText(item, {
// Always use an icon
defaultCardImageIcon: 'folder',
...options
});
}
cardImageContainerOpen = `<div class="${cardBoxClass}"><div class="${cardScalableClass}"><div class="cardPadder cardPadder-${shape}">${cardPadderIcon}</div>${cardImageContainerOpen}`;
cardBoxClose = '</div>';
cardScalableClose = '</div>';
@ -1444,7 +1467,7 @@ import ServerConnections from '../ServerConnections';
const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light';
if (playbackManager.canPlay(item)) {
html += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayFab-primary" data-action="resume"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover play_arrow"></span></button>';
html += '<button is="paper-icon-button-light" class="' + btnCssClass + ' cardOverlayFab-primary" data-action="resume"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover play_arrow" aria-hidden="true"></span></button>';
}
html += '<div class="cardOverlayButton-br flex">';
@ -1454,7 +1477,7 @@ import ServerConnections from '../ServerConnections';
if (itemHelper.canMarkPlayed(item)) {
/* eslint-disable-next-line @babel/no-unused-expressions */
import('../../elements/emby-playstatebutton/emby-playstatebutton');
html += '<button is="emby-playstatebutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check"></span></button>';
html += '<button is="emby-playstatebutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover check" aria-hidden="true"></span></button>';
}
if (itemHelper.canRate(item)) {
@ -1462,10 +1485,10 @@ import ServerConnections from '../ServerConnections';
/* eslint-disable-next-line @babel/no-unused-expressions */
import('../../elements/emby-ratingbutton/emby-ratingbutton');
html += '<button is="emby-ratingbutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite"></span></button>';
html += '<button is="emby-ratingbutton" type="button" data-action="none" class="' + btnCssClass + '" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover favorite" aria-hidden="true"></span></button>';
}
html += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="menu"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover more_vert"></span></button>';
html += '<button is="paper-icon-button-light" class="' + btnCssClass + '" data-action="menu"><span class="material-icons cardOverlayButtonIcon cardOverlayButtonIcon-hover more_vert" aria-hidden="true"></span></button>';
html += '</div>';
html += '</div>';
@ -1480,35 +1503,40 @@ import ServerConnections from '../ServerConnections';
*/
export function getDefaultText(item, options) {
if (item.CollectionType) {
return '<span class="cardImageIcon material-icons ' + imageHelper.getLibraryIcon(item.CollectionType) + '"></span>';
return '<span class="cardImageIcon material-icons ' + imageHelper.getLibraryIcon(item.CollectionType) + '" aria-hidden="true"></span>';
}
switch (item.Type) {
case 'MusicAlbum':
return '<span class="cardImageIcon material-icons album"></span>';
return '<span class="cardImageIcon material-icons album" aria-hidden="true"></span>';
case 'MusicArtist':
case 'Person':
return '<span class="cardImageIcon material-icons person"></span>';
return '<span class="cardImageIcon material-icons person" aria-hidden="true"></span>';
case 'Audio':
return '<span class="cardImageIcon material-icons audiotrack"></span>';
return '<span class="cardImageIcon material-icons audiotrack" aria-hidden="true"></span>';
case 'Movie':
return '<span class="cardImageIcon material-icons movie"></span>';
return '<span class="cardImageIcon material-icons movie" aria-hidden="true"></span>';
case 'Episode':
case 'Series':
return '<span class="cardImageIcon material-icons tv"></span>';
return '<span class="cardImageIcon material-icons tv" aria-hidden="true"></span>';
case 'Program':
return '<span class="cardImageIcon material-icons live_tv" aria-hidden="true"></span>';
case 'Book':
return '<span class="cardImageIcon material-icons book"></span>';
return '<span class="cardImageIcon material-icons book" aria-hidden="true"></span>';
case 'Folder':
return '<span class="cardImageIcon material-icons folder"></span>';
return '<span class="cardImageIcon material-icons folder" aria-hidden="true"></span>';
case 'BoxSet':
return '<span class="cardImageIcon material-icons collections"></span>';
return '<span class="cardImageIcon material-icons collections" aria-hidden="true"></span>';
case 'Playlist':
return '<span class="cardImageIcon material-icons view_list"></span>';
return '<span class="cardImageIcon material-icons view_list" aria-hidden="true"></span>';
case 'Photo':
return '<span class="cardImageIcon material-icons photo" aria-hidden="true"></span>';
case 'PhotoAlbum':
return '<span class="cardImageIcon material-icons photo_album"></span>';
return '<span class="cardImageIcon material-icons photo_album" aria-hidden="true"></span>';
}
if (options && options.defaultCardImageIcon) {
return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '"></span>';
if (options?.defaultCardImageIcon) {
return '<span class="cardImageIcon material-icons ' + options.defaultCardImageIcon + '" aria-hidden="true"></span>';
}
const defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item);
@ -1605,7 +1633,7 @@ import ServerConnections from '../ServerConnections';
indicatorsElem = ensureIndicators(card, indicatorsElem);
indicatorsElem.appendChild(playedIndicator);
}
playedIndicator.innerHTML = '<span class="material-icons indicatorIcon check"></span>';
playedIndicator.innerHTML = '<span class="material-icons indicatorIcon check" aria-hidden="true"></span>';
} else {
playedIndicator = card.querySelector('.playedIndicator');
if (playedIndicator) {
@ -1688,7 +1716,7 @@ import ServerConnections from '../ServerConnections';
const icon = cell.querySelector('.timerIndicator');
if (!icon) {
const indicatorsElem = ensureIndicators(cell);
indicatorsElem.insertAdjacentHTML('beforeend', '<span class="material-icons timerIndicator indicatorIcon fiber_manual_record"></span>');
indicatorsElem.insertAdjacentHTML('beforeend', '<span class="material-icons timerIndicator indicatorIcon fiber_manual_record" aria-hidden="true"></span>');
}
cell.setAttribute('data-timerid', newTimerId);
}

View file

@ -94,7 +94,7 @@ import ServerConnections from '../ServerConnections';
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
if (!imgUrl) {
cardImageContainer += '<span class="material-icons cardImageIcon local_movies"></span>';
cardImageContainer += '<span class="material-icons cardImageIcon local_movies" aria-hidden="true"></span>';
}
let nameHtml = '';

View file

@ -72,7 +72,7 @@ export default class channelMapper {
function getTunerChannelHtml(channel, providerName) {
let html = '';
html += '<div class="listItem">';
html += '<span class="material-icons listItemIcon dvr"></span>';
html += '<span class="material-icons listItemIcon dvr" aria-hidden="true"></span>';
html += '<div class="listItemBody two-line">';
html += '<h3 class="listItemBodyText">';
html += channel.Name;
@ -85,7 +85,7 @@ export default class channelMapper {
html += '</div>';
html += '</div>';
html += `<button class="btnMap autoSize" is="paper-icon-button-light" type="button" data-id="${channel.Id}" data-providerid="${channel.ProviderChannelId}"><span class="material-icons mode_edit"></span></button>`;
html += `<button class="btnMap autoSize" is="paper-icon-button-light" type="button" data-id="${channel.Id}" data-providerid="${channel.ProviderChannelId}"><span class="material-icons mode_edit" aria-hidden="true"></span></button>`;
return html += '</div>';
}
@ -127,7 +127,7 @@ export default class channelMapper {
let html = '';
const title = globalize.translate('MapChannels');
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">';
html += title;
html += '</h3>';

View file

@ -229,7 +229,7 @@ import toast from '../toast/toast';
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">';
html += title;
html += '</h3>';

View file

@ -0,0 +1,58 @@
import React, { FunctionComponent } from 'react';
import datetime from '../../../scripts/datetime';
import globalize from '../../../scripts/globalize';
const createButtonElement = (index: number) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
class='btnDelete listItemButton'
data-index='${index}'
>
<span class='material-icons delete' aria-hidden='true' />
</button>`
});
type IProps = {
index: number;
Id: number;
DayOfWeek?: string;
StartHour?: number ;
EndHour?: number;
}
function getDisplayTime(hours = 0) {
let minutes = 0;
const pct = hours % 1;
if (pct) {
minutes = Math.floor(60 * pct);
}
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
}
const AccessScheduleList: FunctionComponent<IProps> = ({index, DayOfWeek, StartHour, EndHour}: IProps) => {
return (
<div
className='liSchedule listItem'
data-day={ DayOfWeek}
data-start={ StartHour}
data-end={ EndHour}
>
<div className='listItemBody two-line'>
<h3 className='listItemBodyText'>
{globalize.translate(DayOfWeek)}
</h3>
<div className='listItemBodyText secondary'>
{getDisplayTime(StartHour) + ' - ' + getDisplayTime(EndHour)}
</div>
</div>
<div
dangerouslySetInnerHTML={createButtonElement(index)}
/>
</div>
);
};
export default AccessScheduleList;

View file

@ -0,0 +1,36 @@
import React, { FunctionComponent } from 'react';
const createButtonElement = (tag?: string) => ({
__html: `<button
type='button'
is='paper-icon-button-light'
class='blockedTag btnDeleteTag listItemButton'
data-tag='${tag}'
>
<span class='material-icons delete' aria-hidden='true' />
</button>`
});
type IProps = {
tag?: string;
}
const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
return (
<div className='paperList'>
<div className='listItem'>
<div className='listItemBody'>
<h3 className='listItemBodyText'>
{tag}
</h3>
</div>
<div
dangerouslySetInnerHTML={createButtonElement(tag)}
/>
</div>
</div>
);
};
export default BlockedTagList;

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createButtonElement = ({ type, className, title }) => ({
const createButtonElement = ({ type, className, title }: { type?: string, className?: string, title?: string }) => ({
__html: `<button
is="emby-button"
type="${type}"

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createCheckBoxElement = ({ labelClassName, type, className, title }) => ({
const createCheckBoxElement = ({ labelClassName, type, className, title }: { labelClassName?: string, type?: string, className?: string, title?: string }) => ({
__html: `<label class="${labelClassName}">
<input
is="emby-checkbox"

View file

@ -4,30 +4,31 @@ type IProps = {
className?: string;
Name?: string;
Id?: string;
ItemType?: string;
AppName?: string;
checkedAttribute?: string;
}
const createCheckBoxElement = ({className, Name, Id, AppName, checkedAttribute}) => ({
const createCheckBoxElement = ({className, Name, dataAttributes, AppName, checkedAttribute}: {className?: string, Name?: string, dataAttributes?: string, AppName?: string, checkedAttribute?: string}) => ({
__html: `<label>
<input
type="checkbox"
is="emby-checkbox"
class="${className}"
data-id="${Id}" ${checkedAttribute}
${dataAttributes} ${checkedAttribute}
/>
<span>${Name} ${AppName}</span>
</label>`
});
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, AppName, checkedAttribute}: IProps) => {
const CheckBoxListItem: FunctionComponent<IProps> = ({className, Name, Id, ItemType, AppName, checkedAttribute}: IProps) => {
return (
<div
className='sectioncheckbox'
dangerouslySetInnerHTML={createCheckBoxElement({
className: className,
Name: Name,
Id: Id,
dataAttributes: ItemType ? `data-itemtype='${ItemType}'` : `data-id='${Id}'`,
AppName: AppName ? `- ${AppName}` : '',
checkedAttribute: checkedAttribute
})}

View file

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

View file

@ -6,7 +6,7 @@ type IProps = {
className?: string;
}
const createLinkElement = ({ className, title }) => ({
const createLinkElement = ({ className, title }: IProps) => ({
__html: `<a
is="emby-linkbutton"
class="${className}"

View file

@ -5,7 +5,7 @@ type IProps = {
activeTab: string;
}
const createLinkElement = ({ activeTab }) => ({
const createLinkElement = (activeTab: string) => ({
__html: `<a href="#"
is="emby-linkbutton"
data-role="button"
@ -42,9 +42,7 @@ const SectionTabs: FunctionComponent<IProps> = ({activeTab}: IProps) => {
data-role='controlgroup'
data-type='horizontal'
className='localnav'
dangerouslySetInnerHTML={createLinkElement({
activeTab: activeTab
})}
dangerouslySetInnerHTML={createLinkElement(activeTab)}
/>
);
};

View file

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

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createLinkElement = ({ className, title, href }) => ({
const createLinkElement = ({ className, title, href }: { className?: string, title?: string, href?: string }) => ({
__html: `<a
is="emby-linkbutton"
rel="noopener noreferrer"

View file

@ -1,7 +1,7 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, label, option }) => ({
const createSelectElement = ({ className, label, option }: { className?: string, label: string, option: string[] }) => ({
__html: `<select
class="${className}"
is="emby-select"

View file

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

View file

@ -1,9 +1,9 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../scripts/globalize';
const createSelectElement = ({ className, id, label }) => ({
const createSelectElement = ({ className, id, label }: { className?: string, id?: string, label: string }) => ({
__html: `<select
className="${className}"
class="${className}"
is="emby-select"
id="${id}"
label="${label}"

View file

@ -1,10 +1,11 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
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 }) => ({
const createLinkElement = ({ user, renderImgUrl }: { user: UserDto, renderImgUrl: string }) => ({
__html: `<a
is="emby-linkbutton"
class="cardContent"
@ -20,15 +21,15 @@ const createButtonElement = () => ({
type="button"
class="btnUserMenu flex-shrink-zero"
>
<span class="material-icons more_vert"></span>
<span class="material-icons more_vert" aria-hidden="true"></span>
</button>`
});
type IProps = {
user?: Record<string, any>;
user?: UserDto;
}
const getLastSeenText = (lastActivityDate) => {
const getLastSeenText = (lastActivityDate?: string | null) => {
if (lastActivityDate) {
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
}
@ -36,16 +37,16 @@ const getLastSeenText = (lastActivityDate) => {
return '';
};
const UserCardBox: FunctionComponent<IProps> = ({ user = [] }: IProps) => {
const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
let cssClass = 'card squareCard scalableCard squareCard-scalable';
if (user.Policy.IsDisabled) {
if (user.Policy?.IsDisabled) {
cssClass += ' grayscale';
}
let imgUrl;
if (user.PrimaryImageTag) {
if (user.PrimaryImageTag && user.Id) {
imgUrl = window.ApiClient.getUserImageUrl(user.Id, {
width: 300,
tag: user.PrimaryImageTag,
@ -55,7 +56,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = [] }: IProps) => {
let imageClass = 'cardImage';
if (user.Policy.IsDisabled) {
if (user.Policy?.IsDisabled) {
imageClass += ' disabledUser';
}
@ -64,7 +65,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = [] }: IProps) => {
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>
<span class='material-icons cardImageIcon person' aria-hidden='true'></span>
</div>`;
return (

View file

@ -77,7 +77,7 @@ function getItem(cssClass, type, path, name) {
html += name;
html += '</div>';
html += '</div>';
html += '<span class="material-icons arrow_forward" style="font-size:inherit;"></span>';
html += '<span class="material-icons arrow_forward" aria-hidden="true" style="font-size:inherit;"></span>';
html += '</div>';
return html;
}
@ -116,7 +116,7 @@ function getEditorHtml(options, systemInfo) {
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 += `<button type="button" is="paper-icon-button-light" class="btnRefreshDirectories emby-input-iconbutton" title="${globalize.translate('Refresh')}"><span class="material-icons search" aria-hidden="true"></span></button>`;
}
html += '</div>';
if (!readOnlyAttribute) {
@ -235,66 +235,66 @@ function getDefaultPath(options) {
let systemInfo;
class DirectoryBrowser {
currentDialog;
currentDialog;
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');
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');
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);
}
let html = '';
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCloseDialog autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></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);
}
);
};
close = () => {
if (this.currentDialog) {
dialogHelper.close(this.currentDialog);
}
};
);
};
close = () => {
if (this.currentDialog) {
dialogHelper.close(this.currentDialog);
}
};
}
export default DirectoryBrowser;

View file

@ -100,16 +100,6 @@ import template from './displaySettings.template.html';
context.querySelector('.fldDateTimeLocale').classList.add('hide');
}
if (!browser.tizen && !browser.web0s) {
context.querySelector('.fldBackdrops').classList.remove('hide');
context.querySelector('.fldThemeSong').classList.remove('hide');
context.querySelector('.fldThemeVideo').classList.remove('hide');
} else {
context.querySelector('.fldBackdrops').classList.add('hide');
context.querySelector('.fldThemeSong').classList.add('hide');
context.querySelector('.fldThemeVideo').classList.add('hide');
}
fillThemes(context.querySelector('#selectTheme'), userSettings.theme());
fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme());

View file

@ -231,7 +231,7 @@
<div class="fieldDescription checkboxFieldDescription">${EnableDetailsBannerHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldBackdrops hide">
<div class="checkboxContainer checkboxContainer-withDescription fldBackdrops">
<label>
<input type="checkbox" is="emby-checkbox" id="chkBackdrops" />
<span>${Backdrops}</span>
@ -239,7 +239,7 @@
<div class="fieldDescription checkboxFieldDescription">${EnableBackdropsHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldThemeSong hide">
<div class="checkboxContainer checkboxContainer-withDescription fldThemeSong">
<label>
<input type="checkbox" is="emby-checkbox" id="chkThemeSong" />
<span>${ThemeSongs}</span>
@ -247,7 +247,7 @@
<div class="fieldDescription checkboxFieldDescription">${EnableThemeSongsHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldThemeVideo hide">
<div class="checkboxContainer checkboxContainer-withDescription fldThemeVideo">
<label>
<input type="checkbox" is="emby-checkbox" id="chkThemeVideo" />
<span>${ThemeVideos}</span>

View file

@ -145,7 +145,7 @@ import '../elements/emby-itemscontainer/emby-itemscontainer';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate(section.name);
html += '</h2>';
html += '<span class="material-icons chevron_right"></span>';
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>';
} else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>';

View file

@ -228,7 +228,7 @@ class FilterMenu {
let html = '';
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel hide-mouse-idle-tv" tabindex="-1"><span class="material-icons arrow_back"></span></button>';
html += '<button is="paper-icon-button-light" class="btnCancel hide-mouse-idle-tv" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">${Filters}</h3>';
html += '</div>';

View file

@ -166,7 +166,6 @@ function Guide(options) {
stopAutoRefresh();
Events.off(serverNotifications, 'TimerCreated', onTimerCreated);
Events.off(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated);
Events.off(serverNotifications, 'TimerCancelled', onTimerCancelled);
Events.off(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled);
@ -409,7 +408,7 @@ function Guide(options) {
let status;
if (item.Type === 'SeriesTimer') {
return '<span class="material-icons programIcon seriesTimerIcon fiber_smart_record"></span>';
return '<span class="material-icons programIcon seriesTimerIcon fiber_smart_record" aria-hidden="true"></span>';
} else if (item.TimerId || item.SeriesTimerId) {
status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') {
@ -420,13 +419,13 @@ function Guide(options) {
if (item.SeriesTimerId) {
if (status !== 'Cancelled') {
return '<span class="material-icons programIcon seriesTimerIcon fiber_smart_record"></span>';
return '<span class="material-icons programIcon seriesTimerIcon fiber_smart_record" aria-hidden="true"></span>';
}
return '<span class="material-icons programIcon seriesTimerIcon seriesTimerIcon-inactive fiber_smart_record"></span>';
return '<span class="material-icons programIcon seriesTimerIcon seriesTimerIcon-inactive fiber_smart_record" aria-hidden="true"></span>';
}
return '<span class="material-icons programIcon timerIcon fiber_manual_record"></span>';
return '<span class="material-icons programIcon timerIcon fiber_manual_record" aria-hidden="true"></span>';
}
function getChannelProgramsHtml(context, date, channel, programs, options, listInfo) {
@ -537,7 +536,7 @@ function Guide(options) {
html += '<div class="' + guideProgramNameClass + '">';
html += '<div class="guide-programNameCaret hide"><span class="guideProgramNameCaretIcon material-icons keyboard_arrow_left"></span></div>';
html += '<div class="guide-programNameCaret hide"><span class="guideProgramNameCaretIcon material-icons keyboard_arrow_left" aria-hidden="true"></span></div>';
html += '<div class="guideProgramNameText">' + program.Name;
@ -1057,9 +1056,6 @@ function Guide(options) {
}
}
function onSeriesTimerCreated() {
}
function onTimerCancelled(e, apiClient, data) {
const id = data.Id;
// find guide cells by timer id, remove timer icon
@ -1186,7 +1182,6 @@ function Guide(options) {
Events.trigger(self, 'load');
Events.on(serverNotifications, 'TimerCreated', onTimerCreated);
Events.on(serverNotifications, 'SeriesTimerCreated', onSeriesTimerCreated);
Events.on(serverNotifications, 'TimerCancelled', onTimerCancelled);
Events.on(serverNotifications, 'SeriesTimerCancelled', onSeriesTimerCancelled);

View file

@ -177,7 +177,7 @@ import template from './homeScreenSettings.template.html';
currentHtml += `<div class="listItem viewItem" data-viewid="${view.Id}">`;
currentHtml += '<span class="material-icons listItemIcon folder_open"></span>';
currentHtml += '<span class="material-icons listItemIcon folder_open" aria-hidden="true"></span>';
currentHtml += '<div class="listItemBody">';
@ -187,8 +187,8 @@ import template from './homeScreenSettings.template.html';
currentHtml += '</div>';
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemUp btnViewItemMove autoSize" title="${globalize.translate('Up')}"><span class="material-icons keyboard_arrow_up"></span></button>`;
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemDown btnViewItemMove autoSize" title="${globalize.translate('Down')}"><span class="material-icons keyboard_arrow_down"></span></button>`;
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemUp btnViewItemMove autoSize" title="${globalize.translate('Up')}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemDown btnViewItemMove autoSize" title="${globalize.translate('Down')}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
currentHtml += '</div>';

View file

@ -27,6 +27,7 @@
<option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option>
<option value="none">${None}</option>
</select>
@ -41,6 +42,7 @@
<option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option>
<option value="none">${None}</option>
</select>
@ -55,6 +57,7 @@
<option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option>
<option value="none">${None}</option>
</select>
@ -69,6 +72,7 @@
<option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option>
<option value="none">${None}</option>
</select>
@ -83,6 +87,7 @@
<option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option>
<option value="none">${None}</option>
</select>
@ -97,6 +102,7 @@
<option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option>
<option value="none">${None}</option>
</select>
@ -111,6 +117,7 @@
<option value="resumebook">${HeaderContinueReading}</option>
<option value="latestmedia">${HeaderLatestMedia}</option>
<option value="nextup">${NextUp}</option>
<option value="rewatching">${NextUpRewatching}</option>
<option value="livetv">${LiveTV}</option>
<option value="none">${None}</option>
</select>

View file

@ -73,8 +73,7 @@ import ServerConnections from '../ServerConnections';
return Promise.all(promises).then(function () {
return resume(elem, {
refresh: true,
returnPromise: false
refresh: true
});
});
} else {
@ -127,10 +126,7 @@ import ServerConnections from '../ServerConnections';
promises.push(elems[i].resume(options));
}
const promise = Promise.all(promises);
if (!options || options.returnPromise !== false) {
return promise;
}
return Promise.all(promises);
}
function loadSection(page, apiClient, user, userSettings, userViews, allSections, index) {
@ -151,6 +147,8 @@ import ServerConnections from '../ServerConnections';
loadLatestLiveTvRecordings(elem, true, apiClient);
} else if (section === 'nextup') {
loadNextUp(elem, apiClient, userSettings);
} else if (section === 'rewatching') {
loadNextUp(elem, apiClient, userSettings, true);
} else if (section === 'onnow' || section === 'livetv') {
return loadOnNow(elem, apiClient, user);
} else if (section === 'resumebook') {
@ -196,7 +194,7 @@ import ServerConnections from '../ServerConnections';
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
const icon = imageHelper.getLibraryIcon(item.CollectionType);
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '"></span><span class="homeLibraryText">' + item.Name + '</span></a>';
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item) + '" class="raised homeLibraryButton"><span class="material-icons homeLibraryIcon ' + icon + '" aria-hidden="true"></span><span class="homeLibraryText">' + item.Name + '</span></a>';
}
html += '</div>';
@ -287,7 +285,7 @@ import ServerConnections from '../ServerConnections';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate('LatestFromLibrary', parent.Name);
html += '</h2>';
html += '<span class="material-icons chevron_right"></span>';
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>';
} else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('LatestFromLibrary', parent.Name) + '</h2>';
@ -569,7 +567,7 @@ import ServerConnections from '../ServerConnections';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate('HeaderOnNow');
html += '</h2>';
html += '<span class="material-icons chevron_right"></span>';
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>';
} else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('HeaderOnNow') + '</h2>';
@ -600,20 +598,21 @@ import ServerConnections from '../ServerConnections';
});
}
function getNextUpFetchFn(serverId, userSettings) {
function getNextUpFetchFn(serverId, userSettings, rewatching) {
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',
Fields: 'PrimaryImageAspectRatio,DateCreated,BasicSyncInfo,Path,MediaSourceCount',
UserId: apiClient.getCurrentUserId(),
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false,
DisableFirstEpisode: false,
NextUpDateCutoff: oldestDateForNextUp.toISOString()
NextUpDateCutoff: oldestDateForNextUp.toISOString(),
Rewatching: rewatching
});
};
}
@ -639,21 +638,32 @@ import ServerConnections from '../ServerConnections';
};
}
function loadNextUp(elem, apiClient, userSettings) {
function loadNextUp(elem, apiClient, userSettings, rewatching = false) {
let html = '';
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
if (!layoutManager.tv) {
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl('nextup', {
serverId: apiClient.serverId()
serverId: apiClient.serverId(),
rewatching: rewatching
}) + '" class="button-flat button-flat-mini sectionTitleTextButton">';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate('NextUp');
if (rewatching) {
html += globalize.translate('NextUpRewatching');
} else {
html += globalize.translate('NextUp');
}
html += '</h2>';
html += '<span class="material-icons chevron_right"></span>';
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>';
} else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate('NextUp') + '</h2>';
html += '<h2 class="sectionTitle sectionTitle-cards">';
if (rewatching) {
html += globalize.translate('NextUpRewatching');
} else {
html += globalize.translate('NextUp');
}
html += '</h2>';
}
html += '</div>';
@ -673,7 +683,7 @@ import ServerConnections from '../ServerConnections';
elem.innerHTML = html;
const itemsContainer = elem.querySelector('.itemsContainer');
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings);
itemsContainer.fetchData = getNextUpFetchFn(apiClient.serverId(), userSettings, rewatching);
itemsContainer.getItemsHtml = getNextUpItemsHtmlFn(userSettings.useEpisodeImagesInNextUpAndResume());
itemsContainer.parentContainer = elem;
}

View file

@ -197,24 +197,21 @@ import { Events } from 'jellyfin-apiclient';
export function playWithPromise(elem, onErrorFn) {
try {
const promise = elem.play();
if (promise && promise.then) {
// Chrome now returns a promise
return promise.catch(function (e) {
return elem.play()
.catch((e) => {
const errorName = (e.name || '').toLowerCase();
// safari uses aborterror
if (errorName === 'notallowederror' ||
errorName === 'aborterror') {
// swallow this error because the user can still click the play button on the video element
onSuccessfulPlay(elem, onErrorFn);
return Promise.resolve();
}
return Promise.reject();
})
.then(() => {
onSuccessfulPlay(elem, onErrorFn);
return Promise.resolve();
});
} else {
onSuccessfulPlay(elem, onErrorFn);
return Promise.resolve();
}
} catch (err) {
console.error('error calling video.play: ' + err);
return Promise.reject();

View file

@ -31,11 +31,16 @@ import template from './imageDownloader.template.html';
let browsableImageStartIndex = 0;
let browsableImageType = 'Primary';
let selectedProvider;
let browsableParentId;
function getBaseRemoteOptions() {
function getBaseRemoteOptions(page) {
const options = {};
options.itemId = currentItemId;
if (page.querySelector('#chkShowParentImages').checked && browsableParentId) {
options.itemId = browsableParentId;
} else {
options.itemId = currentItemId;
}
return options;
}
@ -43,7 +48,7 @@ import template from './imageDownloader.template.html';
function reloadBrowsableImages(page, apiClient) {
loading.show();
const options = getBaseRemoteOptions();
const options = getBaseRemoteOptions(page);
options.type = browsableImageType;
options.startIndex = browsableImageStartIndex;
@ -124,8 +129,8 @@ import template from './imageDownloader.template.html';
if (showControls) {
html += '<div data-role="controlgroup" data-type="horizontal" style="display:inline-block;">';
html += '<button is="paper-icon-button-light" title="' + globalize.translate('Previous') + '" class="btnPreviousPage autoSize" ' + (startIndex ? '' : 'disabled') + '><span class="material-icons arrow_back"></span></button>';
html += '<button is="paper-icon-button-light" title="' + globalize.translate('Next') + '" class="btnNextPage autoSize" ' + (startIndex + limit >= totalRecordCount ? 'disabled' : '') + '><span class="material-icons arrow_forward"></span></button>';
html += `<button is="paper-icon-button-light" title="${globalize.translate('Previous')}" class="btnPreviousPage autoSize" ${(startIndex ? '' : 'disabled')}><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
html += `<button is="paper-icon-button-light" title="${globalize.translate('Next')}" class="btnNextPage autoSize" ${(startIndex + limit >= totalRecordCount ? 'disabled' : '')}><span class="material-icons arrow_forward" aria-hidden="true"></span></button>`;
html += '</div>';
}
@ -135,7 +140,7 @@ import template from './imageDownloader.template.html';
}
function downloadRemoteImage(page, apiClient, url, type, provider) {
const options = getBaseRemoteOptions();
const options = getBaseRemoteOptions(page);
options.Type = type;
options.ImageUrl = url;
@ -259,7 +264,7 @@ import template from './imageDownloader.template.html';
if (enableFooterButtons) {
html += '<div class="cardText cardTextCentered">';
html += '<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize" raised" title="' + globalize.translate('Download') + '"><span class="material-icons cloud_download"></span></button>';
html += `<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize" raised" title="${globalize.translate('Download')}"><span class="material-icons cloud_download" aria-hidden="true"></span></button>`;
html += '</div>';
}
@ -273,26 +278,31 @@ import template from './imageDownloader.template.html';
return html;
}
function reloadBrowsableImagesFirstPage(page, apiClient) {
browsableImageStartIndex = 0;
reloadBrowsableImages(page, apiClient);
}
function initEditor(page, apiClient) {
page.querySelector('#selectBrowsableImageType').addEventListener('change', function () {
browsableImageType = this.value;
browsableImageStartIndex = 0;
selectedProvider = null;
reloadBrowsableImages(page, apiClient);
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.querySelector('#selectImageProvider').addEventListener('change', function () {
browsableImageStartIndex = 0;
selectedProvider = this.value;
reloadBrowsableImages(page, apiClient);
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.querySelector('#chkAllLanguages').addEventListener('change', function () {
browsableImageStartIndex = 0;
reloadBrowsableImagesFirstPage(page, apiClient);
});
reloadBrowsableImages(page, apiClient);
page.querySelector('#chkShowParentImages').addEventListener('change', function () {
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.addEventListener('click', function (e) {
@ -336,6 +346,10 @@ import template from './imageDownloader.template.html';
scrollHelper.centerFocus.on(dlg, false);
}
if (browsableParentId) {
dlg.querySelector('#lblShowParentImages').classList.remove('hide');
}
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', onDialogClosed);
@ -366,7 +380,7 @@ import template from './imageDownloader.template.html';
}
}
export function show(itemId, serverId, itemType, imageType) {
export function show(itemId, serverId, itemType, imageType, parentId) {
return new Promise(function (resolve, reject) {
currentResolve = resolve;
currentReject = reject;
@ -374,6 +388,7 @@ export function show(itemId, serverId, itemType, imageType) {
browsableImageStartIndex = 0;
browsableImageType = imageType || 'Primary';
selectedProvider = null;
browsableParentId = parentId;
showEditor(itemId, serverId, itemType);
});
}

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${Search}
</h3>
@ -39,6 +39,10 @@
<input id="chkAllLanguages" type="checkbox" is="emby-checkbox" />
<span>${AllLanguages}</span>
</label>
<label id="lblShowParentImages" class="hide" style="margin: 0 0 0 1em;width:auto;">
<input id="chkShowParentImages" type="checkbox" is="emby-checkbox" />
<span>${ShowParentImages}</span>
</label>
</div>
<div class="availableImagesList vertical-wrap centered"></div>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${HeaderImageOptions}
</h3>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${HeaderUploadImage}
</h3>
@ -14,7 +14,7 @@
<h2 style="margin:0;">${HeaderAddUpdateImage}</h2>
<button is="emby-button" type="button" class="raised raised-mini btnBrowse" style="margin-left:1.5em;">
<span class="material-icons folder"></span>
<span class="material-icons folder" aria-hidden="true"></span>
<span>${Browse}</span>
</button>
</div>

View file

@ -165,21 +165,21 @@ import template from './imageeditor.template.html';
if (index > 0) {
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex - 1) + '" title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>';
} else {
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>';
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left" aria-hidden="true"></span></button>';
}
if (index < numImages - 1) {
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex + 1) + '" title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right"></span></button>';
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex + 1) + '" title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
} else {
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right"></span></button>';
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
}
} else {
if (imageProviders.length) {
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search"></span></button>';
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search" aria-hidden="true"></span></button>';
}
}
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete"></span></button>';
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
html += '</div>';
}
@ -280,7 +280,13 @@ import template from './imageeditor.template.html';
function showImageDownloader(page, imageType) {
import('../imageDownloader/imageDownloader').then((ImageDownloader) => {
ImageDownloader.show(currentItem.Id, currentItem.ServerId, currentItem.Type, imageType).then(function () {
ImageDownloader.show(
currentItem.Id,
currentItem.ServerId,
currentItem.Type,
imageType,
currentItem.Type == 'Season' ? currentItem.ParentId : null
).then(function () {
hasChanges = true;
reload(page);
});

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${HeaderEditImages}
</h3>
@ -12,10 +12,10 @@
<div class="imageEditor-buttons first-imageEditor-buttons">
<h2 style="margin:0;">${Images}</h2>
<button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;">
<span class="material-icons search"></span>
<span class="material-icons search" aria-hidden="true"></span>
</button>
<button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div id="images" class="itemsContainer vertical-wrap">
@ -27,10 +27,10 @@
<div class="imageEditor-buttons">
<h2 style="margin:0;">${Backdrops}</h2>
<button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;" data-imagetype="Backdrop">
<span class="material-icons search"></span>
<span class="material-icons search" aria-hidden="true"></span>
</button>
<button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;" data-imagetype="Backdrop">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div id="backdrops" class="itemsContainer vertical-wrap">
@ -42,10 +42,10 @@
<div class="imageEditor-buttons">
<h2 style="margin: 0;">${Screenshots}</h2>
<button type="button" is="emby-button" class="btnBrowseAllImages fab mini autoSize" style="margin-left: 1em;" data-imagetype="Screenshot">
<span class="material-icons search"></span>
<span class="material-icons search" aria-hidden="true"></span>
</button>
<button type="button" is="emby-button" class="btnOpenUploadMenu fab mini hide" style="margin-left: .5em;" data-imagetype="Screenshot">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div id="screenshots" class="itemsContainer vertical-wrap">

View file

@ -95,9 +95,12 @@ worker.addEventListener(
const elem = event.target;
requestAnimationFrame(() => {
const canvas = elem.previousSibling;
if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') {
if (elem.classList.contains('blurhashed') && canvas?.tagName === 'CANVAS') {
canvas.classList.add('lazy-hidden');
}
// HACK: Hide the content of the card padder
elem.parentNode?.querySelector('.cardPadder')?.classList.add('lazy-hidden-children');
});
elem.removeEventListener('animationend', onAnimationEnd);
}
@ -135,10 +138,13 @@ worker.addEventListener(
function emptyImageElement(elem) {
elem.removeEventListener('animationend', onAnimationEnd);
const canvas = elem.previousSibling;
if (canvas && canvas.tagName === 'CANVAS') {
if (canvas?.tagName === 'CANVAS') {
canvas.classList.remove('lazy-hidden');
}
// HACK: Unhide the content of the card padder
elem.parentNode?.querySelector('.cardPadder')?.classList.remove('lazy-hidden-children');
let url;
if (elem.tagName !== 'IMG') {

View file

@ -18,7 +18,8 @@
animation: fadein 0.1s;
}
.lazy-hidden {
.lazy-hidden,
.lazy-hidden-children * {
opacity: 0;
}

View file

@ -88,7 +88,7 @@ export function getPlayedIndicatorHtml(item) {
}
if (userData.PlayedPercentage && userData.PlayedPercentage >= 100 || (userData.Played)) {
return '<div class="playedIndicator indicator"><span class="material-icons indicatorIcon check"></span></div>';
return '<div class="playedIndicator indicator"><span class="material-icons indicatorIcon check" aria-hidden="true"></span></div>';
}
}
@ -109,7 +109,7 @@ export function getTimerIndicator(item) {
let status;
if (item.Type === 'SeriesTimer') {
return '<span class="material-icons timerIndicator indicatorIcon fiber_smart_record"></span>';
return '<span class="material-icons timerIndicator indicatorIcon fiber_smart_record" aria-hidden="true"></span>';
} else if (item.TimerId || item.SeriesTimerId) {
status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') {
@ -120,20 +120,20 @@ export function getTimerIndicator(item) {
if (item.SeriesTimerId) {
if (status !== 'Cancelled') {
return '<span class="material-icons timerIndicator indicatorIcon fiber_smart_record"></span>';
return '<span class="material-icons timerIndicator indicatorIcon fiber_smart_record" aria-hidden="true"></span>';
}
return '<span class="material-icons timerIndicator timerIndicator-inactive indicatorIcon fiber_smart_record"></span>';
return '<span class="material-icons timerIndicator timerIndicator-inactive indicatorIcon fiber_smart_record" aria-hidden="true"></span>';
}
return '<span class="material-icons timerIndicator indicatorIcon fiber_manual_record"></span>';
return '<span class="material-icons timerIndicator indicatorIcon fiber_manual_record" aria-hidden="true"></span>';
}
export function getSyncIndicator(item) {
if (item.SyncPercent === 100) {
return '<div class="syncIndicator indicator fullSyncIndicator"><span class="material-icons indicatorIcon file_download"></span></div>';
return '<div class="syncIndicator indicator fullSyncIndicator"><span class="material-icons indicatorIcon file_download" aria-hidden="true"></span></div>';
} else if (item.SyncPercent != null) {
return '<div class="syncIndicator indicator emptySyncIndicator"><span class="material-icons indicatorIcon file_download"></span></div>';
return '<div class="syncIndicator indicator emptySyncIndicator"><span class="material-icons indicatorIcon file_download" aria-hidden="true"></span></div>';
}
return '';
@ -148,7 +148,7 @@ export function getTypeIndicator(item) {
};
const icon = iconT[item.Type];
return icon ? '<div class="indicator videoIndicator"><span class="material-icons indicatorIcon ' + icon + '"></span></div>' : '';
return icon ? '<div class="indicator videoIndicator"><span class="material-icons indicatorIcon ' + icon + '" aria-hidden="true"></span></div>' : '';
}
export function getMissingIndicator(item) {

View file

@ -1,4 +1,5 @@
import browser from '../scripts/browser';
import { copy } from '../scripts/clipboard';
import globalize from '../scripts/globalize';
import actionsheet from './actionSheet/actionSheet';
import { appHost } from './apphost';
@ -366,32 +367,11 @@ import toast from './toast/toast';
break;
case 'copy-stream': {
const downloadHref = apiClient.getItemDownloadUrl(itemId);
const textAreaCopy = function () {
const textArea = document.createElement('textarea');
textArea.value = downloadHref;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
if (document.execCommand('copy')) {
toast(globalize.translate('CopyStreamURLSuccess'));
} else {
prompt(globalize.translate('CopyStreamURL'), downloadHref);
}
document.body.removeChild(textArea);
};
/* eslint-disable-next-line compat/compat */
if (navigator.clipboard === undefined) {
textAreaCopy();
} else {
/* eslint-disable-next-line compat/compat */
navigator.clipboard.writeText(downloadHref).then(function () {
toast(globalize.translate('CopyStreamURLSuccess'));
}).catch(function () {
textAreaCopy();
});
}
copy(downloadHref).then(() => {
toast(globalize.translate('CopyStreamURLSuccess'));
}).catch(() => {
prompt(globalize.translate('CopyStreamURL'), downloadHref);
});
getResolveFunction(resolve, id)();
break;
}

View file

@ -7,6 +7,9 @@
import dialogHelper from '../dialogHelper/dialogHelper';
import layoutManager from '../layoutManager';
import toast from '../toast/toast';
import { copy } from '../../scripts/clipboard';
import dom from '../../scripts/dom';
import globalize from '../../scripts/globalize';
import loading from '../loading/loading';
import '../../elements/emby-select/emby-select';
@ -19,6 +22,12 @@ import '../../assets/css/flexstyles.scss';
import ServerConnections from '../ServerConnections';
import template from './itemMediaInfo.template.html';
// Do not add extra spaces between tags - they will be copied into the result
const copyButtonHtml = layoutManager.tv ? '' :
`<button is="paper-icon-button-light" class="btnCopy" title="${globalize.translate('Copy')}" aria-label="${globalize.translate('Copy')}"
><span class="material-icons content_copy" aria-hidden="true"></span></button>`;
const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </span>';
function setMediaInfo(user, page, item) {
let html = item.MediaSources.map(version => {
return getMediaSourceHtml(user, item, version);
@ -28,12 +37,25 @@ import template from './itemMediaInfo.template.html';
}
const mediaInfoContent = page.querySelector('#mediaInfoContent');
mediaInfoContent.innerHTML = html;
for (const btn of mediaInfoContent.querySelectorAll('.btnCopy')) {
btn.addEventListener('click', () => {
const infoBlock = dom.parentWithClass(btn, 'mediaInfoStream') || dom.parentWithClass(btn, 'mediaInfoSource') || mediaInfoContent;
copy(infoBlock.textContent).then(() => {
toast(globalize.translate('Copied'));
}).catch(() => {
console.error('Could not copy text');
toast(globalize.translate('CopyFailed'));
});
});
}
}
function getMediaSourceHtml(user, item, version) {
let html = '';
let html = '<div class="mediaInfoSource">';
if (version.Name) {
html += `<div><h2 class="mediaInfoStreamType">${version.Name}</h2></div>`;
html += `<div><h2 class="mediaInfoStreamType">${version.Name}${copyButtonHtml}</h2></div>\n`;
}
if (version.Container) {
html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}<br/>`;
@ -69,7 +91,7 @@ import template from './itemMediaInfo.template.html';
}
const displayType = globalize.translate(translateString);
html += `<h2 class="mediaInfoStreamType">${displayType}</h2>`;
html += `\n<h2 class="mediaInfoStreamType">${displayType}${copyButtonHtml}</h2>\n`;
const attributes = [];
if (stream.DisplayTitle) {
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
@ -143,10 +165,8 @@ import template from './itemMediaInfo.template.html';
if (stream.NalLengthSize) {
attributes.push(createAttribute('NAL', stream.NalLengthSize));
}
if (stream.Type !== 'Video') {
if (stream.Type === 'Subtitle' || stream.Type === 'Audio') {
attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No')));
}
if (stream.Type === 'Subtitle') {
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No')));
attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No')));
}
@ -156,11 +176,12 @@ import template from './itemMediaInfo.template.html';
html += attributes.join('<br/>');
html += '</div>';
}
html += '</div>';
return html;
}
function createAttribute(label, value) {
return `<span class="mediaInfoLabel">${label}</span><span class="mediaInfoAttribute">${value}</span>`;
return `<span class="mediaInfoLabel">${label}</span>${attributeDelimiterHtml}<span class="mediaInfoAttribute">${value}</span>\n`;
}
function loadMediaInfo(itemId, serverId) {

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1">
<span class="material-icons arrow_back"></span>
<span class="material-icons arrow_back" aria-hidden="true"></span>
</button>
<h3 class="formDialogHeaderTitle">${MoreMediaInfo}</h3>
</div>

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1">
<span class="material-icons arrow_back"></span>
<span class="material-icons arrow_back" aria-hidden="true"></span>
</button>
<h3 class="formDialogHeaderTitle">${Identify}</h3>
</div>

View file

@ -69,16 +69,16 @@ import template from './libraryoptionseditor.template.html';
for (let i = 0; i < plugins.length; i++) {
const plugin = plugins[i];
html += `<div class="listItem localReaderOption sortableOption" data-pluginname="${plugin.Name}">`;
html += '<span class="listItemIcon material-icons live_tv"></span>';
html += '<span class="listItemIcon material-icons live_tv" aria-hidden="true"></span>';
html += '<div class="listItemBody">';
html += '<h3 class="listItemBodyText">';
html += plugin.Name;
html += '</h3>';
html += '</div>';
if (i > 0) {
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Up')}" class="btnSortableMoveUp btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_up"></span></button>`;
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Up')}" class="btnSortableMoveUp btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
} else if (plugins.length > 1) {
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Down')}" class="btnSortableMoveDown btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_down"></span></button>`;
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Down')}" class="btnSortableMoveDown btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
}
html += '</div>';
}
@ -132,9 +132,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>';
html += '</div>';
if (index > 0) {
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Up') + '" class="btnSortableMoveUp btnSortable" data-pluginindex="' + index + '"><span class="material-icons keyboard_arrow_up"></span></button>';
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Up') + '" class="btnSortableMoveUp btnSortable" data-pluginindex="' + index + '"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>';
} else if (plugins.length > 1) {
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Down') + '" class="btnSortableMoveDown btnSortable" data-pluginindex="' + index + '"><span class="material-icons keyboard_arrow_down"></span></button>';
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Down') + '" class="btnSortableMoveDown btnSortable" data-pluginindex="' + index + '"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>';
}
html += '</div>';
});
@ -198,9 +198,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>';
html += '</div>';
if (i > 0) {
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Up')}" class="btnSortableMoveUp btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_up"></span></button>`;
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Up')}" class="btnSortableMoveUp btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
} else if (plugins.length > 1) {
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Down')}" class="btnSortableMoveDown btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_down"></span></button>`;
html += `<button type="button" is="paper-icon-button-light" title="${globalize.translate('Down')}" class="btnSortableMoveDown btnSortable" data-pluginindex="${i}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
}
html += '</div>';
}
@ -237,9 +237,9 @@ import template from './libraryoptionseditor.template.html';
html += '</h3>';
html += '</div>';
if (i > 0) {
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Up') + '" class="btnSortableMoveUp btnSortable" data-pluginindex="' + i + '"><span class="material-icons keyboard_arrow_up"></span></button>';
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Up') + '" class="btnSortableMoveUp btnSortable" data-pluginindex="' + i + '"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>';
} else if (plugins.length > 1) {
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Down') + '" class="btnSortableMoveDown btnSortable" data-pluginindex="' + i + '"><span class="material-icons keyboard_arrow_down"></span></button>';
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('Down') + '" class="btnSortableMoveDown btnSortable" data-pluginindex="' + i + '"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>';
}
html += '</div>';
}
@ -411,7 +411,13 @@ import template from './libraryoptionseditor.template.html';
parent.querySelector('.chkEnableEmbeddedEpisodeInfosContainer').classList.add('hide');
}
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies');
if (contentType === 'tvshows' || contentType === 'movies' || contentType === 'musicvideos' || contentType === 'mixed') {
parent.querySelector('.fldAllowEmbeddedSubtitlesContainer').classList.remove('hide');
} else {
parent.querySelector('.fldAllowEmbeddedSubtitlesContainer').classList.add('hide');
}
parent.querySelector('.chkAutomaticallyAddToCollectionContainer').classList.toggle('hide', contentType !== 'movies' && contentType !== 'mixed');
return populateMetadataSettings(parent, contentType);
}
@ -509,6 +515,7 @@ import template from './libraryoptionseditor.template.html';
AutomaticRefreshIntervalDays: parseInt(parent.querySelector('#selectAutoRefreshInterval').value),
EnableEmbeddedTitles: parent.querySelector('#chkEnableEmbeddedTitles').checked,
EnableEmbeddedEpisodeInfos: parent.querySelector('#chkEnableEmbeddedEpisodeInfos').checked,
AllowEmbeddedSubtitles: parent.querySelector('#selectAllowEmbeddedSubtitles').value,
SkipSubtitlesIfEmbeddedSubtitlesPresent: parent.querySelector('#chkSkipIfGraphicalSubsPresent').checked,
SkipSubtitlesIfAudioTrackMatches: parent.querySelector('#chkSkipIfAudioTrackPresent').checked,
SaveSubtitlesWithMedia: parent.querySelector('#chkSaveSubtitlesLocally').checked,
@ -560,7 +567,8 @@ import template from './libraryoptionseditor.template.html';
parent.querySelector('#chkSaveLocal').checked = options.SaveLocalMetadata;
parent.querySelector('.chkAutomaticallyGroupSeries').checked = options.EnableAutomaticSeriesGrouping;
parent.querySelector('#chkEnableEmbeddedTitles').checked = options.EnableEmbeddedTitles;
parent.querySelector('#chkEnableEmbeddedEpisodeInfos').checked = options.EnableEmbeddedEpisodeInfos;
parent.querySelector('#chkEnableEmbeddedEpisodeInfos').value = options.EnableEmbeddedEpisodeInfos;
parent.querySelector('#selectAllowEmbeddedSubtitles').value = options.AllowEmbeddedSubtitles;
parent.querySelector('#chkSkipIfGraphicalSubsPresent').checked = options.SkipSubtitlesIfEmbeddedSubtitlesPresent;
parent.querySelector('#chkSaveSubtitlesLocally').checked = options.SaveSubtitlesWithMedia;
parent.querySelector('#chkSkipIfAudioTrackPresent').checked = options.SkipSubtitlesIfAudioTrackMatches;

View file

@ -30,6 +30,15 @@
</label>
<div class="fieldDescription checkboxFieldDescription">${PreferEmbeddedEpisodeInfosOverFileNamesHelp}</div>
</div>
<div class="selectContainer fldAllowEmbeddedSubtitlesContainer hide advanced" style="margin: 2em 0;">
<select is="emby-select" id="selectAllowEmbeddedSubtitles" label="${AllowEmbeddedSubtitles}">
<option value="AllowAll">${AllowEmbeddedSubtitlesAllowAllOption}</option>
<option value="AllowText">${AllowEmbeddedSubtitlesAllowTextOption}</option>
<option value="AllowImage">${AllowEmbeddedSubtitlesAllowImageOption}</option>
<option value="AllowNone">${AllowEmbeddedSubtitlesAllowNoneOption}</option>
</select>
<div class="fieldDescription">${AllowEmbeddedSubtitlesHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription advanced">
<label>

View file

@ -166,7 +166,7 @@ import ServerConnections from '../ServerConnections';
for (let i = 0, length = options.rightButtons.length; i < length; i++) {
const button = options.rightButtons[i];
html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}"></span></button>`;
html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}" aria-hidden="true"></span></button>`;
}
return html;
@ -257,7 +257,7 @@ import ServerConnections from '../ServerConnections';
}
if (!clickEntireItem && options.dragHandle) {
html += '<span class="listViewDragHandle material-icons listItemIcon listItemIcon-transparent drag_handle"></span>';
html += '<span class="listViewDragHandle material-icons listItemIcon listItemIcon-transparent drag_handle" aria-hidden="true"></span>';
}
if (options.image !== false) {
@ -282,6 +282,11 @@ import ServerConnections from '../ServerConnections';
html += '<div class="' + imageClass + ' cardImageContainer ' + cardBuilder.getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options);
}
const mediaSourceCount = item.MediaSourceCount || 1;
if (mediaSourceCount > 1 && options.disableIndicators !== true) {
html += '<div class="mediaSourceIndicator">' + mediaSourceCount + '</div>';
}
let indicatorsHtml = '';
indicatorsHtml += indicators.getPlayedIndicatorHtml(item);
@ -290,7 +295,7 @@ import ServerConnections from '../ServerConnections';
}
if (playOnImageClick) {
html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow"></span></button>';
html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow" aria-hidden="true"></span></button>';
}
const progressHtml = indicators.getProgressBarHtml(item, {
@ -450,11 +455,11 @@ import ServerConnections from '../ServerConnections';
if (!clickEntireItem) {
if (options.addToListButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add"></span></button>';
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add" aria-hidden="true"></span></button>';
}
if (options.infoButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline"></span></button>';
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline" aria-hidden="true"></span></button>';
}
if (options.rightButtons) {
@ -466,16 +471,16 @@ import ServerConnections from '../ServerConnections';
const likes = userData.Likes == null ? '' : userData.Likes;
if (itemHelper.canMarkPlayed(item) && options.enablePlayedButton !== false) {
html += '<button is="emby-playstatebutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons check"></span></button>';
html += '<button is="emby-playstatebutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons check" aria-hidden="true"></span></button>';
}
if (itemHelper.canRate(item) && options.enableRatingButton !== false) {
html += '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite"></span></button>';
html += '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
}
}
if (options.moreButton !== false) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert"></span></button>';
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
}
}
html += '</div>';

View file

@ -164,6 +164,10 @@
padding: 0.2em;
}
.listItemImage .cardImageIcon {
font-size: 3em;
}
@media all and (max-width: 64em) {
.listItemImage-large {
width: 22vw;

View file

@ -73,7 +73,7 @@ import template from './mediaLibraryCreator.template.html';
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
const value = this.value;
const dlg = $(this).parents('.dialog')[0];
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value == 'mixed' ? '' : value);
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
if (value) {
dlg.querySelector('.libraryOptions').classList.remove('hide');
@ -87,12 +87,11 @@ import template from './mediaLibraryCreator.template.html';
if (index != -1) {
const name = this.options[index].innerHTML.replace('*', '').replace('&amp;', '&');
$('#txtValue', dlg).val(name);
const folderOption = collectionTypeOptions.filter(i => {
return i.value == value;
})[0];
$('.collectionTypeFieldDescription', dlg).html(folderOption.message || '');
}
}
const folderOption = collectionTypeOptions.find(i => i.value === value);
$('.collectionTypeFieldDescription', dlg).html(folderOption?.message || '');
});
page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
page.querySelector('.btnSubmit').addEventListener('click', onAddLibrary);
@ -128,7 +127,7 @@ import template from './mediaLibraryCreator.template.html';
}
html += '</div>';
html += `<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle"></span></button>`;
html += `<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
html += '</div>';
return html;
}

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">${ButtonAddMediaLibrary}</h3>
</div>
@ -20,7 +20,7 @@
<div style="display: flex; align-items: center;">
<h1 style="margin: .5em 0;">${Folders}</h1>
<button is="emby-button" type="button" class="fab btnAddFolder submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div class="paperList folderList hide" style="margin-bottom:2em;"></div>

View file

@ -119,7 +119,7 @@ import template from './mediaLibraryEditor.template.html';
}
html += '</div>';
html += `<button type="button" is="paper-icon-button-light" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle"></span></button>`;
html += `<button type="button" is="paper-icon-button-light" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
html += '</div>';
return html;
}

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"></h3>
</div>
@ -13,7 +13,7 @@
<div style="display: flex; align-items: center;">
<h1 style="margin: .5em 0;">${Folders}</h1>
<button is="emby-button" type="button" class="fab btnAddFolder submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
</div>
<div class="paperList folderList" style="margin-bottom:2em;"></div>

View file

@ -13,7 +13,7 @@ import '../../elements/emby-button/emby-button';
let status;
if (item.Type === 'SeriesTimer') {
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_smart_record"></span>';
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_smart_record" aria-hidden="true"></span>';
} else if (item.TimerId || item.SeriesTimerId) {
status = item.Status || 'Cancelled';
} else if (item.Type === 'Timer') {
@ -24,13 +24,13 @@ import '../../elements/emby-button/emby-button';
if (item.SeriesTimerId) {
if (status !== 'Cancelled') {
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_smart_record"></span>';
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_smart_record" aria-hidden="true"></span>';
}
return '<span class="material-icons mediaInfoItem mediaInfoIconItem fiber_smart_record"></span>';
return '<span class="material-icons mediaInfoItem mediaInfoIconItem fiber_smart_record" aria-hidden="true"></span>';
}
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_manual_record"></span>';
return '<span class="material-icons mediaInfoItem mediaInfoIconItem mediaInfoTimerIcon fiber_manual_record" aria-hidden="true"></span>';
}
function getProgramInfoHtml(item, options) {
@ -358,7 +358,7 @@ import '../../elements/emby-button/emby-button';
if (item.CommunityRating) {
html += '<div class="starRatingContainer mediaInfoItem">';
html += '<span class="material-icons starIcon star"></span>';
html += '<span class="material-icons starIcon star" aria-hidden="true"></span>';
html += item.CommunityRating.toFixed(1);
html += '</div>';
}

View file

@ -460,7 +460,7 @@ import template from './metadataEditor.template.html';
html += '</div>';
if (formatString) {
html += '<button type="button" is="paper-icon-button-light" class="btnOpenExternalId align-self-flex-end" data-fieldid="' + id + '"><span class="material-icons open_in_browser"></span></button>';
html += '<button type="button" is="paper-icon-button-light" class="btnOpenExternalId align-self-flex-end" data-fieldid="' + id + '"><span class="material-icons open_in_browser" aria-hidden="true"></span></button>';
}
html += '</div>';
@ -898,7 +898,7 @@ import template from './metadataEditor.template.html';
for (let i = 0; i < items.length; i++) {
html += '<div class="listItem">';
html += '<span class="material-icons listItemIcon live_tv" style="background-color:#333;"></span>';
html += '<span class="material-icons listItemIcon live_tv" aria-hidden="true" style="background-color:#333;"></span>';
html += '<div class="listItemBody">';
@ -908,7 +908,7 @@ import template from './metadataEditor.template.html';
html += '</div>';
html += '<button type="button" is="paper-icon-button-light" data-index="' + i + '" class="btnRemoveFromEditorList autoSize"><span class="material-icons delete"></span></button>';
html += '<button type="button" is="paper-icon-button-light" data-index="' + i + '" class="btnRemoveFromEditorList autoSize"><span class="material-icons delete" aria-hidden="true"></span></button>';
html += '</div>';
}
@ -945,7 +945,7 @@ import template from './metadataEditor.template.html';
html += '</button>';
html += '</div>';
html += '<button type="button" is="paper-icon-button-light" data-index="' + i + '" class="btnDeletePerson autoSize"><span class="material-icons delete"></span></button>';
html += '<button type="button" is="paper-icon-button-light" data-index="' + i + '" class="btnDeletePerson autoSize"><span class="material-icons delete" aria-hidden="true"></span></button>';
html += '</div>';
}

View file

@ -1,18 +1,18 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel btnBack autoSize hide" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel btnBack autoSize hide" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${Edit}
</h3>
<div style="margin-left: auto;" class="flex align-items-center justify-content-center">
<button is="emby-button" type="button" class="btnHeaderSave button-accent-flat button-flat hide" tabindex="-1">
<span class="material-icons check"></span>
<span class="material-icons check" aria-hidden="true"></span>
<span>${Save}</span>
</button>
<button is="paper-icon-button-light" class="btnMore autoSize" tabindex="-1">
<span class="material-icons more_vert"></span>
<span class="material-icons more_vert" aria-hidden="true"></span>
</button>
<button is="paper-icon-button-light" class="btnCancel btnClose autoSize" tabindex="-1">
<span class="material-icons close"></span>
<span class="material-icons close" aria-hidden="true"></span>
</button>
</div>
</div>
@ -195,7 +195,7 @@
${Genres}
</h2>
<button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
<div class="paperList" id="listGenres"></div>
</div>
@ -204,7 +204,7 @@
${People}
</h2>
<button is="emby-button" type="button" id="btnAddPerson" class="fab btnAddPerson" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
<div id="peopleList" class="paperList">
</div>
@ -214,7 +214,7 @@
${Studios}
</h2>
<button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
<div class="paperList" id="listStudios"></div>
</div>
@ -223,7 +223,7 @@
${Tags}
</h2>
<button is="emby-button" type="button" class="fab btnAddTextItem submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
<span class="material-icons add" aria-hidden="true"></span>
</button>
<div class="paperList" id="listTags"></div>
</div>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${Edit}
</h3>

View file

@ -125,11 +125,11 @@ import itemHelper from '../itemHelper';
let html = '';
html += '<button is="paper-icon-button-light" class="btnCloseSelectionPanel autoSize"><span class="material-icons close"></span></button>';
html += '<button is="paper-icon-button-light" class="btnCloseSelectionPanel autoSize"><span class="material-icons close" aria-hidden="true"></span></button>';
html += '<h1 class="itemSelectionCount"></h1>';
const moreIcon = 'more_vert';
html += `<button is="paper-icon-button-light" class="btnSelectionPanelOptions autoSize" style="margin-left:auto;"><span class="material-icons ${moreIcon}"></span></button>`;
html += `<button is="paper-icon-button-light" class="btnSelectionPanelOptions autoSize" style="margin-left:auto;"><span class="material-icons ${moreIcon}" aria-hidden="true"></span></button>`;
selectionCommandsPanel.innerHTML = html;

View file

@ -59,13 +59,13 @@ import { appRouter } from '../appRouter';
// The onclicks are needed due to the return false above
html += '<div class="nowPlayingBarCenter">';
html += '<button is="paper-icon-button-light" class="previousTrackButton mediaButton"><span class="material-icons skip_previous"></span></button>';
html += '<button is="paper-icon-button-light" class="previousTrackButton mediaButton"><span class="material-icons skip_previous" aria-hidden="true"></span></button>';
html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause"></span></button>';
html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause" aria-hidden="true"></span></button>';
html += '<button is="paper-icon-button-light" class="stopButton mediaButton"><span class="material-icons stop"></span></button>';
html += '<button is="paper-icon-button-light" class="stopButton mediaButton"><span class="material-icons stop" aria-hidden="true"></span></button>';
if (!layoutManager.mobile) {
html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next"></span></button>';
html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next" aria-hidden="true"></span></button>';
}
html += '<div class="nowPlayingBarCurrentTime"></div>';
@ -73,23 +73,23 @@ import { appRouter } from '../appRouter';
html += '<div class="nowPlayingBarRight">';
html += '<button is="paper-icon-button-light" class="muteButton mediaButton"><span class="material-icons volume_up"></span></button>';
html += '<button is="paper-icon-button-light" class="muteButton mediaButton"><span class="material-icons volume_up" aria-hidden="true"></span></button>';
html += '<div class="sliderContainer nowPlayingBarVolumeSliderContainer hide" style="width:9em;vertical-align:middle;display:inline-flex;">';
html += '<input type="range" is="emby-slider" pin step="1" min="0" max="100" value="0" class="slider-medium-thumb nowPlayingBarVolumeSlider"/>';
html += '</div>';
html += '<button is="paper-icon-button-light" class="toggleRepeatButton mediaButton"><span class="material-icons repeat"></span></button>';
html += '<button is="paper-icon-button-light" class="btnShuffleQueue mediaButton"><span class="material-icons shuffle"></span></button>';
html += '<button is="paper-icon-button-light" class="toggleRepeatButton mediaButton"><span class="material-icons repeat" aria-hidden="true"></span></button>';
html += '<button is="paper-icon-button-light" class="btnShuffleQueue mediaButton"><span class="material-icons shuffle" aria-hidden="true"></span></button>';
html += '<div class="nowPlayingBarUserDataButtons">';
html += '</div>';
html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause"></span></button>';
html += '<button is="paper-icon-button-light" class="playPauseButton mediaButton"><span class="material-icons pause" aria-hidden="true"></span></button>';
if (layoutManager.mobile) {
html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next"></span></button>';
html += '<button is="paper-icon-button-light" class="nextTrackButton mediaButton"><span class="material-icons skip_next" aria-hidden="true"></span></button>';
} else {
html += '<button is="paper-icon-button-light" class="btnToggleContextMenu mediaButton"><span class="material-icons more_vert"></span></button>';
html += '<button is="paper-icon-button-light" class="btnToggleContextMenu mediaButton"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
}
html += '</div>';
@ -569,7 +569,7 @@ import { appRouter } from '../appRouter';
});
});
}
nowPlayingUserData.innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton mediaButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite"></span></button>';
nowPlayingUserData.innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton mediaButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
});
}
} else {
@ -658,6 +658,11 @@ import { appRouter } from '../appRouter';
}
function onStateChanged(event, state) {
if (event.type === 'init') {
// skip non-ready state
return;
}
console.debug('nowplaying event: ' + event.type);
const player = this;
@ -728,10 +733,10 @@ import { appRouter } from '../appRouter';
updatePlayerVolumeState(player.isMuted(), player.getVolume());
}
function refreshFromPlayer(player) {
function refreshFromPlayer(player, type) {
const state = playbackManager.getPlayerState(player);
onStateChanged.call(player, { type: 'init' }, state);
onStateChanged.call(player, { type }, state);
}
function bindToPlayer(player) {
@ -747,7 +752,7 @@ import { appRouter } from '../appRouter';
return;
}
refreshFromPlayer(player);
refreshFromPlayer(player, 'init');
Events.on(player, 'playbackstart', onPlaybackStart);
Events.on(player, 'statechange', onPlaybackStart);
@ -775,7 +780,7 @@ import { appRouter } from '../appRouter';
} else if (!isVisibilityAllowed) {
isVisibilityAllowed = true;
if (currentPlayer) {
refreshFromPlayer(currentPlayer);
refreshFromPlayer(currentPlayer, 'refresh');
} else {
hideNowPlayingBar();
}

View file

@ -22,9 +22,9 @@ type ItemsArr = {
}
const NewUserPage: FunctionComponent = () => {
const [ channelsItems, setChannelsItems ] = useState([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState([]);
const element = useRef(null);
const [ channelsItems, setChannelsItems ] = useState<ItemsArr[]>([]);
const [ mediaFoldersItems, setMediaFoldersItems ] = useState<ItemsArr[]>([]);
const element = useRef<HTMLDivElement>(null);
const getItemsResult = (items: ItemsArr[]) => {
return items.map(item =>
@ -36,33 +36,54 @@ const NewUserPage: FunctionComponent = () => {
};
const loadMediaFolders = useCallback((result) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const mediaFolders = getItemsResult(result);
setMediaFoldersItems(mediaFolders);
const folderAccess = element?.current?.querySelector('.folderAccess');
const folderAccess = page.querySelector('.folderAccess') as HTMLDivElement;
folderAccess.dispatchEvent(new CustomEvent('create'));
element.current.querySelector('.chkEnableAllFolders').checked = false;
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked = false;
}, []);
const loadChannels = useCallback((result) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const channels = getItemsResult(result);
setChannelsItems(channels);
const channelAccess = element?.current?.querySelector('.channelAccess');
const channelAccess = page.querySelector('.channelAccess') as HTMLDivElement;
channelAccess.dispatchEvent(new CustomEvent('create'));
const channelAccessContainer = element?.current?.querySelector('.channelAccessContainer');
const channelAccessContainer = page.querySelector('.channelAccessContainer') as HTMLDivElement;
channels.length ? channelAccessContainer.classList.remove('hide') : channelAccessContainer.classList.add('hide');
element.current.querySelector('.chkEnableAllChannels').checked = false;
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked = false;
}, []);
const loadUser = useCallback(() => {
element.current.querySelector('#txtUsername').value = '';
element.current.querySelector('#txtPassword').value = '';
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
(page.querySelector('#txtUsername') as HTMLInputElement).value = '';
(page.querySelector('#txtPassword') as HTMLInputElement).value = '';
loading.show();
const promiseFolders = window.ApiClient.getJSON(window.ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
@ -76,29 +97,44 @@ const NewUserPage: FunctionComponent = () => {
}, [loadChannels, loadMediaFolders]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadUser();
const saveUser = () => {
const userInput: userInput = {};
userInput.Name = element?.current?.querySelector('#txtUsername').value;
userInput.Password = element?.current?.querySelector('#txtPassword').value;
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
window.ApiClient.createUser(userInput).then(function (user) {
user.Policy.EnableAllFolders = element?.current?.querySelector('.chkEnableAllFolders').checked;
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
user.Policy.EnabledFolders = [];
if (!user.Policy.EnableAllFolders) {
user.Policy.EnabledFolders = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkFolder'), function (i) {
user.Policy.EnabledFolders = Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked;
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = [];
if (!user.Policy.EnableAllChannels) {
user.Policy.EnabledChannels = Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (i) {
user.Policy.EnabledChannels = Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
@ -114,7 +150,7 @@ const NewUserPage: FunctionComponent = () => {
});
};
const onSubmit = (e) => {
const onSubmit = (e: Event) => {
loading.show();
saveUser();
e.preventDefault();
@ -122,19 +158,19 @@ const NewUserPage: FunctionComponent = () => {
return false;
};
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) {
const channelAccessListContainer = element?.current?.querySelector('.channelAccessListContainer');
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const channelAccessListContainer = page.querySelector('.channelAccessListContainer') as HTMLDivElement;
this.checked ? channelAccessListContainer.classList.add('hide') : channelAccessListContainer.classList.remove('hide');
});
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) {
const folderAccessListContainer = element?.current?.querySelector('.folderAccessListContainer');
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
const folderAccessListContainer = page.querySelector('.folderAccessListContainer') as HTMLDivElement;
this.checked ? folderAccessListContainer.classList.add('hide') : folderAccessListContainer.classList.remove('hide');
});
element?.current?.querySelector('.newUserProfileForm').addEventListener('submit', onSubmit);
(page.querySelector('.newUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
element?.current?.querySelector('.button-cancel').addEventListener('click', function() {
(page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadUser]);

View file

@ -12,7 +12,7 @@ type SearchProps = {
};
const SearchPage: FunctionComponent<SearchProps> = ({ serverId, parentId, collectionType }: SearchProps) => {
const [ query, setQuery ] = useState(null);
const [ query, setQuery ] = useState<string>();
return (
<>

View file

@ -1,3 +1,4 @@
import { SyncPlayUserAccessType, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import Dashboard from '../../scripts/clientUtils';
import globalize from '../../scripts/globalize';
@ -23,16 +24,16 @@ type ItemsArr = {
const UserEditPage: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState([]);
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ItemsArr[]>([]);
const [ authProviders, setAuthProviders ] = useState([]);
const [ passwordResetProviders, setPasswordResetProviders ] = useState([]);
const [ authenticationProviderId, setAuthenticationProviderId ] = useState('');
const [ passwordResetProviderId, setPasswordResetProviderId ] = useState('');
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
const triggerChange = (select) => {
const triggerChange = (select: HTMLInputElement) => {
const evt = document.createEvent('HTMLEvents');
evt.initEvent('change', false, true);
select.dispatchEvent(evt);
@ -44,7 +45,14 @@ const UserEditPage: FunctionComponent = () => {
};
const loadAuthProviders = useCallback((user, providers) => {
const fldSelectLoginProvider = element?.current?.querySelector('.fldSelectLoginProvider');
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide');
setAuthProviders(providers);
@ -54,7 +62,14 @@ const UserEditPage: FunctionComponent = () => {
}, []);
const loadPasswordResetProviders = useCallback((user, providers) => {
const fldSelectPasswordResetProvider = element?.current?.querySelector('.fldSelectPasswordResetProvider');
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide');
setPasswordResetProviders(providers);
@ -64,6 +79,13 @@ const UserEditPage: FunctionComponent = () => {
}, []);
const loadDeleteFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Channels', {
SupportsMediaDeletion: true
})).then(function (channelsResult) {
@ -93,13 +115,20 @@ const UserEditPage: FunctionComponent = () => {
setDeleteFoldersAccess(itemsArr);
const chkEnableDeleteAllFolders = element.current.querySelector('.chkEnableDeleteAllFolders');
const chkEnableDeleteAllFolders = page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement;
chkEnableDeleteAllFolders.checked = user.Policy.EnableContentDeletion;
triggerChange(chkEnableDeleteAllFolders);
});
}, []);
const loadUser = useCallback((user) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
window.ApiClient.getJSON(window.ApiClient.getUrl('Auth/Providers')).then(function (providers) {
loadAuthProviders(user, providers);
});
@ -112,37 +141,38 @@ const UserEditPage: FunctionComponent = () => {
loadDeleteFolders(user, folders.Items);
});
const disabledUserBanner = element?.current?.querySelector('.disabledUserBanner');
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide');
const txtUserName = element?.current?.querySelector('#txtUserName');
txtUserName.disabled = '';
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
txtUserName.disabled = false;
txtUserName.removeAttribute('disabled');
const lnkEditUserPreferences = element?.current?.querySelector('.lnkEditUserPreferences');
const lnkEditUserPreferences = page.querySelector('.lnkEditUserPreferences') as HTMLDivElement;
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';
(page.querySelector('#txtUserName') as HTMLInputElement).value = user.Name;
(page.querySelector('.chkIsAdmin') as HTMLInputElement).checked = user.Policy.IsAdministrator;
(page.querySelector('.chkDisabled') as HTMLInputElement).checked = user.Policy.IsDisabled;
(page.querySelector('.chkIsHidden') as HTMLInputElement).checked = user.Policy.IsHidden;
(page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked = user.Policy.EnableSharedDeviceControl;
(page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked = user.Policy.EnableRemoteControlOfOtherUsers;
(page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked = user.Policy.EnableContentDownloading;
(page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked = user.Policy.EnableLiveTvManagement;
(page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked = user.Policy.EnableLiveTvAccess;
(page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked = user.Policy.EnableMediaPlayback;
(page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableAudioPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked = user.Policy.EnableVideoPlaybackTranscoding;
(page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked = user.Policy.EnablePlaybackRemuxing;
(page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked = user.Policy.ForceRemoteSourceTranscoding;
(page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked = user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess;
(page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value = user.Policy.RemoteClientBitrateLimit > 0 ?
(user.Policy.RemoteClientBitrateLimit / 1e6).toLocaleString(undefined, {maximumFractionDigits: 6}) : '';
(page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value = user.Policy.LoginAttemptsBeforeLockout || '0';
(page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value = user.Policy.MaxActiveSessions || '0';
if (window.ApiClient.isMinServerVersion('10.6.0')) {
element.current.querySelector('#selectSyncPlayAccess').value = user.Policy.SyncPlayAccess;
(page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value = user.Policy.SyncPlayAccess;
}
loading.hide();
}, [loadAuthProviders, loadPasswordResetProviders, loadDeleteFolders ]);
@ -155,6 +185,13 @@ const UserEditPage: FunctionComponent = () => {
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
function onSaveComplete() {
@ -163,44 +200,52 @@ const UserEditPage: FunctionComponent = () => {
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) {
const saveUser = (user: UserDto) => {
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
user.Policy.IsAdministrator = (page.querySelector('.chkIsAdmin') as HTMLInputElement).checked;
user.Policy.IsHidden = (page.querySelector('.chkIsHidden') as HTMLInputElement).checked;
user.Policy.IsDisabled = (page.querySelector('.chkDisabled') as HTMLInputElement).checked;
user.Policy.EnableRemoteControlOfOtherUsers = (page.querySelector('.chkEnableRemoteControlOtherUsers') as HTMLInputElement).checked;
user.Policy.EnableLiveTvManagement = (page.querySelector('.chkManageLiveTv') as HTMLInputElement).checked;
user.Policy.EnableLiveTvAccess = (page.querySelector('.chkEnableLiveTvAccess') as HTMLInputElement).checked;
user.Policy.EnableSharedDeviceControl = (page.querySelector('.chkRemoteControlSharedDevices') as HTMLInputElement).checked;
user.Policy.EnableMediaPlayback = (page.querySelector('.chkEnableMediaPlayback') as HTMLInputElement).checked;
user.Policy.EnableAudioPlaybackTranscoding = (page.querySelector('.chkEnableAudioPlaybackTranscoding') as HTMLInputElement).checked;
user.Policy.EnableVideoPlaybackTranscoding = (page.querySelector('.chkEnableVideoPlaybackTranscoding') as HTMLInputElement).checked;
user.Policy.EnablePlaybackRemuxing = (page.querySelector('.chkEnableVideoPlaybackRemuxing') as HTMLInputElement).checked;
user.Policy.ForceRemoteSourceTranscoding = (page.querySelector('.chkForceRemoteSourceTranscoding') as HTMLInputElement).checked;
user.Policy.EnableContentDownloading = (page.querySelector('.chkEnableDownloading') as HTMLInputElement).checked;
user.Policy.EnableRemoteAccess = (page.querySelector('.chkRemoteAccess') as HTMLInputElement).checked;
user.Policy.RemoteClientBitrateLimit = Math.floor(1e6 * parseFloat((page.querySelector('#txtRemoteClientBitrateLimit') as HTMLInputElement).value || '0'));
user.Policy.LoginAttemptsBeforeLockout = parseInt((page.querySelector('#txtLoginAttemptsBeforeLockout') as HTMLInputElement).value || '0');
user.Policy.MaxActiveSessions = parseInt((page.querySelector('#txtMaxActiveSessions') as HTMLInputElement).value || '0');
user.Policy.AuthenticationProviderId = (page.querySelector('.selectLoginProvider') as HTMLInputElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('.selectPasswordResetProvider') as HTMLInputElement).value;
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
if (window.ApiClient.isMinServerVersion('10.6.0')) {
user.Policy.SyncPlayAccess = element?.current?.querySelector('#selectSyncPlayAccess').value;
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLInputElement).value as SyncPlayUserAccessType;
}
window.ApiClient.updateUser(user).then(function () {
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
onSaveComplete();
});
});
};
const onSubmit = (e) => {
const onSubmit = (e: Event) => {
loading.show();
getUser().then(function (result) {
saveUser(result);
@ -210,22 +255,22 @@ const UserEditPage: FunctionComponent = () => {
return false;
};
element?.current?.querySelector('.chkEnableDeleteAllFolders').addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
if (this.checked) {
element?.current?.querySelector('.deleteAccess').classList.add('hide');
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide');
} else {
element?.current?.querySelector('.deleteAccess').classList.remove('hide');
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide');
}
});
window.ApiClient.getServerConfiguration().then(function (config) {
const fldRemoteAccess = element?.current?.querySelector('.fldRemoteAccess');
const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement;
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
});
element?.current?.querySelector('.editUserProfileForm').addEventListener('submit', onSubmit);
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);
element?.current?.querySelector('.button-cancel').addEventListener('click', function() {
(page.querySelector('.button-cancel') as HTMLButtonElement).addEventListener('click', function() {
window.history.back();
});
}, [loadData]);

View file

@ -1,3 +1,4 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import loading from '../loading/loading';
@ -20,19 +21,26 @@ type ItemsArr = {
const UserLibraryAccessPage: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [channelsItems, setChannelsItems] = useState([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState([]);
const [devicesItems, setDevicesItems] = useState([]);
const [channelsItems, setChannelsItems] = useState<ItemsArr[]>([]);
const [mediaFoldersItems, setMediaFoldersItems] = useState<ItemsArr[]>([]);
const [devicesItems, setDevicesItems] = useState<ItemsArr[]>([]);
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
const triggerChange = (select) => {
const triggerChange = (select: HTMLInputElement) => {
const evt = document.createEvent('HTMLEvents');
evt.initEvent('change', false, true);
select.dispatchEvent(evt);
};
const loadMediaFolders = useCallback((user, mediaFolders) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const folder of mediaFolders) {
@ -47,12 +55,19 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setMediaFoldersItems(itemsArr);
const chkEnableAllFolders = element.current.querySelector('.chkEnableAllFolders');
const chkEnableAllFolders = page.querySelector('.chkEnableAllFolders') as HTMLInputElement;
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
triggerChange(chkEnableAllFolders);
}, []);
const loadChannels = useCallback((user, channels) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const folder of channels) {
@ -68,17 +83,24 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setChannelsItems(itemsArr);
if (channels.length) {
element?.current?.querySelector('.channelAccessContainer').classList.remove('hide');
(page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.remove('hide');
} else {
element?.current?.querySelector('.channelAccessContainer').classList.add('hide');
(page.querySelector('.channelAccessContainer') as HTMLDivElement).classList.add('hide');
}
const chkEnableAllChannels = element.current.querySelector('.chkEnableAllChannels');
const chkEnableAllChannels = page.querySelector('.chkEnableAllChannels') as HTMLInputElement;
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
triggerChange(chkEnableAllChannels);
}, []);
const loadDevices = useCallback((user, devices) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const itemsArr: ItemsArr[] = [];
for (const device of devices) {
@ -94,14 +116,14 @@ const UserLibraryAccessPage: FunctionComponent = () => {
setDevicesItems(itemsArr);
const chkEnableAllDevices = element.current.querySelector('.chkEnableAllDevices');
const chkEnableAllDevices = page.querySelector('.chkEnableAllDevices') as HTMLInputElement;
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
triggerChange(chkEnableAllDevices);
if (user.Policy.IsAdministrator) {
element?.current?.querySelector('.deviceAccessContainer').classList.add('hide');
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.add('hide');
} else {
element?.current?.querySelector('.deviceAccessContainer').classList.remove('hide');
(page.querySelector('.deviceAccessContainer') as HTMLDivElement).classList.remove('hide');
}
}, []);
@ -129,9 +151,16 @@ const UserLibraryAccessPage: FunctionComponent = () => {
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const onSubmit = (e) => {
const onSubmit = (e: Event) => {
loading.show();
const userId = appRouter.param('userId');
window.ApiClient.getUser(userId).then(function (result) {
@ -142,21 +171,29 @@ const UserLibraryAccessPage: FunctionComponent = () => {
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) {
const saveUser = (user: UserDto) => {
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllChannels = element?.current?.querySelector('.chkEnableAllChannels').checked;
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkChannel'), function (c) {
user.Policy.EnableAllChannels = (page.querySelector('.chkEnableAllChannels') as HTMLInputElement).checked;
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkChannel'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllDevices = element?.current?.querySelector('.chkEnableAllDevices').checked;
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(element?.current?.querySelectorAll('.chkDevice'), function (c) {
user.Policy.EnableAllDevices = (page.querySelector('.chkEnableAllDevices') as HTMLInputElement).checked;
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkDevice'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
@ -173,19 +210,19 @@ const UserLibraryAccessPage: FunctionComponent = () => {
toast(globalize.translate('SettingsSaved'));
};
element?.current?.querySelector('.chkEnableAllDevices').addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.deviceAccessListContainer').classList.toggle('hide', this.checked);
(page.querySelector('.chkEnableAllDevices') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.deviceAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
});
element?.current?.querySelector('.chkEnableAllChannels').addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.channelAccessListContainer').classList.toggle('hide', this.checked);
(page.querySelector('.chkEnableAllChannels') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.channelAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
});
element?.current?.querySelector('.chkEnableAllFolders').addEventListener('change', function (this: HTMLInputElement) {
element?.current?.querySelector('.folderAccessListContainer').classList.toggle('hide', this.checked);
(page.querySelector('.chkEnableAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
(page.querySelector('.folderAccessListContainer') as HTMLDivElement).classList.toggle('hide', this.checked);
});
element?.current?.querySelector('.userLibraryAccessForm').addEventListener('submit', onSubmit);
(page.querySelector('.userLibraryAccessForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadData]);
return (

View file

@ -0,0 +1,429 @@
import { AccessSchedule, DynamicDayOfWeek, UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useState, useRef } from 'react';
import globalize from '../../scripts/globalize';
import LibraryMenu from '../../scripts/libraryMenu';
import { appRouter } from '../appRouter';
import AccessScheduleList from '../dashboard/users/AccessScheduleList';
import BlockedTagList from '../dashboard/users/BlockedTagList';
import ButtonElement from '../dashboard/users/ButtonElement';
import CheckBoxListItem from '../dashboard/users/CheckBoxListItem';
import SectionTitleButtonElement from '../dashboard/users/SectionTitleButtonElement';
import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement';
import SelectMaxParentalRating from '../dashboard/users/SelectMaxParentalRating';
import SectionTabs from '../dashboard/users/SectionTabs';
import loading from '../loading/loading';
import toast from '../toast/toast';
type RatingsArr = {
Name: string;
Value: number;
}
type ItemsArr = {
name: string;
value: string;
checkedAttribute: string
}
const UserParentalControl: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ parentalRatings, setParentalRatings ] = useState<RatingsArr[]>([]);
const [ unratedItems, setUnratedItems ] = useState<ItemsArr[]>([]);
const [ accessSchedules, setAccessSchedules ] = useState<AccessSchedule[]>([]);
const [ blockedTags, setBlockedTags ] = useState([]);
const element = useRef<HTMLDivElement>(null);
const populateRatings = useCallback((allParentalRatings) => {
let rating;
const ratings: RatingsArr[] = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
if (ratings.length) {
const lastRating = ratings[ratings.length - 1];
if (lastRating.Value === rating.Value) {
lastRating.Name += '/' + rating.Name;
continue;
}
}
ratings.push({
Name: rating.Name,
Value: rating.Value
});
}
setParentalRatings(ratings);
}, []);
const loadUnratedItems = useCallback((user) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const items = [{
name: globalize.translate('Books'),
value: 'Book'
}, {
name: globalize.translate('Channels'),
value: 'ChannelContent'
}, {
name: globalize.translate('LiveTV'),
value: 'LiveTvChannel'
}, {
name: globalize.translate('Movies'),
value: 'Movie'
}, {
name: globalize.translate('Music'),
value: 'Music'
}, {
name: globalize.translate('Trailers'),
value: 'Trailer'
}, {
name: globalize.translate('Shows'),
value: 'Series'
}];
const itemsArr: ItemsArr[] = [];
for (const item of items) {
const isChecked = user.Policy.BlockUnratedItems.indexOf(item.value) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
itemsArr.push({
value: item.value,
name: item.name,
checkedAttribute: checkedAttribute
});
}
setUnratedItems(itemsArr);
const blockUnratedItems = page.querySelector('.blockUnratedItems') as HTMLDivElement;
blockUnratedItems.dispatchEvent(new CustomEvent('create'));
}, []);
const loadBlockedTags = useCallback((tags) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setBlockedTags(tags);
const blockedTagsElem = page.querySelector('.blockedTags') as HTMLDivElement;
for (const btnDeleteTag of blockedTagsElem.querySelectorAll('.btnDeleteTag')) {
btnDeleteTag.addEventListener('click', function () {
const tag = btnDeleteTag.getAttribute('data-tag');
const newTags = tags.filter(function (t: string) {
return t != tag;
});
loadBlockedTags(newTags);
});
}
}, []);
const renderAccessSchedule = useCallback((schedules) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setAccessSchedules(schedules);
const accessScheduleList = page.querySelector('.accessScheduleList') as HTMLDivElement;
for (const btnDelete of accessScheduleList.querySelectorAll('.btnDelete')) {
btnDelete.addEventListener('click', function () {
const index = parseInt(btnDelete.getAttribute('data-index') || '0', 10);
schedules.splice(index, 1);
const newindex = schedules.filter(function (i: number) {
return i != index;
});
renderAccessSchedule(newindex);
});
}
}, []);
const loadUser = useCallback((user, allParentalRatings) => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
setUserName(user.Name);
LibraryMenu.setTitle(user.Name);
loadUnratedItems(user);
loadBlockedTags(user.Policy.BlockedTags);
populateRatings(allParentalRatings);
let ratingValue = '';
if (user.Policy.MaxParentalRating) {
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
const rating = allParentalRatings[i];
if (user.Policy.MaxParentalRating >= rating.Value) {
ratingValue = rating.Value;
}
}
}
(page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value = ratingValue;
if (user.Policy.IsAdministrator) {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.accessScheduleSection') as HTMLDivElement).classList.remove('hide');
}
renderAccessSchedule(user.Policy.AccessSchedules || []);
loading.hide();
}, [loadBlockedTags, loadUnratedItems, populateRatings, renderAccessSchedule]);
const loadData = useCallback(() => {
loading.show();
const userId = appRouter.param('userId');
const promise1 = window.ApiClient.getUser(userId);
const promise2 = window.ApiClient.getParentalRatings();
Promise.all([promise1, promise2]).then(function (responses) {
loadUser(responses[0], responses[1]);
});
}, [loadUser]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const onSaveComplete = () => {
loading.hide();
toast(globalize.translate('SettingsSaved'));
};
const saveUser = (user: UserDto) => {
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
user.Policy.MaxParentalRating = parseInt((page.querySelector('.selectMaxParentalRating') as HTMLInputElement).value || '0', 10) || null;
user.Policy.BlockUnratedItems = Array.prototype.filter.call(page.querySelectorAll('.chkUnratedItem'), function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-itemtype');
});
user.Policy.AccessSchedules = getSchedulesFromPage();
user.Policy.BlockedTags = getBlockedTagsFromPage();
window.ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
};
const showSchedulePopup = (schedule: AccessSchedule, index: number) => {
schedule = schedule || {};
import('../../components/accessSchedule/accessSchedule').then(({default: accessschedule}) => {
accessschedule.show({
schedule: schedule
}).then(function (updatedSchedule) {
const schedules = getSchedulesFromPage();
if (index == -1) {
index = schedules.length;
}
schedules[index] = updatedSchedule;
renderAccessSchedule(schedules);
});
});
};
const getSchedulesFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.liSchedule'), function (elem) {
return {
DayOfWeek: elem.getAttribute('data-day'),
StartHour: elem.getAttribute('data-start'),
EndHour: elem.getAttribute('data-end')
};
}) as AccessSchedule[];
};
const getBlockedTagsFromPage = () => {
return Array.prototype.map.call(page.querySelectorAll('.blockedTag'), function (elem) {
return elem.getAttribute('data-tag');
}) as string[];
};
const showBlockedTagPopup = () => {
import('../../components/prompt/prompt').then(({default: prompt}) => {
prompt({
label: globalize.translate('LabelTag')
}).then(function (value) {
const tags = getBlockedTagsFromPage();
if (tags.indexOf(value) == -1) {
tags.push(value);
loadBlockedTags(tags);
}
});
});
};
const onSubmit = (e: Event) => {
loading.show();
const userId = appRouter.param('userId');
window.ApiClient.getUser(userId).then(function (result) {
saveUser(result);
});
e.preventDefault();
e.stopPropagation();
return false;
};
(page.querySelector('.btnAddSchedule') as HTMLButtonElement).addEventListener('click', function () {
showSchedulePopup({
Id: 0,
UserId: '',
DayOfWeek: DynamicDayOfWeek.Sunday,
StartHour: 0,
EndHour: 0
}, -1);
});
(page.querySelector('.btnAddBlockedTag') as HTMLButtonElement).addEventListener('click', function () {
showBlockedTagPopup();
});
(page.querySelector('.userParentalControlForm') as HTMLFormElement).addEventListener('submit', onSubmit);
}, [loadBlockedTags, loadData, renderAccessSchedule]);
return (
<div ref={element}>
<div className='content-primary'>
<div className='verticalSection'>
<div className='sectionTitleContainer flex align-items-center'>
<h2 className='sectionTitle username'>
{userName}
</h2>
<SectionTitleLinkElement
className='raised button-alt headerHelpButton'
title='Help'
url='https://docs.jellyfin.org/general/server/users/'
/>
</div>
</div>
<SectionTabs activeTab='userparentalcontrol'/>
<form className='userParentalControlForm'>
<div className='selectContainer'>
<SelectMaxParentalRating
className= 'selectMaxParentalRating'
label= 'LabelMaxParentalRating'
parentalRatings={parentalRatings}
/>
<div className='fieldDescription'>
{globalize.translate('MaxParentalRatingHelp')}
</div>
</div>
<div>
<div className='blockUnratedItems'>
<h3 className='checkboxListLabel'>
{globalize.translate('HeaderBlockItemsWithNoRating')}
</h3>
<div className='checkboxList paperList' style={{ padding: '.5em 1em' }}>
{unratedItems.map(Item => {
return <CheckBoxListItem
key={Item.value}
className='chkUnratedItem'
ItemType={Item.value}
Name={Item.name}
checkedAttribute={Item.checkedAttribute}
/>;
})}
</div>
</div>
</div>
<br />
<div className='verticalSection' style={{marginBottom: '2em'}}>
<div
className='detailSectionHeader sectionTitleContainer'
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
>
<h2 className='sectionTitle'>
{globalize.translate('LabelBlockContentWithTags')}
</h2>
<SectionTitleButtonElement
className='fab btnAddBlockedTag submit'
title='Add'
icon='add'
/>
</div>
<div className='blockedTags' style={{marginTop: '.5em'}}>
{blockedTags.map((tag, index) => {
return <BlockedTagList
key={index}
tag={tag}
/>;
})}
</div>
</div>
<div className='accessScheduleSection verticalSection' style={{marginBottom: '2em'}}>
<div
className='sectionTitleContainer'
style={{display: 'flex', alignItems: 'center', paddingBottom: '1em'}}
>
<h2 className='sectionTitle'>
{globalize.translate('HeaderAccessSchedule')}
</h2>
<SectionTitleButtonElement
className='fab btnAddSchedule submit'
title='Add'
icon='add'
/>
</div>
<p>{globalize.translate('HeaderAccessScheduleHelp')}</p>
<div className='accessScheduleList paperList'>
{accessSchedules.map((accessSchedule, index) => {
return <AccessScheduleList
key={index}
index={index}
Id={accessSchedule.Id}
DayOfWeek={accessSchedule.DayOfWeek}
StartHour={accessSchedule.StartHour}
EndHour={accessSchedule.EndHour}
/>;
})}
</div>
</div>
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
</div>
</form>
</div>
</div>
);
};
export default UserParentalControl;

View file

@ -1,4 +1,4 @@
import { UserDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, {FunctionComponent, useEffect, useState, useRef} from 'react';
import Dashboard from '../../scripts/clientUtils';
import globalize from '../../scripts/globalize';
@ -21,9 +21,9 @@ type MenuEntry = {
}
const UserProfilesPage: FunctionComponent = () => {
const [ users, setUsers ] = useState([]);
const [ users, setUsers ] = useState<UserDto[]>([]);
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
const loadData = () => {
loading.show();
@ -34,12 +34,24 @@ const UserProfilesPage: FunctionComponent = () => {
};
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
loadData();
const showUserMenu = (elem) => {
const showUserMenu = (elem: HTMLElement) => {
const card = dom.parentWithClass(elem, 'card');
const userId = card.getAttribute('data-userid');
if (!userId) {
console.error('Unexpected null user id');
return;
}
const menuItems: MenuEntry[] = [];
menuItems.push({
@ -67,7 +79,7 @@ const UserProfilesPage: FunctionComponent = () => {
actionsheet.show({
items: menuItems,
positionTo: card,
callback: function (id) {
callback: function (id: string) {
switch (id) {
case 'open':
Dashboard.navigate('useredit.html?userId=' + userId);
@ -89,7 +101,7 @@ const UserProfilesPage: FunctionComponent = () => {
});
};
const deleteUser = (id) => {
const deleteUser = (id: string) => {
const msg = globalize.translate('DeleteUserConfirmation');
confirm({
@ -105,15 +117,15 @@ const UserProfilesPage: FunctionComponent = () => {
});
};
element?.current?.addEventListener('click', function (e) {
const btnUserMenu = dom.parentWithClass(e.target, 'btnUserMenu');
page.addEventListener('click', function (e) {
const btnUserMenu = dom.parentWithClass(e.target as HTMLElement, 'btnUserMenu');
if (btnUserMenu) {
showUserMenu(btnUserMenu);
}
});
element?.current?.querySelector('.btnAddUser').addEventListener('click', function() {
(page.querySelector('.btnAddUser') as HTMLButtonElement).addEventListener('click', function() {
Dashboard.navigate('usernew.html');
});
}, []);

View file

@ -15,7 +15,7 @@ let enableAnimation;
function getOsdElementHtml() {
let html = '';
html += '<span class="material-icons iconOsdIcon brightness_high"></span>';
html += '<span class="material-icons iconOsdIcon brightness_high" aria-hidden="true"></span>';
html += '<div class="iconOsdProgressOuter"><div class="iconOsdProgressInner brightnessOsdProgressInner"></div></div>';

View file

@ -12,6 +12,8 @@ import Screenfull from 'screenfull';
import ServerConnections from '../ServerConnections';
import alert from '../alert';
const UNLIMITED_ITEMS = -1;
function enableLocalPlaylistManagement(player) {
if (player.getPlaylist) {
return false;
@ -119,7 +121,11 @@ function getItemsForPlayback(serverId, query) {
};
});
} else {
query.Limit = query.Limit || 300;
if (query.Limit === UNLIMITED_ITEMS) {
delete query.Limit;
} else {
query.Limit = query.Limit || 300;
}
query.Fields = 'Chapters';
query.ExcludeLocationTypes = 'Virtual';
query.EnableTotalRecordCount = false;
@ -1774,7 +1780,8 @@ class PlaybackManager {
// Setting this to true may cause some incorrect sorting
Recursive: false,
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Photo,Video'
MediaTypes: 'Photo,Video',
Limit: UNLIMITED_ITEMS
}).then(function (result) {
const items = result.Items;
@ -1799,7 +1806,7 @@ class PlaybackManager {
SortBy: options.shuffle ? 'Random' : 'SortName',
// Only include Photos because we do not handle mixed queues currently
MediaTypes: 'Photo',
Limit: 1000
Limit: UNLIMITED_ITEMS
});
} else if (firstItem.Type === 'MusicGenre') {
promise = getItemsForPlayback(serverId, {
@ -1817,7 +1824,7 @@ class PlaybackManager {
SortBy: options.shuffle ? 'Random' : 'SortName',
// Only include Photos because we do not handle mixed queues currently
MediaTypes: 'Photo',
Limit: 1000
Limit: UNLIMITED_ITEMS
}, queryOptions));
} else if (firstItem.IsFolder) {
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
@ -2385,8 +2392,11 @@ class PlaybackManager {
streamInfo.fullscreen = playOptions.fullscreen;
getPlayerData(player).isChangingStream = false;
getPlayerData(player).maxStreamingBitrate = maxBitrate;
const playerData = getPlayerData(player);
playerData.isChangingStream = false;
playerData.maxStreamingBitrate = maxBitrate;
playerData.streamInfo = streamInfo;
return player.play(streamInfo).then(function () {
loading.hide();
@ -3312,6 +3322,7 @@ class PlaybackManager {
mediaSource.MediaStreams = info.MediaStreams;
Events.trigger(player, 'mediastreamschange');
}, function () {
// Swallow errors
});
}

View file

@ -3,6 +3,7 @@ import { Events } from 'jellyfin-apiclient';
import browser from '../../scripts/browser';
import loading from '../loading/loading';
import { playbackManager } from '../playback/playbackmanager';
import { pluginManager } from '../pluginManager';
import { appRouter } from '../appRouter';
import globalize from '../../scripts/globalize';
import { appHost } from '../apphost';
@ -130,6 +131,13 @@ export function show(button) {
menuOptions.enableHistory = false;
}
// Add message when Google Cast is not supported
const isChromecastPluginLoaded = !!pluginManager.plugins.find(plugin => plugin.id === 'chromecast');
// TODO: Add other checks for support (Android app, secure context, etc)
if (!isChromecastPluginLoaded) {
menuOptions.text = `(${globalize.translate('GoogleCastUnsupported')})`;
}
actionsheet.show(menuOptions).then(function (id) {
const target = targets.filter(function (t) {
return t.id === id;

View file

@ -16,7 +16,7 @@ let enableAnimation;
function getOsdElementHtml() {
let html = '';
html += '<span class="material-icons iconOsdIcon volume_up"></span>';
html += '<span class="material-icons iconOsdIcon volume_up" aria-hidden="true"></span>';
html += '<div class="iconOsdProgressOuter"><div class="iconOsdProgressInner"></div></div>';

View file

@ -176,12 +176,6 @@ import template from './playbackSettings.template.html';
context.querySelector('.fldChromecastQuality').classList.add('hide');
}
if (browser.tizen || browser.web0s) {
context.querySelector('.fldEnableNextVideoOverlay').classList.add('hide');
} else {
context.querySelector('.fldEnableNextVideoOverlay').classList.remove('hide');
}
context.querySelector('.chkPlayDefaultAudioTrack').checked = user.Configuration.PlayDefaultAudioTrack || false;
context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();

View file

@ -90,7 +90,7 @@
<div class="fieldDescription checkboxFieldDescription">${SetUsingLastTracksHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldEnableNextVideoOverlay hide">
<div class="checkboxContainer checkboxContainer-withDescription fldEnableNextVideoOverlay">
<label>
<input type="checkbox" is="emby-checkbox" class="chkEnableNextVideoOverlay" />
<span>${EnableNextVideoInfoOverlay}</span>

View file

@ -26,7 +26,7 @@ import ServerConnections from '../ServerConnections';
if (layoutManager.tv) {
button = '';
} else {
button = '<button type="button" is="paper-icon-button-light" class="playerStats-closeButton"><span class="material-icons close"></span></button>';
button = '<button type="button" is="paper-icon-button-light" class="playerStats-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>';
}
const contentClass = layoutManager.tv ? 'playerStats-content playerStats-content-tv' : 'playerStats-content';

View file

@ -242,7 +242,7 @@ import ServerConnections from '../ServerConnections';
const title = globalize.translate('HeaderAddToPlaylist');
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">';
html += title;
html += '</h3>';

View file

@ -1,6 +1,6 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1">
<span class="material-icons arrow_back"></span>
<span class="material-icons arrow_back" aria-hidden="true"></span>
</button>
<h3 class="formDialogHeaderTitle"></h3>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle"></h3>
</div>
<div class="formDialogContent smoothScrollY">

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${HeaderRecordingOptions}
</h3>

View file

@ -2,7 +2,7 @@
<div class="recordSeriesContainer recordingFields-buttons flex align-items-center hide">
<div>
<button is="emby-button" type="button" class="raised recordingButton seriesRecordingButton">
<span class="material-icons recordingIcon fiber_smart_record"></span>
<span class="material-icons recordingIcon fiber_smart_record" aria-hidden="true"></span>
<span class="buttonText">${RecordSeries}</span>
</button>
</div>
@ -14,7 +14,7 @@
<div class="recordingFields-buttons flex align-items-center">
<div>
<button is="emby-button" type="button" class="raised recordingButton singleRecordingButton">
<span class="material-icons recordingIcon fiber_manual_record"></span>
<span class="material-icons recordingIcon fiber_manual_record" aria-hidden="true"></span>
<span class="buttonText">${Record}</span>
</button>
</div>

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${HeaderSeriesOptions}
</h3>

View file

@ -120,7 +120,7 @@ class RefreshDialog {
const title = globalize.translate('RefreshMetadata');
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">';
html += title;
html += '</h3>';

View file

@ -233,8 +233,8 @@ function updateNowPlayingInfo(context, state, serverId) {
apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) {
const userData = fullItem.UserData || {};
const likes = userData.Likes == null ? '' : userData.Likes;
context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + fullItem.Id + '" data-serverid="' + fullItem.ServerId + '" data-itemtype="' + fullItem.Type + '" data-likes="' + likes + '" data-isfavorite="' + userData.IsFavorite + '"><span class="material-icons favorite"></span></button>';
context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + fullItem.Id + '" data-serverid="' + fullItem.ServerId + '" data-itemtype="' + fullItem.Type + '" data-likes="' + likes + '" data-isfavorite="' + userData.IsFavorite + '"><span class="material-icons favorite"></span></button>';
context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + fullItem.Id + '" data-serverid="' + fullItem.ServerId + '" data-itemtype="' + fullItem.Type + '" data-likes="' + likes + '" data-isfavorite="' + userData.IsFavorite + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + fullItem.Id + '" data-serverid="' + fullItem.ServerId + '" data-itemtype="' + fullItem.Type + '" data-likes="' + likes + '" data-isfavorite="' + userData.IsFavorite + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
});
} else {
backdrop.clearBackdrop();
@ -248,15 +248,11 @@ function setImageUrl(context, state, url) {
if (url) {
imgContainer.innerHTML = '<img class="nowPlayingPageImage" src="' + url + '" />';
if (item.Type == 'Audio') {
context.querySelector('.nowPlayingPageImage').classList.add('nowPlayingPageImageAudio');
context.querySelector('.nowPlayingPageImageContainer').classList.remove('nowPlayingPageImageAudio');
} else {
context.querySelector('.nowPlayingPageImageContainer').classList.add('nowPlayingPageImagePoster');
context.querySelector('.nowPlayingPageImage').classList.remove('nowPlayingPageImageAudio');
}
context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImageAudio', item.Type === 'Audio');
context.querySelector('.nowPlayingPageImage').classList.toggle('nowPlayingPageImagePoster', item.Type !== 'Audio');
} else {
imgContainer.innerHTML = '<div class="nowPlayingPageImageContainerNoAlbum"><button data-action="link" class="cardImageContainer coveredImage ' + cardBuilder.getDefaultBackgroundClass(item.Name) + ' cardContent cardContent-shadow itemAction"><span class="cardImageIcon material-icons album"></span></button></div>';
imgContainer.innerHTML = '<div class="nowPlayingPageImageContainerNoAlbum"><button data-action="link" class="cardImageContainer coveredImage ' + cardBuilder.getDefaultBackgroundClass(item.Name) + ' cardContent cardContent-shadow itemAction"><span class="cardImageIcon material-icons album" aria-hidden="true"></span></button></div>';
}
}
@ -385,14 +381,14 @@ export default function () {
const context = dlg;
const toggleRepeatButtons = context.querySelectorAll('.repeatToggleButton');
const cssClass = 'buttonActive';
let innHtml = '<span class="material-icons repeat"></span>';
let innHtml = '<span class="material-icons repeat" aria-hidden="true"></span>';
let repeatOn = true;
switch (repeatMode) {
case 'RepeatAll':
break;
case 'RepeatOne':
innHtml = '<span class="material-icons repeat_one"></span>';
innHtml = '<span class="material-icons repeat_one" aria-hidden="true"></span>';
break;
case 'RepeatNone':
default:
@ -889,7 +885,7 @@ export default function () {
function init(ownerView, context) {
let volumecontrolHtml = '<div class="volumecontrol flex align-items-center flex-wrap-wrap justify-content-center">';
volumecontrolHtml += `<button is="paper-icon-button-light" class="buttonMute autoSize" title=${globalize.translate('Mute')}><span class="xlargePaperIconButton material-icons volume_up"></span></button>`;
volumecontrolHtml += `<button is="paper-icon-button-light" class="buttonMute autoSize" title=${globalize.translate('Mute')}><span class="xlargePaperIconButton material-icons volume_up" aria-hidden="true"></span></button>`;
volumecontrolHtml += '<div class="sliderContainer nowPlayingVolumeSliderContainer"><input is="emby-slider" type="range" step="1" min="0" max="100" value="0" class="nowPlayingVolumeSlider"/></div>';
volumecontrolHtml += '</div>';
const optionsSection = context.querySelector('.playlistSectionButton');

View file

@ -81,8 +81,6 @@
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-flex-shrink: 0;
flex-shrink: 0;
}
@ -145,7 +143,7 @@
width: 100%;
-webkit-box-shadow: 0 0 1.9vh #000;
box-shadow: 0 0 1.9vh #000;
border: 0.1em solid #222;
border-radius: 0.2em;
user-select: none;
-moz-user-select: none;
-webkit-user-drag: none;
@ -203,6 +201,7 @@
.layout-desktop .playlistSectionButton,
.layout-tv .playlistSectionButton {
background: none;
color: inherit;
}
.layout-desktop .nowPlayingPlaylist,
@ -274,9 +273,7 @@
.remoteControlContent {
padding-left: 7.3% !important;
padding-right: 7.3% !important;
display: flex;
height: 100%;
flex-direction: column;
}
.layout-desktop .nowPlayingPageUserDataButtons {
@ -296,7 +293,6 @@
}
.nowPlayingPageTitle {
/* text-align: center; */
margin: 0;
}
@ -317,17 +313,24 @@
}
.nowPlayingInfoButtons {
/* margin: 1.5em 0 0 0; */
-webkit-box-pack: center;
-webkit-justify-content: center;
justify-content: center;
font-size: 1.5em;
height: 100%;
margin-left: -0.5em;
margin-right: -0.5em;
}
.nowPlayingPageImageContainer {
display: flex;
flex-direction: column;
flex-grow: 1;
flex-shrink: 1;
align-items: center;
justify-content: center;
width: 100%;
margin: auto auto 0.5em;
min-height: 0;
margin: 0 auto 0.5em;
}
.nowPlayingPageImageContainerNoAlbum .cardImageContainer .cardImageIcon {
@ -336,7 +339,9 @@
}
.nowPlayingInfoControls {
margin: 0.5em 0 1em 0;
flex-grow: 0;
flex-shrink: 1;
margin: 0.5em 0 0;
width: 100%;
-webkit-box-pack: start !important;
-webkit-justify-content: start !important;
@ -365,19 +370,15 @@
.nowPlayingInfoButtons .btnRepeat,
.nowPlayingInfoButtons .btnRewind {
position: absolute;
left: 0;
margin-left: 0;
padding-left: 7.3%;
margin-right: auto;
font-size: smaller;
}
.nowPlayingInfoButtons .btnShuffleQueue,
.nowPlayingInfoButtons .btnFastForward {
position: absolute;
right: 0;
margin-left: auto;
margin-right: 0;
padding-right: 7.3%;
font-size: smaller;
}
@ -391,25 +392,11 @@
font-size: 2em;
}
.nowPlayingPageImage {
/* width: inherit; */
overflow-y: hidden;
overflow: hidden;
margin: 0 auto;
}
.nowPlayingPageImage.nowPlayingPageImageAudio {
width: 100%;
}
.nowPlayingPageImageContainer.nowPlayingPageImagePoster {
height: 50%;
overflow: hidden;
}
.nowPlayingPageImageContainer.nowPlayingPageImagePoster img {
height: 100%;
.nowPlayingPageImage.nowPlayingPageImageAudio,
.nowPlayingPageImage.nowPlayingPageImagePoster {
width: auto;
max-width: 100%;
max-height: 100%;
}
.playlistSectionButton .volumecontrol {
@ -423,7 +410,6 @@
.nowPlayingButtonsContainer {
display: flex;
height: 100%;
flex-direction: column;
}
}

View file

@ -1,4 +1,6 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import classNames from 'classnames';
import { ApiClient } from 'jellyfin-apiclient';
import React, { FunctionComponent, useEffect, useState } from 'react';
import globalize from '../../scripts/globalize';
@ -27,14 +29,14 @@ type LiveTVSearchResultsProps = {
/*
* React component to display search result rows for live tv library search
*/
const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serverId, parentId, collectionType, query }: LiveTVSearchResultsProps) => {
const [ movies, setMovies ] = useState([]);
const [ episodes, setEpisodes ] = useState([]);
const [ sports, setSports ] = useState([]);
const [ kids, setKids ] = useState([]);
const [ news, setNews ] = useState([]);
const [ programs, setPrograms ] = useState([]);
const [ channels, setChannels ] = useState([]);
const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => {
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
const [ sports, setSports ] = useState<BaseItemDto[]>([]);
const [ kids, setKids ] = useState<BaseItemDto[]>([]);
const [ news, setNews ] = useState<BaseItemDto[]>([]);
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
useEffect(() => {
const getDefaultParameters = () => ({
@ -53,7 +55,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
});
// FIXME: This query does not support Live TV filters
const fetchItems = (apiClient, params = {}) => apiClient?.getItems(
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
apiClient?.getCurrentUserId(),
{
...getDefaultParameters(),
@ -72,8 +74,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
setChannels([]);
if (query && collectionType === 'livetv') {
// TODO: Remove type casting once we're using a properly typed API client
const apiClient = (ServerConnections as any).getApiClient(serverId);
const apiClient = ServerConnections.getApiClient(serverId);
// Movies row
fetchItems(apiClient, {
@ -83,7 +84,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: false,
IsNews: false
}).then(result => setMovies(result.Items));
}).then(result => setMovies(result.Items || []));
// Episodes row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -92,7 +93,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: false,
IsNews: false
}).then(result => setEpisodes(result.Items));
}).then(result => setEpisodes(result.Items || []));
// Sports row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -101,7 +102,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: true,
IsKids: false,
IsNews: false
}).then(result => setSports(result.Items));
}).then(result => setSports(result.Items || []));
// Kids row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -110,7 +111,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: true,
IsNews: false
}).then(result => setKids(result.Items));
}).then(result => setKids(result.Items || []));
// News row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -119,7 +120,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: false,
IsNews: true
}).then(result => setNews(result.Items));
}).then(result => setNews(result.Items || []));
// Programs row
fetchItems(apiClient, {
IncludeItemTypes: 'LiveTvProgram',
@ -128,10 +129,10 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
IsSports: false,
IsKids: false,
IsNews: false
}).then(result => setPrograms(result.Items));
}).then(result => setPrograms(result.Items || []));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items));
.then(result => setChannels(result.Items || []));
}
}, [collectionType, parentId, query, serverId]);

View file

@ -31,20 +31,20 @@ const createInputElement = () => ({
const normalizeInput = (value = '') => value.trim();
type SearchFieldsProps = {
onSearch?: () => void
onSearch?: (query: string) => void
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {} }: SearchFieldsProps) => {
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
const getSearchInput = () => element?.current?.querySelector('.searchfields-txtSearch');
const getSearchInput = () => element?.current?.querySelector<HTMLInputElement>('.searchfields-txtSearch');
const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), [onSearch]);
useEffect(() => {
getSearchInput()?.addEventListener('input', e => {
debouncedOnSearch(normalizeInput(e.target?.value));
debouncedOnSearch(normalizeInput((e.target as HTMLInputElement).value));
});
getSearchInput()?.focus();
@ -53,10 +53,15 @@ const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {
};
}, [debouncedOnSearch]);
const onAlphaPicked = e => {
const value = e.detail.value;
const onAlphaPicked = (e: Event) => {
const value = (e as CustomEvent).detail.value;
const searchInput = getSearchInput();
if (!searchInput) {
console.error('Unexpected null reference');
return;
}
if (value === 'backspace') {
const currentValue = searchInput.value;
searchInput.value = currentValue.length ? currentValue.substring(0, currentValue.length - 1) : '';
@ -73,7 +78,7 @@ const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {
ref={element}
>
<div className='searchFieldsInner flex align-items-center justify-content-center'>
<span className='searchfields-icon material-icons search' />
<span className='searchfields-icon material-icons search' aria-hidden='true' />
<div
className='inputContainer flex-grow'
style={{ marginBottom: 0 }}

View file

@ -1,4 +1,6 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import classNames from 'classnames';
import { ApiClient } from 'jellyfin-apiclient';
import React, { FunctionComponent, useEffect, useState } from 'react';
import globalize from '../../scripts/globalize';
@ -15,22 +17,22 @@ type SearchResultsProps = {
/*
* React component to display search result rows for global search and non-live tv library search
*/
const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parentId, collectionType, query }: SearchResultsProps) => {
const [ movies, setMovies ] = useState([]);
const [ shows, setShows ] = useState([]);
const [ episodes, setEpisodes ] = useState([]);
const [ videos, setVideos ] = useState([]);
const [ programs, setPrograms ] = useState([]);
const [ channels, setChannels ] = useState([]);
const [ playlists, setPlaylists ] = useState([]);
const [ artists, setArtists ] = useState([]);
const [ albums, setAlbums ] = useState([]);
const [ songs, setSongs ] = useState([]);
const [ photoAlbums, setPhotoAlbums ] = useState([]);
const [ photos, setPhotos ] = useState([]);
const [ audioBooks, setAudioBooks ] = useState([]);
const [ books, setBooks ] = useState([]);
const [ people, setPeople ] = useState([]);
const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => {
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
const [ shows, setShows ] = useState<BaseItemDto[]>([]);
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
const [ videos, setVideos ] = useState<BaseItemDto[]>([]);
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
const [ playlists, setPlaylists ] = useState<BaseItemDto[]>([]);
const [ artists, setArtists ] = useState<BaseItemDto[]>([]);
const [ albums, setAlbums ] = useState<BaseItemDto[]>([]);
const [ songs, setSongs ] = useState<BaseItemDto[]>([]);
const [ photoAlbums, setPhotoAlbums ] = useState<BaseItemDto[]>([]);
const [ photos, setPhotos ] = useState<BaseItemDto[]>([]);
const [ audioBooks, setAudioBooks ] = useState<BaseItemDto[]>([]);
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
useEffect(() => {
const getDefaultParameters = () => ({
@ -48,7 +50,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
IncludeArtists: false
});
const fetchArtists = (apiClient, params = {}) => apiClient?.getArtists(
const fetchArtists = (apiClient: ApiClient, params = {}) => apiClient?.getArtists(
apiClient?.getCurrentUserId(),
{
...getDefaultParameters(),
@ -57,7 +59,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
}
);
const fetchItems = (apiClient, params = {}) => apiClient?.getItems(
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
apiClient?.getCurrentUserId(),
{
...getDefaultParameters(),
@ -66,7 +68,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
}
);
const fetchPeople = (apiClient, params = {}) => apiClient?.getPeople(
const fetchPeople = (apiClient: ApiClient, params = {}) => apiClient?.getPeople(
apiClient?.getCurrentUserId(),
{
...getDefaultParameters(),
@ -99,45 +101,44 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
setPeople([]);
if (query) {
// TODO: Remove type casting once we're using a properly typed API client
const apiClient = (ServerConnections as any).getApiClient(serverId);
const apiClient = ServerConnections.getApiClient(serverId);
// Movie libraries
if (!collectionType || isMovies()) {
// Movies row
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
.then(result => setMovies(result.Items));
.then(result => setMovies(result.Items || []));
}
// TV Show libraries
if (!collectionType || isTVShows()) {
// Shows row
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
.then(result => setShows(result.Items));
.then(result => setShows(result.Items || []));
// Episodes row
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
.then(result => setEpisodes(result.Items));
.then(result => setEpisodes(result.Items || []));
}
// People are included for Movies and TV Shows
if (!collectionType || isMovies() || isTVShows()) {
// People row
fetchPeople(apiClient).then(result => setPeople(result.Items));
fetchPeople(apiClient).then(result => setPeople(result.Items || []));
}
// Music libraries
if (!collectionType || isMusic()) {
// Playlists row
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
.then(results => setPlaylists(results.Items));
.then(results => setPlaylists(results.Items || []));
// Artists row
fetchArtists(apiClient).then(result => setArtists(result.Items));
fetchArtists(apiClient).then(result => setArtists(result.Items || []));
// Albums row
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
.then(result => setAlbums(result.Items));
.then(result => setAlbums(result.Items || []));
// Songs row
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
.then(result => setSongs(result.Items));
.then(result => setSongs(result.Items || []));
}
// Other libraries do not support in-library search currently
@ -146,25 +147,25 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId, parent
fetchItems(apiClient, {
MediaTypes: 'Video',
ExcludeItemTypes: 'Movie,Episode,TvChannel'
}).then(result => setVideos(result.Items));
}).then(result => setVideos(result.Items || []));
// Programs row
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
.then(result => setPrograms(result.Items));
.then(result => setPrograms(result.Items || []));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items));
.then(result => setChannels(result.Items || []));
// Photo Albums row
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
.then(results => setPhotoAlbums(results.Items));
.then(results => setPhotoAlbums(results.Items || []));
// Photos row
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
.then(results => setPhotos(results.Items));
.then(results => setPhotos(results.Items || []));
// Audio Books row
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
.then(results => setAudioBooks(results.Items));
.then(results => setAudioBooks(results.Items || []));
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(results => setBooks(results.Items));
.then(results => setBooks(results.Items || []));
}
}
}, [collectionType, parentId, query, serverId]);

View file

@ -1,3 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useEffect, useRef } from 'react';
import cardBuilder from '../cardbuilder/cardBuilder';
@ -15,14 +16,35 @@ const createScroller = ({ title = '' }) => ({
</div>`
});
type CardOptions = {
itemsContainer?: HTMLElement,
parentContainer?: HTMLElement,
allowBottomPadding?: boolean,
centerText?: boolean,
coverImage?: boolean,
inheritThumb?: boolean,
overlayMoreButton?: boolean,
overlayText?: boolean,
preferThumb?: boolean,
scalable?: boolean,
shape?: string,
showParentTitle?: boolean,
showParentTitleOrTitle?: boolean,
showAirTime?: boolean,
showAirDateTime?: boolean,
showChannelName?: boolean,
showTitle?: boolean,
showYear?: boolean
}
type SearchResultsRowProps = {
title?: string;
items?: Array<any>; // TODO: Should be Array<BaseItemDto> once we have a typed API client
cardOptions?: Record<string, any>;
items?: BaseItemDto[];
cardOptions?: CardOptions;
}
const SearchResultsRow: FunctionComponent<SearchResultsRowProps> = ({ title, items = [], cardOptions = {} }: SearchResultsRowProps) => {
const element = useRef(null);
const element = useRef<HTMLDivElement>(null);
useEffect(() => {
cardBuilder.buildCards(items, {

View file

@ -1,3 +1,4 @@
import { BaseItemDto } from '@thornbill/jellyfin-sdk/dist/generated-client';
import React, { FunctionComponent, useEffect, useState } from 'react';
import { appRouter } from '../appRouter';
@ -9,7 +10,7 @@ import '../../elements/emby-button/emby-button';
// There seems to be some compatibility issues here between
// React and our legacy web components, so we need to inject
// them as an html string for now =/
const createSuggestionLink = ({name, href}) => ({
const createSuggestionLink = ({ name, href }: { name: string, href: string }) => ({
__html: `<a
is='emby-linkbutton'
class='button-link'
@ -23,12 +24,11 @@ type SearchSuggestionsProps = {
parentId?: string;
}
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId, parentId }: SearchSuggestionsProps) => {
const [ suggestions, setSuggestions ] = useState([]);
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId = window.ApiClient.serverId(), parentId }: SearchSuggestionsProps) => {
const [ suggestions, setSuggestions ] = useState<BaseItemDto[]>([]);
useEffect(() => {
// TODO: Remove type casting once we're using a properly typed API client
const apiClient = (ServerConnections as any).getApiClient(serverId);
const apiClient = ServerConnections.getApiClient(serverId);
apiClient.getItems(apiClient.getCurrentUserId(), {
SortBy: 'IsFavoriteOrLiked,Random',
@ -39,7 +39,7 @@ const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId
EnableImages: false,
ParentId: parentId,
EnableTotalRecordCount: false
}).then(result => setSuggestions(result.Items));
}).then(result => setSuggestions(result.Items || []));
}, [parentId, serverId]);
return (
@ -58,7 +58,7 @@ const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId
<div
key={`suggestion-${item.Id}`}
dangerouslySetInnerHTML={createSuggestionLink({
name: item.Name,
name: item.Name || '',
href: appRouter.getRouteUrl(item)
})}
/>

View file

@ -114,7 +114,7 @@ function getImgUrl(item, user) {
function getIcon(icon, cssClass, canFocus, autoFocus) {
const tabIndex = canFocus ? '' : ' tabindex="-1"';
autoFocus = autoFocus ? ' autofocus' : '';
return '<button is="paper-icon-button-light" class="autoSize ' + cssClass + '"' + tabIndex + autoFocus + '><span class="material-icons slideshowButtonIcon ' + icon + '"></span></button>';
return '<button is="paper-icon-button-light" class="autoSize ' + cssClass + '"' + tabIndex + autoFocus + '><span class="material-icons slideshowButtonIcon ' + icon + '" aria-hidden="true"></span></button>';
}
/**
@ -216,29 +216,32 @@ export default function (options) {
dialogHelper.close(dialog);
});
dialog.querySelector('.btnSlideshowPrevious')?.addEventListener('click', getClickHandler(null));
dialog.querySelector('.btnSlideshowNext')?.addEventListener('click', getClickHandler(null));
const btnPause = dialog.querySelector('.btnSlideshowPause');
if (btnPause) {
btnPause.addEventListener('click', playPause);
btnPause.addEventListener('click', getClickHandler(playPause));
}
const btnDownload = dialog.querySelector('.btnDownload');
if (btnDownload) {
btnDownload.addEventListener('click', download);
btnDownload.addEventListener('click', getClickHandler(download));
}
const btnShare = dialog.querySelector('.btnShare');
if (btnShare) {
btnShare.addEventListener('click', share);
btnShare.addEventListener('click', getClickHandler(share));
}
const btnFullscreen = dialog.querySelector('.btnFullscreen');
if (btnFullscreen) {
btnFullscreen.addEventListener('click', fullscreen);
btnFullscreen.addEventListener('click', getClickHandler(fullscreen));
}
const btnFullscreenExit = dialog.querySelector('.btnFullscreenExit');
if (btnFullscreenExit) {
btnFullscreenExit.addEventListener('click', fullscreenExit);
btnFullscreenExit.addEventListener('click', getClickHandler(fullscreenExit));
}
if (screenfull.isEnabled) {
@ -756,6 +759,17 @@ export default function (options) {
}
}
/**
* Constructs click event handler.
* @param {function|null|undefined} callback - Click event handler.
*/
function getClickHandler(callback) {
return (e) => {
showOsd();
callback?.(e);
};
}
/**
* Shows the slideshow component.
*/

View file

@ -63,7 +63,7 @@ class SortMenu {
let html = '';
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel hide-mouse-idle-tv" tabindex="-1"><span class="material-icons arrow_back"></span></button>';
html += '<button is="paper-icon-button-light" class="btnCancel hide-mouse-idle-tv" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>';
html += '<h3 class="formDialogHeaderTitle">${Sort}</h3>';
html += '</div>';

View file

@ -98,7 +98,7 @@ function fillSubtitleList(context, item) {
itemHtml += '<' + tagName + ' class="' + className + '" data-index="' + s.Index + '">';
itemHtml += '<span class="listItemIcon material-icons closed_caption"></span>';
itemHtml += '<span class="listItemIcon material-icons closed_caption" aria-hidden="true"></span>';
itemHtml += '<div class="listItemBody two-line">';
@ -115,7 +115,7 @@ function fillSubtitleList(context, item) {
if (!layoutManager.tv) {
if (s.Path) {
itemHtml += '<button is="paper-icon-button-light" data-index="' + s.Index + '" title="' + globalize.translate('Delete') + '" class="btnDelete listItemButton"><span class="material-icons delete"></span></button>';
itemHtml += '<button is="paper-icon-button-light" data-index="' + s.Index + '" title="' + globalize.translate('Delete') + '" class="btnDelete listItemButton"><span class="material-icons delete" aria-hidden="true"></span></button>';
}
}
@ -193,7 +193,7 @@ function renderSearchResults(context, results) {
html += '<' + tagName + ' class="' + className + '" data-subid="' + result.Id + '">';
html += '<span class="listItemIcon material-icons closed_caption"></span>';
html += '<span class="listItemIcon material-icons closed_caption" aria-hidden="true"></span>';
const bodyClass = result.Comment || result.IsHashMatch ? 'three-line' : 'two-line';
@ -222,7 +222,7 @@ function renderSearchResults(context, results) {
html += '</div>';
if (!layoutManager.tv) {
html += '<button type="button" is="paper-icon-button-light" data-subid="' + result.Id + '" class="btnDownload listItemButton"><span class="material-icons file_download"></span></button>';
html += '<button type="button" is="paper-icon-button-light" data-subid="' + result.Id + '" class="btnDownload listItemButton"><span class="material-icons file_download" aria-hidden="true"></span></button>';
}
html += '</' + tagName + '>';

View file

@ -1,8 +1,8 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back"></span></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">${Subtitles}</h3>
<a is="emby-linkbutton" rel="noopener noreferrer" data-autohide="true" class="button-link btnHelp flex align-items-center" href="https://docs.jellyfin.org/general/server/media/subtitles.html" target="_blank" style="margin-left:auto;margin-right:.5em;padding:.25em;" title="${Help}"><span class="material-icons info"></span><span style="margin-left:.25em;">${Help}</span></a>
<a is="emby-linkbutton" rel="noopener noreferrer" data-autohide="true" class="button-link btnHelp flex align-items-center" href="https://docs.jellyfin.org/general/server/media/subtitles.html" target="_blank" style="margin-left:auto;margin-right:.5em;padding:.25em;" title="${Help}"><span class="material-icons info" aria-hidden="true"></span><span style="margin-left:.25em;">${Help}</span></a>
</div>
<div class="formDialogContent smoothScrollY">
<div class="dialogContentInner dialog-content-centered">
@ -17,8 +17,8 @@
<div class="selectContainer flex-grow" style="margin-bottom: 0;">
<select is="emby-select" id="selectLanguage" required="required" label="${LabelLanguage}" autofocus></select>
</div>
<button type="submit" is="paper-icon-button-light" title="${Search}" class="btnSearchSubtitles flex-shrink-zero emby-select-iconbutton"><span class="material-icons search"></span></button>
<button type="button" is="paper-icon-button-light" title="${Upload}" class="btnOpenUploadMenu flex-shrink-zero emby-select-iconbutton"><span class="material-icons add"></span></button>
<button type="submit" is="paper-icon-button-light" title="${Search}" class="btnSearchSubtitles flex-shrink-zero emby-select-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
<button type="button" is="paper-icon-button-light" title="${Upload}" class="btnOpenUploadMenu flex-shrink-zero emby-select-iconbutton"><span class="material-icons add" aria-hidden="true"></span></button>
</div>
<button is="emby-button" type="submit" class="raised btnSubmit block button-submit">${Search}</button>
</form>

View file

@ -1,6 +1,6 @@
<div class="subtitleSync">
<div class="subtitleSyncContainer">
<button type="button" is="paper-icon-button-light" class="subtitleSync-closeButton"><span class="material-icons close"></span></button>
<button type="button" is="paper-icon-button-light" class="subtitleSync-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>
<div class="subtitleSyncTextField" contenteditable="true" spellcheck="false">0s</div>
<div class="sliderContainer subtitleSyncSliderContainer">
<input is="emby-slider" type="range" step=".1" min="0" max="100" value="50" class="subtitleSyncSlider" data-slider-keep-progress="true" />

View file

@ -61,7 +61,7 @@ function setFiles(page, files) {
reader.onload = (function (theFile) {
return function () {
// Render file.
const html = '<a><i class="material-icons" style="transform: translateY(25%);">subtitles</i><span>' + escape(theFile.name) + '</span><a/>';
const html = '<a><span class="material-icons subtitles" aria-hidden="true" style="transform: translateY(25%);"></span><span>' + escape(theFile.name) + '</span><a/>';
page.querySelector('#subtitleOutput').innerHTML = html;
page.querySelector('#fldUpload').classList.remove('hide');

View file

@ -1,5 +1,5 @@
<div class="formDialogHeader">
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><em class="material-icons arrow_back"></em></button>
<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
<h3 class="formDialogHeaderTitle">
${HeaderUploadSubtitle}
</h3>
@ -14,7 +14,7 @@
<h2 style="margin:0;">${HeaderAddUpdateSubtitle}</h2>
<button is="emby-button" type="button" class="raised raised-mini btnBrowse" style="margin-left:1.5em;">
<em class="material-icons">folder</em>
<span class="material-icons folder" aria-hidden="true"></span>
<span>${Browse}</span>
</button>
</div>

View file

@ -71,7 +71,7 @@ class Controller {
const apiClient = this.manager.getApiClient();
const sendPlayRequest = (items) => {
const queue = items.map(item => item.Id);
apiClient.requestSyncPlaySetNewQueue({
return apiClient.requestSyncPlaySetNewQueue({
PlayingQueue: queue,
PlayingItemPosition: options.startIndex ? options.startIndex : 0,
StartPositionTicks: options.startPositionTicks ? options.startPositionTicks : 0
@ -79,12 +79,12 @@ class Controller {
};
if (options.items) {
Helper.translateItemsForPlayback(apiClient, options.items, options).then(sendPlayRequest);
return Helper.translateItemsForPlayback(apiClient, options.items, options).then(sendPlayRequest);
} else {
Helper.getItemsForPlayback(apiClient, {
return Helper.getItemsForPlayback(apiClient, {
Ids: options.ids.join(',')
}).then(function (result) {
Helper.translateItemsForPlayback(apiClient, result.Items, options).then(sendPlayRequest);
return Helper.translateItemsForPlayback(apiClient, result.Items, options).then(sendPlayRequest);
});
}
}

View file

@ -183,14 +183,14 @@ class GenericPlayer {
* Unpauses the player.
*/
localUnpause() {
// Override
}
/**
* Pauses the player.
*/
localPause() {
// Override
}
/**
@ -199,14 +199,14 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localSeek(positionTicks) {
// Override
}
/**
* Stops the player.
*/
localStop() {
// Override
}
/**
@ -215,7 +215,7 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localSendCommand(command) {
// Override
}
/**
@ -224,7 +224,7 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localPlay(options) {
// Override
}
/**
@ -233,7 +233,7 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localSetCurrentPlaylistItem(playlistItemId) {
// Override
}
/**
@ -242,7 +242,7 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localRemoveFromPlaylist(playlistItemIds) {
// Override
}
/**
@ -252,7 +252,7 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localMovePlaylistItem(playlistItemId, newIndex) {
// Override
}
/**
@ -261,7 +261,7 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localQueue(options) {
// Override
}
/**
@ -270,21 +270,21 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localQueueNext(options) {
// Override
}
/**
* Picks next item in playlist.
*/
localNextItem() {
// Override
}
/**
* Picks previous item in playlist.
*/
localPreviousItem() {
// Override
}
/**
@ -293,7 +293,7 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localSetRepeatMode(value) {
// Override
}
/**
@ -302,14 +302,14 @@ class GenericPlayer {
*/
// eslint-disable-next-line no-unused-vars
localSetQueueShuffleMode(value) {
// Override
}
/**
* Toggles shuffle mode.
*/
localToggleQueueShuffleMode() {
// Override
}
}

Some files were not shown because too many files have changed in this diff Show more