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

Merge branch 'master' into fix_long_getItems_request_URL

This commit is contained in:
dann-merlin 2023-04-13 11:05:09 +00:00 committed by GitHub
commit 6d6d03a9c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
220 changed files with 21471 additions and 16761 deletions

View file

@ -64,7 +64,9 @@ module.exports = {
'no-var': ['error'], 'no-var': ['error'],
'no-void': ['error', { 'allowAsStatement': true }], 'no-void': ['error', { 'allowAsStatement': true }],
'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }], 'no-warning-comments': ['warn', { 'terms': ['fixme', 'hack', 'xxx'] }],
'object-curly-spacing': ['error', 'always'],
'one-var': ['error', 'never'], 'one-var': ['error', 'never'],
'operator-linebreak': ['error', 'before', { overrides: { '?': 'after', ':': 'after', '=': 'after' } }],
'padded-blocks': ['error', 'never'], 'padded-blocks': ['error', 'never'],
'prefer-const': ['error', { 'destructuring': 'all' }], 'prefer-const': ['error', { 'destructuring': 'all' }],
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
@ -267,7 +269,6 @@ module.exports = {
'no-useless-constructor': ['off'], 'no-useless-constructor': ['off'],
'@typescript-eslint/no-useless-constructor': ['error'], '@typescript-eslint/no-useless-constructor': ['error'],
'max-params': ['error', 7],
'sonarjs/cognitive-complexity': ['warn'] 'sonarjs/cognitive-complexity': ['warn']
} }
} }

View file

@ -17,4 +17,5 @@ jobs:
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0 - uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
with: with:
dirtyLabel: 'merge conflict' dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }} repoToken: ${{ secrets.JF_BOT_TOKEN }}

View file

@ -19,13 +19,13 @@ jobs:
language: [ 'javascript' ] language: [ 'javascript' ]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@16964e90ba004cdf0cd845b866b5df21038b7723 # v2.2.6 uses: github/codeql-action/init@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@16964e90ba004cdf0cd845b866b5df21038b7723 # v2.2.6 uses: github/codeql-action/autobuild@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16964e90ba004cdf0cd845b866b5df21038b7723 # v2.2.6 uses: github/codeql-action/analyze@04df1262e6247151b5ac09cd2c303ac36ad3f62b # v2.2.9

View file

@ -12,13 +12,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - 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: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0

View file

@ -13,7 +13,7 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
@ -37,7 +37,7 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
@ -58,7 +58,7 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
@ -82,7 +82,7 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0

View file

@ -1,18 +1,24 @@
name: Issue Stale Check name: Stale Check
on: on:
schedule: schedule:
- cron: '30 1 * * *' - cron: '30 1 * * *'
workflow_dispatch: workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs: jobs:
stale: issues:
name: Check issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7.0.0 - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
days-before-stale: 120 days-before-stale: 120
days-before-pr-stale: -1 days-before-pr-stale: -1
days-before-close: 21 days-before-close: 21
@ -25,3 +31,21 @@ jobs:
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://jellyfin.org/contact). This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://jellyfin.org/contact).
prs-conflicts:
name: Check PRs with merge conflicts
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
days-before-close: 90
days-before-issue-close: -1
stale-pr-label: merge conflict
close-pr-message: |-
This PR has been closed due to having unresolved merge conflicts.

View file

@ -13,7 +13,7 @@ jobs:
steps: steps:
- name: Check out Git repository - name: Check out Git repository
uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3 uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
- name: Setup node environment - name: Setup node environment
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0

View file

@ -60,6 +60,7 @@
- [edvwib](https://github.com/edvwib) - [edvwib](https://github.com/edvwib)
- [Rob Farraher](https://github.com/farraherbg) - [Rob Farraher](https://github.com/farraherbg)
- [Pier-Luc Ducharme](https://github.com/pl-ducharme) - [Pier-Luc Ducharme](https://github.com/pl-ducharme)
- [Anantharaju S](https://github.com/Anantharajus)
- [Merlin Sievers](https://github.com/dann-merlin) - [Merlin Sievers](https://github.com/dann-merlin)
# Emby Contributors # Emby Contributors

View file

@ -1,4 +1,4 @@
FROM centos:8 FROM quay.io/centos/centos:stream8
# Docker build arguments # Docker build arguments
ARG SOURCE_DIR=/jellyfin ARG SOURCE_DIR=/jellyfin

1178
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,8 @@
"repository": "https://github.com/jellyfin/jellyfin-web", "repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later", "license": "GPL-2.0-or-later",
"devDependencies": { "devDependencies": {
"@babel/core": "7.21.0", "@babel/core": "7.21.3",
"@babel/eslint-parser": "7.19.1", "@babel/eslint-parser": "7.21.3",
"@babel/eslint-plugin": "7.19.1", "@babel/eslint-plugin": "7.19.1",
"@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/plugin-proposal-private-methods": "7.18.6", "@babel/plugin-proposal-private-methods": "7.18.6",
@ -16,13 +16,13 @@
"@babel/preset-typescript": "7.21.0", "@babel/preset-typescript": "7.21.0",
"@types/escape-html": "1.0.2", "@types/escape-html": "1.0.2",
"@types/loadable__component": "5.13.4", "@types/loadable__component": "5.13.4",
"@types/lodash-es": "4.17.6", "@types/lodash-es": "4.17.7",
"@types/react": "17.0.53", "@types/react": "17.0.53",
"@types/react-dom": "17.0.19", "@types/react-dom": "17.0.19",
"@typescript-eslint/eslint-plugin": "5.54.1", "@typescript-eslint/eslint-plugin": "5.56.0",
"@typescript-eslint/parser": "5.54.1", "@typescript-eslint/parser": "5.56.0",
"@uupaa/dynamic-import-polyfill": "1.0.2", "@uupaa/dynamic-import-polyfill": "1.0.2",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.14",
"babel-loader": "9.1.2", "babel-loader": "9.1.2",
"babel-plugin-dynamic-import-polyfill": "1.0.0", "babel-plugin-dynamic-import-polyfill": "1.0.0",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
@ -32,7 +32,7 @@
"css-loader": "6.7.3", "css-loader": "6.7.3",
"cssnano": "5.1.15", "cssnano": "5.1.15",
"es-check": "7.1.0", "es-check": "7.1.0",
"eslint": "8.35.0", "eslint": "8.36.0",
"eslint-plugin-compat": "4.1.2", "eslint-plugin-compat": "4.1.2",
"eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
@ -40,29 +40,29 @@
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.32.2", "eslint-plugin-react": "7.32.2",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-sonarjs": "0.18.0", "eslint-plugin-sonarjs": "0.19.0",
"expose-loader": "4.0.0", "expose-loader": "4.1.0",
"html-loader": "4.2.0", "html-loader": "4.2.0",
"html-webpack-plugin": "5.5.0", "html-webpack-plugin": "5.5.0",
"mini-css-extract-plugin": "2.7.2", "mini-css-extract-plugin": "2.7.5",
"postcss": "8.4.21", "postcss": "8.4.21",
"postcss-loader": "7.0.2", "postcss-loader": "7.1.0",
"postcss-preset-env": "8.0.1", "postcss-preset-env": "8.0.1",
"postcss-scss": "4.0.6", "postcss-scss": "4.0.6",
"sass": "1.58.3", "sass": "1.60.0",
"sass-loader": "13.2.0", "sass-loader": "13.2.1",
"source-map-loader": "4.0.1", "source-map-loader": "4.0.1",
"style-loader": "3.3.1", "style-loader": "3.3.2",
"stylelint": "15.2.0", "stylelint": "15.3.0",
"stylelint-config-rational-order": "0.1.2", "stylelint-config-rational-order": "0.1.2",
"stylelint-no-browser-hacks": "1.2.1", "stylelint-no-browser-hacks": "1.2.1",
"stylelint-order": "6.0.3", "stylelint-order": "6.0.3",
"stylelint-scss": "4.4.0", "stylelint-scss": "4.5.0",
"ts-loader": "9.4.2", "ts-loader": "9.4.2",
"typescript": "4.9.5", "typescript": "5.0.2",
"webpack": "5.75.0", "webpack": "5.76.3",
"webpack-cli": "5.0.1", "webpack-cli": "5.0.1",
"webpack-dev-server": "4.11.1", "webpack-dev-server": "4.13.1",
"webpack-merge": "5.8.0", "webpack-merge": "5.8.0",
"workbox-webpack-plugin": "6.5.4", "workbox-webpack-plugin": "6.5.4",
"worker-loader": "3.0.8" "worker-loader": "3.0.8"
@ -80,29 +80,29 @@
"blurhash": "2.0.5", "blurhash": "2.0.5",
"classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz",
"classnames": "2.3.2", "classnames": "2.3.2",
"core-js": "3.29.0", "core-js": "3.29.1",
"date-fns": "2.29.3", "date-fns": "2.29.3",
"dompurify": "3.0.1", "dompurify": "3.0.1",
"epubjs": "0.4.2", "epubjs": "0.3.93",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"fast-text-encoding": "1.0.6", "fast-text-encoding": "1.0.6",
"flv.js": "1.6.2", "flv.js": "1.6.2",
"headroom.js": "0.12.0", "headroom.js": "0.12.0",
"history": "5.3.0", "history": "5.3.0",
"hls.js": "1.3.4", "hls.js": "1.2.4",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"jellyfin-apiclient": "1.10.0", "jellyfin-apiclient": "1.10.0",
"jquery": "3.6.3", "jquery": "3.6.4",
"jstree": "3.3.15", "jstree": "3.3.15",
"libarchive.js": "1.3.0", "libarchive.js": "1.3.0",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"marked": "4.2.12", "marked": "4.3.0",
"material-design-icons-iconfont": "6.7.0", "material-design-icons-iconfont": "6.7.0",
"native-promise-only": "0.8.1", "native-promise-only": "0.8.1",
"pdfjs-dist": "2.16.105", "pdfjs-dist": "2.16.105",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-router-dom": "6.8.2", "react-router-dom": "6.9.0",
"resize-observer-polyfill": "1.5.1", "resize-observer-polyfill": "1.5.1",
"screenfull": "6.0.2", "screenfull": "6.0.2",
"sortablejs": "1.15.0", "sortablejs": "1.15.0",

View file

@ -7,8 +7,8 @@ const config = () => ({
plugins: [ plugins: [
// Explicitly specify browserslist to override ones from node_modules // Explicitly specify browserslist to override ones from node_modules
// For example, Swiper has it in its package.json // For example, Swiper has it in its package.json
postcssPresetEnv({browsers: packageConfig.browserslist}), postcssPresetEnv({ browsers: packageConfig.browserslist }),
autoprefixer({overrideBrowserslist: packageConfig.browserslist}), autoprefixer({ overrideBrowserslist: packageConfig.browserslist }),
cssnano() cssnano()
] ]
}); });

View file

@ -3,13 +3,14 @@ import React from 'react';
import { HistoryRouter } from './components/HistoryRouter'; import { HistoryRouter } from './components/HistoryRouter';
import { ApiProvider } from './hooks/useApi'; import { ApiProvider } from './hooks/useApi';
import AppRoutes from './routes/index'; import { AppRoutes, ExperimentalAppRoutes } from './routes';
const App = ({ history }: { history: History }) => { const App = ({ history }: { history: History }) => {
const layoutMode = localStorage.getItem('layout');
return ( return (
<ApiProvider> <ApiProvider>
<HistoryRouter history={history}> <HistoryRouter history={history}>
<AppRoutes /> {layoutMode === 'experimental' ? <ExperimentalAppRoutes /> : <AppRoutes /> }
</HistoryRouter> </HistoryRouter>
</ApiProvider> </ApiProvider>
); );

View file

@ -37,7 +37,7 @@ import template from './accessSchedule.template.html';
context.querySelector('#selectEnd').innerHTML = html; context.querySelector('#selectEnd').innerHTML = html;
} }
function loadSchedule(context, {DayOfWeek, StartHour, EndHour}) { function loadSchedule(context, { DayOfWeek, StartHour, EndHour }) {
context.querySelector('#selectDay').value = DayOfWeek || 'Sunday'; context.querySelector('#selectDay').value = DayOfWeek || 'Sunday';
context.querySelector('#selectStart').value = StartHour || 0; context.querySelector('#selectStart').value = StartHour || 0;
context.querySelector('#selectEnd').value = EndHour || 0; context.querySelector('#selectEnd').value = EndHour || 0;

View file

@ -309,8 +309,8 @@ function askForExit() {
exitPromise = actionsheet.show({ exitPromise = actionsheet.show({
title: globalize.translate('MessageConfirmAppExit'), title: globalize.translate('MessageConfirmAppExit'),
items: [ items: [
{id: 'yes', name: globalize.translate('Yes')}, { id: 'yes', name: globalize.translate('Yes') },
{id: 'no', name: globalize.translate('No')} { id: 'no', name: globalize.translate('No') }
] ]
}).then(function (value) { }).then(function (value) {
if (value === 'yes') { if (value === 'yes') {
@ -366,20 +366,20 @@ export const appHost = {
}; };
}, },
deviceName: function () { deviceName: function () {
return window.NativeShell?.AppHost?.deviceName return window.NativeShell?.AppHost?.deviceName ?
? window.NativeShell.AppHost.deviceName() : getDeviceName(); window.NativeShell.AppHost.deviceName() : getDeviceName();
}, },
deviceId: function () { deviceId: function () {
return window.NativeShell?.AppHost?.deviceId return window.NativeShell?.AppHost?.deviceId ?
? window.NativeShell.AppHost.deviceId() : getDeviceId(); window.NativeShell.AppHost.deviceId() : getDeviceId();
}, },
appName: function () { appName: function () {
return window.NativeShell?.AppHost?.appName return window.NativeShell?.AppHost?.appName ?
? window.NativeShell.AppHost.appName() : appName; window.NativeShell.AppHost.appName() : appName;
}, },
appVersion: function () { appVersion: function () {
return window.NativeShell?.AppHost?.appVersion return window.NativeShell?.AppHost?.appVersion ?
? window.NativeShell.AppHost.appVersion() : Package.version; window.NativeShell.AppHost.appVersion() : Package.version;
}, },
getPushTokenInfo: function () { getPushTokenInfo: function () {
return {}; return {};

View file

@ -896,13 +896,13 @@ import { appRouter } from '../appRouter';
} }
if (options.showYear || options.showSeriesYear) { if (options.showYear || options.showSeriesYear) {
const productionYear = item.ProductionYear && datetime.toLocaleString(item.ProductionYear, {useGrouping: false}); const productionYear = item.ProductionYear && datetime.toLocaleString(item.ProductionYear, { useGrouping: false });
if (item.Type === 'Series') { if (item.Type === 'Series') {
if (item.Status === 'Continuing') { if (item.Status === 'Continuing') {
lines.push(globalize.translate('SeriesYearToPresent', productionYear || '')); lines.push(globalize.translate('SeriesYearToPresent', productionYear || ''));
} else { } else {
if (item.EndDate && item.ProductionYear) { if (item.EndDate && item.ProductionYear) {
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), {useGrouping: false}); const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false });
lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear))); lines.push(productionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear)));
} else { } else {
lines.push(productionYear || ''); lines.push(productionYear || '');

View file

@ -28,7 +28,7 @@ import ServerConnections from '../ServerConnections';
} }
const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || []; const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || [];
const videoStream = mediaStreams.filter(({Type}) => { const videoStream = mediaStreams.filter(({ Type }) => {
return Type === 'Video'; return Type === 'Video';
})[0] || {}; })[0] || {};
@ -68,7 +68,7 @@ import ServerConnections from '../ServerConnections';
return html; return html;
} }
function getImgUrl({Id}, {ImageTag}, index, maxWidth, apiClient) { function getImgUrl({ Id }, { ImageTag }, index, maxWidth, apiClient) {
if (ImageTag) { if (ImageTag) {
return apiClient.getScaledImageUrl(Id, { return apiClient.getScaledImageUrl(Id, {
@ -82,7 +82,7 @@ import ServerConnections from '../ServerConnections';
return null; return null;
} }
function buildChapterCard(item, apiClient, chapter, index, {width, coverImage}, className, shape) { function buildChapterCard(item, apiClient, chapter, index, { width, coverImage }, className, shape) {
const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient); const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient);
let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer'; let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer';

View file

@ -22,7 +22,7 @@ const Filter: FC<FilterProps> = ({
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
const showFilterMenu = useCallback(() => { const showFilterMenu = useCallback(() => {
import('../filtermenu/filtermenu').then(({default: FilterMenu}) => { import('../filtermenu/filtermenu').then(({ default: FilterMenu }) => {
const filterMenu = new FilterMenu(); const filterMenu = new FilterMenu();
filterMenu.show({ filterMenu.show({
settings: viewQuerySettings, settings: viewQuerySettings,

View file

@ -6,7 +6,7 @@ const NewCollection: FC = () => {
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
const showCollectionEditor = useCallback(() => { const showCollectionEditor = useCallback(() => {
import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => { import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const serverId = window.ApiClient.serverId(); const serverId = window.ApiClient.serverId();
const collectionEditor = new CollectionEditor(); const collectionEditor = new CollectionEditor();
collectionEditor.show({ collectionEditor.show({

View file

@ -16,7 +16,7 @@ const SelectView: FC<SelectViewProps> = ({
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
const showViewSettingsMenu = useCallback(() => { const showViewSettingsMenu = useCallback(() => {
import('../viewSettings/viewSettings').then(({default: ViewSettings}) => { import('../viewSettings/viewSettings').then(({ default: ViewSettings }) => {
const viewsettings = new ViewSettings(); const viewsettings = new ViewSettings();
viewsettings.show({ viewsettings.show({
settings: viewQuerySettings, settings: viewQuerySettings,

View file

@ -19,7 +19,7 @@ const Sort: FC<SortProps> = ({
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
const showSortMenu = useCallback(() => { const showSortMenu = useCallback(() => {
import('../sortmenu/sortmenu').then(({default: SortMenu}) => { import('../sortmenu/sortmenu').then(({ default: SortMenu }) => {
const sortMenu = new SortMenu(); const sortMenu = new SortMenu();
sortMenu.show({ sortMenu.show({
settings: viewQuerySettings, settings: viewQuerySettings,

View file

@ -14,7 +14,7 @@ type IProps = {
children?: React.ReactNode 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 ( return (
<div className={containerClassName}> <div className={containerClassName}>
<h2>{globalize.translate(headerTitle)}</h2> <h2>{globalize.translate(headerTitle)}</h2>

View file

@ -22,7 +22,7 @@ function getDisplayTime(hours = 0) {
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 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 ( return (
<div <div
className='liSchedule listItem' className='liSchedule listItem'

View file

@ -5,7 +5,7 @@ type IProps = {
tag?: string; tag?: string;
} }
const BlockedTagList: FunctionComponent<IProps> = ({tag}: IProps) => { const BlockedTagList: FunctionComponent<IProps> = ({ tag }: IProps) => {
return ( return (
<div className='paperList'> <div className='paperList'>
<div className='listItem'> <div className='listItem'>

View file

@ -36,7 +36,7 @@ const createLinkElement = (activeTab: string) => ({
</a>` </a>`
}); });
const SectionTabs: FunctionComponent<IProps> = ({activeTab}: IProps) => { const SectionTabs: FunctionComponent<IProps> = ({ activeTab }: IProps) => {
return ( return (
<div <div
data-role='controlgroup' data-role='controlgroup'

View file

@ -74,7 +74,7 @@ const UserCardBox: FunctionComponent<IProps> = ({ user = {} }: IProps) => {
</div> </div>
<div className='cardFooter visualCardBox-cardFooter'> <div className='cardFooter visualCardBox-cardFooter'>
<div <div
style={{textAlign: 'right', float: 'right', paddingTop: '5px'}} style={{ textAlign: 'right', float: 'right', paddingTop: '5px' }}
> >
<IconButtonElement <IconButtonElement
is='paper-icon-button-light' is='paper-icon-button-light'

View file

@ -14,7 +14,7 @@ type IProps = {
userId: string; userId: string;
} }
const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => { const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
const element = useRef<HTMLDivElement>(null); const element = useRef<HTMLDivElement>(null);
const loadUser = useCallback(() => { const loadUser = useCallback(() => {
@ -76,7 +76,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false; chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
import('../../autoFocuser').then(({default: autoFocuser}) => { import('../../autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page); autoFocuser.autoFocus(page);
}); });
}); });
@ -214,7 +214,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
<div ref={element}> <div ref={element}>
<form <form
className='updatePasswordForm passwordSection hide' className='updatePasswordForm passwordSection hide'
style={{margin: '0 auto 2em'}} style={{ margin: '0 auto 2em' }}
> >
<div className='detailSection'> <div className='detailSection'>
<div id='fldCurrentPassword' className='inputContainer hide'> <div id='fldCurrentPassword' className='inputContainer hide'>
@ -260,7 +260,7 @@ const UserPasswordForm: FunctionComponent<IProps> = ({userId}: IProps) => {
<br /> <br />
<form <form
className='localAccessForm localAccessSection' className='localAccessForm localAccessSection'
style={{margin: '0 auto'}} style={{ margin: '0 auto' }}
> >
<div className='detailSection'> <div className='detailSection'>
<div className='detailSectionHeader'> <div className='detailSectionHeader'>

View file

@ -250,7 +250,6 @@ import '../../styles/scrollstyles.scss';
} }
function isOpened(dlg) { function isOpened(dlg) {
//return dlg.opened;
return !dlg.classList.contains('hide'); return !dlg.classList.contains('hide');
} }

View file

@ -297,10 +297,8 @@ class FilterMenu {
} }
if (submitted) { if (submitted) {
//if (!options.onChange) {
saveValues(dlg, options.settings, options.settingsKey, options.setfilters); saveValues(dlg, options.settings, options.settingsKey, options.setfilters);
return resolve(); return resolve();
//}
} }
return resolve(); return resolve();
}); });

View file

@ -29,7 +29,7 @@ import ServerConnections from '../ServerConnections';
import template from './tvguide.template.html'; import template from './tvguide.template.html';
function showViewSettings(instance) { function showViewSettings(instance) {
import('./guide-settings').then(({default: guideSettingsDialog}) => { import('./guide-settings').then(({ default: guideSettingsDialog }) => {
guideSettingsDialog.show(instance.categoryOptions).then(function () { guideSettingsDialog.show(instance.categoryOptions).then(function () {
instance.refresh(); instance.refresh();
}); });

View file

@ -26,8 +26,8 @@ import Events from '../utils/events.ts';
function canPlayNativeHls() { function canPlayNativeHls() {
const media = document.createElement('video'); const media = document.createElement('video');
return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '') || return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '')
media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, '')); || media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, ''));
} }
export function enableHlsJsPlayer(runTimeTicks, mediaType) { export function enableHlsJsPlayer(runTimeTicks, mediaType) {
@ -51,18 +51,10 @@ import Events from '../utils/events.ts';
return true; return true;
} }
if (browser.edge && mediaType === 'Video') {
//return true;
}
// simple playback should use the native support // simple playback should use the native support
if (runTimeTicks) { if (runTimeTicks) {
//if (!browser.edge) {
return false; return false;
//}
} }
//return false;
} }
return true; return true;
@ -201,8 +193,8 @@ import Events from '../utils/events.ts';
.catch((e) => { .catch((e) => {
const errorName = (e.name || '').toLowerCase(); const errorName = (e.name || '').toLowerCase();
// safari uses aborterror // safari uses aborterror
if (errorName === 'notallowederror' || if (errorName === 'notallowederror'
errorName === 'aborterror') { || errorName === 'aborterror') {
// swallow this error because the user can still click the play button on the video element // swallow this error because the user can still click the play button on the video element
return Promise.resolve(); return Promise.resolve();
} }

View file

@ -282,7 +282,7 @@ import template from './imageeditor.template.html';
const providerCount = parseInt(imageCard.getAttribute('data-providers'), 10); const providerCount = parseInt(imageCard.getAttribute('data-providers'), 10);
const numImages = parseInt(imageCard.getAttribute('data-numimages'), 10); const numImages = parseInt(imageCard.getAttribute('data-numimages'), 10);
import('../actionSheet/actionSheet').then(({default: actionSheet}) => { import('../actionSheet/actionSheet').then(({ default: actionSheet }) => {
const commands = []; const commands = [];
commands.push({ commands.push({
@ -353,7 +353,7 @@ import template from './imageeditor.template.html';
addListeners(context, 'btnOpenUploadMenu', 'click', function () { addListeners(context, 'btnOpenUploadMenu', 'click', function () {
const imageType = this.getAttribute('data-imagetype'); const imageType = this.getAttribute('data-imagetype');
import('../imageUploader/imageUploader').then(({default: imageUploader}) => { import('../imageUploader/imageUploader').then(({ default: imageUploader }) => {
imageUploader.show({ imageUploader.show({
theme: options.theme, theme: options.theme,

View file

@ -326,7 +326,7 @@ import toast from './toast/toast';
// eslint-disable-next-line sonarjs/max-switch-cases // eslint-disable-next-line sonarjs/max-switch-cases
switch (id) { switch (id) {
case 'addtocollection': case 'addtocollection':
import('./collectionEditor/collectionEditor').then(({default: CollectionEditor}) => { import('./collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const collectionEditor = new CollectionEditor(); const collectionEditor = new CollectionEditor();
collectionEditor.show({ collectionEditor.show({
items: [itemId], items: [itemId],
@ -335,7 +335,7 @@ import toast from './toast/toast';
}); });
break; break;
case 'addtoplaylist': case 'addtoplaylist':
import('./playlisteditor/playlisteditor').then(({default: playlistEditor}) => { import('./playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
new playlistEditor({ new playlistEditor({
items: [itemId], items: [itemId],
serverId: serverId serverId: serverId
@ -408,7 +408,7 @@ import toast from './toast/toast';
break; break;
} }
case 'editsubtitles': case 'editsubtitles':
import('./subtitleeditor/subtitleeditor').then(({default: subtitleEditor}) => { import('./subtitleeditor/subtitleeditor').then(({ default: subtitleEditor }) => {
subtitleEditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); subtitleEditor.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
}); });
break; break;
@ -464,7 +464,7 @@ import toast from './toast/toast';
playbackManager.clearQueue(); playbackManager.clearQueue();
break; break;
case 'record': case 'record':
import('./recordingcreator/recordingcreator').then(({default: recordingCreator}) => { import('./recordingcreator/recordingcreator').then(({ default: recordingCreator }) => {
recordingCreator.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); recordingCreator.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
}); });
break; break;
@ -535,7 +535,7 @@ import toast from './toast/toast';
} }
function deleteTimer(apiClient, item, resolve, command) { function deleteTimer(apiClient, item, resolve, command) {
import('./recordingcreator/recordinghelper').then(({default: recordingHelper}) => { import('./recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
const timerId = item.TimerId || item.Id; const timerId = item.TimerId || item.Id;
recordingHelper.cancelTimerWithConfirmation(timerId, item.ServerId).then(function () { recordingHelper.cancelTimerWithConfirmation(timerId, item.ServerId).then(function () {
getResolveFunction(resolve, command, true)(); getResolveFunction(resolve, command, true)();
@ -544,7 +544,7 @@ import toast from './toast/toast';
} }
function deleteSeriesTimer(apiClient, item, resolve, command) { function deleteSeriesTimer(apiClient, item, resolve, command) {
import('./recordingcreator/recordinghelper').then(({default: recordingHelper}) => { import('./recordingcreator/recordinghelper').then(({ default: recordingHelper }) => {
recordingHelper.cancelSeriesTimerWithConfirmation(item.Id, item.ServerId).then(function () { recordingHelper.cancelSeriesTimerWithConfirmation(item.Id, item.ServerId).then(function () {
getResolveFunction(resolve, command, true)(); getResolveFunction(resolve, command, true)();
}); });
@ -585,15 +585,15 @@ import toast from './toast/toast';
const serverId = apiClient.serverInfo().Id; const serverId = apiClient.serverInfo().Id;
if (item.Type === 'Timer') { if (item.Type === 'Timer') {
import('./recordingcreator/recordingeditor').then(({default: recordingEditor}) => { import('./recordingcreator/recordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(item.Id, serverId).then(resolve, reject); recordingEditor.show(item.Id, serverId).then(resolve, reject);
}); });
} else if (item.Type === 'SeriesTimer') { } else if (item.Type === 'SeriesTimer') {
import('./recordingcreator/seriesrecordingeditor').then(({default: recordingEditor}) => { import('./recordingcreator/seriesrecordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(item.Id, serverId).then(resolve, reject); recordingEditor.show(item.Id, serverId).then(resolve, reject);
}); });
} else { } else {
import('./metadataEditor/metadataEditor').then(({default: metadataEditor}) => { import('./metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
metadataEditor.show(item.Id, serverId).then(resolve, reject); metadataEditor.show(item.Id, serverId).then(resolve, reject);
}); });
} }
@ -614,7 +614,7 @@ import toast from './toast/toast';
} }
function refresh(apiClient, item) { function refresh(apiClient, item) {
import('./refreshdialog/refreshdialog').then(({default: refreshDialog}) => { import('./refreshdialog/refreshdialog').then(({ default: refreshDialog }) => {
new refreshDialog({ new refreshDialog({
itemIds: [item.Id], itemIds: [item.Id],
serverId: apiClient.serverInfo().Id, serverId: apiClient.serverInfo().Id,

View file

@ -167,7 +167,7 @@ import datetime from '../../scripts/datetime';
lines.push(escapeHtml(identifyResult.Name)); lines.push(escapeHtml(identifyResult.Name));
if (identifyResult.ProductionYear) { if (identifyResult.ProductionYear) {
lines.push(datetime.toLocaleString(identifyResult.ProductionYear, {useGrouping: false})); lines.push(datetime.toLocaleString(identifyResult.ProductionYear, { useGrouping: false }));
} }
let resultHtml = lines.join('<br/>'); let resultHtml = lines.join('<br/>');

View file

@ -316,7 +316,7 @@ import template from './libraryoptionseditor.template.html';
} }
function showImageOptionsForType(type) { function showImageOptionsForType(type) {
import('../imageOptionsEditor/imageOptionsEditor').then(({default: ImageOptionsEditor}) => { import('../imageOptionsEditor/imageOptionsEditor').then(({ default: ImageOptionsEditor }) => {
let typeOptions = getTypeOptions(currentLibraryOptions, type); let typeOptions = getTypeOptions(currentLibraryOptions, type);
if (!typeOptions) { if (!typeOptions) {
typeOptions = { typeOptions = {

View file

@ -65,7 +65,7 @@ import '../elements/emby-button/emby-button';
} }
}; };
import('../scripts/touchHelper').then(({default: TouchHelper}) => { import('../scripts/touchHelper').then(({ default: TouchHelper }) => {
const touchHelper = new TouchHelper(view.parentNode.parentNode); const touchHelper = new TouchHelper(view.parentNode.parentNode);
Events.on(touchHelper, 'swipeleft', onSwipeLeft); Events.on(touchHelper, 'swipeleft', onSwipeLeft);

View file

@ -25,7 +25,7 @@ import toast from '../toast/toast';
import alert from '../alert'; import alert from '../alert';
import template from './mediaLibraryCreator.template.html'; import template from './mediaLibraryCreator.template.html';
function onAddLibrary() { function onAddLibrary(e) {
if (isCreating) { if (isCreating) {
return false; return false;
} }
@ -62,7 +62,7 @@ import template from './mediaLibraryCreator.template.html';
isCreating = false; isCreating = false;
loading.hide(); loading.hide();
}); });
return false; e.preventDefault();
} }
function getCollectionTypeOptionsHtml(collectionTypeOptions) { function getCollectionTypeOptionsHtml(collectionTypeOptions) {
@ -96,14 +96,14 @@ import template from './mediaLibraryCreator.template.html';
$('.collectionTypeFieldDescription', dlg).html(folderOption?.message || ''); $('.collectionTypeFieldDescription', dlg).html(folderOption?.message || '');
}); });
page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick); page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
page.querySelector('.btnSubmit').addEventListener('click', onAddLibrary); page.querySelector('.addLibraryForm').addEventListener('submit', onAddLibrary);
page.querySelector('.folderList').addEventListener('click', onRemoveClick); page.querySelector('.folderList').addEventListener('click', onRemoveClick);
} }
function onAddButtonClick() { function onAddButtonClick() {
const page = dom.parentWithClass(this, 'dlg-librarycreator'); const page = dom.parentWithClass(this, 'dlg-librarycreator');
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
enableNetworkSharePath: true, enableNetworkSharePath: true,

View file

@ -1,9 +1,10 @@
<div class="formDialogHeader"> <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> <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> <h3 class="formDialogHeaderTitle">${ButtonAddMediaLibrary}</h3>
</div> </div>
<div class="formDialogContent scrollY" style="padding-top:2em;"> <div class="formDialogContent scrollY" style="padding-top:2em;">
<div class="dialogContentInner dialog-content-centered"> <div class="dialogContentInner dialog-content-centered">
<div id="fldCollectionType" class="selectContainer"> <div id="fldCollectionType" class="selectContainer">
@ -28,10 +29,11 @@
<div class="libraryOptions"></div> <div class="libraryOptions"></div>
</div> </div>
</div> </div>
<div class="formDialogFooter"> <div class="formDialogFooter">
<button is="emby-button" type="button" class="raised btnSubmit button-submit block formDialogFooterItem"> <button is="emby-button" type="submit" class="raised btnSubmit button-submit block formDialogFooterItem">
<span>${ButtonOk}</span> <span>${ButtonOk}</span>
</button> </button>
</div> </div>
</form>

View file

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

View file

@ -177,13 +177,13 @@ import * as userSettings from '../../scripts/settings/userSettings';
if (options.year !== false && item.ProductionYear && item.Type === 'Series') { if (options.year !== false && item.ProductionYear && item.Type === 'Series') {
if (item.Status === 'Continuing') { if (item.Status === 'Continuing') {
miscInfo.push(globalize.translate('SeriesYearToPresent', datetime.toLocaleString(item.ProductionYear, {useGrouping: false}))); miscInfo.push(globalize.translate('SeriesYearToPresent', datetime.toLocaleString(item.ProductionYear, { useGrouping: false })));
} else if (item.ProductionYear) { } else if (item.ProductionYear) {
text = datetime.toLocaleString(item.ProductionYear, {useGrouping: false}); text = datetime.toLocaleString(item.ProductionYear, { useGrouping: false });
if (item.EndDate) { if (item.EndDate) {
try { try {
const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), {useGrouping: false}); const endYear = datetime.toLocaleString(datetime.parseISO8601Date(item.EndDate).getFullYear(), { useGrouping: false });
if (endYear !== item.ProductionYear) { if (endYear !== item.ProductionYear) {
text += `-${endYear}`; text += `-${endYear}`;
@ -253,7 +253,7 @@ import * as userSettings from '../../scripts/settings/userSettings';
miscInfo.push(item.ProductionYear); miscInfo.push(item.ProductionYear);
} else if (item.PremiereDate) { } else if (item.PremiereDate) {
try { try {
text = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), {useGrouping: false}); text = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), { useGrouping: false });
miscInfo.push(text); miscInfo.push(text);
} catch (e) { } catch (e) {
console.error('error parsing date:', item.PremiereDate); console.error('error parsing date:', item.PremiereDate);

View file

@ -211,7 +211,7 @@ import template from './metadataEditor.template.html';
} }
function addElementToList(source, sortCallback) { function addElementToList(source, sortCallback) {
import('../prompt/prompt').then(({default: prompt}) => { import('../prompt/prompt').then(({ default: prompt }) => {
prompt({ prompt({
label: 'Value:' label: 'Value:'
}).then(function (text) { }).then(function (text) {
@ -229,7 +229,7 @@ import template from './metadataEditor.template.html';
} }
function editPerson(context, person, index) { function editPerson(context, person, index) {
import('./personEditor').then(({default: personEditor}) => { import('./personEditor').then(({ default: personEditor }) => {
personEditor.show(person).then(function (updatedPerson) { personEditor.show(person).then(function (updatedPerson) {
const isNew = index === -1; const isNew = index === -1;
@ -253,7 +253,7 @@ import template from './metadataEditor.template.html';
} }
function showMoreMenu(context, button, user) { function showMoreMenu(context, button, user) {
import('../itemContextMenu').then(({default: itemContextMenu}) => { import('../itemContextMenu').then(({ default: itemContextMenu }) => {
const item = currentItem; const item = currentItem;
itemContextMenu.show({ itemContextMenu.show({
@ -588,12 +588,12 @@ import template from './metadataEditor.template.html';
hideElement('#collapsibleSpecialEpisodeInfo', context); hideElement('#collapsibleSpecialEpisodeInfo', context);
} }
if (item.Type === 'Person' || if (item.Type === 'Person'
item.Type === 'Genre' || || item.Type === 'Genre'
item.Type === 'Studio' || || item.Type === 'Studio'
item.Type === 'MusicGenre' || || item.Type === 'MusicGenre'
item.Type === 'TvChannel' || || item.Type === 'TvChannel'
item.Type === 'Book') { || item.Type === 'Book') {
hideElement('#peopleCollapsible', context); hideElement('#peopleCollapsible', context);
} else { } else {
showElement('#peopleCollapsible', context); showElement('#peopleCollapsible', context);

View file

@ -205,13 +205,6 @@ import datetime from '../../scripts/datetime';
if (user.Policy.EnableContentDownloading && appHost.supports('filedownload')) { if (user.Policy.EnableContentDownloading && appHost.supports('filedownload')) {
// Disabled because there is no callback for this item // Disabled because there is no callback for this item
/*
menuItems.push({
name: globalize.translate('Download'),
id: 'download',
icon: 'file_download'
});
*/
} }
if (user.Policy.IsAdministrator) { if (user.Policy.IsAdministrator) {
@ -267,7 +260,7 @@ import datetime from '../../scripts/datetime';
} }
break; break;
case 'addtocollection': case 'addtocollection':
import('../collectionEditor/collectionEditor').then(({default: CollectionEditor}) => { import('../collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const collectionEditor = new CollectionEditor(); const collectionEditor = new CollectionEditor();
collectionEditor.show({ collectionEditor.show({
items: items, items: items,
@ -308,7 +301,7 @@ import datetime from '../../scripts/datetime';
dispatchNeedsRefresh(); dispatchNeedsRefresh();
break; break;
case 'refresh': case 'refresh':
import('../refreshdialog/refreshdialog').then(({default: refreshDialog}) => { import('../refreshdialog/refreshdialog').then(({ default: refreshDialog }) => {
new refreshDialog({ new refreshDialog({
itemIds: items, itemIds: items,
serverId: serverId serverId: serverId

View file

@ -69,7 +69,7 @@ import shell from '../../scripts/shell';
const list = []; const list = [];
imageSizes.forEach((size) => { imageSizes.forEach((size) => {
const url = getImageUrl(item, {height: size}); const url = getImageUrl(item, { height: size });
if (url !== null) { if (url !== null) {
list.push(url); list.push(url);
} }

View file

@ -1039,7 +1039,6 @@ class PlaybackManager {
} }
} }
//var mediaType = item.MediaType;
return getPlayer(item, getDefaultPlayOptions()) != null; return getPlayer(item, getDefaultPlayOptions()) != null;
}; };

View file

@ -4,7 +4,8 @@ import Events from '../../utils/events.ts';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import { playbackManager } from '../playback/playbackmanager'; import { playbackManager } from '../playback/playbackmanager';
import playMethodHelper from '../playback/playmethodhelper'; 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 './playerstats.scss';
import ServerConnections from '../ServerConnections'; import ServerConnections from '../ServerConnections';
@ -325,6 +326,12 @@ import ServerConnections from '../ServerConnections';
} }
function getSyncPlayStats() { function getSyncPlayStats() {
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
if (!SyncPlay?.Manager.isSyncPlayEnabled()) {
return [];
}
const syncStats = []; const syncStats = [];
const stats = SyncPlay.Manager.getStats(); const stats = SyncPlay.Manager.getStats();
@ -422,10 +429,10 @@ import ServerConnections from '../ServerConnections';
name: globalize.translate('LabelOriginalMediaInfo') name: globalize.translate('LabelOriginalMediaInfo')
}); });
const apiClient = ServerConnections.getApiClient(playbackManager.currentItem(player).ServerId); const syncPlayStats = getSyncPlayStats();
if (SyncPlay.Manager.isSyncPlayEnabled() && apiClient.isMinServerVersion('10.6.0')) { if (syncPlayStats.length > 0) {
categories.push({ categories.push({
stats: getSyncPlayStats(), stats: syncPlayStats,
name: globalize.translate('LabelSyncPlayInfo') name: globalize.translate('LabelSyncPlayInfo')
}); });
} }

View file

@ -4,10 +4,12 @@ import dialogHelper from '../dialogHelper/dialogHelper';
import loading from '../loading/loading'; import loading from '../loading/loading';
import layoutManager from '../layoutManager'; import layoutManager from '../layoutManager';
import { playbackManager } from '../playback/playbackmanager'; import { playbackManager } from '../playback/playbackmanager';
import SyncPlay from '../../plugins/syncPlay/core'; import { pluginManager } from '../pluginManager';
import * as userSettings from '../../scripts/settings/userSettings'; import * as userSettings from '../../scripts/settings/userSettings';
import { appRouter } from '../appRouter'; import { appRouter } from '../appRouter';
import globalize from '../../scripts/globalize'; import globalize from '../../scripts/globalize';
import { PluginType } from '../../types/plugin.ts';
import '../../elements/emby-button/emby-button'; import '../../elements/emby-button/emby-button';
import '../../elements/emby-input/emby-input'; import '../../elements/emby-input/emby-input';
import '../../elements/emby-button/paper-icon-button-light'; import '../../elements/emby-button/paper-icon-button-light';
@ -117,10 +119,12 @@ import ServerConnections from '../ServerConnections';
}; };
const apiClient = ServerConnections.getApiClient(currentServerId); const apiClient = ServerConnections.getApiClient(currentServerId);
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => { apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
let html = ''; let html = '';
if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay.Manager.isSyncPlayEnabled()) { if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay?.Manager.isSyncPlayEnabled()) {
html += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`; html += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`;
} }

View file

@ -119,9 +119,14 @@ class PluginManager {
} }
ofType(type) { ofType(type) {
return this.pluginsList.filter((o) => { return this.pluginsList.filter(plugin => plugin.type === type);
return o.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) { #mapRoute(plugin, route) {

View file

@ -141,7 +141,7 @@ function onManageRecordingClick() {
} }
const self = this; const self = this;
import('./recordingeditor').then(({default: recordingEditor}) => { import('./recordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(self.TimerId, options.serverId, { recordingEditor.show(self.TimerId, options.serverId, {
enableCancel: false enableCancel: false
}).then(function () { }).then(function () {
@ -159,7 +159,7 @@ function onManageSeriesRecordingClick() {
const self = this; const self = this;
import('./seriesrecordingeditor').then(({default: seriesRecordingEditor}) => { import('./seriesrecordingeditor').then(({ default: seriesRecordingEditor }) => {
seriesRecordingEditor.show(self.SeriesTimerId, options.serverId, { seriesRecordingEditor.show(self.SeriesTimerId, options.serverId, {
enableCancel: false enableCancel: false

View file

@ -389,13 +389,13 @@ import layoutManager from './layoutManager';
if (xScroller !== yScroller) { if (xScroller !== yScroller) {
if (xScroller) { if (xScroller) {
scrollToHelper(xScroller, {left: scrollX, behavior: scrollBehavior}); scrollToHelper(xScroller, { left: scrollX, behavior: scrollBehavior });
} }
if (yScroller) { if (yScroller) {
scrollToHelper(yScroller, {top: scrollY, behavior: scrollBehavior}); scrollToHelper(yScroller, { top: scrollY, behavior: scrollBehavior });
} }
} else if (xScroller) { } else if (xScroller) {
scrollToHelper(xScroller, {left: scrollX, top: scrollY, behavior: scrollBehavior}); scrollToHelper(xScroller, { left: scrollX, top: scrollY, behavior: scrollBehavior });
} }
} }
@ -597,7 +597,7 @@ import layoutManager from './layoutManager';
setTimeout(function() { setTimeout(function() {
scrollToElement(e.target, useSmoothScroll()); scrollToElement(e.target, useSmoothScroll());
}, 0); }, 0);
}, {capture: true}); }, { capture: true });
} }
/* eslint-enable indent */ /* eslint-enable indent */

View file

@ -85,8 +85,8 @@ const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {
dangerouslySetInnerHTML={createInputElement()} dangerouslySetInnerHTML={createInputElement()}
/> />
</div> </div>
{layoutManager.tv && !browser.tv && {layoutManager.tv && !browser.tv
<AlphaPicker onAlphaPicked={onAlphaPicked} /> && <AlphaPicker onAlphaPicked={onAlphaPicked} />
} }
</div> </div>
); );

View file

@ -71,7 +71,7 @@ import toast from './toast/toast';
} }
function showProgramDialog(item) { function showProgramDialog(item) {
import('./recordingcreator/recordingcreator').then(({default:recordingCreator}) => { import('./recordingcreator/recordingcreator').then(({ default:recordingCreator }) => {
recordingCreator.show(item.Id, item.ServerId); recordingCreator.show(item.Id, item.ServerId);
}); });
} }
@ -272,7 +272,7 @@ import toast from './toast/toast';
} }
function addToPlaylist(item) { function addToPlaylist(item) {
import('./playlisteditor/playlisteditor').then(({default: playlistEditor}) => { import('./playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
new playlistEditor().show({ new playlistEditor().show({
items: [item.Id], items: [item.Id],
serverId: item.ServerId serverId: item.ServerId
@ -297,16 +297,16 @@ import toast from './toast/toast';
if (item.Type === 'Timer') { if (item.Type === 'Timer') {
if (item.ProgramId) { if (item.ProgramId) {
import('./recordingcreator/recordingcreator').then(({default: recordingCreator}) => { import('./recordingcreator/recordingcreator').then(({ default: recordingCreator }) => {
recordingCreator.show(item.ProgramId, currentServerId).then(resolve, reject); recordingCreator.show(item.ProgramId, currentServerId).then(resolve, reject);
}); });
} else { } else {
import('./recordingcreator/recordingeditor').then(({default: recordingEditor}) => { import('./recordingcreator/recordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(item.Id, currentServerId).then(resolve, reject); recordingEditor.show(item.Id, currentServerId).then(resolve, reject);
}); });
} }
} else { } else {
import('./metadataEditor/metadataEditor').then(({default: metadataEditor}) => { import('./metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
metadataEditor.show(item.Id, currentServerId).then(resolve, reject); metadataEditor.show(item.Id, currentServerId).then(resolve, reject);
}); });
} }

View file

@ -342,7 +342,7 @@ function showDownloadOptions(button, context, subtitleId) {
} }
function centerFocus(elem, horiz, on) { function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then(({default: scrollHelper}) => { import('../../scripts/scrollHelper').then(({ default: scrollHelper }) => {
const fn = on ? 'on' : 'off'; const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz); scrollHelper.centerFocus[fn](elem, horiz);
}); });
@ -353,7 +353,7 @@ function onOpenUploadMenu(e) {
const selectLanguage = dialog.querySelector('#selectLanguage'); const selectLanguage = dialog.querySelector('#selectLanguage');
const apiClient = ServerConnections.getApiClient(currentItem.ServerId); const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
import('../subtitleuploader/subtitleuploader').then(({default: subtitleUploader}) => { import('../subtitleuploader/subtitleuploader').then(({ default: subtitleUploader }) => {
subtitleUploader.show({ subtitleUploader.show({
languages: { languages: {
list: selectLanguage.innerHTML, list: selectLanguage.innerHTML,

View file

@ -94,9 +94,9 @@ function init(instance) {
subtitleSyncSlider.getBubbleHtml = function (value) { subtitleSyncSlider.getBubbleHtml = function (value) {
const newOffset = getOffsetFromPercentage(value); const newOffset = getOffsetFromPercentage(value);
return '<h1 class="sliderBubbleText">' + return '<h1 class="sliderBubbleText">'
(newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's' + + (newOffset > 0 ? '+' : '') + parseFloat(newOffset) + 's'
'</h1>'; + '</h1>';
}; };
subtitleSyncCloseButton.addEventListener('click', function () { subtitleSyncCloseButton.addEventListener('click', function () {

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

View file

@ -48,7 +48,7 @@ function refreshTunerDevices(page, providerInfo, devices) {
function onSelectPathClick(e) { function onSelectPathClick(e) {
const page = $(e.target).parents('.xmltvForm')[0]; const page = $(e.target).parents('.xmltvForm')[0];
import('../directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
includeFiles: true, includeFiles: true,

View file

@ -97,7 +97,7 @@ function dispatchViewEvent(view, eventInfo, eventName, isCancellable) {
return eventResult; return eventResult;
} }
function getViewEventDetail(view, {state, url, options = {}}, isRestored) { function getViewEventDetail(view, { state, url, options = {} }, isRestored) {
const index = url.indexOf('?'); const index = url.indexOf('?');
// eslint-disable-next-line compat/compat // eslint-disable-next-line compat/compat
const searchParams = new URLSearchParams(url.substring(index + 1)); const searchParams = new URLSearchParams(url.substring(index + 1));

View file

@ -52,7 +52,7 @@ import { pageIdOn } from '../../utils/dashboard';
} }
function showNewKeyPrompt(page) { function showNewKeyPrompt(page) {
import('../../components/prompt/prompt').then(({default: prompt}) => { import('../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({ prompt({
title: globalize.translate('HeaderNewApiKey'), title: globalize.translate('HeaderNewApiKey'),
label: globalize.translate('LabelAppName'), label: globalize.translate('LabelAppName'),

View file

@ -65,7 +65,7 @@ import confirm from '../../components/confirm/confirm';
} }
function showSendMessageForm(btn, session) { function showSendMessageForm(btn, session) {
import('../../components/prompt/prompt').then(({default: prompt}) => { import('../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({ prompt({
title: globalize.translate('HeaderSendMessage'), title: globalize.translate('HeaderSendMessage'),
label: globalize.translate('LabelMessageText'), label: globalize.translate('LabelMessageText'),
@ -82,7 +82,7 @@ import confirm from '../../components/confirm/confirm';
} }
function showOptionsMenu(btn, session) { function showOptionsMenu(btn, session) {
import('../../components/actionSheet/actionSheet').then(({default: actionsheet}) => { import('../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
const menuItems = []; const menuItems = [];
if (session.ServerId && session.DeviceId !== ServerConnections.deviceId()) { if (session.ServerId && session.DeviceId !== ServerConnections.deviceId()) {

View file

@ -68,7 +68,7 @@ import confirm from '../../../components/confirm/confirm';
}); });
} }
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => { import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({ actionsheet.show({
items: menuItems, items: menuItems,
positionTo: btn, positionTo: btn,

View file

@ -111,7 +111,7 @@
<span>${EnableIntelLowPowerHevcHwEncoder}</span> <span>${EnableIntelLowPowerHevcHwEncoder}</span>
</label> </label>
<div class="fieldDescription"> <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> </div>
</div> </div>
@ -253,6 +253,13 @@
</label> </label>
<div class="fieldDescription checkboxFieldDescription">${EnableFallbackFontHelp}</div> <div class="fieldDescription checkboxFieldDescription">${EnableFallbackFontHelp}</div>
</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"> <div class="inputContainer">
<input is="emby-input" type="number" id="txtDownMixAudioBoost" pattern="[0-9]*" required="required" min=".5" max="3" step=".1" label="${LabelDownMixAudioScale}" /> <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> <div class="fieldDescription">${LabelDownMixAudioScaleHelp}</div>

View file

@ -22,6 +22,7 @@ import alert from '../../components/alert';
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding; page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
$('#selectVideoDecoder', page).val(config.HardwareAccelerationType); $('#selectVideoDecoder', page).val(config.HardwareAccelerationType);
$('#selectThreadCount', page).val(config.EncodingThreadCount); $('#selectThreadCount', page).val(config.EncodingThreadCount);
page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr;
$('#txtDownMixAudioBoost', page).val(config.DownMixAudioBoost); $('#txtDownMixAudioBoost', page).val(config.DownMixAudioBoost);
$('#selectStereoDownmixAlgorithm').val(config.DownMixStereoAlgorithm || 'None'); $('#selectStereoDownmixAlgorithm').val(config.DownMixStereoAlgorithm || 'None');
page.querySelector('#txtMaxMuxingQueueSize').value = config.MaxMuxingQueueSize || ''; page.querySelector('#txtMaxMuxingQueueSize').value = config.MaxMuxingQueueSize || '';
@ -78,6 +79,7 @@ import alert from '../../components/alert';
const onDecoderConfirmed = function () { const onDecoderConfirmed = function () {
loading.show(); loading.show();
ApiClient.getNamedConfiguration('encoding').then(function (config) { ApiClient.getNamedConfiguration('encoding').then(function (config) {
config.EnableAudioVbr = form.querySelector('#chkEnableAudioVbr').checked;
config.DownMixAudioBoost = $('#txtDownMixAudioBoost', form).val(); config.DownMixAudioBoost = $('#txtDownMixAudioBoost', form).val();
config.DownMixStereoAlgorithm = $('#selectStereoDownmixAlgorithm', form).val() || 'None'; config.DownMixStereoAlgorithm = $('#selectStereoDownmixAlgorithm', form).val() || 'None';
config.MaxMuxingQueueSize = form.querySelector('#txtMaxMuxingQueueSize').value; config.MaxMuxingQueueSize = form.querySelector('#txtMaxMuxingQueueSize').value;
@ -237,7 +239,7 @@ import alert from '../../components/alert';
setDecodingCodecsVisible(page, this.value); setDecodingCodecsVisible(page, this.value);
}); });
$('#btnSelectEncoderPath', page).on('click.selectDirectory', function () { $('#btnSelectEncoderPath', page).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
includeFiles: true, includeFiles: true,
@ -252,7 +254,7 @@ import alert from '../../components/alert';
}); });
}); });
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () { $('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
callback: function (path) { callback: function (path) {
@ -269,7 +271,7 @@ import alert from '../../components/alert';
}); });
}); });
$('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () { $('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
includeDirectories: true, includeDirectories: true,

View file

@ -80,6 +80,13 @@
</div> </div>
</div> </div>
<div class="verticalSection">
<h2>${HeaderPerformance}</h2>
<div class="inputContainer">
<input is="emby-input" id="txtParallelImageEncodingLimit" label="${LabelParallelImageEncodingLimit}" type="number" pattern="[0-9]*" min="0" step="1" />
<div class="fieldDescription">${LabelParallelImageEncodingLimitHelp}</div>
</div>
</div>
<br /> <br />
<div> <div>
<button is="emby-button" type="submit" class="raised button-submit block"> <button is="emby-button" type="submit" class="raised button-submit block">

View file

@ -21,6 +21,7 @@ import alert from '../../components/alert';
$('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) { $('#selectLocalizationLanguage', page).html(languageOptions.map(function (language) {
return '<option value="' + language.Value + '">' + language.Name + '</option>'; return '<option value="' + language.Value + '">' + language.Name + '</option>';
})).val(config.UICulture); })).val(config.UICulture);
page.querySelector('#txtParallelImageEncodingLimit').value = config.ParallelImageEncodingLimit || '';
loading.hide(); loading.hide();
} }
@ -36,6 +37,7 @@ import alert from '../../components/alert';
config.MetadataPath = $('#txtMetadataPath', form).val(); config.MetadataPath = $('#txtMetadataPath', form).val();
config.MetadataNetworkPath = $('#txtMetadataNetworkPath', form).val(); config.MetadataNetworkPath = $('#txtMetadataNetworkPath', form).val();
config.QuickConnectAvailable = form.querySelector('#chkQuickConnectAvailable').checked; config.QuickConnectAvailable = form.querySelector('#chkQuickConnectAvailable').checked;
config.ParallelImageEncodingLimit = parseInt(form.querySelector('#txtParallelImageEncodingLimit').value || '0', 10);
ApiClient.updateServerConfiguration(config).then(function() { ApiClient.updateServerConfiguration(config).then(function() {
ApiClient.getNamedConfiguration(brandingConfigKey).then(function(brandingConfig) { ApiClient.getNamedConfiguration(brandingConfigKey).then(function(brandingConfig) {
@ -58,7 +60,7 @@ import alert from '../../components/alert';
const brandingConfigKey = 'branding'; const brandingConfigKey = 'branding';
export default function (view) { export default function (view) {
$('#btnSelectCachePath', view).on('click.selectDirectory', function () { $('#btnSelectCachePath', view).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
callback: function (path) { callback: function (path) {
@ -75,7 +77,7 @@ import alert from '../../components/alert';
}); });
}); });
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () { $('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
path: $('#txtMetadataPath', view).val(), path: $('#txtMetadataPath', view).val(),

View file

@ -15,7 +15,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
/* eslint-disable indent */ /* eslint-disable indent */
function addVirtualFolder(page) { function addVirtualFolder(page) {
import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({default: medialibrarycreator}) => { import('../../components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: medialibrarycreator }) => {
new medialibrarycreator({ new medialibrarycreator({
collectionTypeOptions: getCollectionTypeOptions().filter(function (f) { collectionTypeOptions: getCollectionTypeOptions().filter(function (f) {
return !f.hidden; return !f.hidden;
@ -30,7 +30,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
} }
function editVirtualFolder(page, virtualFolder) { function editVirtualFolder(page, virtualFolder) {
import('../../components/mediaLibraryEditor/mediaLibraryEditor').then(({default: medialibraryeditor}) => { import('../../components/mediaLibraryEditor/mediaLibraryEditor').then(({ default: medialibraryeditor }) => {
new medialibraryeditor({ new medialibraryeditor({
refresh: shouldRefreshLibraryAfterChanges(page), refresh: shouldRefreshLibraryAfterChanges(page),
library: virtualFolder library: virtualFolder
@ -64,7 +64,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
} }
function refreshVirtualFolder(page, virtualFolder) { function refreshVirtualFolder(page, virtualFolder) {
import('../../components/refreshdialog/refreshdialog').then(({default: refreshDialog}) => { import('../../components/refreshdialog/refreshdialog').then(({ default: refreshDialog }) => {
new refreshDialog({ new refreshDialog({
itemIds: [virtualFolder.ItemId], itemIds: [virtualFolder.ItemId],
serverId: ApiClient.serverId(), serverId: ApiClient.serverId(),
@ -74,7 +74,7 @@ import cardBuilder from '../../components/cardbuilder/cardBuilder';
} }
function renameVirtualFolder(page, virtualFolder) { function renameVirtualFolder(page, virtualFolder) {
import('../../components/prompt/prompt').then(({default: prompt}) => { import('../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({ prompt({
label: globalize.translate('LabelNewName'), label: globalize.translate('LabelNewName'),
description: globalize.translate('MessageRenameMediaFolder'), description: globalize.translate('MessageRenameMediaFolder'),

View file

@ -181,7 +181,7 @@ import alert from '../../components/alert';
} }
}); });
view.querySelector('#btnSelectCertPath').addEventListener('click', function () { view.querySelector('#btnSelectCertPath').addEventListener('click', function () {
import('../../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
includeFiles: true, includeFiles: true,

View file

@ -0,0 +1,198 @@
<div id="editUserPage" data-role="page" class="page type-interior">
<div>
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle username"></h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
</div>
<div data-role="controlgroup" data-type="horizontal" class="localnav" id="userProfileNavigation" data-mini="true">
<a href="#" is="emby-linkbutton" data-role="button" class="ui-btn-active">${Profile}</a>
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
<a href="#" is="emby-linkbutton" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
</div>
<p class="lnkEditUserPreferencesContainer">
<a class="lnkEditUserPreferences button-link" href="#" is="emby-linkbutton">${ButtonEditOtherUserPreferences}</a>
</p>
<form class="editUserProfileForm">
<div class="disabledUserBanner" style="display: none;">
<div class="btn btnDarkAccent btnStatic">
<div>
${HeaderThisUserIsCurrentlyDisabled}
</div>
<div style="margin-top: 5px;">
${MessageReenableUser}
</div>
</div>
</div>
<div id="fldUserName" class="inputContainer">
<input is="emby-input" id="txtUserName" required type="text" label="${LabelName}" />
</div>
<div class="selectContainer fldSelectLoginProvider hide">
<select class="selectLoginProvider" is="emby-select" label="${LabelAuthProvider}"></select>
<div class="fieldDescription">${AuthProviderHelp}</div>
</div>
<div class="selectContainer fldSelectPasswordResetProvider hide">
<select class="selectPasswordResetProvider" is="emby-select" label="${LabelPasswordResetProvider}"></select>
<div class="fieldDescription">${PasswordResetProviderHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription fldRemoteAccess hide">
<label>
<input type="checkbox" is="emby-checkbox" id="chkRemoteAccess" />
<span>${AllowRemoteAccess}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${AllowRemoteAccessHelp}</div>
</div>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkIsAdmin" />
<span>${OptionAllowUserToManageServer}</span>
</label>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableCollectionManagement" />
<span>${AllowCollectionManagement}</span>
</label>
<div id="featureAccessFields" class="verticalSection">
<h2 class="paperListLabel">${HeaderFeatureAccess}</h2>
<div class="checkboxList paperList" style="padding:.5em 1em;">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableLiveTvAccess" />
<span>${OptionAllowBrowsingLiveTv}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkManageLiveTv" />
<span>${OptionAllowManageLiveTv}</span>
</label>
</div>
</div>
<div class="verticalSection">
<h2 class="paperListLabel">${HeaderPlayback}</h2>
<div class="checkboxList paperList" style="padding:.5em 1em;">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableMediaPlayback" />
<span>${OptionAllowMediaPlayback}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableAudioPlaybackTranscoding" />
<span>${OptionAllowAudioPlaybackTranscoding}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableVideoPlaybackTranscoding" />
<span>${OptionAllowVideoPlaybackTranscoding}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableVideoPlaybackRemuxing" />
<span>${OptionAllowVideoPlaybackRemuxing}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkForceRemoteSourceTranscoding" />
<span>${OptionForceRemoteSourceTranscoding}</span>
</label>
</div>
<div class="fieldDescription">${OptionAllowMediaPlaybackTranscodingHelp}</div>
</div>
<br />
<div class="verticalSection">
<div class="inputContainer">
<input is="emby-input" type="number" id="txtRemoteClientBitrateLimit" inputmode="decimal" pattern="[0-9]*(\.[0-9]+)?" min="0" step=".25" label="${LabelRemoteClientBitrateLimit}" />
<div class="fieldDescription">${LabelRemoteClientBitrateLimitHelp}</div>
<div class="fieldDescription">${LabelUserRemoteClientBitrateLimitHelp}</div>
</div>
</div>
<div class="verticalSection">
<div class="selectContainer fldSelectSyncPlayAccess">
<select class="selectSyncPlayAccess" is="emby-select" id="selectSyncPlayAccess" label="${LabelSyncPlayAccess}">
<option value="CreateAndJoinGroups">${LabelSyncPlayAccessCreateAndJoinGroups}</option>
<option value="JoinGroups">${LabelSyncPlayAccessJoinGroups}</option>
<option value="None">${LabelSyncPlayAccessNone}</option>
</select>
<div class="fieldDescription">${SyncPlayAccessHelp}</div>
</div>
</div>
<div class="verticalSection">
<h2 class="checkboxListLabel" style="margin-bottom:1em;">${HeaderAllowMediaDeletionFrom}</h2>
<div class="checkboxList paperList checkboxList-paperList">
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableDeleteAllFolders" />
<span>${AllLibraries}</span>
</label>
<div class="deleteAccess">
</div>
</div>
</div>
<div class="verticalSection">
<h2 class="checkboxListLabel">${HeaderRemoteControl}</h2>
<div class="checkboxList paperList" style="padding:.5em 1em;">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableRemoteControlOtherUsers" />
<span>${OptionAllowRemoteControlOthers}</span>
</label>
<label>
<input type="checkbox" is="emby-checkbox" id="chkRemoteControlSharedDevices" />
<span>${OptionAllowRemoteSharedDevices}</span>
</label>
</div>
<div class="fieldDescription">${OptionAllowRemoteSharedDevicesHelp}</div>
</div>
<h2 class="checkboxListLabel">${Other}</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableDownloading" />
<span>${OptionAllowContentDownload}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${OptionAllowContentDownloadHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription" id="fldIsEnabled">
<label>
<input type="checkbox" is="emby-checkbox" id="chkDisabled" />
<span>${OptionDisableUser}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${OptionDisableUserHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription" id="fldIsHidden">
<label>
<input type="checkbox" is="emby-checkbox" id="chkIsHidden" />
<span>${OptionHideUser}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${OptionHideUserFromLoginHelp}</div>
</div>
<br/>
<div class=verticalSection>
<div class="inputContainer" id="fldLoginAttemptsBeforeLockout">
<input is="emby-input" type="number" id="txtLoginAttemptsBeforeLockout" min="-1" step="1" label="${LabelUserLoginAttemptsBeforeLockout}"/>
<div class="fieldDescription">${OptionLoginAttemptsBeforeLockout}</div>
<div class="fieldDescription">${OptionLoginAttemptsBeforeLockoutHelp}</div>
</div>
</div>
<br />
<div class=verticalSection>
<div class="inputContainer" id="fldMaxActiveSessions">
<input is="emby-input" type="number" id="txtMaxActiveSessions" min="0" step="1" label="${LabelUserMaxActiveSessions}"/>
<div class="fieldDescription">${OptionMaxActiveSessions}</div>
<div class="fieldDescription">${OptionMaxActiveSessionsHelp}</div>
</div>
</div>
<br />
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
<span>${ButtonCancel}</span>
</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,196 @@
import 'jquery';
import loading from '../../../components/loading/loading';
import libraryMenu from '../../../scripts/libraryMenu';
import globalize from '../../../scripts/globalize';
import Dashboard from '../../../utils/dashboard';
import toast from '../../../components/toast/toast';
import { getParameterByName } from '../../../utils/url.ts';
function loadDeleteFolders(page, user, mediaFolders) {
ApiClient.getJSON(ApiClient.getUrl('Channels', {
SupportsMediaDeletion: true
})).then(function (channelsResult) {
let isChecked;
let checkedAttribute;
let html = '';
for (const folder of mediaFolders) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
}
for (const folder of channelsResult.Items) {
isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
checkedAttribute = isChecked ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
}
$('.deleteAccess', page).html(html).trigger('create');
$('#chkEnableDeleteAllFolders', page).prop('checked', user.Policy.EnableContentDeletion);
});
}
function loadAuthProviders(page, user, providers) {
if (providers.length > 1) {
page.querySelector('.fldSelectLoginProvider').classList.remove('hide');
} else {
page.querySelector('.fldSelectLoginProvider').classList.add('hide');
}
const currentProviderId = user.Policy.AuthenticationProviderId;
page.querySelector('.selectLoginProvider').innerHTML = providers.map(function (provider) {
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
});
}
function loadPasswordResetProviders(page, user, providers) {
if (providers.length > 1) {
page.querySelector('.fldSelectPasswordResetProvider').classList.remove('hide');
} else {
page.querySelector('.fldSelectPasswordResetProvider').classList.add('hide');
}
const currentProviderId = user.Policy.PasswordResetProviderId;
page.querySelector('.selectPasswordResetProvider').innerHTML = providers.map(function (provider) {
const selected = provider.Id === currentProviderId || providers.length < 2 ? ' selected' : '';
return '<option value="' + provider.Id + '"' + selected + '>' + provider.Name + '</option>';
});
}
function loadUser(page, user) {
ApiClient.getJSON(ApiClient.getUrl('Auth/Providers')).then(function (providers) {
loadAuthProviders(page, user, providers);
});
ApiClient.getJSON(ApiClient.getUrl('Auth/PasswordResetProviders')).then(function (providers) {
loadPasswordResetProviders(page, user, providers);
});
ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
})).then(function (folders) {
loadDeleteFolders(page, user, folders.Items);
});
if (user.Policy.IsDisabled) {
$('.disabledUserBanner', page).show();
} else {
$('.disabledUserBanner', page).hide();
}
$('#txtUserName', page).prop('disabled', '').removeAttr('disabled');
$('#fldConnectInfo', page).show();
$('.lnkEditUserPreferences', page).attr('href', 'mypreferencesmenu.html?userId=' + user.Id);
libraryMenu.setTitle(user.Name);
page.querySelector('.username').innerHTML = user.Name;
$('#txtUserName', page).val(user.Name);
$('#chkIsAdmin', page).prop('checked', user.Policy.IsAdministrator);
$('#chkDisabled', page).prop('checked', user.Policy.IsDisabled);
$('#chkIsHidden', page).prop('checked', user.Policy.IsHidden);
$('#chkEnableCollectionManagement', page).prop('checked', user.Policy.chkEnableCollectionManagement);
$('#chkRemoteControlSharedDevices', page).prop('checked', user.Policy.EnableSharedDeviceControl);
$('#chkEnableRemoteControlOtherUsers', page).prop('checked', user.Policy.EnableRemoteControlOfOtherUsers);
$('#chkEnableDownloading', page).prop('checked', user.Policy.EnableContentDownloading);
$('#chkManageLiveTv', page).prop('checked', user.Policy.EnableLiveTvManagement);
$('#chkEnableLiveTvAccess', page).prop('checked', user.Policy.EnableLiveTvAccess);
$('#chkEnableMediaPlayback', page).prop('checked', user.Policy.EnableMediaPlayback);
$('#chkEnableAudioPlaybackTranscoding', page).prop('checked', user.Policy.EnableAudioPlaybackTranscoding);
$('#chkEnableVideoPlaybackTranscoding', page).prop('checked', user.Policy.EnableVideoPlaybackTranscoding);
$('#chkEnableVideoPlaybackRemuxing', page).prop('checked', user.Policy.EnablePlaybackRemuxing);
$('#chkForceRemoteSourceTranscoding', page).prop('checked', user.Policy.ForceRemoteSourceTranscoding);
$('#chkRemoteAccess', page).prop('checked', user.Policy.EnableRemoteAccess == null || user.Policy.EnableRemoteAccess);
$('#txtRemoteClientBitrateLimit', page).val(user.Policy.RemoteClientBitrateLimit / 1e6 || '');
$('#txtLoginAttemptsBeforeLockout', page).val(user.Policy.LoginAttemptsBeforeLockout || '0');
$('#txtMaxActiveSessions', page).val(user.Policy.MaxActiveSessions || '0');
if (ApiClient.isMinServerVersion('10.6.0')) {
$('#selectSyncPlayAccess').val(user.Policy.SyncPlayAccess);
}
loading.hide();
}
function onSaveComplete() {
Dashboard.navigate('userprofiles.html');
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
function saveUser(user, page) {
user.Name = $('#txtUserName', page).val();
user.Policy.IsAdministrator = $('#chkIsAdmin', page).is(':checked');
user.Policy.IsHidden = $('#chkIsHidden', page).is(':checked');
user.Policy.IsDisabled = $('#chkDisabled', page).is(':checked');
user.Policy.EnableRemoteControlOfOtherUsers = $('#chkEnableRemoteControlOtherUsers', page).is(':checked');
user.Policy.EnableLiveTvManagement = $('#chkManageLiveTv', page).is(':checked');
user.Policy.EnableLiveTvAccess = $('#chkEnableLiveTvAccess', page).is(':checked');
user.Policy.EnableSharedDeviceControl = $('#chkRemoteControlSharedDevices', page).is(':checked');
user.Policy.EnableMediaPlayback = $('#chkEnableMediaPlayback', page).is(':checked');
user.Policy.EnableAudioPlaybackTranscoding = $('#chkEnableAudioPlaybackTranscoding', page).is(':checked');
user.Policy.EnableVideoPlaybackTranscoding = $('#chkEnableVideoPlaybackTranscoding', page).is(':checked');
user.Policy.EnablePlaybackRemuxing = $('#chkEnableVideoPlaybackRemuxing', page).is(':checked');
user.Policy.EnableCollectionManagement = $('#chkEnableCollectionManagement', page).is(':checked');
user.Policy.ForceRemoteSourceTranscoding = $('#chkForceRemoteSourceTranscoding', page).is(':checked');
user.Policy.EnableContentDownloading = $('#chkEnableDownloading', page).is(':checked');
user.Policy.EnableRemoteAccess = $('#chkRemoteAccess', page).is(':checked');
user.Policy.RemoteClientBitrateLimit = parseInt(1e6 * parseFloat($('#txtRemoteClientBitrateLimit', page).val() || '0'), 10);
user.Policy.LoginAttemptsBeforeLockout = parseInt($('#txtLoginAttemptsBeforeLockout', page).val() || '0', 10);
user.Policy.MaxActiveSessions = parseInt($('#txtMaxActiveSessions', page).val() || '0', 10);
user.Policy.AuthenticationProviderId = page.querySelector('.selectLoginProvider').value;
user.Policy.PasswordResetProviderId = page.querySelector('.selectPasswordResetProvider').value;
user.Policy.EnableContentDeletion = $('#chkEnableDeleteAllFolders', page).is(':checked');
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : $('.chkFolder', page).get().filter(function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
if (ApiClient.isMinServerVersion('10.6.0')) {
user.Policy.SyncPlayAccess = page.querySelector('#selectSyncPlayAccess').value;
}
ApiClient.updateUser(user).then(function () {
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
});
}
function onSubmit() {
const page = $(this).parents('.page')[0];
loading.show();
getUser().then(function (result) {
saveUser(result, page);
});
return false;
}
function getUser() {
const userId = getParameterByName('userId');
return ApiClient.getUser(userId);
}
function loadData(page) {
loading.show();
getUser().then(function (user) {
loadUser(page, user);
});
}
$(document).on('pageinit', '#editUserPage', function () {
$('.editUserProfileForm').off('submit', onSubmit).on('submit', onSubmit);
const page = this;
$('#chkEnableDeleteAllFolders', this).on('change', function () {
if (this.checked) {
$('.deleteAccess', page).hide();
} else {
$('.deleteAccess', page).show();
}
});
ApiClient.getServerConfiguration().then(function (config) {
if (config.EnableRemoteAccess) {
page.querySelector('.fldRemoteAccess').classList.remove('hide');
} else {
page.querySelector('.fldRemoteAccess').classList.add('hide');
}
});
}).on('pagebeforeshow', '#editUserPage', function () {
loadData(this);
});

View file

@ -0,0 +1,68 @@
<div id="userLibraryAccessPage" data-role="page" class="page type-interior">
<div>
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle username"></h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
</div>
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);" class="ui-btn-active">${TabAccess}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
</div>
<form class="userLibraryAccessForm">
<div class="folderAccessContainer">
<h2>${HeaderLibraryAccess}</h2>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableAllFolders" />
<span>${OptionEnableAccessToAllLibraries}</span>
</label>
<div class="folderAccessListContainer">
<div class="folderAccess">
</div>
<div class="fieldDescription">${LibraryAccessHelp}</div>
</div>
</div>
<div class="channelAccessContainer" style="display:none;">
<h2>${HeaderChannelAccess}</h2>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableAllChannels" />
<span>${OptionEnableAccessToAllChannels}</span>
</label>
<div class="channelAccessListContainer">
<div class="channelAccess">
</div>
<div class="fieldDescription">${ChannelAccessHelp}</div>
</div>
</div>
<br />
<div class="deviceAccessContainer hide">
<h2>${HeaderDeviceAccess}</h2>
<label class="checkboxContainer">
<input type="checkbox" is="emby-checkbox" id="chkEnableAllDevices" />
<span>${OptionEnableAccessFromAllDevices}</span>
</label>
<div class="deviceAccessListContainer">
<div class="deviceAccess">
</div>
<div class="fieldDescription">${DeviceAccessHelp}</div>
</div>
<br />
</div>
<br />
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,184 @@
import 'jquery';
import loading from '../../../components/loading/loading';
import libraryMenu from '../../../scripts/libraryMenu';
import globalize from '../../../scripts/globalize';
import Dashboard from '../../../utils/dashboard';
import toast from '../../../components/toast/toast';
import { getParameterByName } from '../../../utils/url.ts';
function triggerChange(select) {
const evt = document.createEvent('HTMLEvents');
evt.initEvent('change', false, true);
select.dispatchEvent(evt);
}
function loadMediaFolders(page, user, mediaFolders) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0, length = mediaFolders.length; i < length; i++) {
const folder = mediaFolders[i];
const isChecked = user.Policy.EnableAllFolders || user.Policy.EnabledFolders.indexOf(folder.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
}
html += '</div>';
page.querySelector('.folderAccess').innerHTML = html;
const chkEnableAllFolders = page.querySelector('#chkEnableAllFolders');
chkEnableAllFolders.checked = user.Policy.EnableAllFolders;
triggerChange(chkEnableAllFolders);
}
function loadChannels(page, user, channels) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0, length = channels.length; i < length; i++) {
const folder = channels[i];
const isChecked = user.Policy.EnableAllChannels || user.Policy.EnabledChannels.indexOf(folder.Id) != -1;
const checkedAttribute = isChecked ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '" ' + checkedAttribute + '><span>' + folder.Name + '</span></label>';
}
html += '</div>';
$('.channelAccess', page).show().html(html);
if (channels.length) {
$('.channelAccessContainer', page).show();
} else {
$('.channelAccessContainer', page).hide();
}
const chkEnableAllChannels = page.querySelector('#chkEnableAllChannels');
chkEnableAllChannels.checked = user.Policy.EnableAllChannels;
triggerChange(chkEnableAllChannels);
}
function loadDevices(page, user, devices) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderDevices') + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0, length = devices.length; i < length; i++) {
const device = devices[i];
const checkedAttribute = user.Policy.EnableAllDevices || user.Policy.EnabledDevices.indexOf(device.Id) != -1 ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkDevice" data-id="' + device.Id + '" ' + checkedAttribute + '><span>' + device.Name + ' - ' + device.AppName + '</span></label>';
}
html += '</div>';
$('.deviceAccess', page).show().html(html);
const chkEnableAllDevices = page.querySelector('#chkEnableAllDevices');
chkEnableAllDevices.checked = user.Policy.EnableAllDevices;
triggerChange(chkEnableAllDevices);
if (user.Policy.IsAdministrator) {
page.querySelector('.deviceAccessContainer').classList.add('hide');
} else {
page.querySelector('.deviceAccessContainer').classList.remove('hide');
}
}
function loadUser(page, user, loggedInUser, mediaFolders, channels, devices) {
page.querySelector('.username').innerHTML = user.Name;
libraryMenu.setTitle(user.Name);
loadChannels(page, user, channels);
loadMediaFolders(page, user, mediaFolders);
loadDevices(page, user, devices);
loading.hide();
}
function onSaveComplete() {
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
function saveUser(user, page) {
user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked');
user.Policy.EnabledFolders = user.Policy.EnableAllFolders ? [] : $('.chkFolder', page).get().filter(function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked');
user.Policy.EnabledChannels = user.Policy.EnableAllChannels ? [] : $('.chkChannel', page).get().filter(function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.EnableAllDevices = $('#chkEnableAllDevices', page).is(':checked');
user.Policy.EnabledDevices = user.Policy.EnableAllDevices ? [] : $('.chkDevice', page).get().filter(function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
user.Policy.BlockedChannels = null;
user.Policy.BlockedMediaFolders = null;
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
}
function onSubmit() {
const page = $(this).parents('.page');
loading.show();
const userId = getParameterByName('userId');
ApiClient.getUser(userId).then(function (result) {
saveUser(result, page);
});
return false;
}
$(document).on('pageinit', '#userLibraryAccessPage', function () {
const page = this;
$('#chkEnableAllDevices', page).on('change', function () {
if (this.checked) {
$('.deviceAccessListContainer', page).hide();
} else {
$('.deviceAccessListContainer', page).show();
}
});
$('#chkEnableAllChannels', page).on('change', function () {
if (this.checked) {
$('.channelAccessListContainer', page).hide();
} else {
$('.channelAccessListContainer', page).show();
}
});
page.querySelector('#chkEnableAllFolders').addEventListener('change', function () {
if (this.checked) {
page.querySelector('.folderAccessListContainer').classList.add('hide');
} else {
page.querySelector('.folderAccessListContainer').classList.remove('hide');
}
});
$('.userLibraryAccessForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#userLibraryAccessPage', function () {
const page = this;
loading.show();
let promise1;
const userId = getParameterByName('userId');
if (userId) {
promise1 = ApiClient.getUser(userId);
} else {
const deferred = $.Deferred();
deferred.resolveWith(null, [{
Configuration: {}
}]);
promise1 = deferred.promise();
}
const promise2 = Dashboard.getCurrentUser();
const promise4 = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promise5 = ApiClient.getJSON(ApiClient.getUrl('Channels'));
const promise6 = ApiClient.getJSON(ApiClient.getUrl('Devices'));
Promise.all([promise1, promise2, promise4, promise5, promise6]).then(function (responses) {
loadUser(page, responses[0], responses[1], responses[2].Items, responses[3].Items, responses[4].Items);
});
});

View file

@ -0,0 +1,62 @@
<div id="newUserPage" data-role="page" class="page type-interior">
<div>
<div class="content-primary">
<form class="newUserProfileForm">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">${ButtonAddUser}</h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
<div class="inputContainer">
<input is="emby-input" id="txtUsername" required type="text" label="${LabelName}" />
</div>
<div class="inputContainer">
<input is="emby-input" id="txtPassword" type="password" label="${LabelPassword}" />
</div>
</div>
<div class="folderAccessContainer verticalSection">
<h2 class="sectionTitle">${HeaderLibraryAccess}</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableAllFolders" />
<span>${OptionEnableAccessToAllLibraries}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LibraryAccessHelp}</div>
</div>
<div class="folderAccessListContainer">
<div class="folderAccess">
</div>
</div>
</div>
<div class="channelAccessContainer verticalSection verticalSection-extrabottompadding" style="display:none;">
<h2 class="sectionTitle">${HeaderChannelAccess}</h2>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableAllChannels" />
<span>${OptionEnableAccessToAllChannels}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${ChannelAccessHelp}</div>
</div>
<div class="channelAccessListContainer">
<div class="channelAccess">
</div>
</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
<span>${ButtonCancel}</span>
</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,128 @@
import 'jquery';
import loading from '../../../components/loading/loading';
import globalize from '../../../scripts/globalize';
import '../../../elements/emby-checkbox/emby-checkbox';
import Dashboard from '../../../utils/dashboard';
import toast from '../../../components/toast/toast';
function loadMediaFolders(page, mediaFolders) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
for (let i = 0; i < mediaFolders.length; i++) {
const folder = mediaFolders[i];
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
}
html += '</div>';
$('.folderAccess', page).html(html).trigger('create');
$('#chkEnableAllFolders', page).prop('checked', false);
}
function loadChannels(page, channels) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
for (let i = 0; i < channels.length; i++) {
const folder = channels[i];
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
}
html += '</div>';
$('.channelAccess', page).show().html(html).trigger('create');
if (channels.length) {
$('.channelAccessContainer', page).show();
} else {
$('.channelAccessContainer', page).hide();
}
$('#chkEnableAllChannels', page).prop('checked', false);
}
function loadUser(page) {
$('#txtUsername', page).val('');
$('#txtPassword', page).val('');
loading.show();
const promiseFolders = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promiseChannels = ApiClient.getJSON(ApiClient.getUrl('Channels'));
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
loadMediaFolders(page, responses[0].Items);
loadChannels(page, responses[1].Items);
loading.hide();
});
}
function saveUser(page) {
const _user = {
Name: $('#txtUsername', page).val(),
Password: $('#txtPassword', page).val()
};
ApiClient.createUser(_user).then(function (user) {
user.Policy.EnableAllFolders = $('#chkEnableAllFolders', page).is(':checked');
user.Policy.EnabledFolders = [];
if (!user.Policy.EnableAllFolders) {
user.Policy.EnabledFolders = $('.chkFolder', page).get().filter(function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
user.Policy.EnableAllChannels = $('#chkEnableAllChannels', page).is(':checked');
user.Policy.EnabledChannels = [];
if (!user.Policy.EnableAllChannels) {
user.Policy.EnabledChannels = $('.chkChannel', page).get().filter(function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-id');
});
}
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
Dashboard.navigate('useredit.html?userId=' + user.Id);
});
}, function () {
toast(globalize.translate('ErrorDefault'));
loading.hide();
});
}
function onSubmit() {
const page = $(this).parents('.page')[0];
loading.show();
saveUser(page);
return false;
}
function loadData(page) {
loadUser(page);
}
$(document).on('pageinit', '#newUserPage', function () {
const page = this;
$('#chkEnableAllChannels', page).on('change', function () {
if (this.checked) {
$('.channelAccessListContainer', page).hide();
} else {
$('.channelAccessListContainer', page).show();
}
});
$('#chkEnableAllFolders', page).on('change', function () {
if (this.checked) {
$('.folderAccessListContainer', page).hide();
} else {
$('.folderAccessListContainer', page).show();
}
});
$('.newUserProfileForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#newUserPage', function () {
loadData(this);
});

View file

@ -0,0 +1,60 @@
<div id="userParentalControlPage" data-role="page" class="page type-interior">
<div>
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle username"></h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
</div>
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);" class="ui-btn-active">${TabParentalControl}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);">${HeaderPassword}</a>
</div>
<form class="userParentalControlForm">
<div class="selectContainer">
<select is="emby-select" id="selectMaxParentalRating" label="${LabelMaxParentalRating}"></select>
<div class="fieldDescription">${MaxParentalRatingHelp}</div>
</div>
<div>
<div class="blockUnratedItems"></div>
</div>
<br />
<div class="verticalSection" style="margin-bottom:2em;">
<div class="detailSectionHeader sectionTitleContainer">
<h2 class="sectionTitle">${LabelBlockContentWithTags}</h2>
<button is="emby-button" type="button" class="fab btnAddBlockedTag submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
</button>
</div>
<div class="blockedTags" style="margin-top:.5em;"></div>
</div>
<div class="accessScheduleSection verticalSection" style="margin-bottom:2em;">
<div class="sectionTitleContainer">
<h2 class="sectionTitle">${HeaderAccessSchedule}</h2>
<button is="emby-button" type="button" class="fab btnAddSchedule submit" style="margin-left:1em;" title="${Add}">
<span class="material-icons add"></span>
</button>
</div>
<p>${HeaderAccessScheduleHelp}</p>
<div class="accessScheduleList paperList"></div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
</div>
</form>
</div>
</div>
</div>

View file

@ -0,0 +1,278 @@
import 'jquery';
import datetime from '../../../scripts/datetime';
import loading from '../../../components/loading/loading';
import libraryMenu from '../../../scripts/libraryMenu';
import globalize from '../../../scripts/globalize';
import '../../../components/listview/listview.scss';
import '../../../elements/emby-button/paper-icon-button-light';
import toast from '../../../components/toast/toast';
import { getParameterByName } from '../../../utils/url.ts';
function populateRatings(allParentalRatings, page) {
let html = '';
html += "<option value=''></option>";
let rating;
const ratings = [];
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
rating = allParentalRatings[i];
if (ratings.length) {
const lastRating = ratings[ratings.length - 1];
if (lastRating.Value === rating.Value) {
lastRating.Name += '/' + rating.Name;
continue;
}
}
ratings.push({
Name: rating.Name,
Value: rating.Value
});
}
for (let i = 0, length = ratings.length; i < length; i++) {
rating = ratings[i];
html += "<option value='" + rating.Value + "'>" + rating.Name + '</option>';
}
$('#selectMaxParentalRating', page).html(html);
}
function loadUnratedItems(page, user) {
const items = [{
name: globalize.translate('Books'),
value: 'Book'
}, {
name: globalize.translate('Channels'),
value: 'ChannelContent'
}, {
name: globalize.translate('LiveTV'),
value: 'LiveTvChannel'
}, {
name: globalize.translate('Movies'),
value: 'Movie'
}, {
name: globalize.translate('Music'),
value: 'Music'
}, {
name: globalize.translate('Trailers'),
value: 'Trailer'
}, {
name: globalize.translate('Shows'),
value: 'Series'
}];
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderBlockItemsWithNoRating') + '</h3>';
html += '<div class="checkboxList paperList checkboxList-paperList">';
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
const checkedAttribute = user.Policy.BlockUnratedItems.indexOf(item.value) != -1 ? ' checked="checked"' : '';
html += '<label><input type="checkbox" is="emby-checkbox" class="chkUnratedItem" data-itemtype="' + item.value + '" type="checkbox"' + checkedAttribute + '><span>' + item.name + '</span></label>';
}
html += '</div>';
$('.blockUnratedItems', page).html(html).trigger('create');
}
function loadUser(page, user, allParentalRatings) {
page.querySelector('.username').innerHTML = user.Name;
libraryMenu.setTitle(user.Name);
loadUnratedItems(page, user);
loadBlockedTags(page, user.Policy.BlockedTags);
populateRatings(allParentalRatings, page);
let ratingValue = '';
if (user.Policy.MaxParentalRating) {
for (let i = 0, length = allParentalRatings.length; i < length; i++) {
const rating = allParentalRatings[i];
if (user.Policy.MaxParentalRating >= rating.Value) {
ratingValue = rating.Value;
}
}
}
$('#selectMaxParentalRating', page).val(ratingValue);
if (user.Policy.IsAdministrator) {
$('.accessScheduleSection', page).hide();
} else {
$('.accessScheduleSection', page).show();
}
renderAccessSchedule(page, user.Policy.AccessSchedules || []);
loading.hide();
}
function loadBlockedTags(page, tags) {
let html = tags.map(function (h) {
let li = '<div class="listItem">';
li += '<div class="listItemBody">';
li += '<h3 class="listItemBodyText">';
li += h;
li += '</h3>';
li += '</div>';
li += '<button type="button" is="paper-icon-button-light" class="blockedTag btnDeleteTag listItemButton" data-tag="' + h + '"><span class="material-icons delete"></span></button>';
li += '</div>';
return li;
}).join('');
if (html) {
html = '<div class="paperList">' + html + '</div>';
}
const blockedTags = page.querySelector('.blockedTags');
blockedTags.innerHTML = html;
for (const btnDeleteTag of blockedTags.querySelectorAll('.btnDeleteTag')) {
btnDeleteTag.addEventListener('click', function () {
const tag = this.getAttribute('data-tag');
const newTags = tags.filter(function (t) {
return t != tag;
});
loadBlockedTags(page, newTags);
});
}
}
function deleteAccessSchedule(page, schedules, index) {
schedules.splice(index, 1);
renderAccessSchedule(page, schedules);
}
function renderAccessSchedule(page, schedules) {
let html = '';
let index = 0;
html += schedules.map(function (a) {
let itemHtml = '';
itemHtml += '<div class="liSchedule listItem" data-day="' + a.DayOfWeek + '" data-start="' + a.StartHour + '" data-end="' + a.EndHour + '">';
itemHtml += '<div class="listItemBody two-line">';
itemHtml += '<h3 class="listItemBodyText">';
itemHtml += globalize.translate('Option' + a.DayOfWeek);
itemHtml += '</h3>';
itemHtml += '<div class="listItemBodyText secondary">' + getDisplayTime(a.StartHour) + ' - ' + getDisplayTime(a.EndHour) + '</div>';
itemHtml += '</div>';
itemHtml += '<button type="button" is="paper-icon-button-light" class="btnDelete listItemButton" data-index="' + index + '"><span class="material-icons delete"></span></button>';
itemHtml += '</div>';
index++;
return itemHtml;
}).join('');
const accessScheduleList = page.querySelector('.accessScheduleList');
accessScheduleList.innerHTML = html;
$('.btnDelete', accessScheduleList).on('click', function () {
deleteAccessSchedule(page, schedules, parseInt(this.getAttribute('data-index'), 10));
});
}
function onSaveComplete() {
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
function saveUser(user, page) {
user.Policy.MaxParentalRating = $('#selectMaxParentalRating', page).val() || null;
user.Policy.BlockUnratedItems = $('.chkUnratedItem', page).get().filter(function (i) {
return i.checked;
}).map(function (i) {
return i.getAttribute('data-itemtype');
});
user.Policy.AccessSchedules = getSchedulesFromPage(page);
user.Policy.BlockedTags = getBlockedTagsFromPage(page);
ApiClient.updateUserPolicy(user.Id, user.Policy).then(function () {
onSaveComplete();
});
}
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));
}
function showSchedulePopup(page, schedule, index) {
schedule = schedule || {};
import('../../../components/accessSchedule/accessSchedule').then(({ default: accessschedule }) => {
accessschedule.show({
schedule: schedule
}).then(function (updatedSchedule) {
const schedules = getSchedulesFromPage(page);
if (index == -1) {
index = schedules.length;
}
schedules[index] = updatedSchedule;
renderAccessSchedule(page, schedules);
});
});
}
function getSchedulesFromPage(page) {
return $('.liSchedule', page).map(function () {
return {
DayOfWeek: this.getAttribute('data-day'),
StartHour: this.getAttribute('data-start'),
EndHour: this.getAttribute('data-end')
};
}).get();
}
function getBlockedTagsFromPage(page) {
return $('.blockedTag', page).map(function () {
return this.getAttribute('data-tag');
}).get();
}
function showBlockedTagPopup(page) {
import('../../../components/prompt/prompt').then(({ default: prompt }) => {
prompt({
label: globalize.translate('LabelTag')
}).then(function (value) {
const tags = getBlockedTagsFromPage(page);
if (tags.indexOf(value) == -1) {
tags.push(value);
loadBlockedTags(page, tags);
}
});
});
}
window.UserParentalControlPage = {
onSubmit: function () {
const page = $(this).parents('.page');
loading.show();
const userId = getParameterByName('userId');
ApiClient.getUser(userId).then(function (result) {
saveUser(result, page);
});
return false;
}
};
$(document).on('pageinit', '#userParentalControlPage', function () {
const page = this;
$('.btnAddSchedule', page).on('click', function () {
showSchedulePopup(page, {}, -1);
});
$('.btnAddBlockedTag', page).on('click', function () {
showBlockedTagPopup(page);
});
$('.userParentalControlForm').off('submit', UserParentalControlPage.onSubmit).on('submit', UserParentalControlPage.onSubmit);
}).on('pageshow', '#userParentalControlPage', function () {
const page = this;
loading.show();
const userId = getParameterByName('userId');
const promise1 = ApiClient.getUser(userId);
const promise2 = ApiClient.getParentalRatings();
Promise.all([promise1, promise2]).then(function (responses) {
loadUser(page, responses[0], responses[1]);
});
});

View file

@ -0,0 +1,72 @@
<div id="userPasswordPage" data-role="page" class="page type-interior userPasswordPage">
<div>
<div class="content-primary">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle username"></h2>
<a is="emby-linkbutton" rel="noopener noreferrer" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/">${Help}</a>
</div>
</div>
<div data-role="controlgroup" data-type="horizontal" class="localnav" data-mini="true">
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('useredit.html', true);">${Profile}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userlibraryaccess.html', true);">${TabAccess}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userparentalcontrol.html', true);">${TabParentalControl}</a>
<a is="emby-linkbutton" href="#" data-role="button" onclick="Dashboard.navigate('userpassword.html', true);" class="ui-btn-active">${HeaderPassword}</a>
</div>
<div class="readOnlyContent">
<form class="updatePasswordForm passwordSection hide" style="margin: 0 auto 2em;">
<div class="detailSection">
<div id="fldCurrentPassword" class="inputContainer hide">
<input is="emby-input" type="password" id="txtCurrentPassword" label="${LabelCurrentPassword}" autocomplete="off" />
</div>
<div class="inputContainer">
<input is="emby-input" type="password" id="txtNewPassword" label="${LabelNewPassword}" autocomplete="off" />
</div>
<div class="inputContainer">
<input is="emby-input" type="password" id="txtNewPasswordConfirm" label="${LabelNewPasswordConfirm}" autocomplete="off" />
</div>
<br />
<div>
<button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button>
<button is="emby-button" type="button" id="btnResetPassword" class="raised button-cancel block hide">
<span>${ResetPassword}</span>
</button>
</div>
</div>
</form>
<br />
<form class="localAccessForm localAccessSection" style="margin: 0 auto;">
<div class="detailSection">
<div class="detailSectionHeader">
${HeaderEasyPinCode}
</div>
<br />
<div>${EasyPasswordHelp}</div>
<br />
<div class="inputContainer">
<input is="emby-input" type="number" id="txtEasyPassword" label="${LabelEasyPinCode}" autocomplete="off" pattern="[0-9]*" step="1" maxlength="5" />
</div>
<br />
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" class="chkEnableLocalEasyPassword" />
<span>${LabelInNetworkSignInWithEasyPassword}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LabelInNetworkSignInWithEasyPasswordHelp}</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" id="btnResetEasyPassword" class="raised button-cancel block hide">
<span>${ButtonResetEasyPassword}</span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,179 @@
import loading from '../../../components/loading/loading';
import libraryMenu from '../../../scripts/libraryMenu';
import globalize from '../../../scripts/globalize';
import '../../../elements/emby-button/emby-button';
import Dashboard from '../../../utils/dashboard';
import toast from '../../../components/toast/toast';
import confirm from '../../../components/confirm/confirm';
function loadUser(page, params) {
const userid = params.userId;
ApiClient.getUser(userid).then(function (user) {
Dashboard.getCurrentUser().then(function (loggedInUser) {
libraryMenu.setTitle(user.Name);
page.querySelector('.username').innerText = user.Name;
let showPasswordSection = true;
let showLocalAccessSection = false;
if (user.ConnectLinkType == 'Guest') {
page.querySelector('.localAccessSection').classList.add('hide');
showPasswordSection = false;
} else if (user.HasConfiguredPassword) {
page.querySelector('#btnResetPassword').classList.remove('hide');
page.querySelector('#fldCurrentPassword').classList.remove('hide');
showLocalAccessSection = true;
} else {
page.querySelector('#btnResetPassword').classList.add('hide');
page.querySelector('#fldCurrentPassword').classList.add('hide');
}
if (showPasswordSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
page.querySelector('.passwordSection').classList.remove('hide');
} else {
page.querySelector('.passwordSection').classList.add('hide');
}
if (showLocalAccessSection && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
page.querySelector('.localAccessSection').classList.remove('hide');
} else {
page.querySelector('.localAccessSection').classList.add('hide');
}
const txtEasyPassword = page.querySelector('#txtEasyPassword');
txtEasyPassword.value = '';
if (user.HasConfiguredEasyPassword) {
txtEasyPassword.placeholder = '******';
page.querySelector('#btnResetEasyPassword').classList.remove('hide');
} else {
txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = '';
page.querySelector('#btnResetEasyPassword').classList.add('hide');
}
page.querySelector('.chkEnableLocalEasyPassword').checked = user.Configuration.EnableLocalPassword;
import('../../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
});
page.querySelector('#txtCurrentPassword').value = '';
page.querySelector('#txtNewPassword').value = '';
page.querySelector('#txtNewPasswordConfirm').value = '';
}
export default function (view, params) {
function saveEasyPassword() {
const userId = params.userId;
const easyPassword = view.querySelector('#txtEasyPassword').value;
if (easyPassword) {
ApiClient.updateEasyPassword(userId, easyPassword).then(function () {
onEasyPasswordSaved(userId);
});
} else {
onEasyPasswordSaved(userId);
}
}
function onEasyPasswordSaved(userId) {
ApiClient.getUser(userId).then(function (user) {
user.Configuration.EnableLocalPassword = view.querySelector('.chkEnableLocalEasyPassword').checked;
ApiClient.updateUserConfiguration(user.Id, user.Configuration).then(function () {
loading.hide();
toast(globalize.translate('SettingsSaved'));
loadUser(view, params);
});
});
}
function savePassword() {
const userId = params.userId;
let currentPassword = view.querySelector('#txtCurrentPassword').value;
const newPassword = view.querySelector('#txtNewPassword').value;
if (view.querySelector('#fldCurrentPassword').classList.contains('hide')) {
// Firefox does not respect autocomplete=off, so clear it if the field is supposed to be hidden (and blank)
// This should only happen when user.HasConfiguredPassword is false, but this information is not passed on
currentPassword = '';
}
ApiClient.updateUserPassword(userId, currentPassword, newPassword).then(function () {
loading.hide();
toast(globalize.translate('PasswordSaved'));
loadUser(view, params);
}, function () {
loading.hide();
Dashboard.alert({
title: globalize.translate('HeaderLoginFailure'),
message: globalize.translate('MessageInvalidUser')
});
});
}
function onSubmit(e) {
const form = this;
if (form.querySelector('#txtNewPassword').value != form.querySelector('#txtNewPasswordConfirm').value) {
toast(globalize.translate('PasswordMatchError'));
} else {
loading.show();
savePassword();
}
e.preventDefault();
return false;
}
function onLocalAccessSubmit(e) {
loading.show();
saveEasyPassword();
e.preventDefault();
return false;
}
function resetPassword() {
const msg = globalize.translate('PasswordResetConfirmation');
confirm(msg, globalize.translate('ResetPassword')).then(function () {
const userId = params.userId;
loading.show();
ApiClient.resetUserPassword(userId).then(function () {
loading.hide();
Dashboard.alert({
message: globalize.translate('PasswordResetComplete'),
title: globalize.translate('ResetPassword')
});
loadUser(view, params);
});
});
}
function resetEasyPassword() {
const msg = globalize.translate('PinCodeResetConfirmation');
confirm(msg, globalize.translate('HeaderPinCodeReset')).then(function () {
const userId = params.userId;
loading.show();
ApiClient.resetEasyPassword(userId).then(function () {
loading.hide();
Dashboard.alert({
message: globalize.translate('PinCodeResetComplete'),
title: globalize.translate('HeaderPinCodeReset')
});
loadUser(view, params);
});
});
}
view.querySelector('.updatePasswordForm').addEventListener('submit', onSubmit);
view.querySelector('.localAccessForm').addEventListener('submit', onLocalAccessSubmit);
view.querySelector('#btnResetEasyPassword').addEventListener('click', resetEasyPassword);
view.querySelector('#btnResetPassword').addEventListener('click', resetPassword);
view.addEventListener('viewshow', function () {
loadUser(view, params);
});
}

View file

@ -0,0 +1,16 @@
<div id="userProfilesPage" data-role="page" class="page type-interior userProfilesPage fullWidthContent">
<div>
<div class="content-primary">
<div class="verticalSection verticalSection-extrabottompadding">
<div class="sectionTitleContainer sectionTitleContainer-cards">
<h2 class="sectionTitle sectionTitle-cards">${HeaderUsers}</h2>
<button is="emby-button" type="button" class="fab btnAddUser submit sectionTitleButton" style="margin-left:1em;" title="${ButtonAddUser}">
<span class="material-icons add"></span>
</button>
<a is="emby-linkbutton" rel="noopener noreferrer" style="margin-left:2em!important;" class="raised button-alt headerHelpButton" target="_blank" href="https://jellyfin.org/docs/general/server/users/adding-managing-users">${Help}</a>
</div>
<div class="localUsers itemsContainer vertical-wrap"></div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,184 @@
import loading from '../../../components/loading/loading';
import dom from '../../../scripts/dom';
import globalize from '../../../scripts/globalize';
import { formatDistanceToNow } from 'date-fns';
import { getLocaleWithSuffix } from '../../../utils/dateFnsLocale.ts';
import '../../../elements/emby-button/paper-icon-button-light';
import '../../../components/cardbuilder/card.scss';
import '../../../elements/emby-button/emby-button';
import '../../../components/indicators/indicators.scss';
import '../../../styles/flexstyles.scss';
import Dashboard, { pageIdOn } from '../../../utils/dashboard';
import confirm from '../../../components/confirm/confirm';
import cardBuilder from '../../../components/cardbuilder/cardBuilder';
function deleteUser(page, id) {
const msg = globalize.translate('DeleteUserConfirmation');
confirm({
title: globalize.translate('DeleteUser'),
text: msg,
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(function () {
loading.show();
ApiClient.deleteUser(id).then(function () {
loadData(page);
});
});
}
function showUserMenu(elem) {
const card = dom.parentWithClass(elem, 'card');
const page = dom.parentWithClass(card, 'page');
const userId = card.getAttribute('data-userid');
const menuItems = [];
menuItems.push({
name: globalize.translate('ButtonOpen'),
id: 'open',
icon: 'mode_edit'
});
menuItems.push({
name: globalize.translate('ButtonLibraryAccess'),
id: 'access',
icon: 'lock'
});
menuItems.push({
name: globalize.translate('ButtonParentalControl'),
id: 'parentalcontrol',
icon: 'person'
});
menuItems.push({
name: globalize.translate('Delete'),
id: 'delete',
icon: 'delete'
});
import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({
items: menuItems,
positionTo: card,
callback: function (id) {
switch (id) {
case 'open':
Dashboard.navigate('useredit.html?userId=' + userId);
break;
case 'access':
Dashboard.navigate('userlibraryaccess.html?userId=' + userId);
break;
case 'parentalcontrol':
Dashboard.navigate('userparentalcontrol.html?userId=' + userId);
break;
case 'delete':
deleteUser(page, userId);
}
}
});
});
}
function getUserHtml(user) {
let html = '';
let cssClass = 'card squareCard scalableCard squareCard-scalable';
if (user.Policy.IsDisabled) {
cssClass += ' grayscale';
}
html += "<div data-userid='" + user.Id + "' class='" + cssClass + "'>";
html += '<div class="cardBox visualCardBox">';
html += '<div class="cardScalable visualCardBox-cardScalable">';
html += '<div class="cardPadder cardPadder-square"></div>';
html += `<a is="emby-linkbutton" class="cardContent ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()}" href="#!/useredit.html?userId=${user.Id}">`;
let imgUrl;
if (user.PrimaryImageTag) {
imgUrl = ApiClient.getUserImageUrl(user.Id, {
width: 300,
tag: user.PrimaryImageTag,
type: 'Primary'
});
}
let imageClass = 'cardImage';
if (user.Policy.IsDisabled) {
imageClass += ' disabledUser';
}
if (imgUrl) {
html += '<div class="' + imageClass + '" style="background-image:url(\'' + imgUrl + "');\">";
} else {
html += `<div class="${imageClass} ${imgUrl ? '' : cardBuilder.getDefaultBackgroundClass()} flex align-items-center justify-content-center">`;
html += '<span class="material-icons cardImageIcon person"></span>';
}
html += '</div>';
html += '</a>';
html += '</div>';
html += '<div class="cardFooter visualCardBox-cardFooter">';
html += '<div class="cardText flex align-items-center">';
html += '<div class="flex-grow" style="overflow:hidden;text-overflow:ellipsis;">';
html += user.Name;
html += '</div>';
html += '<button type="button" is="paper-icon-button-light" class="btnUserMenu flex-shrink-zero"><span class="material-icons more_vert"></span></button>';
html += '</div>';
html += '<div class="cardText cardText-secondary">';
const lastSeen = getLastSeenText(user.LastActivityDate);
html += lastSeen != '' ? lastSeen : '&nbsp;';
html += '</div>';
html += '</div>';
html += '</div>';
return html + '</div>';
}
// FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix
// how dates are returned by the server when the session is active and show something like 'Active now', instead of past/future sentences
function getLastSeenText(lastActivityDate) {
const localeWithSuffix = getLocaleWithSuffix();
if (lastActivityDate) {
return globalize.translate('LastSeen', formatDistanceToNow(Date.parse(lastActivityDate), localeWithSuffix));
}
return '';
}
function getUserSectionHtml(users) {
return users.map(function (u__q) {
return getUserHtml(u__q);
}).join('');
}
function renderUsers(page, users) {
page.querySelector('.localUsers').innerHTML = getUserSectionHtml(users);
}
function loadData(page) {
loading.show();
ApiClient.getUsers().then(function (users) {
renderUsers(page, users);
loading.hide();
});
}
pageIdOn('pageinit', 'userProfilesPage', function () {
const page = this;
page.querySelector('.btnAddUser').addEventListener('click', function() {
Dashboard.navigate('usernew.html');
});
page.querySelector('.localUsers').addEventListener('click', function (e__e) {
const btnUserMenu = dom.parentWithClass(e__e.target, 'btnUserMenu');
if (btnUserMenu) {
showUserMenu(btnUserMenu);
}
});
});
pageIdOn('pagebeforeshow', 'userProfilesPage', function () {
loadData(this);
});

View file

@ -0,0 +1,9 @@
<div id="indexPage" style="outline: none;" data-role="page" data-dom-cache="true" class="page homePage libraryPage allLibraryPage backdropPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie,series,book">
<div class="tabContent pageTabContent" id="homeTab" data-index="0">
<div class="sections"></div>
</div>
<div class="tabContent pageTabContent" id="favoritesTab" data-index="1">
<div class="sections"></div>
</div>
</div>

65
src/controllers/home.js Normal file
View file

@ -0,0 +1,65 @@
import TabbedView from '../components/tabbedview/tabbedview';
import globalize from '../scripts/globalize';
import '../elements/emby-tabs/emby-tabs';
import '../elements/emby-button/emby-button';
import '../elements/emby-scroller/emby-scroller';
import LibraryMenu from '../scripts/libraryMenu';
class HomeView extends TabbedView {
setTitle() {
LibraryMenu.setTitle(null);
}
onPause() {
super.onPause(this);
document.querySelector('.skinHeader').classList.remove('noHomeButtonHeader');
}
onResume(options) {
super.onResume(this, options);
document.querySelector('.skinHeader').classList.add('noHomeButtonHeader');
}
getDefaultTabIndex() {
return 0;
}
getTabs() {
return [{
name: globalize.translate('Home')
}, {
name: globalize.translate('Favorites')
}];
}
getTabController(index) {
if (index == null) {
throw new Error('index cannot be null');
}
let depends = '';
switch (index) {
case 0:
depends = 'hometab';
break;
case 1:
depends = 'favorites';
}
const instance = this;
return import(/* webpackChunkName: "[request]" */ `../controllers/${depends}`).then(({ default: controllerFactory }) => {
let controller = instance.tabControllers[index];
if (!controller) {
controller = new controllerFactory(instance.view.querySelector(".tabContent[data-index='" + index + "']"), instance.params);
instance.tabControllers[index] = controller;
}
return controller;
});
}
}
export default HomeView;

View file

@ -349,7 +349,7 @@ import LibraryMenu from '../scripts/libraryMenu';
function showViewSettingsMenu() { function showViewSettingsMenu() {
const instance = this; const instance = this;
import('../components/viewSettings/viewSettings').then(({default: ViewSettings}) => { import('../components/viewSettings/viewSettings').then(({ default: ViewSettings }) => {
new ViewSettings().show({ new ViewSettings().show({
settingsKey: instance.getSettingsKey(), settingsKey: instance.getSettingsKey(),
settings: instance.getViewSettings(), settings: instance.getViewSettings(),
@ -364,7 +364,7 @@ import LibraryMenu from '../scripts/libraryMenu';
function showFilterMenu() { function showFilterMenu() {
const instance = this; const instance = this;
import('../components/filtermenu/filtermenu').then(({default: FilterMenu}) => { import('../components/filtermenu/filtermenu').then(({ default: FilterMenu }) => {
new FilterMenu().show({ new FilterMenu().show({
settingsKey: instance.getSettingsKey(), settingsKey: instance.getSettingsKey(),
settings: instance.getFilters(), settings: instance.getFilters(),
@ -383,7 +383,7 @@ import LibraryMenu from '../scripts/libraryMenu';
function showSortMenu() { function showSortMenu() {
const instance = this; const instance = this;
import('../components/sortmenu/sortmenu').then(({default: SortMenu}) => { import('../components/sortmenu/sortmenu').then(({ default: SortMenu }) => {
new SortMenu().show({ new SortMenu().show({
settingsKey: instance.getSettingsKey(), settingsKey: instance.getSettingsKey(),
settings: instance.getSortValues(), settings: instance.getSortValues(),
@ -401,7 +401,7 @@ import LibraryMenu from '../scripts/libraryMenu';
function onNewItemClick() { function onNewItemClick() {
const instance = this; const instance = this;
import('../components/playlisteditor/playlisteditor').then(({default: playlistEditor}) => { import('../components/playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
new playlistEditor({ new playlistEditor({
items: [], items: [],
serverId: instance.params.serverId serverId: instance.params.serverId
@ -772,7 +772,7 @@ class ItemsView {
} }
function autoFocus() { function autoFocus() {
import('../components/autoFocuser').then(({default: autoFocuser}) => { import('../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(view); autoFocuser.autoFocus(view);
}); });
} }

View file

@ -100,7 +100,7 @@ export default function (view, params, tabContent) {
} }
function showFilterMenu(context) { function showFilterMenu(context) {
import('../../components/filterdialog/filterdialog').then(({default: FilterDialog}) => { import('../../components/filterdialog/filterdialog').then(({ default: FilterDialog }) => {
const filterDialog = new FilterDialog({ const filterDialog = new FilterDialog({
query: getQuery(), query: getQuery(),
mode: 'livetvchannels', mode: 'livetvchannels',
@ -124,7 +124,7 @@ export default function (view, params, tabContent) {
loading.hide(); loading.hide();
isLoading = false; isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(context); autoFocuser.autoFocus(context);
}); });
}); });

View file

@ -61,7 +61,7 @@ function loadRecommendedPrograms(page) {
}); });
loading.hide(); loading.hide();
import('../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page); autoFocuser.autoFocus(page);
}); });
}); });
@ -273,7 +273,7 @@ export default function (view, params) {
break; break;
} }
import(`../livetv/${depends}`).then(({default: controllerFactory}) => { import(`../livetv/${depends}`).then(({ default: controllerFactory }) => {
let tabContent; let tabContent;
if (index === 0) { if (index === 0) {

View file

@ -9,7 +9,7 @@ function onListingsSubmitted() {
} }
function init(page, type, providerId) { function init(page, type, providerId) {
import(`../components/tvproviders/${type}`).then(({default: factory}) => { import(`../components/tvproviders/${type}`).then(({ default: factory }) => {
const instance = new factory(page, providerId, {}); const instance = new factory(page, providerId, {});
Events.on(instance, 'submitted', onListingsSubmitted); Events.on(instance, 'submitted', onListingsSubmitted);
instance.init(); instance.init();
@ -17,7 +17,7 @@ function init(page, type, providerId) {
} }
function loadTemplate(page, type, providerId) { function loadTemplate(page, type, providerId) {
import(`../components/tvproviders/${type}.template.html`).then(({default: html}) => { import(`../components/tvproviders/${type}.template.html`).then(({ default: html }) => {
page.querySelector('.providerTemplate').innerHTML = globalize.translateHtml(html); page.querySelector('.providerTemplate').innerHTML = globalize.translateHtml(html);
init(page, type, providerId); init(page, type, providerId);
}); });

View file

@ -64,7 +64,7 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
const page = this; const page = this;
$('.liveTvSettingsForm').off('submit', onSubmit).on('submit', onSubmit); $('.liveTvSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
$('#btnSelectRecordingPath', page).on('click.selectDirectory', function () { $('#btnSelectRecordingPath', page).on('click.selectDirectory', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
callback: function (path) { callback: function (path) {
@ -79,7 +79,7 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
}); });
}); });
$('#btnSelectMovieRecordingPath', page).on('click.selectDirectory', function () { $('#btnSelectMovieRecordingPath', page).on('click.selectDirectory', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
callback: function (path) { callback: function (path) {
@ -94,7 +94,7 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
}); });
}); });
$('#btnSelectSeriesRecordingPath', page).on('click.selectDirectory', function () { $('#btnSelectSeriesRecordingPath', page).on('click.selectDirectory', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
callback: function (path) { callback: function (path) {
@ -109,7 +109,7 @@ $(document).on('pageinit', '#liveTvSettingsPage', function () {
}); });
}); });
$('#btnSelectPostProcessorPath', page).on('click.selectDirectory', function () { $('#btnSelectPostProcessorPath', page).on('click.selectDirectory', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
includeFiles: true, includeFiles: true,

View file

@ -147,7 +147,7 @@ function showProviderOptions(page, providerId, button) {
id: 'map' id: 'map'
}); });
import('../components/actionSheet/actionSheet').then(({default: actionsheet}) => { import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({ actionsheet.show({
items: items, items: items,
positionTo: button positionTo: button
@ -165,7 +165,7 @@ function showProviderOptions(page, providerId, button) {
} }
function mapChannels(page, providerId) { function mapChannels(page, providerId) {
import('../components/channelMapper/channelMapper').then(({default: channelMapper}) => { import('../components/channelMapper/channelMapper').then(({ default: channelMapper }) => {
new channelMapper({ new channelMapper({
serverId: ApiClient.serverInfo().Id, serverId: ApiClient.serverInfo().Id,
providerId: providerId providerId: providerId
@ -237,7 +237,7 @@ function addProvider(button) {
id: 'xmltv' id: 'xmltv'
}); });
import('../components/actionSheet/actionSheet').then(({default: actionsheet}) => { import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({ actionsheet.show({
items: menuItems, items: menuItems,
positionTo: button, positionTo: button,
@ -263,7 +263,7 @@ function showDeviceMenu(button, tunerDeviceId) {
id: 'edit' id: 'edit'
}); });
import('../components/actionSheet/actionSheet').then(({default: actionsheet}) => { import('../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({ actionsheet.show({
items: items, items: items,
positionTo: button positionTo: button

View file

@ -106,7 +106,7 @@ function submitForm(page) {
} }
function getDetectedDevice() { function getDetectedDevice() {
return import('../components/tunerPicker').then(({default: tunerPicker}) => { return import('../components/tunerPicker').then(({ default: tunerPicker }) => {
return new tunerPicker().show({ return new tunerPicker().show({
serverId: ApiClient.serverId() serverId: ApiClient.serverId()
}); });
@ -222,7 +222,7 @@ export default function (view, params) {
}); });
}); });
view.querySelector('.btnSelectPath').addEventListener('click', function () { view.querySelector('.btnSelectPath').addEventListener('click', function () {
import('../components/directorybrowser/directorybrowser').then(({default: DirectoryBrowser}) => { import('../components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser(); const picker = new DirectoryBrowser();
picker.show({ picker.show({
includeFiles: true, includeFiles: true,

View file

@ -0,0 +1,261 @@
import loading from '../../components/loading/loading';
import libraryBrowser from '../../scripts/libraryBrowser';
import imageLoader from '../../components/images/imageLoader';
import listView from '../../components/listview/listview';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import * as userSettings from '../../scripts/settings/userSettings';
import globalize from '../../scripts/globalize';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
export default function (view, params, tabContent) {
function getPageData() {
const key = getSavedQueryKey();
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'BoxSet',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,SortName',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
StartIndex: 0
},
view: libraryBrowser.getSavedView(key) || 'Poster'
};
if (userSettings.libraryPageSize() > 0) {
pageData.query['Limit'] = userSettings.libraryPageSize();
}
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
return pageData;
}
function getQuery() {
return getPageData().query;
}
function getSavedQueryKey() {
return params.topParentId + '-' + 'moviecollections';
}
const onViewStyleChange = () => {
const viewStyle = this.getCurrentViewStyle();
const itemsContainer = tabContent.querySelector('.itemsContainer');
if (viewStyle == 'List') {
itemsContainer.classList.add('vertical-list');
itemsContainer.classList.remove('vertical-wrap');
} else {
itemsContainer.classList.remove('vertical-list');
itemsContainer.classList.add('vertical-wrap');
}
itemsContainer.innerHTML = '';
};
const reloadItems = (page) => {
loading.show();
isLoading = true;
const query = getQuery();
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then((result) => {
function onNextPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex += query.Limit;
}
reloadItems(tabContent);
}
function onPreviousPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
}
reloadItems(tabContent);
}
window.scrollTo(0, 0);
let html;
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
const viewStyle = this.getCurrentViewStyle();
if (viewStyle == 'Thumb') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
overlayPlayButton: true,
centerText: true,
showTitle: true
});
} else if (viewStyle == 'ThumbCard') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
lazy: true,
cardLayout: true,
showTitle: true
});
} else if (viewStyle == 'Banner') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'banner',
preferBanner: true,
context: 'movies',
lazy: true
});
} else if (viewStyle == 'List') {
html = listView.getListViewHtml({
items: result.Items,
context: 'movies',
sortBy: query.SortBy
});
} else if (viewStyle == 'PosterCard') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'auto',
context: 'movies',
showTitle: true,
centerText: false,
cardLayout: true
});
} else {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'auto',
context: 'movies',
centerText: true,
overlayPlayButton: true,
showTitle: true
});
}
let elems = tabContent.querySelectorAll('.paging');
for (const elem of elems) {
elem.innerHTML = pagingHtml;
}
elems = tabContent.querySelectorAll('.btnNextPage');
for (const elem of elems) {
elem.addEventListener('click', onNextPageClick);
}
elems = tabContent.querySelectorAll('.btnPreviousPage');
for (const elem of elems) {
elem.addEventListener('click', onPreviousPageClick);
}
if (!result.Items.length) {
html = '';
html += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoCollectionsAvailable') + '</p>';
html += '</div>';
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
isLoading = false;
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
};
const data = {};
let isLoading = false;
this.getCurrentViewStyle = function () {
return getPageData().view;
};
const initPage = (tabElement) => {
tabElement.querySelector('.btnSort').addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('Name'),
id: 'SortName'
}, {
name: globalize.translate('OptionImdbRating'),
id: 'CommunityRating,SortName'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName'
}, {
name: globalize.translate('OptionParentalRating'),
id: 'OfficialRating,SortName'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,SortName'
}],
callback: function () {
getQuery().StartIndex = 0;
reloadItems(tabElement);
},
query: getQuery(),
button: e.target
});
});
const btnSelectView = tabElement.querySelector('.btnSelectView');
btnSelectView.addEventListener('click', (e) => {
libraryBrowser.showLayoutMenu(e.target, this.getCurrentViewStyle(), 'List,Poster,PosterCard,Thumb,ThumbCard'.split(','));
});
btnSelectView.addEventListener('layoutchange', function (e) {
const viewStyle = e.detail.viewStyle;
getPageData().view = viewStyle;
libraryBrowser.saveViewSetting(getSavedQueryKey(), viewStyle);
getQuery().StartIndex = 0;
onViewStyleChange();
reloadItems(tabElement);
});
tabElement.querySelector('.btnNewCollection').addEventListener('click', () => {
import('../../components/collectionEditor/collectionEditor').then(({ default: CollectionEditor }) => {
const serverId = ApiClient.serverInfo().Id;
const collectionEditor = new CollectionEditor();
collectionEditor.show({
items: [],
serverId: serverId
});
});
});
};
initPage(tabContent);
onViewStyleChange();
this.renderTab = function () {
reloadItems(tabContent);
};
}

View file

@ -0,0 +1,221 @@
import escapeHtml from 'escape-html';
import layoutManager from '../../components/layoutManager';
import loading from '../../components/loading/loading';
import libraryBrowser from '../../scripts/libraryBrowser';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver';
import globalize from '../../scripts/globalize';
import { appRouter } from '../../components/appRouter';
import '../../elements/emby-button/emby-button';
export default function (view, params, tabContent) {
function getPageData() {
const key = getSavedQueryKey();
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Movie',
Recursive: true,
EnableTotalRecordCount: false
},
view: 'Poster'
};
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
return pageData;
}
function getQuery() {
return getPageData().query;
}
function getSavedQueryKey() {
return params.topParentId + '-' + 'moviegenres';
}
function getPromise() {
loading.show();
const query = getQuery();
return ApiClient.getGenres(ApiClient.getCurrentUserId(), query);
}
function enableScrollX() {
return !layoutManager.desktop;
}
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
const fillItemsContainer = (entry) => {
const elem = entry.target;
const id = elem.getAttribute('data-id');
const viewStyle = this.getCurrentViewStyle();
let limit = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 5 : 9;
if (enableScrollX()) {
limit = 10;
}
const enableImageTypes = viewStyle == 'Thumb' || viewStyle == 'ThumbCard' ? 'Primary,Backdrop,Thumb' : 'Primary';
const query = {
SortBy: 'Random',
SortOrder: 'Ascending',
IncludeItemTypes: 'Movie',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: enableImageTypes,
Limit: limit,
GenreIds: id,
EnableTotalRecordCount: false,
ParentId: params.topParentId
};
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then(function (result) {
if (viewStyle == 'Thumb') {
cardBuilder.buildCards(result.Items, {
itemsContainer: elem,
shape: getThumbShape(),
preferThumb: true,
showTitle: true,
scalable: true,
centerText: true,
overlayMoreButton: true,
allowBottomPadding: false
});
} else if (viewStyle == 'ThumbCard') {
cardBuilder.buildCards(result.Items, {
itemsContainer: elem,
shape: getThumbShape(),
preferThumb: true,
showTitle: true,
scalable: true,
centerText: false,
cardLayout: true,
showYear: true
});
} else if (viewStyle == 'PosterCard') {
cardBuilder.buildCards(result.Items, {
itemsContainer: elem,
shape: getPortraitShape(),
showTitle: true,
scalable: true,
centerText: false,
cardLayout: true,
showYear: true
});
} else if (viewStyle == 'Poster') {
cardBuilder.buildCards(result.Items, {
itemsContainer: elem,
shape: getPortraitShape(),
scalable: true,
overlayMoreButton: true,
allowBottomPadding: true,
showTitle: true,
centerText: true,
showYear: true
});
}
if (result.Items.length >= query.Limit) {
tabContent.querySelector('.btnMoreFromGenre' + id + ' .material-icons').classList.remove('hide');
}
});
};
function reloadItems(context, promise) {
const query = getQuery();
promise.then(function (result) {
const elem = context.querySelector('#items');
let html = '';
const items = result.Items;
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
html += '<div class="verticalSection">';
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item, {
context: 'movies',
parentId: params.topParentId
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre' + item.Id + '">';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += escapeHtml(item.Name);
html += '</h2>';
html += '<span class="material-icons hide chevron_right" aria-hidden="true"></span>';
html += '</a>';
html += '</div>';
if (enableScrollX()) {
let scrollXClass = 'scrollX hiddenScrollX';
if (layoutManager.tv) {
scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale';
}
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' lazy padded-left padded-right" data-id="' + item.Id + '">';
} else {
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap lazy padded-left padded-right" data-id="' + item.Id + '">';
}
html += '</div>';
html += '</div>';
}
if (!result.Items.length) {
html = '';
html += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoGenresAvailable') + '</p>';
html += '</div>';
}
elem.innerHTML = html;
lazyLoader.lazyChildren(elem, fillItemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
});
}
const fullyReload = () => {
this.preRender();
this.renderTab();
};
const data = {};
this.getViewStyles = function () {
return 'Poster,PosterCard,Thumb,ThumbCard'.split(',');
};
this.getCurrentViewStyle = function () {
return getPageData().view;
};
this.setCurrentViewStyle = function (viewStyle) {
getPageData().view = viewStyle;
libraryBrowser.saveViewSetting(getSavedQueryKey(), viewStyle);
fullyReload();
};
this.enableViewSelection = true;
let promise;
this.preRender = function () {
promise = getPromise();
};
this.renderTab = function () {
reloadItems(tabContent, promise);
};
}

View file

@ -0,0 +1,92 @@
<div id="moviesPage" data-role="page" data-dom-cache="true" class="page libraryPage backdropPage collectionEditorPage pageWithAbsoluteTabs withTabs" data-backdroptype="movie">
<div class="pageTabContent" id="moviesTab" data-index="0">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnShuffle autoSize hide" title="${Shuffle}"><span class="material-icons shuffle" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnFilter autoSize" title="${Filter}"><span class="material-icons filter_list" aria-hidden="true"></span></button>
</div>
<div class="alphaPicker alphaPicker-fixed alphaPicker-vertical">
</div>
<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right">
</div>
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
</div>
</div>
<div class="pageTabContent" id="suggestionsTab" data-index="1">
<div id="resumableSection" class="verticalSection hide">
<div class="sectionTitleContainer sectionTitleContainer-cards">
<h2 class="sectionTitle sectionTitle-cards padded-left">${HeaderContinueWatching}</h2>
</div>
<div is="emby-itemscontainer" id="resumableItems" class="itemsContainer padded-left padded-right">
</div>
</div>
<div class="verticalSection">
<div class="sectionTitleContainer sectionTitleContainer-cards">
<h2 class="sectionTitle sectionTitle-cards padded-left">${HeaderLatestMovies}</h2>
</div>
<div is="emby-itemscontainer" id="recentlyAddedItems" class="itemsContainer padded-left padded-right">
</div>
</div>
<div class="recommendations">
</div>
<div class="noItemsMessage hide padded-left padded-right">
<br />
<p>${MessageNoMovieSuggestionsAvailable}</p>
</div>
</div>
<div class="pageTabContent" id="trailersTab" data-index="2">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnFilter autoSize" title="${Filter}"><span class="material-icons filter_list" aria-hidden="true"></span></button>
</div>
<div class="alphaPicker alphaPicker-fixed alphaPicker-fixed-right alphaPicker-vertical">
</div>
<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">
</div>
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
</div>
</div>
<div class="pageTabContent" id="favoritesTab" data-index="3">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
</div>
<div is="emby-itemscontainer" class="itemsContainer padded-left padded-right">
</div>
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
</div>
</div>
<div class="pageTabContent" id="collectionsTab" data-index="4">
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
<button is="paper-icon-button-light" class="btnSelectView autoSize" title="${ButtonSelectView}"><span class="material-icons view_comfy" aria-hidden="true"></span></button>
<button is="paper-icon-button-light" class="btnSort autoSize" title="${Sort}"><span class="material-icons sort_by_alpha" aria-hidden="true"></span></button>
<button type="button" is="paper-icon-button-light" class="btnNewCollection autoSize" title="${NewCollection}"><span class="material-icons add" aria-hidden="true"></span></button>
</div>
<div is="emby-itemscontainer" class="itemsContainer vertical-wrap centered padded-left padded-right" style="text-align:center;">
</div>
<div class="flex align-items-center justify-content-center flex-wrap-wrap padded-top padded-left padded-right padded-bottom focuscontainer-x">
<div class="paging"></div>
</div>
</div>
<div class="pageTabContent" id="genresTab" data-index="5">
<div id="items"></div>
</div>
</div>

View file

@ -0,0 +1,324 @@
import loading from '../../components/loading/loading';
import * as userSettings from '../../scripts/settings/userSettings';
import libraryBrowser from '../../scripts/libraryBrowser';
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
import listView from '../../components/listview/listview';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import globalize from '../../scripts/globalize';
import Events from '../../utils/events.ts';
import { playbackManager } from '../../components/playback/playbackmanager';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
export default function (view, params, tabContent, options) {
const onViewStyleChange = () => {
if (this.getCurrentViewStyle() == 'List') {
itemsContainer.classList.add('vertical-list');
itemsContainer.classList.remove('vertical-wrap');
} else {
itemsContainer.classList.remove('vertical-list');
itemsContainer.classList.add('vertical-wrap');
}
itemsContainer.innerHTML = '';
};
function fetchData() {
isLoading = true;
loading.show();
return ApiClient.getItems(ApiClient.getCurrentUserId(), query);
}
function shuffle() {
ApiClient.getItem(
ApiClient.getCurrentUserId(),
params.topParentId
).then((item) => {
playbackManager.shuffle(item);
});
}
const afterRefresh = (result) => {
function onNextPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex += query.Limit;
}
itemsContainer.refreshItems();
}
function onPreviousPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
}
itemsContainer.refreshItems();
}
window.scrollTo(0, 0);
this.alphaPicker?.updateControls(query);
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
for (const elem of tabContent.querySelectorAll('.paging')) {
elem.innerHTML = pagingHtml;
}
for (const elem of tabContent.querySelectorAll('.btnNextPage')) {
elem.addEventListener('click', onNextPageClick);
}
for (const elem of tabContent.querySelectorAll('.btnPreviousPage')) {
elem.addEventListener('click', onPreviousPageClick);
}
tabContent.querySelector('.btnShuffle').classList.toggle('hide', result.TotalRecordCount < 1);
isLoading = false;
loading.hide();
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(tabContent);
});
};
const getItemsHtml = (items) => {
let html;
const viewStyle = this.getCurrentViewStyle();
if (viewStyle == 'Thumb') {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
lazy: true,
overlayPlayButton: true,
showTitle: true,
showYear: true,
centerText: true
});
} else if (viewStyle == 'ThumbCard') {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
lazy: true,
cardLayout: true,
showTitle: true,
showYear: true,
centerText: true
});
} else if (viewStyle == 'Banner') {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'banner',
preferBanner: true,
context: 'movies',
lazy: true
});
} else if (viewStyle == 'List') {
html = listView.getListViewHtml({
items: items,
context: 'movies',
sortBy: query.SortBy
});
} else if (viewStyle == 'PosterCard') {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'portrait',
context: 'movies',
showTitle: true,
showYear: true,
centerText: true,
lazy: true,
cardLayout: true
});
} else {
html = cardBuilder.getCardsHtml({
items: items,
shape: 'portrait',
context: 'movies',
overlayPlayButton: true,
showTitle: true,
showYear: true,
centerText: true
});
}
return html;
};
const initPage = (tabElement) => {
itemsContainer.fetchData = fetchData;
itemsContainer.getItemsHtml = getItemsHtml;
itemsContainer.afterRefresh = afterRefresh;
const alphaPickerElement = tabElement.querySelector('.alphaPicker');
if (alphaPickerElement) {
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
const newValue = e.detail.value;
if (newValue === '#') {
query.NameLessThan = 'A';
delete query.NameStartsWith;
} else {
query.NameStartsWith = newValue;
delete query.NameLessThan;
}
query.StartIndex = 0;
itemsContainer.refreshItems();
});
this.alphaPicker = new AlphaPicker({
element: alphaPickerElement,
valueChangeEvent: 'click'
});
tabElement.querySelector('.alphaPicker').classList.add('alphabetPicker-right');
alphaPickerElement.classList.add('alphaPicker-fixed-right');
itemsContainer.classList.add('padded-right-withalphapicker');
}
const btnFilter = tabElement.querySelector('.btnFilter');
if (btnFilter) {
btnFilter.addEventListener('click', () => {
this.showFilterMenu();
});
}
const btnSort = tabElement.querySelector('.btnSort');
if (btnSort) {
btnSort.addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('Name'),
id: 'SortName,ProductionYear'
}, {
name: globalize.translate('OptionRandom'),
id: 'Random'
}, {
name: globalize.translate('OptionImdbRating'),
id: 'CommunityRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionCriticRating'),
id: 'CriticRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName,ProductionYear'
}, {
name: globalize.translate('OptionDatePlayed'),
id: 'DatePlayed,SortName,ProductionYear'
}, {
name: globalize.translate('OptionParentalRating'),
id: 'OfficialRating,SortName,ProductionYear'
}, {
name: globalize.translate('OptionPlayCount'),
id: 'PlayCount,SortName,ProductionYear'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,SortName,ProductionYear'
}, {
name: globalize.translate('Runtime'),
id: 'Runtime,SortName,ProductionYear'
}],
callback: function () {
query.StartIndex = 0;
userSettings.saveQuerySettings(savedQueryKey, query);
itemsContainer.refreshItems();
},
query: query,
button: e.target
});
});
}
const btnSelectView = tabElement.querySelector('.btnSelectView');
btnSelectView.addEventListener('click', (e) => {
libraryBrowser.showLayoutMenu(e.target, this.getCurrentViewStyle(), 'Banner,List,Poster,PosterCard,Thumb,ThumbCard'.split(','));
});
btnSelectView.addEventListener('layoutchange', function (e) {
const viewStyle = e.detail.viewStyle;
userSettings.set(savedViewKey, viewStyle);
query.StartIndex = 0;
onViewStyleChange();
itemsContainer.refreshItems();
});
tabElement.querySelector('.btnShuffle').addEventListener('click', shuffle);
};
let itemsContainer = tabContent.querySelector('.itemsContainer');
const savedQueryKey = params.topParentId + '-' + options.mode;
const savedViewKey = savedQueryKey + '-view';
let query = {
SortBy: 'SortName,ProductionYear',
SortOrder: 'Ascending',
IncludeItemTypes: 'Movie',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
StartIndex: 0,
ParentId: params.topParentId
};
if (userSettings.libraryPageSize() > 0) {
query['Limit'] = userSettings.libraryPageSize();
}
let isLoading = false;
if (options.mode === 'favorites') {
query.IsFavorite = true;
}
query = userSettings.loadQuerySettings(savedQueryKey, query);
this.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({
query: query,
mode: 'movies',
serverId: ApiClient.serverId()
});
Events.on(filterDialog, 'filterchange', () => {
query.StartIndex = 0;
itemsContainer.refreshItems();
});
filterDialog.show();
});
};
this.getCurrentViewStyle = function () {
return userSettings.get(savedViewKey) || 'Poster';
};
this.initTab = function () {
initPage(tabContent);
onViewStyleChange();
};
this.renderTab = () => {
itemsContainer.refreshItems();
this.alphaPicker?.updateControls(query);
};
this.destroy = function () {
itemsContainer = null;
};
}

View file

@ -0,0 +1,425 @@
import escapeHtml from 'escape-html';
import layoutManager from '../../components/layoutManager';
import inputManager from '../../scripts/inputManager';
import * as userSettings from '../../scripts/settings/userSettings';
import libraryMenu from '../../scripts/libraryMenu';
import * as mainTabsManager from '../../components/maintabsmanager';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import dom from '../../scripts/dom';
import imageLoader from '../../components/images/imageLoader';
import { playbackManager } from '../../components/playback/playbackmanager';
import globalize from '../../scripts/globalize';
import Dashboard from '../../utils/dashboard';
import Events from '../../utils/events.ts';
import '../../elements/emby-scroller/emby-scroller';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
import '../../elements/emby-tabs/emby-tabs';
import '../../elements/emby-button/emby-button';
function enableScrollX() {
return !layoutManager.desktop;
}
function getPortraitShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function loadLatest(page, userId, parentId) {
const options = {
IncludeItemTypes: 'Movie',
Limit: 18,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ParentId: parentId,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false
};
ApiClient.getJSON(ApiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(function (items) {
const allowBottomPadding = !enableScrollX();
const container = page.querySelector('#recentlyAddedItems');
cardBuilder.buildCards(items, {
itemsContainer: container,
shape: getPortraitShape(),
scalable: true,
overlayPlayButton: true,
allowBottomPadding: allowBottomPadding,
showTitle: true,
showYear: true,
centerText: true
});
// FIXME: Wait for all sections to load
autoFocus(page);
});
}
function loadResume(page, userId, parentId) {
const screenWidth = dom.getWindowSize().innerWidth;
const options = {
SortBy: 'DatePlayed',
SortOrder: 'Descending',
IncludeItemTypes: 'Movie',
Filters: 'IsResumable',
Limit: screenWidth >= 1600 ? 5 : 3,
Recursive: true,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
CollapseBoxSetItems: false,
ParentId: parentId,
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
EnableTotalRecordCount: false
};
ApiClient.getItems(userId, options).then(function (result) {
if (result.Items.length) {
page.querySelector('#resumableSection').classList.remove('hide');
} else {
page.querySelector('#resumableSection').classList.add('hide');
}
const allowBottomPadding = !enableScrollX();
const container = page.querySelector('#resumableItems');
cardBuilder.buildCards(result.Items, {
itemsContainer: container,
preferThumb: true,
shape: getThumbShape(),
scalable: true,
overlayPlayButton: true,
allowBottomPadding: allowBottomPadding,
cardLayout: false,
showTitle: true,
showYear: true,
centerText: true
});
// FIXME: Wait for all sections to load
autoFocus(page);
});
}
function getRecommendationHtml(recommendation) {
let html = '';
let title = '';
switch (recommendation.RecommendationType) {
case 'SimilarToRecentlyPlayed':
title = globalize.translate('RecommendationBecauseYouWatched', recommendation.BaselineItemName);
break;
case 'SimilarToLikedItem':
title = globalize.translate('RecommendationBecauseYouLike', recommendation.BaselineItemName);
break;
case 'HasDirectorFromRecentlyPlayed':
case 'HasLikedDirector':
title = globalize.translate('RecommendationDirectedBy', recommendation.BaselineItemName);
break;
case 'HasActorFromRecentlyPlayed':
case 'HasLikedActor':
title = globalize.translate('RecommendationStarring', recommendation.BaselineItemName);
break;
}
html += '<div class="verticalSection">';
html += '<h2 class="sectionTitle sectionTitle-cards padded-left">' + escapeHtml(title) + '</h2>';
const allowBottomPadding = true;
if (enableScrollX()) {
html += '<div is="emby-scroller" class="padded-top-focusscale padded-bottom-focusscale" data-mousewheel="false" data-centerfocus="true">';
html += '<div is="emby-itemscontainer" class="itemsContainer scrollSlider focuscontainer-x">';
} else {
html += '<div is="emby-itemscontainer" class="itemsContainer focuscontainer-x padded-left padded-right vertical-wrap">';
}
html += cardBuilder.getCardsHtml(recommendation.Items, {
shape: getPortraitShape(),
scalable: true,
overlayPlayButton: true,
allowBottomPadding: allowBottomPadding,
showTitle: true,
showYear: true,
centerText: true
});
if (enableScrollX()) {
html += '</div>';
}
html += '</div>';
html += '</div>';
return html;
}
function loadSuggestions(page, userId) {
const screenWidth = dom.getWindowSize().innerWidth;
let itemLimit = 5;
if (screenWidth >= 1600) {
itemLimit = 8;
} else if (screenWidth >= 1200) {
itemLimit = 6;
}
const url = ApiClient.getUrl('Movies/Recommendations', {
userId: userId,
categoryLimit: 6,
ItemLimit: itemLimit,
Fields: 'PrimaryImageAspectRatio,MediaSourceCount,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb'
});
ApiClient.getJSON(url).then(function (recommendations) {
if (!recommendations.length) {
page.querySelector('.noItemsMessage').classList.remove('hide');
page.querySelector('.recommendations').innerHTML = '';
return;
}
const html = recommendations.map(getRecommendationHtml).join('');
page.querySelector('.noItemsMessage').classList.add('hide');
const recs = page.querySelector('.recommendations');
recs.innerHTML = html;
imageLoader.lazyChildren(recs);
// FIXME: Wait for all sections to load
autoFocus(page);
});
}
function autoFocus(page) {
import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
}
function setScrollClasses(elem, scrollX) {
if (scrollX) {
elem.classList.add('hiddenScrollX');
if (layoutManager.tv) {
elem.classList.add('smoothScrollX');
elem.classList.add('padded-top-focusscale');
elem.classList.add('padded-bottom-focusscale');
}
elem.classList.add('scrollX');
elem.classList.remove('vertical-wrap');
} else {
elem.classList.remove('hiddenScrollX');
elem.classList.remove('smoothScrollX');
elem.classList.remove('scrollX');
elem.classList.add('vertical-wrap');
}
}
function initSuggestedTab(page, tabContent) {
const containers = tabContent.querySelectorAll('.itemsContainer');
for (const container of containers) {
setScrollClasses(container, enableScrollX());
}
}
function loadSuggestionsTab(view, params, tabContent) {
const parentId = params.topParentId;
const userId = ApiClient.getCurrentUserId();
loadResume(tabContent, userId, parentId);
loadLatest(tabContent, userId, parentId);
loadSuggestions(tabContent, userId);
}
function getTabs() {
return [{
name: globalize.translate('Movies')
}, {
name: globalize.translate('Suggestions')
}, {
name: globalize.translate('Trailers')
}, {
name: globalize.translate('Favorites')
}, {
name: globalize.translate('Collections')
}, {
name: globalize.translate('Genres')
}];
}
function getDefaultTabIndex(folderId) {
switch (userSettings.get('landing-' + folderId)) {
case 'suggestions':
return 1;
case 'favorites':
return 3;
case 'collections':
return 4;
case 'genres':
return 5;
default:
return 0;
}
}
export default function (view, params) {
function onBeforeTabChange(e) {
preLoadTab(view, parseInt(e.detail.selectedTabIndex, 10));
}
function onTabChange(e) {
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
loadTab(view, newIndex);
}
function getTabContainers() {
return view.querySelectorAll('.pageTabContent');
}
function initTabs() {
mainTabsManager.setTabs(view, currentTabIndex, getTabs, getTabContainers, onBeforeTabChange, onTabChange);
}
const getTabController = (page, index, callback) => {
let depends = '';
switch (index) {
case 0:
depends = 'movies';
break;
case 1:
depends = 'moviesrecommended.js';
break;
case 2:
depends = 'movietrailers';
break;
case 3:
depends = 'movies';
break;
case 4:
depends = 'moviecollections';
break;
case 5:
depends = 'moviegenres';
break;
}
import(`../movies/${depends}`).then(({ default: controllerFactory }) => {
let tabContent;
if (index === suggestionsTabIndex) {
tabContent = view.querySelector(".pageTabContent[data-index='" + index + "']");
this.tabContent = tabContent;
}
let controller = tabControllers[index];
if (!controller) {
tabContent = view.querySelector(".pageTabContent[data-index='" + index + "']");
if (index === suggestionsTabIndex) {
controller = this;
} else if (index == 0 || index == 3) {
controller = new controllerFactory(view, params, tabContent, {
mode: index ? 'favorites' : 'movies'
});
} else {
controller = new controllerFactory(view, params, tabContent);
}
tabControllers[index] = controller;
if (controller.initTab) {
controller.initTab();
}
}
callback(controller);
});
};
function preLoadTab(page, index) {
getTabController(page, index, function (controller) {
if (renderedTabs.indexOf(index) == -1 && controller.preRender) {
controller.preRender();
}
});
}
function loadTab(page, index) {
currentTabIndex = index;
getTabController(page, index, ((controller) => {
if (renderedTabs.indexOf(index) == -1) {
renderedTabs.push(index);
controller.renderTab();
}
}));
}
function onPlaybackStop(e, state) {
if (state.NowPlayingItem && state.NowPlayingItem.MediaType == 'Video') {
renderedTabs = [];
mainTabsManager.getTabsElement().triggerTabChange();
}
}
function onInputCommand(e) {
if (e.detail.command === 'search') {
e.preventDefault();
Dashboard.navigate('search.html?collectionType=movies&parentId=' + params.topParentId);
}
}
let currentTabIndex = parseInt(params.tab || getDefaultTabIndex(params.topParentId), 10);
const suggestionsTabIndex = 1;
this.initTab = function () {
const tabContent = view.querySelector(".pageTabContent[data-index='" + suggestionsTabIndex + "']");
initSuggestedTab(view, tabContent);
};
this.renderTab = function () {
const tabContent = view.querySelector(".pageTabContent[data-index='" + suggestionsTabIndex + "']");
loadSuggestionsTab(view, params, tabContent);
};
const tabControllers = [];
let renderedTabs = [];
view.addEventListener('viewshow', function () {
initTabs();
if (!view.getAttribute('data-title')) {
const parentId = params.topParentId;
if (parentId) {
ApiClient.getItem(ApiClient.getCurrentUserId(), parentId).then(function (item) {
view.setAttribute('data-title', item.Name);
libraryMenu.setTitle(item.Name);
});
} else {
view.setAttribute('data-title', globalize.translate('Movies'));
libraryMenu.setTitle(globalize.translate('Movies'));
}
}
Events.on(playbackManager, 'playbackstop', onPlaybackStop);
inputManager.on(window, onInputCommand);
});
view.addEventListener('viewbeforehide', function () {
inputManager.off(window, onInputCommand);
});
for (const tabController of tabControllers) {
if (tabController.destroy) {
tabController.destroy();
}
}
}

View file

@ -0,0 +1,272 @@
import loading from '../../components/loading/loading';
import libraryBrowser from '../../scripts/libraryBrowser';
import imageLoader from '../../components/images/imageLoader';
import { AlphaPicker } from '../../components/alphaPicker/alphaPicker';
import listView from '../../components/listview/listview';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import * as userSettings from '../../scripts/settings/userSettings';
import globalize from '../../scripts/globalize';
import Events from '../../utils/events.ts';
import '../../elements/emby-itemscontainer/emby-itemscontainer';
export default function (view, params, tabContent) {
function getPageData() {
const key = getSavedQueryKey();
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Trailer',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo',
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
StartIndex: 0
},
view: libraryBrowser.getSavedView(key) || 'Poster'
};
if (userSettings.libraryPageSize() > 0) {
pageData.query['Limit'] = userSettings.libraryPageSize();
}
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
return pageData;
}
function getQuery() {
return getPageData().query;
}
function getSavedQueryKey() {
return params.topParentId + '-' + 'trailers';
}
const reloadItems = () => {
loading.show();
isLoading = true;
const query = getQuery();
ApiClient.getItems(ApiClient.getCurrentUserId(), query).then((result) => {
function onNextPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex += query.Limit;
}
reloadItems();
}
function onPreviousPageClick() {
if (isLoading) {
return;
}
if (userSettings.libraryPageSize() > 0) {
query.StartIndex = Math.max(0, query.StartIndex - query.Limit);
}
reloadItems();
}
window.scrollTo(0, 0);
this.alphaPicker?.updateControls(query);
const pagingHtml = libraryBrowser.getQueryPagingHtml({
startIndex: query.StartIndex,
limit: query.Limit,
totalRecordCount: result.TotalRecordCount,
showLimit: false,
updatePageSizeSetting: false,
addLayoutButton: false,
sortButton: false,
filterButton: false
});
let html;
const viewStyle = this.getCurrentViewStyle();
if (viewStyle == 'Thumb') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
overlayPlayButton: true
});
} else if (viewStyle == 'ThumbCard') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'backdrop',
preferThumb: true,
context: 'movies',
cardLayout: true,
showTitle: true,
showYear: true,
centerText: true
});
} else if (viewStyle == 'Banner') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'banner',
preferBanner: true,
context: 'movies'
});
} else if (viewStyle == 'List') {
html = listView.getListViewHtml({
items: result.Items,
context: 'movies',
sortBy: query.SortBy
});
} else if (viewStyle == 'PosterCard') {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'portrait',
context: 'movies',
showTitle: true,
showYear: true,
cardLayout: true,
centerText: true
});
} else {
html = cardBuilder.getCardsHtml({
items: result.Items,
shape: 'portrait',
context: 'movies',
centerText: true,
overlayPlayButton: true,
showTitle: true,
showYear: true
});
}
let elems = tabContent.querySelectorAll('.paging');
for (const elem of elems) {
elem.innerHTML = pagingHtml;
}
elems = tabContent.querySelectorAll('.btnNextPage');
for (const elem of elems) {
elem.addEventListener('click', onNextPageClick);
}
elems = tabContent.querySelectorAll('.btnPreviousPage');
for (const elem of elems) {
elem.addEventListener('click', onPreviousPageClick);
}
if (!result.Items.length) {
html = '';
html += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoTrailersFound') + '</p>';
html += '</div>';
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
isLoading = false;
});
};
const data = {};
let isLoading = false;
this.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({
query: getQuery(),
mode: 'movies',
serverId: ApiClient.serverId()
});
Events.on(filterDialog, 'filterchange', function () {
getQuery().StartIndex = 0;
reloadItems();
});
filterDialog.show();
});
};
this.getCurrentViewStyle = function () {
return getPageData().view;
};
const initPage = (tabElement) => {
const alphaPickerElement = tabElement.querySelector('.alphaPicker');
const itemsContainer = tabElement.querySelector('.itemsContainer');
alphaPickerElement.addEventListener('alphavaluechanged', function (e) {
const newValue = e.detail.value;
const query = getQuery();
if (newValue === '#') {
query.NameLessThan = 'A';
delete query.NameStartsWith;
} else {
query.NameStartsWith = newValue;
delete query.NameLessThan;
}
query.StartIndex = 0;
reloadItems();
});
this.alphaPicker = new AlphaPicker({
element: alphaPickerElement,
valueChangeEvent: 'click'
});
tabElement.querySelector('.alphaPicker').classList.add('alphabetPicker-right');
alphaPickerElement.classList.add('alphaPicker-fixed-right');
itemsContainer.classList.add('padded-right-withalphapicker');
tabElement.querySelector('.btnFilter').addEventListener('click', () => {
this.showFilterMenu();
});
tabElement.querySelector('.btnSort').addEventListener('click', function (e) {
libraryBrowser.showSortMenu({
items: [{
name: globalize.translate('Name'),
id: 'SortName'
}, {
name: globalize.translate('OptionImdbRating'),
id: 'CommunityRating,SortName'
}, {
name: globalize.translate('OptionDateAdded'),
id: 'DateCreated,SortName'
}, {
name: globalize.translate('OptionDatePlayed'),
id: 'DatePlayed,SortName'
}, {
name: globalize.translate('OptionParentalRating'),
id: 'OfficialRating,SortName'
}, {
name: globalize.translate('OptionPlayCount'),
id: 'PlayCount,SortName'
}, {
name: globalize.translate('OptionReleaseDate'),
id: 'PremiereDate,SortName'
}],
callback: function () {
getQuery().StartIndex = 0;
reloadItems();
},
query: getQuery(),
button: e.target
});
});
};
initPage(tabContent);
this.renderTab = () => {
reloadItems();
this.alphaPicker?.updateControls(getQuery());
};
}

View file

@ -180,7 +180,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
loading.hide(); loading.hide();
isLoading = false; isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(tabContent); autoFocuser.autoFocus(tabContent);
}); });
}); });
@ -191,7 +191,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
let isLoading = false; let isLoading = false;
this.showFilterMenu = function () { this.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({ const filterDialog = new filterDialogFactory({
query: getQuery(), query: getQuery(),
mode: 'albums', mode: 'albums',

View file

@ -162,7 +162,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
loading.hide(); loading.hide();
isLoading = false; isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(tabContent); autoFocuser.autoFocus(tabContent);
}); });
}); });
@ -172,7 +172,7 @@ import '../../elements/emby-itemscontainer/emby-itemscontainer';
let isLoading = false; let isLoading = false;
this.showFilterMenu = function () { this.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({ const filterDialog = new filterDialogFactory({
query: getQuery(tabContent), query: getQuery(tabContent),
mode: this.mode, mode: this.mode,

View file

@ -92,7 +92,7 @@ import loading from '../../components/loading/loading';
libraryBrowser.saveQueryValues(getSavedQueryKey(), query); libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide(); loading.hide();
import('../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(context); autoFocuser.autoFocus(context);
}); });
}); });

View file

@ -63,7 +63,7 @@ import loading from '../../components/loading/loading';
libraryBrowser.saveQueryValues(getSavedQueryKey(), query); libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide(); loading.hide();
import('../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(context); autoFocuser.autoFocus(context);
}); });
}); });

View file

@ -75,7 +75,7 @@ import Dashboard from '../../utils/dashboard';
imageLoader.lazyChildren(elem); imageLoader.lazyChildren(elem);
loading.hide(); loading.hide();
import('../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page); autoFocuser.autoFocus(page);
}); });
}); });
@ -171,7 +171,7 @@ import Dashboard from '../../utils/dashboard';
loadRecentlyPlayed(tabContent, parentId); loadRecentlyPlayed(tabContent, parentId);
loadFrequentlyPlayed(tabContent, parentId); loadFrequentlyPlayed(tabContent, parentId);
import('../../components/favoriteitems').then(({default: favoriteItems}) => { import('../../components/favoriteitems').then(({ default: favoriteItems }) => {
favoriteItems.render(tabContent, ApiClient.getCurrentUserId(), parentId, ['favoriteArtists', 'favoriteAlbums', 'favoriteSongs']); favoriteItems.render(tabContent, ApiClient.getCurrentUserId(), parentId, ['favoriteArtists', 'favoriteAlbums', 'favoriteSongs']);
}); });
} }
@ -290,7 +290,7 @@ import Dashboard from '../../utils/dashboard';
break; break;
} }
import(`../music/${depends}`).then(({default: controllerFactory}) => { import(`../music/${depends}`).then(({ default: controllerFactory }) => {
let tabContent; let tabContent;
if (index == 1) { if (index == 1) {

View file

@ -124,7 +124,7 @@ export default function (view, params, tabContent) {
loading.hide(); loading.hide();
isLoading = false; isLoading = false;
import('../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page); autoFocuser.autoFocus(page);
}); });
}); });
@ -135,7 +135,7 @@ export default function (view, params, tabContent) {
let isLoading = false; let isLoading = false;
self.showFilterMenu = function () { self.showFilterMenu = function () {
import('../../components/filterdialog/filterdialog').then(({default: filterDialogFactory}) => { import('../../components/filterdialog/filterdialog').then(({ default: filterDialogFactory }) => {
const filterDialog = new filterDialogFactory({ const filterDialog = new filterDialogFactory({
query: getQuery(tabContent), query: getQuery(tabContent),
mode: 'songs', mode: 'songs',

View file

@ -1,6 +1,5 @@
import escapeHtml from 'escape-html'; import escapeHtml from 'escape-html';
import { playbackManager } from '../../../components/playback/playbackmanager'; import { playbackManager } from '../../../components/playback/playbackmanager';
import SyncPlay from '../../../plugins/syncPlay/core';
import browser from '../../../scripts/browser'; import browser from '../../../scripts/browser';
import dom from '../../../scripts/dom'; import dom from '../../../scripts/dom';
import inputManager from '../../../scripts/inputManager'; import inputManager from '../../../scripts/inputManager';
@ -25,6 +24,8 @@ import SubtitleSync from '../../../components/subtitlesync/subtitlesync';
import { appRouter } from '../../../components/appRouter'; import { appRouter } from '../../../components/appRouter';
import LibraryMenu from '../../../scripts/libraryMenu'; import LibraryMenu from '../../../scripts/libraryMenu';
import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components/backdrop/backdrop'; import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components/backdrop/backdrop';
import { pluginManager } from '../../../components/pluginManager';
import { PluginType } from '../../../types/plugin.ts';
/* eslint-disable indent */ /* eslint-disable indent */
const TICKS_PER_MINUTE = 600000000; const TICKS_PER_MINUTE = 600000000;
@ -64,7 +65,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
ServerConnections.getApiClient(item.ServerId).getCurrentUser().then(function (user) { ServerConnections.getApiClient(item.ServerId).getCurrentUser().then(function (user) {
if (user.Policy.EnableLiveTvManagement) { if (user.Policy.EnableLiveTvManagement) {
import('../../../components/recordingcreator/recordingbutton').then(({default: RecordingButton}) => { import('../../../components/recordingcreator/recordingbutton').then(({ default: RecordingButton }) => {
if (recordingButtonManager) { if (recordingButtonManager) {
recordingButtonManager.refreshItem(item); recordingButtonManager.refreshItem(item);
return; return;
@ -216,7 +217,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
let title = itemName; let title = itemName;
if (item.PremiereDate) { if (item.PremiereDate) {
try { try {
const year = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), {useGrouping: false}); const year = datetime.toLocaleString(datetime.parseISO8601Date(item.PremiereDate).getFullYear(), { useGrouping: false });
title += ` (${year})`; title += ` (${year})`;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -622,7 +623,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
} }
function showComingUpNext(player) { function showComingUpNext(player) {
import('../../../components/upnextdialog/upnextdialog').then(({default: UpNextDialog}) => { import('../../../components/upnextdialog/upnextdialog').then(({ default: UpNextDialog }) => {
if (!(currentVisibleMenu || currentUpNextDialog)) { if (!(currentVisibleMenu || currentUpNextDialog)) {
currentVisibleMenu = 'upnext'; currentVisibleMenu = 'upnext';
comingUpNextDisplayed = true; comingUpNextDisplayed = true;
@ -896,8 +897,8 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
const state = playbackManager.getPlayerState(player); const state = playbackManager.getPlayerState(player);
// show subtitle offset feature only if player and media support it // show subtitle offset feature only if player and media support it
const showSubOffset = playbackManager.supportSubtitleOffset(player) && const showSubOffset = playbackManager.supportSubtitleOffset(player)
playbackManager.canHandleOffsetOnCurrentSubtitle(player); && playbackManager.canHandleOffsetOnCurrentSubtitle(player);
playerSettingsMenu.show({ playerSettingsMenu.show({
mediaType: 'Video', mediaType: 'Video',
@ -929,7 +930,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
} }
function toggleStats() { function toggleStats() {
import('../../../components/playerstats/playerstats').then(({default: PlayerStats}) => { import('../../../components/playerstats/playerstats').then(({ default: PlayerStats }) => {
const player = currentPlayer; const player = currentPlayer;
if (player) { if (player) {
@ -969,7 +970,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
}); });
const positionTo = this; const positionTo = this;
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => { import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({ actionsheet.show({
items: menuItems, items: menuItems,
title: globalize.translate('Audio'), title: globalize.translate('Audio'),
@ -1086,7 +1087,7 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
const positionTo = this; const positionTo = this;
import('../../../components/actionSheet/actionSheet').then(({default: actionsheet}) => { import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
actionsheet.show({ actionsheet.show({
title: globalize.translate('Subtitles'), title: globalize.translate('Subtitles'),
items: menuItems, items: menuItems,
@ -1774,20 +1775,20 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
}, iconVisibilityTime); }, iconVisibilityTime);
}; };
Events.on(SyncPlay.Manager, 'enabled', (event, enabled) => { const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
if (enabled) { if (SyncPlay) {
// SyncPlay enabled Events.on(SyncPlay.Manager, 'enabled', (_event, enabled) => {
} else { if (!enabled) {
const syncPlayIcon = view.querySelector('#syncPlayIcon'); const syncPlayIcon = view.querySelector('#syncPlayIcon');
syncPlayIcon.style.visibility = 'hidden'; syncPlayIcon.style.visibility = 'hidden';
} }
}); });
Events.on(SyncPlay.Manager, 'notify-osd', (event, action) => { Events.on(SyncPlay.Manager, 'notify-osd', (_event, action) => {
showIcon(action); showIcon(action);
}); });
Events.on(SyncPlay.Manager, 'group-state-update', (event, state, reason) => { Events.on(SyncPlay.Manager, 'group-state-update', (_event, state, reason) => {
if (state === 'Playing' && reason === 'Unpause') { if (state === 'Playing' && reason === 'Unpause') {
showIcon('schedule-play'); showIcon('schedule-play');
} else if (state === 'Playing' && reason === 'Ready') { } else if (state === 'Playing' && reason === 'Ready') {
@ -1807,5 +1808,6 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
} }
}); });
} }
}
/* eslint-enable indent */ /* eslint-enable indent */

View file

@ -54,7 +54,7 @@ import { ConnectionState } from '../../../utils/jellyfin-apiclient/ConnectionSta
view.querySelector('.addServerForm').addEventListener('submit', onServerSubmit); view.querySelector('.addServerForm').addEventListener('submit', onServerSubmit);
view.querySelector('.btnCancel').addEventListener('click', goBack); view.querySelector('.btnCancel').addEventListener('click', goBack);
import('../../../components/autoFocuser').then(({default: autoFocuser}) => { import('../../../components/autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(view); autoFocuser.autoFocus(view);
}); });
@ -65,7 +65,7 @@ import { ConnectionState } from '../../../utils/jellyfin-apiclient/ConnectionSta
} }
function goBack() { function goBack() {
import('../../../components/appRouter').then(({appRouter}) => { import('../../../components/appRouter').then(({ appRouter }) => {
appRouter.back(); appRouter.back();
}); });
} }

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