123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846 |
- import { addParent, autoPlural, debounce, fetchAdvanced, pauseFor } from "@sv443-network/userutils";
- import { getFeature, getFeatures } from "../config.js";
- import { siteEvents } from "../siteEvents.js";
- import { addSelectorListener } from "../observers.js";
- import { error, getResourceUrl, log, warn, t, onInteraction, openInTab, getBestThumbnailUrl, getDomain, currentMediaType, domLoaded, waitVideoElementReady, addStyleFromResource, fetchVideoVotes, getWatchId, getLocale, tp, getVideoTime } from "../utils/index.js";
- import { mode, scriptInfo } from "../constants.js";
- import { openCfgMenu } from "../menu/menu_old.js";
- import { createCircularBtn, createRipple } from "../components/index.js";
- import type { NumberNotation, ResourceKey, VideoVotesObj } from "../types.js";
- import "./layout.css";
- //#region cfg menu btns
- let logoExchanged = false, improveLogoCalled = false;
- /** Adds a watermark beneath the logo */
- export async function addWatermark() {
- const watermark = document.createElement("a");
- watermark.role = "button";
- watermark.id = "bytm-watermark";
- watermark.classList.add("style-scope", "ytmusic-nav-bar", "bytm-no-select");
- watermark.textContent = scriptInfo.name;
- watermark.ariaLabel = watermark.title = t("open_menu_tooltip", scriptInfo.name);
- watermark.tabIndex = 0;
- improveLogo();
- const watermarkOpenMenu = (e: MouseEvent | KeyboardEvent) => {
- e.stopPropagation();
- if((!e.shiftKey && !e.ctrlKey) || logoExchanged)
- openCfgMenu();
- if(!logoExchanged && (e.shiftKey || e.ctrlKey))
- exchangeLogo();
- };
- onInteraction(watermark, watermarkOpenMenu);
- addSelectorListener("navBar", "ytmusic-nav-bar #left-content", {
- listener: (logoElem) => logoElem.insertAdjacentElement("afterend", watermark),
- });
- log("Added watermark element");
- }
- /** Turns the regular `<img>`-based logo into inline SVG to be able to animate and modify parts of it */
- export async function improveLogo() {
- try {
- if(improveLogoCalled)
- return;
- improveLogoCalled = true;
- const res = await fetchAdvanced("https://music.youtube.com/img/on_platform_logo_dark.svg");
- const svg = await res.text();
- addSelectorListener("navBar", "ytmusic-logo a", {
- listener: (logoElem) => {
- logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
- logoElem.innerHTML = svg;
- logoElem.querySelectorAll("ellipse").forEach((e) => {
- e.classList.add("bytm-mod-logo-ellipse");
- });
- logoElem.querySelector("path")?.classList.add("bytm-mod-logo-path");
- log("Swapped logo to inline SVG");
- },
- });
- }
- catch(err) {
- error("Couldn't improve logo due to an error:", err);
- }
- }
- /** Exchanges the default YTM logo into BetterYTM's logo with a sick ass animation */
- function exchangeLogo() {
- addSelectorListener("navBar", ".bytm-mod-logo", {
- listener: async (logoElem) => {
- if(logoElem.classList.contains("bytm-logo-exchanged"))
- return;
- logoExchanged = true;
- logoElem.classList.add("bytm-logo-exchanged");
- const iconUrl = await getResourceUrl(mode === "development" ? "img-logo_dev" : "img-logo");
- const newLogo = document.createElement("img");
- newLogo.classList.add("bytm-mod-logo-img");
- newLogo.src = iconUrl;
- logoElem.insertBefore(newLogo, logoElem.querySelector("svg"));
- document.head.querySelectorAll<HTMLLinkElement>("link[rel=\"icon\"]").forEach((e) => {
- e.href = iconUrl;
- });
- setTimeout(() => {
- logoElem.querySelectorAll(".bytm-mod-logo-ellipse").forEach(e => e.remove());
- }, 1000);
- },
- });
- }
- /** Called whenever the avatar popover menu exists on YTM to add a BYTM config menu button to the user menu popover */
- export async function addConfigMenuOptionYTM(container: HTMLElement) {
- const cfgOptElem = document.createElement("div");
- cfgOptElem.classList.add("bytm-cfg-menu-option");
- const cfgOptItemElem = document.createElement("div");
- cfgOptItemElem.classList.add("bytm-cfg-menu-option-item");
- cfgOptItemElem.role = "button";
- cfgOptItemElem.tabIndex = 0;
- cfgOptItemElem.ariaLabel = cfgOptItemElem.title = t("open_menu_tooltip", scriptInfo.name);
- onInteraction(cfgOptItemElem, async (e: MouseEvent | KeyboardEvent) => {
- const settingsBtnElem = document.querySelector<HTMLElement>("ytmusic-nav-bar ytmusic-settings-button tp-yt-paper-icon-button");
- settingsBtnElem?.click();
- await pauseFor(20);
- if((!e.shiftKey && !e.ctrlKey) || logoExchanged)
- openCfgMenu();
- if(!logoExchanged && (e.shiftKey || e.ctrlKey))
- exchangeLogo();
- });
- const cfgOptIconElem = document.createElement("img");
- cfgOptIconElem.classList.add("bytm-cfg-menu-option-icon");
- cfgOptIconElem.src = await getResourceUrl(mode === "development" ? "img-logo_dev" : "img-logo");
- const cfgOptTextElem = document.createElement("div");
- cfgOptTextElem.classList.add("bytm-cfg-menu-option-text");
- cfgOptTextElem.textContent = t("config_menu_option", scriptInfo.name);
- cfgOptItemElem.appendChild(cfgOptIconElem);
- cfgOptItemElem.appendChild(cfgOptTextElem);
- cfgOptElem.appendChild(cfgOptItemElem);
- container.appendChild(cfgOptElem);
- improveLogo();
- log("Added BYTM-Configuration button to menu popover");
- }
- /** Called whenever the titlebar (masthead) exists on YT to add a BYTM config menu button */
- export async function addConfigMenuOptionYT(container: HTMLElement) {
- const cfgOptWrapperElem = document.createElement("div");
- cfgOptWrapperElem.classList.add("bytm-yt-cfg-menu-option", "darkreader-ignore");
- cfgOptWrapperElem.role = "button";
- cfgOptWrapperElem.tabIndex = 0;
- cfgOptWrapperElem.ariaLabel = cfgOptWrapperElem.title = t("open_menu_tooltip", scriptInfo.name);
- const cfgOptElem = document.createElement("div");
- cfgOptElem.classList.add("bytm-yt-cfg-menu-option-inner");
- const cfgOptImgElem = document.createElement("img");
- cfgOptImgElem.classList.add("bytm-yt-cfg-menu-option-icon");
- cfgOptImgElem.src = await getResourceUrl(mode === "development" ? "img-logo_dev" : "img-logo");
- const cfgOptItemElem = document.createElement("div");
- cfgOptItemElem.classList.add("bytm-yt-cfg-menu-option-item");
- cfgOptItemElem.textContent = scriptInfo.name;
- cfgOptElem.appendChild(cfgOptImgElem);
- cfgOptElem.appendChild(cfgOptItemElem);
- cfgOptWrapperElem.appendChild(cfgOptElem);
- onInteraction(cfgOptWrapperElem, openCfgMenu);
- const firstChild = container?.firstElementChild;
- if(firstChild)
- container.insertBefore(cfgOptWrapperElem, firstChild);
- else
- return error("Couldn't add config menu option to YT titlebar - couldn't find container element");
- }
- //#region anchor impr.
- /** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
- export async function addAnchorImprovements() {
- try {
- await addStyleFromResource("css-anchor_improvements");
- }
- catch(err) {
- error("Couldn't add anchor improvements CSS due to an error:", err);
- }
- //#region carousel shelves
- try {
- const preventDefault = (e: MouseEvent) => e.preventDefault();
- /** Adds anchor improvements to <ytmusic-responsive-list-item-renderer> */
- const addListItemAnchors = (items: NodeListOf<HTMLElement>) => {
- for(const item of items) {
- if(item.classList.contains("bytm-anchor-improved"))
- continue;
- item.classList.add("bytm-anchor-improved");
- const thumbnailElem = item.querySelector<HTMLElement>(".left-items");
- const titleElem = item.querySelector<HTMLAnchorElement>(".title-column .title a");
- if(!thumbnailElem || !titleElem)
- continue;
- const anchorElem = document.createElement("a");
- anchorElem.classList.add("bytm-anchor", "bytm-carousel-shelf-anchor");
- anchorElem.href = titleElem?.href ?? "#";
- anchorElem.target = "_self";
- anchorElem.role = "button";
- anchorElem.addEventListener("click", preventDefault);
- addParent(thumbnailElem, anchorElem);
- }
- };
- // home page
- addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-carousel-shelf-renderer ytmusic-responsive-list-item-renderer", {
- continuous: true,
- all: true,
- listener: addListItemAnchors,
- });
- // related tab in /watch
- addSelectorListener("body", "ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] ytmusic-responsive-list-item-renderer", {
- continuous: true,
- all: true,
- listener: addListItemAnchors,
- });
- // playlists
- addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer", {
- continuous: true,
- all: true,
- listener: addListItemAnchors,
- });
- // generic shelves
- addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer", {
- continuous: true,
- all: true,
- listener: addListItemAnchors,
- });
- }
- catch(err) {
- error("Couldn't improve carousel shelf anchors due to an error:", err);
- }
- //#region sidebar
- try {
- const addSidebarAnchors = (sidebarCont: HTMLElement) => {
- const items = sidebarCont.parentNode!.querySelectorAll<HTMLElement>("ytmusic-guide-entry-renderer tp-yt-paper-item");
- improveSidebarAnchors(items);
- return items.length;
- };
- addSelectorListener("sideBar", "#contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
- listener: (sidebarCont) => {
- const itemsAmt = addSidebarAnchors(sidebarCont);
- log(`Added anchors around ${itemsAmt} sidebar ${autoPlural("item", itemsAmt)}`);
- },
- });
- addSelectorListener("sideBarMini", "ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
- listener: (miniSidebarCont) => {
- const itemsAmt = addSidebarAnchors(miniSidebarCont);
- log(`Added anchors around ${itemsAmt} mini sidebar ${autoPlural("item", itemsAmt)}`);
- },
- });
- }
- catch(err) {
- error("Couldn't add anchors to sidebar items due to an error:", err);
- }
- }
- const sidebarPaths = [
- "/",
- "/explore",
- "/library",
- ];
- /**
- * Adds anchors to the sidebar items so they can be opened in a new tab
- * @param sidebarItem
- */
- function improveSidebarAnchors(sidebarItems: NodeListOf<HTMLElement>) {
- sidebarItems.forEach((item, i) => {
- const anchorElem = document.createElement("a");
- anchorElem.classList.add("bytm-anchor", "bytm-no-select");
- anchorElem.role = "button";
- anchorElem.target = "_self";
- anchorElem.href = sidebarPaths[i] ?? "#";
- anchorElem.ariaLabel = anchorElem.title = t("middle_click_open_tab");
- anchorElem.addEventListener("click", (e) => {
- e.preventDefault();
- });
- addParent(item, anchorElem);
- });
- }
- //#region share track par.
- /** Removes the ?si tracking parameter from share URLs */
- export async function initRemShareTrackParam() {
- const removeSiParam = (inputElem: HTMLInputElement) => {
- try {
- if(!inputElem.value.match(/(&|\?)si=/i))
- return;
- const url = new URL(inputElem.value);
- url.searchParams.delete("si");
- inputElem.value = String(url);
- log(`Removed tracking parameter from share link -> ${url}`);
- }
- catch(err) {
- warn("Couldn't remove tracking parameter from share link due to error:", err);
- }
- };
- const [sharePanelSel, inputSel] = (() => {
- switch(getDomain()) {
- case "ytm": return ["tp-yt-paper-dialog ytmusic-unified-share-panel-renderer", "input#share-url"];
- case "yt": return ["ytd-unified-share-panel-renderer", "input#share-url"];
- }
- })();
- addSelectorListener("body", sharePanelSel, {
- listener: (sharePanelEl) => {
- const obs = new MutationObserver(() => {
- const inputElem = sharePanelEl.querySelector<HTMLInputElement>(inputSel);
- inputElem && removeSiParam(inputElem);
- });
- obs.observe(sharePanelEl, {
- childList: true,
- subtree: true,
- characterData: true,
- attributeFilter: ["aria-hidden", "aria-checked", "checked"],
- });
- },
- });
- }
- //#region fix spacing
- /** Applies global CSS to fix various spacings */
- export async function fixSpacing() {
- if(!await addStyleFromResource("css-fix_spacing"))
- error("Couldn't fix spacing");
- }
- //#region ab.queue btns
- export async function initAboveQueueBtns() {
- const { scrollToActiveSongBtn, clearQueueBtn } = getFeatures();
- const contBtns = [
- {
- condition: scrollToActiveSongBtn,
- id: "scroll-to-active",
- resourceName: "icon-skip_to",
- titleKey: "scroll_to_playing",
- async interaction(evt: KeyboardEvent | MouseEvent) {
- const activeItem = document.querySelector<HTMLElement>("#side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"loading\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"playing\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"paused\"]");
- if(!activeItem)
- return;
- activeItem.scrollIntoView({
- behavior: evt.shiftKey ? "instant" : "smooth",
- block: evt.ctrlKey || evt.altKey ? "start" : "center",
- inline: "center",
- });
- },
- },
- {
- condition: clearQueueBtn,
- id: "clear-queue",
- resourceName: "icon-clear_list",
- titleKey: "clear_list",
- async interaction(evt: KeyboardEvent | MouseEvent) {
- try {
- // TODO: better confirmation dialog?
- if(evt.shiftKey || confirm(t("clear_list_confirm"))) {
- const url = new URL(location.href);
- url.searchParams.delete("list");
- url.searchParams.set("time_continue", String(await getVideoTime(0)));
- location.assign(url);
- }
- }
- catch(err) {
- error("Couldn't clear queue due to an error:", err);
- }
- },
- },
- ];
- if(!contBtns.some(b => Boolean(b.condition)))
- return;
- addSelectorListener("sidePanel", "ytmusic-tab-renderer ytmusic-queue-header-renderer #buttons", {
- async listener(rightBtnsEl) {
- try {
- const aboveQueueBtnCont = document.createElement("div");
- aboveQueueBtnCont.id = "bytm-above-queue-btn-cont";
- addParent(rightBtnsEl, aboveQueueBtnCont);
- const headerEl = rightBtnsEl.closest<HTMLElement>("ytmusic-queue-header-renderer");
- if(!headerEl)
- return error("Couldn't find queue header element while adding above queue buttons");
- siteEvents.on("fullscreenToggled", (isFullscreen) => {
- headerEl.classList[isFullscreen ? "add" : "remove"]("hidden");
- });
- if(!await addStyleFromResource("css-above_queue_btns"))
- return error("Couldn't add CSS for above queue buttons");
- const wrapperElem = document.createElement("div");
- wrapperElem.id = "bytm-above-queue-btn-wrapper";
- for(const item of contBtns) {
- if(Boolean(item.condition) === false)
- continue;
- const btnElem = await createCircularBtn({
- resourceName: item.resourceName as ResourceKey & `icon-${string}`,
- onClick: item.interaction,
- title: t(item.titleKey),
- });
- btnElem.id = `bytm-${item.id}-btn`;
- btnElem.classList.add("ytmusic-player-bar", "bytm-generic-btn", "bytm-above-queue-btn");
- wrapperElem.appendChild(btnElem);
- }
- rightBtnsEl.insertAdjacentElement("beforebegin", wrapperElem);
- }
- catch(err) {
- error("Couldn't add above queue buttons due to an error:", err);
- }
- },
- });
- }
- //#region thumb.overlay
- /** To be changed when the toggle button is pressed - used to invert the state of "showOverlay" */
- let invertOverlay = false;
- export async function initThumbnailOverlay() {
- const toggleBtnShown = getFeature("thumbnailOverlayToggleBtnShown");
- if(getFeature("thumbnailOverlayBehavior") === "never" && !toggleBtnShown)
- return;
- // so the script init doesn't keep waiting until a /watch page is loaded
- waitVideoElementReady().then(() => {
- const playerSelector = "ytmusic-player#player";
- const playerEl = document.querySelector<HTMLElement>(playerSelector);
- if(!playerEl)
- return error("Couldn't find video player element while adding thumbnail overlay");
- /** Checks and updates the overlay and toggle button states based on the current song type (yt video or ytm song) */
- const updateOverlayVisibility = async () => {
- if(!domLoaded)
- return;
- const behavior = getFeature("thumbnailOverlayBehavior");
- let showOverlay = behavior === "always";
- const isVideo = currentMediaType() === "video";
- if(behavior === "videosOnly" && isVideo)
- showOverlay = true;
- else if(behavior === "songsOnly" && !isVideo)
- showOverlay = true;
- showOverlay = invertOverlay ? !showOverlay : showOverlay;
- const overlayElem = document.querySelector<HTMLElement>("#bytm-thumbnail-overlay");
- const thumbElem = document.querySelector<HTMLElement>("#bytm-thumbnail-overlay-img");
- const indicatorElem = document.querySelector<HTMLElement>("#bytm-thumbnail-overlay-indicator");
- if(overlayElem)
- overlayElem.style.display = showOverlay ? "block" : "none";
- if(thumbElem)
- thumbElem.ariaHidden = String(!showOverlay);
- if(indicatorElem) {
- indicatorElem.style.display = showOverlay ? "block" : "none";
- indicatorElem.ariaHidden = String(!showOverlay);
- }
- if(getFeature("thumbnailOverlayToggleBtnShown")) {
- addSelectorListener("playerBarMiddleButtons", "#bytm-thumbnail-overlay-toggle", {
- async listener(toggleBtnElem) {
- const toggleBtnImgElem = toggleBtnElem.querySelector<HTMLImageElement>("img");
- if(toggleBtnImgElem)
- toggleBtnImgElem.src = await getResourceUrl(`icon-image${showOverlay ? "_filled" : ""}` as "icon-image" | "icon-image_filled");
- if(toggleBtnElem)
- toggleBtnElem.ariaLabel = toggleBtnElem.title = t(`thumbnail_overlay_toggle_btn_tooltip${showOverlay ? "_hide" : "_show"}`);
- },
- });
- }
- };
- const applyThumbUrl = async (watchId: string) => {
- try {
- const thumbUrl = await getBestThumbnailUrl(watchId);
- if(thumbUrl) {
- const toggleBtnElem = document.querySelector<HTMLAnchorElement>("#bytm-thumbnail-overlay-toggle");
- const thumbImgElem = document.querySelector<HTMLImageElement>("#bytm-thumbnail-overlay-img");
- if(toggleBtnElem)
- toggleBtnElem.href = thumbUrl;
- if(thumbImgElem)
- thumbImgElem.src = thumbUrl;
- log("Applied thumbnail URL to overlay:", thumbUrl);
- }
- else error("Couldn't get thumbnail URL for watch ID", watchId);
- }
- catch(err) {
- error("Couldn't apply thumbnail URL to overlay due to an error:", err);
- }
- };
- const unsubWatchIdChanged = siteEvents.on("watchIdChanged", (watchId) => {
- unsubWatchIdChanged();
- addSelectorListener("body", "#bytm-thumbnail-overlay", {
- listener: () => {
- applyThumbUrl(watchId);
- updateOverlayVisibility();
- },
- });
- });
- const createElements = async () => {
- try {
- // overlay
- const overlayElem = document.createElement("div");
- overlayElem.id = "bytm-thumbnail-overlay";
- overlayElem.title = ""; // prevent child titles from propagating
- overlayElem.classList.add("bytm-no-select");
- overlayElem.style.display = "none";
- let indicatorElem: HTMLImageElement | undefined;
- if(getFeature("thumbnailOverlayShowIndicator")) {
- indicatorElem = document.createElement("img");
- indicatorElem.id = "bytm-thumbnail-overlay-indicator";
- indicatorElem.src = await getResourceUrl("icon-image");
- indicatorElem.role = "presentation";
- indicatorElem.title = indicatorElem.ariaLabel = t("thumbnail_overlay_indicator_tooltip");
- indicatorElem.ariaHidden = "true";
- indicatorElem.style.display = "none";
- indicatorElem.style.opacity = String(getFeature("thumbnailOverlayIndicatorOpacity") / 100);
- }
-
- const thumbImgElem = document.createElement("img");
- thumbImgElem.id = "bytm-thumbnail-overlay-img";
- thumbImgElem.role = "presentation";
- thumbImgElem.ariaHidden = "true";
- thumbImgElem.style.objectFit = getFeature("thumbnailOverlayImageFit");
-
- overlayElem.appendChild(thumbImgElem);
- playerEl.appendChild(overlayElem);
- indicatorElem && playerEl.appendChild(indicatorElem);
- siteEvents.on("watchIdChanged", async (watchId) => {
- invertOverlay = false;
- applyThumbUrl(watchId);
- updateOverlayVisibility();
- });
- const params = new URL(location.href).searchParams;
- if(params.has("v")) {
- applyThumbUrl(params.get("v")!);
- updateOverlayVisibility();
- }
-
- // toggle button
- if(toggleBtnShown) {
- const toggleBtnElem = createRipple(document.createElement("a"));
- toggleBtnElem.id = "bytm-thumbnail-overlay-toggle";
- toggleBtnElem.role = "button";
- toggleBtnElem.tabIndex = 0;
- toggleBtnElem.classList.add("ytmusic-player-bar", "bytm-generic-btn", "bytm-no-select");
-
- onInteraction(toggleBtnElem, (e) => {
- if(e.shiftKey)
- return openInTab(toggleBtnElem.href, false);
- invertOverlay = !invertOverlay;
- updateOverlayVisibility();
- });
-
- const imgElem = document.createElement("img");
- imgElem.classList.add("bytm-generic-btn-img");
-
- toggleBtnElem.appendChild(imgElem);
-
- addSelectorListener("playerBarMiddleButtons", "ytmusic-like-button-renderer#like-button-renderer", {
- listener: (likeContainer) => likeContainer.insertAdjacentElement("afterend", toggleBtnElem),
- });
- }
- log("Added thumbnail overlay");
- }
- catch(err) {
- error("Couldn't create thumbnail overlay elements due to an error:", err);
- }
- };
- addSelectorListener("mainPanel", playerSelector, {
- listener(playerEl) {
- if(playerEl.getAttribute("player-ui-state") === "INACTIVE") {
- const obs = new MutationObserver(() => {
- if(playerEl.getAttribute("player-ui-state") === "INACTIVE")
- return;
- createElements();
- obs.disconnect();
- });
- obs.observe(playerEl, {
- attributes: true,
- attributeFilter: ["player-ui-state"],
- });
- }
- else
- createElements();
- },
- });
- });
- }
- //#region idle hide cursor
- export async function initHideCursorOnIdle() {
- addSelectorListener("mainPanel", "ytmusic-player#player", {
- listener(vidContainer) {
- const overlaySelector = "ytmusic-player #song-media-window";
- const overlayElem = document.querySelector<HTMLElement>(overlaySelector);
- if(!overlayElem)
- return warn("Couldn't find overlay element while initializing cursor hiding");
- /** Timer after which the cursor is hidden */
- let cursorHideTimer: ReturnType<typeof setTimeout>;
- /** Timer for the opacity transition while switching to the hidden state */
- let hideTransTimer: ReturnType<typeof setTimeout> | undefined;
- const hide = () => {
- if(!getFeature("hideCursorOnIdle"))
- return;
- if(vidContainer.classList.contains("bytm-cursor-hidden"))
- return;
- overlayElem.style.opacity = ".000001 !important";
- hideTransTimer = setTimeout(() => {
- overlayElem.style.display = "none";
- vidContainer.style.cursor = "none";
- vidContainer.classList.add("bytm-cursor-hidden");
- hideTransTimer = undefined;
- }, 200);
- };
- const show = () => {
- hideTransTimer && clearTimeout(hideTransTimer);
- if(!vidContainer.classList.contains("bytm-cursor-hidden"))
- return;
- vidContainer.classList.remove("bytm-cursor-hidden");
- vidContainer.style.cursor = "initial";
- overlayElem.style.display = "initial";
- overlayElem.style.opacity = "1 !important";
- };
- const cursorHideTimerCb = () =>
- cursorHideTimer = setTimeout(hide, getFeature("hideCursorOnIdleDelay") * 1000);
- const onMove = () => {
- cursorHideTimer && clearTimeout(cursorHideTimer);
- show();
- cursorHideTimerCb();
- };
- vidContainer.addEventListener("mouseenter", onMove);
- vidContainer.addEventListener("mousemove", debounce(onMove, 200, "rising"));
- vidContainer.addEventListener("mouseleave", () => {
- cursorHideTimer && clearTimeout(cursorHideTimer);
- hideTransTimer && clearTimeout(hideTransTimer);
- hide();
- });
- vidContainer.addEventListener("click", () => {
- show();
- cursorHideTimerCb();
- setTimeout(hide, 3000);
- });
- log("Initialized cursor hiding on idle");
- },
- });
- }
- //#region fix HDR
- /** Prevents visual issues when using HDR */
- export async function fixHdrIssues() {
- if(!await addStyleFromResource("css-fix_hdr"))
- error("Couldn't load stylesheet to fix HDR issues");
- else
- log("Fixed HDR issues");
- }
- //#region show vote nums
- /** Shows the amount of likes and dislikes on the current song */
- export async function initShowVotes() {
- addSelectorListener("playerBar", ".middle-controls-buttons ytmusic-like-button-renderer", {
- async listener(voteCont: HTMLElement): Promise<void> {
- try {
- const watchId = getWatchId();
- if(!watchId) {
- await siteEvents.once("watchIdChanged");
- return initShowVotes();
- }
- const voteObj = await fetchVideoVotes(watchId);
- if(!voteObj || !("likes" in voteObj) || !("dislikes" in voteObj) || !("rating" in voteObj))
- return error("Couldn't fetch votes from the Return YouTube Dislike API");
- if(getFeature("showVotes")) {
- addVoteNumbers(voteCont, voteObj);
- siteEvents.on("watchIdChanged", async (watchId) => {
- const labelLikes = document.querySelector<HTMLElement>("ytmusic-like-button-renderer .bytm-vote-label.likes");
- const labelDislikes = document.querySelector<HTMLElement>("ytmusic-like-button-renderer .bytm-vote-label.dislikes");
- if(!labelLikes || !labelDislikes)
- return error("Couldn't find vote label elements while updating like and dislike counts");
- if(labelLikes.dataset.watchId === watchId && labelDislikes.dataset.watchId === watchId)
- return log("Vote labels already updated for this video");
- const voteObj = await fetchVideoVotes(watchId);
- if(!voteObj || !("likes" in voteObj) || !("dislikes" in voteObj) || !("rating" in voteObj))
- return error("Couldn't fetch votes from the Return YouTube Dislike API");
- const likesLabelText = tp("vote_label_likes", voteObj.likes, formatVoteNumber(voteObj.likes, "long"));
- const dislikesLabelText = tp("vote_label_dislikes", voteObj.dislikes, formatVoteNumber(voteObj.dislikes, "long"));
- labelLikes.dataset.watchId = getWatchId() ?? "";
- labelLikes.textContent = formatVoteNumber(voteObj.likes);
- labelLikes.title = labelLikes.ariaLabel = likesLabelText;
- labelDislikes.textContent = formatVoteNumber(voteObj.dislikes);
- labelDislikes.title = labelDislikes.ariaLabel = dislikesLabelText;
- labelDislikes.dataset.watchId = getWatchId() ?? "";
- addSelectorListener("playerBar", "ytmusic-like-button-renderer#like-button-renderer", {
- listener: (bar) => upsertVoteBtnLabels(bar, likesLabelText, dislikesLabelText),
- });
- });
- }
- }
- catch(err) {
- error("Couldn't initialize show votes feature due to an error:", err);
- }
- }
- });
- }
- function addVoteNumbers(voteCont: HTMLElement, voteObj: VideoVotesObj) {
- const likeBtn = voteCont.querySelector<HTMLElement>("#button-shape-like");
- const dislikeBtn = voteCont.querySelector<HTMLElement>("#button-shape-dislike");
- if(!likeBtn || !dislikeBtn)
- return error("Couldn't find like or dislike button while adding vote numbers");
- const createLabel = (amount: number, type: "likes" | "dislikes"): HTMLElement => {
- const label = document.createElement("span");
- label.classList.add("bytm-vote-label", "bytm-no-select", type);
- label.textContent = String(formatVoteNumber(amount));
- label.title = label.ariaLabel = tp(`vote_label_${type}`, amount, formatVoteNumber(amount, "long"));
- label.dataset.watchId = getWatchId() ?? "";
- label.addEventListener("click", (e) => {
- e.preventDefault();
- e.stopPropagation();
- (type === "likes" ? likeBtn : dislikeBtn).querySelector("button")?.click();
- });
- return label;
- };
- addStyleFromResource("css-show_votes")
- .catch((e) => error("Couldn't add CSS for show votes feature due to an error:", e));
- const likeLblEl = createLabel(voteObj.likes, "likes");
- likeBtn.insertAdjacentElement("afterend", likeLblEl);
- const dislikeLblEl = createLabel(voteObj.dislikes, "dislikes");
- dislikeBtn.insertAdjacentElement("afterend", dislikeLblEl);
- upsertVoteBtnLabels(voteCont, likeLblEl.title, dislikeLblEl.title);
- log("Added vote number labels to like and dislike buttons");
- }
- /** Formats a number formatted based on the config or the passed {@linkcode notation} */
- function formatVoteNumber(num: number, notation?: NumberNotation) {
- return num.toLocaleString(
- getLocale().replace(/_/g, "-"),
- (notation ?? getFeature("showVotesFormat")) === "short"
- ? {
- notation: "compact",
- compactDisplay: "short",
- maximumFractionDigits: 1,
- }
- : {
- style: "decimal",
- maximumFractionDigits: 0,
- },
- );
- }
- /** Updates or inserts the labels on the native like and dislike buttons */
- function upsertVoteBtnLabels(parentEl: HTMLElement, likesLabelText: string, dislikesLabelText: string) {
- const likeBtn = parentEl.querySelector<HTMLElement>("#button-shape-like button");
- const dislikeBtn = parentEl.querySelector<HTMLElement>("#button-shape-dislike button");
- if(likeBtn)
- likeBtn.title = likeBtn.ariaLabel = likesLabelText;
- if(dislikeBtn)
- dislikeBtn.title = dislikeBtn.ariaLabel = dislikesLabelText;
- };
|