Files
return-youtube-dislike/Extensions/UserScript/src/userscript-entry.js
T
Anarios e500088e65 Prepare 4.0.5 Firefox update and Shorts fixes
Add Firefox data consent, repair contributor sign-in, and introduce optional Hide clutter buttons. Harden video navigation, voting, and action layouts with shared browser regression coverage and reproducible release packaging.
2026-09-06 12:51:12 +02:00

2586 lines
86 KiB
JavaScript

import { createVoteClient } from "../../common/vote-client";
import { resolveDelegatedVoteActivation } from "../../common/delegated-vote-activation";
import {
actionBarHasHydratedData,
captureShortsNativeControlInventory,
getShortsControlSurfaceStabilityMs,
getShortsIdentityLinkVideoIds,
isShortsControlSurfaceReadyForMutation as sharedShortsSurfaceIsReady,
shortsNativeControlInventoryIsReadyForFallback as sharedShortsNativeInventoryIsReadyForFallback,
shortsNativeControlInventoryMatches,
} from "../../common/shorts-control-readiness";
import {
LIKED_STATE,
DISLIKED_STATE,
NEUTRAL_STATE,
LIKE_ACTION,
DISLIKE_ACTION,
resolveVoteTransition,
applyVoteTransitionCounts,
shouldSubmitVote,
} from "../../common/vote-transition";
import { createGmCredentialStore } from "./gm-credential-store";
import { createGmSyntheticDislikeStore } from "./gm-synthetic-dislike-store";
import USER_SCRIPT_VERSION from "../userscript-version.json";
if (__RYD_LIVE_TEST_BUILD__) {
document.documentElement.setAttribute("data-ryd-userscript-version", USER_SCRIPT_VERSION);
document.documentElement.setAttribute("data-ryd-userscript-build", __RYD_LIVE_BUILD_ID__);
}
const API_BASE_URL = "https://returnyoutubedislikeapi.com";
const fetchImpl = globalThis.fetch.bind(globalThis);
const voteClient = createVoteClient({
apiBaseUrl: API_BASE_URL,
fetchImpl,
credentialStore: createGmCredentialStore(),
cryptoImpl: globalThis.crypto,
});
const syntheticDislikeStore = createGmSyntheticDislikeStore();
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)
disableLogging: true, // [true*, false] Disable Logging API Response in JavaScript Console.
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: true, // [true*, false] Enables ratio bar under like/dislike buttons
// END USER OPTIONS
};
let previousState = NEUTRAL_STATE;
let likesvalue = 0;
let dislikesvalue = 0;
let isMobile = location.hostname == "m.youtube.com";
function getShortVideoIdFromPathname(pathname) {
return pathname.match(/^\/shorts\/([^/]+)\/?$/)?.[1] ?? null;
}
let isShorts = () => getShortVideoIdFromPathname(location.pathname) !== null;
let mobileDislikes = 0;
let suppressNextLikeActivation = false;
const boundDislikeButtons = new WeakSet();
const boundActivationVideoIds = new WeakMap();
const suppressedStaleRefreshTargets = new WeakSet();
const removingSyntheticShortsDislikes = new WeakSet();
const pendingWatchControlResets = new Map();
let pendingWatchNavigationBoundary = null;
const hydratingShortsActivationTargets = new WeakMap();
const shortsHydrationTails = new Map();
let shortsLifecycleObserver = null;
let shortsLifecycleObserverTarget = null;
let initializationGeneration = 0;
let initializationTimer = null;
let pendingShortsMutationSurface = null;
let activeCountRequest = null;
let countStateVideoId = null;
let countStateLoaded = false;
let countStateEpoch = 0;
let shortsSubmittedStateVideoId = null;
let shortsSubmittedState = NEUTRAL_STATE;
let watchRateBarObserver = null;
let watchRateBarObserverTarget = null;
let watchRateBarObserverVideoId = null;
let watchRateBarRepairTimer = null;
const SYNTHETIC_SHORTS_DISLIKE_SELECTOR = "[data-ryd-synthetic-shorts-dislike]";
const SHORTS_DISLIKE_ICON_PATH =
"m8.482 1.5.294.005a9.01 9.01 0 013.918 1.04l.257.143.203.116c.17.097.357.16.55.185l.194.012h1.477l.115.006c.53.054.95.475 1.004 1.005l.006.114v4.499c0 .621-.504 1.125-1.125 1.125h-1.343a.75.75 0 00-.66.395l-.048.107-2.24 6.402a.75.75 0 01-.832.491l-.78-.13a3 3 0 01-2.439-3.587L7.5 11.25H4.454a2.749 2.749 0 01-2.683-2.151 2.762 2.762 0 01.479-2.237l-.016-.065A2.862 2.862 0 013 4.125v-.032c0-.227.037-.453.108-.668l.08-.211A2.816 2.816 0 015.78 1.5h2.703ZM5.78 3c-.566 0-1.069.362-1.248.9a.613.613 0 00-.031.193v.654l-.44.44c-.333.332-.47.813-.364 1.271l.015.065.157.675-.413.557a1.248 1.248 0 00.999 1.995H7.5a1.501 1.501 0 011.467 1.815L8.5 13.742a1.5 1.5 0 001.22 1.794l.157.027 2.031-5.806a2.25 2.25 0 012.124-1.507H15V4.501h-1.102a3.001 3.001 0 01-1.489-.396l-.202-.116A7.504 7.504 0 008.482 3H5.78Z";
function cLog(text, subtext = "") {
if (!extConfig.disableLogging) {
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 intersectsViewport(element) {
const rect = element.getBoundingClientRect();
const height = innerHeight || document.documentElement.clientHeight;
const width = innerWidth || document.documentElement.clientWidth;
return (
rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && rect.top < height && rect.left < width
);
}
function hasMeaningfulViewportPresence(element) {
const rect = element?.getBoundingClientRect?.();
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
const height = innerHeight || document.documentElement.clientHeight;
const width = innerWidth || document.documentElement.clientWidth;
const visibleWidth = Math.max(0, Math.min(rect.right, width) - Math.max(rect.left, 0));
const visibleHeight = Math.max(0, Math.min(rect.bottom, height) - Math.max(rect.top, 0));
return (visibleWidth * visibleHeight) / (rect.width * rect.height) >= 0.5;
}
function hasRenderedBox(element) {
if (!element?.isConnected || element.closest("[hidden], [aria-hidden='true'], [inert]")) {
return false;
}
for (let current = element; current; current = current.parentElement) {
const style = getComputedStyle(current);
if (
style.display === "none" ||
style.visibility === "hidden" ||
style.visibility === "collapse" ||
Number.parseFloat(style.opacity) === 0
) {
return false;
}
}
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function getRendererShortVideoIds(renderer) {
const identities = new Set();
if (!renderer) {
return identities;
}
const attributeVideoId = renderer.getAttribute("video-id");
if (attributeVideoId) {
identities.add(attributeVideoId);
}
for (const linkVideoId of getShortsIdentityLinkVideoIds(renderer, location.href)) {
identities.add(linkVideoId);
}
return identities;
}
function rendererMatchesShort(renderer, videoId) {
const videoIds = getRendererShortVideoIds(renderer);
return Boolean(videoId) && videoIds.size === 1 && videoIds.has(videoId);
}
function getControlOwnershipVideoIds(container) {
const identities = new Set();
if (!container) {
return identities;
}
const ownedElements = [container, ...container.querySelectorAll("[data-ryd-video-id]")];
for (const element of ownedElements) {
const ownedVideoId = element.getAttribute?.("data-ryd-video-id");
if (ownedVideoId) {
identities.add(ownedVideoId);
}
}
for (const target of container.querySelectorAll("button, tp-yt-paper-button#button")) {
const boundVideoId = boundActivationVideoIds.get(target);
if (boundVideoId) {
identities.add(boundVideoId);
}
}
return identities;
}
function hasConflictingControlOwnership(container, videoId) {
return Array.from(getControlOwnershipVideoIds(container)).some((ownedVideoId) => ownedVideoId !== videoId);
}
function clearPendingWatchControlObservers() {
for (const resetState of pendingWatchControlResets.values()) {
resetState.observer.disconnect();
}
pendingWatchControlResets.clear();
}
function clearPendingWatchNavigationBoundary() {
pendingWatchNavigationBoundary?.observer?.disconnect();
pendingWatchNavigationBoundary = null;
}
function activationTargetHasVideoIdentity(target, videoId, buttons) {
let current = target;
while (current) {
if (current.getAttribute?.("video-id") === videoId || current.getAttribute?.("data-video-id") === videoId) {
return true;
}
if (current === buttons) {
break;
}
current = current.parentElement;
}
return false;
}
function watchTargetIsReady(targetState, buttons, videoId) {
return (
!buttons.contains(targetState.activationTarget) ||
activationTargetHasVideoIdentity(targetState.activationTarget, videoId, buttons) ||
targetState.refreshed
);
}
function originalWatchTargetsAreReady(resetState, buttons, videoId) {
return [resetState.like, resetState.dislike].every((targetState) =>
watchTargetIsReady(targetState, buttons, videoId),
);
}
function currentWatchTargetIsReady(target, targetState, buttons, videoId) {
const boundVideoId = boundActivationVideoIds.get(target);
return (
!boundVideoId ||
boundVideoId === videoId ||
activationTargetHasVideoIdentity(target, videoId, buttons) ||
(target === targetState.activationTarget && targetState.refreshed)
);
}
function elementIsOwnedDisplayMutation(element, targetState) {
if (!element || !targetState.host.contains(element)) {
return false;
}
return Boolean(
element.closest(
"#text, [role='text'], yt-formatted-string, #return-youtube-dislike-bar-container, #return-youtube-dislike-bar, .ryd-tooltip, [data-ryd-synthetic-shorts-dislike]",
),
);
}
function mutationIsMeaningfulWatchRefresh(mutation, targetState, videoId, buttons) {
if (suppressedStaleRefreshTargets.has(targetState.activationTarget)) {
return false;
}
const mutationElement =
mutation.target.nodeType === Node.ELEMENT_NODE ? mutation.target : mutation.target.parentElement;
if (!mutationElement || !targetState.host.contains(mutationElement)) {
return false;
}
if (mutation.type === "attributes") {
if (["data-video-id", "video-id"].includes(mutation.attributeName)) {
return activationTargetHasVideoIdentity(targetState.activationTarget, videoId, buttons);
}
return (
["aria-disabled", "aria-label", "disabled", "title"].includes(mutation.attributeName) &&
(mutationElement === targetState.activationTarget || mutationElement === targetState.host)
);
}
if (mutation.type !== "childList" || elementIsOwnedDisplayMutation(mutationElement, targetState)) {
return false;
}
const changedElements = [...mutation.addedNodes, ...mutation.removedNodes]
.map((node) => (node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement))
.filter(Boolean);
return changedElements.some((element) => !elementIsOwnedDisplayMutation(element, targetState));
}
function captureMeaningfulWatchRefreshes(resetState, mutations, videoId, buttons) {
for (const targetState of [resetState.like, resetState.dislike]) {
if (
!targetState.refreshed &&
mutations.some((mutation) => mutationIsMeaningfulWatchRefresh(mutation, targetState, videoId, buttons))
) {
targetState.refreshed = true;
}
}
}
function captureWatchNavigationBoundaryRefreshes(boundary, mutations) {
for (const targetState of [boundary.like, boundary.dislike]) {
if (
!targetState.refreshed &&
mutations.some((mutation) => mutationIsMeaningfulWatchRefresh(mutation, targetState, "", boundary.buttons))
) {
targetState.refreshed = true;
}
}
}
function seedWatchTargetFromNavigationBoundary(targetState, boundaryTargetState, buttons, videoId) {
if (targetState.activationTarget === boundaryTargetState.activationTarget) {
targetState.refreshed ||= boundaryTargetState.refreshed;
return;
}
const boundVideoId = boundActivationVideoIds.get(targetState.activationTarget);
targetState.refreshed ||=
!boundVideoId ||
boundVideoId === videoId ||
activationTargetHasVideoIdentity(targetState.activationTarget, videoId, buttons);
}
function seedWatchResetFromNavigationBoundary(resetState, buttons, videoId) {
const boundary = pendingWatchNavigationBoundary;
if (!boundary || boundary.sourceVideoId === videoId) {
return;
}
captureWatchNavigationBoundaryRefreshes(boundary, boundary.observer.takeRecords());
clearPendingWatchNavigationBoundary();
if (boundary.buttons !== buttons) {
return;
}
seedWatchTargetFromNavigationBoundary(resetState.like, boundary.like, buttons, videoId);
seedWatchTargetFromNavigationBoundary(resetState.dislike, boundary.dislike, buttons, videoId);
}
function suppressStaleTargetRefresh(target) {
suppressedStaleRefreshTargets.add(target);
setTimeout(() => suppressedStaleRefreshTargets.delete(target), 0);
}
function watchControlsAreReadyForVideo(buttons, likeButton, dislikeButton, videoId) {
if (!hasConflictingControlOwnership(buttons, videoId)) {
clearPendingWatchControlObservers();
if (pendingWatchNavigationBoundary?.sourceVideoId !== videoId) {
clearPendingWatchNavigationBoundary();
}
return true;
}
// YouTube occasionally completes a watch-to-watch navigation while reusing
// the exact same reaction-control nodes and without mutating anything inside
// them. In that state the old per-node ownership is the only conflicting
// signal, so waiting for a control mutation can never finish. A completed
// navigation plus a matching current watch root is the route-level ownership
// proof for this otherwise indistinguishable case.
const watchRoot = buttons.closest("ytd-watch-flexy, ytd-watch-grid");
if (
pendingWatchNavigationBoundary?.completedVideoId === videoId &&
pendingWatchNavigationBoundary.sourceVideoId !== videoId &&
pendingWatchNavigationBoundary.buttons === buttons &&
watchRoot?.getAttribute("video-id") === videoId &&
getButtons() === buttons &&
hasRenderedBox(buttons)
) {
clearPendingWatchControlObservers();
clearPendingWatchNavigationBoundary();
return true;
}
const existingReset = pendingWatchControlResets.get(buttons);
if (existingReset?.videoId === videoId) {
seedWatchResetFromNavigationBoundary(existingReset, buttons, videoId);
if (!originalWatchTargetsAreReady(existingReset, buttons, videoId)) {
return false;
}
const currentLikeButton = getLikeButton();
const currentDislikeButton = getDislikeButton();
if (!currentLikeButton || !currentDislikeButton || getButtons() !== buttons) {
return false;
}
const currentLikeTarget = getActivationTarget(currentLikeButton);
const currentDislikeTarget = getActivationTarget(currentDislikeButton);
if (
!currentWatchTargetIsReady(currentLikeTarget, existingReset.like, buttons, videoId) ||
!currentWatchTargetIsReady(currentDislikeTarget, existingReset.dislike, buttons, videoId)
) {
return false;
}
existingReset.observer.disconnect();
pendingWatchControlResets.delete(buttons);
return true;
}
existingReset?.observer.disconnect();
const likeActivationTarget = getActivationTarget(likeButton);
const dislikeActivationTarget = getActivationTarget(dislikeButton);
const resetState = {
dislike: {
activationTarget: dislikeActivationTarget,
host: dislikeButton,
refreshed: false,
},
like: {
activationTarget: likeActivationTarget,
host: likeButton,
refreshed: false,
},
observer: null,
videoId,
};
seedWatchResetFromNavigationBoundary(resetState, buttons, videoId);
if (
originalWatchTargetsAreReady(resetState, buttons, videoId) &&
currentWatchTargetIsReady(likeActivationTarget, resetState.like, buttons, videoId) &&
currentWatchTargetIsReady(dislikeActivationTarget, resetState.dislike, buttons, videoId)
) {
pendingWatchControlResets.delete(buttons);
return true;
}
const observer = new MutationObserver((mutations) => {
captureMeaningfulWatchRefreshes(resetState, mutations, videoId, buttons);
if (originalWatchTargetsAreReady(resetState, buttons, videoId)) {
observer.disconnect();
setEventListeners();
}
});
resetState.observer = observer;
pendingWatchControlResets.set(buttons, resetState);
observer.observe(buttons, {
attributeFilter: ["aria-disabled", "aria-label", "data-video-id", "disabled", "title", "video-id"],
attributes: true,
childList: true,
subtree: true,
});
return false;
}
function getActiveDesktopShortsActionBar() {
const videoId = getVideoId();
const candidates = Array.from(document.querySelectorAll("ytd-reel-video-renderer"))
.filter((renderer) => intersectsViewport(renderer))
.map((renderer) => ({
actionBar: renderer.querySelector("reel-action-bar-view-model"),
renderer,
}))
.filter(({ actionBar }) => actionBar);
const matchingCandidates = candidates.filter(({ renderer }) => rendererMatchesShort(renderer, videoId));
if (matchingCandidates.length === 1) {
return matchingCandidates[0].actionBar;
}
// During channel/watch -> Shorts SPA transitions, YouTube can render the active
// reel before its video-id/link metadata is hydrated. A single visible reel is
// still unambiguous; waiting for metadata in this state leaves the controls
// permanently uninitialized on some page variants.
if (
candidates.length === 1 &&
getRendererShortVideoIds(candidates[0].renderer).size === 0 &&
!hasConflictingControlOwnership(candidates[0].actionBar, videoId)
) {
return candidates[0].actionBar;
}
const fullyVisibleCandidates = candidates.filter(
({ actionBar, renderer }) =>
isInViewport(renderer) &&
getRendererShortVideoIds(renderer).size === 0 &&
!hasConflictingControlOwnership(actionBar, videoId),
);
return fullyVisibleCandidates.length === 1 ? fullyVisibleCandidates[0].actionBar : null;
}
function getShortsControlSurfaceSignature(buttons) {
return (
Array.from(getRendererShortVideoIds(buttons?.closest("ytd-reel-video-renderer")))
.sort()
.join("|") || "identityless"
);
}
function getVisibleDesktopShortsControlSurfaceCount() {
return Array.from(document.querySelectorAll("reel-action-bar-view-model")).filter(
(candidate) => hasRenderedBox(candidate) && hasMeaningfulViewportPresence(candidate),
).length;
}
function shortsControlSurfaceIsReadyForMutation(
buttons,
currentVideoId,
{ allowUnhydratedFallback = false, isHydrated = false, isStable = false } = {},
) {
return sharedShortsSurfaceIsReady({
allowUnhydratedFallback,
candidateVideoIds: getRendererShortVideoIds(buttons?.closest("ytd-reel-video-renderer")),
currentVideoId,
isConnected: buttons?.isConnected,
isHydrated,
isRendered: hasRenderedBox(buttons),
isStable,
isViewportIntersecting: hasMeaningfulViewportPresence(buttons),
visibleCandidateCount: getVisibleDesktopShortsControlSurfaceCount(),
});
}
function shortsNativeControlInventoryIsReadyForFallback(inventory) {
return sharedShortsNativeInventoryIsReadyForFallback(inventory, {
getTopmostElement:
typeof document.elementFromPoint === "function" ? (x, y) => document.elementFromPoint(x, y) : null,
isMeaningfullyInViewport: hasMeaningfulViewportPresence,
isRendered: hasRenderedBox,
});
}
function getActiveMobileShortsButtons() {
const videoId = getVideoId();
const candidates = Array.from(document.querySelectorAll("ytm-like-button-renderer"))
.filter((buttons) => isInViewport(buttons))
.map((buttons) => ({ buttons, ...getMobileShortOwnership(buttons) }));
const matchingCandidates = candidates.filter(({ identities }) => identities.has(videoId));
if (matchingCandidates.length === 1) {
return matchingCandidates[0].buttons;
}
if (
candidates.length === 1 &&
candidates[0].identities.size === 0 &&
!hasConflictingControlOwnership(candidates[0].buttons, videoId)
) {
return candidates[0].buttons;
}
return null;
}
function getExactShortLinkVideoIds(element) {
return getShortsIdentityLinkVideoIds(element, location.href);
}
function getMobileShortOwnership(buttons) {
const ancestors = [];
let current = buttons;
while (current && current !== document.body) {
if (current.matches("ytm-shorts, ytm-shorts-container, #shorts-container, #shorts-inner-container")) {
break;
}
ancestors.push(current);
current = current.parentElement;
}
for (const ancestor of ancestors) {
const attributeVideoId = ancestor.getAttribute("video-id") || ancestor.getAttribute("data-video-id");
if (attributeVideoId) {
return { identities: new Set([attributeVideoId]), owner: ancestor };
}
}
for (const ancestor of ancestors) {
const identities = getExactShortLinkVideoIds(ancestor);
if (identities.size > 0) {
return { identities, owner: ancestor };
}
}
const owner =
ancestors.find((ancestor) => ancestor.matches("ytm-reel-video-renderer, ytm-shorts-video-renderer")) ??
ancestors.find((ancestor) => ancestor.matches("ytm-reel-player-overlay-renderer")) ??
buttons;
return { identities: new Set(), owner };
}
function getDesktopWatchButtonCandidates() {
return Array.from(
new Set(
document.querySelectorAll(
"#menu-container #top-level-buttons-computed, ytd-menu-renderer.ytd-watch-metadata > div, ytd-menu-renderer.ytd-video-primary-info-renderer > div",
),
),
).filter((candidate) =>
candidate.querySelector(
"segmented-like-dislike-button-view-model, ytd-segmented-like-dislike-button-renderer, like-button-view-model, #segmented-like-button",
),
);
}
function selectCurrentWatchButtons(candidates) {
const videoId = getVideoId();
return (
candidates
.map((candidate, index) => {
const watchRoot = candidate.closest("ytd-watch-flexy, ytd-watch-grid");
const rootVideoId = watchRoot?.getAttribute("video-id");
const rootMatches = Boolean(videoId && rootVideoId === videoId);
const rendered = hasRenderedBox(candidate);
const inViewport = rendered && intersectsViewport(candidate);
const conflicts = Boolean(videoId && hasConflictingControlOwnership(candidate, videoId));
// YouTube can retain several button groups under the same current
// ytd-watch-flexy while an SPA navigation settles. A matching root is
// therefore useful ownership evidence, but it must never make a
// hidden stale group outrank the rendered controls the user can see.
const tier =
rootMatches && inViewport && !conflicts
? 10
: rootMatches && inViewport
? 9
: inViewport && !conflicts
? 8
: inViewport
? 7
: rootMatches && rendered && !conflicts
? 6
: rootMatches && rendered
? 5
: rendered && !conflicts
? 4
: rendered
? 3
: rootMatches && !conflicts
? 2
: !conflicts
? 1
: 0;
return { candidate, index, tier };
})
.sort((left, right) => right.tier - left.tier || left.index - right.index)[0]?.candidate ?? null
);
}
function getButtons() {
if (isShorts()) {
if (!isMobile) {
const actionBar = getActiveDesktopShortsActionBar();
if (actionBar) {
return actionBar;
}
} else {
const buttons = getActiveMobileShortsButtons();
if (buttons) {
return buttons;
}
}
// Never bind watch/channel controls that are still connected while a Shorts
// SPA route is mounting. The initialization retry loop will pick up the real
// reel controls as soon as they are available.
return null;
}
if (isMobile) {
return (
document.querySelector(".slim-video-action-bar-actions .segmented-buttons") ??
document.querySelector(".slim-video-action-bar-actions")
);
}
return selectCurrentWatchButtons(getDesktopWatchButtonCandidates());
}
function removeSyntheticShortsDislike(syntheticDislike) {
if (!syntheticDislike || removingSyntheticShortsDislikes.has(syntheticDislike)) {
return;
}
removingSyntheticShortsDislikes.add(syntheticDislike);
try {
syntheticDislike.remove();
} finally {
removingSyntheticShortsDislikes.delete(syntheticDislike);
}
}
function ensureSyntheticShortsDislikeButton(
buttons,
{ allowUnhydratedFallback = false, currentVideoId = getVideoId(), isHydrated = false, isStable = false } = {},
) {
if (!isShorts() || isMobile || !buttons) {
return;
}
const syntheticDislikes = Array.from(buttons.querySelectorAll(SYNTHETIC_SHORTS_DISLIKE_SELECTOR));
const syntheticDislike = syntheticDislikes[0];
const nativeDislike = buttons.querySelector("dislike-button-view-model, #dislike-button");
const readyForMutation = shortsControlSurfaceIsReadyForMutation(buttons, currentVideoId, {
allowUnhydratedFallback,
isHydrated: isHydrated && actionBarHasHydratedData(buttons),
isStable,
});
if (nativeDislike) {
if (readyForMutation) syntheticDislikes.forEach(removeSyntheticShortsDislike);
return;
}
if (!readyForMutation) {
return;
}
if (syntheticDislike) {
syntheticDislikes.slice(1).forEach(removeSyntheticShortsDislike);
const videoId = currentVideoId;
if (videoId && syntheticDislike.getAttribute("data-ryd-video-id") !== videoId) {
syntheticDislike.setAttribute("data-ryd-video-id", videoId);
setSyntheticShortsPressed(false, syntheticDislike);
const button = syntheticDislike.querySelector("button");
if (button) {
button.disabled = true;
button.setAttribute("aria-disabled", "true");
}
const count = syntheticDislike.querySelector("#text, [role='text']");
if (count) {
count.textContent = "";
}
}
return;
}
const likeButton = buttons.querySelector("like-button-view-model");
const nativeLikeButton = likeButton?.querySelector("button");
if (!likeButton || !nativeLikeButton) {
return;
}
const ownedDislike = document.createElement("div");
ownedDislike.className = likeButton.getAttribute("class") || "";
ownedDislike.setAttribute("data-ryd-synthetic-shorts-dislike", "true");
ownedDislike.setAttribute("data-ryd-role", "dislike");
ownedDislike.setAttribute("data-ryd-video-id", getVideoId());
ownedDislike.classList.add("ryd-synthetic-shorts-dislike");
const button = document.createElement("button");
button.type = "button";
button.className = nativeLikeButton.className;
button.setAttribute("aria-label", "Dislike this video");
button.setAttribute("aria-pressed", "false");
button.setAttribute("aria-disabled", "true");
button.disabled = true;
const icon = document.createElement("div");
icon.className =
nativeLikeButton.querySelector(".ytSpecButtonShapeNextIcon")?.getAttribute("class") || "ytSpecButtonShapeNextIcon";
icon.setAttribute("aria-hidden", "true");
const svgNamespace = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNamespace, "svg");
svg.setAttribute("height", "24");
svg.setAttribute("viewBox", "0 0 18 18");
svg.setAttribute("width", "24");
svg.setAttribute("focusable", "false");
svg.setAttribute("aria-hidden", "true");
const path = document.createElementNS(svgNamespace, "path");
path.setAttribute("d", SHORTS_DISLIKE_ICON_PATH);
svg.appendChild(path);
icon.appendChild(svg);
const countContainer = document.createElement("div");
countContainer.className =
likeButton.querySelector(".ytSpecButtonShapeWithLabelLabel")?.className || "ytSpecButtonShapeWithLabelLabel";
const count = document.createElement("span");
count.id = "text";
count.className =
likeButton.querySelector('span[role="text"]')?.className ||
"ytAttributedStringHost ytAttributedStringTextAlignmentCenter";
count.setAttribute("role", "text");
countContainer.appendChild(count);
button.appendChild(icon);
const buttonAndCount = document.createElement("label");
buttonAndCount.className = nativeLikeButton.closest("label")?.className || "ytSpecButtonShapeWithLabelHost";
buttonAndCount.classList.add("ryd-synthetic-shorts-dislike-label");
buttonAndCount.append(button, countContainer);
ownedDislike.appendChild(buttonAndCount);
setSyntheticShortsPressed(false, ownedDislike);
likeButton.insertAdjacentElement("afterend", ownedDislike);
}
function getDislikeButton(buttons = getButtons()) {
if (buttons?.tagName === "REEL-ACTION-BAR-VIEW-MODEL") {
return (
buttons.querySelector("dislike-button-view-model, #dislike-button") ??
buttons.querySelector(SYNTHETIC_SHORTS_DISLIKE_SELECTOR)
);
}
const firstButton = buttons?.children?.[0];
if (!firstButton) {
return null;
}
if (firstButton.tagName === "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER") {
if (firstButton.children[1] === undefined) {
return buttons.querySelector("#segmented-dislike-button");
} else {
return firstButton.children[1];
}
} else {
if (buttons.querySelector("segmented-like-dislike-button-view-model")) {
const dislikeViewModel = buttons.querySelector("dislike-button-view-model");
if (!dislikeViewModel) cLog("Dislike button wasn't added to DOM yet...");
return dislikeViewModel;
} else {
return buttons.children[1] ?? null;
}
}
}
function getLikeButton(buttons = getButtons()) {
const firstButton = buttons?.children?.[0];
if (!firstButton) {
return null;
}
return firstButton.tagName === "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER"
? buttons.querySelector("#segmented-like-button") !== null
? buttons.querySelector("#segmented-like-button")
: firstButton.children[0]
: buttons.querySelector("like-button-view-model") ?? firstButton;
}
function getLikeTextContainer() {
return (
getLikeButton().querySelector("#text") ??
getLikeButton().getElementsByTagName("yt-formatted-string")[0] ??
getLikeButton().querySelector("span[role='text']")
);
}
function getDislikeTextContainer(dislikeButton = getDislikeButton()) {
if (!dislikeButton) {
return null;
}
let result =
dislikeButton.querySelector("#text") ??
dislikeButton.getElementsByTagName("yt-formatted-string")[0] ??
dislikeButton.querySelector("span[role='text']");
if (result == null) {
const activationTarget = getActivationTarget(dislikeButton);
if (!activationTarget?.matches("button, tp-yt-paper-button#button")) {
return null;
}
const textSpan = document.createElement("span");
textSpan.id = "text";
textSpan.setAttribute("role", "text");
textSpan.style.marginLeft = "6px";
activationTarget.appendChild(textSpan);
activationTarget.style.width = "auto";
result = textSpan;
}
return result;
}
function setSyntheticShortsPressed(pressed, dislikeButton = getDislikeButton()) {
if (!dislikeButton?.matches(SYNTHETIC_SHORTS_DISLIKE_SELECTOR)) {
return;
}
dislikeButton.classList.toggle("style-default-active", pressed);
dislikeButton.classList.toggle("style-text", !pressed);
dislikeButton.querySelector("button")?.setAttribute("aria-pressed", String(pressed));
}
function persistSyntheticShortsState(videoId, disliked) {
void syntheticDislikeStore.setDisliked(videoId, disliked).catch(reportVoteFailure);
}
async function readSyntheticShortsDisliked(videoId) {
try {
return await syntheticDislikeStore.isDisliked(videoId);
} catch (error) {
reportVoteFailure(error);
return false;
}
}
async function restoreSyntheticShortsState(
videoId,
dislikeButton = getDislikeButton(),
initialVisibleState = getState(),
) {
if (!dislikeButton?.matches(SYNTHETIC_SHORTS_DISLIKE_SELECTOR)) {
return false;
}
const disliked = await readSyntheticShortsDisliked(videoId);
return {
disliked,
submittedState: initialVisibleState === LIKED_STATE ? LIKED_STATE : disliked ? DISLIKED_STATE : initialVisibleState,
};
}
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();
};
observerWrapper.takeRecords = function () {
return this.observer.takeRecords();
};
return observerWrapper;
}
let shortsObserver = null;
function getShortsObserver() {
if (shortsObserver) {
return shortsObserver;
}
cLog("Initializing shorts mutation observer");
shortsObserver = createObserver(
{
attributes: true,
attributeFilter: ["aria-pressed"],
},
(mutationList) => {
mutationList.forEach((mutation) => {
if (mutation.type === "attributes") {
cLog("Short thumb button status changed");
if (mutation.target.getAttribute("aria-pressed") === "true") {
mutation.target.style.color = mutation.target.closest("like-button-view-model")
? getColorFromTheme(true)
: getColorFromTheme(false);
} else {
mutation.target.style.color = "unset";
}
}
});
},
);
return shortsObserver;
}
function isVideoLiked() {
const likeButton = getLikeButton();
const nativeButton = likeButton?.querySelector("button");
if (isMobile) {
return nativeButton?.getAttribute("aria-pressed") === "true" || nativeButton?.getAttribute("aria-label") === "true";
}
return (
likeButton?.classList.contains("style-default-active") || nativeButton?.getAttribute("aria-pressed") === "true"
);
}
function isVideoDisliked() {
const dislikeButton = getDislikeButton();
const nativeButton = dislikeButton?.querySelector("button");
if (isMobile) {
return nativeButton?.getAttribute("aria-pressed") === "true" || nativeButton?.getAttribute("aria-label") === "true";
}
return (
dislikeButton?.classList.contains("style-default-active") || nativeButton?.getAttribute("aria-pressed") === "true"
);
}
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 isSignedOut() {
const signInLink = document.querySelector("a[href^='https://accounts.google.com/ServiceLogin']");
return signInLink !== null || (!isMobile && document.querySelector("#avatar-btn") === null);
}
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;
}
const _container = getDislikeTextContainer();
if (!_container) {
return;
}
_container.removeAttribute("is-empty");
if (_container.innerText !== dislikesCount) {
_container.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: #737373;
background: color-mix(
in srgb,
var(--yt-spec-text-primary, #f1f1f1) 55%,
var(--yt-spec-base-background, #0f0f0f) 45%
);
border-radius: 2px;
}
#return-youtube-dislike-bar {
background: var(--yt-spec-text-primary);
border-radius: 2px;
transition: all 0.15s ease-in-out;
}
.ryd-synthetic-shorts-dislike svg {
display: block;
fill: currentColor;
height: 24px;
pointer-events: none;
width: 24px;
}
.ryd-synthetic-shorts-dislike {
box-sizing: content-box;
display: block;
flex: 0 0 auto;
height: 70px;
margin: 0 !important;
padding: 0 0 8px;
width: 100%;
}
.ryd-synthetic-shorts-dislike-label {
align-items: center;
display: flex;
flex-direction: column;
}
.ryd-synthetic-shorts-dislike .ytSpecButtonShapeNextIcon {
flex: 0 0 24px;
height: 24px;
min-width: 24px;
width: 24px;
}
.ryd-synthetic-shorts-dislike button {
color: inherit;
cursor: pointer;
}
.ryd-synthetic-shorts-dislike button[aria-pressed="true"] {
color: var(--yt-spec-call-to-action, #3ea6ff);
}
.ryd-tooltip {
bottom: -10px;
display: block;
height: 2px;
outline: none;
position: absolute;
}
.ryd-tooltip-bar-container {
width: 100%;
height: 2px;
position: absolute;
padding-top: 6px;
padding-bottom: 12px;
top: -6px;
}
.ryd-tooltip-label {
background: rgba(28, 28, 28, 0.96);
border-radius: 4px;
bottom: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
box-sizing: border-box;
color: #fff;
font-family: Roboto, Arial, sans-serif;
font-size: 12px;
font-weight: 500;
line-height: 16px;
max-width: calc(100vw - 24px);
opacity: 0;
overflow: hidden;
padding: 6px 8px;
pointer-events: none;
position: absolute;
right: 0;
text-overflow: ellipsis;
transform: translateY(4px);
transition: opacity 0.12s ease-out, transform 0.12s ease-out, visibility 0s linear 0.12s;
visibility: hidden;
white-space: nowrap;
width: max-content;
z-index: 2200;
}
.ryd-tooltip:hover .ryd-tooltip-label,
.ryd-tooltip:focus-within .ryd-tooltip-label {
opacity: 1;
transform: translateY(0);
transition-delay: 0s;
visibility: visible;
}
.ryd-tooltip:focus-visible .ryd-tooltip-bar-container {
outline: 2px solid var(--yt-spec-call-to-action, #3ea6ff);
outline-offset: 2px;
}
ytd-menu-renderer.ytd-watch-metadata {
overflow-y: visible !important;
}
#top-level-buttons-computed {
position: relative !important;
}
`);
function createRateBar(likes, dislikes) {
if (isMobile || isShorts() || !extConfig.rateBarEnabled) {
return;
}
const buttons = getButtons();
const likeButton = getLikeButton();
const dislikeButton = getDislikeButton();
if (!buttons || !likeButton) {
return;
}
// YouTube retains the outgoing watch metadata tree during some SPA
// transitions. A document-wide ID lookup can therefore find the old bar,
// update it with the new video's counts, and then lose it when YouTube
// removes that stale tree. Keep the single owned bar scoped to the active
// reaction controls instead.
for (const candidate of document.querySelectorAll("#return-youtube-dislike-bar-container")) {
if (!buttons.contains(candidate)) {
(candidate.closest(".ryd-tooltip") ?? candidate).remove();
}
}
let rateBar = buttons.querySelector("#return-youtube-dislike-bar-container");
if (rateBar && !watchRateBarIsHealthy(buttons, getVideoId())) {
removeWatchRateBarArtifacts(buttons);
rateBar = null;
}
const widthPx = likeButton.clientWidth + (dislikeButton?.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();
const separator = "\u00a0/\u00a0";
const percentageSeparator = "\u00a0\u00a0-\u00a0\u00a0";
let tooltipText;
switch (extConfig.tooltipPercentageMode) {
case "dash_like":
tooltipText = `${likes.toLocaleString()}${separator}${dislikes.toLocaleString()}${percentageSeparator}${likePercentage}%`;
break;
case "dash_dislike":
tooltipText = `${likes.toLocaleString()}${separator}${dislikes.toLocaleString()}${percentageSeparator}${dislikePercentage}%`;
break;
case "both":
tooltipText = `${likePercentage}%${separator}${dislikePercentage}%`;
break;
case "only_like":
tooltipText = `${likePercentage}%`;
break;
case "only_dislike":
tooltipText = `${dislikePercentage}%`;
break;
default:
tooltipText = `${likes.toLocaleString()}${separator}${dislikes.toLocaleString()}`;
}
if (!rateBar && !isMobile) {
const tooltip = document.createElement("div");
tooltip.className = "ryd-tooltip";
tooltip.setAttribute("data-ryd-rate-bar-wrapper", "true");
tooltip.setAttribute("data-ryd-video-id", getVideoId());
tooltip.style.width = `${widthPx}px`;
tooltip.setAttribute("aria-describedby", "ryd-dislike-tooltip");
tooltip.setAttribute("tabindex", "0");
const tooltipBarContainer = document.createElement("div");
tooltipBarContainer.className = "ryd-tooltip-bar-container";
rateBar = document.createElement("div");
rateBar.id = "return-youtube-dislike-bar-container";
rateBar.style.width = "100%";
rateBar.style.height = "2px";
const rateBarFill = document.createElement("div");
rateBarFill.id = "return-youtube-dislike-bar";
rateBarFill.style.width = `${widthPercent}%`;
rateBarFill.style.height = "100%";
if (extConfig.coloredBar) {
rateBar.style.backgroundColor = getColorFromTheme(false);
rateBarFill.style.backgroundColor = getColorFromTheme(true);
}
rateBar.appendChild(rateBarFill);
tooltipBarContainer.appendChild(rateBar);
const tooltipLabel = document.createElement("div");
tooltipLabel.id = "ryd-dislike-tooltip";
tooltipLabel.className = "ryd-tooltip-label";
tooltipLabel.setAttribute("role", "tooltip");
tooltipLabel.textContent = tooltipText;
tooltip.append(tooltipBarContainer, tooltipLabel);
buttons.appendChild(tooltip);
const descriptionAndActionsElement = buttons.closest("#top-row");
if (descriptionAndActionsElement) {
descriptionAndActionsElement.style.borderBottom = "1px solid var(--yt-spec-10-percent-layer)";
descriptionAndActionsElement.style.paddingBottom = "10px";
}
} else {
const tooltip = rateBar.closest(".ryd-tooltip");
const rateBarFill = rateBar.querySelector("#return-youtube-dislike-bar");
if (!tooltip || !rateBarFill) {
(tooltip ?? rateBar).remove();
createRateBar(likes, dislikes);
return;
}
tooltip.setAttribute("data-ryd-video-id", getVideoId());
tooltip.style.width = widthPx + "px";
rateBarFill.style.width = widthPercent + "%";
const tooltipLabel = tooltip.querySelector("#ryd-dislike-tooltip");
if (tooltipLabel) {
tooltipLabel.textContent = tooltipText;
}
if (extConfig.coloredBar) {
rateBar.style.backgroundColor = getColorFromTheme(false);
rateBarFill.style.backgroundColor = getColorFromTheme(true);
}
}
}
function elementTouchesWatchRateBar(element) {
return Boolean(
element?.matches?.(
".ryd-tooltip, .ryd-tooltip-bar-container, .ryd-tooltip-label, #return-youtube-dislike-bar-container, #return-youtube-dislike-bar, #ryd-dislike-tooltip",
) ||
element?.matches?.('[data-ryd-rate-bar-wrapper="true"]') ||
element?.closest?.(".ryd-tooltip, .ryd-tooltip-bar-container, #return-youtube-dislike-bar-container") ||
element?.querySelector?.(
".ryd-tooltip, .ryd-tooltip-bar-container, .ryd-tooltip-label, #return-youtube-dislike-bar-container, #return-youtube-dislike-bar, #ryd-dislike-tooltip",
),
);
}
function mutationTouchesWatchRateBar(mutation) {
if (mutation.type === "attributes") {
return elementTouchesWatchRateBar(mutation.target);
}
return (
mutation.type === "childList" &&
[...mutation.addedNodes, ...mutation.removedNodes].some(
(node) => node.nodeType === Node.ELEMENT_NODE && elementTouchesWatchRateBar(node),
)
);
}
function watchRateBarIsHealthy(buttons, videoId) {
if (!buttons || !videoId) {
return false;
}
const wrappers = Array.from(buttons.querySelectorAll('[data-ryd-rate-bar-wrapper="true"]'));
const containers = Array.from(buttons.querySelectorAll("#return-youtube-dislike-bar-container"));
const fills = Array.from(buttons.querySelectorAll("#return-youtube-dislike-bar"));
const labels = Array.from(buttons.querySelectorAll("#ryd-dislike-tooltip"));
if (wrappers.length !== 1 || containers.length !== 1 || fills.length !== 1 || labels.length !== 1) {
return false;
}
const [wrapper] = wrappers;
const [container] = containers;
const [fill] = fills;
const [label] = labels;
if (
wrapper.parentElement !== buttons ||
!wrapper.matches(".ryd-tooltip") ||
wrapper.getAttribute("data-ryd-video-id") !== videoId ||
!wrapper.contains(container) ||
!container.contains(fill) ||
!wrapper.contains(label) ||
!hasRenderedBox(wrapper) ||
!hasRenderedBox(container)
) {
return false;
}
const fillStyle = getComputedStyle(fill);
const fillBounds = fill.getBoundingClientRect();
return (
fillStyle.display !== "none" &&
fillStyle.visibility !== "hidden" &&
fillStyle.visibility !== "collapse" &&
Number.parseFloat(fillStyle.opacity) !== 0 &&
fillBounds.height > 0
);
}
function removeWatchRateBarArtifacts(buttons) {
if (!buttons) {
return;
}
for (const wrapper of buttons.querySelectorAll('.ryd-tooltip, [data-ryd-rate-bar-wrapper="true"]')) {
wrapper.remove();
}
for (const fragment of buttons.querySelectorAll(
".ryd-tooltip-bar-container, .ryd-tooltip-label, #return-youtube-dislike-bar-container, #return-youtube-dislike-bar, #ryd-dislike-tooltip",
)) {
fragment.remove();
}
}
function clearWatchPresentation(buttons, dislikeButton) {
removeWatchRateBarArtifacts(buttons);
const dislikeText =
dislikeButton?.querySelector("#text") ??
dislikeButton?.getElementsByTagName("yt-formatted-string")[0] ??
dislikeButton?.querySelector("span[role='text']");
if (dislikeText) {
dislikeText.textContent = "";
}
}
function clearStaleWatchPresentation(buttons, dislikeButton, videoId) {
if (isMobile || isShorts() || !initializedVideoId || initializedVideoId === videoId) {
return;
}
clearWatchPresentation(buttons, dislikeButton);
}
function canRepairWatchRateBar(buttons, videoId) {
if (
isMobile ||
isShorts() ||
!extConfig.rateBarEnabled ||
!videoId ||
!countStateLoaded ||
countStateVideoId !== videoId ||
initializedVideoId !== videoId ||
getVideoId() !== videoId ||
!buttons?.isConnected ||
getButtons() !== buttons ||
!hasRenderedBox(buttons)
) {
return false;
}
const likeButton = getLikeButton();
const dislikeButton = getDislikeButton();
return (
likeButton === initializedLikeButton &&
dislikeButton === initializedDislikeButton &&
buttons.contains(likeButton) &&
buttons.contains(dislikeButton) &&
!watchRateBarIsHealthy(buttons, videoId)
);
}
function repairWatchRateBar(buttons = getButtons(), videoId = getVideoId()) {
if (canRepairWatchRateBar(buttons, videoId)) {
removeWatchRateBarArtifacts(buttons);
createRateBar(likesvalue, dislikesvalue);
}
}
function scheduleWatchRateBarRepair() {
if (watchRateBarRepairTimer !== null) {
return;
}
watchRateBarRepairTimer = setTimeout(() => {
watchRateBarRepairTimer = null;
repairWatchRateBar(watchRateBarObserverTarget, watchRateBarObserverVideoId);
}, 0);
}
function disconnectWatchRateBarObserver() {
watchRateBarObserver?.disconnect();
watchRateBarObserver = null;
watchRateBarObserverTarget = null;
watchRateBarObserverVideoId = null;
if (watchRateBarRepairTimer !== null) {
clearTimeout(watchRateBarRepairTimer);
watchRateBarRepairTimer = null;
}
}
function observeWatchRateBar(buttons, videoId) {
if (isMobile || isShorts() || !extConfig.rateBarEnabled || !buttons) {
disconnectWatchRateBarObserver();
return;
}
if (watchRateBarObserverTarget === buttons) {
watchRateBarObserverVideoId = videoId;
return;
}
disconnectWatchRateBarObserver();
watchRateBarObserverTarget = buttons;
watchRateBarObserverVideoId = videoId;
watchRateBarObserver = new MutationObserver((mutations) => {
if (mutations.some(mutationTouchesWatchRateBar)) {
scheduleWatchRateBarRepair();
}
});
watchRateBarObserver.observe(buttons, {
attributeFilter: ["aria-hidden", "class", "hidden", "inert", "style"],
attributes: true,
childList: true,
subtree: true,
});
}
function setState() {
const videoId = getVideoId();
previousState = getState();
if (countStateVideoId === videoId && (activeCountRequest?.videoId === videoId || countStateLoaded)) {
updateDOMDislikes();
refreshFormattedLikes();
return;
}
if (countStateVideoId !== videoId) {
likesvalue = 0;
dislikesvalue = 0;
countStateVideoId = videoId;
countStateLoaded = false;
countStateEpoch += 1;
}
const countRequest = {
dislikesDelta: 0,
likesDelta: 0,
videoId,
};
activeCountRequest = countRequest;
cLog("Fetching votes...");
fetchImpl(`${API_BASE_URL}/votes?videoId=${videoId}`)
.then((response) => response.json())
.then((json) => {
if (getVideoId() !== videoId || activeCountRequest !== countRequest) {
return;
}
if (json && !("traceId" in json)) {
const { dislikes, likes } = json;
cLog(`Received count: ${dislikes}`);
likesvalue = Math.max(0, likes + countRequest.likesDelta);
dislikesvalue = Math.max(0, dislikes + countRequest.dislikesDelta);
countStateLoaded = true;
setDislikes(numberFormat(dislikesvalue));
if (extConfig.numberDisplayReformatLikes === true) {
const nativeLikes = getLikeCountFromButton();
if (nativeLikes !== false) {
setLikes(numberFormat(nativeLikes));
}
}
createRateBar(likesvalue, dislikesvalue);
if (extConfig.coloredThumbs === true) {
const dislikeButton = getDislikeButton();
if (isShorts()) {
// for shorts, leave deactived buttons in default color
const shortLikeButton = getLikeButton()?.querySelector("button, tp-yt-paper-button#button");
const shortDislikeButton = dislikeButton?.querySelector("button, 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);
}
const observer = getShortsObserver();
if (shortLikeButton) observer.observe(shortLikeButton);
if (shortDislikeButton) observer.observe(shortDislikeButton);
} else {
getLikeButton().style.color = getColorFromTheme(true);
if (dislikeButton) dislikeButton.style.color = getColorFromTheme(false);
}
}
}
})
.catch((error) => cLog("Fetching votes failed", error instanceof Error ? error.message : String(error)))
.finally(() => {
if (activeCountRequest === countRequest) {
activeCountRequest = null;
}
});
}
function updateDOMDislikes() {
const videoId = getVideoId();
if (!videoId || !countStateLoaded || countStateVideoId !== videoId) {
return false;
}
setDislikes(numberFormat(dislikesvalue));
createRateBar(likesvalue, dislikesvalue);
return true;
}
function reportVoteFailure(error) {
const message = error instanceof Error ? error.message : String(error);
cLog("Vote submission failed", message);
}
function refreshFormattedLikes() {
if (extConfig.numberDisplayReformatLikes !== true) {
return;
}
const nativeLikes = getLikeCountFromButton();
if (nativeLikes !== false) {
setLikes(numberFormat(nativeLikes));
}
}
function getVoteStateCounts(state) {
return {
dislikes: state === DISLIKED_STATE ? 1 : 0,
likes: state === LIKED_STATE ? 1 : 0,
};
}
function getShortsCountTransition(videoId, transition) {
if (!isShorts() || shortsSubmittedStateVideoId !== videoId) {
return transition;
}
const previousCounts = getVoteStateCounts(shortsSubmittedState);
const nextCounts = getVoteStateCounts(transition.nextState);
return {
...transition,
dislikesDelta: nextCounts.dislikes - previousCounts.dislikes,
likesDelta: nextCounts.likes - previousCounts.likes,
};
}
function applyCountTransition(videoId, countTransition) {
const counts = applyVoteTransitionCounts(likesvalue, dislikesvalue, countTransition);
if (activeCountRequest?.videoId === videoId) {
activeCountRequest.likesDelta += countTransition.likesDelta;
activeCountRequest.dislikesDelta += countTransition.dislikesDelta;
}
likesvalue = counts.likes;
dislikesvalue = counts.dislikes;
}
function applyVoteTransition(videoId, transition, syntheticShortsDislike) {
const countTransition = getShortsCountTransition(videoId, transition);
applyCountTransition(videoId, countTransition);
previousState = transition.nextState;
if (syntheticShortsDislike) {
setSyntheticShortsPressed(previousState === DISLIKED_STATE);
}
if (isShorts()) {
shortsSubmittedStateVideoId = videoId;
shortsSubmittedState = previousState;
persistSyntheticShortsState(videoId, previousState === DISLIKED_STATE);
}
updateDOMDislikes();
refreshFormattedLikes();
}
function applyHydratingVoteTransition(hydration, transition, syntheticShortsDislike) {
applyCountTransition(hydration.videoId, transition);
previousState = transition.nextState;
if (syntheticShortsDislike) {
setSyntheticShortsPressed(previousState === DISLIKED_STATE);
}
updateDOMDislikes();
refreshFormattedLikes();
}
function reconcileHydratingVoteTransition(hydration, transition, syntheticShortsDislike) {
const submittedCountTransition = getShortsCountTransition(hydration.videoId, transition);
if (transition.optimisticCountStateEpoch === countStateEpoch) {
applyCountTransition(hydration.videoId, {
...transition,
dislikesDelta: submittedCountTransition.dislikesDelta - transition.dislikesDelta,
likesDelta: submittedCountTransition.likesDelta - transition.likesDelta,
});
}
previousState = transition.nextState;
shortsSubmittedStateVideoId = hydration.videoId;
shortsSubmittedState = previousState;
persistSyntheticShortsState(hydration.videoId, previousState === DISLIKED_STATE);
if (syntheticShortsDislike) {
setSyntheticShortsPressed(previousState === DISLIKED_STATE);
}
}
function submitVoteTransition(videoId, transition, signedOut) {
if (shouldSubmitVote({ disableVoteSubmission: extConfig.disableVoteSubmission, signedOut })) {
void voteClient.submitVote(videoId, transition.value).catch(reportVoteFailure);
}
}
function clearNativeLikeForSyntheticDislike(action, stateBeforeActivation, syntheticShortsDislike) {
if (!syntheticShortsDislike || action !== DISLIKE_ACTION || stateBeforeActivation !== LIKED_STATE) {
return;
}
const nativeLikeButton = getLikeButton()?.querySelector("button");
if (nativeLikeButton && isVideoLiked()) {
suppressNextLikeActivation = true;
try {
nativeLikeButton.click();
} finally {
suppressNextLikeActivation = false;
}
}
}
function handleVoteActivation(action) {
const signedOut = isSignedOut();
if (signedOut) {
return;
}
const videoId = getVideoId();
if (!videoId) {
return;
}
const transition = resolveVoteTransition(previousState, action);
const syntheticShortsDislike = getDislikeButton()?.matches(SYNTHETIC_SHORTS_DISLIKE_SELECTOR) === true;
clearNativeLikeForSyntheticDislike(action, previousState, syntheticShortsDislike);
applyVoteTransition(videoId, transition, syntheticShortsDislike);
submitVoteTransition(videoId, transition, signedOut);
}
function captureHydratingShortsActivation(event, action) {
const hydration = hydratingShortsActivationTargets.get(event.currentTarget);
if (!hydration) {
return false;
}
if (hydration.videoId !== getVideoId()) {
return true;
}
const signedOut = isSignedOut();
const syntheticShortsDislike = getDislikeButton()?.matches(SYNTHETIC_SHORTS_DISLIKE_SELECTOR) === true;
if (!signedOut) {
clearNativeLikeForSyntheticDislike(action, hydration.visibleState, syntheticShortsDislike);
}
const transition = {
...resolveVoteTransition(hydration.visibleState, action),
optimisticCountStateEpoch: countStateEpoch,
};
hydration.visibleState = transition.nextState;
if (!signedOut) {
hydration.activations.push(transition);
applyHydratingVoteTransition(hydration, transition, syntheticShortsDislike);
submitVoteTransition(hydration.videoId, transition, signedOut);
}
return true;
}
function likeClicked(event) {
if (suppressNextLikeActivation) {
return;
}
if (boundActivationVideoIds.get(event.currentTarget) !== getVideoId()) {
suppressStaleTargetRefresh(event.currentTarget);
return;
}
if (captureHydratingShortsActivation(event, LIKE_ACTION)) {
return;
}
handleVoteActivation(LIKE_ACTION);
}
function dislikeClicked(event) {
if (boundActivationVideoIds.get(event.currentTarget) !== getVideoId()) {
suppressStaleTargetRefresh(event.currentTarget);
return;
}
if (captureHydratingShortsActivation(event, DISLIKE_ACTION)) {
return;
}
handleVoteActivation(DISLIKE_ACTION);
}
function handleDelegatedVoteClick(event) {
const buttons = getButtons();
const likeButton = getLikeButton(buttons);
const dislikeButton = getDislikeButton(buttons);
const activation = resolveDelegatedVoteActivation({
buttons,
dislikeButton,
event,
getActivationTarget,
likeButton,
});
if (!activation) {
return;
}
const videoId = getVideoId();
if (!videoId) {
return;
}
const targetHydration = hydratingShortsActivationTargets.get(activation.activationTarget);
const hydration = targetHydration?.videoId === videoId ? targetHydration : null;
if (targetHydration && !hydration) {
hydratingShortsActivationTargets.delete(activation.activationTarget);
}
const targetIsBoundToCurrentVideo = boundActivationVideoIds.get(activation.activationTarget) === videoId;
const currentCountStateIsLoaded = countStateLoaded && countStateVideoId === videoId;
const isUnboundCurrentShortsActivation = isShorts() && !hydration && !targetIsBoundToCurrentVideo;
if (!hydration && !targetIsBoundToCurrentVideo && !currentCountStateIsLoaded && !isUnboundCurrentShortsActivation) {
return;
}
if (isUnboundCurrentShortsActivation) {
previousState = getState();
}
boundActivationVideoIds.set(activation.activationTarget, videoId);
const delegatedEvent = { currentTarget: activation.activationTarget };
if (activation.action === LIKE_ACTION) {
likeClicked(delegatedEvent);
} else {
dislikeClicked(delegatedEvent);
}
}
function refreshDislikesForBoundControl(event) {
if (boundActivationVideoIds.get(event.currentTarget) === getVideoId()) {
updateDOMDislikes();
}
}
function getVideoId() {
const urlObject = new URL(window.location.href);
const pathname = urlObject.pathname;
if (pathname.startsWith("/clip")) {
return (document.querySelector("meta[itemprop='videoId']") || document.querySelector("meta[itemprop='identifier']"))
?.content;
} else {
const shortVideoId = getShortVideoIdFromPathname(pathname);
if (shortVideoId) {
return shortVideoId;
}
return urlObject.searchParams.get("v");
}
}
function isVideoLoaded() {
if (isMobile) {
return document.getElementById("player")?.getAttribute("loading") == "false";
}
const videoId = getVideoId();
return (
// desktop: spring 2024 UI
document.querySelector(`ytd-watch-grid[video-id='${videoId}']`) !== null ||
// desktop: older UI
document.querySelector(`ytd-watch-flexy[video-id='${videoId}']`) !== null ||
// mobile: no video-id attribute
document.querySelector('#player[loading="false"]:not([hidden])') !== null
);
}
function roundDown(num) {
if (num < 1000) return num;
const int = Math.floor(Math.log10(num) - 2);
const decimal = int + (int % 3 ? 1 : 0);
const value = Math.floor(num / 10 ** decimal);
return value * 10 ** decimal;
}
function numberFormat(numberState) {
let numberDisplay;
if (extConfig.numberDisplayRoundDown === false) {
numberDisplay = numberState;
} else {
numberDisplay = roundDown(numberState);
}
return getNumberFormatter(extConfig.numberDisplayFormat).format(numberDisplay);
}
function getNumberFormatter(optionSelect) {
let userLocales;
if (document.documentElement.lang) {
userLocales = document.documentElement.lang;
} else if (navigator.language) {
userLocales = navigator.language;
} else {
try {
userLocales = new URL(
Array.from(document.querySelectorAll("head > link[rel='search']"))
?.find((n) => n?.getAttribute("href")?.includes("?locale="))
?.getAttribute("href"),
)?.searchParams?.get("locale");
} catch {
cLog("Cannot find browser locale. Use en as default for number formatting.");
userLocales = "en";
}
}
let formatterNotation;
let formatterCompactDisplay;
switch (optionSelect) {
case "compactLong":
formatterNotation = "compact";
formatterCompactDisplay = "long";
break;
case "standard":
formatterNotation = "standard";
formatterCompactDisplay = "short";
break;
case "compactShort":
default:
formatterNotation = "compact";
formatterCompactDisplay = "short";
}
const formatter = Intl.NumberFormat(userLocales, {
notation: formatterNotation,
compactDisplay: formatterCompactDisplay,
});
return formatter;
}
function getColorFromTheme(voteIsLike) {
let colorString;
switch (extConfig.colorTheme) {
case "accessible":
if (voteIsLike === true) {
colorString = "dodgerblue";
} else {
colorString = "gold";
}
break;
case "neon":
if (voteIsLike === true) {
colorString = "aqua";
} else {
colorString = "magenta";
}
break;
case "classic":
default:
if (voteIsLike === true) {
colorString = "lime";
} else {
colorString = "red";
}
}
return colorString;
}
let smartimationObserver = null;
let initializedVideoId = null;
let initializedButtons = null;
let initializedLikeButton = null;
let initializedDislikeButton = null;
let lifecyclePageKey = null;
const SHORTS_RENDERER_SELECTOR =
"ytd-reel-video-renderer, ytm-reel-video-renderer, ytm-shorts-video-renderer, ytm-reel-player-overlay-renderer";
const SHORTS_CONTROL_SELECTOR = `like-button-view-model, dislike-button-view-model, ${SYNTHETIC_SHORTS_DISLIKE_SELECTOR}`;
function getShortsRenderer(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
return null;
}
return element.matches(SHORTS_RENDERER_SELECTOR) ? element : element.closest?.(SHORTS_RENDERER_SELECTOR) ?? null;
}
function rendererOwnsCurrentShort(renderer) {
if (!renderer) {
return false;
}
return (
renderer.hasAttribute("is-active") ||
rendererMatchesShort(renderer, getVideoId()) ||
(initializedLikeButton && renderer.contains(initializedLikeButton)) ||
(initializedDislikeButton && renderer.contains(initializedDislikeButton))
);
}
function elementTouchesCurrentShortRenderer(element) {
const containingRenderer = getShortsRenderer(element);
if (containingRenderer) {
return rendererOwnsCurrentShort(containingRenderer);
}
return Array.from(element?.querySelectorAll?.(SHORTS_RENDERER_SELECTOR) ?? []).some(rendererOwnsCurrentShort);
}
function mutationTouchesShortsControls(mutation) {
if (mutation.type === "attributes") {
if (mutation.attributeName === "is-active" && mutation.target.matches(SHORTS_RENDERER_SELECTOR)) {
return true;
}
if (
(mutation.target.matches(SHORTS_RENDERER_SELECTOR) || mutation.target.matches("a")) &&
elementTouchesCurrentShortRenderer(mutation.target)
) {
return true;
}
return false;
}
if (mutation.type !== "childList") {
return false;
}
const changedElements = [...mutation.addedNodes, ...mutation.removedNodes].filter(
(node) => node.nodeType === Node.ELEMENT_NODE,
);
if (
changedElements.some(
(node) =>
node.matches(`reel-action-bar-view-model, ${SHORTS_CONTROL_SELECTOR}`) ||
node.querySelector?.(`reel-action-bar-view-model, ${SHORTS_CONTROL_SELECTOR}`),
)
) {
return [mutation.target, ...changedElements].some(elementTouchesCurrentShortRenderer);
}
return (
mutation.target.closest?.(SHORTS_CONTROL_SELECTOR) &&
elementTouchesCurrentShortRenderer(mutation.target) &&
changedElements.some(
(node) =>
node.matches("button, tp-yt-paper-button#button") || node.querySelector?.("button, tp-yt-paper-button#button"),
)
);
}
function shortsControlsNeedInitialization() {
const videoId = getVideoId();
if (!isShorts() || !videoId) {
return false;
}
const buttons = getButtons();
const likeButton = getLikeButton();
const dislikeButton = getDislikeButton();
const syntheticDislikes = buttons?.querySelectorAll?.(SYNTHETIC_SHORTS_DISLIKE_SELECTOR).length ?? 0;
const hasNativeDislike = Boolean(buttons?.querySelector?.("dislike-button-view-model, #dislike-button"));
if (!buttons || !likeButton || !dislikeButton) {
return true;
}
if ((hasNativeDislike && syntheticDislikes > 0) || (!hasNativeDislike && syntheticDislikes !== 1)) {
return true;
}
if (
initializedVideoId !== videoId ||
initializedLikeButton !== likeButton ||
initializedDislikeButton !== dislikeButton
) {
return true;
}
return (
boundActivationVideoIds.get(getActivationTarget(likeButton)) !== videoId ||
boundActivationVideoIds.get(getActivationTarget(dislikeButton)) !== videoId
);
}
function disconnectShortsLifecycleObserver() {
shortsLifecycleObserver?.disconnect();
shortsLifecycleObserverTarget = null;
}
function observeShortsLifecycle(buttons) {
if (!isShorts() || !buttons) {
disconnectShortsLifecycleObserver();
return;
}
clearPendingWatchControlObservers();
const isDesktopActionBar = buttons.tagName === "REEL-ACTION-BAR-VIEW-MODEL";
const isMobileActionBar = isMobile && buttons.tagName === "YTM-LIKE-BUTTON-RENDERER";
if (!isDesktopActionBar && !isMobileActionBar) {
disconnectShortsLifecycleObserver();
return;
}
const renderer = isMobileActionBar
? buttons.closest("ytm-reel-video-renderer, ytm-shorts-video-renderer") ?? getMobileShortOwnership(buttons).owner
: buttons.closest("ytd-reel-video-renderer") ?? buttons;
const observerTarget =
renderer.closest(
"ytd-shorts, ytd-shorts-container, ytm-shorts, ytm-shorts-container, #shorts-container, #shorts-inner-container",
) ??
renderer.parentElement ??
document.body;
if (shortsLifecycleObserverTarget === observerTarget) {
return;
}
disconnectShortsLifecycleObserver();
shortsLifecycleObserver = new MutationObserver((mutations) => {
if (mutations.some(mutationTouchesShortsControls) && shortsControlsNeedInitialization()) {
setEventListeners();
}
});
shortsLifecycleObserver.observe(observerTarget, {
attributeFilter: ["href", "is-active", "video-id"],
attributes: true,
childList: true,
subtree: true,
});
shortsLifecycleObserverTarget = observerTarget;
}
function getActivationTarget(control) {
if (control.matches("button, tp-yt-paper-button#button")) {
return control;
}
return control.querySelector("button, tp-yt-paper-button#button") ?? control;
}
function beginShortsHydration(videoId, likeButton, dislikeButton, initialVisibleState) {
const previousCompletion = shortsHydrationTails.get(videoId) ?? Promise.resolve();
let resolveCompletion;
const completion = new Promise((resolve) => {
resolveCompletion = resolve;
});
const hydration = {
activations: [],
completion,
initialVisibleState,
previousCompletion,
resolveCompletion,
videoId,
visibleState: initialVisibleState,
};
shortsHydrationTails.set(videoId, completion);
hydratingShortsActivationTargets.set(getActivationTarget(likeButton), hydration);
hydratingShortsActivationTargets.set(getActivationTarget(dislikeButton), hydration);
return hydration;
}
function finishShortsHydration(hydration, likeButton, dislikeButton) {
for (const target of [getActivationTarget(likeButton), getActivationTarget(dislikeButton)]) {
if (hydratingShortsActivationTargets.get(target) === hydration) {
hydratingShortsActivationTargets.delete(target);
}
}
if (shortsHydrationTails.get(hydration.videoId) === hydration.completion) {
shortsHydrationTails.delete(hydration.videoId);
}
hydration.resolveCompletion();
}
function persistFinalHydratingActivation(hydration) {
const finalActivation = hydration.activations[hydration.activations.length - 1];
if (finalActivation) {
persistSyntheticShortsState(hydration.videoId, finalActivation.nextState === DISLIKED_STATE);
}
}
function reconcileStaleShortsHydration(hydration, submittedState, storedDisliked) {
if (getVideoId() !== hydration.videoId || hydration.activations.length === 0) {
persistFinalHydratingActivation(hydration);
return;
}
const currentDislikeButton = getDislikeButton();
if (!currentDislikeButton) {
persistFinalHydratingActivation(hydration);
return;
}
applyHydratedShortsState(hydration, submittedState, currentDislikeButton, storedDisliked);
}
function applyHydratedShortsState(hydration, submittedState, dislikeButton, storedDisliked) {
const syntheticShortsDislike = dislikeButton.matches(SYNTHETIC_SHORTS_DISLIKE_SELECTOR);
shortsSubmittedStateVideoId = hydration.videoId;
shortsSubmittedState = submittedState;
if (hydration.activations.length === 0) {
previousState = syntheticShortsDislike ? submittedState : hydration.initialVisibleState;
if (syntheticShortsDislike) {
setSyntheticShortsPressed(submittedState === DISLIKED_STATE, dislikeButton);
}
if (hydration.initialVisibleState === LIKED_STATE && storedDisliked) {
persistSyntheticShortsState(hydration.videoId, false);
}
return;
}
for (const transition of hydration.activations) {
reconcileHydratingVoteTransition(hydration, transition, syntheticShortsDislike);
}
updateDOMDislikes();
refreshFormattedLikes();
}
function bindVoteButtonListeners(likeButton, dislikeButton, { enableSynthetic = true } = {}) {
const likeActivationTarget = getActivationTarget(likeButton);
const dislikeActivationTarget = getActivationTarget(dislikeButton);
const videoId = getVideoId();
boundActivationVideoIds.set(likeActivationTarget, videoId);
boundActivationVideoIds.set(dislikeActivationTarget, videoId);
if (!boundDislikeButtons.has(dislikeActivationTarget)) {
dislikeActivationTarget.addEventListener("focusin", refreshDislikesForBoundControl);
dislikeActivationTarget.addEventListener("focusout", refreshDislikesForBoundControl);
boundDislikeButtons.add(dislikeActivationTarget);
}
if (enableSynthetic && dislikeButton.matches(SYNTHETIC_SHORTS_DISLIKE_SELECTOR)) {
dislikeActivationTarget.disabled = false;
dislikeActivationTarget.setAttribute("aria-disabled", "false");
}
}
function getStableDesktopShortsControls(videoId, generation) {
const buttons = getButtons();
const inventory = captureShortsNativeControlInventory(buttons);
const isHydrated = actionBarHasHydratedData(buttons);
if (
!buttons ||
!inventory ||
(!isHydrated && !shortsNativeControlInventoryIsReadyForFallback(inventory)) ||
!shortsControlSurfaceIsReadyForMutation(buttons, videoId, {
allowUnhydratedFallback: !isHydrated,
isHydrated,
isStable: true,
})
) {
pendingShortsMutationSurface = null;
return null;
}
const signature = getShortsControlSurfaceSignature(buttons);
const now = performance.now();
if (
pendingShortsMutationSurface?.buttons !== buttons ||
pendingShortsMutationSurface.generation !== generation ||
pendingShortsMutationSurface.videoId !== videoId ||
pendingShortsMutationSurface.signature !== signature ||
!shortsNativeControlInventoryMatches(pendingShortsMutationSurface.inventory, inventory)
) {
pendingShortsMutationSurface = {
buttons,
generation,
inventory,
observedAt: now,
signature,
unhydratedObservedAt: isHydrated ? null : now,
videoId,
};
return null;
}
if (isHydrated) {
pendingShortsMutationSurface.unhydratedObservedAt = null;
} else if (pendingShortsMutationSurface.unhydratedObservedAt == null) {
pendingShortsMutationSurface.unhydratedObservedAt = now;
}
const stabilityObservedAt = isHydrated
? pendingShortsMutationSurface.observedAt
: pendingShortsMutationSurface.unhydratedObservedAt;
if (now - stabilityObservedAt < getShortsControlSurfaceStabilityMs(isHydrated)) {
return null;
}
const allowUnhydratedFallback = !isHydrated;
const preMutationButtons = getButtons();
const preMutationInventory = captureShortsNativeControlInventory(preMutationButtons);
const preMutationIsHydrated = actionBarHasHydratedData(preMutationButtons);
const preMutationAllowsFallback = !preMutationIsHydrated && allowUnhydratedFallback;
if (
preMutationButtons !== buttons ||
!shortsNativeControlInventoryMatches(inventory, preMutationInventory) ||
(preMutationAllowsFallback && !shortsNativeControlInventoryIsReadyForFallback(preMutationInventory)) ||
getShortsControlSurfaceSignature(preMutationButtons) !== signature ||
!shortsControlSurfaceIsReadyForMutation(preMutationButtons, videoId, {
allowUnhydratedFallback: preMutationAllowsFallback,
isHydrated: preMutationIsHydrated,
isStable: true,
})
) {
pendingShortsMutationSurface = null;
return null;
}
ensureSyntheticShortsDislikeButton(preMutationButtons, {
allowUnhydratedFallback: preMutationAllowsFallback,
currentVideoId: videoId,
isHydrated: preMutationIsHydrated,
isStable: true,
});
const confirmedButtons = getButtons();
const confirmedInventory = captureShortsNativeControlInventory(confirmedButtons);
const confirmedIsHydrated = actionBarHasHydratedData(confirmedButtons);
if (
confirmedButtons !== buttons ||
!shortsNativeControlInventoryMatches(inventory, confirmedInventory) ||
(preMutationAllowsFallback && !shortsNativeControlInventoryIsReadyForFallback(confirmedInventory)) ||
getShortsControlSurfaceSignature(confirmedButtons) !== signature ||
!shortsControlSurfaceIsReadyForMutation(confirmedButtons, videoId, {
allowUnhydratedFallback: !confirmedIsHydrated && preMutationAllowsFallback,
isHydrated: confirmedIsHydrated,
isStable: true,
})
) {
pendingShortsMutationSurface = null;
return null;
}
pendingShortsMutationSurface = null;
return confirmedButtons;
}
async function initializeCurrentButtons(generation) {
const videoId = getVideoId();
if (!videoId) {
// Channel/search/home pages have no video controls to initialize. The
// lightweight lifecycle monitor will restart initialization when a video
// route appears, instead of polling the whole page every 111 ms forever.
return true;
}
if (!(isShorts() || (hasRenderedBox(getButtons()) && isVideoLoaded()))) {
return false;
}
let buttons = getButtons();
const needsStableShortsControls =
isShorts() &&
!isMobile &&
buttons?.tagName === "REEL-ACTION-BAR-VIEW-MODEL" &&
(!buttons.querySelector("dislike-button-view-model, #dislike-button") ||
buttons.querySelector(SYNTHETIC_SHORTS_DISLIKE_SELECTOR) ||
!actionBarHasHydratedData(buttons));
if (needsStableShortsControls) {
buttons = getStableDesktopShortsControls(videoId, generation);
if (!buttons) {
return false;
}
}
const likeButton = getLikeButton(buttons);
const dislikeButton = getDislikeButton(buttons);
if (!buttons || !likeButton || !dislikeButton) {
return false;
}
if (!isShorts() && !watchControlsAreReadyForVideo(buttons, likeButton, dislikeButton, videoId)) {
return false;
}
if (!isShorts() && !getDislikeTextContainer(dislikeButton)) {
return false;
}
observeShortsLifecycle(buttons);
observeWatchRateBar(buttons, videoId);
const stateNeedsInitialization =
initializedVideoId !== videoId ||
initializedButtons !== buttons ||
initializedLikeButton !== likeButton ||
initializedDislikeButton !== dislikeButton;
if (stateNeedsInitialization) {
clearStaleWatchPresentation(buttons, dislikeButton, videoId);
initializedVideoId = videoId;
initializedButtons = buttons;
initializedLikeButton = likeButton;
initializedDislikeButton = dislikeButton;
setState();
}
if (isShorts()) {
const initialVisibleState = getState();
const likeActivationTarget = getActivationTarget(likeButton);
const dislikeActivationTarget = getActivationTarget(dislikeButton);
const existingLikeHydration = hydratingShortsActivationTargets.get(likeActivationTarget);
const existingDislikeHydration = hydratingShortsActivationTargets.get(dislikeActivationTarget);
if (
existingLikeHydration &&
existingLikeHydration === existingDislikeHydration &&
existingLikeHydration.videoId === videoId
) {
return false;
}
const hydration = beginShortsHydration(videoId, likeButton, dislikeButton, initialVisibleState);
bindVoteButtonListeners(likeButton, dislikeButton, { enableSynthetic: false });
let storedDisliked;
let submittedState;
try {
await hydration.previousCompletion;
if (dislikeButton.matches(SYNTHETIC_SHORTS_DISLIKE_SELECTOR)) {
const restored = await restoreSyntheticShortsState(videoId, dislikeButton, initialVisibleState);
if (!restored) {
persistFinalHydratingActivation(hydration);
return false;
}
storedDisliked = restored.disliked;
submittedState = restored.submittedState;
} else {
storedDisliked = await readSyntheticShortsDisliked(videoId);
submittedState =
initialVisibleState === LIKED_STATE ? LIKED_STATE : storedDisliked ? DISLIKED_STATE : initialVisibleState;
}
if (
generation !== initializationGeneration ||
getVideoId() !== videoId ||
getLikeButton() !== likeButton ||
getDislikeButton() !== dislikeButton
) {
reconcileStaleShortsHydration(hydration, submittedState, storedDisliked);
return false;
}
applyHydratedShortsState(hydration, submittedState, dislikeButton, storedDisliked);
bindVoteButtonListeners(likeButton, dislikeButton);
} finally {
finishShortsHydration(hydration, likeButton, dislikeButton);
}
} else {
if (
generation !== initializationGeneration ||
getVideoId() !== videoId ||
getLikeButton() !== likeButton ||
getDislikeButton() !== dislikeButton
) {
return false;
}
bindVoteButtonListeners(likeButton, dislikeButton);
}
if (!smartimationObserver) {
smartimationObserver = createObserver(
{
attributes: true,
subtree: true,
childList: true,
},
updateDOMDislikes,
);
smartimationObserver.container = null;
}
const smartimationContainer = buttons.querySelector("yt-smartimation");
if (smartimationContainer && smartimationObserver.container != smartimationContainer) {
cLog("Initializing smartimation mutation observer");
smartimationObserver.disconnect();
smartimationObserver.observe(smartimationContainer);
smartimationObserver.container = smartimationContainer;
}
return true;
}
function setEventListeners(evt) {
const generation = ++initializationGeneration;
let checkRunning = false;
if (initializationTimer) {
clearInterval(initializationTimer);
}
async function checkForJSFinish() {
if (generation !== initializationGeneration || checkRunning) {
return;
}
checkRunning = true;
try {
const initialized = await initializeCurrentButtons(generation);
if (initialized && generation === initializationGeneration) {
clearInterval(initializationTimer);
initializationTimer = null;
}
} catch (error) {
reportVoteFailure(error);
} finally {
checkRunning = false;
}
}
cLog("Setting up...");
initializationTimer = setInterval(() => void checkForJSFinish(), 111);
void checkForJSFinish();
}
function getLifecyclePageKey() {
const videoId = getVideoId();
if (!videoId) {
return null;
}
return `${isShorts() ? "shorts" : "watch"}:${videoId}`;
}
function watchControlsNeedReinitialization() {
if (isMobile || isShorts() || initializationTimer !== null) {
return false;
}
const videoId = getVideoId();
if (!videoId || initializedVideoId !== videoId) {
return false;
}
const buttons = getButtons();
const likeButton = getLikeButton();
const dislikeButton = getDislikeButton();
return (
!initializedButtons?.isConnected ||
buttons !== initializedButtons ||
likeButton !== initializedLikeButton ||
dislikeButton !== initializedDislikeButton ||
!initializedLikeButton?.isConnected ||
!initializedDislikeButton?.isConnected ||
!buttons?.contains(initializedLikeButton) ||
!buttons?.contains(initializedDislikeButton)
);
}
function checkPageLifecycle() {
const pageKey = getLifecyclePageKey();
if (pageKey !== lifecyclePageKey) {
lifecyclePageKey = pageKey;
if (!isShorts()) {
disconnectShortsLifecycleObserver();
if (!pageKey) {
clearPendingWatchControlObservers();
}
}
setEventListeners();
return;
}
if (watchControlsNeedReinitialization()) {
setEventListeners();
return;
}
repairWatchRateBar();
}
function handleNavigateStart() {
pendingShortsMutationSurface = null;
disconnectWatchRateBarObserver();
if (smartimationObserver) {
smartimationObserver.takeRecords();
smartimationObserver.disconnect();
smartimationObserver.container = null;
}
clearPendingWatchNavigationBoundary();
clearPendingWatchControlObservers();
if (isShorts()) {
return;
}
const buttons = getButtons();
const likeButton = getLikeButton();
const dislikeButton = getDislikeButton();
if (!buttons || !likeButton || !dislikeButton) {
return;
}
const likeActivationTarget = getActivationTarget(likeButton);
const dislikeActivationTarget = getActivationTarget(dislikeButton);
const likeVideoId = boundActivationVideoIds.get(likeActivationTarget);
const dislikeVideoId = boundActivationVideoIds.get(dislikeActivationTarget);
if (
!likeVideoId ||
likeVideoId !== dislikeVideoId ||
likeVideoId !== getVideoId() ||
!buttons.contains(likeActivationTarget) ||
!buttons.contains(dislikeActivationTarget)
) {
return;
}
activeCountRequest = null;
countStateLoaded = false;
countStateEpoch += 1;
clearWatchPresentation(buttons, dislikeButton);
const boundary = {
buttons,
completedVideoId: null,
dislike: {
activationTarget: dislikeActivationTarget,
host: dislikeButton,
refreshed: false,
},
like: {
activationTarget: likeActivationTarget,
host: likeButton,
refreshed: false,
},
observer: null,
sourceVideoId: likeVideoId,
};
const observer = new MutationObserver((mutations) => {
captureWatchNavigationBoundaryRefreshes(boundary, mutations);
});
boundary.observer = observer;
pendingWatchNavigationBoundary = boundary;
observer.observe(buttons, {
attributeFilter: ["aria-disabled", "aria-label", "data-video-id", "disabled", "title", "video-id"],
attributes: true,
childList: true,
subtree: true,
});
}
function handleNavigateFinish(event) {
lifecyclePageKey = getLifecyclePageKey();
if (isShorts()) {
clearPendingWatchControlObservers();
clearPendingWatchNavigationBoundary();
} else {
disconnectShortsLifecycleObserver();
const videoId = getVideoId();
if (pendingWatchNavigationBoundary && videoId !== pendingWatchNavigationBoundary.sourceVideoId) {
pendingWatchNavigationBoundary.completedVideoId = videoId;
}
if (!getVideoId()) {
clearPendingWatchControlObservers();
clearPendingWatchNavigationBoundary();
}
}
setEventListeners(event);
}
(function () {
"use strict";
void voteClient.ensureRegistered().catch(reportVoteFailure);
document.addEventListener("click", handleDelegatedVoteClick, true);
window.addEventListener("yt-navigate-start", handleNavigateStart, true);
window.addEventListener("yt-navigate-finish", handleNavigateFinish, true);
window.addEventListener("popstate", checkPageLifecycle, true);
lifecyclePageKey = getLifecyclePageKey();
setInterval(checkPageLifecycle, 500);
setEventListeners();
})();
if (isMobile) {
setInterval(() => {
const dislikeButton = getDislikeButton();
if (dislikeButton?.querySelector(".button-renderer-text") === null) {
getDislikeTextContainer().innerText = mobileDislikes;
} else {
if (dislikeButton) dislikeButton.querySelector(".button-renderer-text").innerText = mobileDislikes;
}
}, 1000);
}