diff --git a/CHANGELOG.md b/CHANGELOG.md index 90396508..8d222c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,11 @@ ## v3.1.8-beta +- The button's tooltip may now show additional information on the shuffle status. +- Fixed a bug where clicking the shuffle button while the shuffle was running would start a second shuffle at the same time. - Fixed a bug where the shuffle button would sometimes not be added to the page if it was opened directly from a new tab. - Fixed a bug where the playlist created by the extension would sometimes not be renamed correctly. +- Fixed an animation bug when ignoring shorts and shuffling a channel with many videos from a shorts page. ## v3.1.7 @@ -299,4 +302,4 @@ ## v0.0.1 -- Initial release. \ No newline at end of file +- Initial release. diff --git a/src/content.js b/src/content.js index fbf605a2..161e24ca 100644 --- a/src/content.js +++ b/src/content.js @@ -1,7 +1,7 @@ // Content script that is injected into YouTube pages import { setDOMTextWithDelay, updateSmallButtonStyleForText, getPageTypeFromURL, RandomYoutubeVideoError, delay } from "./utils.js"; import { configSync, setSyncStorageValue } from "./chromeStorage.js"; -import { buildShuffleButton, shuffleButton, shuffleButtonTextElement, tryRenameUntitledList } from "./domManipulation.js"; +import { buildShuffleButton, shuffleButton, shuffleButtonTextElement, shuffleButtonTooltipElement, tryRenameUntitledList } from "./domManipulation.js"; import { chooseRandomVideo } from "./shuffleVideo.js"; // ---------- Initialization ---------- @@ -22,6 +22,9 @@ if (videoShuffleButton || channelShuffleButton || shortShuffleButton) { window.location.reload(true); } +// To track if the shuffle is already running and prevent bugs if the user clicks the button multiple times +let isShuffling = false; + // After every navigation event, we need to check if this page needs a 'Shuffle' button document.addEventListener("yt-navigate-finish", startDOMObserver); @@ -136,9 +139,11 @@ function resetShuffleButtonText() { if (shuffleButtonTextElement) { if (shuffleButtonTextElement.id.includes("large-shuffle-button")) { shuffleButtonTextElement.innerText = "\xa0Shuffle"; + shuffleButtonTooltipElement.innerText = "Shuffle from this channel"; } else if (shuffleButtonTextElement.innerText !== "autorenew") { updateSmallButtonStyleForText(shuffleButtonTextElement, false); shuffleButtonTextElement.innerText = "shuffle"; + shuffleButtonTooltipElement.innerText = "Shuffle from channel"; } } } @@ -146,6 +151,11 @@ function resetShuffleButtonText() { // ---------- Shuffle ---------- // Called when the 'Shuffle' button is clicked async function shuffleVideos() { + if (isShuffling) { + return; + } + + isShuffling = true; resetShuffleButtonText(); // Shorts pages make a copy of the shuffleButtonTextElement to be able to spin it even if the user scrolls to another short, to keep the animation going @@ -175,6 +185,7 @@ async function shuffleVideos() { // Only use this text if the button is the large shuffle button, the small one only has space for an icon if (shuffleButtonTextElement.id.includes("large-shuffle-button")) { shuffleButtonTextElement.innerText = "\xa0Shuffling..."; + shuffleButtonTooltipElement.innerText = "The shuffle has started, please wait while the extension gets the video data for this channel..."; setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Still on it...", 5000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Shuffling..." || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); }); if (configSync.shuffleIgnoreShortsOption != "1") { setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Sorting shorts...", 10000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Still on it..." || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); }); @@ -188,6 +199,7 @@ async function shuffleVideos() { setDOMTextWithDelay(shuffleButtonTextElement, "\xa0Still shuffling...", 20000, () => { return ((shuffleButtonTextElement.innerText === "\xa0Still on it..." || shuffleButtonTextElement.innerText === "\xa0Fetching: 100%") && !hasBeenShuffled); }); } } else { + shuffleButtonTooltipElement.innerText = "Shuffling..."; let iterationsWaited = 0; let checkInterval = setInterval(async () => { @@ -196,7 +208,7 @@ async function shuffleVideos() { await delay(400); // If we have finished the shuffle between the check and the delay, we don't want to change the text - if (hasBeenShuffled) { + if (hasBeenShuffled || (shuffleButtonTextElementCopy.innerText != "100%" && shuffleButtonTextElementCopy.innerText != "shuffle")) { return; } @@ -205,33 +217,31 @@ async function shuffleVideos() { let rotation = 0; let rotateInterval = setInterval(() => { - if (hasBeenShuffled) { + if (hasBeenShuffled || shuffleButtonTextElementCopy.innerText != "autorenew") { clearInterval(rotateInterval); return; } shuffleButtonTextElementCopy.style.transform = `rotate(${rotation}deg)`; rotation = (rotation + 5) % 360; }, 25); - } else if (hasBeenShuffled) { + } else if (hasBeenShuffled && shuffleButtonTextElementCopy.innerText != "autorenew") { clearInterval(checkInterval); } }, 150); } - await chooseRandomVideo(channelId, false, shuffleButtonTextElement); + await chooseRandomVideo(channelId, false, shuffleButtonTextElement, shuffleButtonTooltipElement); + + isShuffling = false; hasBeenShuffled = true; - // Reset the button text in case we opened the video in a new tab - if (shuffleButtonTextElement.id.includes("large-shuffle-button")) { - shuffleButtonTextElement.innerText = "\xa0Shuffle"; - } else { - updateSmallButtonStyleForText(shuffleButtonTextElementCopy, false); - shuffleButtonTextElementCopy.innerText = "shuffle"; - } + resetShuffleButtonText(); } catch (error) { console.error(error); + isShuffling = false; hasBeenShuffled = true; + if (shuffleButton.id.includes("small-shuffle-button")) { updateSmallButtonStyleForText(shuffleButtonTextElementCopy, true); } diff --git a/src/domManipulation.js b/src/domManipulation.js index 8036ca95..5b90c2ac 100644 --- a/src/domManipulation.js +++ b/src/domManipulation.js @@ -3,6 +3,7 @@ // ----- Public ----- export let shuffleButton; export let shuffleButtonTextElement; +export let shuffleButtonTooltipElement; export function buildShuffleButton(pageType, channelId, eventVersion, clickHandler) { let buttonDivID; @@ -217,7 +218,7 @@ function finalizeButton(pageType, channelId, clickHandler, isLargeButton, button if (isLargeButton) { buttonTooltip.innerText = "Shuffle from this channel"; } else { - buttonTooltip.innerText = "Shuffle channel"; + buttonTooltip.innerText = "Shuffle from channel"; } // Remove the original button tooltip, it does not have all required attributes @@ -261,4 +262,5 @@ function finalizeButton(pageType, channelId, clickHandler, isLargeButton, button } else { shuffleButtonTextElement = shuffleButton.children[0].children[0].children[0].children[0].children[0]; } + shuffleButtonTooltipElement = shuffleButton.children[0].children[1].children[0]; } \ No newline at end of file diff --git a/src/shuffleVideo.js b/src/shuffleVideo.js index 80183577..12f7f1ce 100644 --- a/src/shuffleVideo.js +++ b/src/shuffleVideo.js @@ -15,7 +15,7 @@ let shuffleStartTime = null; // --------------- Public --------------- // Chooses a random video uploaded on the current YouTube channel -export async function chooseRandomVideo(channelId, firedFromPopup, progressTextElement) { +export async function chooseRandomVideo(channelId, firedFromPopup, progressTextElement, shuffleButtonTooltipElement = null) { /* c8 ignore start */ try { // The service worker will get stopped after 30 seconds @@ -78,14 +78,14 @@ export async function chooseRandomVideo(channelId, firedFromPopup, progressTextE } else { console.log("Fetching the uploads playlist for this channel from the YouTube API..."); } - ({ playlistInfo, userQuotaRemainingToday } = await getPlaylistFromAPI(uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement)); + ({ playlistInfo, userQuotaRemainingToday } = await getPlaylistFromAPI(uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement)); shouldUpdateDatabase = true; } else if (databaseSharing && (playlistInfo["lastUpdatedDBAt"] ?? new Date(0).toISOString()) < addHours(new Date(), -48).toISOString()) { // If the playlist exists in the database but is outdated, update it from the API. console.log("Uploads playlist for this channel may be outdated in the database. Updating from the YouTube API..."); - ({ playlistInfo, userQuotaRemainingToday } = await updatePlaylistFromAPI(playlistInfo, uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement)); + ({ playlistInfo, userQuotaRemainingToday } = await updatePlaylistFromAPI(playlistInfo, uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement)); shouldUpdateDatabase = true; } @@ -104,13 +104,13 @@ export async function chooseRandomVideo(channelId, firedFromPopup, progressTextE // With the current functionality and db rules, this shouldn't happen, except if the user has opted out of database sharing. if (isEmpty(playlistInfo)) { console.log(`${databaseSharing ? "Uploads playlist for this channel does not exist in the database. " : "Fetching it from the YouTube API..."}`); - ({ playlistInfo, userQuotaRemainingToday } = await getPlaylistFromAPI(uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement)); + ({ playlistInfo, userQuotaRemainingToday } = await getPlaylistFromAPI(uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement)); shouldUpdateDatabase = true; // If the playlist exists in the database but is outdated there as well, update it from the API. } else if ((playlistInfo["lastUpdatedDBAt"] ?? new Date(0).toISOString()) < addHours(new Date(), -48).toISOString()) { console.log("Uploads playlist for this channel may be outdated in the database. Updating from the YouTube API..."); - ({ playlistInfo, userQuotaRemainingToday } = await updatePlaylistFromAPI(playlistInfo, uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement)); + ({ playlistInfo, userQuotaRemainingToday } = await updatePlaylistFromAPI(playlistInfo, uploadsPlaylistId, null, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement)); shouldUpdateDatabase = true; } @@ -128,7 +128,7 @@ export async function chooseRandomVideo(channelId, firedFromPopup, progressTextE let chosenVideos; var encounteredDeletedVideos; - ({ chosenVideos, playlistInfo, shouldUpdateDatabase, encounteredDeletedVideos } = await chooseRandomVideosFromPlaylist(playlistInfo, channelId, shouldUpdateDatabase, progressTextElement)); + ({ chosenVideos, playlistInfo, shouldUpdateDatabase, encounteredDeletedVideos } = await chooseRandomVideosFromPlaylist(playlistInfo, channelId, shouldUpdateDatabase, progressTextElement, shuffleButtonTooltipElement)); // Save the playlist to the database and locally playlistInfo = await handlePlaylistDatabaseUpload(playlistInfo, uploadsPlaylistId, shouldUpdateDatabase, databaseSharing, encounteredDeletedVideos); @@ -291,7 +291,7 @@ async function uploadPlaylistToDatabase(playlistInfo, videosToDatabase, uploadsP } // ---------- YouTube API ---------- -async function getPlaylistFromAPI(playlistId, useAPIKeyAtIndex, userQuotaRemainingToday, progressTextElement, disregardUserQuota = false) { +async function getPlaylistFromAPI(playlistId, useAPIKeyAtIndex, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement, disregardUserQuota = false) { // Get an API key let { APIKey, isCustomKey, keyIndex } = await getAPIKey(useAPIKeyAtIndex); // We need to keep track of the original key's index, so we know when we have tried all keys @@ -348,7 +348,7 @@ async function getPlaylistFromAPI(playlistId, useAPIKeyAtIndex, userQuotaRemaini // If there are less than 50 videos, we don't need to show a progress percentage if (totalResults > 50) { const percentage = Math.round(resultsFetchedCount / totalResults * 100); - updateProgressTextElement(progressTextElement, `\xa0Fetching: ${percentage}%`, `${percentage}%`); + updateProgressTextElement(progressTextElement, `\xa0Fetching: ${percentage}%`, `${percentage}%`, shuffleButtonTooltipElement, "Fetching videos may take longer if the channel has a lot of uploads or your network speed is slow. Please wait..."); } // For each video, add an entry in the form of videoId: uploadTime @@ -380,7 +380,7 @@ async function getPlaylistFromAPI(playlistId, useAPIKeyAtIndex, userQuotaRemaini } // Get snippets from the API as long as new videos are being found -async function updatePlaylistFromAPI(playlistInfo, playlistId, useAPIKeyAtIndex, userQuotaRemainingToday, progressTextElement) { +async function updatePlaylistFromAPI(playlistInfo, playlistId, useAPIKeyAtIndex, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement) { // Get an API key let { APIKey, isCustomKey, keyIndex } = await getAPIKey(useAPIKeyAtIndex); // We need to keep track of the original key's index, so we know when we have tried all keys @@ -429,7 +429,7 @@ async function updatePlaylistFromAPI(playlistInfo, playlistId, useAPIKeyAtIndex, // If there are less than 50 new videos, we don't need to show a progress percentage if (totalExpectedNewResults > 50) { const percentage = Math.min(Math.round(resultsFetchedCount / totalExpectedNewResults * 100), 100); - updateProgressTextElement(progressTextElement, `\xa0Fetching: ${percentage}%`, `${percentage}%`); + updateProgressTextElement(progressTextElement, `\xa0Fetching: ${percentage}%`, `${percentage}%`, shuffleButtonTooltipElement, "Fetching videos may take longer if the channel has a lot of uploads or your network speed is slow. Please wait..."); } // Update the "last video published at" date (only for the most recent video) @@ -443,7 +443,7 @@ async function updatePlaylistFromAPI(playlistInfo, playlistId, useAPIKeyAtIndex, // Make sure that we are not missing any videos in the database if (totalNumVideosOnChannel > numLocallyKnownVideos) { console.log(`There are less videos saved in the database than are uploaded on the channel (${numLocallyKnownVideos}/${totalNumVideosOnChannel}), so some videos are missing. Refetching all videos...`); - return await getPlaylistFromAPI(playlistId, keyIndex, userQuotaRemainingToday, progressTextElement, true); + return await getPlaylistFromAPI(playlistId, keyIndex, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement, true); } return { playlistInfo, userQuotaRemainingToday }; @@ -490,7 +490,7 @@ async function updatePlaylistFromAPI(playlistInfo, playlistId, useAPIKeyAtIndex, const numVideosInDatabase = numLocallyKnownVideos + getLength(playlistInfo["newVideos"]); if (totalNumVideosOnChannel > numVideosInDatabase) { console.log(`There are less videos saved in the database than are uploaded on the channel (${numVideosInDatabase}/${totalNumVideosOnChannel}), so some videos are missing. Refetching all videos...`); - return await getPlaylistFromAPI(playlistId, keyIndex, userQuotaRemainingToday, progressTextElement, true); + return await getPlaylistFromAPI(playlistId, keyIndex, userQuotaRemainingToday, progressTextElement, shuffleButtonTooltipElement, true); } return { playlistInfo, userQuotaRemainingToday }; @@ -706,7 +706,7 @@ async function getAPIKey(useAPIKeyAtIndex = null) { return { APIKey, isCustomKey, keyIndex }; } -async function chooseRandomVideosFromPlaylist(playlistInfo, channelId, shouldUpdateDatabase, progressTextElement) { +async function chooseRandomVideosFromPlaylist(playlistInfo, channelId, shouldUpdateDatabase, progressTextElement, shuffleButtonTooltipElement) { let activeShuffleFilterOption = configSync.channelSettings[channelId]?.activeOption ?? "allVideosOption"; let activeOptionValue; @@ -903,7 +903,7 @@ async function chooseRandomVideosFromPlaylist(playlistInfo, channelId, shouldUpd if (new Date() - shuffleStartTime > 1000) { // We display either the percentage of videos processed or the percentage of videos chosen (vs. needed), whichever is higher const percentage = Math.max(Math.round(chosenVideos.length / numVideosToChoose * 100), Math.round(numVideosProcessed / initialTotalNumVideos * 100)); - updateProgressTextElement(progressTextElement, `\xa0Sorting: ${percentage}%`, `${percentage}%`); + updateProgressTextElement(progressTextElement, `\xa0Sorting: ${percentage}%`, `${percentage}%`, shuffleButtonTooltipElement, "The extension is currently separating shorts and videos. Please wait...", "Sorting shorts..."); } } else { // We are not ignoring shorts and the video exists @@ -1145,18 +1145,27 @@ function validatePlaylistInfo(playlistInfo) { } /* c8 ignore stop */ -function updateProgressTextElement(progressTextElement, largeButtonText, smallButtonText) { +function updateProgressTextElement(progressTextElement, largeButtonText, smallButtonText, shuffleButtonTooltipElement = null, tooltipText = null, smallButtonTooltipText = null) { if (progressTextElement.id.includes("large-shuffle-button") || progressTextElement.id == "fetchPercentageNoticeShufflingPage") { progressTextElement.innerText = largeButtonText; } else { - // Make it the icon style if an icon is set, otherwise the text style - if (!["shuffle", "close"].includes(smallButtonText)) { - updateSmallButtonStyleForText(progressTextElement, true); - } else { + // Make it the text style if no icon is set, otherwise the icon style + if (["shuffle", "close"].includes(smallButtonText)) { updateSmallButtonStyleForText(progressTextElement, false); + } else { + updateSmallButtonStyleForText(progressTextElement, true); } progressTextElement.innerText = smallButtonText; } + + // Update the tooltip if requested + if (shuffleButtonTooltipElement) { + if (progressTextElement.id.includes("large-shuffle-button")) { + shuffleButtonTooltipElement.innerText = tooltipText; + } else if (smallButtonTooltipText) { + shuffleButtonTooltipElement.innerText = smallButtonTooltipText; + } + } } // ---------- Local storage ----------