mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2025-02-25 17:01:24 +00:00
Add playlist editing
This commit is contained in:
parent
9a192c7e5c
commit
363171b56d
@ -1,4 +1,4 @@
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
@ -113,6 +113,7 @@ const MoreCommandsButton: FC<MoreCommandsButtonProps> = ({
|
||||
itemId: selectedItemId || itemId || ''
|
||||
});
|
||||
const parentId = item?.SeasonId || item?.SeriesId || item?.ParentId;
|
||||
const [ hasCommands, setHasCommands ] = useState(false);
|
||||
|
||||
const playlistItem = useMemo(() => {
|
||||
let PlaylistItemId: string | null = null;
|
||||
@ -198,10 +199,15 @@ const MoreCommandsButton: FC<MoreCommandsButtonProps> = ({
|
||||
[defaultMenuOptions, item, itemId, items, parentId, queryClient, queryKey]
|
||||
);
|
||||
|
||||
if (
|
||||
item
|
||||
&& itemContextMenu.getCommands(defaultMenuOptions).length
|
||||
) {
|
||||
useEffect(() => {
|
||||
const getCommands = async () => {
|
||||
const commands = await itemContextMenu.getCommands(defaultMenuOptions);
|
||||
setHasCommands(commands.length > 0);
|
||||
};
|
||||
void getCommands();
|
||||
}, [ defaultMenuOptions ]);
|
||||
|
||||
if (item && hasCommands) {
|
||||
return (
|
||||
<IconButton
|
||||
className='button-flat btnMoreCommands'
|
||||
|
@ -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;
|
||||
|
||||
|
@ -582,11 +582,13 @@ function reloadFromItem(instance, page, params, item, user) {
|
||||
page.querySelector('.btnSplitVersions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (itemContextMenu.getCommands(getContextMenuOptions(item, user)).length) {
|
||||
hideAll(page, 'btnMoreCommands', true);
|
||||
} else {
|
||||
hideAll(page, 'btnMoreCommands');
|
||||
}
|
||||
itemContextMenu.getCommands(getContextMenuOptions(item, user)).then(commands => {
|
||||
if (commands.length) {
|
||||
hideAll(page, 'btnMoreCommands', true);
|
||||
} else {
|
||||
hideAll(page, 'btnMoreCommands');
|
||||
}
|
||||
});
|
||||
|
||||
const itemBirthday = page.querySelector('#itemBirthday');
|
||||
|
||||
|
@ -411,6 +411,7 @@
|
||||
"HeaderDummyChapter": "Chapter Images",
|
||||
"HeaderDVR": "DVR",
|
||||
"HeaderEditImages": "Edit Images",
|
||||
"HeaderEditPlaylist": "Edit Playlist",
|
||||
"HeaderEnabledFields": "Enabled Fields",
|
||||
"HeaderEnabledFieldsHelp": "Uncheck a field to lock it and prevent its data from being changed.",
|
||||
"HeaderEpisodesStatus": "Episodes Status",
|
||||
@ -456,6 +457,7 @@
|
||||
"HeaderNetworking": "IP Protocols",
|
||||
"HeaderNewApiKey": "New API Key",
|
||||
"HeaderNewDevices": "New Devices",
|
||||
"HeaderNewPlaylist": "New Playlist",
|
||||
"HeaderNewRepository": "New Repository",
|
||||
"HeaderNextItem": "Next {0}",
|
||||
"HeaderNextItemPlayingInValue": "Next {0} Playing in {1}",
|
||||
@ -1326,6 +1328,7 @@
|
||||
"PlayFromBeginning": "Play from beginning",
|
||||
"PlaylistError.AddFailed": "Error adding to playlist",
|
||||
"PlaylistError.CreateFailed": "Error creating playlist",
|
||||
"PlaylistError.UpdateFailed": "Error updating playlist",
|
||||
"PlaylistPublic": "Allow public access",
|
||||
"PlaylistPublicDescription": "Allow this playlist to be viewed by any logged in user.",
|
||||
"Playlists": "Playlists",
|
||||
|
Loading…
x
Reference in New Issue
Block a user