diff --git a/youtube extension b/youtube extension new file mode 100644 index 0000000000..b61eb44726 --- /dev/null +++ b/youtube extension @@ -0,0 +1,748 @@ + +// @name Return YouTube Dislike +// @namespace https://www.returnyoutubedislike.com/ +// @homepage https://www.returnyoutubedislike.com/ +// @version 3.1.4 +// @encoding utf-8 +// @description Return of the YouTube Dislike, Based off https://www.returnyoutubedislike.com/ +// @icon https://github.com/Anarios/return-youtube-dislike/raw/main/Icons/Return%20Youtube%20Dislike%20-%20Transparent.png +// @author Anarios & JRWR +// @match *://*.youtube.com/* +// @exclude *://music.youtube.com/* +// @exclude *://*.music.youtube.com/* +// @compatible chrome +// @compatible firefox +// @compatible opera +// @compatible safari +// @compatible edge +// @downloadURL https://github.com/Anarios/return-youtube-dislike/raw/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js +// @updateURL https://github.com/Anarios/return-youtube-dislike/raw/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js +// @grant GM.xmlHttpRequest +// @connect youtube.com +// @grant GM_addStyle +// @run-at document-end +// ==/UserScript== + +const extConfig = { + // BEGIN USER OPTIONS + // You may change the following variables to allowed values listed in the corresponding brackets (* means default). Keep the style and keywords intact. + showUpdatePopup: false, // [true, false*] Show a popup tab after extension update (See what's new) + disableVoteSubmission: false, // [true, false*] Disable like/dislike submission (Stops counting your likes and dislikes) + coloredThumbs: false, // [true, false*] Colorize thumbs (Use custom colors for thumb icons) + coloredBar: false, // [true, false*] Colorize ratio bar (Use custom colors for ratio bar) + colorTheme: "classic", // [classic*, accessible, neon] Color theme (red/green, blue/yellow, pink/cyan) + numberDisplayFormat: "compactShort", // [compactShort*, compactLong, standard] Number format (For non-English locale users, you may be able to improve appearance with a different option. Please file a feature request if your locale is not covered) + numberDisplayRoundDown: true, // [true*, false] Round down numbers (Show rounded down numbers) + tooltipPercentageMode: "none", // [none*, dash_like, dash_dislike, both, only_like, only_dislike] Mode of showing percentage in like/dislike bar tooltip. + numberDisplayReformatLikes: false, // [true, false*] Re-format like numbers (Make likes and dislikes format consistent) + rateBarEnabled: false, // [true, false*] Enables ratio bar under like/dislike buttons + // END USER OPTIONS +}; + +const LIKED_STATE = "LIKED_STATE"; +const DISLIKED_STATE = "DISLIKED_STATE"; +const NEUTRAL_STATE = "NEUTRAL_STATE"; +let previousState = 3; //1=LIKED, 2=DISLIKED, 3=NEUTRAL +let likesvalue = 0; +let dislikesvalue = 0; +let preNavigateLikeButton = null; + +let isMobile = location.hostname == "m.youtube.com"; +let isShorts = () => location.pathname.startsWith("/shorts"); +let mobileDislikes = 0; +function cLog(text, subtext = "") { + subtext = subtext.trim() === "" ? "" : `(${subtext})`; + console.log(`[Return YouTube Dislikes] ${text} ${subtext}`); +} + +function isInViewport(element) { + const rect = element.getBoundingClientRect(); + const height = innerHeight || document.documentElement.clientHeight; + const width = innerWidth || document.documentElement.clientWidth; + return ( + // When short (channel) is ignored, the element (like/dislike AND short itself) is + // hidden with a 0 DOMRect. In this case, consider it outside of Viewport + !(rect.top == 0 && rect.left == 0 && rect.bottom == 0 && rect.right == 0) && + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= height && + rect.right <= width + ); +} + +function getButtons() { + if (isShorts()) { + let elements = document.querySelectorAll( + isMobile + ? "ytm-like-button-renderer" + : "#like-button > ytd-like-button-renderer", + ); + for (let element of elements) { + if (isInViewport(element)) { + return element; + } + } + } + if (isMobile) { + return ( + document.querySelector( + ".slim-video-action-bar-actions .segmented-buttons", + ) ?? document.querySelector(".slim-video-action-bar-actions") + ); + } + if (document.getElementById("menu-container")?.offsetParent === null) { + return ( + document.querySelector("ytd-menu-renderer.ytd-watch-metadata > div") ?? + document.querySelector( + "ytd-menu-renderer.ytd-video-primary-info-renderer > div", + ) + ); + } else { + return document + .getElementById("menu-container") + ?.querySelector("#top-level-buttons-computed"); + } +} + +function getDislikeButton() { + if ( + getButtons().children[0].tagName === + "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER" + ) { + if (getButtons().children[0].children[1] === undefined) { + return document.querySelector("#segmented-dislike-button"); + } else { + return getButtons().children[0].children[1]; + } + } else { + if ( + getButtons().querySelector("segmented-like-dislike-button-view-model") + ) { + const dislikeViewModel = getButtons().querySelector( + "dislike-button-view-model", + ); + if (!dislikeViewModel) cLog("Dislike button wasn't added to DOM yet..."); + return dislikeViewModel; + } else { + return getButtons().children[1]; + } + } +} + +function getLikeButton() { + return getButtons().children[0].tagName === + "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER" + ? document.querySelector("#segmented-like-button") !== null + ? document.querySelector("#segmented-like-button") + : getButtons().children[0].children[0] + : getButtons().querySelector("like-button-view-model") ?? + getButtons().children[0]; +} + +function getLikeTextContainer() { + return ( + getLikeButton().querySelector("#text") ?? + getLikeButton().getElementsByTagName("yt-formatted-string")[0] ?? + getLikeButton().querySelector("span[role='text']") + ); +} + +function getDislikeTextContainer() { + const dislikeButton = getDislikeButton(); + let result = + dislikeButton?.querySelector("#text") ?? + dislikeButton?.getElementsByTagName("yt-formatted-string")[0] ?? + dislikeButton?.querySelector("span[role='text']"); + if (result === null) { + let textSpan = document.createElement("span"); + textSpan.id = "text"; + textSpan.style.marginLeft = "6px"; + dislikeButton?.querySelector("button").appendChild(textSpan); + if (dislikeButton) + dislikeButton.querySelector("button").style.width = "auto"; + result = textSpan; + } + return result; +} + +function createObserver(options, callback) { + const observerWrapper = new Object(); + observerWrapper.options = options; + observerWrapper.observer = new MutationObserver(callback); + observerWrapper.observe = function (element) { + this.observer.observe(element, this.options); + }; + observerWrapper.disconnect = function () { + this.observer.disconnect(); + }; + return observerWrapper; +} + +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 isVideoLiked() { + if (isMobile) { + return ( + getLikeButton().querySelector("button").getAttribute("aria-label") == + "true" + ); + } + return getLikeButton().classList.contains("style-default-active"); +} + +function isVideoDisliked() { + if (isMobile) { + return ( + getDislikeButton()?.querySelector("button").getAttribute("aria-label") == + "true" + ); + } + return getDislikeButton()?.classList.contains("style-default-active"); +} + +function isVideoNotLiked() { + if (isMobile) { + return !isVideoLiked(); + } + return getLikeButton().classList.contains("style-text"); +} + +function isVideoNotDisliked() { + if (isMobile) { + return !isVideoDisliked(); + } + return getDislikeButton()?.classList.contains("style-text"); +} + +function checkForUserAvatarButton() { + if (isMobile) { + return; + } + if (document.querySelector("#avatar-btn")) { + return true; + } else { + return false; + } +} + +function getState() { + if (isVideoLiked()) { + return LIKED_STATE; + } + if (isVideoDisliked()) { + return DISLIKED_STATE; + } + return NEUTRAL_STATE; +} + +function setLikes(likesCount) { + if (isMobile) { + getButtons().children[0].querySelector(".button-renderer-text").innerText = + likesCount; + return; + } + getLikeTextContainer().innerText = likesCount; +} + +function setDislikes(dislikesCount) { + if (isMobile) { + mobileDislikes = dislikesCount; + return; + } + getDislikeTextContainer()?.removeAttribute("is-empty"); + getDislikeTextContainer().innerText = dislikesCount; +} + +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; + } +} + +(typeof GM_addStyle != "undefined" + ? GM_addStyle + : (styles) => { + let styleNode = document.createElement("style"); + styleNode.type = "text/css"; + styleNode.innerText = styles; + document.head.appendChild(styleNode); + })(` + #return-youtube-dislike-bar-container { + background: var(--yt-spec-icon-disabled); + border-radius: 2px; + } + + #return-youtube-dislike-bar { + background: var(--yt-spec-text-primary); + border-radius: 2px; + transition: all 0.15s ease-in-out; + } + + .ryd-tooltip { + position: absolute; + display: block; + height: 2px; + bottom: -10px; + } + + .ryd-tooltip-bar-container { + width: 100%; + height: 2px; + position: absolute; + padding-top: 6px; + padding-bottom: 12px; + top: -6px; + } + + ytd-menu-renderer.ytd-watch-metadata { + overflow-y: visible !important; + } + + #top-level-buttons-computed { + position: relative !important; + } + `); + +function createRateBar(likes, dislikes) { + if (isMobile || !extConfig.rateBarEnabled) { + return; + } + let rateBar = document.getElementById("return-youtube-dislike-bar-container"); + + const widthPx = + getLikeButton().clientWidth + (getDislikeButton()?.clientWidth ?? 52); + + const widthPercent = + likes + dislikes > 0 ? (likes / (likes + dislikes)) * 100 : 50; + + var likePercentage = parseFloat(widthPercent.toFixed(1)); + const dislikePercentage = (100 - likePercentage).toLocaleString(); + likePercentage = likePercentage.toLocaleString(); + + var tooltipInnerHTML; + switch (extConfig.tooltipPercentageMode) { + case "dash_like": + tooltipInnerHTML = `${likes.toLocaleString()} / ${dislikes.toLocaleString()} - ${likePercentage}%`; + break; + case "dash_dislike": + tooltipInnerHTML = `${likes.toLocaleString()} / ${dislikes.toLocaleString()} - ${dislikePercentage}%`; + break; + case "both": + tooltipInnerHTML = `${likePercentage}% / ${dislikePercentage}%`; + break; + case "only_like": + tooltipInnerHTML = `${likePercentage}%`; + break; + case "only_dislike": + tooltipInnerHTML = `${dislikePercentage}%`; + break; + default: + tooltipInnerHTML = `${likes.toLocaleString()} / ${dislikes.toLocaleString()}`; + } + + if (!rateBar && !isMobile) { + let colorLikeStyle = ""; + let colorDislikeStyle = ""; + if (extConfig.coloredBar) { + colorLikeStyle = "; background-color: " + getColorFromTheme(true); + colorDislikeStyle = "; background-color: " + getColorFromTheme(false); + } + + getButtons().insertAdjacentHTML( + "beforeend", + ` +