Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change the button tooltip during the shuffle #312

Merged
merged 8 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
## v3.1.8-beta

<!--Releasenotes start-->
- 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.
<!--Releasenotes end-->

## v3.1.7
Expand Down Expand Up @@ -299,4 +302,4 @@

## v0.0.1

- Initial release.
- Initial release.
34 changes: 22 additions & 12 deletions src/content.js
Original file line number Diff line number Diff line change
@@ -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 ----------
Expand All @@ -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);

Expand Down Expand Up @@ -136,16 +139,23 @@ 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";
}
}
}

// ---------- 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
Expand Down Expand Up @@ -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); });
Expand All @@ -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 () => {
Expand All @@ -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;
}

Expand All @@ -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);
}
Expand Down
4 changes: 3 additions & 1 deletion src/domManipulation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// ----- Public -----
export let shuffleButton;
export let shuffleButtonTextElement;
export let shuffleButtonTooltipElement;

export function buildShuffleButton(pageType, channelId, eventVersion, clickHandler) {
let buttonDivID;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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];
}
47 changes: 28 additions & 19 deletions src/shuffleVideo.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 };
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ----------
Expand Down
Loading