diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 186dbcd12..9f8563f6a 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1,6 +1,2 @@
-.ci @dkanada @EraYaN
+* @jellyfin/web
.github @jellyfin/core
-fedora @joshuaboniface
-debian @joshuaboniface
-.copr @joshuaboniface
-deployment @joshuaboniface
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 2f10d09e0..84e5e1e75 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2.1.1
+ uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
@@ -26,3 +26,11 @@ jobs:
uses: cirrus-actions/rebase@b87d48154a87a85666003575337e27b8cd65f691 # 1.8
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
+ - name: Comment on failure
+ if: failure()
+ uses: peter-evans/create-or-update-comment@3383acd359705b10cb1eeef05c0e88c056ea4666 # v3.0.0
+ with:
+ token: ${{ secrets.JF_BOT_TOKEN }}
+ issue-number: ${{ github.event.issue.number }}
+ body: |
+ I'm sorry @${{ github.event.comment.user.login }}, I'm afraid I can't do that.
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index f3dbfec6d..06659768a 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -60,6 +60,7 @@
- [edvwib](https://github.com/edvwib)
- [Rob Farraher](https://github.com/farraherbg)
- [Pier-Luc Ducharme](https://github.com/pl-ducharme)
+ - [Anantharaju S](https://github.com/Anantharajus)
# Emby Contributors
diff --git a/deployment/Dockerfile.centos b/deployment/Dockerfile.centos
index cb8bd3593..3d64fbf64 100644
--- a/deployment/Dockerfile.centos
+++ b/deployment/Dockerfile.centos
@@ -1,4 +1,4 @@
-FROM centos:8
+FROM quay.io/centos/centos:stream8
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
diff --git a/package-lock.json b/package-lock.json
index c2c37b5e2..17a1a027d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -99,7 +99,7 @@
"postcss-loader": "7.1.0",
"postcss-preset-env": "8.0.1",
"postcss-scss": "4.0.6",
- "sass": "1.59.3",
+ "sass": "1.60.0",
"sass-loader": "13.2.1",
"source-map-loader": "4.0.1",
"style-loader": "3.3.2",
@@ -2640,12 +2640,14 @@
"dev": true
},
"node_modules/@jellyfin/sdk": {
- "version": "0.0.0-unstable.202303130502",
- "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202303130502.tgz",
- "integrity": "sha512-j3ntDjTnZlU511J0CpuPVSSSYrx9so4Y3q6qYOVsB6/evH4/2BNkWYRbKgCnUtCULIV90T6KGc2EcS4GGxojCg==",
+ "version": "0.0.0-unstable.202304122102",
+ "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202304122102.tgz",
+ "integrity": "sha512-KToOmK3GmbjovtFPgb3dYx8cV6bopo46fhTkHDnKLqsmwqBz5/QKk7Z8NbR+5YaojNAP4LUYnenZmMK9HQ2YeA==",
"dependencies": {
- "axios": "1.3.4",
"compare-versions": "5.0.3"
+ },
+ "peerDependencies": {
+ "axios": "^1.3.4"
}
},
"node_modules/@jridgewell/gen-mapping": {
@@ -4073,7 +4075,8 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "peer": true
},
"node_modules/at-least-node": {
"version": "1.0.0",
@@ -4154,6 +4157,7 @@
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
+ "peer": true,
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -4956,6 +4960,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -5868,6 +5873,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "peer": true,
"engines": {
"node": ">=0.4.0"
}
@@ -7947,6 +7953,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -13062,7 +13069,8 @@
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "peer": true
},
"node_modules/punycode": {
"version": "2.1.1",
@@ -13770,9 +13778,9 @@
"dev": true
},
"node_modules/sass": {
- "version": "1.59.3",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.59.3.tgz",
- "integrity": "sha512-QCq98N3hX1jfTCoUAsF3eyGuXLsY7BCnCEg9qAact94Yc21npG2/mVOqoDvE0fCbWDqiM4WlcJQla0gWG2YlxQ==",
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.60.0.tgz",
+ "integrity": "sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@@ -21001,11 +21009,10 @@
"dev": true
},
"@jellyfin/sdk": {
- "version": "0.0.0-unstable.202303130502",
- "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202303130502.tgz",
- "integrity": "sha512-j3ntDjTnZlU511J0CpuPVSSSYrx9so4Y3q6qYOVsB6/evH4/2BNkWYRbKgCnUtCULIV90T6KGc2EcS4GGxojCg==",
+ "version": "0.0.0-unstable.202304122102",
+ "resolved": "https://registry.npmjs.org/@jellyfin/sdk/-/sdk-0.0.0-unstable.202304122102.tgz",
+ "integrity": "sha512-KToOmK3GmbjovtFPgb3dYx8cV6bopo46fhTkHDnKLqsmwqBz5/QKk7Z8NbR+5YaojNAP4LUYnenZmMK9HQ2YeA==",
"requires": {
- "axios": "1.3.4",
"compare-versions": "5.0.3"
}
},
@@ -22143,7 +22150,8 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "peer": true
},
"at-least-node": {
"version": "1.0.0",
@@ -22187,6 +22195,7 @@
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.4.tgz",
"integrity": "sha512-toYm+Bsyl6VC5wSkfkbbNB6ROv7KY93PEBBL6xyDczaIHasAiv4wPqQ/c4RjoQzipxRD2W5g21cOqQulZ7rHwQ==",
+ "peer": true,
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@@ -22812,6 +22821,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "peer": true,
"requires": {
"delayed-stream": "~1.0.0"
}
@@ -23468,7 +23478,8 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "peer": true
},
"depd": {
"version": "1.1.2",
@@ -25077,6 +25088,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "peer": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -28753,7 +28765,8 @@
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "peer": true
},
"punycode": {
"version": "2.1.1",
@@ -29286,9 +29299,9 @@
"dev": true
},
"sass": {
- "version": "1.59.3",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.59.3.tgz",
- "integrity": "sha512-QCq98N3hX1jfTCoUAsF3eyGuXLsY7BCnCEg9qAact94Yc21npG2/mVOqoDvE0fCbWDqiM4WlcJQla0gWG2YlxQ==",
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.60.0.tgz",
+ "integrity": "sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ==",
"dev": true,
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
diff --git a/package.json b/package.json
index 113fa3c07..26e6e9ae4 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
"postcss-loader": "7.1.0",
"postcss-preset-env": "8.0.1",
"postcss-scss": "4.0.6",
- "sass": "1.59.3",
+ "sass": "1.60.0",
"sass-loader": "13.2.1",
"source-map-loader": "4.0.1",
"style-loader": "3.3.2",
diff --git a/src/App.tsx b/src/App.tsx
index 79828b6bd..1a544723a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,15 +1,38 @@
import { History } from '@remix-run/router';
-import React from 'react';
+import React, { useEffect } from 'react';
import { HistoryRouter } from './components/HistoryRouter';
import { ApiProvider } from './hooks/useApi';
-import AppRoutes from './routes/index';
+import { AppRoutes, ExperimentalAppRoutes } from './routes';
const App = ({ history }: { history: History }) => {
+ const layoutMode = localStorage.getItem('layout');
+
+ useEffect(() => {
+ Promise.all([
+ // Initialize the UI components after first render
+ import('./scripts/libraryMenu'),
+ import('./scripts/autoBackdrops')
+ ]);
+ }, []);
+
return (
-
+
+
+
+
+
+
+
+
+ {layoutMode === 'experimental' ?
:
}
+
+
+
);
diff --git a/src/components/dialogHelper/dialogHelper.js b/src/components/dialogHelper/dialogHelper.js
index 681152546..32fa10504 100644
--- a/src/components/dialogHelper/dialogHelper.js
+++ b/src/components/dialogHelper/dialogHelper.js
@@ -250,7 +250,6 @@ import '../../styles/scrollstyles.scss';
}
function isOpened(dlg) {
- //return dlg.opened;
return !dlg.classList.contains('hide');
}
diff --git a/src/components/filtermenu/filtermenu.js b/src/components/filtermenu/filtermenu.js
index 4c1b6d625..d7e9c8765 100644
--- a/src/components/filtermenu/filtermenu.js
+++ b/src/components/filtermenu/filtermenu.js
@@ -297,10 +297,8 @@ class FilterMenu {
}
if (submitted) {
- //if (!options.onChange) {
saveValues(dlg, options.settings, options.settingsKey, options.setfilters);
return resolve();
- //}
}
return resolve();
});
diff --git a/src/components/htmlMediaHelper.js b/src/components/htmlMediaHelper.js
index 22816aee3..92fb4d013 100644
--- a/src/components/htmlMediaHelper.js
+++ b/src/components/htmlMediaHelper.js
@@ -51,18 +51,10 @@ import Events from '../utils/events.ts';
return true;
}
- if (browser.edge && mediaType === 'Video') {
- //return true;
- }
-
// simple playback should use the native support
if (runTimeTicks) {
- //if (!browser.edge) {
return false;
- //}
}
-
- //return false;
}
return true;
diff --git a/src/components/multiSelect/multiSelect.js b/src/components/multiSelect/multiSelect.js
index 398496817..c361c182f 100644
--- a/src/components/multiSelect/multiSelect.js
+++ b/src/components/multiSelect/multiSelect.js
@@ -205,13 +205,6 @@ import datetime from '../../scripts/datetime';
if (user.Policy.EnableContentDownloading && appHost.supports('filedownload')) {
// Disabled because there is no callback for this item
- /*
- menuItems.push({
- name: globalize.translate('Download'),
- id: 'download',
- icon: 'file_download'
- });
- */
}
if (user.Policy.IsAdministrator) {
diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js
index 31b04e0fb..00030e321 100644
--- a/src/components/playback/playbackmanager.js
+++ b/src/components/playback/playbackmanager.js
@@ -1038,7 +1038,6 @@ class PlaybackManager {
}
}
- //var mediaType = item.MediaType;
return getPlayer(item, getDefaultPlayOptions()) != null;
};
diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js
index 26b7220bc..9952286ae 100644
--- a/src/components/remotecontrol/remotecontrol.js
+++ b/src/components/remotecontrol/remotecontrol.js
@@ -325,10 +325,11 @@ export default function () {
if (layoutManager.mobile) {
const playingVideo = playbackManager.isPlayingVideo() && item !== null;
const playingAudio = !playbackManager.isPlayingVideo() && item !== null;
- buttonVisible(context.querySelector('.btnRepeat'), playingAudio);
- buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio);
- buttonVisible(context.querySelector('.btnRewind'), playingVideo);
- buttonVisible(context.querySelector('.btnFastForward'), playingVideo);
+ const playingAudioBook = playingAudio && item.Type == 'AudioBook';
+ buttonVisible(context.querySelector('.btnRepeat'), playingAudio && !playingAudioBook);
+ buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio && !playingAudioBook);
+ buttonVisible(context.querySelector('.btnRewind'), playingVideo || playingAudioBook);
+ buttonVisible(context.querySelector('.btnFastForward'), playingVideo || playingAudioBook);
buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnShuffleQueue'), playingVideo);
buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnRepeat'), playingVideo);
} else {
diff --git a/src/components/tabbedview/tabbedview.js b/src/components/tabbedview/tabbedview.js
new file mode 100644
index 000000000..8c865ec9f
--- /dev/null
+++ b/src/components/tabbedview/tabbedview.js
@@ -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;
diff --git a/src/components/upnextdialog/upnextdialog.js b/src/components/upnextdialog/upnextdialog.js
index 2a37fcbe4..bfb3ca18c 100644
--- a/src/components/upnextdialog/upnextdialog.js
+++ b/src/components/upnextdialog/upnextdialog.js
@@ -69,9 +69,9 @@ import '../../styles/flexstyles.scss';
const elem = instance.options.parent;
elem.querySelector('.upNextDialog-mediainfo').innerHTML = mediaInfo.getPrimaryMediaInfoHtml(item, {
- criticRating: false,
+ criticRating: true,
originalAirDate: false,
- starRating: false,
+ starRating: true,
subtitles: false
});
diff --git a/src/components/viewContainer.js b/src/components/viewContainer.js
index 82e7ef40c..c65b5f8a0 100644
--- a/src/components/viewContainer.js
+++ b/src/components/viewContainer.js
@@ -2,6 +2,14 @@ import { importModule } from '@uupaa/dynamic-import-polyfill';
import './viewManager/viewContainer.scss';
import Dashboard from '../utils/dashboard';
+const getMainAnimatedPages = () => {
+ if (!mainAnimatedPages) {
+ mainAnimatedPages = document.querySelector('.mainAnimatedPages');
+ }
+
+ return mainAnimatedPages;
+};
+
/* eslint-disable indent */
function setControllerClass(view, options) {
@@ -55,6 +63,11 @@ import Dashboard from '../utils/dashboard';
view.classList.add('mainAnimatedPage');
+ if (!getMainAnimatedPages()) {
+ console.warn('[viewContainer] main animated pages element is not present');
+ return;
+ }
+
if (currentPage) {
if (newViewInfo.hasScript && window.$) {
mainAnimatedPages.removeChild(currentPage);
@@ -225,18 +238,18 @@ import Dashboard from '../utils/dashboard';
export function reset() {
allPages = [];
currentUrls = [];
- mainAnimatedPages.innerHTML = '';
+ if (mainAnimatedPages) mainAnimatedPages.innerHTML = '';
selectedPageIndex = -1;
}
let onBeforeChange;
- const mainAnimatedPages = document.querySelector('.mainAnimatedPages');
+ let mainAnimatedPages;
let allPages = [];
let currentUrls = [];
const pageContainerCount = 3;
let selectedPageIndex = -1;
reset();
- mainAnimatedPages.classList.remove('hide');
+ getMainAnimatedPages()?.classList.remove('hide');
/* eslint-enable indent */
diff --git a/src/controllers/dashboard/encodingsettings.html b/src/controllers/dashboard/encodingsettings.html
index f9790189e..759c19d7d 100644
--- a/src/controllers/dashboard/encodingsettings.html
+++ b/src/controllers/dashboard/encodingsettings.html
@@ -111,7 +111,7 @@
${EnableIntelLowPowerHevcHwEncoder}
diff --git a/src/controllers/dashboard/users/useredit.html b/src/controllers/dashboard/users/useredit.html
new file mode 100644
index 000000000..cf7d727ff
--- /dev/null
+++ b/src/controllers/dashboard/users/useredit.html
@@ -0,0 +1,198 @@
+
diff --git a/src/controllers/dashboard/users/useredit.js b/src/controllers/dashboard/users/useredit.js
new file mode 100644
index 000000000..98aa0dd40
--- /dev/null
+++ b/src/controllers/dashboard/users/useredit.js
@@ -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 += '';
+ }
+
+ for (const folder of channelsResult.Items) {
+ isChecked = user.Policy.EnableContentDeletion || user.Policy.EnableContentDeletionFromFolders.indexOf(folder.Id) != -1;
+ checkedAttribute = isChecked ? ' checked="checked"' : '';
+ html += '';
+ }
+
+ $('.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 '';
+ });
+}
+
+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 '';
+ });
+}
+
+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);
+});
+
diff --git a/src/controllers/dashboard/users/userlibraryaccess.html b/src/controllers/dashboard/users/userlibraryaccess.html
new file mode 100644
index 000000000..bf6ba9340
--- /dev/null
+++ b/src/controllers/dashboard/users/userlibraryaccess.html
@@ -0,0 +1,68 @@
+
diff --git a/src/controllers/dashboard/users/userlibraryaccess.js b/src/controllers/dashboard/users/userlibraryaccess.js
new file mode 100644
index 000000000..e84638e8e
--- /dev/null
+++ b/src/controllers/dashboard/users/userlibraryaccess.js
@@ -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 += '' + globalize.translate('HeaderLibraries') + '
';
+ html += '';
+
+ 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 += '';
+ }
+
+ html += '
';
+ 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 += '' + globalize.translate('Channels') + '
';
+ html += '';
+
+ 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 += '';
+ }
+
+ html += '
';
+ $('.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 += '' + globalize.translate('HeaderDevices') + '
';
+ html += '';
+
+ 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 += '';
+ }
+
+ html += '
';
+ $('.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);
+ });
+});
+
diff --git a/src/controllers/dashboard/users/usernew.html b/src/controllers/dashboard/users/usernew.html
new file mode 100644
index 000000000..5d50ede80
--- /dev/null
+++ b/src/controllers/dashboard/users/usernew.html
@@ -0,0 +1,62 @@
+
diff --git a/src/controllers/dashboard/users/usernew.js b/src/controllers/dashboard/users/usernew.js
new file mode 100644
index 000000000..9477506ac
--- /dev/null
+++ b/src/controllers/dashboard/users/usernew.js
@@ -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 += '' + globalize.translate('HeaderLibraries') + '
';
+ html += '';
+
+ for (let i = 0; i < mediaFolders.length; i++) {
+ const folder = mediaFolders[i];
+ html += '';
+ }
+
+ html += '
';
+ $('.folderAccess', page).html(html).trigger('create');
+ $('#chkEnableAllFolders', page).prop('checked', false);
+}
+
+function loadChannels(page, channels) {
+ let html = '';
+ html += '' + globalize.translate('Channels') + '
';
+ html += '';
+
+ for (let i = 0; i < channels.length; i++) {
+ const folder = channels[i];
+ html += '';
+ }
+
+ html += '
';
+ $('.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);
+});
+
diff --git a/src/controllers/dashboard/users/userparentalcontrol.html b/src/controllers/dashboard/users/userparentalcontrol.html
new file mode 100644
index 000000000..5b58047c6
--- /dev/null
+++ b/src/controllers/dashboard/users/userparentalcontrol.html
@@ -0,0 +1,60 @@
+
diff --git a/src/controllers/dashboard/users/userparentalcontrol.js b/src/controllers/dashboard/users/userparentalcontrol.js
new file mode 100644
index 000000000..0b527e09e
--- /dev/null
+++ b/src/controllers/dashboard/users/userparentalcontrol.js
@@ -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 += "";
+ 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 += "';
+ }
+
+ $('#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 += '' + globalize.translate('HeaderBlockItemsWithNoRating') + '
';
+ html += '';
+
+ 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 += '';
+ }
+
+ html += '
';
+ $('.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 = '';
+ li += '
';
+ li += '
';
+ li += h;
+ li += '
';
+ li += '';
+ li += '
';
+ li += '
';
+ return li;
+ }).join('');
+
+ if (html) {
+ html = '' + html + '
';
+ }
+
+ 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 += '';
+ itemHtml += '
';
+ itemHtml += '
';
+ itemHtml += globalize.translate('Option' + a.DayOfWeek);
+ itemHtml += '
';
+ itemHtml += '
' + getDisplayTime(a.StartHour) + ' - ' + getDisplayTime(a.EndHour) + '
';
+ itemHtml += '
';
+ itemHtml += '
';
+ itemHtml += '
';
+ 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]);
+ });
+});
+
diff --git a/src/controllers/dashboard/users/userpassword.html b/src/controllers/dashboard/users/userpassword.html
new file mode 100644
index 000000000..897f0e7bd
--- /dev/null
+++ b/src/controllers/dashboard/users/userpassword.html
@@ -0,0 +1,72 @@
+
diff --git a/src/controllers/dashboard/users/userpasswordpage.js b/src/controllers/dashboard/users/userpasswordpage.js
new file mode 100644
index 000000000..4171c55d6
--- /dev/null
+++ b/src/controllers/dashboard/users/userpasswordpage.js
@@ -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);
+ });
+}
+
diff --git a/src/controllers/dashboard/users/userprofiles.html b/src/controllers/dashboard/users/userprofiles.html
new file mode 100644
index 000000000..9e2908266
--- /dev/null
+++ b/src/controllers/dashboard/users/userprofiles.html
@@ -0,0 +1,16 @@
+
diff --git a/src/controllers/dashboard/users/userprofilespage.js b/src/controllers/dashboard/users/userprofilespage.js
new file mode 100644
index 000000000..59d61a443
--- /dev/null
+++ b/src/controllers/dashboard/users/userprofilespage.js
@@ -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 += "";
+ html += '
';
+ html += '
';
+ return html + '
';
+}
+// 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);
+});
+
diff --git a/src/controllers/home.html b/src/controllers/home.html
new file mode 100644
index 000000000..240caef6c
--- /dev/null
+++ b/src/controllers/home.html
@@ -0,0 +1,9 @@
+
diff --git a/src/controllers/home.js b/src/controllers/home.js
new file mode 100644
index 000000000..657d406f6
--- /dev/null
+++ b/src/controllers/home.js
@@ -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;
diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js
index 5c5948329..943b5f74a 100644
--- a/src/controllers/itemDetails/index.js
+++ b/src/controllers/itemDetails/index.js
@@ -113,6 +113,11 @@ function getProgramScheduleHtml(items, action = 'none') {
});
}
+function getSelectedMediaSource(page, mediaSources) {
+ const mediaSourceId = page.querySelector('.selectSource').value;
+ return mediaSources.filter(m => m.Id === mediaSourceId)[0];
+}
+
function renderSeriesTimerSchedule(page, apiClient, seriesTimerId) {
apiClient.getLiveTvTimers({
UserId: apiClient.getCurrentUserId(),
@@ -206,10 +211,7 @@ function renderTrackSelections(page, instance, item, forceReload) {
}
function renderVideoSelections(page, mediaSources) {
- const mediaSourceId = page.querySelector('.selectSource').value;
- const mediaSource = mediaSources.filter(function (m) {
- return m.Id === mediaSourceId;
- })[0];
+ const mediaSource = getSelectedMediaSource(page, mediaSources);
const tracks = mediaSource.MediaStreams.filter(function (m) {
return m.Type === 'Video';
@@ -243,10 +245,8 @@ function renderVideoSelections(page, mediaSources) {
}
function renderAudioSelections(page, mediaSources) {
- const mediaSourceId = page.querySelector('.selectSource').value;
- const mediaSource = mediaSources.filter(function (m) {
- return m.Id === mediaSourceId;
- })[0];
+ const mediaSource = getSelectedMediaSource(page, mediaSources);
+
const tracks = mediaSource.MediaStreams.filter(function (m) {
return m.Type === 'Audio';
});
@@ -273,10 +273,8 @@ function renderAudioSelections(page, mediaSources) {
}
function renderSubtitleSelections(page, mediaSources) {
- const mediaSourceId = page.querySelector('.selectSource').value;
- const mediaSource = mediaSources.filter(function (m) {
- return m.Id === mediaSourceId;
- })[0];
+ const mediaSource = getSelectedMediaSource(page, mediaSources);
+
const tracks = mediaSource.MediaStreams.filter(function (m) {
return m.Type === 'Subtitle';
});
@@ -2029,6 +2027,7 @@ export default function (view, params) {
renderVideoSelections(view, self._currentPlaybackMediaSources);
renderAudioSelections(view, self._currentPlaybackMediaSources);
renderSubtitleSelections(view, self._currentPlaybackMediaSources);
+ updateMiscInfo();
});
view.addEventListener('viewshow', function (e) {
const page = this;
@@ -2063,5 +2062,14 @@ export default function (view, params) {
});
}
+ function updateMiscInfo() {
+ const selectedMediaSource = getSelectedMediaSource(view, self._currentPlaybackMediaSources);
+ renderMiscInfo(view, {
+ // patch currentItem (primary item) with details from the selected MediaSource:
+ ...currentItem,
+ ...selectedMediaSource
+ });
+ }
+
init();
}
diff --git a/src/controllers/movies/moviecollections.js b/src/controllers/movies/moviecollections.js
new file mode 100644
index 000000000..6e6af9dc9
--- /dev/null
+++ b/src/controllers/movies/moviecollections.js
@@ -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 += '
';
+ html += '
' + globalize.translate('MessageNothingHere') + '
';
+ html += '
' + globalize.translate('MessageNoCollectionsAvailable') + '
';
+ html += '
';
+ }
+
+ 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);
+ };
+}
+
diff --git a/src/controllers/movies/moviegenres.js b/src/controllers/movies/moviegenres.js
new file mode 100644
index 000000000..ee0b04837
--- /dev/null
+++ b/src/controllers/movies/moviegenres.js
@@ -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 += '
';
+ html += '
';
+ if (enableScrollX()) {
+ let scrollXClass = 'scrollX hiddenScrollX';
+
+ if (layoutManager.tv) {
+ scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale';
+ }
+
+ html += '
';
+ }
+
+ if (!result.Items.length) {
+ html = '';
+
+ html += '
';
+ html += '
' + globalize.translate('MessageNothingHere') + '
';
+ html += '
' + globalize.translate('MessageNoGenresAvailable') + '
';
+ html += '
';
+ }
+
+ 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);
+ };
+}
+
diff --git a/src/controllers/movies/movies.html b/src/controllers/movies/movies.html
new file mode 100644
index 000000000..7a08694b2
--- /dev/null
+++ b/src/controllers/movies/movies.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
${HeaderContinueWatching}
+
+
+
+
+
+
+
+
+
${HeaderLatestMovies}
+
+
+
+
+
+
+
+
+
+
+
${MessageNoMovieSuggestionsAvailable}
+
+
+
+
+
+
+
diff --git a/src/controllers/movies/movies.js b/src/controllers/movies/movies.js
new file mode 100644
index 000000000..a8e2aca1f
--- /dev/null
+++ b/src/controllers/movies/movies.js
@@ -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;
+ };
+}
+
diff --git a/src/controllers/movies/moviesrecommended.js b/src/controllers/movies/moviesrecommended.js
new file mode 100644
index 000000000..a7fab0008
--- /dev/null
+++ b/src/controllers/movies/moviesrecommended.js
@@ -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 += '
';
+ html += '
' + escapeHtml(title) + '
';
+ const allowBottomPadding = true;
+
+ if (enableScrollX()) {
+ html += '
';
+ html += '
';
+ html += '
';
+ 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();
+ }
+ }
+}
+
diff --git a/src/controllers/movies/movietrailers.js b/src/controllers/movies/movietrailers.js
new file mode 100644
index 000000000..a96d7e51a
--- /dev/null
+++ b/src/controllers/movies/movietrailers.js
@@ -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 += '
';
+ html += '
' + globalize.translate('MessageNothingHere') + '
';
+ html += '
' + globalize.translate('MessageNoTrailersFound') + '
';
+ html += '
';
+ }
+
+ 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());
+ };
+}
+
diff --git a/src/controllers/playback/video/index.html b/src/controllers/playback/video/index.html
index 002a81831..ca49f872c 100644
--- a/src/controllers/playback/video/index.html
+++ b/src/controllers/playback/video/index.html
@@ -65,6 +65,9 @@
+
+
+