Ver código fonte

feat: anchor improvements

Sv443 1 ano atrás
pai
commit
ab562424b1
5 arquivos alterados com 96 adições e 14 exclusões
  1. 2 3
      src/features/index.ts
  2. 10 0
      src/features/layout.css
  3. 61 2
      src/features/layout.ts
  4. 4 3
      src/index.ts
  5. 19 6
      src/utils.ts

+ 2 - 3
src/features/index.ts

@@ -3,7 +3,7 @@ import { scriptInfo } from "../constants";
 export * from "./input";
 export * from "./layout";
 export * from "./lyrics";
-export { initMenu } from "./menu/menu"; // TODO
+export { initMenu } from "./menu/menu";
 export * from "./menu/menu_old";
 
 /** Contains all possible features with their default values and other config */
@@ -40,11 +40,10 @@ export const featInfo = {
     default: false,
   },
   anchorImprovements: {
-    desc: "TODO: Make it so middle clicking a song to open it in a new tab is easier",
+    desc: "Make it so middle clicking a song to open it in a new tab is easier",
     type: "toggle",
     category: "input",
     default: true,
-    visible: false,
   },
 
   //#SECTION layout

+ 10 - 0
src/features/layout.css

@@ -120,3 +120,13 @@ yt-multi-page-menu-section-renderer.ytd-multi-page-menu-renderer {
 ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown[data-bytm-hidden=true] {
   display: none !important;
 }
+
+/* #MARKER anchor improvements */
+
+ytmusic-responsive-list-item-renderer .left-items {
+  margin-right: 0 !important;
+}
+
+.bytm-carousel-shelf-anchor {
+  margin: 0 var(--ytmusic-responsive-list-item-thumbnail-margin-right, 16px) 0 0;
+}

+ 61 - 2
src/features/layout.ts

@@ -2,7 +2,7 @@ import type { Event } from "@billjs/event-emitter";
 import type { FeatureConfig } from "../types";
 import { scriptInfo } from "../constants";
 import { getFeatures } from "../config";
-import { addGlobalStyle, autoPlural, error, getAssetUrl, insertAfter, log, onSelectorExists, openInNewTab, pauseFor } from "../utils";
+import { addGlobalStyle, addParent, autoPlural, error, getAssetUrl, insertAfter, log, onSelectorExists, openInNewTab, pauseFor } from "../utils";
 import { getEvtData, siteEvents } from "../events";
 import { openMenu } from "./menu/menu_old";
 import { getGeniusUrl, createLyricsBtn, sanitizeArtists, sanitizeSong, getLyricsCacheEntry } from "./lyrics";
@@ -282,6 +282,65 @@ async function addQueueButtons(queueItem: HTMLElement) {
 //#MARKER better clickable stuff
 
 // TODO: account for the fact initially the elements might not exist, if the site was opened directly with the /watch path
+/** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
 export function addAnchorImprovements() {
-  void 0;
+  /** Only adds anchor improvements for carousel shelves that contain the regular list-item-renderer, not the two-row-item-renderer */
+  const conditionalAnchorImprovements = (el: HTMLElement) => {
+    const listItemRenderer = el.querySelector("ytmusic-responsive-list-item-renderer");
+    if(listItemRenderer) {
+      const itemsElem = el.querySelector<HTMLElement>("ul#items");
+      if(itemsElem) {
+        log("Adding anchor improvements to carousel shelf");
+        improveCarouselAnchors(itemsElem);
+      }
+    }
+  };
+
+  // initial three shelves aren't included in the event fire
+  onSelectorExists("ytmusic-carousel-shelf-renderer", () => {
+    const carouselShelves = document.body.querySelectorAll<HTMLElement>("ytmusic-carousel-shelf-renderer");
+    carouselShelves.forEach(conditionalAnchorImprovements);
+  });
+
+  // every shelf that's loaded by scrolling:
+  siteEvents.on("carouselShelvesChanged", (evt) => {
+    const { addedNodes, removedNodes } = getEvtData<Record<"addedNodes" | "removedNodes", NodeListOf<HTMLElement>>>(evt);
+    void removedNodes;
+
+    if(addedNodes.length > 0)
+      addedNodes.forEach(conditionalAnchorImprovements);
+  });
+}
+
+/**
+ * Actually adds the anchor improvements to carousel shelf items
+ * @param itemsElement The container with the selector `ul#items` inside of each `ytmusic-carousel`
+ */
+function improveCarouselAnchors(itemsElement: HTMLElement) {
+  if(itemsElement.classList.contains("bytm-anchors-improved"))
+    return;
+  itemsElement.classList.add("bytm-anchors-improved");
+
+  for(const listItem of itemsElement.querySelectorAll<HTMLElement>("ytmusic-responsive-list-item-renderer")) {
+    try {
+      const thumbnailElem = listItem.querySelector<HTMLElement>(".left-items");
+      const titleElem = listItem.querySelector<HTMLAnchorElement>(".title-column yt-formatted-string.title a");
+
+      if(!thumbnailElem || !titleElem) {
+        error("Couldn't add carousel shelf anchor improvements because either the thumbnail or title element couldn't be found");
+        continue;
+      }
+
+      const thumbnailAnchor = document.createElement("a");
+      thumbnailAnchor.className = "bytm-carousel-shelf-anchor";
+      thumbnailAnchor.href = titleElem.href;
+      thumbnailAnchor.target = "_self";
+      thumbnailAnchor.role = "button";
+
+      addParent(thumbnailElem, thumbnailAnchor);
+    }
+    catch(err) {
+      error("Couldn't add anchor improvements due to error:", err);
+    }
+  }
 }

+ 4 - 3
src/index.ts

@@ -1,6 +1,6 @@
 import { loadFeatureConf } from "./config";
 import { logLevel, scriptInfo } from "./constants";
-import { addGlobalStyle, error, getAssetUrl, getDomain, initSelectorExistsCheck, log, onSelectorExists, precacheImage, setLogLevel } from "./utils";
+import { addGlobalStyle, error, getAssetUrl, getDomain, initSelectorExistsCheck, log, onSelectorExists, precacheImages, setLogLevel } from "./utils";
 import { initSiteEvents } from "./events";
 import {
   // layout
@@ -56,8 +56,9 @@ async function init() {
   try {
     document.addEventListener("DOMContentLoaded", onDomLoad);
 
-    Promise.all(precacheImgs.map(imgSrc => precacheImage(imgSrc)))
-      .then(() => log(`Pre-cached ${precacheImgs.length} images`));
+    precacheImages(precacheImgs)
+      .then(() => log(`Pre-cached ${precacheImgs.length} images`))
+      .catch((e) => error(`Pre-caching error: ${e}`));
   }
   catch(err) {
     console.error(`${scriptInfo.name} - General Error:`, err);

+ 19 - 6
src/utils.ts

@@ -153,6 +153,19 @@ export function insertAfter(beforeNode: HTMLElement, afterNode: HTMLElement) {
   return afterNode;
 }
 
+/** Adds a parent container around the provided element - returns the new parent node */
+export function addParent(element: HTMLElement, newParent: HTMLElement) {
+  const oldParent = element.parentNode;
+
+  if(!oldParent)
+    throw new Error("Element doesn't have a parent node");
+
+  oldParent.replaceChild(newParent, element);
+  newParent.appendChild(element);
+
+  return newParent;
+}
+
 /**
  * Adds global CSS style through a `<style>` element in the document's `<head>`
  * @param style CSS string
@@ -210,16 +223,16 @@ export function initSelectorExistsCheck() {
   log("Initialized \"selector exists\" MutationObserver");
 }
 
-/** Preloads an image by URL so it can be loaded from cache later on */
-export function precacheImage(src: string, rejects = false) {
-  return new Promise((res, rej) => {
+/** Preloads an array of image URLs so they can be loaded instantly from cache later on */
+export function precacheImages(srcUrls: string[], rejects = false) {
+  const promises = srcUrls.map(src => new Promise((res, rej) => {
     const image = new Image();
-
     image.src = src;
-
     image.addEventListener("load", () => res(image));
     image.addEventListener("error", () => rejects && rej(`Failed to preload image with URL '${src}'`));
-  });
+  }));
+
+  return Promise.allSettled(promises);
 }
 
 //#SECTION misc