Override play button on Playlist cards to use the Resume Playlist functionality

This commit is contained in:
HighImKevin
2026-09-19 02:04:21 -04:00
parent 70cb7d332c
commit 8a5d3297c3
+142 -51
View File
@@ -451,6 +451,42 @@
}
}
/**
* Play playlist items in sort order, optionally resuming from first unplayed.
* @param {string} playlistId
* @param {{ resume?: boolean }} [options]
*/
async function playPlaylistItems(playlistId, options = {}) {
const resume = options.resume === true;
const sortedItems = await getSortedPlaylistItems(playlistId, resume);
const sortedIds = sortedItems.map(item => item.Id);
if (sortedIds.length === 0) {
WARN(resume ? 'No items to resume for playlist:' : 'No items to play for playlist:', playlistId);
return;
}
LOG(
resume
? `Resuming ${sortedIds.length} sorted items from playlist:`
: `Playing ${sortedIds.length} items from playlist:`,
playlistId
);
const playOptions = {};
if (resume && sortedItems[0].UserData?.PlaybackPositionTicks > 0) {
playOptions.startPositionTicks = sortedItems[0].UserData.PlaybackPositionTicks;
}
if (window.apiHelper && window.apiHelper.playItem) {
await window.apiHelper.playItem(sortedIds, playOptions);
} else if (window.ApiClient && typeof window.ApiClient.play === 'function') {
await window.ApiClient.play({ ids: sortedIds, ...playOptions });
} else {
ERR('No play method available');
}
}
/**
* Override play button to use sorted playlist items
* @param {HTMLElement} mainDetailButtons - Main detail buttons container
@@ -514,33 +550,10 @@
clonedButton.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation(); // Prevent any other handlers
e.stopImmediatePropagation();
try {
// Get sorted item IDs with resume mode (skip played items from start)
const sortedItems = await getSortedPlaylistItems(playlistId, true);
const sortedIds = sortedItems.map(item => item.Id);
if (sortedIds.length === 0) {
WARN('No items to resume for playlist:', playlistId);
return;
}
LOG(`Resuming ${sortedIds.length} sorted items from playlist:`, playlistId);
let options = {};
// Check if the first item has playback progress
if (sortedItems[0].UserData?.PlaybackPositionTicks > 0) {
// Add the playback position to the options
options.startPositionTicks = sortedItems[0].UserData?.PlaybackPositionTicks;
}
// Use apiHelper if available, otherwise fallback to ApiClient
if (window.apiHelper && window.apiHelper.playItem) {
await window.apiHelper.playItem(sortedIds, options);
} else {
ERR('No play method available');
}
await playPlaylistItems(playlistId, { resume: true });
} catch (error) {
ERR('Error resuming sorted playlist:', error);
}
@@ -693,30 +706,7 @@
e.stopImmediatePropagation();
try {
// Get sorted item IDs without resume mode (play all items from beginning)
const sortedItems = await getSortedPlaylistItems(playlistId, false);
const sortedIds = sortedItems.map(item => item.Id);
if (sortedIds.length === 0) {
WARN('No items to play for playlist:', playlistId);
return;
}
LOG(`Playing from beginning ${sortedIds.length} items from playlist:`, playlistId);
// Use apiHelper if available, otherwise fallback to ApiClient
if (window.apiHelper && window.apiHelper.playItem) {
await window.apiHelper.playItem(sortedIds);
} else if (window.ApiClient && typeof window.ApiClient.play === 'function') {
await window.ApiClient.play({ ids: sortedIds });
} else if (window.PlaybackManager && window.PlaybackManager.play) {
await window.PlaybackManager.play({
ids: sortedIds,
serverId: window.ApiClient.serverId()
});
} else {
ERR('No play method available');
}
await playPlaylistItems(playlistId, { resume: false });
} catch (error) {
ERR('Error playing from beginning:', error);
}
@@ -1182,6 +1172,104 @@
document.head.appendChild(style);
}
/**
* Override overlay Play on Playlist cards so play resumes mid-playlist.
* @param {HTMLElement} overlayContainer
*/
function overridePlaylistCardPlayButton(overlayContainer) {
if (!overlayContainer) return;
const card = overlayContainer.closest('.card');
if (!card || card.getAttribute('data-type') !== 'Playlist') return;
const playlistId = card.getAttribute('data-id');
if (!playlistId) {
WARN('Playlist card missing data-id');
return;
}
let playButton = overlayContainer.querySelector('.cardOverlayFab-primary') ||
overlayContainer.querySelector('button[data-action="resume"]') ||
overlayContainer.querySelector('button[data-action="play"]');
if (!playButton) {
const buttons = overlayContainer.querySelectorAll('button');
playButton = Array.from(buttons).find(btn => {
const icon = btn.querySelector('.material-icons');
if (!icon) return false;
const iconText = icon.textContent.trim();
return iconText === 'play_arrow' || iconText === 'play';
});
}
if (!playButton) return;
if (playButton.dataset.ktPlaylistCardOverridden === 'true') return;
LOG('Overriding card overlay play for playlist:', playlistId);
const clonedButton = playButton.cloneNode(true);
clonedButton.className = playButton.className;
clonedButton.id = playButton.id;
clonedButton.title = 'Resume';
clonedButton.setAttribute('is', playButton.getAttribute('is') || '');
clonedButton.type = playButton.type || 'button';
clonedButton.removeAttribute('data-action');
clonedButton.removeAttribute('onclick');
clonedButton.onclick = null;
playButton.parentNode.replaceChild(clonedButton, playButton);
clonedButton.onclick = async (e) => {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
try {
await playPlaylistItems(playlistId, { resume: true });
} catch (error) {
ERR('Error resuming playlist from card:', error);
}
};
clonedButton.dataset.ktPlaylistCardOverridden = 'true';
}
function processExistingPlaylistCardOverlays() {
document.querySelectorAll('.cardOverlayContainer').forEach((overlayContainer) => {
overridePlaylistCardPlayButton(overlayContainer);
});
}
function setupPlaylistCardPlayObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type !== 'childList') return;
mutation.addedNodes.forEach((node) => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.classList && node.classList.contains('cardOverlayContainer')) {
overridePlaylistCardPlayButton(node);
}
const overlayContainers = node.querySelectorAll && node.querySelectorAll('.cardOverlayContainer');
if (overlayContainers && overlayContainers.length > 0) {
overlayContainers.forEach((overlayContainer) => {
overridePlaylistCardPlayButton(overlayContainer);
});
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
processExistingPlaylistCardOverlays();
}
/**
* Initialize playlist hook using utils
*/
@@ -1195,6 +1283,8 @@
addPlaylistCSS();
LOG('Registering playlist handler with KefinTweaksUtils');
setupPlaylistCardPlayObserver();
// Register handler for details pages
window.KefinTweaksUtils.onViewPage(async (view, element, hash, itemPromise) => {
@@ -1204,14 +1294,15 @@
if (item && item.Type === 'Playlist') {
// Run both original functionality and sorting functionality
modifyPlaylistPage();
await addPlaylistSorting(item);
// Small delay to ensure details DOM is ready for sorting
setTimeout(async () => {
/* setTimeout(async () => {
if (item && item.Id) {
await addPlaylistSorting(item);
}
}, 100);
return;
return; */
}
}, {
pages: ['details']