diff --git a/src/apiclient.d.ts b/src/apiclient.d.ts index a9f7f61ccd..1bda81641d 100644 --- a/src/apiclient.d.ts +++ b/src/apiclient.d.ts @@ -76,6 +76,7 @@ declare module 'jellyfin-apiclient' { accessToken(): string; addMediaPath(virtualFolderName: string, mediaPath: string, networkSharePath: string, refreshLibrary?: boolean): Promise; addVirtualFolder(name: string, type?: string, refreshLibrary?: boolean, libraryOptions?: any): Promise; + ajax(request: any): Promise; appName(): string; appVersion(): string; authenticateUserByName(name: string, password: string): Promise; diff --git a/src/components/playlisteditor/playlisteditor.js b/src/components/playlisteditor/playlisteditor.js deleted file mode 100644 index 625135a472..0000000000 --- a/src/components/playlisteditor/playlisteditor.js +++ /dev/null @@ -1,283 +0,0 @@ -import escapeHtml from 'escape-html'; -import dom from '../../scripts/dom'; -import dialogHelper from '../dialogHelper/dialogHelper'; -import loading from '../loading/loading'; -import layoutManager from '../layoutManager'; -import { playbackManager } from '../playback/playbackmanager'; -import { pluginManager } from '../pluginManager'; -import * as userSettings from '../../scripts/settings/userSettings'; -import { appRouter } from '../router/appRouter'; -import globalize from '../../scripts/globalize'; -import { PluginType } from '../../types/plugin.ts'; - -import '../../elements/emby-button/emby-button'; -import '../../elements/emby-input/emby-input'; -import '../../elements/emby-button/paper-icon-button-light'; -import '../../elements/emby-select/emby-select'; -import 'material-design-icons-iconfont'; -import '../formdialog.scss'; -import ServerConnections from '../ServerConnections'; - -let currentServerId; - -function onSubmit(e) { - const panel = dom.parentWithClass(this, 'dialog'); - - const playlistId = panel.querySelector('#selectPlaylistToAddTo').value; - const apiClient = ServerConnections.getApiClient(currentServerId); - - if (playlistId) { - userSettings.set('playlisteditor-lastplaylistid', playlistId); - addToPlaylist(apiClient, panel, playlistId); - } else { - createPlaylist(apiClient, panel); - } - - e.preventDefault(); - return false; -} - -function createPlaylist(apiClient, dlg) { - loading.show(); - - const url = apiClient.getUrl('Playlists', { - Name: dlg.querySelector('#txtNewPlaylistName').value, - Ids: dlg.querySelector('.fldSelectedItemIds').value || '', - userId: apiClient.getCurrentUserId() - - }); - - apiClient.ajax({ - type: 'POST', - url: url, - dataType: 'json', - contentType: 'application/json' - }).then(result => { - loading.hide(); - - const id = result.Id; - dlg.submitted = true; - dialogHelper.close(dlg); - redirectToPlaylist(apiClient, id); - }); -} - -function redirectToPlaylist(apiClient, id) { - appRouter.showItem(id, apiClient.serverId()); -} - -function addToPlaylist(apiClient, dlg, id) { - const itemIds = dlg.querySelector('.fldSelectedItemIds').value || ''; - - if (id === 'queue') { - playbackManager.queue({ - serverId: apiClient.serverId(), - ids: itemIds.split(',') - }); - dlg.submitted = true; - dialogHelper.close(dlg); - return; - } - - loading.show(); - - const url = apiClient.getUrl(`Playlists/${id}/Items`, { - Ids: itemIds, - userId: apiClient.getCurrentUserId() - }); - - apiClient.ajax({ - type: 'POST', - url: url - - }).then(() => { - loading.hide(); - - dlg.submitted = true; - dialogHelper.close(dlg); - }); -} - -function triggerChange(select) { - select.dispatchEvent(new CustomEvent('change', {})); -} - -function populatePlaylists(editorOptions, panel) { - const select = panel.querySelector('#selectPlaylistToAddTo'); - - loading.hide(); - - panel.querySelector('.newPlaylistInfo').classList.add('hide'); - - const options = { - Recursive: true, - IncludeItemTypes: 'Playlist', - SortBy: 'SortName', - EnableTotalRecordCount: false - }; - - const apiClient = ServerConnections.getApiClient(currentServerId); - const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance; - - apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => { - let html = ''; - - if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay?.Manager.isSyncPlayEnabled()) { - html += ``; - } - - html += ``; - - html += result.Items.map(i => { - return ``; - }); - - select.innerHTML = html; - - let defaultValue = editorOptions.defaultValue; - if (!defaultValue) { - defaultValue = userSettings.get('playlisteditor-lastplaylistid') || ''; - } - select.value = defaultValue === 'new' ? '' : defaultValue; - - // If the value is empty set it again, in case we tried to set a lastplaylistid that is no longer valid - if (!select.value) { - select.value = ''; - } - - triggerChange(select); - - loading.hide(); - }); -} - -function getEditorHtml(items) { - let html = ''; - - html += '
'; - html += '
'; - html += '
'; - - html += '
'; - let autoFocus = items.length ? ' autofocus' : ''; - html += ``; - html += '
'; - - html += '
'; - - html += '
'; - autoFocus = items.length ? '' : ' autofocus'; - html += ``; - html += '
'; - - // newPlaylistInfo - html += '
'; - - html += '
'; - html += ``; - html += '
'; - - html += ''; - - html += '
'; - html += '
'; - html += '
'; - - return html; -} - -function initEditor(content, options, items) { - content.querySelector('#selectPlaylistToAddTo').addEventListener('change', function () { - if (this.value) { - content.querySelector('.newPlaylistInfo').classList.add('hide'); - content.querySelector('#txtNewPlaylistName').removeAttribute('required'); - } else { - content.querySelector('.newPlaylistInfo').classList.remove('hide'); - content.querySelector('#txtNewPlaylistName').setAttribute('required', 'required'); - } - }); - - content.querySelector('form').addEventListener('submit', onSubmit); - - content.querySelector('.fldSelectedItemIds', content).value = items.join(','); - - if (items.length) { - content.querySelector('.fldSelectPlaylist').classList.remove('hide'); - populatePlaylists(options, content); - } else { - content.querySelector('.fldSelectPlaylist').classList.add('hide'); - - const selectPlaylistToAddTo = content.querySelector('#selectPlaylistToAddTo'); - selectPlaylistToAddTo.innerHTML = ''; - selectPlaylistToAddTo.value = ''; - triggerChange(selectPlaylistToAddTo); - } -} - -function centerFocus(elem, horiz, on) { - import('../../scripts/scrollHelper').then((scrollHelper) => { - const fn = on ? 'on' : 'off'; - scrollHelper.centerFocus[fn](elem, horiz); - }); -} - -export class PlaylistEditor { - show(options) { - const items = options.items || {}; - currentServerId = options.serverId; - - const dialogOptions = { - removeOnClose: true, - scrollY: false - }; - - if (layoutManager.tv) { - dialogOptions.size = 'fullscreen'; - } else { - dialogOptions.size = 'small'; - } - - const dlg = dialogHelper.createDialog(dialogOptions); - - dlg.classList.add('formDialog'); - - let html = ''; - const title = globalize.translate('HeaderAddToPlaylist'); - - html += '
'; - html += ``; - html += '

'; - html += title; - html += '

'; - - html += '
'; - - html += getEditorHtml(items); - - dlg.innerHTML = html; - - initEditor(dlg, options, items); - - dlg.querySelector('.btnCancel').addEventListener('click', () => { - dialogHelper.close(dlg); - }); - - if (layoutManager.tv) { - centerFocus(dlg.querySelector('.formDialogContent'), false, true); - } - - return dialogHelper.open(dlg).then(() => { - if (layoutManager.tv) { - centerFocus(dlg.querySelector('.formDialogContent'), false, false); - } - - if (dlg.submitted) { - return Promise.resolve(); - } - - return Promise.reject(); - }); - } -} - -export default PlaylistEditor; diff --git a/src/components/playlisteditor/playlisteditor.ts b/src/components/playlisteditor/playlisteditor.ts new file mode 100644 index 0000000000..4335fa17ec --- /dev/null +++ b/src/components/playlisteditor/playlisteditor.ts @@ -0,0 +1,321 @@ +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'; +import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; +import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api'; +import escapeHtml from 'escape-html'; + +import dom from 'scripts/dom'; +import globalize from 'scripts/globalize'; +import { currentSettings as userSettings } from 'scripts/settings/userSettings'; +import { PluginType } from 'types/plugin'; +import { toApi } from 'utils/jellyfin-apiclient/compat'; + +import dialogHelper from '../dialogHelper/dialogHelper'; +import loading from '../loading/loading'; +import layoutManager from '../layoutManager'; +import { playbackManager } from '../playback/playbackmanager'; +import { pluginManager } from '../pluginManager'; +import { appRouter } from '../router/appRouter'; +import ServerConnections from '../ServerConnections'; + +import 'elements/emby-button/emby-button'; +import 'elements/emby-input/emby-input'; +import 'elements/emby-button/paper-icon-button-light'; +import 'elements/emby-select/emby-select'; + +import 'material-design-icons-iconfont'; +import '../formdialog.scss'; + +interface DialogElement extends HTMLDivElement { + submitted?: boolean +} + +interface PlaylistEditorOptions { + items: string[], + serverId: string, + enableAddToPlayQueue?: boolean, + defaultValue?: string +} + +let currentServerId: string; + +function onSubmit(this: HTMLElement, e: Event) { + const panel = dom.parentWithClass(this, 'dialog') as DialogElement | null; + + if (panel) { + const playlistId = panel.querySelector('#selectPlaylistToAddTo')?.value; + + loading.show(); + + if (playlistId) { + userSettings.set('playlisteditor-lastplaylistid', playlistId); + addToPlaylist(panel, playlistId) + .catch(err => { + console.error('[PlaylistEditor] Failed to add to playlist %s', playlistId, err); + }) + .finally(loading.hide); + } else { + createPlaylist(panel) + .catch(err => { + console.error('[PlaylistEditor] Failed to create playlist', err); + }) + .finally(loading.hide); + } + } else { + console.error('[PlaylistEditor] Dialog element is missing!'); + } + + e.preventDefault(); + return false; +} + +function createPlaylist(dlg: DialogElement) { + const apiClient = ServerConnections.getApiClient(currentServerId); + const api = toApi(apiClient); + + const itemIds = dlg.querySelector('.fldSelectedItemIds')?.value || ''; + + return getPlaylistsApi(api) + .createPlaylist({ + name: dlg.querySelector('#txtNewPlaylistName')?.value, + ids: itemIds.split(','), + userId: apiClient.getCurrentUserId() + }) + .then(result => { + dlg.submitted = true; + dialogHelper.close(dlg); + + redirectToPlaylist(result.data.Id); + }); +} + +function redirectToPlaylist(id: string | undefined) { + appRouter.showItem(id, currentServerId); +} + +function addToPlaylist(dlg: DialogElement, id: string) { + const apiClient = ServerConnections.getApiClient(currentServerId); + const api = toApi(apiClient); + const itemIds = dlg.querySelector('.fldSelectedItemIds')?.value || ''; + + if (id === 'queue') { + playbackManager.queue({ + serverId: currentServerId, + ids: itemIds.split(',') + }); + dlg.submitted = true; + dialogHelper.close(dlg); + return Promise.resolve(); + } + + return getPlaylistsApi(api) + .addItemToPlaylist({ + playlistId: id, + ids: itemIds.split(','), + userId: apiClient.getCurrentUserId() + }) + .then(() => { + dlg.submitted = true; + dialogHelper.close(dlg); + }); +} + +function triggerChange(select: HTMLSelectElement) { + select.dispatchEvent(new CustomEvent('change', {})); +} + +function populatePlaylists(editorOptions: PlaylistEditorOptions, panel: DialogElement) { + const select = panel.querySelector('#selectPlaylistToAddTo'); + + if (!select) { + return Promise.reject(new Error('Playlist `; + html += ''; + + html += '
'; + + html += '
'; + autoFocus = items.length ? '' : ' autofocus'; + html += ``; + html += '
'; + + // newPlaylistInfo + html += '
'; + + html += '
'; + html += ``; + html += '
'; + + html += ''; + + html += ''; + html += ''; + html += ''; + + return html; +} + +function initEditor(content: DialogElement, options: PlaylistEditorOptions, items: string[]) { + content.querySelector('#selectPlaylistToAddTo')?.addEventListener('change', function(this: HTMLSelectElement) { + if (this.value) { + content.querySelector('.newPlaylistInfo')?.classList.add('hide'); + content.querySelector('#txtNewPlaylistName')?.removeAttribute('required'); + } else { + content.querySelector('.newPlaylistInfo')?.classList.remove('hide'); + content.querySelector('#txtNewPlaylistName')?.setAttribute('required', 'required'); + } + }); + + content.querySelector('form')?.addEventListener('submit', onSubmit); + + const selectedItemsInput = content.querySelector('.fldSelectedItemIds'); + if (selectedItemsInput) { + selectedItemsInput.value = items.join(','); + } + + if (items.length) { + content.querySelector('.fldSelectPlaylist')?.classList.remove('hide'); + populatePlaylists(options, content) + .catch(err => { + console.error('[PlaylistEditor] failed to populate playlists', err); + }) + .finally(loading.hide); + } else { + content.querySelector('.fldSelectPlaylist')?.classList.add('hide'); + + const selectPlaylistToAddTo = content.querySelector('#selectPlaylistToAddTo'); + if (selectPlaylistToAddTo) { + selectPlaylistToAddTo.innerHTML = ''; + selectPlaylistToAddTo.value = ''; + triggerChange(selectPlaylistToAddTo); + } + } +} + +function centerFocus(elem: HTMLDivElement | null, horiz: boolean, on: boolean) { + if (!elem) { + console.error('[PlaylistEditor] cannot focus null element'); + return; + } + + import('../../scripts/scrollHelper') + .then((scrollHelper) => { + const fn = on ? 'on' : 'off'; + scrollHelper.centerFocus[fn](elem, horiz); + }) + .catch(err => { + console.error('[PlaylistEditor] failed to load scroll helper', err); + }); +} + +export class PlaylistEditor { + show(options: PlaylistEditorOptions) { + const items = options.items || []; + currentServerId = options.serverId; + + const dialogOptions = { + removeOnClose: true, + scrollY: false, + size: layoutManager.tv ? 'fullscreen' : 'small' + }; + + const dlg: DialogElement = dialogHelper.createDialog(dialogOptions); + + dlg.classList.add('formDialog'); + + let html = ''; + const title = globalize.translate('HeaderAddToPlaylist'); + + html += '
'; + html += ``; + html += '

'; + html += title; + html += '

'; + + html += '
'; + + html += getEditorHtml(items); + + dlg.innerHTML = html; + + initEditor(dlg, options, items); + + dlg.querySelector('.btnCancel')?.addEventListener('click', () => { + dialogHelper.close(dlg); + }); + + if (layoutManager.tv) { + centerFocus(dlg.querySelector('.formDialogContent'), false, true); + } + + return dialogHelper.open(dlg).then(() => { + if (layoutManager.tv) { + centerFocus(dlg.querySelector('.formDialogContent'), false, false); + } + + if (dlg.submitted) { + return Promise.resolve(); + } + + return Promise.reject(new Error()); + }); + } +} + +export default PlaylistEditor; diff --git a/src/scripts/playlists.js b/src/scripts/playlists.js deleted file mode 100644 index 33963bf8a2..0000000000 --- a/src/scripts/playlists.js +++ /dev/null @@ -1,203 +0,0 @@ -import loading from '../components/loading/loading'; -import listView from '../components/listview/listview'; -import cardBuilder from '../components/cardbuilder/cardBuilder'; -import libraryMenu from './libraryMenu'; -import libraryBrowser from './libraryBrowser'; -import imageLoader from '../components/images/imageLoader'; -import * as userSettings from './settings/userSettings'; -import '../elements/emby-itemscontainer/emby-itemscontainer'; -import Dashboard from '../utils/dashboard'; - -export default function (view) { - function getPageData() { - const key = getSavedQueryKey(); - let pageData = data[key]; - - if (!pageData) { - pageData = data[key] = { - query: { - SortBy: 'SortName', - SortOrder: 'Ascending', - IncludeItemTypes: 'Playlist', - Recursive: true, - Fields: 'PrimaryImageAspectRatio,SortName,CumulativeRunTimeTicks,CanDelete', - StartIndex: 0 - }, - view: userSettings.getSavedView(key) || 'Poster' - }; - - if (userSettings.libraryPageSize() > 0) { - pageData.query['Limit'] = userSettings.libraryPageSize(); - } - - pageData.query.ParentId = libraryMenu.getTopParentId(); - userSettings.loadQuerySettings(key, pageData.query); - } - - return pageData; - } - - function getQuery() { - return getPageData().query; - } - - function getSavedQueryKey() { - return `${libraryMenu.getTopParentId()}-playlists`; - } - - function showLoadingMessage() { - loading.show(); - } - - function hideLoadingMessage() { - loading.hide(); - } - - function onViewStyleChange() { - const viewStyle = getPageData().view; - const itemsContainer = view.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 = ''; - } - - function reloadItems() { - showLoadingMessage(); - const query = getQuery(); - const promise1 = ApiClient.getItems(Dashboard.getCurrentUserId(), query); - // TODO: promise2 is unused, check if necessary. - const promise2 = Dashboard.getCurrentUser(); - Promise.all([promise1, promise2]).then(function (responses) { - const result = responses[0]; - // TODO: Is the scroll necessary? - window.scrollTo(0, 0); - let html = ''; - const viewStyle = getPageData().view; - view.querySelector('.listTopPaging').innerHTML = libraryBrowser.getQueryPagingHtml({ - startIndex: query.StartIndex, - limit: query.Limit, - totalRecordCount: result.TotalRecordCount, - viewButton: false, - showLimit: false, - updatePageSizeSetting: false, - addLayoutButton: true, - layouts: 'List,Poster,PosterCard,Thumb,ThumbCard', - currentLayout: viewStyle - }); - - if (result.TotalRecordCount) { - if (viewStyle == 'List') { - html = listView.getListViewHtml({ - items: result.Items, - sortBy: query.SortBy - }); - } else if (viewStyle == 'PosterCard') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'square', - coverImage: true, - showTitle: true, - cardLayout: true - }); - } else if (viewStyle == 'Thumb') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'backdrop', - showTitle: true, - centerText: true, - preferThumb: true, - overlayPlayButton: true - }); - } else if (viewStyle == 'ThumbCard') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'backdrop', - showTitle: true, - preferThumb: true, - cardLayout: true - }); - } else { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'square', - showTitle: true, - coverImage: true, - centerText: true, - overlayPlayButton: true - }); - } - view.querySelector('.noItemsMessage').classList.add('hide'); - } else { - view.querySelector('.noItemsMessage').classList.remove('hide'); - } - - const elem = view.querySelector('.itemsContainer'); - elem.innerHTML = html; - imageLoader.lazyChildren(elem); - const btnNextPage = view.querySelector('.btnNextPage'); - - if (btnNextPage) { - btnNextPage.addEventListener('click', function () { - if (userSettings.libraryPageSize() > 0) { - query.StartIndex += query.Limit; - } - reloadItems(); - }); - } - - const btnPreviousPage = view.querySelector('.btnPreviousPage'); - - if (btnPreviousPage) { - btnPreviousPage.addEventListener('click', function () { - if (userSettings.libraryPageSize() > 0) { - query.StartIndex = Math.max(0, query.StartIndex - query.Limit); - } - reloadItems(); - }); - } - - const btnChangeLayout = view.querySelector('.btnChangeLayout'); - - if (btnChangeLayout) { - btnChangeLayout.addEventListener('layoutchange', function (e) { - const layout = e.detail.viewStyle; - getPageData().view = layout; - userSettings.saveViewSetting(getSavedQueryKey(), layout); - onViewStyleChange(); - reloadItems(); - }); - } - - userSettings.saveQuerySettings(getSavedQueryKey(), query); - hideLoadingMessage(); - }); - } - - const data = {}; - view.addEventListener('viewbeforeshow', function () { - reloadItems(); - }); - view.querySelector('.btnNewPlaylist').addEventListener('click', function () { - import('../components/playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => { - const serverId = ApiClient.serverInfo().Id; - const playlistEditor = new PlaylistEditor(); - playlistEditor.show({ - items: [], - serverId: serverId - }).catch(() => { - // Dialog closed - }); - }).catch(err => { - console.error('[btnNewPlaylist] failed to load playlist editor', err); - }); - }); - onViewStyleChange(); -} - diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index b72bbd5627..ddfb8e8194 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -68,7 +68,7 @@ export class UserSettings { * Set value of setting. * @param {string} name - Name of setting. * @param {mixed} value - Value of setting. - * @param {boolean} enableOnServer - Flag to save preferences on server. + * @param {boolean} [enableOnServer] - Flag to save preferences on server. */ set(name, value, enableOnServer) { const userId = this.currentUserId; @@ -90,7 +90,7 @@ export class UserSettings { /** * Get value of setting. * @param {string} name - Name of setting. - * @param {boolean} enableOnServer - Flag to return preferences from server (cached). + * @param {boolean} [enableOnServer] - Flag to return preferences from server (cached). * @return {string} Value of setting. */ get(name, enableOnServer) {