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

After adding an item to a playlist, I often want to move it to the top and it's tedious to drag and drop if the playlist is large. This adds 'Move to Top' and 'Move to Bottom' options to a playlist item context menu.
456 lines
15 KiB
JavaScript
456 lines
15 KiB
JavaScript
/**
|
|
* Module shortcuts.
|
|
* @module components/shortcuts
|
|
*/
|
|
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api';
|
|
|
|
import { playbackManager } from './playback/playbackmanager';
|
|
import inputManager from '../scripts/inputManager';
|
|
import { appRouter } from './router/appRouter';
|
|
import globalize from '../lib/globalize';
|
|
import dom from '../scripts/dom';
|
|
import recordingHelper from './recordingcreator/recordinghelper';
|
|
import ServerConnections from './ServerConnections';
|
|
import toast from './toast/toast';
|
|
import * as userSettings from '../scripts/settings/userSettings';
|
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
|
|
|
function playAllFromHere(card, serverId, queue) {
|
|
const parent = card.parentNode;
|
|
const className = card.classList.length ? (`.${card.classList[0]}`) : '';
|
|
const cards = parent.querySelectorAll(`${className}[data-id]`);
|
|
|
|
const ids = [];
|
|
|
|
let foundCard = false;
|
|
let startIndex;
|
|
|
|
for (let i = 0, length = cards.length; i < length; i++) {
|
|
if (cards[i] === card) {
|
|
foundCard = true;
|
|
startIndex = i;
|
|
}
|
|
if (foundCard || !queue) {
|
|
ids.push(cards[i].getAttribute('data-id'));
|
|
}
|
|
}
|
|
|
|
const itemsContainer = dom.parentWithClass(card, 'itemsContainer');
|
|
if (itemsContainer?.fetchData) {
|
|
const queryOptions = queue ? { StartIndex: startIndex } : {};
|
|
|
|
return itemsContainer.fetchData(queryOptions).then(result => {
|
|
if (queue) {
|
|
return playbackManager.queue({
|
|
items: result.Items
|
|
});
|
|
} else {
|
|
return playbackManager.play({
|
|
items: result.Items,
|
|
startIndex: startIndex
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!ids.length) {
|
|
return;
|
|
}
|
|
|
|
if (queue) {
|
|
return playbackManager.queue({
|
|
ids: ids,
|
|
serverId: serverId
|
|
});
|
|
} else {
|
|
return playbackManager.play({
|
|
ids: ids,
|
|
serverId: serverId,
|
|
startIndex: startIndex
|
|
});
|
|
}
|
|
}
|
|
|
|
function showProgramDialog(item) {
|
|
import('./recordingcreator/recordingcreator').then(({ default:recordingCreator }) => {
|
|
recordingCreator.show(item.Id, item.ServerId);
|
|
});
|
|
}
|
|
|
|
function getItem(button) {
|
|
button = dom.parentWithAttribute(button, 'data-id');
|
|
const serverId = button.getAttribute('data-serverid');
|
|
const id = button.getAttribute('data-id');
|
|
const type = button.getAttribute('data-type');
|
|
|
|
const apiClient = ServerConnections.getApiClient(serverId);
|
|
|
|
if (type === 'Timer') {
|
|
return apiClient.getLiveTvTimer(id);
|
|
}
|
|
if (type === 'SeriesTimer') {
|
|
return apiClient.getLiveTvSeriesTimer(id);
|
|
}
|
|
return apiClient.getItem(apiClient.getCurrentUserId(), id);
|
|
}
|
|
|
|
function notifyRefreshNeeded(childElement, itemsContainer) {
|
|
itemsContainer = itemsContainer || dom.parentWithAttribute(childElement, 'is', 'emby-itemscontainer');
|
|
|
|
if (itemsContainer) {
|
|
itemsContainer.notifyRefreshNeeded(true);
|
|
}
|
|
}
|
|
|
|
function showContextMenu(card, options = {}) {
|
|
getItem(card).then(item => {
|
|
const playlistId = card.getAttribute('data-playlistid');
|
|
const collectionId = card.getAttribute('data-collectionid');
|
|
|
|
if (playlistId) {
|
|
const elem = dom.parentWithAttribute(card, 'data-playlistitemid');
|
|
item.PlaylistItemId = elem ? elem.getAttribute('data-playlistitemid') : null;
|
|
|
|
const itemsContainer = dom.parentWithAttribute(card, 'is', 'emby-itemscontainer');
|
|
if (itemsContainer) {
|
|
let index = 0;
|
|
for (const listItem of itemsContainer.querySelectorAll('.listItem')) {
|
|
const playlistItemId = listItem.getAttribute('data-playlistitemid');
|
|
if (playlistItemId == item.PlaylistItemId) {
|
|
item.PlaylistIndex = index;
|
|
}
|
|
index++;
|
|
}
|
|
item.PlaylistItems = index;
|
|
}
|
|
}
|
|
|
|
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
|
const api = toApi(apiClient);
|
|
|
|
Promise.all([
|
|
// Import the item menu component
|
|
import('./itemContextMenu'),
|
|
// Fetch the current user
|
|
apiClient.getCurrentUser(),
|
|
// Fetch playlist perms if item is a child of a playlist
|
|
playlistId ?
|
|
getPlaylistsApi(api)
|
|
.getPlaylistUser({
|
|
playlistId,
|
|
userId: apiClient.getCurrentUserId()
|
|
})
|
|
.then(({ data }) => data)
|
|
.catch(err => {
|
|
// If a user doesn't have access, then the request will 404 and throw
|
|
console.info('[Shortcuts] Failed to fetch playlist permissions', err);
|
|
return { CanEdit: false };
|
|
}) :
|
|
// Not a playlist item
|
|
Promise.resolve({ CanEdit: false })
|
|
])
|
|
.then(([
|
|
itemContextMenu,
|
|
user,
|
|
playlistPerms
|
|
]) => {
|
|
return itemContextMenu.show({
|
|
item,
|
|
play: true,
|
|
queue: true,
|
|
playAllFromHere: item.Type === 'Season' || !item.IsFolder,
|
|
queueAllFromHere: !item.IsFolder,
|
|
playlistId,
|
|
canEditPlaylist: !!playlistPerms.CanEdit,
|
|
collectionId,
|
|
user,
|
|
...options
|
|
});
|
|
})
|
|
.then(result => {
|
|
if (result.command === 'playallfromhere' || result.command === 'queueallfromhere') {
|
|
executeAction(card, options.positionTo, result.command);
|
|
} else if (result.updated || result.deleted) {
|
|
notifyRefreshNeeded(card, options.itemsContainer);
|
|
}
|
|
})
|
|
.catch(() => { /* no-op */ });
|
|
});
|
|
}
|
|
|
|
function getItemInfoFromCard(card) {
|
|
return {
|
|
Type: card.getAttribute('data-type'),
|
|
Id: card.getAttribute('data-id'),
|
|
TimerId: card.getAttribute('data-timerid'),
|
|
CollectionType: card.getAttribute('data-collectiontype'),
|
|
ChannelId: card.getAttribute('data-channelid'),
|
|
SeriesId: card.getAttribute('data-seriesid'),
|
|
ServerId: card.getAttribute('data-serverid'),
|
|
MediaType: card.getAttribute('data-mediatype'),
|
|
Path: card.getAttribute('data-path'),
|
|
IsFolder: card.getAttribute('data-isfolder') === 'true',
|
|
StartDate: card.getAttribute('data-startdate'),
|
|
EndDate: card.getAttribute('data-enddate'),
|
|
UserData: {
|
|
PlaybackPositionTicks: parseInt(card.getAttribute('data-positionticks') || '0', 10)
|
|
}
|
|
};
|
|
}
|
|
|
|
function showPlayMenu(card, target) {
|
|
const item = getItemInfoFromCard(card);
|
|
|
|
import('./playmenu').then((playMenu) => {
|
|
playMenu.show({
|
|
|
|
item: item,
|
|
positionTo: target
|
|
});
|
|
});
|
|
}
|
|
|
|
function executeAction(card, target, action) {
|
|
target = target || card;
|
|
|
|
let id = card.getAttribute('data-id');
|
|
|
|
if (!id) {
|
|
card = dom.parentWithAttribute(card, 'data-id');
|
|
id = card.getAttribute('data-id');
|
|
}
|
|
|
|
const item = getItemInfoFromCard(card);
|
|
|
|
const itemsContainer = dom.parentWithClass(card, 'itemsContainer');
|
|
|
|
const sortParentId = 'items-' + (item.IsFolder ? item.Id : itemsContainer?.getAttribute('data-parentid')) + '-Folder';
|
|
|
|
const serverId = item.ServerId;
|
|
const type = item.Type;
|
|
|
|
const playableItemId = type === 'Program' ? item.ChannelId : item.Id;
|
|
|
|
if (item.MediaType === 'Photo' && action === 'link') {
|
|
action = 'play';
|
|
}
|
|
|
|
if (action === 'link') {
|
|
appRouter.showItem(item, {
|
|
context: card.getAttribute('data-context'),
|
|
parentId: card.getAttribute('data-parentid')
|
|
});
|
|
} else if (action === 'programdialog') {
|
|
showProgramDialog(item);
|
|
} else if (action === 'instantmix') {
|
|
playbackManager.instantMix({
|
|
Id: playableItemId,
|
|
ServerId: serverId
|
|
});
|
|
} else if (action === 'play' || action === 'resume') {
|
|
const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10);
|
|
const sortValues = userSettings.getSortValuesLegacy(sortParentId, 'SortName');
|
|
|
|
if (playbackManager.canPlay(item)) {
|
|
playbackManager.play({
|
|
ids: [playableItemId],
|
|
startPositionTicks: startPositionTicks,
|
|
serverId: serverId,
|
|
queryOptions: {
|
|
SortBy: sortValues.sortBy,
|
|
SortOrder: sortValues.sortOrder
|
|
}
|
|
});
|
|
} else {
|
|
console.warn('Unable to play item', item);
|
|
}
|
|
} else if (action === 'queue') {
|
|
if (playbackManager.isPlaying()) {
|
|
playbackManager.queue({
|
|
ids: [playableItemId],
|
|
serverId: serverId
|
|
});
|
|
toast(globalize.translate('MediaQueued'));
|
|
} else {
|
|
playbackManager.queue({
|
|
ids: [playableItemId],
|
|
serverId: serverId
|
|
});
|
|
}
|
|
} else if (action === 'playallfromhere') {
|
|
playAllFromHere(card, serverId);
|
|
} else if (action === 'queueallfromhere') {
|
|
playAllFromHere(card, serverId, true);
|
|
} else if (action === 'setplaylistindex') {
|
|
playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid'));
|
|
} else if (action === 'record') {
|
|
onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid'));
|
|
} else if (action === 'menu') {
|
|
const options = target.getAttribute('data-playoptions') === 'false' ?
|
|
{
|
|
shuffle: false,
|
|
instantMix: false,
|
|
play: false,
|
|
playAllFromHere: false,
|
|
queue: false,
|
|
queueAllFromHere: false
|
|
} :
|
|
{};
|
|
|
|
options.positionTo = target;
|
|
|
|
showContextMenu(card, options);
|
|
} else if (action === 'playmenu') {
|
|
showPlayMenu(card, target);
|
|
} else if (action === 'edit') {
|
|
getItem(target).then(itemToEdit => {
|
|
editItem(itemToEdit, serverId);
|
|
});
|
|
} else if (action === 'playtrailer') {
|
|
getItem(target).then(playTrailer);
|
|
} else if (action === 'addtoplaylist') {
|
|
getItem(target).then(addToPlaylist);
|
|
} else if (action === 'custom') {
|
|
const customAction = target.getAttribute('data-customaction');
|
|
|
|
card.dispatchEvent(new CustomEvent(`action-${customAction}`, {
|
|
detail: {
|
|
playlistItemId: card.getAttribute('data-playlistitemid')
|
|
},
|
|
cancelable: false,
|
|
bubbles: true
|
|
}));
|
|
}
|
|
}
|
|
|
|
function addToPlaylist(item) {
|
|
import('./playlisteditor/playlisteditor').then(({ default: PlaylistEditor }) => {
|
|
const playlistEditor = new PlaylistEditor();
|
|
playlistEditor.show({
|
|
items: [item.Id],
|
|
serverId: item.ServerId
|
|
}).catch(() => {
|
|
// Dialog closed
|
|
});
|
|
}).catch(err => {
|
|
console.error('[addToPlaylist] failed to load playlist editor', err);
|
|
});
|
|
}
|
|
|
|
function playTrailer(item) {
|
|
const apiClient = ServerConnections.getApiClient(item.ServerId);
|
|
|
|
apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(trailers => {
|
|
playbackManager.play({ items: trailers });
|
|
});
|
|
}
|
|
|
|
function editItem(item, serverId) {
|
|
const apiClient = ServerConnections.getApiClient(serverId);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const currentServerId = apiClient.serverInfo().Id;
|
|
|
|
if (item.Type === 'Timer') {
|
|
if (item.ProgramId) {
|
|
import('./recordingcreator/recordingcreator').then(({ default: recordingCreator }) => {
|
|
recordingCreator.show(item.ProgramId, currentServerId).then(resolve, reject);
|
|
});
|
|
} else {
|
|
import('./recordingcreator/recordingeditor').then(({ default: recordingEditor }) => {
|
|
recordingEditor.show(item.Id, currentServerId).then(resolve, reject);
|
|
});
|
|
}
|
|
} else {
|
|
import('./metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
|
|
metadataEditor.show(item.Id, currentServerId).then(resolve, reject);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function onRecordCommand(serverId, id, type, timerId, seriesTimerId) {
|
|
if (type === 'Program' || timerId || seriesTimerId) {
|
|
const programId = type === 'Program' ? id : null;
|
|
recordingHelper.toggleRecording(serverId, programId, timerId, seriesTimerId);
|
|
}
|
|
}
|
|
|
|
export function onClick(e) {
|
|
const card = dom.parentWithClass(e.target, 'itemAction');
|
|
|
|
if (card) {
|
|
let actionElement = card;
|
|
let action = actionElement.getAttribute('data-action');
|
|
|
|
if (!action) {
|
|
actionElement = dom.parentWithAttribute(actionElement, 'data-action');
|
|
if (actionElement) {
|
|
action = actionElement.getAttribute('data-action');
|
|
}
|
|
}
|
|
|
|
if (action) {
|
|
executeAction(card, actionElement, action);
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function onCommand(e) {
|
|
const cmd = e.detail.command;
|
|
|
|
if (cmd === 'play' || cmd === 'resume' || cmd === 'record' || cmd === 'menu' || cmd === 'info') {
|
|
const target = e.target;
|
|
const card = dom.parentWithClass(target, 'itemAction') || dom.parentWithAttribute(target, 'data-id');
|
|
|
|
if (card) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
executeAction(card, card, cmd);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function on(context, options) {
|
|
options = options || {};
|
|
|
|
if (options.click !== false) {
|
|
context.addEventListener('click', onClick);
|
|
}
|
|
|
|
if (options.command !== false) {
|
|
inputManager.on(context, onCommand);
|
|
}
|
|
}
|
|
|
|
export function off(context, options) {
|
|
options = options || {};
|
|
|
|
context.removeEventListener('click', onClick);
|
|
|
|
if (options.command !== false) {
|
|
inputManager.off(context, onCommand);
|
|
}
|
|
}
|
|
|
|
export function getShortcutAttributesHtml(item, serverId) {
|
|
let html = `data-id="${item.Id}" data-serverid="${serverId || item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-channelid="${item.ChannelId}" data-isfolder="${item.IsFolder}"`;
|
|
|
|
const collectionType = item.CollectionType;
|
|
if (collectionType) {
|
|
html += ` data-collectiontype="${collectionType}"`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
export default {
|
|
on,
|
|
off,
|
|
onClick,
|
|
getShortcutAttributesHtml
|
|
};
|