mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Add playlist editing
This commit is contained in:
parent
9a192c7e5c
commit
363171b56d
6 changed files with 143 additions and 27 deletions
|
@ -5,7 +5,7 @@ import globalize from '../lib/globalize';
|
|||
import actionsheet from './actionSheet/actionSheet';
|
||||
import { appHost } from './apphost';
|
||||
import { appRouter } from './router/appRouter';
|
||||
import itemHelper from './itemHelper';
|
||||
import itemHelper, { canEditPlaylist } from './itemHelper';
|
||||
import { playbackManager } from './playback/playbackmanager';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import toast from './toast/toast';
|
||||
|
@ -29,7 +29,7 @@ function getDeleteLabel(type) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getCommands(options) {
|
||||
export async function getCommands(options) {
|
||||
const item = options.item;
|
||||
const user = options.user;
|
||||
|
||||
|
@ -209,6 +209,17 @@ export function getCommands(options) {
|
|||
});
|
||||
}
|
||||
|
||||
if (item.Type === BaseItemKind.Playlist) {
|
||||
const _canEditPlaylist = await canEditPlaylist(user, item);
|
||||
if (_canEditPlaylist) {
|
||||
commands.push({
|
||||
name: globalize.translate('Edit'),
|
||||
id: 'editplaylist',
|
||||
icon: 'edit'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const canEdit = itemHelper.canEdit(user, item);
|
||||
if (canEdit && options.edit !== false && item.Type !== 'SeriesTimer') {
|
||||
const text = (item.Type === 'Timer' || item.Type === 'SeriesTimer') ? globalize.translate('Edit') : globalize.translate('EditMetadata');
|
||||
|
@ -466,6 +477,15 @@ function executeCommand(item, id, options) {
|
|||
case 'edit':
|
||||
editItem(apiClient, item).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||
break;
|
||||
case 'editplaylist':
|
||||
import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
||||
const playlistEditor = new PlaylistEditor();
|
||||
playlistEditor.show({
|
||||
id: itemId,
|
||||
serverId
|
||||
}).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id));
|
||||
});
|
||||
break;
|
||||
case 'editimages':
|
||||
import('./imageeditor/imageeditor').then((imageEditor) => {
|
||||
imageEditor.show({
|
||||
|
@ -712,19 +732,19 @@ function refresh(apiClient, item) {
|
|||
});
|
||||
}
|
||||
|
||||
export function show(options) {
|
||||
const commands = getCommands(options);
|
||||
export async function show(options) {
|
||||
const commands = await getCommands(options);
|
||||
if (!commands.length) {
|
||||
return Promise.reject();
|
||||
throw new Error('No item commands present');
|
||||
}
|
||||
|
||||
return actionsheet.show({
|
||||
const id = await actionsheet.show({
|
||||
items: commands,
|
||||
positionTo: options.positionTo,
|
||||
resolveOnClick: ['share']
|
||||
}).then(function (id) {
|
||||
return executeCommand(options.item, id, options);
|
||||
});
|
||||
|
||||
return executeCommand(options.item, id, options);
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { appHost } from './apphost';
|
||||
import globalize from 'lib/globalize';
|
||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||
import { LocationType } from '@jellyfin/sdk/lib/generated-client/models/location-type';
|
||||
import { RecordingStatus } from '@jellyfin/sdk/lib/generated-client/models/recording-status';
|
||||
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
|
||||
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
||||
|
||||
import { appHost } from './apphost';
|
||||
import globalize from 'lib/globalize';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
|
||||
export function getDisplayName(item, options = {}) {
|
||||
if (!item) {
|
||||
|
@ -159,6 +163,25 @@ export function canEditImages (user, item) {
|
|||
return itemType !== 'Timer' && itemType !== 'SeriesTimer' && canEdit(user, item) && !isLocalItem(item);
|
||||
}
|
||||
|
||||
export async function canEditPlaylist(user, item) {
|
||||
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
||||
const api = toApi(apiClient);
|
||||
|
||||
try {
|
||||
const { data: permissions } = await getPlaylistsApi(api)
|
||||
.getPlaylistUser({
|
||||
userId: user.Id,
|
||||
playlistId: item.Id
|
||||
});
|
||||
|
||||
return !!permissions.CanEdit;
|
||||
} catch (err) {
|
||||
console.error('Failed to get playlist permissions', err);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canEditSubtitles (user, item) {
|
||||
if (item.MediaType !== MediaType.Video) {
|
||||
return false;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-ite
|
|||
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 { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api';
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import toast from 'components/toast/toast';
|
||||
|
@ -28,11 +29,13 @@ import 'material-design-icons-iconfont';
|
|||
import '../formdialog.scss';
|
||||
|
||||
interface DialogElement extends HTMLDivElement {
|
||||
playlistId?: string
|
||||
submitted?: boolean
|
||||
}
|
||||
|
||||
interface PlaylistEditorOptions {
|
||||
items: string[],
|
||||
id?: string,
|
||||
serverId: string,
|
||||
enableAddToPlayQueue?: boolean,
|
||||
defaultValue?: string
|
||||
|
@ -56,6 +59,13 @@ function onSubmit(this: HTMLElement, e: Event) {
|
|||
toast(globalize.translate('PlaylistError.AddFailed'));
|
||||
})
|
||||
.finally(loading.hide);
|
||||
} else if (panel.playlistId) {
|
||||
updatePlaylist(panel)
|
||||
.catch(err => {
|
||||
console.error('[PlaylistEditor] Failed to update to playlist %s', panel.playlistId, err);
|
||||
toast(globalize.translate('PlaylistError.UpdateFailed'));
|
||||
})
|
||||
.finally(loading.hide);
|
||||
} else {
|
||||
createPlaylist(panel)
|
||||
.catch(err => {
|
||||
|
@ -99,6 +109,26 @@ function redirectToPlaylist(id: string | undefined) {
|
|||
appRouter.showItem(id, currentServerId);
|
||||
}
|
||||
|
||||
function updatePlaylist(dlg: DialogElement) {
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
const api = toApi(apiClient);
|
||||
|
||||
if (!dlg.playlistId) return Promise.reject(new Error('Missing playlist ID'));
|
||||
|
||||
return getPlaylistsApi(api)
|
||||
.updatePlaylist({
|
||||
playlistId: dlg.playlistId,
|
||||
updatePlaylistDto: {
|
||||
Name: dlg.querySelector<HTMLInputElement>('#txtNewPlaylistName')?.value,
|
||||
IsPublic: dlg.querySelector<HTMLInputElement>('#chkPlaylistPublic')?.checked
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
dlg.submitted = true;
|
||||
dialogHelper.close(dlg);
|
||||
});
|
||||
}
|
||||
|
||||
function addToPlaylist(dlg: DialogElement, id: string) {
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
const api = toApi(apiClient);
|
||||
|
@ -210,7 +240,7 @@ function populatePlaylists(editorOptions: PlaylistEditorOptions, panel: DialogEl
|
|||
});
|
||||
}
|
||||
|
||||
function getEditorHtml(items: string[]) {
|
||||
function getEditorHtml(items: string[], options: PlaylistEditorOptions) {
|
||||
let html = '';
|
||||
|
||||
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
|
||||
|
@ -232,7 +262,7 @@ function getEditorHtml(items: string[]) {
|
|||
html += `
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkPlaylistPublic" checked />
|
||||
<input type="checkbox" is="emby-checkbox" id="chkPlaylistPublic" />
|
||||
<span>${globalize.translate('PlaylistPublic')}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">
|
||||
|
@ -244,7 +274,7 @@ function getEditorHtml(items: string[]) {
|
|||
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 += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${options.id ? globalize.translate('Save') : globalize.translate('Add')}</button>`;
|
||||
html += '</div>';
|
||||
|
||||
html += '<input type="hidden" class="fldSelectedItemIds" />';
|
||||
|
@ -281,6 +311,34 @@ function initEditor(content: DialogElement, options: PlaylistEditorOptions, item
|
|||
console.error('[PlaylistEditor] failed to populate playlists', err);
|
||||
})
|
||||
.finally(loading.hide);
|
||||
} else if (options.id) {
|
||||
content.querySelector('.fldSelectPlaylist')?.classList.add('hide');
|
||||
const panel = dom.parentWithClass(content, 'dialog') as DialogElement | null;
|
||||
if (!panel) {
|
||||
console.error('[PlaylistEditor] could not find dialog element');
|
||||
return;
|
||||
}
|
||||
|
||||
const apiClient = ServerConnections.getApiClient(currentServerId);
|
||||
const api = toApi(apiClient);
|
||||
Promise.all([
|
||||
getUserLibraryApi(api)
|
||||
.getItem({ itemId: options.id }),
|
||||
getPlaylistsApi(api)
|
||||
.getPlaylist({ playlistId: options.id })
|
||||
])
|
||||
.then(([ { data: playlistItem }, { data: playlist } ]) => {
|
||||
panel.playlistId = options.id;
|
||||
|
||||
const nameField = panel.querySelector<HTMLInputElement>('#txtNewPlaylistName');
|
||||
if (nameField) nameField.value = playlistItem.Name || '';
|
||||
|
||||
const publicField = panel.querySelector<HTMLInputElement>('#chkPlaylistPublic');
|
||||
if (publicField) publicField.checked = !!playlist.OpenAccess;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[playlistEditor] failed to get playlist details', err);
|
||||
});
|
||||
} else {
|
||||
content.querySelector('.fldSelectPlaylist')?.classList.add('hide');
|
||||
|
||||
|
@ -325,17 +383,21 @@ export class PlaylistEditor {
|
|||
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;
|
||||
if (items.length) {
|
||||
html += globalize.translate('HeaderAddToPlaylist');
|
||||
} else if (options.id) {
|
||||
html += globalize.translate('HeaderEditPlaylist');
|
||||
} else {
|
||||
html += globalize.translate('HeaderNewPlaylist');
|
||||
}
|
||||
html += '</h3>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
html += getEditorHtml(items);
|
||||
html += getEditorHtml(items, options);
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue