mirror of
https://github.com/Anarios/return-youtube-dislike.git
synced 2026-09-12 10:22:10 +02:00
d62130b898
* add jest and rewire for testing * wrap loose code for initMutationObserver() in function and call it instead of just executing it Otherwise this code is executed everytime state.js is imported somewhere in a file. This leads to not being able to test properly, since state.js is imported in a lot of the src files and fails when running the tests since we are missing global environment settings such as document, window etc in the jest environment. * export roundDown and getNumberformatter to make them testable as well * add basic acceptance tests for utils.js, create snapshots and add jest config * add failing test for buggy behaviour in a separate test case and skip it until fixed * Fixes after merge conflicts --------- Co-authored-by: Dmitrii Selivanov <selivano.d@gmail.com>
402 lines
12 KiB
JavaScript
402 lines
12 KiB
JavaScript
import { getLikeButton, getDislikeButton, getButtons, getLikeTextContainer, getDislikeTextContainer } from "./buttons";
|
|
import { createRateBar } from "./bar";
|
|
import {
|
|
getBrowser,
|
|
getVideoId,
|
|
cLog,
|
|
numberFormat,
|
|
getColorFromTheme,
|
|
querySelector,
|
|
localize,
|
|
createObserver,
|
|
} from "./utils";
|
|
|
|
//TODO: Do not duplicate here and in ryd.background.js
|
|
const apiUrl = "https://returnyoutubedislikeapi.com";
|
|
const LIKED_STATE = "LIKED_STATE";
|
|
const DISLIKED_STATE = "DISLIKED_STATE";
|
|
const NEUTRAL_STATE = "NEUTRAL_STATE";
|
|
|
|
let extConfig = {
|
|
disableVoteSubmission: false,
|
|
disableLogging: false,
|
|
coloredThumbs: false,
|
|
coloredBar: false,
|
|
colorTheme: "classic",
|
|
numberDisplayFormat: "compactShort",
|
|
showTooltipPercentage: false,
|
|
tooltipPercentageMode: "dash_like",
|
|
numberDisplayReformatLikes: false,
|
|
selectors: {
|
|
dislikeTextContainer: [],
|
|
likeTextContainer: [],
|
|
buttons: {
|
|
shorts: {
|
|
mobile: [],
|
|
desktop: [],
|
|
},
|
|
regular: {
|
|
mobile: [],
|
|
desktopMenu: [],
|
|
desktopNoMenu: [],
|
|
},
|
|
likeButton: {
|
|
segmented: [],
|
|
segmentedGetButtons: [],
|
|
notSegmented: [],
|
|
},
|
|
dislikeButton: {
|
|
segmented: [],
|
|
segmentedGetButtons: [],
|
|
notSegmented: [],
|
|
},
|
|
},
|
|
menuContainer: [],
|
|
roundedDesign: [],
|
|
},
|
|
};
|
|
|
|
let storedData = {
|
|
likes: 0,
|
|
dislikes: 0,
|
|
previousState: NEUTRAL_STATE,
|
|
};
|
|
|
|
function isMobile() {
|
|
return location.hostname == "m.youtube.com";
|
|
}
|
|
|
|
function isShorts() {
|
|
return location.pathname.startsWith("/shorts");
|
|
}
|
|
|
|
function isNewDesign() {
|
|
return document.getElementById("comment-teaser") !== null;
|
|
}
|
|
|
|
function isRoundedDesign() {
|
|
return querySelector(extConfig.selectors.roundedDesign) !== null;
|
|
}
|
|
|
|
let shortsObserver = null;
|
|
|
|
if (isShorts() && !shortsObserver) {
|
|
cLog("Initializing shorts mutation observer");
|
|
shortsObserver = createObserver(
|
|
{
|
|
attributes: true,
|
|
},
|
|
(mutationList) => {
|
|
mutationList.forEach((mutation) => {
|
|
if (
|
|
mutation.type === "attributes" &&
|
|
mutation.target.nodeName === "TP-YT-PAPER-BUTTON" &&
|
|
mutation.target.id === "button"
|
|
) {
|
|
// cLog('Short thumb button status changed');
|
|
if (mutation.target.getAttribute("aria-pressed") === "true") {
|
|
mutation.target.style.color =
|
|
mutation.target.parentElement.parentElement.id === "like-button"
|
|
? getColorFromTheme(true)
|
|
: getColorFromTheme(false);
|
|
} else {
|
|
mutation.target.style.color = "unset";
|
|
}
|
|
return;
|
|
}
|
|
cLog("Unexpected mutation observer event: " + mutation.target + mutation.type);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
function isLikesDisabled() {
|
|
// return true if the like button's text doesn't contain any number
|
|
if (isMobile()) {
|
|
return /^\D*$/.test(getButtons().children[0].querySelector(".button-renderer-text").innerText);
|
|
}
|
|
return /^\D*$/.test(getLikeTextContainer().innerText);
|
|
}
|
|
|
|
function isVideoLiked() {
|
|
if (isMobile()) {
|
|
return getLikeButton().querySelector("button").getAttribute("aria-label") === "true";
|
|
}
|
|
return (
|
|
getLikeButton().classList.contains("style-default-active") ||
|
|
getLikeButton().querySelector("button")?.getAttribute("aria-pressed") === "true"
|
|
);
|
|
}
|
|
|
|
function isVideoDisliked() {
|
|
if (isMobile()) {
|
|
return getDislikeButton().querySelector("button").getAttribute("aria-label") === "true";
|
|
}
|
|
return (
|
|
getDislikeButton().classList.contains("style-default-active") ||
|
|
getDislikeButton().querySelector("button")?.getAttribute("aria-pressed") === "true"
|
|
);
|
|
}
|
|
|
|
function getState(storedData) {
|
|
if (isVideoLiked()) {
|
|
return { current: LIKED_STATE, previous: storedData.previousState };
|
|
}
|
|
if (isVideoDisliked()) {
|
|
return { current: DISLIKED_STATE, previous: storedData.previousState };
|
|
}
|
|
return { current: NEUTRAL_STATE, previous: storedData.previousState };
|
|
}
|
|
|
|
//--- Sets The Likes And Dislikes Values ---//
|
|
function setLikes(likesCount) {
|
|
cLog(`SET likes ${likesCount}`);
|
|
getLikeTextContainer().innerText = likesCount;
|
|
}
|
|
|
|
function setDislikes(dislikesCount) {
|
|
cLog(`SET dislikes ${dislikesCount}`);
|
|
getDislikeTextContainer()?.removeAttribute("is-empty");
|
|
if (!isLikesDisabled()) {
|
|
if (isMobile()) {
|
|
getButtons().children[1].querySelector(".button-renderer-text").innerText = dislikesCount;
|
|
return;
|
|
}
|
|
getDislikeTextContainer().innerText = dislikesCount;
|
|
} else {
|
|
cLog("likes count disabled by creator");
|
|
if (isMobile()) {
|
|
getButtons().children[1].querySelector(".button-renderer-text").innerText = localize("TextLikesDisabled");
|
|
return;
|
|
}
|
|
getDislikeTextContainer().innerText = localize("TextLikesDisabled");
|
|
}
|
|
}
|
|
|
|
function getLikeCountFromButton() {
|
|
try {
|
|
if (isShorts()) {
|
|
//Youtube Shorts don't work with this query. It's not necessary; we can skip it and still see the results.
|
|
//It should be possible to fix this function, but it's not critical to showing the dislike count.
|
|
return false;
|
|
}
|
|
|
|
let likeButton =
|
|
getLikeButton().querySelector("yt-formatted-string#text") ?? getLikeButton().querySelector("button");
|
|
|
|
let likesStr = likeButton.getAttribute("aria-label").replace(/\D/g, "");
|
|
return likesStr.length > 0 ? parseInt(likesStr) : false;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function processResponse(response, storedData) {
|
|
const formattedDislike = numberFormat(response.dislikes);
|
|
setDislikes(formattedDislike);
|
|
if (extConfig.numberDisplayReformatLikes === true) {
|
|
const nativeLikes = getLikeCountFromButton();
|
|
if (nativeLikes !== false) {
|
|
setLikes(numberFormat(nativeLikes));
|
|
}
|
|
}
|
|
storedData.dislikes = parseInt(response.dislikes);
|
|
storedData.likes = getLikeCountFromButton() || parseInt(response.likes);
|
|
createRateBar(storedData.likes, storedData.dislikes);
|
|
if (extConfig.coloredThumbs === true) {
|
|
if (isShorts()) {
|
|
// for shorts, leave deactivated buttons in default color
|
|
let shortLikeButton = getLikeButton().querySelector("tp-yt-paper-button#button");
|
|
let shortDislikeButton = getDislikeButton().querySelector("tp-yt-paper-button#button");
|
|
if (shortLikeButton.getAttribute("aria-pressed") === "true") {
|
|
shortLikeButton.style.color = getColorFromTheme(true);
|
|
}
|
|
if (shortDislikeButton.getAttribute("aria-pressed") === "true") {
|
|
shortDislikeButton.style.color = getColorFromTheme(false);
|
|
}
|
|
shortsObserver.observe(shortLikeButton);
|
|
shortsObserver.observe(shortDislikeButton);
|
|
} else {
|
|
getLikeButton().style.color = getColorFromTheme(true);
|
|
getDislikeButton().style.color = getColorFromTheme(false);
|
|
}
|
|
}
|
|
//Temporary disabling this - it breaks all places where getButtons()[1] is used
|
|
// createStarRating(response.rating, isMobile());
|
|
}
|
|
|
|
// Tells the user if the API is down
|
|
function displayError(error) {
|
|
getDislikeTextContainer().innerText = localize("textTempUnavailable");
|
|
}
|
|
|
|
async function setState(storedData) {
|
|
storedData.previousState = isVideoDisliked() ? DISLIKED_STATE : isVideoLiked() ? LIKED_STATE : NEUTRAL_STATE;
|
|
let statsSet = false;
|
|
cLog("Video is loaded. Adding buttons...");
|
|
|
|
let videoId = getVideoId(window.location.href);
|
|
let likeCount = getLikeCountFromButton() || null;
|
|
|
|
let response = await fetch(`${apiUrl}/votes?videoId=${videoId}&likeCount=${likeCount || ""}`, {
|
|
method: "GET",
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
})
|
|
.then((response) => {
|
|
if (!response.ok) displayError(response.error);
|
|
return response;
|
|
})
|
|
.then((response) => response.json())
|
|
.catch(displayError);
|
|
cLog("response from api:");
|
|
cLog(JSON.stringify(response));
|
|
if (response !== undefined && !("traceId" in response) && !statsSet) {
|
|
processResponse(response, storedData);
|
|
}
|
|
}
|
|
|
|
async function setInitialState() {
|
|
await setState(storedData);
|
|
}
|
|
|
|
async function initExtConfig() {
|
|
initializeDisableVoteSubmission();
|
|
initializeDisableLogging();
|
|
initializeColoredThumbs();
|
|
initializeColoredBar();
|
|
initializeColorTheme();
|
|
initializeNumberDisplayFormat();
|
|
initializeTooltipPercentage();
|
|
initializeTooltipPercentageMode();
|
|
initializeNumberDisplayReformatLikes();
|
|
await initializeSelectors();
|
|
}
|
|
|
|
async function initializeSelectors() {
|
|
console.log("initializing selectors");
|
|
let result = await fetch(`${apiUrl}/configs/selectors`, {
|
|
method: "GET",
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
})
|
|
.then((response) => response.json())
|
|
.catch((error) => {});
|
|
extConfig.selectors = result ?? extConfig.selectors;
|
|
console.log(result);
|
|
}
|
|
|
|
function initializeDisableVoteSubmission() {
|
|
getBrowser().storage.sync.get(["disableVoteSubmission"], (res) => {
|
|
if (res.disableVoteSubmission === undefined) {
|
|
getBrowser().storage.sync.set({ disableVoteSubmission: false });
|
|
} else {
|
|
extConfig.disableVoteSubmission = res.disableVoteSubmission;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeDisableLogging() {
|
|
getBrowser().storage.sync.get(["disableLogging"], (res) => {
|
|
if (res.disableLogging === undefined) {
|
|
getBrowser().storage.sync.set({ disableLogging: true });
|
|
} else {
|
|
extConfig.disableLogging = res.disableLogging;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeColoredThumbs() {
|
|
getBrowser().storage.sync.get(["coloredThumbs"], (res) => {
|
|
if (res.coloredThumbs === undefined) {
|
|
getBrowser().storage.sync.set({ coloredThumbs: false });
|
|
} else {
|
|
extConfig.coloredThumbs = res.coloredThumbs;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeColoredBar() {
|
|
getBrowser().storage.sync.get(["coloredBar"], (res) => {
|
|
if (res.coloredBar === undefined) {
|
|
getBrowser().storage.sync.set({ coloredBar: false });
|
|
} else {
|
|
extConfig.coloredBar = res.coloredBar;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeColorTheme() {
|
|
getBrowser().storage.sync.get(["colorTheme"], (res) => {
|
|
if (res.colorTheme === undefined) {
|
|
getBrowser().storage.sync.set({ colorTheme: false });
|
|
} else {
|
|
extConfig.colorTheme = res.colorTheme;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeNumberDisplayFormat() {
|
|
getBrowser().storage.sync.get(["numberDisplayFormat"], (res) => {
|
|
if (res.numberDisplayFormat === undefined) {
|
|
getBrowser().storage.sync.set({ numberDisplayFormat: "compactShort" });
|
|
} else {
|
|
extConfig.numberDisplayFormat = res.numberDisplayFormat;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeTooltipPercentage() {
|
|
getBrowser().storage.sync.get(["showTooltipPercentage"], (res) => {
|
|
if (res.showTooltipPercentage === undefined) {
|
|
getBrowser().storage.sync.set({ showTooltipPercentage: false });
|
|
} else {
|
|
extConfig.showTooltipPercentage = res.showTooltipPercentage;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeTooltipPercentageMode() {
|
|
getBrowser().storage.sync.get(["tooltipPercentageMode"], (res) => {
|
|
if (res.tooltipPercentageMode === undefined) {
|
|
getBrowser().storage.sync.set({ tooltipPercentageMode: "dash_like" });
|
|
} else {
|
|
extConfig.tooltipPercentageMode = res.tooltipPercentageMode;
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeNumberDisplayReformatLikes() {
|
|
getBrowser().storage.sync.get(["numberDisplayReformatLikes"], (res) => {
|
|
if (res.numberDisplayReformatLikes === undefined) {
|
|
getBrowser().storage.sync.set({ numberDisplayReformatLikes: false });
|
|
} else {
|
|
extConfig.numberDisplayReformatLikes = res.numberDisplayReformatLikes;
|
|
}
|
|
});
|
|
}
|
|
|
|
export {
|
|
isMobile,
|
|
isShorts,
|
|
isVideoDisliked,
|
|
isVideoLiked,
|
|
isNewDesign,
|
|
isRoundedDesign,
|
|
getState,
|
|
setState,
|
|
setInitialState,
|
|
setLikes,
|
|
setDislikes,
|
|
getLikeCountFromButton,
|
|
LIKED_STATE,
|
|
DISLIKED_STATE,
|
|
NEUTRAL_STATE,
|
|
extConfig,
|
|
initExtConfig,
|
|
storedData,
|
|
isLikesDisabled,
|
|
};
|