From 5dcf2c1f441cbb21b5aa550854c255cb462701bf Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Fri, 23 Jan 2026 16:00:18 -0500 Subject: [PATCH] Fix broken Playlist re-ordering by adding "Default" sort option to Playlist sorting --- scripts/playlist.js | 172 +++++++++++++++++++++++------------------ skins/flow-kefin.css | 10 ++- skins/scyfin-kefin.css | 8 ++ 3 files changed, 114 insertions(+), 76 deletions(-) diff --git a/scripts/playlist.js b/scripts/playlist.js index 61641d8..2ebbe04 100644 --- a/scripts/playlist.js +++ b/scripts/playlist.js @@ -22,6 +22,7 @@ // Sort options const SORT_OPTIONS = { + 'default': { label: 'Default', field: 'Default', defaultDirection: 'asc' }, 'sortTitle': { label: 'Sort Title', field: 'SortName', defaultDirection: 'asc' }, 'releaseDate': { label: 'Release Date', field: 'PremiereDate', defaultDirection: 'asc' }, 'dateAdded': { label: 'Date Added', field: 'DateCreated', defaultDirection: 'desc' }, @@ -34,6 +35,7 @@ // Single MutationObserver for the current playlist (only one can exist at a time) let playlistObserver = null; + let ignoreNextMutation = false; /** * Get current sort preference from localStorage @@ -46,7 +48,7 @@ if (savedSort && SORT_OPTIONS[savedSort]) { return savedSort; } - return 'sortTitle'; // Default sort + return 'default'; // Default sort } /** @@ -86,24 +88,12 @@ async function fetchPlaylistChildren(playlistId) { try { const userId = ApiClient.getCurrentUserId(); - - // Get current sort preferences - const sortKey = getCurrentSort(playlistId); - const direction = getCurrentSortDirection(playlistId, sortKey); - - // Map sort key to Jellyfin API SortBy parameter - const sortOption = SORT_OPTIONS[sortKey]; - const sortBy = sortOption ? sortOption.field : 'SortName'; - - // Map direction to Jellyfin API SortOrder parameter - const sortOrder = direction === 'asc' ? 'Ascending' : 'Descending'; - + const response = await ApiClient.getItems(userId, { ParentId: playlistId, - Fields: 'PrimaryImageAspectRatio,DateCreated,CommunityRating,CriticRating,SortName,PremiereDate,UserData', - SortBy: sortBy, - SortOrder: sortOrder + Fields: 'PrimaryImageAspectRatio,DateCreated,CommunityRating,CriticRating,SortName,PremiereDate,UserData' }); + return response.Items || []; } catch (error) { ERR('Error fetching playlist children:', error); @@ -125,6 +115,10 @@ return items; } + if (sortKey === 'default') { + return items; + } + const field = sortOption.field; const sorted = [...items].sort((a, b) => { let valueA = a[field]; @@ -225,26 +219,12 @@ * Sort playlist items and update DOM * @param {string} playlistId - Playlist item ID */ - function sortPlaylistItems(playlistId) { - const data = playlistData.get(playlistId); - if (!data || !data.items || !data.itemsContainer) { - WARN('Playlist data not found for:', playlistId); - return; - } - - const sortKey = getCurrentSort(playlistId); - const direction = getCurrentSortDirection(playlistId, sortKey); - - LOG(`Sorting playlist ${playlistId} by ${sortKey} (${direction})`); - - // Sort items - const sortedItems = sortItems(data.items, sortKey, direction); - - // Update stored items - data.items = sortedItems; + async function sortPlaylistItems(playlistId) { + const sortedItems = await getSortedPlaylistItems(playlistId); // Reorder DOM - reorderListItems(data.itemsContainer, sortedItems); + ignoreNextMutation = true; + reorderListItems(getPlaylistItemsContainer(), sortedItems); // Update play button text and "Play from beginning" button visibility in case first item changed const activePage = document.querySelector('.libraryPage:not(.hide)'); @@ -254,10 +234,10 @@ const playButton = mainDetailButtons.querySelector('.btnPlay') || mainDetailButtons.querySelector('button[data-kt-playlist-sorting-overridden="true"]'); if (playButton) { - updatePlayButtonText(playButton, playlistId); + updatePlayButtonText(playButton, playlistId, sortedItems); } // Update "Play from beginning" button visibility - addResumeButton(mainDetailButtons, playlistId); + addResumeButton(mainDetailButtons, playlistId, sortedItems); } } } @@ -382,19 +362,24 @@ * @returns {Promise>} Array of sorted item IDs */ async function getSortedPlaylistItems(playlistId, resume = false) { - // Check if we have cached data - const data = playlistData.get(playlistId); - let items = data?.items; - - // If no cached data, fetch it (already sorted by server) - if (!items || items.length === 0) { - items = await fetchPlaylistChildren(playlistId); - if (items.length === 0) { - WARN('No items found for playlist:', playlistId); - return []; - } + let items = await fetchPlaylistChildren(playlistId); + if (items.length === 0) { + WARN('No items found for playlist:', playlistId); + return []; } + const sortKey = getCurrentSort(playlistId); + const direction = getCurrentSortDirection(playlistId, sortKey); + + // Add data-sort-key and data-sort-direction attributes to the playlist items container + const itemsContainer = getPlaylistItemsContainer(); + if (itemsContainer) { + itemsContainer.setAttribute('data-sort-key', sortKey); + itemsContainer.setAttribute('data-sort-direction', direction); + } + + items = sortItems(items, sortKey, direction); + // Items are already sorted by the server, no need to sort client-side // If resume mode, filter out played items from the start @@ -429,15 +414,11 @@ * @param {HTMLElement} playButton - The play button element * @param {string} playlistId - Playlist item ID */ - async function updatePlayButtonText(playButton, playlistId) { + async function updatePlayButtonText(playButton, playlistId, sortedPlaylistItems) { try { - // Check if we have cached data - const data = playlistData.get(playlistId); - let items = data?.items; - - // If no cached data, fetch it (already sorted by server) - if (!items || items.length === 0) { - items = await fetchPlaylistChildren(playlistId); + let items = sortedPlaylistItems; + if (!items) { + items = await getSortedPlaylistItems(playlistId); } if (items && items.length > 0) { @@ -631,19 +612,16 @@ * @param {HTMLElement} mainDetailButtons - Main detail buttons container * @param {string} playlistId - Playlist item ID */ - async function addResumeButton(mainDetailButtons, playlistId) { + async function addResumeButton(mainDetailButtons, playlistId, sortedPlaylistItems) { // Check if button already exists - if so, update its visibility const existingButton = mainDetailButtons.querySelector('.kt-playlist-resume-btn'); // Check if first item is played let isFirstItemPlayed = false; try { - const data = playlistData.get(playlistId); - let items = data?.items; - - // If no cached data, fetch it (already sorted by server) - if (!items || items.length === 0) { - items = await fetchPlaylistChildren(playlistId); + let items = sortedPlaylistItems; + if (!items) { + items = await getSortedPlaylistItems(playlistId); } if (items && items.length > 0) { @@ -773,6 +751,29 @@ return null; } + function getPlaylistItemsContainer() { + const activePage = document.querySelector('.libraryPage:not(.hide)'); + if (!activePage) { + return; + } + + let playlistItemsContainer = activePage.querySelector('.childrenItemsContainer'); + if (!playlistItemsContainer) { + playlistItemsContainer = activePage.querySelector('#listChildrenCollapsible'); + if (!playlistItemsContainer) { + WARN('Playlist items container not found'); + return; + } + } + + // Find items container + const itemsContainer = playlistItemsContainer.querySelector('.childrenItemsContainer') || + playlistItemsContainer.querySelector('#listChildrenCollapsible') || + playlistItemsContainer; + + return itemsContainer; + } + /** * Re-apply sorting after container re-render * @param {string} playlistId - Playlist item ID @@ -805,16 +806,7 @@ } } - // Update stored items container reference (in case it was recreated) - const data = playlistData.get(playlistId); - if (data) { - data.itemsContainer = itemsContainer; - - // Re-apply sort - sortPlaylistItems(playlistId); - } else { - WARN('Playlist data not found for re-render:', playlistId); - } + sortPlaylistItems(playlistId); } /** @@ -833,6 +825,17 @@ let debounceTimer = null; playlistObserver = new MutationObserver((mutations) => { + if (ignoreNextMutation) { + ignoreNextMutation = false; + return; + } + + const sortKey = getCurrentSort(playlistId); + + if (sortKey === 'default') { + return; + } + let shouldReapply = false; mutations.forEach((mutation) => { @@ -964,8 +967,6 @@ activePage.dataset.playlistSortingAdded = 'true'; } - // ========== ORIGINAL PLAYLIST FUNCTIONALITY ========== - /** * Modifies playlist item click behavior to navigate to details page */ @@ -1221,6 +1222,25 @@ pollForElements(); } + + function addPlaylistCSS() { + // Check if the CSS is already added + if (document.querySelector('style[data-kefin-playlist-css]')) { + return; + } + + + const css = ` + .itemsContainer:not([data-sort-key="default"]) .listViewDragHandle { + pointer-events: none; + } + `; + + const style = document.createElement('style'); + style.setAttribute('data-kefin-playlist-css', 'true'); + style.textContent = css; + document.head.appendChild(style); + } /** * Initialize playlist hook using utils @@ -1231,7 +1251,9 @@ setTimeout(initializePlaylistHook, 1000); return; } - + + addPlaylistCSS(); + LOG('Registering playlist handler with KefinTweaksUtils'); // Register handler for details pages diff --git a/skins/flow-kefin.css b/skins/flow-kefin.css index 240efad..becbd98 100644 --- a/skins/flow-kefin.css +++ b/skins/flow-kefin.css @@ -11,4 +11,12 @@ body.home-screen .emby-tabs-slider { } body.home-screen .emby-tab-button > div { align-self: normal; -} */ \ No newline at end of file +} */ +.layout-desktop .mainDetailButtons:has(.btnReplay:not(.hide)) .btnPlay::after { + content: 'Resume' !important; +} +.layout-desktop .mainDetailButtons:has(.btnReplay:not(.hide)) .btnPlay { + margin-right: -115px !important; + padding-right: 20px !important; + right: 140px !important; +} \ No newline at end of file diff --git a/skins/scyfin-kefin.css b/skins/scyfin-kefin.css index 4cb2ed1..2108f81 100644 --- a/skins/scyfin-kefin.css +++ b/skins/scyfin-kefin.css @@ -14,4 +14,12 @@ body.home-screen .emby-tab-button > div { } .series-episodes-section { grid-area: nextup; +} +.layout-desktop .mainDetailButtons:has(.btnReplay:not(.hide)) .btnPlay::after { + content: 'Resume' !important; +} +.layout-desktop .mainDetailButtons:has(.btnReplay:not(.hide)) .btnPlay { + margin-right: -115px !important; + padding-right: 20px !important; + right: 140px !important; } \ No newline at end of file