mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
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.PlaylistItemCount = 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
|
|
};
|