';
+}
+// 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 0000000000..240caef6c6
--- /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 0000000000..657d406f67
--- /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/movies/moviecollections.js b/src/controllers/movies/moviecollections.js
new file mode 100644
index 0000000000..9452038650
--- /dev/null
+++ b/src/controllers/movies/moviecollections.js
@@ -0,0 +1,264 @@
+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(context) {
+ const key = getSavedQueryKey(context);
+ 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(context) {
+ return getPageData(context).query;
+ }
+
+ function getSavedQueryKey(context) {
+ if (!context.savedQueryKey) {
+ context.savedQueryKey = libraryBrowser.getSavedQueryKey('moviecollections');
+ }
+
+ return context.savedQueryKey;
+ }
+
+ 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(page);
+ 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 += '
';
+ }
+
+ const itemsContainer = tabContent.querySelector('.itemsContainer');
+ itemsContainer.innerHTML = html;
+ imageLoader.lazyChildren(itemsContainer);
+ libraryBrowser.saveQueryValues(getSavedQueryKey(page), query);
+ loading.hide();
+ isLoading = false;
+
+ import('../../components/autoFocuser').then(({ default: autoFocuser }) => {
+ autoFocuser.autoFocus(page);
+ });
+ });
+ };
+
+ const data = {};
+ let isLoading = false;
+
+ this.getCurrentViewStyle = function () {
+ return getPageData(tabContent).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(tabElement).StartIndex = 0;
+ reloadItems(tabElement);
+ },
+ query: getQuery(tabElement),
+ 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(tabElement).view = viewStyle;
+ libraryBrowser.saveViewSetting(getSavedQueryKey(tabElement), viewStyle);
+ getQuery(tabElement).StartIndex = 0;
+ onViewStyleChange();
+ reloadItems(tabElement);
+ });
+ tabElement.querySelector('.btnNewCollection').addEventListener('click', () => {
+ import('../../components/collectionEditor/collectionEditor').then(({ default: collectionEditor }) => {
+ const serverId = ApiClient.serverInfo().Id;
+ new collectionEditor({
+ 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 0000000000..5e0478042e
--- /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 libraryBrowser.getSavedQueryKey('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 0000000000..7a08694b2a
--- /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 0000000000..a8e2aca1fa
--- /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 0000000000..a7fab00085
--- /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 0000000000..9d5e25ff02
--- /dev/null
+++ b/src/controllers/movies/movietrailers.js
@@ -0,0 +1,276 @@
+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(context) {
+ const key = getSavedQueryKey(context);
+ 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(context) {
+ return getPageData(context).query;
+ }
+
+ function getSavedQueryKey(context) {
+ if (!context.savedQueryKey) {
+ context.savedQueryKey = libraryBrowser.getSavedQueryKey('trailers');
+ }
+
+ return context.savedQueryKey;
+ }
+
+ const reloadItems = () => {
+ loading.show();
+ isLoading = true;
+ const query = getQuery(tabContent);
+ 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(tabContent), 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(tabContent),
+ mode: 'movies',
+ serverId: ApiClient.serverId()
+ });
+ Events.on(filterDialog, 'filterchange', function () {
+ getQuery(tabContent).StartIndex = 0;
+ reloadItems();
+ });
+ filterDialog.show();
+ });
+ };
+
+ this.getCurrentViewStyle = function () {
+ return getPageData(tabContent).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(tabElement);
+ 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(tabElement).StartIndex = 0;
+ reloadItems();
+ },
+ query: getQuery(tabElement),
+ button: e.target
+ });
+ });
+ };
+
+ initPage(tabContent);
+
+ this.renderTab = () => {
+ reloadItems();
+ this.alphaPicker?.updateControls(getQuery(tabContent));
+ };
+}
+
diff --git a/src/controllers/user/profile/index.html b/src/controllers/user/profile/index.html
new file mode 100644
index 0000000000..3eaa2f7299
--- /dev/null
+++ b/src/controllers/user/profile/index.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/controllers/user/profile/index.js b/src/controllers/user/profile/index.js
new file mode 100644
index 0000000000..07bab611c3
--- /dev/null
+++ b/src/controllers/user/profile/index.js
@@ -0,0 +1,105 @@
+import UserPasswordPage from '../../dashboard/users/userpasswordpage';
+import loading from '../../../components/loading/loading';
+import libraryMenu from '../../../scripts/libraryMenu';
+import { appHost } from '../../../components/apphost';
+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';
+import { getParameterByName } from '../../../utils/url.ts';
+
+function reloadUser(page) {
+ const userId = getParameterByName('userId');
+ loading.show();
+ ApiClient.getUser(userId).then(function (user) {
+ page.querySelector('.username').innerText = user.Name;
+ libraryMenu.setTitle(user.Name);
+
+ let imageUrl = 'assets/img/avatar.png';
+ if (user.PrimaryImageTag) {
+ imageUrl = ApiClient.getUserImageUrl(user.Id, {
+ tag: user.PrimaryImageTag,
+ type: 'Primary'
+ });
+ }
+
+ const userImage = page.querySelector('#image');
+ userImage.style.backgroundImage = 'url(' + imageUrl + ')';
+
+ Dashboard.getCurrentUser().then(function (loggedInUser) {
+ if (user.PrimaryImageTag) {
+ page.querySelector('#btnAddImage').classList.add('hide');
+ page.querySelector('#btnDeleteImage').classList.remove('hide');
+ } else if (appHost.supports('fileinput') && (loggedInUser.Policy.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
+ page.querySelector('#btnDeleteImage').classList.add('hide');
+ page.querySelector('#btnAddImage').classList.remove('hide');
+ }
+ });
+ loading.hide();
+ });
+}
+
+function onFileReaderError(evt) {
+ loading.hide();
+ switch (evt.target.error.code) {
+ case evt.target.error.NOT_FOUND_ERR:
+ toast(globalize.translate('FileNotFound'));
+ break;
+ case evt.target.error.ABORT_ERR:
+ onFileReaderAbort();
+ break;
+ case evt.target.error.NOT_READABLE_ERR:
+ default:
+ toast(globalize.translate('FileReadError'));
+ }
+}
+
+function onFileReaderAbort() {
+ loading.hide();
+ toast(globalize.translate('FileReadCancelled'));
+}
+
+function setFiles(page, files) {
+ const userImage = page.querySelector('#image');
+ const file = files[0];
+
+ if (!file || !file.type.match('image.*')) {
+ return false;
+ }
+
+ const reader = new FileReader();
+ reader.onerror = onFileReaderError;
+ reader.onabort = onFileReaderAbort;
+ reader.onload = function (evt) {
+ userImage.style.backgroundImage = 'url(' + evt.target.result + ')';
+ const userId = getParameterByName('userId');
+ ApiClient.uploadUserImage(userId, 'Primary', file).then(function () {
+ loading.hide();
+ reloadUser(page);
+ });
+ };
+
+ reader.readAsDataURL(file);
+}
+
+export default function (view, params) {
+ reloadUser(view);
+ new UserPasswordPage(view, params);
+ view.querySelector('#btnDeleteImage').addEventListener('click', function () {
+ confirm(globalize.translate('DeleteImageConfirmation'), globalize.translate('DeleteImage')).then(function () {
+ loading.show();
+ const userId = getParameterByName('userId');
+ ApiClient.deleteUserImage(userId, 'primary').then(function () {
+ loading.hide();
+ reloadUser(view);
+ });
+ });
+ });
+ view.querySelector('#btnAddImage').addEventListener('click', function () {
+ view.querySelector('#uploadImage').click();
+ });
+ view.querySelector('#uploadImage').addEventListener('change', function (evt) {
+ setFiles(view, evt.target.files);
+ });
+}
diff --git a/src/routes/asyncRoutes/user.ts b/src/routes/asyncRoutes/user.ts
index 1abf687034..ef3ad7c4ff 100644
--- a/src/routes/asyncRoutes/user.ts
+++ b/src/routes/asyncRoutes/user.ts
@@ -1,8 +1,5 @@
import { AsyncRoute } from '.';
export const ASYNC_USER_ROUTES: AsyncRoute[] = [
- { path: 'search.html', page: 'search' },
- { path: 'userprofile.html', page: 'user/userprofile' },
- { path: 'home.html', page: 'home' },
- { path: 'movies.html', page: 'movies' }
+ { path: 'search.html', page: 'search' }
];
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 06e3badaf3..a628efcf3d 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
-import { ASYNC_ADMIN_ROUTES, ASYNC_USER_ROUTES, toAsyncPageRoute } from './asyncRoutes';
+import { ASYNC_USER_ROUTES, toAsyncPageRoute } from './asyncRoutes';
import ConnectionRequired from '../components/ConnectionRequired';
import ServerContentPage from '../components/ServerContentPage';
import { LEGACY_ADMIN_ROUTES, LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES, toViewManagerPageRoute } from './legacyRoutes';
@@ -17,7 +17,6 @@ const AppRoutes = () => (
{/* Admin routes */}
}>
- {ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}