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

Merge pull request #5392 from thornbill/playlist-editor-ts

This commit is contained in:
Bill Thornton 2024-04-21 23:43:44 -04:00 committed by GitHub
commit 7158be9aaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 324 additions and 488 deletions

View file

@ -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 += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`;
}
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
html += result.Items.map(i => {
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
});
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 += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
html += '<div class="dialogContentInner dialog-content-centered">';
html += '<form style="margin:auto;">';
html += '<div class="fldSelectPlaylist selectContainer">';
let autoFocus = items.length ? ' autofocus' : '';
html += `<select is="emby-select" id="selectPlaylistToAddTo" label="${globalize.translate('LabelPlaylist')}"${autoFocus}></select>`;
html += '</div>';
html += '<div class="newPlaylistInfo">';
html += '<div class="inputContainer">';
autoFocus = items.length ? '' : ' autofocus';
html += `<input is="emby-input" type="text" id="txtNewPlaylistName" required="required" label="${globalize.translate('LabelName')}"${autoFocus} />`;
html += '</div>';
// newPlaylistInfo
html += '</div>';
html += '<div class="formDialogFooter">';
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('Add')}</button>`;
html += '</div>';
html += '<input type="hidden" class="fldSelectedItemIds" />';
html += '</form>';
html += '</div>';
html += '</div>';
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 += '<div class="formDialogHeader">';
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
html += '<h3 class="formDialogHeaderTitle">';
html += title;
html += '</h3>';
html += '</div>';
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;

View file

@ -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<HTMLSelectElement>('#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<HTMLInputElement>('.fldSelectedItemIds')?.value || '';
return getPlaylistsApi(api)
.createPlaylist({
name: dlg.querySelector<HTMLInputElement>('#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<HTMLInputElement>('.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<HTMLSelectElement>('#selectPlaylistToAddTo');
if (!select) {
return Promise.reject(new Error('Playlist <select> element is missing'));
}
loading.show();
panel.querySelector('.newPlaylistInfo')?.classList.add('hide');
const apiClient = ServerConnections.getApiClient(currentServerId);
const api = toApi(apiClient);
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
return getItemsApi(api)
.getItems({
userId: apiClient.getCurrentUserId(),
includeItemTypes: [ BaseItemKind.Playlist ],
sortBy: [ ItemSortBy.SortName ],
recursive: true
})
.then(({ data }) => {
let html = '';
if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay?.Manager.isSyncPlayEnabled()) {
html += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`;
}
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
html += data.Items?.map(i => {
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
});
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);
});
}
function getEditorHtml(items: string[]) {
let html = '';
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
html += '<div class="dialogContentInner dialog-content-centered">';
html += '<form style="margin:auto;">';
html += '<div class="fldSelectPlaylist selectContainer">';
let autoFocus = items.length ? ' autofocus' : '';
html += `<select is="emby-select" id="selectPlaylistToAddTo" label="${globalize.translate('LabelPlaylist')}"${autoFocus}></select>`;
html += '</div>';
html += '<div class="newPlaylistInfo">';
html += '<div class="inputContainer">';
autoFocus = items.length ? '' : ' autofocus';
html += `<input is="emby-input" type="text" id="txtNewPlaylistName" required="required" label="${globalize.translate('LabelName')}"${autoFocus} />`;
html += '</div>';
// newPlaylistInfo
html += '</div>';
html += '<div class="formDialogFooter">';
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('Add')}</button>`;
html += '</div>';
html += '<input type="hidden" class="fldSelectedItemIds" />';
html += '</form>';
html += '</div>';
html += '</div>';
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<HTMLInputElement>('.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<HTMLSelectElement>('#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 += '<div class="formDialogHeader">';
html += `<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
html += '<h3 class="formDialogHeaderTitle">';
html += title;
html += '</h3>';
html += '</div>';
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;