mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2026-01-15 16:33:35 -03:00
Merge branch 'master' into audio-normalization
This commit is contained in:
@@ -6,7 +6,6 @@
|
||||
"./dist/libraries/pdf.worker.js",
|
||||
"./dist/libraries/worker-bundle.js",
|
||||
"./dist/libraries/wasm-gen/libarchive.js",
|
||||
"./dist/node_modules.@jellyfin.libass-wasm.*.chunk.js",
|
||||
"./dist/serviceworker.js"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -64,7 +64,9 @@ module.exports = {
|
||||
'no-var': ['error'],
|
||||
'no-void': ['error', { 'allowAsStatement': true }],
|
||||
'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'one-var': ['error', 'never'],
|
||||
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
|
||||
'padded-blocks': ['error', 'never'],
|
||||
'prefer-const': ['error', { 'destructuring': 'all' }],
|
||||
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
|
||||
@@ -267,7 +269,6 @@ module.exports = {
|
||||
'no-useless-constructor': ['off'],
|
||||
'@typescript-eslint/no-useless-constructor': ['error'],
|
||||
|
||||
'max-params': ['error', 7],
|
||||
'sonarjs/cognitive-complexity': ['warn']
|
||||
}
|
||||
}
|
||||
|
||||
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -1,6 +1,2 @@
|
||||
.ci @dkanada @EraYaN
|
||||
* @jellyfin/web
|
||||
.github @jellyfin/core
|
||||
fedora @joshuaboniface
|
||||
debian @joshuaboniface
|
||||
.copr @joshuaboniface
|
||||
deployment @joshuaboniface
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -19,13 +19,13 @@ jobs:
|
||||
language: [ 'javascript' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7
|
||||
uses: github/codeql-action/init@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7
|
||||
uses: github/codeql-action/autobuild@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2.2.7
|
||||
uses: github/codeql-action/analyze@b2c19fb9a2a485599ccf4ed5d65527d94bc57226 # v2.3.0
|
||||
|
||||
12
.github/workflows/commands.yml
vendored
12
.github/workflows/commands.yml
vendored
@@ -12,13 +12,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
|
||||
uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -26,3 +26,11 @@ jobs:
|
||||
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
- name: Comment on failure
|
||||
if: failure()
|
||||
uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
I'm sorry @${{ github.event.comment.user.login }}, I'm afraid I can't do that.
|
||||
|
||||
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
|
||||
4
.github/workflows/repo-stale.yaml
vendored
4
.github/workflows/repo-stale.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
operations-per-run: 75
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
operations-per-run: 75
|
||||
|
||||
2
.github/workflows/tsc.yml
vendored
2
.github/workflows/tsc.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3.4.0
|
||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
||||
|
||||
- name: Setup node environment
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
- [Rob Farraher](https://github.com/farraherbg)
|
||||
- [TelepathicWalrus](https://github.com/TelepathicWalrus)
|
||||
- [Pier-Luc Ducharme](https://github.com/pl-ducharme)
|
||||
- [Anantharaju S](https://github.com/Anantharajus)
|
||||
- [Merlin Sievers](https://github.com/dann-merlin)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -12,14 +12,7 @@ module.exports = {
|
||||
corejs: 3
|
||||
}
|
||||
],
|
||||
'@babel/preset-react',
|
||||
[
|
||||
'@babel/preset-typescript',
|
||||
{
|
||||
isTSX: true,
|
||||
allExtensions: true
|
||||
}
|
||||
]
|
||||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM centos:8
|
||||
FROM quay.io/centos/centos:stream8
|
||||
|
||||
# Docker build arguments
|
||||
ARG SOURCE_DIR=/jellyfin
|
||||
@@ -12,7 +12,7 @@ ENV IS_DOCKER=YES
|
||||
# Prepare CentOS environment
|
||||
RUN yum update -y \
|
||||
&& yum install -y epel-release \
|
||||
&& yum install -y @buildsys-build rpmdevtools git yum-plugins-core autoconf automake glibc-devel gcc-c++ make \
|
||||
&& yum install -y rpmdevtools git autoconf automake glibc-devel gcc-c++ make \
|
||||
&& curl -fsSL https://rpm.nodesource.com/setup_16.x | bash - \
|
||||
&& yum install -y nodejs
|
||||
|
||||
|
||||
5745
package-lock.json
generated
5745
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
72
package.json
72
package.json
@@ -5,24 +5,23 @@
|
||||
"repository": "https://github.com/jellyfin/jellyfin-web",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.0",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@babel/core": "7.21.4",
|
||||
"@babel/eslint-parser": "7.21.3",
|
||||
"@babel/eslint-plugin": "7.19.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.18.6",
|
||||
"@babel/plugin-proposal-private-methods": "7.18.6",
|
||||
"@babel/plugin-transform-modules-umd": "7.18.6",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/preset-env": "7.21.4",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.21.0",
|
||||
"@types/escape-html": "1.0.2",
|
||||
"@types/loadable__component": "5.13.4",
|
||||
"@types/lodash-es": "4.17.6",
|
||||
"@types/react": "17.0.53",
|
||||
"@types/lodash-es": "4.17.7",
|
||||
"@types/react": "17.0.58",
|
||||
"@types/react-dom": "17.0.19",
|
||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||
"@typescript-eslint/parser": "5.54.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.58.0",
|
||||
"@typescript-eslint/parser": "5.58.0",
|
||||
"@uupaa/dynamic-import-polyfill": "1.0.2",
|
||||
"autoprefixer": "10.4.13",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.2",
|
||||
"babel-plugin-dynamic-import-polyfill": "1.0.0",
|
||||
"clean-webpack-plugin": "4.0.0",
|
||||
@@ -30,39 +29,39 @@
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "6.7.3",
|
||||
"cssnano": "5.1.15",
|
||||
"es-check": "7.1.0",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-plugin-compat": "4.1.2",
|
||||
"cssnano": "6.0.0",
|
||||
"es-check": "7.1.1",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-plugin-compat": "4.1.4",
|
||||
"eslint-plugin-eslint-comments": "3.2.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-sonarjs": "0.18.0",
|
||||
"expose-loader": "4.0.0",
|
||||
"eslint-plugin-sonarjs": "0.19.0",
|
||||
"expose-loader": "4.1.0",
|
||||
"html-loader": "4.2.0",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"mini-css-extract-plugin": "2.7.3",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.21",
|
||||
"postcss-loader": "7.0.2",
|
||||
"postcss-preset-env": "8.0.1",
|
||||
"postcss-loader": "7.2.4",
|
||||
"postcss-preset-env": "8.3.1",
|
||||
"postcss-scss": "4.0.6",
|
||||
"sass": "1.58.3",
|
||||
"sass-loader": "13.2.0",
|
||||
"sass": "1.62.0",
|
||||
"sass-loader": "13.2.2",
|
||||
"source-map-loader": "4.0.1",
|
||||
"style-loader": "3.3.1",
|
||||
"stylelint": "15.2.0",
|
||||
"style-loader": "3.3.2",
|
||||
"stylelint": "15.4.0",
|
||||
"stylelint-config-rational-order": "0.1.2",
|
||||
"stylelint-no-browser-hacks": "1.2.1",
|
||||
"stylelint-order": "6.0.3",
|
||||
"stylelint-scss": "4.4.0",
|
||||
"stylelint-scss": "4.6.0",
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript": "4.9.5",
|
||||
"webpack": "5.76.0",
|
||||
"typescript": "5.0.4",
|
||||
"webpack": "5.79.0",
|
||||
"webpack-cli": "5.0.1",
|
||||
"webpack-dev-server": "4.11.1",
|
||||
"webpack-dev-server": "4.13.2",
|
||||
"webpack-merge": "5.8.0",
|
||||
"workbox-webpack-plugin": "6.5.4",
|
||||
"worker-loader": "3.0.8"
|
||||
@@ -74,35 +73,36 @@
|
||||
"@fontsource/noto-sans-kr": "4.5.12",
|
||||
"@fontsource/noto-sans-sc": "4.5.12",
|
||||
"@fontsource/noto-sans-tc": "4.5.12",
|
||||
"@jellyfin/libass-wasm": "4.1.1",
|
||||
"@jellyfin/sdk": "unstable",
|
||||
"@loadable/component": "5.15.3",
|
||||
"blurhash": "2.0.5",
|
||||
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.29.0",
|
||||
"core-js": "3.30.1",
|
||||
"date-fns": "2.29.3",
|
||||
"dompurify": "3.0.1",
|
||||
"epubjs": "0.4.2",
|
||||
"epubjs": "0.3.93",
|
||||
"escape-html": "1.0.3",
|
||||
"event-target-polyfill": "github:ThaUnknown/event-target-polyfill",
|
||||
"fast-text-encoding": "1.0.6",
|
||||
"flv.js": "1.6.2",
|
||||
"headroom.js": "0.12.0",
|
||||
"history": "5.3.0",
|
||||
"hls.js": "1.2.4",
|
||||
"hls.js": "1.4.0",
|
||||
"intersection-observer": "0.12.2",
|
||||
"jassub": "1.5.12",
|
||||
"jellyfin-apiclient": "1.10.0",
|
||||
"jquery": "3.6.3",
|
||||
"jquery": "3.6.4",
|
||||
"jstree": "3.3.15",
|
||||
"libarchive.js": "1.3.0",
|
||||
"lodash-es": "4.17.21",
|
||||
"marked": "4.2.12",
|
||||
"marked": "4.3.0",
|
||||
"material-design-icons-iconfont": "6.7.0",
|
||||
"native-promise-only": "0.8.1",
|
||||
"pdfjs-dist": "2.16.105",
|
||||
"pdfjs-dist": "3.5.141",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-router-dom": "6.8.2",
|
||||
"react-router-dom": "6.10.0",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"screenfull": "6.0.2",
|
||||
"sortablejs": "1.15.0",
|
||||
@@ -131,8 +131,8 @@
|
||||
"scripts": {
|
||||
"start": "npm run serve",
|
||||
"serve": "webpack serve --config webpack.dev.js",
|
||||
"build:development": "webpack --config webpack.dev.js",
|
||||
"build:production": "cross-env NODE_ENV=\"production\" webpack --config webpack.prod.js",
|
||||
"build:development": "cross-env NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.dev.js",
|
||||
"build:production": "cross-env NODE_ENV=\"production\" NODE_OPTIONS=\"--max_old_space_size=6144\" webpack --config webpack.prod.js",
|
||||
"build:check": "tsc --noEmit",
|
||||
"escheck": "es-check",
|
||||
"lint": "eslint \"./\"",
|
||||
|
||||
@@ -7,8 +7,8 @@ const config = () => ({
|
||||
plugins: [
|
||||
// Explicitly specify browserslist to override ones from node_modules
|
||||
// For example, Swiper has it in its package.json
|
||||
postcssPresetEnv({browsers: packageConfig.browserslist}),
|
||||
autoprefixer({overrideBrowserslist: packageConfig.browserslist}),
|
||||
postcssPresetEnv({ browsers: packageConfig.browserslist }),
|
||||
autoprefixer({ overrideBrowserslist: packageConfig.browserslist }),
|
||||
cssnano()
|
||||
]
|
||||
});
|
||||
|
||||
29
src/App.tsx
29
src/App.tsx
@@ -1,15 +1,38 @@
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { HistoryRouter } from './components/HistoryRouter';
|
||||
import { ApiProvider } from './hooks/useApi';
|
||||
import AppRoutes from './routes/index';
|
||||
import { AppRoutes, ExperimentalAppRoutes } from './routes';
|
||||
|
||||
const App = ({ history }: { history: History }) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
// Initialize the UI components after first render
|
||||
import('./scripts/libraryMenu'),
|
||||
import('./scripts/autoBackdrops')
|
||||
]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ApiProvider>
|
||||
<HistoryRouter history={history}>
|
||||
<AppRoutes />
|
||||
<div className='backdropContainer' />
|
||||
<div className='backgroundContainer' />
|
||||
|
||||
<div className='mainDrawer hide'>
|
||||
<div className='mainDrawer-scrollContainer scrollContainer focuscontainer-y' />
|
||||
</div>
|
||||
<div className='skinHeader focuscontainer-x' />
|
||||
|
||||
<div className='mainAnimatedPages skinBody' />
|
||||
<div className='skinBody'>
|
||||
{layoutMode === 'experimental' ? <ExperimentalAppRoutes /> : <AppRoutes /> }
|
||||
</div>
|
||||
|
||||
<div className='mainDrawerHandle' />
|
||||
</HistoryRouter>
|
||||
</ApiProvider>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for controlling user parental control from.
|
||||
* @module components/accessSchedule/accessSchedule
|
||||
@@ -14,84 +11,82 @@ import '../../elements/emby-button/paper-icon-button-light';
|
||||
import '../formdialog.scss';
|
||||
import template from './accessSchedule.template.html';
|
||||
|
||||
function getDisplayTime(hours) {
|
||||
let minutes = 0;
|
||||
const pct = hours % 1;
|
||||
function getDisplayTime(hours) {
|
||||
let minutes = 0;
|
||||
const pct = hours % 1;
|
||||
|
||||
if (pct) {
|
||||
minutes = parseInt(60 * pct, 10);
|
||||
}
|
||||
|
||||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
if (pct) {
|
||||
minutes = parseInt(60 * pct, 10);
|
||||
}
|
||||
|
||||
function populateHours(context) {
|
||||
let html = '';
|
||||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
}
|
||||
|
||||
for (let i = 0; i < 24; i += 0.5) {
|
||||
html += `<option value="${i}">${getDisplayTime(i)}</option>`;
|
||||
}
|
||||
function populateHours(context) {
|
||||
let html = '';
|
||||
|
||||
html += `<option value="24">${getDisplayTime(0)}</option>`;
|
||||
context.querySelector('#selectStart').innerHTML = html;
|
||||
context.querySelector('#selectEnd').innerHTML = html;
|
||||
for (let i = 0; i < 24; i += 0.5) {
|
||||
html += `<option value="${i}">${getDisplayTime(i)}</option>`;
|
||||
}
|
||||
|
||||
function loadSchedule(context, {DayOfWeek, StartHour, EndHour}) {
|
||||
context.querySelector('#selectDay').value = DayOfWeek || 'Sunday';
|
||||
context.querySelector('#selectStart').value = StartHour || 0;
|
||||
context.querySelector('#selectEnd').value = EndHour || 0;
|
||||
html += `<option value="24">${getDisplayTime(0)}</option>`;
|
||||
context.querySelector('#selectStart').innerHTML = html;
|
||||
context.querySelector('#selectEnd').innerHTML = html;
|
||||
}
|
||||
|
||||
function loadSchedule(context, { DayOfWeek, StartHour, EndHour }) {
|
||||
context.querySelector('#selectDay').value = DayOfWeek || 'Sunday';
|
||||
context.querySelector('#selectStart').value = StartHour || 0;
|
||||
context.querySelector('#selectEnd').value = EndHour || 0;
|
||||
}
|
||||
|
||||
function submitSchedule(context, options) {
|
||||
const updatedSchedule = {
|
||||
DayOfWeek: context.querySelector('#selectDay').value,
|
||||
StartHour: context.querySelector('#selectStart').value,
|
||||
EndHour: context.querySelector('#selectEnd').value
|
||||
};
|
||||
|
||||
if (parseFloat(updatedSchedule.StartHour) >= parseFloat(updatedSchedule.EndHour)) {
|
||||
alert(globalize.translate('ErrorStartHourGreaterThanEnd'));
|
||||
return;
|
||||
}
|
||||
|
||||
function submitSchedule(context, options) {
|
||||
const updatedSchedule = {
|
||||
DayOfWeek: context.querySelector('#selectDay').value,
|
||||
StartHour: context.querySelector('#selectStart').value,
|
||||
EndHour: context.querySelector('#selectEnd').value
|
||||
};
|
||||
context.submitted = true;
|
||||
options.schedule = Object.assign(options.schedule, updatedSchedule);
|
||||
dialogHelper.close(context);
|
||||
}
|
||||
|
||||
if (parseFloat(updatedSchedule.StartHour) >= parseFloat(updatedSchedule.EndHour)) {
|
||||
alert(globalize.translate('ErrorStartHourGreaterThanEnd'));
|
||||
return;
|
||||
}
|
||||
|
||||
context.submitted = true;
|
||||
options.schedule = Object.assign(options.schedule, updatedSchedule);
|
||||
dialogHelper.close(context);
|
||||
}
|
||||
|
||||
export function show(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = dialogHelper.createDialog({
|
||||
removeOnClose: true,
|
||||
size: 'small'
|
||||
});
|
||||
dlg.classList.add('formDialog');
|
||||
let html = '';
|
||||
html += globalize.translateHtml(template);
|
||||
dlg.innerHTML = html;
|
||||
populateHours(dlg);
|
||||
loadSchedule(dlg, options.schedule);
|
||||
dialogHelper.open(dlg);
|
||||
dlg.addEventListener('close', () => {
|
||||
if (dlg.submitted) {
|
||||
resolve(options.schedule);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
dlg.querySelector('form').addEventListener('submit', event => {
|
||||
submitSchedule(dlg, options);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
export function show(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = dialogHelper.createDialog({
|
||||
removeOnClose: true,
|
||||
size: 'small'
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
dlg.classList.add('formDialog');
|
||||
let html = '';
|
||||
html += globalize.translateHtml(template);
|
||||
dlg.innerHTML = html;
|
||||
populateHours(dlg);
|
||||
loadSchedule(dlg, options.schedule);
|
||||
dialogHelper.open(dlg);
|
||||
dlg.addEventListener('close', () => {
|
||||
if (dlg.submitted) {
|
||||
resolve(options.schedule);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
dlg.querySelector('form').addEventListener('submit', event => {
|
||||
submitSchedule(dlg, options);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
show: show
|
||||
|
||||
@@ -11,130 +11,128 @@ import alert from './alert';
|
||||
import { getLocale } from '../utils/dateFnsLocale.ts';
|
||||
import { toBoolean } from '../utils/string.ts';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function getEntryHtml(entry, apiClient) {
|
||||
let html = '';
|
||||
html += '<div class="listItem listItem-border">';
|
||||
let color = '#00a4dc';
|
||||
let icon = 'notifications';
|
||||
|
||||
function getEntryHtml(entry, apiClient) {
|
||||
let html = '';
|
||||
html += '<div class="listItem listItem-border">';
|
||||
let color = '#00a4dc';
|
||||
let icon = 'notifications';
|
||||
if (entry.Severity == 'Error' || entry.Severity == 'Fatal' || entry.Severity == 'Warn') {
|
||||
color = '#cc0000';
|
||||
icon = 'notification_important';
|
||||
}
|
||||
|
||||
if (entry.Severity == 'Error' || entry.Severity == 'Fatal' || entry.Severity == 'Warn') {
|
||||
color = '#cc0000';
|
||||
icon = 'notification_important';
|
||||
}
|
||||
if (entry.UserId && entry.UserPrimaryImageTag) {
|
||||
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
|
||||
type: 'Primary',
|
||||
tag: entry.UserPrimaryImageTag
|
||||
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
|
||||
} else {
|
||||
html += '<span class="listItemIcon material-icons ' + icon + '" aria-hidden="true" style="background-color:' + color + '"></span>';
|
||||
}
|
||||
|
||||
if (entry.UserId && entry.UserPrimaryImageTag) {
|
||||
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
|
||||
type: 'Primary',
|
||||
tag: entry.UserPrimaryImageTag
|
||||
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
|
||||
} else {
|
||||
html += '<span class="listItemIcon material-icons ' + icon + '" aria-hidden="true" style="background-color:' + color + '"></span>';
|
||||
}
|
||||
html += '<div class="listItemBody three-line">';
|
||||
html += '<div class="listItemBodyText">';
|
||||
html += escapeHtml(entry.Name);
|
||||
html += '</div>';
|
||||
html += '<div class="listItemBodyText secondary">';
|
||||
html += formatRelative(Date.parse(entry.Date), Date.now(), { locale: getLocale() });
|
||||
html += '</div>';
|
||||
html += '<div class="listItemBodyText secondary listItemBodyText-nowrap">';
|
||||
html += escapeHtml(entry.ShortOverview || '');
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="listItemBody three-line">';
|
||||
html += '<div class="listItemBodyText">';
|
||||
html += escapeHtml(entry.Name);
|
||||
html += '</div>';
|
||||
html += '<div class="listItemBodyText secondary">';
|
||||
html += formatRelative(Date.parse(entry.Date), Date.now(), { locale: getLocale() });
|
||||
html += '</div>';
|
||||
html += '<div class="listItemBodyText secondary listItemBodyText-nowrap">';
|
||||
html += escapeHtml(entry.ShortOverview || '');
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
if (entry.Overview) {
|
||||
html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">
|
||||
if (entry.Overview) {
|
||||
html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">
|
||||
<span class="material-icons info" aria-hidden="true"></span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderList(elem, apiClient, result) {
|
||||
elem.innerHTML = result.Items.map(function (i) {
|
||||
return getEntryHtml(i, apiClient);
|
||||
}).join('');
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderList(elem, apiClient, result) {
|
||||
elem.innerHTML = result.Items.map(function (i) {
|
||||
return getEntryHtml(i, apiClient);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function reloadData(instance, elem, apiClient, startIndex, limit) {
|
||||
if (startIndex == null) {
|
||||
startIndex = parseInt(elem.getAttribute('data-activitystartindex') || '0', 10);
|
||||
}
|
||||
|
||||
function reloadData(instance, elem, apiClient, startIndex, limit) {
|
||||
if (startIndex == null) {
|
||||
startIndex = parseInt(elem.getAttribute('data-activitystartindex') || '0', 10);
|
||||
}
|
||||
limit = limit || parseInt(elem.getAttribute('data-activitylimit') || '7', 10);
|
||||
const minDate = new Date();
|
||||
const hasUserId = toBoolean(elem.getAttribute('data-useractivity'), true);
|
||||
|
||||
limit = limit || parseInt(elem.getAttribute('data-activitylimit') || '7', 10);
|
||||
const minDate = new Date();
|
||||
const hasUserId = toBoolean(elem.getAttribute('data-useractivity'), true);
|
||||
|
||||
// TODO: Use date-fns
|
||||
if (hasUserId) {
|
||||
minDate.setTime(minDate.getTime() - 24 * 60 * 60 * 1000); // one day back
|
||||
} else {
|
||||
minDate.setTime(minDate.getTime() - 7 * 24 * 60 * 60 * 1000); // one week back
|
||||
}
|
||||
|
||||
ApiClient.getJSON(ApiClient.getUrl('System/ActivityLog/Entries', {
|
||||
startIndex: startIndex,
|
||||
limit: limit,
|
||||
minDate: minDate.toISOString(),
|
||||
hasUserId: hasUserId
|
||||
})).then(function (result) {
|
||||
elem.setAttribute('data-activitystartindex', startIndex);
|
||||
elem.setAttribute('data-activitylimit', limit);
|
||||
if (!startIndex) {
|
||||
const activityContainer = dom.parentWithClass(elem, 'activityContainer');
|
||||
|
||||
if (activityContainer) {
|
||||
if (result.Items.length) {
|
||||
activityContainer.classList.remove('hide');
|
||||
} else {
|
||||
activityContainer.classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance.items = result.Items;
|
||||
renderList(elem, apiClient, result);
|
||||
});
|
||||
// TODO: Use date-fns
|
||||
if (hasUserId) {
|
||||
minDate.setTime(minDate.getTime() - 24 * 60 * 60 * 1000); // one day back
|
||||
} else {
|
||||
minDate.setTime(minDate.getTime() - 7 * 24 * 60 * 60 * 1000); // one week back
|
||||
}
|
||||
|
||||
function onActivityLogUpdate(e, apiClient) {
|
||||
const options = this.options;
|
||||
ApiClient.getJSON(ApiClient.getUrl('System/ActivityLog/Entries', {
|
||||
startIndex: startIndex,
|
||||
limit: limit,
|
||||
minDate: minDate.toISOString(),
|
||||
hasUserId: hasUserId
|
||||
})).then(function (result) {
|
||||
elem.setAttribute('data-activitystartindex', startIndex);
|
||||
elem.setAttribute('data-activitylimit', limit);
|
||||
if (!startIndex) {
|
||||
const activityContainer = dom.parentWithClass(elem, 'activityContainer');
|
||||
|
||||
if (options && options.serverId === apiClient.serverId()) {
|
||||
reloadData(this, options.element, apiClient);
|
||||
}
|
||||
}
|
||||
|
||||
function onListClick(e) {
|
||||
const btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo');
|
||||
|
||||
if (btnEntryInfo) {
|
||||
const id = btnEntryInfo.getAttribute('data-id');
|
||||
const items = this.items;
|
||||
|
||||
if (items) {
|
||||
const item = items.filter(function (i) {
|
||||
return i.Id.toString() === id;
|
||||
})[0];
|
||||
|
||||
if (item) {
|
||||
showItemOverview(item);
|
||||
if (activityContainer) {
|
||||
if (result.Items.length) {
|
||||
activityContainer.classList.remove('hide');
|
||||
} else {
|
||||
activityContainer.classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showItemOverview(item) {
|
||||
alert({
|
||||
text: item.Overview
|
||||
});
|
||||
instance.items = result.Items;
|
||||
renderList(elem, apiClient, result);
|
||||
});
|
||||
}
|
||||
|
||||
function onActivityLogUpdate(e, apiClient) {
|
||||
const options = this.options;
|
||||
|
||||
if (options && options.serverId === apiClient.serverId()) {
|
||||
reloadData(this, options.element, apiClient);
|
||||
}
|
||||
}
|
||||
|
||||
function onListClick(e) {
|
||||
const btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo');
|
||||
|
||||
if (btnEntryInfo) {
|
||||
const id = btnEntryInfo.getAttribute('data-id');
|
||||
const items = this.items;
|
||||
|
||||
if (items) {
|
||||
const item = items.filter(function (i) {
|
||||
return i.Id.toString() === id;
|
||||
})[0];
|
||||
|
||||
if (item) {
|
||||
showItemOverview(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showItemOverview(item) {
|
||||
alert({
|
||||
text: item.Overview
|
||||
});
|
||||
}
|
||||
|
||||
class ActivityLog {
|
||||
constructor(options) {
|
||||
@@ -169,5 +167,3 @@ class ActivityLog {
|
||||
}
|
||||
|
||||
export default ActivityLog;
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -3,45 +3,41 @@ import browser from '../scripts/browser';
|
||||
import dialog from './dialog/dialog';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function useNativeAlert() {
|
||||
// webOS seems to block modals
|
||||
// Tizen 2.x seems to block modals
|
||||
return !browser.web0s
|
||||
function useNativeAlert() {
|
||||
// webOS seems to block modals
|
||||
// Tizen 2.x seems to block modals
|
||||
return !browser.web0s
|
||||
&& !(browser.tizenVersion && browser.tizenVersion < 3)
|
||||
&& browser.tv
|
||||
&& window.alert;
|
||||
}
|
||||
|
||||
export default async function (text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
} else {
|
||||
options = text;
|
||||
}
|
||||
|
||||
export default async function (text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
} else {
|
||||
options = text;
|
||||
}
|
||||
await appRouter.ready();
|
||||
|
||||
await appRouter.ready();
|
||||
if (useNativeAlert()) {
|
||||
alert((options.text || '').replaceAll('<br/>', '\n'));
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const items = [];
|
||||
|
||||
if (useNativeAlert()) {
|
||||
alert((options.text || '').replaceAll('<br/>', '\n'));
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
const items = [];
|
||||
items.push({
|
||||
name: globalize.translate('ButtonGotIt'),
|
||||
id: 'ok',
|
||||
type: 'submit'
|
||||
});
|
||||
|
||||
items.push({
|
||||
name: globalize.translate('ButtonGotIt'),
|
||||
id: 'ok',
|
||||
type: 'submit'
|
||||
});
|
||||
|
||||
options.buttons = items;
|
||||
return dialog.show(options);
|
||||
}
|
||||
options.buttons = items;
|
||||
return dialog.show(options);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module alphaPicker.
|
||||
* @module components/alphaPicker/alphaPicker
|
||||
@@ -13,312 +11,311 @@ import './style.scss';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
import 'material-design-icons-iconfont';
|
||||
|
||||
const selectedButtonClass = 'alphaPickerButton-selected';
|
||||
const selectedButtonClass = 'alphaPickerButton-selected';
|
||||
|
||||
function focus() {
|
||||
const scope = this;
|
||||
const selected = scope.querySelector(`.${selectedButtonClass}`);
|
||||
function focus() {
|
||||
const scope = this;
|
||||
const selected = scope.querySelector(`.${selectedButtonClass}`);
|
||||
|
||||
if (selected) {
|
||||
focusManager.focus(selected);
|
||||
} else {
|
||||
focusManager.autoFocus(scope, true);
|
||||
}
|
||||
if (selected) {
|
||||
focusManager.focus(selected);
|
||||
} else {
|
||||
focusManager.autoFocus(scope, true);
|
||||
}
|
||||
}
|
||||
|
||||
function getAlphaPickerButtonClassName(vertical) {
|
||||
let alphaPickerButtonClassName = 'alphaPickerButton';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
alphaPickerButtonClassName += ' alphaPickerButton-tv';
|
||||
}
|
||||
|
||||
function getAlphaPickerButtonClassName(vertical) {
|
||||
let alphaPickerButtonClassName = 'alphaPickerButton';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
alphaPickerButtonClassName += ' alphaPickerButton-tv';
|
||||
}
|
||||
|
||||
if (vertical) {
|
||||
alphaPickerButtonClassName += ' alphaPickerButton-vertical';
|
||||
}
|
||||
|
||||
return alphaPickerButtonClassName;
|
||||
if (vertical) {
|
||||
alphaPickerButtonClassName += ' alphaPickerButton-vertical';
|
||||
}
|
||||
|
||||
function getLetterButton(l, vertical) {
|
||||
return `<button data-value="${l}" class="${getAlphaPickerButtonClassName(vertical)}">${l}</button>`;
|
||||
return alphaPickerButtonClassName;
|
||||
}
|
||||
|
||||
function getLetterButton(l, vertical) {
|
||||
return `<button data-value="${l}" class="${getAlphaPickerButtonClassName(vertical)}">${l}</button>`;
|
||||
}
|
||||
|
||||
function mapLetters(letters, vertical) {
|
||||
return letters.map(l => {
|
||||
return getLetterButton(l, vertical);
|
||||
});
|
||||
}
|
||||
|
||||
function render(element, options) {
|
||||
element.classList.add('alphaPicker');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
element.classList.add('alphaPicker-tv');
|
||||
}
|
||||
|
||||
function mapLetters(letters, vertical) {
|
||||
return letters.map(l => {
|
||||
return getLetterButton(l, vertical);
|
||||
});
|
||||
const vertical = element.classList.contains('alphaPicker-vertical');
|
||||
|
||||
if (!vertical) {
|
||||
element.classList.add('focuscontainer-x');
|
||||
}
|
||||
|
||||
function render(element, options) {
|
||||
element.classList.add('alphaPicker');
|
||||
let html = '';
|
||||
let letters;
|
||||
|
||||
if (layoutManager.tv) {
|
||||
element.classList.add('alphaPicker-tv');
|
||||
}
|
||||
const alphaPickerButtonClassName = getAlphaPickerButtonClassName(vertical);
|
||||
|
||||
const vertical = element.classList.contains('alphaPicker-vertical');
|
||||
let rowClassName = 'alphaPickerRow';
|
||||
|
||||
if (!vertical) {
|
||||
element.classList.add('focuscontainer-x');
|
||||
}
|
||||
if (vertical) {
|
||||
rowClassName += ' alphaPickerRow-vertical';
|
||||
}
|
||||
|
||||
let html = '';
|
||||
let letters;
|
||||
|
||||
const alphaPickerButtonClassName = getAlphaPickerButtonClassName(vertical);
|
||||
|
||||
let rowClassName = 'alphaPickerRow';
|
||||
|
||||
if (vertical) {
|
||||
rowClassName += ' alphaPickerRow-vertical';
|
||||
}
|
||||
|
||||
html += `<div class="${rowClassName}">`;
|
||||
if (options.mode === 'keyboard') {
|
||||
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonSpace')}"><span class="material-icons alphaPickerButtonIcon space_bar" aria-hidden="true"></span></button>`;
|
||||
} else {
|
||||
letters = ['#'];
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
}
|
||||
|
||||
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
||||
html += `<div class="${rowClassName}">`;
|
||||
if (options.mode === 'keyboard') {
|
||||
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonSpace')}"><span class="material-icons alphaPickerButtonIcon space_bar" aria-hidden="true"></span></button>`;
|
||||
} else {
|
||||
letters = ['#'];
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
|
||||
if (options.mode === 'keyboard') {
|
||||
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonBackspace')}"><span class="material-icons alphaPickerButtonIcon backspace" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
|
||||
letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
html += `<div class="${rowClassName}">`;
|
||||
html += '<br/>';
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
element.innerHTML = html;
|
||||
|
||||
element.classList.add('focusable');
|
||||
element.focus = focus;
|
||||
}
|
||||
|
||||
export class AlphaPicker {
|
||||
constructor(options) {
|
||||
const self = this;
|
||||
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
|
||||
this.options = options;
|
||||
if (options.mode === 'keyboard') {
|
||||
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonBackspace')}"><span class="material-icons alphaPickerButtonIcon backspace" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
|
||||
const element = options.element;
|
||||
const itemsContainer = options.itemsContainer;
|
||||
const itemClass = options.itemClass;
|
||||
letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
html += `<div class="${rowClassName}">`;
|
||||
html += '<br/>';
|
||||
html += mapLetters(letters, vertical).join('');
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
let itemFocusValue;
|
||||
let itemFocusTimeout;
|
||||
element.innerHTML = html;
|
||||
|
||||
function onItemFocusTimeout() {
|
||||
itemFocusTimeout = null;
|
||||
self.value(itemFocusValue);
|
||||
}
|
||||
element.classList.add('focusable');
|
||||
element.focus = focus;
|
||||
}
|
||||
|
||||
let alphaFocusedElement;
|
||||
let alphaFocusTimeout;
|
||||
export class AlphaPicker {
|
||||
constructor(options) {
|
||||
const self = this;
|
||||
|
||||
function onAlphaFocusTimeout() {
|
||||
alphaFocusTimeout = null;
|
||||
this.options = options;
|
||||
|
||||
if (document.activeElement === alphaFocusedElement) {
|
||||
const value = alphaFocusedElement.getAttribute('data-value');
|
||||
self.value(value, true);
|
||||
}
|
||||
}
|
||||
const element = options.element;
|
||||
const itemsContainer = options.itemsContainer;
|
||||
const itemClass = options.itemClass;
|
||||
|
||||
function onAlphaPickerInKeyboardModeClick(e) {
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
let itemFocusValue;
|
||||
let itemFocusTimeout;
|
||||
|
||||
if (alphaPickerButton) {
|
||||
const value = alphaPickerButton.getAttribute('data-value');
|
||||
|
||||
element.dispatchEvent(new CustomEvent('alphavalueclicked', {
|
||||
cancelable: false,
|
||||
detail: {
|
||||
value
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function onAlphaPickerClick(e) {
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
const value = alphaPickerButton.getAttribute('data-value');
|
||||
if ((this._currentValue || '').toUpperCase() === value.toUpperCase()) {
|
||||
this.value(null, true);
|
||||
} else {
|
||||
this.value(value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onAlphaPickerFocusIn(e) {
|
||||
if (alphaFocusTimeout) {
|
||||
clearTimeout(alphaFocusTimeout);
|
||||
alphaFocusTimeout = null;
|
||||
}
|
||||
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
alphaFocusedElement = alphaPickerButton;
|
||||
alphaFocusTimeout = setTimeout(onAlphaFocusTimeout, 600);
|
||||
}
|
||||
}
|
||||
|
||||
function onItemsFocusIn(e) {
|
||||
const item = dom.parentWithClass(e.target, itemClass);
|
||||
|
||||
if (item) {
|
||||
const prefix = item.getAttribute('data-prefix');
|
||||
if (prefix && prefix.length) {
|
||||
itemFocusValue = prefix[0];
|
||||
if (itemFocusTimeout) {
|
||||
clearTimeout(itemFocusTimeout);
|
||||
}
|
||||
itemFocusTimeout = setTimeout(onItemFocusTimeout, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.enabled = function (enabled) {
|
||||
if (enabled) {
|
||||
if (itemsContainer) {
|
||||
itemsContainer.addEventListener('focus', onItemsFocusIn, true);
|
||||
}
|
||||
|
||||
if (options.mode === 'keyboard') {
|
||||
element.addEventListener('click', onAlphaPickerInKeyboardModeClick);
|
||||
}
|
||||
|
||||
if (options.valueChangeEvent !== 'click') {
|
||||
element.addEventListener('focus', onAlphaPickerFocusIn, true);
|
||||
} else {
|
||||
element.addEventListener('click', onAlphaPickerClick.bind(this));
|
||||
}
|
||||
} else {
|
||||
if (itemsContainer) {
|
||||
itemsContainer.removeEventListener('focus', onItemsFocusIn, true);
|
||||
}
|
||||
|
||||
element.removeEventListener('click', onAlphaPickerInKeyboardModeClick);
|
||||
element.removeEventListener('focus', onAlphaPickerFocusIn, true);
|
||||
element.removeEventListener('click', onAlphaPickerClick.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
render(element, options);
|
||||
|
||||
this.enabled(true);
|
||||
this.visible(true);
|
||||
function onItemFocusTimeout() {
|
||||
itemFocusTimeout = null;
|
||||
self.value(itemFocusValue);
|
||||
}
|
||||
|
||||
value(value, applyValue) {
|
||||
const element = this.options.element;
|
||||
let btn;
|
||||
let selected;
|
||||
let alphaFocusedElement;
|
||||
let alphaFocusTimeout;
|
||||
|
||||
if (value !== undefined) {
|
||||
if (value != null) {
|
||||
value = value.toUpperCase();
|
||||
this._currentValue = value;
|
||||
function onAlphaFocusTimeout() {
|
||||
alphaFocusTimeout = null;
|
||||
|
||||
if (this.options.mode !== 'keyboard') {
|
||||
selected = element.querySelector(`.${selectedButtonClass}`);
|
||||
|
||||
try {
|
||||
btn = element.querySelector(`.alphaPickerButton[data-value='${value}']`);
|
||||
} catch (err) {
|
||||
console.error('error in querySelector:', err);
|
||||
}
|
||||
|
||||
if (btn && btn !== selected) {
|
||||
btn.classList.add(selectedButtonClass);
|
||||
}
|
||||
if (selected && selected !== btn) {
|
||||
selected.classList.remove(selectedButtonClass);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._currentValue = value;
|
||||
|
||||
selected = element.querySelector(`.${selectedButtonClass}`);
|
||||
if (selected) {
|
||||
selected.classList.remove(selectedButtonClass);
|
||||
}
|
||||
}
|
||||
if (document.activeElement === alphaFocusedElement) {
|
||||
const value = alphaFocusedElement.getAttribute('data-value');
|
||||
self.value(value, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (applyValue) {
|
||||
element.dispatchEvent(new CustomEvent('alphavaluechanged', {
|
||||
function onAlphaPickerInKeyboardModeClick(e) {
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
const value = alphaPickerButton.getAttribute('data-value');
|
||||
|
||||
element.dispatchEvent(new CustomEvent('alphavalueclicked', {
|
||||
cancelable: false,
|
||||
detail: {
|
||||
value
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return this._currentValue;
|
||||
}
|
||||
|
||||
on(name, fn) {
|
||||
const element = this.options.element;
|
||||
element.addEventListener(name, fn);
|
||||
function onAlphaPickerClick(e) {
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
const value = alphaPickerButton.getAttribute('data-value');
|
||||
if ((this._currentValue || '').toUpperCase() === value.toUpperCase()) {
|
||||
this.value(null, true);
|
||||
} else {
|
||||
this.value(value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
off(name, fn) {
|
||||
const element = this.options.element;
|
||||
element.removeEventListener(name, fn);
|
||||
function onAlphaPickerFocusIn(e) {
|
||||
if (alphaFocusTimeout) {
|
||||
clearTimeout(alphaFocusTimeout);
|
||||
alphaFocusTimeout = null;
|
||||
}
|
||||
|
||||
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
|
||||
|
||||
if (alphaPickerButton) {
|
||||
alphaFocusedElement = alphaPickerButton;
|
||||
alphaFocusTimeout = setTimeout(onAlphaFocusTimeout, 600);
|
||||
}
|
||||
}
|
||||
|
||||
updateControls(query) {
|
||||
if (query.NameLessThan) {
|
||||
this.value('#');
|
||||
function onItemsFocusIn(e) {
|
||||
const item = dom.parentWithClass(e.target, itemClass);
|
||||
|
||||
if (item) {
|
||||
const prefix = item.getAttribute('data-prefix');
|
||||
if (prefix && prefix.length) {
|
||||
itemFocusValue = prefix[0];
|
||||
if (itemFocusTimeout) {
|
||||
clearTimeout(itemFocusTimeout);
|
||||
}
|
||||
itemFocusTimeout = setTimeout(onItemFocusTimeout, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.enabled = function (enabled) {
|
||||
if (enabled) {
|
||||
if (itemsContainer) {
|
||||
itemsContainer.addEventListener('focus', onItemsFocusIn, true);
|
||||
}
|
||||
|
||||
if (options.mode === 'keyboard') {
|
||||
element.addEventListener('click', onAlphaPickerInKeyboardModeClick);
|
||||
}
|
||||
|
||||
if (options.valueChangeEvent !== 'click') {
|
||||
element.addEventListener('focus', onAlphaPickerFocusIn, true);
|
||||
} else {
|
||||
element.addEventListener('click', onAlphaPickerClick.bind(this));
|
||||
}
|
||||
} else {
|
||||
this.value(query.NameStartsWith);
|
||||
if (itemsContainer) {
|
||||
itemsContainer.removeEventListener('focus', onItemsFocusIn, true);
|
||||
}
|
||||
|
||||
element.removeEventListener('click', onAlphaPickerInKeyboardModeClick);
|
||||
element.removeEventListener('focus', onAlphaPickerFocusIn, true);
|
||||
element.removeEventListener('click', onAlphaPickerClick.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
this.visible(query.SortBy.indexOf('SortName') !== -1);
|
||||
}
|
||||
render(element, options);
|
||||
|
||||
visible(visible) {
|
||||
const element = this.options.element;
|
||||
element.style.visibility = visible ? 'visible' : 'hidden';
|
||||
}
|
||||
|
||||
values() {
|
||||
const element = this.options.element;
|
||||
const elems = element.querySelectorAll('.alphaPickerButton');
|
||||
const values = [];
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
values.push(elems[i].getAttribute('data-value'));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
focus() {
|
||||
const element = this.options.element;
|
||||
focusManager.autoFocus(element, true);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const element = this.options.element;
|
||||
this.enabled(false);
|
||||
element.classList.remove('focuscontainer-x');
|
||||
this.options = null;
|
||||
}
|
||||
this.enabled(true);
|
||||
this.visible(true);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
value(value, applyValue) {
|
||||
const element = this.options.element;
|
||||
let btn;
|
||||
let selected;
|
||||
|
||||
if (value !== undefined) {
|
||||
if (value != null) {
|
||||
value = value.toUpperCase();
|
||||
this._currentValue = value;
|
||||
|
||||
if (this.options.mode !== 'keyboard') {
|
||||
selected = element.querySelector(`.${selectedButtonClass}`);
|
||||
|
||||
try {
|
||||
btn = element.querySelector(`.alphaPickerButton[data-value='${value}']`);
|
||||
} catch (err) {
|
||||
console.error('error in querySelector:', err);
|
||||
}
|
||||
|
||||
if (btn && btn !== selected) {
|
||||
btn.classList.add(selectedButtonClass);
|
||||
}
|
||||
if (selected && selected !== btn) {
|
||||
selected.classList.remove(selectedButtonClass);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._currentValue = value;
|
||||
|
||||
selected = element.querySelector(`.${selectedButtonClass}`);
|
||||
if (selected) {
|
||||
selected.classList.remove(selectedButtonClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (applyValue) {
|
||||
element.dispatchEvent(new CustomEvent('alphavaluechanged', {
|
||||
cancelable: false,
|
||||
detail: {
|
||||
value
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return this._currentValue;
|
||||
}
|
||||
|
||||
on(name, fn) {
|
||||
const element = this.options.element;
|
||||
element.addEventListener(name, fn);
|
||||
}
|
||||
|
||||
off(name, fn) {
|
||||
const element = this.options.element;
|
||||
element.removeEventListener(name, fn);
|
||||
}
|
||||
|
||||
updateControls(query) {
|
||||
if (query.NameLessThan) {
|
||||
this.value('#');
|
||||
} else {
|
||||
this.value(query.NameStartsWith);
|
||||
}
|
||||
|
||||
this.visible(query.SortBy.indexOf('SortName') !== -1);
|
||||
}
|
||||
|
||||
visible(visible) {
|
||||
const element = this.options.element;
|
||||
element.style.visibility = visible ? 'visible' : 'hidden';
|
||||
}
|
||||
|
||||
values() {
|
||||
const element = this.options.element;
|
||||
const elems = element.querySelectorAll('.alphaPickerButton');
|
||||
const values = [];
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
values.push(elems[i].getAttribute('data-value'));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
focus() {
|
||||
const element = this.options.element;
|
||||
focusManager.autoFocus(element, true);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
const element = this.options.element;
|
||||
this.enabled(false);
|
||||
element.classList.remove('focuscontainer-x');
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default AlphaPicker;
|
||||
|
||||
@@ -309,8 +309,8 @@ function askForExit() {
|
||||
exitPromise = actionsheet.show({
|
||||
title: globalize.translate('MessageConfirmAppExit'),
|
||||
items: [
|
||||
{id: 'yes', name: globalize.translate('Yes')},
|
||||
{id: 'no', name: globalize.translate('No')}
|
||||
{ id: 'yes', name: globalize.translate('Yes') },
|
||||
{ id: 'no', name: globalize.translate('No') }
|
||||
]
|
||||
}).then(function (value) {
|
||||
if (value === 'yes') {
|
||||
@@ -366,20 +366,20 @@ export const appHost = {
|
||||
};
|
||||
},
|
||||
deviceName: function () {
|
||||
return window.NativeShell?.AppHost?.deviceName
|
||||
? window.NativeShell.AppHost.deviceName() : getDeviceName();
|
||||
return window.NativeShell?.AppHost?.deviceName ?
|
||||
window.NativeShell.AppHost.deviceName() : getDeviceName();
|
||||
},
|
||||
deviceId: function () {
|
||||
return window.NativeShell?.AppHost?.deviceId
|
||||
? window.NativeShell.AppHost.deviceId() : getDeviceId();
|
||||
return window.NativeShell?.AppHost?.deviceId ?
|
||||
window.NativeShell.AppHost.deviceId() : getDeviceId();
|
||||
},
|
||||
appName: function () {
|
||||
return window.NativeShell?.AppHost?.appName
|
||||
? window.NativeShell.AppHost.appName() : appName;
|
||||
return window.NativeShell?.AppHost?.appName ?
|
||||
window.NativeShell.AppHost.appName() : appName;
|
||||
},
|
||||
appVersion: function () {
|
||||
return window.NativeShell?.AppHost?.appVersion
|
||||
? window.NativeShell.AppHost.appVersion() : Package.version;
|
||||
return window.NativeShell?.AppHost?.appVersion ?
|
||||
window.NativeShell.AppHost.appVersion() : Package.version;
|
||||
},
|
||||
getPushTokenInfo: function () {
|
||||
return {};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for performing auto-focus.
|
||||
* @module components/autoFocuser
|
||||
@@ -8,93 +6,91 @@
|
||||
import focusManager from './focusManager';
|
||||
import layoutManager from './layoutManager';
|
||||
|
||||
/**
|
||||
/**
|
||||
* Previously selected element.
|
||||
*/
|
||||
let activeElement;
|
||||
let activeElement;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns _true_ if AutoFocuser is enabled.
|
||||
*/
|
||||
export function isEnabled() {
|
||||
return layoutManager.tv;
|
||||
}
|
||||
export function isEnabled() {
|
||||
return layoutManager.tv;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Start AutoFocuser.
|
||||
*/
|
||||
export function enable() {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('focusin', function (e) {
|
||||
activeElement = e.target;
|
||||
});
|
||||
|
||||
console.debug('AutoFocuser enabled');
|
||||
export function enable() {
|
||||
if (!isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
window.addEventListener('focusin', function (e) {
|
||||
activeElement = e.target;
|
||||
});
|
||||
|
||||
console.debug('AutoFocuser enabled');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set focus on a suitable element, taking into account the previously selected.
|
||||
* @param {HTMLElement} [container] - Element to limit scope.
|
||||
* @returns {HTMLElement} Focused element.
|
||||
*/
|
||||
export function autoFocus(container) {
|
||||
if (!isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
container = container || document.body;
|
||||
|
||||
let candidates = [];
|
||||
|
||||
if (activeElement) {
|
||||
// These elements are recreated
|
||||
if (activeElement.classList.contains('btnPreviousPage')) {
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
candidates.push(container.querySelector('.btnNextPage'));
|
||||
} else if (activeElement.classList.contains('btnNextPage')) {
|
||||
candidates.push(container.querySelector('.btnNextPage'));
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
} else if (activeElement.classList.contains('btnSelectView')) {
|
||||
candidates.push(container.querySelector('.btnSelectView'));
|
||||
}
|
||||
|
||||
candidates.push(activeElement);
|
||||
}
|
||||
|
||||
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay')));
|
||||
|
||||
let focusedElement;
|
||||
|
||||
candidates.every(function (element) {
|
||||
if (focusManager.isCurrentlyFocusable(element)) {
|
||||
focusManager.focus(element);
|
||||
focusedElement = element;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!focusedElement) {
|
||||
// FIXME: Multiple itemsContainers
|
||||
const itemsContainer = container.querySelector('.itemsContainer');
|
||||
|
||||
if (itemsContainer) {
|
||||
focusedElement = focusManager.autoFocus(itemsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!focusedElement) {
|
||||
focusedElement = focusManager.autoFocus(container);
|
||||
}
|
||||
|
||||
return focusedElement;
|
||||
export function autoFocus(container) {
|
||||
if (!isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
container = container || document.body;
|
||||
|
||||
let candidates = [];
|
||||
|
||||
if (activeElement) {
|
||||
// These elements are recreated
|
||||
if (activeElement.classList.contains('btnPreviousPage')) {
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
candidates.push(container.querySelector('.btnNextPage'));
|
||||
} else if (activeElement.classList.contains('btnNextPage')) {
|
||||
candidates.push(container.querySelector('.btnNextPage'));
|
||||
candidates.push(container.querySelector('.btnPreviousPage'));
|
||||
} else if (activeElement.classList.contains('btnSelectView')) {
|
||||
candidates.push(container.querySelector('.btnSelectView'));
|
||||
}
|
||||
|
||||
candidates.push(activeElement);
|
||||
}
|
||||
|
||||
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay')));
|
||||
|
||||
let focusedElement;
|
||||
|
||||
candidates.every(function (element) {
|
||||
if (focusManager.isCurrentlyFocusable(element)) {
|
||||
focusManager.focus(element);
|
||||
focusedElement = element;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!focusedElement) {
|
||||
// FIXME: Multiple itemsContainers
|
||||
const itemsContainer = container.querySelector('.itemsContainer');
|
||||
|
||||
if (itemsContainer) {
|
||||
focusedElement = focusManager.autoFocus(itemsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
if (!focusedElement) {
|
||||
focusedElement = focusManager.autoFocus(container);
|
||||
}
|
||||
|
||||
return focusedElement;
|
||||
}
|
||||
|
||||
export default {
|
||||
isEnabled: isEnabled,
|
||||
|
||||
@@ -7,282 +7,278 @@ import ServerConnections from '../ServerConnections';
|
||||
|
||||
import './backdrop.scss';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function enableAnimation() {
|
||||
return !browser.slow;
|
||||
}
|
||||
|
||||
function enableAnimation() {
|
||||
return !browser.slow;
|
||||
}
|
||||
|
||||
function enableRotation() {
|
||||
return !browser.tv
|
||||
function enableRotation() {
|
||||
return !browser.tv
|
||||
// Causes high cpu usage
|
||||
&& !browser.firefox;
|
||||
}
|
||||
}
|
||||
|
||||
class Backdrop {
|
||||
load(url, parent, existingBackdropImage) {
|
||||
const img = new Image();
|
||||
const self = this;
|
||||
class Backdrop {
|
||||
load(url, parent, existingBackdropImage) {
|
||||
const img = new Image();
|
||||
const self = this;
|
||||
|
||||
img.onload = () => {
|
||||
if (self.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backdropImage = document.createElement('div');
|
||||
backdropImage.classList.add('backdropImage');
|
||||
backdropImage.classList.add('displayingBackdropImage');
|
||||
backdropImage.style.backgroundImage = `url('${url}')`;
|
||||
backdropImage.setAttribute('data-url', url);
|
||||
|
||||
backdropImage.classList.add('backdropImageFadeIn');
|
||||
parent.appendChild(backdropImage);
|
||||
|
||||
if (!enableAnimation()) {
|
||||
if (existingBackdropImage && existingBackdropImage.parentNode) {
|
||||
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
|
||||
}
|
||||
internalBackdrop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const onAnimationComplete = () => {
|
||||
dom.removeEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
if (backdropImage === self.currentAnimatingElement) {
|
||||
self.currentAnimatingElement = null;
|
||||
}
|
||||
if (existingBackdropImage && existingBackdropImage.parentNode) {
|
||||
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
|
||||
}
|
||||
};
|
||||
|
||||
dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
|
||||
internalBackdrop(true);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
cancelAnimation() {
|
||||
const elem = this.currentAnimatingElement;
|
||||
if (elem) {
|
||||
elem.classList.remove('backdropImageFadeIn');
|
||||
this.currentAnimatingElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.isDestroyed = true;
|
||||
this.cancelAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
let backdropContainer;
|
||||
function getBackdropContainer() {
|
||||
if (!backdropContainer) {
|
||||
backdropContainer = document.querySelector('.backdropContainer');
|
||||
}
|
||||
|
||||
if (!backdropContainer) {
|
||||
backdropContainer = document.createElement('div');
|
||||
backdropContainer.classList.add('backdropContainer');
|
||||
document.body.insertBefore(backdropContainer, document.body.firstChild);
|
||||
}
|
||||
|
||||
return backdropContainer;
|
||||
}
|
||||
|
||||
export function clearBackdrop(clearAll) {
|
||||
clearRotation();
|
||||
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
elem.innerHTML = '';
|
||||
|
||||
if (clearAll) {
|
||||
hasExternalBackdrop = false;
|
||||
}
|
||||
|
||||
internalBackdrop(false);
|
||||
}
|
||||
|
||||
let backgroundContainer;
|
||||
function getBackgroundContainer() {
|
||||
if (!backgroundContainer) {
|
||||
backgroundContainer = document.querySelector('.backgroundContainer');
|
||||
}
|
||||
return backgroundContainer;
|
||||
}
|
||||
|
||||
function setBackgroundContainerBackgroundEnabled() {
|
||||
if (hasInternalBackdrop || hasExternalBackdrop) {
|
||||
getBackgroundContainer().classList.add('withBackdrop');
|
||||
} else {
|
||||
getBackgroundContainer().classList.remove('withBackdrop');
|
||||
}
|
||||
}
|
||||
|
||||
let hasInternalBackdrop;
|
||||
function internalBackdrop(isEnabled) {
|
||||
hasInternalBackdrop = isEnabled;
|
||||
setBackgroundContainerBackgroundEnabled();
|
||||
}
|
||||
|
||||
let hasExternalBackdrop;
|
||||
export function externalBackdrop(isEnabled) {
|
||||
hasExternalBackdrop = isEnabled;
|
||||
setBackgroundContainerBackgroundEnabled();
|
||||
}
|
||||
|
||||
let currentLoadingBackdrop;
|
||||
function setBackdropImage(url) {
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
const existingBackdropImage = elem.querySelector('.displayingBackdropImage');
|
||||
|
||||
if (existingBackdropImage && existingBackdropImage.getAttribute('data-url') === url) {
|
||||
if (existingBackdropImage.getAttribute('data-url') === url) {
|
||||
img.onload = () => {
|
||||
if (self.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
existingBackdropImage.classList.remove('displayingBackdropImage');
|
||||
}
|
||||
|
||||
const instance = new Backdrop();
|
||||
instance.load(url, elem, existingBackdropImage);
|
||||
currentLoadingBackdrop = instance;
|
||||
}
|
||||
const backdropImage = document.createElement('div');
|
||||
backdropImage.classList.add('backdropImage');
|
||||
backdropImage.classList.add('displayingBackdropImage');
|
||||
backdropImage.style.backgroundImage = `url('${url}')`;
|
||||
backdropImage.setAttribute('data-url', url);
|
||||
|
||||
function getItemImageUrls(item, imageOptions) {
|
||||
imageOptions = imageOptions || {};
|
||||
backdropImage.classList.add('backdropImageFadeIn');
|
||||
parent.appendChild(backdropImage);
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
if (item.BackdropImageTags && item.BackdropImageTags.length > 0) {
|
||||
return item.BackdropImageTags.map((imgTag, index) => {
|
||||
return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, {
|
||||
type: 'Backdrop',
|
||||
tag: imgTag,
|
||||
maxWidth: dom.getScreenWidth(),
|
||||
index: index
|
||||
}));
|
||||
if (!enableAnimation()) {
|
||||
if (existingBackdropImage && existingBackdropImage.parentNode) {
|
||||
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
|
||||
}
|
||||
internalBackdrop(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const onAnimationComplete = () => {
|
||||
dom.removeEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
if (backdropImage === self.currentAnimatingElement) {
|
||||
self.currentAnimatingElement = null;
|
||||
}
|
||||
if (existingBackdropImage && existingBackdropImage.parentNode) {
|
||||
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
|
||||
}
|
||||
};
|
||||
|
||||
dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
|
||||
if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
|
||||
return item.ParentBackdropImageTags.map((imgTag, index) => {
|
||||
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, Object.assign(imageOptions, {
|
||||
type: 'Backdrop',
|
||||
tag: imgTag,
|
||||
maxWidth: dom.getScreenWidth(),
|
||||
index: index
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getImageUrls(items, imageOptions) {
|
||||
const list = [];
|
||||
const onImg = img => {
|
||||
list.push(img);
|
||||
internalBackdrop(true);
|
||||
};
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const itemImages = getItemImageUrls(items[i], imageOptions);
|
||||
itemImages.forEach(onImg);
|
||||
}
|
||||
|
||||
return list;
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
function enabled() {
|
||||
return userSettings.enableBackdrops();
|
||||
}
|
||||
|
||||
let rotationInterval;
|
||||
let currentRotatingImages = [];
|
||||
let currentRotationIndex = -1;
|
||||
export function setBackdrops(items, imageOptions, enableImageRotation) {
|
||||
if (enabled()) {
|
||||
const images = getImageUrls(items, imageOptions);
|
||||
|
||||
if (images.length) {
|
||||
startRotation(images, enableImageRotation);
|
||||
} else {
|
||||
clearBackdrop();
|
||||
}
|
||||
cancelAnimation() {
|
||||
const elem = this.currentAnimatingElement;
|
||||
if (elem) {
|
||||
elem.classList.remove('backdropImageFadeIn');
|
||||
this.currentAnimatingElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startRotation(images, enableImageRotation) {
|
||||
if (isEqual(images, currentRotatingImages)) {
|
||||
destroy() {
|
||||
this.isDestroyed = true;
|
||||
this.cancelAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
let backdropContainer;
|
||||
function getBackdropContainer() {
|
||||
if (!backdropContainer) {
|
||||
backdropContainer = document.querySelector('.backdropContainer');
|
||||
}
|
||||
|
||||
if (!backdropContainer) {
|
||||
backdropContainer = document.createElement('div');
|
||||
backdropContainer.classList.add('backdropContainer');
|
||||
document.body.insertBefore(backdropContainer, document.body.firstChild);
|
||||
}
|
||||
|
||||
return backdropContainer;
|
||||
}
|
||||
|
||||
export function clearBackdrop(clearAll) {
|
||||
clearRotation();
|
||||
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
elem.innerHTML = '';
|
||||
|
||||
if (clearAll) {
|
||||
hasExternalBackdrop = false;
|
||||
}
|
||||
|
||||
internalBackdrop(false);
|
||||
}
|
||||
|
||||
let backgroundContainer;
|
||||
function getBackgroundContainer() {
|
||||
if (!backgroundContainer) {
|
||||
backgroundContainer = document.querySelector('.backgroundContainer');
|
||||
}
|
||||
return backgroundContainer;
|
||||
}
|
||||
|
||||
function setBackgroundContainerBackgroundEnabled() {
|
||||
if (hasInternalBackdrop || hasExternalBackdrop) {
|
||||
getBackgroundContainer().classList.add('withBackdrop');
|
||||
} else {
|
||||
getBackgroundContainer().classList.remove('withBackdrop');
|
||||
}
|
||||
}
|
||||
|
||||
let hasInternalBackdrop;
|
||||
function internalBackdrop(isEnabled) {
|
||||
hasInternalBackdrop = isEnabled;
|
||||
setBackgroundContainerBackgroundEnabled();
|
||||
}
|
||||
|
||||
let hasExternalBackdrop;
|
||||
export function externalBackdrop(isEnabled) {
|
||||
hasExternalBackdrop = isEnabled;
|
||||
setBackgroundContainerBackgroundEnabled();
|
||||
}
|
||||
|
||||
let currentLoadingBackdrop;
|
||||
function setBackdropImage(url) {
|
||||
if (currentLoadingBackdrop) {
|
||||
currentLoadingBackdrop.destroy();
|
||||
currentLoadingBackdrop = null;
|
||||
}
|
||||
|
||||
const elem = getBackdropContainer();
|
||||
const existingBackdropImage = elem.querySelector('.displayingBackdropImage');
|
||||
|
||||
if (existingBackdropImage && existingBackdropImage.getAttribute('data-url') === url) {
|
||||
if (existingBackdropImage.getAttribute('data-url') === url) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearRotation();
|
||||
|
||||
currentRotatingImages = images;
|
||||
currentRotationIndex = -1;
|
||||
|
||||
if (images.length > 1 && enableImageRotation !== false && enableRotation()) {
|
||||
rotationInterval = setInterval(onRotationInterval, 24000);
|
||||
}
|
||||
|
||||
onRotationInterval();
|
||||
existingBackdropImage.classList.remove('displayingBackdropImage');
|
||||
}
|
||||
|
||||
function onRotationInterval() {
|
||||
if (playbackManager.isPlayingLocally(['Video'])) {
|
||||
return;
|
||||
}
|
||||
const instance = new Backdrop();
|
||||
instance.load(url, elem, existingBackdropImage);
|
||||
currentLoadingBackdrop = instance;
|
||||
}
|
||||
|
||||
let newIndex = currentRotationIndex + 1;
|
||||
if (newIndex >= currentRotatingImages.length) {
|
||||
newIndex = 0;
|
||||
}
|
||||
function getItemImageUrls(item, imageOptions) {
|
||||
imageOptions = imageOptions || {};
|
||||
|
||||
currentRotationIndex = newIndex;
|
||||
setBackdropImage(currentRotatingImages[newIndex]);
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
if (item.BackdropImageTags && item.BackdropImageTags.length > 0) {
|
||||
return item.BackdropImageTags.map((imgTag, index) => {
|
||||
return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, {
|
||||
type: 'Backdrop',
|
||||
tag: imgTag,
|
||||
maxWidth: dom.getScreenWidth(),
|
||||
index: index
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function clearRotation() {
|
||||
const interval = rotationInterval;
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
rotationInterval = null;
|
||||
currentRotatingImages = [];
|
||||
currentRotationIndex = -1;
|
||||
if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
|
||||
return item.ParentBackdropImageTags.map((imgTag, index) => {
|
||||
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, Object.assign(imageOptions, {
|
||||
type: 'Backdrop',
|
||||
tag: imgTag,
|
||||
maxWidth: dom.getScreenWidth(),
|
||||
index: index
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export function setBackdrop(url, imageOptions) {
|
||||
if (url && typeof url !== 'string') {
|
||||
url = getImageUrls([url], imageOptions)[0];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
clearRotation();
|
||||
setBackdropImage(url);
|
||||
function getImageUrls(items, imageOptions) {
|
||||
const list = [];
|
||||
const onImg = img => {
|
||||
list.push(img);
|
||||
};
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const itemImages = getItemImageUrls(items[i], imageOptions);
|
||||
itemImages.forEach(onImg);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function enabled() {
|
||||
return userSettings.enableBackdrops();
|
||||
}
|
||||
|
||||
let rotationInterval;
|
||||
let currentRotatingImages = [];
|
||||
let currentRotationIndex = -1;
|
||||
export function setBackdrops(items, imageOptions, enableImageRotation) {
|
||||
if (enabled()) {
|
||||
const images = getImageUrls(items, imageOptions);
|
||||
|
||||
if (images.length) {
|
||||
startRotation(images, enableImageRotation);
|
||||
} else {
|
||||
clearBackdrop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
function startRotation(images, enableImageRotation) {
|
||||
if (isEqual(images, currentRotatingImages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearRotation();
|
||||
|
||||
currentRotatingImages = images;
|
||||
currentRotationIndex = -1;
|
||||
|
||||
if (images.length > 1 && enableImageRotation !== false && enableRotation()) {
|
||||
rotationInterval = setInterval(onRotationInterval, 24000);
|
||||
}
|
||||
|
||||
onRotationInterval();
|
||||
}
|
||||
|
||||
function onRotationInterval() {
|
||||
if (playbackManager.isPlayingLocally(['Video'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newIndex = currentRotationIndex + 1;
|
||||
if (newIndex >= currentRotatingImages.length) {
|
||||
newIndex = 0;
|
||||
}
|
||||
|
||||
currentRotationIndex = newIndex;
|
||||
setBackdropImage(currentRotatingImages[newIndex]);
|
||||
}
|
||||
|
||||
function clearRotation() {
|
||||
const interval = rotationInterval;
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
rotationInterval = null;
|
||||
currentRotatingImages = [];
|
||||
currentRotationIndex = -1;
|
||||
}
|
||||
|
||||
export function setBackdrop(url, imageOptions) {
|
||||
if (url && typeof url !== 'string') {
|
||||
url = getImageUrls([url], imageOptions)[0];
|
||||
}
|
||||
|
||||
if (url) {
|
||||
clearRotation();
|
||||
setBackdropImage(url);
|
||||
} else {
|
||||
clearBackdrop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @enum TransparencyLevel
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for building cards from item data.
|
||||
@@ -12,123 +11,121 @@ import layoutManager from '../layoutManager';
|
||||
import browser from '../../scripts/browser';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
function buildChapterCardsHtml(item, chapters, options) {
|
||||
// TODO move card creation code to Card component
|
||||
function buildChapterCardsHtml(item, chapters, options) {
|
||||
// TODO move card creation code to Card component
|
||||
|
||||
let className = 'card itemAction chapterCard';
|
||||
let className = 'card itemAction chapterCard';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
className += ' show-focus';
|
||||
if (layoutManager.tv) {
|
||||
className += ' show-focus';
|
||||
|
||||
if (enableFocusTransform) {
|
||||
className += ' show-animation';
|
||||
}
|
||||
if (enableFocusTransform) {
|
||||
className += ' show-animation';
|
||||
}
|
||||
|
||||
const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || [];
|
||||
const videoStream = mediaStreams.filter(({Type}) => {
|
||||
return Type === 'Video';
|
||||
})[0] || {};
|
||||
|
||||
let shape = (options.backdropShape || 'backdrop');
|
||||
|
||||
if (videoStream.Width && videoStream.Height && (videoStream.Width / videoStream.Height) <= 1.2) {
|
||||
shape = (options.squareShape || 'square');
|
||||
}
|
||||
|
||||
className += ` ${shape}Card`;
|
||||
|
||||
if (options.block || options.rows) {
|
||||
className += ' block';
|
||||
}
|
||||
|
||||
let html = '';
|
||||
let itemsInRow = 0;
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
for (let i = 0, length = chapters.length; i < length; i++) {
|
||||
if (options.rows && itemsInRow === 0) {
|
||||
html += '<div class="cardColumn">';
|
||||
}
|
||||
|
||||
const chapter = chapters[i];
|
||||
|
||||
html += buildChapterCard(item, apiClient, chapter, i, options, className, shape);
|
||||
itemsInRow++;
|
||||
|
||||
if (options.rows && itemsInRow >= options.rows) {
|
||||
itemsInRow = 0;
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function getImgUrl({Id}, {ImageTag}, index, maxWidth, apiClient) {
|
||||
if (ImageTag) {
|
||||
return apiClient.getScaledImageUrl(Id, {
|
||||
const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || [];
|
||||
const videoStream = mediaStreams.filter(({ Type }) => {
|
||||
return Type === 'Video';
|
||||
})[0] || {};
|
||||
|
||||
maxWidth: maxWidth,
|
||||
tag: ImageTag,
|
||||
type: 'Chapter',
|
||||
index
|
||||
});
|
||||
}
|
||||
let shape = (options.backdropShape || 'backdrop');
|
||||
|
||||
return null;
|
||||
if (videoStream.Width && videoStream.Height && (videoStream.Width / videoStream.Height) <= 1.2) {
|
||||
shape = (options.squareShape || 'square');
|
||||
}
|
||||
|
||||
function buildChapterCard(item, apiClient, chapter, index, {width, coverImage}, className, shape) {
|
||||
const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient);
|
||||
className += ` ${shape}Card`;
|
||||
|
||||
let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer';
|
||||
if (coverImage) {
|
||||
cardImageContainerClass += ' coveredImage';
|
||||
}
|
||||
const dataAttributes = ` data-action="play" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-positionticks="${chapter.StartPositionTicks}"`;
|
||||
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
|
||||
|
||||
if (!imgUrl) {
|
||||
cardImageContainer += '<span class="material-icons cardImageIcon local_movies" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
let nameHtml = '';
|
||||
nameHtml += `<div class="cardText">${escapeHtml(chapter.Name)}</div>`;
|
||||
nameHtml += `<div class="cardText">${datetime.getDisplayRunningTime(chapter.StartPositionTicks)}</div>`;
|
||||
|
||||
const cardBoxCssClass = 'cardBox';
|
||||
const cardScalableClass = 'cardScalable';
|
||||
|
||||
return `<button type="button" class="${className}"${dataAttributes}><div class="${cardBoxCssClass}"><div class="${cardScalableClass}"><div class="cardPadder-${shape}"></div>${cardImageContainer}</div><div class="innerCardFooter">${nameHtml}</div></div></div></button>`;
|
||||
if (options.block || options.rows) {
|
||||
className += ' block';
|
||||
}
|
||||
|
||||
export function buildChapterCards(item, chapters, options) {
|
||||
if (options.parentContainer) {
|
||||
// Abort if the container has been disposed
|
||||
if (!document.body.contains(options.parentContainer)) {
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
let itemsInRow = 0;
|
||||
|
||||
if (chapters.length) {
|
||||
options.parentContainer.classList.remove('hide');
|
||||
} else {
|
||||
options.parentContainer.classList.add('hide');
|
||||
return;
|
||||
}
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
for (let i = 0, length = chapters.length; i < length; i++) {
|
||||
if (options.rows && itemsInRow === 0) {
|
||||
html += '<div class="cardColumn">';
|
||||
}
|
||||
|
||||
const html = buildChapterCardsHtml(item, chapters, options);
|
||||
const chapter = chapters[i];
|
||||
|
||||
options.itemsContainer.innerHTML = html;
|
||||
html += buildChapterCard(item, apiClient, chapter, i, options, className, shape);
|
||||
itemsInRow++;
|
||||
|
||||
imageLoader.lazyChildren(options.itemsContainer);
|
||||
if (options.rows && itemsInRow >= options.rows) {
|
||||
itemsInRow = 0;
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
return html;
|
||||
}
|
||||
|
||||
function getImgUrl({ Id }, { ImageTag }, index, maxWidth, apiClient) {
|
||||
if (ImageTag) {
|
||||
return apiClient.getScaledImageUrl(Id, {
|
||||
|
||||
maxWidth: maxWidth,
|
||||
tag: ImageTag,
|
||||
type: 'Chapter',
|
||||
index
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildChapterCard(item, apiClient, chapter, index, { width, coverImage }, className, shape) {
|
||||
const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient);
|
||||
|
||||
let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer';
|
||||
if (coverImage) {
|
||||
cardImageContainerClass += ' coveredImage';
|
||||
}
|
||||
const dataAttributes = ` data-action="play" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-positionticks="${chapter.StartPositionTicks}"`;
|
||||
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
|
||||
|
||||
if (!imgUrl) {
|
||||
cardImageContainer += '<span class="material-icons cardImageIcon local_movies" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
let nameHtml = '';
|
||||
nameHtml += `<div class="cardText">${escapeHtml(chapter.Name)}</div>`;
|
||||
nameHtml += `<div class="cardText">${datetime.getDisplayRunningTime(chapter.StartPositionTicks)}</div>`;
|
||||
|
||||
const cardBoxCssClass = 'cardBox';
|
||||
const cardScalableClass = 'cardScalable';
|
||||
|
||||
return `<button type="button" class="${className}"${dataAttributes}><div class="${cardBoxCssClass}"><div class="${cardScalableClass}"><div class="cardPadder-${shape}"></div>${cardImageContainer}</div><div class="innerCardFooter">${nameHtml}</div></div></div></button>`;
|
||||
}
|
||||
|
||||
export function buildChapterCards(item, chapters, options) {
|
||||
if (options.parentContainer) {
|
||||
// Abort if the container has been disposed
|
||||
if (!document.body.contains(options.parentContainer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chapters.length) {
|
||||
options.parentContainer.classList.remove('hide');
|
||||
} else {
|
||||
options.parentContainer.classList.add('hide');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const html = buildChapterCardsHtml(item, chapters, options);
|
||||
|
||||
options.itemsContainer.innerHTML = html;
|
||||
|
||||
imageLoader.lazyChildren(options.itemsContainer);
|
||||
}
|
||||
|
||||
export default {
|
||||
buildChapterCards: buildChapterCards
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for building cards from item data.
|
||||
@@ -7,20 +6,18 @@
|
||||
|
||||
import cardBuilder from './cardBuilder';
|
||||
|
||||
export function buildPeopleCards(items, options) {
|
||||
options = Object.assign(options || {}, {
|
||||
cardLayout: false,
|
||||
centerText: true,
|
||||
showTitle: true,
|
||||
cardFooterAside: 'none',
|
||||
showPersonRoleOrType: true,
|
||||
cardCssClass: 'personCard',
|
||||
defaultCardImageIcon: 'person'
|
||||
});
|
||||
cardBuilder.buildCards(items, options);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export function buildPeopleCards(items, options) {
|
||||
options = Object.assign(options || {}, {
|
||||
cardLayout: false,
|
||||
centerText: true,
|
||||
showTitle: true,
|
||||
cardFooterAside: 'none',
|
||||
showPersonRoleOrType: true,
|
||||
cardCssClass: 'personCard',
|
||||
defaultCardImageIcon: 'person'
|
||||
});
|
||||
cardBuilder.buildCards(items, options);
|
||||
}
|
||||
|
||||
export default {
|
||||
buildPeopleCards: buildPeopleCards
|
||||
|
||||
@@ -16,254 +16,251 @@ import '../../styles/flexstyles.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
|
||||
/* eslint-disable indent */
|
||||
let currentServerId;
|
||||
|
||||
let currentServerId;
|
||||
function onSubmit(e) {
|
||||
loading.show();
|
||||
|
||||
function onSubmit(e) {
|
||||
loading.show();
|
||||
const panel = dom.parentWithClass(this, 'dialog');
|
||||
|
||||
const panel = dom.parentWithClass(this, 'dialog');
|
||||
const collectionId = panel.querySelector('#selectCollectionToAddTo').value;
|
||||
|
||||
const collectionId = panel.querySelector('#selectCollectionToAddTo').value;
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
|
||||
if (collectionId) {
|
||||
addToCollection(apiClient, panel, collectionId);
|
||||
} else {
|
||||
createCollection(apiClient, panel);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
if (collectionId) {
|
||||
addToCollection(apiClient, panel, collectionId);
|
||||
} else {
|
||||
createCollection(apiClient, panel);
|
||||
}
|
||||
|
||||
function createCollection(apiClient, dlg) {
|
||||
const url = apiClient.getUrl('Collections', {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
Name: dlg.querySelector('#txtNewCollectionName').value,
|
||||
IsLocked: !dlg.querySelector('#chkEnableInternetMetadata').checked,
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
|
||||
});
|
||||
function createCollection(apiClient, dlg) {
|
||||
const url = apiClient.getUrl('Collections', {
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json'
|
||||
Name: dlg.querySelector('#txtNewCollectionName').value,
|
||||
IsLocked: !dlg.querySelector('#chkEnableInternetMetadata').checked,
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
|
||||
});
|
||||
|
||||
}).then(result => {
|
||||
loading.hide();
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json'
|
||||
|
||||
const id = result.Id;
|
||||
}).then(result => {
|
||||
loading.hide();
|
||||
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
redirectToCollection(apiClient, id);
|
||||
});
|
||||
}
|
||||
const id = result.Id;
|
||||
|
||||
function redirectToCollection(apiClient, id) {
|
||||
appRouter.showItem(id, apiClient.serverId());
|
||||
}
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
redirectToCollection(apiClient, id);
|
||||
});
|
||||
}
|
||||
|
||||
function addToCollection(apiClient, dlg, id) {
|
||||
const url = apiClient.getUrl(`Collections/${id}/Items`, {
|
||||
function redirectToCollection(apiClient, id) {
|
||||
appRouter.showItem(id, apiClient.serverId());
|
||||
}
|
||||
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
|
||||
});
|
||||
function addToCollection(apiClient, dlg, id) {
|
||||
const url = apiClient.getUrl(`Collections/${id}/Items`, {
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
|
||||
});
|
||||
|
||||
}).then(() => {
|
||||
loading.hide();
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url
|
||||
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
}).then(() => {
|
||||
loading.hide();
|
||||
|
||||
toast(globalize.translate('MessageItemsAdded'));
|
||||
});
|
||||
}
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
|
||||
function triggerChange(select) {
|
||||
select.dispatchEvent(new CustomEvent('change', {}));
|
||||
}
|
||||
toast(globalize.translate('MessageItemsAdded'));
|
||||
});
|
||||
}
|
||||
|
||||
function populateCollections(panel) {
|
||||
loading.show();
|
||||
function triggerChange(select) {
|
||||
select.dispatchEvent(new CustomEvent('change', {}));
|
||||
}
|
||||
|
||||
const select = panel.querySelector('#selectCollectionToAddTo');
|
||||
function populateCollections(panel) {
|
||||
loading.show();
|
||||
|
||||
panel.querySelector('.newCollectionInfo').classList.add('hide');
|
||||
const select = panel.querySelector('#selectCollectionToAddTo');
|
||||
|
||||
const options = {
|
||||
panel.querySelector('.newCollectionInfo').classList.add('hide');
|
||||
|
||||
Recursive: true,
|
||||
IncludeItemTypes: 'BoxSet',
|
||||
SortBy: 'SortName',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
const options = {
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
|
||||
let html = '';
|
||||
Recursive: true,
|
||||
IncludeItemTypes: 'BoxSet',
|
||||
SortBy: 'SortName',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
|
||||
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
|
||||
|
||||
html += result.Items.map(i => {
|
||||
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
|
||||
});
|
||||
|
||||
select.innerHTML = html;
|
||||
select.value = '';
|
||||
triggerChange(select);
|
||||
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function getEditorHtml() {
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
||||
html += '<div class="dialogContentInner dialog-content-centered">';
|
||||
html += '<form class="newCollectionForm" style="margin:auto;">';
|
||||
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
|
||||
|
||||
html += '<div>';
|
||||
html += globalize.translate('NewCollectionHelp');
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="fldSelectCollection">';
|
||||
html += '<br/>';
|
||||
html += '<br/>';
|
||||
html += '<div class="selectContainer">';
|
||||
html += `<select is="emby-select" label="${globalize.translate('LabelCollection')}" id="selectCollectionToAddTo" autofocus></select>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="newCollectionInfo">';
|
||||
|
||||
html += '<div class="inputContainer">';
|
||||
html += `<input is="emby-input" type="text" id="txtNewCollectionName" required="required" label="${globalize.translate('LabelName')}" />`;
|
||||
html += `<div class="fieldDescription">${globalize.translate('NewCollectionNameExample')}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<label class="checkboxContainer">';
|
||||
html += '<input is="emby-checkbox" type="checkbox" id="chkEnableInternetMetadata" />';
|
||||
html += `<span>${globalize.translate('SearchForCollectionInternetMetadata')}</span>`;
|
||||
html += '</label>';
|
||||
|
||||
// newCollectionInfo
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="formDialogFooter">';
|
||||
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('ButtonOk')}</button>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
||||
|
||||
html += '</form>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function initEditor(content, items) {
|
||||
content.querySelector('#selectCollectionToAddTo').addEventListener('change', function () {
|
||||
if (this.value) {
|
||||
content.querySelector('.newCollectionInfo').classList.add('hide');
|
||||
content.querySelector('#txtNewCollectionName').removeAttribute('required');
|
||||
} else {
|
||||
content.querySelector('.newCollectionInfo').classList.remove('hide');
|
||||
content.querySelector('#txtNewCollectionName').setAttribute('required', 'required');
|
||||
}
|
||||
html += result.Items.map(i => {
|
||||
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
|
||||
});
|
||||
|
||||
content.querySelector('form').addEventListener('submit', onSubmit);
|
||||
select.innerHTML = html;
|
||||
select.value = '';
|
||||
triggerChange(select);
|
||||
|
||||
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length) {
|
||||
content.querySelector('.fldSelectCollection').classList.remove('hide');
|
||||
populateCollections(content);
|
||||
function getEditorHtml() {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
||||
html += '<div class="dialogContentInner dialog-content-centered">';
|
||||
html += '<form class="newCollectionForm" style="margin:auto;">';
|
||||
|
||||
html += '<div>';
|
||||
html += globalize.translate('NewCollectionHelp');
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="fldSelectCollection">';
|
||||
html += '<br/>';
|
||||
html += '<br/>';
|
||||
html += '<div class="selectContainer">';
|
||||
html += `<select is="emby-select" label="${globalize.translate('LabelCollection')}" id="selectCollectionToAddTo" autofocus></select>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="newCollectionInfo">';
|
||||
|
||||
html += '<div class="inputContainer">';
|
||||
html += `<input is="emby-input" type="text" id="txtNewCollectionName" required="required" label="${globalize.translate('LabelName')}" />`;
|
||||
html += `<div class="fieldDescription">${globalize.translate('NewCollectionNameExample')}</div>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<label class="checkboxContainer">';
|
||||
html += '<input is="emby-checkbox" type="checkbox" id="chkEnableInternetMetadata" />';
|
||||
html += `<span>${globalize.translate('SearchForCollectionInternetMetadata')}</span>`;
|
||||
html += '</label>';
|
||||
|
||||
// newCollectionInfo
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="formDialogFooter">';
|
||||
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('ButtonOk')}</button>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
||||
|
||||
html += '</form>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function initEditor(content, items) {
|
||||
content.querySelector('#selectCollectionToAddTo').addEventListener('change', function () {
|
||||
if (this.value) {
|
||||
content.querySelector('.newCollectionInfo').classList.add('hide');
|
||||
content.querySelector('#txtNewCollectionName').removeAttribute('required');
|
||||
} else {
|
||||
content.querySelector('.fldSelectCollection').classList.add('hide');
|
||||
|
||||
const selectCollectionToAddTo = content.querySelector('#selectCollectionToAddTo');
|
||||
selectCollectionToAddTo.innerHTML = '';
|
||||
selectCollectionToAddTo.value = '';
|
||||
triggerChange(selectCollectionToAddTo);
|
||||
content.querySelector('.newCollectionInfo').classList.remove('hide');
|
||||
content.querySelector('#txtNewCollectionName').setAttribute('required', 'required');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
content.querySelector('form').addEventListener('submit', onSubmit);
|
||||
|
||||
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
|
||||
|
||||
if (items.length) {
|
||||
content.querySelector('.fldSelectCollection').classList.remove('hide');
|
||||
populateCollections(content);
|
||||
} else {
|
||||
content.querySelector('.fldSelectCollection').classList.add('hide');
|
||||
|
||||
const selectCollectionToAddTo = content.querySelector('#selectCollectionToAddTo');
|
||||
selectCollectionToAddTo.innerHTML = '';
|
||||
selectCollectionToAddTo.value = '';
|
||||
triggerChange(selectCollectionToAddTo);
|
||||
}
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
}
|
||||
|
||||
class CollectionEditor {
|
||||
show(options) {
|
||||
const items = options.items || {};
|
||||
currentServerId = options.serverId;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
let html = '';
|
||||
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
|
||||
|
||||
html += '<div class="formDialogHeader">';
|
||||
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += '<h3 class="formDialogHeaderTitle">';
|
||||
html += title;
|
||||
html += '</h3>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += getEditorHtml();
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
initEditor(dlg, items);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||
}
|
||||
|
||||
if (dlg.submitted) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CollectionEditor {
|
||||
show(options) {
|
||||
const items = options.items || {};
|
||||
currentServerId = options.serverId;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
let html = '';
|
||||
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
|
||||
|
||||
html += '<div class="formDialogHeader">';
|
||||
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += '<h3 class="formDialogHeaderTitle">';
|
||||
html += title;
|
||||
html += '</h3>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += getEditorHtml();
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
initEditor(dlg, items);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||
}
|
||||
|
||||
if (dlg.submitted) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default CollectionEditor;
|
||||
|
||||
@@ -22,7 +22,7 @@ const Filter: FC<FilterProps> = ({
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showFilterMenu = useCallback(() => {
|
||||
import('../filtermenu/filtermenu').then(({default: FilterMenu}) => {
|
||||
import('../filtermenu/filtermenu').then(({ default: FilterMenu }) => {
|
||||
const filterMenu = new FilterMenu();
|
||||
filterMenu.show({
|
||||
settings: viewQuerySettings,
|
||||
|
||||
@@ -6,7 +6,7 @@ const NewCollection: FC = () => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showCollectionEditor = useCallback(() => {
|
||||
import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => {
|
||||
import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
|
||||
const serverId = window.ApiClient.serverId();
|
||||
const collectionEditor = new CollectionEditor();
|
||||
collectionEditor.show({
|
||||
|
||||
@@ -16,7 +16,7 @@ const SelectView: FC<SelectViewProps> = ({
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showViewSettingsMenu = useCallback(() => {
|
||||
import('../viewSettings/viewSettings').then(({default: ViewSettings}) => {
|
||||
import('../viewSettings/viewSettings').then(({ default: ViewSettings }) => {
|
||||
const viewsettings = new ViewSettings();
|
||||
viewsettings.show({
|
||||
settings: viewQuerySettings,
|
||||
|
||||
@@ -19,7 +19,7 @@ const Sort: FC<SortProps> = ({
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showSortMenu = useCallback(() => {
|
||||
import('../sortmenu/sortmenu').then(({default: SortMenu}) => {
|
||||
import('../sortmenu/sortmenu').then(({ default: SortMenu }) => {
|
||||
const sortMenu = new SortMenu();
|
||||
sortMenu.show({
|
||||
settings: viewQuerySettings,
|
||||
|
||||
@@ -14,7 +14,7 @@ type IProps = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const AccessContainer: FunctionComponent<IProps> = ({containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
|
||||
const AccessContainer: FunctionComponent<IProps> = ({ containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => {
|
||||
return (
|
||||
<div className={containerClassName}>
|
||||
<h2>{globalize.translate(headerTitle)}</h2>
|
||||
|
||||
@@ -22,7 +22,7 @@ function getDisplayTime(hours = 0) {
|
||||
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
|
||||
}
|
||||
|
||||
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({index, DayOfWeek, StartHour, EndHour}: AccessScheduleListProps) => {
|
||||
const AccessScheduleList: FunctionComponent<AccessScheduleListProps> = ({ index, DayOfWeek, StartHour, EndHour }: AccessScheduleListProps) => {
|
||||
return (
|
||||
<div
|
||||
className='liSchedule listItem'
|
||||
|
||||
@@ -5,7 +5,7 @@ type IProps = {
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => {
|
||||
const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
|
||||
return (
|
||||
<div className='paperList'>
|
||||
<div className='listItem'>
|
||||
|
||||
@@ -36,7 +36,7 @@ const createLinkElement = (activeTab: string) => ({
|
||||
</a>`
|
||||
});
|
||||
|
||||
const SectionTabs: FunctionComponent<IProps> = ({activeTab}: IProps) => {
|
||||
const SectionTabs: FunctionComponent<IProps> = ({ activeTab }: IProps) => {
|
||||
return (
|
||||
<div
|
||||
data-role='controlgroup'
|
||||
|
||||
@@ -74,7 +74,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
|
||||
</div>
|
||||
<div className='cardFooter visualCardBox-cardFooter'>
|
||||
<div
|
||||
style={{textAlign: 'right', float: 'right', paddingTop: '5px'}}
|
||||
style={{ textAlign: 'right', float: 'right', paddingTop: '5px' }}
|
||||
>
|
||||
<IconButtonElement
|
||||
is='paper-icon-button-light'
|
||||
|
||||
@@ -14,7 +14,7 @@ type IProps = {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
||||
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
const loadUser = useCallback(() => {
|
||||
@@ -76,7 +76,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
||||
|
||||
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
|
||||
|
||||
import('../../autoFocuser').then(({default: autoFocuser}) => {
|
||||
import('../../autoFocuser').then(({ default: autoFocuser }) => {
|
||||
autoFocuser.autoFocus(page);
|
||||
});
|
||||
});
|
||||
@@ -214,7 +214,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
||||
<div ref={element}>
|
||||
<form
|
||||
className='updatePasswordForm passwordSection hide'
|
||||
style={{margin: '0 auto 2em'}}
|
||||
style={{ margin: '0 auto 2em' }}
|
||||
>
|
||||
<div className='detailSection'>
|
||||
<div id='fldCurrentPassword' className='inputContainer hide'>
|
||||
@@ -260,7 +260,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
|
||||
<br />
|
||||
<form
|
||||
className='localAccessForm localAccessSection'
|
||||
style={{margin: '0 auto'}}
|
||||
style={{ margin: '0 auto' }}
|
||||
>
|
||||
<div className='detailSection'>
|
||||
<div className='detailSectionHeader'>
|
||||
|
||||
@@ -13,129 +13,126 @@ import '../formdialog.scss';
|
||||
import '../../styles/flexstyles.scss';
|
||||
import template from './dialog.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function showDialog(options = { dialogOptions: {}, buttons: [] }) {
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false,
|
||||
...options.dialogOptions
|
||||
};
|
||||
|
||||
function showDialog(options = { dialogOptions: {}, buttons: [] }) {
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false,
|
||||
...options.dialogOptions
|
||||
};
|
||||
const enableTvLayout = layoutManager.tv;
|
||||
|
||||
const enableTvLayout = layoutManager.tv;
|
||||
if (enableTvLayout) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
}
|
||||
|
||||
if (enableTvLayout) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
dlg.classList.add('align-items-center');
|
||||
dlg.classList.add('justify-content-center');
|
||||
const formDialogContent = dlg.querySelector('.formDialogContent');
|
||||
formDialogContent.classList.add('no-grow');
|
||||
|
||||
if (enableTvLayout) {
|
||||
formDialogContent.style['max-width'] = '50%';
|
||||
formDialogContent.style['max-height'] = '60%';
|
||||
scrollHelper.centerFocus.on(formDialogContent, false);
|
||||
} else {
|
||||
formDialogContent.style.maxWidth = `${Math.min((options.buttons.length * 150) + 200, dom.getWindowSize().innerWidth - 50)}px`;
|
||||
dlg.classList.add('dialog-fullscreen-lowres');
|
||||
}
|
||||
|
||||
if (options.title) {
|
||||
dlg.querySelector('.formDialogHeaderTitle').innerText = options.title || '';
|
||||
} else {
|
||||
dlg.querySelector('.formDialogHeaderTitle').classList.add('hide');
|
||||
}
|
||||
|
||||
const displayText = options.html || options.text || '';
|
||||
dlg.querySelector('.text').innerHTML = DOMPurify.sanitize(displayText);
|
||||
|
||||
if (!displayText) {
|
||||
dlg.querySelector('.dialogContentInner').classList.add('hide');
|
||||
}
|
||||
|
||||
let i;
|
||||
let length;
|
||||
let html = '';
|
||||
let hasDescriptions = false;
|
||||
|
||||
for (i = 0, length = options.buttons.length; i < length; i++) {
|
||||
const item = options.buttons[i];
|
||||
const autoFocus = i === 0 ? ' autofocus' : '';
|
||||
|
||||
let buttonClass = 'btnOption raised formDialogFooterItem formDialogFooterItem-autosize';
|
||||
|
||||
if (item.type) {
|
||||
buttonClass += ` button-${item.type}`;
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
dlg.classList.add('align-items-center');
|
||||
dlg.classList.add('justify-content-center');
|
||||
const formDialogContent = dlg.querySelector('.formDialogContent');
|
||||
formDialogContent.classList.add('no-grow');
|
||||
|
||||
if (enableTvLayout) {
|
||||
formDialogContent.style['max-width'] = '50%';
|
||||
formDialogContent.style['max-height'] = '60%';
|
||||
scrollHelper.centerFocus.on(formDialogContent, false);
|
||||
} else {
|
||||
formDialogContent.style.maxWidth = `${Math.min((options.buttons.length * 150) + 200, dom.getWindowSize().innerWidth - 50)}px`;
|
||||
dlg.classList.add('dialog-fullscreen-lowres');
|
||||
if (item.description) {
|
||||
hasDescriptions = true;
|
||||
}
|
||||
|
||||
if (options.title) {
|
||||
dlg.querySelector('.formDialogHeaderTitle').innerText = options.title || '';
|
||||
} else {
|
||||
dlg.querySelector('.formDialogHeaderTitle').classList.add('hide');
|
||||
}
|
||||
|
||||
const displayText = options.html || options.text || '';
|
||||
dlg.querySelector('.text').innerHTML = DOMPurify.sanitize(displayText);
|
||||
|
||||
if (!displayText) {
|
||||
dlg.querySelector('.dialogContentInner').classList.add('hide');
|
||||
}
|
||||
|
||||
let i;
|
||||
let length;
|
||||
let html = '';
|
||||
let hasDescriptions = false;
|
||||
|
||||
for (i = 0, length = options.buttons.length; i < length; i++) {
|
||||
const item = options.buttons[i];
|
||||
const autoFocus = i === 0 ? ' autofocus' : '';
|
||||
|
||||
let buttonClass = 'btnOption raised formDialogFooterItem formDialogFooterItem-autosize';
|
||||
|
||||
if (item.type) {
|
||||
buttonClass += ` button-${item.type}`;
|
||||
}
|
||||
|
||||
if (item.description) {
|
||||
hasDescriptions = true;
|
||||
}
|
||||
|
||||
if (hasDescriptions) {
|
||||
buttonClass += ' formDialogFooterItem-vertical formDialogFooterItem-nomarginbottom';
|
||||
}
|
||||
|
||||
html += `<button is="emby-button" type="button" class="${buttonClass}" data-id="${item.id}"${autoFocus}>${escapeHtml(item.name)}</button>`;
|
||||
|
||||
if (item.description) {
|
||||
html += `<div class="formDialogFooterItem formDialogFooterItem-autosize fieldDescription" style="margin-top:.25em!important;margin-bottom:1.25em!important;">${item.description}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
dlg.querySelector('.formDialogFooter').innerHTML = html;
|
||||
|
||||
if (hasDescriptions) {
|
||||
dlg.querySelector('.formDialogFooter').classList.add('formDialogFooter-vertical');
|
||||
buttonClass += ' formDialogFooterItem-vertical formDialogFooterItem-nomarginbottom';
|
||||
}
|
||||
|
||||
let dialogResult;
|
||||
function onButtonClick() {
|
||||
dialogResult = this.getAttribute('data-id');
|
||||
dialogHelper.close(dlg);
|
||||
html += `<button is="emby-button" type="button" class="${buttonClass}" data-id="${item.id}"${autoFocus}>${escapeHtml(item.name)}</button>`;
|
||||
|
||||
if (item.description) {
|
||||
html += `<div class="formDialogFooterItem formDialogFooterItem-autosize fieldDescription" style="margin-top:.25em!important;margin-bottom:1.25em!important;">${item.description}</div>`;
|
||||
}
|
||||
|
||||
const buttons = dlg.querySelectorAll('.btnOption');
|
||||
for (i = 0, length = buttons.length; i < length; i++) {
|
||||
buttons[i].addEventListener('click', onButtonClick);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (enableTvLayout) {
|
||||
scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false);
|
||||
}
|
||||
|
||||
if (dialogResult) {
|
||||
return dialogResult;
|
||||
} else {
|
||||
return Promise.reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function show(text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
dlg.querySelector('.formDialogFooter').innerHTML = html;
|
||||
|
||||
if (hasDescriptions) {
|
||||
dlg.querySelector('.formDialogFooter').classList.add('formDialogFooter-vertical');
|
||||
}
|
||||
|
||||
let dialogResult;
|
||||
function onButtonClick() {
|
||||
dialogResult = this.getAttribute('data-id');
|
||||
dialogHelper.close(dlg);
|
||||
}
|
||||
|
||||
const buttons = dlg.querySelectorAll('.btnOption');
|
||||
for (i = 0, length = buttons.length; i < length; i++) {
|
||||
buttons[i].addEventListener('click', onButtonClick);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (enableTvLayout) {
|
||||
scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false);
|
||||
}
|
||||
|
||||
if (dialogResult) {
|
||||
return dialogResult;
|
||||
} else {
|
||||
options = text;
|
||||
return Promise.reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return showDialog(options);
|
||||
export function show(text, title) {
|
||||
let options;
|
||||
if (typeof text === 'string') {
|
||||
options = {
|
||||
title: title,
|
||||
text: text
|
||||
};
|
||||
} else {
|
||||
options = text;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
return showDialog(options);
|
||||
}
|
||||
|
||||
export default {
|
||||
show: show
|
||||
};
|
||||
|
||||
@@ -9,506 +9,501 @@ import dom from '../../scripts/dom';
|
||||
import './dialoghelper.scss';
|
||||
import '../../styles/scrollstyles.scss';
|
||||
|
||||
/* eslint-disable indent */
|
||||
let globalOnOpenCallback;
|
||||
|
||||
let globalOnOpenCallback;
|
||||
|
||||
function enableAnimation() {
|
||||
// too slow
|
||||
if (browser.tv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return browser.supportsCssAnimation();
|
||||
function enableAnimation() {
|
||||
// too slow
|
||||
if (browser.tv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function removeCenterFocus(dlg) {
|
||||
if (layoutManager.tv) {
|
||||
if (dlg.classList.contains('scrollX')) {
|
||||
centerFocus(dlg, true, false);
|
||||
} else if (dlg.classList.contains('smoothScrollY')) {
|
||||
centerFocus(dlg, false, false);
|
||||
}
|
||||
return browser.supportsCssAnimation();
|
||||
}
|
||||
|
||||
function removeCenterFocus(dlg) {
|
||||
if (layoutManager.tv) {
|
||||
if (dlg.classList.contains('scrollX')) {
|
||||
centerFocus(dlg, true, false);
|
||||
} else if (dlg.classList.contains('smoothScrollY')) {
|
||||
centerFocus(dlg, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tryRemoveElement(elem) {
|
||||
const parentNode = elem.parentNode;
|
||||
if (parentNode) {
|
||||
// Seeing crashes in edge webview
|
||||
try {
|
||||
parentNode.removeChild(elem);
|
||||
} catch (err) {
|
||||
console.error('[dialogHelper] error removing dialog element: ' + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function DialogHashHandler(dlg, hash, resolve) {
|
||||
const self = this;
|
||||
self.originalUrl = window.location.href;
|
||||
const activeElement = document.activeElement;
|
||||
let removeScrollLockOnClose = false;
|
||||
let unlisten;
|
||||
|
||||
function onHashChange({ location }) {
|
||||
const dialogs = location.state?.dialogs || [];
|
||||
const shouldClose = !dialogs.includes(hash);
|
||||
|
||||
if ((shouldClose || !isOpened(dlg)) && unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
if (shouldClose) {
|
||||
close(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
function tryRemoveElement(elem) {
|
||||
const parentNode = elem.parentNode;
|
||||
if (parentNode) {
|
||||
// Seeing crashes in edge webview
|
||||
try {
|
||||
parentNode.removeChild(elem);
|
||||
} catch (err) {
|
||||
console.error('[dialogHelper] error removing dialog element: ' + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function DialogHashHandler(dlg, hash, resolve) {
|
||||
const self = this;
|
||||
self.originalUrl = window.location.href;
|
||||
const activeElement = document.activeElement;
|
||||
let removeScrollLockOnClose = false;
|
||||
let unlisten;
|
||||
|
||||
function onHashChange({ location }) {
|
||||
const dialogs = location.state?.dialogs || [];
|
||||
const shouldClose = !dialogs.includes(hash);
|
||||
|
||||
if ((shouldClose || !isOpened(dlg)) && unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
if (shouldClose) {
|
||||
close(dlg);
|
||||
}
|
||||
function finishClose() {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
function finishClose() {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
dlg.dispatchEvent(new CustomEvent('close', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
resolve({
|
||||
element: dlg
|
||||
});
|
||||
}
|
||||
|
||||
function onBackCommand(e) {
|
||||
if (e.detail.command === 'back') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
close(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
if (!isHistoryEnabled(dlg)) {
|
||||
inputManager.off(dlg, onBackCommand);
|
||||
}
|
||||
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
removeBackdrop(dlg);
|
||||
dlg.classList.remove('opened');
|
||||
|
||||
if (removeScrollLockOnClose) {
|
||||
document.body.classList.remove('noScroll');
|
||||
}
|
||||
|
||||
if (isHistoryEnabled(dlg)) {
|
||||
const state = history.location.state || {};
|
||||
if (state.dialogs?.length > 0) {
|
||||
if (state.dialogs[state.dialogs.length - 1] === hash) {
|
||||
unlisten = history.listen(finishClose);
|
||||
history.back();
|
||||
} else if (state.dialogs.includes(hash)) {
|
||||
console.warn('[dialogHelper] dialog "%s" was closed, but is not the last dialog opened', hash);
|
||||
|
||||
unlisten = history.listen(finishClose);
|
||||
|
||||
// Remove the closed dialog hash from the history state
|
||||
history.replace(
|
||||
`${history.location.pathname}${history.location.search}`,
|
||||
{
|
||||
...state,
|
||||
dialogs: state.dialogs.filter(dialog => dialog !== hash)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
focusManager.focus(activeElement);
|
||||
}
|
||||
|
||||
if (toBoolean(dlg.getAttribute('data-removeonclose'), true)) {
|
||||
removeCenterFocus(dlg);
|
||||
|
||||
const dialogContainer = dlg.dialogContainer;
|
||||
if (dialogContainer) {
|
||||
tryRemoveElement(dialogContainer);
|
||||
dlg.dialogContainer = null;
|
||||
} else {
|
||||
tryRemoveElement(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
if (!unlisten) {
|
||||
finishClose();
|
||||
}
|
||||
}
|
||||
|
||||
dlg.addEventListener('_close', onDialogClosed);
|
||||
|
||||
const center = !dlg.classList.contains('dialog-fixedSize');
|
||||
if (center) {
|
||||
dlg.classList.add('centeredDialog');
|
||||
}
|
||||
|
||||
dlg.classList.remove('hide');
|
||||
|
||||
addBackdropOverlay(dlg);
|
||||
|
||||
dlg.classList.add('opened');
|
||||
dlg.dispatchEvent(new CustomEvent('open', {
|
||||
dlg.dispatchEvent(new CustomEvent('close', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
if (dlg.getAttribute('data-lockscroll') === 'true' && !document.body.classList.contains('noScroll')) {
|
||||
document.body.classList.add('noScroll');
|
||||
removeScrollLockOnClose = true;
|
||||
resolve({
|
||||
element: dlg
|
||||
});
|
||||
}
|
||||
|
||||
function onBackCommand(e) {
|
||||
if (e.detail.command === 'back') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
close(dlg);
|
||||
}
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
if (!isHistoryEnabled(dlg)) {
|
||||
inputManager.off(dlg, onBackCommand);
|
||||
}
|
||||
|
||||
animateDialogOpen(dlg);
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
removeBackdrop(dlg);
|
||||
dlg.classList.remove('opened');
|
||||
|
||||
if (removeScrollLockOnClose) {
|
||||
document.body.classList.remove('noScroll');
|
||||
}
|
||||
|
||||
if (isHistoryEnabled(dlg)) {
|
||||
const state = history.location.state || {};
|
||||
const dialogs = state.dialogs || [];
|
||||
// Add new dialog to the list of open dialogs
|
||||
dialogs.push(hash);
|
||||
if (state.dialogs?.length > 0) {
|
||||
if (state.dialogs[state.dialogs.length - 1] === hash) {
|
||||
unlisten = history.listen(finishClose);
|
||||
history.back();
|
||||
} else if (state.dialogs.includes(hash)) {
|
||||
console.warn('[dialogHelper] dialog "%s" was closed, but is not the last dialog opened', hash);
|
||||
|
||||
history.push(
|
||||
`${history.location.pathname}${history.location.search}`,
|
||||
{
|
||||
...state,
|
||||
dialogs
|
||||
unlisten = history.listen(finishClose);
|
||||
|
||||
// Remove the closed dialog hash from the history state
|
||||
history.replace(
|
||||
`${history.location.pathname}${history.location.search}`,
|
||||
{
|
||||
...state,
|
||||
dialogs: state.dialogs.filter(dialog => dialog !== hash)
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
unlisten = history.listen(onHashChange);
|
||||
} else {
|
||||
inputManager.on(dlg, onBackCommand);
|
||||
}
|
||||
}
|
||||
|
||||
function addBackdropOverlay(dlg) {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.classList.add('dialogBackdrop');
|
||||
|
||||
const backdropParent = dlg.dialogContainer || dlg;
|
||||
backdropParent.parentNode.insertBefore(backdrop, backdropParent);
|
||||
dlg.backdrop = backdrop;
|
||||
|
||||
// trigger reflow or the backdrop will not animate
|
||||
void backdrop.offsetWidth;
|
||||
backdrop.classList.add('dialogBackdropOpened');
|
||||
|
||||
dom.addEventListener((dlg.dialogContainer || backdrop), 'click', e => {
|
||||
if (e.target === dlg.dialogContainer) {
|
||||
close(dlg);
|
||||
}
|
||||
}, {
|
||||
passive: true
|
||||
});
|
||||
}
|
||||
|
||||
dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', e => {
|
||||
if (e.target === dlg.dialogContainer) {
|
||||
// Close the application dialog menu
|
||||
close(dlg);
|
||||
// Prevent the default browser context menu from appearing
|
||||
e.preventDefault();
|
||||
if (layoutManager.tv) {
|
||||
focusManager.focus(activeElement);
|
||||
}
|
||||
|
||||
if (toBoolean(dlg.getAttribute('data-removeonclose'), true)) {
|
||||
removeCenterFocus(dlg);
|
||||
|
||||
const dialogContainer = dlg.dialogContainer;
|
||||
if (dialogContainer) {
|
||||
tryRemoveElement(dialogContainer);
|
||||
dlg.dialogContainer = null;
|
||||
} else {
|
||||
tryRemoveElement(dlg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isHistoryEnabled(dlg) {
|
||||
return dlg.getAttribute('data-history') === 'true';
|
||||
}
|
||||
|
||||
export function open(dlg) {
|
||||
if (globalOnOpenCallback) {
|
||||
globalOnOpenCallback(dlg);
|
||||
}
|
||||
|
||||
const parent = dlg.parentNode;
|
||||
if (parent) {
|
||||
parent.removeChild(dlg);
|
||||
if (!unlisten) {
|
||||
finishClose();
|
||||
}
|
||||
|
||||
const dialogContainer = document.createElement('div');
|
||||
dialogContainer.classList.add('dialogContainer');
|
||||
dialogContainer.appendChild(dlg);
|
||||
dlg.dialogContainer = dialogContainer;
|
||||
document.body.appendChild(dialogContainer);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
new DialogHashHandler(dlg, `dlg${new Date().getTime()}`, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function isOpened(dlg) {
|
||||
//return dlg.opened;
|
||||
return !dlg.classList.contains('hide');
|
||||
dlg.addEventListener('_close', onDialogClosed);
|
||||
|
||||
const center = !dlg.classList.contains('dialog-fixedSize');
|
||||
if (center) {
|
||||
dlg.classList.add('centeredDialog');
|
||||
}
|
||||
|
||||
export function close(dlg) {
|
||||
if (!dlg.classList.contains('hide')) {
|
||||
dlg.dispatchEvent(new CustomEvent('closing', {
|
||||
dlg.classList.remove('hide');
|
||||
|
||||
addBackdropOverlay(dlg);
|
||||
|
||||
dlg.classList.add('opened');
|
||||
dlg.dispatchEvent(new CustomEvent('open', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
if (dlg.getAttribute('data-lockscroll') === 'true' && !document.body.classList.contains('noScroll')) {
|
||||
document.body.classList.add('noScroll');
|
||||
removeScrollLockOnClose = true;
|
||||
}
|
||||
|
||||
animateDialogOpen(dlg);
|
||||
|
||||
if (isHistoryEnabled(dlg)) {
|
||||
const state = history.location.state || {};
|
||||
const dialogs = state.dialogs || [];
|
||||
// Add new dialog to the list of open dialogs
|
||||
dialogs.push(hash);
|
||||
|
||||
history.push(
|
||||
`${history.location.pathname}${history.location.search}`,
|
||||
{
|
||||
...state,
|
||||
dialogs
|
||||
}
|
||||
);
|
||||
|
||||
unlisten = history.listen(onHashChange);
|
||||
} else {
|
||||
inputManager.on(dlg, onBackCommand);
|
||||
}
|
||||
}
|
||||
|
||||
function addBackdropOverlay(dlg) {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.classList.add('dialogBackdrop');
|
||||
|
||||
const backdropParent = dlg.dialogContainer || dlg;
|
||||
backdropParent.parentNode.insertBefore(backdrop, backdropParent);
|
||||
dlg.backdrop = backdrop;
|
||||
|
||||
// trigger reflow or the backdrop will not animate
|
||||
void backdrop.offsetWidth;
|
||||
backdrop.classList.add('dialogBackdropOpened');
|
||||
|
||||
dom.addEventListener((dlg.dialogContainer || backdrop), 'click', e => {
|
||||
if (e.target === dlg.dialogContainer) {
|
||||
close(dlg);
|
||||
}
|
||||
}, {
|
||||
passive: true
|
||||
});
|
||||
|
||||
dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', e => {
|
||||
if (e.target === dlg.dialogContainer) {
|
||||
// Close the application dialog menu
|
||||
close(dlg);
|
||||
// Prevent the default browser context menu from appearing
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isHistoryEnabled(dlg) {
|
||||
return dlg.getAttribute('data-history') === 'true';
|
||||
}
|
||||
|
||||
export function open(dlg) {
|
||||
if (globalOnOpenCallback) {
|
||||
globalOnOpenCallback(dlg);
|
||||
}
|
||||
|
||||
const parent = dlg.parentNode;
|
||||
if (parent) {
|
||||
parent.removeChild(dlg);
|
||||
}
|
||||
|
||||
const dialogContainer = document.createElement('div');
|
||||
dialogContainer.classList.add('dialogContainer');
|
||||
dialogContainer.appendChild(dlg);
|
||||
dlg.dialogContainer = dialogContainer;
|
||||
document.body.appendChild(dialogContainer);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
new DialogHashHandler(dlg, `dlg${new Date().getTime()}`, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function isOpened(dlg) {
|
||||
return !dlg.classList.contains('hide');
|
||||
}
|
||||
|
||||
export function close(dlg) {
|
||||
if (!dlg.classList.contains('hide')) {
|
||||
dlg.dispatchEvent(new CustomEvent('closing', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
|
||||
const onAnimationFinish = () => {
|
||||
focusManager.popScope(dlg);
|
||||
|
||||
dlg.classList.add('hide');
|
||||
dlg.dispatchEvent(new CustomEvent('_close', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
};
|
||||
|
||||
const onAnimationFinish = () => {
|
||||
focusManager.popScope(dlg);
|
||||
|
||||
dlg.classList.add('hide');
|
||||
dlg.dispatchEvent(new CustomEvent('_close', {
|
||||
bubbles: false,
|
||||
cancelable: false
|
||||
}));
|
||||
};
|
||||
|
||||
animateDialogClose(dlg, onAnimationFinish);
|
||||
}
|
||||
animateDialogClose(dlg, onAnimationFinish);
|
||||
}
|
||||
}
|
||||
|
||||
const getAnimationEndHandler = (dlg, callback) => function handler() {
|
||||
dom.removeEventListener(dlg, dom.whichAnimationEvent(), handler, { once: true });
|
||||
callback();
|
||||
const getAnimationEndHandler = (dlg, callback) => function handler() {
|
||||
dom.removeEventListener(dlg, dom.whichAnimationEvent(), handler, { once: true });
|
||||
callback();
|
||||
};
|
||||
|
||||
function animateDialogOpen(dlg) {
|
||||
const onAnimationFinish = () => {
|
||||
focusManager.pushScope(dlg);
|
||||
|
||||
if (dlg.getAttribute('data-autofocus') === 'true') {
|
||||
focusManager.autoFocus(dlg);
|
||||
}
|
||||
|
||||
if (document.activeElement && !dlg.contains(document.activeElement)) {
|
||||
// Blur foreign element to prevent triggering of an action from the previous scope
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
function animateDialogOpen(dlg) {
|
||||
const onAnimationFinish = () => {
|
||||
focusManager.pushScope(dlg);
|
||||
if (enableAnimation()) {
|
||||
dom.addEventListener(
|
||||
dlg,
|
||||
dom.whichAnimationEvent(),
|
||||
getAnimationEndHandler(dlg, onAnimationFinish),
|
||||
{ once: true });
|
||||
|
||||
if (dlg.getAttribute('data-autofocus') === 'true') {
|
||||
focusManager.autoFocus(dlg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.activeElement && !dlg.contains(document.activeElement)) {
|
||||
// Blur foreign element to prevent triggering of an action from the previous scope
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
onAnimationFinish();
|
||||
}
|
||||
|
||||
if (enableAnimation()) {
|
||||
dom.addEventListener(
|
||||
dlg,
|
||||
dom.whichAnimationEvent(),
|
||||
getAnimationEndHandler(dlg, onAnimationFinish),
|
||||
{ once: true });
|
||||
function animateDialogClose(dlg, onAnimationFinish) {
|
||||
if (enableAnimation()) {
|
||||
let animated = true;
|
||||
|
||||
switch (dlg.animationConfig.exit.name) {
|
||||
case 'fadeout':
|
||||
dlg.style.animation = `fadeout ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'scaledown':
|
||||
dlg.style.animation = `scaledown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'slidedown':
|
||||
dlg.style.animation = `slidedown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
default:
|
||||
animated = false;
|
||||
break;
|
||||
}
|
||||
|
||||
dom.addEventListener(
|
||||
dlg,
|
||||
dom.whichAnimationEvent(),
|
||||
getAnimationEndHandler(dlg, onAnimationFinish),
|
||||
{ once: true });
|
||||
|
||||
if (animated) {
|
||||
return;
|
||||
}
|
||||
|
||||
onAnimationFinish();
|
||||
}
|
||||
|
||||
function animateDialogClose(dlg, onAnimationFinish) {
|
||||
if (enableAnimation()) {
|
||||
let animated = true;
|
||||
onAnimationFinish();
|
||||
}
|
||||
|
||||
switch (dlg.animationConfig.exit.name) {
|
||||
case 'fadeout':
|
||||
dlg.style.animation = `fadeout ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'scaledown':
|
||||
dlg.style.animation = `scaledown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'slidedown':
|
||||
dlg.style.animation = `slidedown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
|
||||
break;
|
||||
default:
|
||||
animated = false;
|
||||
break;
|
||||
const supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style;
|
||||
|
||||
function shouldLockDocumentScroll(options) {
|
||||
if (options.lockScroll != null) {
|
||||
return options.lockScroll;
|
||||
}
|
||||
|
||||
if (options.size === 'fullscreen') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (supportsOverscrollBehavior && (options.size || !browser.touch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return browser.touch;
|
||||
}
|
||||
|
||||
function removeBackdrop(dlg) {
|
||||
const backdrop = dlg.backdrop;
|
||||
|
||||
if (!backdrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
dlg.backdrop = null;
|
||||
|
||||
const onAnimationFinish = () => {
|
||||
tryRemoveElement(backdrop);
|
||||
};
|
||||
|
||||
if (enableAnimation()) {
|
||||
backdrop.classList.remove('dialogBackdropOpened');
|
||||
|
||||
// this is not firing animationend
|
||||
setTimeout(onAnimationFinish, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
onAnimationFinish();
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
}
|
||||
|
||||
export function createDialog(options = {}) {
|
||||
// If there's no native dialog support, use a plain div
|
||||
// Also not working well in samsung tizen browser, content inside not clickable
|
||||
// Just go ahead and always use a plain div because we're seeing issues overlaying absoltutely positioned content over a modal dialog
|
||||
const dlg = document.createElement('div');
|
||||
|
||||
// Add an id so we can access the dialog element
|
||||
if (options.id) {
|
||||
dlg.id = options.id;
|
||||
}
|
||||
|
||||
dlg.classList.add('focuscontainer');
|
||||
dlg.classList.add('hide');
|
||||
|
||||
if (shouldLockDocumentScroll(options)) {
|
||||
dlg.setAttribute('data-lockscroll', 'true');
|
||||
}
|
||||
|
||||
if (options.enableHistory !== false) {
|
||||
dlg.setAttribute('data-history', 'true');
|
||||
}
|
||||
|
||||
// without this safari will scroll the background instead of the dialog contents
|
||||
// but not needed here since this is already on top of an existing dialog
|
||||
// but skip it in IE because it's causing the entire browser to hang
|
||||
// Also have to disable for firefox because it's causing select elements to not be clickable
|
||||
if (options.modal !== false) {
|
||||
dlg.setAttribute('modal', 'modal');
|
||||
}
|
||||
|
||||
if (options.autoFocus !== false) {
|
||||
dlg.setAttribute('data-autofocus', 'true');
|
||||
}
|
||||
|
||||
const defaultEntryAnimation = 'scaleup';
|
||||
const defaultExitAnimation = 'scaledown';
|
||||
const entryAnimation = options.entryAnimation || defaultEntryAnimation;
|
||||
const exitAnimation = options.exitAnimation || defaultExitAnimation;
|
||||
|
||||
// If it's not fullscreen then lower the default animation speed to make it open really fast
|
||||
const entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280);
|
||||
const exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220);
|
||||
|
||||
dlg.animationConfig = {
|
||||
// scale up
|
||||
'entry': {
|
||||
name: entryAnimation,
|
||||
timing: {
|
||||
duration: entryAnimationDuration,
|
||||
easing: 'ease-out'
|
||||
}
|
||||
|
||||
dom.addEventListener(
|
||||
dlg,
|
||||
dom.whichAnimationEvent(),
|
||||
getAnimationEndHandler(dlg, onAnimationFinish),
|
||||
{ once: true });
|
||||
|
||||
if (animated) {
|
||||
return;
|
||||
},
|
||||
// fade out
|
||||
'exit': {
|
||||
name: exitAnimation,
|
||||
timing: {
|
||||
duration: exitAnimationDuration,
|
||||
easing: 'ease-out',
|
||||
fill: 'both'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onAnimationFinish();
|
||||
dlg.classList.add('dialog');
|
||||
|
||||
if (options.scrollX) {
|
||||
dlg.classList.add('scrollX');
|
||||
dlg.classList.add('smoothScrollX');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg, true, true);
|
||||
}
|
||||
} else if (options.scrollY !== false) {
|
||||
dlg.classList.add('smoothScrollY');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
const supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style;
|
||||
|
||||
function shouldLockDocumentScroll(options) {
|
||||
if (options.lockScroll != null) {
|
||||
return options.lockScroll;
|
||||
}
|
||||
|
||||
if (options.size === 'fullscreen') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (supportsOverscrollBehavior && (options.size || !browser.touch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return browser.touch;
|
||||
if (options.removeOnClose) {
|
||||
dlg.setAttribute('data-removeonclose', 'true');
|
||||
}
|
||||
|
||||
function removeBackdrop(dlg) {
|
||||
const backdrop = dlg.backdrop;
|
||||
|
||||
if (!backdrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
dlg.backdrop = null;
|
||||
|
||||
const onAnimationFinish = () => {
|
||||
tryRemoveElement(backdrop);
|
||||
};
|
||||
|
||||
if (enableAnimation()) {
|
||||
backdrop.classList.remove('dialogBackdropOpened');
|
||||
|
||||
// this is not firing animationend
|
||||
setTimeout(onAnimationFinish, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
onAnimationFinish();
|
||||
if (options.size) {
|
||||
dlg.classList.add('dialog-fixedSize');
|
||||
dlg.classList.add(`dialog-${options.size}`);
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
if (enableAnimation()) {
|
||||
switch (dlg.animationConfig.entry.name) {
|
||||
case 'fadein':
|
||||
dlg.style.animation = `fadein ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
case 'scaleup':
|
||||
dlg.style.animation = `scaleup ${entryAnimationDuration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'slideup':
|
||||
dlg.style.animation = `slideup ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
case 'slidedown':
|
||||
dlg.style.animation = `slidedown ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function createDialog(options = {}) {
|
||||
// If there's no native dialog support, use a plain div
|
||||
// Also not working well in samsung tizen browser, content inside not clickable
|
||||
// Just go ahead and always use a plain div because we're seeing issues overlaying absoltutely positioned content over a modal dialog
|
||||
const dlg = document.createElement('div');
|
||||
return dlg;
|
||||
}
|
||||
|
||||
// Add an id so we can access the dialog element
|
||||
if (options.id) {
|
||||
dlg.id = options.id;
|
||||
}
|
||||
|
||||
dlg.classList.add('focuscontainer');
|
||||
dlg.classList.add('hide');
|
||||
|
||||
if (shouldLockDocumentScroll(options)) {
|
||||
dlg.setAttribute('data-lockscroll', 'true');
|
||||
}
|
||||
|
||||
if (options.enableHistory !== false) {
|
||||
dlg.setAttribute('data-history', 'true');
|
||||
}
|
||||
|
||||
// without this safari will scroll the background instead of the dialog contents
|
||||
// but not needed here since this is already on top of an existing dialog
|
||||
// but skip it in IE because it's causing the entire browser to hang
|
||||
// Also have to disable for firefox because it's causing select elements to not be clickable
|
||||
if (options.modal !== false) {
|
||||
dlg.setAttribute('modal', 'modal');
|
||||
}
|
||||
|
||||
if (options.autoFocus !== false) {
|
||||
dlg.setAttribute('data-autofocus', 'true');
|
||||
}
|
||||
|
||||
const defaultEntryAnimation = 'scaleup';
|
||||
const defaultExitAnimation = 'scaledown';
|
||||
const entryAnimation = options.entryAnimation || defaultEntryAnimation;
|
||||
const exitAnimation = options.exitAnimation || defaultExitAnimation;
|
||||
|
||||
// If it's not fullscreen then lower the default animation speed to make it open really fast
|
||||
const entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280);
|
||||
const exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220);
|
||||
|
||||
dlg.animationConfig = {
|
||||
// scale up
|
||||
'entry': {
|
||||
name: entryAnimation,
|
||||
timing: {
|
||||
duration: entryAnimationDuration,
|
||||
easing: 'ease-out'
|
||||
}
|
||||
},
|
||||
// fade out
|
||||
'exit': {
|
||||
name: exitAnimation,
|
||||
timing: {
|
||||
duration: exitAnimationDuration,
|
||||
easing: 'ease-out',
|
||||
fill: 'both'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dlg.classList.add('dialog');
|
||||
|
||||
if (options.scrollX) {
|
||||
dlg.classList.add('scrollX');
|
||||
dlg.classList.add('smoothScrollX');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg, true, true);
|
||||
}
|
||||
} else if (options.scrollY !== false) {
|
||||
dlg.classList.add('smoothScrollY');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.removeOnClose) {
|
||||
dlg.setAttribute('data-removeonclose', 'true');
|
||||
}
|
||||
|
||||
if (options.size) {
|
||||
dlg.classList.add('dialog-fixedSize');
|
||||
dlg.classList.add(`dialog-${options.size}`);
|
||||
}
|
||||
|
||||
if (enableAnimation()) {
|
||||
switch (dlg.animationConfig.entry.name) {
|
||||
case 'fadein':
|
||||
dlg.style.animation = `fadein ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
case 'scaleup':
|
||||
dlg.style.animation = `scaleup ${entryAnimationDuration}ms ease-out normal both`;
|
||||
break;
|
||||
case 'slideup':
|
||||
dlg.style.animation = `slideup ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
case 'slidedown':
|
||||
dlg.style.animation = `slidedown ${entryAnimationDuration}ms ease-out normal`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dlg;
|
||||
}
|
||||
|
||||
export function setOnOpen(val) {
|
||||
globalOnOpenCallback = val;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export function setOnOpen(val) {
|
||||
globalOnOpenCallback = val;
|
||||
}
|
||||
|
||||
export default {
|
||||
open: open,
|
||||
|
||||
@@ -18,238 +18,235 @@ import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './displaySettings.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function fillThemes(select, selectedTheme) {
|
||||
skinManager.getThemes().then(themes => {
|
||||
select.innerHTML = themes.map(t => {
|
||||
return `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
|
||||
}).join('');
|
||||
|
||||
// get default theme
|
||||
const defaultTheme = themes.find(theme => theme.default);
|
||||
|
||||
// set the current theme
|
||||
select.value = selectedTheme || defaultTheme.id;
|
||||
});
|
||||
}
|
||||
|
||||
function loadScreensavers(context, userSettings) {
|
||||
const selectScreensaver = context.querySelector('.selectScreensaver');
|
||||
const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => {
|
||||
return {
|
||||
name: plugin.name,
|
||||
value: plugin.id
|
||||
};
|
||||
});
|
||||
|
||||
options.unshift({
|
||||
name: globalize.translate('None'),
|
||||
value: 'none'
|
||||
});
|
||||
|
||||
selectScreensaver.innerHTML = options.map(o => {
|
||||
return `<option value="${o.value}">${escapeHtml(o.name)}</option>`;
|
||||
function fillThemes(select, selectedTheme) {
|
||||
skinManager.getThemes().then(themes => {
|
||||
select.innerHTML = themes.map(t => {
|
||||
return `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
|
||||
}).join('');
|
||||
|
||||
selectScreensaver.value = userSettings.screensaver();
|
||||
// get default theme
|
||||
const defaultTheme = themes.find(theme => theme.default);
|
||||
|
||||
if (!selectScreensaver.value) {
|
||||
// TODO: set the default instead of none
|
||||
selectScreensaver.value = 'none';
|
||||
}
|
||||
// set the current theme
|
||||
select.value = selectedTheme || defaultTheme.id;
|
||||
});
|
||||
}
|
||||
|
||||
function loadScreensavers(context, userSettings) {
|
||||
const selectScreensaver = context.querySelector('.selectScreensaver');
|
||||
const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => {
|
||||
return {
|
||||
name: plugin.name,
|
||||
value: plugin.id
|
||||
};
|
||||
});
|
||||
|
||||
options.unshift({
|
||||
name: globalize.translate('None'),
|
||||
value: 'none'
|
||||
});
|
||||
|
||||
selectScreensaver.innerHTML = options.map(o => {
|
||||
return `<option value="${o.value}">${escapeHtml(o.name)}</option>`;
|
||||
}).join('');
|
||||
|
||||
selectScreensaver.value = userSettings.screensaver();
|
||||
|
||||
if (!selectScreensaver.value) {
|
||||
// TODO: set the default instead of none
|
||||
selectScreensaver.value = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function showOrHideMissingEpisodesField(context) {
|
||||
if (browser.tizen || browser.web0s) {
|
||||
context.querySelector('.fldDisplayMissingEpisodes').classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
function showOrHideMissingEpisodesField(context) {
|
||||
if (browser.tizen || browser.web0s) {
|
||||
context.querySelector('.fldDisplayMissingEpisodes').classList.add('hide');
|
||||
return;
|
||||
}
|
||||
context.querySelector('.fldDisplayMissingEpisodes').classList.remove('hide');
|
||||
}
|
||||
|
||||
context.querySelector('.fldDisplayMissingEpisodes').classList.remove('hide');
|
||||
function loadForm(context, user, userSettings) {
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
context.querySelector('.languageSection').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.languageSection').classList.add('hide');
|
||||
}
|
||||
|
||||
function loadForm(context, user, userSettings) {
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
context.querySelector('.languageSection').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.languageSection').classList.add('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('displaymode')) {
|
||||
context.querySelector('.fldDisplayMode').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDisplayMode').classList.add('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('externallinks')) {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator);
|
||||
|
||||
if (appHost.supports('screensaver')) {
|
||||
context.querySelector('.selectScreensaverContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.selectScreensaverContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
if (datetime.supportsLocalization()) {
|
||||
context.querySelector('.fldDateTimeLocale').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDateTimeLocale').classList.add('hide');
|
||||
}
|
||||
|
||||
fillThemes(context.querySelector('#selectTheme'), userSettings.theme());
|
||||
fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme());
|
||||
|
||||
loadScreensavers(context, userSettings);
|
||||
|
||||
context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false;
|
||||
|
||||
context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs();
|
||||
context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos();
|
||||
context.querySelector('#chkFadein').checked = userSettings.enableFastFadein();
|
||||
context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash();
|
||||
context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops();
|
||||
context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner();
|
||||
|
||||
context.querySelector('#chkDisableCustomCss').checked = userSettings.disableCustomCss();
|
||||
context.querySelector('#txtLocalCustomCss').value = userSettings.customCss();
|
||||
|
||||
context.querySelector('#selectLanguage').value = userSettings.language() || '';
|
||||
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
|
||||
|
||||
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
|
||||
|
||||
context.querySelector('#txtMaxDaysForNextUp').value = userSettings.maxDaysForNextUp();
|
||||
context.querySelector('#chkRewatchingNextUp').checked = userSettings.enableRewatchingInNextUp();
|
||||
context.querySelector('#chkUseEpisodeImagesInNextUp').checked = userSettings.useEpisodeImagesInNextUpAndResume();
|
||||
|
||||
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
|
||||
|
||||
showOrHideMissingEpisodesField(context);
|
||||
|
||||
loading.hide();
|
||||
if (appHost.supports('displaymode')) {
|
||||
context.querySelector('.fldDisplayMode').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDisplayMode').classList.add('hide');
|
||||
}
|
||||
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked;
|
||||
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
userSettingsInstance.language(context.querySelector('#selectLanguage').value);
|
||||
}
|
||||
|
||||
userSettingsInstance.dateTimeLocale(context.querySelector('.selectDateTimeLocale').value);
|
||||
|
||||
userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked);
|
||||
userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked);
|
||||
userSettingsInstance.theme(context.querySelector('#selectTheme').value);
|
||||
userSettingsInstance.dashboardTheme(context.querySelector('#selectDashboardTheme').value);
|
||||
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
|
||||
|
||||
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
|
||||
|
||||
userSettingsInstance.maxDaysForNextUp(context.querySelector('#txtMaxDaysForNextUp').value);
|
||||
userSettingsInstance.enableRewatchingInNextUp(context.querySelector('#chkRewatchingNextUp').checked);
|
||||
userSettingsInstance.useEpisodeImagesInNextUpAndResume(context.querySelector('#chkUseEpisodeImagesInNextUp').checked);
|
||||
|
||||
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
|
||||
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
|
||||
userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked);
|
||||
userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked);
|
||||
|
||||
userSettingsInstance.disableCustomCss(context.querySelector('#chkDisableCustomCss').checked);
|
||||
userSettingsInstance.customCss(context.querySelector('#txtLocalCustomCss').value);
|
||||
|
||||
if (user.Id === apiClient.getCurrentUserId()) {
|
||||
skinManager.setTheme(userSettingsInstance.theme());
|
||||
}
|
||||
|
||||
layoutManager.setLayout(context.querySelector('.selectLayout').value);
|
||||
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
|
||||
if (appHost.supports('externallinks')) {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.learnHowToContributeContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
|
||||
context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator);
|
||||
|
||||
if (appHost.supports('screensaver')) {
|
||||
context.querySelector('.selectScreensaverContainer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.selectScreensaverContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
if (datetime.supportsLocalization()) {
|
||||
context.querySelector('.fldDateTimeLocale').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldDateTimeLocale').classList.add('hide');
|
||||
}
|
||||
|
||||
fillThemes(context.querySelector('#selectTheme'), userSettings.theme());
|
||||
fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme());
|
||||
|
||||
loadScreensavers(context, userSettings);
|
||||
|
||||
context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false;
|
||||
|
||||
context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs();
|
||||
context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos();
|
||||
context.querySelector('#chkFadein').checked = userSettings.enableFastFadein();
|
||||
context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash();
|
||||
context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops();
|
||||
context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner();
|
||||
|
||||
context.querySelector('#chkDisableCustomCss').checked = userSettings.disableCustomCss();
|
||||
context.querySelector('#txtLocalCustomCss').value = userSettings.customCss();
|
||||
|
||||
context.querySelector('#selectLanguage').value = userSettings.language() || '';
|
||||
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
|
||||
|
||||
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
|
||||
|
||||
context.querySelector('#txtMaxDaysForNextUp').value = userSettings.maxDaysForNextUp();
|
||||
context.querySelector('#chkRewatchingNextUp').checked = userSettings.enableRewatchingInNextUp();
|
||||
context.querySelector('#chkUseEpisodeImagesInNextUp').checked = userSettings.useEpisodeImagesInNextUpAndResume();
|
||||
|
||||
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
|
||||
|
||||
showOrHideMissingEpisodesField(context);
|
||||
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked;
|
||||
|
||||
if (appHost.supports('displaylanguage')) {
|
||||
userSettingsInstance.language(context.querySelector('#selectLanguage').value);
|
||||
}
|
||||
|
||||
userSettingsInstance.dateTimeLocale(context.querySelector('.selectDateTimeLocale').value);
|
||||
|
||||
userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked);
|
||||
userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked);
|
||||
userSettingsInstance.theme(context.querySelector('#selectTheme').value);
|
||||
userSettingsInstance.dashboardTheme(context.querySelector('#selectDashboardTheme').value);
|
||||
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
|
||||
|
||||
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
|
||||
|
||||
userSettingsInstance.maxDaysForNextUp(context.querySelector('#txtMaxDaysForNextUp').value);
|
||||
userSettingsInstance.enableRewatchingInNextUp(context.querySelector('#chkRewatchingNextUp').checked);
|
||||
userSettingsInstance.useEpisodeImagesInNextUpAndResume(context.querySelector('#chkUseEpisodeImagesInNextUp').checked);
|
||||
|
||||
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
|
||||
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
|
||||
userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked);
|
||||
userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked);
|
||||
|
||||
userSettingsInstance.disableCustomCss(context.querySelector('#chkDisableCustomCss').checked);
|
||||
userSettingsInstance.customCss(context.querySelector('#txtLocalCustomCss').value);
|
||||
|
||||
if (user.Id === apiClient.getCurrentUserId()) {
|
||||
skinManager.setTheme(userSettingsInstance.theme());
|
||||
}
|
||||
|
||||
layoutManager.setLayout(context.querySelector('.selectLayout').value);
|
||||
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
|
||||
}
|
||||
|
||||
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
|
||||
loading.show();
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
saveUser(context, user, userSettings, apiClient).then(() => {
|
||||
loading.hide();
|
||||
if (enableSaveConfirmation) {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
Events.trigger(instance, 'saved');
|
||||
}, () => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userId = self.options.userId;
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
const enableSaveConfirmation = self.options.enableSaveConfirmation;
|
||||
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
|
||||
});
|
||||
|
||||
// Disable default form submission
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function embed(options, self) {
|
||||
options.element.innerHTML = globalize.translateHtml(template, 'core');
|
||||
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
|
||||
if (options.enableSaveButton) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
self.loadData(options.autoFocus);
|
||||
}
|
||||
|
||||
class DisplaySettings {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options, this);
|
||||
}
|
||||
|
||||
loadData(autoFocus) {
|
||||
const self = this;
|
||||
const context = self.options.element;
|
||||
|
||||
loading.show();
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
saveUser(context, user, userSettings, apiClient).then(() => {
|
||||
loading.hide();
|
||||
if (enableSaveConfirmation) {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
Events.trigger(instance, 'saved');
|
||||
}, () => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userId = self.options.userId;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
const enableSaveConfirmation = self.options.enableSaveConfirmation;
|
||||
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
|
||||
});
|
||||
|
||||
// Disable default form submission
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function embed(options, self) {
|
||||
options.element.innerHTML = globalize.translateHtml(template, 'core');
|
||||
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
|
||||
if (options.enableSaveButton) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
self.loadData(options.autoFocus);
|
||||
}
|
||||
|
||||
class DisplaySettings {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options, this);
|
||||
}
|
||||
|
||||
loadData(autoFocus) {
|
||||
const self = this;
|
||||
const context = self.options.element;
|
||||
|
||||
loading.show();
|
||||
|
||||
const userId = self.options.userId;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
return apiClient.getUser(userId).then(user => {
|
||||
return userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
self.dataLoaded = true;
|
||||
loadForm(context, user, userSettings);
|
||||
if (autoFocus) {
|
||||
focusManager.autoFocus(context);
|
||||
}
|
||||
});
|
||||
return apiClient.getUser(userId).then(user => {
|
||||
return userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
self.dataLoaded = true;
|
||||
loadForm(context, user, userSettings);
|
||||
if (autoFocus) {
|
||||
focusManager.autoFocus(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
onSubmit.call(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.options = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
submit() {
|
||||
onSubmit.call(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default DisplaySettings;
|
||||
|
||||
@@ -9,236 +9,232 @@ import { getParameterByName } from '../utils/url.ts';
|
||||
import '../styles/scrollstyles.scss';
|
||||
import '../elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function enableScrollX() {
|
||||
return !layoutManager.desktop;
|
||||
}
|
||||
|
||||
function enableScrollX() {
|
||||
return !layoutManager.desktop;
|
||||
function getThumbShape() {
|
||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
||||
}
|
||||
|
||||
function getPosterShape() {
|
||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||
}
|
||||
|
||||
function getSquareShape() {
|
||||
return enableScrollX() ? 'overflowSquare' : 'square';
|
||||
}
|
||||
|
||||
function getSections() {
|
||||
return [{
|
||||
name: 'Movies',
|
||||
types: 'Movie',
|
||||
id: 'favoriteMovies',
|
||||
shape: getPosterShape(),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Shows',
|
||||
types: 'Series',
|
||||
id: 'favoriteShows',
|
||||
shape: getPosterShape(),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Episodes',
|
||||
types: 'Episode',
|
||||
id: 'favoriteEpisode',
|
||||
shape: getThumbShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
overlayPlayButton: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
}, {
|
||||
name: 'Videos',
|
||||
types: 'Video,MusicVideo',
|
||||
id: 'favoriteVideos',
|
||||
shape: getThumbShape(),
|
||||
preferThumb: true,
|
||||
showTitle: true,
|
||||
overlayPlayButton: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
}, {
|
||||
name: 'Artists',
|
||||
types: 'MusicArtist',
|
||||
id: 'favoriteArtists',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: false,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Albums',
|
||||
types: 'MusicAlbum',
|
||||
id: 'favoriteAlbums',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: true,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Songs',
|
||||
types: 'Audio',
|
||||
id: 'favoriteSongs',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: true,
|
||||
centerText: true,
|
||||
overlayMoreButton: true,
|
||||
action: 'instantmix',
|
||||
coverImage: true
|
||||
}];
|
||||
}
|
||||
|
||||
function loadSection(elem, userId, topParentId, section, isSingleSection) {
|
||||
const screenWidth = dom.getWindowSize().innerWidth;
|
||||
const options = {
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
Filters: 'IsFavorite',
|
||||
Recursive: true,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
CollapseBoxSetItems: false,
|
||||
ExcludeLocationTypes: 'Virtual',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
|
||||
if (topParentId) {
|
||||
options.ParentId = topParentId;
|
||||
}
|
||||
|
||||
function getThumbShape() {
|
||||
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
|
||||
}
|
||||
if (!isSingleSection) {
|
||||
options.Limit = 6;
|
||||
|
||||
function getPosterShape() {
|
||||
return enableScrollX() ? 'overflowPortrait' : 'portrait';
|
||||
}
|
||||
|
||||
function getSquareShape() {
|
||||
return enableScrollX() ? 'overflowSquare' : 'square';
|
||||
}
|
||||
|
||||
function getSections() {
|
||||
return [{
|
||||
name: 'Movies',
|
||||
types: 'Movie',
|
||||
id: 'favoriteMovies',
|
||||
shape: getPosterShape(),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Shows',
|
||||
types: 'Series',
|
||||
id: 'favoriteShows',
|
||||
shape: getPosterShape(),
|
||||
showTitle: false,
|
||||
overlayPlayButton: true
|
||||
}, {
|
||||
name: 'Episodes',
|
||||
types: 'Episode',
|
||||
id: 'favoriteEpisode',
|
||||
shape: getThumbShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
overlayPlayButton: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
}, {
|
||||
name: 'Videos',
|
||||
types: 'Video,MusicVideo',
|
||||
id: 'favoriteVideos',
|
||||
shape: getThumbShape(),
|
||||
preferThumb: true,
|
||||
showTitle: true,
|
||||
overlayPlayButton: true,
|
||||
overlayText: false,
|
||||
centerText: true
|
||||
}, {
|
||||
name: 'Artists',
|
||||
types: 'MusicArtist',
|
||||
id: 'favoriteArtists',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: false,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Albums',
|
||||
types: 'MusicAlbum',
|
||||
id: 'favoriteAlbums',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: true,
|
||||
centerText: true,
|
||||
overlayPlayButton: true,
|
||||
coverImage: true
|
||||
}, {
|
||||
name: 'Songs',
|
||||
types: 'Audio',
|
||||
id: 'favoriteSongs',
|
||||
shape: getSquareShape(),
|
||||
preferThumb: false,
|
||||
showTitle: true,
|
||||
overlayText: false,
|
||||
showParentTitle: true,
|
||||
centerText: true,
|
||||
overlayMoreButton: true,
|
||||
action: 'instantmix',
|
||||
coverImage: true
|
||||
}];
|
||||
}
|
||||
|
||||
function loadSection(elem, userId, topParentId, section, isSingleSection) {
|
||||
const screenWidth = dom.getWindowSize().innerWidth;
|
||||
const options = {
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
Filters: 'IsFavorite',
|
||||
Recursive: true,
|
||||
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
|
||||
CollapseBoxSetItems: false,
|
||||
ExcludeLocationTypes: 'Virtual',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
|
||||
if (topParentId) {
|
||||
options.ParentId = topParentId;
|
||||
if (enableScrollX()) {
|
||||
options.Limit = 20;
|
||||
} else if (screenWidth >= 1920) {
|
||||
options.Limit = 10;
|
||||
} else if (screenWidth >= 1440) {
|
||||
options.Limit = 8;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSingleSection) {
|
||||
options.Limit = 6;
|
||||
let promise;
|
||||
|
||||
if (section.types === 'MusicArtist') {
|
||||
promise = ApiClient.getArtists(userId, options);
|
||||
} else {
|
||||
options.IncludeItemTypes = section.types;
|
||||
promise = ApiClient.getItems(userId, options);
|
||||
}
|
||||
|
||||
return promise.then(function (result) {
|
||||
let html = '';
|
||||
|
||||
if (result.Items.length) {
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
|
||||
if (!layoutManager.tv && options.Limit && result.Items.length >= options.Limit) {
|
||||
html += '<a is="emby-linkbutton" href="' + ('#/list.html?serverId=' + ApiClient.serverId() + '&type=' + section.types + '&IsFavorite=true') + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate(section.name);
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
if (enableScrollX()) {
|
||||
options.Limit = 20;
|
||||
} else if (screenWidth >= 1920) {
|
||||
options.Limit = 10;
|
||||
} else if (screenWidth >= 1440) {
|
||||
options.Limit = 8;
|
||||
}
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
if (section.types === 'MusicArtist') {
|
||||
promise = ApiClient.getArtists(userId, options);
|
||||
} else {
|
||||
options.IncludeItemTypes = section.types;
|
||||
promise = ApiClient.getItems(userId, options);
|
||||
}
|
||||
|
||||
return promise.then(function (result) {
|
||||
let html = '';
|
||||
|
||||
if (result.Items.length) {
|
||||
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
|
||||
|
||||
if (!layoutManager.tv && options.Limit && result.Items.length >= options.Limit) {
|
||||
html += '<a is="emby-linkbutton" href="' + ('#/list.html?serverId=' + ApiClient.serverId() + '&type=' + section.types + '&IsFavorite=true') + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">';
|
||||
html += globalize.translate(section.name);
|
||||
html += '</h2>';
|
||||
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
} else {
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>';
|
||||
let scrollXClass = 'scrollX hiddenScrollX';
|
||||
if (layoutManager.tv) {
|
||||
scrollXClass += ' smoothScrollX';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
if (enableScrollX()) {
|
||||
let scrollXClass = 'scrollX hiddenScrollX';
|
||||
if (layoutManager.tv) {
|
||||
scrollXClass += ' smoothScrollX';
|
||||
}
|
||||
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' padded-left padded-right">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">';
|
||||
}
|
||||
|
||||
let cardLayout = appHost.preferVisualCards && section.autoCardLayout && section.showTitle;
|
||||
cardLayout = false;
|
||||
html += cardBuilder.getCardsHtml(result.Items, {
|
||||
preferThumb: section.preferThumb,
|
||||
shape: section.shape,
|
||||
centerText: section.centerText && !cardLayout,
|
||||
overlayText: section.overlayText !== false,
|
||||
showTitle: section.showTitle,
|
||||
showParentTitle: section.showParentTitle,
|
||||
scalable: true,
|
||||
coverImage: section.coverImage,
|
||||
overlayPlayButton: section.overlayPlayButton,
|
||||
overlayMoreButton: section.overlayMoreButton && !cardLayout,
|
||||
action: section.action,
|
||||
allowBottomPadding: !enableScrollX(),
|
||||
cardLayout: cardLayout
|
||||
});
|
||||
html += '</div>';
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' padded-left padded-right">';
|
||||
} else {
|
||||
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
let cardLayout = appHost.preferVisualCards && section.autoCardLayout && section.showTitle;
|
||||
cardLayout = false;
|
||||
html += cardBuilder.getCardsHtml(result.Items, {
|
||||
preferThumb: section.preferThumb,
|
||||
shape: section.shape,
|
||||
centerText: section.centerText && !cardLayout,
|
||||
overlayText: section.overlayText !== false,
|
||||
showTitle: section.showTitle,
|
||||
showParentTitle: section.showParentTitle,
|
||||
scalable: true,
|
||||
coverImage: section.coverImage,
|
||||
overlayPlayButton: section.overlayPlayButton,
|
||||
overlayMoreButton: section.overlayMoreButton && !cardLayout,
|
||||
action: section.action,
|
||||
allowBottomPadding: !enableScrollX(),
|
||||
cardLayout: cardLayout
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
});
|
||||
}
|
||||
|
||||
export function loadSections(page, userId, topParentId, types) {
|
||||
loading.show();
|
||||
let sections = getSections();
|
||||
const sectionid = getParameterByName('sectionid');
|
||||
|
||||
if (sectionid) {
|
||||
sections = sections.filter(function (s) {
|
||||
return s.id === sectionid;
|
||||
});
|
||||
}
|
||||
|
||||
export function loadSections(page, userId, topParentId, types) {
|
||||
loading.show();
|
||||
let sections = getSections();
|
||||
const sectionid = getParameterByName('sectionid');
|
||||
if (types) {
|
||||
sections = sections.filter(function (s) {
|
||||
return types.indexOf(s.id) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
if (sectionid) {
|
||||
sections = sections.filter(function (s) {
|
||||
return s.id === sectionid;
|
||||
});
|
||||
}
|
||||
let elem = page.querySelector('.favoriteSections');
|
||||
|
||||
if (types) {
|
||||
sections = sections.filter(function (s) {
|
||||
return types.indexOf(s.id) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
let elem = page.querySelector('.favoriteSections');
|
||||
|
||||
if (!elem.innerHTML) {
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = sections.length; i < length; i++) {
|
||||
html += '<div class="verticalSection section' + sections[i].id + '"></div>';
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
if (!elem.innerHTML) {
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = sections.length; i < length; i++) {
|
||||
const section = sections[i];
|
||||
elem = page.querySelector('.section' + section.id);
|
||||
promises.push(loadSection(elem, userId, topParentId, section, sections.length === 1));
|
||||
html += '<div class="verticalSection section' + sections[i].id + '"></div>';
|
||||
}
|
||||
|
||||
Promise.all(promises).then(function () {
|
||||
loading.hide();
|
||||
});
|
||||
elem.innerHTML = html;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0, length = sections.length; i < length; i++) {
|
||||
const section = sections[i];
|
||||
elem = page.querySelector('.section' + section.id);
|
||||
promises.push(loadSection(elem, userId, topParentId, section, sections.length === 1));
|
||||
}
|
||||
|
||||
Promise.all(promises).then(function () {
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
render: loadSections
|
||||
};
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -1,110 +1,108 @@
|
||||
/* eslint-disable indent */
|
||||
export function getFetchPromise(request) {
|
||||
const headers = request.headers || {};
|
||||
export function getFetchPromise(request) {
|
||||
const headers = request.headers || {};
|
||||
|
||||
if (request.dataType === 'json') {
|
||||
headers.accept = 'application/json';
|
||||
}
|
||||
|
||||
const fetchRequest = {
|
||||
headers: headers,
|
||||
method: request.type,
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
let contentType = request.contentType;
|
||||
|
||||
if (request.data) {
|
||||
if (typeof request.data === 'string') {
|
||||
fetchRequest.body = request.data;
|
||||
} else {
|
||||
fetchRequest.body = paramsToString(request.data);
|
||||
|
||||
contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType) {
|
||||
headers['Content-Type'] = contentType;
|
||||
}
|
||||
|
||||
let url = request.url;
|
||||
|
||||
if (request.query) {
|
||||
const paramString = paramsToString(request.query);
|
||||
if (paramString) {
|
||||
url += `?${paramString}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!request.timeout) {
|
||||
return fetch(url, fetchRequest);
|
||||
}
|
||||
|
||||
return fetchWithTimeout(url, fetchRequest, request.timeout);
|
||||
if (request.dataType === 'json') {
|
||||
headers.accept = 'application/json';
|
||||
}
|
||||
|
||||
function fetchWithTimeout(url, options, timeoutMs) {
|
||||
console.debug(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`);
|
||||
const fetchRequest = {
|
||||
headers: headers,
|
||||
method: request.type,
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const timeout = setTimeout(reject, timeoutMs);
|
||||
let contentType = request.contentType;
|
||||
|
||||
options = options || {};
|
||||
options.credentials = 'same-origin';
|
||||
if (request.data) {
|
||||
if (typeof request.data === 'string') {
|
||||
fetchRequest.body = request.data;
|
||||
} else {
|
||||
fetchRequest.body = paramsToString(request.data);
|
||||
|
||||
fetch(url, options).then(function (response) {
|
||||
clearTimeout(timeout);
|
||||
contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8';
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(`fetchWithTimeout: succeeded connecting to url: ${url}`);
|
||||
if (contentType) {
|
||||
headers['Content-Type'] = contentType;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
}, function (error) {
|
||||
clearTimeout(timeout);
|
||||
let url = request.url;
|
||||
|
||||
console.debug(`fetchWithTimeout: timed out connecting to url: ${url}`);
|
||||
if (request.query) {
|
||||
const paramString = paramsToString(request.query);
|
||||
if (paramString) {
|
||||
url += `?${paramString}`;
|
||||
}
|
||||
}
|
||||
|
||||
reject(error);
|
||||
});
|
||||
if (!request.timeout) {
|
||||
return fetch(url, fetchRequest);
|
||||
}
|
||||
|
||||
return fetchWithTimeout(url, fetchRequest, request.timeout);
|
||||
}
|
||||
|
||||
function fetchWithTimeout(url, options, timeoutMs) {
|
||||
console.debug(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const timeout = setTimeout(reject, timeoutMs);
|
||||
|
||||
options = options || {};
|
||||
options.credentials = 'same-origin';
|
||||
|
||||
fetch(url, options).then(function (response) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
console.debug(`fetchWithTimeout: succeeded connecting to url: ${url}`);
|
||||
|
||||
resolve(response);
|
||||
}, function (error) {
|
||||
clearTimeout(timeout);
|
||||
|
||||
console.debug(`fetchWithTimeout: timed out connecting to url: ${url}`);
|
||||
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @param params {Record<string, string | number | boolean>}
|
||||
* @returns {string} Query string
|
||||
*/
|
||||
function paramsToString(params) {
|
||||
return Object.entries(params)
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
function paramsToString(params) {
|
||||
return Object.entries(params)
|
||||
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||
.join('&');
|
||||
}
|
||||
|
||||
export function ajax(request) {
|
||||
if (!request) {
|
||||
throw new Error('Request cannot be null');
|
||||
}
|
||||
|
||||
export function ajax(request) {
|
||||
if (!request) {
|
||||
throw new Error('Request cannot be null');
|
||||
}
|
||||
request.headers = request.headers || {};
|
||||
|
||||
request.headers = request.headers || {};
|
||||
console.debug(`requesting url: ${request.url}`);
|
||||
|
||||
console.debug(`requesting url: ${request.url}`);
|
||||
|
||||
return getFetchPromise(request).then(function (response) {
|
||||
console.debug(`response status: ${response.status}, url: ${request.url}`);
|
||||
if (response.status < 400) {
|
||||
if (request.dataType === 'json' || request.headers.accept === 'application/json') {
|
||||
return response.json();
|
||||
} else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().startsWith('text/')) {
|
||||
return response.text();
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
return getFetchPromise(request).then(function (response) {
|
||||
console.debug(`response status: ${response.status}, url: ${request.url}`);
|
||||
if (response.status < 400) {
|
||||
if (request.dataType === 'json' || request.headers.accept === 'application/json') {
|
||||
return response.json();
|
||||
} else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().startsWith('text/')) {
|
||||
return response.text();
|
||||
} else {
|
||||
return Promise.reject(response);
|
||||
return response;
|
||||
}
|
||||
}, function (err) {
|
||||
console.error(`request failed to url: ${request.url}`);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
} else {
|
||||
return Promise.reject(response);
|
||||
}
|
||||
}, function (err) {
|
||||
console.error(`request failed to url: ${request.url}`);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,417 +8,414 @@ import './style.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './filterdialog.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function renderOptions(context, selector, cssClass, items, isCheckedFn) {
|
||||
const elem = context.querySelector(selector);
|
||||
if (items.length) {
|
||||
elem.classList.remove('hide');
|
||||
} else {
|
||||
elem.classList.add('hide');
|
||||
}
|
||||
let html = '';
|
||||
html += '<div class="checkboxList">';
|
||||
html += items.map(function (filter) {
|
||||
let itemHtml = '';
|
||||
const checkedHtml = isCheckedFn(filter) ? 'checked' : '';
|
||||
itemHtml += '<label>';
|
||||
itemHtml += `<input is="emby-checkbox" type="checkbox" ${checkedHtml} data-filter="${filter}" class="${cssClass}"/>`;
|
||||
itemHtml += `<span>${filter}</span>`;
|
||||
itemHtml += '</label>';
|
||||
return itemHtml;
|
||||
}).join('');
|
||||
html += '</div>';
|
||||
elem.querySelector('.filterOptions').innerHTML = html;
|
||||
function renderOptions(context, selector, cssClass, items, isCheckedFn) {
|
||||
const elem = context.querySelector(selector);
|
||||
if (items.length) {
|
||||
elem.classList.remove('hide');
|
||||
} else {
|
||||
elem.classList.add('hide');
|
||||
}
|
||||
let html = '';
|
||||
html += '<div class="checkboxList">';
|
||||
html += items.map(function (filter) {
|
||||
let itemHtml = '';
|
||||
const checkedHtml = isCheckedFn(filter) ? 'checked' : '';
|
||||
itemHtml += '<label>';
|
||||
itemHtml += `<input is="emby-checkbox" type="checkbox" ${checkedHtml} data-filter="${filter}" class="${cssClass}"/>`;
|
||||
itemHtml += `<span>${filter}</span>`;
|
||||
itemHtml += '</label>';
|
||||
return itemHtml;
|
||||
}).join('');
|
||||
html += '</div>';
|
||||
elem.querySelector('.filterOptions').innerHTML = html;
|
||||
}
|
||||
|
||||
function renderFilters(context, result, query) {
|
||||
renderOptions(context, '.genreFilters', 'chkGenreFilter', result.Genres, function (i) {
|
||||
const delimeter = '|';
|
||||
return (delimeter + (query.Genres || '') + delimeter).includes(delimeter + i + delimeter);
|
||||
});
|
||||
renderOptions(context, '.officialRatingFilters', 'chkOfficialRatingFilter', result.OfficialRatings, function (i) {
|
||||
const delimeter = '|';
|
||||
return (delimeter + (query.OfficialRatings || '') + delimeter).includes(delimeter + i + delimeter);
|
||||
});
|
||||
renderOptions(context, '.tagFilters', 'chkTagFilter', result.Tags, function (i) {
|
||||
const delimeter = '|';
|
||||
return (delimeter + (query.Tags || '') + delimeter).includes(delimeter + i + delimeter);
|
||||
});
|
||||
renderOptions(context, '.yearFilters', 'chkYearFilter', result.Years, function (i) {
|
||||
const delimeter = ',';
|
||||
return (delimeter + (query.Years || '') + delimeter).includes(delimeter + i + delimeter);
|
||||
});
|
||||
}
|
||||
function renderFilters(context, result, query) {
|
||||
renderOptions(context, '.genreFilters', 'chkGenreFilter', result.Genres, function (i) {
|
||||
const delimeter = '|';
|
||||
return (delimeter + (query.Genres || '') + delimeter).includes(delimeter + i + delimeter);
|
||||
});
|
||||
renderOptions(context, '.officialRatingFilters', 'chkOfficialRatingFilter', result.OfficialRatings, function (i) {
|
||||
const delimeter = '|';
|
||||
return (delimeter + (query.OfficialRatings || '') + delimeter).includes(delimeter + i + delimeter);
|
||||
});
|
||||
renderOptions(context, '.tagFilters', 'chkTagFilter', result.Tags, function (i) {
|
||||
const delimeter = '|';
|
||||
return (delimeter + (query.Tags || '') + delimeter).includes(delimeter + i + delimeter);
|
||||
});
|
||||
renderOptions(context, '.yearFilters', 'chkYearFilter', result.Years, function (i) {
|
||||
const delimeter = ',';
|
||||
return (delimeter + (query.Years || '') + delimeter).includes(delimeter + i + delimeter);
|
||||
});
|
||||
}
|
||||
|
||||
function loadDynamicFilters(context, apiClient, userId, itemQuery) {
|
||||
return apiClient.getJSON(apiClient.getUrl('Items/Filters', {
|
||||
UserId: userId,
|
||||
ParentId: itemQuery.ParentId,
|
||||
IncludeItemTypes: itemQuery.IncludeItemTypes
|
||||
})).then(function (result) {
|
||||
renderFilters(context, result, itemQuery);
|
||||
});
|
||||
}
|
||||
function loadDynamicFilters(context, apiClient, userId, itemQuery) {
|
||||
return apiClient.getJSON(apiClient.getUrl('Items/Filters', {
|
||||
UserId: userId,
|
||||
ParentId: itemQuery.ParentId,
|
||||
IncludeItemTypes: itemQuery.IncludeItemTypes
|
||||
})).then(function (result) {
|
||||
renderFilters(context, result, itemQuery);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @param context {HTMLDivElement} Dialog
|
||||
* @param options {any} Options
|
||||
*/
|
||||
function updateFilterControls(context, options) {
|
||||
const query = options.query;
|
||||
function updateFilterControls(context, options) {
|
||||
const query = options.query;
|
||||
|
||||
if (options.mode === 'livetvchannels') {
|
||||
context.querySelector('.chkFavorite').checked = query.IsFavorite === true;
|
||||
if (options.mode === 'livetvchannels') {
|
||||
context.querySelector('.chkFavorite').checked = query.IsFavorite === true;
|
||||
} else {
|
||||
for (const elem of context.querySelectorAll('.chkStandardFilter')) {
|
||||
const filters = `,${query.Filters || ''}`;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
elem.checked = filters.includes(`,${filterName}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const elem of context.querySelectorAll('.chkVideoTypeFilter')) {
|
||||
const filters = `,${query.VideoTypes || ''}`;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
elem.checked = filters.includes(`,${filterName}`);
|
||||
}
|
||||
context.querySelector('.chk3DFilter').checked = query.Is3D === true;
|
||||
context.querySelector('.chkHDFilter').checked = query.IsHD === true;
|
||||
context.querySelector('.chk4KFilter').checked = query.Is4K === true;
|
||||
context.querySelector('.chkSDFilter').checked = query.IsHD === false;
|
||||
context.querySelector('#chkSubtitle').checked = query.HasSubtitles === true;
|
||||
context.querySelector('#chkTrailer').checked = query.HasTrailer === true;
|
||||
context.querySelector('#chkThemeSong').checked = query.HasThemeSong === true;
|
||||
context.querySelector('#chkThemeVideo').checked = query.HasThemeVideo === true;
|
||||
context.querySelector('#chkSpecialFeature').checked = query.HasSpecialFeature === true;
|
||||
context.querySelector('#chkSpecialEpisode').checked = query.ParentIndexNumber === 0;
|
||||
context.querySelector('#chkMissingEpisode').checked = query.IsMissing === true;
|
||||
context.querySelector('#chkFutureEpisode').checked = query.IsUnaired === true;
|
||||
for (const elem of context.querySelectorAll('.chkStatus')) {
|
||||
const filters = `,${query.SeriesStatus || ''}`;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
elem.checked = filters.includes(`,${filterName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param instance {FilterDialog} An instance of FilterDialog
|
||||
*/
|
||||
function triggerChange(instance) {
|
||||
Events.trigger(instance, 'filterchange');
|
||||
}
|
||||
|
||||
function setVisibility(context, options) {
|
||||
if (options.mode === 'livetvchannels' || options.mode === 'albums' || options.mode === 'artists' || options.mode === 'albumartists' || options.mode === 'songs') {
|
||||
hideByClass(context, 'videoStandard');
|
||||
}
|
||||
|
||||
if (enableDynamicFilters(options.mode)) {
|
||||
context.querySelector('.genreFilters').classList.remove('hide');
|
||||
context.querySelector('.officialRatingFilters').classList.remove('hide');
|
||||
context.querySelector('.tagFilters').classList.remove('hide');
|
||||
context.querySelector('.yearFilters').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (options.mode === 'movies' || options.mode === 'episodes') {
|
||||
context.querySelector('.videoTypeFilters').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (options.mode === 'movies' || options.mode === 'series' || options.mode === 'episodes') {
|
||||
context.querySelector('.features').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (options.mode === 'series') {
|
||||
context.querySelector('.seriesStatus').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (options.mode === 'episodes') {
|
||||
showByClass(context, 'episodeFilter');
|
||||
}
|
||||
}
|
||||
|
||||
function showByClass(context, className) {
|
||||
for (const elem of context.querySelectorAll(`.${className}`)) {
|
||||
elem.classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function hideByClass(context, className) {
|
||||
for (const elem of context.querySelectorAll(`.${className}`)) {
|
||||
elem.classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function enableDynamicFilters(mode) {
|
||||
return mode === 'movies' || mode === 'series' || mode === 'albums' || mode === 'albumartists' || mode === 'artists' || mode === 'songs' || mode === 'episodes';
|
||||
}
|
||||
|
||||
class FilterDialog {
|
||||
constructor(options) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onFavoriteChange(elem) {
|
||||
const query = this.options.query;
|
||||
query.StartIndex = 0;
|
||||
query.IsFavorite = !!elem.checked || null;
|
||||
triggerChange(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onStandardFilterChange(elem) {
|
||||
const query = this.options.query;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
let filters = query.Filters || '';
|
||||
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
|
||||
|
||||
if (elem.checked) {
|
||||
filters = filters ? `${filters},${filterName}` : filterName;
|
||||
}
|
||||
|
||||
query.StartIndex = 0;
|
||||
query.Filters = filters;
|
||||
triggerChange(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onVideoTypeFilterChange(elem) {
|
||||
const query = this.options.query;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
let filters = query.VideoTypes || '';
|
||||
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
|
||||
|
||||
if (elem.checked) {
|
||||
filters = filters ? `${filters},${filterName}` : filterName;
|
||||
}
|
||||
|
||||
query.StartIndex = 0;
|
||||
query.VideoTypes = filters;
|
||||
triggerChange(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onStatusChange(elem) {
|
||||
const query = this.options.query;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
let filters = query.SeriesStatus || '';
|
||||
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
|
||||
|
||||
if (elem.checked) {
|
||||
filters = filters ? `${filters},${filterName}` : filterName;
|
||||
}
|
||||
|
||||
query.SeriesStatus = filters;
|
||||
query.StartIndex = 0;
|
||||
triggerChange(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context {HTMLDivElement} The dialog
|
||||
*/
|
||||
bindEvents(context) {
|
||||
const query = this.options.query;
|
||||
|
||||
if (this.options.mode === 'livetvchannels') {
|
||||
for (const elem of context.querySelectorAll('.chkFavorite')) {
|
||||
elem.addEventListener('change', () => this.onFavoriteChange(elem));
|
||||
}
|
||||
} else {
|
||||
for (const elem of context.querySelectorAll('.chkStandardFilter')) {
|
||||
const filters = `,${query.Filters || ''}`;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
elem.checked = filters.includes(`,${filterName}`);
|
||||
elem.addEventListener('change', () => this.onStandardFilterChange(elem));
|
||||
}
|
||||
}
|
||||
|
||||
for (const elem of context.querySelectorAll('.chkVideoTypeFilter')) {
|
||||
const filters = `,${query.VideoTypes || ''}`;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
elem.checked = filters.includes(`,${filterName}`);
|
||||
elem.addEventListener('change', () => this.onVideoTypeFilterChange(elem));
|
||||
}
|
||||
context.querySelector('.chk3DFilter').checked = query.Is3D === true;
|
||||
context.querySelector('.chkHDFilter').checked = query.IsHD === true;
|
||||
context.querySelector('.chk4KFilter').checked = query.Is4K === true;
|
||||
context.querySelector('.chkSDFilter').checked = query.IsHD === false;
|
||||
context.querySelector('#chkSubtitle').checked = query.HasSubtitles === true;
|
||||
context.querySelector('#chkTrailer').checked = query.HasTrailer === true;
|
||||
context.querySelector('#chkThemeSong').checked = query.HasThemeSong === true;
|
||||
context.querySelector('#chkThemeVideo').checked = query.HasThemeVideo === true;
|
||||
context.querySelector('#chkSpecialFeature').checked = query.HasSpecialFeature === true;
|
||||
context.querySelector('#chkSpecialEpisode').checked = query.ParentIndexNumber === 0;
|
||||
context.querySelector('#chkMissingEpisode').checked = query.IsMissing === true;
|
||||
context.querySelector('#chkFutureEpisode').checked = query.IsUnaired === true;
|
||||
for (const elem of context.querySelectorAll('.chkStatus')) {
|
||||
const filters = `,${query.SeriesStatus || ''}`;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
elem.checked = filters.includes(`,${filterName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param instance {FilterDialog} An instance of FilterDialog
|
||||
*/
|
||||
function triggerChange(instance) {
|
||||
Events.trigger(instance, 'filterchange');
|
||||
}
|
||||
|
||||
function setVisibility(context, options) {
|
||||
if (options.mode === 'livetvchannels' || options.mode === 'albums' || options.mode === 'artists' || options.mode === 'albumartists' || options.mode === 'songs') {
|
||||
hideByClass(context, 'videoStandard');
|
||||
}
|
||||
|
||||
if (enableDynamicFilters(options.mode)) {
|
||||
context.querySelector('.genreFilters').classList.remove('hide');
|
||||
context.querySelector('.officialRatingFilters').classList.remove('hide');
|
||||
context.querySelector('.tagFilters').classList.remove('hide');
|
||||
context.querySelector('.yearFilters').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (options.mode === 'movies' || options.mode === 'episodes') {
|
||||
context.querySelector('.videoTypeFilters').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (options.mode === 'movies' || options.mode === 'series' || options.mode === 'episodes') {
|
||||
context.querySelector('.features').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (options.mode === 'series') {
|
||||
context.querySelector('.seriesStatus').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (options.mode === 'episodes') {
|
||||
showByClass(context, 'episodeFilter');
|
||||
}
|
||||
}
|
||||
|
||||
function showByClass(context, className) {
|
||||
for (const elem of context.querySelectorAll(`.${className}`)) {
|
||||
elem.classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function hideByClass(context, className) {
|
||||
for (const elem of context.querySelectorAll(`.${className}`)) {
|
||||
elem.classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function enableDynamicFilters(mode) {
|
||||
return mode === 'movies' || mode === 'series' || mode === 'albums' || mode === 'albumartists' || mode === 'artists' || mode === 'songs' || mode === 'episodes';
|
||||
}
|
||||
|
||||
class FilterDialog {
|
||||
constructor(options) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onFavoriteChange(elem) {
|
||||
const query = this.options.query;
|
||||
const chk3DFilter = context.querySelector('.chk3DFilter');
|
||||
chk3DFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.IsFavorite = !!elem.checked || null;
|
||||
query.Is3D = chk3DFilter.checked ? true : null;
|
||||
triggerChange(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onStandardFilterChange(elem) {
|
||||
const query = this.options.query;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
let filters = query.Filters || '';
|
||||
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
|
||||
|
||||
if (elem.checked) {
|
||||
filters = filters ? `${filters},${filterName}` : filterName;
|
||||
}
|
||||
|
||||
});
|
||||
const chk4KFilter = context.querySelector('.chk4KFilter');
|
||||
chk4KFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.Filters = filters;
|
||||
query.Is4K = chk4KFilter.checked ? true : null;
|
||||
triggerChange(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onVideoTypeFilterChange(elem) {
|
||||
const query = this.options.query;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
let filters = query.VideoTypes || '';
|
||||
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
|
||||
|
||||
if (elem.checked) {
|
||||
filters = filters ? `${filters},${filterName}` : filterName;
|
||||
}
|
||||
|
||||
});
|
||||
const chkHDFilter = context.querySelector('.chkHDFilter');
|
||||
const chkSDFilter = context.querySelector('.chkSDFilter');
|
||||
chkHDFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.VideoTypes = filters;
|
||||
triggerChange(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onStatusChange(elem) {
|
||||
const query = this.options.query;
|
||||
const filterName = elem.getAttribute('data-filter');
|
||||
let filters = query.SeriesStatus || '';
|
||||
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
|
||||
|
||||
if (elem.checked) {
|
||||
filters = filters ? `${filters},${filterName}` : filterName;
|
||||
}
|
||||
|
||||
query.SeriesStatus = filters;
|
||||
query.StartIndex = 0;
|
||||
triggerChange(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context {HTMLDivElement} The dialog
|
||||
*/
|
||||
bindEvents(context) {
|
||||
const query = this.options.query;
|
||||
|
||||
if (this.options.mode === 'livetvchannels') {
|
||||
for (const elem of context.querySelectorAll('.chkFavorite')) {
|
||||
elem.addEventListener('change', () => this.onFavoriteChange(elem));
|
||||
}
|
||||
if (chkHDFilter.checked) {
|
||||
chkSDFilter.checked = false;
|
||||
query.IsHD = true;
|
||||
} else {
|
||||
for (const elem of context.querySelectorAll('.chkStandardFilter')) {
|
||||
elem.addEventListener('change', () => this.onStandardFilterChange(elem));
|
||||
}
|
||||
query.IsHD = null;
|
||||
}
|
||||
|
||||
for (const elem of context.querySelectorAll('.chkVideoTypeFilter')) {
|
||||
elem.addEventListener('change', () => this.onVideoTypeFilterChange(elem));
|
||||
triggerChange(this);
|
||||
});
|
||||
chkSDFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
if (chkSDFilter.checked) {
|
||||
chkHDFilter.checked = false;
|
||||
query.IsHD = false;
|
||||
} else {
|
||||
query.IsHD = null;
|
||||
}
|
||||
const chk3DFilter = context.querySelector('.chk3DFilter');
|
||||
chk3DFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.Is3D = chk3DFilter.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chk4KFilter = context.querySelector('.chk4KFilter');
|
||||
chk4KFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.Is4K = chk4KFilter.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkHDFilter = context.querySelector('.chkHDFilter');
|
||||
const chkSDFilter = context.querySelector('.chkSDFilter');
|
||||
chkHDFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
if (chkHDFilter.checked) {
|
||||
chkSDFilter.checked = false;
|
||||
query.IsHD = true;
|
||||
} else {
|
||||
query.IsHD = null;
|
||||
}
|
||||
triggerChange(this);
|
||||
});
|
||||
chkSDFilter.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
if (chkSDFilter.checked) {
|
||||
chkHDFilter.checked = false;
|
||||
query.IsHD = false;
|
||||
} else {
|
||||
query.IsHD = null;
|
||||
}
|
||||
triggerChange(this);
|
||||
});
|
||||
for (const elem of context.querySelectorAll('.chkStatus')) {
|
||||
elem.addEventListener('change', () => this.onStatusChange(elem));
|
||||
}
|
||||
const chkTrailer = context.querySelector('#chkTrailer');
|
||||
chkTrailer.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasTrailer = chkTrailer.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkThemeSong = context.querySelector('#chkThemeSong');
|
||||
chkThemeSong.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasThemeSong = chkThemeSong.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkSpecialFeature = context.querySelector('#chkSpecialFeature');
|
||||
chkSpecialFeature.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasSpecialFeature = chkSpecialFeature.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkThemeVideo = context.querySelector('#chkThemeVideo');
|
||||
chkThemeVideo.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasThemeVideo = chkThemeVideo.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkMissingEpisode = context.querySelector('#chkMissingEpisode');
|
||||
chkMissingEpisode.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.IsMissing = !!chkMissingEpisode.checked;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkSpecialEpisode = context.querySelector('#chkSpecialEpisode');
|
||||
chkSpecialEpisode.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.ParentIndexNumber = chkSpecialEpisode.checked ? 0 : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkFutureEpisode = context.querySelector('#chkFutureEpisode');
|
||||
chkFutureEpisode.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
if (chkFutureEpisode.checked) {
|
||||
query.IsUnaired = true;
|
||||
query.IsVirtualUnaired = null;
|
||||
} else {
|
||||
query.IsUnaired = null;
|
||||
query.IsVirtualUnaired = false;
|
||||
}
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkSubtitle = context.querySelector('#chkSubtitle');
|
||||
chkSubtitle.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasSubtitles = chkSubtitle.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
context.addEventListener('change', (e) => {
|
||||
const chkGenreFilter = dom.parentWithClass(e.target, 'chkGenreFilter');
|
||||
if (chkGenreFilter) {
|
||||
const filterName = chkGenreFilter.getAttribute('data-filter');
|
||||
let filters = query.Genres || '';
|
||||
const delimiter = '|';
|
||||
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
|
||||
if (chkGenreFilter.checked) {
|
||||
filters = filters ? (filters + delimiter + filterName) : filterName;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
query.Genres = filters;
|
||||
triggerChange(this);
|
||||
return;
|
||||
}
|
||||
const chkTagFilter = dom.parentWithClass(e.target, 'chkTagFilter');
|
||||
if (chkTagFilter) {
|
||||
const filterName = chkTagFilter.getAttribute('data-filter');
|
||||
let filters = query.Tags || '';
|
||||
const delimiter = '|';
|
||||
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
|
||||
if (chkTagFilter.checked) {
|
||||
filters = filters ? (filters + delimiter + filterName) : filterName;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
query.Tags = filters;
|
||||
triggerChange(this);
|
||||
return;
|
||||
}
|
||||
const chkYearFilter = dom.parentWithClass(e.target, 'chkYearFilter');
|
||||
if (chkYearFilter) {
|
||||
const filterName = chkYearFilter.getAttribute('data-filter');
|
||||
let filters = query.Years || '';
|
||||
const delimiter = ',';
|
||||
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
|
||||
if (chkYearFilter.checked) {
|
||||
filters = filters ? (filters + delimiter + filterName) : filterName;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
query.Years = filters;
|
||||
triggerChange(this);
|
||||
return;
|
||||
}
|
||||
const chkOfficialRatingFilter = dom.parentWithClass(e.target, 'chkOfficialRatingFilter');
|
||||
if (chkOfficialRatingFilter) {
|
||||
const filterName = chkOfficialRatingFilter.getAttribute('data-filter');
|
||||
let filters = query.OfficialRatings || '';
|
||||
const delimiter = '|';
|
||||
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
|
||||
if (chkOfficialRatingFilter.checked) {
|
||||
filters = filters ? (filters + delimiter + filterName) : filterName;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
query.OfficialRatings = filters;
|
||||
triggerChange(this);
|
||||
}
|
||||
});
|
||||
triggerChange(this);
|
||||
});
|
||||
for (const elem of context.querySelectorAll('.chkStatus')) {
|
||||
elem.addEventListener('change', () => this.onStatusChange(elem));
|
||||
}
|
||||
|
||||
show() {
|
||||
return new Promise((resolve) => {
|
||||
const dlg = dialogHelper.createDialog({
|
||||
removeOnClose: true,
|
||||
modal: false
|
||||
});
|
||||
dlg.classList.add('ui-body-a');
|
||||
dlg.classList.add('background-theme-a');
|
||||
dlg.classList.add('formDialog');
|
||||
dlg.classList.add('filterDialog');
|
||||
dlg.innerHTML = globalize.translateHtml(template);
|
||||
setVisibility(dlg, this.options);
|
||||
dialogHelper.open(dlg);
|
||||
dlg.addEventListener('close', resolve);
|
||||
updateFilterControls(dlg, this.options);
|
||||
this.bindEvents(dlg);
|
||||
if (enableDynamicFilters(this.options.mode)) {
|
||||
dlg.classList.add('dynamicFilterDialog');
|
||||
const apiClient = ServerConnections.getApiClient(this.options.serverId);
|
||||
loadDynamicFilters(dlg, apiClient, apiClient.getCurrentUserId(), this.options.query);
|
||||
const chkTrailer = context.querySelector('#chkTrailer');
|
||||
chkTrailer.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasTrailer = chkTrailer.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkThemeSong = context.querySelector('#chkThemeSong');
|
||||
chkThemeSong.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasThemeSong = chkThemeSong.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkSpecialFeature = context.querySelector('#chkSpecialFeature');
|
||||
chkSpecialFeature.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasSpecialFeature = chkSpecialFeature.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkThemeVideo = context.querySelector('#chkThemeVideo');
|
||||
chkThemeVideo.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasThemeVideo = chkThemeVideo.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkMissingEpisode = context.querySelector('#chkMissingEpisode');
|
||||
chkMissingEpisode.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.IsMissing = !!chkMissingEpisode.checked;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkSpecialEpisode = context.querySelector('#chkSpecialEpisode');
|
||||
chkSpecialEpisode.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.ParentIndexNumber = chkSpecialEpisode.checked ? 0 : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkFutureEpisode = context.querySelector('#chkFutureEpisode');
|
||||
chkFutureEpisode.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
if (chkFutureEpisode.checked) {
|
||||
query.IsUnaired = true;
|
||||
query.IsVirtualUnaired = null;
|
||||
} else {
|
||||
query.IsUnaired = null;
|
||||
query.IsVirtualUnaired = false;
|
||||
}
|
||||
triggerChange(this);
|
||||
});
|
||||
const chkSubtitle = context.querySelector('#chkSubtitle');
|
||||
chkSubtitle.addEventListener('change', () => {
|
||||
query.StartIndex = 0;
|
||||
query.HasSubtitles = chkSubtitle.checked ? true : null;
|
||||
triggerChange(this);
|
||||
});
|
||||
context.addEventListener('change', (e) => {
|
||||
const chkGenreFilter = dom.parentWithClass(e.target, 'chkGenreFilter');
|
||||
if (chkGenreFilter) {
|
||||
const filterName = chkGenreFilter.getAttribute('data-filter');
|
||||
let filters = query.Genres || '';
|
||||
const delimiter = '|';
|
||||
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
|
||||
if (chkGenreFilter.checked) {
|
||||
filters = filters ? (filters + delimiter + filterName) : filterName;
|
||||
}
|
||||
});
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
query.Genres = filters;
|
||||
triggerChange(this);
|
||||
return;
|
||||
}
|
||||
const chkTagFilter = dom.parentWithClass(e.target, 'chkTagFilter');
|
||||
if (chkTagFilter) {
|
||||
const filterName = chkTagFilter.getAttribute('data-filter');
|
||||
let filters = query.Tags || '';
|
||||
const delimiter = '|';
|
||||
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
|
||||
if (chkTagFilter.checked) {
|
||||
filters = filters ? (filters + delimiter + filterName) : filterName;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
query.Tags = filters;
|
||||
triggerChange(this);
|
||||
return;
|
||||
}
|
||||
const chkYearFilter = dom.parentWithClass(e.target, 'chkYearFilter');
|
||||
if (chkYearFilter) {
|
||||
const filterName = chkYearFilter.getAttribute('data-filter');
|
||||
let filters = query.Years || '';
|
||||
const delimiter = ',';
|
||||
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
|
||||
if (chkYearFilter.checked) {
|
||||
filters = filters ? (filters + delimiter + filterName) : filterName;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
query.Years = filters;
|
||||
triggerChange(this);
|
||||
return;
|
||||
}
|
||||
const chkOfficialRatingFilter = dom.parentWithClass(e.target, 'chkOfficialRatingFilter');
|
||||
if (chkOfficialRatingFilter) {
|
||||
const filterName = chkOfficialRatingFilter.getAttribute('data-filter');
|
||||
let filters = query.OfficialRatings || '';
|
||||
const delimiter = '|';
|
||||
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
|
||||
if (chkOfficialRatingFilter.checked) {
|
||||
filters = filters ? (filters + delimiter + filterName) : filterName;
|
||||
}
|
||||
query.StartIndex = 0;
|
||||
query.OfficialRatings = filters;
|
||||
triggerChange(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
show() {
|
||||
return new Promise((resolve) => {
|
||||
const dlg = dialogHelper.createDialog({
|
||||
removeOnClose: true,
|
||||
modal: false
|
||||
});
|
||||
dlg.classList.add('ui-body-a');
|
||||
dlg.classList.add('background-theme-a');
|
||||
dlg.classList.add('formDialog');
|
||||
dlg.classList.add('filterDialog');
|
||||
dlg.innerHTML = globalize.translateHtml(template);
|
||||
setVisibility(dlg, this.options);
|
||||
dialogHelper.open(dlg);
|
||||
dlg.addEventListener('close', resolve);
|
||||
updateFilterControls(dlg, this.options);
|
||||
this.bindEvents(dlg);
|
||||
if (enableDynamicFilters(this.options.mode)) {
|
||||
dlg.classList.add('dynamicFilterDialog');
|
||||
const apiClient = ServerConnections.getApiClient(this.options.serverId);
|
||||
loadDynamicFilters(dlg, apiClient, apiClient.getCurrentUserId(), this.options.query);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterDialog;
|
||||
|
||||
@@ -297,10 +297,8 @@ class FilterMenu {
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
//if (!options.onChange) {
|
||||
saveValues(dlg, options.settings, options.settingsKey, options.setfilters);
|
||||
return resolve();
|
||||
//}
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
|
||||
@@ -1,470 +1,466 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
import dom from '../scripts/dom';
|
||||
import scrollManager from './scrollManager';
|
||||
|
||||
const scopes = [];
|
||||
function pushScope(elem) {
|
||||
scopes.push(elem);
|
||||
}
|
||||
const scopes = [];
|
||||
function pushScope(elem) {
|
||||
scopes.push(elem);
|
||||
}
|
||||
|
||||
function popScope() {
|
||||
if (scopes.length) {
|
||||
scopes.length -= 1;
|
||||
function popScope() {
|
||||
if (scopes.length) {
|
||||
scopes.length -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
function autoFocus(view, defaultToFirst, findAutoFocusElement) {
|
||||
let element;
|
||||
if (findAutoFocusElement !== false) {
|
||||
element = view.querySelector('*[autofocus]');
|
||||
if (element) {
|
||||
focus(element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
function autoFocus(view, defaultToFirst, findAutoFocusElement) {
|
||||
let element;
|
||||
if (findAutoFocusElement !== false) {
|
||||
element = view.querySelector('*[autofocus]');
|
||||
if (element) {
|
||||
focus(element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
if (defaultToFirst !== false) {
|
||||
element = getFocusableElements(view, 1, 'noautofocus')[0];
|
||||
|
||||
if (defaultToFirst !== false) {
|
||||
element = getFocusableElements(view, 1, 'noautofocus')[0];
|
||||
|
||||
if (element) {
|
||||
focus(element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function focus(element) {
|
||||
try {
|
||||
element.focus({
|
||||
preventScroll: scrollManager.isEnabled()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error in focusManager.autoFocus: ' + err);
|
||||
if (element) {
|
||||
focus(element);
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
const focusableTagNames = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
|
||||
const focusableContainerTagNames = ['BODY', 'DIALOG'];
|
||||
const focusableQuery = focusableTagNames.map(function (t) {
|
||||
if (t === 'INPUT') {
|
||||
t += ':not([type="range"]):not([type="file"])';
|
||||
}
|
||||
return t + ':not([tabindex="-1"]):not(:disabled)';
|
||||
}).join(',') + ',.focusable';
|
||||
return null;
|
||||
}
|
||||
|
||||
function isFocusable(elem) {
|
||||
return focusableTagNames.indexOf(elem.tagName) !== -1
|
||||
function focus(element) {
|
||||
try {
|
||||
element.focus({
|
||||
preventScroll: scrollManager.isEnabled()
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error in focusManager.autoFocus: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
const focusableTagNames = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
|
||||
const focusableContainerTagNames = ['BODY', 'DIALOG'];
|
||||
const focusableQuery = focusableTagNames.map(function (t) {
|
||||
if (t === 'INPUT') {
|
||||
t += ':not([type="range"]):not([type="file"])';
|
||||
}
|
||||
return t + ':not([tabindex="-1"]):not(:disabled)';
|
||||
}).join(',') + ',.focusable';
|
||||
|
||||
function isFocusable(elem) {
|
||||
return focusableTagNames.indexOf(elem.tagName) !== -1
|
||||
|| (elem.classList?.contains('focusable'));
|
||||
}
|
||||
|
||||
function normalizeFocusable(elem, originalElement) {
|
||||
if (elem) {
|
||||
const tagName = elem.tagName;
|
||||
if (!tagName || tagName === 'HTML' || tagName === 'BODY') {
|
||||
elem = originalElement;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeFocusable(elem, originalElement) {
|
||||
if (elem) {
|
||||
const tagName = elem.tagName;
|
||||
if (!tagName || tagName === 'HTML' || tagName === 'BODY') {
|
||||
elem = originalElement;
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
|
||||
function focusableParent(elem) {
|
||||
const originalElement = elem;
|
||||
|
||||
while (!isFocusable(elem)) {
|
||||
const parent = elem.parentNode;
|
||||
|
||||
if (!parent) {
|
||||
return normalizeFocusable(elem, originalElement);
|
||||
}
|
||||
|
||||
return elem;
|
||||
elem = parent;
|
||||
}
|
||||
|
||||
function focusableParent(elem) {
|
||||
const originalElement = elem;
|
||||
return normalizeFocusable(elem, originalElement);
|
||||
}
|
||||
|
||||
while (!isFocusable(elem)) {
|
||||
const parent = elem.parentNode;
|
||||
|
||||
if (!parent) {
|
||||
return normalizeFocusable(elem, originalElement);
|
||||
}
|
||||
|
||||
elem = parent;
|
||||
}
|
||||
|
||||
return normalizeFocusable(elem, originalElement);
|
||||
}
|
||||
|
||||
// Determines if a focusable element can be focused at a given point in time
|
||||
function isCurrentlyFocusableInternal(elem) {
|
||||
// http://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
|
||||
return elem.offsetParent !== null;
|
||||
}
|
||||
|
||||
// Determines if a focusable element can be focused at a given point in time
|
||||
function isCurrentlyFocusable(elem) {
|
||||
if (elem.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (elem.getAttribute('tabindex') === '-1') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (elem.tagName === 'INPUT') {
|
||||
const type = elem.type;
|
||||
if (type === 'range') {
|
||||
return false;
|
||||
}
|
||||
if (type === 'file') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return isCurrentlyFocusableInternal(elem);
|
||||
}
|
||||
|
||||
function getDefaultScope() {
|
||||
return scopes[0] || document.body;
|
||||
}
|
||||
|
||||
function getFocusableElements(parent, limit, excludeClass) {
|
||||
const elems = (parent || getDefaultScope()).querySelectorAll(focusableQuery);
|
||||
const focusableElements = [];
|
||||
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const elem = elems[i];
|
||||
|
||||
if (excludeClass && elem.classList.contains(excludeClass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCurrentlyFocusableInternal(elem)) {
|
||||
focusableElements.push(elem);
|
||||
|
||||
if (limit && focusableElements.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return focusableElements;
|
||||
}
|
||||
|
||||
function isFocusContainer(elem, direction) {
|
||||
if (focusableContainerTagNames.indexOf(elem.tagName) !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const classList = elem.classList;
|
||||
|
||||
if (classList.contains('focuscontainer')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (direction === 0) {
|
||||
if (classList.contains('focuscontainer-x')) {
|
||||
return true;
|
||||
}
|
||||
if (classList.contains('focuscontainer-left')) {
|
||||
return true;
|
||||
}
|
||||
} else if (direction === 1) {
|
||||
if (classList.contains('focuscontainer-x')) {
|
||||
return true;
|
||||
}
|
||||
if (classList.contains('focuscontainer-right')) {
|
||||
return true;
|
||||
}
|
||||
} else if (direction === 2) {
|
||||
if (classList.contains('focuscontainer-y')) {
|
||||
return true;
|
||||
}
|
||||
} else if (direction === 3) {
|
||||
if (classList.contains('focuscontainer-y')) {
|
||||
return true;
|
||||
}
|
||||
if (classList.contains('focuscontainer-down')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Determines if a focusable element can be focused at a given point in time
|
||||
function isCurrentlyFocusableInternal(elem) {
|
||||
// http://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
|
||||
return elem.offsetParent !== null;
|
||||
}
|
||||
|
||||
// Determines if a focusable element can be focused at a given point in time
|
||||
function isCurrentlyFocusable(elem) {
|
||||
if (elem.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getFocusContainer(elem, direction) {
|
||||
while (!isFocusContainer(elem, direction)) {
|
||||
elem = elem.parentNode;
|
||||
if (elem.getAttribute('tabindex') === '-1') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!elem) {
|
||||
return getDefaultScope();
|
||||
if (elem.tagName === 'INPUT') {
|
||||
const type = elem.type;
|
||||
if (type === 'range') {
|
||||
return false;
|
||||
}
|
||||
if (type === 'file') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return isCurrentlyFocusableInternal(elem);
|
||||
}
|
||||
|
||||
function getDefaultScope() {
|
||||
return scopes[0] || document.body;
|
||||
}
|
||||
|
||||
function getFocusableElements(parent, limit, excludeClass) {
|
||||
const elems = (parent || getDefaultScope()).querySelectorAll(focusableQuery);
|
||||
const focusableElements = [];
|
||||
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const elem = elems[i];
|
||||
|
||||
if (excludeClass && elem.classList.contains(excludeClass)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCurrentlyFocusableInternal(elem)) {
|
||||
focusableElements.push(elem);
|
||||
|
||||
if (limit && focusableElements.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
function getOffset(elem) {
|
||||
let box;
|
||||
return focusableElements;
|
||||
}
|
||||
|
||||
// Support: BlackBerry 5, iOS 3 (original iPhone)
|
||||
// If we don't have gBCR, just use 0,0 rather than error
|
||||
if (elem.getBoundingClientRect) {
|
||||
box = elem.getBoundingClientRect();
|
||||
} else {
|
||||
box = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (box.right === null) {
|
||||
// Create a new object because some browsers will throw an error when trying to set data onto the Rect object
|
||||
const newBox = {
|
||||
top: box.top,
|
||||
left: box.left,
|
||||
width: box.width,
|
||||
height: box.height
|
||||
};
|
||||
|
||||
box = newBox;
|
||||
|
||||
box.right = box.left + box.width;
|
||||
box.bottom = box.top + box.height;
|
||||
}
|
||||
|
||||
return box;
|
||||
function isFocusContainer(elem, direction) {
|
||||
if (focusableContainerTagNames.indexOf(elem.tagName) !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function nav(activeElement, direction, container, focusableElements) {
|
||||
activeElement = activeElement || document.activeElement;
|
||||
const classList = elem.classList;
|
||||
|
||||
if (classList.contains('focuscontainer')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (direction === 0) {
|
||||
if (classList.contains('focuscontainer-x')) {
|
||||
return true;
|
||||
}
|
||||
if (classList.contains('focuscontainer-left')) {
|
||||
return true;
|
||||
}
|
||||
} else if (direction === 1) {
|
||||
if (classList.contains('focuscontainer-x')) {
|
||||
return true;
|
||||
}
|
||||
if (classList.contains('focuscontainer-right')) {
|
||||
return true;
|
||||
}
|
||||
} else if (direction === 2) {
|
||||
if (classList.contains('focuscontainer-y')) {
|
||||
return true;
|
||||
}
|
||||
} else if (direction === 3) {
|
||||
if (classList.contains('focuscontainer-y')) {
|
||||
return true;
|
||||
}
|
||||
if (classList.contains('focuscontainer-down')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getFocusContainer(elem, direction) {
|
||||
while (!isFocusContainer(elem, direction)) {
|
||||
elem = elem.parentNode;
|
||||
|
||||
if (!elem) {
|
||||
return getDefaultScope();
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
function getOffset(elem) {
|
||||
let box;
|
||||
|
||||
// Support: BlackBerry 5, iOS 3 (original iPhone)
|
||||
// If we don't have gBCR, just use 0,0 rather than error
|
||||
if (elem.getBoundingClientRect) {
|
||||
box = elem.getBoundingClientRect();
|
||||
} else {
|
||||
box = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (box.right === null) {
|
||||
// Create a new object because some browsers will throw an error when trying to set data onto the Rect object
|
||||
const newBox = {
|
||||
top: box.top,
|
||||
left: box.left,
|
||||
width: box.width,
|
||||
height: box.height
|
||||
};
|
||||
|
||||
box = newBox;
|
||||
|
||||
box.right = box.left + box.width;
|
||||
box.bottom = box.top + box.height;
|
||||
}
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
function nav(activeElement, direction, container, focusableElements) {
|
||||
activeElement = activeElement || document.activeElement;
|
||||
|
||||
if (activeElement) {
|
||||
activeElement = focusableParent(activeElement);
|
||||
}
|
||||
|
||||
container = container || (activeElement ? getFocusContainer(activeElement, direction) : getDefaultScope());
|
||||
|
||||
if (!activeElement) {
|
||||
autoFocus(container, true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const focusableContainer = dom.parentWithClass(activeElement, 'focusable');
|
||||
|
||||
const rect = getOffset(activeElement);
|
||||
|
||||
// Get elements and work out x/y points
|
||||
const point1x = parseFloat(rect.left) || 0;
|
||||
const point1y = parseFloat(rect.top) || 0;
|
||||
const point2x = parseFloat(point1x + rect.width - 1) || point1x;
|
||||
const point2y = parseFloat(point1y + rect.height - 1) || point1y;
|
||||
|
||||
const sourceMidX = rect.left + (rect.width / 2);
|
||||
const sourceMidY = rect.top + (rect.height / 2);
|
||||
|
||||
const focusable = focusableElements || container.querySelectorAll(focusableQuery);
|
||||
|
||||
const maxDistance = Infinity;
|
||||
let minDistance = maxDistance;
|
||||
let nearestElement;
|
||||
|
||||
for (let i = 0, length = focusable.length; i < length; i++) {
|
||||
const curr = focusable[i];
|
||||
|
||||
if (curr === activeElement) {
|
||||
continue;
|
||||
}
|
||||
// Don't refocus into the same container
|
||||
if (curr === focusableContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const elementRect = getOffset(curr);
|
||||
|
||||
// not currently visible
|
||||
if (!elementRect.width && !elementRect.height) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case 0:
|
||||
// left
|
||||
if (elementRect.left >= rect.left) {
|
||||
continue;
|
||||
}
|
||||
if (elementRect.right === rect.right) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
// right
|
||||
if (elementRect.right <= rect.right) {
|
||||
continue;
|
||||
}
|
||||
if (elementRect.left === rect.left) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
// up
|
||||
if (elementRect.top >= rect.top) {
|
||||
continue;
|
||||
}
|
||||
if (elementRect.bottom >= rect.bottom) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
// down
|
||||
if (elementRect.bottom <= rect.bottom) {
|
||||
continue;
|
||||
}
|
||||
if (elementRect.top <= rect.top) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const x = elementRect.left;
|
||||
const y = elementRect.top;
|
||||
const x2 = x + elementRect.width - 1;
|
||||
const y2 = y + elementRect.height - 1;
|
||||
|
||||
const intersectX = intersects(point1x, point2x, x, x2);
|
||||
const intersectY = intersects(point1y, point2y, y, y2);
|
||||
|
||||
const midX = elementRect.left + (elementRect.width / 2);
|
||||
const midY = elementRect.top + (elementRect.height / 2);
|
||||
|
||||
let distX;
|
||||
let distY;
|
||||
|
||||
switch (direction) {
|
||||
case 0:
|
||||
// left
|
||||
distX = Math.abs(point1x - Math.min(point1x, x2));
|
||||
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
|
||||
break;
|
||||
case 1:
|
||||
// right
|
||||
distX = Math.abs(point2x - Math.max(point2x, x));
|
||||
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
|
||||
break;
|
||||
case 2:
|
||||
// up
|
||||
distY = Math.abs(point1y - Math.min(point1y, y2));
|
||||
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
|
||||
break;
|
||||
case 3:
|
||||
// down
|
||||
distY = Math.abs(point2y - Math.max(point2y, y));
|
||||
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const dist = Math.sqrt(distX * distX + distY * distY);
|
||||
|
||||
if (dist < minDistance) {
|
||||
nearestElement = curr;
|
||||
minDistance = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestElement) {
|
||||
// See if there's a focusable container, and if so, send the focus command to that
|
||||
if (activeElement) {
|
||||
activeElement = focusableParent(activeElement);
|
||||
}
|
||||
|
||||
container = container || (activeElement ? getFocusContainer(activeElement, direction) : getDefaultScope());
|
||||
|
||||
if (!activeElement) {
|
||||
autoFocus(container, true, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const focusableContainer = dom.parentWithClass(activeElement, 'focusable');
|
||||
|
||||
const rect = getOffset(activeElement);
|
||||
|
||||
// Get elements and work out x/y points
|
||||
const point1x = parseFloat(rect.left) || 0;
|
||||
const point1y = parseFloat(rect.top) || 0;
|
||||
const point2x = parseFloat(point1x + rect.width - 1) || point1x;
|
||||
const point2y = parseFloat(point1y + rect.height - 1) || point1y;
|
||||
|
||||
const sourceMidX = rect.left + (rect.width / 2);
|
||||
const sourceMidY = rect.top + (rect.height / 2);
|
||||
|
||||
const focusable = focusableElements || container.querySelectorAll(focusableQuery);
|
||||
|
||||
const maxDistance = Infinity;
|
||||
let minDistance = maxDistance;
|
||||
let nearestElement;
|
||||
|
||||
for (let i = 0, length = focusable.length; i < length; i++) {
|
||||
const curr = focusable[i];
|
||||
|
||||
if (curr === activeElement) {
|
||||
continue;
|
||||
}
|
||||
// Don't refocus into the same container
|
||||
if (curr === focusableContainer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const elementRect = getOffset(curr);
|
||||
|
||||
// not currently visible
|
||||
if (!elementRect.width && !elementRect.height) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case 0:
|
||||
// left
|
||||
if (elementRect.left >= rect.left) {
|
||||
continue;
|
||||
}
|
||||
if (elementRect.right === rect.right) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
// right
|
||||
if (elementRect.right <= rect.right) {
|
||||
continue;
|
||||
}
|
||||
if (elementRect.left === rect.left) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
// up
|
||||
if (elementRect.top >= rect.top) {
|
||||
continue;
|
||||
}
|
||||
if (elementRect.bottom >= rect.bottom) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
// down
|
||||
if (elementRect.bottom <= rect.bottom) {
|
||||
continue;
|
||||
}
|
||||
if (elementRect.top <= rect.top) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const x = elementRect.left;
|
||||
const y = elementRect.top;
|
||||
const x2 = x + elementRect.width - 1;
|
||||
const y2 = y + elementRect.height - 1;
|
||||
|
||||
const intersectX = intersects(point1x, point2x, x, x2);
|
||||
const intersectY = intersects(point1y, point2y, y, y2);
|
||||
|
||||
const midX = elementRect.left + (elementRect.width / 2);
|
||||
const midY = elementRect.top + (elementRect.height / 2);
|
||||
|
||||
let distX;
|
||||
let distY;
|
||||
|
||||
switch (direction) {
|
||||
case 0:
|
||||
// left
|
||||
distX = Math.abs(point1x - Math.min(point1x, x2));
|
||||
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
|
||||
break;
|
||||
case 1:
|
||||
// right
|
||||
distX = Math.abs(point2x - Math.max(point2x, x));
|
||||
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
|
||||
break;
|
||||
case 2:
|
||||
// up
|
||||
distY = Math.abs(point1y - Math.min(point1y, y2));
|
||||
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
|
||||
break;
|
||||
case 3:
|
||||
// down
|
||||
distY = Math.abs(point2y - Math.max(point2y, y));
|
||||
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const dist = Math.sqrt(distX * distX + distY * distY);
|
||||
|
||||
if (dist < minDistance) {
|
||||
nearestElement = curr;
|
||||
minDistance = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestElement) {
|
||||
// See if there's a focusable container, and if so, send the focus command to that
|
||||
if (activeElement) {
|
||||
const nearestElementFocusableParent = dom.parentWithClass(nearestElement, 'focusable');
|
||||
if (nearestElementFocusableParent
|
||||
const nearestElementFocusableParent = dom.parentWithClass(nearestElement, 'focusable');
|
||||
if (nearestElementFocusableParent
|
||||
&& nearestElementFocusableParent !== nearestElement
|
||||
&& focusableContainer !== nearestElementFocusableParent
|
||||
) {
|
||||
nearestElement = nearestElementFocusableParent;
|
||||
}
|
||||
}
|
||||
focus(nearestElement);
|
||||
}
|
||||
}
|
||||
|
||||
function intersectsInternal(a1, a2, b1, b2) {
|
||||
return (b1 >= a1 && b1 <= a2) || (b2 >= a1 && b2 <= a2);
|
||||
}
|
||||
|
||||
function intersects(a1, a2, b1, b2) {
|
||||
return intersectsInternal(a1, a2, b1, b2) || intersectsInternal(b1, b2, a1, a2);
|
||||
}
|
||||
|
||||
function sendText(text) {
|
||||
const elem = document.activeElement;
|
||||
|
||||
elem.value = text;
|
||||
}
|
||||
|
||||
function focusFirst(container, focusableSelector) {
|
||||
const elems = container.querySelectorAll(focusableSelector);
|
||||
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const elem = elems[i];
|
||||
|
||||
if (isCurrentlyFocusableInternal(elem)) {
|
||||
focus(elem);
|
||||
break;
|
||||
) {
|
||||
nearestElement = nearestElementFocusableParent;
|
||||
}
|
||||
}
|
||||
focus(nearestElement);
|
||||
}
|
||||
}
|
||||
|
||||
function focusLast(container, focusableSelector) {
|
||||
const elems = [].slice.call(container.querySelectorAll(focusableSelector), 0).reverse();
|
||||
function intersectsInternal(a1, a2, b1, b2) {
|
||||
return (b1 >= a1 && b1 <= a2) || (b2 >= a1 && b2 <= a2);
|
||||
}
|
||||
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const elem = elems[i];
|
||||
function intersects(a1, a2, b1, b2) {
|
||||
return intersectsInternal(a1, a2, b1, b2) || intersectsInternal(b1, b2, a1, a2);
|
||||
}
|
||||
|
||||
if (isCurrentlyFocusableInternal(elem)) {
|
||||
focus(elem);
|
||||
break;
|
||||
}
|
||||
function sendText(text) {
|
||||
const elem = document.activeElement;
|
||||
|
||||
elem.value = text;
|
||||
}
|
||||
|
||||
function focusFirst(container, focusableSelector) {
|
||||
const elems = container.querySelectorAll(focusableSelector);
|
||||
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const elem = elems[i];
|
||||
|
||||
if (isCurrentlyFocusableInternal(elem)) {
|
||||
focus(elem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function focusLast(container, focusableSelector) {
|
||||
const elems = [].slice.call(container.querySelectorAll(focusableSelector), 0).reverse();
|
||||
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const elem = elems[i];
|
||||
|
||||
if (isCurrentlyFocusableInternal(elem)) {
|
||||
focus(elem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveFocus(sourceElement, container, focusableSelector, offset) {
|
||||
const elems = container.querySelectorAll(focusableSelector);
|
||||
const list = [];
|
||||
let i;
|
||||
let length;
|
||||
let elem;
|
||||
|
||||
for (i = 0, length = elems.length; i < length; i++) {
|
||||
elem = elems[i];
|
||||
|
||||
if (isCurrentlyFocusableInternal(elem)) {
|
||||
list.push(elem);
|
||||
}
|
||||
}
|
||||
|
||||
function moveFocus(sourceElement, container, focusableSelector, offset) {
|
||||
const elems = container.querySelectorAll(focusableSelector);
|
||||
const list = [];
|
||||
let i;
|
||||
let length;
|
||||
let elem;
|
||||
let currentIndex = -1;
|
||||
|
||||
for (i = 0, length = elems.length; i < length; i++) {
|
||||
elem = elems[i];
|
||||
for (i = 0, length = list.length; i < length; i++) {
|
||||
elem = list[i];
|
||||
|
||||
if (isCurrentlyFocusableInternal(elem)) {
|
||||
list.push(elem);
|
||||
}
|
||||
}
|
||||
|
||||
let currentIndex = -1;
|
||||
|
||||
for (i = 0, length = list.length; i < length; i++) {
|
||||
elem = list[i];
|
||||
|
||||
if (sourceElement === elem || elem.contains(sourceElement)) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newIndex = currentIndex + offset;
|
||||
newIndex = Math.max(0, newIndex);
|
||||
newIndex = Math.min(newIndex, list.length - 1);
|
||||
|
||||
const newElem = list[newIndex];
|
||||
if (newElem) {
|
||||
focus(newElem);
|
||||
if (sourceElement === elem || elem.contains(sourceElement)) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
if (currentIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newIndex = currentIndex + offset;
|
||||
newIndex = Math.max(0, newIndex);
|
||||
newIndex = Math.min(newIndex, list.length - 1);
|
||||
|
||||
const newElem = list[newIndex];
|
||||
if (newElem) {
|
||||
focus(newElem);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
autoFocus: autoFocus,
|
||||
|
||||
@@ -1,47 +1,43 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
import dom from '../scripts/dom';
|
||||
import { appRouter } from './appRouter';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
import ServerConnections from './ServerConnections';
|
||||
|
||||
function onGroupedCardClick(e, card) {
|
||||
const itemId = card.getAttribute('data-id');
|
||||
const serverId = card.getAttribute('data-serverid');
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
const playedIndicator = card.querySelector('.playedIndicator');
|
||||
const playedIndicatorHtml = playedIndicator ? playedIndicator.innerHTML : null;
|
||||
const options = {
|
||||
Limit: parseInt(playedIndicatorHtml || '10', 10),
|
||||
Fields: 'PrimaryImageAspectRatio,DateCreated',
|
||||
ParentId: itemId,
|
||||
GroupItems: false
|
||||
};
|
||||
const actionableParent = dom.parentWithTag(e.target, ['A', 'BUTTON', 'INPUT']);
|
||||
function onGroupedCardClick(e, card) {
|
||||
const itemId = card.getAttribute('data-id');
|
||||
const serverId = card.getAttribute('data-serverid');
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
const userId = apiClient.getCurrentUserId();
|
||||
const playedIndicator = card.querySelector('.playedIndicator');
|
||||
const playedIndicatorHtml = playedIndicator ? playedIndicator.innerHTML : null;
|
||||
const options = {
|
||||
Limit: parseInt(playedIndicatorHtml || '10', 10),
|
||||
Fields: 'PrimaryImageAspectRatio,DateCreated',
|
||||
ParentId: itemId,
|
||||
GroupItems: false
|
||||
};
|
||||
const actionableParent = dom.parentWithTag(e.target, ['A', 'BUTTON', 'INPUT']);
|
||||
|
||||
if (!actionableParent || actionableParent.classList.contains('cardContent')) {
|
||||
apiClient.getJSON(apiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(function (items) {
|
||||
if (items.length === 1) {
|
||||
appRouter.showItem(items[0]);
|
||||
return;
|
||||
}
|
||||
if (!actionableParent || actionableParent.classList.contains('cardContent')) {
|
||||
apiClient.getJSON(apiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(function (items) {
|
||||
if (items.length === 1) {
|
||||
appRouter.showItem(items[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = 'details?id=' + itemId + '&serverId=' + serverId;
|
||||
Dashboard.navigate(url);
|
||||
});
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
const url = 'details?id=' + itemId + '&serverId=' + serverId;
|
||||
Dashboard.navigate(url);
|
||||
});
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default function onItemsContainerClick(e) {
|
||||
const groupedCard = dom.parentWithClass(e.target, 'groupedCard');
|
||||
export default function onItemsContainerClick(e) {
|
||||
const groupedCard = dom.parentWithClass(e.target, 'groupedCard');
|
||||
|
||||
if (groupedCard) {
|
||||
onGroupedCardClick(e, groupedCard);
|
||||
}
|
||||
if (groupedCard) {
|
||||
onGroupedCardClick(e, groupedCard);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import ServerConnections from '../ServerConnections';
|
||||
import template from './tvguide.template.html';
|
||||
|
||||
function showViewSettings(instance) {
|
||||
import('./guide-settings').then(({default: guideSettingsDialog}) => {
|
||||
import('./guide-settings').then(({ default: guideSettingsDialog }) => {
|
||||
guideSettingsDialog.show(instance.categoryOptions).then(function () {
|
||||
instance.refresh();
|
||||
});
|
||||
|
||||
@@ -14,498 +14,494 @@ import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './homeScreenSettings.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
const numConfigurableSections = 7;
|
||||
|
||||
const numConfigurableSections = 7;
|
||||
function renderViews(page, user, result) {
|
||||
let folderHtml = '';
|
||||
|
||||
function renderViews(page, user, result) {
|
||||
let folderHtml = '';
|
||||
folderHtml += '<div class="checkboxList">';
|
||||
folderHtml += result.map(i => {
|
||||
let currentHtml = '';
|
||||
|
||||
folderHtml += '<div class="checkboxList">';
|
||||
folderHtml += result.map(i => {
|
||||
let currentHtml = '';
|
||||
const id = `chkGroupFolder${i.Id}`;
|
||||
|
||||
const id = `chkGroupFolder${i.Id}`;
|
||||
const isChecked = user.Configuration.GroupedFolders.includes(i.Id);
|
||||
|
||||
const isChecked = user.Configuration.GroupedFolders.includes(i.Id);
|
||||
const checkedHtml = isChecked ? ' checked="checked"' : '';
|
||||
|
||||
const checkedHtml = isChecked ? ' checked="checked"' : '';
|
||||
currentHtml += '<label>';
|
||||
currentHtml += `<input type="checkbox" is="emby-checkbox" class="chkGroupFolder" data-folderid="${i.Id}" id="${id}"${checkedHtml}/>`;
|
||||
currentHtml += `<span>${escapeHtml(i.Name)}</span>`;
|
||||
currentHtml += '</label>';
|
||||
|
||||
currentHtml += '<label>';
|
||||
currentHtml += `<input type="checkbox" is="emby-checkbox" class="chkGroupFolder" data-folderid="${i.Id}" id="${id}"${checkedHtml}/>`;
|
||||
currentHtml += `<span>${escapeHtml(i.Name)}</span>`;
|
||||
currentHtml += '</label>';
|
||||
return currentHtml;
|
||||
}).join('');
|
||||
|
||||
return currentHtml;
|
||||
}).join('');
|
||||
folderHtml += '</div>';
|
||||
|
||||
folderHtml += '</div>';
|
||||
page.querySelector('.folderGroupList').innerHTML = folderHtml;
|
||||
}
|
||||
|
||||
page.querySelector('.folderGroupList').innerHTML = folderHtml;
|
||||
function getLandingScreenOptions(type) {
|
||||
const list = [];
|
||||
|
||||
if (type === 'movies') {
|
||||
list.push({
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'movies',
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Suggestions'),
|
||||
value: 'suggestions'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Trailers'),
|
||||
value: 'trailers'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Favorites'),
|
||||
value: 'favorites'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Collections'),
|
||||
value: 'collections'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Genres'),
|
||||
value: 'genres'
|
||||
});
|
||||
} else if (type === 'tvshows') {
|
||||
list.push({
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'shows',
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Suggestions'),
|
||||
value: 'suggestions'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('TabUpcoming'),
|
||||
value: 'upcoming'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Genres'),
|
||||
value: 'genres'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('TabNetworks'),
|
||||
value: 'networks'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Episodes'),
|
||||
value: 'episodes'
|
||||
});
|
||||
} else if (type === 'music') {
|
||||
list.push({
|
||||
name: globalize.translate('Albums'),
|
||||
value: 'albums',
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Suggestions'),
|
||||
value: 'suggestions'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('HeaderAlbumArtists'),
|
||||
value: 'albumartists'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Artists'),
|
||||
value: 'artists'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Playlists'),
|
||||
value: 'playlists'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Songs'),
|
||||
value: 'songs'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Genres'),
|
||||
value: 'genres'
|
||||
});
|
||||
} else if (type === 'livetv') {
|
||||
list.push({
|
||||
name: globalize.translate('Programs'),
|
||||
value: 'programs',
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Guide'),
|
||||
value: 'guide'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Channels'),
|
||||
value: 'channels'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Recordings'),
|
||||
value: 'recordings'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Schedule'),
|
||||
value: 'schedule'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Series'),
|
||||
value: 'series'
|
||||
});
|
||||
}
|
||||
|
||||
function getLandingScreenOptions(type) {
|
||||
const list = [];
|
||||
return list;
|
||||
}
|
||||
|
||||
if (type === 'movies') {
|
||||
list.push({
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'movies',
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Suggestions'),
|
||||
value: 'suggestions'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Trailers'),
|
||||
value: 'trailers'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Favorites'),
|
||||
value: 'favorites'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Collections'),
|
||||
value: 'collections'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Genres'),
|
||||
value: 'genres'
|
||||
});
|
||||
} else if (type === 'tvshows') {
|
||||
list.push({
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'shows',
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Suggestions'),
|
||||
value: 'suggestions'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('TabUpcoming'),
|
||||
value: 'upcoming'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Genres'),
|
||||
value: 'genres'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('TabNetworks'),
|
||||
value: 'networks'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Episodes'),
|
||||
value: 'episodes'
|
||||
});
|
||||
} else if (type === 'music') {
|
||||
list.push({
|
||||
name: globalize.translate('Albums'),
|
||||
value: 'albums',
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Suggestions'),
|
||||
value: 'suggestions'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('HeaderAlbumArtists'),
|
||||
value: 'albumartists'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Artists'),
|
||||
value: 'artists'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Playlists'),
|
||||
value: 'playlists'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Songs'),
|
||||
value: 'songs'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Genres'),
|
||||
value: 'genres'
|
||||
});
|
||||
} else if (type === 'livetv') {
|
||||
list.push({
|
||||
name: globalize.translate('Programs'),
|
||||
value: 'programs',
|
||||
isDefault: true
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Guide'),
|
||||
value: 'guide'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Channels'),
|
||||
value: 'channels'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Recordings'),
|
||||
value: 'recordings'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Schedule'),
|
||||
value: 'schedule'
|
||||
});
|
||||
list.push({
|
||||
name: globalize.translate('Series'),
|
||||
value: 'series'
|
||||
});
|
||||
function getLandingScreenOptionsHtml(type, userValue) {
|
||||
return getLandingScreenOptions(type).map(o => {
|
||||
const selected = userValue === o.value || (o.isDefault && !userValue);
|
||||
const selectedHtml = selected ? ' selected' : '';
|
||||
const optionValue = o.isDefault ? '' : o.value;
|
||||
|
||||
return `<option value="${optionValue}"${selectedHtml}>${escapeHtml(o.name)}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderViewOrder(context, user, result) {
|
||||
let html = '';
|
||||
|
||||
html += result.Items.map((view) => {
|
||||
let currentHtml = '';
|
||||
|
||||
currentHtml += `<div class="listItem viewItem" data-viewid="${view.Id}">`;
|
||||
|
||||
currentHtml += '<span class="material-icons listItemIcon folder_open" aria-hidden="true"></span>';
|
||||
|
||||
currentHtml += '<div class="listItemBody">';
|
||||
|
||||
currentHtml += '<div>';
|
||||
currentHtml += escapeHtml(view.Name);
|
||||
currentHtml += '</div>';
|
||||
|
||||
currentHtml += '</div>';
|
||||
|
||||
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemUp btnViewItemMove autoSize" title="${globalize.translate('Up')}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
|
||||
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemDown btnViewItemMove autoSize" title="${globalize.translate('Down')}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
|
||||
|
||||
currentHtml += '</div>';
|
||||
|
||||
return currentHtml;
|
||||
}).join('');
|
||||
|
||||
context.querySelector('.viewOrderList').innerHTML = html;
|
||||
}
|
||||
|
||||
function updateHomeSectionValues(context, userSettings) {
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
const select = context.querySelector(`#selectHomeSection${i}`);
|
||||
const defaultValue = homeSections.getDefaultSection(i - 1);
|
||||
|
||||
const option = select.querySelector(`option[value=${defaultValue}]`) || select.querySelector('option[value=""]');
|
||||
|
||||
const userValue = userSettings.get(`homesection${i - 1}`);
|
||||
|
||||
option.value = '';
|
||||
|
||||
if (userValue === defaultValue || !userValue) {
|
||||
select.value = '';
|
||||
} else {
|
||||
select.value = userValue;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function getLandingScreenOptionsHtml(type, userValue) {
|
||||
return getLandingScreenOptions(type).map(o => {
|
||||
const selected = userValue === o.value || (o.isDefault && !userValue);
|
||||
const selectedHtml = selected ? ' selected' : '';
|
||||
const optionValue = o.isDefault ? '' : o.value;
|
||||
context.querySelector('.selectTVHomeScreen').value = userSettings.get('tvhome') || '';
|
||||
}
|
||||
|
||||
return `<option value="${optionValue}"${selectedHtml}>${escapeHtml(o.name)}</option>`;
|
||||
}).join('');
|
||||
function getPerLibrarySettingsHtml(item, user, userSettings) {
|
||||
let html = '';
|
||||
|
||||
let isChecked;
|
||||
|
||||
if (item.Type === 'Channel' || item.CollectionType === 'boxsets' || item.CollectionType === 'playlists') {
|
||||
isChecked = !(user.Configuration.MyMediaExcludes || []).includes(item.Id);
|
||||
html += '<div>';
|
||||
html += '<label>';
|
||||
html += `<input type="checkbox" is="emby-checkbox" class="chkIncludeInMyMedia" data-folderid="${item.Id}"${isChecked ? ' checked="checked"' : ''}/>`;
|
||||
html += `<span>${globalize.translate('DisplayInMyMedia')}</span>`;
|
||||
html += '</label>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
function renderViewOrder(context, user, result) {
|
||||
let html = '';
|
||||
|
||||
html += result.Items.map((view) => {
|
||||
let currentHtml = '';
|
||||
|
||||
currentHtml += `<div class="listItem viewItem" data-viewid="${view.Id}">`;
|
||||
|
||||
currentHtml += '<span class="material-icons listItemIcon folder_open" aria-hidden="true"></span>';
|
||||
|
||||
currentHtml += '<div class="listItemBody">';
|
||||
|
||||
currentHtml += '<div>';
|
||||
currentHtml += escapeHtml(view.Name);
|
||||
currentHtml += '</div>';
|
||||
|
||||
currentHtml += '</div>';
|
||||
|
||||
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemUp btnViewItemMove autoSize" title="${globalize.translate('Up')}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
|
||||
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemDown btnViewItemMove autoSize" title="${globalize.translate('Down')}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
|
||||
|
||||
currentHtml += '</div>';
|
||||
|
||||
return currentHtml;
|
||||
}).join('');
|
||||
|
||||
context.querySelector('.viewOrderList').innerHTML = html;
|
||||
const excludeFromLatest = ['playlists', 'livetv', 'boxsets', 'channels'];
|
||||
if (!excludeFromLatest.includes(item.CollectionType || '')) {
|
||||
isChecked = !user.Configuration.LatestItemsExcludes.includes(item.Id);
|
||||
html += '<label class="fldIncludeInLatest">';
|
||||
html += `<input type="checkbox" is="emby-checkbox" class="chkIncludeInLatest" data-folderid="${item.Id}"${isChecked ? ' checked="checked"' : ''}/>`;
|
||||
html += `<span>${globalize.translate('DisplayInOtherHomeScreenSections')}</span>`;
|
||||
html += '</label>';
|
||||
}
|
||||
|
||||
function updateHomeSectionValues(context, userSettings) {
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
const select = context.querySelector(`#selectHomeSection${i}`);
|
||||
const defaultValue = homeSections.getDefaultSection(i - 1);
|
||||
if (html) {
|
||||
html = `<div class="checkboxListContainer">${html}</div>`;
|
||||
}
|
||||
|
||||
const option = select.querySelector(`option[value=${defaultValue}]`) || select.querySelector('option[value=""]');
|
||||
if (item.CollectionType === 'movies' || item.CollectionType === 'tvshows' || item.CollectionType === 'music' || item.CollectionType === 'livetv') {
|
||||
const idForLanding = item.CollectionType === 'livetv' ? item.CollectionType : item.Id;
|
||||
html += '<div class="selectContainer">';
|
||||
html += `<select is="emby-select" class="selectLanding" data-folderid="${idForLanding}" label="${globalize.translate('LabelDefaultScreen')}">`;
|
||||
|
||||
const userValue = userSettings.get(`homesection${i - 1}`);
|
||||
const userValue = userSettings.get(`landing-${idForLanding}`);
|
||||
|
||||
option.value = '';
|
||||
html += getLandingScreenOptionsHtml(item.CollectionType, userValue);
|
||||
|
||||
if (userValue === defaultValue || !userValue) {
|
||||
select.value = '';
|
||||
html += '</select>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (html) {
|
||||
let prefix = '';
|
||||
prefix += '<div class="verticalSection">';
|
||||
|
||||
prefix += '<h2 class="sectionTitle">';
|
||||
prefix += escapeHtml(item.Name);
|
||||
prefix += '</h2>';
|
||||
|
||||
html = prefix + html;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderPerLibrarySettings(context, user, userViews, userSettings) {
|
||||
const elem = context.querySelector('.perLibrarySettings');
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = userViews.length; i < length; i++) {
|
||||
html += getPerLibrarySettingsHtml(userViews[i], user, userSettings);
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
}
|
||||
|
||||
function loadForm(context, user, userSettings, apiClient) {
|
||||
context.querySelector('.chkHidePlayedFromLatest').checked = user.Configuration.HidePlayedInLatest || false;
|
||||
|
||||
updateHomeSectionValues(context, userSettings);
|
||||
|
||||
const promise1 = apiClient.getUserViews({ IncludeHidden: true }, user.Id);
|
||||
const promise2 = apiClient.getJSON(apiClient.getUrl(`Users/${user.Id}/GroupingOptions`));
|
||||
|
||||
Promise.all([promise1, promise2]).then(responses => {
|
||||
renderViewOrder(context, user, responses[0]);
|
||||
|
||||
renderPerLibrarySettings(context, user, responses[0].Items, userSettings);
|
||||
|
||||
renderViews(context, user, responses[1]);
|
||||
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function onSectionOrderListClick(e) {
|
||||
const target = dom.parentWithClass(e.target, 'btnViewItemMove');
|
||||
|
||||
if (target) {
|
||||
const viewItem = dom.parentWithClass(target, 'viewItem');
|
||||
|
||||
if (viewItem) {
|
||||
if (target.classList.contains('btnViewItemDown')) {
|
||||
const next = viewItem.nextSibling;
|
||||
|
||||
if (next) {
|
||||
viewItem.parentNode.removeChild(viewItem);
|
||||
next.parentNode.insertBefore(viewItem, next.nextSibling);
|
||||
focusManager.focus(e.target);
|
||||
}
|
||||
} else {
|
||||
select.value = userValue;
|
||||
const prev = viewItem.previousSibling;
|
||||
|
||||
if (prev) {
|
||||
viewItem.parentNode.removeChild(viewItem);
|
||||
prev.parentNode.insertBefore(viewItem, prev);
|
||||
focusManager.focus(e.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.querySelector('.selectTVHomeScreen').value = userSettings.get('tvhome') || '';
|
||||
function getCheckboxItems(selector, context, isChecked) {
|
||||
const inputs = context.querySelectorAll(selector);
|
||||
const list = [];
|
||||
|
||||
for (let i = 0, length = inputs.length; i < length; i++) {
|
||||
if (inputs[i].checked === isChecked) {
|
||||
list.push(inputs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function getPerLibrarySettingsHtml(item, user, userSettings) {
|
||||
let html = '';
|
||||
return list;
|
||||
}
|
||||
|
||||
let isChecked;
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
user.Configuration.HidePlayedInLatest = context.querySelector('.chkHidePlayedFromLatest').checked;
|
||||
|
||||
if (item.Type === 'Channel' || item.CollectionType === 'boxsets' || item.CollectionType === 'playlists') {
|
||||
isChecked = !(user.Configuration.MyMediaExcludes || []).includes(item.Id);
|
||||
html += '<div>';
|
||||
html += '<label>';
|
||||
html += `<input type="checkbox" is="emby-checkbox" class="chkIncludeInMyMedia" data-folderid="${item.Id}"${isChecked ? ' checked="checked"' : ''}/>`;
|
||||
html += `<span>${globalize.translate('DisplayInMyMedia')}</span>`;
|
||||
html += '</label>';
|
||||
html += '</div>';
|
||||
}
|
||||
user.Configuration.LatestItemsExcludes = getCheckboxItems('.chkIncludeInLatest', context, false).map(i => {
|
||||
return i.getAttribute('data-folderid');
|
||||
});
|
||||
|
||||
const excludeFromLatest = ['playlists', 'livetv', 'boxsets', 'channels'];
|
||||
if (!excludeFromLatest.includes(item.CollectionType || '')) {
|
||||
isChecked = !user.Configuration.LatestItemsExcludes.includes(item.Id);
|
||||
html += '<label class="fldIncludeInLatest">';
|
||||
html += `<input type="checkbox" is="emby-checkbox" class="chkIncludeInLatest" data-folderid="${item.Id}"${isChecked ? ' checked="checked"' : ''}/>`;
|
||||
html += `<span>${globalize.translate('DisplayInOtherHomeScreenSections')}</span>`;
|
||||
html += '</label>';
|
||||
}
|
||||
user.Configuration.MyMediaExcludes = getCheckboxItems('.chkIncludeInMyMedia', context, false).map(i => {
|
||||
return i.getAttribute('data-folderid');
|
||||
});
|
||||
|
||||
if (html) {
|
||||
html = `<div class="checkboxListContainer">${html}</div>`;
|
||||
}
|
||||
user.Configuration.GroupedFolders = getCheckboxItems('.chkGroupFolder', context, true).map(i => {
|
||||
return i.getAttribute('data-folderid');
|
||||
});
|
||||
|
||||
if (item.CollectionType === 'movies' || item.CollectionType === 'tvshows' || item.CollectionType === 'music' || item.CollectionType === 'livetv') {
|
||||
const idForLanding = item.CollectionType === 'livetv' ? item.CollectionType : item.Id;
|
||||
html += '<div class="selectContainer">';
|
||||
html += `<select is="emby-select" class="selectLanding" data-folderid="${idForLanding}" label="${globalize.translate('LabelDefaultScreen')}">`;
|
||||
|
||||
const userValue = userSettings.get(`landing-${idForLanding}`);
|
||||
|
||||
html += getLandingScreenOptionsHtml(item.CollectionType, userValue);
|
||||
|
||||
html += '</select>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (html) {
|
||||
let prefix = '';
|
||||
prefix += '<div class="verticalSection">';
|
||||
|
||||
prefix += '<h2 class="sectionTitle">';
|
||||
prefix += escapeHtml(item.Name);
|
||||
prefix += '</h2>';
|
||||
|
||||
html = prefix + html;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
const viewItems = context.querySelectorAll('.viewItem');
|
||||
const orderedViews = [];
|
||||
let i;
|
||||
let length;
|
||||
for (i = 0, length = viewItems.length; i < length; i++) {
|
||||
orderedViews.push(viewItems[i].getAttribute('data-viewid'));
|
||||
}
|
||||
|
||||
function renderPerLibrarySettings(context, user, userViews, userSettings) {
|
||||
const elem = context.querySelector('.perLibrarySettings');
|
||||
let html = '';
|
||||
user.Configuration.OrderedViews = orderedViews;
|
||||
|
||||
for (let i = 0, length = userViews.length; i < length; i++) {
|
||||
html += getPerLibrarySettingsHtml(userViews[i], user, userSettings);
|
||||
}
|
||||
userSettingsInstance.set('tvhome', context.querySelector('.selectTVHomeScreen').value);
|
||||
|
||||
elem.innerHTML = html;
|
||||
userSettingsInstance.set('homesection0', context.querySelector('#selectHomeSection1').value);
|
||||
userSettingsInstance.set('homesection1', context.querySelector('#selectHomeSection2').value);
|
||||
userSettingsInstance.set('homesection2', context.querySelector('#selectHomeSection3').value);
|
||||
userSettingsInstance.set('homesection3', context.querySelector('#selectHomeSection4').value);
|
||||
userSettingsInstance.set('homesection4', context.querySelector('#selectHomeSection5').value);
|
||||
userSettingsInstance.set('homesection5', context.querySelector('#selectHomeSection6').value);
|
||||
userSettingsInstance.set('homesection6', context.querySelector('#selectHomeSection7').value);
|
||||
|
||||
const selectLandings = context.querySelectorAll('.selectLanding');
|
||||
for (i = 0, length = selectLandings.length; i < length; i++) {
|
||||
const selectLanding = selectLandings[i];
|
||||
userSettingsInstance.set(`landing-${selectLanding.getAttribute('data-folderid')}`, selectLanding.value);
|
||||
}
|
||||
|
||||
function loadForm(context, user, userSettings, apiClient) {
|
||||
context.querySelector('.chkHidePlayedFromLatest').checked = user.Configuration.HidePlayedInLatest || false;
|
||||
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
|
||||
}
|
||||
|
||||
updateHomeSectionValues(context, userSettings);
|
||||
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
|
||||
loading.show();
|
||||
|
||||
const promise1 = apiClient.getUserViews({ IncludeHidden: true }, user.Id);
|
||||
const promise2 = apiClient.getJSON(apiClient.getUrl(`Users/${user.Id}/GroupingOptions`));
|
||||
|
||||
Promise.all([promise1, promise2]).then(responses => {
|
||||
renderViewOrder(context, user, responses[0]);
|
||||
|
||||
renderPerLibrarySettings(context, user, responses[0].Items, userSettings);
|
||||
|
||||
renderViews(context, user, responses[1]);
|
||||
apiClient.getUser(userId).then(user => {
|
||||
saveUser(context, user, userSettings, apiClient).then(() => {
|
||||
loading.hide();
|
||||
if (enableSaveConfirmation) {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
Events.trigger(instance, 'saved');
|
||||
}, () => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userId = self.options.userId;
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
const enableSaveConfirmation = self.options.enableSaveConfirmation;
|
||||
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
|
||||
});
|
||||
|
||||
// Disable default form submission
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function onChange(e) {
|
||||
const chkIncludeInMyMedia = dom.parentWithClass(e.target, 'chkIncludeInMyMedia');
|
||||
if (!chkIncludeInMyMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
function onSectionOrderListClick(e) {
|
||||
const target = dom.parentWithClass(e.target, 'btnViewItemMove');
|
||||
|
||||
if (target) {
|
||||
const viewItem = dom.parentWithClass(target, 'viewItem');
|
||||
|
||||
if (viewItem) {
|
||||
if (target.classList.contains('btnViewItemDown')) {
|
||||
const next = viewItem.nextSibling;
|
||||
|
||||
if (next) {
|
||||
viewItem.parentNode.removeChild(viewItem);
|
||||
next.parentNode.insertBefore(viewItem, next.nextSibling);
|
||||
focusManager.focus(e.target);
|
||||
}
|
||||
} else {
|
||||
const prev = viewItem.previousSibling;
|
||||
|
||||
if (prev) {
|
||||
viewItem.parentNode.removeChild(viewItem);
|
||||
prev.parentNode.insertBefore(viewItem, prev);
|
||||
focusManager.focus(e.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
const section = dom.parentWithClass(chkIncludeInMyMedia, 'verticalSection');
|
||||
const fldIncludeInLatest = section.querySelector('.fldIncludeInLatest');
|
||||
if (fldIncludeInLatest) {
|
||||
if (chkIncludeInMyMedia.checked) {
|
||||
fldIncludeInLatest.classList.remove('hide');
|
||||
} else {
|
||||
fldIncludeInLatest.classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCheckboxItems(selector, context, isChecked) {
|
||||
const inputs = context.querySelectorAll(selector);
|
||||
const list = [];
|
||||
|
||||
for (let i = 0, length = inputs.length; i < length; i++) {
|
||||
if (inputs[i].checked === isChecked) {
|
||||
list.push(inputs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
function embed(options, self) {
|
||||
let workingTemplate = template;
|
||||
for (let i = 1; i <= numConfigurableSections; i++) {
|
||||
workingTemplate = workingTemplate.replace(`{section${i}label}`, globalize.translate('LabelHomeScreenSectionValue', i));
|
||||
}
|
||||
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
user.Configuration.HidePlayedInLatest = context.querySelector('.chkHidePlayedFromLatest').checked;
|
||||
options.element.innerHTML = globalize.translateHtml(workingTemplate, 'core');
|
||||
|
||||
user.Configuration.LatestItemsExcludes = getCheckboxItems('.chkIncludeInLatest', context, false).map(i => {
|
||||
return i.getAttribute('data-folderid');
|
||||
});
|
||||
options.element.querySelector('.viewOrderList').addEventListener('click', onSectionOrderListClick);
|
||||
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
|
||||
options.element.addEventListener('change', onChange);
|
||||
|
||||
user.Configuration.MyMediaExcludes = getCheckboxItems('.chkIncludeInMyMedia', context, false).map(i => {
|
||||
return i.getAttribute('data-folderid');
|
||||
});
|
||||
|
||||
user.Configuration.GroupedFolders = getCheckboxItems('.chkGroupFolder', context, true).map(i => {
|
||||
return i.getAttribute('data-folderid');
|
||||
});
|
||||
|
||||
const viewItems = context.querySelectorAll('.viewItem');
|
||||
const orderedViews = [];
|
||||
let i;
|
||||
let length;
|
||||
for (i = 0, length = viewItems.length; i < length; i++) {
|
||||
orderedViews.push(viewItems[i].getAttribute('data-viewid'));
|
||||
}
|
||||
|
||||
user.Configuration.OrderedViews = orderedViews;
|
||||
|
||||
userSettingsInstance.set('tvhome', context.querySelector('.selectTVHomeScreen').value);
|
||||
|
||||
userSettingsInstance.set('homesection0', context.querySelector('#selectHomeSection1').value);
|
||||
userSettingsInstance.set('homesection1', context.querySelector('#selectHomeSection2').value);
|
||||
userSettingsInstance.set('homesection2', context.querySelector('#selectHomeSection3').value);
|
||||
userSettingsInstance.set('homesection3', context.querySelector('#selectHomeSection4').value);
|
||||
userSettingsInstance.set('homesection4', context.querySelector('#selectHomeSection5').value);
|
||||
userSettingsInstance.set('homesection5', context.querySelector('#selectHomeSection6').value);
|
||||
userSettingsInstance.set('homesection6', context.querySelector('#selectHomeSection7').value);
|
||||
|
||||
const selectLandings = context.querySelectorAll('.selectLanding');
|
||||
for (i = 0, length = selectLandings.length; i < length; i++) {
|
||||
const selectLanding = selectLandings[i];
|
||||
userSettingsInstance.set(`landing-${selectLanding.getAttribute('data-folderid')}`, selectLanding.value);
|
||||
}
|
||||
|
||||
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
|
||||
if (options.enableSaveButton) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
|
||||
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
|
||||
if (layoutManager.tv) {
|
||||
options.element.querySelector('.selectTVHomeScreenContainer').classList.remove('hide');
|
||||
} else {
|
||||
options.element.querySelector('.selectTVHomeScreenContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
self.loadData(options.autoFocus);
|
||||
}
|
||||
|
||||
class HomeScreenSettings {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options, this);
|
||||
}
|
||||
|
||||
loadData(autoFocus) {
|
||||
const self = this;
|
||||
const context = self.options.element;
|
||||
|
||||
loading.show();
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
saveUser(context, user, userSettings, apiClient).then(() => {
|
||||
loading.hide();
|
||||
if (enableSaveConfirmation) {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
Events.trigger(instance, 'saved');
|
||||
}, () => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userId = self.options.userId;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
const enableSaveConfirmation = self.options.enableSaveConfirmation;
|
||||
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
|
||||
});
|
||||
apiClient.getUser(userId).then(user => {
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
self.dataLoaded = true;
|
||||
|
||||
// Disable default form submission
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
loadForm(context, user, userSettings, apiClient);
|
||||
|
||||
function onChange(e) {
|
||||
const chkIncludeInMyMedia = dom.parentWithClass(e.target, 'chkIncludeInMyMedia');
|
||||
if (!chkIncludeInMyMedia) {
|
||||
return;
|
||||
}
|
||||
|
||||
const section = dom.parentWithClass(chkIncludeInMyMedia, 'verticalSection');
|
||||
const fldIncludeInLatest = section.querySelector('.fldIncludeInLatest');
|
||||
if (fldIncludeInLatest) {
|
||||
if (chkIncludeInMyMedia.checked) {
|
||||
fldIncludeInLatest.classList.remove('hide');
|
||||
} else {
|
||||
fldIncludeInLatest.classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function embed(options, self) {
|
||||
let workingTemplate = template;
|
||||
for (let i = 1; i <= numConfigurableSections; i++) {
|
||||
workingTemplate = workingTemplate.replace(`{section${i}label}`, globalize.translate('LabelHomeScreenSectionValue', i));
|
||||
}
|
||||
|
||||
options.element.innerHTML = globalize.translateHtml(workingTemplate, 'core');
|
||||
|
||||
options.element.querySelector('.viewOrderList').addEventListener('click', onSectionOrderListClick);
|
||||
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
|
||||
options.element.addEventListener('change', onChange);
|
||||
|
||||
if (options.enableSaveButton) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
options.element.querySelector('.selectTVHomeScreenContainer').classList.remove('hide');
|
||||
} else {
|
||||
options.element.querySelector('.selectTVHomeScreenContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
self.loadData(options.autoFocus);
|
||||
}
|
||||
|
||||
class HomeScreenSettings {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options, this);
|
||||
}
|
||||
|
||||
loadData(autoFocus) {
|
||||
const self = this;
|
||||
const context = self.options.element;
|
||||
|
||||
loading.show();
|
||||
|
||||
const userId = self.options.userId;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
self.dataLoaded = true;
|
||||
|
||||
loadForm(context, user, userSettings, apiClient);
|
||||
|
||||
if (autoFocus) {
|
||||
focusManager.autoFocus(context);
|
||||
}
|
||||
});
|
||||
if (autoFocus) {
|
||||
focusManager.autoFocus(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
onSubmit.call(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.options = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
submit() {
|
||||
onSubmit.call(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default HomeScreenSettings;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,395 +1,382 @@
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
import appSettings from '../scripts/settings/appSettings' ;
|
||||
import browser from '../scripts/browser';
|
||||
import Events from '../utils/events.ts';
|
||||
|
||||
export function getSavedVolume() {
|
||||
return appSettings.get('volume') || 1;
|
||||
export function getSavedVolume() {
|
||||
return appSettings.get('volume') || 1;
|
||||
}
|
||||
|
||||
export function saveVolume(value) {
|
||||
if (value) {
|
||||
appSettings.set('volume', value);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCrossOriginValue(mediaSource) {
|
||||
if (mediaSource.IsRemote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function saveVolume(value) {
|
||||
if (value) {
|
||||
appSettings.set('volume', value);
|
||||
}
|
||||
return 'anonymous';
|
||||
}
|
||||
|
||||
function canPlayNativeHls() {
|
||||
const media = document.createElement('video');
|
||||
|
||||
return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '')
|
||||
|| media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, ''));
|
||||
}
|
||||
|
||||
export function enableHlsJsPlayer(runTimeTicks, mediaType) {
|
||||
if (window.MediaSource == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getCrossOriginValue(mediaSource) {
|
||||
if (mediaSource.IsRemote) {
|
||||
return null;
|
||||
// hls.js is only in beta. needs more testing.
|
||||
if (browser.iOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The native players on these devices support seeking live streams, no need to use hls.js here
|
||||
if (browser.tizen || browser.web0s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (canPlayNativeHls()) {
|
||||
// Having trouble with chrome's native support and transcoded music
|
||||
if (browser.android && mediaType === 'Audio') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'anonymous';
|
||||
}
|
||||
|
||||
function canPlayNativeHls() {
|
||||
const media = document.createElement('video');
|
||||
|
||||
return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '') ||
|
||||
media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, ''));
|
||||
}
|
||||
|
||||
export function enableHlsJsPlayer(runTimeTicks, mediaType) {
|
||||
if (window.MediaSource == null) {
|
||||
// simple playback should use the native support
|
||||
if (runTimeTicks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// hls.js is only in beta. needs more testing.
|
||||
if (browser.iOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The native players on these devices support seeking live streams, no need to use hls.js here
|
||||
if (browser.tizen || browser.web0s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (canPlayNativeHls()) {
|
||||
// Having trouble with chrome's native support and transcoded music
|
||||
if (browser.android && mediaType === 'Audio') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (browser.edge && mediaType === 'Video') {
|
||||
//return true;
|
||||
}
|
||||
|
||||
// simple playback should use the native support
|
||||
if (runTimeTicks) {
|
||||
//if (!browser.edge) {
|
||||
return false;
|
||||
//}
|
||||
}
|
||||
|
||||
//return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
let recoverDecodingErrorDate;
|
||||
let recoverSwapAudioCodecDate;
|
||||
export function handleHlsJsMediaError(instance, reject) {
|
||||
const hlsPlayer = instance._hlsPlayer;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!hlsPlayer) {
|
||||
return;
|
||||
}
|
||||
let recoverDecodingErrorDate;
|
||||
let recoverSwapAudioCodecDate;
|
||||
export function handleHlsJsMediaError(instance, reject) {
|
||||
const hlsPlayer = instance._hlsPlayer;
|
||||
|
||||
let now = Date.now();
|
||||
if (!hlsPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.performance && window.performance.now) {
|
||||
now = performance.now(); // eslint-disable-line compat/compat
|
||||
}
|
||||
let now = Date.now();
|
||||
|
||||
if (!recoverDecodingErrorDate || (now - recoverDecodingErrorDate) > 3000) {
|
||||
recoverDecodingErrorDate = now;
|
||||
console.debug('try to recover media Error ...');
|
||||
if (window.performance && window.performance.now) {
|
||||
now = performance.now(); // eslint-disable-line compat/compat
|
||||
}
|
||||
|
||||
if (!recoverDecodingErrorDate || (now - recoverDecodingErrorDate) > 3000) {
|
||||
recoverDecodingErrorDate = now;
|
||||
console.debug('try to recover media Error ...');
|
||||
hlsPlayer.recoverMediaError();
|
||||
} else {
|
||||
if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) {
|
||||
recoverSwapAudioCodecDate = now;
|
||||
console.debug('try to swap Audio Codec and recover media Error ...');
|
||||
hlsPlayer.swapAudioCodec();
|
||||
hlsPlayer.recoverMediaError();
|
||||
} else {
|
||||
if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) {
|
||||
recoverSwapAudioCodecDate = now;
|
||||
console.debug('try to swap Audio Codec and recover media Error ...');
|
||||
hlsPlayer.swapAudioCodec();
|
||||
hlsPlayer.recoverMediaError();
|
||||
console.error('cannot recover, last media error recovery failed ...');
|
||||
|
||||
if (reject) {
|
||||
reject();
|
||||
} else {
|
||||
console.error('cannot recover, last media error recovery failed ...');
|
||||
|
||||
if (reject) {
|
||||
reject();
|
||||
} else {
|
||||
onErrorInternal(instance, 'mediadecodeerror');
|
||||
}
|
||||
onErrorInternal(instance, 'mediadecodeerror');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function onErrorInternal(instance, type) {
|
||||
// Needed for video
|
||||
if (instance.destroyCustomTrack) {
|
||||
instance.destroyCustomTrack(instance._mediaElement);
|
||||
}
|
||||
|
||||
Events.trigger(instance, 'error', [
|
||||
{
|
||||
type: type
|
||||
}
|
||||
]);
|
||||
export function onErrorInternal(instance, type) {
|
||||
// Needed for video
|
||||
if (instance.destroyCustomTrack) {
|
||||
instance.destroyCustomTrack(instance._mediaElement);
|
||||
}
|
||||
|
||||
export function isValidDuration(duration) {
|
||||
return duration
|
||||
Events.trigger(instance, 'error', [
|
||||
{
|
||||
type: type
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
export function isValidDuration(duration) {
|
||||
return duration
|
||||
&& !isNaN(duration)
|
||||
&& duration !== Number.POSITIVE_INFINITY
|
||||
&& duration !== Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
function setCurrentTimeIfNeeded(element, seconds) {
|
||||
// If it's worth skipping (1 sec or less of a difference)
|
||||
if (Math.abs((element.currentTime || 0) - seconds) >= 1) {
|
||||
element.currentTime = seconds;
|
||||
function setCurrentTimeIfNeeded(element, seconds) {
|
||||
// If it's worth skipping (1 sec or less of a difference)
|
||||
if (Math.abs((element.currentTime || 0) - seconds) >= 1) {
|
||||
element.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
export function seekOnPlaybackStart(instance, element, ticks, onMediaReady) {
|
||||
const seconds = (ticks || 0) / 10000000;
|
||||
|
||||
if (seconds) {
|
||||
// Appending #t=xxx to the query string doesn't seem to work with HLS
|
||||
// For plain video files, not all browsers support it either
|
||||
|
||||
if (element.duration >= seconds) {
|
||||
// media is ready, seek immediately
|
||||
setCurrentTimeIfNeeded(element, seconds);
|
||||
if (onMediaReady) onMediaReady();
|
||||
} else {
|
||||
// update video player position when media is ready to be sought
|
||||
const events = ['durationchange', 'loadeddata', 'play', 'loadedmetadata'];
|
||||
const onMediaChange = function(e) {
|
||||
if (element.currentTime === 0 && element.duration >= seconds) {
|
||||
// seek only when video position is exactly zero,
|
||||
// as this is true only if video hasn't started yet or
|
||||
// user rewound to the very beginning
|
||||
// (but rewinding cannot happen as the first event with media of non-empty duration)
|
||||
console.debug(`seeking to ${seconds} on ${e.type} event`);
|
||||
setCurrentTimeIfNeeded(element, seconds);
|
||||
events.forEach(name => {
|
||||
element.removeEventListener(name, onMediaChange);
|
||||
});
|
||||
if (onMediaReady) onMediaReady();
|
||||
}
|
||||
};
|
||||
events.forEach(name => {
|
||||
element.addEventListener(name, onMediaChange);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function seekOnPlaybackStart(instance, element, ticks, onMediaReady) {
|
||||
const seconds = (ticks || 0) / 10000000;
|
||||
export function applySrc(elem, src, options) {
|
||||
if (window.Windows && options.mediaSource && options.mediaSource.IsLocal) {
|
||||
return Windows.Storage.StorageFile.getFileFromPathAsync(options.url).then(function (file) {
|
||||
const playlist = new Windows.Media.Playback.MediaPlaybackList();
|
||||
|
||||
if (seconds) {
|
||||
// Appending #t=xxx to the query string doesn't seem to work with HLS
|
||||
// For plain video files, not all browsers support it either
|
||||
|
||||
if (element.duration >= seconds) {
|
||||
// media is ready, seek immediately
|
||||
setCurrentTimeIfNeeded(element, seconds);
|
||||
if (onMediaReady) onMediaReady();
|
||||
} else {
|
||||
// update video player position when media is ready to be sought
|
||||
const events = ['durationchange', 'loadeddata', 'play', 'loadedmetadata'];
|
||||
const onMediaChange = function(e) {
|
||||
if (element.currentTime === 0 && element.duration >= seconds) {
|
||||
// seek only when video position is exactly zero,
|
||||
// as this is true only if video hasn't started yet or
|
||||
// user rewound to the very beginning
|
||||
// (but rewinding cannot happen as the first event with media of non-empty duration)
|
||||
console.debug(`seeking to ${seconds} on ${e.type} event`);
|
||||
setCurrentTimeIfNeeded(element, seconds);
|
||||
events.forEach(name => {
|
||||
element.removeEventListener(name, onMediaChange);
|
||||
});
|
||||
if (onMediaReady) onMediaReady();
|
||||
}
|
||||
};
|
||||
events.forEach(name => {
|
||||
element.addEventListener(name, onMediaChange);
|
||||
});
|
||||
}
|
||||
}
|
||||
const source1 = Windows.Media.Core.MediaSource.createFromStorageFile(file);
|
||||
const startTime = (options.playerStartPositionTicks || 0) / 10000;
|
||||
playlist.items.append(new Windows.Media.Playback.MediaPlaybackItem(source1, startTime));
|
||||
elem.src = URL.createObjectURL(playlist, { oneTimeOnly: true });
|
||||
return Promise.resolve();
|
||||
});
|
||||
} else {
|
||||
elem.src = src;
|
||||
}
|
||||
|
||||
export function applySrc(elem, src, options) {
|
||||
if (window.Windows && options.mediaSource && options.mediaSource.IsLocal) {
|
||||
return Windows.Storage.StorageFile.getFileFromPathAsync(options.url).then(function (file) {
|
||||
const playlist = new Windows.Media.Playback.MediaPlaybackList();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const source1 = Windows.Media.Core.MediaSource.createFromStorageFile(file);
|
||||
const startTime = (options.playerStartPositionTicks || 0) / 10000;
|
||||
playlist.items.append(new Windows.Media.Playback.MediaPlaybackItem(source1, startTime));
|
||||
elem.src = URL.createObjectURL(playlist, { oneTimeOnly: true });
|
||||
export function resetSrc(elem) {
|
||||
elem.src = '';
|
||||
elem.innerHTML = '';
|
||||
elem.removeAttribute('src');
|
||||
}
|
||||
|
||||
function onSuccessfulPlay(elem, onErrorFn) {
|
||||
elem.addEventListener('error', onErrorFn);
|
||||
}
|
||||
|
||||
export function playWithPromise(elem, onErrorFn) {
|
||||
try {
|
||||
return elem.play()
|
||||
.catch((e) => {
|
||||
const errorName = (e.name || '').toLowerCase();
|
||||
// safari uses aborterror
|
||||
if (errorName === 'notallowederror'
|
||||
|| errorName === 'aborterror') {
|
||||
// swallow this error because the user can still click the play button on the video element
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject();
|
||||
})
|
||||
.then(() => {
|
||||
onSuccessfulPlay(elem, onErrorFn);
|
||||
return Promise.resolve();
|
||||
});
|
||||
} else {
|
||||
elem.src = src;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (err) {
|
||||
console.error('error calling video.play: ' + err);
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
|
||||
export function resetSrc(elem) {
|
||||
elem.src = '';
|
||||
elem.innerHTML = '';
|
||||
elem.removeAttribute('src');
|
||||
}
|
||||
|
||||
function onSuccessfulPlay(elem, onErrorFn) {
|
||||
elem.addEventListener('error', onErrorFn);
|
||||
}
|
||||
|
||||
export function playWithPromise(elem, onErrorFn) {
|
||||
export function destroyCastPlayer(instance) {
|
||||
const player = instance._castPlayer;
|
||||
if (player) {
|
||||
try {
|
||||
return elem.play()
|
||||
.catch((e) => {
|
||||
const errorName = (e.name || '').toLowerCase();
|
||||
// safari uses aborterror
|
||||
if (errorName === 'notallowederror' ||
|
||||
errorName === 'aborterror') {
|
||||
// swallow this error because the user can still click the play button on the video element
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject();
|
||||
})
|
||||
.then(() => {
|
||||
onSuccessfulPlay(elem, onErrorFn);
|
||||
return Promise.resolve();
|
||||
});
|
||||
player.unload();
|
||||
} catch (err) {
|
||||
console.error('error calling video.play: ' + err);
|
||||
return Promise.reject();
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyCastPlayer(instance) {
|
||||
const player = instance._castPlayer;
|
||||
if (player) {
|
||||
try {
|
||||
player.unload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
instance._castPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyHlsPlayer(instance) {
|
||||
const player = instance._hlsPlayer;
|
||||
if (player) {
|
||||
try {
|
||||
player.destroy();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
instance._hlsPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyFlvPlayer(instance) {
|
||||
const player = instance._flvPlayer;
|
||||
if (player) {
|
||||
try {
|
||||
player.unload();
|
||||
player.detachMediaElement();
|
||||
player.destroy();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
instance._flvPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, reject) {
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||
playWithPromise(elem, onErrorFn).then(resolve, function () {
|
||||
if (reject) {
|
||||
reject();
|
||||
reject = null;
|
||||
}
|
||||
|
||||
instance._castPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyHlsPlayer(instance) {
|
||||
const player = instance._hlsPlayer;
|
||||
if (player) {
|
||||
try {
|
||||
player.destroy();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
instance._hlsPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function destroyFlvPlayer(instance) {
|
||||
const player = instance._flvPlayer;
|
||||
if (player) {
|
||||
try {
|
||||
player.unload();
|
||||
player.detachMediaElement();
|
||||
player.destroy();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
instance._flvPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, reject) {
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||
playWithPromise(elem, onErrorFn).then(resolve, function () {
|
||||
if (reject) {
|
||||
reject();
|
||||
reject = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||
console.error('HLS Error: Type: ' + data.type + ' Details: ' + (data.details || '') + ' Fatal: ' + (data.fatal || false));
|
||||
hls.on(Hls.Events.ERROR, function (event, data) {
|
||||
console.error('HLS Error: Type: ' + data.type + ' Details: ' + (data.details || '') + ' Fatal: ' + (data.fatal || false));
|
||||
|
||||
// try to recover network error
|
||||
if (data.type === Hls.ErrorTypes.NETWORK_ERROR
|
||||
// try to recover network error
|
||||
if (data.type === Hls.ErrorTypes.NETWORK_ERROR
|
||||
&& data.response?.code && data.response.code >= 400
|
||||
) {
|
||||
console.debug('hls.js response error code: ' + data.response.code);
|
||||
) {
|
||||
console.debug('hls.js response error code: ' + data.response.code);
|
||||
|
||||
// Trigger failure differently depending on whether this is prior to start of playback, or after
|
||||
hls.destroy();
|
||||
// Trigger failure differently depending on whether this is prior to start of playback, or after
|
||||
hls.destroy();
|
||||
|
||||
if (reject) {
|
||||
reject('servererror');
|
||||
reject = null;
|
||||
} else {
|
||||
onErrorInternal(instance, 'servererror');
|
||||
}
|
||||
|
||||
return;
|
||||
if (reject) {
|
||||
reject('servererror');
|
||||
reject = null;
|
||||
} else {
|
||||
onErrorInternal(instance, 'servererror');
|
||||
}
|
||||
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.response && data.response.code === 0) {
|
||||
// This could be a CORS error related to access control response headers
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
|
||||
console.debug('hls.js response error code: ' + data.response.code);
|
||||
if (data.response && data.response.code === 0) {
|
||||
// This could be a CORS error related to access control response headers
|
||||
|
||||
// Trigger failure differently depending on whether this is prior to start of playback, or after
|
||||
hls.destroy();
|
||||
console.debug('hls.js response error code: ' + data.response.code);
|
||||
|
||||
if (reject) {
|
||||
reject('network');
|
||||
reject = null;
|
||||
} else {
|
||||
onErrorInternal(instance, 'network');
|
||||
}
|
||||
} else {
|
||||
console.debug('fatal network error encountered, try to recover');
|
||||
hls.startLoad();
|
||||
}
|
||||
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.debug('fatal media error encountered, try to recover');
|
||||
handleHlsJsMediaError(instance, reject);
|
||||
reject = null;
|
||||
break;
|
||||
default:
|
||||
|
||||
console.debug('Cannot recover from hls error - destroy and trigger error');
|
||||
// cannot recover
|
||||
// Trigger failure differently depending on whether this is prior to start of playback, or after
|
||||
hls.destroy();
|
||||
|
||||
if (reject) {
|
||||
reject();
|
||||
reject('network');
|
||||
reject = null;
|
||||
} else {
|
||||
onErrorInternal(instance, 'mediadecodeerror');
|
||||
onErrorInternal(instance, 'network');
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
console.debug('fatal network error encountered, try to recover');
|
||||
hls.startLoad();
|
||||
}
|
||||
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
console.debug('fatal media error encountered, try to recover');
|
||||
handleHlsJsMediaError(instance, reject);
|
||||
reject = null;
|
||||
break;
|
||||
default:
|
||||
|
||||
console.debug('Cannot recover from hls error - destroy and trigger error');
|
||||
// cannot recover
|
||||
// Trigger failure differently depending on whether this is prior to start of playback, or after
|
||||
hls.destroy();
|
||||
|
||||
if (reject) {
|
||||
reject();
|
||||
reject = null;
|
||||
} else {
|
||||
onErrorInternal(instance, 'mediadecodeerror');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function onEndedInternal(instance, elem, onErrorFn) {
|
||||
elem.removeEventListener('error', onErrorFn);
|
||||
|
||||
resetSrc(elem);
|
||||
|
||||
destroyHlsPlayer(instance);
|
||||
destroyFlvPlayer(instance);
|
||||
destroyCastPlayer(instance);
|
||||
|
||||
const stopInfo = {
|
||||
src: instance._currentSrc
|
||||
};
|
||||
|
||||
Events.trigger(instance, 'stopped', [stopInfo]);
|
||||
|
||||
instance._currentTime = null;
|
||||
instance._currentSrc = null;
|
||||
instance._currentPlayOptions = null;
|
||||
}
|
||||
|
||||
export function getBufferedRanges(instance, elem) {
|
||||
const ranges = [];
|
||||
const seekable = elem.buffered || [];
|
||||
|
||||
let offset;
|
||||
const currentPlayOptions = instance._currentPlayOptions;
|
||||
if (currentPlayOptions) {
|
||||
offset = currentPlayOptions.transcodingOffsetTicks;
|
||||
}
|
||||
|
||||
offset = offset || 0;
|
||||
|
||||
for (let i = 0, length = seekable.length; i < length; i++) {
|
||||
let start = seekable.start(i);
|
||||
let end = seekable.end(i);
|
||||
|
||||
if (!isValidDuration(start)) {
|
||||
start = 0;
|
||||
}
|
||||
if (!isValidDuration(end)) {
|
||||
end = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
ranges.push({
|
||||
start: (start * 10000000) + offset,
|
||||
end: (end * 10000000) + offset
|
||||
});
|
||||
}
|
||||
|
||||
export function onEndedInternal(instance, elem, onErrorFn) {
|
||||
elem.removeEventListener('error', onErrorFn);
|
||||
|
||||
resetSrc(elem);
|
||||
|
||||
destroyHlsPlayer(instance);
|
||||
destroyFlvPlayer(instance);
|
||||
destroyCastPlayer(instance);
|
||||
|
||||
const stopInfo = {
|
||||
src: instance._currentSrc
|
||||
};
|
||||
|
||||
Events.trigger(instance, 'stopped', [stopInfo]);
|
||||
|
||||
instance._currentTime = null;
|
||||
instance._currentSrc = null;
|
||||
instance._currentPlayOptions = null;
|
||||
}
|
||||
|
||||
export function getBufferedRanges(instance, elem) {
|
||||
const ranges = [];
|
||||
const seekable = elem.buffered || [];
|
||||
|
||||
let offset;
|
||||
const currentPlayOptions = instance._currentPlayOptions;
|
||||
if (currentPlayOptions) {
|
||||
offset = currentPlayOptions.transcodingOffsetTicks;
|
||||
}
|
||||
|
||||
offset = offset || 0;
|
||||
|
||||
for (let i = 0, length = seekable.length; i < length; i++) {
|
||||
let start = seekable.start(i);
|
||||
let end = seekable.end(i);
|
||||
|
||||
if (!isValidDuration(start)) {
|
||||
start = 0;
|
||||
}
|
||||
if (!isValidDuration(end)) {
|
||||
end = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
ranges.push({
|
||||
start: (start * 10000000) + offset,
|
||||
end: (end * 10000000) + offset
|
||||
});
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
return ranges;
|
||||
}
|
||||
|
||||
@@ -15,371 +15,369 @@ import '../cardbuilder/card.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import template from './imageDownloader.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
let currentItemId;
|
||||
let currentItemType;
|
||||
let currentResolve;
|
||||
let currentReject;
|
||||
let hasChanges = false;
|
||||
|
||||
let currentItemId;
|
||||
let currentItemType;
|
||||
let currentResolve;
|
||||
let currentReject;
|
||||
let hasChanges = false;
|
||||
// These images can be large and we're seeing memory problems in safari
|
||||
const browsableImagePageSize = browser.slow ? 6 : 30;
|
||||
|
||||
// These images can be large and we're seeing memory problems in safari
|
||||
const browsableImagePageSize = browser.slow ? 6 : 30;
|
||||
let browsableImageStartIndex = 0;
|
||||
let browsableImageType = 'Primary';
|
||||
let selectedProvider;
|
||||
let browsableParentId;
|
||||
|
||||
let browsableImageStartIndex = 0;
|
||||
let browsableImageType = 'Primary';
|
||||
let selectedProvider;
|
||||
let browsableParentId;
|
||||
function getBaseRemoteOptions(page, forceCurrentItemId = false) {
|
||||
const options = {};
|
||||
|
||||
function getBaseRemoteOptions(page, forceCurrentItemId = false) {
|
||||
const options = {};
|
||||
|
||||
if (!forceCurrentItemId && page.querySelector('#chkShowParentImages').checked && browsableParentId) {
|
||||
options.itemId = browsableParentId;
|
||||
} else {
|
||||
options.itemId = currentItemId;
|
||||
}
|
||||
|
||||
return options;
|
||||
if (!forceCurrentItemId && page.querySelector('#chkShowParentImages').checked && browsableParentId) {
|
||||
options.itemId = browsableParentId;
|
||||
} else {
|
||||
options.itemId = currentItemId;
|
||||
}
|
||||
|
||||
function reloadBrowsableImages(page, apiClient) {
|
||||
loading.show();
|
||||
return options;
|
||||
}
|
||||
|
||||
const options = getBaseRemoteOptions(page);
|
||||
function reloadBrowsableImages(page, apiClient) {
|
||||
loading.show();
|
||||
|
||||
options.type = browsableImageType;
|
||||
options.startIndex = browsableImageStartIndex;
|
||||
options.limit = browsableImagePageSize;
|
||||
options.IncludeAllLanguages = page.querySelector('#chkAllLanguages').checked;
|
||||
const options = getBaseRemoteOptions(page);
|
||||
|
||||
const provider = selectedProvider || '';
|
||||
options.type = browsableImageType;
|
||||
options.startIndex = browsableImageStartIndex;
|
||||
options.limit = browsableImagePageSize;
|
||||
options.IncludeAllLanguages = page.querySelector('#chkAllLanguages').checked;
|
||||
|
||||
if (provider) {
|
||||
options.ProviderName = provider;
|
||||
}
|
||||
const provider = selectedProvider || '';
|
||||
|
||||
apiClient.getAvailableRemoteImages(options).then(function (result) {
|
||||
renderRemoteImages(page, apiClient, result, browsableImageType, options.startIndex, options.limit);
|
||||
|
||||
page.querySelector('#selectBrowsableImageType').value = browsableImageType;
|
||||
|
||||
const providersHtml = result.Providers.map(function (p) {
|
||||
return '<option value="' + p + '">' + p + '</option>';
|
||||
});
|
||||
|
||||
const selectImageProvider = page.querySelector('#selectImageProvider');
|
||||
selectImageProvider.innerHTML = '<option value="">' + globalize.translate('All') + '</option>' + providersHtml;
|
||||
selectImageProvider.value = provider;
|
||||
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function renderRemoteImages(page, apiClient, imagesResult, imageType, startIndex, limit) {
|
||||
page.querySelector('.availableImagesPaging').innerHTML = getPagingHtml(startIndex, limit, imagesResult.TotalRecordCount);
|
||||
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = imagesResult.Images.length; i < length; i++) {
|
||||
html += getRemoteImageHtml(imagesResult.Images[i], imageType);
|
||||
}
|
||||
|
||||
const availableImagesList = page.querySelector('.availableImagesList');
|
||||
availableImagesList.innerHTML = html;
|
||||
imageLoader.lazyChildren(availableImagesList);
|
||||
|
||||
const btnNextPage = page.querySelector('.btnNextPage');
|
||||
const btnPreviousPage = page.querySelector('.btnPreviousPage');
|
||||
|
||||
if (btnNextPage) {
|
||||
btnNextPage.addEventListener('click', function () {
|
||||
browsableImageStartIndex += browsableImagePageSize;
|
||||
reloadBrowsableImages(page, apiClient);
|
||||
});
|
||||
}
|
||||
|
||||
if (btnPreviousPage) {
|
||||
btnPreviousPage.addEventListener('click', function () {
|
||||
browsableImageStartIndex -= browsableImagePageSize;
|
||||
reloadBrowsableImages(page, apiClient);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getPagingHtml(startIndex, limit, totalRecordCount) {
|
||||
let html = '';
|
||||
|
||||
const recordsEnd = Math.min(startIndex + limit, totalRecordCount);
|
||||
|
||||
// 20 is the minimum page size
|
||||
const showControls = totalRecordCount > limit;
|
||||
|
||||
html += '<div class="listPaging">';
|
||||
|
||||
html += '<span style="margin-right: 10px;">';
|
||||
|
||||
const startAtDisplay = totalRecordCount ? startIndex + 1 : 0;
|
||||
html += globalize.translate('ListPaging', startAtDisplay, recordsEnd, totalRecordCount);
|
||||
|
||||
html += '</span>';
|
||||
|
||||
if (showControls) {
|
||||
html += '<div data-role="controlgroup" data-type="horizontal" style="display:inline-block;">';
|
||||
|
||||
html += `<button is="paper-icon-button-light" title="${globalize.translate('Previous')}" class="btnPreviousPage autoSize" ${(startIndex ? '' : 'disabled')}><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += `<button is="paper-icon-button-light" title="${globalize.translate('Next')}" class="btnNextPage autoSize" ${(startIndex + limit >= totalRecordCount ? 'disabled' : '')}><span class="material-icons arrow_forward" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function downloadRemoteImage(page, apiClient, url, type, provider) {
|
||||
const options = getBaseRemoteOptions(page, true);
|
||||
|
||||
options.Type = type;
|
||||
options.ImageUrl = url;
|
||||
if (provider) {
|
||||
options.ProviderName = provider;
|
||||
|
||||
loading.show();
|
||||
|
||||
apiClient.downloadRemoteImage(options).then(function () {
|
||||
hasChanges = true;
|
||||
const dlg = dom.parentWithClass(page, 'dialog');
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
|
||||
function getRemoteImageHtml(image, imageType) {
|
||||
const tagName = layoutManager.tv ? 'button' : 'div';
|
||||
const enableFooterButtons = !layoutManager.tv;
|
||||
apiClient.getAvailableRemoteImages(options).then(function (result) {
|
||||
renderRemoteImages(page, apiClient, result, browsableImageType, options.startIndex, options.limit);
|
||||
|
||||
// TODO move card creation code to Card component
|
||||
page.querySelector('#selectBrowsableImageType').value = browsableImageType;
|
||||
|
||||
let html = '';
|
||||
|
||||
let cssClass = 'card scalableCard imageEditorCard';
|
||||
const cardBoxCssClass = 'cardBox visualCardBox';
|
||||
|
||||
let shape;
|
||||
if (imageType === 'Backdrop' || imageType === 'Art' || imageType === 'Thumb' || imageType === 'Logo') {
|
||||
shape = 'backdrop';
|
||||
} else if (imageType === 'Banner') {
|
||||
shape = 'banner';
|
||||
} else if (imageType === 'Disc') {
|
||||
shape = 'square';
|
||||
} else {
|
||||
if (currentItemType === 'Episode') {
|
||||
shape = 'backdrop';
|
||||
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
|
||||
shape = 'square';
|
||||
} else {
|
||||
shape = 'portrait';
|
||||
}
|
||||
}
|
||||
|
||||
cssClass += ' ' + shape + 'Card ' + shape + 'Card-scalable';
|
||||
if (tagName === 'button') {
|
||||
cssClass += ' btnImageCard';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' show-focus';
|
||||
|
||||
if (enableFocusTransform) {
|
||||
cssClass += ' show-animation';
|
||||
}
|
||||
}
|
||||
|
||||
html += '<button type="button" class="' + cssClass + '"';
|
||||
} else {
|
||||
html += '<div class="' + cssClass + '"';
|
||||
}
|
||||
|
||||
html += ' data-imageprovider="' + image.ProviderName + '" data-imageurl="' + image.Url + '" data-imagetype="' + image.Type + '"';
|
||||
|
||||
html += '>';
|
||||
|
||||
html += '<div class="' + cardBoxCssClass + '">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable" style="background-color:transparent;">';
|
||||
html += '<div class="cardPadder-' + shape + '"></div>';
|
||||
html += '<div class="cardContent">';
|
||||
|
||||
if (layoutManager.tv || !appHost.supports('externallinks')) {
|
||||
html += '<div class="cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain;"></div>';
|
||||
} else {
|
||||
html += '<a is="emby-linkbutton" target="_blank" href="' + image.Url + '" class="button-link cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain"></a>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
// begin footer
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">';
|
||||
|
||||
html += '<div class="cardText cardTextCentered">' + image.ProviderName + '</div>';
|
||||
|
||||
if (image.Width || image.Height || image.Language) {
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
|
||||
if (image.Width && image.Height) {
|
||||
html += image.Width + ' x ' + image.Height;
|
||||
|
||||
if (image.Language) {
|
||||
html += ' • ' + image.Language;
|
||||
}
|
||||
} else {
|
||||
if (image.Language) {
|
||||
html += image.Language;
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (image.CommunityRating != null) {
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
|
||||
if (image.RatingType === 'Likes') {
|
||||
html += image.CommunityRating + (image.CommunityRating === 1 ? ' like' : ' likes');
|
||||
} else {
|
||||
if (image.CommunityRating) {
|
||||
html += image.CommunityRating.toFixed(1);
|
||||
|
||||
if (image.VoteCount) {
|
||||
html += ' • ' + image.VoteCount + (image.VoteCount === 1 ? ' vote' : ' votes');
|
||||
}
|
||||
} else {
|
||||
html += 'Unrated';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (enableFooterButtons) {
|
||||
html += '<div class="cardText cardTextCentered">';
|
||||
|
||||
html += `<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize" raised" title="${globalize.translate('Download')}"><span class="material-icons cloud_download" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
// end footer
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += '</' + tagName + '>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function reloadBrowsableImagesFirstPage(page, apiClient) {
|
||||
browsableImageStartIndex = 0;
|
||||
reloadBrowsableImages(page, apiClient);
|
||||
}
|
||||
|
||||
function initEditor(page, apiClient) {
|
||||
page.querySelector('#selectBrowsableImageType').addEventListener('change', function () {
|
||||
browsableImageType = this.value;
|
||||
selectedProvider = null;
|
||||
|
||||
reloadBrowsableImagesFirstPage(page, apiClient);
|
||||
const providersHtml = result.Providers.map(function (p) {
|
||||
return '<option value="' + p + '">' + p + '</option>';
|
||||
});
|
||||
|
||||
page.querySelector('#selectImageProvider').addEventListener('change', function () {
|
||||
selectedProvider = this.value;
|
||||
|
||||
reloadBrowsableImagesFirstPage(page, apiClient);
|
||||
});
|
||||
|
||||
page.querySelector('#chkAllLanguages').addEventListener('change', function () {
|
||||
reloadBrowsableImagesFirstPage(page, apiClient);
|
||||
});
|
||||
|
||||
page.querySelector('#chkShowParentImages').addEventListener('change', function () {
|
||||
reloadBrowsableImagesFirstPage(page, apiClient);
|
||||
});
|
||||
|
||||
page.addEventListener('click', function (e) {
|
||||
const btnDownloadRemoteImage = dom.parentWithClass(e.target, 'btnDownloadRemoteImage');
|
||||
if (btnDownloadRemoteImage) {
|
||||
const card = dom.parentWithClass(btnDownloadRemoteImage, 'card');
|
||||
downloadRemoteImage(page, apiClient, card.getAttribute('data-imageurl'), card.getAttribute('data-imagetype'), card.getAttribute('data-imageprovider'));
|
||||
return;
|
||||
}
|
||||
|
||||
const btnImageCard = dom.parentWithClass(e.target, 'btnImageCard');
|
||||
if (btnImageCard) {
|
||||
downloadRemoteImage(page, apiClient, btnImageCard.getAttribute('data-imageurl'), btnImageCard.getAttribute('data-imagetype'), btnImageCard.getAttribute('data-imageprovider'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor(itemId, serverId, itemType) {
|
||||
loading.show();
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
currentItemId = itemId;
|
||||
currentItemType = itemType;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg, false);
|
||||
}
|
||||
|
||||
if (browsableParentId) {
|
||||
dlg.querySelector('#lblShowParentImages').classList.remove('hide');
|
||||
}
|
||||
|
||||
// Has to be assigned a z-index after the call to .open()
|
||||
dlg.addEventListener('close', onDialogClosed);
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
const editorContent = dlg.querySelector('.formDialogContent');
|
||||
initEditor(editorContent, apiClient);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
reloadBrowsableImages(editorContent, apiClient);
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
const dlg = this;
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.off(dlg, false);
|
||||
}
|
||||
const selectImageProvider = page.querySelector('#selectImageProvider');
|
||||
selectImageProvider.innerHTML = '<option value="">' + globalize.translate('All') + '</option>' + providersHtml;
|
||||
selectImageProvider.value = provider;
|
||||
|
||||
loading.hide();
|
||||
if (hasChanges) {
|
||||
currentResolve();
|
||||
});
|
||||
}
|
||||
|
||||
function renderRemoteImages(page, apiClient, imagesResult, imageType, startIndex, limit) {
|
||||
page.querySelector('.availableImagesPaging').innerHTML = getPagingHtml(startIndex, limit, imagesResult.TotalRecordCount);
|
||||
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = imagesResult.Images.length; i < length; i++) {
|
||||
html += getRemoteImageHtml(imagesResult.Images[i], imageType);
|
||||
}
|
||||
|
||||
const availableImagesList = page.querySelector('.availableImagesList');
|
||||
availableImagesList.innerHTML = html;
|
||||
imageLoader.lazyChildren(availableImagesList);
|
||||
|
||||
const btnNextPage = page.querySelector('.btnNextPage');
|
||||
const btnPreviousPage = page.querySelector('.btnPreviousPage');
|
||||
|
||||
if (btnNextPage) {
|
||||
btnNextPage.addEventListener('click', function () {
|
||||
browsableImageStartIndex += browsableImagePageSize;
|
||||
reloadBrowsableImages(page, apiClient);
|
||||
});
|
||||
}
|
||||
|
||||
if (btnPreviousPage) {
|
||||
btnPreviousPage.addEventListener('click', function () {
|
||||
browsableImageStartIndex -= browsableImagePageSize;
|
||||
reloadBrowsableImages(page, apiClient);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getPagingHtml(startIndex, limit, totalRecordCount) {
|
||||
let html = '';
|
||||
|
||||
const recordsEnd = Math.min(startIndex + limit, totalRecordCount);
|
||||
|
||||
// 20 is the minimum page size
|
||||
const showControls = totalRecordCount > limit;
|
||||
|
||||
html += '<div class="listPaging">';
|
||||
|
||||
html += '<span style="margin-right: 10px;">';
|
||||
|
||||
const startAtDisplay = totalRecordCount ? startIndex + 1 : 0;
|
||||
html += globalize.translate('ListPaging', startAtDisplay, recordsEnd, totalRecordCount);
|
||||
|
||||
html += '</span>';
|
||||
|
||||
if (showControls) {
|
||||
html += '<div data-role="controlgroup" data-type="horizontal" style="display:inline-block;">';
|
||||
|
||||
html += `<button is="paper-icon-button-light" title="${globalize.translate('Previous')}" class="btnPreviousPage autoSize" ${(startIndex ? '' : 'disabled')}><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += `<button is="paper-icon-button-light" title="${globalize.translate('Next')}" class="btnNextPage autoSize" ${(startIndex + limit >= totalRecordCount ? 'disabled' : '')}><span class="material-icons arrow_forward" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function downloadRemoteImage(page, apiClient, url, type, provider) {
|
||||
const options = getBaseRemoteOptions(page, true);
|
||||
|
||||
options.Type = type;
|
||||
options.ImageUrl = url;
|
||||
options.ProviderName = provider;
|
||||
|
||||
loading.show();
|
||||
|
||||
apiClient.downloadRemoteImage(options).then(function () {
|
||||
hasChanges = true;
|
||||
const dlg = dom.parentWithClass(page, 'dialog');
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
|
||||
function getRemoteImageHtml(image, imageType) {
|
||||
const tagName = layoutManager.tv ? 'button' : 'div';
|
||||
const enableFooterButtons = !layoutManager.tv;
|
||||
|
||||
// TODO move card creation code to Card component
|
||||
|
||||
let html = '';
|
||||
|
||||
let cssClass = 'card scalableCard imageEditorCard';
|
||||
const cardBoxCssClass = 'cardBox visualCardBox';
|
||||
|
||||
let shape;
|
||||
if (imageType === 'Backdrop' || imageType === 'Art' || imageType === 'Thumb' || imageType === 'Logo') {
|
||||
shape = 'backdrop';
|
||||
} else if (imageType === 'Banner') {
|
||||
shape = 'banner';
|
||||
} else if (imageType === 'Disc') {
|
||||
shape = 'square';
|
||||
} else {
|
||||
if (currentItemType === 'Episode') {
|
||||
shape = 'backdrop';
|
||||
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
|
||||
shape = 'square';
|
||||
} else {
|
||||
currentReject();
|
||||
shape = 'portrait';
|
||||
}
|
||||
}
|
||||
|
||||
cssClass += ' ' + shape + 'Card ' + shape + 'Card-scalable';
|
||||
if (tagName === 'button') {
|
||||
cssClass += ' btnImageCard';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' show-focus';
|
||||
|
||||
if (enableFocusTransform) {
|
||||
cssClass += ' show-animation';
|
||||
}
|
||||
}
|
||||
|
||||
html += '<button type="button" class="' + cssClass + '"';
|
||||
} else {
|
||||
html += '<div class="' + cssClass + '"';
|
||||
}
|
||||
|
||||
html += ' data-imageprovider="' + image.ProviderName + '" data-imageurl="' + image.Url + '" data-imagetype="' + image.Type + '"';
|
||||
|
||||
html += '>';
|
||||
|
||||
html += '<div class="' + cardBoxCssClass + '">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable" style="background-color:transparent;">';
|
||||
html += '<div class="cardPadder-' + shape + '"></div>';
|
||||
html += '<div class="cardContent">';
|
||||
|
||||
if (layoutManager.tv || !appHost.supports('externallinks')) {
|
||||
html += '<div class="cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain;"></div>';
|
||||
} else {
|
||||
html += '<a is="emby-linkbutton" target="_blank" href="' + image.Url + '" class="button-link cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain"></a>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
// begin footer
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">';
|
||||
|
||||
html += '<div class="cardText cardTextCentered">' + image.ProviderName + '</div>';
|
||||
|
||||
if (image.Width || image.Height || image.Language) {
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
|
||||
if (image.Width && image.Height) {
|
||||
html += image.Width + ' x ' + image.Height;
|
||||
|
||||
if (image.Language) {
|
||||
html += ' • ' + image.Language;
|
||||
}
|
||||
} else {
|
||||
if (image.Language) {
|
||||
html += image.Language;
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (image.CommunityRating != null) {
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
|
||||
if (image.RatingType === 'Likes') {
|
||||
html += image.CommunityRating + (image.CommunityRating === 1 ? ' like' : ' likes');
|
||||
} else {
|
||||
if (image.CommunityRating) {
|
||||
html += image.CommunityRating.toFixed(1);
|
||||
|
||||
if (image.VoteCount) {
|
||||
html += ' • ' + image.VoteCount + (image.VoteCount === 1 ? ' vote' : ' votes');
|
||||
}
|
||||
} else {
|
||||
html += 'Unrated';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (enableFooterButtons) {
|
||||
html += '<div class="cardText cardTextCentered">';
|
||||
|
||||
html += `<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize" raised" title="${globalize.translate('Download')}"><span class="material-icons cloud_download" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
// end footer
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += '</' + tagName + '>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function reloadBrowsableImagesFirstPage(page, apiClient) {
|
||||
browsableImageStartIndex = 0;
|
||||
reloadBrowsableImages(page, apiClient);
|
||||
}
|
||||
|
||||
function initEditor(page, apiClient) {
|
||||
page.querySelector('#selectBrowsableImageType').addEventListener('change', function () {
|
||||
browsableImageType = this.value;
|
||||
selectedProvider = null;
|
||||
|
||||
reloadBrowsableImagesFirstPage(page, apiClient);
|
||||
});
|
||||
|
||||
page.querySelector('#selectImageProvider').addEventListener('change', function () {
|
||||
selectedProvider = this.value;
|
||||
|
||||
reloadBrowsableImagesFirstPage(page, apiClient);
|
||||
});
|
||||
|
||||
page.querySelector('#chkAllLanguages').addEventListener('change', function () {
|
||||
reloadBrowsableImagesFirstPage(page, apiClient);
|
||||
});
|
||||
|
||||
page.querySelector('#chkShowParentImages').addEventListener('change', function () {
|
||||
reloadBrowsableImagesFirstPage(page, apiClient);
|
||||
});
|
||||
|
||||
page.addEventListener('click', function (e) {
|
||||
const btnDownloadRemoteImage = dom.parentWithClass(e.target, 'btnDownloadRemoteImage');
|
||||
if (btnDownloadRemoteImage) {
|
||||
const card = dom.parentWithClass(btnDownloadRemoteImage, 'card');
|
||||
downloadRemoteImage(page, apiClient, card.getAttribute('data-imageurl'), card.getAttribute('data-imagetype'), card.getAttribute('data-imageprovider'));
|
||||
return;
|
||||
}
|
||||
|
||||
const btnImageCard = dom.parentWithClass(e.target, 'btnImageCard');
|
||||
if (btnImageCard) {
|
||||
downloadRemoteImage(page, apiClient, btnImageCard.getAttribute('data-imageurl'), btnImageCard.getAttribute('data-imagetype'), btnImageCard.getAttribute('data-imageprovider'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor(itemId, serverId, itemType) {
|
||||
loading.show();
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
currentItemId = itemId;
|
||||
currentItemType = itemType;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg, false);
|
||||
}
|
||||
|
||||
if (browsableParentId) {
|
||||
dlg.querySelector('#lblShowParentImages').classList.remove('hide');
|
||||
}
|
||||
|
||||
// Has to be assigned a z-index after the call to .open()
|
||||
dlg.addEventListener('close', onDialogClosed);
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
const editorContent = dlg.querySelector('.formDialogContent');
|
||||
initEditor(editorContent, apiClient);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
reloadBrowsableImages(editorContent, apiClient);
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
const dlg = this;
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.off(dlg, false);
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
if (hasChanges) {
|
||||
currentResolve();
|
||||
} else {
|
||||
currentReject();
|
||||
}
|
||||
}
|
||||
|
||||
export function show(itemId, serverId, itemType, imageType, parentId) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
currentResolve = resolve;
|
||||
@@ -397,4 +395,3 @@ export default {
|
||||
show: show
|
||||
};
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for image Options Editor.
|
||||
@@ -13,95 +12,95 @@ import '../../elements/emby-select/emby-select';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import template from './imageOptionsEditor.template.html';
|
||||
|
||||
function getDefaultImageConfig(itemType, type) {
|
||||
return {
|
||||
Type: type,
|
||||
MinWidth: 0,
|
||||
Limit: type === 'Primary' ? 1 : 0
|
||||
};
|
||||
}
|
||||
function getDefaultImageConfig(itemType, type) {
|
||||
return {
|
||||
Type: type,
|
||||
MinWidth: 0,
|
||||
Limit: type === 'Primary' ? 1 : 0
|
||||
};
|
||||
}
|
||||
|
||||
function findImageOptions(imageOptions, type) {
|
||||
return imageOptions.filter(i => {
|
||||
return i.Type == type;
|
||||
})[0];
|
||||
}
|
||||
function findImageOptions(imageOptions, type) {
|
||||
return imageOptions.filter(i => {
|
||||
return i.Type == type;
|
||||
})[0];
|
||||
}
|
||||
|
||||
function getImageConfig(options, availableOptions, imageType, itemType) {
|
||||
return findImageOptions(options.ImageOptions || [], imageType) || findImageOptions(availableOptions.DefaultImageOptions || [], imageType) || getDefaultImageConfig(itemType, imageType);
|
||||
}
|
||||
function getImageConfig(options, availableOptions, imageType, itemType) {
|
||||
return findImageOptions(options.ImageOptions || [], imageType) || findImageOptions(availableOptions.DefaultImageOptions || [], imageType) || getDefaultImageConfig(itemType, imageType);
|
||||
}
|
||||
|
||||
function setVisibilityOfBackdrops(elem, visible) {
|
||||
if (visible) {
|
||||
elem.classList.remove('hide');
|
||||
elem.querySelector('input').setAttribute('required', 'required');
|
||||
function setVisibilityOfBackdrops(elem, visible) {
|
||||
if (visible) {
|
||||
elem.classList.remove('hide');
|
||||
elem.querySelector('input').setAttribute('required', 'required');
|
||||
} else {
|
||||
elem.classList.add('hide');
|
||||
elem.querySelector('input').setAttribute('required', '');
|
||||
elem.querySelector('input').removeAttribute('required');
|
||||
}
|
||||
}
|
||||
|
||||
function loadValues(context, itemType, options, availableOptions) {
|
||||
const supportedImageTypes = availableOptions.SupportedImageTypes || [];
|
||||
setVisibilityOfBackdrops(context.querySelector('.backdropFields'), supportedImageTypes.includes('Backdrop'));
|
||||
Array.prototype.forEach.call(context.querySelectorAll('.imageType'), i => {
|
||||
const imageType = i.getAttribute('data-imagetype');
|
||||
const container = dom.parentWithTag(i, 'LABEL');
|
||||
|
||||
if (!supportedImageTypes.includes(imageType)) {
|
||||
container.classList.add('hide');
|
||||
} else {
|
||||
elem.classList.add('hide');
|
||||
elem.querySelector('input').setAttribute('required', '');
|
||||
elem.querySelector('input').removeAttribute('required');
|
||||
container.classList.remove('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function loadValues(context, itemType, options, availableOptions) {
|
||||
const supportedImageTypes = availableOptions.SupportedImageTypes || [];
|
||||
setVisibilityOfBackdrops(context.querySelector('.backdropFields'), supportedImageTypes.includes('Backdrop'));
|
||||
Array.prototype.forEach.call(context.querySelectorAll('.imageType'), i => {
|
||||
const imageType = i.getAttribute('data-imagetype');
|
||||
const container = dom.parentWithTag(i, 'LABEL');
|
||||
if (getImageConfig(options, availableOptions, imageType, itemType).Limit) {
|
||||
i.checked = true;
|
||||
} else {
|
||||
i.checked = false;
|
||||
}
|
||||
});
|
||||
const backdropConfig = getImageConfig(options, availableOptions, 'Backdrop', itemType);
|
||||
context.querySelector('#txtMaxBackdrops').value = backdropConfig.Limit;
|
||||
context.querySelector('#txtMinBackdropDownloadWidth').value = backdropConfig.MinWidth;
|
||||
}
|
||||
|
||||
if (!supportedImageTypes.includes(imageType)) {
|
||||
container.classList.add('hide');
|
||||
} else {
|
||||
container.classList.remove('hide');
|
||||
}
|
||||
function saveValues(context, options) {
|
||||
options.ImageOptions = Array.prototype.map.call(context.querySelectorAll('.imageType:not(.hide)'), c => {
|
||||
return {
|
||||
Type: c.getAttribute('data-imagetype'),
|
||||
Limit: c.checked ? 1 : 0,
|
||||
MinWidth: 0
|
||||
};
|
||||
});
|
||||
options.ImageOptions.push({
|
||||
Type: 'Backdrop',
|
||||
Limit: context.querySelector('#txtMaxBackdrops').value,
|
||||
MinWidth: context.querySelector('#txtMinBackdropDownloadWidth').value
|
||||
});
|
||||
}
|
||||
|
||||
if (getImageConfig(options, availableOptions, imageType, itemType).Limit) {
|
||||
i.checked = true;
|
||||
} else {
|
||||
i.checked = false;
|
||||
}
|
||||
});
|
||||
const backdropConfig = getImageConfig(options, availableOptions, 'Backdrop', itemType);
|
||||
context.querySelector('#txtMaxBackdrops').value = backdropConfig.Limit;
|
||||
context.querySelector('#txtMinBackdropDownloadWidth').value = backdropConfig.MinWidth;
|
||||
}
|
||||
|
||||
function saveValues(context, options) {
|
||||
options.ImageOptions = Array.prototype.map.call(context.querySelectorAll('.imageType:not(.hide)'), c => {
|
||||
return {
|
||||
Type: c.getAttribute('data-imagetype'),
|
||||
Limit: c.checked ? 1 : 0,
|
||||
MinWidth: 0
|
||||
};
|
||||
});
|
||||
options.ImageOptions.push({
|
||||
Type: 'Backdrop',
|
||||
Limit: context.querySelector('#txtMaxBackdrops').value,
|
||||
MinWidth: context.querySelector('#txtMinBackdropDownloadWidth').value
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor(itemType, options, availableOptions) {
|
||||
const dlg = dialogHelper.createDialog({
|
||||
size: 'small',
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
});
|
||||
dlg.classList.add('formDialog');
|
||||
dlg.innerHTML = globalize.translateHtml(template);
|
||||
dlg.addEventListener('close', function () {
|
||||
saveValues(dlg, options);
|
||||
});
|
||||
loadValues(dlg, itemType, options, availableOptions);
|
||||
dialogHelper.open(dlg).then(() => {
|
||||
return;
|
||||
}).catch(() => {
|
||||
return;
|
||||
});
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
function showEditor(itemType, options, availableOptions) {
|
||||
const dlg = dialogHelper.createDialog({
|
||||
size: 'small',
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
});
|
||||
dlg.classList.add('formDialog');
|
||||
dlg.innerHTML = globalize.translateHtml(template);
|
||||
dlg.addEventListener('close', function () {
|
||||
saveValues(dlg, options);
|
||||
});
|
||||
loadValues(dlg, itemType, options, availableOptions);
|
||||
dialogHelper.open(dlg).then(() => {
|
||||
return;
|
||||
}).catch(() => {
|
||||
return;
|
||||
});
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
|
||||
export class editor {
|
||||
constructor() {
|
||||
@@ -109,5 +108,4 @@ export class editor {
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default editor;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for imageUploader.
|
||||
@@ -19,169 +18,168 @@ import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './imageUploader.template.html';
|
||||
|
||||
let currentItemId;
|
||||
let currentServerId;
|
||||
let currentFile;
|
||||
let hasChanges = false;
|
||||
let currentItemId;
|
||||
let currentServerId;
|
||||
let currentFile;
|
||||
let hasChanges = false;
|
||||
|
||||
function onFileReaderError(evt) {
|
||||
function onFileReaderError(evt) {
|
||||
loading.hide();
|
||||
|
||||
switch (evt.target.error.code) {
|
||||
case evt.target.error.NOT_FOUND_ERR:
|
||||
toast(globalize.translate('MessageFileReadError'));
|
||||
break;
|
||||
case evt.target.error.ABORT_ERR:
|
||||
break; // noop
|
||||
default:
|
||||
toast(globalize.translate('MessageFileReadError'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function setFiles(page, files) {
|
||||
const file = files[0];
|
||||
|
||||
if (!file || !file.type.match('image.*')) {
|
||||
page.querySelector('#imageOutput').innerHTML = '';
|
||||
page.querySelector('#fldUpload').classList.add('hide');
|
||||
currentFile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
currentFile = file;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onerror = onFileReaderError;
|
||||
reader.onloadstart = () => {
|
||||
page.querySelector('#fldUpload').classList.add('hide');
|
||||
};
|
||||
reader.onabort = () => {
|
||||
loading.hide();
|
||||
console.debug('File read cancelled');
|
||||
};
|
||||
|
||||
switch (evt.target.error.code) {
|
||||
case evt.target.error.NOT_FOUND_ERR:
|
||||
toast(globalize.translate('MessageFileReadError'));
|
||||
break;
|
||||
case evt.target.error.ABORT_ERR:
|
||||
break; // noop
|
||||
default:
|
||||
toast(globalize.translate('MessageFileReadError'));
|
||||
break;
|
||||
}
|
||||
// Closure to capture the file information.
|
||||
reader.onload = (theFile => {
|
||||
return e => {
|
||||
// Render thumbnail.
|
||||
const html = ['<img style="max-width:100%;max-height:100%;" src="', e.target.result, '" title="', escape(theFile.name), '"/>'].join('');
|
||||
|
||||
page.querySelector('#imageOutput').innerHTML = html;
|
||||
page.querySelector('#dropImageText').classList.add('hide');
|
||||
page.querySelector('#fldUpload').classList.remove('hide');
|
||||
};
|
||||
})(file);
|
||||
|
||||
// Read in the image file as a data URL.
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const file = currentFile;
|
||||
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function setFiles(page, files) {
|
||||
const file = files[0];
|
||||
|
||||
if (!file || !file.type.match('image.*')) {
|
||||
page.querySelector('#imageOutput').innerHTML = '';
|
||||
page.querySelector('#fldUpload').classList.add('hide');
|
||||
currentFile = null;
|
||||
return;
|
||||
}
|
||||
|
||||
currentFile = file;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onerror = onFileReaderError;
|
||||
reader.onloadstart = () => {
|
||||
page.querySelector('#fldUpload').classList.add('hide');
|
||||
};
|
||||
reader.onabort = () => {
|
||||
loading.hide();
|
||||
console.debug('File read cancelled');
|
||||
};
|
||||
|
||||
// Closure to capture the file information.
|
||||
reader.onload = (theFile => {
|
||||
return e => {
|
||||
// Render thumbnail.
|
||||
const html = ['<img style="max-width:100%;max-height:100%;" src="', e.target.result, '" title="', escape(theFile.name), '"/>'].join('');
|
||||
|
||||
page.querySelector('#imageOutput').innerHTML = html;
|
||||
page.querySelector('#dropImageText').classList.add('hide');
|
||||
page.querySelector('#fldUpload').classList.remove('hide');
|
||||
};
|
||||
})(file);
|
||||
|
||||
// Read in the image file as a data URL.
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const file = currentFile;
|
||||
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast(globalize.translate('MessageImageFileTypeAllowed'));
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
loading.show();
|
||||
|
||||
const dlg = dom.parentWithClass(this, 'dialog');
|
||||
|
||||
const imageType = dlg.querySelector('#selectImageType').value;
|
||||
if (imageType === 'None') {
|
||||
toast(globalize.translate('MessageImageTypeNotSelected'));
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
ServerConnections.getApiClient(currentServerId).uploadItemImage(currentItemId, imageType, file).then(() => {
|
||||
dlg.querySelector('#uploadImage').value = '';
|
||||
|
||||
loading.hide();
|
||||
hasChanges = true;
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast(globalize.translate('MessageImageFileTypeAllowed'));
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
function initEditor(page) {
|
||||
page.querySelector('form').addEventListener('submit', onSubmit);
|
||||
loading.show();
|
||||
|
||||
page.querySelector('#uploadImage').addEventListener('change', function () {
|
||||
setFiles(page, this.files);
|
||||
});
|
||||
const dlg = dom.parentWithClass(this, 'dialog');
|
||||
|
||||
page.querySelector('.btnBrowse').addEventListener('click', () => {
|
||||
page.querySelector('#uploadImage').click();
|
||||
});
|
||||
const imageType = dlg.querySelector('#selectImageType').value;
|
||||
if (imageType === 'None') {
|
||||
toast(globalize.translate('MessageImageTypeNotSelected'));
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
function showEditor(options, resolve) {
|
||||
options = options || {};
|
||||
ServerConnections.getApiClient(currentServerId).uploadItemImage(currentItemId, imageType, file).then(() => {
|
||||
dlg.querySelector('#uploadImage').value = '';
|
||||
|
||||
currentItemId = options.itemId;
|
||||
currentServerId = options.serverId;
|
||||
loading.hide();
|
||||
hasChanges = true;
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true
|
||||
};
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
function initEditor(page) {
|
||||
page.querySelector('form').addEventListener('submit', onSubmit);
|
||||
|
||||
page.querySelector('#uploadImage').addEventListener('change', function () {
|
||||
setFiles(page, this.files);
|
||||
});
|
||||
|
||||
page.querySelector('.btnBrowse').addEventListener('click', () => {
|
||||
page.querySelector('#uploadImage').click();
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor(options, resolve) {
|
||||
options = options || {};
|
||||
|
||||
currentItemId = options.itemId;
|
||||
currentServerId = options.serverId;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg, false);
|
||||
}
|
||||
|
||||
// Has to be assigned a z-index after the call to .open()
|
||||
dlg.addEventListener('close', () => {
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
scrollHelper.centerFocus.off(dlg, false);
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
loading.hide();
|
||||
resolve(hasChanges);
|
||||
});
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
initEditor(dlg);
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg, false);
|
||||
}
|
||||
dlg.querySelector('#selectImageType').value = options.imageType || 'Primary';
|
||||
|
||||
// Has to be assigned a z-index after the call to .open()
|
||||
dlg.addEventListener('close', () => {
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.off(dlg, false);
|
||||
}
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
resolve(hasChanges);
|
||||
});
|
||||
export function show(options) {
|
||||
return new Promise(resolve => {
|
||||
hasChanges = false;
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
showEditor(options, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
initEditor(dlg);
|
||||
|
||||
dlg.querySelector('#selectImageType').value = options.imageType || 'Primary';
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
|
||||
export function show(options) {
|
||||
return new Promise(resolve => {
|
||||
hasChanges = false;
|
||||
|
||||
showEditor(options, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default {
|
||||
show: show
|
||||
};
|
||||
|
||||
@@ -18,441 +18,439 @@ import alert from '../alert';
|
||||
import confirm from '../confirm/confirm';
|
||||
import template from './imageeditor.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
let currentItem;
|
||||
let hasChanges = false;
|
||||
|
||||
let currentItem;
|
||||
let hasChanges = false;
|
||||
function getBaseRemoteOptions() {
|
||||
return { itemId: currentItem.Id };
|
||||
}
|
||||
|
||||
function getBaseRemoteOptions() {
|
||||
return { itemId: currentItem.Id };
|
||||
function reload(page, item, focusContext) {
|
||||
loading.show();
|
||||
|
||||
let apiClient;
|
||||
|
||||
if (item) {
|
||||
apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
reloadItem(page, item, apiClient, focusContext);
|
||||
} else {
|
||||
apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), currentItem.Id).then(function (itemToReload) {
|
||||
reloadItem(page, itemToReload, apiClient, focusContext);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function reload(page, item, focusContext) {
|
||||
loading.show();
|
||||
|
||||
let apiClient;
|
||||
|
||||
if (item) {
|
||||
apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
reloadItem(page, item, apiClient, focusContext);
|
||||
} else {
|
||||
apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), currentItem.Id).then(function (itemToReload) {
|
||||
reloadItem(page, itemToReload, apiClient, focusContext);
|
||||
});
|
||||
function addListeners(container, className, eventName, fn) {
|
||||
container.addEventListener(eventName, function (e) {
|
||||
const elem = dom.parentWithClass(e.target, className);
|
||||
if (elem) {
|
||||
fn.call(elem, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addListeners(container, className, eventName, fn) {
|
||||
container.addEventListener(eventName, function (e) {
|
||||
const elem = dom.parentWithClass(e.target, className);
|
||||
if (elem) {
|
||||
fn.call(elem, e);
|
||||
function reloadItem(page, item, apiClient, focusContext) {
|
||||
currentItem = item;
|
||||
|
||||
apiClient.getRemoteImageProviders(getBaseRemoteOptions()).then(function (providers) {
|
||||
const btnBrowseAllImages = page.querySelectorAll('.btnBrowseAllImages');
|
||||
for (let i = 0, length = btnBrowseAllImages.length; i < length; i++) {
|
||||
if (providers.length) {
|
||||
btnBrowseAllImages[i].classList.remove('hide');
|
||||
} else {
|
||||
btnBrowseAllImages[i].classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
apiClient.getItemImageInfos(currentItem.Id).then(function (imageInfos) {
|
||||
renderStandardImages(page, apiClient, item, imageInfos, providers);
|
||||
renderBackdrops(page, apiClient, item, imageInfos, providers);
|
||||
loading.hide();
|
||||
|
||||
if (layoutManager.tv) {
|
||||
focusManager.autoFocus((focusContext || page));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getImageUrl(item, apiClient, type, index, options) {
|
||||
options = options || {};
|
||||
options.type = type;
|
||||
options.index = index;
|
||||
|
||||
if (type === 'Backdrop') {
|
||||
options.tag = item.BackdropImageTags[index];
|
||||
} else if (type === 'Primary') {
|
||||
options.tag = item.PrimaryImageTag || item.ImageTags[type];
|
||||
} else {
|
||||
options.tag = item.ImageTags[type];
|
||||
}
|
||||
|
||||
function reloadItem(page, item, apiClient, focusContext) {
|
||||
currentItem = item;
|
||||
// For search hints
|
||||
return apiClient.getScaledImageUrl(item.Id || item.ItemId, options);
|
||||
}
|
||||
|
||||
apiClient.getRemoteImageProviders(getBaseRemoteOptions()).then(function (providers) {
|
||||
const btnBrowseAllImages = page.querySelectorAll('.btnBrowseAllImages');
|
||||
for (let i = 0, length = btnBrowseAllImages.length; i < length; i++) {
|
||||
if (providers.length) {
|
||||
btnBrowseAllImages[i].classList.remove('hide');
|
||||
} else {
|
||||
btnBrowseAllImages[i].classList.add('hide');
|
||||
}
|
||||
function getCardHtml(image, apiClient, options) {
|
||||
// TODO move card creation code to Card component
|
||||
|
||||
let html = '';
|
||||
|
||||
let cssClass = 'card scalableCard imageEditorCard';
|
||||
const cardBoxCssClass = 'cardBox visualCardBox';
|
||||
|
||||
cssClass += ' backdropCard backdropCard-scalable';
|
||||
|
||||
if (options.tagName === 'button') {
|
||||
cssClass += ' btnImageCard';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' show-focus';
|
||||
|
||||
if (enableFocusTransform) {
|
||||
cssClass += ' show-animation';
|
||||
}
|
||||
}
|
||||
|
||||
html += '<button type="button" class="' + cssClass + '"';
|
||||
} else {
|
||||
html += '<div class="' + cssClass + '"';
|
||||
}
|
||||
|
||||
html += ' data-id="' + currentItem.Id + '" data-serverid="' + apiClient.serverId() + '" data-index="' + options.index + '" data-numimages="' + options.numImages + '" data-imagetype="' + image.ImageType + '" data-providers="' + options.imageProviders.length + '"';
|
||||
|
||||
html += '>';
|
||||
|
||||
html += '<div class="' + cardBoxCssClass + '">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable" style="background-color:transparent;">';
|
||||
html += '<div class="cardPadder-backdrop"></div>';
|
||||
|
||||
html += '<div class="cardContent">';
|
||||
|
||||
const imageUrl = getImageUrl(currentItem, apiClient, image.ImageType, image.ImageIndex, { maxWidth: options.imageSize });
|
||||
|
||||
html += '<div class="cardImageContainer" style="background-image:url(\'' + imageUrl + '\');background-position:center center;background-size:contain;"></div>';
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">';
|
||||
|
||||
html += '<h3 class="cardText cardTextCentered" style="margin:0;">' + globalize.translate('' + image.ImageType) + '</h3>';
|
||||
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
if (image.Width && image.Height) {
|
||||
html += image.Width + ' X ' + image.Height;
|
||||
} else {
|
||||
html += ' ';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableFooterButtons) {
|
||||
html += '<div class="cardText cardTextCentered">';
|
||||
|
||||
if (image.ImageType === 'Backdrop') {
|
||||
if (options.index > 0) {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex - 1) + '" title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>';
|
||||
} else {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
apiClient.getItemImageInfos(currentItem.Id).then(function (imageInfos) {
|
||||
renderStandardImages(page, apiClient, item, imageInfos, providers);
|
||||
renderBackdrops(page, apiClient, item, imageInfos, providers);
|
||||
loading.hide();
|
||||
if (options.index < options.numImages - 1) {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex + 1) + '" title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
|
||||
} else {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
|
||||
}
|
||||
} else {
|
||||
if (options.imageProviders.length) {
|
||||
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search" aria-hidden="true"></span></button>';
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
focusManager.autoFocus((focusContext || page));
|
||||
}
|
||||
});
|
||||
});
|
||||
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
function getImageUrl(item, apiClient, type, index, options) {
|
||||
options = options || {};
|
||||
options.type = type;
|
||||
options.index = index;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</' + options.tagName + '>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function deleteImage(context, itemId, type, index, apiClient, enableConfirmation) {
|
||||
const afterConfirm = function () {
|
||||
apiClient.deleteItemImage(itemId, type, index).then(function () {
|
||||
hasChanges = true;
|
||||
reload(context);
|
||||
});
|
||||
};
|
||||
|
||||
if (!enableConfirmation) {
|
||||
afterConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
confirm({
|
||||
text: globalize.translate('ConfirmDeleteImage'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(afterConfirm);
|
||||
}
|
||||
|
||||
function moveImage(context, apiClient, itemId, type, index, newIndex, focusContext) {
|
||||
apiClient.updateItemImageIndex(itemId, type, index, newIndex).then(function () {
|
||||
hasChanges = true;
|
||||
reload(context, null, focusContext);
|
||||
}, function () {
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
});
|
||||
}
|
||||
|
||||
function renderImages(page, item, apiClient, images, imageProviders, elem) {
|
||||
let html = '';
|
||||
|
||||
let imageSize = 300;
|
||||
const windowSize = dom.getWindowSize();
|
||||
if (windowSize.innerWidth >= 1280) {
|
||||
imageSize = Math.round(windowSize.innerWidth / 4);
|
||||
}
|
||||
|
||||
const tagName = layoutManager.tv ? 'button' : 'div';
|
||||
const enableFooterButtons = !layoutManager.tv;
|
||||
|
||||
for (let i = 0, length = images.length; i < length; i++) {
|
||||
const image = images[i];
|
||||
const options = { index: i, numImages: length, imageProviders, imageSize, tagName, enableFooterButtons };
|
||||
html += getCardHtml(image, apiClient, options);
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
}
|
||||
|
||||
function renderStandardImages(page, apiClient, item, imageInfos, imageProviders) {
|
||||
const images = imageInfos.filter(function (i) {
|
||||
return i.ImageType !== 'Backdrop' && i.ImageType !== 'Chapter';
|
||||
});
|
||||
|
||||
renderImages(page, item, apiClient, images, imageProviders, page.querySelector('#images'));
|
||||
}
|
||||
|
||||
function renderBackdrops(page, apiClient, item, imageInfos, imageProviders) {
|
||||
const images = imageInfos.filter(function (i) {
|
||||
return i.ImageType === 'Backdrop';
|
||||
}).sort(function (a, b) {
|
||||
return a.ImageIndex - b.ImageIndex;
|
||||
});
|
||||
|
||||
if (images.length) {
|
||||
page.querySelector('#backdropsContainer', page).classList.remove('hide');
|
||||
renderImages(page, item, apiClient, images, imageProviders, page.querySelector('#backdrops'));
|
||||
} else {
|
||||
page.querySelector('#backdropsContainer', page).classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function showImageDownloader(page, imageType) {
|
||||
import('../imageDownloader/imageDownloader').then((ImageDownloader) => {
|
||||
ImageDownloader.show(
|
||||
currentItem.Id,
|
||||
currentItem.ServerId,
|
||||
currentItem.Type,
|
||||
imageType,
|
||||
currentItem.Type == 'Season' ? currentItem.ParentId : null
|
||||
).then(function () {
|
||||
hasChanges = true;
|
||||
reload(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showActionSheet(context, imageCard) {
|
||||
const itemId = imageCard.getAttribute('data-id');
|
||||
const serverId = imageCard.getAttribute('data-serverid');
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
const type = imageCard.getAttribute('data-imagetype');
|
||||
const index = parseInt(imageCard.getAttribute('data-index'), 10);
|
||||
const providerCount = parseInt(imageCard.getAttribute('data-providers'), 10);
|
||||
const numImages = parseInt(imageCard.getAttribute('data-numimages'), 10);
|
||||
|
||||
import('../actionSheet/actionSheet').then(({ default: actionSheet }) => {
|
||||
const commands = [];
|
||||
|
||||
commands.push({
|
||||
name: globalize.translate('Delete'),
|
||||
id: 'delete'
|
||||
});
|
||||
|
||||
if (type === 'Backdrop') {
|
||||
options.tag = item.BackdropImageTags[index];
|
||||
} else if (type === 'Primary') {
|
||||
options.tag = item.PrimaryImageTag || item.ImageTags[type];
|
||||
} else {
|
||||
options.tag = item.ImageTags[type];
|
||||
}
|
||||
|
||||
// For search hints
|
||||
return apiClient.getScaledImageUrl(item.Id || item.ItemId, options);
|
||||
}
|
||||
|
||||
function getCardHtml(image, apiClient, options) {
|
||||
// TODO move card creation code to Card component
|
||||
|
||||
let html = '';
|
||||
|
||||
let cssClass = 'card scalableCard imageEditorCard';
|
||||
const cardBoxCssClass = 'cardBox visualCardBox';
|
||||
|
||||
cssClass += ' backdropCard backdropCard-scalable';
|
||||
|
||||
if (options.tagName === 'button') {
|
||||
cssClass += ' btnImageCard';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' show-focus';
|
||||
|
||||
if (enableFocusTransform) {
|
||||
cssClass += ' show-animation';
|
||||
}
|
||||
if (index > 0) {
|
||||
commands.push({
|
||||
name: globalize.translate('MoveLeft'),
|
||||
id: 'moveleft'
|
||||
});
|
||||
}
|
||||
|
||||
html += '<button type="button" class="' + cssClass + '"';
|
||||
} else {
|
||||
html += '<div class="' + cssClass + '"';
|
||||
}
|
||||
|
||||
html += ' data-id="' + currentItem.Id + '" data-serverid="' + apiClient.serverId() + '" data-index="' + options.index + '" data-numimages="' + options.numImages + '" data-imagetype="' + image.ImageType + '" data-providers="' + options.imageProviders.length + '"';
|
||||
|
||||
html += '>';
|
||||
|
||||
html += '<div class="' + cardBoxCssClass + '">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable" style="background-color:transparent;">';
|
||||
html += '<div class="cardPadder-backdrop"></div>';
|
||||
|
||||
html += '<div class="cardContent">';
|
||||
|
||||
const imageUrl = getImageUrl(currentItem, apiClient, image.ImageType, image.ImageIndex, { maxWidth: options.imageSize });
|
||||
|
||||
html += '<div class="cardImageContainer" style="background-image:url(\'' + imageUrl + '\');background-position:center center;background-size:contain;"></div>';
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">';
|
||||
|
||||
html += '<h3 class="cardText cardTextCentered" style="margin:0;">' + globalize.translate('' + image.ImageType) + '</h3>';
|
||||
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
if (image.Width && image.Height) {
|
||||
html += image.Width + ' X ' + image.Height;
|
||||
} else {
|
||||
html += ' ';
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (options.enableFooterButtons) {
|
||||
html += '<div class="cardText cardTextCentered">';
|
||||
|
||||
if (image.ImageType === 'Backdrop') {
|
||||
if (options.index > 0) {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex - 1) + '" title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>';
|
||||
} else {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
if (options.index < options.numImages - 1) {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex + 1) + '" title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
|
||||
} else {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
|
||||
}
|
||||
} else {
|
||||
if (options.imageProviders.length) {
|
||||
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search" aria-hidden="true"></span></button>';
|
||||
}
|
||||
if (index < numImages - 1) {
|
||||
commands.push({
|
||||
name: globalize.translate('MoveRight'),
|
||||
id: 'moveright'
|
||||
});
|
||||
}
|
||||
|
||||
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</' + options.tagName + '>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function deleteImage(context, itemId, type, index, apiClient, enableConfirmation) {
|
||||
const afterConfirm = function () {
|
||||
apiClient.deleteItemImage(itemId, type, index).then(function () {
|
||||
hasChanges = true;
|
||||
reload(context);
|
||||
if (providerCount) {
|
||||
commands.push({
|
||||
name: globalize.translate('Search'),
|
||||
id: 'search'
|
||||
});
|
||||
}
|
||||
|
||||
actionSheet.show({
|
||||
|
||||
items: commands,
|
||||
positionTo: imageCard
|
||||
|
||||
}).then(function (id) {
|
||||
switch (id) {
|
||||
case 'delete':
|
||||
deleteImage(context, itemId, type, index, apiClient, false);
|
||||
break;
|
||||
case 'search':
|
||||
showImageDownloader(context, type);
|
||||
break;
|
||||
case 'moveleft':
|
||||
moveImage(context, apiClient, itemId, type, index, index - 1, dom.parentWithClass(imageCard, 'itemsContainer'));
|
||||
break;
|
||||
case 'moveright':
|
||||
moveImage(context, apiClient, itemId, type, index, index + 1, dom.parentWithClass(imageCard, 'itemsContainer'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initEditor(context, options) {
|
||||
const uploadButtons = context.querySelectorAll('.btnOpenUploadMenu');
|
||||
const isFileInputSupported = appHost.supports('fileinput');
|
||||
for (let i = 0, length = uploadButtons.length; i < length; i++) {
|
||||
if (isFileInputSupported) {
|
||||
uploadButtons[i].classList.remove('hide');
|
||||
} else {
|
||||
uploadButtons[i].classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
addListeners(context, 'btnOpenUploadMenu', 'click', function () {
|
||||
const imageType = this.getAttribute('data-imagetype');
|
||||
|
||||
import('../imageUploader/imageUploader').then(({ default: imageUploader }) => {
|
||||
imageUploader.show({
|
||||
|
||||
theme: options.theme,
|
||||
imageType: imageType,
|
||||
itemId: currentItem.Id,
|
||||
serverId: currentItem.ServerId
|
||||
|
||||
}).then(function (hasChanged) {
|
||||
if (hasChanged) {
|
||||
hasChanges = true;
|
||||
reload(context);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
addListeners(context, 'btnSearchImages', 'click', function () {
|
||||
showImageDownloader(context, this.getAttribute('data-imagetype'));
|
||||
});
|
||||
|
||||
addListeners(context, 'btnBrowseAllImages', 'click', function () {
|
||||
showImageDownloader(context, this.getAttribute('data-imagetype') || 'Primary');
|
||||
});
|
||||
|
||||
addListeners(context, 'btnImageCard', 'click', function () {
|
||||
showActionSheet(context, this);
|
||||
});
|
||||
|
||||
addListeners(context, 'btnDeleteImage', 'click', function () {
|
||||
const type = this.getAttribute('data-imagetype');
|
||||
let index = this.getAttribute('data-index');
|
||||
index = index === 'null' ? null : parseInt(index, 10);
|
||||
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||
deleteImage(context, currentItem.Id, type, index, apiClient, true);
|
||||
});
|
||||
|
||||
addListeners(context, 'btnMoveImage', 'click', function () {
|
||||
const type = this.getAttribute('data-imagetype');
|
||||
const index = this.getAttribute('data-index');
|
||||
const newIndex = this.getAttribute('data-newindex');
|
||||
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||
moveImage(context, apiClient, currentItem.Id, type, index, newIndex, dom.parentWithClass(this, 'itemsContainer'));
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor(options, resolve, reject) {
|
||||
const itemId = options.itemId;
|
||||
const serverId = options.serverId;
|
||||
|
||||
loading.show();
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (item) {
|
||||
const dialogOptions = {
|
||||
removeOnClose: true
|
||||
};
|
||||
|
||||
if (!enableConfirmation) {
|
||||
afterConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
confirm({
|
||||
text: globalize.translate('ConfirmDeleteImage'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(afterConfirm);
|
||||
}
|
||||
|
||||
function moveImage(context, apiClient, itemId, type, index, newIndex, focusContext) {
|
||||
apiClient.updateItemImageIndex(itemId, type, index, newIndex).then(function () {
|
||||
hasChanges = true;
|
||||
reload(context, null, focusContext);
|
||||
}, function () {
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
});
|
||||
}
|
||||
|
||||
function renderImages(page, item, apiClient, images, imageProviders, elem) {
|
||||
let html = '';
|
||||
|
||||
let imageSize = 300;
|
||||
const windowSize = dom.getWindowSize();
|
||||
if (windowSize.innerWidth >= 1280) {
|
||||
imageSize = Math.round(windowSize.innerWidth / 4);
|
||||
}
|
||||
|
||||
const tagName = layoutManager.tv ? 'button' : 'div';
|
||||
const enableFooterButtons = !layoutManager.tv;
|
||||
|
||||
for (let i = 0, length = images.length; i < length; i++) {
|
||||
const image = images[i];
|
||||
const options = { index: i, numImages: length, imageProviders, imageSize, tagName, enableFooterButtons };
|
||||
html += getCardHtml(image, apiClient, options);
|
||||
}
|
||||
|
||||
elem.innerHTML = html;
|
||||
imageLoader.lazyChildren(elem);
|
||||
}
|
||||
|
||||
function renderStandardImages(page, apiClient, item, imageInfos, imageProviders) {
|
||||
const images = imageInfos.filter(function (i) {
|
||||
return i.ImageType !== 'Backdrop' && i.ImageType !== 'Chapter';
|
||||
});
|
||||
|
||||
renderImages(page, item, apiClient, images, imageProviders, page.querySelector('#images'));
|
||||
}
|
||||
|
||||
function renderBackdrops(page, apiClient, item, imageInfos, imageProviders) {
|
||||
const images = imageInfos.filter(function (i) {
|
||||
return i.ImageType === 'Backdrop';
|
||||
}).sort(function (a, b) {
|
||||
return a.ImageIndex - b.ImageIndex;
|
||||
});
|
||||
|
||||
if (images.length) {
|
||||
page.querySelector('#backdropsContainer', page).classList.remove('hide');
|
||||
renderImages(page, item, apiClient, images, imageProviders, page.querySelector('#backdrops'));
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
page.querySelector('#backdropsContainer', page).classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function showImageDownloader(page, imageType) {
|
||||
import('../imageDownloader/imageDownloader').then((ImageDownloader) => {
|
||||
ImageDownloader.show(
|
||||
currentItem.Id,
|
||||
currentItem.ServerId,
|
||||
currentItem.Type,
|
||||
imageType,
|
||||
currentItem.Type == 'Season' ? currentItem.ParentId : null
|
||||
).then(function () {
|
||||
hasChanges = true;
|
||||
reload(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showActionSheet(context, imageCard) {
|
||||
const itemId = imageCard.getAttribute('data-id');
|
||||
const serverId = imageCard.getAttribute('data-serverid');
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
const type = imageCard.getAttribute('data-imagetype');
|
||||
const index = parseInt(imageCard.getAttribute('data-index'), 10);
|
||||
const providerCount = parseInt(imageCard.getAttribute('data-providers'), 10);
|
||||
const numImages = parseInt(imageCard.getAttribute('data-numimages'), 10);
|
||||
|
||||
import('../actionSheet/actionSheet').then(({default: actionSheet}) => {
|
||||
const commands = [];
|
||||
|
||||
commands.push({
|
||||
name: globalize.translate('Delete'),
|
||||
id: 'delete'
|
||||
});
|
||||
|
||||
if (type === 'Backdrop') {
|
||||
if (index > 0) {
|
||||
commands.push({
|
||||
name: globalize.translate('MoveLeft'),
|
||||
id: 'moveleft'
|
||||
});
|
||||
}
|
||||
|
||||
if (index < numImages - 1) {
|
||||
commands.push({
|
||||
name: globalize.translate('MoveRight'),
|
||||
id: 'moveright'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (providerCount) {
|
||||
commands.push({
|
||||
name: globalize.translate('Search'),
|
||||
id: 'search'
|
||||
});
|
||||
}
|
||||
|
||||
actionSheet.show({
|
||||
|
||||
items: commands,
|
||||
positionTo: imageCard
|
||||
|
||||
}).then(function (id) {
|
||||
switch (id) {
|
||||
case 'delete':
|
||||
deleteImage(context, itemId, type, index, apiClient, false);
|
||||
break;
|
||||
case 'search':
|
||||
showImageDownloader(context, type);
|
||||
break;
|
||||
case 'moveleft':
|
||||
moveImage(context, apiClient, itemId, type, index, index - 1, dom.parentWithClass(imageCard, 'itemsContainer'));
|
||||
break;
|
||||
case 'moveright':
|
||||
moveImage(context, apiClient, itemId, type, index, index + 1, dom.parentWithClass(imageCard, 'itemsContainer'));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initEditor(context, options) {
|
||||
const uploadButtons = context.querySelectorAll('.btnOpenUploadMenu');
|
||||
const isFileInputSupported = appHost.supports('fileinput');
|
||||
for (let i = 0, length = uploadButtons.length; i < length; i++) {
|
||||
if (isFileInputSupported) {
|
||||
uploadButtons[i].classList.remove('hide');
|
||||
} else {
|
||||
uploadButtons[i].classList.add('hide');
|
||||
}
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
addListeners(context, 'btnOpenUploadMenu', 'click', function () {
|
||||
const imageType = this.getAttribute('data-imagetype');
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
import('../imageUploader/imageUploader').then(({default: imageUploader}) => {
|
||||
imageUploader.show({
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
theme: options.theme,
|
||||
imageType: imageType,
|
||||
itemId: currentItem.Id,
|
||||
serverId: currentItem.ServerId
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
}).then(function (hasChanged) {
|
||||
if (hasChanged) {
|
||||
hasChanges = true;
|
||||
reload(context);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg, false);
|
||||
}
|
||||
|
||||
addListeners(context, 'btnSearchImages', 'click', function () {
|
||||
showImageDownloader(context, this.getAttribute('data-imagetype'));
|
||||
});
|
||||
|
||||
addListeners(context, 'btnBrowseAllImages', 'click', function () {
|
||||
showImageDownloader(context, this.getAttribute('data-imagetype') || 'Primary');
|
||||
});
|
||||
|
||||
addListeners(context, 'btnImageCard', 'click', function () {
|
||||
showActionSheet(context, this);
|
||||
});
|
||||
|
||||
addListeners(context, 'btnDeleteImage', 'click', function () {
|
||||
const type = this.getAttribute('data-imagetype');
|
||||
let index = this.getAttribute('data-index');
|
||||
index = index === 'null' ? null : parseInt(index, 10);
|
||||
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||
deleteImage(context, currentItem.Id, type, index, apiClient, true);
|
||||
});
|
||||
|
||||
addListeners(context, 'btnMoveImage', 'click', function () {
|
||||
const type = this.getAttribute('data-imagetype');
|
||||
const index = this.getAttribute('data-index');
|
||||
const newIndex = this.getAttribute('data-newindex');
|
||||
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||
moveImage(context, apiClient, currentItem.Id, type, index, newIndex, dom.parentWithClass(this, 'itemsContainer'));
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor(options, resolve, reject) {
|
||||
const itemId = options.itemId;
|
||||
const serverId = options.serverId;
|
||||
|
||||
loading.show();
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (item) {
|
||||
const dialogOptions = {
|
||||
removeOnClose: true
|
||||
};
|
||||
initEditor(dlg, options);
|
||||
|
||||
// Has to be assigned a z-index after the call to .open()
|
||||
dlg.addEventListener('close', function () {
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
scrollHelper.centerFocus.off(dlg, false);
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
|
||||
if (hasChanges) {
|
||||
resolve();
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
reject();
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
dlg.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg, false);
|
||||
}
|
||||
|
||||
initEditor(dlg, options);
|
||||
|
||||
// Has to be assigned a z-index after the call to .open()
|
||||
dlg.addEventListener('close', function () {
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.off(dlg, false);
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
|
||||
if (hasChanges) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
reload(dlg, item);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
reload(dlg, item);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function show (options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
@@ -465,4 +463,3 @@ export default {
|
||||
show
|
||||
};
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -17,237 +17,235 @@ worker.addEventListener(
|
||||
}
|
||||
}
|
||||
);
|
||||
/* eslint-disable indent */
|
||||
|
||||
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
fillImageElement(elem, source);
|
||||
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
function drawBlurhash(target, pixels, width, height) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imgData = ctx.createImageData(width, height);
|
||||
fillImageElement(elem, source);
|
||||
}
|
||||
|
||||
imgData.data.set(pixels);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
function drawBlurhash(target, pixels, width, height) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('aria-hidden', 'true');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imgData = ctx.createImageData(width, height);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// This class is just an utility class, so users can customize the canvas using their own CSS.
|
||||
canvas.classList.add('blurhash-canvas');
|
||||
imgData.data.set(pixels);
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
target.parentNode.insertBefore(canvas, target);
|
||||
target.classList.add('blurhashed');
|
||||
target.removeAttribute('data-blurhash');
|
||||
requestAnimationFrame(() => {
|
||||
// This class is just an utility class, so users can customize the canvas using their own CSS.
|
||||
canvas.classList.add('blurhash-canvas');
|
||||
|
||||
target.parentNode.insertBefore(canvas, target);
|
||||
target.classList.add('blurhashed');
|
||||
target.removeAttribute('data-blurhash');
|
||||
});
|
||||
}
|
||||
|
||||
function itemBlurhashing(target, hash) {
|
||||
try {
|
||||
// Although the default values recommended by Blurhash developers is 32x32, a size of 20x20 seems to be the sweet spot for us,
|
||||
// improving the performance and reducing the memory usage, while retaining almost full blur quality.
|
||||
// Lower values had more visible pixelation
|
||||
const width = 20;
|
||||
const height = 20;
|
||||
targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target);
|
||||
targetDic[hash].push(target);
|
||||
|
||||
worker.postMessage({
|
||||
hash,
|
||||
width,
|
||||
height
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
target.classList.add('non-blurhashable');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function fillImage(entry) {
|
||||
if (!entry) {
|
||||
throw new Error('entry cannot be null');
|
||||
}
|
||||
const target = entry.target;
|
||||
let source = undefined;
|
||||
|
||||
if (target) {
|
||||
source = target.getAttribute('data-src');
|
||||
} else {
|
||||
source = entry;
|
||||
}
|
||||
|
||||
function itemBlurhashing(target, hash) {
|
||||
try {
|
||||
// Although the default values recommended by Blurhash developers is 32x32, a size of 20x20 seems to be the sweet spot for us,
|
||||
// improving the performance and reducing the memory usage, while retaining almost full blur quality.
|
||||
// Lower values had more visible pixelation
|
||||
const width = 20;
|
||||
const height = 20;
|
||||
targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target);
|
||||
targetDic[hash].push(target);
|
||||
|
||||
worker.postMessage({
|
||||
hash,
|
||||
width,
|
||||
height
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
target.classList.add('non-blurhashable');
|
||||
return;
|
||||
if (entry.isIntersecting) {
|
||||
if (source) {
|
||||
fillImageElement(target, source);
|
||||
}
|
||||
} else if (!source) {
|
||||
emptyImageElement(target);
|
||||
}
|
||||
}
|
||||
|
||||
export function fillImage(entry) {
|
||||
if (!entry) {
|
||||
throw new Error('entry cannot be null');
|
||||
}
|
||||
const target = entry.target;
|
||||
let source = undefined;
|
||||
|
||||
if (target) {
|
||||
source = target.getAttribute('data-src');
|
||||
} else {
|
||||
source = entry;
|
||||
}
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
if (source) {
|
||||
fillImageElement(target, source);
|
||||
}
|
||||
} else if (!source) {
|
||||
emptyImageElement(target);
|
||||
}
|
||||
}
|
||||
|
||||
function onAnimationEnd(event) {
|
||||
const elem = event.target;
|
||||
requestAnimationFrame(() => {
|
||||
const canvas = elem.previousSibling;
|
||||
if (elem.classList.contains('blurhashed') && canvas?.tagName === 'CANVAS') {
|
||||
canvas.classList.add('lazy-hidden');
|
||||
}
|
||||
|
||||
// HACK: Hide the content of the card padder
|
||||
elem.parentNode?.querySelector('.cardPadder')?.classList.add('lazy-hidden-children');
|
||||
});
|
||||
elem.removeEventListener('animationend', onAnimationEnd);
|
||||
}
|
||||
|
||||
function fillImageElement(elem, url) {
|
||||
if (url === undefined) {
|
||||
throw new TypeError('url cannot be undefined');
|
||||
}
|
||||
|
||||
const preloaderImg = new Image();
|
||||
preloaderImg.src = url;
|
||||
|
||||
elem.classList.add('lazy-hidden');
|
||||
elem.addEventListener('animationend', onAnimationEnd);
|
||||
|
||||
preloaderImg.addEventListener('load', () => {
|
||||
requestAnimationFrame(() => {
|
||||
if (elem.tagName !== 'IMG') {
|
||||
elem.style.backgroundImage = "url('" + url + "')";
|
||||
} else {
|
||||
elem.setAttribute('src', url);
|
||||
}
|
||||
elem.removeAttribute('data-src');
|
||||
|
||||
if (userSettings.enableFastFadein()) {
|
||||
elem.classList.add('lazy-image-fadein-fast');
|
||||
} else {
|
||||
elem.classList.add('lazy-image-fadein');
|
||||
}
|
||||
elem.classList.remove('lazy-hidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function emptyImageElement(elem) {
|
||||
elem.removeEventListener('animationend', onAnimationEnd);
|
||||
function onAnimationEnd(event) {
|
||||
const elem = event.target;
|
||||
requestAnimationFrame(() => {
|
||||
const canvas = elem.previousSibling;
|
||||
if (canvas?.tagName === 'CANVAS') {
|
||||
canvas.classList.remove('lazy-hidden');
|
||||
if (elem.classList.contains('blurhashed') && canvas?.tagName === 'CANVAS') {
|
||||
canvas.classList.add('lazy-hidden');
|
||||
}
|
||||
|
||||
// HACK: Unhide the content of the card padder
|
||||
elem.parentNode?.querySelector('.cardPadder')?.classList.remove('lazy-hidden-children');
|
||||
// HACK: Hide the content of the card padder
|
||||
elem.parentNode?.querySelector('.cardPadder')?.classList.add('lazy-hidden-children');
|
||||
});
|
||||
elem.removeEventListener('animationend', onAnimationEnd);
|
||||
}
|
||||
|
||||
let url;
|
||||
|
||||
if (elem.tagName !== 'IMG') {
|
||||
url = elem.style.backgroundImage.slice(4, -1).replace(/"/g, '');
|
||||
elem.style.backgroundImage = 'none';
|
||||
} else {
|
||||
url = elem.getAttribute('src');
|
||||
elem.setAttribute('src', '');
|
||||
}
|
||||
elem.setAttribute('data-src', url);
|
||||
|
||||
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
|
||||
elem.classList.add('lazy-hidden');
|
||||
function fillImageElement(elem, url) {
|
||||
if (url === undefined) {
|
||||
throw new TypeError('url cannot be undefined');
|
||||
}
|
||||
|
||||
export function lazyChildren(elem) {
|
||||
if (userSettings.enableBlurhash()) {
|
||||
for (const lazyElem of elem.querySelectorAll('.lazy')) {
|
||||
const blurhashstr = lazyElem.getAttribute('data-blurhash');
|
||||
if (!lazyElem.classList.contains('blurhashed', 'non-blurhashable') && blurhashstr) {
|
||||
itemBlurhashing(lazyElem, blurhashstr);
|
||||
} else if (!blurhashstr && !lazyElem.classList.contains('blurhashed')) {
|
||||
lazyElem.classList.add('non-blurhashable');
|
||||
}
|
||||
const preloaderImg = new Image();
|
||||
preloaderImg.src = url;
|
||||
|
||||
elem.classList.add('lazy-hidden');
|
||||
elem.addEventListener('animationend', onAnimationEnd);
|
||||
|
||||
preloaderImg.addEventListener('load', () => {
|
||||
requestAnimationFrame(() => {
|
||||
if (elem.tagName !== 'IMG') {
|
||||
elem.style.backgroundImage = "url('" + url + "')";
|
||||
} else {
|
||||
elem.setAttribute('src', url);
|
||||
}
|
||||
}
|
||||
elem.removeAttribute('data-src');
|
||||
|
||||
lazyLoader.lazyChildren(elem, fillImage);
|
||||
}
|
||||
|
||||
export function getPrimaryImageAspectRatio(items) {
|
||||
const values = [];
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const ratio = items[i].PrimaryImageAspectRatio || 0;
|
||||
|
||||
if (!ratio) {
|
||||
continue;
|
||||
if (userSettings.enableFastFadein()) {
|
||||
elem.classList.add('lazy-image-fadein-fast');
|
||||
} else {
|
||||
elem.classList.add('lazy-image-fadein');
|
||||
}
|
||||
|
||||
values[values.length] = ratio;
|
||||
}
|
||||
|
||||
if (!values.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the median
|
||||
values.sort(function (a, b) {
|
||||
return a - b;
|
||||
elem.classList.remove('lazy-hidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const half = Math.floor(values.length / 2);
|
||||
|
||||
let result;
|
||||
|
||||
if (values.length % 2) {
|
||||
result = values[half];
|
||||
} else {
|
||||
result = (values[half - 1] + values[half]) / 2.0;
|
||||
}
|
||||
|
||||
// If really close to 2:3 (poster image), just return 2:3
|
||||
const aspect2x3 = 2 / 3;
|
||||
if (Math.abs(aspect2x3 - result) <= 0.15) {
|
||||
return aspect2x3;
|
||||
}
|
||||
|
||||
// If really close to 16:9 (episode image), just return 16:9
|
||||
const aspect16x9 = 16 / 9;
|
||||
if (Math.abs(aspect16x9 - result) <= 0.2) {
|
||||
return aspect16x9;
|
||||
}
|
||||
|
||||
// If really close to 1 (square image), just return 1
|
||||
if (Math.abs(1 - result) <= 0.15) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If really close to 4:3 (poster image), just return 2:3
|
||||
const aspect4x3 = 4 / 3;
|
||||
if (Math.abs(aspect4x3 - result) <= 0.15) {
|
||||
return aspect4x3;
|
||||
}
|
||||
|
||||
return result;
|
||||
function emptyImageElement(elem) {
|
||||
elem.removeEventListener('animationend', onAnimationEnd);
|
||||
const canvas = elem.previousSibling;
|
||||
if (canvas?.tagName === 'CANVAS') {
|
||||
canvas.classList.remove('lazy-hidden');
|
||||
}
|
||||
|
||||
export function fillImages(elems) {
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const elem = elems[0];
|
||||
fillImage(elem);
|
||||
// HACK: Unhide the content of the card padder
|
||||
elem.parentNode?.querySelector('.cardPadder')?.classList.remove('lazy-hidden-children');
|
||||
|
||||
let url;
|
||||
|
||||
if (elem.tagName !== 'IMG') {
|
||||
url = elem.style.backgroundImage.slice(4, -1).replace(/"/g, '');
|
||||
elem.style.backgroundImage = 'none';
|
||||
} else {
|
||||
url = elem.getAttribute('src');
|
||||
elem.setAttribute('src', '');
|
||||
}
|
||||
elem.setAttribute('data-src', url);
|
||||
|
||||
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
|
||||
elem.classList.add('lazy-hidden');
|
||||
}
|
||||
|
||||
export function lazyChildren(elem) {
|
||||
if (userSettings.enableBlurhash()) {
|
||||
for (const lazyElem of elem.querySelectorAll('.lazy')) {
|
||||
const blurhashstr = lazyElem.getAttribute('data-blurhash');
|
||||
if (!lazyElem.classList.contains('blurhashed', 'non-blurhashable') && blurhashstr) {
|
||||
itemBlurhashing(lazyElem, blurhashstr);
|
||||
} else if (!blurhashstr && !lazyElem.classList.contains('blurhashed')) {
|
||||
lazyElem.classList.add('non-blurhashable');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setLazyImage(element, url) {
|
||||
element.classList.add('lazy');
|
||||
element.setAttribute('data-src', url);
|
||||
lazyImage(element);
|
||||
lazyLoader.lazyChildren(elem, fillImage);
|
||||
}
|
||||
|
||||
export function getPrimaryImageAspectRatio(items) {
|
||||
const values = [];
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const ratio = items[i].PrimaryImageAspectRatio || 0;
|
||||
|
||||
if (!ratio) {
|
||||
continue;
|
||||
}
|
||||
|
||||
values[values.length] = ratio;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
if (!values.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the median
|
||||
values.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
const half = Math.floor(values.length / 2);
|
||||
|
||||
let result;
|
||||
|
||||
if (values.length % 2) {
|
||||
result = values[half];
|
||||
} else {
|
||||
result = (values[half - 1] + values[half]) / 2.0;
|
||||
}
|
||||
|
||||
// If really close to 2:3 (poster image), just return 2:3
|
||||
const aspect2x3 = 2 / 3;
|
||||
if (Math.abs(aspect2x3 - result) <= 0.15) {
|
||||
return aspect2x3;
|
||||
}
|
||||
|
||||
// If really close to 16:9 (episode image), just return 16:9
|
||||
const aspect16x9 = 16 / 9;
|
||||
if (Math.abs(aspect16x9 - result) <= 0.2) {
|
||||
return aspect16x9;
|
||||
}
|
||||
|
||||
// If really close to 1 (square image), just return 1
|
||||
if (Math.abs(1 - result) <= 0.15) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If really close to 4:3 (poster image), just return 2:3
|
||||
const aspect4x3 = 4 / 3;
|
||||
if (Math.abs(aspect4x3 - result) <= 0.15) {
|
||||
return aspect4x3;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function fillImages(elems) {
|
||||
for (let i = 0, length = elems.length; i < length; i++) {
|
||||
const elem = elems[0];
|
||||
fillImage(elem);
|
||||
}
|
||||
}
|
||||
|
||||
export function setLazyImage(element, url) {
|
||||
element.classList.add('lazy');
|
||||
element.setAttribute('data-src', url);
|
||||
lazyImage(element);
|
||||
}
|
||||
|
||||
export default {
|
||||
setLazyImage: setLazyImage,
|
||||
fillImages: fillImages,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for display media info.
|
||||
@@ -30,229 +29,228 @@ const copyButtonHtml = layoutManager.tv ? '' :
|
||||
><span class="material-icons content_copy" aria-hidden="true"></span></button>`;
|
||||
const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </span>';
|
||||
|
||||
function setMediaInfo(user, page, item) {
|
||||
let html = item.MediaSources.map(version => {
|
||||
return getMediaSourceHtml(user, item, version);
|
||||
}).join('<div style="border-top:1px solid #444;margin: 1em 0;"></div>');
|
||||
if (item.MediaSources.length > 1) {
|
||||
html = `<br/>${html}`;
|
||||
}
|
||||
const mediaInfoContent = page.querySelector('#mediaInfoContent');
|
||||
mediaInfoContent.innerHTML = html;
|
||||
|
||||
for (const btn of mediaInfoContent.querySelectorAll('.btnCopy')) {
|
||||
btn.addEventListener('click', () => {
|
||||
const infoBlock = dom.parentWithClass(btn, 'mediaInfoStream') || dom.parentWithClass(btn, 'mediaInfoSource') || mediaInfoContent;
|
||||
|
||||
copy(infoBlock.textContent).then(() => {
|
||||
toast(globalize.translate('Copied'));
|
||||
}).catch(() => {
|
||||
console.error('Could not copy text');
|
||||
toast(globalize.translate('CopyFailed'));
|
||||
});
|
||||
});
|
||||
}
|
||||
function setMediaInfo(user, page, item) {
|
||||
let html = item.MediaSources.map(version => {
|
||||
return getMediaSourceHtml(user, item, version);
|
||||
}).join('<div style="border-top:1px solid #444;margin: 1em 0;"></div>');
|
||||
if (item.MediaSources.length > 1) {
|
||||
html = `<br/>${html}`;
|
||||
}
|
||||
const mediaInfoContent = page.querySelector('#mediaInfoContent');
|
||||
mediaInfoContent.innerHTML = html;
|
||||
|
||||
function getMediaSourceHtml(user, item, version) {
|
||||
let html = '<div class="mediaInfoSource">';
|
||||
if (version.Name) {
|
||||
html += `<div><h2 class="mediaInfoStreamType">${escapeHtml(version.Name)}${copyButtonHtml}</h2></div>\n`;
|
||||
}
|
||||
if (version.Container) {
|
||||
html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}<br/>`;
|
||||
}
|
||||
if (version.Formats && version.Formats.length) {
|
||||
html += `${createAttribute(globalize.translate('MediaInfoFormat'), version.Formats.join(','))}<br/>`;
|
||||
}
|
||||
if (version.Path && user && user.Policy.IsAdministrator) {
|
||||
html += `${createAttribute(globalize.translate('MediaInfoPath'), version.Path, true)}<br/>`;
|
||||
}
|
||||
if (version.Size) {
|
||||
const size = `${(version.Size / (1024 * 1024)).toFixed(0)} MB`;
|
||||
html += `${createAttribute(globalize.translate('MediaInfoSize'), size)}<br/>`;
|
||||
}
|
||||
version.MediaStreams.sort(itemHelper.sortTracks);
|
||||
for (const stream of version.MediaStreams) {
|
||||
if (stream.Type === 'Data') {
|
||||
continue;
|
||||
}
|
||||
for (const btn of mediaInfoContent.querySelectorAll('.btnCopy')) {
|
||||
btn.addEventListener('click', () => {
|
||||
const infoBlock = dom.parentWithClass(btn, 'mediaInfoStream') || dom.parentWithClass(btn, 'mediaInfoSource') || mediaInfoContent;
|
||||
|
||||
html += '<div class="mediaInfoStream">';
|
||||
let translateString;
|
||||
switch (stream.Type) {
|
||||
case 'Audio':
|
||||
case 'Data':
|
||||
case 'Subtitle':
|
||||
case 'Video':
|
||||
translateString = stream.Type;
|
||||
break;
|
||||
case 'EmbeddedImage':
|
||||
translateString = 'Image';
|
||||
break;
|
||||
}
|
||||
|
||||
const displayType = globalize.translate(translateString);
|
||||
html += `\n<h2 class="mediaInfoStreamType">${displayType}${copyButtonHtml}</h2>\n`;
|
||||
const attributes = [];
|
||||
if (stream.DisplayTitle) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
|
||||
}
|
||||
if (stream.Language && stream.Type !== 'Video') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoLanguage'), stream.Language));
|
||||
}
|
||||
if (stream.Codec) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoCodec'), stream.Codec.toUpperCase()));
|
||||
}
|
||||
if (stream.CodecTag) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoCodecTag'), stream.CodecTag));
|
||||
}
|
||||
if (stream.IsAVC != null) {
|
||||
attributes.push(createAttribute('AVC', (stream.IsAVC ? 'Yes' : 'No')));
|
||||
}
|
||||
if (stream.Profile) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoProfile'), stream.Profile));
|
||||
}
|
||||
if (stream.Level > 0) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoLevel'), stream.Level));
|
||||
}
|
||||
if (stream.Width || stream.Height) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoResolution'), `${stream.Width}x${stream.Height}`));
|
||||
}
|
||||
if (stream.AspectRatio && stream.Codec !== 'mjpeg') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoAspectRatio'), stream.AspectRatio));
|
||||
}
|
||||
if (stream.Type === 'Video') {
|
||||
if (stream.IsAnamorphic != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoAnamorphic'), (stream.IsAnamorphic ? 'Yes' : 'No')));
|
||||
}
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoInterlaced'), (stream.IsInterlaced ? 'Yes' : 'No')));
|
||||
}
|
||||
if ((stream.AverageFrameRate || stream.RealFrameRate) && stream.Type === 'Video') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoFramerate'), (stream.AverageFrameRate || stream.RealFrameRate)));
|
||||
}
|
||||
if (stream.ChannelLayout) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoLayout'), stream.ChannelLayout));
|
||||
}
|
||||
if (stream.Channels) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoChannels'), `${stream.Channels} ch`));
|
||||
}
|
||||
if (stream.BitRate) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoBitrate'), `${parseInt(stream.BitRate / 1000, 10)} kbps`));
|
||||
}
|
||||
if (stream.SampleRate) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoSampleRate'), `${stream.SampleRate} Hz`));
|
||||
}
|
||||
if (stream.BitDepth) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoBitDepth'), `${stream.BitDepth} bit`));
|
||||
}
|
||||
if (stream.VideoRange) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRange'), stream.VideoRange));
|
||||
}
|
||||
if (stream.VideoRangeType) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRangeType'), stream.VideoRangeType));
|
||||
}
|
||||
if (stream.VideoDoViTitle) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDoViTitle'), stream.VideoDoViTitle));
|
||||
if (stream.DvVersionMajor != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMajor'), stream.DvVersionMajor));
|
||||
}
|
||||
if (stream.DvVersionMinor != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMinor'), stream.DvVersionMinor));
|
||||
}
|
||||
if (stream.DvProfile != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvProfile'), stream.DvProfile));
|
||||
}
|
||||
if (stream.DvLevel != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvLevel'), stream.DvLevel));
|
||||
}
|
||||
if (stream.RpuPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoRpuPresentFlag'), stream.RpuPresentFlag));
|
||||
}
|
||||
if (stream.ElPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoElPresentFlag'), stream.ElPresentFlag));
|
||||
}
|
||||
if (stream.BlPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoBlPresentFlag'), stream.BlPresentFlag));
|
||||
}
|
||||
if (stream.DvBlSignalCompatibilityId != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvBlSignalCompatibilityId'), stream.DvBlSignalCompatibilityId));
|
||||
}
|
||||
}
|
||||
if (stream.ColorSpace) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoColorSpace'), stream.ColorSpace));
|
||||
}
|
||||
if (stream.ColorTransfer) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoColorTransfer'), stream.ColorTransfer));
|
||||
}
|
||||
if (stream.ColorPrimaries) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoColorPrimaries'), stream.ColorPrimaries));
|
||||
}
|
||||
if (stream.PixelFormat) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoPixelFormat'), stream.PixelFormat));
|
||||
}
|
||||
if (stream.RefFrames) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoRefFrames'), stream.RefFrames));
|
||||
}
|
||||
if (stream.NalLengthSize) {
|
||||
attributes.push(createAttribute('NAL', stream.NalLengthSize));
|
||||
}
|
||||
if (stream.Type === 'Subtitle' || stream.Type === 'Audio') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No')));
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No')));
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No')));
|
||||
}
|
||||
if (stream.Type === 'Video' && version.Timestamp) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoTimestamp'), version.Timestamp));
|
||||
}
|
||||
html += attributes.join('<br/>');
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// File Paths should be always ltr. The isLtr parameter allows this.
|
||||
function createAttribute(label, value, isLtr) {
|
||||
return `<span class="mediaInfoLabel">${label}</span>${attributeDelimiterHtml}<span class="mediaInfoAttribute" ${isLtr && 'dir="ltr"'}>${escapeHtml(value)}</span>\n`;
|
||||
}
|
||||
|
||||
function loadMediaInfo(itemId, serverId) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(item => {
|
||||
const dialogOptions = {
|
||||
size: 'small',
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
}
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
dlg.classList.add('formDialog');
|
||||
let html = '';
|
||||
html += globalize.translateHtml(template, 'core');
|
||||
dlg.innerHTML = html;
|
||||
if (layoutManager.tv) {
|
||||
dlg.querySelector('.formDialogContent');
|
||||
}
|
||||
dialogHelper.open(dlg);
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
copy(infoBlock.textContent).then(() => {
|
||||
toast(globalize.translate('Copied'));
|
||||
}).catch(() => {
|
||||
console.error('Could not copy text');
|
||||
toast(globalize.translate('CopyFailed'));
|
||||
});
|
||||
apiClient.getCurrentUser().then(user => {
|
||||
setMediaInfo(user, dlg, item);
|
||||
});
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function show(itemId, serverId) {
|
||||
loading.show();
|
||||
return loadMediaInfo(itemId, serverId);
|
||||
function getMediaSourceHtml(user, item, version) {
|
||||
let html = '<div class="mediaInfoSource">';
|
||||
if (version.Name) {
|
||||
html += `<div><h2 class="mediaInfoStreamType">${escapeHtml(version.Name)}${copyButtonHtml}</h2></div>\n`;
|
||||
}
|
||||
if (version.Container) {
|
||||
html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}<br/>`;
|
||||
}
|
||||
if (version.Formats && version.Formats.length) {
|
||||
html += `${createAttribute(globalize.translate('MediaInfoFormat'), version.Formats.join(','))}<br/>`;
|
||||
}
|
||||
if (version.Path && user && user.Policy.IsAdministrator) {
|
||||
html += `${createAttribute(globalize.translate('MediaInfoPath'), version.Path, true)}<br/>`;
|
||||
}
|
||||
if (version.Size) {
|
||||
const size = `${(version.Size / (1024 * 1024)).toFixed(0)} MB`;
|
||||
html += `${createAttribute(globalize.translate('MediaInfoSize'), size)}<br/>`;
|
||||
}
|
||||
version.MediaStreams.sort(itemHelper.sortTracks);
|
||||
for (const stream of version.MediaStreams) {
|
||||
if (stream.Type === 'Data') {
|
||||
continue;
|
||||
}
|
||||
|
||||
html += '<div class="mediaInfoStream">';
|
||||
let translateString;
|
||||
switch (stream.Type) {
|
||||
case 'Audio':
|
||||
case 'Data':
|
||||
case 'Subtitle':
|
||||
case 'Video':
|
||||
translateString = stream.Type;
|
||||
break;
|
||||
case 'EmbeddedImage':
|
||||
translateString = 'Image';
|
||||
break;
|
||||
}
|
||||
|
||||
const displayType = globalize.translate(translateString);
|
||||
html += `\n<h2 class="mediaInfoStreamType">${displayType}${copyButtonHtml}</h2>\n`;
|
||||
const attributes = [];
|
||||
if (stream.DisplayTitle) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
|
||||
}
|
||||
if (stream.Language && stream.Type !== 'Video') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoLanguage'), stream.Language));
|
||||
}
|
||||
if (stream.Codec) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoCodec'), stream.Codec.toUpperCase()));
|
||||
}
|
||||
if (stream.CodecTag) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoCodecTag'), stream.CodecTag));
|
||||
}
|
||||
if (stream.IsAVC != null) {
|
||||
attributes.push(createAttribute('AVC', (stream.IsAVC ? 'Yes' : 'No')));
|
||||
}
|
||||
if (stream.Profile) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoProfile'), stream.Profile));
|
||||
}
|
||||
if (stream.Level > 0) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoLevel'), stream.Level));
|
||||
}
|
||||
if (stream.Width || stream.Height) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoResolution'), `${stream.Width}x${stream.Height}`));
|
||||
}
|
||||
if (stream.AspectRatio && stream.Codec !== 'mjpeg') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoAspectRatio'), stream.AspectRatio));
|
||||
}
|
||||
if (stream.Type === 'Video') {
|
||||
if (stream.IsAnamorphic != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoAnamorphic'), (stream.IsAnamorphic ? 'Yes' : 'No')));
|
||||
}
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoInterlaced'), (stream.IsInterlaced ? 'Yes' : 'No')));
|
||||
}
|
||||
if ((stream.AverageFrameRate || stream.RealFrameRate) && stream.Type === 'Video') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoFramerate'), (stream.AverageFrameRate || stream.RealFrameRate)));
|
||||
}
|
||||
if (stream.ChannelLayout) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoLayout'), stream.ChannelLayout));
|
||||
}
|
||||
if (stream.Channels) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoChannels'), `${stream.Channels} ch`));
|
||||
}
|
||||
if (stream.BitRate) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoBitrate'), `${parseInt(stream.BitRate / 1000, 10)} kbps`));
|
||||
}
|
||||
if (stream.SampleRate) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoSampleRate'), `${stream.SampleRate} Hz`));
|
||||
}
|
||||
if (stream.BitDepth) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoBitDepth'), `${stream.BitDepth} bit`));
|
||||
}
|
||||
if (stream.VideoRange) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRange'), stream.VideoRange));
|
||||
}
|
||||
if (stream.VideoRangeType) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRangeType'), stream.VideoRangeType));
|
||||
}
|
||||
if (stream.VideoDoViTitle) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDoViTitle'), stream.VideoDoViTitle));
|
||||
if (stream.DvVersionMajor != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMajor'), stream.DvVersionMajor));
|
||||
}
|
||||
if (stream.DvVersionMinor != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMinor'), stream.DvVersionMinor));
|
||||
}
|
||||
if (stream.DvProfile != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvProfile'), stream.DvProfile));
|
||||
}
|
||||
if (stream.DvLevel != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvLevel'), stream.DvLevel));
|
||||
}
|
||||
if (stream.RpuPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoRpuPresentFlag'), stream.RpuPresentFlag));
|
||||
}
|
||||
if (stream.ElPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoElPresentFlag'), stream.ElPresentFlag));
|
||||
}
|
||||
if (stream.BlPresentFlag != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoBlPresentFlag'), stream.BlPresentFlag));
|
||||
}
|
||||
if (stream.DvBlSignalCompatibilityId != null) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDvBlSignalCompatibilityId'), stream.DvBlSignalCompatibilityId));
|
||||
}
|
||||
}
|
||||
if (stream.ColorSpace) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoColorSpace'), stream.ColorSpace));
|
||||
}
|
||||
if (stream.ColorTransfer) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoColorTransfer'), stream.ColorTransfer));
|
||||
}
|
||||
if (stream.ColorPrimaries) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoColorPrimaries'), stream.ColorPrimaries));
|
||||
}
|
||||
if (stream.PixelFormat) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoPixelFormat'), stream.PixelFormat));
|
||||
}
|
||||
if (stream.RefFrames) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoRefFrames'), stream.RefFrames));
|
||||
}
|
||||
if (stream.NalLengthSize) {
|
||||
attributes.push(createAttribute('NAL', stream.NalLengthSize));
|
||||
}
|
||||
if (stream.Type === 'Subtitle' || stream.Type === 'Audio') {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No')));
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No')));
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No')));
|
||||
}
|
||||
if (stream.Type === 'Video' && version.Timestamp) {
|
||||
attributes.push(createAttribute(globalize.translate('MediaInfoTimestamp'), version.Timestamp));
|
||||
}
|
||||
html += attributes.join('<br/>');
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// File Paths should be always ltr. The isLtr parameter allows this.
|
||||
function createAttribute(label, value, isLtr) {
|
||||
return `<span class="mediaInfoLabel">${label}</span>${attributeDelimiterHtml}<span class="mediaInfoAttribute" ${isLtr && 'dir="ltr"'}>${escapeHtml(value)}</span>\n`;
|
||||
}
|
||||
|
||||
function loadMediaInfo(itemId, serverId) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(item => {
|
||||
const dialogOptions = {
|
||||
size: 'small',
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
}
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
dlg.classList.add('formDialog');
|
||||
let html = '';
|
||||
html += globalize.translateHtml(template, 'core');
|
||||
dlg.innerHTML = html;
|
||||
if (layoutManager.tv) {
|
||||
dlg.querySelector('.formDialogContent');
|
||||
}
|
||||
dialogHelper.open(dlg);
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
apiClient.getCurrentUser().then(user => {
|
||||
setMediaInfo(user, dlg, item);
|
||||
});
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
export function show(itemId, serverId) {
|
||||
loading.show();
|
||||
return loadMediaInfo(itemId, serverId);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default {
|
||||
show: show
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for itemidentifier media item.
|
||||
@@ -24,388 +23,314 @@ import toast from '../toast/toast';
|
||||
import template from './itemidentifier.template.html';
|
||||
import datetime from '../../scripts/datetime';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
let currentItem;
|
||||
let currentItemType;
|
||||
let currentServerId;
|
||||
let currentResolve;
|
||||
let currentReject;
|
||||
let hasChanges = false;
|
||||
let currentSearchResult;
|
||||
let currentItem;
|
||||
let currentItemType;
|
||||
let currentServerId;
|
||||
let currentResolve;
|
||||
let currentReject;
|
||||
let hasChanges = false;
|
||||
let currentSearchResult;
|
||||
|
||||
function getApiClient() {
|
||||
return ServerConnections.getApiClient(currentServerId);
|
||||
function getApiClient() {
|
||||
return ServerConnections.getApiClient(currentServerId);
|
||||
}
|
||||
|
||||
function searchForIdentificationResults(page) {
|
||||
let lookupInfo = {
|
||||
ProviderIds: {}
|
||||
};
|
||||
|
||||
let i;
|
||||
let length;
|
||||
const identifyField = page.querySelectorAll('.identifyField');
|
||||
let value;
|
||||
for (i = 0, length = identifyField.length; i < length; i++) {
|
||||
value = identifyField[i].value;
|
||||
|
||||
if (value) {
|
||||
if (identifyField[i].type === 'number') {
|
||||
value = parseInt(value, 10);
|
||||
}
|
||||
|
||||
lookupInfo[identifyField[i].getAttribute('data-lookup')] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function searchForIdentificationResults(page) {
|
||||
let lookupInfo = {
|
||||
ProviderIds: {}
|
||||
};
|
||||
let hasId = false;
|
||||
|
||||
let i;
|
||||
let length;
|
||||
const identifyField = page.querySelectorAll('.identifyField');
|
||||
let value;
|
||||
for (i = 0, length = identifyField.length; i < length; i++) {
|
||||
value = identifyField[i].value;
|
||||
const txtLookupId = page.querySelectorAll('.txtLookupId');
|
||||
for (i = 0, length = txtLookupId.length; i < length; i++) {
|
||||
value = txtLookupId[i].value;
|
||||
|
||||
if (value) {
|
||||
if (identifyField[i].type === 'number') {
|
||||
value = parseInt(value, 10);
|
||||
}
|
||||
|
||||
lookupInfo[identifyField[i].getAttribute('data-lookup')] = value;
|
||||
}
|
||||
if (value) {
|
||||
hasId = true;
|
||||
}
|
||||
lookupInfo.ProviderIds[txtLookupId[i].getAttribute('data-providerkey')] = value;
|
||||
}
|
||||
|
||||
let hasId = false;
|
||||
if (!hasId && !lookupInfo.Name) {
|
||||
toast(globalize.translate('PleaseEnterNameOrId'));
|
||||
return;
|
||||
}
|
||||
|
||||
const txtLookupId = page.querySelectorAll('.txtLookupId');
|
||||
for (i = 0, length = txtLookupId.length; i < length; i++) {
|
||||
value = txtLookupId[i].value;
|
||||
lookupInfo = {
|
||||
SearchInfo: lookupInfo
|
||||
};
|
||||
|
||||
if (value) {
|
||||
hasId = true;
|
||||
}
|
||||
lookupInfo.ProviderIds[txtLookupId[i].getAttribute('data-providerkey')] = value;
|
||||
}
|
||||
if (currentItem && currentItem.Id) {
|
||||
lookupInfo.ItemId = currentItem.Id;
|
||||
} else {
|
||||
lookupInfo.IncludeDisabledProviders = true;
|
||||
}
|
||||
|
||||
if (!hasId && !lookupInfo.Name) {
|
||||
toast(globalize.translate('PleaseEnterNameOrId'));
|
||||
return;
|
||||
}
|
||||
loading.show();
|
||||
|
||||
lookupInfo = {
|
||||
SearchInfo: lookupInfo
|
||||
};
|
||||
const apiClient = getApiClient();
|
||||
|
||||
if (currentItem && currentItem.Id) {
|
||||
lookupInfo.ItemId = currentItem.Id;
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: apiClient.getUrl(`Items/RemoteSearch/${currentItemType}`),
|
||||
data: JSON.stringify(lookupInfo),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json'
|
||||
|
||||
}).then(results => {
|
||||
loading.hide();
|
||||
showIdentificationSearchResults(page, results);
|
||||
});
|
||||
}
|
||||
|
||||
function showIdentificationSearchResults(page, results) {
|
||||
const identificationSearchResults = page.querySelector('.identificationSearchResults');
|
||||
|
||||
page.querySelector('.popupIdentifyForm').classList.add('hide');
|
||||
identificationSearchResults.classList.remove('hide');
|
||||
page.querySelector('.identifyOptionsForm').classList.add('hide');
|
||||
page.querySelector('.dialogContentInner').classList.remove('dialog-content-centered');
|
||||
|
||||
let html = '';
|
||||
let i;
|
||||
let length;
|
||||
for (i = 0, length = results.length; i < length; i++) {
|
||||
const result = results[i];
|
||||
html += getSearchResultHtml(result, i);
|
||||
}
|
||||
|
||||
const elem = page.querySelector('.identificationSearchResultList');
|
||||
elem.innerHTML = html;
|
||||
|
||||
function onSearchImageClick() {
|
||||
const index = parseInt(this.getAttribute('data-index'), 10);
|
||||
|
||||
const currentResult = results[index];
|
||||
|
||||
if (currentItem != null) {
|
||||
showIdentifyOptions(page, currentResult);
|
||||
} else {
|
||||
lookupInfo.IncludeDisabledProviders = true;
|
||||
}
|
||||
|
||||
loading.show();
|
||||
|
||||
const apiClient = getApiClient();
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: apiClient.getUrl(`Items/RemoteSearch/${currentItemType}`),
|
||||
data: JSON.stringify(lookupInfo),
|
||||
contentType: 'application/json',
|
||||
dataType: 'json'
|
||||
|
||||
}).then(results => {
|
||||
loading.hide();
|
||||
showIdentificationSearchResults(page, results);
|
||||
});
|
||||
}
|
||||
|
||||
function showIdentificationSearchResults(page, results) {
|
||||
const identificationSearchResults = page.querySelector('.identificationSearchResults');
|
||||
|
||||
page.querySelector('.popupIdentifyForm').classList.add('hide');
|
||||
identificationSearchResults.classList.remove('hide');
|
||||
page.querySelector('.identifyOptionsForm').classList.add('hide');
|
||||
page.querySelector('.dialogContentInner').classList.remove('dialog-content-centered');
|
||||
|
||||
let html = '';
|
||||
let i;
|
||||
let length;
|
||||
for (i = 0, length = results.length; i < length; i++) {
|
||||
const result = results[i];
|
||||
html += getSearchResultHtml(result, i);
|
||||
}
|
||||
|
||||
const elem = page.querySelector('.identificationSearchResultList');
|
||||
elem.innerHTML = html;
|
||||
|
||||
function onSearchImageClick() {
|
||||
const index = parseInt(this.getAttribute('data-index'), 10);
|
||||
|
||||
const currentResult = results[index];
|
||||
|
||||
if (currentItem != null) {
|
||||
showIdentifyOptions(page, currentResult);
|
||||
} else {
|
||||
finishFindNewDialog(page, currentResult);
|
||||
}
|
||||
}
|
||||
|
||||
const searchImages = elem.querySelectorAll('.card');
|
||||
for (i = 0, length = searchImages.length; i < length; i++) {
|
||||
searchImages[i].addEventListener('click', onSearchImageClick);
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
focusManager.autoFocus(identificationSearchResults);
|
||||
finishFindNewDialog(page, currentResult);
|
||||
}
|
||||
}
|
||||
|
||||
function finishFindNewDialog(dlg, identifyResult) {
|
||||
currentSearchResult = identifyResult;
|
||||
const searchImages = elem.querySelectorAll('.card');
|
||||
for (i = 0, length = searchImages.length; i < length; i++) {
|
||||
searchImages[i].addEventListener('click', onSearchImageClick);
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
focusManager.autoFocus(identificationSearchResults);
|
||||
}
|
||||
}
|
||||
|
||||
function finishFindNewDialog(dlg, identifyResult) {
|
||||
currentSearchResult = identifyResult;
|
||||
hasChanges = true;
|
||||
loading.hide();
|
||||
|
||||
dialogHelper.close(dlg);
|
||||
}
|
||||
|
||||
function showIdentifyOptions(page, identifyResult) {
|
||||
const identifyOptionsForm = page.querySelector('.identifyOptionsForm');
|
||||
|
||||
page.querySelector('.popupIdentifyForm').classList.add('hide');
|
||||
page.querySelector('.identificationSearchResults').classList.add('hide');
|
||||
identifyOptionsForm.classList.remove('hide');
|
||||
page.querySelector('#chkIdentifyReplaceImages').checked = true;
|
||||
page.querySelector('.dialogContentInner').classList.add('dialog-content-centered');
|
||||
|
||||
currentSearchResult = identifyResult;
|
||||
|
||||
const lines = [];
|
||||
lines.push(escapeHtml(identifyResult.Name));
|
||||
|
||||
if (identifyResult.ProductionYear) {
|
||||
lines.push(datetime.toLocaleString(identifyResult.ProductionYear, { useGrouping: false }));
|
||||
}
|
||||
|
||||
let resultHtml = lines.join('<br/>');
|
||||
|
||||
if (identifyResult.ImageUrl) {
|
||||
resultHtml = `<div style="display:flex;align-items:center;"><img src="${identifyResult.ImageUrl}" style="max-height:240px;" /><div style="margin-left:1em;">${resultHtml}</div>`;
|
||||
}
|
||||
|
||||
page.querySelector('.selectedSearchResult').innerHTML = resultHtml;
|
||||
|
||||
focusManager.focus(identifyOptionsForm.querySelector('.btnSubmit'));
|
||||
}
|
||||
|
||||
function getSearchResultHtml(result, index) {
|
||||
// TODO move card creation code to Card component
|
||||
|
||||
let html = '';
|
||||
let cssClass = 'card scalableCard';
|
||||
let cardBoxCssClass = 'cardBox';
|
||||
let padderClass;
|
||||
|
||||
if (currentItemType === 'Episode') {
|
||||
cssClass += ' backdropCard backdropCard-scalable';
|
||||
padderClass = 'cardPadder-backdrop';
|
||||
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
|
||||
cssClass += ' squareCard squareCard-scalable';
|
||||
padderClass = 'cardPadder-square';
|
||||
} else {
|
||||
cssClass += ' portraitCard portraitCard-scalable';
|
||||
padderClass = 'cardPadder-portrait';
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' show-focus';
|
||||
|
||||
if (enableFocusTransform) {
|
||||
cssClass += ' show-animation';
|
||||
}
|
||||
}
|
||||
|
||||
cardBoxCssClass += ' cardBox-bottompadded';
|
||||
|
||||
html += `<button type="button" class="${cssClass}" data-index="${index}">`;
|
||||
html += `<div class="${cardBoxCssClass}">`;
|
||||
html += '<div class="cardScalable">';
|
||||
html += `<div class="${padderClass}"></div>`;
|
||||
|
||||
html += '<div class="cardContent searchImage">';
|
||||
|
||||
if (result.ImageUrl) {
|
||||
html += `<div class="cardImageContainer coveredImage" style="background-image:url('${result.ImageUrl}');"></div>`;
|
||||
} else {
|
||||
html += `<div class="cardImageContainer coveredImage defaultCardBackground defaultCardBackground1"><div class="cardText cardCenteredText">${escapeHtml(result.Name)}</div></div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
let numLines = 3;
|
||||
if (currentItemType === 'MusicAlbum') {
|
||||
numLines++;
|
||||
}
|
||||
|
||||
const lines = [result.Name];
|
||||
|
||||
lines.push(result.SearchProviderName);
|
||||
|
||||
if (result.AlbumArtist) {
|
||||
lines.push(result.AlbumArtist.Name);
|
||||
}
|
||||
if (result.ProductionYear) {
|
||||
lines.push(result.ProductionYear);
|
||||
}
|
||||
|
||||
for (let i = 0; i < numLines; i++) {
|
||||
if (i === 0) {
|
||||
html += '<div class="cardText cardText-first cardTextCentered">';
|
||||
} else {
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
}
|
||||
html += escapeHtml(lines[i] || '') || ' ';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</button>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function submitIdentficationResult(page) {
|
||||
loading.show();
|
||||
|
||||
const options = {
|
||||
ReplaceAllImages: page.querySelector('#chkIdentifyReplaceImages').checked
|
||||
};
|
||||
|
||||
const apiClient = getApiClient();
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: apiClient.getUrl(`Items/RemoteSearch/Apply/${currentItem.Id}`, options),
|
||||
data: JSON.stringify(currentSearchResult),
|
||||
contentType: 'application/json'
|
||||
|
||||
}).then(() => {
|
||||
hasChanges = true;
|
||||
loading.hide();
|
||||
|
||||
dialogHelper.close(dlg);
|
||||
}
|
||||
dialogHelper.close(page);
|
||||
}, () => {
|
||||
loading.hide();
|
||||
|
||||
function showIdentifyOptions(page, identifyResult) {
|
||||
const identifyOptionsForm = page.querySelector('.identifyOptionsForm');
|
||||
dialogHelper.close(page);
|
||||
});
|
||||
}
|
||||
|
||||
page.querySelector('.popupIdentifyForm').classList.add('hide');
|
||||
page.querySelector('.identificationSearchResults').classList.add('hide');
|
||||
identifyOptionsForm.classList.remove('hide');
|
||||
page.querySelector('#chkIdentifyReplaceImages').checked = true;
|
||||
page.querySelector('.dialogContentInner').classList.add('dialog-content-centered');
|
||||
|
||||
currentSearchResult = identifyResult;
|
||||
|
||||
const lines = [];
|
||||
lines.push(escapeHtml(identifyResult.Name));
|
||||
|
||||
if (identifyResult.ProductionYear) {
|
||||
lines.push(datetime.toLocaleString(identifyResult.ProductionYear, {useGrouping: false}));
|
||||
}
|
||||
|
||||
let resultHtml = lines.join('<br/>');
|
||||
|
||||
if (identifyResult.ImageUrl) {
|
||||
resultHtml = `<div style="display:flex;align-items:center;"><img src="${identifyResult.ImageUrl}" style="max-height:240px;" /><div style="margin-left:1em;">${resultHtml}</div>`;
|
||||
}
|
||||
|
||||
page.querySelector('.selectedSearchResult').innerHTML = resultHtml;
|
||||
|
||||
focusManager.focus(identifyOptionsForm.querySelector('.btnSubmit'));
|
||||
}
|
||||
|
||||
function getSearchResultHtml(result, index) {
|
||||
// TODO move card creation code to Card component
|
||||
function showIdentificationForm(page, item) {
|
||||
const apiClient = getApiClient();
|
||||
|
||||
apiClient.getJSON(apiClient.getUrl(`Items/${item.Id}/ExternalIdInfos`)).then(idList => {
|
||||
let html = '';
|
||||
let cssClass = 'card scalableCard';
|
||||
let cardBoxCssClass = 'cardBox';
|
||||
let padderClass;
|
||||
|
||||
if (currentItemType === 'Episode') {
|
||||
cssClass += ' backdropCard backdropCard-scalable';
|
||||
padderClass = 'cardPadder-backdrop';
|
||||
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
|
||||
cssClass += ' squareCard squareCard-scalable';
|
||||
padderClass = 'cardPadder-square';
|
||||
} else {
|
||||
cssClass += ' portraitCard portraitCard-scalable';
|
||||
padderClass = 'cardPadder-portrait';
|
||||
}
|
||||
for (let i = 0, length = idList.length; i < length; i++) {
|
||||
const idInfo = idList[i];
|
||||
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' show-focus';
|
||||
const id = `txtLookup${idInfo.Key}`;
|
||||
|
||||
if (enableFocusTransform) {
|
||||
cssClass += ' show-animation';
|
||||
html += '<div class="inputContainer">';
|
||||
|
||||
let fullName = idInfo.Name;
|
||||
if (idInfo.Type) {
|
||||
fullName = `${idInfo.Name} ${globalize.translate(idInfo.Type)}`;
|
||||
}
|
||||
}
|
||||
|
||||
cardBoxCssClass += ' cardBox-bottompadded';
|
||||
const idLabel = globalize.translate('LabelDynamicExternalId', escapeHtml(fullName));
|
||||
|
||||
html += `<button type="button" class="${cssClass}" data-index="${index}">`;
|
||||
html += `<div class="${cardBoxCssClass}">`;
|
||||
html += '<div class="cardScalable">';
|
||||
html += `<div class="${padderClass}"></div>`;
|
||||
html += `<input is="emby-input" class="txtLookupId" data-providerkey="${idInfo.Key}" id="${id}" label="${idLabel}"/>`;
|
||||
|
||||
html += '<div class="cardContent searchImage">';
|
||||
|
||||
if (result.ImageUrl) {
|
||||
html += `<div class="cardImageContainer coveredImage" style="background-image:url('${result.ImageUrl}');"></div>`;
|
||||
} else {
|
||||
html += `<div class="cardImageContainer coveredImage defaultCardBackground defaultCardBackground1"><div class="cardText cardCenteredText">${escapeHtml(result.Name)}</div></div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
let numLines = 3;
|
||||
if (currentItemType === 'MusicAlbum') {
|
||||
numLines++;
|
||||
}
|
||||
|
||||
const lines = [result.Name];
|
||||
|
||||
lines.push(result.SearchProviderName);
|
||||
|
||||
if (result.AlbumArtist) {
|
||||
lines.push(result.AlbumArtist.Name);
|
||||
}
|
||||
if (result.ProductionYear) {
|
||||
lines.push(result.ProductionYear);
|
||||
}
|
||||
|
||||
for (let i = 0; i < numLines; i++) {
|
||||
if (i === 0) {
|
||||
html += '<div class="cardText cardText-first cardTextCentered">';
|
||||
} else {
|
||||
html += '<div class="cardText cardText-secondary cardTextCentered">';
|
||||
}
|
||||
html += escapeHtml(lines[i] || '') || ' ';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</button>';
|
||||
return html;
|
||||
}
|
||||
page.querySelector('#txtLookupName').value = '';
|
||||
|
||||
function submitIdentficationResult(page) {
|
||||
loading.show();
|
||||
|
||||
const options = {
|
||||
ReplaceAllImages: page.querySelector('#chkIdentifyReplaceImages').checked
|
||||
};
|
||||
|
||||
const apiClient = getApiClient();
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: apiClient.getUrl(`Items/RemoteSearch/Apply/${currentItem.Id}`, options),
|
||||
data: JSON.stringify(currentSearchResult),
|
||||
contentType: 'application/json'
|
||||
|
||||
}).then(() => {
|
||||
hasChanges = true;
|
||||
loading.hide();
|
||||
|
||||
dialogHelper.close(page);
|
||||
}, () => {
|
||||
loading.hide();
|
||||
|
||||
dialogHelper.close(page);
|
||||
});
|
||||
}
|
||||
|
||||
function showIdentificationForm(page, item) {
|
||||
const apiClient = getApiClient();
|
||||
|
||||
apiClient.getJSON(apiClient.getUrl(`Items/${item.Id}/ExternalIdInfos`)).then(idList => {
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = idList.length; i < length; i++) {
|
||||
const idInfo = idList[i];
|
||||
|
||||
const id = `txtLookup${idInfo.Key}`;
|
||||
|
||||
html += '<div class="inputContainer">';
|
||||
|
||||
let fullName = idInfo.Name;
|
||||
if (idInfo.Type) {
|
||||
fullName = `${idInfo.Name} ${globalize.translate(idInfo.Type)}`;
|
||||
}
|
||||
|
||||
const idLabel = globalize.translate('LabelDynamicExternalId', escapeHtml(fullName));
|
||||
|
||||
html += `<input is="emby-input" class="txtLookupId" data-providerkey="${idInfo.Key}" id="${id}" label="${idLabel}"/>`;
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
page.querySelector('#txtLookupName').value = '';
|
||||
|
||||
if (item.Type === 'Person' || item.Type === 'BoxSet') {
|
||||
page.querySelector('.fldLookupYear').classList.add('hide');
|
||||
page.querySelector('#txtLookupYear').value = '';
|
||||
} else {
|
||||
page.querySelector('.fldLookupYear').classList.remove('hide');
|
||||
page.querySelector('#txtLookupYear').value = '';
|
||||
}
|
||||
|
||||
page.querySelector('.identifyProviderIds').innerHTML = html;
|
||||
|
||||
page.querySelector('.formDialogHeaderTitle').innerHTML = globalize.translate('Identify');
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor(itemId) {
|
||||
loading.show();
|
||||
|
||||
const apiClient = getApiClient();
|
||||
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(item => {
|
||||
currentItem = item;
|
||||
currentItemType = currentItem.Type;
|
||||
|
||||
const dialogOptions = {
|
||||
size: 'small',
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
dlg.classList.add('recordingDialog');
|
||||
|
||||
let html = '';
|
||||
html += globalize.translateHtml(template, 'core');
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
// Has to be assigned a z-index after the call to .open()
|
||||
dlg.addEventListener('close', onDialogClosed);
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false);
|
||||
}
|
||||
|
||||
if (item.Path) {
|
||||
dlg.querySelector('.fldPath').classList.remove('hide');
|
||||
} else {
|
||||
dlg.querySelector('.fldPath').classList.add('hide');
|
||||
}
|
||||
|
||||
dlg.querySelector('.txtPath').innerText = item.Path || '';
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
dlg.querySelector('.popupIdentifyForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
searchForIdentificationResults(dlg);
|
||||
return false;
|
||||
});
|
||||
|
||||
dlg.querySelector('.identifyOptionsForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
submitIdentficationResult(dlg);
|
||||
return false;
|
||||
});
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
dlg.classList.add('identifyDialog');
|
||||
|
||||
showIdentificationForm(dlg, item);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
loading.hide();
|
||||
if (hasChanges) {
|
||||
currentResolve();
|
||||
if (item.Type === 'Person' || item.Type === 'BoxSet') {
|
||||
page.querySelector('.fldLookupYear').classList.add('hide');
|
||||
page.querySelector('#txtLookupYear').value = '';
|
||||
} else {
|
||||
currentReject();
|
||||
page.querySelector('.fldLookupYear').classList.remove('hide');
|
||||
page.querySelector('#txtLookupYear').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// TODO investigate where this was used
|
||||
function showEditorFindNew(itemName, itemYear, itemType, resolveFunc) {
|
||||
currentItem = null;
|
||||
currentItemType = itemType;
|
||||
page.querySelector('.identifyProviderIds').innerHTML = html;
|
||||
|
||||
page.querySelector('.formDialogHeaderTitle').innerHTML = globalize.translate('Identify');
|
||||
});
|
||||
}
|
||||
|
||||
function showEditor(itemId) {
|
||||
loading.show();
|
||||
|
||||
const apiClient = getApiClient();
|
||||
|
||||
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(item => {
|
||||
currentItem = item;
|
||||
currentItemType = currentItem.Type;
|
||||
|
||||
const dialogOptions = {
|
||||
size: 'small',
|
||||
@@ -427,15 +352,22 @@ import datetime from '../../scripts/datetime';
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
// Has to be assigned a z-index after the call to .open()
|
||||
dlg.addEventListener('close', onDialogClosed);
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false);
|
||||
}
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
if (item.Path) {
|
||||
dlg.querySelector('.fldPath').classList.remove('hide');
|
||||
} else {
|
||||
dlg.querySelector('.fldPath').classList.add('hide');
|
||||
}
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
dlg.querySelector('.txtPath').innerText = item.Path || '';
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
dlg.querySelector('.popupIdentifyForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
@@ -443,53 +375,119 @@ import datetime from '../../scripts/datetime';
|
||||
return false;
|
||||
});
|
||||
|
||||
dlg.addEventListener('close', () => {
|
||||
loading.hide();
|
||||
const foundItem = hasChanges ? currentSearchResult : null;
|
||||
dlg.querySelector('.identifyOptionsForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
submitIdentficationResult(dlg);
|
||||
return false;
|
||||
});
|
||||
|
||||
resolveFunc(foundItem);
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
dlg.classList.add('identifyDialog');
|
||||
|
||||
showIdentificationFormFindNew(dlg, itemName, itemYear, itemType);
|
||||
showIdentificationForm(dlg, item);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
loading.hide();
|
||||
if (hasChanges) {
|
||||
currentResolve();
|
||||
} else {
|
||||
currentReject();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO investigate where this was used
|
||||
function showEditorFindNew(itemName, itemYear, itemType, resolveFunc) {
|
||||
currentItem = null;
|
||||
currentItemType = itemType;
|
||||
|
||||
const dialogOptions = {
|
||||
size: 'small',
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
}
|
||||
|
||||
function showIdentificationFormFindNew(dlg, itemName, itemYear, itemType) {
|
||||
dlg.querySelector('#txtLookupName').value = itemName;
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
if (itemType === 'Person' || itemType === 'BoxSet') {
|
||||
dlg.querySelector('.fldLookupYear').classList.add('hide');
|
||||
dlg.querySelector('#txtLookupYear').value = '';
|
||||
} else {
|
||||
dlg.querySelector('.fldLookupYear').classList.remove('hide');
|
||||
dlg.querySelector('#txtLookupYear').value = itemYear;
|
||||
}
|
||||
dlg.classList.add('formDialog');
|
||||
dlg.classList.add('recordingDialog');
|
||||
|
||||
dlg.querySelector('.formDialogHeaderTitle').innerHTML = globalize.translate('Search');
|
||||
let html = '';
|
||||
html += globalize.translateHtml(template, 'core');
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
if (layoutManager.tv) {
|
||||
scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false);
|
||||
}
|
||||
|
||||
export function show(itemId, serverId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
currentResolve = resolve;
|
||||
currentReject = reject;
|
||||
currentServerId = serverId;
|
||||
hasChanges = false;
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
showEditor(itemId);
|
||||
});
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
dlg.querySelector('.popupIdentifyForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
searchForIdentificationResults(dlg);
|
||||
return false;
|
||||
});
|
||||
|
||||
dlg.addEventListener('close', () => {
|
||||
loading.hide();
|
||||
const foundItem = hasChanges ? currentSearchResult : null;
|
||||
|
||||
resolveFunc(foundItem);
|
||||
});
|
||||
|
||||
dlg.classList.add('identifyDialog');
|
||||
|
||||
showIdentificationFormFindNew(dlg, itemName, itemYear, itemType);
|
||||
}
|
||||
|
||||
function showIdentificationFormFindNew(dlg, itemName, itemYear, itemType) {
|
||||
dlg.querySelector('#txtLookupName').value = itemName;
|
||||
|
||||
if (itemType === 'Person' || itemType === 'BoxSet') {
|
||||
dlg.querySelector('.fldLookupYear').classList.add('hide');
|
||||
dlg.querySelector('#txtLookupYear').value = '';
|
||||
} else {
|
||||
dlg.querySelector('.fldLookupYear').classList.remove('hide');
|
||||
dlg.querySelector('#txtLookupYear').value = itemYear;
|
||||
}
|
||||
|
||||
export function showFindNew(itemName, itemYear, itemType, serverId) {
|
||||
return new Promise((resolve) => {
|
||||
currentServerId = serverId;
|
||||
dlg.querySelector('.formDialogHeaderTitle').innerHTML = globalize.translate('Search');
|
||||
}
|
||||
|
||||
hasChanges = false;
|
||||
showEditorFindNew(itemName, itemYear, itemType, resolve);
|
||||
});
|
||||
}
|
||||
export function show(itemId, serverId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
currentResolve = resolve;
|
||||
currentReject = reject;
|
||||
currentServerId = serverId;
|
||||
hasChanges = false;
|
||||
|
||||
showEditor(itemId);
|
||||
});
|
||||
}
|
||||
|
||||
export function showFindNew(itemName, itemYear, itemType, serverId) {
|
||||
return new Promise((resolve) => {
|
||||
currentServerId = serverId;
|
||||
|
||||
hasChanges = false;
|
||||
showEditorFindNew(itemName, itemYear, itemType, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default {
|
||||
show: show,
|
||||
showFindNew: showFindNew
|
||||
|
||||
@@ -1,69 +1,68 @@
|
||||
/* eslint-disable indent */
|
||||
export class LazyLoader {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
createObserver() {
|
||||
const callback = this.options.callback;
|
||||
export class LazyLoader {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach(entry => {
|
||||
callback(entry);
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: '50%',
|
||||
threshold: 0
|
||||
createObserver() {
|
||||
const callback = this.options.callback;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach(entry => {
|
||||
callback(entry);
|
||||
});
|
||||
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
addElements(elements) {
|
||||
let observer = this.observer;
|
||||
|
||||
if (!observer) {
|
||||
this.createObserver();
|
||||
observer = this.observer;
|
||||
}
|
||||
|
||||
Array.from(elements).forEach(element => {
|
||||
observer.observe(element);
|
||||
},
|
||||
{
|
||||
rootMargin: '50%',
|
||||
threshold: 0
|
||||
});
|
||||
}
|
||||
|
||||
destroyObserver() {
|
||||
const observer = this.observer;
|
||||
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.destroyObserver();
|
||||
this.options = null;
|
||||
}
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
function unveilElements(elements, root, callback) {
|
||||
if (!elements.length) {
|
||||
return;
|
||||
addElements(elements) {
|
||||
let observer = this.observer;
|
||||
|
||||
if (!observer) {
|
||||
this.createObserver();
|
||||
observer = this.observer;
|
||||
}
|
||||
const lazyLoader = new LazyLoader({
|
||||
callback: callback
|
||||
|
||||
Array.from(elements).forEach(element => {
|
||||
observer.observe(element);
|
||||
});
|
||||
lazyLoader.addElements(elements);
|
||||
}
|
||||
|
||||
export function lazyChildren(elem, callback) {
|
||||
unveilElements(elem.getElementsByClassName('lazy'), elem, callback);
|
||||
destroyObserver() {
|
||||
const observer = this.observer;
|
||||
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
destroy() {
|
||||
this.destroyObserver();
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
function unveilElements(elements, root, callback) {
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
const lazyLoader = new LazyLoader({
|
||||
callback: callback
|
||||
});
|
||||
lazyLoader.addElements(elements);
|
||||
}
|
||||
|
||||
export function lazyChildren(elem, callback) {
|
||||
unveilElements(elem.getElementsByClassName('lazy'), elem, callback);
|
||||
}
|
||||
|
||||
export default {
|
||||
LazyLoader: LazyLoader,
|
||||
lazyChildren: lazyChildren
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for display list view.
|
||||
@@ -18,484 +17,483 @@ import '../../elements/emby-ratingbutton/emby-ratingbutton';
|
||||
import '../../elements/emby-playstatebutton/emby-playstatebutton';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
function getIndex(item, options) {
|
||||
if (options.index === 'disc') {
|
||||
return item.ParentIndexNumber == null ? '' : globalize.translate('ValueDiscNumber', item.ParentIndexNumber);
|
||||
}
|
||||
|
||||
const sortBy = (options.sortBy || '').toLowerCase();
|
||||
let code;
|
||||
let name;
|
||||
|
||||
if (sortBy.indexOf('sortname') === 0) {
|
||||
if (item.Type === 'Episode') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// SortName
|
||||
name = (item.SortName || item.Name || '?')[0].toUpperCase();
|
||||
|
||||
code = name.charCodeAt(0);
|
||||
if (code < 65 || code > 90) {
|
||||
return '#';
|
||||
}
|
||||
|
||||
return name.toUpperCase();
|
||||
}
|
||||
if (sortBy.indexOf('officialrating') === 0) {
|
||||
return item.OfficialRating || globalize.translate('Unrated');
|
||||
}
|
||||
if (sortBy.indexOf('communityrating') === 0) {
|
||||
if (item.CommunityRating == null) {
|
||||
return globalize.translate('Unrated');
|
||||
}
|
||||
|
||||
return Math.floor(item.CommunityRating);
|
||||
}
|
||||
if (sortBy.indexOf('criticrating') === 0) {
|
||||
if (item.CriticRating == null) {
|
||||
return globalize.translate('Unrated');
|
||||
}
|
||||
|
||||
return Math.floor(item.CriticRating);
|
||||
}
|
||||
if (sortBy.indexOf('albumartist') === 0) {
|
||||
// SortName
|
||||
if (!item.AlbumArtist) {
|
||||
return '';
|
||||
}
|
||||
|
||||
name = item.AlbumArtist[0].toUpperCase();
|
||||
|
||||
code = name.charCodeAt(0);
|
||||
if (code < 65 || code > 90) {
|
||||
return '#';
|
||||
}
|
||||
|
||||
return name.toUpperCase();
|
||||
}
|
||||
return '';
|
||||
function getIndex(item, options) {
|
||||
if (options.index === 'disc') {
|
||||
return item.ParentIndexNumber == null ? '' : globalize.translate('ValueDiscNumber', item.ParentIndexNumber);
|
||||
}
|
||||
|
||||
function getImageUrl(item, size) {
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
let itemId;
|
||||
const sortBy = (options.sortBy || '').toLowerCase();
|
||||
let code;
|
||||
let name;
|
||||
|
||||
const options = {
|
||||
fillWidth: size,
|
||||
fillHeight: size,
|
||||
type: 'Primary'
|
||||
};
|
||||
|
||||
if (item.ImageTags && item.ImageTags.Primary) {
|
||||
options.tag = item.ImageTags.Primary;
|
||||
itemId = item.Id;
|
||||
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
|
||||
options.tag = item.AlbumPrimaryImageTag;
|
||||
itemId = item.AlbumId;
|
||||
} else if (item.SeriesId && item.SeriesPrimaryImageTag) {
|
||||
options.tag = item.SeriesPrimaryImageTag;
|
||||
itemId = item.SeriesId;
|
||||
} else if (item.ParentPrimaryImageTag) {
|
||||
options.tag = item.ParentPrimaryImageTag;
|
||||
itemId = item.ParentPrimaryImageItemId;
|
||||
if (sortBy.indexOf('sortname') === 0) {
|
||||
if (item.Type === 'Episode') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (itemId) {
|
||||
return apiClient.getScaledImageUrl(itemId, options);
|
||||
// SortName
|
||||
name = (item.SortName || item.Name || '?')[0].toUpperCase();
|
||||
|
||||
code = name.charCodeAt(0);
|
||||
if (code < 65 || code > 90) {
|
||||
return '#';
|
||||
}
|
||||
return null;
|
||||
|
||||
return name.toUpperCase();
|
||||
}
|
||||
if (sortBy.indexOf('officialrating') === 0) {
|
||||
return item.OfficialRating || globalize.translate('Unrated');
|
||||
}
|
||||
if (sortBy.indexOf('communityrating') === 0) {
|
||||
if (item.CommunityRating == null) {
|
||||
return globalize.translate('Unrated');
|
||||
}
|
||||
|
||||
return Math.floor(item.CommunityRating);
|
||||
}
|
||||
if (sortBy.indexOf('criticrating') === 0) {
|
||||
if (item.CriticRating == null) {
|
||||
return globalize.translate('Unrated');
|
||||
}
|
||||
|
||||
return Math.floor(item.CriticRating);
|
||||
}
|
||||
if (sortBy.indexOf('albumartist') === 0) {
|
||||
// SortName
|
||||
if (!item.AlbumArtist) {
|
||||
return '';
|
||||
}
|
||||
|
||||
name = item.AlbumArtist[0].toUpperCase();
|
||||
|
||||
code = name.charCodeAt(0);
|
||||
if (code < 65 || code > 90) {
|
||||
return '#';
|
||||
}
|
||||
|
||||
return name.toUpperCase();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getImageUrl(item, size) {
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
let itemId;
|
||||
|
||||
const options = {
|
||||
fillWidth: size,
|
||||
fillHeight: size,
|
||||
type: 'Primary'
|
||||
};
|
||||
|
||||
if (item.ImageTags && item.ImageTags.Primary) {
|
||||
options.tag = item.ImageTags.Primary;
|
||||
itemId = item.Id;
|
||||
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
|
||||
options.tag = item.AlbumPrimaryImageTag;
|
||||
itemId = item.AlbumId;
|
||||
} else if (item.SeriesId && item.SeriesPrimaryImageTag) {
|
||||
options.tag = item.SeriesPrimaryImageTag;
|
||||
itemId = item.SeriesId;
|
||||
} else if (item.ParentPrimaryImageTag) {
|
||||
options.tag = item.ParentPrimaryImageTag;
|
||||
itemId = item.ParentPrimaryImageItemId;
|
||||
}
|
||||
|
||||
function getChannelImageUrl(item, size) {
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
const options = {
|
||||
fillWidth: size,
|
||||
fillHeight: size,
|
||||
type: 'Primary'
|
||||
};
|
||||
if (itemId) {
|
||||
return apiClient.getScaledImageUrl(itemId, options);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item.ChannelId && item.ChannelPrimaryImageTag) {
|
||||
options.tag = item.ChannelPrimaryImageTag;
|
||||
}
|
||||
function getChannelImageUrl(item, size) {
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
const options = {
|
||||
fillWidth: size,
|
||||
fillHeight: size,
|
||||
type: 'Primary'
|
||||
};
|
||||
|
||||
if (item.ChannelId) {
|
||||
return apiClient.getScaledImageUrl(item.ChannelId, options);
|
||||
}
|
||||
if (item.ChannelId && item.ChannelPrimaryImageTag) {
|
||||
options.tag = item.ChannelPrimaryImageTag;
|
||||
}
|
||||
|
||||
function getTextLinesHtml(textlines, isLargeStyle) {
|
||||
let html = '';
|
||||
if (item.ChannelId) {
|
||||
return apiClient.getScaledImageUrl(item.ChannelId, options);
|
||||
}
|
||||
}
|
||||
|
||||
const largeTitleTagName = layoutManager.tv ? 'h2' : 'div';
|
||||
function getTextLinesHtml(textlines, isLargeStyle) {
|
||||
let html = '';
|
||||
|
||||
for (const [i, text] of textlines.entries()) {
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
const largeTitleTagName = layoutManager.tv ? 'h2' : 'div';
|
||||
|
||||
let elem;
|
||||
for (const [i, text] of textlines.entries()) {
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
if (isLargeStyle) {
|
||||
elem = document.createElement(largeTitleTagName);
|
||||
} else {
|
||||
elem = document.createElement('div');
|
||||
}
|
||||
let elem;
|
||||
|
||||
if (i === 0) {
|
||||
if (isLargeStyle) {
|
||||
elem = document.createElement(largeTitleTagName);
|
||||
} else {
|
||||
elem = document.createElement('div');
|
||||
elem.classList.add('secondary');
|
||||
}
|
||||
|
||||
elem.classList.add('listItemBodyText');
|
||||
|
||||
elem.innerHTML = '<bdi>' + escapeHtml(text) + '</bdi>';
|
||||
|
||||
html += elem.outerHTML;
|
||||
} else {
|
||||
elem = document.createElement('div');
|
||||
elem.classList.add('secondary');
|
||||
}
|
||||
|
||||
return html;
|
||||
elem.classList.add('listItemBodyText');
|
||||
|
||||
elem.innerHTML = '<bdi>' + escapeHtml(text) + '</bdi>';
|
||||
|
||||
html += elem.outerHTML;
|
||||
}
|
||||
|
||||
function getRightButtonsHtml(options) {
|
||||
return html;
|
||||
}
|
||||
|
||||
function getRightButtonsHtml(options) {
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = options.rightButtons.length; i < length; i++) {
|
||||
const button = options.rightButtons[i];
|
||||
|
||||
html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}" aria-hidden="true"></span></button>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export function getListViewHtml(options) {
|
||||
const items = options.items;
|
||||
|
||||
let groupTitle = '';
|
||||
const action = options.action || 'link';
|
||||
|
||||
const isLargeStyle = options.imageSize === 'large';
|
||||
const enableOverview = options.enableOverview;
|
||||
|
||||
const clickEntireItem = layoutManager.tv ? true : false;
|
||||
const outerTagName = clickEntireItem ? 'button' : 'div';
|
||||
const enableSideMediaInfo = options.enableSideMediaInfo != null ? options.enableSideMediaInfo : true;
|
||||
|
||||
let outerHtml = '';
|
||||
|
||||
const enableContentWrapper = options.enableOverview && !layoutManager.tv;
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
let html = '';
|
||||
|
||||
for (let i = 0, length = options.rightButtons.length; i < length; i++) {
|
||||
const button = options.rightButtons[i];
|
||||
if (options.showIndex) {
|
||||
const itemGroupTitle = getIndex(item, options);
|
||||
|
||||
html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}" aria-hidden="true"></span></button>`;
|
||||
if (itemGroupTitle !== groupTitle) {
|
||||
if (html) {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
html += '<h2 class="listGroupHeader listGroupHeader-first">';
|
||||
} else {
|
||||
html += '<h2 class="listGroupHeader">';
|
||||
}
|
||||
html += escapeHtml(itemGroupTitle);
|
||||
html += '</h2>';
|
||||
|
||||
html += '<div>';
|
||||
|
||||
groupTitle = itemGroupTitle;
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
let cssClass = 'listItem';
|
||||
|
||||
export function getListViewHtml(options) {
|
||||
const items = options.items;
|
||||
if (options.border || (options.highlight !== false && !layoutManager.tv)) {
|
||||
cssClass += ' listItem-border';
|
||||
}
|
||||
|
||||
let groupTitle = '';
|
||||
const action = options.action || 'link';
|
||||
if (clickEntireItem) {
|
||||
cssClass += ' itemAction listItem-button';
|
||||
}
|
||||
|
||||
const isLargeStyle = options.imageSize === 'large';
|
||||
const enableOverview = options.enableOverview;
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' listItem-focusscale';
|
||||
}
|
||||
|
||||
const clickEntireItem = layoutManager.tv ? true : false;
|
||||
const outerTagName = clickEntireItem ? 'button' : 'div';
|
||||
const enableSideMediaInfo = options.enableSideMediaInfo != null ? options.enableSideMediaInfo : true;
|
||||
let downloadWidth = 80;
|
||||
|
||||
let outerHtml = '';
|
||||
if (isLargeStyle) {
|
||||
cssClass += ' listItem-largeImage';
|
||||
downloadWidth = 500;
|
||||
}
|
||||
|
||||
const enableContentWrapper = options.enableOverview && !layoutManager.tv;
|
||||
const playlistItemId = item.PlaylistItemId ? (` data-playlistitemid="${item.PlaylistItemId}"`) : '';
|
||||
|
||||
for (let i = 0, length = items.length; i < length; i++) {
|
||||
const item = items[i];
|
||||
const positionTicksData = item.UserData && item.UserData.PlaybackPositionTicks ? (` data-positionticks="${item.UserData.PlaybackPositionTicks}"`) : '';
|
||||
const collectionIdData = options.collectionId ? (` data-collectionid="${options.collectionId}"`) : '';
|
||||
const playlistIdData = options.playlistId ? (` data-playlistid="${options.playlistId}"`) : '';
|
||||
const mediaTypeData = item.MediaType ? (` data-mediatype="${item.MediaType}"`) : '';
|
||||
const collectionTypeData = item.CollectionType ? (` data-collectiontype="${item.CollectionType}"`) : '';
|
||||
const channelIdData = item.ChannelId ? (` data-channelid="${item.ChannelId}"`) : '';
|
||||
|
||||
let html = '';
|
||||
if (enableContentWrapper) {
|
||||
cssClass += ' listItem-withContentWrapper';
|
||||
}
|
||||
|
||||
if (options.showIndex) {
|
||||
const itemGroupTitle = getIndex(item, options);
|
||||
html += `<${outerTagName} class="${cssClass}"${playlistItemId} data-action="${action}" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}"${mediaTypeData}${collectionTypeData}${channelIdData}${positionTicksData}${collectionIdData}${playlistIdData}>`;
|
||||
|
||||
if (itemGroupTitle !== groupTitle) {
|
||||
if (html) {
|
||||
html += '</div>';
|
||||
}
|
||||
if (enableContentWrapper) {
|
||||
html += '<div class="listItem-content">';
|
||||
}
|
||||
|
||||
if (i === 0) {
|
||||
html += '<h2 class="listGroupHeader listGroupHeader-first">';
|
||||
} else {
|
||||
html += '<h2 class="listGroupHeader">';
|
||||
}
|
||||
html += escapeHtml(itemGroupTitle);
|
||||
html += '</h2>';
|
||||
if (!clickEntireItem && options.dragHandle) {
|
||||
html += '<span class="listViewDragHandle material-icons listItemIcon listItemIcon-transparent drag_handle" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
html += '<div>';
|
||||
if (options.image !== false) {
|
||||
const imgUrl = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth);
|
||||
let imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage';
|
||||
|
||||
groupTitle = itemGroupTitle;
|
||||
}
|
||||
if (options.imageSource === 'channel') {
|
||||
imageClass += ' listItemImage-channel';
|
||||
}
|
||||
|
||||
let cssClass = 'listItem';
|
||||
|
||||
if (options.border || (options.highlight !== false && !layoutManager.tv)) {
|
||||
cssClass += ' listItem-border';
|
||||
if (isLargeStyle && layoutManager.tv) {
|
||||
imageClass += ' listItemImage-large-tv';
|
||||
}
|
||||
|
||||
if (clickEntireItem) {
|
||||
cssClass += ' itemAction listItem-button';
|
||||
const playOnImageClick = options.imagePlayButton && !layoutManager.tv;
|
||||
|
||||
if (!clickEntireItem) {
|
||||
imageClass += ' itemAction';
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' listItem-focusscale';
|
||||
const imageAction = playOnImageClick ? 'link' : action;
|
||||
|
||||
if (imgUrl) {
|
||||
html += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" item-icon>';
|
||||
} else {
|
||||
html += '<div class="' + imageClass + ' cardImageContainer ' + cardBuilder.getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options);
|
||||
}
|
||||
|
||||
let downloadWidth = 80;
|
||||
|
||||
if (isLargeStyle) {
|
||||
cssClass += ' listItem-largeImage';
|
||||
downloadWidth = 500;
|
||||
const mediaSourceCount = item.MediaSourceCount || 1;
|
||||
if (mediaSourceCount > 1 && options.disableIndicators !== true) {
|
||||
html += '<div class="mediaSourceIndicator">' + mediaSourceCount + '</div>';
|
||||
}
|
||||
|
||||
const playlistItemId = item.PlaylistItemId ? (` data-playlistitemid="${item.PlaylistItemId}"`) : '';
|
||||
let indicatorsHtml = '';
|
||||
indicatorsHtml += indicators.getPlayedIndicatorHtml(item);
|
||||
|
||||
const positionTicksData = item.UserData && item.UserData.PlaybackPositionTicks ? (` data-positionticks="${item.UserData.PlaybackPositionTicks}"`) : '';
|
||||
const collectionIdData = options.collectionId ? (` data-collectionid="${options.collectionId}"`) : '';
|
||||
const playlistIdData = options.playlistId ? (` data-playlistid="${options.playlistId}"`) : '';
|
||||
const mediaTypeData = item.MediaType ? (` data-mediatype="${item.MediaType}"`) : '';
|
||||
const collectionTypeData = item.CollectionType ? (` data-collectiontype="${item.CollectionType}"`) : '';
|
||||
const channelIdData = item.ChannelId ? (` data-channelid="${item.ChannelId}"`) : '';
|
||||
|
||||
if (enableContentWrapper) {
|
||||
cssClass += ' listItem-withContentWrapper';
|
||||
if (indicatorsHtml) {
|
||||
html += `<div class="indicators listItemIndicators">${indicatorsHtml}</div>`;
|
||||
}
|
||||
|
||||
html += `<${outerTagName} class="${cssClass}"${playlistItemId} data-action="${action}" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}"${mediaTypeData}${collectionTypeData}${channelIdData}${positionTicksData}${collectionIdData}${playlistIdData}>`;
|
||||
|
||||
if (enableContentWrapper) {
|
||||
html += '<div class="listItem-content">';
|
||||
if (playOnImageClick) {
|
||||
html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
if (!clickEntireItem && options.dragHandle) {
|
||||
html += '<span class="listViewDragHandle material-icons listItemIcon listItemIcon-transparent drag_handle" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
if (options.image !== false) {
|
||||
const imgUrl = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth);
|
||||
let imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage';
|
||||
|
||||
if (options.imageSource === 'channel') {
|
||||
imageClass += ' listItemImage-channel';
|
||||
}
|
||||
|
||||
if (isLargeStyle && layoutManager.tv) {
|
||||
imageClass += ' listItemImage-large-tv';
|
||||
}
|
||||
|
||||
const playOnImageClick = options.imagePlayButton && !layoutManager.tv;
|
||||
|
||||
if (!clickEntireItem) {
|
||||
imageClass += ' itemAction';
|
||||
}
|
||||
|
||||
const imageAction = playOnImageClick ? 'link' : action;
|
||||
|
||||
if (imgUrl) {
|
||||
html += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" item-icon>';
|
||||
} else {
|
||||
html += '<div class="' + imageClass + ' cardImageContainer ' + cardBuilder.getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options);
|
||||
}
|
||||
|
||||
const mediaSourceCount = item.MediaSourceCount || 1;
|
||||
if (mediaSourceCount > 1 && options.disableIndicators !== true) {
|
||||
html += '<div class="mediaSourceIndicator">' + mediaSourceCount + '</div>';
|
||||
}
|
||||
|
||||
let indicatorsHtml = '';
|
||||
indicatorsHtml += indicators.getPlayedIndicatorHtml(item);
|
||||
|
||||
if (indicatorsHtml) {
|
||||
html += `<div class="indicators listItemIndicators">${indicatorsHtml}</div>`;
|
||||
}
|
||||
|
||||
if (playOnImageClick) {
|
||||
html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
const progressHtml = indicators.getProgressBarHtml(item, {
|
||||
containerClass: 'listItemProgressBar'
|
||||
});
|
||||
|
||||
if (progressHtml) {
|
||||
html += progressHtml;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (options.showIndexNumberLeft) {
|
||||
html += '<div class="listItem-indexnumberleft">';
|
||||
html += (item.IndexNumber || ' ');
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
const textlines = [];
|
||||
|
||||
if (options.showProgramDateTime) {
|
||||
textlines.push(datetime.toLocaleString(datetime.parseISO8601Date(item.StartDate), {
|
||||
|
||||
weekday: 'long',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
}));
|
||||
}
|
||||
|
||||
if (options.showProgramTime) {
|
||||
textlines.push(datetime.getDisplayTime(datetime.parseISO8601Date(item.StartDate)));
|
||||
}
|
||||
|
||||
if (options.showChannel && item.ChannelName) {
|
||||
textlines.push(item.ChannelName);
|
||||
}
|
||||
|
||||
let parentTitle = null;
|
||||
|
||||
if (options.showParentTitle) {
|
||||
if (item.Type === 'Episode') {
|
||||
parentTitle = item.SeriesName;
|
||||
} else if (item.IsSeries || (item.EpisodeTitle && item.Name)) {
|
||||
parentTitle = item.Name;
|
||||
}
|
||||
}
|
||||
|
||||
let displayName = itemHelper.getDisplayName(item, {
|
||||
includeParentInfo: options.includeParentInfoInTitle
|
||||
const progressHtml = indicators.getProgressBarHtml(item, {
|
||||
containerClass: 'listItemProgressBar'
|
||||
});
|
||||
|
||||
if (options.showIndexNumber && item.IndexNumber != null) {
|
||||
displayName = `${item.IndexNumber}. ${displayName}`;
|
||||
if (progressHtml) {
|
||||
html += progressHtml;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (options.showIndexNumberLeft) {
|
||||
html += '<div class="listItem-indexnumberleft">';
|
||||
html += (item.IndexNumber || ' ');
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
const textlines = [];
|
||||
|
||||
if (options.showProgramDateTime) {
|
||||
textlines.push(datetime.toLocaleString(datetime.parseISO8601Date(item.StartDate), {
|
||||
|
||||
weekday: 'long',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
}));
|
||||
}
|
||||
|
||||
if (options.showProgramTime) {
|
||||
textlines.push(datetime.getDisplayTime(datetime.parseISO8601Date(item.StartDate)));
|
||||
}
|
||||
|
||||
if (options.showChannel && item.ChannelName) {
|
||||
textlines.push(item.ChannelName);
|
||||
}
|
||||
|
||||
let parentTitle = null;
|
||||
|
||||
if (options.showParentTitle) {
|
||||
if (item.Type === 'Episode') {
|
||||
parentTitle = item.SeriesName;
|
||||
} else if (item.IsSeries || (item.EpisodeTitle && item.Name)) {
|
||||
parentTitle = item.Name;
|
||||
}
|
||||
}
|
||||
|
||||
let displayName = itemHelper.getDisplayName(item, {
|
||||
includeParentInfo: options.includeParentInfoInTitle
|
||||
});
|
||||
|
||||
if (options.showIndexNumber && item.IndexNumber != null) {
|
||||
displayName = `${item.IndexNumber}. ${displayName}`;
|
||||
}
|
||||
|
||||
if (options.showParentTitle && options.parentTitleWithTitle) {
|
||||
if (displayName) {
|
||||
if (parentTitle) {
|
||||
parentTitle += ' - ';
|
||||
}
|
||||
parentTitle = (parentTitle || '') + displayName;
|
||||
}
|
||||
|
||||
if (options.showParentTitle && options.parentTitleWithTitle) {
|
||||
if (displayName) {
|
||||
if (parentTitle) {
|
||||
parentTitle += ' - ';
|
||||
}
|
||||
parentTitle = (parentTitle || '') + displayName;
|
||||
textlines.push(parentTitle || '');
|
||||
} else if (options.showParentTitle) {
|
||||
textlines.push(parentTitle || '');
|
||||
}
|
||||
|
||||
if (displayName && !options.parentTitleWithTitle) {
|
||||
textlines.push(displayName);
|
||||
}
|
||||
|
||||
if (item.IsFolder) {
|
||||
if (options.artist !== false && item.AlbumArtist && item.Type === 'MusicAlbum') {
|
||||
textlines.push(item.AlbumArtist);
|
||||
}
|
||||
} else {
|
||||
if (options.artist) {
|
||||
const artistItems = item.ArtistItems;
|
||||
if (artistItems && item.Type !== 'MusicAlbum') {
|
||||
textlines.push(artistItems.map(a => {
|
||||
return a.Name;
|
||||
}).join(', '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Type === 'TvChannel' && item.CurrentProgram) {
|
||||
textlines.push(itemHelper.getDisplayName(item.CurrentProgram));
|
||||
}
|
||||
|
||||
cssClass = 'listItemBody';
|
||||
if (!clickEntireItem) {
|
||||
cssClass += ' itemAction';
|
||||
}
|
||||
|
||||
if (options.image === false) {
|
||||
cssClass += ' listItemBody-noleftpadding';
|
||||
}
|
||||
|
||||
html += `<div class="${cssClass}">`;
|
||||
|
||||
html += getTextLinesHtml(textlines, isLargeStyle);
|
||||
|
||||
if (options.mediaInfo !== false && !enableSideMediaInfo) {
|
||||
const mediaInfoClass = 'secondary listItemMediaInfo listItemBodyText';
|
||||
|
||||
html += `<div class="${mediaInfoClass}">`;
|
||||
html += mediaInfo.getPrimaryMediaInfoHtml(item, {
|
||||
episodeTitle: false,
|
||||
originalAirDate: false,
|
||||
subtitles: false
|
||||
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (enableOverview && item.Overview) {
|
||||
html += '<div class="secondary listItem-overview listItemBodyText">';
|
||||
html += '<bdi>' + item.Overview + '</bdi>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
if (options.mediaInfo !== false && enableSideMediaInfo) {
|
||||
html += '<div class="secondary listItemMediaInfo">';
|
||||
html += mediaInfo.getPrimaryMediaInfoHtml(item, {
|
||||
|
||||
year: false,
|
||||
container: false,
|
||||
episodeTitle: false,
|
||||
criticRating: false,
|
||||
officialRating: false,
|
||||
endsAt: false
|
||||
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (!options.recordButton && (item.Type === 'Timer' || item.Type === 'Program')) {
|
||||
html += indicators.getTimerIndicator(item).replace('indicatorIcon', 'indicatorIcon listItemAside');
|
||||
}
|
||||
|
||||
html += '<div class="listViewUserDataButtons">';
|
||||
|
||||
if (!clickEntireItem) {
|
||||
if (options.addToListButton) {
|
||||
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
if (options.infoButton) {
|
||||
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
if (options.rightButtons) {
|
||||
html += getRightButtonsHtml(options);
|
||||
}
|
||||
|
||||
if (options.enableUserDataButtons !== false) {
|
||||
const userData = item.UserData || {};
|
||||
const likes = userData.Likes == null ? '' : userData.Likes;
|
||||
|
||||
if (itemHelper.canMarkPlayed(item) && options.enablePlayedButton !== false) {
|
||||
html += '<button is="emby-playstatebutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons check" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
textlines.push(parentTitle || '');
|
||||
} else if (options.showParentTitle) {
|
||||
textlines.push(parentTitle || '');
|
||||
}
|
||||
|
||||
if (displayName && !options.parentTitleWithTitle) {
|
||||
textlines.push(displayName);
|
||||
}
|
||||
|
||||
if (item.IsFolder) {
|
||||
if (options.artist !== false && item.AlbumArtist && item.Type === 'MusicAlbum') {
|
||||
textlines.push(item.AlbumArtist);
|
||||
}
|
||||
} else {
|
||||
if (options.artist) {
|
||||
const artistItems = item.ArtistItems;
|
||||
if (artistItems && item.Type !== 'MusicAlbum') {
|
||||
textlines.push(artistItems.map(a => {
|
||||
return a.Name;
|
||||
}).join(', '));
|
||||
}
|
||||
if (itemHelper.canRate(item) && options.enableRatingButton !== false) {
|
||||
html += '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
|
||||
}
|
||||
}
|
||||
|
||||
if (item.Type === 'TvChannel' && item.CurrentProgram) {
|
||||
textlines.push(itemHelper.getDisplayName(item.CurrentProgram));
|
||||
if (options.moreButton !== false) {
|
||||
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
cssClass = 'listItemBody';
|
||||
if (!clickEntireItem) {
|
||||
cssClass += ' itemAction';
|
||||
}
|
||||
|
||||
if (options.image === false) {
|
||||
cssClass += ' listItemBody-noleftpadding';
|
||||
}
|
||||
|
||||
html += `<div class="${cssClass}">`;
|
||||
|
||||
html += getTextLinesHtml(textlines, isLargeStyle);
|
||||
|
||||
if (options.mediaInfo !== false && !enableSideMediaInfo) {
|
||||
const mediaInfoClass = 'secondary listItemMediaInfo listItemBodyText';
|
||||
|
||||
html += `<div class="${mediaInfoClass}">`;
|
||||
html += mediaInfo.getPrimaryMediaInfoHtml(item, {
|
||||
episodeTitle: false,
|
||||
originalAirDate: false,
|
||||
subtitles: false
|
||||
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
if (enableContentWrapper) {
|
||||
html += '</div>';
|
||||
|
||||
if (enableOverview && item.Overview) {
|
||||
html += '<div class="secondary listItem-overview listItemBodyText">';
|
||||
html += '<div class="listItem-bottomoverview secondary">';
|
||||
html += '<bdi>' + item.Overview + '</bdi>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
if (options.mediaInfo !== false && enableSideMediaInfo) {
|
||||
html += '<div class="secondary listItemMediaInfo">';
|
||||
html += mediaInfo.getPrimaryMediaInfoHtml(item, {
|
||||
|
||||
year: false,
|
||||
container: false,
|
||||
episodeTitle: false,
|
||||
criticRating: false,
|
||||
officialRating: false,
|
||||
endsAt: false
|
||||
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (!options.recordButton && (item.Type === 'Timer' || item.Type === 'Program')) {
|
||||
html += indicators.getTimerIndicator(item).replace('indicatorIcon', 'indicatorIcon listItemAside');
|
||||
}
|
||||
|
||||
html += '<div class="listViewUserDataButtons">';
|
||||
|
||||
if (!clickEntireItem) {
|
||||
if (options.addToListButton) {
|
||||
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
if (options.infoButton) {
|
||||
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
if (options.rightButtons) {
|
||||
html += getRightButtonsHtml(options);
|
||||
}
|
||||
|
||||
if (options.enableUserDataButtons !== false) {
|
||||
const userData = item.UserData || {};
|
||||
const likes = userData.Likes == null ? '' : userData.Likes;
|
||||
|
||||
if (itemHelper.canMarkPlayed(item) && options.enablePlayedButton !== false) {
|
||||
html += '<button is="emby-playstatebutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons check" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
if (itemHelper.canRate(item) && options.enableRatingButton !== false) {
|
||||
html += '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
|
||||
}
|
||||
}
|
||||
|
||||
if (options.moreButton !== false) {
|
||||
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (enableContentWrapper) {
|
||||
html += '</div>';
|
||||
|
||||
if (enableOverview && item.Overview) {
|
||||
html += '<div class="listItem-bottomoverview secondary">';
|
||||
html += '<bdi>' + item.Overview + '</bdi>';
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
html += `</${outerTagName}>`;
|
||||
|
||||
outerHtml += html;
|
||||
}
|
||||
|
||||
return outerHtml;
|
||||
html += `</${outerTagName}>`;
|
||||
|
||||
outerHtml += html;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
return outerHtml;
|
||||
}
|
||||
|
||||
export default {
|
||||
getListViewHtml: getListViewHtml
|
||||
};
|
||||
|
||||
@@ -4,200 +4,196 @@ import Events from '../utils/events.ts';
|
||||
import '../elements/emby-tabs/emby-tabs';
|
||||
import '../elements/emby-button/emby-button';
|
||||
|
||||
/* eslint-disable indent */
|
||||
let tabOwnerView;
|
||||
const queryScope = document.querySelector('.skinHeader');
|
||||
let headerTabsContainer;
|
||||
let tabsElem;
|
||||
|
||||
let tabOwnerView;
|
||||
const queryScope = document.querySelector('.skinHeader');
|
||||
let headerTabsContainer;
|
||||
let tabsElem;
|
||||
|
||||
function ensureElements() {
|
||||
if (!headerTabsContainer) {
|
||||
headerTabsContainer = queryScope.querySelector('.headerTabs');
|
||||
}
|
||||
function ensureElements() {
|
||||
if (!headerTabsContainer) {
|
||||
headerTabsContainer = queryScope.querySelector('.headerTabs');
|
||||
}
|
||||
}
|
||||
|
||||
function onViewTabsReady() {
|
||||
this.selectedIndex(this.readySelectedIndex);
|
||||
this.readySelectedIndex = null;
|
||||
}
|
||||
function onViewTabsReady() {
|
||||
this.selectedIndex(this.readySelectedIndex);
|
||||
this.readySelectedIndex = null;
|
||||
}
|
||||
|
||||
function allowSwipe(target) {
|
||||
function allowSwipeOn(elem) {
|
||||
if (dom.parentWithTag(elem, 'input')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const classList = elem.classList;
|
||||
if (classList) {
|
||||
return !classList.contains('scrollX') && !classList.contains('animatedScrollX');
|
||||
}
|
||||
|
||||
return true;
|
||||
function allowSwipe(target) {
|
||||
function allowSwipeOn(elem) {
|
||||
if (dom.parentWithTag(elem, 'input')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let parent = target;
|
||||
while (parent != null) {
|
||||
if (!allowSwipeOn(parent)) {
|
||||
return false;
|
||||
}
|
||||
parent = parent.parentNode;
|
||||
const classList = elem.classList;
|
||||
if (classList) {
|
||||
return !classList.contains('scrollX') && !classList.contains('animatedScrollX');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function configureSwipeTabs(view, currentElement) {
|
||||
if (!browser.touch) {
|
||||
return;
|
||||
let parent = target;
|
||||
while (parent != null) {
|
||||
if (!allowSwipeOn(parent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// implement without hammer
|
||||
const onSwipeLeft = function (e, target) {
|
||||
if (allowSwipe(target) && view.contains(target)) {
|
||||
currentElement.selectNext();
|
||||
}
|
||||
};
|
||||
|
||||
const onSwipeRight = function (e, target) {
|
||||
if (allowSwipe(target) && view.contains(target)) {
|
||||
currentElement.selectPrevious();
|
||||
}
|
||||
};
|
||||
|
||||
import('../scripts/touchHelper').then(({default: TouchHelper}) => {
|
||||
const touchHelper = new TouchHelper(view.parentNode.parentNode);
|
||||
|
||||
Events.on(touchHelper, 'swipeleft', onSwipeLeft);
|
||||
Events.on(touchHelper, 'swiperight', onSwipeRight);
|
||||
|
||||
view.addEventListener('viewdestroy', function () {
|
||||
touchHelper.destroy();
|
||||
});
|
||||
});
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
|
||||
export function setTabs(view, selectedIndex, getTabsFn, getTabContainersFn, onBeforeTabChange, onTabChange, setSelectedIndex) {
|
||||
ensureElements();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!view) {
|
||||
if (tabOwnerView) {
|
||||
document.body.classList.remove('withSectionTabs');
|
||||
function configureSwipeTabs(view, currentElement) {
|
||||
if (!browser.touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
headerTabsContainer.innerHTML = '';
|
||||
headerTabsContainer.classList.add('hide');
|
||||
|
||||
tabOwnerView = null;
|
||||
}
|
||||
return {
|
||||
tabsContainer: headerTabsContainer,
|
||||
replaced: false
|
||||
};
|
||||
// implement without hammer
|
||||
const onSwipeLeft = function (e, target) {
|
||||
if (allowSwipe(target) && view.contains(target)) {
|
||||
currentElement.selectNext();
|
||||
}
|
||||
};
|
||||
|
||||
const tabsContainerElem = headerTabsContainer;
|
||||
|
||||
if (!tabOwnerView) {
|
||||
tabsContainerElem.classList.remove('hide');
|
||||
const onSwipeRight = function (e, target) {
|
||||
if (allowSwipe(target) && view.contains(target)) {
|
||||
currentElement.selectPrevious();
|
||||
}
|
||||
};
|
||||
|
||||
if (tabOwnerView !== view) {
|
||||
let index = 0;
|
||||
import('../scripts/touchHelper').then(({ default: TouchHelper }) => {
|
||||
const touchHelper = new TouchHelper(view.parentNode.parentNode);
|
||||
|
||||
const indexAttribute = selectedIndex == null ? '' : (' data-index="' + selectedIndex + '"');
|
||||
const tabsHtml = '<div is="emby-tabs"' + indexAttribute + ' class="tabs-viewmenubar"><div class="emby-tabs-slider" style="white-space:nowrap;">' + getTabsFn().map(function (t) {
|
||||
let tabClass = 'emby-tab-button';
|
||||
Events.on(touchHelper, 'swipeleft', onSwipeLeft);
|
||||
Events.on(touchHelper, 'swiperight', onSwipeRight);
|
||||
|
||||
if (t.enabled === false) {
|
||||
tabClass += ' hide';
|
||||
}
|
||||
view.addEventListener('viewdestroy', function () {
|
||||
touchHelper.destroy();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let tabHtml;
|
||||
export function setTabs(view, selectedIndex, getTabsFn, getTabContainersFn, onBeforeTabChange, onTabChange, setSelectedIndex) {
|
||||
ensureElements();
|
||||
|
||||
if (t.cssClass) {
|
||||
tabClass += ' ' + t.cssClass;
|
||||
}
|
||||
if (!view) {
|
||||
if (tabOwnerView) {
|
||||
document.body.classList.remove('withSectionTabs');
|
||||
|
||||
if (t.href) {
|
||||
tabHtml = '<a href="' + t.href + '" is="emby-linkbutton" class="' + tabClass + '" data-index="' + index + '"><div class="emby-button-foreground">' + t.name + '</div></a>';
|
||||
} else {
|
||||
tabHtml = '<button type="button" is="emby-button" class="' + tabClass + '" data-index="' + index + '"><div class="emby-button-foreground">' + t.name + '</div></button>';
|
||||
}
|
||||
headerTabsContainer.innerHTML = '';
|
||||
headerTabsContainer.classList.add('hide');
|
||||
|
||||
index++;
|
||||
return tabHtml;
|
||||
}).join('') + '</div></div>';
|
||||
|
||||
tabsContainerElem.innerHTML = tabsHtml;
|
||||
window.CustomElements.upgradeSubtree(tabsContainerElem);
|
||||
|
||||
document.body.classList.add('withSectionTabs');
|
||||
tabOwnerView = view;
|
||||
|
||||
tabsElem = tabsContainerElem.querySelector('[is="emby-tabs"]');
|
||||
|
||||
configureSwipeTabs(view, tabsElem);
|
||||
|
||||
if (getTabContainersFn) {
|
||||
tabsElem.addEventListener('beforetabchange', function (e) {
|
||||
const tabContainers = getTabContainersFn();
|
||||
if (e.detail.previousIndex != null) {
|
||||
const previousPanel = tabContainers[e.detail.previousIndex];
|
||||
if (previousPanel) {
|
||||
previousPanel.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
|
||||
const newPanel = tabContainers[e.detail.selectedTabIndex];
|
||||
|
||||
if (newPanel) {
|
||||
newPanel.classList.add('is-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onBeforeTabChange) {
|
||||
tabsElem.addEventListener('beforetabchange', onBeforeTabChange);
|
||||
}
|
||||
if (onTabChange) {
|
||||
tabsElem.addEventListener('tabchange', onTabChange);
|
||||
}
|
||||
|
||||
if (setSelectedIndex !== false) {
|
||||
if (tabsElem.selectedIndex) {
|
||||
tabsElem.selectedIndex(selectedIndex);
|
||||
} else {
|
||||
tabsElem.readySelectedIndex = selectedIndex;
|
||||
tabsElem.addEventListener('ready', onViewTabsReady);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tabsContainer: tabsContainerElem,
|
||||
tabs: tabsElem,
|
||||
replaced: true
|
||||
};
|
||||
tabOwnerView = null;
|
||||
}
|
||||
|
||||
tabsElem.selectedIndex(selectedIndex);
|
||||
|
||||
return {
|
||||
tabsContainer: tabsContainerElem,
|
||||
tabs: tabsElem,
|
||||
tabsContainer: headerTabsContainer,
|
||||
replaced: false
|
||||
};
|
||||
}
|
||||
|
||||
export function selectedTabIndex(index) {
|
||||
if (index != null) {
|
||||
tabsElem.selectedIndex(index);
|
||||
} else {
|
||||
tabsElem.triggerTabChange();
|
||||
const tabsContainerElem = headerTabsContainer;
|
||||
|
||||
if (!tabOwnerView) {
|
||||
tabsContainerElem.classList.remove('hide');
|
||||
}
|
||||
|
||||
if (tabOwnerView !== view) {
|
||||
let index = 0;
|
||||
|
||||
const indexAttribute = selectedIndex == null ? '' : (' data-index="' + selectedIndex + '"');
|
||||
const tabsHtml = '<div is="emby-tabs"' + indexAttribute + ' class="tabs-viewmenubar"><div class="emby-tabs-slider" style="white-space:nowrap;">' + getTabsFn().map(function (t) {
|
||||
let tabClass = 'emby-tab-button';
|
||||
|
||||
if (t.enabled === false) {
|
||||
tabClass += ' hide';
|
||||
}
|
||||
|
||||
let tabHtml;
|
||||
|
||||
if (t.cssClass) {
|
||||
tabClass += ' ' + t.cssClass;
|
||||
}
|
||||
|
||||
if (t.href) {
|
||||
tabHtml = '<a href="' + t.href + '" is="emby-linkbutton" class="' + tabClass + '" data-index="' + index + '"><div class="emby-button-foreground">' + t.name + '</div></a>';
|
||||
} else {
|
||||
tabHtml = '<button type="button" is="emby-button" class="' + tabClass + '" data-index="' + index + '"><div class="emby-button-foreground">' + t.name + '</div></button>';
|
||||
}
|
||||
|
||||
index++;
|
||||
return tabHtml;
|
||||
}).join('') + '</div></div>';
|
||||
|
||||
tabsContainerElem.innerHTML = tabsHtml;
|
||||
window.CustomElements.upgradeSubtree(tabsContainerElem);
|
||||
|
||||
document.body.classList.add('withSectionTabs');
|
||||
tabOwnerView = view;
|
||||
|
||||
tabsElem = tabsContainerElem.querySelector('[is="emby-tabs"]');
|
||||
|
||||
configureSwipeTabs(view, tabsElem);
|
||||
|
||||
if (getTabContainersFn) {
|
||||
tabsElem.addEventListener('beforetabchange', function (e) {
|
||||
const tabContainers = getTabContainersFn();
|
||||
if (e.detail.previousIndex != null) {
|
||||
const previousPanel = tabContainers[e.detail.previousIndex];
|
||||
if (previousPanel) {
|
||||
previousPanel.classList.remove('is-active');
|
||||
}
|
||||
}
|
||||
|
||||
const newPanel = tabContainers[e.detail.selectedTabIndex];
|
||||
|
||||
if (newPanel) {
|
||||
newPanel.classList.add('is-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onBeforeTabChange) {
|
||||
tabsElem.addEventListener('beforetabchange', onBeforeTabChange);
|
||||
}
|
||||
if (onTabChange) {
|
||||
tabsElem.addEventListener('tabchange', onTabChange);
|
||||
}
|
||||
|
||||
if (setSelectedIndex !== false) {
|
||||
if (tabsElem.selectedIndex) {
|
||||
tabsElem.selectedIndex(selectedIndex);
|
||||
} else {
|
||||
tabsElem.readySelectedIndex = selectedIndex;
|
||||
tabsElem.addEventListener('ready', onViewTabsReady);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tabsContainer: tabsContainerElem,
|
||||
tabs: tabsElem,
|
||||
replaced: true
|
||||
};
|
||||
}
|
||||
|
||||
export function getTabsElement() {
|
||||
return document.querySelector('.tabs-viewmenubar');
|
||||
}
|
||||
tabsElem.selectedIndex(selectedIndex);
|
||||
|
||||
/* eslint-enable indent */
|
||||
return {
|
||||
tabsContainer: tabsContainerElem,
|
||||
tabs: tabsElem,
|
||||
replaced: false
|
||||
};
|
||||
}
|
||||
|
||||
export function selectedTabIndex(index) {
|
||||
if (index != null) {
|
||||
tabsElem.selectedIndex(index);
|
||||
} else {
|
||||
tabsElem.triggerTabChange();
|
||||
}
|
||||
}
|
||||
|
||||
export function getTabsElement() {
|
||||
return document.querySelector('.tabs-viewmenubar');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for media library creator.
|
||||
@@ -25,167 +24,169 @@ import toast from '../toast/toast';
|
||||
import alert from '../alert';
|
||||
import template from './mediaLibraryCreator.template.html';
|
||||
|
||||
function onAddLibrary() {
|
||||
if (isCreating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pathInfos.length == 0) {
|
||||
alert({
|
||||
text: globalize.translate('PleaseAddAtLeastOneFolder'),
|
||||
type: 'error'
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
isCreating = true;
|
||||
loading.show();
|
||||
const dlg = dom.parentWithClass(this, 'dlg-librarycreator');
|
||||
const name = $('#txtValue', dlg).val();
|
||||
let type = $('#selectCollectionType', dlg).val();
|
||||
|
||||
if (type == 'mixed') {
|
||||
type = null;
|
||||
}
|
||||
|
||||
const libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector('.libraryOptions'));
|
||||
libraryOptions.PathInfos = pathInfos;
|
||||
ApiClient.addVirtualFolder(name, type, currentOptions.refresh, libraryOptions).then(() => {
|
||||
hasChanges = true;
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
dialogHelper.close(dlg);
|
||||
}, () => {
|
||||
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
|
||||
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
});
|
||||
function onAddLibrary(e) {
|
||||
if (isCreating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCollectionTypeOptionsHtml(collectionTypeOptions) {
|
||||
return collectionTypeOptions.map(i => {
|
||||
return `<option value="${i.value}">${i.name}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function initEditor(page, collectionTypeOptions) {
|
||||
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
|
||||
const value = this.value;
|
||||
const dlg = $(this).parents('.dialog')[0];
|
||||
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
|
||||
|
||||
if (value) {
|
||||
dlg.querySelector('.libraryOptions').classList.remove('hide');
|
||||
} else {
|
||||
dlg.querySelector('.libraryOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (value != 'mixed') {
|
||||
const index = this.selectedIndex;
|
||||
|
||||
if (index != -1) {
|
||||
const name = this.options[index].innerHTML.replace('*', '').replace('&', '&');
|
||||
$('#txtValue', dlg).val(name);
|
||||
}
|
||||
}
|
||||
|
||||
const folderOption = collectionTypeOptions.find(i => i.value === value);
|
||||
$('.collectionTypeFieldDescription', dlg).html(folderOption?.message || '');
|
||||
if (pathInfos.length == 0) {
|
||||
alert({
|
||||
text: globalize.translate('PleaseAddAtLeastOneFolder'),
|
||||
type: 'error'
|
||||
});
|
||||
page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
|
||||
page.querySelector('.btnSubmit').addEventListener('click', onAddLibrary);
|
||||
page.querySelector('.folderList').addEventListener('click', onRemoveClick);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function onAddButtonClick() {
|
||||
const page = dom.parentWithClass(this, 'dlg-librarycreator');
|
||||
isCreating = true;
|
||||
loading.show();
|
||||
const dlg = dom.parentWithClass(this, 'dlg-librarycreator');
|
||||
const name = $('#txtValue', dlg).val();
|
||||
let type = $('#selectCollectionType', dlg).val();
|
||||
|
||||
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
enableNetworkSharePath: true,
|
||||
callback: function (path, networkSharePath) {
|
||||
if (path) {
|
||||
addMediaLocation(page, path, networkSharePath);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
if (type == 'mixed') {
|
||||
type = null;
|
||||
}
|
||||
|
||||
function getFolderHtml(pathInfo, index) {
|
||||
let html = '';
|
||||
html += '<div class="listItem listItem-border lnkPath">';
|
||||
html += `<div class="${pathInfo.NetworkPath ? 'listItemBody two-line' : 'listItemBody'}">`;
|
||||
html += `<div class="listItemBodyText" dir="ltr">${escapeHtml(pathInfo.Path)}</div>`;
|
||||
const libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector('.libraryOptions'));
|
||||
libraryOptions.PathInfos = pathInfos;
|
||||
ApiClient.addVirtualFolder(name, type, currentOptions.refresh, libraryOptions).then(() => {
|
||||
hasChanges = true;
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
dialogHelper.close(dlg);
|
||||
}, () => {
|
||||
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
|
||||
|
||||
if (pathInfo.NetworkPath) {
|
||||
html += `<div class="listItemBodyText secondary" dir="ltr">${escapeHtml(pathInfo.NetworkPath)}</div>`;
|
||||
}
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += `<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
function getCollectionTypeOptionsHtml(collectionTypeOptions) {
|
||||
return collectionTypeOptions.map(i => {
|
||||
return `<option value="${i.value}">${i.name}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderPaths(page) {
|
||||
const foldersHtml = pathInfos.map(getFolderHtml).join('');
|
||||
const folderList = page.querySelector('.folderList');
|
||||
folderList.innerHTML = foldersHtml;
|
||||
function initEditor(page, collectionTypeOptions) {
|
||||
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
|
||||
const value = this.value;
|
||||
const dlg = $(this).parents('.dialog')[0];
|
||||
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
|
||||
|
||||
if (foldersHtml) {
|
||||
folderList.classList.remove('hide');
|
||||
if (value) {
|
||||
dlg.querySelector('.libraryOptions').classList.remove('hide');
|
||||
} else {
|
||||
folderList.classList.add('hide');
|
||||
dlg.querySelector('.libraryOptions').classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function addMediaLocation(page, path, networkSharePath) {
|
||||
const pathLower = path.toLowerCase();
|
||||
const pathFilter = pathInfos.filter(p => {
|
||||
return p.Path.toLowerCase() == pathLower;
|
||||
});
|
||||
if (value != 'mixed') {
|
||||
const index = this.selectedIndex;
|
||||
|
||||
if (!pathFilter.length) {
|
||||
const pathInfo = {
|
||||
Path: path
|
||||
};
|
||||
|
||||
if (networkSharePath) {
|
||||
pathInfo.NetworkPath = networkSharePath;
|
||||
if (index != -1) {
|
||||
const name = this.options[index].innerHTML
|
||||
.replaceAll('*', '')
|
||||
.replaceAll('&', '&');
|
||||
$('#txtValue', dlg).val(name);
|
||||
}
|
||||
|
||||
pathInfos.push(pathInfo);
|
||||
renderPaths(page);
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoveClick(e) {
|
||||
const button = dom.parentWithClass(e.target, 'btnRemovePath');
|
||||
const index = parseInt(button.getAttribute('data-index'), 10);
|
||||
const location = pathInfos[index].Path;
|
||||
const locationLower = location.toLowerCase();
|
||||
pathInfos = pathInfos.filter(p => {
|
||||
return p.Path.toLowerCase() != locationLower;
|
||||
const folderOption = collectionTypeOptions.find(i => i.value === value);
|
||||
$('.collectionTypeFieldDescription', dlg).html(folderOption?.message || '');
|
||||
});
|
||||
page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
|
||||
page.querySelector('.addLibraryForm').addEventListener('submit', onAddLibrary);
|
||||
page.querySelector('.folderList').addEventListener('click', onRemoveClick);
|
||||
}
|
||||
|
||||
function onAddButtonClick() {
|
||||
const page = dom.parentWithClass(this, 'dlg-librarycreator');
|
||||
|
||||
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
enableNetworkSharePath: true,
|
||||
callback: function (path, networkSharePath) {
|
||||
if (path) {
|
||||
addMediaLocation(page, path, networkSharePath);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
renderPaths(dom.parentWithClass(button, 'dlg-librarycreator'));
|
||||
});
|
||||
}
|
||||
|
||||
function getFolderHtml(pathInfo, index) {
|
||||
let html = '';
|
||||
html += '<div class="listItem listItem-border lnkPath">';
|
||||
html += `<div class="${pathInfo.NetworkPath ? 'listItemBody two-line' : 'listItemBody'}">`;
|
||||
html += `<div class="listItemBodyText" dir="ltr">${escapeHtml(pathInfo.Path)}</div>`;
|
||||
|
||||
if (pathInfo.NetworkPath) {
|
||||
html += `<div class="listItemBodyText secondary" dir="ltr">${escapeHtml(pathInfo.NetworkPath)}</div>`;
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
currentResolve(hasChanges);
|
||||
}
|
||||
html += '</div>';
|
||||
html += `<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function initLibraryOptions(dlg) {
|
||||
libraryoptionseditor.embed(dlg.querySelector('.libraryOptions')).then(() => {
|
||||
$('#selectCollectionType', dlg).trigger('change');
|
||||
});
|
||||
function renderPaths(page) {
|
||||
const foldersHtml = pathInfos.map(getFolderHtml).join('');
|
||||
const folderList = page.querySelector('.folderList');
|
||||
folderList.innerHTML = foldersHtml;
|
||||
|
||||
if (foldersHtml) {
|
||||
folderList.classList.remove('hide');
|
||||
} else {
|
||||
folderList.classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function addMediaLocation(page, path, networkSharePath) {
|
||||
const pathLower = path.toLowerCase();
|
||||
const pathFilter = pathInfos.filter(p => {
|
||||
return p.Path.toLowerCase() == pathLower;
|
||||
});
|
||||
|
||||
if (!pathFilter.length) {
|
||||
const pathInfo = {
|
||||
Path: path
|
||||
};
|
||||
|
||||
if (networkSharePath) {
|
||||
pathInfo.NetworkPath = networkSharePath;
|
||||
}
|
||||
|
||||
pathInfos.push(pathInfo);
|
||||
renderPaths(page);
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoveClick(e) {
|
||||
const button = dom.parentWithClass(e.target, 'btnRemovePath');
|
||||
const index = parseInt(button.getAttribute('data-index'), 10);
|
||||
const location = pathInfos[index].Path;
|
||||
const locationLower = location.toLowerCase();
|
||||
pathInfos = pathInfos.filter(p => {
|
||||
return p.Path.toLowerCase() != locationLower;
|
||||
});
|
||||
renderPaths(dom.parentWithClass(button, 'dlg-librarycreator'));
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
currentResolve(hasChanges);
|
||||
}
|
||||
|
||||
function initLibraryOptions(dlg) {
|
||||
libraryoptionseditor.embed(dlg.querySelector('.libraryOptions')).then(() => {
|
||||
$('#selectCollectionType', dlg).trigger('change');
|
||||
});
|
||||
}
|
||||
|
||||
export class showEditor {
|
||||
constructor(options) {
|
||||
@@ -217,11 +218,10 @@ export class showEditor {
|
||||
}
|
||||
}
|
||||
|
||||
let pathInfos = [];
|
||||
let currentResolve;
|
||||
let currentOptions;
|
||||
let hasChanges = false;
|
||||
let isCreating = false;
|
||||
let pathInfos = [];
|
||||
let currentResolve;
|
||||
let currentOptions;
|
||||
let hasChanges = false;
|
||||
let isCreating = false;
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default showEditor;
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
<div class="formDialogHeader">
|
||||
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
|
||||
<h3 class="formDialogHeaderTitle">${ButtonAddMediaLibrary}</h3>
|
||||
</div>
|
||||
|
||||
<div class="formDialogContent scrollY" style="padding-top:2em;">
|
||||
<div class="dialogContentInner dialog-content-centered">
|
||||
|
||||
<div id="fldCollectionType" class="selectContainer">
|
||||
<select is="emby-select" id="selectCollectionType" data-mini="true" required="required" label="${LabelContentType}"></select>
|
||||
<div class="collectionTypeFieldDescription fieldDescription">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtValue" required="required" label="${LabelDisplayName}" />
|
||||
</div>
|
||||
|
||||
<div class="folders">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<h1 style="margin: .5em 0;">${Folders}</h1>
|
||||
<button is="emby-button" type="button" class="fab btnAddFolder submit" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="paperList folderList hide" style="margin-bottom:2em;"></div>
|
||||
</div>
|
||||
|
||||
<div class="libraryOptions"></div>
|
||||
<form class="addLibraryForm" style="max-width:100%;">
|
||||
<div class="formDialogHeader">
|
||||
<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${ButtonBack}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>
|
||||
<h3 class="formDialogHeaderTitle">${ButtonAddMediaLibrary}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="formDialogFooter">
|
||||
<button is="emby-button" type="button" class="raised btnSubmit button-submit block formDialogFooterItem">
|
||||
<span>${ButtonOk}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="formDialogContent scrollY" style="padding-top:2em;">
|
||||
<div class="dialogContentInner dialog-content-centered">
|
||||
|
||||
<div id="fldCollectionType" class="selectContainer">
|
||||
<select is="emby-select" id="selectCollectionType" data-mini="true" required="required" label="${LabelContentType}"></select>
|
||||
<div class="collectionTypeFieldDescription fieldDescription">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtValue" required="required" label="${LabelDisplayName}" />
|
||||
</div>
|
||||
|
||||
<div class="folders">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<h1 style="margin: .5em 0;">${Folders}</h1>
|
||||
<button is="emby-button" type="button" class="fab btnAddFolder submit" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="paperList folderList hide" style="margin-bottom:2em;"></div>
|
||||
</div>
|
||||
|
||||
<div class="libraryOptions"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="formDialogFooter">
|
||||
<button is="emby-button" type="submit" class="raised btnSubmit button-submit block formDialogFooterItem">
|
||||
<span>${ButtonOk}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for media library editor.
|
||||
@@ -23,180 +22,180 @@ import toast from '../toast/toast';
|
||||
import confirm from '../confirm/confirm';
|
||||
import template from './mediaLibraryEditor.template.html';
|
||||
|
||||
function onEditLibrary() {
|
||||
if (isCreating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
isCreating = true;
|
||||
loading.show();
|
||||
const dlg = dom.parentWithClass(this, 'dlg-libraryeditor');
|
||||
let libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector('.libraryOptions'));
|
||||
libraryOptions = Object.assign(currentOptions.library.LibraryOptions || {}, libraryOptions);
|
||||
ApiClient.updateVirtualFolderOptions(currentOptions.library.ItemId, libraryOptions).then(() => {
|
||||
hasChanges = true;
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
dialogHelper.close(dlg);
|
||||
}, () => {
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
});
|
||||
function onEditLibrary() {
|
||||
if (isCreating) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function addMediaLocation(page, path, networkSharePath) {
|
||||
const virtualFolder = currentOptions.library;
|
||||
isCreating = true;
|
||||
loading.show();
|
||||
const dlg = dom.parentWithClass(this, 'dlg-libraryeditor');
|
||||
let libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector('.libraryOptions'));
|
||||
libraryOptions = Object.assign(currentOptions.library.LibraryOptions || {}, libraryOptions);
|
||||
ApiClient.updateVirtualFolderOptions(currentOptions.library.ItemId, libraryOptions).then(() => {
|
||||
hasChanges = true;
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
dialogHelper.close(dlg);
|
||||
}, () => {
|
||||
isCreating = false;
|
||||
loading.hide();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function addMediaLocation(page, path, networkSharePath) {
|
||||
const virtualFolder = currentOptions.library;
|
||||
const refreshAfterChange = currentOptions.refresh;
|
||||
ApiClient.addMediaPath(virtualFolder.Name, path, networkSharePath, refreshAfterChange).then(() => {
|
||||
hasChanges = true;
|
||||
refreshLibraryFromServer(page);
|
||||
}, () => {
|
||||
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
|
||||
});
|
||||
}
|
||||
|
||||
function updateMediaLocation(page, path, networkSharePath) {
|
||||
const virtualFolder = currentOptions.library;
|
||||
ApiClient.updateMediaPath(virtualFolder.Name, {
|
||||
Path: path,
|
||||
NetworkPath: networkSharePath
|
||||
}).then(() => {
|
||||
hasChanges = true;
|
||||
refreshLibraryFromServer(page);
|
||||
}, () => {
|
||||
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
|
||||
});
|
||||
}
|
||||
|
||||
function onRemoveClick(btnRemovePath, location) {
|
||||
const button = btnRemovePath;
|
||||
const virtualFolder = currentOptions.library;
|
||||
|
||||
confirm({
|
||||
title: globalize.translate('HeaderRemoveMediaLocation'),
|
||||
text: globalize.translate('MessageConfirmRemoveMediaLocation'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(() => {
|
||||
const refreshAfterChange = currentOptions.refresh;
|
||||
ApiClient.addMediaPath(virtualFolder.Name, path, networkSharePath, refreshAfterChange).then(() => {
|
||||
ApiClient.removeMediaPath(virtualFolder.Name, location, refreshAfterChange).then(() => {
|
||||
hasChanges = true;
|
||||
refreshLibraryFromServer(page);
|
||||
refreshLibraryFromServer(dom.parentWithClass(button, 'dlg-libraryeditor'));
|
||||
}, () => {
|
||||
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
|
||||
toast(globalize.translate('ErrorDefault'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onListItemClick(e) {
|
||||
const listItem = dom.parentWithClass(e.target, 'listItem');
|
||||
|
||||
if (listItem) {
|
||||
const index = parseInt(listItem.getAttribute('data-index'), 10);
|
||||
const pathInfos = (currentOptions.library.LibraryOptions || {}).PathInfos || [];
|
||||
const pathInfo = index == null ? {} : pathInfos[index] || {};
|
||||
const originalPath = pathInfo.Path || (index == null ? null : currentOptions.library.Locations[index]);
|
||||
const btnRemovePath = dom.parentWithClass(e.target, 'btnRemovePath');
|
||||
|
||||
if (btnRemovePath) {
|
||||
onRemoveClick(btnRemovePath, originalPath);
|
||||
return;
|
||||
}
|
||||
|
||||
showDirectoryBrowser(dom.parentWithClass(listItem, 'dlg-libraryeditor'), originalPath, pathInfo.NetworkPath);
|
||||
}
|
||||
}
|
||||
|
||||
function getFolderHtml(pathInfo, index) {
|
||||
let html = '';
|
||||
html += `<div class="listItem listItem-border lnkPath" data-index="${index}">`;
|
||||
html += `<div class="${pathInfo.NetworkPath ? 'listItemBody two-line' : 'listItemBody'}">`;
|
||||
html += '<h3 class="listItemBodyText">';
|
||||
html += escapeHtml(pathInfo.Path);
|
||||
html += '</h3>';
|
||||
|
||||
if (pathInfo.NetworkPath) {
|
||||
html += `<div class="listItemBodyText secondary">${escapeHtml(pathInfo.NetworkPath)}</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += `<button type="button" is="paper-icon-button-light" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function refreshLibraryFromServer(page) {
|
||||
ApiClient.getVirtualFolders().then(result => {
|
||||
const library = result.filter(f => {
|
||||
return f.Name === currentOptions.library.Name;
|
||||
})[0];
|
||||
|
||||
if (library) {
|
||||
currentOptions.library = library;
|
||||
renderLibrary(page, currentOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderLibrary(page, options) {
|
||||
let pathInfos = (options.library.LibraryOptions || {}).PathInfos || [];
|
||||
|
||||
if (!pathInfos.length) {
|
||||
pathInfos = options.library.Locations.map(p => {
|
||||
return {
|
||||
Path: p
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function updateMediaLocation(page, path, networkSharePath) {
|
||||
const virtualFolder = currentOptions.library;
|
||||
ApiClient.updateMediaPath(virtualFolder.Name, {
|
||||
Path: path,
|
||||
NetworkPath: networkSharePath
|
||||
}).then(() => {
|
||||
hasChanges = true;
|
||||
refreshLibraryFromServer(page);
|
||||
}, () => {
|
||||
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
|
||||
});
|
||||
if (options.library.CollectionType === 'boxsets') {
|
||||
page.querySelector('.folders').classList.add('hide');
|
||||
} else {
|
||||
page.querySelector('.folders').classList.remove('hide');
|
||||
}
|
||||
|
||||
function onRemoveClick(btnRemovePath, location) {
|
||||
const button = btnRemovePath;
|
||||
const virtualFolder = currentOptions.library;
|
||||
page.querySelector('.folderList').innerHTML = pathInfos.map(getFolderHtml).join('');
|
||||
}
|
||||
|
||||
confirm({
|
||||
title: globalize.translate('HeaderRemoveMediaLocation'),
|
||||
text: globalize.translate('MessageConfirmRemoveMediaLocation'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(() => {
|
||||
const refreshAfterChange = currentOptions.refresh;
|
||||
ApiClient.removeMediaPath(virtualFolder.Name, location, refreshAfterChange).then(() => {
|
||||
hasChanges = true;
|
||||
refreshLibraryFromServer(dom.parentWithClass(button, 'dlg-libraryeditor'));
|
||||
}, () => {
|
||||
toast(globalize.translate('ErrorDefault'));
|
||||
});
|
||||
});
|
||||
}
|
||||
function onAddButtonClick() {
|
||||
showDirectoryBrowser(dom.parentWithClass(this, 'dlg-libraryeditor'));
|
||||
}
|
||||
|
||||
function onListItemClick(e) {
|
||||
const listItem = dom.parentWithClass(e.target, 'listItem');
|
||||
|
||||
if (listItem) {
|
||||
const index = parseInt(listItem.getAttribute('data-index'), 10);
|
||||
const pathInfos = (currentOptions.library.LibraryOptions || {}).PathInfos || [];
|
||||
const pathInfo = index == null ? {} : pathInfos[index] || {};
|
||||
const originalPath = pathInfo.Path || (index == null ? null : currentOptions.library.Locations[index]);
|
||||
const btnRemovePath = dom.parentWithClass(e.target, 'btnRemovePath');
|
||||
|
||||
if (btnRemovePath) {
|
||||
onRemoveClick(btnRemovePath, originalPath);
|
||||
return;
|
||||
}
|
||||
|
||||
showDirectoryBrowser(dom.parentWithClass(listItem, 'dlg-libraryeditor'), originalPath, pathInfo.NetworkPath);
|
||||
}
|
||||
}
|
||||
|
||||
function getFolderHtml(pathInfo, index) {
|
||||
let html = '';
|
||||
html += `<div class="listItem listItem-border lnkPath" data-index="${index}">`;
|
||||
html += `<div class="${pathInfo.NetworkPath ? 'listItemBody two-line' : 'listItemBody'}">`;
|
||||
html += '<h3 class="listItemBodyText">';
|
||||
html += escapeHtml(pathInfo.Path);
|
||||
html += '</h3>';
|
||||
|
||||
if (pathInfo.NetworkPath) {
|
||||
html += `<div class="listItemBodyText secondary">${escapeHtml(pathInfo.NetworkPath)}</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += `<button type="button" is="paper-icon-button-light" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function refreshLibraryFromServer(page) {
|
||||
ApiClient.getVirtualFolders().then(result => {
|
||||
const library = result.filter(f => {
|
||||
return f.Name === currentOptions.library.Name;
|
||||
})[0];
|
||||
|
||||
if (library) {
|
||||
currentOptions.library = library;
|
||||
renderLibrary(page, currentOptions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderLibrary(page, options) {
|
||||
let pathInfos = (options.library.LibraryOptions || {}).PathInfos || [];
|
||||
|
||||
if (!pathInfos.length) {
|
||||
pathInfos = options.library.Locations.map(p => {
|
||||
return {
|
||||
Path: p
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (options.library.CollectionType === 'boxsets') {
|
||||
page.querySelector('.folders').classList.add('hide');
|
||||
} else {
|
||||
page.querySelector('.folders').classList.remove('hide');
|
||||
}
|
||||
|
||||
page.querySelector('.folderList').innerHTML = pathInfos.map(getFolderHtml).join('');
|
||||
}
|
||||
|
||||
function onAddButtonClick() {
|
||||
showDirectoryBrowser(dom.parentWithClass(this, 'dlg-libraryeditor'));
|
||||
}
|
||||
|
||||
function showDirectoryBrowser(context, originalPath, networkPath) {
|
||||
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
enableNetworkSharePath: true,
|
||||
pathReadOnly: originalPath != null,
|
||||
path: originalPath,
|
||||
networkSharePath: networkPath,
|
||||
callback: function (path, networkSharePath) {
|
||||
if (path) {
|
||||
if (originalPath) {
|
||||
updateMediaLocation(context, originalPath, networkSharePath);
|
||||
} else {
|
||||
addMediaLocation(context, path, networkSharePath);
|
||||
}
|
||||
function showDirectoryBrowser(context, originalPath, networkPath) {
|
||||
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
enableNetworkSharePath: true,
|
||||
pathReadOnly: originalPath != null,
|
||||
path: originalPath,
|
||||
networkSharePath: networkPath,
|
||||
callback: function (path, networkSharePath) {
|
||||
if (path) {
|
||||
if (originalPath) {
|
||||
updateMediaLocation(context, originalPath, networkSharePath);
|
||||
} else {
|
||||
addMediaLocation(context, path, networkSharePath);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initEditor(dlg, options) {
|
||||
renderLibrary(dlg, options);
|
||||
dlg.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
|
||||
dlg.querySelector('.folderList').addEventListener('click', onListItemClick);
|
||||
dlg.querySelector('.btnSubmit').addEventListener('click', onEditLibrary);
|
||||
libraryoptionseditor.embed(dlg.querySelector('.libraryOptions'), options.library.CollectionType, options.library.LibraryOptions);
|
||||
}
|
||||
function initEditor(dlg, options) {
|
||||
renderLibrary(dlg, options);
|
||||
dlg.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
|
||||
dlg.querySelector('.folderList').addEventListener('click', onListItemClick);
|
||||
dlg.querySelector('.btnSubmit').addEventListener('click', onEditLibrary);
|
||||
libraryoptionseditor.embed(dlg.querySelector('.libraryOptions'), options.library.CollectionType, options.library.LibraryOptions);
|
||||
}
|
||||
|
||||
function onDialogClosed() {
|
||||
currentDeferred.resolveWith(null, [hasChanges]);
|
||||
}
|
||||
function onDialogClosed() {
|
||||
currentDeferred.resolveWith(null, [hasChanges]);
|
||||
}
|
||||
|
||||
export class showEditor {
|
||||
constructor(options) {
|
||||
@@ -227,10 +226,9 @@ export class showEditor {
|
||||
}
|
||||
}
|
||||
|
||||
let currentDeferred;
|
||||
let currentOptions;
|
||||
let hasChanges = false;
|
||||
let isCreating = false;
|
||||
let currentDeferred;
|
||||
let currentOptions;
|
||||
let hasChanges = false;
|
||||
let isCreating = false;
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default showEditor;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,94 +8,91 @@ import '../../elements/emby-select/emby-select';
|
||||
import '../formdialog.scss';
|
||||
import template from './personEditor.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
}
|
||||
function show(person) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
function show(person) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
let html = '';
|
||||
let submitted = false;
|
||||
|
||||
html += globalize.translateHtml(template, 'core');
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
dlg.querySelector('.txtPersonName', dlg).value = person.Name || '';
|
||||
dlg.querySelector('.selectPersonType', dlg).value = person.Type || '';
|
||||
dlg.querySelector('.txtPersonRole', dlg).value = person.Role || '';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||
}
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
dlg.addEventListener('close', function () {
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
resolve(person);
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
reject();
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
let html = '';
|
||||
let submitted = false;
|
||||
|
||||
html += globalize.translateHtml(template, 'core');
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
dlg.querySelector('.txtPersonName', dlg).value = person.Name || '';
|
||||
dlg.querySelector('.selectPersonType', dlg).value = person.Type || '';
|
||||
dlg.querySelector('.txtPersonRole', dlg).value = person.Role || '';
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||
}
|
||||
|
||||
dialogHelper.open(dlg);
|
||||
|
||||
dlg.addEventListener('close', function () {
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
resolve(person);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
|
||||
dlg.querySelector('.selectPersonType').addEventListener('change', function () {
|
||||
if (this.value === 'Actor') {
|
||||
dlg.querySelector('.fldRole').classList.remove('hide');
|
||||
} else {
|
||||
dlg.querySelector('.fldRole').classList.add('hide');
|
||||
}
|
||||
});
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
dlg.querySelector('form').addEventListener('submit', function (e) {
|
||||
submitted = true;
|
||||
|
||||
person.Name = dlg.querySelector('.txtPersonName', dlg).value;
|
||||
person.Type = dlg.querySelector('.selectPersonType', dlg).value;
|
||||
person.Role = dlg.querySelector('.txtPersonRole', dlg).value || null;
|
||||
|
||||
dialogHelper.close(dlg);
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
dlg.querySelector('.selectPersonType').dispatchEvent(new CustomEvent('change', {
|
||||
bubbles: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
dlg.querySelector('.selectPersonType').addEventListener('change', function () {
|
||||
if (this.value === 'Actor') {
|
||||
dlg.querySelector('.fldRole').classList.remove('hide');
|
||||
} else {
|
||||
dlg.querySelector('.fldRole').classList.add('hide');
|
||||
}
|
||||
});
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', function () {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
dlg.querySelector('form').addEventListener('submit', function (e) {
|
||||
submitted = true;
|
||||
|
||||
person.Name = dlg.querySelector('.txtPersonName', dlg).value;
|
||||
person.Type = dlg.querySelector('.selectPersonType', dlg).value;
|
||||
person.Role = dlg.querySelector('.txtPersonRole', dlg).value || null;
|
||||
|
||||
dialogHelper.close(dlg);
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
|
||||
dlg.querySelector('.selectPersonType').dispatchEvent(new CustomEvent('change', {
|
||||
bubbles: true
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
show: show
|
||||
};
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import serverNotifications from '../../scripts/serverNotifications';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import Events from '../../utils/events.ts';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
|
||||
|
||||
import NotificationIcon from './notificationicon.png';
|
||||
|
||||
@@ -130,7 +131,7 @@ function onLibraryChanged(data, apiClient) {
|
||||
newItems.length = 12;
|
||||
}
|
||||
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), {
|
||||
getItems(apiClient, apiClient.getCurrentUserId(), {
|
||||
|
||||
Recursive: true,
|
||||
Limit: 3,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,17 @@
|
||||
import appSettings from '../scripts/settings/appSettings';
|
||||
import { pluginManager } from './pluginManager';
|
||||
/* eslint-disable indent */
|
||||
|
||||
class PackageManager {
|
||||
#packagesList = [];
|
||||
#settingsKey = 'installedpackages1';
|
||||
class PackageManager {
|
||||
#packagesList = [];
|
||||
#settingsKey = 'installedpackages1';
|
||||
|
||||
init() {
|
||||
console.groupCollapsed('loading packages');
|
||||
const manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
|
||||
init() {
|
||||
console.groupCollapsed('loading packages');
|
||||
const manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
|
||||
|
||||
return Promise.all(manifestUrls.map((url) => {
|
||||
return this.loadPackage(url);
|
||||
}))
|
||||
return Promise.all(manifestUrls.map((url) => {
|
||||
return this.loadPackage(url);
|
||||
}))
|
||||
.then(() => {
|
||||
console.debug('finished loading packages');
|
||||
return Promise.resolve();
|
||||
@@ -22,119 +21,117 @@ import { pluginManager } from './pluginManager';
|
||||
}).finally(() => {
|
||||
console.groupEnd('loading packages');
|
||||
});
|
||||
}
|
||||
|
||||
get packages() {
|
||||
return this.#packagesList.slice(0);
|
||||
}
|
||||
|
||||
install(url) {
|
||||
return this.loadPackage(url, true).then((pkg) => {
|
||||
const manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
|
||||
|
||||
if (!manifestUrls.includes(url)) {
|
||||
manifestUrls.push(url);
|
||||
appSettings.set(this.#settingsKey, JSON.stringify(manifestUrls));
|
||||
}
|
||||
|
||||
return pkg;
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(name) {
|
||||
const pkg = this.#packagesList.filter((p) => {
|
||||
return p.name === name;
|
||||
})[0];
|
||||
|
||||
if (pkg) {
|
||||
this.#packagesList = this.#packagesList.filter((p) => {
|
||||
return p.name !== name;
|
||||
});
|
||||
|
||||
this.removeUrl(pkg.url);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
mapPath(pkg, pluginUrl) {
|
||||
const urlLower = pluginUrl.toLowerCase();
|
||||
if (urlLower.startsWith('http:') || urlLower.startsWith('https:') || urlLower.startsWith('file:')) {
|
||||
return pluginUrl;
|
||||
}
|
||||
|
||||
let packageUrl = pkg.url;
|
||||
packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf('/'));
|
||||
|
||||
packageUrl += '/';
|
||||
packageUrl += pluginUrl;
|
||||
|
||||
return packageUrl;
|
||||
}
|
||||
|
||||
addPackage(pkg) {
|
||||
this.#packagesList = this.#packagesList.filter((p) => {
|
||||
return p.name !== pkg.name;
|
||||
});
|
||||
|
||||
this.#packagesList.push(pkg);
|
||||
}
|
||||
|
||||
removeUrl(url) {
|
||||
let manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
|
||||
|
||||
manifestUrls = manifestUrls.filter((i) => {
|
||||
return i !== url;
|
||||
});
|
||||
|
||||
appSettings.set(this.#settingsKey, JSON.stringify(manifestUrls));
|
||||
}
|
||||
|
||||
loadPackage(url, throwError = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const originalUrl = url;
|
||||
url += url.indexOf('?') === -1 ? '?' : '&';
|
||||
url += 't=' + new Date().getTime();
|
||||
|
||||
xhr.open('GET', url, true);
|
||||
|
||||
const onError = () => {
|
||||
if (throwError === true) {
|
||||
reject();
|
||||
} else {
|
||||
this.removeUrl(originalUrl);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (this.status < 400) {
|
||||
const pkg = JSON.parse(this.response);
|
||||
pkg.url = originalUrl;
|
||||
|
||||
this.addPackage(pkg);
|
||||
|
||||
const plugins = pkg.plugins || [];
|
||||
if (pkg.plugin) {
|
||||
plugins.push(pkg.plugin);
|
||||
}
|
||||
const promises = plugins.map((pluginUrl) => {
|
||||
return pluginManager.loadPlugin(this.mapPath(pkg, pluginUrl));
|
||||
});
|
||||
Promise.all(promises).then(resolve, resolve);
|
||||
} else {
|
||||
onError();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = onError;
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
get packages() {
|
||||
return this.#packagesList.slice(0);
|
||||
}
|
||||
|
||||
install(url) {
|
||||
return this.loadPackage(url, true).then((pkg) => {
|
||||
const manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
|
||||
|
||||
if (!manifestUrls.includes(url)) {
|
||||
manifestUrls.push(url);
|
||||
appSettings.set(this.#settingsKey, JSON.stringify(manifestUrls));
|
||||
}
|
||||
|
||||
return pkg;
|
||||
});
|
||||
}
|
||||
|
||||
uninstall(name) {
|
||||
const pkg = this.#packagesList.filter((p) => {
|
||||
return p.name === name;
|
||||
})[0];
|
||||
|
||||
if (pkg) {
|
||||
this.#packagesList = this.#packagesList.filter((p) => {
|
||||
return p.name !== name;
|
||||
});
|
||||
|
||||
this.removeUrl(pkg.url);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
mapPath(pkg, pluginUrl) {
|
||||
const urlLower = pluginUrl.toLowerCase();
|
||||
if (urlLower.startsWith('http:') || urlLower.startsWith('https:') || urlLower.startsWith('file:')) {
|
||||
return pluginUrl;
|
||||
}
|
||||
|
||||
let packageUrl = pkg.url;
|
||||
packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf('/'));
|
||||
|
||||
packageUrl += '/';
|
||||
packageUrl += pluginUrl;
|
||||
|
||||
return packageUrl;
|
||||
}
|
||||
|
||||
addPackage(pkg) {
|
||||
this.#packagesList = this.#packagesList.filter((p) => {
|
||||
return p.name !== pkg.name;
|
||||
});
|
||||
|
||||
this.#packagesList.push(pkg);
|
||||
}
|
||||
|
||||
removeUrl(url) {
|
||||
let manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
|
||||
|
||||
manifestUrls = manifestUrls.filter((i) => {
|
||||
return i !== url;
|
||||
});
|
||||
|
||||
appSettings.set(this.#settingsKey, JSON.stringify(manifestUrls));
|
||||
}
|
||||
|
||||
loadPackage(url, throwError = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const originalUrl = url;
|
||||
url += url.indexOf('?') === -1 ? '?' : '&';
|
||||
url += 't=' + new Date().getTime();
|
||||
|
||||
xhr.open('GET', url, true);
|
||||
|
||||
const onError = () => {
|
||||
if (throwError === true) {
|
||||
reject();
|
||||
} else {
|
||||
this.removeUrl(originalUrl);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (this.status < 400) {
|
||||
const pkg = JSON.parse(this.response);
|
||||
pkg.url = originalUrl;
|
||||
|
||||
this.addPackage(pkg);
|
||||
|
||||
const plugins = pkg.plugins || [];
|
||||
if (pkg.plugin) {
|
||||
plugins.push(pkg.plugin);
|
||||
}
|
||||
const promises = plugins.map((pluginUrl) => {
|
||||
return pluginManager.loadPlugin(this.mapPath(pkg, pluginUrl));
|
||||
});
|
||||
Promise.all(promises).then(resolve, resolve);
|
||||
} else {
|
||||
onError();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = onError;
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new PackageManager();
|
||||
|
||||
@@ -4,259 +4,256 @@ import Events from '../../utils/events.ts';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
import shell from '../../scripts/shell';
|
||||
|
||||
/* eslint-disable indent */
|
||||
// Reports media playback to the device for lock screen control
|
||||
|
||||
// Reports media playback to the device for lock screen control
|
||||
let currentPlayer;
|
||||
|
||||
let currentPlayer;
|
||||
function seriesImageUrl(item, options = {}) {
|
||||
options.type = options.type || 'Primary';
|
||||
|
||||
function seriesImageUrl(item, options = {}) {
|
||||
options.type = options.type || 'Primary';
|
||||
if (item.Type !== 'Episode') {
|
||||
return null;
|
||||
} else if (options.type === 'Primary' && item.SeriesPrimaryImageTag) {
|
||||
options.tag = item.SeriesPrimaryImageTag;
|
||||
|
||||
if (item.Type !== 'Episode') {
|
||||
return null;
|
||||
} else if (options.type === 'Primary' && item.SeriesPrimaryImageTag) {
|
||||
options.tag = item.SeriesPrimaryImageTag;
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options);
|
||||
} else if (options.type === 'Thumb') {
|
||||
if (item.SeriesThumbImageTag) {
|
||||
options.tag = item.SeriesThumbImageTag;
|
||||
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options);
|
||||
} else if (options.type === 'Thumb') {
|
||||
if (item.SeriesThumbImageTag) {
|
||||
options.tag = item.SeriesThumbImageTag;
|
||||
} else if (item.ParentThumbImageTag) {
|
||||
options.tag = item.ParentThumbImageTag;
|
||||
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options);
|
||||
} else if (item.ParentThumbImageTag) {
|
||||
options.tag = item.ParentThumbImageTag;
|
||||
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options);
|
||||
}
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function imageUrl(item, options = {}) {
|
||||
options.type = options.type || 'Primary';
|
||||
|
||||
if (item.ImageTags && item.ImageTags[options.type]) {
|
||||
options.tag = item.ImageTags[options.type];
|
||||
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.Id, options);
|
||||
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
|
||||
options.tag = item.AlbumPrimaryImageTag;
|
||||
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getImageUrl(item, imageOptions = {}) {
|
||||
const url = seriesImageUrl(item, imageOptions) || imageUrl(item, imageOptions);
|
||||
|
||||
if (url) {
|
||||
const height = imageOptions.height || imageOptions.maxHeight;
|
||||
|
||||
return {
|
||||
src: url,
|
||||
sizes: height + 'x' + height
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function imageUrl(item, options = {}) {
|
||||
options.type = options.type || 'Primary';
|
||||
function getImageUrls(item, imageSizes = [96, 128, 192, 256, 384, 512]) {
|
||||
const list = [];
|
||||
|
||||
if (item.ImageTags && item.ImageTags[options.type]) {
|
||||
options.tag = item.ImageTags[options.type];
|
||||
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.Id, options);
|
||||
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
|
||||
options.tag = item.AlbumPrimaryImageTag;
|
||||
|
||||
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options);
|
||||
imageSizes.forEach((size) => {
|
||||
const url = getImageUrl(item, { height: size });
|
||||
if (url !== null) {
|
||||
list.push(url);
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
return list;
|
||||
}
|
||||
|
||||
function updatePlayerState(player, state, eventName) {
|
||||
// Don't go crazy reporting position changes
|
||||
if (eventName === 'timeupdate') {
|
||||
// Only report if this item hasn't been reported yet, or if there's an actual playback change.
|
||||
// Don't report on simple time updates
|
||||
return;
|
||||
}
|
||||
|
||||
function getImageUrl(item, imageOptions = {}) {
|
||||
const url = seriesImageUrl(item, imageOptions) || imageUrl(item, imageOptions);
|
||||
const item = state.NowPlayingItem;
|
||||
|
||||
if (url) {
|
||||
const height = imageOptions.height || imageOptions.maxHeight;
|
||||
|
||||
return {
|
||||
src: url,
|
||||
sizes: height + 'x' + height
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getImageUrls(item, imageSizes = [96, 128, 192, 256, 384, 512]) {
|
||||
const list = [];
|
||||
|
||||
imageSizes.forEach((size) => {
|
||||
const url = getImageUrl(item, {height: size});
|
||||
if (url !== null) {
|
||||
list.push(url);
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function updatePlayerState(player, state, eventName) {
|
||||
// Don't go crazy reporting position changes
|
||||
if (eventName === 'timeupdate') {
|
||||
// Only report if this item hasn't been reported yet, or if there's an actual playback change.
|
||||
// Don't report on simple time updates
|
||||
return;
|
||||
}
|
||||
|
||||
const item = state.NowPlayingItem;
|
||||
|
||||
if (!item) {
|
||||
hideMediaControls();
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventName === 'init') { // transform "init" event into "timeupdate" to restraint update rate
|
||||
eventName = 'timeupdate';
|
||||
}
|
||||
|
||||
const isVideo = item.MediaType === 'Video';
|
||||
const isLocalPlayer = player.isLocalPlayer || false;
|
||||
|
||||
// Local players do their own notifications
|
||||
if (isLocalPlayer && isVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playState = state.PlayState || {};
|
||||
const parts = nowPlayingHelper.getNowPlayingNames(item);
|
||||
const artist = parts[parts.length - 1].text;
|
||||
const title = parts.length === 1 ? '' : parts[0].text;
|
||||
|
||||
const album = item.Album || '';
|
||||
const itemId = item.Id;
|
||||
|
||||
// Convert to ms
|
||||
const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10);
|
||||
const currentTime = parseInt(playState.PositionTicks ? (playState.PositionTicks / 10000) : 0, 10);
|
||||
|
||||
const isPaused = playState.IsPaused || false;
|
||||
const canSeek = playState.CanSeek || false;
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: title,
|
||||
artist: artist,
|
||||
album: album,
|
||||
artwork: getImageUrls(item)
|
||||
});
|
||||
} else {
|
||||
const itemImageUrl = seriesImageUrl(item, { maxHeight: 3000 }) || imageUrl(item, { maxHeight: 3000 });
|
||||
shell.updateMediaSession({
|
||||
action: eventName,
|
||||
isLocalPlayer: isLocalPlayer,
|
||||
itemId: itemId,
|
||||
title: title,
|
||||
artist: artist,
|
||||
album: album,
|
||||
duration: duration,
|
||||
position: currentTime,
|
||||
imageUrl: itemImageUrl,
|
||||
canSeek: canSeek,
|
||||
isPaused: isPaused
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onGeneralEvent(e) {
|
||||
const state = playbackManager.getPlayerState(this);
|
||||
|
||||
updatePlayerState(this, state, e.type);
|
||||
}
|
||||
|
||||
function onStateChanged(e, state) {
|
||||
updatePlayerState(this, state, 'statechange');
|
||||
}
|
||||
|
||||
function onPlaybackStart(e, state) {
|
||||
updatePlayerState(this, state, e.type);
|
||||
}
|
||||
|
||||
function onPlaybackStopped() {
|
||||
if (!item) {
|
||||
hideMediaControls();
|
||||
return;
|
||||
}
|
||||
|
||||
function releaseCurrentPlayer() {
|
||||
if (currentPlayer) {
|
||||
Events.off(currentPlayer, 'playbackstart', onPlaybackStart);
|
||||
Events.off(currentPlayer, 'playbackstop', onPlaybackStopped);
|
||||
Events.off(currentPlayer, 'unpause', onGeneralEvent);
|
||||
Events.off(currentPlayer, 'pause', onGeneralEvent);
|
||||
Events.off(currentPlayer, 'statechange', onStateChanged);
|
||||
Events.off(currentPlayer, 'timeupdate', onGeneralEvent);
|
||||
|
||||
currentPlayer = null;
|
||||
|
||||
hideMediaControls();
|
||||
}
|
||||
if (eventName === 'init') { // transform "init" event into "timeupdate" to restraint update rate
|
||||
eventName = 'timeupdate';
|
||||
}
|
||||
|
||||
function hideMediaControls() {
|
||||
if ('mediaSession' in navigator) {
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.metadata = null;
|
||||
} else {
|
||||
shell.hideMediaSession();
|
||||
}
|
||||
const isVideo = item.MediaType === 'Video';
|
||||
const isLocalPlayer = player.isLocalPlayer || false;
|
||||
|
||||
// Local players do their own notifications
|
||||
if (isLocalPlayer && isVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
function bindToPlayer(player) {
|
||||
releaseCurrentPlayer();
|
||||
const playState = state.PlayState || {};
|
||||
const parts = nowPlayingHelper.getNowPlayingNames(item);
|
||||
const artist = parts[parts.length - 1].text;
|
||||
const title = parts.length === 1 ? '' : parts[0].text;
|
||||
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
const album = item.Album || '';
|
||||
const itemId = item.Id;
|
||||
|
||||
currentPlayer = player;
|
||||
// Convert to ms
|
||||
const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10);
|
||||
const currentTime = parseInt(playState.PositionTicks ? (playState.PositionTicks / 10000) : 0, 10);
|
||||
|
||||
const state = playbackManager.getPlayerState(player);
|
||||
updatePlayerState(player, state, 'init');
|
||||
|
||||
Events.on(currentPlayer, 'playbackstart', onPlaybackStart);
|
||||
Events.on(currentPlayer, 'playbackstop', onPlaybackStopped);
|
||||
Events.on(currentPlayer, 'unpause', onGeneralEvent);
|
||||
Events.on(currentPlayer, 'pause', onGeneralEvent);
|
||||
Events.on(currentPlayer, 'statechange', onStateChanged);
|
||||
Events.on(currentPlayer, 'timeupdate', onGeneralEvent);
|
||||
}
|
||||
|
||||
function execute(name) {
|
||||
playbackManager[name](currentPlayer);
|
||||
}
|
||||
const isPaused = playState.IsPaused || false;
|
||||
const canSeek = playState.CanSeek || false;
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('previoustrack', function () {
|
||||
execute('previousTrack');
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: title,
|
||||
artist: artist,
|
||||
album: album,
|
||||
artwork: getImageUrls(item)
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('nexttrack', function () {
|
||||
execute('nextTrack');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('play', function () {
|
||||
execute('unpause');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('pause', function () {
|
||||
execute('pause');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('seekbackward', function () {
|
||||
execute('rewind');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('seekforward', function () {
|
||||
execute('fastForward');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('seekto', function (object) {
|
||||
const item = playbackManager.getPlayerState(currentPlayer).NowPlayingItem;
|
||||
// Convert to ms
|
||||
const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10);
|
||||
const wantedTime = object.seekTime * 1000;
|
||||
playbackManager.seekPercent(wantedTime / duration * 100, currentPlayer);
|
||||
} else {
|
||||
const itemImageUrl = seriesImageUrl(item, { maxHeight: 3000 }) || imageUrl(item, { maxHeight: 3000 });
|
||||
shell.updateMediaSession({
|
||||
action: eventName,
|
||||
isLocalPlayer: isLocalPlayer,
|
||||
itemId: itemId,
|
||||
title: title,
|
||||
artist: artist,
|
||||
album: album,
|
||||
duration: duration,
|
||||
position: currentTime,
|
||||
imageUrl: itemImageUrl,
|
||||
canSeek: canSeek,
|
||||
isPaused: isPaused
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Events.on(playbackManager, 'playerchange', function () {
|
||||
bindToPlayer(playbackManager.getCurrentPlayer());
|
||||
function onGeneralEvent(e) {
|
||||
const state = playbackManager.getPlayerState(this);
|
||||
|
||||
updatePlayerState(this, state, e.type);
|
||||
}
|
||||
|
||||
function onStateChanged(e, state) {
|
||||
updatePlayerState(this, state, 'statechange');
|
||||
}
|
||||
|
||||
function onPlaybackStart(e, state) {
|
||||
updatePlayerState(this, state, e.type);
|
||||
}
|
||||
|
||||
function onPlaybackStopped() {
|
||||
hideMediaControls();
|
||||
}
|
||||
|
||||
function releaseCurrentPlayer() {
|
||||
if (currentPlayer) {
|
||||
Events.off(currentPlayer, 'playbackstart', onPlaybackStart);
|
||||
Events.off(currentPlayer, 'playbackstop', onPlaybackStopped);
|
||||
Events.off(currentPlayer, 'unpause', onGeneralEvent);
|
||||
Events.off(currentPlayer, 'pause', onGeneralEvent);
|
||||
Events.off(currentPlayer, 'statechange', onStateChanged);
|
||||
Events.off(currentPlayer, 'timeupdate', onGeneralEvent);
|
||||
|
||||
currentPlayer = null;
|
||||
|
||||
hideMediaControls();
|
||||
}
|
||||
}
|
||||
|
||||
function hideMediaControls() {
|
||||
if ('mediaSession' in navigator) {
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.metadata = null;
|
||||
} else {
|
||||
shell.hideMediaSession();
|
||||
}
|
||||
}
|
||||
|
||||
function bindToPlayer(player) {
|
||||
releaseCurrentPlayer();
|
||||
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentPlayer = player;
|
||||
|
||||
const state = playbackManager.getPlayerState(player);
|
||||
updatePlayerState(player, state, 'init');
|
||||
|
||||
Events.on(currentPlayer, 'playbackstart', onPlaybackStart);
|
||||
Events.on(currentPlayer, 'playbackstop', onPlaybackStopped);
|
||||
Events.on(currentPlayer, 'unpause', onGeneralEvent);
|
||||
Events.on(currentPlayer, 'pause', onGeneralEvent);
|
||||
Events.on(currentPlayer, 'statechange', onStateChanged);
|
||||
Events.on(currentPlayer, 'timeupdate', onGeneralEvent);
|
||||
}
|
||||
|
||||
function execute(name) {
|
||||
playbackManager[name](currentPlayer);
|
||||
}
|
||||
|
||||
if ('mediaSession' in navigator) {
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('previoustrack', function () {
|
||||
execute('previousTrack');
|
||||
});
|
||||
|
||||
bindToPlayer(playbackManager.getCurrentPlayer());
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('nexttrack', function () {
|
||||
execute('nextTrack');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('play', function () {
|
||||
execute('unpause');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('pause', function () {
|
||||
execute('pause');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('seekbackward', function () {
|
||||
execute('rewind');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('seekforward', function () {
|
||||
execute('fastForward');
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line compat/compat */
|
||||
navigator.mediaSession.setActionHandler('seekto', function (object) {
|
||||
const item = playbackManager.getPlayerState(currentPlayer).NowPlayingItem;
|
||||
// Convert to ms
|
||||
const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10);
|
||||
const wantedTime = object.seekTime * 1000;
|
||||
playbackManager.seekPercent(wantedTime / duration * 100, currentPlayer);
|
||||
});
|
||||
}
|
||||
|
||||
Events.on(playbackManager, 'playerchange', function () {
|
||||
bindToPlayer(playbackManager.getCurrentPlayer());
|
||||
});
|
||||
|
||||
bindToPlayer(playbackManager.getCurrentPlayer());
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -13,6 +13,7 @@ import ServerConnections from '../ServerConnections';
|
||||
import alert from '../alert';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
import { includesAny } from '../../utils/container.ts';
|
||||
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
|
||||
|
||||
const UNLIMITED_ITEMS = -1;
|
||||
|
||||
@@ -127,7 +128,7 @@ function getItemsForPlayback(serverId, query) {
|
||||
query.EnableTotalRecordCount = false;
|
||||
query.CollapseBoxSetItems = false;
|
||||
|
||||
return apiClient.getItems(apiClient.getCurrentUserId(), query);
|
||||
return getItems(apiClient, apiClient.getCurrentUserId(), query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1038,7 +1039,6 @@ class PlaybackManager {
|
||||
}
|
||||
}
|
||||
|
||||
//var mediaType = item.MediaType;
|
||||
return getPlayer(item, getDefaultPlayOptions()) != null;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,102 +12,120 @@ import ServerConnections from '../ServerConnections';
|
||||
import toast from '../toast/toast';
|
||||
import template from './playbackSettings.template.html';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function fillSkipLengths(select) {
|
||||
const options = [5, 10, 15, 20, 25, 30];
|
||||
|
||||
function fillSkipLengths(select) {
|
||||
const options = [5, 10, 15, 20, 25, 30];
|
||||
select.innerHTML = options.map(option => {
|
||||
return {
|
||||
name: globalize.translate('ValueSeconds', option),
|
||||
value: option * 1000
|
||||
};
|
||||
}).map(o => {
|
||||
return `<option value="${o.value}">${o.name}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
select.innerHTML = options.map(option => {
|
||||
return {
|
||||
name: globalize.translate('ValueSeconds', option),
|
||||
value: option * 1000
|
||||
};
|
||||
}).map(o => {
|
||||
return `<option value="${o.value}">${o.name}</option>`;
|
||||
}).join('');
|
||||
function populateLanguages(select, languages) {
|
||||
let html = '';
|
||||
|
||||
html += `<option value=''>${globalize.translate('AnyLanguage')}</option>`;
|
||||
|
||||
for (let i = 0, length = languages.length; i < length; i++) {
|
||||
const culture = languages[i];
|
||||
|
||||
html += `<option value='${culture.ThreeLetterISOLanguageName}'>${culture.DisplayName}</option>`;
|
||||
}
|
||||
|
||||
function populateLanguages(select, languages) {
|
||||
let html = '';
|
||||
select.innerHTML = html;
|
||||
}
|
||||
|
||||
html += `<option value=''>${globalize.translate('AnyLanguage')}</option>`;
|
||||
function fillQuality(select, isInNetwork, mediatype, maxVideoWidth) {
|
||||
const options = mediatype === 'Audio' ? qualityoptions.getAudioQualityOptions({
|
||||
|
||||
for (let i = 0, length = languages.length; i < length; i++) {
|
||||
const culture = languages[i];
|
||||
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
|
||||
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
|
||||
enableAuto: true
|
||||
|
||||
html += `<option value='${culture.ThreeLetterISOLanguageName}'>${culture.DisplayName}</option>`;
|
||||
}
|
||||
}) : qualityoptions.getVideoQualityOptions({
|
||||
|
||||
select.innerHTML = html;
|
||||
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
|
||||
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
|
||||
enableAuto: true,
|
||||
maxVideoWidth
|
||||
|
||||
});
|
||||
|
||||
select.innerHTML = options.map(i => {
|
||||
// render empty string instead of 0 for the auto option
|
||||
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function setMaxBitrateIntoField(select, isInNetwork, mediatype) {
|
||||
fillQuality(select, isInNetwork, mediatype);
|
||||
|
||||
if (appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype)) {
|
||||
select.value = '';
|
||||
} else {
|
||||
select.value = appSettings.maxStreamingBitrate(isInNetwork, mediatype);
|
||||
}
|
||||
}
|
||||
|
||||
function fillChromecastQuality(select, maxVideoWidth) {
|
||||
const options = qualityoptions.getVideoQualityOptions({
|
||||
|
||||
currentMaxBitrate: appSettings.maxChromecastBitrate(),
|
||||
isAutomaticBitrateEnabled: !appSettings.maxChromecastBitrate(),
|
||||
enableAuto: true,
|
||||
maxVideoWidth
|
||||
});
|
||||
|
||||
select.innerHTML = options.map(i => {
|
||||
// render empty string instead of 0 for the auto option
|
||||
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
|
||||
}).join('');
|
||||
|
||||
select.value = appSettings.maxChromecastBitrate() || '';
|
||||
}
|
||||
|
||||
function setMaxBitrateFromField(select, isInNetwork, mediatype) {
|
||||
if (select.value) {
|
||||
appSettings.maxStreamingBitrate(isInNetwork, mediatype, select.value);
|
||||
appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype, false);
|
||||
} else {
|
||||
appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype, true);
|
||||
}
|
||||
}
|
||||
|
||||
function showHideQualityFields(context, user, apiClient) {
|
||||
if (user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
context.querySelector('.videoQualitySection').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.videoQualitySection').classList.add('hide');
|
||||
}
|
||||
|
||||
function fillQuality(select, isInNetwork, mediatype, maxVideoWidth) {
|
||||
const options = mediatype === 'Audio' ? qualityoptions.getAudioQualityOptions({
|
||||
if (appHost.supports('multiserver')) {
|
||||
context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide');
|
||||
context.querySelector('.fldVideoInternetQuality').classList.remove('hide');
|
||||
|
||||
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
|
||||
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
|
||||
enableAuto: true
|
||||
|
||||
}) : qualityoptions.getVideoQualityOptions({
|
||||
|
||||
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
|
||||
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
|
||||
enableAuto: true,
|
||||
maxVideoWidth
|
||||
|
||||
});
|
||||
|
||||
select.innerHTML = options.map(i => {
|
||||
// render empty string instead of 0 for the auto option
|
||||
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function setMaxBitrateIntoField(select, isInNetwork, mediatype) {
|
||||
fillQuality(select, isInNetwork, mediatype);
|
||||
|
||||
if (appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype)) {
|
||||
select.value = '';
|
||||
if (user.Policy.EnableAudioPlaybackTranscoding) {
|
||||
context.querySelector('.musicQualitySection').classList.remove('hide');
|
||||
} else {
|
||||
select.value = appSettings.maxStreamingBitrate(isInNetwork, mediatype);
|
||||
}
|
||||
}
|
||||
|
||||
function fillChromecastQuality(select, maxVideoWidth) {
|
||||
const options = qualityoptions.getVideoQualityOptions({
|
||||
|
||||
currentMaxBitrate: appSettings.maxChromecastBitrate(),
|
||||
isAutomaticBitrateEnabled: !appSettings.maxChromecastBitrate(),
|
||||
enableAuto: true,
|
||||
maxVideoWidth
|
||||
});
|
||||
|
||||
select.innerHTML = options.map(i => {
|
||||
// render empty string instead of 0 for the auto option
|
||||
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
|
||||
}).join('');
|
||||
|
||||
select.value = appSettings.maxChromecastBitrate() || '';
|
||||
}
|
||||
|
||||
function setMaxBitrateFromField(select, isInNetwork, mediatype) {
|
||||
if (select.value) {
|
||||
appSettings.maxStreamingBitrate(isInNetwork, mediatype, select.value);
|
||||
appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype, false);
|
||||
} else {
|
||||
appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype, true);
|
||||
}
|
||||
}
|
||||
|
||||
function showHideQualityFields(context, user, apiClient) {
|
||||
if (user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
context.querySelector('.videoQualitySection').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.videoQualitySection').classList.add('hide');
|
||||
context.querySelector('.musicQualitySection').classList.add('hide');
|
||||
}
|
||||
|
||||
if (appHost.supports('multiserver')) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiClient.getEndpointInfo().then(endpointInfo => {
|
||||
if (endpointInfo.IsInNetwork) {
|
||||
context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide');
|
||||
|
||||
context.querySelector('.fldVideoInternetQuality').classList.add('hide');
|
||||
context.querySelector('.musicQualitySection').classList.add('hide');
|
||||
} else {
|
||||
context.querySelector('.fldVideoInNetworkQuality').classList.add('hide');
|
||||
|
||||
context.querySelector('.fldVideoInternetQuality').classList.remove('hide');
|
||||
|
||||
if (user.Policy.EnableAudioPlaybackTranscoding) {
|
||||
@@ -115,250 +133,229 @@ import template from './playbackSettings.template.html';
|
||||
} else {
|
||||
context.querySelector('.musicQualitySection').classList.add('hide');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
apiClient.getEndpointInfo().then(endpointInfo => {
|
||||
if (endpointInfo.IsInNetwork) {
|
||||
context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide');
|
||||
|
||||
context.querySelector('.fldVideoInternetQuality').classList.add('hide');
|
||||
context.querySelector('.musicQualitySection').classList.add('hide');
|
||||
} else {
|
||||
context.querySelector('.fldVideoInNetworkQuality').classList.add('hide');
|
||||
|
||||
context.querySelector('.fldVideoInternetQuality').classList.remove('hide');
|
||||
|
||||
if (user.Policy.EnableAudioPlaybackTranscoding) {
|
||||
context.querySelector('.musicQualitySection').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.musicQualitySection').classList.add('hide');
|
||||
}
|
||||
}
|
||||
});
|
||||
function showOrHideEpisodesField(context) {
|
||||
if (browser.tizen || browser.web0s) {
|
||||
context.querySelector('.fldEpisodeAutoPlay').classList.add('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
function showOrHideEpisodesField(context) {
|
||||
if (browser.tizen || browser.web0s) {
|
||||
context.querySelector('.fldEpisodeAutoPlay').classList.add('hide');
|
||||
return;
|
||||
}
|
||||
context.querySelector('.fldEpisodeAutoPlay').classList.remove('hide');
|
||||
}
|
||||
|
||||
context.querySelector('.fldEpisodeAutoPlay').classList.remove('hide');
|
||||
function loadForm(context, user, userSettings, apiClient) {
|
||||
const loggedInUserId = apiClient.getCurrentUserId();
|
||||
const userId = user.Id;
|
||||
|
||||
showHideQualityFields(context, user, apiClient);
|
||||
|
||||
context.querySelector('#selectAllowedAudioChannels').value = userSettings.allowedAudioChannels();
|
||||
|
||||
apiClient.getCultures().then(allCultures => {
|
||||
populateLanguages(context.querySelector('#selectAudioLanguage'), allCultures);
|
||||
|
||||
context.querySelector('#selectAudioLanguage', context).value = user.Configuration.AudioLanguagePreference || '';
|
||||
context.querySelector('.chkEpisodeAutoPlay').checked = user.Configuration.EnableNextEpisodeAutoPlay || false;
|
||||
});
|
||||
|
||||
if (appHost.supports('externalplayerintent') && userId === loggedInUserId) {
|
||||
context.querySelector('.fldExternalPlayer').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldExternalPlayer').classList.add('hide');
|
||||
}
|
||||
|
||||
function loadForm(context, user, userSettings, apiClient) {
|
||||
const loggedInUserId = apiClient.getCurrentUserId();
|
||||
const userId = user.Id;
|
||||
if (userId === loggedInUserId && (user.Policy.EnableVideoPlaybackTranscoding || user.Policy.EnableAudioPlaybackTranscoding)) {
|
||||
context.querySelector('.qualitySections').classList.remove('hide');
|
||||
|
||||
showHideQualityFields(context, user, apiClient);
|
||||
|
||||
context.querySelector('#selectAllowedAudioChannels').value = userSettings.allowedAudioChannels();
|
||||
|
||||
apiClient.getCultures().then(allCultures => {
|
||||
populateLanguages(context.querySelector('#selectAudioLanguage'), allCultures);
|
||||
|
||||
context.querySelector('#selectAudioLanguage', context).value = user.Configuration.AudioLanguagePreference || '';
|
||||
context.querySelector('.chkEpisodeAutoPlay').checked = user.Configuration.EnableNextEpisodeAutoPlay || false;
|
||||
});
|
||||
|
||||
if (appHost.supports('externalplayerintent') && userId === loggedInUserId) {
|
||||
context.querySelector('.fldExternalPlayer').classList.remove('hide');
|
||||
if (appHost.supports('chromecast') && user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
context.querySelector('.fldChromecastQuality').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldExternalPlayer').classList.add('hide');
|
||||
}
|
||||
|
||||
if (userId === loggedInUserId && (user.Policy.EnableVideoPlaybackTranscoding || user.Policy.EnableAudioPlaybackTranscoding)) {
|
||||
context.querySelector('.qualitySections').classList.remove('hide');
|
||||
|
||||
if (appHost.supports('chromecast') && user.Policy.EnableVideoPlaybackTranscoding) {
|
||||
context.querySelector('.fldChromecastQuality').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.fldChromecastQuality').classList.add('hide');
|
||||
}
|
||||
} else {
|
||||
context.querySelector('.qualitySections').classList.add('hide');
|
||||
context.querySelector('.fldChromecastQuality').classList.add('hide');
|
||||
}
|
||||
|
||||
context.querySelector('.chkPlayDefaultAudioTrack').checked = user.Configuration.PlayDefaultAudioTrack || false;
|
||||
context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
|
||||
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();
|
||||
context.querySelector('.chkEnableAudioNormalization').checked = userSettings.enableAudioNormalization();
|
||||
context.querySelector('.chkEnableNextVideoOverlay').checked = userSettings.enableNextVideoInfoOverlay();
|
||||
context.querySelector('.chkRememberAudioSelections').checked = user.Configuration.RememberAudioSelections || false;
|
||||
context.querySelector('.chkRememberSubtitleSelections').checked = user.Configuration.RememberSubtitleSelections || false;
|
||||
context.querySelector('.chkExternalVideoPlayer').checked = appSettings.enableSystemExternalPlayers();
|
||||
|
||||
setMaxBitrateIntoField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
||||
setMaxBitrateIntoField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
||||
setMaxBitrateIntoField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
|
||||
|
||||
fillChromecastQuality(context.querySelector('.selectChromecastVideoQuality'));
|
||||
|
||||
const selectChromecastVersion = context.querySelector('.selectChromecastVersion');
|
||||
selectChromecastVersion.value = userSettings.chromecastVersion();
|
||||
|
||||
const selectLabelMaxVideoWidth = context.querySelector('.selectLabelMaxVideoWidth');
|
||||
selectLabelMaxVideoWidth.value = appSettings.maxVideoWidth();
|
||||
|
||||
const selectSkipForwardLength = context.querySelector('.selectSkipForwardLength');
|
||||
fillSkipLengths(selectSkipForwardLength);
|
||||
selectSkipForwardLength.value = userSettings.skipForwardLength();
|
||||
|
||||
const selectSkipBackLength = context.querySelector('.selectSkipBackLength');
|
||||
fillSkipLengths(selectSkipBackLength);
|
||||
selectSkipBackLength.value = userSettings.skipBackLength();
|
||||
|
||||
showOrHideEpisodesField(context);
|
||||
|
||||
loading.hide();
|
||||
} else {
|
||||
context.querySelector('.qualitySections').classList.add('hide');
|
||||
context.querySelector('.fldChromecastQuality').classList.add('hide');
|
||||
}
|
||||
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
appSettings.enableSystemExternalPlayers(context.querySelector('.chkExternalVideoPlayer').checked);
|
||||
context.querySelector('.chkPlayDefaultAudioTrack').checked = user.Configuration.PlayDefaultAudioTrack || false;
|
||||
context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
|
||||
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();
|
||||
context.querySelector('.chkEnableAudioNormalization').checked = userSettings.enableAudioNormalization();
|
||||
context.querySelector('.chkEnableNextVideoOverlay').checked = userSettings.enableNextVideoInfoOverlay();
|
||||
context.querySelector('.chkRememberAudioSelections').checked = user.Configuration.RememberAudioSelections || false;
|
||||
context.querySelector('.chkRememberSubtitleSelections').checked = user.Configuration.RememberSubtitleSelections || false;
|
||||
context.querySelector('.chkExternalVideoPlayer').checked = appSettings.enableSystemExternalPlayers();
|
||||
|
||||
appSettings.maxChromecastBitrate(context.querySelector('.selectChromecastVideoQuality').value);
|
||||
appSettings.maxVideoWidth(context.querySelector('.selectLabelMaxVideoWidth').value);
|
||||
setMaxBitrateIntoField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
||||
setMaxBitrateIntoField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
||||
setMaxBitrateIntoField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
|
||||
|
||||
setMaxBitrateFromField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
||||
setMaxBitrateFromField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
||||
setMaxBitrateFromField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
|
||||
fillChromecastQuality(context.querySelector('.selectChromecastVideoQuality'));
|
||||
|
||||
userSettingsInstance.allowedAudioChannels(context.querySelector('#selectAllowedAudioChannels').value);
|
||||
user.Configuration.AudioLanguagePreference = context.querySelector('#selectAudioLanguage').value;
|
||||
user.Configuration.PlayDefaultAudioTrack = context.querySelector('.chkPlayDefaultAudioTrack').checked;
|
||||
user.Configuration.EnableNextEpisodeAutoPlay = context.querySelector('.chkEpisodeAutoPlay').checked;
|
||||
userSettingsInstance.preferFmp4HlsContainer(context.querySelector('.chkPreferFmp4HlsContainer').checked);
|
||||
userSettingsInstance.enableCinemaMode(context.querySelector('.chkEnableCinemaMode').checked);
|
||||
userSettingsInstance.enableAudioNormalization(context.querySelector('.chkEnableAudioNormalization').checked);
|
||||
userSettingsInstance.enableNextVideoInfoOverlay(context.querySelector('.chkEnableNextVideoOverlay').checked);
|
||||
user.Configuration.RememberAudioSelections = context.querySelector('.chkRememberAudioSelections').checked;
|
||||
user.Configuration.RememberSubtitleSelections = context.querySelector('.chkRememberSubtitleSelections').checked;
|
||||
userSettingsInstance.chromecastVersion(context.querySelector('.selectChromecastVersion').value);
|
||||
userSettingsInstance.skipForwardLength(context.querySelector('.selectSkipForwardLength').value);
|
||||
userSettingsInstance.skipBackLength(context.querySelector('.selectSkipBackLength').value);
|
||||
const selectChromecastVersion = context.querySelector('.selectChromecastVersion');
|
||||
selectChromecastVersion.value = userSettings.chromecastVersion();
|
||||
|
||||
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
|
||||
const selectLabelMaxVideoWidth = context.querySelector('.selectLabelMaxVideoWidth');
|
||||
selectLabelMaxVideoWidth.value = appSettings.maxVideoWidth();
|
||||
|
||||
const selectSkipForwardLength = context.querySelector('.selectSkipForwardLength');
|
||||
fillSkipLengths(selectSkipForwardLength);
|
||||
selectSkipForwardLength.value = userSettings.skipForwardLength();
|
||||
|
||||
const selectSkipBackLength = context.querySelector('.selectSkipBackLength');
|
||||
fillSkipLengths(selectSkipBackLength);
|
||||
selectSkipBackLength.value = userSettings.skipBackLength();
|
||||
|
||||
showOrHideEpisodesField(context);
|
||||
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function saveUser(context, user, userSettingsInstance, apiClient) {
|
||||
appSettings.enableSystemExternalPlayers(context.querySelector('.chkExternalVideoPlayer').checked);
|
||||
|
||||
appSettings.maxChromecastBitrate(context.querySelector('.selectChromecastVideoQuality').value);
|
||||
appSettings.maxVideoWidth(context.querySelector('.selectLabelMaxVideoWidth').value);
|
||||
|
||||
setMaxBitrateFromField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
|
||||
setMaxBitrateFromField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
|
||||
setMaxBitrateFromField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
|
||||
|
||||
userSettingsInstance.allowedAudioChannels(context.querySelector('#selectAllowedAudioChannels').value);
|
||||
user.Configuration.AudioLanguagePreference = context.querySelector('#selectAudioLanguage').value;
|
||||
user.Configuration.PlayDefaultAudioTrack = context.querySelector('.chkPlayDefaultAudioTrack').checked;
|
||||
user.Configuration.EnableNextEpisodeAutoPlay = context.querySelector('.chkEpisodeAutoPlay').checked;
|
||||
userSettingsInstance.preferFmp4HlsContainer(context.querySelector('.chkPreferFmp4HlsContainer').checked);
|
||||
userSettingsInstance.enableCinemaMode(context.querySelector('.chkEnableCinemaMode').checked);
|
||||
userSettingsInstance.enableAudioNormalization(context.querySelector('.chkEnableAudioNormalization').checked);
|
||||
userSettingsInstance.enableNextVideoInfoOverlay(context.querySelector('.chkEnableNextVideoOverlay').checked);
|
||||
user.Configuration.RememberAudioSelections = context.querySelector('.chkRememberAudioSelections').checked;
|
||||
user.Configuration.RememberSubtitleSelections = context.querySelector('.chkRememberSubtitleSelections').checked;
|
||||
userSettingsInstance.chromecastVersion(context.querySelector('.selectChromecastVersion').value);
|
||||
userSettingsInstance.skipForwardLength(context.querySelector('.selectSkipForwardLength').value);
|
||||
userSettingsInstance.skipBackLength(context.querySelector('.selectSkipBackLength').value);
|
||||
|
||||
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
|
||||
}
|
||||
|
||||
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
|
||||
loading.show();
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
saveUser(context, user, userSettings, apiClient).then(() => {
|
||||
loading.hide();
|
||||
if (enableSaveConfirmation) {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
Events.trigger(instance, 'saved');
|
||||
}, () => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setSelectValue(select, value, defaultValue) {
|
||||
select.value = value;
|
||||
|
||||
if (select.selectedIndex < 0) {
|
||||
select.value = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function onMaxVideoWidthChange(e) {
|
||||
const context = this.options.element;
|
||||
|
||||
const selectVideoInNetworkQuality = context.querySelector('.selectVideoInNetworkQuality');
|
||||
const selectVideoInternetQuality = context.querySelector('.selectVideoInternetQuality');
|
||||
const selectChromecastVideoQuality = context.querySelector('.selectChromecastVideoQuality');
|
||||
|
||||
const selectVideoInNetworkQualityValue = selectVideoInNetworkQuality.value;
|
||||
const selectVideoInternetQualityValue = selectVideoInternetQuality.value;
|
||||
const selectChromecastVideoQualityValue = selectChromecastVideoQuality.value;
|
||||
|
||||
const maxVideoWidth = parseInt(e.target.value || '0', 10) || 0;
|
||||
|
||||
fillQuality(selectVideoInNetworkQuality, true, 'Video', maxVideoWidth);
|
||||
fillQuality(selectVideoInternetQuality, false, 'Video', maxVideoWidth);
|
||||
fillChromecastQuality(selectChromecastVideoQuality, maxVideoWidth);
|
||||
|
||||
setSelectValue(selectVideoInNetworkQuality, selectVideoInNetworkQualityValue, '');
|
||||
setSelectValue(selectVideoInternetQuality, selectVideoInternetQualityValue, '');
|
||||
setSelectValue(selectChromecastVideoQuality, selectChromecastVideoQualityValue, '');
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userId = self.options.userId;
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
const enableSaveConfirmation = self.options.enableSaveConfirmation;
|
||||
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
|
||||
});
|
||||
|
||||
// Disable default form submission
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function embed(options, self) {
|
||||
options.element.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
|
||||
|
||||
if (options.enableSaveButton) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
|
||||
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
|
||||
options.element.querySelector('.selectLabelMaxVideoWidth').addEventListener('change', onMaxVideoWidthChange.bind(self));
|
||||
|
||||
self.loadData();
|
||||
|
||||
if (options.autoFocus) {
|
||||
focusManager.autoFocus(options.element);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaybackSettings {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options, this);
|
||||
}
|
||||
|
||||
loadData() {
|
||||
const self = this;
|
||||
const context = self.options.element;
|
||||
|
||||
loading.show();
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
saveUser(context, user, userSettings, apiClient).then(() => {
|
||||
loading.hide();
|
||||
if (enableSaveConfirmation) {
|
||||
toast(globalize.translate('SettingsSaved'));
|
||||
}
|
||||
|
||||
Events.trigger(instance, 'saved');
|
||||
}, () => {
|
||||
loading.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setSelectValue(select, value, defaultValue) {
|
||||
select.value = value;
|
||||
|
||||
if (select.selectedIndex < 0) {
|
||||
select.value = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function onMaxVideoWidthChange(e) {
|
||||
const context = this.options.element;
|
||||
|
||||
const selectVideoInNetworkQuality = context.querySelector('.selectVideoInNetworkQuality');
|
||||
const selectVideoInternetQuality = context.querySelector('.selectVideoInternetQuality');
|
||||
const selectChromecastVideoQuality = context.querySelector('.selectChromecastVideoQuality');
|
||||
|
||||
const selectVideoInNetworkQualityValue = selectVideoInNetworkQuality.value;
|
||||
const selectVideoInternetQualityValue = selectVideoInternetQuality.value;
|
||||
const selectChromecastVideoQualityValue = selectChromecastVideoQuality.value;
|
||||
|
||||
const maxVideoWidth = parseInt(e.target.value || '0', 10) || 0;
|
||||
|
||||
fillQuality(selectVideoInNetworkQuality, true, 'Video', maxVideoWidth);
|
||||
fillQuality(selectVideoInternetQuality, false, 'Video', maxVideoWidth);
|
||||
fillChromecastQuality(selectChromecastVideoQuality, maxVideoWidth);
|
||||
|
||||
setSelectValue(selectVideoInNetworkQuality, selectVideoInNetworkQualityValue, '');
|
||||
setSelectValue(selectVideoInternetQuality, selectVideoInternetQualityValue, '');
|
||||
setSelectValue(selectChromecastVideoQuality, selectChromecastVideoQualityValue, '');
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const self = this;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userId = self.options.userId;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
const enableSaveConfirmation = self.options.enableSaveConfirmation;
|
||||
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
|
||||
});
|
||||
apiClient.getUser(userId).then(user => {
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
self.dataLoaded = true;
|
||||
|
||||
// Disable default form submission
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function embed(options, self) {
|
||||
options.element.innerHTML = globalize.translateHtml(template, 'core');
|
||||
|
||||
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
|
||||
|
||||
if (options.enableSaveButton) {
|
||||
options.element.querySelector('.btnSave').classList.remove('hide');
|
||||
}
|
||||
|
||||
options.element.querySelector('.selectLabelMaxVideoWidth').addEventListener('change', onMaxVideoWidthChange.bind(self));
|
||||
|
||||
self.loadData();
|
||||
|
||||
if (options.autoFocus) {
|
||||
focusManager.autoFocus(options.element);
|
||||
}
|
||||
}
|
||||
|
||||
class PlaybackSettings {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
embed(options, this);
|
||||
}
|
||||
|
||||
loadData() {
|
||||
const self = this;
|
||||
const context = self.options.element;
|
||||
|
||||
loading.show();
|
||||
|
||||
const userId = self.options.userId;
|
||||
const apiClient = ServerConnections.getApiClient(self.options.serverId);
|
||||
const userSettings = self.options.userSettings;
|
||||
|
||||
apiClient.getUser(userId).then(user => {
|
||||
userSettings.setUserInfo(userId, apiClient).then(() => {
|
||||
self.dataLoaded = true;
|
||||
|
||||
loadForm(context, user, userSettings, apiClient);
|
||||
});
|
||||
loadForm(context, user, userSettings, apiClient);
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
onSubmit.call(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.options = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
submit() {
|
||||
onSubmit.call(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.options = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default PlaybackSettings;
|
||||
|
||||
@@ -4,471 +4,476 @@ import Events from '../../utils/events.ts';
|
||||
import layoutManager from '../layoutManager';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import playMethodHelper from '../playback/playmethodhelper';
|
||||
import SyncPlay from '../../plugins/syncPlay/core';
|
||||
import { pluginManager } from '../pluginManager';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
import './playerstats.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function init(instance) {
|
||||
const parent = document.createElement('div');
|
||||
|
||||
function init(instance) {
|
||||
const parent = document.createElement('div');
|
||||
parent.classList.add('playerStats');
|
||||
|
||||
parent.classList.add('playerStats');
|
||||
|
||||
if (layoutManager.tv) {
|
||||
parent.classList.add('playerStats-tv');
|
||||
}
|
||||
|
||||
parent.classList.add('hide');
|
||||
|
||||
let button;
|
||||
|
||||
if (layoutManager.tv) {
|
||||
button = '';
|
||||
} else {
|
||||
button = '<button type="button" is="paper-icon-button-light" class="playerStats-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
const contentClass = layoutManager.tv ? 'playerStats-content playerStats-content-tv' : 'playerStats-content';
|
||||
|
||||
parent.innerHTML = '<div class="' + contentClass + '">' + button + '<div class="playerStats-stats"></div></div>';
|
||||
|
||||
button = parent.querySelector('.playerStats-closeButton');
|
||||
|
||||
if (button) {
|
||||
button.addEventListener('click', onCloseButtonClick.bind(instance));
|
||||
}
|
||||
|
||||
document.body.appendChild(parent);
|
||||
|
||||
instance.element = parent;
|
||||
if (layoutManager.tv) {
|
||||
parent.classList.add('playerStats-tv');
|
||||
}
|
||||
|
||||
function onCloseButtonClick() {
|
||||
this.enabled(false);
|
||||
parent.classList.add('hide');
|
||||
|
||||
let button;
|
||||
|
||||
if (layoutManager.tv) {
|
||||
button = '';
|
||||
} else {
|
||||
button = '<button type="button" is="paper-icon-button-light" class="playerStats-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
function renderStats(elem, categories) {
|
||||
elem.querySelector('.playerStats-stats').innerHTML = categories.map(function (category) {
|
||||
let categoryHtml = '';
|
||||
const contentClass = layoutManager.tv ? 'playerStats-content playerStats-content-tv' : 'playerStats-content';
|
||||
|
||||
const stats = category.stats;
|
||||
parent.innerHTML = '<div class="' + contentClass + '">' + button + '<div class="playerStats-stats"></div></div>';
|
||||
|
||||
if (stats.length && category.name) {
|
||||
categoryHtml += '<div class="playerStats-stat playerStats-stat-header">';
|
||||
button = parent.querySelector('.playerStats-closeButton');
|
||||
|
||||
categoryHtml += '<div class="playerStats-stat-label">';
|
||||
categoryHtml += category.name;
|
||||
categoryHtml += '</div>';
|
||||
|
||||
categoryHtml += '<div class="playerStats-stat-value">';
|
||||
categoryHtml += category.subText || '';
|
||||
categoryHtml += '</div>';
|
||||
|
||||
categoryHtml += '</div>';
|
||||
}
|
||||
|
||||
for (let i = 0, length = stats.length; i < length; i++) {
|
||||
categoryHtml += '<div class="playerStats-stat">';
|
||||
|
||||
const stat = stats[i];
|
||||
|
||||
categoryHtml += '<div class="playerStats-stat-label">';
|
||||
categoryHtml += stat.label;
|
||||
categoryHtml += '</div>';
|
||||
|
||||
categoryHtml += '<div class="playerStats-stat-value">';
|
||||
categoryHtml += stat.value;
|
||||
categoryHtml += '</div>';
|
||||
|
||||
categoryHtml += '</div>';
|
||||
}
|
||||
|
||||
return categoryHtml;
|
||||
}).join('');
|
||||
if (button) {
|
||||
button.addEventListener('click', onCloseButtonClick.bind(instance));
|
||||
}
|
||||
|
||||
function getSession(instance, player) {
|
||||
const now = new Date().getTime();
|
||||
document.body.appendChild(parent);
|
||||
|
||||
if ((now - (instance.lastSessionTime || 0)) < 10000) {
|
||||
return Promise.resolve(instance.lastSession);
|
||||
instance.element = parent;
|
||||
}
|
||||
|
||||
function onCloseButtonClick() {
|
||||
this.enabled(false);
|
||||
}
|
||||
|
||||
function renderStats(elem, categories) {
|
||||
elem.querySelector('.playerStats-stats').innerHTML = categories.map(function (category) {
|
||||
let categoryHtml = '';
|
||||
|
||||
const stats = category.stats;
|
||||
|
||||
if (stats.length && category.name) {
|
||||
categoryHtml += '<div class="playerStats-stat playerStats-stat-header">';
|
||||
|
||||
categoryHtml += '<div class="playerStats-stat-label">';
|
||||
categoryHtml += category.name;
|
||||
categoryHtml += '</div>';
|
||||
|
||||
categoryHtml += '<div class="playerStats-stat-value">';
|
||||
categoryHtml += category.subText || '';
|
||||
categoryHtml += '</div>';
|
||||
|
||||
categoryHtml += '</div>';
|
||||
}
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(playbackManager.currentItem(player).ServerId);
|
||||
for (let i = 0, length = stats.length; i < length; i++) {
|
||||
categoryHtml += '<div class="playerStats-stat">';
|
||||
|
||||
return apiClient.getSessions({
|
||||
deviceId: apiClient.deviceId()
|
||||
}).then(function (sessions) {
|
||||
instance.lastSession = sessions[0] || {};
|
||||
instance.lastSessionTime = new Date().getTime();
|
||||
const stat = stats[i];
|
||||
|
||||
return Promise.resolve(instance.lastSession);
|
||||
}, function () {
|
||||
return Promise.resolve({});
|
||||
categoryHtml += '<div class="playerStats-stat-label">';
|
||||
categoryHtml += stat.label;
|
||||
categoryHtml += '</div>';
|
||||
|
||||
categoryHtml += '<div class="playerStats-stat-value">';
|
||||
categoryHtml += stat.value;
|
||||
categoryHtml += '</div>';
|
||||
|
||||
categoryHtml += '</div>';
|
||||
}
|
||||
|
||||
return categoryHtml;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getSession(instance, player) {
|
||||
const now = new Date().getTime();
|
||||
|
||||
if ((now - (instance.lastSessionTime || 0)) < 10000) {
|
||||
return Promise.resolve(instance.lastSession);
|
||||
}
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(playbackManager.currentItem(player).ServerId);
|
||||
|
||||
return apiClient.getSessions({
|
||||
deviceId: apiClient.deviceId()
|
||||
}).then(function (sessions) {
|
||||
instance.lastSession = sessions[0] || {};
|
||||
instance.lastSessionTime = new Date().getTime();
|
||||
|
||||
return Promise.resolve(instance.lastSession);
|
||||
}, function () {
|
||||
return Promise.resolve({});
|
||||
});
|
||||
}
|
||||
|
||||
function translateReason(reason) {
|
||||
return globalize.translate('' + reason);
|
||||
}
|
||||
|
||||
function getTranscodingStats(session, player, displayPlayMethod) {
|
||||
const sessionStats = [];
|
||||
|
||||
let videoCodec;
|
||||
let audioCodec;
|
||||
let totalBitrate;
|
||||
let audioChannels;
|
||||
|
||||
if (session.TranscodingInfo) {
|
||||
videoCodec = session.TranscodingInfo.VideoCodec;
|
||||
audioCodec = session.TranscodingInfo.AudioCodec;
|
||||
totalBitrate = session.TranscodingInfo.Bitrate;
|
||||
audioChannels = session.TranscodingInfo.AudioChannels;
|
||||
}
|
||||
|
||||
if (videoCodec) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoCodec'),
|
||||
value: session.TranscodingInfo.IsVideoDirect ? (videoCodec.toUpperCase() + ' (direct)') : videoCodec.toUpperCase()
|
||||
});
|
||||
}
|
||||
|
||||
function translateReason(reason) {
|
||||
return globalize.translate('' + reason);
|
||||
if (audioCodec) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioCodec'),
|
||||
value: session.TranscodingInfo.IsAudioDirect ? (audioCodec.toUpperCase() + ' (direct)') : audioCodec.toUpperCase()
|
||||
});
|
||||
}
|
||||
|
||||
function getTranscodingStats(session, player, displayPlayMethod) {
|
||||
const sessionStats = [];
|
||||
|
||||
let videoCodec;
|
||||
let audioCodec;
|
||||
let totalBitrate;
|
||||
let audioChannels;
|
||||
|
||||
if (session.TranscodingInfo) {
|
||||
videoCodec = session.TranscodingInfo.VideoCodec;
|
||||
audioCodec = session.TranscodingInfo.AudioCodec;
|
||||
totalBitrate = session.TranscodingInfo.Bitrate;
|
||||
audioChannels = session.TranscodingInfo.AudioChannels;
|
||||
}
|
||||
|
||||
if (videoCodec) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoCodec'),
|
||||
value: session.TranscodingInfo.IsVideoDirect ? (videoCodec.toUpperCase() + ' (direct)') : videoCodec.toUpperCase()
|
||||
});
|
||||
}
|
||||
|
||||
if (audioCodec) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioCodec'),
|
||||
value: session.TranscodingInfo.IsAudioDirect ? (audioCodec.toUpperCase() + ' (direct)') : audioCodec.toUpperCase()
|
||||
});
|
||||
}
|
||||
|
||||
if (displayPlayMethod === 'Transcode') {
|
||||
if (audioChannels) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioChannels'),
|
||||
value: audioChannels
|
||||
});
|
||||
}
|
||||
if (totalBitrate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelBitrate'),
|
||||
value: getDisplayBitrate(totalBitrate)
|
||||
});
|
||||
}
|
||||
if (session.TranscodingInfo.CompletionPercentage) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelTranscodingProgress'),
|
||||
value: session.TranscodingInfo.CompletionPercentage.toFixed(1) + '%'
|
||||
});
|
||||
}
|
||||
if (session.TranscodingInfo.Framerate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelTranscodingFramerate'),
|
||||
value: session.TranscodingInfo.Framerate + ' fps'
|
||||
});
|
||||
}
|
||||
if (session.TranscodingInfo.TranscodeReasons && session.TranscodingInfo.TranscodeReasons.length) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelReasonForTranscoding'),
|
||||
value: session.TranscodingInfo.TranscodeReasons.map(translateReason).join('<br/>')
|
||||
});
|
||||
}
|
||||
if (session.TranscodingInfo.HardwareAccelerationType) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelHardwareEncoding'),
|
||||
value: session.TranscodingInfo.HardwareAccelerationType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sessionStats;
|
||||
}
|
||||
|
||||
function getDisplayBitrate(bitrate) {
|
||||
if (bitrate > 1000000) {
|
||||
return (bitrate / 1000000).toFixed(1) + ' Mbps';
|
||||
} else {
|
||||
return Math.floor(bitrate / 1000) + ' kbps';
|
||||
}
|
||||
}
|
||||
|
||||
function getReadableSize(size) {
|
||||
if (size >= 1073741824) {
|
||||
return parseFloat((size / 1073741824).toFixed(1)) + ' GiB';
|
||||
} else if (size >= 1048576) {
|
||||
return parseFloat((size / 1048576).toFixed(1)) + ' MiB';
|
||||
} else {
|
||||
return Math.floor(size / 1024) + ' KiB';
|
||||
}
|
||||
}
|
||||
|
||||
function getMediaSourceStats(session, player) {
|
||||
const sessionStats = [];
|
||||
|
||||
const mediaSource = playbackManager.currentMediaSource(player) || {};
|
||||
const totalBitrate = mediaSource.Bitrate;
|
||||
const mediaFileSize = mediaSource.Size;
|
||||
|
||||
if (mediaSource.Container) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelProfileContainer'),
|
||||
value: mediaSource.Container
|
||||
});
|
||||
}
|
||||
|
||||
if (mediaFileSize) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelSize'),
|
||||
value: getReadableSize(mediaFileSize)
|
||||
});
|
||||
}
|
||||
|
||||
if (totalBitrate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelBitrate'),
|
||||
value: getDisplayBitrate(totalBitrate)
|
||||
});
|
||||
}
|
||||
|
||||
const mediaStreams = mediaSource.MediaStreams || [];
|
||||
const videoStream = mediaStreams.filter(function (s) {
|
||||
return s.Type === 'Video';
|
||||
})[0] || {};
|
||||
|
||||
const videoCodec = videoStream.Codec;
|
||||
|
||||
const audioStreamIndex = playbackManager.getAudioStreamIndex(player);
|
||||
const audioStream = playbackManager.audioTracks(player).filter(function (s) {
|
||||
return s.Type === 'Audio' && s.Index === audioStreamIndex;
|
||||
})[0] || {};
|
||||
|
||||
const audioCodec = audioStream.Codec;
|
||||
const audioChannels = audioStream.Channels;
|
||||
|
||||
const videoInfos = [];
|
||||
|
||||
if (videoCodec) {
|
||||
videoInfos.push(videoCodec.toUpperCase());
|
||||
}
|
||||
|
||||
if (videoStream.Profile) {
|
||||
videoInfos.push(videoStream.Profile);
|
||||
}
|
||||
|
||||
if (videoInfos.length) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoCodec'),
|
||||
value: videoInfos.join(' ')
|
||||
});
|
||||
}
|
||||
|
||||
if (videoStream.BitRate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoBitrate'),
|
||||
value: getDisplayBitrate(videoStream.BitRate)
|
||||
});
|
||||
}
|
||||
|
||||
if (videoStream.VideoRangeType) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoRangeType'),
|
||||
value: videoStream.VideoRangeType
|
||||
});
|
||||
}
|
||||
|
||||
const audioInfos = [];
|
||||
|
||||
if (audioCodec) {
|
||||
audioInfos.push(audioCodec.toUpperCase());
|
||||
}
|
||||
|
||||
if (audioStream.Profile) {
|
||||
audioInfos.push(audioStream.Profile);
|
||||
}
|
||||
|
||||
if (audioInfos.length) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioCodec'),
|
||||
value: audioInfos.join(' ')
|
||||
});
|
||||
}
|
||||
|
||||
if (audioStream.BitRate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioBitrate'),
|
||||
value: getDisplayBitrate(audioStream.BitRate)
|
||||
});
|
||||
}
|
||||
|
||||
if (displayPlayMethod === 'Transcode') {
|
||||
if (audioChannels) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioChannels'),
|
||||
value: audioChannels
|
||||
});
|
||||
}
|
||||
|
||||
if (audioStream.SampleRate) {
|
||||
if (totalBitrate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioSampleRate'),
|
||||
value: audioStream.SampleRate + ' Hz'
|
||||
label: globalize.translate('LabelBitrate'),
|
||||
value: getDisplayBitrate(totalBitrate)
|
||||
});
|
||||
}
|
||||
|
||||
if (audioStream.BitDepth) {
|
||||
if (session.TranscodingInfo.CompletionPercentage) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioBitDepth'),
|
||||
value: audioStream.BitDepth
|
||||
label: globalize.translate('LabelTranscodingProgress'),
|
||||
value: session.TranscodingInfo.CompletionPercentage.toFixed(1) + '%'
|
||||
});
|
||||
}
|
||||
if (session.TranscodingInfo.Framerate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelTranscodingFramerate'),
|
||||
value: session.TranscodingInfo.Framerate + ' fps'
|
||||
});
|
||||
}
|
||||
if (session.TranscodingInfo.TranscodeReasons && session.TranscodingInfo.TranscodeReasons.length) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelReasonForTranscoding'),
|
||||
value: session.TranscodingInfo.TranscodeReasons.map(translateReason).join('<br/>')
|
||||
});
|
||||
}
|
||||
if (session.TranscodingInfo.HardwareAccelerationType) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelHardwareEncoding'),
|
||||
value: session.TranscodingInfo.HardwareAccelerationType
|
||||
});
|
||||
}
|
||||
|
||||
return sessionStats;
|
||||
}
|
||||
|
||||
function getSyncPlayStats() {
|
||||
const syncStats = [];
|
||||
const stats = SyncPlay.Manager.getStats();
|
||||
return sessionStats;
|
||||
}
|
||||
|
||||
syncStats.push({
|
||||
label: globalize.translate('LabelSyncPlayTimeSyncDevice'),
|
||||
value: stats.TimeSyncDevice
|
||||
function getDisplayBitrate(bitrate) {
|
||||
if (bitrate > 1000000) {
|
||||
return (bitrate / 1000000).toFixed(1) + ' Mbps';
|
||||
} else {
|
||||
return Math.floor(bitrate / 1000) + ' kbps';
|
||||
}
|
||||
}
|
||||
|
||||
function getReadableSize(size) {
|
||||
if (size >= 1073741824) {
|
||||
return parseFloat((size / 1073741824).toFixed(1)) + ' GiB';
|
||||
} else if (size >= 1048576) {
|
||||
return parseFloat((size / 1048576).toFixed(1)) + ' MiB';
|
||||
} else {
|
||||
return Math.floor(size / 1024) + ' KiB';
|
||||
}
|
||||
}
|
||||
|
||||
function getMediaSourceStats(session, player) {
|
||||
const sessionStats = [];
|
||||
|
||||
const mediaSource = playbackManager.currentMediaSource(player) || {};
|
||||
const totalBitrate = mediaSource.Bitrate;
|
||||
const mediaFileSize = mediaSource.Size;
|
||||
|
||||
if (mediaSource.Container) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelProfileContainer'),
|
||||
value: mediaSource.Container
|
||||
});
|
||||
|
||||
syncStats.push({
|
||||
// TODO: clean old string 'LabelSyncPlayTimeOffset' from translations.
|
||||
label: globalize.translate('LabelSyncPlayTimeSyncOffset'),
|
||||
value: stats.TimeSyncOffset + ' ' + globalize.translate('MillisecondsUnit')
|
||||
});
|
||||
|
||||
syncStats.push({
|
||||
label: globalize.translate('LabelSyncPlayPlaybackDiff'),
|
||||
value: stats.PlaybackDiff + ' ' + globalize.translate('MillisecondsUnit')
|
||||
});
|
||||
|
||||
syncStats.push({
|
||||
label: globalize.translate('LabelSyncPlaySyncMethod'),
|
||||
value: stats.SyncMethod
|
||||
});
|
||||
|
||||
return syncStats;
|
||||
}
|
||||
|
||||
function getStats(instance, player) {
|
||||
const statsPromise = player.getStats ? player.getStats() : Promise.resolve({});
|
||||
const sessionPromise = getSession(instance, player);
|
||||
if (mediaFileSize) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelSize'),
|
||||
value: getReadableSize(mediaFileSize)
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.all([statsPromise, sessionPromise]).then(function (responses) {
|
||||
const playerStatsResult = responses[0];
|
||||
const playerStats = playerStatsResult.categories || [];
|
||||
const session = responses[1];
|
||||
if (totalBitrate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelBitrate'),
|
||||
value: getDisplayBitrate(totalBitrate)
|
||||
});
|
||||
}
|
||||
|
||||
const displayPlayMethod = playMethodHelper.getDisplayPlayMethod(session);
|
||||
let localizedDisplayMethod = displayPlayMethod;
|
||||
const mediaStreams = mediaSource.MediaStreams || [];
|
||||
const videoStream = mediaStreams.filter(function (s) {
|
||||
return s.Type === 'Video';
|
||||
})[0] || {};
|
||||
|
||||
if (displayPlayMethod === 'DirectPlay') {
|
||||
localizedDisplayMethod = globalize.translate('DirectPlaying');
|
||||
} else if (displayPlayMethod === 'Remux') {
|
||||
localizedDisplayMethod = globalize.translate('Remuxing');
|
||||
} else if (displayPlayMethod === 'DirectStream') {
|
||||
localizedDisplayMethod = globalize.translate('DirectStreaming');
|
||||
} else if (displayPlayMethod === 'Transcode') {
|
||||
localizedDisplayMethod = globalize.translate('Transcoding');
|
||||
const videoCodec = videoStream.Codec;
|
||||
|
||||
const audioStreamIndex = playbackManager.getAudioStreamIndex(player);
|
||||
const audioStream = playbackManager.audioTracks(player).filter(function (s) {
|
||||
return s.Type === 'Audio' && s.Index === audioStreamIndex;
|
||||
})[0] || {};
|
||||
|
||||
const audioCodec = audioStream.Codec;
|
||||
const audioChannels = audioStream.Channels;
|
||||
|
||||
const videoInfos = [];
|
||||
|
||||
if (videoCodec) {
|
||||
videoInfos.push(videoCodec.toUpperCase());
|
||||
}
|
||||
|
||||
if (videoStream.Profile) {
|
||||
videoInfos.push(videoStream.Profile);
|
||||
}
|
||||
|
||||
if (videoInfos.length) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoCodec'),
|
||||
value: videoInfos.join(' ')
|
||||
});
|
||||
}
|
||||
|
||||
if (videoStream.BitRate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoBitrate'),
|
||||
value: getDisplayBitrate(videoStream.BitRate)
|
||||
});
|
||||
}
|
||||
|
||||
if (videoStream.VideoRangeType) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelVideoRangeType'),
|
||||
value: videoStream.VideoRangeType
|
||||
});
|
||||
}
|
||||
|
||||
const audioInfos = [];
|
||||
|
||||
if (audioCodec) {
|
||||
audioInfos.push(audioCodec.toUpperCase());
|
||||
}
|
||||
|
||||
if (audioStream.Profile) {
|
||||
audioInfos.push(audioStream.Profile);
|
||||
}
|
||||
|
||||
if (audioInfos.length) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioCodec'),
|
||||
value: audioInfos.join(' ')
|
||||
});
|
||||
}
|
||||
|
||||
if (audioStream.BitRate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioBitrate'),
|
||||
value: getDisplayBitrate(audioStream.BitRate)
|
||||
});
|
||||
}
|
||||
|
||||
if (audioChannels) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioChannels'),
|
||||
value: audioChannels
|
||||
});
|
||||
}
|
||||
|
||||
if (audioStream.SampleRate) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioSampleRate'),
|
||||
value: audioStream.SampleRate + ' Hz'
|
||||
});
|
||||
}
|
||||
|
||||
if (audioStream.BitDepth) {
|
||||
sessionStats.push({
|
||||
label: globalize.translate('LabelAudioBitDepth'),
|
||||
value: audioStream.BitDepth
|
||||
});
|
||||
}
|
||||
|
||||
return sessionStats;
|
||||
}
|
||||
|
||||
function getSyncPlayStats() {
|
||||
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
|
||||
|
||||
if (!SyncPlay?.Manager.isSyncPlayEnabled()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const syncStats = [];
|
||||
const stats = SyncPlay.Manager.getStats();
|
||||
|
||||
syncStats.push({
|
||||
label: globalize.translate('LabelSyncPlayTimeSyncDevice'),
|
||||
value: stats.TimeSyncDevice
|
||||
});
|
||||
|
||||
syncStats.push({
|
||||
// TODO: clean old string 'LabelSyncPlayTimeOffset' from translations.
|
||||
label: globalize.translate('LabelSyncPlayTimeSyncOffset'),
|
||||
value: stats.TimeSyncOffset + ' ' + globalize.translate('MillisecondsUnit')
|
||||
});
|
||||
|
||||
syncStats.push({
|
||||
label: globalize.translate('LabelSyncPlayPlaybackDiff'),
|
||||
value: stats.PlaybackDiff + ' ' + globalize.translate('MillisecondsUnit')
|
||||
});
|
||||
|
||||
syncStats.push({
|
||||
label: globalize.translate('LabelSyncPlaySyncMethod'),
|
||||
value: stats.SyncMethod
|
||||
});
|
||||
|
||||
return syncStats;
|
||||
}
|
||||
|
||||
function getStats(instance, player) {
|
||||
const statsPromise = player.getStats ? player.getStats() : Promise.resolve({});
|
||||
const sessionPromise = getSession(instance, player);
|
||||
|
||||
return Promise.all([statsPromise, sessionPromise]).then(function (responses) {
|
||||
const playerStatsResult = responses[0];
|
||||
const playerStats = playerStatsResult.categories || [];
|
||||
const session = responses[1];
|
||||
|
||||
const displayPlayMethod = playMethodHelper.getDisplayPlayMethod(session);
|
||||
let localizedDisplayMethod = displayPlayMethod;
|
||||
|
||||
if (displayPlayMethod === 'DirectPlay') {
|
||||
localizedDisplayMethod = globalize.translate('DirectPlaying');
|
||||
} else if (displayPlayMethod === 'Remux') {
|
||||
localizedDisplayMethod = globalize.translate('Remuxing');
|
||||
} else if (displayPlayMethod === 'DirectStream') {
|
||||
localizedDisplayMethod = globalize.translate('DirectStreaming');
|
||||
} else if (displayPlayMethod === 'Transcode') {
|
||||
localizedDisplayMethod = globalize.translate('Transcoding');
|
||||
}
|
||||
|
||||
const baseCategory = {
|
||||
stats: [],
|
||||
name: globalize.translate('LabelPlaybackInfo')
|
||||
};
|
||||
|
||||
baseCategory.stats.unshift({
|
||||
label: globalize.translate('LabelPlayMethod'),
|
||||
value: localizedDisplayMethod
|
||||
});
|
||||
|
||||
baseCategory.stats.unshift({
|
||||
label: globalize.translate('LabelPlayer'),
|
||||
value: player.name
|
||||
});
|
||||
|
||||
const categories = [];
|
||||
|
||||
categories.push(baseCategory);
|
||||
|
||||
for (let i = 0, length = playerStats.length; i < length; i++) {
|
||||
const category = playerStats[i];
|
||||
if (category.type === 'audio') {
|
||||
category.name = globalize.translate('LabelAudioInfo');
|
||||
} else if (category.type === 'video') {
|
||||
category.name = globalize.translate('LabelVideoInfo');
|
||||
}
|
||||
categories.push(category);
|
||||
}
|
||||
|
||||
const baseCategory = {
|
||||
stats: [],
|
||||
name: globalize.translate('LabelPlaybackInfo')
|
||||
};
|
||||
|
||||
baseCategory.stats.unshift({
|
||||
label: globalize.translate('LabelPlayMethod'),
|
||||
value: localizedDisplayMethod
|
||||
});
|
||||
|
||||
baseCategory.stats.unshift({
|
||||
label: globalize.translate('LabelPlayer'),
|
||||
value: player.name
|
||||
});
|
||||
|
||||
const categories = [];
|
||||
|
||||
categories.push(baseCategory);
|
||||
|
||||
for (let i = 0, length = playerStats.length; i < length; i++) {
|
||||
const category = playerStats[i];
|
||||
if (category.type === 'audio') {
|
||||
category.name = globalize.translate('LabelAudioInfo');
|
||||
} else if (category.type === 'video') {
|
||||
category.name = globalize.translate('LabelVideoInfo');
|
||||
}
|
||||
categories.push(category);
|
||||
}
|
||||
|
||||
let localizedTranscodingInfo = globalize.translate('LabelTranscodingInfo');
|
||||
if (displayPlayMethod === 'Remux') {
|
||||
localizedTranscodingInfo = globalize.translate('LabelRemuxingInfo');
|
||||
} else if (displayPlayMethod === 'DirectStream') {
|
||||
localizedTranscodingInfo = globalize.translate('LabelDirectStreamingInfo');
|
||||
}
|
||||
|
||||
if (session.TranscodingInfo) {
|
||||
categories.push({
|
||||
stats: getTranscodingStats(session, player, displayPlayMethod),
|
||||
name: localizedTranscodingInfo
|
||||
});
|
||||
}
|
||||
let localizedTranscodingInfo = globalize.translate('LabelTranscodingInfo');
|
||||
if (displayPlayMethod === 'Remux') {
|
||||
localizedTranscodingInfo = globalize.translate('LabelRemuxingInfo');
|
||||
} else if (displayPlayMethod === 'DirectStream') {
|
||||
localizedTranscodingInfo = globalize.translate('LabelDirectStreamingInfo');
|
||||
}
|
||||
|
||||
if (session.TranscodingInfo) {
|
||||
categories.push({
|
||||
stats: getMediaSourceStats(session, player),
|
||||
name: globalize.translate('LabelOriginalMediaInfo')
|
||||
stats: getTranscodingStats(session, player, displayPlayMethod),
|
||||
name: localizedTranscodingInfo
|
||||
});
|
||||
}
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(playbackManager.currentItem(player).ServerId);
|
||||
if (SyncPlay.Manager.isSyncPlayEnabled() && apiClient.isMinServerVersion('10.6.0')) {
|
||||
categories.push({
|
||||
stats: getSyncPlayStats(),
|
||||
name: globalize.translate('LabelSyncPlayInfo')
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(categories);
|
||||
categories.push({
|
||||
stats: getMediaSourceStats(session, player),
|
||||
name: globalize.translate('LabelOriginalMediaInfo')
|
||||
});
|
||||
|
||||
const syncPlayStats = getSyncPlayStats();
|
||||
if (syncPlayStats.length > 0) {
|
||||
categories.push({
|
||||
stats: syncPlayStats,
|
||||
name: globalize.translate('LabelSyncPlayInfo')
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(categories);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPlayerStats(instance, player) {
|
||||
const now = new Date().getTime();
|
||||
|
||||
if ((now - (instance.lastRender || 0)) < 700) {
|
||||
return;
|
||||
}
|
||||
|
||||
function renderPlayerStats(instance, player) {
|
||||
const now = new Date().getTime();
|
||||
instance.lastRender = now;
|
||||
|
||||
if ((now - (instance.lastRender || 0)) < 700) {
|
||||
getStats(instance, player).then(function (stats) {
|
||||
const elem = instance.element;
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.lastRender = now;
|
||||
renderStats(elem, stats);
|
||||
});
|
||||
}
|
||||
|
||||
getStats(instance, player).then(function (stats) {
|
||||
const elem = instance.element;
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
function bindEvents(instance, player) {
|
||||
const localOnTimeUpdate = function () {
|
||||
renderPlayerStats(instance, player);
|
||||
};
|
||||
|
||||
renderStats(elem, stats);
|
||||
});
|
||||
}
|
||||
|
||||
function bindEvents(instance, player) {
|
||||
const localOnTimeUpdate = function () {
|
||||
renderPlayerStats(instance, player);
|
||||
};
|
||||
|
||||
instance.onTimeUpdate = localOnTimeUpdate;
|
||||
Events.on(player, 'timeupdate', localOnTimeUpdate);
|
||||
}
|
||||
|
||||
function unbindEvents(instance, player) {
|
||||
const localOnTimeUpdate = instance.onTimeUpdate;
|
||||
|
||||
if (localOnTimeUpdate) {
|
||||
Events.off(player, 'timeupdate', localOnTimeUpdate);
|
||||
}
|
||||
instance.onTimeUpdate = localOnTimeUpdate;
|
||||
Events.on(player, 'timeupdate', localOnTimeUpdate);
|
||||
}
|
||||
|
||||
function unbindEvents(instance, player) {
|
||||
const localOnTimeUpdate = instance.onTimeUpdate;
|
||||
|
||||
if (localOnTimeUpdate) {
|
||||
Events.off(player, 'timeupdate', localOnTimeUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerStats {
|
||||
constructor(options) {
|
||||
@@ -520,6 +525,4 @@ class PlayerStats {
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
export default PlayerStats;
|
||||
|
||||
@@ -4,10 +4,12 @@ import dialogHelper from '../dialogHelper/dialogHelper';
|
||||
import loading from '../loading/loading';
|
||||
import layoutManager from '../layoutManager';
|
||||
import { playbackManager } from '../playback/playbackmanager';
|
||||
import SyncPlay from '../../plugins/syncPlay/core';
|
||||
import { pluginManager } from '../pluginManager';
|
||||
import * as userSettings from '../../scripts/settings/userSettings';
|
||||
import { appRouter } from '../appRouter';
|
||||
import globalize from '../../scripts/globalize';
|
||||
import { PluginType } from '../../types/plugin.ts';
|
||||
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../elements/emby-input/emby-input';
|
||||
import '../../elements/emby-button/paper-icon-button-light';
|
||||
@@ -16,267 +18,266 @@ import 'material-design-icons-iconfont';
|
||||
import '../formdialog.scss';
|
||||
import ServerConnections from '../ServerConnections';
|
||||
|
||||
/* eslint-disable indent */
|
||||
let currentServerId;
|
||||
|
||||
let currentServerId;
|
||||
function onSubmit(e) {
|
||||
const panel = dom.parentWithClass(this, 'dialog');
|
||||
|
||||
function onSubmit(e) {
|
||||
const panel = dom.parentWithClass(this, 'dialog');
|
||||
const playlistId = panel.querySelector('#selectPlaylistToAddTo').value;
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
|
||||
const playlistId = panel.querySelector('#selectPlaylistToAddTo').value;
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
|
||||
if (playlistId) {
|
||||
userSettings.set('playlisteditor-lastplaylistid', playlistId);
|
||||
addToPlaylist(apiClient, panel, playlistId);
|
||||
} else {
|
||||
createPlaylist(apiClient, panel);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
return false;
|
||||
if (playlistId) {
|
||||
userSettings.set('playlisteditor-lastplaylistid', playlistId);
|
||||
addToPlaylist(apiClient, panel, playlistId);
|
||||
} else {
|
||||
createPlaylist(apiClient, panel);
|
||||
}
|
||||
|
||||
function createPlaylist(apiClient, dlg) {
|
||||
loading.show();
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
const url = apiClient.getUrl('Playlists', {
|
||||
Name: dlg.querySelector('#txtNewPlaylistName').value,
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || '',
|
||||
userId: apiClient.getCurrentUserId()
|
||||
function createPlaylist(apiClient, dlg) {
|
||||
loading.show();
|
||||
|
||||
});
|
||||
const url = apiClient.getUrl('Playlists', {
|
||||
Name: dlg.querySelector('#txtNewPlaylistName').value,
|
||||
Ids: dlg.querySelector('.fldSelectedItemIds').value || '',
|
||||
userId: apiClient.getCurrentUserId()
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json'
|
||||
}).then(result => {
|
||||
loading.hide();
|
||||
|
||||
const id = result.Id;
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
redirectToPlaylist(apiClient, id);
|
||||
});
|
||||
}
|
||||
|
||||
function redirectToPlaylist(apiClient, id) {
|
||||
appRouter.showItem(id, apiClient.serverId());
|
||||
}
|
||||
|
||||
function addToPlaylist(apiClient, dlg, id) {
|
||||
const itemIds = dlg.querySelector('.fldSelectedItemIds').value || '';
|
||||
|
||||
if (id === 'queue') {
|
||||
playbackManager.queue({
|
||||
serverId: apiClient.serverId(),
|
||||
ids: itemIds.split(',')
|
||||
});
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
return;
|
||||
}
|
||||
|
||||
loading.show();
|
||||
|
||||
const url = apiClient.getUrl(`Playlists/${id}/Items`, {
|
||||
Ids: itemIds,
|
||||
userId: apiClient.getCurrentUserId()
|
||||
});
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url
|
||||
|
||||
}).then(() => {
|
||||
loading.hide();
|
||||
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
|
||||
function triggerChange(select) {
|
||||
select.dispatchEvent(new CustomEvent('change', {}));
|
||||
}
|
||||
|
||||
function populatePlaylists(editorOptions, panel) {
|
||||
const select = panel.querySelector('#selectPlaylistToAddTo');
|
||||
});
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json'
|
||||
}).then(result => {
|
||||
loading.hide();
|
||||
|
||||
panel.querySelector('.newPlaylistInfo').classList.add('hide');
|
||||
const id = result.Id;
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
redirectToPlaylist(apiClient, id);
|
||||
});
|
||||
}
|
||||
|
||||
const options = {
|
||||
Recursive: true,
|
||||
IncludeItemTypes: 'Playlist',
|
||||
SortBy: 'SortName',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
function redirectToPlaylist(apiClient, id) {
|
||||
appRouter.showItem(id, apiClient.serverId());
|
||||
}
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
|
||||
let html = '';
|
||||
function addToPlaylist(apiClient, dlg, id) {
|
||||
const itemIds = dlg.querySelector('.fldSelectedItemIds').value || '';
|
||||
|
||||
if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay.Manager.isSyncPlayEnabled()) {
|
||||
html += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`;
|
||||
}
|
||||
|
||||
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
|
||||
|
||||
html += result.Items.map(i => {
|
||||
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
|
||||
});
|
||||
|
||||
select.innerHTML = html;
|
||||
|
||||
let defaultValue = editorOptions.defaultValue;
|
||||
if (!defaultValue) {
|
||||
defaultValue = userSettings.get('playlisteditor-lastplaylistid') || '';
|
||||
}
|
||||
select.value = defaultValue === 'new' ? '' : defaultValue;
|
||||
|
||||
// If the value is empty set it again, in case we tried to set a lastplaylistid that is no longer valid
|
||||
if (!select.value) {
|
||||
select.value = '';
|
||||
}
|
||||
|
||||
triggerChange(select);
|
||||
|
||||
loading.hide();
|
||||
if (id === 'queue') {
|
||||
playbackManager.queue({
|
||||
serverId: apiClient.serverId(),
|
||||
ids: itemIds.split(',')
|
||||
});
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
return;
|
||||
}
|
||||
|
||||
function getEditorHtml(items) {
|
||||
loading.show();
|
||||
|
||||
const url = apiClient.getUrl(`Playlists/${id}/Items`, {
|
||||
Ids: itemIds,
|
||||
userId: apiClient.getCurrentUserId()
|
||||
});
|
||||
|
||||
apiClient.ajax({
|
||||
type: 'POST',
|
||||
url: url
|
||||
|
||||
}).then(() => {
|
||||
loading.hide();
|
||||
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
|
||||
function triggerChange(select) {
|
||||
select.dispatchEvent(new CustomEvent('change', {}));
|
||||
}
|
||||
|
||||
function populatePlaylists(editorOptions, panel) {
|
||||
const select = panel.querySelector('#selectPlaylistToAddTo');
|
||||
|
||||
loading.hide();
|
||||
|
||||
panel.querySelector('.newPlaylistInfo').classList.add('hide');
|
||||
|
||||
const options = {
|
||||
Recursive: true,
|
||||
IncludeItemTypes: 'Playlist',
|
||||
SortBy: 'SortName',
|
||||
EnableTotalRecordCount: false
|
||||
};
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
|
||||
|
||||
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
||||
html += '<div class="dialogContentInner dialog-content-centered">';
|
||||
html += '<form style="margin:auto;">';
|
||||
if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay?.Manager.isSyncPlayEnabled()) {
|
||||
html += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`;
|
||||
}
|
||||
|
||||
html += '<div class="fldSelectPlaylist selectContainer">';
|
||||
let autoFocus = items.length ? ' autofocus' : '';
|
||||
html += `<select is="emby-select" id="selectPlaylistToAddTo" label="${globalize.translate('LabelPlaylist')}"${autoFocus}></select>`;
|
||||
html += '</div>';
|
||||
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
|
||||
|
||||
html += '<div class="newPlaylistInfo">';
|
||||
|
||||
html += '<div class="inputContainer">';
|
||||
autoFocus = items.length ? '' : ' autofocus';
|
||||
html += `<input is="emby-input" type="text" id="txtNewPlaylistName" required="required" label="${globalize.translate('LabelName')}"${autoFocus} />`;
|
||||
html += '</div>';
|
||||
|
||||
// newPlaylistInfo
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="formDialogFooter">';
|
||||
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('Add')}</button>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
||||
|
||||
html += '</form>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function initEditor(content, options, items) {
|
||||
content.querySelector('#selectPlaylistToAddTo').addEventListener('change', function () {
|
||||
if (this.value) {
|
||||
content.querySelector('.newPlaylistInfo').classList.add('hide');
|
||||
content.querySelector('#txtNewPlaylistName').removeAttribute('required');
|
||||
} else {
|
||||
content.querySelector('.newPlaylistInfo').classList.remove('hide');
|
||||
content.querySelector('#txtNewPlaylistName').setAttribute('required', 'required');
|
||||
}
|
||||
html += result.Items.map(i => {
|
||||
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
|
||||
});
|
||||
|
||||
content.querySelector('form').addEventListener('submit', onSubmit);
|
||||
select.innerHTML = html;
|
||||
|
||||
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
|
||||
let defaultValue = editorOptions.defaultValue;
|
||||
if (!defaultValue) {
|
||||
defaultValue = userSettings.get('playlisteditor-lastplaylistid') || '';
|
||||
}
|
||||
select.value = defaultValue === 'new' ? '' : defaultValue;
|
||||
|
||||
if (items.length) {
|
||||
content.querySelector('.fldSelectPlaylist').classList.remove('hide');
|
||||
populatePlaylists(options, content);
|
||||
// If the value is empty set it again, in case we tried to set a lastplaylistid that is no longer valid
|
||||
if (!select.value) {
|
||||
select.value = '';
|
||||
}
|
||||
|
||||
triggerChange(select);
|
||||
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function getEditorHtml(items) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
||||
html += '<div class="dialogContentInner dialog-content-centered">';
|
||||
html += '<form style="margin:auto;">';
|
||||
|
||||
html += '<div class="fldSelectPlaylist selectContainer">';
|
||||
let autoFocus = items.length ? ' autofocus' : '';
|
||||
html += `<select is="emby-select" id="selectPlaylistToAddTo" label="${globalize.translate('LabelPlaylist')}"${autoFocus}></select>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="newPlaylistInfo">';
|
||||
|
||||
html += '<div class="inputContainer">';
|
||||
autoFocus = items.length ? '' : ' autofocus';
|
||||
html += `<input is="emby-input" type="text" id="txtNewPlaylistName" required="required" label="${globalize.translate('LabelName')}"${autoFocus} />`;
|
||||
html += '</div>';
|
||||
|
||||
// newPlaylistInfo
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="formDialogFooter">';
|
||||
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('Add')}</button>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
||||
|
||||
html += '</form>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function initEditor(content, options, items) {
|
||||
content.querySelector('#selectPlaylistToAddTo').addEventListener('change', function () {
|
||||
if (this.value) {
|
||||
content.querySelector('.newPlaylistInfo').classList.add('hide');
|
||||
content.querySelector('#txtNewPlaylistName').removeAttribute('required');
|
||||
} else {
|
||||
content.querySelector('.fldSelectPlaylist').classList.add('hide');
|
||||
|
||||
const selectPlaylistToAddTo = content.querySelector('#selectPlaylistToAddTo');
|
||||
selectPlaylistToAddTo.innerHTML = '';
|
||||
selectPlaylistToAddTo.value = '';
|
||||
triggerChange(selectPlaylistToAddTo);
|
||||
content.querySelector('.newPlaylistInfo').classList.remove('hide');
|
||||
content.querySelector('#txtNewPlaylistName').setAttribute('required', 'required');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
content.querySelector('form').addEventListener('submit', onSubmit);
|
||||
|
||||
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
|
||||
|
||||
if (items.length) {
|
||||
content.querySelector('.fldSelectPlaylist').classList.remove('hide');
|
||||
populatePlaylists(options, content);
|
||||
} else {
|
||||
content.querySelector('.fldSelectPlaylist').classList.add('hide');
|
||||
|
||||
const selectPlaylistToAddTo = content.querySelector('#selectPlaylistToAddTo');
|
||||
selectPlaylistToAddTo.innerHTML = '';
|
||||
selectPlaylistToAddTo.value = '';
|
||||
triggerChange(selectPlaylistToAddTo);
|
||||
}
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then((scrollHelper) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
}
|
||||
|
||||
export class showEditor {
|
||||
constructor(options) {
|
||||
const items = options.items || {};
|
||||
currentServerId = options.serverId;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
let html = '';
|
||||
const title = globalize.translate('HeaderAddToPlaylist');
|
||||
|
||||
html += '<div class="formDialogHeader">';
|
||||
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += '<h3 class="formDialogHeaderTitle">';
|
||||
html += title;
|
||||
html += '</h3>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += getEditorHtml(items);
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
initEditor(dlg, options, items);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||
}
|
||||
|
||||
if (dlg.submitted) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class showEditor {
|
||||
constructor(options) {
|
||||
const items = options.items || {};
|
||||
currentServerId = options.serverId;
|
||||
|
||||
const dialogOptions = {
|
||||
removeOnClose: true,
|
||||
scrollY: false
|
||||
};
|
||||
|
||||
if (layoutManager.tv) {
|
||||
dialogOptions.size = 'fullscreen';
|
||||
} else {
|
||||
dialogOptions.size = 'small';
|
||||
}
|
||||
|
||||
const dlg = dialogHelper.createDialog(dialogOptions);
|
||||
|
||||
dlg.classList.add('formDialog');
|
||||
|
||||
let html = '';
|
||||
const title = globalize.translate('HeaderAddToPlaylist');
|
||||
|
||||
html += '<div class="formDialogHeader">';
|
||||
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += '<h3 class="formDialogHeaderTitle">';
|
||||
html += title;
|
||||
html += '</h3>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += getEditorHtml(items);
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
initEditor(dlg, options, items);
|
||||
|
||||
dlg.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
|
||||
}
|
||||
|
||||
return dialogHelper.open(dlg).then(() => {
|
||||
if (layoutManager.tv) {
|
||||
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
|
||||
}
|
||||
|
||||
if (dlg.submitted) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
export default showEditor;
|
||||
|
||||
@@ -119,9 +119,14 @@ class PluginManager {
|
||||
}
|
||||
|
||||
ofType(type) {
|
||||
return this.pluginsList.filter((o) => {
|
||||
return o.type === type;
|
||||
});
|
||||
return this.pluginsList.filter(plugin => plugin.type === type);
|
||||
}
|
||||
|
||||
firstOfType(type) {
|
||||
// Get all plugins of the specified type
|
||||
return this.ofType(type)
|
||||
// Return the plugin with the "highest" (lowest numeric value) priority
|
||||
.sort((p1, p2) => (p1.priority || 0) - (p2.priority || 0))[0];
|
||||
}
|
||||
|
||||
#mapRoute(plugin, route) {
|
||||
|
||||
@@ -141,7 +141,7 @@ function onManageRecordingClick() {
|
||||
}
|
||||
|
||||
const self = this;
|
||||
import('./recordingeditor').then(({default: recordingEditor}) => {
|
||||
import('./recordingeditor').then(({ default: recordingEditor }) => {
|
||||
recordingEditor.show(self.TimerId, options.serverId, {
|
||||
enableCancel: false
|
||||
}).then(function () {
|
||||
@@ -159,7 +159,7 @@ function onManageSeriesRecordingClick() {
|
||||
|
||||
const self = this;
|
||||
|
||||
import('./seriesrecordingeditor').then(({default: seriesRecordingEditor}) => {
|
||||
import('./seriesrecordingeditor').then(({ default: seriesRecordingEditor }) => {
|
||||
seriesRecordingEditor.show(self.SeriesTimerId, options.serverId, {
|
||||
|
||||
enableCancel: false
|
||||
|
||||
@@ -325,10 +325,11 @@ export default function () {
|
||||
if (layoutManager.mobile) {
|
||||
const playingVideo = playbackManager.isPlayingVideo() && item !== null;
|
||||
const playingAudio = !playbackManager.isPlayingVideo() && item !== null;
|
||||
buttonVisible(context.querySelector('.btnRepeat'), playingAudio);
|
||||
buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio);
|
||||
buttonVisible(context.querySelector('.btnRewind'), playingVideo);
|
||||
buttonVisible(context.querySelector('.btnFastForward'), playingVideo);
|
||||
const playingAudioBook = playingAudio && item.Type == 'AudioBook';
|
||||
buttonVisible(context.querySelector('.btnRepeat'), playingAudio && !playingAudioBook);
|
||||
buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio && !playingAudioBook);
|
||||
buttonVisible(context.querySelector('.btnRewind'), playingVideo || playingAudioBook);
|
||||
buttonVisible(context.querySelector('.btnFastForward'), playingVideo || playingAudioBook);
|
||||
buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnShuffleQueue'), playingVideo);
|
||||
buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnRepeat'), playingVideo);
|
||||
} else {
|
||||
@@ -763,18 +764,22 @@ export default function () {
|
||||
|
||||
context.querySelector('.btnPreviousTrack').addEventListener('click', function (e) {
|
||||
if (currentPlayer) {
|
||||
if (lastPlayerState.NowPlayingItem.MediaType === 'Audio' && (currentPlayer._currentTime >= 5 || !playbackManager.previousTrack(currentPlayer))) {
|
||||
// Cancel this event if doubleclick is fired
|
||||
if (e.detail > 1 && playbackManager.previousTrack(currentPlayer)) {
|
||||
if (lastPlayerState.NowPlayingItem.MediaType === 'Audio') {
|
||||
// Cancel this event if doubleclick is fired. The actual previousTrack will be processed by the 'dblclick' event
|
||||
if (e.detail > 1 ) {
|
||||
return;
|
||||
}
|
||||
// Return to start of track, unless we are already (almost) at the beginning. In the latter case, continue and move
|
||||
// to the previous track, unless we are at the first track so no previous track exists.
|
||||
if (currentPlayer._currentTime >= 5 || playbackManager.getCurrentPlaylistIndex(currentPlayer) <= 1) {
|
||||
playbackManager.seekPercent(0, currentPlayer);
|
||||
// This is done automatically by playbackManager, however, setting this here gives instant visual feedback.
|
||||
// TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround.
|
||||
positionSlider.value = 0;
|
||||
return;
|
||||
}
|
||||
playbackManager.seekPercent(0, currentPlayer);
|
||||
// This is done automatically by playbackManager. However, setting this here gives instant visual feedback.
|
||||
// TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround.
|
||||
positionSlider.value = 0;
|
||||
} else {
|
||||
playbackManager.previousTrack(currentPlayer);
|
||||
}
|
||||
playbackManager.previousTrack(currentPlayer);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module for controlling scroll behavior.
|
||||
* @module components/scrollManager
|
||||
@@ -9,50 +7,50 @@ import dom from '../scripts/dom';
|
||||
import browser from '../scripts/browser';
|
||||
import layoutManager from './layoutManager';
|
||||
|
||||
/**
|
||||
/**
|
||||
* Scroll time in ms.
|
||||
*/
|
||||
const ScrollTime = 270;
|
||||
const ScrollTime = 270;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Epsilon for comparing values.
|
||||
*/
|
||||
const Epsilon = 1e-6;
|
||||
const Epsilon = 1e-6;
|
||||
|
||||
// FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers
|
||||
/**
|
||||
// FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers
|
||||
/**
|
||||
* Returns minimum vertical scroll.
|
||||
* Scroll less than that value will be zeroed.
|
||||
*
|
||||
* @return {number} Minimum vertical scroll.
|
||||
*/
|
||||
function minimumScrollY() {
|
||||
const topMenu = document.querySelector('.headerTop');
|
||||
if (topMenu) {
|
||||
return topMenu.clientHeight;
|
||||
function minimumScrollY() {
|
||||
const topMenu = document.querySelector('.headerTop');
|
||||
if (topMenu) {
|
||||
return topMenu.clientHeight;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style;
|
||||
|
||||
let supportsScrollToOptions = false;
|
||||
try {
|
||||
const elem = document.createElement('div');
|
||||
|
||||
const opts = Object.defineProperty({}, 'behavior', {
|
||||
// eslint-disable-next-line getter-return
|
||||
get: function () {
|
||||
supportsScrollToOptions = true;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style;
|
||||
elem.scrollTo(opts);
|
||||
} catch (e) {
|
||||
console.error('error checking ScrollToOptions support');
|
||||
}
|
||||
|
||||
let supportsScrollToOptions = false;
|
||||
try {
|
||||
const elem = document.createElement('div');
|
||||
|
||||
const opts = Object.defineProperty({}, 'behavior', {
|
||||
// eslint-disable-next-line getter-return
|
||||
get: function () {
|
||||
supportsScrollToOptions = true;
|
||||
}
|
||||
});
|
||||
|
||||
elem.scrollTo(opts);
|
||||
} catch (e) {
|
||||
console.error('error checking ScrollToOptions support');
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns value clamped by range [min, max].
|
||||
*
|
||||
* @param {number} value - Clamped value.
|
||||
@@ -60,16 +58,16 @@ import layoutManager from './layoutManager';
|
||||
* @param {number} max - Ending of range.
|
||||
* @return {number} Clamped value.
|
||||
*/
|
||||
function clamp(value, min, max) {
|
||||
if (value <= min) {
|
||||
return min;
|
||||
} else if (value >= max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
function clamp(value, min, max) {
|
||||
if (value <= min) {
|
||||
return min;
|
||||
} else if (value >= max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns the required delta to fit range 1 into range 2.
|
||||
* In case of range 1 is bigger than range 2 returns delta to fit most out of range part.
|
||||
*
|
||||
@@ -79,28 +77,28 @@ import layoutManager from './layoutManager';
|
||||
* @param {number} end2 - Ending of range 2.
|
||||
* @return {number} Delta: <0 move range1 to the left, >0 - to the right.
|
||||
*/
|
||||
function fitRange(begin1, end1, begin2, end2) {
|
||||
const delta1 = begin1 - begin2;
|
||||
const delta2 = end2 - end1;
|
||||
if (delta1 < 0 && delta1 < delta2) {
|
||||
return -delta1;
|
||||
} else if (delta2 < 0) {
|
||||
return delta2;
|
||||
}
|
||||
return 0;
|
||||
function fitRange(begin1, end1, begin2, end2) {
|
||||
const delta1 = begin1 - begin2;
|
||||
const delta2 = end2 - end1;
|
||||
if (delta1 < 0 && delta1 < delta2) {
|
||||
return -delta1;
|
||||
} else if (delta2 < 0) {
|
||||
return delta2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Ease value.
|
||||
*
|
||||
* @param {number} t - Value in range [0, 1].
|
||||
* @return {number} Eased value in range [0, 1].
|
||||
*/
|
||||
function ease(t) {
|
||||
return t * (2 - t); // easeOutQuad === ease-out
|
||||
}
|
||||
function ease(t) {
|
||||
return t * (2 - t); // easeOutQuad === ease-out
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @typedef {Object} Rect
|
||||
* @property {number} left - X coordinate of top-left corner.
|
||||
* @property {number} top - Y coordinate of top-left corner.
|
||||
@@ -108,7 +106,7 @@ import layoutManager from './layoutManager';
|
||||
* @property {number} height - Height.
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* Document scroll wrapper helps to unify scrolling and fix issues of some browsers.
|
||||
*
|
||||
* webOS 2 Browser: scrolls documentElement (and window), but body has a scroll size
|
||||
@@ -121,157 +119,157 @@ import layoutManager from './layoutManager';
|
||||
*
|
||||
* Tizen 5 Browser/Native: scrolls documentElement (and window); has a document.scrollingElement
|
||||
*/
|
||||
class DocumentScroller {
|
||||
/**
|
||||
class DocumentScroller {
|
||||
/**
|
||||
* Horizontal scroll position.
|
||||
* @type {number}
|
||||
*/
|
||||
get scrollLeft() {
|
||||
return window.pageXOffset;
|
||||
}
|
||||
get scrollLeft() {
|
||||
return window.pageXOffset;
|
||||
}
|
||||
|
||||
set scrollLeft(val) {
|
||||
window.scroll(val, window.pageYOffset);
|
||||
}
|
||||
set scrollLeft(val) {
|
||||
window.scroll(val, window.pageYOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Vertical scroll position.
|
||||
* @type {number}
|
||||
*/
|
||||
get scrollTop() {
|
||||
return window.pageYOffset;
|
||||
}
|
||||
get scrollTop() {
|
||||
return window.pageYOffset;
|
||||
}
|
||||
|
||||
set scrollTop(val) {
|
||||
window.scroll(window.pageXOffset, val);
|
||||
}
|
||||
set scrollTop(val) {
|
||||
window.scroll(window.pageXOffset, val);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Horizontal scroll size (scroll width).
|
||||
* @type {number}
|
||||
*/
|
||||
get scrollWidth() {
|
||||
return Math.max(document.documentElement.scrollWidth, document.body.scrollWidth);
|
||||
}
|
||||
get scrollWidth() {
|
||||
return Math.max(document.documentElement.scrollWidth, document.body.scrollWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Vertical scroll size (scroll height).
|
||||
* @type {number}
|
||||
*/
|
||||
get scrollHeight() {
|
||||
return Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
|
||||
}
|
||||
get scrollHeight() {
|
||||
return Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Horizontal client size (client width).
|
||||
* @type {number}
|
||||
*/
|
||||
get clientWidth() {
|
||||
return Math.min(document.documentElement.clientWidth, document.body.clientWidth);
|
||||
}
|
||||
get clientWidth() {
|
||||
return Math.min(document.documentElement.clientWidth, document.body.clientWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Vertical client size (client height).
|
||||
* @type {number}
|
||||
*/
|
||||
get clientHeight() {
|
||||
return Math.min(document.documentElement.clientHeight, document.body.clientHeight);
|
||||
}
|
||||
get clientHeight() {
|
||||
return Math.min(document.documentElement.clientHeight, document.body.clientHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns attribute value.
|
||||
* @param {string} attributeName - Attibute name.
|
||||
* @return {string} Attibute value.
|
||||
*/
|
||||
getAttribute(attributeName) {
|
||||
return document.body.getAttribute(attributeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bounding client rect.
|
||||
* @return {Rect} Bounding client rect.
|
||||
*/
|
||||
getBoundingClientRect() {
|
||||
// Make valid viewport coordinates: documentElement.getBoundingClientRect returns rect of entire document relative to viewport
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: this.clientWidth,
|
||||
height: this.clientHeight
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls window.
|
||||
* @param {...mixed} args See window.scrollTo.
|
||||
*/
|
||||
scrollTo() {
|
||||
window.scrollTo.apply(window, arguments);
|
||||
}
|
||||
getAttribute(attributeName) {
|
||||
return document.body.getAttribute(attributeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default (document) scroller.
|
||||
*/
|
||||
const documentScroller = new DocumentScroller();
|
||||
|
||||
const scrollerHints = {
|
||||
x: {
|
||||
nameScroll: 'scrollWidth',
|
||||
nameClient: 'clientWidth',
|
||||
nameStyle: 'overflowX',
|
||||
nameScrollMode: 'data-scroll-mode-x'
|
||||
},
|
||||
y: {
|
||||
nameScroll: 'scrollHeight',
|
||||
nameClient: 'clientHeight',
|
||||
nameStyle: 'overflowY',
|
||||
nameScrollMode: 'data-scroll-mode-y'
|
||||
}
|
||||
};
|
||||
* Returns bounding client rect.
|
||||
* @return {Rect} Bounding client rect.
|
||||
*/
|
||||
getBoundingClientRect() {
|
||||
// Make valid viewport coordinates: documentElement.getBoundingClientRect returns rect of entire document relative to viewport
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: this.clientWidth,
|
||||
height: this.clientHeight
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls window.
|
||||
* @param {...mixed} args See window.scrollTo.
|
||||
*/
|
||||
scrollTo() {
|
||||
window.scrollTo.apply(window, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default (document) scroller.
|
||||
*/
|
||||
const documentScroller = new DocumentScroller();
|
||||
|
||||
const scrollerHints = {
|
||||
x: {
|
||||
nameScroll: 'scrollWidth',
|
||||
nameClient: 'clientWidth',
|
||||
nameStyle: 'overflowX',
|
||||
nameScrollMode: 'data-scroll-mode-x'
|
||||
},
|
||||
y: {
|
||||
nameScroll: 'scrollHeight',
|
||||
nameClient: 'clientHeight',
|
||||
nameStyle: 'overflowY',
|
||||
nameScrollMode: 'data-scroll-mode-y'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns parent element that can be scrolled. If no such, returns document scroller.
|
||||
*
|
||||
* @param {HTMLElement} element - Element for which parent is being searched.
|
||||
* @param {boolean} vertical - Search for vertical scrollable parent.
|
||||
* @param {HTMLElement|DocumentScroller} Parent element that can be scrolled or document scroller.
|
||||
*/
|
||||
function getScrollableParent(element, vertical) {
|
||||
if (element) {
|
||||
const scrollerHint = vertical ? scrollerHints.y : scrollerHints.x;
|
||||
function getScrollableParent(element, vertical) {
|
||||
if (element) {
|
||||
const scrollerHint = vertical ? scrollerHints.y : scrollerHints.x;
|
||||
|
||||
let parent = element.parentElement;
|
||||
let parent = element.parentElement;
|
||||
|
||||
while (parent && parent !== document.body) {
|
||||
const scrollMode = parent.getAttribute(scrollerHint.nameScrollMode);
|
||||
while (parent && parent !== document.body) {
|
||||
const scrollMode = parent.getAttribute(scrollerHint.nameScrollMode);
|
||||
|
||||
// Stop on self-scrolled containers
|
||||
if (scrollMode === 'custom') {
|
||||
return parent;
|
||||
}
|
||||
|
||||
const styles = window.getComputedStyle(parent);
|
||||
|
||||
// Stop on fixed parent
|
||||
if (styles.position === 'fixed') {
|
||||
return parent;
|
||||
}
|
||||
|
||||
const overflow = styles[scrollerHint.nameStyle];
|
||||
|
||||
if (overflow === 'scroll' || overflow === 'auto' && parent[scrollerHint.nameScroll] > parent[scrollerHint.nameClient]) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent = parent.parentElement;
|
||||
// Stop on self-scrolled containers
|
||||
if (scrollMode === 'custom') {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
return documentScroller;
|
||||
const styles = window.getComputedStyle(parent);
|
||||
|
||||
// Stop on fixed parent
|
||||
if (styles.position === 'fixed') {
|
||||
return parent;
|
||||
}
|
||||
|
||||
const overflow = styles[scrollerHint.nameStyle];
|
||||
|
||||
if (overflow === 'scroll' || overflow === 'auto' && parent[scrollerHint.nameScroll] > parent[scrollerHint.nameClient]) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
return documentScroller;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} ScrollerData
|
||||
* @property {number} scrollPos - Current scroll position.
|
||||
* @property {number} scrollSize - Scroll size.
|
||||
@@ -280,34 +278,34 @@ import layoutManager from './layoutManager';
|
||||
* @property {boolean} custom - Custom scrolling mode.
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns scroller data for specified orientation.
|
||||
*
|
||||
* @param {HTMLElement} scroller - Scroller.
|
||||
* @param {boolean} vertical - Vertical scroller data.
|
||||
* @return {ScrollerData} Scroller data.
|
||||
*/
|
||||
function getScrollerData(scroller, vertical) {
|
||||
const data = {};
|
||||
function getScrollerData(scroller, vertical) {
|
||||
const data = {};
|
||||
|
||||
if (!vertical) {
|
||||
data.scrollPos = scroller.scrollLeft;
|
||||
data.scrollSize = scroller.scrollWidth;
|
||||
data.clientSize = scroller.clientWidth;
|
||||
data.mode = scroller.getAttribute(scrollerHints.x.nameScrollMode);
|
||||
} else {
|
||||
data.scrollPos = scroller.scrollTop;
|
||||
data.scrollSize = scroller.scrollHeight;
|
||||
data.clientSize = scroller.clientHeight;
|
||||
data.mode = scroller.getAttribute(scrollerHints.y.nameScrollMode);
|
||||
}
|
||||
|
||||
data.custom = data.mode === 'custom';
|
||||
|
||||
return data;
|
||||
if (!vertical) {
|
||||
data.scrollPos = scroller.scrollLeft;
|
||||
data.scrollSize = scroller.scrollWidth;
|
||||
data.clientSize = scroller.clientWidth;
|
||||
data.mode = scroller.getAttribute(scrollerHints.x.nameScrollMode);
|
||||
} else {
|
||||
data.scrollPos = scroller.scrollTop;
|
||||
data.scrollSize = scroller.scrollHeight;
|
||||
data.clientSize = scroller.clientHeight;
|
||||
data.mode = scroller.getAttribute(scrollerHints.y.nameScrollMode);
|
||||
}
|
||||
|
||||
/**
|
||||
data.custom = data.mode === 'custom';
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns position of child of scroller for specified orientation.
|
||||
*
|
||||
* @param {HTMLElement} scroller - Scroller.
|
||||
@@ -315,18 +313,18 @@ import layoutManager from './layoutManager';
|
||||
* @param {boolean} vertical - Vertical scroll.
|
||||
* @return {number} Child position.
|
||||
*/
|
||||
function getScrollerChildPos(scroller, element, vertical) {
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const scrollerRect = scroller.getBoundingClientRect();
|
||||
function getScrollerChildPos(scroller, element, vertical) {
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const scrollerRect = scroller.getBoundingClientRect();
|
||||
|
||||
if (!vertical) {
|
||||
return scroller.scrollLeft + elementRect.left - scrollerRect.left;
|
||||
} else {
|
||||
return scroller.scrollTop + elementRect.top - scrollerRect.top;
|
||||
}
|
||||
if (!vertical) {
|
||||
return scroller.scrollLeft + elementRect.left - scrollerRect.left;
|
||||
} else {
|
||||
return scroller.scrollTop + elementRect.top - scrollerRect.top;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns scroll position for element.
|
||||
*
|
||||
* @param {ScrollerData} scrollerData - Scroller data.
|
||||
@@ -335,47 +333,47 @@ import layoutManager from './layoutManager';
|
||||
* @param {boolean} centered - Scroll to center.
|
||||
* @return {number} Scroll position.
|
||||
*/
|
||||
function calcScroll(scrollerData, elementPos, elementSize, centered) {
|
||||
const maxScroll = scrollerData.scrollSize - scrollerData.clientSize;
|
||||
function calcScroll(scrollerData, elementPos, elementSize, centered) {
|
||||
const maxScroll = scrollerData.scrollSize - scrollerData.clientSize;
|
||||
|
||||
let scroll;
|
||||
let scroll;
|
||||
|
||||
if (centered) {
|
||||
scroll = elementPos + (elementSize - scrollerData.clientSize) / 2;
|
||||
} else {
|
||||
const delta = fitRange(elementPos, elementPos + elementSize - 1, scrollerData.scrollPos, scrollerData.scrollPos + scrollerData.clientSize - 1);
|
||||
scroll = scrollerData.scrollPos - delta;
|
||||
}
|
||||
|
||||
return clamp(Math.round(scroll), 0, maxScroll);
|
||||
if (centered) {
|
||||
scroll = elementPos + (elementSize - scrollerData.clientSize) / 2;
|
||||
} else {
|
||||
const delta = fitRange(elementPos, elementPos + elementSize - 1, scrollerData.scrollPos, scrollerData.scrollPos + scrollerData.clientSize - 1);
|
||||
scroll = scrollerData.scrollPos - delta;
|
||||
}
|
||||
|
||||
/**
|
||||
return clamp(Math.round(scroll), 0, maxScroll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls scrollTo function in proper way.
|
||||
*
|
||||
* @param {HTMLElement} scroller - Scroller.
|
||||
* @param {ScrollToOptions} options - Scroll options.
|
||||
*/
|
||||
function scrollToHelper(scroller, options) {
|
||||
if ('scrollTo' in scroller) {
|
||||
if (!supportsScrollToOptions) {
|
||||
const scrollX = (options.left !== undefined ? options.left : scroller.scrollLeft);
|
||||
const scrollY = (options.top !== undefined ? options.top : scroller.scrollTop);
|
||||
scroller.scrollTo(scrollX, scrollY);
|
||||
} else {
|
||||
scroller.scrollTo(options);
|
||||
}
|
||||
} else if ('scrollLeft' in scroller) {
|
||||
if (options.left !== undefined) {
|
||||
scroller.scrollLeft = options.left;
|
||||
}
|
||||
if (options.top !== undefined) {
|
||||
scroller.scrollTop = options.top;
|
||||
}
|
||||
function scrollToHelper(scroller, options) {
|
||||
if ('scrollTo' in scroller) {
|
||||
if (!supportsScrollToOptions) {
|
||||
const scrollX = (options.left !== undefined ? options.left : scroller.scrollLeft);
|
||||
const scrollY = (options.top !== undefined ? options.top : scroller.scrollTop);
|
||||
scroller.scrollTo(scrollX, scrollY);
|
||||
} else {
|
||||
scroller.scrollTo(options);
|
||||
}
|
||||
} else if ('scrollLeft' in scroller) {
|
||||
if (options.left !== undefined) {
|
||||
scroller.scrollLeft = options.left;
|
||||
}
|
||||
if (options.top !== undefined) {
|
||||
scroller.scrollTop = options.top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Performs built-in scroll.
|
||||
*
|
||||
* @param {HTMLElement} xScroller - Horizontal scroller.
|
||||
@@ -384,35 +382,35 @@ import layoutManager from './layoutManager';
|
||||
* @param {number} scrollY - Vertical coordinate.
|
||||
* @param {boolean} smooth - Smooth scrolling.
|
||||
*/
|
||||
function builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
|
||||
const scrollBehavior = smooth ? 'smooth' : 'instant';
|
||||
function builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
|
||||
const scrollBehavior = smooth ? 'smooth' : 'instant';
|
||||
|
||||
if (xScroller !== yScroller) {
|
||||
if (xScroller) {
|
||||
scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior});
|
||||
}
|
||||
if (yScroller) {
|
||||
scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior});
|
||||
}
|
||||
} else if (xScroller) {
|
||||
scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior});
|
||||
if (xScroller !== yScroller) {
|
||||
if (xScroller) {
|
||||
scrollToHelper(xScroller, { left: scrollX, behavior: scrollBehavior });
|
||||
}
|
||||
if (yScroller) {
|
||||
scrollToHelper(yScroller, { top: scrollY, behavior: scrollBehavior });
|
||||
}
|
||||
} else if (xScroller) {
|
||||
scrollToHelper(xScroller, { left: scrollX, top: scrollY, behavior: scrollBehavior });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Requested frame for animated scroll.
|
||||
*/
|
||||
let scrollTimer;
|
||||
let scrollTimer;
|
||||
|
||||
/**
|
||||
/**
|
||||
* Resets scroll timer to stop scrolling.
|
||||
*/
|
||||
function resetScrollTimer() {
|
||||
cancelAnimationFrame(scrollTimer);
|
||||
scrollTimer = undefined;
|
||||
}
|
||||
function resetScrollTimer() {
|
||||
cancelAnimationFrame(scrollTimer);
|
||||
scrollTimer = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Performs animated scroll.
|
||||
*
|
||||
* @param {HTMLElement} xScroller - Horizontal scroller.
|
||||
@@ -420,43 +418,43 @@ import layoutManager from './layoutManager';
|
||||
* @param {HTMLElement} yScroller - Vertical scroller.
|
||||
* @param {number} scrollY - Vertical coordinate.
|
||||
*/
|
||||
function animateScroll(xScroller, scrollX, yScroller, scrollY) {
|
||||
const ox = xScroller ? xScroller.scrollLeft : scrollX;
|
||||
const oy = yScroller ? yScroller.scrollTop : scrollY;
|
||||
const dx = scrollX - ox;
|
||||
const dy = scrollY - oy;
|
||||
function animateScroll(xScroller, scrollX, yScroller, scrollY) {
|
||||
const ox = xScroller ? xScroller.scrollLeft : scrollX;
|
||||
const oy = yScroller ? yScroller.scrollTop : scrollY;
|
||||
const dx = scrollX - ox;
|
||||
const dy = scrollY - oy;
|
||||
|
||||
if (Math.abs(dx) < Epsilon && Math.abs(dy) < Epsilon) {
|
||||
if (Math.abs(dx) < Epsilon && Math.abs(dy) < Epsilon) {
|
||||
return;
|
||||
}
|
||||
|
||||
let start;
|
||||
|
||||
function scrollAnim(currentTimestamp) {
|
||||
start = start || currentTimestamp;
|
||||
|
||||
let k = Math.min(1, (currentTimestamp - start) / ScrollTime);
|
||||
|
||||
if (k === 1) {
|
||||
resetScrollTimer();
|
||||
builtinScroll(xScroller, scrollX, yScroller, scrollY, false);
|
||||
return;
|
||||
}
|
||||
|
||||
let start;
|
||||
k = ease(k);
|
||||
|
||||
function scrollAnim(currentTimestamp) {
|
||||
start = start || currentTimestamp;
|
||||
const x = ox + dx * k;
|
||||
const y = oy + dy * k;
|
||||
|
||||
let k = Math.min(1, (currentTimestamp - start) / ScrollTime);
|
||||
|
||||
if (k === 1) {
|
||||
resetScrollTimer();
|
||||
builtinScroll(xScroller, scrollX, yScroller, scrollY, false);
|
||||
return;
|
||||
}
|
||||
|
||||
k = ease(k);
|
||||
|
||||
const x = ox + dx * k;
|
||||
const y = oy + dy * k;
|
||||
|
||||
builtinScroll(xScroller, x, yScroller, y, false);
|
||||
|
||||
scrollTimer = requestAnimationFrame(scrollAnim);
|
||||
}
|
||||
builtinScroll(xScroller, x, yScroller, y, false);
|
||||
|
||||
scrollTimer = requestAnimationFrame(scrollAnim);
|
||||
}
|
||||
|
||||
/**
|
||||
scrollTimer = requestAnimationFrame(scrollAnim);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs scroll.
|
||||
*
|
||||
* @param {HTMLElement} xScroller - Horizontal scroller.
|
||||
@@ -465,142 +463,140 @@ import layoutManager from './layoutManager';
|
||||
* @param {number} scrollY - Vertical coordinate.
|
||||
* @param {boolean} smooth - Smooth scrolling.
|
||||
*/
|
||||
function doScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
|
||||
resetScrollTimer();
|
||||
function doScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
|
||||
resetScrollTimer();
|
||||
|
||||
if (smooth && useAnimatedScroll()) {
|
||||
animateScroll(xScroller, scrollX, yScroller, scrollY);
|
||||
} else {
|
||||
builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth);
|
||||
}
|
||||
if (smooth && useAnimatedScroll()) {
|
||||
animateScroll(xScroller, scrollX, yScroller, scrollY);
|
||||
} else {
|
||||
builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns true if smooth scroll must be used.
|
||||
*/
|
||||
function useSmoothScroll() {
|
||||
return !!browser.tizen;
|
||||
}
|
||||
function useSmoothScroll() {
|
||||
return !!browser.tizen;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns true if animated implementation of smooth scroll must be used.
|
||||
*/
|
||||
function useAnimatedScroll() {
|
||||
// Add block to force using (or not) of animated implementation
|
||||
function useAnimatedScroll() {
|
||||
// Add block to force using (or not) of animated implementation
|
||||
|
||||
return !supportsSmoothScroll;
|
||||
}
|
||||
return !supportsSmoothScroll;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns true if scroll manager is enabled.
|
||||
*/
|
||||
export function isEnabled() {
|
||||
return layoutManager.tv;
|
||||
}
|
||||
export function isEnabled() {
|
||||
return layoutManager.tv;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Scrolls the document to a given position.
|
||||
*
|
||||
* @param {number} scrollX - Horizontal coordinate.
|
||||
* @param {number} scrollY - Vertical coordinate.
|
||||
* @param {boolean} [smooth=false] - Smooth scrolling.
|
||||
*/
|
||||
export function scrollTo(scrollX, scrollY, smooth) {
|
||||
smooth = !!smooth;
|
||||
export function scrollTo(scrollX, scrollY, smooth) {
|
||||
smooth = !!smooth;
|
||||
|
||||
// Scroller is document itself by default
|
||||
const scroller = getScrollableParent(null, false);
|
||||
// Scroller is document itself by default
|
||||
const scroller = getScrollableParent(null, false);
|
||||
|
||||
const xScrollerData = getScrollerData(scroller, false);
|
||||
const yScrollerData = getScrollerData(scroller, true);
|
||||
const xScrollerData = getScrollerData(scroller, false);
|
||||
const yScrollerData = getScrollerData(scroller, true);
|
||||
|
||||
scrollX = clamp(Math.round(scrollX), 0, xScrollerData.scrollSize - xScrollerData.clientSize);
|
||||
scrollY = clamp(Math.round(scrollY), 0, yScrollerData.scrollSize - yScrollerData.clientSize);
|
||||
scrollX = clamp(Math.round(scrollX), 0, xScrollerData.scrollSize - xScrollerData.clientSize);
|
||||
scrollY = clamp(Math.round(scrollY), 0, yScrollerData.scrollSize - yScrollerData.clientSize);
|
||||
|
||||
doScroll(scroller, scrollX, scroller, scrollY, smooth);
|
||||
}
|
||||
doScroll(scroller, scrollX, scroller, scrollY, smooth);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Scrolls the document to a given element.
|
||||
*
|
||||
* @param {HTMLElement} element - Target element of scroll task.
|
||||
* @param {boolean} [smooth=false] - Smooth scrolling.
|
||||
*/
|
||||
export function scrollToElement(element, smooth) {
|
||||
smooth = !!smooth;
|
||||
export function scrollToElement(element, smooth) {
|
||||
smooth = !!smooth;
|
||||
|
||||
let scrollCenterX = true;
|
||||
let scrollCenterY = true;
|
||||
let scrollCenterX = true;
|
||||
let scrollCenterY = true;
|
||||
|
||||
const offsetParent = element.offsetParent;
|
||||
const offsetParent = element.offsetParent;
|
||||
|
||||
// In Firefox offsetParent.offsetParent is BODY
|
||||
const isFixed = offsetParent && (!offsetParent.offsetParent || window.getComputedStyle(offsetParent).position === 'fixed');
|
||||
// In Firefox offsetParent.offsetParent is BODY
|
||||
const isFixed = offsetParent && (!offsetParent.offsetParent || window.getComputedStyle(offsetParent).position === 'fixed');
|
||||
|
||||
// Scroll fixed elements to nearest edge (or do not scroll at all)
|
||||
if (isFixed) {
|
||||
scrollCenterX = scrollCenterY = false;
|
||||
}
|
||||
|
||||
let xScroller = getScrollableParent(element, false);
|
||||
let yScroller = getScrollableParent(element, true);
|
||||
|
||||
const xScrollerData = getScrollerData(xScroller, false);
|
||||
const yScrollerData = getScrollerData(yScroller, true);
|
||||
|
||||
// Exit, since we have no control over scrolling in this container
|
||||
if (xScroller === yScroller && (xScrollerData.custom || yScrollerData.custom)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Exit, since we have no control over scrolling in these containers
|
||||
if (xScrollerData.custom && yScrollerData.custom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
let scrollX = 0;
|
||||
let scrollY = 0;
|
||||
|
||||
if (!xScrollerData.custom) {
|
||||
const xPos = getScrollerChildPos(xScroller, element, false);
|
||||
scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX);
|
||||
} else {
|
||||
xScroller = null;
|
||||
}
|
||||
|
||||
if (!yScrollerData.custom) {
|
||||
const yPos = getScrollerChildPos(yScroller, element, true);
|
||||
scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY);
|
||||
|
||||
// HACK: Scroll to top for top menu because it is hidden
|
||||
// FIXME: Need a marker to scroll top/bottom
|
||||
if (isFixed && elementRect.bottom < 0) {
|
||||
scrollY = 0;
|
||||
}
|
||||
|
||||
// HACK: Ensure we are at the top
|
||||
// FIXME: Need a marker to scroll top/bottom
|
||||
if (scrollY < minimumScrollY() && yScroller === documentScroller) {
|
||||
scrollY = 0;
|
||||
}
|
||||
} else {
|
||||
yScroller = null;
|
||||
}
|
||||
|
||||
doScroll(xScroller, scrollX, yScroller, scrollY, smooth);
|
||||
// Scroll fixed elements to nearest edge (or do not scroll at all)
|
||||
if (isFixed) {
|
||||
scrollCenterX = scrollCenterY = false;
|
||||
}
|
||||
|
||||
if (isEnabled()) {
|
||||
dom.addEventListener(window, 'focusin', function(e) {
|
||||
setTimeout(function() {
|
||||
scrollToElement(e.target, useSmoothScroll());
|
||||
}, 0);
|
||||
}, {capture: true});
|
||||
let xScroller = getScrollableParent(element, false);
|
||||
let yScroller = getScrollableParent(element, true);
|
||||
|
||||
const xScrollerData = getScrollerData(xScroller, false);
|
||||
const yScrollerData = getScrollerData(yScroller, true);
|
||||
|
||||
// Exit, since we have no control over scrolling in this container
|
||||
if (xScroller === yScroller && (xScrollerData.custom || yScrollerData.custom)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
// Exit, since we have no control over scrolling in these containers
|
||||
if (xScrollerData.custom && yScrollerData.custom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
|
||||
let scrollX = 0;
|
||||
let scrollY = 0;
|
||||
|
||||
if (!xScrollerData.custom) {
|
||||
const xPos = getScrollerChildPos(xScroller, element, false);
|
||||
scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX);
|
||||
} else {
|
||||
xScroller = null;
|
||||
}
|
||||
|
||||
if (!yScrollerData.custom) {
|
||||
const yPos = getScrollerChildPos(yScroller, element, true);
|
||||
scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY);
|
||||
|
||||
// HACK: Scroll to top for top menu because it is hidden
|
||||
// FIXME: Need a marker to scroll top/bottom
|
||||
if (isFixed && elementRect.bottom < 0) {
|
||||
scrollY = 0;
|
||||
}
|
||||
|
||||
// HACK: Ensure we are at the top
|
||||
// FIXME: Need a marker to scroll top/bottom
|
||||
if (scrollY < minimumScrollY() && yScroller === documentScroller) {
|
||||
scrollY = 0;
|
||||
}
|
||||
} else {
|
||||
yScroller = null;
|
||||
}
|
||||
|
||||
doScroll(xScroller, scrollX, yScroller, scrollY, smooth);
|
||||
}
|
||||
|
||||
if (isEnabled()) {
|
||||
dom.addEventListener(window, 'focusin', function(e) {
|
||||
setTimeout(function() {
|
||||
scrollToElement(e.target, useSmoothScroll());
|
||||
}, 0);
|
||||
}, { capture: true });
|
||||
}
|
||||
|
||||
export default {
|
||||
isEnabled: isEnabled,
|
||||
|
||||
@@ -85,8 +85,8 @@ const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {
|
||||
dangerouslySetInnerHTML={createInputElement()}
|
||||
/>
|
||||
</div>
|
||||
{layoutManager.tv && !browser.tv &&
|
||||
<AlphaPicker onAlphaPicked={onAlphaPicked} />
|
||||
{layoutManager.tv && !browser.tv
|
||||
&& <AlphaPicker onAlphaPicked={onAlphaPicked} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* Module shortcuts.
|
||||
* @module components/shortcuts
|
||||
@@ -14,385 +12,383 @@ import recordingHelper from './recordingcreator/recordinghelper';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import toast from './toast/toast';
|
||||
|
||||
function playAllFromHere(card, serverId, queue) {
|
||||
const parent = card.parentNode;
|
||||
const className = card.classList.length ? (`.${card.classList[0]}`) : '';
|
||||
const cards = parent.querySelectorAll(`${className}[data-id]`);
|
||||
function playAllFromHere(card, serverId, queue) {
|
||||
const parent = card.parentNode;
|
||||
const className = card.classList.length ? (`.${card.classList[0]}`) : '';
|
||||
const cards = parent.querySelectorAll(`${className}[data-id]`);
|
||||
|
||||
const ids = [];
|
||||
const ids = [];
|
||||
|
||||
let foundCard = false;
|
||||
let startIndex;
|
||||
let foundCard = false;
|
||||
let startIndex;
|
||||
|
||||
for (let i = 0, length = cards.length; i < length; i++) {
|
||||
if (cards[i] === card) {
|
||||
foundCard = true;
|
||||
startIndex = i;
|
||||
}
|
||||
if (foundCard || !queue) {
|
||||
ids.push(cards[i].getAttribute('data-id'));
|
||||
for (let i = 0, length = cards.length; i < length; i++) {
|
||||
if (cards[i] === card) {
|
||||
foundCard = true;
|
||||
startIndex = i;
|
||||
}
|
||||
if (foundCard || !queue) {
|
||||
ids.push(cards[i].getAttribute('data-id'));
|
||||
}
|
||||
}
|
||||
|
||||
const itemsContainer = dom.parentWithClass(card, 'itemsContainer');
|
||||
if (itemsContainer && itemsContainer.fetchData) {
|
||||
const queryOptions = queue ? { StartIndex: startIndex } : {};
|
||||
|
||||
return itemsContainer.fetchData(queryOptions).then(result => {
|
||||
if (queue) {
|
||||
return playbackManager.queue({
|
||||
items: result.Items
|
||||
});
|
||||
} else {
|
||||
return playbackManager.play({
|
||||
items: result.Items,
|
||||
startIndex: startIndex
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!ids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (queue) {
|
||||
return playbackManager.queue({
|
||||
ids: ids,
|
||||
serverId: serverId
|
||||
});
|
||||
} else {
|
||||
return playbackManager.play({
|
||||
ids: ids,
|
||||
serverId: serverId,
|
||||
startIndex: startIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showProgramDialog(item) {
|
||||
import('./recordingcreator/recordingcreator').then(({ default:recordingCreator }) => {
|
||||
recordingCreator.show(item.Id, item.ServerId);
|
||||
});
|
||||
}
|
||||
|
||||
function getItem(button) {
|
||||
button = dom.parentWithAttribute(button, 'data-id');
|
||||
const serverId = button.getAttribute('data-serverid');
|
||||
const id = button.getAttribute('data-id');
|
||||
const type = button.getAttribute('data-type');
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
if (type === 'Timer') {
|
||||
return apiClient.getLiveTvTimer(id);
|
||||
}
|
||||
if (type === 'SeriesTimer') {
|
||||
return apiClient.getLiveTvSeriesTimer(id);
|
||||
}
|
||||
return apiClient.getItem(apiClient.getCurrentUserId(), id);
|
||||
}
|
||||
|
||||
function notifyRefreshNeeded(childElement, itemsContainer) {
|
||||
itemsContainer = itemsContainer || dom.parentWithAttribute(childElement, 'is', 'emby-itemscontainer');
|
||||
|
||||
if (itemsContainer) {
|
||||
itemsContainer.notifyRefreshNeeded(true);
|
||||
}
|
||||
}
|
||||
|
||||
function showContextMenu(card, options) {
|
||||
getItem(card).then(item => {
|
||||
const playlistId = card.getAttribute('data-playlistid');
|
||||
const collectionId = card.getAttribute('data-collectionid');
|
||||
|
||||
if (playlistId) {
|
||||
const elem = dom.parentWithAttribute(card, 'data-playlistitemid');
|
||||
item.PlaylistItemId = elem ? elem.getAttribute('data-playlistitemid') : null;
|
||||
}
|
||||
|
||||
const itemsContainer = dom.parentWithClass(card, 'itemsContainer');
|
||||
if (itemsContainer && itemsContainer.fetchData) {
|
||||
const queryOptions = queue ? { StartIndex: startIndex } : {};
|
||||
import('./itemContextMenu').then((itemContextMenu) => {
|
||||
ServerConnections.getApiClient(item.ServerId).getCurrentUser().then(user => {
|
||||
itemContextMenu.show(Object.assign({
|
||||
item: item,
|
||||
play: true,
|
||||
queue: true,
|
||||
playAllFromHere: !item.IsFolder,
|
||||
queueAllFromHere: !item.IsFolder,
|
||||
playlistId: playlistId,
|
||||
collectionId: collectionId,
|
||||
user: user
|
||||
|
||||
return itemsContainer.fetchData(queryOptions).then(result => {
|
||||
if (queue) {
|
||||
return playbackManager.queue({
|
||||
items: result.Items
|
||||
});
|
||||
} else {
|
||||
return playbackManager.play({
|
||||
items: result.Items,
|
||||
startIndex: startIndex
|
||||
});
|
||||
}
|
||||
}, options || {})).then(result => {
|
||||
if (result.command === 'playallfromhere' || result.command === 'queueallfromhere') {
|
||||
executeAction(card, options.positionTo, result.command);
|
||||
} else if (result.updated || result.deleted) {
|
||||
notifyRefreshNeeded(card, options.itemsContainer);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!ids.length) {
|
||||
return;
|
||||
function getItemInfoFromCard(card) {
|
||||
return {
|
||||
Type: card.getAttribute('data-type'),
|
||||
Id: card.getAttribute('data-id'),
|
||||
TimerId: card.getAttribute('data-timerid'),
|
||||
CollectionType: card.getAttribute('data-collectiontype'),
|
||||
ChannelId: card.getAttribute('data-channelid'),
|
||||
SeriesId: card.getAttribute('data-seriesid'),
|
||||
ServerId: card.getAttribute('data-serverid'),
|
||||
MediaType: card.getAttribute('data-mediatype'),
|
||||
Path: card.getAttribute('data-path'),
|
||||
IsFolder: card.getAttribute('data-isfolder') === 'true',
|
||||
StartDate: card.getAttribute('data-startdate'),
|
||||
EndDate: card.getAttribute('data-enddate'),
|
||||
UserData: {
|
||||
PlaybackPositionTicks: parseInt(card.getAttribute('data-positionticks') || '0', 10)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (queue) {
|
||||
return playbackManager.queue({
|
||||
ids: ids,
|
||||
function showPlayMenu(card, target) {
|
||||
const item = getItemInfoFromCard(card);
|
||||
|
||||
import('./playmenu').then((playMenu) => {
|
||||
playMenu.show({
|
||||
|
||||
item: item,
|
||||
positionTo: target
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function executeAction(card, target, action) {
|
||||
target = target || card;
|
||||
|
||||
let id = card.getAttribute('data-id');
|
||||
|
||||
if (!id) {
|
||||
card = dom.parentWithAttribute(card, 'data-id');
|
||||
id = card.getAttribute('data-id');
|
||||
}
|
||||
|
||||
const item = getItemInfoFromCard(card);
|
||||
|
||||
const serverId = item.ServerId;
|
||||
const type = item.Type;
|
||||
|
||||
const playableItemId = type === 'Program' ? item.ChannelId : item.Id;
|
||||
|
||||
if (item.MediaType === 'Photo' && action === 'link') {
|
||||
action = 'play';
|
||||
}
|
||||
|
||||
if (action === 'link') {
|
||||
appRouter.showItem(item, {
|
||||
context: card.getAttribute('data-context'),
|
||||
parentId: card.getAttribute('data-parentid')
|
||||
});
|
||||
} else if (action === 'programdialog') {
|
||||
showProgramDialog(item);
|
||||
} else if (action === 'instantmix') {
|
||||
playbackManager.instantMix({
|
||||
Id: playableItemId,
|
||||
ServerId: serverId
|
||||
});
|
||||
} else if (action === 'play' || action === 'resume') {
|
||||
const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10);
|
||||
|
||||
if (playbackManager.canPlay(item)) {
|
||||
playbackManager.play({
|
||||
ids: [playableItemId],
|
||||
startPositionTicks: startPositionTicks,
|
||||
serverId: serverId
|
||||
});
|
||||
} else {
|
||||
return playbackManager.play({
|
||||
ids: ids,
|
||||
serverId: serverId,
|
||||
startIndex: startIndex
|
||||
console.warn('Unable to play item', item);
|
||||
}
|
||||
} else if (action === 'queue') {
|
||||
if (playbackManager.isPlaying()) {
|
||||
playbackManager.queue({
|
||||
ids: [playableItemId],
|
||||
serverId: serverId
|
||||
});
|
||||
toast(globalize.translate('MediaQueued'));
|
||||
} else {
|
||||
playbackManager.queue({
|
||||
ids: [playableItemId],
|
||||
serverId: serverId
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (action === 'playallfromhere') {
|
||||
playAllFromHere(card, serverId);
|
||||
} else if (action === 'queueallfromhere') {
|
||||
playAllFromHere(card, serverId, true);
|
||||
} else if (action === 'setplaylistindex') {
|
||||
playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid'));
|
||||
} else if (action === 'record') {
|
||||
onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid'));
|
||||
} else if (action === 'menu') {
|
||||
const options = target.getAttribute('data-playoptions') === 'false' ?
|
||||
{
|
||||
shuffle: false,
|
||||
instantMix: false,
|
||||
play: false,
|
||||
playAllFromHere: false,
|
||||
queue: false,
|
||||
queueAllFromHere: false
|
||||
} :
|
||||
{};
|
||||
|
||||
function showProgramDialog(item) {
|
||||
import('./recordingcreator/recordingcreator').then(({default:recordingCreator}) => {
|
||||
recordingCreator.show(item.Id, item.ServerId);
|
||||
options.positionTo = target;
|
||||
|
||||
showContextMenu(card, options);
|
||||
} else if (action === 'playmenu') {
|
||||
showPlayMenu(card, target);
|
||||
} else if (action === 'edit') {
|
||||
getItem(target).then(itemToEdit => {
|
||||
editItem(itemToEdit, serverId);
|
||||
});
|
||||
} else if (action === 'playtrailer') {
|
||||
getItem(target).then(playTrailer);
|
||||
} else if (action === 'addtoplaylist') {
|
||||
getItem(target).then(addToPlaylist);
|
||||
} else if (action === 'custom') {
|
||||
const customAction = target.getAttribute('data-customaction');
|
||||
|
||||
card.dispatchEvent(new CustomEvent(`action-${customAction}`, {
|
||||
detail: {
|
||||
playlistItemId: card.getAttribute('data-playlistitemid')
|
||||
},
|
||||
cancelable: false,
|
||||
bubbles: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function getItem(button) {
|
||||
button = dom.parentWithAttribute(button, 'data-id');
|
||||
const serverId = button.getAttribute('data-serverid');
|
||||
const id = button.getAttribute('data-id');
|
||||
const type = button.getAttribute('data-type');
|
||||
function addToPlaylist(item) {
|
||||
import('./playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
|
||||
new playlistEditor().show({
|
||||
items: [item.Id],
|
||||
serverId: item.ServerId
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
if (type === 'Timer') {
|
||||
return apiClient.getLiveTvTimer(id);
|
||||
}
|
||||
if (type === 'SeriesTimer') {
|
||||
return apiClient.getLiveTvSeriesTimer(id);
|
||||
}
|
||||
return apiClient.getItem(apiClient.getCurrentUserId(), id);
|
||||
}
|
||||
|
||||
function notifyRefreshNeeded(childElement, itemsContainer) {
|
||||
itemsContainer = itemsContainer || dom.parentWithAttribute(childElement, 'is', 'emby-itemscontainer');
|
||||
|
||||
if (itemsContainer) {
|
||||
itemsContainer.notifyRefreshNeeded(true);
|
||||
}
|
||||
}
|
||||
|
||||
function showContextMenu(card, options) {
|
||||
getItem(card).then(item => {
|
||||
const playlistId = card.getAttribute('data-playlistid');
|
||||
const collectionId = card.getAttribute('data-collectionid');
|
||||
|
||||
if (playlistId) {
|
||||
const elem = dom.parentWithAttribute(card, 'data-playlistitemid');
|
||||
item.PlaylistItemId = elem ? elem.getAttribute('data-playlistitemid') : null;
|
||||
}
|
||||
|
||||
import('./itemContextMenu').then((itemContextMenu) => {
|
||||
ServerConnections.getApiClient(item.ServerId).getCurrentUser().then(user => {
|
||||
itemContextMenu.show(Object.assign({
|
||||
item: item,
|
||||
play: true,
|
||||
queue: true,
|
||||
playAllFromHere: !item.IsFolder,
|
||||
queueAllFromHere: !item.IsFolder,
|
||||
playlistId: playlistId,
|
||||
collectionId: collectionId,
|
||||
user: user
|
||||
|
||||
}, options || {})).then(result => {
|
||||
if (result.command === 'playallfromhere' || result.command === 'queueallfromhere') {
|
||||
executeAction(card, options.positionTo, result.command);
|
||||
} else if (result.updated || result.deleted) {
|
||||
notifyRefreshNeeded(card, options.itemsContainer);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getItemInfoFromCard(card) {
|
||||
return {
|
||||
Type: card.getAttribute('data-type'),
|
||||
Id: card.getAttribute('data-id'),
|
||||
TimerId: card.getAttribute('data-timerid'),
|
||||
CollectionType: card.getAttribute('data-collectiontype'),
|
||||
ChannelId: card.getAttribute('data-channelid'),
|
||||
SeriesId: card.getAttribute('data-seriesid'),
|
||||
ServerId: card.getAttribute('data-serverid'),
|
||||
MediaType: card.getAttribute('data-mediatype'),
|
||||
Path: card.getAttribute('data-path'),
|
||||
IsFolder: card.getAttribute('data-isfolder') === 'true',
|
||||
StartDate: card.getAttribute('data-startdate'),
|
||||
EndDate: card.getAttribute('data-enddate'),
|
||||
UserData: {
|
||||
PlaybackPositionTicks: parseInt(card.getAttribute('data-positionticks') || '0', 10)
|
||||
}
|
||||
};
|
||||
}
|
||||
function playTrailer(item) {
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
function showPlayMenu(card, target) {
|
||||
const item = getItemInfoFromCard(card);
|
||||
apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(trailers => {
|
||||
playbackManager.play({ items: trailers });
|
||||
});
|
||||
}
|
||||
|
||||
import('./playmenu').then((playMenu) => {
|
||||
playMenu.show({
|
||||
function editItem(item, serverId) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
item: item,
|
||||
positionTo: target
|
||||
});
|
||||
});
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const currentServerId = apiClient.serverInfo().Id;
|
||||
|
||||
function executeAction(card, target, action) {
|
||||
target = target || card;
|
||||
|
||||
let id = card.getAttribute('data-id');
|
||||
|
||||
if (!id) {
|
||||
card = dom.parentWithAttribute(card, 'data-id');
|
||||
id = card.getAttribute('data-id');
|
||||
}
|
||||
|
||||
const item = getItemInfoFromCard(card);
|
||||
|
||||
const serverId = item.ServerId;
|
||||
const type = item.Type;
|
||||
|
||||
const playableItemId = type === 'Program' ? item.ChannelId : item.Id;
|
||||
|
||||
if (item.MediaType === 'Photo' && action === 'link') {
|
||||
action = 'play';
|
||||
}
|
||||
|
||||
if (action === 'link') {
|
||||
appRouter.showItem(item, {
|
||||
context: card.getAttribute('data-context'),
|
||||
parentId: card.getAttribute('data-parentid')
|
||||
});
|
||||
} else if (action === 'programdialog') {
|
||||
showProgramDialog(item);
|
||||
} else if (action === 'instantmix') {
|
||||
playbackManager.instantMix({
|
||||
Id: playableItemId,
|
||||
ServerId: serverId
|
||||
});
|
||||
} else if (action === 'play' || action === 'resume') {
|
||||
const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10);
|
||||
|
||||
if (playbackManager.canPlay(item)) {
|
||||
playbackManager.play({
|
||||
ids: [playableItemId],
|
||||
startPositionTicks: startPositionTicks,
|
||||
serverId: serverId
|
||||
if (item.Type === 'Timer') {
|
||||
if (item.ProgramId) {
|
||||
import('./recordingcreator/recordingcreator').then(({ default: recordingCreator }) => {
|
||||
recordingCreator.show(item.ProgramId, currentServerId).then(resolve, reject);
|
||||
});
|
||||
} else {
|
||||
console.warn('Unable to play item', item);
|
||||
}
|
||||
} else if (action === 'queue') {
|
||||
if (playbackManager.isPlaying()) {
|
||||
playbackManager.queue({
|
||||
ids: [playableItemId],
|
||||
serverId: serverId
|
||||
});
|
||||
toast(globalize.translate('MediaQueued'));
|
||||
} else {
|
||||
playbackManager.queue({
|
||||
ids: [playableItemId],
|
||||
serverId: serverId
|
||||
import('./recordingcreator/recordingeditor').then(({ default: recordingEditor }) => {
|
||||
recordingEditor.show(item.Id, currentServerId).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
} else if (action === 'playallfromhere') {
|
||||
playAllFromHere(card, serverId);
|
||||
} else if (action === 'queueallfromhere') {
|
||||
playAllFromHere(card, serverId, true);
|
||||
} else if (action === 'setplaylistindex') {
|
||||
playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid'));
|
||||
} else if (action === 'record') {
|
||||
onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid'));
|
||||
} else if (action === 'menu') {
|
||||
const options = target.getAttribute('data-playoptions') === 'false' ?
|
||||
{
|
||||
shuffle: false,
|
||||
instantMix: false,
|
||||
play: false,
|
||||
playAllFromHere: false,
|
||||
queue: false,
|
||||
queueAllFromHere: false
|
||||
} :
|
||||
{};
|
||||
|
||||
options.positionTo = target;
|
||||
|
||||
showContextMenu(card, options);
|
||||
} else if (action === 'playmenu') {
|
||||
showPlayMenu(card, target);
|
||||
} else if (action === 'edit') {
|
||||
getItem(target).then(itemToEdit => {
|
||||
editItem(itemToEdit, serverId);
|
||||
} else {
|
||||
import('./metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
|
||||
metadataEditor.show(item.Id, currentServerId).then(resolve, reject);
|
||||
});
|
||||
} else if (action === 'playtrailer') {
|
||||
getItem(target).then(playTrailer);
|
||||
} else if (action === 'addtoplaylist') {
|
||||
getItem(target).then(addToPlaylist);
|
||||
} else if (action === 'custom') {
|
||||
const customAction = target.getAttribute('data-customaction');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
card.dispatchEvent(new CustomEvent(`action-${customAction}`, {
|
||||
detail: {
|
||||
playlistItemId: card.getAttribute('data-playlistitemid')
|
||||
},
|
||||
cancelable: false,
|
||||
bubbles: true
|
||||
}));
|
||||
function onRecordCommand(serverId, id, type, timerId, seriesTimerId) {
|
||||
if (type === 'Program' || timerId || seriesTimerId) {
|
||||
const programId = type === 'Program' ? id : null;
|
||||
recordingHelper.toggleRecording(serverId, programId, timerId, seriesTimerId);
|
||||
}
|
||||
}
|
||||
|
||||
export function onClick(e) {
|
||||
const card = dom.parentWithClass(e.target, 'itemAction');
|
||||
|
||||
if (card) {
|
||||
let actionElement = card;
|
||||
let action = actionElement.getAttribute('data-action');
|
||||
|
||||
if (!action) {
|
||||
actionElement = dom.parentWithAttribute(actionElement, 'data-action');
|
||||
if (actionElement) {
|
||||
action = actionElement.getAttribute('data-action');
|
||||
}
|
||||
}
|
||||
|
||||
if (action) {
|
||||
executeAction(card, actionElement, action);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addToPlaylist(item) {
|
||||
import('./playlisteditor/playlisteditor').then(({default: playlistEditor}) => {
|
||||
new playlistEditor().show({
|
||||
items: [item.Id],
|
||||
serverId: item.ServerId
|
||||
function onCommand(e) {
|
||||
const cmd = e.detail.command;
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function playTrailer(item) {
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
|
||||
apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(trailers => {
|
||||
playbackManager.play({ items: trailers });
|
||||
});
|
||||
}
|
||||
|
||||
function editItem(item, serverId) {
|
||||
const apiClient = ServerConnections.getApiClient(serverId);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const currentServerId = apiClient.serverInfo().Id;
|
||||
|
||||
if (item.Type === 'Timer') {
|
||||
if (item.ProgramId) {
|
||||
import('./recordingcreator/recordingcreator').then(({default: recordingCreator}) => {
|
||||
recordingCreator.show(item.ProgramId, currentServerId).then(resolve, reject);
|
||||
});
|
||||
} else {
|
||||
import('./recordingcreator/recordingeditor').then(({default: recordingEditor}) => {
|
||||
recordingEditor.show(item.Id, currentServerId).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
import('./metadataEditor/metadataEditor').then(({default: metadataEditor}) => {
|
||||
metadataEditor.show(item.Id, currentServerId).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onRecordCommand(serverId, id, type, timerId, seriesTimerId) {
|
||||
if (type === 'Program' || timerId || seriesTimerId) {
|
||||
const programId = type === 'Program' ? id : null;
|
||||
recordingHelper.toggleRecording(serverId, programId, timerId, seriesTimerId);
|
||||
}
|
||||
}
|
||||
|
||||
export function onClick(e) {
|
||||
const card = dom.parentWithClass(e.target, 'itemAction');
|
||||
if (cmd === 'play' || cmd === 'resume' || cmd === 'record' || cmd === 'menu' || cmd === 'info') {
|
||||
const target = e.target;
|
||||
const card = dom.parentWithClass(target, 'itemAction') || dom.parentWithAttribute(target, 'data-id');
|
||||
|
||||
if (card) {
|
||||
let actionElement = card;
|
||||
let action = actionElement.getAttribute('data-action');
|
||||
|
||||
if (!action) {
|
||||
actionElement = dom.parentWithAttribute(actionElement, 'data-action');
|
||||
if (actionElement) {
|
||||
action = actionElement.getAttribute('data-action');
|
||||
}
|
||||
}
|
||||
|
||||
if (action) {
|
||||
executeAction(card, actionElement, action);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
executeAction(card, card, cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onCommand(e) {
|
||||
const cmd = e.detail.command;
|
||||
export function on(context, options) {
|
||||
options = options || {};
|
||||
|
||||
if (cmd === 'play' || cmd === 'resume' || cmd === 'record' || cmd === 'menu' || cmd === 'info') {
|
||||
const target = e.target;
|
||||
const card = dom.parentWithClass(target, 'itemAction') || dom.parentWithAttribute(target, 'data-id');
|
||||
|
||||
if (card) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
executeAction(card, card, cmd);
|
||||
}
|
||||
}
|
||||
if (options.click !== false) {
|
||||
context.addEventListener('click', onClick);
|
||||
}
|
||||
|
||||
export function on(context, options) {
|
||||
options = options || {};
|
||||
if (options.command !== false) {
|
||||
inputManager.on(context, onCommand);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.click !== false) {
|
||||
context.addEventListener('click', onClick);
|
||||
}
|
||||
export function off(context, options) {
|
||||
options = options || {};
|
||||
|
||||
if (options.command !== false) {
|
||||
inputManager.on(context, onCommand);
|
||||
}
|
||||
context.removeEventListener('click', onClick);
|
||||
|
||||
if (options.command !== false) {
|
||||
inputManager.off(context, onCommand);
|
||||
}
|
||||
}
|
||||
|
||||
export function getShortcutAttributesHtml(item, serverId) {
|
||||
let html = `data-id="${item.Id}" data-serverid="${serverId || item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-channelid="${item.ChannelId}" data-isfolder="${item.IsFolder}"`;
|
||||
|
||||
const collectionType = item.CollectionType;
|
||||
if (collectionType) {
|
||||
html += ` data-collectiontype="${collectionType}"`;
|
||||
}
|
||||
|
||||
export function off(context, options) {
|
||||
options = options || {};
|
||||
|
||||
context.removeEventListener('click', onClick);
|
||||
|
||||
if (options.command !== false) {
|
||||
inputManager.off(context, onCommand);
|
||||
}
|
||||
}
|
||||
|
||||
export function getShortcutAttributesHtml(item, serverId) {
|
||||
let html = `data-id="${item.Id}" data-serverid="${serverId || item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-channelid="${item.ChannelId}" data-isfolder="${item.IsFolder}"`;
|
||||
|
||||
const collectionType = item.CollectionType;
|
||||
if (collectionType) {
|
||||
html += ` data-collectiontype="${collectionType}"`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
return html;
|
||||
}
|
||||
|
||||
export default {
|
||||
on: on,
|
||||
|
||||
@@ -342,7 +342,7 @@ function showDownloadOptions(button, context, subtitleId) {
|
||||
}
|
||||
|
||||
function centerFocus(elem, horiz, on) {
|
||||
import('../../scripts/scrollHelper').then(({default: scrollHelper}) => {
|
||||
import('../../scripts/scrollHelper').then(({ default: scrollHelper }) => {
|
||||
const fn = on ? 'on' : 'off';
|
||||
scrollHelper.centerFocus[fn](elem, horiz);
|
||||
});
|
||||
@@ -353,7 +353,7 @@ function onOpenUploadMenu(e) {
|
||||
const selectLanguage = dialog.querySelector('#selectLanguage');
|
||||
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
|
||||
|
||||
import('../subtitleuploader/subtitleuploader').then(({default: subtitleUploader}) => {
|
||||
import('../subtitleuploader/subtitleuploader').then(({ default: subtitleUploader }) => {
|
||||
subtitleUploader.show({
|
||||
languages: {
|
||||
list: selectLanguage.innerHTML,
|
||||
|
||||
@@ -94,9 +94,9 @@ function init(instance) {
|
||||
|
||||
subtitleSyncSlider.getBubbleHtml = function (value) {
|
||||
const newOffset = getOffsetFromPercentage(value);
|
||||
return '<h1 class="sliderBubbleText">' +
|
||||
(newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's' +
|
||||
'</h1>';
|
||||
return '<h1 class="sliderBubbleText">'
|
||||
+ (newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's'
|
||||
+ '</h1>';
|
||||
};
|
||||
|
||||
subtitleSyncCloseButton.addEventListener('click', function () {
|
||||
|
||||
114
src/components/tabbedview/tabbedview.js
Normal file
114
src/components/tabbedview/tabbedview.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import { clearBackdrop } from '../backdrop/backdrop';
|
||||
import * as mainTabsManager from '../maintabsmanager';
|
||||
import layoutManager from '../layoutManager';
|
||||
import '../../elements/emby-tabs/emby-tabs';
|
||||
import LibraryMenu from '../../scripts/libraryMenu';
|
||||
|
||||
function onViewDestroy() {
|
||||
const tabControllers = this.tabControllers;
|
||||
|
||||
if (tabControllers) {
|
||||
tabControllers.forEach(function (t) {
|
||||
if (t.destroy) {
|
||||
t.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
this.tabControllers = null;
|
||||
}
|
||||
|
||||
this.view = null;
|
||||
this.params = null;
|
||||
this.currentTabController = null;
|
||||
this.initialTabIndex = null;
|
||||
}
|
||||
|
||||
class TabbedView {
|
||||
constructor(view, params) {
|
||||
this.tabControllers = [];
|
||||
this.view = view;
|
||||
this.params = params;
|
||||
|
||||
const self = this;
|
||||
|
||||
let currentTabIndex = parseInt(params.tab || this.getDefaultTabIndex(params.parentId), 10);
|
||||
this.initialTabIndex = currentTabIndex;
|
||||
|
||||
function validateTabLoad(index) {
|
||||
return self.validateTabLoad ? self.validateTabLoad(index) : Promise.resolve();
|
||||
}
|
||||
|
||||
function loadTab(index, previousIndex) {
|
||||
validateTabLoad(index).then(function () {
|
||||
self.getTabController(index).then(function (controller) {
|
||||
const refresh = !controller.refreshed;
|
||||
|
||||
controller.onResume({
|
||||
autoFocus: previousIndex == null && layoutManager.tv,
|
||||
refresh: refresh
|
||||
});
|
||||
|
||||
controller.refreshed = true;
|
||||
|
||||
currentTabIndex = index;
|
||||
self.currentTabController = controller;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTabContainers() {
|
||||
return view.querySelectorAll('.tabContent');
|
||||
}
|
||||
|
||||
function onTabChange(e) {
|
||||
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
|
||||
const previousIndex = e.detail.previousIndex;
|
||||
|
||||
const previousTabController = previousIndex == null ? null : self.tabControllers[previousIndex];
|
||||
if (previousTabController && previousTabController.onPause) {
|
||||
previousTabController.onPause();
|
||||
}
|
||||
|
||||
loadTab(newIndex, previousIndex);
|
||||
}
|
||||
|
||||
view.addEventListener('viewbeforehide', this.onPause.bind(this));
|
||||
|
||||
view.addEventListener('viewbeforeshow', function () {
|
||||
mainTabsManager.setTabs(view, currentTabIndex, self.getTabs, getTabContainers, null, onTabChange, false);
|
||||
});
|
||||
|
||||
view.addEventListener('viewshow', function (e) {
|
||||
self.onResume(e.detail);
|
||||
});
|
||||
|
||||
view.addEventListener('viewdestroy', onViewDestroy.bind(this));
|
||||
}
|
||||
|
||||
onResume() {
|
||||
this.setTitle();
|
||||
clearBackdrop();
|
||||
|
||||
const currentTabController = this.currentTabController;
|
||||
|
||||
if (!currentTabController) {
|
||||
mainTabsManager.selectedTabIndex(this.initialTabIndex);
|
||||
} else if (currentTabController && currentTabController.onResume) {
|
||||
currentTabController.onResume({});
|
||||
}
|
||||
}
|
||||
|
||||
onPause() {
|
||||
const currentTabController = this.currentTabController;
|
||||
|
||||
if (currentTabController && currentTabController.onPause) {
|
||||
currentTabController.onPause();
|
||||
}
|
||||
}
|
||||
|
||||
setTitle() {
|
||||
LibraryMenu.setTitle('');
|
||||
}
|
||||
}
|
||||
|
||||
export default TabbedView;
|
||||
@@ -48,7 +48,7 @@ function refreshTunerDevices(page, providerInfo, devices) {
|
||||
function onSelectPathClick(e) {
|
||||
const page = $(e.target).parents('.xmltvForm')[0];
|
||||
|
||||
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
|
||||
@@ -10,199 +10,197 @@ import './upnextdialog.scss';
|
||||
import '../../elements/emby-button/emby-button';
|
||||
import '../../styles/flexstyles.scss';
|
||||
|
||||
/* eslint-disable indent */
|
||||
const transitionEndEventName = dom.whichTransitionEvent();
|
||||
|
||||
const transitionEndEventName = dom.whichTransitionEvent();
|
||||
function getHtml() {
|
||||
let html = '';
|
||||
|
||||
function getHtml() {
|
||||
let html = '';
|
||||
html += '<div class="flex flex-direction-column flex-grow">';
|
||||
|
||||
html += '<div class="flex flex-direction-column flex-grow">';
|
||||
html += '<h2 class="upNextDialog-nextVideoText" style="margin:.25em 0;"> </h2>';
|
||||
|
||||
html += '<h2 class="upNextDialog-nextVideoText" style="margin:.25em 0;"> </h2>';
|
||||
html += '<h3 class="upNextDialog-title" style="margin:.25em 0 .5em;"></h3>';
|
||||
|
||||
html += '<h3 class="upNextDialog-title" style="margin:.25em 0 .5em;"></h3>';
|
||||
html += '<div class="flex flex-direction-row upNextDialog-mediainfo">';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="flex flex-direction-row upNextDialog-mediainfo">';
|
||||
html += '</div>';
|
||||
html += '<div class="flex flex-direction-row upNextDialog-buttons" style="margin-top:1em;">';
|
||||
|
||||
html += '<div class="flex flex-direction-row upNextDialog-buttons" style="margin-top:1em;">';
|
||||
html += '<button type="button" is="emby-button" class="raised raised-mini btnStartNow upNextDialog-button">';
|
||||
html += globalize.translate('HeaderStartNow');
|
||||
html += '</button>';
|
||||
|
||||
html += '<button type="button" is="emby-button" class="raised raised-mini btnStartNow upNextDialog-button">';
|
||||
html += globalize.translate('HeaderStartNow');
|
||||
html += '</button>';
|
||||
html += '<button type="button" is="emby-button" class="raised raised-mini btnHide upNextDialog-button">';
|
||||
html += globalize.translate('Hide');
|
||||
html += '</button>';
|
||||
|
||||
html += '<button type="button" is="emby-button" class="raised raised-mini btnHide upNextDialog-button">';
|
||||
html += globalize.translate('Hide');
|
||||
html += '</button>';
|
||||
// buttons
|
||||
html += '</div>';
|
||||
|
||||
// buttons
|
||||
html += '</div>';
|
||||
// main
|
||||
html += '</div>';
|
||||
|
||||
// main
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
return html;
|
||||
function setNextVideoText() {
|
||||
const instance = this;
|
||||
|
||||
const elem = instance.options.parent;
|
||||
|
||||
const secondsRemaining = Math.max(Math.round(getTimeRemainingMs(instance) / 1000), 0);
|
||||
|
||||
console.debug('up next seconds remaining: ' + secondsRemaining);
|
||||
|
||||
const timeText = '<span class="upNextDialog-countdownText">' + globalize.translate('HeaderSecondsValue', secondsRemaining) + '</span>';
|
||||
|
||||
const nextVideoText = instance.itemType === 'Episode' ?
|
||||
globalize.translate('HeaderNextEpisodePlayingInValue', timeText) :
|
||||
globalize.translate('HeaderNextVideoPlayingInValue', timeText);
|
||||
|
||||
elem.querySelector('.upNextDialog-nextVideoText').innerHTML = nextVideoText;
|
||||
}
|
||||
|
||||
function fillItem(item) {
|
||||
const instance = this;
|
||||
|
||||
const elem = instance.options.parent;
|
||||
|
||||
elem.querySelector('.upNextDialog-mediainfo').innerHTML = mediaInfo.getPrimaryMediaInfoHtml(item, {
|
||||
criticRating: true,
|
||||
originalAirDate: false,
|
||||
starRating: true,
|
||||
subtitles: false
|
||||
});
|
||||
|
||||
let title = itemHelper.getDisplayName(item);
|
||||
if (item.SeriesName) {
|
||||
title = item.SeriesName + ' - ' + title;
|
||||
}
|
||||
|
||||
function setNextVideoText() {
|
||||
const instance = this;
|
||||
elem.querySelector('.upNextDialog-title').innerText = title || '';
|
||||
|
||||
const elem = instance.options.parent;
|
||||
instance.itemType = item.Type;
|
||||
|
||||
const secondsRemaining = Math.max(Math.round(getTimeRemainingMs(instance) / 1000), 0);
|
||||
instance.show();
|
||||
}
|
||||
|
||||
console.debug('up next seconds remaining: ' + secondsRemaining);
|
||||
function clearCountdownTextTimeout(instance) {
|
||||
if (instance._countdownTextTimeout) {
|
||||
clearInterval(instance._countdownTextTimeout);
|
||||
instance._countdownTextTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
const timeText = '<span class="upNextDialog-countdownText">' + globalize.translate('HeaderSecondsValue', secondsRemaining) + '</span>';
|
||||
async function onStartNowClick() {
|
||||
const options = this.options;
|
||||
|
||||
const nextVideoText = instance.itemType === 'Episode' ?
|
||||
globalize.translate('HeaderNextEpisodePlayingInValue', timeText) :
|
||||
globalize.translate('HeaderNextVideoPlayingInValue', timeText);
|
||||
if (options) {
|
||||
const player = options.player;
|
||||
|
||||
elem.querySelector('.upNextDialog-nextVideoText').innerHTML = nextVideoText;
|
||||
await this.hide();
|
||||
|
||||
playbackManager.nextTrack(player);
|
||||
}
|
||||
}
|
||||
|
||||
function init(instance, options) {
|
||||
options.parent.innerHTML = getHtml();
|
||||
|
||||
options.parent.classList.add('hide');
|
||||
options.parent.classList.add('upNextDialog');
|
||||
options.parent.classList.add('upNextDialog-hidden');
|
||||
|
||||
fillItem.call(instance, options.nextItem);
|
||||
|
||||
options.parent.querySelector('.btnHide').addEventListener('click', instance.hide.bind(instance));
|
||||
options.parent.querySelector('.btnStartNow').addEventListener('click', onStartNowClick.bind(instance));
|
||||
}
|
||||
|
||||
function clearHideAnimationEventListeners(instance, elem) {
|
||||
const fn = instance._onHideAnimationComplete;
|
||||
|
||||
if (fn) {
|
||||
dom.removeEventListener(elem, transitionEndEventName, fn, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onHideAnimationComplete(e) {
|
||||
const instance = this;
|
||||
const elem = e.target;
|
||||
|
||||
elem.classList.add('hide');
|
||||
|
||||
clearHideAnimationEventListeners(instance, elem);
|
||||
Events.trigger(instance, 'hide');
|
||||
}
|
||||
|
||||
async function hideComingUpNext() {
|
||||
const instance = this;
|
||||
clearCountdownTextTimeout(this);
|
||||
|
||||
if (!instance.options) {
|
||||
return;
|
||||
}
|
||||
|
||||
function fillItem(item) {
|
||||
const instance = this;
|
||||
const elem = instance.options.parent;
|
||||
|
||||
const elem = instance.options.parent;
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
elem.querySelector('.upNextDialog-mediainfo').innerHTML = mediaInfo.getPrimaryMediaInfoHtml(item, {
|
||||
criticRating: false,
|
||||
originalAirDate: false,
|
||||
starRating: false,
|
||||
subtitles: false
|
||||
clearHideAnimationEventListeners(this, elem);
|
||||
|
||||
if (elem.classList.contains('upNextDialog-hidden')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fn = onHideAnimationComplete.bind(instance);
|
||||
instance._onHideAnimationComplete = fn;
|
||||
|
||||
const transitionEvent = await new Promise((resolve) => {
|
||||
dom.addEventListener(elem, transitionEndEventName, resolve, {
|
||||
once: true
|
||||
});
|
||||
|
||||
let title = itemHelper.getDisplayName(item);
|
||||
if (item.SeriesName) {
|
||||
title = item.SeriesName + ' - ' + title;
|
||||
}
|
||||
// trigger a reflow to force it to animate again
|
||||
void elem.offsetWidth;
|
||||
|
||||
elem.querySelector('.upNextDialog-title').innerText = title || '';
|
||||
elem.classList.add('upNextDialog-hidden');
|
||||
});
|
||||
|
||||
instance.itemType = item.Type;
|
||||
instance._onHideAnimationComplete(transitionEvent);
|
||||
}
|
||||
|
||||
instance.show();
|
||||
}
|
||||
function getTimeRemainingMs(instance) {
|
||||
const options = instance.options;
|
||||
if (options) {
|
||||
const runtimeTicks = playbackManager.duration(options.player);
|
||||
|
||||
function clearCountdownTextTimeout(instance) {
|
||||
if (instance._countdownTextTimeout) {
|
||||
clearInterval(instance._countdownTextTimeout);
|
||||
instance._countdownTextTimeout = null;
|
||||
if (runtimeTicks) {
|
||||
const timeRemainingTicks = runtimeTicks - playbackManager.currentTime(options.player) * 10000;
|
||||
|
||||
return Math.round(timeRemainingTicks / 10000);
|
||||
}
|
||||
}
|
||||
|
||||
async function onStartNowClick() {
|
||||
const options = this.options;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
const player = options.player;
|
||||
function startComingUpNextHideTimer(instance) {
|
||||
const timeRemainingMs = getTimeRemainingMs(instance);
|
||||
|
||||
await this.hide();
|
||||
|
||||
playbackManager.nextTrack(player);
|
||||
}
|
||||
if (timeRemainingMs <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
function init(instance, options) {
|
||||
options.parent.innerHTML = getHtml();
|
||||
setNextVideoText.call(instance);
|
||||
clearCountdownTextTimeout(instance);
|
||||
|
||||
options.parent.classList.add('hide');
|
||||
options.parent.classList.add('upNextDialog');
|
||||
options.parent.classList.add('upNextDialog-hidden');
|
||||
|
||||
fillItem.call(instance, options.nextItem);
|
||||
|
||||
options.parent.querySelector('.btnHide').addEventListener('click', instance.hide.bind(instance));
|
||||
options.parent.querySelector('.btnStartNow').addEventListener('click', onStartNowClick.bind(instance));
|
||||
}
|
||||
|
||||
function clearHideAnimationEventListeners(instance, elem) {
|
||||
const fn = instance._onHideAnimationComplete;
|
||||
|
||||
if (fn) {
|
||||
dom.removeEventListener(elem, transitionEndEventName, fn, {
|
||||
once: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onHideAnimationComplete(e) {
|
||||
const instance = this;
|
||||
const elem = e.target;
|
||||
|
||||
elem.classList.add('hide');
|
||||
|
||||
clearHideAnimationEventListeners(instance, elem);
|
||||
Events.trigger(instance, 'hide');
|
||||
}
|
||||
|
||||
async function hideComingUpNext() {
|
||||
const instance = this;
|
||||
clearCountdownTextTimeout(this);
|
||||
|
||||
if (!instance.options) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elem = instance.options.parent;
|
||||
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearHideAnimationEventListeners(this, elem);
|
||||
|
||||
if (elem.classList.contains('upNextDialog-hidden')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fn = onHideAnimationComplete.bind(instance);
|
||||
instance._onHideAnimationComplete = fn;
|
||||
|
||||
const transitionEvent = await new Promise((resolve) => {
|
||||
dom.addEventListener(elem, transitionEndEventName, resolve, {
|
||||
once: true
|
||||
});
|
||||
|
||||
// trigger a reflow to force it to animate again
|
||||
void elem.offsetWidth;
|
||||
|
||||
elem.classList.add('upNextDialog-hidden');
|
||||
});
|
||||
|
||||
instance._onHideAnimationComplete(transitionEvent);
|
||||
}
|
||||
|
||||
function getTimeRemainingMs(instance) {
|
||||
const options = instance.options;
|
||||
if (options) {
|
||||
const runtimeTicks = playbackManager.duration(options.player);
|
||||
|
||||
if (runtimeTicks) {
|
||||
const timeRemainingTicks = runtimeTicks - playbackManager.currentTime(options.player) * 10000;
|
||||
|
||||
return Math.round(timeRemainingTicks / 10000);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function startComingUpNextHideTimer(instance) {
|
||||
const timeRemainingMs = getTimeRemainingMs(instance);
|
||||
|
||||
if (timeRemainingMs <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setNextVideoText.call(instance);
|
||||
clearCountdownTextTimeout(instance);
|
||||
|
||||
instance._countdownTextTimeout = setInterval(setNextVideoText.bind(instance), 400);
|
||||
}
|
||||
instance._countdownTextTimeout = setInterval(setNextVideoText.bind(instance), 400);
|
||||
}
|
||||
|
||||
class UpNextDialog {
|
||||
constructor(options) {
|
||||
@@ -243,4 +241,3 @@ class UpNextDialog {
|
||||
|
||||
export default UpNextDialog;
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -2,243 +2,252 @@ import { importModule } from '@uupaa/dynamic-import-polyfill';
|
||||
import './viewManager/viewContainer.scss';
|
||||
import Dashboard from '../utils/dashboard';
|
||||
|
||||
/* eslint-disable indent */
|
||||
const getMainAnimatedPages = () => {
|
||||
if (!mainAnimatedPages) {
|
||||
mainAnimatedPages = document.querySelector('.mainAnimatedPages');
|
||||
}
|
||||
|
||||
function setControllerClass(view, options) {
|
||||
if (options.controllerFactory) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let controllerUrl = view.getAttribute('data-controller');
|
||||
|
||||
if (controllerUrl) {
|
||||
if (controllerUrl.indexOf('__plugin/') === 0) {
|
||||
controllerUrl = controllerUrl.substring('__plugin/'.length);
|
||||
}
|
||||
|
||||
controllerUrl = Dashboard.getPluginUrl(controllerUrl);
|
||||
const apiUrl = ApiClient.getUrl('/web/' + controllerUrl);
|
||||
return importModule(apiUrl).then((ControllerFactory) => {
|
||||
options.controllerFactory = ControllerFactory;
|
||||
});
|
||||
}
|
||||
return mainAnimatedPages;
|
||||
};
|
||||
|
||||
function setControllerClass(view, options) {
|
||||
if (options.controllerFactory) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export function loadView(options) {
|
||||
if (!options.cancel) {
|
||||
let controllerUrl = view.getAttribute('data-controller');
|
||||
|
||||
if (controllerUrl) {
|
||||
if (controllerUrl.indexOf('__plugin/') === 0) {
|
||||
controllerUrl = controllerUrl.substring('__plugin/'.length);
|
||||
}
|
||||
|
||||
controllerUrl = Dashboard.getPluginUrl(controllerUrl);
|
||||
const apiUrl = ApiClient.getUrl('/web/' + controllerUrl);
|
||||
return importModule(apiUrl).then((ControllerFactory) => {
|
||||
options.controllerFactory = ControllerFactory;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
export function loadView(options) {
|
||||
if (!options.cancel) {
|
||||
const selected = selectedPageIndex;
|
||||
const previousAnimatable = selected === -1 ? null : allPages[selected];
|
||||
let pageIndex = selected + 1;
|
||||
|
||||
if (pageIndex >= pageContainerCount) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
|
||||
const isPluginpage = options.url.includes('configurationpage');
|
||||
const newViewInfo = normalizeNewView(options, isPluginpage);
|
||||
const newView = newViewInfo.elem;
|
||||
|
||||
const currentPage = allPages[pageIndex];
|
||||
|
||||
if (currentPage) {
|
||||
triggerDestroy(currentPage);
|
||||
}
|
||||
|
||||
let view = newView;
|
||||
|
||||
if (typeof view == 'string') {
|
||||
view = document.createElement('div');
|
||||
view.innerHTML = newView;
|
||||
}
|
||||
|
||||
view.classList.add('mainAnimatedPage');
|
||||
|
||||
if (!getMainAnimatedPages()) {
|
||||
console.warn('[viewContainer] main animated pages element is not present');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPage) {
|
||||
if (newViewInfo.hasScript && window.$) {
|
||||
mainAnimatedPages.removeChild(currentPage);
|
||||
view = $(view).appendTo(mainAnimatedPages)[0];
|
||||
} else {
|
||||
mainAnimatedPages.replaceChild(view, currentPage);
|
||||
}
|
||||
} else {
|
||||
if (newViewInfo.hasScript && window.$) {
|
||||
view = $(view).appendTo(mainAnimatedPages)[0];
|
||||
} else {
|
||||
mainAnimatedPages.appendChild(view);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.type) {
|
||||
view.setAttribute('data-type', options.type);
|
||||
}
|
||||
|
||||
const properties = [];
|
||||
|
||||
if (options.fullscreen) {
|
||||
properties.push('fullscreen');
|
||||
}
|
||||
|
||||
if (properties.length) {
|
||||
view.setAttribute('data-properties', properties.join(','));
|
||||
}
|
||||
|
||||
allPages[pageIndex] = view;
|
||||
|
||||
return setControllerClass(view, options)
|
||||
// Timeout for polyfilled CustomElements (webOS 1.2)
|
||||
.then(() => new Promise((resolve) => setTimeout(resolve, 0)))
|
||||
.then(() => {
|
||||
if (onBeforeChange) {
|
||||
onBeforeChange(view, false, options);
|
||||
}
|
||||
|
||||
beforeAnimate(allPages, pageIndex, selected);
|
||||
selectedPageIndex = pageIndex;
|
||||
currentUrls[pageIndex] = options.url;
|
||||
|
||||
if (!options.cancel && previousAnimatable) {
|
||||
afterAnimate(allPages, pageIndex);
|
||||
}
|
||||
|
||||
if (window.$) {
|
||||
$.mobile = $.mobile || {};
|
||||
$.mobile.activePage = view;
|
||||
}
|
||||
|
||||
return view;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseHtml(html, hasScript) {
|
||||
if (hasScript) {
|
||||
html = html
|
||||
.replaceAll('\x3c!--<script', '<script')
|
||||
.replaceAll('</script>--\x3e', '</script>');
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html;
|
||||
return wrapper.querySelector('div[data-role="page"]');
|
||||
}
|
||||
|
||||
function normalizeNewView(options, isPluginpage) {
|
||||
const viewHtml = options.view;
|
||||
|
||||
if (viewHtml.indexOf('data-role="page"') === -1) {
|
||||
return viewHtml;
|
||||
}
|
||||
|
||||
let hasScript = viewHtml.indexOf('<script') !== -1;
|
||||
const elem = parseHtml(viewHtml, hasScript);
|
||||
|
||||
if (hasScript) {
|
||||
hasScript = elem.querySelector('script') != null;
|
||||
}
|
||||
|
||||
let hasjQuery = false;
|
||||
let hasjQuerySelect = false;
|
||||
let hasjQueryChecked = false;
|
||||
|
||||
if (isPluginpage) {
|
||||
hasjQuery = viewHtml.indexOf('jQuery') != -1 || viewHtml.indexOf('$(') != -1 || viewHtml.indexOf('$.') != -1;
|
||||
hasjQueryChecked = viewHtml.indexOf('.checked(') != -1;
|
||||
hasjQuerySelect = viewHtml.indexOf('.selectmenu(') != -1;
|
||||
}
|
||||
|
||||
return {
|
||||
elem: elem,
|
||||
hasScript: hasScript,
|
||||
hasjQuerySelect: hasjQuerySelect,
|
||||
hasjQueryChecked: hasjQueryChecked,
|
||||
hasjQuery: hasjQuery
|
||||
};
|
||||
}
|
||||
|
||||
function beforeAnimate(allPages, newPageIndex, oldPageIndex) {
|
||||
for (let index = 0, length = allPages.length; index < length; index++) {
|
||||
if (newPageIndex !== index && oldPageIndex !== index) {
|
||||
allPages[index].classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function afterAnimate(allPages, newPageIndex) {
|
||||
for (let index = 0, length = allPages.length; index < length; index++) {
|
||||
if (newPageIndex !== index) {
|
||||
allPages[index].classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setOnBeforeChange(fn) {
|
||||
onBeforeChange = fn;
|
||||
}
|
||||
|
||||
export function tryRestoreView(options) {
|
||||
const url = options.url;
|
||||
const index = currentUrls.indexOf(url);
|
||||
|
||||
if (index !== -1) {
|
||||
const animatable = allPages[index];
|
||||
const view = animatable;
|
||||
|
||||
if (view) {
|
||||
if (options.cancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = selectedPageIndex;
|
||||
const previousAnimatable = selected === -1 ? null : allPages[selected];
|
||||
let pageIndex = selected + 1;
|
||||
|
||||
if (pageIndex >= pageContainerCount) {
|
||||
pageIndex = 0;
|
||||
}
|
||||
|
||||
const isPluginpage = options.url.includes('configurationpage');
|
||||
const newViewInfo = normalizeNewView(options, isPluginpage);
|
||||
const newView = newViewInfo.elem;
|
||||
|
||||
const currentPage = allPages[pageIndex];
|
||||
|
||||
if (currentPage) {
|
||||
triggerDestroy(currentPage);
|
||||
}
|
||||
|
||||
let view = newView;
|
||||
|
||||
if (typeof view == 'string') {
|
||||
view = document.createElement('div');
|
||||
view.innerHTML = newView;
|
||||
}
|
||||
|
||||
view.classList.add('mainAnimatedPage');
|
||||
|
||||
if (currentPage) {
|
||||
if (newViewInfo.hasScript && window.$) {
|
||||
mainAnimatedPages.removeChild(currentPage);
|
||||
view = $(view).appendTo(mainAnimatedPages)[0];
|
||||
} else {
|
||||
mainAnimatedPages.replaceChild(view, currentPage);
|
||||
}
|
||||
} else {
|
||||
if (newViewInfo.hasScript && window.$) {
|
||||
view = $(view).appendTo(mainAnimatedPages)[0];
|
||||
} else {
|
||||
mainAnimatedPages.appendChild(view);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.type) {
|
||||
view.setAttribute('data-type', options.type);
|
||||
}
|
||||
|
||||
const properties = [];
|
||||
|
||||
if (options.fullscreen) {
|
||||
properties.push('fullscreen');
|
||||
}
|
||||
|
||||
if (properties.length) {
|
||||
view.setAttribute('data-properties', properties.join(','));
|
||||
}
|
||||
|
||||
allPages[pageIndex] = view;
|
||||
|
||||
return setControllerClass(view, options)
|
||||
// Timeout for polyfilled CustomElements (webOS 1.2)
|
||||
.then(() => new Promise((resolve) => setTimeout(resolve, 0)))
|
||||
.then(() => {
|
||||
if (onBeforeChange) {
|
||||
onBeforeChange(view, false, options);
|
||||
}
|
||||
|
||||
beforeAnimate(allPages, pageIndex, selected);
|
||||
selectedPageIndex = pageIndex;
|
||||
currentUrls[pageIndex] = options.url;
|
||||
|
||||
if (!options.cancel && previousAnimatable) {
|
||||
afterAnimate(allPages, pageIndex);
|
||||
}
|
||||
|
||||
if (window.$) {
|
||||
$.mobile = $.mobile || {};
|
||||
$.mobile.activePage = view;
|
||||
}
|
||||
|
||||
return view;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function parseHtml(html, hasScript) {
|
||||
if (hasScript) {
|
||||
html = html
|
||||
.replaceAll('\x3c!--<script', '<script')
|
||||
.replaceAll('</script>--\x3e', '</script>');
|
||||
}
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html;
|
||||
return wrapper.querySelector('div[data-role="page"]');
|
||||
}
|
||||
|
||||
function normalizeNewView(options, isPluginpage) {
|
||||
const viewHtml = options.view;
|
||||
|
||||
if (viewHtml.indexOf('data-role="page"') === -1) {
|
||||
return viewHtml;
|
||||
}
|
||||
|
||||
let hasScript = viewHtml.indexOf('<script') !== -1;
|
||||
const elem = parseHtml(viewHtml, hasScript);
|
||||
|
||||
if (hasScript) {
|
||||
hasScript = elem.querySelector('script') != null;
|
||||
}
|
||||
|
||||
let hasjQuery = false;
|
||||
let hasjQuerySelect = false;
|
||||
let hasjQueryChecked = false;
|
||||
|
||||
if (isPluginpage) {
|
||||
hasjQuery = viewHtml.indexOf('jQuery') != -1 || viewHtml.indexOf('$(') != -1 || viewHtml.indexOf('$.') != -1;
|
||||
hasjQueryChecked = viewHtml.indexOf('.checked(') != -1;
|
||||
hasjQuerySelect = viewHtml.indexOf('.selectmenu(') != -1;
|
||||
}
|
||||
|
||||
return {
|
||||
elem: elem,
|
||||
hasScript: hasScript,
|
||||
hasjQuerySelect: hasjQuerySelect,
|
||||
hasjQueryChecked: hasjQueryChecked,
|
||||
hasjQuery: hasjQuery
|
||||
};
|
||||
}
|
||||
|
||||
function beforeAnimate(allPages, newPageIndex, oldPageIndex) {
|
||||
for (let index = 0, length = allPages.length; index < length; index++) {
|
||||
if (newPageIndex !== index && oldPageIndex !== index) {
|
||||
allPages[index].classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function afterAnimate(allPages, newPageIndex) {
|
||||
for (let index = 0, length = allPages.length; index < length; index++) {
|
||||
if (newPageIndex !== index) {
|
||||
allPages[index].classList.add('hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setOnBeforeChange(fn) {
|
||||
onBeforeChange = fn;
|
||||
}
|
||||
|
||||
export function tryRestoreView(options) {
|
||||
const url = options.url;
|
||||
const index = currentUrls.indexOf(url);
|
||||
|
||||
if (index !== -1) {
|
||||
const animatable = allPages[index];
|
||||
const view = animatable;
|
||||
|
||||
if (view) {
|
||||
if (options.cancel) {
|
||||
return;
|
||||
return setControllerClass(view, options).then(() => {
|
||||
if (onBeforeChange) {
|
||||
onBeforeChange(view, true, options);
|
||||
}
|
||||
|
||||
const selected = selectedPageIndex;
|
||||
const previousAnimatable = selected === -1 ? null : allPages[selected];
|
||||
return setControllerClass(view, options).then(() => {
|
||||
if (onBeforeChange) {
|
||||
onBeforeChange(view, true, options);
|
||||
}
|
||||
beforeAnimate(allPages, index, selected);
|
||||
animatable.classList.remove('hide');
|
||||
selectedPageIndex = index;
|
||||
|
||||
beforeAnimate(allPages, index, selected);
|
||||
animatable.classList.remove('hide');
|
||||
selectedPageIndex = index;
|
||||
if (!options.cancel && previousAnimatable) {
|
||||
afterAnimate(allPages, index);
|
||||
}
|
||||
|
||||
if (!options.cancel && previousAnimatable) {
|
||||
afterAnimate(allPages, index);
|
||||
}
|
||||
if (window.$) {
|
||||
$.mobile = $.mobile || {};
|
||||
$.mobile.activePage = view;
|
||||
}
|
||||
|
||||
if (window.$) {
|
||||
$.mobile = $.mobile || {};
|
||||
$.mobile.activePage = view;
|
||||
}
|
||||
|
||||
return view;
|
||||
});
|
||||
}
|
||||
return view;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
function triggerDestroy(view) {
|
||||
view.dispatchEvent(new CustomEvent('viewdestroy', {}));
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
allPages = [];
|
||||
currentUrls = [];
|
||||
mainAnimatedPages.innerHTML = '';
|
||||
selectedPageIndex = -1;
|
||||
}
|
||||
function triggerDestroy(view) {
|
||||
view.dispatchEvent(new CustomEvent('viewdestroy', {}));
|
||||
}
|
||||
|
||||
let onBeforeChange;
|
||||
const mainAnimatedPages = document.querySelector('.mainAnimatedPages');
|
||||
let allPages = [];
|
||||
let currentUrls = [];
|
||||
const pageContainerCount = 3;
|
||||
let selectedPageIndex = -1;
|
||||
reset();
|
||||
mainAnimatedPages.classList.remove('hide');
|
||||
export function reset() {
|
||||
allPages = [];
|
||||
currentUrls = [];
|
||||
if (mainAnimatedPages) mainAnimatedPages.innerHTML = '';
|
||||
selectedPageIndex = -1;
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
let onBeforeChange;
|
||||
let mainAnimatedPages;
|
||||
let allPages = [];
|
||||
let currentUrls = [];
|
||||
const pageContainerCount = 3;
|
||||
let selectedPageIndex = -1;
|
||||
reset();
|
||||
getMainAnimatedPages()?.classList.remove('hide');
|
||||
|
||||
export default {
|
||||
loadView: loadView,
|
||||
|
||||
@@ -97,7 +97,7 @@ function dispatchViewEvent(view, eventInfo, eventName, isCancellable) {
|
||||
return eventResult;
|
||||
}
|
||||
|
||||
function getViewEventDetail(view, {state, url, options = {}}, isRestored) {
|
||||
function getViewEventDetail(view, { state, url, options = {} }, isRestored) {
|
||||
const index = url.indexOf('?');
|
||||
// eslint-disable-next-line compat/compat
|
||||
const searchParams = new URLSearchParams(url.substring(index + 1));
|
||||
|
||||
@@ -6,85 +6,82 @@ import '../../elements/emby-button/emby-button';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import { pageIdOn } from '../../utils/dashboard';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function revoke(page, key) {
|
||||
confirm(globalize.translate('MessageConfirmRevokeApiKey'), globalize.translate('HeaderConfirmRevokeApiKey')).then(function () {
|
||||
loading.show();
|
||||
ApiClient.ajax({
|
||||
type: 'DELETE',
|
||||
url: ApiClient.getUrl('Auth/Keys/' + key)
|
||||
}).then(function () {
|
||||
loadData(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function revoke(page, key) {
|
||||
confirm(globalize.translate('MessageConfirmRevokeApiKey'), globalize.translate('HeaderConfirmRevokeApiKey')).then(function () {
|
||||
loading.show();
|
||||
function renderKeys(page, keys) {
|
||||
const rows = keys.map(function (item) {
|
||||
let html = '';
|
||||
html += '<tr class="detailTableBodyRow detailTableBodyRow-shaded">';
|
||||
html += '<td class="detailTableBodyCell">';
|
||||
html += '<button type="button" is="emby-button" data-token="' + item.AccessToken + '" class="raised raised-mini btnRevoke" data-mini="true" title="' + globalize.translate('ButtonRevoke') + '" style="margin:0;">' + globalize.translate('ButtonRevoke') + '</button>';
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
html += item.AccessToken;
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
html += item.AppName || '';
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
const date = datetime.parseISO8601Date(item.DateCreated, true);
|
||||
html += datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date);
|
||||
html += '</td>';
|
||||
html += '</tr>';
|
||||
return html;
|
||||
}).join('');
|
||||
page.querySelector('.resultBody').innerHTML = rows;
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function loadData(page) {
|
||||
loading.show();
|
||||
ApiClient.getJSON(ApiClient.getUrl('Auth/Keys')).then(function (result) {
|
||||
renderKeys(page, result.Items);
|
||||
});
|
||||
}
|
||||
|
||||
function showNewKeyPrompt(page) {
|
||||
import('../../components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
title: globalize.translate('HeaderNewApiKey'),
|
||||
label: globalize.translate('LabelAppName'),
|
||||
description: globalize.translate('LabelAppNameExample')
|
||||
}).then(function (value) {
|
||||
ApiClient.ajax({
|
||||
type: 'DELETE',
|
||||
url: ApiClient.getUrl('Auth/Keys/' + key)
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl('Auth/Keys', {
|
||||
App: value
|
||||
})
|
||||
}).then(function () {
|
||||
loadData(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderKeys(page, keys) {
|
||||
const rows = keys.map(function (item) {
|
||||
let html = '';
|
||||
html += '<tr class="detailTableBodyRow detailTableBodyRow-shaded">';
|
||||
html += '<td class="detailTableBodyCell">';
|
||||
html += '<button type="button" is="emby-button" data-token="' + item.AccessToken + '" class="raised raised-mini btnRevoke" data-mini="true" title="' + globalize.translate('ButtonRevoke') + '" style="margin:0;">' + globalize.translate('ButtonRevoke') + '</button>';
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
html += item.AccessToken;
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
html += item.AppName || '';
|
||||
html += '</td>';
|
||||
html += '<td class="detailTableBodyCell" style="vertical-align:middle;">';
|
||||
const date = datetime.parseISO8601Date(item.DateCreated, true);
|
||||
html += datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date);
|
||||
html += '</td>';
|
||||
html += '</tr>';
|
||||
return html;
|
||||
}).join('');
|
||||
page.querySelector('.resultBody').innerHTML = rows;
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function loadData(page) {
|
||||
loading.show();
|
||||
ApiClient.getJSON(ApiClient.getUrl('Auth/Keys')).then(function (result) {
|
||||
renderKeys(page, result.Items);
|
||||
});
|
||||
}
|
||||
|
||||
function showNewKeyPrompt(page) {
|
||||
import('../../components/prompt/prompt').then(({default: prompt}) => {
|
||||
prompt({
|
||||
title: globalize.translate('HeaderNewApiKey'),
|
||||
label: globalize.translate('LabelAppName'),
|
||||
description: globalize.translate('LabelAppNameExample')
|
||||
}).then(function (value) {
|
||||
ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl('Auth/Keys', {
|
||||
App: value
|
||||
})
|
||||
}).then(function () {
|
||||
loadData(page);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pageIdOn('pageinit', 'apiKeysPage', function () {
|
||||
const page = this;
|
||||
page.querySelector('.btnNewKey').addEventListener('click', function () {
|
||||
showNewKeyPrompt(page);
|
||||
});
|
||||
page.querySelector('.tblApiKeys').addEventListener('click', function (e) {
|
||||
const btnRevoke = dom.parentWithClass(e.target, 'btnRevoke');
|
||||
|
||||
if (btnRevoke) {
|
||||
revoke(page, btnRevoke.getAttribute('data-token'));
|
||||
}
|
||||
});
|
||||
});
|
||||
pageIdOn('pagebeforeshow', 'apiKeysPage', function () {
|
||||
loadData(this);
|
||||
});
|
||||
}
|
||||
|
||||
pageIdOn('pageinit', 'apiKeysPage', function () {
|
||||
const page = this;
|
||||
page.querySelector('.btnNewKey').addEventListener('click', function () {
|
||||
showNewKeyPrompt(page);
|
||||
});
|
||||
page.querySelector('.tblApiKeys').addEventListener('click', function (e) {
|
||||
const btnRevoke = dom.parentWithClass(e.target, 'btnRevoke');
|
||||
|
||||
if (btnRevoke) {
|
||||
revoke(page, btnRevoke.getAttribute('data-token'));
|
||||
}
|
||||
});
|
||||
});
|
||||
pageIdOn('pagebeforeshow', 'apiKeysPage', function () {
|
||||
loadData(this);
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,53 +5,50 @@ import '../../../elements/emby-button/emby-button';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import { getParameterByName } from '../../../utils/url.ts';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function load(page, device, deviceOptions) {
|
||||
page.querySelector('#txtCustomName', page).value = deviceOptions.CustomName || '';
|
||||
page.querySelector('.reportedName', page).innerText = device.Name || '';
|
||||
}
|
||||
|
||||
function load(page, device, deviceOptions) {
|
||||
page.querySelector('#txtCustomName', page).value = deviceOptions.CustomName || '';
|
||||
page.querySelector('.reportedName', page).innerText = device.Name || '';
|
||||
}
|
||||
function loadData() {
|
||||
const page = this;
|
||||
loading.show();
|
||||
const id = getParameterByName('id');
|
||||
const promise1 = ApiClient.getJSON(ApiClient.getUrl('Devices/Info', {
|
||||
Id: id
|
||||
}));
|
||||
const promise2 = ApiClient.getJSON(ApiClient.getUrl('Devices/Options', {
|
||||
Id: id
|
||||
}));
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
load(page, responses[0], responses[1]);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
const page = this;
|
||||
loading.show();
|
||||
const id = getParameterByName('id');
|
||||
const promise1 = ApiClient.getJSON(ApiClient.getUrl('Devices/Info', {
|
||||
function save(page) {
|
||||
const id = getParameterByName('id');
|
||||
ApiClient.ajax({
|
||||
url: ApiClient.getUrl('Devices/Options', {
|
||||
Id: id
|
||||
}));
|
||||
const promise2 = ApiClient.getJSON(ApiClient.getUrl('Devices/Options', {
|
||||
Id: id
|
||||
}));
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
load(page, responses[0], responses[1]);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
}),
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
CustomName: page.querySelector('#txtCustomName').value
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
}
|
||||
|
||||
function save(page) {
|
||||
const id = getParameterByName('id');
|
||||
ApiClient.ajax({
|
||||
url: ApiClient.getUrl('Devices/Options', {
|
||||
Id: id
|
||||
}),
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
CustomName: page.querySelector('#txtCustomName').value
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
}
|
||||
function onSubmit(e) {
|
||||
const form = this;
|
||||
save(dom.parentWithClass(form, 'page'));
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const form = this;
|
||||
save(dom.parentWithClass(form, 'page'));
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
export default function (view) {
|
||||
view.querySelector('form').addEventListener('submit', onSubmit);
|
||||
view.addEventListener('viewshow', loadData);
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
view.querySelector('form').addEventListener('submit', onSubmit);
|
||||
view.addEventListener('viewshow', loadData);
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -12,159 +12,157 @@ import '../../../components/cardbuilder/card.scss';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import confirm from '../../../components/confirm/confirm';
|
||||
|
||||
/* eslint-disable indent */
|
||||
// Local cache of loaded
|
||||
let deviceIds = [];
|
||||
|
||||
// Local cache of loaded
|
||||
let deviceIds = [];
|
||||
function canDelete(deviceId) {
|
||||
return deviceId !== ApiClient.deviceId();
|
||||
}
|
||||
|
||||
function canDelete(deviceId) {
|
||||
return deviceId !== ApiClient.deviceId();
|
||||
}
|
||||
function deleteAllDevices(page) {
|
||||
const msg = globalize.translate('DeleteDevicesConfirmation');
|
||||
|
||||
function deleteAllDevices(page) {
|
||||
const msg = globalize.translate('DeleteDevicesConfirmation');
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderDeleteDevices'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(async () => {
|
||||
loading.show();
|
||||
await Promise.all(
|
||||
deviceIds.filter(canDelete).map((id) => ApiClient.deleteDevice(id))
|
||||
);
|
||||
loadData(page);
|
||||
});
|
||||
}
|
||||
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderDeleteDevices'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(async () => {
|
||||
loading.show();
|
||||
await Promise.all(
|
||||
deviceIds.filter(canDelete).map((id) => ApiClient.deleteDevice(id))
|
||||
);
|
||||
loadData(page);
|
||||
function deleteDevice(page, id) {
|
||||
const msg = globalize.translate('DeleteDeviceConfirmation');
|
||||
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderDeleteDevice'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(async () => {
|
||||
loading.show();
|
||||
await ApiClient.deleteDevice(id);
|
||||
loadData(page);
|
||||
});
|
||||
}
|
||||
|
||||
function showDeviceMenu(view, btn, deviceId) {
|
||||
const menuItems = [{
|
||||
name: globalize.translate('Edit'),
|
||||
id: 'open',
|
||||
icon: 'mode_edit'
|
||||
}];
|
||||
|
||||
if (canDelete(deviceId)) {
|
||||
menuItems.push({
|
||||
name: globalize.translate('Delete'),
|
||||
id: 'delete',
|
||||
icon: 'delete'
|
||||
});
|
||||
}
|
||||
|
||||
function deleteDevice(page, id) {
|
||||
const msg = globalize.translate('DeleteDeviceConfirmation');
|
||||
import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: btn,
|
||||
callback: function (id) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('device.html?id=' + deviceId);
|
||||
break;
|
||||
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderDeleteDevice'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(async () => {
|
||||
loading.show();
|
||||
await ApiClient.deleteDevice(id);
|
||||
loadData(page);
|
||||
case 'delete':
|
||||
deleteDevice(view, deviceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showDeviceMenu(view, btn, deviceId) {
|
||||
const menuItems = [{
|
||||
name: globalize.translate('Edit'),
|
||||
id: 'open',
|
||||
icon: 'mode_edit'
|
||||
}];
|
||||
function load(page, devices) {
|
||||
const localeWithSuffix = getLocaleWithSuffix();
|
||||
|
||||
if (canDelete(deviceId)) {
|
||||
menuItems.push({
|
||||
name: globalize.translate('Delete'),
|
||||
id: 'delete',
|
||||
icon: 'delete'
|
||||
});
|
||||
let html = '';
|
||||
html += devices.map(function (device) {
|
||||
let deviceHtml = '';
|
||||
deviceHtml += "<div data-id='" + device.Id + "' class='card backdropCard'>";
|
||||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#/device.html?id=${device.Id}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
if (iconUrl) {
|
||||
deviceHtml += '<div class="cardImage" style="background-image:url(\'' + iconUrl + "');background-size: auto 64%;background-position:center center;\">";
|
||||
deviceHtml += '</div>';
|
||||
} else {
|
||||
deviceHtml += '<span class="cardImageIcon material-icons tablet_android" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: btn,
|
||||
callback: function (id) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('device.html?id=' + deviceId);
|
||||
break;
|
||||
deviceHtml += '</a>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '<div class="cardFooter">';
|
||||
|
||||
case 'delete':
|
||||
deleteDevice(view, deviceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function load(page, devices) {
|
||||
const localeWithSuffix = getLocaleWithSuffix();
|
||||
|
||||
let html = '';
|
||||
html += devices.map(function (device) {
|
||||
let deviceHtml = '';
|
||||
deviceHtml += "<div data-id='" + device.Id + "' class='card backdropCard'>";
|
||||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#/device.html?id=${device.Id}" class="cardContent cardImageContainer ${cardBuilder.getDefaultBackgroundClass()}">`;
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
if (iconUrl) {
|
||||
deviceHtml += '<div class="cardImage" style="background-image:url(\'' + iconUrl + "');background-size: auto 64%;background-position:center center;\">";
|
||||
deviceHtml += '</div>';
|
||||
} else {
|
||||
deviceHtml += '<span class="cardImageIcon material-icons tablet_android" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
deviceHtml += '</a>';
|
||||
if (canDelete(device.Id)) {
|
||||
if (globalize.getIsRTL())
|
||||
deviceHtml += '<div style="text-align:left; float:left;padding-top:5px;">';
|
||||
else
|
||||
deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">';
|
||||
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + device.Id + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '<div class="cardFooter">';
|
||||
}
|
||||
|
||||
if (canDelete(device.Id)) {
|
||||
if (globalize.getIsRTL())
|
||||
deviceHtml += '<div style="text-align:left; float:left;padding-top:5px;">';
|
||||
else
|
||||
deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">';
|
||||
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + device.Id + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
deviceHtml += '</div>';
|
||||
}
|
||||
deviceHtml += "<div class='cardText'>";
|
||||
deviceHtml += escapeHtml(device.Name);
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += "<div class='cardText cardText-secondary'>";
|
||||
deviceHtml += escapeHtml(device.AppName + ' ' + device.AppVersion);
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += "<div class='cardText cardText-secondary'>";
|
||||
|
||||
deviceHtml += "<div class='cardText'>";
|
||||
deviceHtml += escapeHtml(device.Name);
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += "<div class='cardText cardText-secondary'>";
|
||||
deviceHtml += escapeHtml(device.AppName + ' ' + device.AppVersion);
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += "<div class='cardText cardText-secondary'>";
|
||||
if (device.LastUserName) {
|
||||
deviceHtml += escapeHtml(device.LastUserName);
|
||||
deviceHtml += ', ' + formatDistanceToNow(Date.parse(device.DateLastActivity), localeWithSuffix);
|
||||
}
|
||||
|
||||
if (device.LastUserName) {
|
||||
deviceHtml += escapeHtml(device.LastUserName);
|
||||
deviceHtml += ', ' + formatDistanceToNow(Date.parse(device.DateLastActivity), localeWithSuffix);
|
||||
}
|
||||
deviceHtml += ' ';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
return deviceHtml;
|
||||
}).join('');
|
||||
page.querySelector('.devicesList').innerHTML = html;
|
||||
}
|
||||
|
||||
deviceHtml += ' ';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
return deviceHtml;
|
||||
}).join('');
|
||||
page.querySelector('.devicesList').innerHTML = html;
|
||||
}
|
||||
function loadData(page) {
|
||||
loading.show();
|
||||
ApiClient.getJSON(ApiClient.getUrl('Devices')).then(function (result) {
|
||||
load(page, result.Items);
|
||||
deviceIds = result.Items.map((device) => device.Id);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function loadData(page) {
|
||||
loading.show();
|
||||
ApiClient.getJSON(ApiClient.getUrl('Devices')).then(function (result) {
|
||||
load(page, result.Items);
|
||||
deviceIds = result.Items.map((device) => device.Id);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
export default function (view) {
|
||||
view.querySelector('.devicesList').addEventListener('click', function (e) {
|
||||
const btnDeviceMenu = dom.parentWithClass(e.target, 'btnDeviceMenu');
|
||||
|
||||
export default function (view) {
|
||||
view.querySelector('.devicesList').addEventListener('click', function (e) {
|
||||
const btnDeviceMenu = dom.parentWithClass(e.target, 'btnDeviceMenu');
|
||||
if (btnDeviceMenu) {
|
||||
showDeviceMenu(view, btnDeviceMenu, btnDeviceMenu.getAttribute('data-id'));
|
||||
}
|
||||
});
|
||||
view.addEventListener('viewshow', function () {
|
||||
loadData(this);
|
||||
});
|
||||
|
||||
if (btnDeviceMenu) {
|
||||
showDeviceMenu(view, btnDeviceMenu, btnDeviceMenu.getAttribute('data-id'));
|
||||
}
|
||||
});
|
||||
view.addEventListener('viewshow', function () {
|
||||
loadData(this);
|
||||
});
|
||||
view.querySelector('#deviceDeleteAll').addEventListener('click', function() {
|
||||
deleteAllDevices(view);
|
||||
});
|
||||
}
|
||||
|
||||
view.querySelector('#deviceDeleteAll').addEventListener('click', function() {
|
||||
deleteAllDevices(view);
|
||||
});
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,90 +7,87 @@ import '../../../components/listview/listview.scss';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import confirm from '../../../components/confirm/confirm';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function loadProfiles(page) {
|
||||
loading.show();
|
||||
ApiClient.getJSON(ApiClient.getUrl('Dlna/ProfileInfos')).then(function (result) {
|
||||
renderUserProfiles(page, result);
|
||||
renderSystemProfiles(page, result);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function renderUserProfiles(page, profiles) {
|
||||
renderProfiles(page, page.querySelector('.customProfiles'), profiles.filter(function (p) {
|
||||
return p.Type == 'User';
|
||||
}));
|
||||
}
|
||||
|
||||
function renderSystemProfiles(page, profiles) {
|
||||
renderProfiles(page, page.querySelector('.systemProfiles'), profiles.filter(function (p) {
|
||||
return p.Type == 'System';
|
||||
}));
|
||||
}
|
||||
|
||||
function renderProfiles(page, element, profiles) {
|
||||
let html = '';
|
||||
|
||||
if (profiles.length) {
|
||||
html += '<div class="paperList">';
|
||||
}
|
||||
|
||||
for (let i = 0, length = profiles.length; i < length; i++) {
|
||||
const profile = profiles[i];
|
||||
html += '<div class="listItem listItem-border">';
|
||||
html += '<span class="listItemIcon material-icons live_tv" aria-hidden="true"></span>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
html += "<a is='emby-linkbutton' style='padding:0;margin:0;' data-ripple='false' class='clearLink' href='#/dlnaprofile.html?id=" + profile.Id + "'>";
|
||||
html += '<div>' + escapeHtml(profile.Name) + '</div>';
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
|
||||
if (profile.Type == 'User') {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnDeleteProfile" data-profileid="' + profile.Id + '" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (profiles.length) {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
element.innerHTML = html;
|
||||
$('.btnDeleteProfile', element).on('click', function () {
|
||||
const id = this.getAttribute('data-profileid');
|
||||
deleteProfile(page, id);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteProfile(page, id) {
|
||||
confirm(globalize.translate('MessageConfirmProfileDeletion'), globalize.translate('HeaderConfirmProfileDeletion')).then(function () {
|
||||
loading.show();
|
||||
ApiClient.ajax({
|
||||
type: 'DELETE',
|
||||
url: ApiClient.getUrl('Dlna/Profiles/' + id)
|
||||
}).then(function () {
|
||||
loading.hide();
|
||||
loadProfiles(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dlnasettings.html',
|
||||
name: globalize.translate('Settings')
|
||||
}, {
|
||||
href: '#/dlnaprofiles.html',
|
||||
name: globalize.translate('TabProfiles')
|
||||
}];
|
||||
}
|
||||
|
||||
$(document).on('pageshow', '#dlnaProfilesPage', function () {
|
||||
libraryMenu.setTabs('dlna', 1, getTabs);
|
||||
loadProfiles(this);
|
||||
function loadProfiles(page) {
|
||||
loading.show();
|
||||
ApiClient.getJSON(ApiClient.getUrl('Dlna/ProfileInfos')).then(function (result) {
|
||||
renderUserProfiles(page, result);
|
||||
renderSystemProfiles(page, result);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function renderUserProfiles(page, profiles) {
|
||||
renderProfiles(page, page.querySelector('.customProfiles'), profiles.filter(function (p) {
|
||||
return p.Type == 'User';
|
||||
}));
|
||||
}
|
||||
|
||||
function renderSystemProfiles(page, profiles) {
|
||||
renderProfiles(page, page.querySelector('.systemProfiles'), profiles.filter(function (p) {
|
||||
return p.Type == 'System';
|
||||
}));
|
||||
}
|
||||
|
||||
function renderProfiles(page, element, profiles) {
|
||||
let html = '';
|
||||
|
||||
if (profiles.length) {
|
||||
html += '<div class="paperList">';
|
||||
}
|
||||
|
||||
for (let i = 0, length = profiles.length; i < length; i++) {
|
||||
const profile = profiles[i];
|
||||
html += '<div class="listItem listItem-border">';
|
||||
html += '<span class="listItemIcon material-icons live_tv" aria-hidden="true"></span>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
html += "<a is='emby-linkbutton' style='padding:0;margin:0;' data-ripple='false' class='clearLink' href='#/dlnaprofile.html?id=" + profile.Id + "'>";
|
||||
html += '<div>' + escapeHtml(profile.Name) + '</div>';
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
|
||||
if (profile.Type == 'User') {
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnDeleteProfile" data-profileid="' + profile.Id + '" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (profiles.length) {
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
element.innerHTML = html;
|
||||
$('.btnDeleteProfile', element).on('click', function () {
|
||||
const id = this.getAttribute('data-profileid');
|
||||
deleteProfile(page, id);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteProfile(page, id) {
|
||||
confirm(globalize.translate('MessageConfirmProfileDeletion'), globalize.translate('HeaderConfirmProfileDeletion')).then(function () {
|
||||
loading.show();
|
||||
ApiClient.ajax({
|
||||
type: 'DELETE',
|
||||
url: ApiClient.getUrl('Dlna/Profiles/' + id)
|
||||
}).then(function () {
|
||||
loading.hide();
|
||||
loadProfiles(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dlnasettings.html',
|
||||
name: globalize.translate('Settings')
|
||||
}, {
|
||||
href: '#/dlnaprofiles.html',
|
||||
name: globalize.translate('TabProfiles')
|
||||
}];
|
||||
}
|
||||
|
||||
$(document).on('pageshow', '#dlnaProfilesPage', function () {
|
||||
libraryMenu.setTabs('dlna', 1, getTabs);
|
||||
loadProfiles(this);
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -5,59 +5,56 @@ import libraryMenu from '../../../scripts/libraryMenu';
|
||||
import globalize from '../../../scripts/globalize';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function loadPage(page, config, users) {
|
||||
page.querySelector('#chkEnablePlayTo').checked = config.EnablePlayTo;
|
||||
page.querySelector('#chkEnableDlnaDebugLogging').checked = config.EnableDebugLog;
|
||||
$('#txtClientDiscoveryInterval', page).val(config.ClientDiscoveryIntervalSeconds);
|
||||
$('#chkEnableServer', page).prop('checked', config.EnableServer);
|
||||
$('#chkBlastAliveMessages', page).prop('checked', config.BlastAliveMessages);
|
||||
$('#txtBlastInterval', page).val(config.BlastAliveMessageIntervalSeconds);
|
||||
const usersHtml = users.map(function (u) {
|
||||
return '<option value="' + u.Id + '">' + escapeHtml(u.Name) + '</option>';
|
||||
}).join('');
|
||||
$('#selectUser', page).html(usersHtml).val(config.DefaultUserId || '');
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function loadPage(page, config, users) {
|
||||
page.querySelector('#chkEnablePlayTo').checked = config.EnablePlayTo;
|
||||
page.querySelector('#chkEnableDlnaDebugLogging').checked = config.EnableDebugLog;
|
||||
$('#txtClientDiscoveryInterval', page).val(config.ClientDiscoveryIntervalSeconds);
|
||||
$('#chkEnableServer', page).prop('checked', config.EnableServer);
|
||||
$('#chkBlastAliveMessages', page).prop('checked', config.BlastAliveMessages);
|
||||
$('#txtBlastInterval', page).val(config.BlastAliveMessageIntervalSeconds);
|
||||
const usersHtml = users.map(function (u) {
|
||||
return '<option value="' + u.Id + '">' + escapeHtml(u.Name) + '</option>';
|
||||
}).join('');
|
||||
$('#selectUser', page).html(usersHtml).val(config.DefaultUserId || '');
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getNamedConfiguration('dlna').then(function (config) {
|
||||
config.EnablePlayTo = form.querySelector('#chkEnablePlayTo').checked;
|
||||
config.EnableDebugLog = form.querySelector('#chkEnableDlnaDebugLogging').checked;
|
||||
config.ClientDiscoveryIntervalSeconds = $('#txtClientDiscoveryInterval', form).val();
|
||||
config.EnableServer = $('#chkEnableServer', form).is(':checked');
|
||||
config.BlastAliveMessages = $('#chkBlastAliveMessages', form).is(':checked');
|
||||
config.BlastAliveMessageIntervalSeconds = $('#txtBlastInterval', form).val();
|
||||
config.DefaultUserId = $('#selectUser', form).val();
|
||||
ApiClient.updateNamedConfiguration('dlna', config).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dlnasettings.html',
|
||||
name: globalize.translate('Settings')
|
||||
}, {
|
||||
href: '#/dlnaprofiles.html',
|
||||
name: globalize.translate('TabProfiles')
|
||||
}];
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#dlnaSettingsPage', function () {
|
||||
$('.dlnaSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#dlnaSettingsPage', function () {
|
||||
libraryMenu.setTabs('dlna', 0, getTabs);
|
||||
loading.show();
|
||||
const page = this;
|
||||
const promise1 = ApiClient.getNamedConfiguration('dlna');
|
||||
const promise2 = ApiClient.getUsers();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
loadPage(page, responses[0], responses[1]);
|
||||
});
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getNamedConfiguration('dlna').then(function (config) {
|
||||
config.EnablePlayTo = form.querySelector('#chkEnablePlayTo').checked;
|
||||
config.EnableDebugLog = form.querySelector('#chkEnableDlnaDebugLogging').checked;
|
||||
config.ClientDiscoveryIntervalSeconds = $('#txtClientDiscoveryInterval', form).val();
|
||||
config.EnableServer = $('#chkEnableServer', form).is(':checked');
|
||||
config.BlastAliveMessages = $('#chkBlastAliveMessages', form).is(':checked');
|
||||
config.BlastAliveMessageIntervalSeconds = $('#txtBlastInterval', form).val();
|
||||
config.DefaultUserId = $('#selectUser', form).val();
|
||||
ApiClient.updateNamedConfiguration('dlna', config).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/dlnasettings.html',
|
||||
name: globalize.translate('Settings')
|
||||
}, {
|
||||
href: '#/dlnaprofiles.html',
|
||||
name: globalize.translate('TabProfiles')
|
||||
}];
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#dlnaSettingsPage', function () {
|
||||
$('.dlnaSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#dlnaSettingsPage', function () {
|
||||
libraryMenu.setTabs('dlna', 0, getTabs);
|
||||
loading.show();
|
||||
const page = this;
|
||||
const promise1 = ApiClient.getNamedConfiguration('dlna');
|
||||
const promise2 = ApiClient.getUsers();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
loadPage(page, responses[0], responses[1]);
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
<span>${EnableIntelLowPowerHevcHwEncoder}</span>
|
||||
</label>
|
||||
<div class="fieldDescription">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://01.org/linuxgraphics/downloads/firmware" target="_blank">${IntelLowPowerEncHelp}</a>
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org/docs/general/administration/hardware-acceleration/intel#configure-and-verify-lp-mode-on-linux" target="_blank">${IntelLowPowerEncHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -253,6 +253,13 @@
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${EnableFallbackFontHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkEnableAudioVbr" />
|
||||
<span>${LabelEnableAudioVbr}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableAudioVbrHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtDownMixAudioBoost" pattern="[0-9]*" required="required" min=".5" max="3" step=".1" label="${LabelDownMixAudioScale}" />
|
||||
<div class="fieldDescription">${LabelDownMixAudioScaleHelp}</div>
|
||||
|
||||
@@ -6,295 +6,294 @@ import libraryMenu from '../../scripts/libraryMenu';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import alert from '../../components/alert';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function loadPage(page, config, systemInfo) {
|
||||
Array.prototype.forEach.call(page.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
c.checked = (config.HardwareDecodingCodecs || []).indexOf(c.getAttribute('data-codec')) !== -1;
|
||||
});
|
||||
page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc;
|
||||
page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9;
|
||||
page.querySelector('#chkEnhancedNvdecDecoder').checked = config.EnableEnhancedNvdecDecoder;
|
||||
page.querySelector('#chkSystemNativeHwDecoder').checked = config.PreferSystemNativeHwDecoder;
|
||||
page.querySelector('#chkIntelLpH264HwEncoder').checked = config.EnableIntelLowPowerH264HwEncoder;
|
||||
page.querySelector('#chkIntelLpHevcHwEncoder').checked = config.EnableIntelLowPowerHevcHwEncoder;
|
||||
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
|
||||
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
||||
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
|
||||
$('#selectThreadCount', page).val(config.EncodingThreadCount);
|
||||
page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr;
|
||||
$('#txtDownMixAudioBoost', page).val(config.DownMixAudioBoost);
|
||||
$('#selectStereoDownmixAlgorithm').val(config.DownMixStereoAlgorithm || 'None');
|
||||
page.querySelector('#txtMaxMuxingQueueSize').value = config.MaxMuxingQueueSize || '';
|
||||
page.querySelector('.txtEncoderPath').value = config.EncoderAppPathDisplay || '';
|
||||
$('#txtTranscodingTempPath', page).val(systemInfo.TranscodingTempPath || '');
|
||||
page.querySelector('#txtFallbackFontPath').value = config.FallbackFontPath || '';
|
||||
page.querySelector('#chkEnableFallbackFont').checked = config.EnableFallbackFont;
|
||||
$('#txtVaapiDevice', page).val(config.VaapiDevice || '');
|
||||
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
||||
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
||||
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
||||
page.querySelector('#txtTonemappingThreshold').value = config.TonemappingThreshold;
|
||||
page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak;
|
||||
page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || '';
|
||||
page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness;
|
||||
page.querySelector('#txtVppTonemappingContrast').value = config.VppTonemappingContrast;
|
||||
page.querySelector('#selectEncoderPreset').value = config.EncoderPreset || '';
|
||||
page.querySelector('#txtH264Crf').value = config.H264Crf || '';
|
||||
page.querySelector('#txtH265Crf').value = config.H265Crf || '';
|
||||
page.querySelector('#selectDeinterlaceMethod').value = config.DeinterlaceMethod || '';
|
||||
page.querySelector('#chkDoubleRateDeinterlacing').checked = config.DeinterlaceDoubleRate;
|
||||
page.querySelector('#chkEnableSubtitleExtraction').checked = config.EnableSubtitleExtraction || false;
|
||||
page.querySelector('#chkEnableThrottling').checked = config.EnableThrottling || false;
|
||||
page.querySelector('#selectVideoDecoder').dispatchEvent(new CustomEvent('change', {
|
||||
bubbles: true
|
||||
}));
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function loadPage(page, config, systemInfo) {
|
||||
Array.prototype.forEach.call(page.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
c.checked = (config.HardwareDecodingCodecs || []).indexOf(c.getAttribute('data-codec')) !== -1;
|
||||
});
|
||||
page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc;
|
||||
page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9;
|
||||
page.querySelector('#chkEnhancedNvdecDecoder').checked = config.EnableEnhancedNvdecDecoder;
|
||||
page.querySelector('#chkSystemNativeHwDecoder').checked = config.PreferSystemNativeHwDecoder;
|
||||
page.querySelector('#chkIntelLpH264HwEncoder').checked = config.EnableIntelLowPowerH264HwEncoder;
|
||||
page.querySelector('#chkIntelLpHevcHwEncoder').checked = config.EnableIntelLowPowerHevcHwEncoder;
|
||||
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
|
||||
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
||||
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
|
||||
$('#selectThreadCount', page).val(config.EncodingThreadCount);
|
||||
$('#txtDownMixAudioBoost', page).val(config.DownMixAudioBoost);
|
||||
$('#selectStereoDownmixAlgorithm').val(config.DownMixStereoAlgorithm || 'None');
|
||||
page.querySelector('#txtMaxMuxingQueueSize').value = config.MaxMuxingQueueSize || '';
|
||||
page.querySelector('.txtEncoderPath').value = config.EncoderAppPathDisplay || '';
|
||||
$('#txtTranscodingTempPath', page).val(systemInfo.TranscodingTempPath || '');
|
||||
page.querySelector('#txtFallbackFontPath').value = config.FallbackFontPath || '';
|
||||
page.querySelector('#chkEnableFallbackFont').checked = config.EnableFallbackFont;
|
||||
$('#txtVaapiDevice', page).val(config.VaapiDevice || '');
|
||||
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
||||
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm;
|
||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange;
|
||||
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
||||
page.querySelector('#txtTonemappingThreshold').value = config.TonemappingThreshold;
|
||||
page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak;
|
||||
page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || '';
|
||||
page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness;
|
||||
page.querySelector('#txtVppTonemappingContrast').value = config.VppTonemappingContrast;
|
||||
page.querySelector('#selectEncoderPreset').value = config.EncoderPreset || '';
|
||||
page.querySelector('#txtH264Crf').value = config.H264Crf || '';
|
||||
page.querySelector('#txtH265Crf').value = config.H265Crf || '';
|
||||
page.querySelector('#selectDeinterlaceMethod').value = config.DeinterlaceMethod || '';
|
||||
page.querySelector('#chkDoubleRateDeinterlacing').checked = config.DeinterlaceDoubleRate;
|
||||
page.querySelector('#chkEnableSubtitleExtraction').checked = config.EnableSubtitleExtraction || false;
|
||||
page.querySelector('#chkEnableThrottling').checked = config.EnableThrottling || false;
|
||||
page.querySelector('#selectVideoDecoder').dispatchEvent(new CustomEvent('change', {
|
||||
bubbles: true
|
||||
}));
|
||||
loading.hide();
|
||||
}
|
||||
function onSaveEncodingPathFailure() {
|
||||
loading.hide();
|
||||
alert(globalize.translate('FFmpegSavePathNotFound'));
|
||||
}
|
||||
|
||||
function onSaveEncodingPathFailure() {
|
||||
loading.hide();
|
||||
alert(globalize.translate('FFmpegSavePathNotFound'));
|
||||
}
|
||||
function updateEncoder(form) {
|
||||
return ApiClient.getSystemInfo().then(function () {
|
||||
return ApiClient.ajax({
|
||||
url: ApiClient.getUrl('System/MediaEncoder/Path'),
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
Path: form.querySelector('.txtEncoderPath').value,
|
||||
PathType: 'Custom'
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).then(Dashboard.processServerConfigurationUpdateResult, onSaveEncodingPathFailure);
|
||||
});
|
||||
}
|
||||
|
||||
function updateEncoder(form) {
|
||||
return ApiClient.getSystemInfo().then(function () {
|
||||
return ApiClient.ajax({
|
||||
url: ApiClient.getUrl('System/MediaEncoder/Path'),
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
Path: form.querySelector('.txtEncoderPath').value,
|
||||
PathType: 'Custom'
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).then(Dashboard.processServerConfigurationUpdateResult, onSaveEncodingPathFailure);
|
||||
});
|
||||
}
|
||||
function onSubmit() {
|
||||
const form = this;
|
||||
|
||||
function onSubmit() {
|
||||
const form = this;
|
||||
|
||||
const onDecoderConfirmed = function () {
|
||||
loading.show();
|
||||
ApiClient.getNamedConfiguration('encoding').then(function (config) {
|
||||
config.DownMixAudioBoost = $('#txtDownMixAudioBoost', form).val();
|
||||
config.DownMixStereoAlgorithm = $('#selectStereoDownmixAlgorithm', form).val() || 'None';
|
||||
config.MaxMuxingQueueSize = form.querySelector('#txtMaxMuxingQueueSize').value;
|
||||
config.TranscodingTempPath = $('#txtTranscodingTempPath', form).val();
|
||||
config.FallbackFontPath = form.querySelector('#txtFallbackFontPath').value;
|
||||
config.EnableFallbackFont = form.querySelector('#txtFallbackFontPath').value ? form.querySelector('#chkEnableFallbackFont').checked : false;
|
||||
config.EncodingThreadCount = $('#selectThreadCount', form).val();
|
||||
config.HardwareAccelerationType = $('#selectVideoDecoder', form).val();
|
||||
config.VaapiDevice = $('#txtVaapiDevice', form).val();
|
||||
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
||||
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
||||
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
||||
config.TonemappingRange = form.querySelector('#selectTonemappingRange').value;
|
||||
config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value;
|
||||
config.TonemappingThreshold = form.querySelector('#txtTonemappingThreshold').value;
|
||||
config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value;
|
||||
config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0';
|
||||
config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value;
|
||||
config.VppTonemappingContrast = form.querySelector('#txtVppTonemappingContrast').value;
|
||||
config.EncoderPreset = form.querySelector('#selectEncoderPreset').value;
|
||||
config.H264Crf = parseInt(form.querySelector('#txtH264Crf').value || '0', 10);
|
||||
config.H265Crf = parseInt(form.querySelector('#txtH265Crf').value || '0', 10);
|
||||
config.DeinterlaceMethod = form.querySelector('#selectDeinterlaceMethod').value;
|
||||
config.DeinterlaceDoubleRate = form.querySelector('#chkDoubleRateDeinterlacing').checked;
|
||||
config.EnableSubtitleExtraction = form.querySelector('#chkEnableSubtitleExtraction').checked;
|
||||
config.EnableThrottling = form.querySelector('#chkEnableThrottling').checked;
|
||||
config.HardwareDecodingCodecs = Array.prototype.map.call(Array.prototype.filter.call(form.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
return c.checked;
|
||||
}), function (c) {
|
||||
return c.getAttribute('data-codec');
|
||||
});
|
||||
config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked;
|
||||
config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked;
|
||||
config.EnableEnhancedNvdecDecoder = form.querySelector('#chkEnhancedNvdecDecoder').checked;
|
||||
config.PreferSystemNativeHwDecoder = form.querySelector('#chkSystemNativeHwDecoder').checked;
|
||||
config.EnableIntelLowPowerH264HwEncoder = form.querySelector('#chkIntelLpH264HwEncoder').checked;
|
||||
config.EnableIntelLowPowerHevcHwEncoder = form.querySelector('#chkIntelLpHevcHwEncoder').checked;
|
||||
config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
|
||||
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
|
||||
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
|
||||
updateEncoder(form);
|
||||
}, function () {
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if ($('#selectVideoDecoder', form).val()) {
|
||||
alert({
|
||||
title: globalize.translate('TitleHardwareAcceleration'),
|
||||
text: globalize.translate('HardwareAccelerationWarning')
|
||||
}).then(onDecoderConfirmed);
|
||||
} else {
|
||||
onDecoderConfirmed();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setDecodingCodecsVisible(context, value) {
|
||||
value = value || '';
|
||||
let any;
|
||||
Array.prototype.forEach.call(context.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
if (c.getAttribute('data-types').split(',').indexOf(value) === -1) {
|
||||
dom.parentWithTag(c, 'LABEL').classList.add('hide');
|
||||
} else {
|
||||
dom.parentWithTag(c, 'LABEL').classList.remove('hide');
|
||||
any = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (any) {
|
||||
context.querySelector('.decodingCodecsList').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.decodingCodecsList').classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/encodingsettings.html',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/playbackconfiguration.html',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/streamingsettings.html',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}];
|
||||
}
|
||||
|
||||
let systemInfo;
|
||||
function getSystemInfo() {
|
||||
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
|
||||
info => {
|
||||
systemInfo = info;
|
||||
return info;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#encodingSettingsPage', function () {
|
||||
const page = this;
|
||||
getSystemInfo();
|
||||
page.querySelector('#selectVideoDecoder').addEventListener('change', function () {
|
||||
if (this.value == 'vaapi') {
|
||||
page.querySelector('.fldVaapiDevice').classList.remove('hide');
|
||||
page.querySelector('#txtVaapiDevice').setAttribute('required', 'required');
|
||||
} else {
|
||||
page.querySelector('.fldVaapiDevice').classList.add('hide');
|
||||
page.querySelector('#txtVaapiDevice').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'videotoolbox') {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.tonemappingOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.fldIntelLp').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldIntelLp').classList.add('hide');
|
||||
}
|
||||
|
||||
if (systemInfo.OperatingSystem.toLowerCase() === 'linux' && (this.value == 'qsv' || this.value == 'vaapi')) {
|
||||
page.querySelector('.vppTonemappingOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.vppTonemappingOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv') {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'nvenc') {
|
||||
page.querySelector('.fldEnhancedNvdec').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldEnhancedNvdec').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value) {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
setDecodingCodecsVisible(page, this.value);
|
||||
});
|
||||
$('#btnSelectEncoderPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('.txtEncoderPath', page).val(path);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('#txtTranscodingTempPath', page).val(path);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectTranscodingPath'),
|
||||
instruction: globalize.translate('HeaderSelectTranscodingPathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeDirectories: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
page.querySelector('#txtFallbackFontPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
header: globalize.translate('HeaderSelectFallbackFontPath'),
|
||||
instruction: globalize.translate('HeaderSelectFallbackFontPathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('.encodingSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#encodingSettingsPage', function () {
|
||||
const onDecoderConfirmed = function () {
|
||||
loading.show();
|
||||
libraryMenu.setTabs('playback', 0, getTabs);
|
||||
const page = this;
|
||||
ApiClient.getNamedConfiguration('encoding').then(function (config) {
|
||||
ApiClient.getSystemInfo().then(function (fetchedSystemInfo) {
|
||||
loadPage(page, config, fetchedSystemInfo);
|
||||
config.EnableAudioVbr = form.querySelector('#chkEnableAudioVbr').checked;
|
||||
config.DownMixAudioBoost = $('#txtDownMixAudioBoost', form).val();
|
||||
config.DownMixStereoAlgorithm = $('#selectStereoDownmixAlgorithm', form).val() || 'None';
|
||||
config.MaxMuxingQueueSize = form.querySelector('#txtMaxMuxingQueueSize').value;
|
||||
config.TranscodingTempPath = $('#txtTranscodingTempPath', form).val();
|
||||
config.FallbackFontPath = form.querySelector('#txtFallbackFontPath').value;
|
||||
config.EnableFallbackFont = form.querySelector('#txtFallbackFontPath').value ? form.querySelector('#chkEnableFallbackFont').checked : false;
|
||||
config.EncodingThreadCount = $('#selectThreadCount', form).val();
|
||||
config.HardwareAccelerationType = $('#selectVideoDecoder', form).val();
|
||||
config.VaapiDevice = $('#txtVaapiDevice', form).val();
|
||||
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
||||
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
||||
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
||||
config.TonemappingRange = form.querySelector('#selectTonemappingRange').value;
|
||||
config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value;
|
||||
config.TonemappingThreshold = form.querySelector('#txtTonemappingThreshold').value;
|
||||
config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value;
|
||||
config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0';
|
||||
config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value;
|
||||
config.VppTonemappingContrast = form.querySelector('#txtVppTonemappingContrast').value;
|
||||
config.EncoderPreset = form.querySelector('#selectEncoderPreset').value;
|
||||
config.H264Crf = parseInt(form.querySelector('#txtH264Crf').value || '0', 10);
|
||||
config.H265Crf = parseInt(form.querySelector('#txtH265Crf').value || '0', 10);
|
||||
config.DeinterlaceMethod = form.querySelector('#selectDeinterlaceMethod').value;
|
||||
config.DeinterlaceDoubleRate = form.querySelector('#chkDoubleRateDeinterlacing').checked;
|
||||
config.EnableSubtitleExtraction = form.querySelector('#chkEnableSubtitleExtraction').checked;
|
||||
config.EnableThrottling = form.querySelector('#chkEnableThrottling').checked;
|
||||
config.HardwareDecodingCodecs = Array.prototype.map.call(Array.prototype.filter.call(form.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
return c.checked;
|
||||
}), function (c) {
|
||||
return c.getAttribute('data-codec');
|
||||
});
|
||||
config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked;
|
||||
config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked;
|
||||
config.EnableEnhancedNvdecDecoder = form.querySelector('#chkEnhancedNvdecDecoder').checked;
|
||||
config.PreferSystemNativeHwDecoder = form.querySelector('#chkSystemNativeHwDecoder').checked;
|
||||
config.EnableIntelLowPowerH264HwEncoder = form.querySelector('#chkIntelLpH264HwEncoder').checked;
|
||||
config.EnableIntelLowPowerHevcHwEncoder = form.querySelector('#chkIntelLpHevcHwEncoder').checked;
|
||||
config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
|
||||
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
|
||||
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
|
||||
updateEncoder(form);
|
||||
}, function () {
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if ($('#selectVideoDecoder', form).val()) {
|
||||
alert({
|
||||
title: globalize.translate('TitleHardwareAcceleration'),
|
||||
text: globalize.translate('HardwareAccelerationWarning')
|
||||
}).then(onDecoderConfirmed);
|
||||
} else {
|
||||
onDecoderConfirmed();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setDecodingCodecsVisible(context, value) {
|
||||
value = value || '';
|
||||
let any;
|
||||
Array.prototype.forEach.call(context.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
if (c.getAttribute('data-types').split(',').indexOf(value) === -1) {
|
||||
dom.parentWithTag(c, 'LABEL').classList.add('hide');
|
||||
} else {
|
||||
dom.parentWithTag(c, 'LABEL').classList.remove('hide');
|
||||
any = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (any) {
|
||||
context.querySelector('.decodingCodecsList').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.decodingCodecsList').classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/encodingsettings.html',
|
||||
name: globalize.translate('Transcoding')
|
||||
}, {
|
||||
href: '#/playbackconfiguration.html',
|
||||
name: globalize.translate('ButtonResume')
|
||||
}, {
|
||||
href: '#/streamingsettings.html',
|
||||
name: globalize.translate('TabStreaming')
|
||||
}];
|
||||
}
|
||||
|
||||
let systemInfo;
|
||||
function getSystemInfo() {
|
||||
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
|
||||
info => {
|
||||
systemInfo = info;
|
||||
return info;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#encodingSettingsPage', function () {
|
||||
const page = this;
|
||||
getSystemInfo();
|
||||
page.querySelector('#selectVideoDecoder').addEventListener('change', function () {
|
||||
if (this.value == 'vaapi') {
|
||||
page.querySelector('.fldVaapiDevice').classList.remove('hide');
|
||||
page.querySelector('#txtVaapiDevice').setAttribute('required', 'required');
|
||||
} else {
|
||||
page.querySelector('.fldVaapiDevice').classList.add('hide');
|
||||
page.querySelector('#txtVaapiDevice').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'videotoolbox') {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.tonemappingOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.fldIntelLp').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldIntelLp').classList.add('hide');
|
||||
}
|
||||
|
||||
if (systemInfo.OperatingSystem.toLowerCase() === 'linux' && (this.value == 'qsv' || this.value == 'vaapi')) {
|
||||
page.querySelector('.vppTonemappingOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.vppTonemappingOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv') {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'nvenc') {
|
||||
page.querySelector('.fldEnhancedNvdec').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldEnhancedNvdec').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value) {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
setDecodingCodecsVisible(page, this.value);
|
||||
});
|
||||
$('#btnSelectEncoderPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('.txtEncoderPath', page).val(path);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
$('#txtTranscodingTempPath', page).val(path);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectTranscodingPath'),
|
||||
instruction: globalize.translate('HeaderSelectTranscodingPathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeDirectories: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
page.querySelector('#txtFallbackFontPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
header: globalize.translate('HeaderSelectFallbackFontPath'),
|
||||
instruction: globalize.translate('HeaderSelectFallbackFontPathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('.encodingSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#encodingSettingsPage', function () {
|
||||
loading.show();
|
||||
libraryMenu.setTabs('playback', 0, getTabs);
|
||||
const page = this;
|
||||
ApiClient.getNamedConfiguration('encoding').then(function (config) {
|
||||
ApiClient.getSystemInfo().then(function (fetchedSystemInfo) {
|
||||
loadPage(page, config, fetchedSystemInfo);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -10,110 +10,107 @@ import '../../elements/emby-button/emby-button';
|
||||
import Dashboard from '../../utils/dashboard';
|
||||
import alert from '../../components/alert';
|
||||
|
||||
/* eslint-disable indent */
|
||||
function loadPage(page, config, languageOptions, systemInfo) {
|
||||
page.querySelector('#txtServerName').value = systemInfo.ServerName;
|
||||
page.querySelector('#txtCachePath').value = systemInfo.CachePath || '';
|
||||
page.querySelector('#chkQuickConnectAvailable').checked = config.QuickConnectAvailable === true;
|
||||
$('#txtMetadataPath', page).val(systemInfo.InternalMetadataPath || '');
|
||||
$('#txtMetadataNetworkPath', page).val(systemInfo.MetadataNetworkPath || '');
|
||||
$('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) {
|
||||
return '<option value="' + language.Value + '">' + language.Name + '</option>';
|
||||
})).val(config.UICulture);
|
||||
page.querySelector('#txtParallelImageEncodingLimit').value = config.ParallelImageEncodingLimit || '';
|
||||
|
||||
function loadPage(page, config, languageOptions, systemInfo) {
|
||||
page.querySelector('#txtServerName').value = systemInfo.ServerName;
|
||||
page.querySelector('#txtCachePath').value = systemInfo.CachePath || '';
|
||||
page.querySelector('#chkQuickConnectAvailable').checked = config.QuickConnectAvailable === true;
|
||||
$('#txtMetadataPath', page).val(systemInfo.InternalMetadataPath || '');
|
||||
$('#txtMetadataNetworkPath', page).val(systemInfo.MetadataNetworkPath || '');
|
||||
$('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) {
|
||||
return '<option value="' + language.Value + '">' + language.Name + '</option>';
|
||||
})).val(config.UICulture);
|
||||
page.querySelector('#txtParallelImageEncodingLimit').value = config.ParallelImageEncodingLimit || '';
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
loading.hide();
|
||||
}
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
$(form).parents('.page');
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
config.ServerName = $('#txtServerName', form).val();
|
||||
config.UICulture = $('#selectLocalizationLanguage', form).val();
|
||||
config.CachePath = form.querySelector('#txtCachePath').value;
|
||||
config.MetadataPath = $('#txtMetadataPath', form).val();
|
||||
config.MetadataNetworkPath = $('#txtMetadataNetworkPath', form).val();
|
||||
config.QuickConnectAvailable = form.querySelector('#chkQuickConnectAvailable').checked;
|
||||
config.ParallelImageEncodingLimit = parseInt(form.querySelector('#txtParallelImageEncodingLimit').value || '0', 10);
|
||||
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
$(form).parents('.page');
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
config.ServerName = $('#txtServerName', form).val();
|
||||
config.UICulture = $('#selectLocalizationLanguage', form).val();
|
||||
config.CachePath = form.querySelector('#txtCachePath').value;
|
||||
config.MetadataPath = $('#txtMetadataPath', form).val();
|
||||
config.MetadataNetworkPath = $('#txtMetadataNetworkPath', form).val();
|
||||
config.QuickConnectAvailable = form.querySelector('#chkQuickConnectAvailable').checked;
|
||||
config.ParallelImageEncodingLimit = parseInt(form.querySelector('#txtParallelImageEncodingLimit').value || '0', 10);
|
||||
ApiClient.updateServerConfiguration(config).then(function() {
|
||||
ApiClient.getNamedConfiguration(brandingConfigKey).then(function(brandingConfig) {
|
||||
brandingConfig.LoginDisclaimer = form.querySelector('#txtLoginDisclaimer').value;
|
||||
brandingConfig.CustomCss = form.querySelector('#txtCustomCss').value;
|
||||
brandingConfig.SplashscreenEnabled = form.querySelector('#chkSplashScreenAvailable').checked;
|
||||
|
||||
ApiClient.updateServerConfiguration(config).then(function() {
|
||||
ApiClient.getNamedConfiguration(brandingConfigKey).then(function(brandingConfig) {
|
||||
brandingConfig.LoginDisclaimer = form.querySelector('#txtLoginDisclaimer').value;
|
||||
brandingConfig.CustomCss = form.querySelector('#txtCustomCss').value;
|
||||
brandingConfig.SplashscreenEnabled = form.querySelector('#chkSplashScreenAvailable').checked;
|
||||
|
||||
ApiClient.updateNamedConfiguration(brandingConfigKey, brandingConfig).then(function () {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
});
|
||||
}, function () {
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const brandingConfigKey = 'branding';
|
||||
export default function (view) {
|
||||
$('#btnSelectCachePath', view).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
view.querySelector('#txtCachePath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectServerCachePath'),
|
||||
instruction: globalize.translate('HeaderSelectServerCachePathHelp')
|
||||
ApiClient.updateNamedConfiguration(brandingConfigKey, brandingConfig).then(function () {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
});
|
||||
}, function () {
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
path: $('#txtMetadataPath', view).val(),
|
||||
networkSharePath: $('#txtMetadataNetworkPath', view).val(),
|
||||
callback: function (path, networkPath) {
|
||||
if (path) {
|
||||
$('#txtMetadataPath', view).val(path);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (networkPath) {
|
||||
$('#txtMetadataNetworkPath', view).val(networkPath);
|
||||
}
|
||||
const brandingConfigKey = 'branding';
|
||||
export default function (view) {
|
||||
$('#btnSelectCachePath', view).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
view.querySelector('#txtCachePath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectMetadataPath'),
|
||||
instruction: globalize.translate('HeaderSelectMetadataPathHelp'),
|
||||
enableNetworkSharePath: true
|
||||
});
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectServerCachePath'),
|
||||
instruction: globalize.translate('HeaderSelectServerCachePathHelp')
|
||||
});
|
||||
});
|
||||
$('.dashboardGeneralForm', view).off('submit', onSubmit).on('submit', onSubmit);
|
||||
view.addEventListener('viewshow', function () {
|
||||
const promiseConfig = ApiClient.getServerConfiguration();
|
||||
const promiseLanguageOptions = ApiClient.getJSON(ApiClient.getUrl('Localization/Options'));
|
||||
const promiseSystemInfo = ApiClient.getSystemInfo();
|
||||
Promise.all([promiseConfig, promiseLanguageOptions, promiseSystemInfo]).then(function (responses) {
|
||||
loadPage(view, responses[0], responses[1], responses[2]);
|
||||
});
|
||||
ApiClient.getNamedConfiguration(brandingConfigKey).then(function (config) {
|
||||
view.querySelector('#txtLoginDisclaimer').value = config.LoginDisclaimer || '';
|
||||
view.querySelector('#txtCustomCss').value = config.CustomCss || '';
|
||||
view.querySelector('#chkSplashScreenAvailable').checked = config.SplashscreenEnabled === true;
|
||||
});
|
||||
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
|
||||
import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
path: $('#txtMetadataPath', view).val(),
|
||||
networkSharePath: $('#txtMetadataNetworkPath', view).val(),
|
||||
callback: function (path, networkPath) {
|
||||
if (path) {
|
||||
$('#txtMetadataPath', view).val(path);
|
||||
}
|
||||
|
||||
if (networkPath) {
|
||||
$('#txtMetadataNetworkPath', view).val(networkPath);
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectMetadataPath'),
|
||||
instruction: globalize.translate('HeaderSelectMetadataPathHelp'),
|
||||
enableNetworkSharePath: true
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
$('.dashboardGeneralForm', view).off('submit', onSubmit).on('submit', onSubmit);
|
||||
view.addEventListener('viewshow', function () {
|
||||
const promiseConfig = ApiClient.getServerConfiguration();
|
||||
const promiseLanguageOptions = ApiClient.getJSON(ApiClient.getUrl('Localization/Options'));
|
||||
const promiseSystemInfo = ApiClient.getSystemInfo();
|
||||
Promise.all([promiseConfig, promiseLanguageOptions, promiseSystemInfo]).then(function (responses) {
|
||||
loadPage(view, responses[0], responses[1], responses[2]);
|
||||
});
|
||||
ApiClient.getNamedConfiguration(brandingConfigKey).then(function (config) {
|
||||
view.querySelector('#txtLoginDisclaimer').value = config.LoginDisclaimer || '';
|
||||
view.querySelector('#txtCustomCss').value = config.CustomCss || '';
|
||||
view.querySelector('#chkSplashScreenAvailable').checked = config.SplashscreenEnabled === true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
@@ -12,399 +12,396 @@ import Dashboard, { pageClassOn, pageIdOn } from '../../utils/dashboard';
|
||||
import confirm from '../../components/confirm/confirm';
|
||||
import cardBuilder from '../../components/cardbuilder/cardBuilder';
|
||||
|
||||
/* eslint-disable indent */
|
||||
|
||||
function addVirtualFolder(page) {
|
||||
import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({default: medialibrarycreator}) => {
|
||||
new medialibrarycreator({
|
||||
collectionTypeOptions: getCollectionTypeOptions().filter(function (f) {
|
||||
return !f.hidden;
|
||||
}),
|
||||
refresh: shouldRefreshLibraryAfterChanges(page)
|
||||
}).then(function (hasChanges) {
|
||||
if (hasChanges) {
|
||||
reloadLibrary(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function editVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/mediaLibraryEditor/mediaLibraryEditor').then(({default: medialibraryeditor}) => {
|
||||
new medialibraryeditor({
|
||||
refresh: shouldRefreshLibraryAfterChanges(page),
|
||||
library: virtualFolder
|
||||
}).then(function (hasChanges) {
|
||||
if (hasChanges) {
|
||||
reloadLibrary(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteVirtualFolder(page, virtualFolder) {
|
||||
let msg = globalize.translate('MessageAreYouSureYouWishToRemoveMediaFolder');
|
||||
|
||||
if (virtualFolder.Locations.length) {
|
||||
msg += '<br/><br/>' + globalize.translate('MessageTheFollowingLocationWillBeRemovedFromLibrary') + '<br/><br/>';
|
||||
msg += virtualFolder.Locations.join('<br/>');
|
||||
}
|
||||
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderRemoveMediaFolder'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(function () {
|
||||
const refreshAfterChange = shouldRefreshLibraryAfterChanges(page);
|
||||
ApiClient.removeVirtualFolder(virtualFolder.Name, refreshAfterChange).then(function () {
|
||||
function addVirtualFolder(page) {
|
||||
import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: medialibrarycreator }) => {
|
||||
new medialibrarycreator({
|
||||
collectionTypeOptions: getCollectionTypeOptions().filter(function (f) {
|
||||
return !f.hidden;
|
||||
}),
|
||||
refresh: shouldRefreshLibraryAfterChanges(page)
|
||||
}).then(function (hasChanges) {
|
||||
if (hasChanges) {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function editVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/mediaLibraryEditor/mediaLibraryEditor').then(({ default: medialibraryeditor }) => {
|
||||
new medialibraryeditor({
|
||||
refresh: shouldRefreshLibraryAfterChanges(page),
|
||||
library: virtualFolder
|
||||
}).then(function (hasChanges) {
|
||||
if (hasChanges) {
|
||||
reloadLibrary(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteVirtualFolder(page, virtualFolder) {
|
||||
let msg = globalize.translate('MessageAreYouSureYouWishToRemoveMediaFolder');
|
||||
|
||||
if (virtualFolder.Locations.length) {
|
||||
msg += '<br/><br/>' + globalize.translate('MessageTheFollowingLocationWillBeRemovedFromLibrary') + '<br/><br/>';
|
||||
msg += virtualFolder.Locations.join('<br/>');
|
||||
}
|
||||
|
||||
function refreshVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/refreshdialog/refreshdialog').then(({default: refreshDialog}) => {
|
||||
new refreshDialog({
|
||||
itemIds: [virtualFolder.ItemId],
|
||||
serverId: ApiClient.serverId(),
|
||||
mode: 'scan'
|
||||
}).show();
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderRemoveMediaFolder'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(function () {
|
||||
const refreshAfterChange = shouldRefreshLibraryAfterChanges(page);
|
||||
ApiClient.removeVirtualFolder(virtualFolder.Name, refreshAfterChange).then(function () {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renameVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/prompt/prompt').then(({default: prompt}) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelNewName'),
|
||||
description: globalize.translate('MessageRenameMediaFolder'),
|
||||
confirmText: globalize.translate('ButtonRename')
|
||||
}).then(function (newName) {
|
||||
if (newName && newName != virtualFolder.Name) {
|
||||
const refreshAfterChange = shouldRefreshLibraryAfterChanges(page);
|
||||
ApiClient.renameVirtualFolder(virtualFolder.Name, newName, refreshAfterChange).then(function () {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
function refreshVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/refreshdialog/refreshdialog').then(({ default: refreshDialog }) => {
|
||||
new refreshDialog({
|
||||
itemIds: [virtualFolder.ItemId],
|
||||
serverId: ApiClient.serverId(),
|
||||
mode: 'scan'
|
||||
}).show();
|
||||
});
|
||||
}
|
||||
|
||||
function renameVirtualFolder(page, virtualFolder) {
|
||||
import('../../components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelNewName'),
|
||||
description: globalize.translate('MessageRenameMediaFolder'),
|
||||
confirmText: globalize.translate('ButtonRename')
|
||||
}).then(function (newName) {
|
||||
if (newName && newName != virtualFolder.Name) {
|
||||
const refreshAfterChange = shouldRefreshLibraryAfterChanges(page);
|
||||
ApiClient.renameVirtualFolder(virtualFolder.Name, newName, refreshAfterChange).then(function () {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showCardMenu(page, elem, virtualFolders) {
|
||||
const card = dom.parentWithClass(elem, 'card');
|
||||
const index = parseInt(card.getAttribute('data-index'), 10);
|
||||
const virtualFolder = virtualFolders[index];
|
||||
const menuItems = [];
|
||||
menuItems.push({
|
||||
name: globalize.translate('EditImages'),
|
||||
id: 'editimages',
|
||||
icon: 'photo'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ManageLibrary'),
|
||||
id: 'edit',
|
||||
icon: 'folder'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonRename'),
|
||||
id: 'rename',
|
||||
icon: 'mode_edit'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ScanLibrary'),
|
||||
id: 'refresh',
|
||||
icon: 'refresh'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonRemove'),
|
||||
id: 'delete',
|
||||
icon: 'delete'
|
||||
});
|
||||
|
||||
import('../../components/actionSheet/actionSheet').then((actionsheet) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: elem,
|
||||
callback: function (resultId) {
|
||||
switch (resultId) {
|
||||
case 'edit':
|
||||
editVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'editimages':
|
||||
editImages(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'rename':
|
||||
renameVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
deleteVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'refresh':
|
||||
refreshVirtualFolder(page, virtualFolder);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reloadLibrary(page) {
|
||||
loading.show();
|
||||
ApiClient.getVirtualFolders().then(function (result) {
|
||||
reloadVirtualFolders(page, result);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldRefreshLibraryAfterChanges(page) {
|
||||
return page.id === 'mediaLibraryPage';
|
||||
}
|
||||
|
||||
function reloadVirtualFolders(page, virtualFolders) {
|
||||
let html = '';
|
||||
virtualFolders.push({
|
||||
Name: globalize.translate('ButtonAddMediaLibrary'),
|
||||
icon: 'add_circle',
|
||||
Locations: [],
|
||||
showType: false,
|
||||
showLocations: false,
|
||||
showMenu: false,
|
||||
showNameWithIcon: false,
|
||||
elementId: 'addLibrary'
|
||||
});
|
||||
|
||||
for (let i = 0; i < virtualFolders.length; i++) {
|
||||
const virtualFolder = virtualFolders[i];
|
||||
html += getVirtualFolderHtml(page, virtualFolder, i);
|
||||
}
|
||||
|
||||
function showCardMenu(page, elem, virtualFolders) {
|
||||
const card = dom.parentWithClass(elem, 'card');
|
||||
const divVirtualFolders = page.querySelector('#divVirtualFolders');
|
||||
divVirtualFolders.innerHTML = html;
|
||||
divVirtualFolders.classList.add('itemsContainer');
|
||||
divVirtualFolders.classList.add('vertical-wrap');
|
||||
$('.btnCardMenu', divVirtualFolders).on('click', function () {
|
||||
showCardMenu(page, this, virtualFolders);
|
||||
});
|
||||
divVirtualFolders.querySelector('#addLibrary').addEventListener('click', function () {
|
||||
addVirtualFolder(page);
|
||||
});
|
||||
$('.editLibrary', divVirtualFolders).on('click', function () {
|
||||
const card = $(this).parents('.card')[0];
|
||||
const index = parseInt(card.getAttribute('data-index'), 10);
|
||||
const virtualFolder = virtualFolders[index];
|
||||
const menuItems = [];
|
||||
menuItems.push({
|
||||
name: globalize.translate('EditImages'),
|
||||
id: 'editimages',
|
||||
icon: 'photo'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ManageLibrary'),
|
||||
id: 'edit',
|
||||
icon: 'folder'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonRename'),
|
||||
id: 'rename',
|
||||
icon: 'mode_edit'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ScanLibrary'),
|
||||
id: 'refresh',
|
||||
icon: 'refresh'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonRemove'),
|
||||
id: 'delete',
|
||||
icon: 'delete'
|
||||
});
|
||||
|
||||
import('../../components/actionSheet/actionSheet').then((actionsheet) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: elem,
|
||||
callback: function (resultId) {
|
||||
switch (resultId) {
|
||||
case 'edit':
|
||||
editVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'editimages':
|
||||
editImages(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'rename':
|
||||
renameVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
deleteVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'refresh':
|
||||
refreshVirtualFolder(page, virtualFolder);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reloadLibrary(page) {
|
||||
loading.show();
|
||||
ApiClient.getVirtualFolders().then(function (result) {
|
||||
reloadVirtualFolders(page, result);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldRefreshLibraryAfterChanges(page) {
|
||||
return page.id === 'mediaLibraryPage';
|
||||
}
|
||||
|
||||
function reloadVirtualFolders(page, virtualFolders) {
|
||||
let html = '';
|
||||
virtualFolders.push({
|
||||
Name: globalize.translate('ButtonAddMediaLibrary'),
|
||||
icon: 'add_circle',
|
||||
Locations: [],
|
||||
showType: false,
|
||||
showLocations: false,
|
||||
showMenu: false,
|
||||
showNameWithIcon: false,
|
||||
elementId: 'addLibrary'
|
||||
});
|
||||
|
||||
for (let i = 0; i < virtualFolders.length; i++) {
|
||||
const virtualFolder = virtualFolders[i];
|
||||
html += getVirtualFolderHtml(page, virtualFolder, i);
|
||||
if (virtualFolder.ItemId) {
|
||||
editVirtualFolder(page, virtualFolder);
|
||||
}
|
||||
});
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
const divVirtualFolders = page.querySelector('#divVirtualFolders');
|
||||
divVirtualFolders.innerHTML = html;
|
||||
divVirtualFolders.classList.add('itemsContainer');
|
||||
divVirtualFolders.classList.add('vertical-wrap');
|
||||
$('.btnCardMenu', divVirtualFolders).on('click', function () {
|
||||
showCardMenu(page, this, virtualFolders);
|
||||
function editImages(page, virtualFolder) {
|
||||
import('../../components/imageeditor/imageeditor').then((imageEditor) => {
|
||||
imageEditor.show({
|
||||
itemId: virtualFolder.ItemId,
|
||||
serverId: ApiClient.serverId()
|
||||
}).then(function () {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
divVirtualFolders.querySelector('#addLibrary').addEventListener('click', function () {
|
||||
addVirtualFolder(page);
|
||||
});
|
||||
$('.editLibrary', divVirtualFolders).on('click', function () {
|
||||
const card = $(this).parents('.card')[0];
|
||||
const index = parseInt(card.getAttribute('data-index'), 10);
|
||||
const virtualFolder = virtualFolders[index];
|
||||
});
|
||||
}
|
||||
|
||||
if (virtualFolder.ItemId) {
|
||||
editVirtualFolder(page, virtualFolder);
|
||||
}
|
||||
});
|
||||
loading.hide();
|
||||
function getLink(text, url) {
|
||||
return globalize.translate(text, '<a is="emby-linkbutton" class="button-link" href="' + url + '" target="_blank" data-autohide="true">', '</a>');
|
||||
}
|
||||
|
||||
function getCollectionTypeOptions() {
|
||||
return [{
|
||||
name: '',
|
||||
value: ''
|
||||
}, {
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'movies',
|
||||
message: getLink('MovieLibraryHelp', 'https://jellyfin.org/docs/general/server/media/movies')
|
||||
}, {
|
||||
name: globalize.translate('TabMusic'),
|
||||
value: 'music',
|
||||
message: getLink('MusicLibraryHelp', 'https://jellyfin.org/docs/general/server/media/music')
|
||||
}, {
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'tvshows',
|
||||
message: getLink('TvLibraryHelp', 'https://jellyfin.org/docs/general/server/media/shows')
|
||||
}, {
|
||||
name: globalize.translate('Books'),
|
||||
value: 'books',
|
||||
message: getLink('BookLibraryHelp', 'https://jellyfin.org/docs/general/server/media/books')
|
||||
}, {
|
||||
name: globalize.translate('HomeVideosPhotos'),
|
||||
value: 'homevideos'
|
||||
}, {
|
||||
name: globalize.translate('MusicVideos'),
|
||||
value: 'musicvideos'
|
||||
}, {
|
||||
name: globalize.translate('MixedMoviesShows'),
|
||||
value: 'mixed',
|
||||
message: globalize.translate('MessageUnsetContentHelp')
|
||||
}];
|
||||
}
|
||||
|
||||
function getVirtualFolderHtml(page, virtualFolder, index) {
|
||||
let html = '';
|
||||
let style = '';
|
||||
|
||||
if (page.classList.contains('wizardPage')) {
|
||||
style += 'min-width:33.3%;';
|
||||
}
|
||||
|
||||
function editImages(page, virtualFolder) {
|
||||
import('../../components/imageeditor/imageeditor').then((imageEditor) => {
|
||||
imageEditor.show({
|
||||
itemId: virtualFolder.ItemId,
|
||||
serverId: ApiClient.serverId()
|
||||
}).then(function () {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
const elementId = virtualFolder.elementId ? `id="${virtualFolder.elementId}" ` : '';
|
||||
html += '<div ' + elementId + 'class="card backdropCard scalableCard backdropCard-scalable" style="' + style + '" data-index="' + index + '" data-id="' + virtualFolder.ItemId + '">';
|
||||
|
||||
html += '<div class="cardBox visualCardBox">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
||||
html += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
html += '<div class="cardContent">';
|
||||
let imgUrl = '';
|
||||
|
||||
if (virtualFolder.PrimaryImageItemId) {
|
||||
imgUrl = ApiClient.getScaledImageUrl(virtualFolder.PrimaryImageItemId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.40),
|
||||
type: 'Primary'
|
||||
});
|
||||
}
|
||||
|
||||
function getLink(text, url) {
|
||||
return globalize.translate(text, '<a is="emby-linkbutton" class="button-link" href="' + url + '" target="_blank" data-autohide="true">', '</a>');
|
||||
let hasCardImageContainer;
|
||||
|
||||
if (imgUrl) {
|
||||
html += `<div class="cardImageContainer editLibrary ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()}" style="cursor:pointer">`;
|
||||
html += `<img src="${imgUrl}" style="width:100%" />`;
|
||||
hasCardImageContainer = true;
|
||||
} else if (!virtualFolder.showNameWithIcon) {
|
||||
html += `<div class="cardImageContainer editLibrary ${cardBuilder.getDefaultBackgroundClass()}" style="cursor:pointer;">`;
|
||||
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
|
||||
hasCardImageContainer = true;
|
||||
}
|
||||
|
||||
function getCollectionTypeOptions() {
|
||||
return [{
|
||||
name: '',
|
||||
value: ''
|
||||
}, {
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'movies',
|
||||
message: getLink('MovieLibraryHelp', 'https://jellyfin.org/docs/general/server/media/movies')
|
||||
}, {
|
||||
name: globalize.translate('TabMusic'),
|
||||
value: 'music',
|
||||
message: getLink('MusicLibraryHelp', 'https://jellyfin.org/docs/general/server/media/music')
|
||||
}, {
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'tvshows',
|
||||
message: getLink('TvLibraryHelp', 'https://jellyfin.org/docs/general/server/media/shows')
|
||||
}, {
|
||||
name: globalize.translate('Books'),
|
||||
value: 'books',
|
||||
message: getLink('BookLibraryHelp', 'https://jellyfin.org/docs/general/server/media/books')
|
||||
}, {
|
||||
name: globalize.translate('HomeVideosPhotos'),
|
||||
value: 'homevideos'
|
||||
}, {
|
||||
name: globalize.translate('MusicVideos'),
|
||||
value: 'musicvideos'
|
||||
}, {
|
||||
name: globalize.translate('MixedMoviesShows'),
|
||||
value: 'mixed',
|
||||
message: globalize.translate('MessageUnsetContentHelp')
|
||||
}];
|
||||
}
|
||||
|
||||
function getVirtualFolderHtml(page, virtualFolder, index) {
|
||||
let html = '';
|
||||
let style = '';
|
||||
|
||||
if (page.classList.contains('wizardPage')) {
|
||||
style += 'min-width:33.3%;';
|
||||
}
|
||||
|
||||
const elementId = virtualFolder.elementId ? `id="${virtualFolder.elementId}" ` : '';
|
||||
html += '<div ' + elementId + 'class="card backdropCard scalableCard backdropCard-scalable" style="' + style + '" data-index="' + index + '" data-id="' + virtualFolder.ItemId + '">';
|
||||
|
||||
html += '<div class="cardBox visualCardBox">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
||||
html += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
html += '<div class="cardContent">';
|
||||
let imgUrl = '';
|
||||
|
||||
if (virtualFolder.PrimaryImageItemId) {
|
||||
imgUrl = ApiClient.getScaledImageUrl(virtualFolder.PrimaryImageItemId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.40),
|
||||
type: 'Primary'
|
||||
});
|
||||
}
|
||||
|
||||
let hasCardImageContainer;
|
||||
|
||||
if (imgUrl) {
|
||||
html += `<div class="cardImageContainer editLibrary ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()}" style="cursor:pointer">`;
|
||||
html += `<img src="${imgUrl}" style="width:100%" />`;
|
||||
hasCardImageContainer = true;
|
||||
} else if (!virtualFolder.showNameWithIcon) {
|
||||
html += `<div class="cardImageContainer editLibrary ${cardBuilder.getDefaultBackgroundClass()}" style="cursor:pointer;">`;
|
||||
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
|
||||
hasCardImageContainer = true;
|
||||
}
|
||||
|
||||
if (hasCardImageContainer) {
|
||||
html += '<div class="cardIndicators backdropCardIndicators">';
|
||||
html += '<div is="emby-itemrefreshindicator"' + (virtualFolder.RefreshProgress || virtualFolder.RefreshStatus && virtualFolder.RefreshStatus !== 'Idle' ? '' : ' class="hide"') + ' data-progress="' + (virtualFolder.RefreshProgress || 0) + '" data-status="' + virtualFolder.RefreshStatus + '"></div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (!imgUrl && virtualFolder.showNameWithIcon) {
|
||||
html += '<h3 class="cardImageContainer addLibrary" style="position:absolute;top:0;left:0;right:0;bottom:0;cursor:pointer;flex-direction:column;">';
|
||||
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
|
||||
|
||||
if (virtualFolder.showNameWithIcon) {
|
||||
html += '<div style="margin:1em 0;position:width:100%;">';
|
||||
html += escapeHtml(virtualFolder.Name);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</h3>';
|
||||
}
|
||||
|
||||
if (hasCardImageContainer) {
|
||||
html += '<div class="cardIndicators backdropCardIndicators">';
|
||||
html += '<div is="emby-itemrefreshindicator"' + (virtualFolder.RefreshProgress || virtualFolder.RefreshStatus && virtualFolder.RefreshStatus !== 'Idle' ? '' : ' class="hide"') + ' data-progress="' + (virtualFolder.RefreshProgress || 0) + '" data-status="' + virtualFolder.RefreshStatus + '"></div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">'; // always show menu unless explicitly hidden
|
||||
}
|
||||
|
||||
if (virtualFolder.showMenu !== false) {
|
||||
let dirTextAlign = 'right';
|
||||
if (globalize.getIsRTL())
|
||||
dirTextAlign = 'left';
|
||||
html += '<div style="text-align:' + dirTextAlign + '; float:' + dirTextAlign + ';padding-top:5px;">';
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += "<div class='cardText'>";
|
||||
if (!imgUrl && virtualFolder.showNameWithIcon) {
|
||||
html += '<h3 class="cardImageContainer addLibrary" style="position:absolute;top:0;left:0;right:0;bottom:0;cursor:pointer;flex-direction:column;">';
|
||||
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
|
||||
|
||||
if (virtualFolder.showNameWithIcon) {
|
||||
html += ' ';
|
||||
} else {
|
||||
html += '<div style="margin:1em 0;position:width:100%;">';
|
||||
html += escapeHtml(virtualFolder.Name);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</h3>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">'; // always show menu unless explicitly hidden
|
||||
|
||||
if (virtualFolder.showMenu !== false) {
|
||||
let dirTextAlign = 'right';
|
||||
if (globalize.getIsRTL())
|
||||
dirTextAlign = 'left';
|
||||
html += '<div style="text-align:' + dirTextAlign + '; float:' + dirTextAlign + ';padding-top:5px;">';
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
let typeName = getCollectionTypeOptions().filter(function (t) {
|
||||
return t.value == virtualFolder.CollectionType;
|
||||
})[0];
|
||||
typeName = typeName ? typeName.name : globalize.translate('Other');
|
||||
}
|
||||
|
||||
html += "<div class='cardText'>";
|
||||
|
||||
if (virtualFolder.showNameWithIcon) {
|
||||
html += ' ';
|
||||
} else {
|
||||
html += escapeHtml(virtualFolder.Name);
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
let typeName = getCollectionTypeOptions().filter(function (t) {
|
||||
return t.value == virtualFolder.CollectionType;
|
||||
})[0];
|
||||
typeName = typeName ? typeName.name : globalize.translate('Other');
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
|
||||
if (virtualFolder.showType === false) {
|
||||
html += ' ';
|
||||
} else {
|
||||
html += typeName;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
if (virtualFolder.showLocations === false) {
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
|
||||
if (virtualFolder.showType === false) {
|
||||
html += ' ';
|
||||
} else {
|
||||
html += typeName;
|
||||
}
|
||||
|
||||
html += ' ';
|
||||
html += '</div>';
|
||||
|
||||
if (virtualFolder.showLocations === false) {
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
html += ' ';
|
||||
html += '</div>';
|
||||
} else if (virtualFolder.Locations.length && virtualFolder.Locations.length === 1) {
|
||||
html += "<div class='cardText cardText-secondary' dir='ltr' style='text-align:left;'>";
|
||||
html += virtualFolder.Locations[0];
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
html += globalize.translate('NumLocationsValue', virtualFolder.Locations.length);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
} else if (virtualFolder.Locations.length && virtualFolder.Locations.length === 1) {
|
||||
html += "<div class='cardText cardText-secondary' dir='ltr' style='text-align:left;'>";
|
||||
html += virtualFolder.Locations[0];
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
html += globalize.translate('NumLocationsValue', virtualFolder.Locations.length);
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function getTabs() {
|
||||
return [{
|
||||
href: '#/library.html',
|
||||
name: globalize.translate('HeaderLibraries')
|
||||
}, {
|
||||
href: '#/librarydisplay.html',
|
||||
name: globalize.translate('Display')
|
||||
}, {
|
||||
href: '#/metadataimages.html',
|
||||
name: globalize.translate('Metadata')
|
||||
}, {
|
||||
href: '#/metadatanfo.html',
|
||||
name: globalize.translate('TabNfoSettings')
|
||||
}];
|
||||
}
|
||||
|
||||
window.WizardLibraryPage = {
|
||||
next: function () {
|
||||
Dashboard.navigate('wizardsettings.html');
|
||||
}
|
||||
};
|
||||
pageClassOn('pageshow', 'mediaLibraryPage', function () {
|
||||
reloadLibrary(this);
|
||||
});
|
||||
pageIdOn('pageshow', 'mediaLibraryPage', function () {
|
||||
libraryMenu.setTabs('librarysetup', 0, getTabs);
|
||||
|
||||
window.WizardLibraryPage = {
|
||||
next: function () {
|
||||
Dashboard.navigate('wizardsettings.html');
|
||||
}
|
||||
};
|
||||
pageClassOn('pageshow', 'mediaLibraryPage', function () {
|
||||
reloadLibrary(this);
|
||||
const page = this;
|
||||
taskButton({
|
||||
mode: 'on',
|
||||
progressElem: page.querySelector('.refreshProgress'),
|
||||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
pageIdOn('pageshow', 'mediaLibraryPage', function () {
|
||||
libraryMenu.setTabs('librarysetup', 0, getTabs);
|
||||
});
|
||||
pageIdOn('pagebeforehide', 'mediaLibraryPage', function () {
|
||||
const page = this;
|
||||
taskButton({
|
||||
mode: 'off',
|
||||
progressElem: page.querySelector('.refreshProgress'),
|
||||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
});
|
||||
|
||||
const page = this;
|
||||
taskButton({
|
||||
mode: 'on',
|
||||
progressElem: page.querySelector('.refreshProgress'),
|
||||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
});
|
||||
pageIdOn('pagebeforehide', 'mediaLibraryPage', function () {
|
||||
const page = this;
|
||||
taskButton({
|
||||
mode: 'off',
|
||||
progressElem: page.querySelector('.refreshProgress'),
|
||||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-enable indent */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user