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

revert asyncRoutes to legacyRoutes

This commit is contained in:
grafixeyehero 2023-04-10 22:58:14 +03:00
parent d9bcfe19aa
commit 6cb6e10083
27 changed files with 3647 additions and 6 deletions

View file

@ -0,0 +1,114 @@
import { clearBackdrop } from '../backdrop/backdrop';
import * as mainTabsManager from '../maintabsmanager';
import layoutManager from '../layoutManager';
import '../../elements/emby-tabs/emby-tabs';
import LibraryMenu from '../../scripts/libraryMenu';
function onViewDestroy() {
const tabControllers = this.tabControllers;
if (tabControllers) {
tabControllers.forEach(function (t) {
if (t.destroy) {
t.destroy();
}
});
this.tabControllers = null;
}
this.view = null;
this.params = null;
this.currentTabController = null;
this.initialTabIndex = null;
}
class TabbedView {
constructor(view, params) {
this.tabControllers = [];
this.view = view;
this.params = params;
const self = this;
let currentTabIndex = parseInt(params.tab || this.getDefaultTabIndex(params.parentId), 10);
this.initialTabIndex = currentTabIndex;
function validateTabLoad(index) {
return self.validateTabLoad ? self.validateTabLoad(index) : Promise.resolve();
}
function loadTab(index, previousIndex) {
validateTabLoad(index).then(function () {
self.getTabController(index).then(function (controller) {
const refresh = !controller.refreshed;
controller.onResume({
autoFocus: previousIndex == null && layoutManager.tv,
refresh: refresh
});
controller.refreshed = true;
currentTabIndex = index;
self.currentTabController = controller;
});
});
}
function getTabContainers() {
return view.querySelectorAll('.tabContent');
}
function onTabChange(e) {
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
const previousIndex = e.detail.previousIndex;
const previousTabController = previousIndex == null ? null : self.tabControllers[previousIndex];
if (previousTabController && previousTabController.onPause) {
previousTabController.onPause();
}
loadTab(newIndex, previousIndex);
}
view.addEventListener('viewbeforehide', this.onPause.bind(this));
view.addEventListener('viewbeforeshow', function () {
mainTabsManager.setTabs(view, currentTabIndex, self.getTabs, getTabContainers, null, onTabChange, false);
});
view.addEventListener('viewshow', function (e) {
self.onResume(e.detail);
});
view.addEventListener('viewdestroy', onViewDestroy.bind(this));
}
onResume() {
this.setTitle();
clearBackdrop();
const currentTabController = this.currentTabController;
if (!currentTabController) {
mainTabsManager.selectedTabIndex(this.initialTabIndex);
} else if (currentTabController && currentTabController.onResume) {
currentTabController.onResume({});
}
}
onPause() {
const currentTabController = this.currentTabController;
if (currentTabController && currentTabController.onPause) {
currentTabController.onPause();
}
}
setTitle() {
LibraryMenu.setTitle('');
}
}
export default TabbedView;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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 += '<h3 class="checkboxListLabel">' + globalize.translate('HeaderLibraries') + '</h3>';
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
for (let i = 0; i < mediaFolders.length; i++) {
const folder = mediaFolders[i];
html += '<label><input type="checkbox" is="emby-checkbox" class="chkFolder" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
}
html += '</div>';
$('.folderAccess', page).html(html).trigger('create');
$('#chkEnableAllFolders', page).prop('checked', false);
}
function loadChannels(page, channels) {
let html = '';
html += '<h3 class="checkboxListLabel">' + globalize.translate('Channels') + '</h3>';
html += '<div class="checkboxList paperList" style="padding:.5em 1em;">';
for (let i = 0; i < channels.length; i++) {
const folder = channels[i];
html += '<label><input type="checkbox" is="emby-checkbox" class="chkChannel" data-id="' + folder.Id + '"/><span>' + folder.Name + '</span></label>';
}
html += '</div>';
$('.channelAccess', page).show().html(html).trigger('create');
if (channels.length) {
$('.channelAccessContainer', page).show();
} else {
$('.channelAccessContainer', page).hide();
}
$('#chkEnableAllChannels', page).prop('checked', false);
}
function loadUser(page) {
$('#txtUsername', page).val('');
$('#txtPassword', page).val('');
loading.show();
const promiseFolders = ApiClient.getJSON(ApiClient.getUrl('Library/MediaFolders', {
IsHidden: false
}));
const promiseChannels = ApiClient.getJSON(ApiClient.getUrl('Channels'));
Promise.all([promiseFolders, promiseChannels]).then(function (responses) {
loadMediaFolders(page, responses[0].Items);
loadChannels(page, responses[1].Items);
loading.hide();
});
}
function saveUser(page) {
const _user = {
Name: $('#txtUsername', page).val(),
Password: $('#txtPassword', page).val()
};
//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);
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -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 += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoCollectionsAvailable') + '</p>';
html += '</div>';
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(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);
};
}

View file

@ -0,0 +1,221 @@
import escapeHtml from 'escape-html';
import layoutManager from '../../components/layoutManager';
import loading from '../../components/loading/loading';
import libraryBrowser from '../../scripts/libraryBrowser';
import cardBuilder from '../../components/cardbuilder/cardBuilder';
import lazyLoader from '../../components/lazyLoader/lazyLoaderIntersectionObserver';
import globalize from '../../scripts/globalize';
import { appRouter } from '../../components/appRouter';
import '../../elements/emby-button/emby-button';
export default function (view, params, tabContent) {
function getPageData() {
const key = getSavedQueryKey();
let pageData = data[key];
if (!pageData) {
pageData = data[key] = {
query: {
SortBy: 'SortName',
SortOrder: 'Ascending',
IncludeItemTypes: 'Movie',
Recursive: true,
EnableTotalRecordCount: false
},
view: 'Poster'
};
pageData.query.ParentId = params.topParentId;
libraryBrowser.loadSavedQueryValues(key, pageData.query);
}
return pageData;
}
function getQuery() {
return getPageData().query;
}
function getSavedQueryKey() {
return 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 += '<div class="verticalSection">';
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
html += '<a is="emby-linkbutton" href="' + appRouter.getRouteUrl(item, {
context: 'movies',
parentId: params.topParentId
}) + '" class="more button-flat button-flat-mini sectionTitleTextButton btnMoreFromGenre' + item.Id + '">';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += escapeHtml(item.Name);
html += '</h2>';
html += '<span class="material-icons hide chevron_right" aria-hidden="true"></span>';
html += '</a>';
html += '</div>';
if (enableScrollX()) {
let scrollXClass = 'scrollX hiddenScrollX';
if (layoutManager.tv) {
scrollXClass += 'smoothScrollX padded-top-focusscale padded-bottom-focusscale';
}
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' lazy padded-left padded-right" data-id="' + item.Id + '">';
} else {
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap lazy padded-left padded-right" data-id="' + item.Id + '">';
}
html += '</div>';
html += '</div>';
}
if (!result.Items.length) {
html = '';
html += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoGenresAvailable') + '</p>';
html += '</div>';
}
elem.innerHTML = html;
lazyLoader.lazyChildren(elem, fillItemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(), query);
loading.hide();
});
}
const fullyReload = () => {
this.preRender();
this.renderTab();
};
const data = {};
this.getViewStyles = function () {
return 'Poster,PosterCard,Thumb,ThumbCard'.split(',');
};
this.getCurrentViewStyle = function () {
return getPageData().view;
};
this.setCurrentViewStyle = function (viewStyle) {
getPageData().view = viewStyle;
libraryBrowser.saveViewSetting(getSavedQueryKey(), viewStyle);
fullyReload();
};
this.enableViewSelection = true;
let promise;
this.preRender = function () {
promise = getPromise();
};
this.renderTab = function () {
reloadItems(tabContent, promise);
};
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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 += '<div class="noItemsMessage centerMessage">';
html += '<h1>' + globalize.translate('MessageNothingHere') + '</h1>';
html += '<p>' + globalize.translate('MessageNoTrailersFound') + '</p>';
html += '</div>';
}
const itemsContainer = tabContent.querySelector('.itemsContainer');
itemsContainer.innerHTML = html;
imageLoader.lazyChildren(itemsContainer);
libraryBrowser.saveQueryValues(getSavedQueryKey(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));
};
}

View file

@ -0,0 +1,69 @@
<div id="userImagePage" data-role="page" class="page libraryPage userPreferencesPage userPasswordPage noSecondaryNavPage" data-title="${Profile}" data-menubutton="false">
<div class="padded-left padded-right padded-bottom-page">
<div class="readOnlyContent" style="margin: 0 auto; padding: 0 1em;">
<div style="position:relative;display:inline-block;max-width:200px;">
<input id="uploadImage" type="file" accept="image/*" style="position:absolute;right:0;width:100%;height:100%;opacity:0;" />
<div id="image" style="width:200px;height:200px;background-repeat:no-repeat;background-position:center;border-radius:100%;background-size:cover;"></div>
</div>
<div style="vertical-align:top;margin:1em 2em;display:inline-block;">
<h2 class="username" style="margin:0;font-size:xx-large;"></h2>
<br/>
<button is="emby-button" type="button" class="raised hide" id="btnAddImage">
<span>${ButtonAddImage}</span>
</button>
<button is="emby-button" type="button" class="raised hide" id="btnDeleteImage">
<span>${DeleteImage}</span>
</button>
</div>
</div>
<form class="updatePasswordForm passwordSection userProfileSettingsForm hide" style="margin: 3em auto 0;">
<div class="verticalSection">
<h2 class="sectionTitle">
${HeaderPassword}
</h2>
<div id="fldCurrentPassword" class="inputContainer hide">
<input is="emby-input" type="password" id="txtCurrentPassword" label="${LabelCurrentPassword}" autocomplete="off" />
</div>
<div class="inputContainer">
<input is="emby-input" type="password" id="txtNewPassword" label="${LabelNewPassword}" autocomplete="off" />
</div>
<div class="inputContainer">
<input is="emby-input" type="password" id="txtNewPasswordConfirm" label="${LabelNewPasswordConfirm}" autocomplete="off" />
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" id="btnResetPassword" class="raised cancel block hide">
<span>${ResetPassword}</span>
</button>
</div>
</div>
</form>
<form class="localAccessForm localAccessSection userProfileSettingsForm hide" style="margin: 3em auto 0;">
<div class="verticalSection">
<h2 class="sectionTitle">${HeaderEasyPinCode}</h2>
<div>${EasyPasswordHelp}</div>
<br />
<div class="inputContainer">
<input is="emby-input" type="number" id="txtEasyPassword" label="${LabelEasyPinCode}" autocomplete="off" pattern="[0-9]*" step="1" maxlength="5" />
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" class="chkEnableLocalEasyPassword" />
<span>${LabelInNetworkSignInWithEasyPassword}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LabelInNetworkSignInWithEasyPasswordHelp}</div>
</div>
<div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
<button is="emby-button" type="button" id="btnResetEasyPassword" class="raised cancel block hide">
<span>${ButtonResetEasyPassword}</span>
</button>
</div>
</div>
</form>
</div>
</div>

View file

@ -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);
});
}

View file

@ -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' }
];

View file

@ -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 */}
<Route path='/' element={<ConnectionRequired isAdminRequired />}>
{ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute)}
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
<Route path='configurationpage' element={

View file

@ -193,5 +193,41 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
view: 'dashboard/streaming.html',
controller: 'dashboard/streaming'
}
}, {
path: 'usernew.html',
pageProps: {
view: 'dashboard/users/usernew.html',
controller: 'dashboard/users/usernew'
}
}, {
path: 'userprofiles.html',
pageProps: {
view: 'dashboard/users/userprofiles.html',
controller: 'dashboard/users/userprofilespage'
}
}, {
path: 'useredit.html',
pageProps: {
view: 'dashboard/users/useredit.html',
controller: 'dashboard/users/useredit'
}
}, {
path: 'userlibraryaccess.html',
pageProps: {
view: 'dashboard/users/userlibraryaccess.html',
controller: 'dashboard/users/userlibraryaccess'
}
}, {
path: 'userparentalcontrol.html',
pageProps: {
view: 'dashboard/users/userparentalcontrol.html',
controller: 'dashboard/users/userparentalcontrol'
}
}, {
path: 'userpassword.html',
pageProps: {
view: 'dashboard/users/userpassword.html',
controller: 'dashboard/users/userpasswordpage'
}
}
];

View file

@ -92,5 +92,23 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [
isNowPlayingBarEnabled: false,
isThemeMediaSupported: true
}
}, {
path: 'userprofile.html',
pageProps: {
controller: 'user/profile/index',
view: 'user/profile/index.html'
}
}, {
path: 'home.html',
pageProps: {
controller: 'home',
view: 'home.html'
}
}, {
path: 'movies.html',
pageProps: {
controller: 'movies/moviesrecommended',
view: 'movies/movies.html'
}
}
];