mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge branch 'master' into audio-normalization
This commit is contained in:
commit
8bfc387b85
320 changed files with 55219 additions and 49479 deletions
|
@ -187,25 +187,25 @@ function supportsCssAnimation(allowPrefix) {
|
|||
const uaMatch = function (ua) {
|
||||
ua = ua.toLowerCase();
|
||||
|
||||
const match = /(edg)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(edga)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(edgios)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(edge)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(opera)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(opr)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(chrome)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(safari)[ /]([\w.]+)/.exec(ua) ||
|
||||
/(firefox)[ /]([\w.]+)/.exec(ua) ||
|
||||
ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
|
||||
[];
|
||||
const match = /(edg)[ /]([\w.]+)/.exec(ua)
|
||||
|| /(edga)[ /]([\w.]+)/.exec(ua)
|
||||
|| /(edgios)[ /]([\w.]+)/.exec(ua)
|
||||
|| /(edge)[ /]([\w.]+)/.exec(ua)
|
||||
|| /(opera)[ /]([\w.]+)/.exec(ua)
|
||||
|| /(opr)[ /]([\w.]+)/.exec(ua)
|
||||
|| /(chrome)[ /]([\w.]+)/.exec(ua)
|
||||
|| /(safari)[ /]([\w.]+)/.exec(ua)
|
||||
|| /(firefox)[ /]([\w.]+)/.exec(ua)
|
||||
|| ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua)
|
||||
|| [];
|
||||
|
||||
const versionMatch = /(version)[ /]([\w.]+)/.exec(ua);
|
||||
|
||||
let platform_match = /(ipad)/.exec(ua) ||
|
||||
/(iphone)/.exec(ua) ||
|
||||
/(windows)/.exec(ua) ||
|
||||
/(android)/.exec(ua) ||
|
||||
[];
|
||||
let platform_match = /(ipad)/.exec(ua)
|
||||
|| /(iphone)/.exec(ua)
|
||||
|| /(windows)/.exec(ua)
|
||||
|| /(android)/.exec(ua)
|
||||
|| [];
|
||||
|
||||
let browser = match[1] || '';
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,289 +1,286 @@
|
|||
import globalize from './globalize';
|
||||
|
||||
/* eslint-disable indent */
|
||||
export function parseISO8601Date(s, toLocal) {
|
||||
// parenthese matches:
|
||||
// year month day hours minutes seconds
|
||||
// dotmilliseconds
|
||||
// tzstring plusminus hours minutes
|
||||
const re = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|([+-])(\d{2}):(\d{2}))?/;
|
||||
|
||||
export function parseISO8601Date(s, toLocal) {
|
||||
// parenthese matches:
|
||||
// year month day hours minutes seconds
|
||||
// dotmilliseconds
|
||||
// tzstring plusminus hours minutes
|
||||
const re = /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(Z|([+-])(\d{2}):(\d{2}))?/;
|
||||
const d = s.match(re);
|
||||
|
||||
const d = s.match(re);
|
||||
// "2010-12-07T11:00:00.000-09:00" parses to:
|
||||
// ["2010-12-07T11:00:00.000-09:00", "2010", "12", "07", "11",
|
||||
// "00", "00", ".000", "-09:00", "-", "09", "00"]
|
||||
// "2010-12-07T11:00:00.000Z" parses to:
|
||||
// ["2010-12-07T11:00:00.000Z", "2010", "12", "07", "11",
|
||||
// "00", "00", ".000", "Z", undefined, undefined, undefined]
|
||||
|
||||
// "2010-12-07T11:00:00.000-09:00" parses to:
|
||||
// ["2010-12-07T11:00:00.000-09:00", "2010", "12", "07", "11",
|
||||
// "00", "00", ".000", "-09:00", "-", "09", "00"]
|
||||
// "2010-12-07T11:00:00.000Z" parses to:
|
||||
// ["2010-12-07T11:00:00.000Z", "2010", "12", "07", "11",
|
||||
// "00", "00", ".000", "Z", undefined, undefined, undefined]
|
||||
|
||||
if (!d) {
|
||||
throw new Error("Couldn't parse ISO 8601 date string '" + s + "'");
|
||||
}
|
||||
|
||||
// parse strings, leading zeros into proper ints
|
||||
const a = [1, 2, 3, 4, 5, 6, 10, 11];
|
||||
for (const i in a) {
|
||||
d[a[i]] = parseInt(d[a[i]], 10);
|
||||
}
|
||||
d[7] = parseFloat(d[7]);
|
||||
|
||||
// Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])
|
||||
// note that month is 0-11, not 1-12
|
||||
// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/UTC
|
||||
let ms = Date.UTC(d[1], d[2] - 1, d[3], d[4], d[5], d[6]);
|
||||
|
||||
// if there are milliseconds, add them
|
||||
if (d[7] > 0) {
|
||||
ms += Math.round(d[7] * 1000);
|
||||
}
|
||||
|
||||
// if there's a timezone, calculate it
|
||||
if (d[8] !== 'Z' && d[10]) {
|
||||
let offset = d[10] * 60 * 60 * 1000;
|
||||
if (d[11]) {
|
||||
offset += d[11] * 60 * 1000;
|
||||
}
|
||||
if (d[9] === '-') {
|
||||
ms -= offset;
|
||||
} else {
|
||||
ms += offset;
|
||||
}
|
||||
} else if (toLocal === false) {
|
||||
ms += new Date().getTimezoneOffset() * 60000;
|
||||
}
|
||||
|
||||
return new Date(ms);
|
||||
if (!d) {
|
||||
throw new Error("Couldn't parse ISO 8601 date string '" + s + "'");
|
||||
}
|
||||
|
||||
/**
|
||||
// parse strings, leading zeros into proper ints
|
||||
const a = [1, 2, 3, 4, 5, 6, 10, 11];
|
||||
for (const i in a) {
|
||||
d[a[i]] = parseInt(d[a[i]], 10);
|
||||
}
|
||||
d[7] = parseFloat(d[7]);
|
||||
|
||||
// Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])
|
||||
// note that month is 0-11, not 1-12
|
||||
// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Date/UTC
|
||||
let ms = Date.UTC(d[1], d[2] - 1, d[3], d[4], d[5], d[6]);
|
||||
|
||||
// if there are milliseconds, add them
|
||||
if (d[7] > 0) {
|
||||
ms += Math.round(d[7] * 1000);
|
||||
}
|
||||
|
||||
// if there's a timezone, calculate it
|
||||
if (d[8] !== 'Z' && d[10]) {
|
||||
let offset = d[10] * 60 * 60 * 1000;
|
||||
if (d[11]) {
|
||||
offset += d[11] * 60 * 1000;
|
||||
}
|
||||
if (d[9] === '-') {
|
||||
ms -= offset;
|
||||
} else {
|
||||
ms += offset;
|
||||
}
|
||||
} else if (toLocal === false) {
|
||||
ms += new Date().getTimezoneOffset() * 60000;
|
||||
}
|
||||
|
||||
return new Date(ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string in '{}h {}m' format for duration.
|
||||
* @param {number} ticks - Duration in ticks.
|
||||
*/
|
||||
export function getDisplayDuration(ticks) {
|
||||
const totalMinutes = Math.round(ticks / 600000000) || 1;
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
const remainderMinutes = totalMinutes % 60;
|
||||
const result = [];
|
||||
if (totalHours > 0) {
|
||||
result.push(`${totalHours}h`);
|
||||
}
|
||||
result.push(`${remainderMinutes}m`);
|
||||
return result.join(' ');
|
||||
export function getDisplayDuration(ticks) {
|
||||
const totalMinutes = Math.round(ticks / 600000000) || 1;
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
const remainderMinutes = totalMinutes % 60;
|
||||
const result = [];
|
||||
if (totalHours > 0) {
|
||||
result.push(`${totalHours}h`);
|
||||
}
|
||||
result.push(`${remainderMinutes}m`);
|
||||
return result.join(' ');
|
||||
}
|
||||
|
||||
export function getDisplayRunningTime(ticks) {
|
||||
const ticksPerHour = 36000000000;
|
||||
const ticksPerMinute = 600000000;
|
||||
const ticksPerSecond = 10000000;
|
||||
|
||||
const parts = [];
|
||||
|
||||
let hours = ticks / ticksPerHour;
|
||||
hours = Math.floor(hours);
|
||||
|
||||
if (hours) {
|
||||
parts.push(hours.toLocaleString(globalize.getCurrentDateTimeLocale()));
|
||||
}
|
||||
|
||||
export function getDisplayRunningTime(ticks) {
|
||||
const ticksPerHour = 36000000000;
|
||||
const ticksPerMinute = 600000000;
|
||||
const ticksPerSecond = 10000000;
|
||||
ticks -= (hours * ticksPerHour);
|
||||
|
||||
const parts = [];
|
||||
let minutes = ticks / ticksPerMinute;
|
||||
minutes = Math.floor(minutes);
|
||||
|
||||
let hours = ticks / ticksPerHour;
|
||||
hours = Math.floor(hours);
|
||||
ticks -= (minutes * ticksPerMinute);
|
||||
|
||||
if (hours) {
|
||||
parts.push(hours.toLocaleString(globalize.getCurrentDateTimeLocale()));
|
||||
}
|
||||
if (minutes < 10 && hours) {
|
||||
minutes = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||
} else {
|
||||
minutes = minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||
}
|
||||
parts.push(minutes);
|
||||
|
||||
ticks -= (hours * ticksPerHour);
|
||||
let seconds = ticks / ticksPerSecond;
|
||||
seconds = Math.floor(seconds);
|
||||
|
||||
let minutes = ticks / ticksPerMinute;
|
||||
minutes = Math.floor(minutes);
|
||||
if (seconds < 10) {
|
||||
seconds = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + seconds.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||
} else {
|
||||
seconds = seconds.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||
}
|
||||
parts.push(seconds);
|
||||
|
||||
ticks -= (minutes * ticksPerMinute);
|
||||
return parts.join(':');
|
||||
}
|
||||
|
||||
if (minutes < 10 && hours) {
|
||||
minutes = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||
} else {
|
||||
minutes = minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||
}
|
||||
parts.push(minutes);
|
||||
const toLocaleTimeStringSupportsLocales = function () {
|
||||
try {
|
||||
new Date().toLocaleTimeString('i');
|
||||
} catch (e) {
|
||||
return e.name === 'RangeError';
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
||||
let seconds = ticks / ticksPerSecond;
|
||||
seconds = Math.floor(seconds);
|
||||
function getOptionList(options) {
|
||||
const list = [];
|
||||
|
||||
if (seconds < 10) {
|
||||
seconds = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + seconds.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||
} else {
|
||||
seconds = seconds.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||
}
|
||||
parts.push(seconds);
|
||||
|
||||
return parts.join(':');
|
||||
for (const i in options) {
|
||||
list.push({
|
||||
name: i,
|
||||
value: options[i]
|
||||
});
|
||||
}
|
||||
|
||||
const toLocaleTimeStringSupportsLocales = function () {
|
||||
return list;
|
||||
}
|
||||
|
||||
export function toLocaleString(date, options) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (toLocaleTimeStringSupportsLocales) {
|
||||
const currentLocale = globalize.getCurrentDateTimeLocale();
|
||||
|
||||
if (currentLocale) {
|
||||
return date.toLocaleString(currentLocale, options);
|
||||
}
|
||||
}
|
||||
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
export function toLocaleDateString(date, options) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (toLocaleTimeStringSupportsLocales) {
|
||||
const currentLocale = globalize.getCurrentDateTimeLocale();
|
||||
|
||||
if (currentLocale) {
|
||||
return date.toLocaleDateString(currentLocale, options);
|
||||
}
|
||||
}
|
||||
|
||||
// This is essentially a hard-coded polyfill
|
||||
const optionList = getOptionList(options);
|
||||
if (optionList.length === 1 && optionList[0].name === 'weekday') {
|
||||
const weekday = [];
|
||||
weekday[0] = 'Sun';
|
||||
weekday[1] = 'Mon';
|
||||
weekday[2] = 'Tue';
|
||||
weekday[3] = 'Wed';
|
||||
weekday[4] = 'Thu';
|
||||
weekday[5] = 'Fri';
|
||||
weekday[6] = 'Sat';
|
||||
return weekday[date.getDay()];
|
||||
}
|
||||
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
export function toLocaleTimeString(date, options) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (toLocaleTimeStringSupportsLocales) {
|
||||
const currentLocale = globalize.getCurrentDateTimeLocale();
|
||||
|
||||
if (currentLocale) {
|
||||
return date.toLocaleTimeString(currentLocale, options);
|
||||
}
|
||||
}
|
||||
|
||||
return date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
export function getDisplayTime(date) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
|
||||
if ((typeof date).toString().toLowerCase() === 'string') {
|
||||
try {
|
||||
new Date().toLocaleTimeString('i');
|
||||
} catch (e) {
|
||||
return e.name === 'RangeError';
|
||||
date = parseISO8601Date(date, true);
|
||||
} catch (err) {
|
||||
return date;
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
||||
function getOptionList(options) {
|
||||
const list = [];
|
||||
|
||||
for (const i in options) {
|
||||
list.push({
|
||||
name: i,
|
||||
value: options[i]
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
export function toLocaleString(date, options) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
if (toLocaleTimeStringSupportsLocales) {
|
||||
return toLocaleTimeString(date, {
|
||||
|
||||
options = options || {};
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
|
||||
if (toLocaleTimeStringSupportsLocales) {
|
||||
const currentLocale = globalize.getCurrentDateTimeLocale();
|
||||
|
||||
if (currentLocale) {
|
||||
return date.toLocaleString(currentLocale, options);
|
||||
}
|
||||
}
|
||||
|
||||
return date.toLocaleString();
|
||||
});
|
||||
}
|
||||
|
||||
export function toLocaleDateString(date, options) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
let time = toLocaleTimeString(date);
|
||||
|
||||
const timeLower = time.toLowerCase();
|
||||
|
||||
if (timeLower.indexOf('am') !== -1 || timeLower.indexOf('pm') !== -1) {
|
||||
time = timeLower;
|
||||
let hour = date.getHours() % 12;
|
||||
const suffix = date.getHours() > 11 ? 'pm' : 'am';
|
||||
if (!hour) {
|
||||
hour = 12;
|
||||
}
|
||||
let minutes = date.getMinutes();
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = '0' + minutes;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
minutes = ':' + minutes;
|
||||
time = hour + minutes + suffix;
|
||||
} else {
|
||||
const timeParts = time.split(':');
|
||||
|
||||
if (toLocaleTimeStringSupportsLocales) {
|
||||
const currentLocale = globalize.getCurrentDateTimeLocale();
|
||||
|
||||
if (currentLocale) {
|
||||
return date.toLocaleDateString(currentLocale, options);
|
||||
}
|
||||
// Trim off seconds
|
||||
if (timeParts.length > 2) {
|
||||
// setting to 2 also handles '21:00:28 GMT+9:30'
|
||||
timeParts.length = 2;
|
||||
time = timeParts.join(':');
|
||||
}
|
||||
|
||||
// This is essentially a hard-coded polyfill
|
||||
const optionList = getOptionList(options);
|
||||
if (optionList.length === 1 && optionList[0].name === 'weekday') {
|
||||
const weekday = [];
|
||||
weekday[0] = 'Sun';
|
||||
weekday[1] = 'Mon';
|
||||
weekday[2] = 'Tue';
|
||||
weekday[3] = 'Wed';
|
||||
weekday[4] = 'Thu';
|
||||
weekday[5] = 'Fri';
|
||||
weekday[6] = 'Sat';
|
||||
return weekday[date.getDay()];
|
||||
}
|
||||
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
export function toLocaleTimeString(date, options) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (toLocaleTimeStringSupportsLocales) {
|
||||
const currentLocale = globalize.getCurrentDateTimeLocale();
|
||||
|
||||
if (currentLocale) {
|
||||
return date.toLocaleTimeString(currentLocale, options);
|
||||
}
|
||||
}
|
||||
|
||||
return date.toLocaleTimeString();
|
||||
export function isRelativeDay(date, offsetInDays) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
|
||||
export function getDisplayTime(date) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
const yesterday = new Date();
|
||||
const day = yesterday.getDate() + offsetInDays;
|
||||
|
||||
if ((typeof date).toString().toLowerCase() === 'string') {
|
||||
try {
|
||||
date = parseISO8601Date(date, true);
|
||||
} catch (err) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
yesterday.setDate(day); // automatically adjusts month/year appropriately
|
||||
|
||||
if (toLocaleTimeStringSupportsLocales) {
|
||||
return toLocaleTimeString(date, {
|
||||
return date.getFullYear() === yesterday.getFullYear() && date.getMonth() === yesterday.getMonth() && date.getDate() === day;
|
||||
}
|
||||
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
let time = toLocaleTimeString(date);
|
||||
|
||||
const timeLower = time.toLowerCase();
|
||||
|
||||
if (timeLower.indexOf('am') !== -1 || timeLower.indexOf('pm') !== -1) {
|
||||
time = timeLower;
|
||||
let hour = date.getHours() % 12;
|
||||
const suffix = date.getHours() > 11 ? 'pm' : 'am';
|
||||
if (!hour) {
|
||||
hour = 12;
|
||||
}
|
||||
let minutes = date.getMinutes();
|
||||
|
||||
if (minutes < 10) {
|
||||
minutes = '0' + minutes;
|
||||
}
|
||||
|
||||
minutes = ':' + minutes;
|
||||
time = hour + minutes + suffix;
|
||||
} else {
|
||||
const timeParts = time.split(':');
|
||||
|
||||
// Trim off seconds
|
||||
if (timeParts.length > 2) {
|
||||
// setting to 2 also handles '21:00:28 GMT+9:30'
|
||||
timeParts.length = 2;
|
||||
time = timeParts.join(':');
|
||||
}
|
||||
}
|
||||
|
||||
return time;
|
||||
export default {
|
||||
parseISO8601Date: parseISO8601Date,
|
||||
getDisplayRunningTime: getDisplayRunningTime,
|
||||
getDisplayDuration,
|
||||
toLocaleDateString: toLocaleDateString,
|
||||
toLocaleString: toLocaleString,
|
||||
getDisplayTime: getDisplayTime,
|
||||
isRelativeDay: isRelativeDay,
|
||||
toLocaleTimeString: toLocaleTimeString,
|
||||
supportsLocalization: function () {
|
||||
return toLocaleTimeStringSupportsLocales;
|
||||
}
|
||||
};
|
||||
|
||||
export function isRelativeDay(date, offsetInDays) {
|
||||
if (!date) {
|
||||
throw new Error('date cannot be null');
|
||||
}
|
||||
|
||||
const yesterday = new Date();
|
||||
const day = yesterday.getDate() + offsetInDays;
|
||||
|
||||
yesterday.setDate(day); // automatically adjusts month/year appropriately
|
||||
|
||||
return date.getFullYear() === yesterday.getFullYear() && date.getMonth() === yesterday.getMonth() && date.getDate() === day;
|
||||
}
|
||||
|
||||
export default {
|
||||
parseISO8601Date: parseISO8601Date,
|
||||
getDisplayRunningTime: getDisplayRunningTime,
|
||||
getDisplayDuration,
|
||||
toLocaleDateString: toLocaleDateString,
|
||||
toLocaleString: toLocaleString,
|
||||
getDisplayTime: getDisplayTime,
|
||||
isRelativeDay: isRelativeDay,
|
||||
toLocaleTimeString: toLocaleTimeString,
|
||||
supportsLocalization: function () {
|
||||
return toLocaleTimeStringSupportsLocales;
|
||||
}
|
||||
};
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
|
|
@ -1,272 +1,269 @@
|
|||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Useful DOM utilities.
|
||||
* @module components/dom
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns parent of element with specified attribute value.
|
||||
* @param {HTMLElement} elem - Element whose parent need to find.
|
||||
* @param {string} name - Attribute name.
|
||||
* @param {mixed} [value] - Attribute value.
|
||||
* @returns {HTMLElement} Parent with specified attribute value.
|
||||
*/
|
||||
export function parentWithAttribute(elem, name, value) {
|
||||
while ((value ? elem.getAttribute(name) !== value : !elem.getAttribute(name))) {
|
||||
elem = elem.parentNode;
|
||||
export function parentWithAttribute(elem, name, value) {
|
||||
while ((value ? elem.getAttribute(name) !== value : !elem.getAttribute(name))) {
|
||||
elem = elem.parentNode;
|
||||
|
||||
if (!elem || !elem.getAttribute) {
|
||||
return null;
|
||||
}
|
||||
if (!elem || !elem.getAttribute) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
/**
|
||||
return elem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns parent of element with one of specified tag names.
|
||||
* @param {HTMLElement} elem - Element whose parent need to find.
|
||||
* @param {(string|Array)} tagNames - Tag name or array of tag names.
|
||||
* @returns {HTMLElement} Parent with one of specified tag names.
|
||||
*/
|
||||
export function parentWithTag(elem, tagNames) {
|
||||
// accept both string and array passed in
|
||||
if (!Array.isArray(tagNames)) {
|
||||
tagNames = [tagNames];
|
||||
}
|
||||
|
||||
while (tagNames.indexOf(elem.tagName || '') === -1) {
|
||||
elem = elem.parentNode;
|
||||
|
||||
if (!elem) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
export function parentWithTag(elem, tagNames) {
|
||||
// accept both string and array passed in
|
||||
if (!Array.isArray(tagNames)) {
|
||||
tagNames = [tagNames];
|
||||
}
|
||||
|
||||
/**
|
||||
while (tagNames.indexOf(elem.tagName || '') === -1) {
|
||||
elem = elem.parentNode;
|
||||
|
||||
if (!elem) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns _true_ if class list contains one of specified names.
|
||||
* @param {DOMTokenList} classList - Class list.
|
||||
* @param {Array} classNames - Array of class names.
|
||||
* @returns {boolean} _true_ if class list contains one of specified names.
|
||||
*/
|
||||
function containsAnyClass(classList, classNames) {
|
||||
for (let i = 0, length = classNames.length; i < length; i++) {
|
||||
if (classList.contains(classNames[i])) {
|
||||
return true;
|
||||
}
|
||||
function containsAnyClass(classList, classNames) {
|
||||
for (let i = 0, length = classNames.length; i < length; i++) {
|
||||
if (classList.contains(classNames[i])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns parent of element with one of specified class names.
|
||||
* @param {HTMLElement} elem - Element whose parent need to find.
|
||||
* @param {(string|Array)} classNames - Class name or array of class names.
|
||||
* @returns {HTMLElement} Parent with one of specified class names.
|
||||
*/
|
||||
export function parentWithClass(elem, classNames) {
|
||||
// accept both string and array passed in
|
||||
if (!Array.isArray(classNames)) {
|
||||
classNames = [classNames];
|
||||
}
|
||||
|
||||
while (!elem.classList || !containsAnyClass(elem.classList, classNames)) {
|
||||
elem = elem.parentNode;
|
||||
|
||||
if (!elem) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
export function parentWithClass(elem, classNames) {
|
||||
// accept both string and array passed in
|
||||
if (!Array.isArray(classNames)) {
|
||||
classNames = [classNames];
|
||||
}
|
||||
|
||||
let supportsCaptureOption = false;
|
||||
try {
|
||||
const opts = Object.defineProperty({}, 'capture', {
|
||||
// eslint-disable-next-line getter-return
|
||||
get: function () {
|
||||
supportsCaptureOption = true;
|
||||
}
|
||||
});
|
||||
window.addEventListener('test', null, opts);
|
||||
} catch (e) {
|
||||
console.debug('error checking capture support');
|
||||
while (!elem.classList || !containsAnyClass(elem.classList, classNames)) {
|
||||
elem = elem.parentNode;
|
||||
|
||||
if (!elem) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
return elem;
|
||||
}
|
||||
|
||||
let supportsCaptureOption = false;
|
||||
try {
|
||||
const opts = Object.defineProperty({}, 'capture', {
|
||||
// eslint-disable-next-line getter-return
|
||||
get: function () {
|
||||
supportsCaptureOption = true;
|
||||
}
|
||||
});
|
||||
window.addEventListener('test', null, opts);
|
||||
} catch (e) {
|
||||
console.debug('error checking capture support');
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event listener to specified target.
|
||||
* @param {EventTarget} target - Event target.
|
||||
* @param {string} type - Event type.
|
||||
* @param {function} handler - Event handler.
|
||||
* @param {Object} [options] - Listener options.
|
||||
*/
|
||||
export function addEventListener(target, type, handler, options) {
|
||||
let optionsOrCapture = options || {};
|
||||
if (!supportsCaptureOption) {
|
||||
optionsOrCapture = optionsOrCapture.capture;
|
||||
}
|
||||
target.addEventListener(type, handler, optionsOrCapture);
|
||||
export function addEventListener(target, type, handler, options) {
|
||||
let optionsOrCapture = options || {};
|
||||
if (!supportsCaptureOption) {
|
||||
optionsOrCapture = optionsOrCapture.capture;
|
||||
}
|
||||
target.addEventListener(type, handler, optionsOrCapture);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Removes event listener from specified target.
|
||||
* @param {EventTarget} target - Event target.
|
||||
* @param {string} type - Event type.
|
||||
* @param {function} handler - Event handler.
|
||||
* @param {Object} [options] - Listener options.
|
||||
*/
|
||||
export function removeEventListener(target, type, handler, options) {
|
||||
let optionsOrCapture = options || {};
|
||||
if (!supportsCaptureOption) {
|
||||
optionsOrCapture = optionsOrCapture.capture;
|
||||
}
|
||||
target.removeEventListener(type, handler, optionsOrCapture);
|
||||
export function removeEventListener(target, type, handler, options) {
|
||||
let optionsOrCapture = options || {};
|
||||
if (!supportsCaptureOption) {
|
||||
optionsOrCapture = optionsOrCapture.capture;
|
||||
}
|
||||
target.removeEventListener(type, handler, optionsOrCapture);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Cached window size.
|
||||
*/
|
||||
let windowSize;
|
||||
let windowSize;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Flag of event listener bound.
|
||||
*/
|
||||
let windowSizeEventsBound;
|
||||
let windowSizeEventsBound;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Resets cached window size.
|
||||
*/
|
||||
function clearWindowSize() {
|
||||
windowSize = null;
|
||||
}
|
||||
function clearWindowSize() {
|
||||
windowSize = null;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef {Object} windowSize
|
||||
* @property {number} innerHeight - window innerHeight.
|
||||
* @property {number} innerWidth - window innerWidth.
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns window size.
|
||||
* @returns {windowSize} Window size.
|
||||
*/
|
||||
export function getWindowSize() {
|
||||
if (!windowSize) {
|
||||
windowSize = {
|
||||
innerHeight: window.innerHeight,
|
||||
innerWidth: window.innerWidth
|
||||
};
|
||||
export function getWindowSize() {
|
||||
if (!windowSize) {
|
||||
windowSize = {
|
||||
innerHeight: window.innerHeight,
|
||||
innerWidth: window.innerWidth
|
||||
};
|
||||
|
||||
if (!windowSizeEventsBound) {
|
||||
windowSizeEventsBound = true;
|
||||
addEventListener(window, 'orientationchange', clearWindowSize, { passive: true });
|
||||
addEventListener(window, 'resize', clearWindowSize, { passive: true });
|
||||
}
|
||||
if (!windowSizeEventsBound) {
|
||||
windowSizeEventsBound = true;
|
||||
addEventListener(window, 'orientationchange', clearWindowSize, { passive: true });
|
||||
addEventListener(window, 'resize', clearWindowSize, { passive: true });
|
||||
}
|
||||
|
||||
return windowSize;
|
||||
}
|
||||
|
||||
/**
|
||||
return windowSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard screen widths.
|
||||
*/
|
||||
const standardWidths = [480, 720, 1280, 1440, 1920, 2560, 3840, 5120, 7680];
|
||||
const standardWidths = [480, 720, 1280, 1440, 1920, 2560, 3840, 5120, 7680];
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns screen width.
|
||||
* @returns {number} Screen width.
|
||||
*/
|
||||
export function getScreenWidth() {
|
||||
let width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
export function getScreenWidth() {
|
||||
let width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
if (height > width) {
|
||||
width = height * (16.0 / 9.0);
|
||||
}
|
||||
|
||||
standardWidths.sort((a, b) => Math.abs(width - a) - Math.abs(width - b));
|
||||
|
||||
return standardWidths[0];
|
||||
if (height > width) {
|
||||
width = height * (16.0 / 9.0);
|
||||
}
|
||||
|
||||
/**
|
||||
standardWidths.sort((a, b) => Math.abs(width - a) - Math.abs(width - b));
|
||||
|
||||
return standardWidths[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of animation end event.
|
||||
*/
|
||||
let _animationEvent;
|
||||
let _animationEvent;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns name of animation end event.
|
||||
* @returns {string} Name of animation end event.
|
||||
*/
|
||||
export function whichAnimationEvent() {
|
||||
if (_animationEvent) {
|
||||
return _animationEvent;
|
||||
}
|
||||
|
||||
const el = document.createElement('div');
|
||||
const animations = {
|
||||
'animation': 'animationend',
|
||||
'OAnimation': 'oAnimationEnd',
|
||||
'MozAnimation': 'animationend',
|
||||
'WebkitAnimation': 'webkitAnimationEnd'
|
||||
};
|
||||
for (const t in animations) {
|
||||
if (el.style[t] !== undefined) {
|
||||
_animationEvent = animations[t];
|
||||
return animations[t];
|
||||
}
|
||||
}
|
||||
|
||||
_animationEvent = 'animationend';
|
||||
export function whichAnimationEvent() {
|
||||
if (_animationEvent) {
|
||||
return _animationEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
const el = document.createElement('div');
|
||||
const animations = {
|
||||
'animation': 'animationend',
|
||||
'OAnimation': 'oAnimationEnd',
|
||||
'MozAnimation': 'animationend',
|
||||
'WebkitAnimation': 'webkitAnimationEnd'
|
||||
};
|
||||
for (const t in animations) {
|
||||
if (el.style[t] !== undefined) {
|
||||
_animationEvent = animations[t];
|
||||
return animations[t];
|
||||
}
|
||||
}
|
||||
|
||||
_animationEvent = 'animationend';
|
||||
return _animationEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns name of animation cancel event.
|
||||
* @returns {string} Name of animation cancel event.
|
||||
*/
|
||||
export function whichAnimationCancelEvent() {
|
||||
return whichAnimationEvent().replace('animationend', 'animationcancel').replace('AnimationEnd', 'AnimationCancel');
|
||||
}
|
||||
export function whichAnimationCancelEvent() {
|
||||
return whichAnimationEvent().replace('animationend', 'animationcancel').replace('AnimationEnd', 'AnimationCancel');
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Name of transition end event.
|
||||
*/
|
||||
let _transitionEvent;
|
||||
let _transitionEvent;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns name of transition end event.
|
||||
* @returns {string} Name of transition end event.
|
||||
*/
|
||||
export function whichTransitionEvent() {
|
||||
if (_transitionEvent) {
|
||||
return _transitionEvent;
|
||||
}
|
||||
|
||||
const el = document.createElement('div');
|
||||
const transitions = {
|
||||
'transition': 'transitionend',
|
||||
'OTransition': 'oTransitionEnd',
|
||||
'MozTransition': 'transitionend',
|
||||
'WebkitTransition': 'webkitTransitionEnd'
|
||||
};
|
||||
for (const t in transitions) {
|
||||
if (el.style[t] !== undefined) {
|
||||
_transitionEvent = transitions[t];
|
||||
return transitions[t];
|
||||
}
|
||||
}
|
||||
|
||||
_transitionEvent = 'transitionend';
|
||||
export function whichTransitionEvent() {
|
||||
if (_transitionEvent) {
|
||||
return _transitionEvent;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
const el = document.createElement('div');
|
||||
const transitions = {
|
||||
'transition': 'transitionend',
|
||||
'OTransition': 'oTransitionEnd',
|
||||
'MozTransition': 'transitionend',
|
||||
'WebkitTransition': 'webkitTransitionEnd'
|
||||
};
|
||||
for (const t in transitions) {
|
||||
if (el.style[t] !== undefined) {
|
||||
_transitionEvent = transitions[t];
|
||||
return transitions[t];
|
||||
}
|
||||
}
|
||||
|
||||
_transitionEvent = 'transitionend';
|
||||
return _transitionEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets title and ARIA-label of element.
|
||||
|
|
|
@ -5,338 +5,335 @@ import 'material-design-icons-iconfont';
|
|||
import Dashboard from '../utils/dashboard';
|
||||
import { getParameterByName } from '../utils/url.ts';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function getNode(item, folderState, selected) {
|
||||
const htmlName = getNodeInnerHtml(item);
|
||||
const node = {
|
||||
id: item.Id,
|
||||
text: htmlName,
|
||||
state: {
|
||||
opened: item.IsFolder && folderState == 'open',
|
||||
selected: selected
|
||||
},
|
||||
li_attr: {
|
||||
serveritemtype: item.Type,
|
||||
collectiontype: item.CollectionType
|
||||
}
|
||||
};
|
||||
if (item.IsFolder) {
|
||||
node.children = [{
|
||||
text: 'Loading...',
|
||||
icon: false
|
||||
}];
|
||||
node.icon = false;
|
||||
} else {
|
||||
node.icon = false;
|
||||
}
|
||||
if (node.state.opened) {
|
||||
node.li_attr.loadedFromServer = true;
|
||||
}
|
||||
if (selected) {
|
||||
selectedNodeId = item.Id;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function getNode(item, folderState, selected) {
|
||||
const htmlName = getNodeInnerHtml(item);
|
||||
const node = {
|
||||
id: item.Id,
|
||||
text: htmlName,
|
||||
function getNodeInnerHtml(item) {
|
||||
let name = item.Name;
|
||||
if (item.Number) {
|
||||
name = item.Number + ' - ' + name;
|
||||
}
|
||||
if (item.IndexNumber != null && item.Type != 'Season') {
|
||||
name = item.IndexNumber + ' - ' + name;
|
||||
}
|
||||
let htmlName = "<div class='editorNode'>";
|
||||
if (item.IsFolder) {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon folder" aria-hidden="true"></span>';
|
||||
} else if (item.MediaType === 'Video') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon movie" aria-hidden="true"></span>';
|
||||
} else if (item.MediaType === 'Audio') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon audiotrack" aria-hidden="true"></span>';
|
||||
} else if (item.Type === 'TvChannel') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon live_tv" aria-hidden="true"></span>';
|
||||
} else if (item.MediaType === 'Photo') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon photo" aria-hidden="true"></span>';
|
||||
} else if (item.MediaType === 'Book') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon book" aria-hidden="true"></span>';
|
||||
}
|
||||
if (item.LockData) {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon lock" aria-hidden="true"></span>';
|
||||
}
|
||||
htmlName += escapeHtml(name);
|
||||
htmlName += '</div>';
|
||||
return htmlName;
|
||||
}
|
||||
|
||||
function loadChildrenOfRootNode(page, scope, callback) {
|
||||
ApiClient.getLiveTvChannels({
|
||||
limit: 0
|
||||
}).then(function (result) {
|
||||
const nodes = [];
|
||||
nodes.push({
|
||||
id: 'MediaFolders',
|
||||
text: globalize.translate('HeaderMediaFolders'),
|
||||
state: {
|
||||
opened: item.IsFolder && folderState == 'open',
|
||||
selected: selected
|
||||
opened: true
|
||||
},
|
||||
li_attr: {
|
||||
serveritemtype: item.Type,
|
||||
collectiontype: item.CollectionType
|
||||
}
|
||||
};
|
||||
if (item.IsFolder) {
|
||||
node.children = [{
|
||||
text: 'Loading...',
|
||||
icon: false
|
||||
}];
|
||||
node.icon = false;
|
||||
} else {
|
||||
node.icon = false;
|
||||
}
|
||||
if (node.state.opened) {
|
||||
node.li_attr.loadedFromServer = true;
|
||||
}
|
||||
if (selected) {
|
||||
selectedNodeId = item.Id;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function getNodeInnerHtml(item) {
|
||||
let name = item.Name;
|
||||
if (item.Number) {
|
||||
name = item.Number + ' - ' + name;
|
||||
}
|
||||
if (item.IndexNumber != null && item.Type != 'Season') {
|
||||
name = item.IndexNumber + ' - ' + name;
|
||||
}
|
||||
let htmlName = "<div class='editorNode'>";
|
||||
if (item.IsFolder) {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon folder" aria-hidden="true"></span>';
|
||||
} else if (item.MediaType === 'Video') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon movie" aria-hidden="true"></span>';
|
||||
} else if (item.MediaType === 'Audio') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon audiotrack" aria-hidden="true"></span>';
|
||||
} else if (item.Type === 'TvChannel') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon live_tv" aria-hidden="true"></span>';
|
||||
} else if (item.MediaType === 'Photo') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon photo" aria-hidden="true"></span>';
|
||||
} else if (item.MediaType === 'Book') {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon book" aria-hidden="true"></span>';
|
||||
}
|
||||
if (item.LockData) {
|
||||
htmlName += '<span class="material-icons metadataSidebarIcon lock" aria-hidden="true"></span>';
|
||||
}
|
||||
htmlName += escapeHtml(name);
|
||||
htmlName += '</div>';
|
||||
return htmlName;
|
||||
}
|
||||
|
||||
function loadChildrenOfRootNode(page, scope, callback) {
|
||||
ApiClient.getLiveTvChannels({
|
||||
limit: 0
|
||||
}).then(function (result) {
|
||||
const nodes = [];
|
||||
itemtype: 'mediafolders',
|
||||
loadedFromServer: true
|
||||
},
|
||||
icon: false
|
||||
});
|
||||
if (result.TotalRecordCount) {
|
||||
nodes.push({
|
||||
id: 'MediaFolders',
|
||||
text: globalize.translate('HeaderMediaFolders'),
|
||||
id: 'livetv',
|
||||
text: globalize.translate('LiveTV'),
|
||||
state: {
|
||||
opened: true
|
||||
opened: false
|
||||
},
|
||||
li_attr: {
|
||||
itemtype: 'mediafolders',
|
||||
loadedFromServer: true
|
||||
itemtype: 'livetv'
|
||||
},
|
||||
children: [{
|
||||
text: 'Loading...',
|
||||
icon: false
|
||||
}],
|
||||
icon: false
|
||||
});
|
||||
if (result.TotalRecordCount) {
|
||||
nodes.push({
|
||||
id: 'livetv',
|
||||
text: globalize.translate('LiveTV'),
|
||||
state: {
|
||||
opened: false
|
||||
},
|
||||
li_attr: {
|
||||
itemtype: 'livetv'
|
||||
},
|
||||
children: [{
|
||||
text: 'Loading...',
|
||||
icon: false
|
||||
}],
|
||||
icon: false
|
||||
});
|
||||
}
|
||||
callback.call(scope, nodes);
|
||||
nodesToLoad.push('MediaFolders');
|
||||
});
|
||||
}
|
||||
|
||||
function loadLiveTvChannels(service, openItems, callback) {
|
||||
ApiClient.getLiveTvChannels({
|
||||
ServiceName: service,
|
||||
AddCurrentProgram: false
|
||||
}).then(function (result) {
|
||||
const nodes = result.Items.map(function (i) {
|
||||
const state = openItems.indexOf(i.Id) == -1 ? 'closed' : 'open';
|
||||
return getNode(i, state, false);
|
||||
});
|
||||
callback(nodes);
|
||||
});
|
||||
}
|
||||
|
||||
function loadMediaFolders(page, scope, openItems, callback) {
|
||||
ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders')).then(function (result) {
|
||||
const nodes = result.Items.map(function (n) {
|
||||
const state = openItems.indexOf(n.Id) == -1 ? 'closed' : 'open';
|
||||
return getNode(n, state, false);
|
||||
});
|
||||
callback.call(scope, nodes);
|
||||
for (let i = 0, length = nodes.length; i < length; i++) {
|
||||
if (nodes[i].state.opened) {
|
||||
nodesToLoad.push(nodes[i].id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadNode(page, scope, node, openItems, selectedId, currentUser, callback) {
|
||||
const id = node.id;
|
||||
if (id == '#') {
|
||||
loadChildrenOfRootNode(page, scope, callback);
|
||||
return;
|
||||
}
|
||||
if (id == 'livetv') {
|
||||
loadLiveTvChannels(id, openItems, callback);
|
||||
return;
|
||||
}
|
||||
if (id == 'MediaFolders') {
|
||||
loadMediaFolders(page, scope, openItems, callback);
|
||||
return;
|
||||
}
|
||||
const query = {
|
||||
ParentId: id,
|
||||
Fields: 'Settings',
|
||||
IsVirtualUnaired: false,
|
||||
IsMissing: false,
|
||||
EnableTotalRecordCount: false,
|
||||
EnableImages: false,
|
||||
EnableUserData: false
|
||||
};
|
||||
const itemtype = node.li_attr.itemtype;
|
||||
if (itemtype != 'Season' && itemtype != 'Series') {
|
||||
query.SortBy = 'SortName';
|
||||
}
|
||||
ApiClient.getItems(Dashboard.getCurrentUserId(), query).then(function (result) {
|
||||
const nodes = result.Items.map(function (n) {
|
||||
const state = openItems.indexOf(n.Id) == -1 ? 'closed' : 'open';
|
||||
return getNode(n, state, n.Id == selectedId);
|
||||
});
|
||||
callback.call(scope, nodes);
|
||||
for (let i = 0, length = nodes.length; i < length; i++) {
|
||||
if (nodes[i].state.opened) {
|
||||
nodesToLoad.push(nodes[i].id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scrollToNode(id) {
|
||||
const elem = $('#' + id)[0];
|
||||
if (elem) {
|
||||
elem.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTree(page, currentUser, openItems, selectedId) {
|
||||
Promise.all([
|
||||
import('jstree'),
|
||||
import('jstree/dist/themes/default/style.css')
|
||||
]).then(() => {
|
||||
initializeTreeInternal(page, currentUser, openItems, selectedId);
|
||||
});
|
||||
}
|
||||
|
||||
function onNodeSelect(event, data) {
|
||||
const node = data.node;
|
||||
const eventData = {
|
||||
id: node.id,
|
||||
itemType: node.li_attr.itemtype,
|
||||
serverItemType: node.li_attr.serveritemtype,
|
||||
collectionType: node.li_attr.collectiontype
|
||||
};
|
||||
if (eventData.itemType != 'livetv' && eventData.itemType != 'mediafolders') {
|
||||
{
|
||||
this.dispatchEvent(new CustomEvent('itemclicked', {
|
||||
detail: eventData,
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
}));
|
||||
}
|
||||
document.querySelector('.editPageSidebar').classList.add('editPageSidebar-withcontent');
|
||||
} else {
|
||||
document.querySelector('.editPageSidebar').classList.remove('editPageSidebar-withcontent');
|
||||
}
|
||||
}
|
||||
|
||||
function onNodeOpen(_, data) {
|
||||
const page = $(this).parents('.page')[0];
|
||||
const node = data.node;
|
||||
if (node.children) {
|
||||
loadNodesToLoad(page, node);
|
||||
}
|
||||
if (node.li_attr && node.id != '#' && !node.li_attr.loadedFromServer) {
|
||||
node.li_attr.loadedFromServer = true;
|
||||
$.jstree.reference('.libraryTree', page).load_node(node.id, loadNodeCallback);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTreeInternal(page, currentUser, openItems, selectedId) {
|
||||
nodesToLoad = [];
|
||||
selectedNodeId = null;
|
||||
$.jstree.destroy();
|
||||
$('.libraryTree', page).jstree({
|
||||
'plugins': ['wholerow'],
|
||||
core: {
|
||||
check_callback: true,
|
||||
data: function (node, callback) {
|
||||
loadNode(page, this, node, openItems, selectedId, currentUser, callback);
|
||||
},
|
||||
themes: {
|
||||
variant: 'large'
|
||||
}
|
||||
}
|
||||
})
|
||||
.off('select_node.jstree', onNodeSelect)
|
||||
.on('select_node.jstree', onNodeSelect)
|
||||
.off('open_node.jstree', onNodeOpen)
|
||||
.on('open_node.jstree', onNodeOpen)
|
||||
.off('load_node.jstree', onNodeOpen)
|
||||
.on('load_node.jstree', onNodeOpen);
|
||||
}
|
||||
|
||||
function loadNodesToLoad(page, node) {
|
||||
const children = node.children;
|
||||
for (let i = 0, length = children.length; i < length; i++) {
|
||||
const child = children[i];
|
||||
if (nodesToLoad.indexOf(child) != -1) {
|
||||
nodesToLoad = nodesToLoad.filter(function (n) {
|
||||
return n != child;
|
||||
});
|
||||
$.jstree.reference('.libraryTree', page).load_node(child, loadNodeCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadNodeCallback(node) {
|
||||
if (selectedNodeId && node.children && node.children.indexOf(selectedNodeId) != -1) {
|
||||
setTimeout(function () {
|
||||
scrollToNode(selectedNodeId);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function updateEditorNode(page, item) {
|
||||
const elem = $('#' + item.Id + '>a', page)[0];
|
||||
if (elem == null) {
|
||||
return;
|
||||
}
|
||||
$('.editorNode', elem).remove();
|
||||
$(elem).append(getNodeInnerHtml(item));
|
||||
if (item.IsFolder) {
|
||||
const tree = jQuery.jstree._reference('.libraryTree');
|
||||
const currentNode = tree._get_node(null, false);
|
||||
tree.refresh(currentNode);
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentItemId(id) {
|
||||
itemId = id;
|
||||
}
|
||||
|
||||
function getCurrentItemId() {
|
||||
if (itemId) {
|
||||
return itemId;
|
||||
}
|
||||
return getParameterByName('id');
|
||||
}
|
||||
|
||||
let nodesToLoad = [];
|
||||
let selectedNodeId;
|
||||
$(document).on('itemsaved', '.metadataEditorPage', function (e, item) {
|
||||
updateEditorNode(this, item);
|
||||
}).on('pagebeforeshow', '.metadataEditorPage', function () {
|
||||
/* eslint-disable-next-line @babel/no-unused-expressions */
|
||||
import('../styles/metadataeditor.scss');
|
||||
}).on('pagebeforeshow', '.metadataEditorPage', function () {
|
||||
const page = this;
|
||||
Dashboard.getCurrentUser().then(function (user) {
|
||||
const id = getCurrentItemId();
|
||||
if (id) {
|
||||
ApiClient.getAncestorItems(id, user.Id).then(function (ancestors) {
|
||||
const ids = ancestors.map(function (i) {
|
||||
return i.Id;
|
||||
});
|
||||
initializeTree(page, user, ids, id);
|
||||
});
|
||||
} else {
|
||||
initializeTree(page, user, []);
|
||||
}
|
||||
});
|
||||
}).on('pagebeforehide', '.metadataEditorPage', function () {
|
||||
const page = this;
|
||||
$('.libraryTree', page)
|
||||
.off('select_node.jstree', onNodeSelect)
|
||||
.off('open_node.jstree', onNodeOpen)
|
||||
.off('load_node.jstree', onNodeOpen);
|
||||
callback.call(scope, nodes);
|
||||
nodesToLoad.push('MediaFolders');
|
||||
});
|
||||
let itemId;
|
||||
window.MetadataEditor = {
|
||||
getItemPromise: function () {
|
||||
const currentItemId = getCurrentItemId();
|
||||
if (currentItemId) {
|
||||
return ApiClient.getItem(Dashboard.getCurrentUserId(), currentItemId);
|
||||
}
|
||||
return ApiClient.getRootFolder(Dashboard.getCurrentUserId());
|
||||
},
|
||||
getCurrentItemId: getCurrentItemId,
|
||||
setCurrentItemId: setCurrentItemId
|
||||
};
|
||||
}
|
||||
|
||||
function loadLiveTvChannels(service, openItems, callback) {
|
||||
ApiClient.getLiveTvChannels({
|
||||
ServiceName: service,
|
||||
AddCurrentProgram: false
|
||||
}).then(function (result) {
|
||||
const nodes = result.Items.map(function (i) {
|
||||
const state = openItems.indexOf(i.Id) == -1 ? 'closed' : 'open';
|
||||
return getNode(i, state, false);
|
||||
});
|
||||
callback(nodes);
|
||||
});
|
||||
}
|
||||
|
||||
function loadMediaFolders(page, scope, openItems, callback) {
|
||||
ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders')).then(function (result) {
|
||||
const nodes = result.Items.map(function (n) {
|
||||
const state = openItems.indexOf(n.Id) == -1 ? 'closed' : 'open';
|
||||
return getNode(n, state, false);
|
||||
});
|
||||
callback.call(scope, nodes);
|
||||
for (let i = 0, length = nodes.length; i < length; i++) {
|
||||
if (nodes[i].state.opened) {
|
||||
nodesToLoad.push(nodes[i].id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadNode(page, scope, node, openItems, selectedId, currentUser, callback) {
|
||||
const id = node.id;
|
||||
if (id == '#') {
|
||||
loadChildrenOfRootNode(page, scope, callback);
|
||||
return;
|
||||
}
|
||||
if (id == 'livetv') {
|
||||
loadLiveTvChannels(id, openItems, callback);
|
||||
return;
|
||||
}
|
||||
if (id == 'MediaFolders') {
|
||||
loadMediaFolders(page, scope, openItems, callback);
|
||||
return;
|
||||
}
|
||||
const query = {
|
||||
ParentId: id,
|
||||
Fields: 'Settings',
|
||||
IsVirtualUnaired: false,
|
||||
IsMissing: false,
|
||||
EnableTotalRecordCount: false,
|
||||
EnableImages: false,
|
||||
EnableUserData: false
|
||||
};
|
||||
const itemtype = node.li_attr.itemtype;
|
||||
if (itemtype != 'Season' && itemtype != 'Series') {
|
||||
query.SortBy = 'SortName';
|
||||
}
|
||||
ApiClient.getItems(Dashboard.getCurrentUserId(), query).then(function (result) {
|
||||
const nodes = result.Items.map(function (n) {
|
||||
const state = openItems.indexOf(n.Id) == -1 ? 'closed' : 'open';
|
||||
return getNode(n, state, n.Id == selectedId);
|
||||
});
|
||||
callback.call(scope, nodes);
|
||||
for (let i = 0, length = nodes.length; i < length; i++) {
|
||||
if (nodes[i].state.opened) {
|
||||
nodesToLoad.push(nodes[i].id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scrollToNode(id) {
|
||||
const elem = $('#' + id)[0];
|
||||
if (elem) {
|
||||
elem.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTree(page, currentUser, openItems, selectedId) {
|
||||
Promise.all([
|
||||
import('jstree'),
|
||||
import('jstree/dist/themes/default/style.css')
|
||||
]).then(() => {
|
||||
initializeTreeInternal(page, currentUser, openItems, selectedId);
|
||||
});
|
||||
}
|
||||
|
||||
function onNodeSelect(event, data) {
|
||||
const node = data.node;
|
||||
const eventData = {
|
||||
id: node.id,
|
||||
itemType: node.li_attr.itemtype,
|
||||
serverItemType: node.li_attr.serveritemtype,
|
||||
collectionType: node.li_attr.collectiontype
|
||||
};
|
||||
if (eventData.itemType != 'livetv' && eventData.itemType != 'mediafolders') {
|
||||
{
|
||||
this.dispatchEvent(new CustomEvent('itemclicked', {
|
||||
detail: eventData,
|
||||
bubbles: true,
|
||||
cancelable: false
|
||||
}));
|
||||
}
|
||||
document.querySelector('.editPageSidebar').classList.add('editPageSidebar-withcontent');
|
||||
} else {
|
||||
document.querySelector('.editPageSidebar').classList.remove('editPageSidebar-withcontent');
|
||||
}
|
||||
}
|
||||
|
||||
function onNodeOpen(_, data) {
|
||||
const page = $(this).parents('.page')[0];
|
||||
const node = data.node;
|
||||
if (node.children) {
|
||||
loadNodesToLoad(page, node);
|
||||
}
|
||||
if (node.li_attr && node.id != '#' && !node.li_attr.loadedFromServer) {
|
||||
node.li_attr.loadedFromServer = true;
|
||||
$.jstree.reference('.libraryTree', page).load_node(node.id, loadNodeCallback);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTreeInternal(page, currentUser, openItems, selectedId) {
|
||||
nodesToLoad = [];
|
||||
selectedNodeId = null;
|
||||
$.jstree.destroy();
|
||||
$('.libraryTree', page).jstree({
|
||||
'plugins': ['wholerow'],
|
||||
core: {
|
||||
check_callback: true,
|
||||
data: function (node, callback) {
|
||||
loadNode(page, this, node, openItems, selectedId, currentUser, callback);
|
||||
},
|
||||
themes: {
|
||||
variant: 'large'
|
||||
}
|
||||
}
|
||||
})
|
||||
.off('select_node.jstree', onNodeSelect)
|
||||
.on('select_node.jstree', onNodeSelect)
|
||||
.off('open_node.jstree', onNodeOpen)
|
||||
.on('open_node.jstree', onNodeOpen)
|
||||
.off('load_node.jstree', onNodeOpen)
|
||||
.on('load_node.jstree', onNodeOpen);
|
||||
}
|
||||
|
||||
function loadNodesToLoad(page, node) {
|
||||
const children = node.children;
|
||||
for (let i = 0, length = children.length; i < length; i++) {
|
||||
const child = children[i];
|
||||
if (nodesToLoad.indexOf(child) != -1) {
|
||||
nodesToLoad = nodesToLoad.filter(function (n) {
|
||||
return n != child;
|
||||
});
|
||||
$.jstree.reference('.libraryTree', page).load_node(child, loadNodeCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadNodeCallback(node) {
|
||||
if (selectedNodeId && node.children && node.children.indexOf(selectedNodeId) != -1) {
|
||||
setTimeout(function () {
|
||||
scrollToNode(selectedNodeId);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function updateEditorNode(page, item) {
|
||||
const elem = $('#' + item.Id + '>a', page)[0];
|
||||
if (elem == null) {
|
||||
return;
|
||||
}
|
||||
$('.editorNode', elem).remove();
|
||||
$(elem).append(getNodeInnerHtml(item));
|
||||
if (item.IsFolder) {
|
||||
const tree = jQuery.jstree._reference('.libraryTree');
|
||||
const currentNode = tree._get_node(null, false);
|
||||
tree.refresh(currentNode);
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentItemId(id) {
|
||||
itemId = id;
|
||||
}
|
||||
|
||||
function getCurrentItemId() {
|
||||
if (itemId) {
|
||||
return itemId;
|
||||
}
|
||||
return getParameterByName('id');
|
||||
}
|
||||
|
||||
let nodesToLoad = [];
|
||||
let selectedNodeId;
|
||||
$(document).on('itemsaved', '.metadataEditorPage', function (e, item) {
|
||||
updateEditorNode(this, item);
|
||||
}).on('pagebeforeshow', '.metadataEditorPage', function () {
|
||||
/* eslint-disable-next-line @babel/no-unused-expressions */
|
||||
import('../styles/metadataeditor.scss');
|
||||
}).on('pagebeforeshow', '.metadataEditorPage', function () {
|
||||
const page = this;
|
||||
Dashboard.getCurrentUser().then(function (user) {
|
||||
const id = getCurrentItemId();
|
||||
if (id) {
|
||||
ApiClient.getAncestorItems(id, user.Id).then(function (ancestors) {
|
||||
const ids = ancestors.map(function (i) {
|
||||
return i.Id;
|
||||
});
|
||||
initializeTree(page, user, ids, id);
|
||||
});
|
||||
} else {
|
||||
initializeTree(page, user, []);
|
||||
}
|
||||
});
|
||||
}).on('pagebeforehide', '.metadataEditorPage', function () {
|
||||
const page = this;
|
||||
$('.libraryTree', page)
|
||||
.off('select_node.jstree', onNodeSelect)
|
||||
.off('open_node.jstree', onNodeOpen)
|
||||
.off('load_node.jstree', onNodeOpen);
|
||||
});
|
||||
let itemId;
|
||||
window.MetadataEditor = {
|
||||
getItemPromise: function () {
|
||||
const currentItemId = getCurrentItemId();
|
||||
if (currentItemId) {
|
||||
return ApiClient.getItem(Dashboard.getCurrentUserId(), currentItemId);
|
||||
}
|
||||
return ApiClient.getRootFolder(Dashboard.getCurrentUserId());
|
||||
},
|
||||
getCurrentItemId: getCurrentItemId,
|
||||
setCurrentItemId: setCurrentItemId
|
||||
};
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
|
|
@ -9,285 +9,283 @@ const Direction = {
|
|||
ltr: 'ltr'
|
||||
};
|
||||
|
||||
/* eslint-disable indent */
|
||||
const fallbackCulture = 'en-us';
|
||||
const RTL_LANGS = ['ar', 'fa', 'ur', 'he'];
|
||||
|
||||
const fallbackCulture = 'en-us';
|
||||
const RTL_LANGS = ['ar', 'fa', 'ur', 'he'];
|
||||
const allTranslations = {};
|
||||
let currentCulture;
|
||||
let currentDateTimeCulture;
|
||||
let isRTL = false;
|
||||
|
||||
const allTranslations = {};
|
||||
let currentCulture;
|
||||
let currentDateTimeCulture;
|
||||
let isRTL = false;
|
||||
export function getCurrentLocale() {
|
||||
return currentCulture;
|
||||
}
|
||||
|
||||
export function getCurrentLocale() {
|
||||
return currentCulture;
|
||||
export function getCurrentDateTimeLocale() {
|
||||
return currentDateTimeCulture;
|
||||
}
|
||||
|
||||
function getDefaultLanguage() {
|
||||
const culture = document.documentElement.getAttribute('data-culture');
|
||||
if (culture) {
|
||||
return culture;
|
||||
}
|
||||
|
||||
export function getCurrentDateTimeLocale() {
|
||||
return currentDateTimeCulture;
|
||||
if (navigator.language) {
|
||||
return navigator.language;
|
||||
}
|
||||
if (navigator.userLanguage) {
|
||||
return navigator.userLanguage;
|
||||
}
|
||||
if (navigator.languages && navigator.languages.length) {
|
||||
return navigator.languages[0];
|
||||
}
|
||||
|
||||
function getDefaultLanguage() {
|
||||
const culture = document.documentElement.getAttribute('data-culture');
|
||||
if (culture) {
|
||||
return culture;
|
||||
}
|
||||
return fallbackCulture;
|
||||
}
|
||||
|
||||
if (navigator.language) {
|
||||
return navigator.language;
|
||||
}
|
||||
if (navigator.userLanguage) {
|
||||
return navigator.userLanguage;
|
||||
}
|
||||
if (navigator.languages && navigator.languages.length) {
|
||||
return navigator.languages[0];
|
||||
}
|
||||
export function getIsRTL() {
|
||||
return isRTL;
|
||||
}
|
||||
|
||||
return fallbackCulture;
|
||||
function checkAndProcessDir(culture) {
|
||||
isRTL = false;
|
||||
console.log(culture);
|
||||
for (const lang of RTL_LANGS) {
|
||||
if (culture.includes(lang)) {
|
||||
isRTL = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function getIsRTL() {
|
||||
return isRTL;
|
||||
setDocumentDirection(isRTL ? Direction.rtl : Direction.ltr);
|
||||
}
|
||||
|
||||
function setDocumentDirection(direction) {
|
||||
document.getElementsByTagName('body')[0].setAttribute('dir', direction);
|
||||
document.getElementsByTagName('html')[0].setAttribute('dir', direction);
|
||||
if (direction === Direction.rtl)
|
||||
import('../styles/rtl.scss');
|
||||
}
|
||||
|
||||
export function getIsElementRTL(element) {
|
||||
if (window.getComputedStyle) { // all browsers
|
||||
return window.getComputedStyle(element, null).getPropertyValue('direction') == 'rtl';
|
||||
}
|
||||
return element.currentStyle.direction == 'rtl';
|
||||
}
|
||||
|
||||
export function updateCurrentCulture() {
|
||||
let culture;
|
||||
try {
|
||||
culture = userSettings.language();
|
||||
} catch (err) {
|
||||
console.error('no language set in user settings');
|
||||
}
|
||||
culture = culture || getDefaultLanguage();
|
||||
checkAndProcessDir(culture);
|
||||
|
||||
currentCulture = normalizeLocaleName(culture);
|
||||
|
||||
document.documentElement.setAttribute('lang', currentCulture);
|
||||
|
||||
let dateTimeCulture;
|
||||
try {
|
||||
dateTimeCulture = userSettings.dateTimeLocale();
|
||||
} catch (err) {
|
||||
console.error('no date format set in user settings');
|
||||
}
|
||||
|
||||
function checkAndProcessDir(culture) {
|
||||
isRTL = false;
|
||||
console.log(culture);
|
||||
for (const lang of RTL_LANGS) {
|
||||
if (culture.includes(lang)) {
|
||||
isRTL = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setDocumentDirection(isRTL ? Direction.rtl : Direction.ltr);
|
||||
if (dateTimeCulture) {
|
||||
currentDateTimeCulture = normalizeLocaleName(dateTimeCulture);
|
||||
} else {
|
||||
currentDateTimeCulture = currentCulture;
|
||||
}
|
||||
updateLocale(currentDateTimeCulture);
|
||||
|
||||
function setDocumentDirection(direction) {
|
||||
document.getElementsByTagName('body')[0].setAttribute('dir', direction);
|
||||
document.getElementsByTagName('html')[0].setAttribute('dir', direction);
|
||||
if (direction === Direction.rtl)
|
||||
import('../styles/rtl.scss');
|
||||
ensureTranslations(currentCulture);
|
||||
}
|
||||
|
||||
function ensureTranslations(culture) {
|
||||
for (const i in allTranslations) {
|
||||
ensureTranslation(allTranslations[i], culture);
|
||||
}
|
||||
|
||||
export function getIsElementRTL(element) {
|
||||
if (window.getComputedStyle) { // all browsers
|
||||
return window.getComputedStyle(element, null).getPropertyValue('direction') == 'rtl';
|
||||
}
|
||||
return element.currentStyle.direction == 'rtl';
|
||||
}
|
||||
|
||||
export function updateCurrentCulture() {
|
||||
let culture;
|
||||
try {
|
||||
culture = userSettings.language();
|
||||
} catch (err) {
|
||||
console.error('no language set in user settings');
|
||||
}
|
||||
culture = culture || getDefaultLanguage();
|
||||
checkAndProcessDir(culture);
|
||||
|
||||
currentCulture = normalizeLocaleName(culture);
|
||||
|
||||
document.documentElement.setAttribute('lang', currentCulture);
|
||||
|
||||
let dateTimeCulture;
|
||||
try {
|
||||
dateTimeCulture = userSettings.dateTimeLocale();
|
||||
} catch (err) {
|
||||
console.error('no date format set in user settings');
|
||||
}
|
||||
|
||||
if (dateTimeCulture) {
|
||||
currentDateTimeCulture = normalizeLocaleName(dateTimeCulture);
|
||||
} else {
|
||||
currentDateTimeCulture = currentCulture;
|
||||
}
|
||||
updateLocale(currentDateTimeCulture);
|
||||
|
||||
ensureTranslations(currentCulture);
|
||||
}
|
||||
|
||||
function ensureTranslations(culture) {
|
||||
if (culture !== fallbackCulture) {
|
||||
for (const i in allTranslations) {
|
||||
ensureTranslation(allTranslations[i], culture);
|
||||
}
|
||||
if (culture !== fallbackCulture) {
|
||||
for (const i in allTranslations) {
|
||||
ensureTranslation(allTranslations[i], fallbackCulture);
|
||||
}
|
||||
ensureTranslation(allTranslations[i], fallbackCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureTranslation(translationInfo, culture) {
|
||||
if (translationInfo.dictionaries[culture]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return loadTranslation(translationInfo.translations, culture).then(function (dictionary) {
|
||||
translationInfo.dictionaries[culture] = dictionary;
|
||||
});
|
||||
function ensureTranslation(translationInfo, culture) {
|
||||
if (translationInfo.dictionaries[culture]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function normalizeLocaleName(culture) {
|
||||
return culture.replace('_', '-').toLowerCase();
|
||||
return loadTranslation(translationInfo.translations, culture).then(function (dictionary) {
|
||||
translationInfo.dictionaries[culture] = dictionary;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeLocaleName(culture) {
|
||||
return culture.replace('_', '-').toLowerCase();
|
||||
}
|
||||
|
||||
function getDictionary(module, locale) {
|
||||
if (!module) {
|
||||
module = defaultModule();
|
||||
}
|
||||
|
||||
function getDictionary(module, locale) {
|
||||
if (!module) {
|
||||
module = defaultModule();
|
||||
}
|
||||
|
||||
const translations = allTranslations[module];
|
||||
if (!translations) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return translations.dictionaries[locale];
|
||||
const translations = allTranslations[module];
|
||||
if (!translations) {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function register(options) {
|
||||
allTranslations[options.name] = {
|
||||
translations: options.strings || options.translations,
|
||||
dictionaries: {}
|
||||
};
|
||||
return translations.dictionaries[locale];
|
||||
}
|
||||
|
||||
export function register(options) {
|
||||
allTranslations[options.name] = {
|
||||
translations: options.strings || options.translations,
|
||||
dictionaries: {}
|
||||
};
|
||||
}
|
||||
|
||||
export function loadStrings(options) {
|
||||
const locale = getCurrentLocale();
|
||||
const promises = [];
|
||||
let optionsName;
|
||||
if (typeof options === 'string') {
|
||||
optionsName = options;
|
||||
} else {
|
||||
optionsName = options.name;
|
||||
register(options);
|
||||
}
|
||||
promises.push(ensureTranslation(allTranslations[optionsName], locale));
|
||||
promises.push(ensureTranslation(allTranslations[optionsName], fallbackCulture));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
export function loadStrings(options) {
|
||||
const locale = getCurrentLocale();
|
||||
const promises = [];
|
||||
let optionsName;
|
||||
if (typeof options === 'string') {
|
||||
optionsName = options;
|
||||
} else {
|
||||
optionsName = options.name;
|
||||
register(options);
|
||||
}
|
||||
promises.push(ensureTranslation(allTranslations[optionsName], locale));
|
||||
promises.push(ensureTranslation(allTranslations[optionsName], fallbackCulture));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
function loadTranslation(translations, lang) {
|
||||
lang = normalizeLocaleName(lang);
|
||||
|
||||
function loadTranslation(translations, lang) {
|
||||
lang = normalizeLocaleName(lang);
|
||||
let filtered = translations.filter(function (t) {
|
||||
return normalizeLocaleName(t.lang) === lang;
|
||||
});
|
||||
|
||||
let filtered = translations.filter(function (t) {
|
||||
if (!filtered.length) {
|
||||
lang = lang.replace(/-.*/, '');
|
||||
|
||||
filtered = translations.filter(function (t) {
|
||||
return normalizeLocaleName(t.lang) === lang;
|
||||
});
|
||||
|
||||
if (!filtered.length) {
|
||||
lang = lang.replace(/-.*/, '');
|
||||
|
||||
filtered = translations.filter(function (t) {
|
||||
return normalizeLocaleName(t.lang) === lang;
|
||||
return normalizeLocaleName(t.lang) === fallbackCulture;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!filtered.length) {
|
||||
filtered = translations.filter(function (t) {
|
||||
return normalizeLocaleName(t.lang) === fallbackCulture;
|
||||
});
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
if (!filtered.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
if (!filtered.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const url = filtered[0].path;
|
||||
|
||||
const url = filtered[0].path;
|
||||
|
||||
import(/* webpackChunkName: "[request]" */ `../strings/${url}`).then((fileContent) => {
|
||||
resolve(fileContent);
|
||||
}).catch(() => {
|
||||
resolve({});
|
||||
});
|
||||
import(/* webpackChunkName: "[request]" */ `../strings/${url}`).then((fileContent) => {
|
||||
resolve(fileContent);
|
||||
}).catch(() => {
|
||||
resolve({});
|
||||
});
|
||||
}
|
||||
|
||||
function translateKey(key) {
|
||||
const parts = key.split('#');
|
||||
let module;
|
||||
|
||||
if (parts.length > 1) {
|
||||
module = parts[0];
|
||||
key = parts[1];
|
||||
}
|
||||
|
||||
return translateKeyFromModule(key, module);
|
||||
}
|
||||
|
||||
function translateKeyFromModule(key, module) {
|
||||
let dictionary = getDictionary(module, getCurrentLocale());
|
||||
if (dictionary && dictionary[key]) {
|
||||
return dictionary[key];
|
||||
}
|
||||
|
||||
dictionary = getDictionary(module, fallbackCulture);
|
||||
if (dictionary && dictionary[key]) {
|
||||
return dictionary[key];
|
||||
}
|
||||
|
||||
if (!dictionary || isEmpty(dictionary)) {
|
||||
console.warn('Translation dictionary is empty.');
|
||||
} else {
|
||||
console.error(`Translation key is missing from dictionary: ${key}`);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
export function translate(key) {
|
||||
let val = translateKey(key);
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
val = val.replaceAll('{' + (i - 1) + '}', arguments[i].toLocaleString(currentCulture));
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
export function translateHtml(html, module) {
|
||||
html = html.default || html;
|
||||
|
||||
if (!module) {
|
||||
module = defaultModule();
|
||||
}
|
||||
if (!module) {
|
||||
throw new Error('module cannot be null or empty');
|
||||
}
|
||||
|
||||
let startIndex = html.indexOf('${');
|
||||
if (startIndex === -1) {
|
||||
return html;
|
||||
}
|
||||
|
||||
startIndex += 2;
|
||||
const endIndex = html.indexOf('}', startIndex);
|
||||
if (endIndex === -1) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const key = html.substring(startIndex, endIndex);
|
||||
const val = translateKeyFromModule(key, module);
|
||||
|
||||
html = html.replace('${' + key + '}', val);
|
||||
return translateHtml(html, module);
|
||||
}
|
||||
|
||||
let _defaultModule;
|
||||
export function defaultModule(val) {
|
||||
if (val) {
|
||||
_defaultModule = val;
|
||||
}
|
||||
return _defaultModule;
|
||||
}
|
||||
|
||||
updateCurrentCulture();
|
||||
|
||||
Events.on(userSettings, 'change', function (e, name) {
|
||||
if (name === 'language' || name === 'datetimelocale') {
|
||||
updateCurrentCulture();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function translateKey(key) {
|
||||
const parts = key.split('#');
|
||||
let module;
|
||||
|
||||
if (parts.length > 1) {
|
||||
module = parts[0];
|
||||
key = parts[1];
|
||||
}
|
||||
|
||||
return translateKeyFromModule(key, module);
|
||||
}
|
||||
|
||||
function translateKeyFromModule(key, module) {
|
||||
let dictionary = getDictionary(module, getCurrentLocale());
|
||||
if (dictionary && dictionary[key]) {
|
||||
return dictionary[key];
|
||||
}
|
||||
|
||||
dictionary = getDictionary(module, fallbackCulture);
|
||||
if (dictionary && dictionary[key]) {
|
||||
return dictionary[key];
|
||||
}
|
||||
|
||||
if (!dictionary || isEmpty(dictionary)) {
|
||||
console.warn('Translation dictionary is empty.');
|
||||
} else {
|
||||
console.error(`Translation key is missing from dictionary: ${key}`);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
export function translate(key) {
|
||||
let val = translateKey(key);
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
val = val.replaceAll('{' + (i - 1) + '}', arguments[i].toLocaleString(currentCulture));
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
export function translateHtml(html, module) {
|
||||
html = html.default || html;
|
||||
|
||||
if (!module) {
|
||||
module = defaultModule();
|
||||
}
|
||||
if (!module) {
|
||||
throw new Error('module cannot be null or empty');
|
||||
}
|
||||
|
||||
let startIndex = html.indexOf('${');
|
||||
if (startIndex === -1) {
|
||||
return html;
|
||||
}
|
||||
|
||||
startIndex += 2;
|
||||
const endIndex = html.indexOf('}', startIndex);
|
||||
if (endIndex === -1) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const key = html.substring(startIndex, endIndex);
|
||||
const val = translateKeyFromModule(key, module);
|
||||
|
||||
html = html.replace('${' + key + '}', val);
|
||||
return translateHtml(html, module);
|
||||
}
|
||||
|
||||
let _defaultModule;
|
||||
export function defaultModule(val) {
|
||||
if (val) {
|
||||
_defaultModule = val;
|
||||
}
|
||||
return _defaultModule;
|
||||
}
|
||||
|
||||
updateCurrentCulture();
|
||||
|
||||
Events.on(userSettings, 'change', function (e, name) {
|
||||
if (name === 'language' || name === 'datetimelocale') {
|
||||
updateCurrentCulture();
|
||||
}
|
||||
});
|
||||
|
||||
export default {
|
||||
translate,
|
||||
|
@ -302,4 +300,3 @@ export default {
|
|||
getIsElementRTL
|
||||
};
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
|
|
@ -30,71 +30,67 @@ function getWebDeviceIcon(browser) {
|
|||
}
|
||||
}
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
export function getDeviceIcon(device) {
|
||||
switch (device.AppName || device.Client) {
|
||||
case 'Samsung Smart TV':
|
||||
return BASE_DEVICE_IMAGE_URL + 'samsung.svg';
|
||||
case 'Xbox One':
|
||||
return BASE_DEVICE_IMAGE_URL + 'xbox.svg';
|
||||
case 'Sony PS4':
|
||||
return BASE_DEVICE_IMAGE_URL + 'playstation.svg';
|
||||
case 'Kodi':
|
||||
case 'Kodi JellyCon':
|
||||
return BASE_DEVICE_IMAGE_URL + 'kodi.svg';
|
||||
case 'Jellyfin Android':
|
||||
case 'AndroidTV':
|
||||
case 'Android TV':
|
||||
return BASE_DEVICE_IMAGE_URL + 'android.svg';
|
||||
case 'Jellyfin Mobile (iOS)':
|
||||
case 'Jellyfin Mobile (iPadOS)':
|
||||
case 'Jellyfin iOS':
|
||||
case 'Infuse':
|
||||
return BASE_DEVICE_IMAGE_URL + 'apple.svg';
|
||||
case 'Home Assistant':
|
||||
return BASE_DEVICE_IMAGE_URL + 'home-assistant.svg';
|
||||
case 'Jellyfin Roku':
|
||||
return BASE_DEVICE_IMAGE_URL + 'roku.svg';
|
||||
case 'Finamp':
|
||||
return BASE_DEVICE_IMAGE_URL + 'finamp.svg';
|
||||
case 'Jellyfin Web':
|
||||
return getWebDeviceIcon(device.Name || device.DeviceName);
|
||||
default:
|
||||
return BASE_DEVICE_IMAGE_URL + 'other.svg';
|
||||
}
|
||||
export function getDeviceIcon(device) {
|
||||
switch (device.AppName || device.Client) {
|
||||
case 'Samsung Smart TV':
|
||||
return BASE_DEVICE_IMAGE_URL + 'samsung.svg';
|
||||
case 'Xbox One':
|
||||
return BASE_DEVICE_IMAGE_URL + 'xbox.svg';
|
||||
case 'Sony PS4':
|
||||
return BASE_DEVICE_IMAGE_URL + 'playstation.svg';
|
||||
case 'Kodi':
|
||||
case 'Kodi JellyCon':
|
||||
return BASE_DEVICE_IMAGE_URL + 'kodi.svg';
|
||||
case 'Jellyfin Android':
|
||||
case 'AndroidTV':
|
||||
case 'Android TV':
|
||||
return BASE_DEVICE_IMAGE_URL + 'android.svg';
|
||||
case 'Jellyfin Mobile (iOS)':
|
||||
case 'Jellyfin Mobile (iPadOS)':
|
||||
case 'Jellyfin iOS':
|
||||
case 'Infuse':
|
||||
return BASE_DEVICE_IMAGE_URL + 'apple.svg';
|
||||
case 'Home Assistant':
|
||||
return BASE_DEVICE_IMAGE_URL + 'home-assistant.svg';
|
||||
case 'Jellyfin Roku':
|
||||
return BASE_DEVICE_IMAGE_URL + 'roku.svg';
|
||||
case 'Finamp':
|
||||
return BASE_DEVICE_IMAGE_URL + 'finamp.svg';
|
||||
case 'Jellyfin Web':
|
||||
return getWebDeviceIcon(device.Name || device.DeviceName);
|
||||
default:
|
||||
return BASE_DEVICE_IMAGE_URL + 'other.svg';
|
||||
}
|
||||
}
|
||||
|
||||
export function getLibraryIcon(library) {
|
||||
switch (library) {
|
||||
case 'movies':
|
||||
return 'video_library';
|
||||
case 'music':
|
||||
return 'library_music';
|
||||
case 'photos':
|
||||
return 'photo_library';
|
||||
case 'livetv':
|
||||
return 'live_tv';
|
||||
case 'tvshows':
|
||||
return 'tv';
|
||||
case 'trailers':
|
||||
return 'local_movies';
|
||||
case 'homevideos':
|
||||
return 'photo_library';
|
||||
case 'musicvideos':
|
||||
return 'music_video';
|
||||
case 'books':
|
||||
return 'library_books';
|
||||
case 'channels':
|
||||
return 'videocam';
|
||||
case 'playlists':
|
||||
return 'view_list';
|
||||
default:
|
||||
return 'folder';
|
||||
}
|
||||
export function getLibraryIcon(library) {
|
||||
switch (library) {
|
||||
case 'movies':
|
||||
return 'video_library';
|
||||
case 'music':
|
||||
return 'library_music';
|
||||
case 'photos':
|
||||
return 'photo_library';
|
||||
case 'livetv':
|
||||
return 'live_tv';
|
||||
case 'tvshows':
|
||||
return 'tv';
|
||||
case 'trailers':
|
||||
return 'local_movies';
|
||||
case 'homevideos':
|
||||
return 'photo_library';
|
||||
case 'musicvideos':
|
||||
return 'music_video';
|
||||
case 'books':
|
||||
return 'library_books';
|
||||
case 'channels':
|
||||
return 'videocam';
|
||||
case 'playlists':
|
||||
return 'view_list';
|
||||
default:
|
||||
return 'folder';
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
}
|
||||
|
||||
export default {
|
||||
getDeviceIcon: getDeviceIcon,
|
||||
|
|
|
@ -4,256 +4,252 @@ import { appRouter } from '../components/appRouter';
|
|||
import dom from './dom';
|
||||
import { appHost } from '../components/apphost';
|
||||
|
||||
/* eslint-disable indent */
|
||||
let lastInputTime = new Date().getTime();
|
||||
|
||||
let lastInputTime = new Date().getTime();
|
||||
export function notify() {
|
||||
lastInputTime = new Date().getTime();
|
||||
handleCommand('unknown');
|
||||
}
|
||||
|
||||
export function notify() {
|
||||
lastInputTime = new Date().getTime();
|
||||
handleCommand('unknown');
|
||||
export function notifyMouseMove() {
|
||||
lastInputTime = new Date().getTime();
|
||||
}
|
||||
|
||||
export function idleTime() {
|
||||
return new Date().getTime() - lastInputTime;
|
||||
}
|
||||
|
||||
export function select(sourceElement) {
|
||||
sourceElement.click();
|
||||
}
|
||||
|
||||
let eventListenerCount = 0;
|
||||
export function on(scope, fn) {
|
||||
eventListenerCount++;
|
||||
dom.addEventListener(scope, 'command', fn, {});
|
||||
}
|
||||
|
||||
export function off(scope, fn) {
|
||||
if (eventListenerCount) {
|
||||
eventListenerCount--;
|
||||
}
|
||||
|
||||
export function notifyMouseMove() {
|
||||
lastInputTime = new Date().getTime();
|
||||
dom.removeEventListener(scope, 'command', fn, {});
|
||||
}
|
||||
|
||||
const commandTimes = {};
|
||||
|
||||
function checkCommandTime(command) {
|
||||
const last = commandTimes[command] || 0;
|
||||
const now = new Date().getTime();
|
||||
|
||||
if ((now - last) < 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function idleTime() {
|
||||
return new Date().getTime() - lastInputTime;
|
||||
commandTimes[command] = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function handleCommand(commandName, options) {
|
||||
lastInputTime = new Date().getTime();
|
||||
|
||||
let sourceElement = (options ? options.sourceElement : null);
|
||||
|
||||
if (sourceElement) {
|
||||
sourceElement = focusManager.focusableParent(sourceElement);
|
||||
}
|
||||
|
||||
export function select(sourceElement) {
|
||||
sourceElement.click();
|
||||
}
|
||||
if (!sourceElement) {
|
||||
sourceElement = document.activeElement || window;
|
||||
|
||||
let eventListenerCount = 0;
|
||||
export function on(scope, fn) {
|
||||
eventListenerCount++;
|
||||
dom.addEventListener(scope, 'command', fn, {});
|
||||
}
|
||||
const dialogs = document.querySelectorAll('.dialogContainer .dialog.opened');
|
||||
|
||||
export function off(scope, fn) {
|
||||
if (eventListenerCount) {
|
||||
eventListenerCount--;
|
||||
// Suppose the top open dialog is active
|
||||
const dlg = dialogs.length ? dialogs[dialogs.length - 1] : null;
|
||||
|
||||
if (dlg && !dlg.contains(sourceElement)) {
|
||||
sourceElement = dlg;
|
||||
}
|
||||
|
||||
dom.removeEventListener(scope, 'command', fn, {});
|
||||
}
|
||||
|
||||
const commandTimes = {};
|
||||
if (eventListenerCount) {
|
||||
const customEvent = new CustomEvent('command', {
|
||||
detail: {
|
||||
command: commandName
|
||||
},
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
|
||||
function checkCommandTime(command) {
|
||||
const last = commandTimes[command] || 0;
|
||||
const now = new Date().getTime();
|
||||
|
||||
if ((now - last) < 1000) {
|
||||
return false;
|
||||
const eventResult = sourceElement.dispatchEvent(customEvent);
|
||||
if (!eventResult) {
|
||||
// event cancelled
|
||||
return;
|
||||
}
|
||||
|
||||
commandTimes[command] = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function handleCommand(commandName, options) {
|
||||
lastInputTime = new Date().getTime();
|
||||
|
||||
let sourceElement = (options ? options.sourceElement : null);
|
||||
|
||||
if (sourceElement) {
|
||||
sourceElement = focusManager.focusableParent(sourceElement);
|
||||
}
|
||||
|
||||
if (!sourceElement) {
|
||||
sourceElement = document.activeElement || window;
|
||||
|
||||
const dialogs = document.querySelectorAll('.dialogContainer .dialog.opened');
|
||||
|
||||
// Suppose the top open dialog is active
|
||||
const dlg = dialogs.length ? dialogs[dialogs.length - 1] : null;
|
||||
|
||||
if (dlg && !dlg.contains(sourceElement)) {
|
||||
sourceElement = dlg;
|
||||
const keyActions = (command) => ({
|
||||
'up': () => {
|
||||
focusManager.moveUp(sourceElement);
|
||||
},
|
||||
'down': () => {
|
||||
focusManager.moveDown(sourceElement);
|
||||
},
|
||||
'left': () => {
|
||||
focusManager.moveLeft(sourceElement);
|
||||
},
|
||||
'right': () => {
|
||||
focusManager.moveRight(sourceElement);
|
||||
},
|
||||
'home': () => {
|
||||
appRouter.goHome();
|
||||
},
|
||||
'settings': () => {
|
||||
appRouter.showSettings();
|
||||
},
|
||||
'back': () => {
|
||||
if (appRouter.canGoBack()) {
|
||||
appRouter.back();
|
||||
} else if (appHost.supports('exit')) {
|
||||
appHost.exit();
|
||||
}
|
||||
}
|
||||
|
||||
if (eventListenerCount) {
|
||||
const customEvent = new CustomEvent('command', {
|
||||
detail: {
|
||||
command: commandName
|
||||
},
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
|
||||
const eventResult = sourceElement.dispatchEvent(customEvent);
|
||||
if (!eventResult) {
|
||||
// event cancelled
|
||||
return;
|
||||
},
|
||||
'select': () => {
|
||||
select(sourceElement);
|
||||
},
|
||||
'nextchapter': () => {
|
||||
playbackManager.nextChapter();
|
||||
},
|
||||
'next': () => {
|
||||
playbackManager.nextTrack();
|
||||
},
|
||||
'nexttrack': () => {
|
||||
playbackManager.nextTrack();
|
||||
},
|
||||
'previous': () => {
|
||||
playbackManager.previousTrack();
|
||||
},
|
||||
'previoustrack': () => {
|
||||
playbackManager.previousTrack();
|
||||
},
|
||||
'previouschapter': () => {
|
||||
playbackManager.previousChapter();
|
||||
},
|
||||
'guide': () => {
|
||||
appRouter.showGuide();
|
||||
},
|
||||
'recordedtv': () => {
|
||||
appRouter.showRecordedTV();
|
||||
},
|
||||
'livetv': () => {
|
||||
appRouter.showLiveTV();
|
||||
},
|
||||
'mute': () => {
|
||||
playbackManager.setMute(true);
|
||||
},
|
||||
'unmute': () => {
|
||||
playbackManager.setMute(false);
|
||||
},
|
||||
'togglemute': () => {
|
||||
playbackManager.toggleMute();
|
||||
},
|
||||
'channelup': () => {
|
||||
playbackManager.channelUp();
|
||||
},
|
||||
'channeldown': () => {
|
||||
playbackManager.channelDown();
|
||||
},
|
||||
'volumedown': () => {
|
||||
playbackManager.volumeDown();
|
||||
},
|
||||
'volumeup': () => {
|
||||
playbackManager.volumeUp();
|
||||
},
|
||||
'play': () => {
|
||||
playbackManager.unpause();
|
||||
},
|
||||
'pause': () => {
|
||||
playbackManager.pause();
|
||||
},
|
||||
'playpause': () => {
|
||||
playbackManager.playPause();
|
||||
},
|
||||
'stop': () => {
|
||||
if (checkCommandTime('stop')) {
|
||||
playbackManager.stop();
|
||||
}
|
||||
},
|
||||
'changezoom': () => {
|
||||
playbackManager.toggleAspectRatio();
|
||||
},
|
||||
'increaseplaybackrate': () => {
|
||||
playbackManager.increasePlaybackRate();
|
||||
},
|
||||
'decreaseplaybackrate': () => {
|
||||
playbackManager.decreasePlaybackRate();
|
||||
},
|
||||
'changeaudiotrack': () => {
|
||||
playbackManager.changeAudioStream();
|
||||
},
|
||||
'changesubtitletrack': () => {
|
||||
playbackManager.changeSubtitleStream();
|
||||
},
|
||||
'search': () => {
|
||||
appRouter.showSearch();
|
||||
},
|
||||
'favorites': () => {
|
||||
appRouter.showFavorites();
|
||||
},
|
||||
'fastforward': () => {
|
||||
playbackManager.fastForward();
|
||||
},
|
||||
'rewind': () => {
|
||||
playbackManager.rewind();
|
||||
},
|
||||
'seek': () => {
|
||||
playbackManager.seekMs(options);
|
||||
},
|
||||
'togglefullscreen': () => {
|
||||
playbackManager.toggleFullscreen();
|
||||
},
|
||||
'disabledisplaymirror': () => {
|
||||
playbackManager.enableDisplayMirroring(false);
|
||||
},
|
||||
'enabledisplaymirror': () => {
|
||||
playbackManager.enableDisplayMirroring(true);
|
||||
},
|
||||
'toggledisplaymirror': () => {
|
||||
playbackManager.toggleDisplayMirroring();
|
||||
},
|
||||
'nowplaying': () => {
|
||||
appRouter.showNowPlaying();
|
||||
},
|
||||
'repeatnone': () => {
|
||||
playbackManager.setRepeatMode('RepeatNone');
|
||||
},
|
||||
'repeatall': () => {
|
||||
playbackManager.setRepeatMode('RepeatAll');
|
||||
},
|
||||
'repeatone': () => {
|
||||
playbackManager.setRepeatMode('RepeatOne');
|
||||
},
|
||||
'unknown': () => {
|
||||
// This is the command given by 'notify', it's a no-op
|
||||
}
|
||||
})[command];
|
||||
|
||||
const keyActions = (command) => ({
|
||||
'up': () => {
|
||||
focusManager.moveUp(sourceElement);
|
||||
},
|
||||
'down': () => {
|
||||
focusManager.moveDown(sourceElement);
|
||||
},
|
||||
'left': () => {
|
||||
focusManager.moveLeft(sourceElement);
|
||||
},
|
||||
'right': () => {
|
||||
focusManager.moveRight(sourceElement);
|
||||
},
|
||||
'home': () => {
|
||||
appRouter.goHome();
|
||||
},
|
||||
'settings': () => {
|
||||
appRouter.showSettings();
|
||||
},
|
||||
'back': () => {
|
||||
if (appRouter.canGoBack()) {
|
||||
appRouter.back();
|
||||
} else if (appHost.supports('exit')) {
|
||||
appHost.exit();
|
||||
}
|
||||
},
|
||||
'select': () => {
|
||||
select(sourceElement);
|
||||
},
|
||||
'nextchapter': () => {
|
||||
playbackManager.nextChapter();
|
||||
},
|
||||
'next': () => {
|
||||
playbackManager.nextTrack();
|
||||
},
|
||||
'nexttrack': () => {
|
||||
playbackManager.nextTrack();
|
||||
},
|
||||
'previous': () => {
|
||||
playbackManager.previousTrack();
|
||||
},
|
||||
'previoustrack': () => {
|
||||
playbackManager.previousTrack();
|
||||
},
|
||||
'previouschapter': () => {
|
||||
playbackManager.previousChapter();
|
||||
},
|
||||
'guide': () => {
|
||||
appRouter.showGuide();
|
||||
},
|
||||
'recordedtv': () => {
|
||||
appRouter.showRecordedTV();
|
||||
},
|
||||
'livetv': () => {
|
||||
appRouter.showLiveTV();
|
||||
},
|
||||
'mute': () => {
|
||||
playbackManager.setMute(true);
|
||||
},
|
||||
'unmute': () => {
|
||||
playbackManager.setMute(false);
|
||||
},
|
||||
'togglemute': () => {
|
||||
playbackManager.toggleMute();
|
||||
},
|
||||
'channelup': () => {
|
||||
playbackManager.channelUp();
|
||||
},
|
||||
'channeldown': () => {
|
||||
playbackManager.channelDown();
|
||||
},
|
||||
'volumedown': () => {
|
||||
playbackManager.volumeDown();
|
||||
},
|
||||
'volumeup': () => {
|
||||
playbackManager.volumeUp();
|
||||
},
|
||||
'play': () => {
|
||||
playbackManager.unpause();
|
||||
},
|
||||
'pause': () => {
|
||||
playbackManager.pause();
|
||||
},
|
||||
'playpause': () => {
|
||||
playbackManager.playPause();
|
||||
},
|
||||
'stop': () => {
|
||||
if (checkCommandTime('stop')) {
|
||||
playbackManager.stop();
|
||||
}
|
||||
},
|
||||
'changezoom': () => {
|
||||
playbackManager.toggleAspectRatio();
|
||||
},
|
||||
'increaseplaybackrate': () => {
|
||||
playbackManager.increasePlaybackRate();
|
||||
},
|
||||
'decreaseplaybackrate': () => {
|
||||
playbackManager.decreasePlaybackRate();
|
||||
},
|
||||
'changeaudiotrack': () => {
|
||||
playbackManager.changeAudioStream();
|
||||
},
|
||||
'changesubtitletrack': () => {
|
||||
playbackManager.changeSubtitleStream();
|
||||
},
|
||||
'search': () => {
|
||||
appRouter.showSearch();
|
||||
},
|
||||
'favorites': () => {
|
||||
appRouter.showFavorites();
|
||||
},
|
||||
'fastforward': () => {
|
||||
playbackManager.fastForward();
|
||||
},
|
||||
'rewind': () => {
|
||||
playbackManager.rewind();
|
||||
},
|
||||
'seek': () => {
|
||||
playbackManager.seekMs(options);
|
||||
},
|
||||
'togglefullscreen': () => {
|
||||
playbackManager.toggleFullscreen();
|
||||
},
|
||||
'disabledisplaymirror': () => {
|
||||
playbackManager.enableDisplayMirroring(false);
|
||||
},
|
||||
'enabledisplaymirror': () => {
|
||||
playbackManager.enableDisplayMirroring(true);
|
||||
},
|
||||
'toggledisplaymirror': () => {
|
||||
playbackManager.toggleDisplayMirroring();
|
||||
},
|
||||
'nowplaying': () => {
|
||||
appRouter.showNowPlaying();
|
||||
},
|
||||
'repeatnone': () => {
|
||||
playbackManager.setRepeatMode('RepeatNone');
|
||||
},
|
||||
'repeatall': () => {
|
||||
playbackManager.setRepeatMode('RepeatAll');
|
||||
},
|
||||
'repeatone': () => {
|
||||
playbackManager.setRepeatMode('RepeatOne');
|
||||
},
|
||||
'unknown': () => {
|
||||
// This is the command given by 'notify', it's a no-op
|
||||
}
|
||||
})[command];
|
||||
|
||||
const action = keyActions(commandName);
|
||||
if (action !== undefined) {
|
||||
action.call();
|
||||
} else {
|
||||
console.debug(`inputManager: tried to process command with no action assigned: ${commandName}`);
|
||||
}
|
||||
const action = keyActions(commandName);
|
||||
if (action !== undefined) {
|
||||
action.call();
|
||||
} else {
|
||||
console.debug(`inputManager: tried to process command with no action assigned: ${commandName}`);
|
||||
}
|
||||
}
|
||||
|
||||
dom.addEventListener(document, 'click', notify, {
|
||||
passive: true
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
||||
dom.addEventListener(document, 'click', notify, {
|
||||
passive: true
|
||||
});
|
||||
|
||||
export default {
|
||||
handleCommand: handleCommand,
|
||||
|
|
|
@ -55,7 +55,7 @@ export function showLayoutMenu (button, currentLayout, views) {
|
|||
};
|
||||
});
|
||||
|
||||
import('../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
||||
import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: button,
|
||||
|
@ -122,7 +122,7 @@ export function showSortMenu (options) {
|
|||
Promise.all([
|
||||
import('../components/dialogHelper/dialogHelper'),
|
||||
import('../elements/emby-radio/emby-radio')
|
||||
]).then(([{default: dialogHelper}]) => {
|
||||
]).then(([{ default: dialogHelper }]) => {
|
||||
function onSortByChange() {
|
||||
const newValue = this.value;
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,169 +4,166 @@ import browser from './browser';
|
|||
import layoutManager from '../components/layoutManager';
|
||||
import dom from './dom';
|
||||
import Events from '../utils/events.ts';
|
||||
/* eslint-disable indent */
|
||||
|
||||
const self = {};
|
||||
const self = {};
|
||||
|
||||
let lastMouseInputTime = new Date().getTime();
|
||||
let isMouseIdle;
|
||||
let lastMouseInputTime = new Date().getTime();
|
||||
let isMouseIdle;
|
||||
|
||||
function mouseIdleTime() {
|
||||
return new Date().getTime() - lastMouseInputTime;
|
||||
function mouseIdleTime() {
|
||||
return new Date().getTime() - lastMouseInputTime;
|
||||
}
|
||||
|
||||
function notifyApp() {
|
||||
inputManager.notifyMouseMove();
|
||||
}
|
||||
|
||||
function removeIdleClasses() {
|
||||
const classList = document.body.classList;
|
||||
|
||||
classList.remove('mouseIdle');
|
||||
classList.remove('mouseIdle-tv');
|
||||
}
|
||||
|
||||
function addIdleClasses() {
|
||||
const classList = document.body.classList;
|
||||
|
||||
classList.add('mouseIdle');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
classList.add('mouseIdle-tv');
|
||||
}
|
||||
}
|
||||
|
||||
function notifyApp() {
|
||||
inputManager.notifyMouseMove();
|
||||
}
|
||||
|
||||
function removeIdleClasses() {
|
||||
const classList = document.body.classList;
|
||||
|
||||
classList.remove('mouseIdle');
|
||||
classList.remove('mouseIdle-tv');
|
||||
}
|
||||
|
||||
function addIdleClasses() {
|
||||
const classList = document.body.classList;
|
||||
|
||||
classList.add('mouseIdle');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
classList.add('mouseIdle-tv');
|
||||
}
|
||||
}
|
||||
|
||||
export function showCursor() {
|
||||
if (isMouseIdle) {
|
||||
isMouseIdle = false;
|
||||
removeIdleClasses();
|
||||
Events.trigger(self, 'mouseactive');
|
||||
}
|
||||
}
|
||||
|
||||
export function hideCursor() {
|
||||
if (!isMouseIdle) {
|
||||
isMouseIdle = true;
|
||||
addIdleClasses();
|
||||
Events.trigger(self, 'mouseidle');
|
||||
}
|
||||
}
|
||||
|
||||
let lastPointerMoveData;
|
||||
function onPointerMove(e) {
|
||||
const eventX = e.screenX || e.clientX;
|
||||
const eventY = e.screenY || e.clientY;
|
||||
|
||||
// if coord don't exist how could it move
|
||||
if (typeof eventX === 'undefined' && typeof eventY === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const obj = lastPointerMoveData;
|
||||
if (!obj) {
|
||||
lastPointerMoveData = {
|
||||
x: eventX,
|
||||
y: eventY
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// if coord are same, it didn't move
|
||||
if (Math.abs(eventX - obj.x) < 10 && Math.abs(eventY - obj.y) < 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
obj.x = eventX;
|
||||
obj.y = eventY;
|
||||
|
||||
lastMouseInputTime = new Date().getTime();
|
||||
notifyApp();
|
||||
|
||||
showCursor();
|
||||
}
|
||||
|
||||
function onPointerEnter(e) {
|
||||
const pointerType = e.pointerType || (layoutManager.mobile ? 'touch' : 'mouse');
|
||||
|
||||
if (pointerType === 'mouse' && !isMouseIdle) {
|
||||
const parent = focusManager.focusableParent(e.target);
|
||||
if (parent) {
|
||||
focusManager.focus(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enableFocusWithMouse() {
|
||||
if (!layoutManager.tv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (browser.web0s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!browser.tv;
|
||||
}
|
||||
|
||||
function onMouseInterval() {
|
||||
if (!isMouseIdle && mouseIdleTime() >= 5000) {
|
||||
hideCursor();
|
||||
}
|
||||
}
|
||||
|
||||
let mouseInterval;
|
||||
function startMouseInterval() {
|
||||
if (!mouseInterval) {
|
||||
mouseInterval = setInterval(onMouseInterval, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopMouseInterval() {
|
||||
const interval = mouseInterval;
|
||||
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
mouseInterval = null;
|
||||
}
|
||||
|
||||
export function showCursor() {
|
||||
if (isMouseIdle) {
|
||||
isMouseIdle = false;
|
||||
removeIdleClasses();
|
||||
Events.trigger(self, 'mouseactive');
|
||||
}
|
||||
}
|
||||
|
||||
export function hideCursor() {
|
||||
if (!isMouseIdle) {
|
||||
isMouseIdle = true;
|
||||
addIdleClasses();
|
||||
Events.trigger(self, 'mouseidle');
|
||||
}
|
||||
}
|
||||
|
||||
let lastPointerMoveData;
|
||||
function onPointerMove(e) {
|
||||
const eventX = e.screenX || e.clientX;
|
||||
const eventY = e.screenY || e.clientY;
|
||||
|
||||
// if coord don't exist how could it move
|
||||
if (typeof eventX === 'undefined' && typeof eventY === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
function initMouse() {
|
||||
stopMouseInterval();
|
||||
const obj = lastPointerMoveData;
|
||||
if (!obj) {
|
||||
lastPointerMoveData = {
|
||||
x: eventX,
|
||||
y: eventY
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
dom.removeEventListener(document, (window.PointerEvent ? 'pointermove' : 'mousemove'), onPointerMove, {
|
||||
// if coord are same, it didn't move
|
||||
if (Math.abs(eventX - obj.x) < 10 && Math.abs(eventY - obj.y) < 10) {
|
||||
return;
|
||||
}
|
||||
|
||||
obj.x = eventX;
|
||||
obj.y = eventY;
|
||||
|
||||
lastMouseInputTime = new Date().getTime();
|
||||
notifyApp();
|
||||
|
||||
showCursor();
|
||||
}
|
||||
|
||||
function onPointerEnter(e) {
|
||||
const pointerType = e.pointerType || (layoutManager.mobile ? 'touch' : 'mouse');
|
||||
|
||||
if (pointerType === 'mouse' && !isMouseIdle) {
|
||||
const parent = focusManager.focusableParent(e.target);
|
||||
if (parent) {
|
||||
focusManager.focus(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enableFocusWithMouse() {
|
||||
if (!layoutManager.tv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (browser.web0s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!browser.tv;
|
||||
}
|
||||
|
||||
function onMouseInterval() {
|
||||
if (!isMouseIdle && mouseIdleTime() >= 5000) {
|
||||
hideCursor();
|
||||
}
|
||||
}
|
||||
|
||||
let mouseInterval;
|
||||
function startMouseInterval() {
|
||||
if (!mouseInterval) {
|
||||
mouseInterval = setInterval(onMouseInterval, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopMouseInterval() {
|
||||
const interval = mouseInterval;
|
||||
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
mouseInterval = null;
|
||||
}
|
||||
|
||||
removeIdleClasses();
|
||||
}
|
||||
|
||||
function initMouse() {
|
||||
stopMouseInterval();
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
dom.removeEventListener(document, (window.PointerEvent ? 'pointermove' : 'mousemove'), onPointerMove, {
|
||||
passive: true
|
||||
});
|
||||
|
||||
if (!layoutManager.mobile) {
|
||||
startMouseInterval();
|
||||
|
||||
dom.addEventListener(document, (window.PointerEvent ? 'pointermove' : 'mousemove'), onPointerMove, {
|
||||
passive: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!layoutManager.mobile) {
|
||||
startMouseInterval();
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
dom.removeEventListener(document, (window.PointerEvent ? 'pointerenter' : 'mouseenter'), onPointerEnter, {
|
||||
capture: true,
|
||||
passive: true
|
||||
});
|
||||
|
||||
dom.addEventListener(document, (window.PointerEvent ? 'pointermove' : 'mousemove'), onPointerMove, {
|
||||
passive: true
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
dom.removeEventListener(document, (window.PointerEvent ? 'pointerenter' : 'mouseenter'), onPointerEnter, {
|
||||
if (enableFocusWithMouse()) {
|
||||
dom.addEventListener(document, (window.PointerEvent ? 'pointerenter' : 'mouseenter'), onPointerEnter, {
|
||||
capture: true,
|
||||
passive: true
|
||||
});
|
||||
|
||||
if (enableFocusWithMouse()) {
|
||||
dom.addEventListener(document, (window.PointerEvent ? 'pointerenter' : 'mouseenter'), onPointerEnter, {
|
||||
capture: true,
|
||||
passive: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initMouse();
|
||||
initMouse();
|
||||
|
||||
Events.on(layoutManager, 'modechange', initMouse);
|
||||
|
||||
/* eslint-enable indent */
|
||||
Events.on(layoutManager, 'modechange', initMouse);
|
||||
|
||||
export default {
|
||||
hideCursor,
|
||||
|
|
|
@ -189,7 +189,7 @@ export default function (view) {
|
|||
reloadItems();
|
||||
});
|
||||
view.querySelector('.btnNewPlaylist').addEventListener('click', function () {
|
||||
import('../components/playlisteditor/playlisteditor').then(({default: playlistEditor}) => {
|
||||
import('../components/playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
|
||||
const serverId = ApiClient.serverInfo().Id;
|
||||
new playlistEditor({
|
||||
items: [],
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { playbackManager } from '../components/playback/playbackmanager';
|
||||
import SyncPlay from '../plugins/syncPlay/core';
|
||||
import { pluginManager } from '../components/pluginManager';
|
||||
import inputManager from '../scripts/inputManager';
|
||||
import focusManager from '../components/focusManager';
|
||||
import { appRouter } from '../components/appRouter';
|
||||
|
@ -7,6 +7,7 @@ import ServerConnections from '../components/ServerConnections';
|
|||
import toast from '../components/toast/toast';
|
||||
import alert from '../components/alert';
|
||||
import Events from '../utils/events.ts';
|
||||
import { PluginType } from '../types/plugin.ts';
|
||||
|
||||
const serverNotifications = {};
|
||||
|
||||
|
@ -140,6 +141,8 @@ function processGeneralCommand(cmd, apiClient) {
|
|||
|
||||
function onMessageReceived(e, msg) {
|
||||
const apiClient = this;
|
||||
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
|
||||
|
||||
if (msg.MessageType === 'Play') {
|
||||
notifyApp();
|
||||
const serverId = apiClient.serverInfo().Id;
|
||||
|
@ -186,9 +189,9 @@ function onMessageReceived(e, msg) {
|
|||
}
|
||||
}
|
||||
} else if (msg.MessageType === 'SyncPlayCommand') {
|
||||
SyncPlay.Manager.processCommand(msg.Data, apiClient);
|
||||
SyncPlay?.Manager.processCommand(msg.Data, apiClient);
|
||||
} else if (msg.MessageType === 'SyncPlayGroupUpdate') {
|
||||
SyncPlay.Manager.processGroupUpdate(msg.Data, apiClient);
|
||||
SyncPlay?.Manager.processGroupUpdate(msg.Data, apiClient);
|
||||
} else {
|
||||
Events.trigger(serverNotifications, msg.MessageType, [apiClient, msg.Data]);
|
||||
}
|
||||
|
|
|
@ -1,44 +1,7 @@
|
|||
import DefaultConfig from '../../config.json';
|
||||
import fetchLocal from '../../utils/fetchLocal.ts';
|
||||
|
||||
let data;
|
||||
const urlResolver = document.createElement('a');
|
||||
|
||||
// `fetch` with `file:` support
|
||||
// Recent browsers seem to support `file` protocol under some conditions.
|
||||
// Based on https://github.com/github/fetch/pull/92#issuecomment-174730593
|
||||
// https://github.com/github/fetch/pull/92#issuecomment-512187452
|
||||
async function fetchLocal(url, options) {
|
||||
urlResolver.href = url;
|
||||
|
||||
const requestURL = urlResolver.href;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest;
|
||||
|
||||
xhr.onload = () => {
|
||||
// `file` protocol has invalid OK status
|
||||
let status = xhr.status;
|
||||
if (requestURL.startsWith('file:') && status === 0) {
|
||||
status = 200;
|
||||
}
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
resolve(new Response(xhr.responseText, {status: status}));
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
reject(new TypeError('Local request failed'));
|
||||
};
|
||||
|
||||
xhr.open('GET', url);
|
||||
|
||||
if (options && options.cache) {
|
||||
xhr.setRequestHeader('Cache-Control', options.cache);
|
||||
}
|
||||
|
||||
xhr.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
async function getConfig() {
|
||||
if (data) return Promise.resolve(data);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue