diff --git a/src/components/tabbedview/tabbedview.js b/src/components/tabbedview/tabbedview.js new file mode 100644 index 0000000000..8c865ec9f6 --- /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/controllers/dashboard/users/useredit.html b/src/controllers/dashboard/users/useredit.html new file mode 100644 index 0000000000..cf7d727ff7 --- /dev/null +++ b/src/controllers/dashboard/users/useredit.html @@ -0,0 +1,198 @@ +
+ +
+
+ +
+
+

+ ${Help} +
+
+ + +

+ ${ButtonEditOtherUserPreferences} +

+
+ + +
+ +
+ +
+ +
${AuthProviderHelp}
+
+ +
+ +
${PasswordResetProviderHelp}
+
+ +
+ +
${AllowRemoteAccessHelp}
+
+ + +
+

${HeaderFeatureAccess}

+
+ + +
+
+
+

${HeaderPlayback}

+
+ + + + + +
+
${OptionAllowMediaPlaybackTranscodingHelp}
+
+
+
+
+ +
${LabelRemoteClientBitrateLimitHelp}
+
${LabelUserRemoteClientBitrateLimitHelp}
+
+
+
+
+ +
${SyncPlayAccessHelp}
+
+
+
+

${HeaderAllowMediaDeletionFrom}

+
+ +
+
+
+
+
+

${HeaderRemoteControl}

+
+ + + +
+
${OptionAllowRemoteSharedDevicesHelp}
+
+

${Other}

+
+ +
${OptionAllowContentDownloadHelp}
+
+
+ +
${OptionDisableUserHelp}
+
+
+ +
${OptionHideUserFromLoginHelp}
+
+
+
+
+ +
${OptionLoginAttemptsBeforeLockout}
+
${OptionLoginAttemptsBeforeLockoutHelp}
+
+
+
+
+
+ +
${OptionMaxActiveSessions}
+
${OptionMaxActiveSessionsHelp}
+
+
+
+
+ + + +
+
+
+
+
diff --git a/src/controllers/dashboard/users/useredit.js b/src/controllers/dashboard/users/useredit.js new file mode 100644 index 0000000000..98aa0dd40e --- /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 0000000000..bf6ba93408 --- /dev/null +++ b/src/controllers/dashboard/users/userlibraryaccess.html @@ -0,0 +1,68 @@ +
+ +
+
+ +
+
+

+ ${Help} +
+
+ + +
+ +
+

${HeaderLibraryAccess}

+ +
+
+
+
${LibraryAccessHelp}
+
+
+ +
+
+

${HeaderDeviceAccess}

+ +
+
+
+
${DeviceAccessHelp}
+
+
+
+
+
+ +
+
+
+
+
diff --git a/src/controllers/dashboard/users/userlibraryaccess.js b/src/controllers/dashboard/users/userlibraryaccess.js new file mode 100644 index 0000000000..e84638e8e0 --- /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 0000000000..5d50ede80a --- /dev/null +++ b/src/controllers/dashboard/users/usernew.html @@ -0,0 +1,62 @@ +
+
+
+
+
+
+

${ButtonAddUser}

+ ${Help} +
+ +
+ +
+ +
+ +
+
+ +
+

${HeaderLibraryAccess}

+
+ +
${LibraryAccessHelp}
+
+
+
+
+
+
+ + + +
+ + + +
+
+
+
+
diff --git a/src/controllers/dashboard/users/usernew.js b/src/controllers/dashboard/users/usernew.js new file mode 100644 index 0000000000..dd6ff65496 --- /dev/null +++ b/src/controllers/dashboard/users/usernew.js @@ -0,0 +1,130 @@ +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() + }; + //user.Name = $('#txtUsername', page).val(); + //user.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 0000000000..5b58047c60 --- /dev/null +++ b/src/controllers/dashboard/users/userparentalcontrol.html @@ -0,0 +1,60 @@ +
+
+
+
+
+

+ ${Help} +
+
+ + + +
+
+ +
${MaxParentalRatingHelp}
+
+ +
+
+
+ +
+ +
+
+

${LabelBlockContentWithTags}

+ +
+
+
+ +
+
+

${HeaderAccessSchedule}

+ +
+ +

${HeaderAccessScheduleHelp}

+
+
+ +
+ +
+
+
+
+
diff --git a/src/controllers/dashboard/users/userparentalcontrol.js b/src/controllers/dashboard/users/userparentalcontrol.js new file mode 100644 index 0000000000..0b527e09ef --- /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 0000000000..897f0e7bd5 --- /dev/null +++ b/src/controllers/dashboard/users/userpassword.html @@ -0,0 +1,72 @@ +
+
+
+
+
+

+ ${Help} +
+
+ + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ ${HeaderEasyPinCode} +
+
+
${EasyPasswordHelp}
+
+
+ +
+
+
+ +
${LabelInNetworkSignInWithEasyPasswordHelp}
+
+
+ + +
+
+
+
+
+
+
diff --git a/src/controllers/dashboard/users/userpasswordpage.js b/src/controllers/dashboard/users/userpasswordpage.js new file mode 100644 index 0000000000..4171c55d6e --- /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 0000000000..9e2908266b --- /dev/null +++ b/src/controllers/dashboard/users/userprofiles.html @@ -0,0 +1,16 @@ +
+
+
+
+
+

${HeaderUsers}

+ + ${Help} +
+
+
+
+
+
diff --git a/src/controllers/dashboard/users/userprofilespage.js b/src/controllers/dashboard/users/userprofilespage.js new file mode 100644 index 0000000000..59d61a443f --- /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 += '
'; + html += '
'; + html += ``; + let imgUrl; + + if (user.PrimaryImageTag) { + imgUrl = ApiClient.getUserImageUrl(user.Id, { + width: 300, + tag: user.PrimaryImageTag, + type: 'Primary' + }); + } + + let imageClass = 'cardImage'; + + if (user.Policy.IsDisabled) { + imageClass += ' disabledUser'; + } + + if (imgUrl) { + html += ''; + html += '
'; + html += '
'; + html += '
'; + html += user.Name; + html += '
'; + html += ''; + html += '
'; + html += '
'; + const lastSeen = getLastSeenText(user.LastActivityDate); + html += lastSeen != '' ? lastSeen : ' '; + 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 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 += '
'; + html += '

' + globalize.translate('MessageNothingHere') + '

'; + html += '

' + globalize.translate('MessageNoCollectionsAvailable') + '

'; + 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 += '
'; + } else { + html += '
'; + } + + html += '
'; + 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 += '
'; + } else { + html += '
'; + } + + html += cardBuilder.getCardsHtml(recommendation.Items, { + shape: getPortraitShape(), + scalable: true, + overlayPlayButton: true, + allowBottomPadding: allowBottomPadding, + showTitle: true, + showYear: true, + centerText: 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 @@ +
+
+
+
+ +
+
+
+

+
+ + +
+
+
+
+

+ ${HeaderPassword} +

+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+
+
+

${HeaderEasyPinCode}

+
${EasyPasswordHelp}
+
+
+ +
+
+ +
${LabelInNetworkSignInWithEasyPasswordHelp}
+
+
+ + +
+
+
+
+
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)}