import { compress, decompress, fetchAdvanced, isDomLoaded, pauseFor, setInnerHtmlUnsafe, type Stringifiable } from "@sv443-network/userutils"; import { addStyle, addStyleFromResource, getResourceUrl, reloadTab, setGlobalCssVars, warn } from "./utils/index.js"; import { clearConfig, getFeatures, initConfig } from "./config.js"; import { buildNumber, compressionFormat, defaultLogLevel, mode, scriptInfo } from "./constants.js"; import { dbg, error, getDomain, info, getSessionId, log, setLogLevel, initTranslations, setLocale } from "./utils/index.js"; import { initSiteEvents } from "./siteEvents.js"; import { emitInterface, initInterface, initPlugins } from "./interface.js"; import { initObservers, addSelectorListener, globservers } from "./observers.js"; import { downloadData, getStoreSerializer } from "./serializer.js"; import { MarkdownDialog } from "./components/MarkdownDialog.js"; import { getWelcomeDialog } from "./dialogs/welcome.js"; import { showPrompt } from "./dialogs/prompt.js"; import { // layout category: addWatermark, initRemShareTrackParam, fixSpacing, initThumbnailOverlay, initHideCursorOnIdle, fixHdrIssues, initShowVotes, // volume category: initVolumeFeatures, // song lists category: initQueueButtons, initAboveQueueBtns, // behavior category: initBeforeUnloadHook, enableDiscardBeforeUnload, initAutoCloseToasts, initRememberSongTime, initAutoScrollToActiveSong, // input category: initArrowKeySkip, initFrameSkip, initSiteSwitch, addAnchorImprovements, initNumKeysSkip, initAutoLike, // lyrics category: addPlayerBarLyricsBtn, initLyricsCache, // integrations category: disableDarkReader, fixSponsorBlock, fixPlayerPageTheming, fixThemeSong, // general category: initVersionCheck, // menu: addConfigMenuOptionYT, addConfigMenuOptionYTM, } from "./features/index.js"; // import { getAllDataExImDialog } from "./dialogs/allDataExIm.js"; //#region cns. watermark { // console watermark with sexy gradient const [styleGradient, gradientContBg] = (() => { switch(mode) { case "production": return ["background: rgb(165, 57, 36); background: linear-gradient(90deg, rgb(154, 31, 103) 0%, rgb(135, 31, 31) 40%, rgb(165, 57, 36) 100%);", "rgb(165, 57, 36)"]; case "development": return ["background: rgb(72, 66, 178); background: linear-gradient(90deg, rgb(38, 160, 172) 0%, rgb(33, 48, 158) 40%, rgb(72, 66, 178) 100%);", "rgb(72, 66, 178)"]; } })(); const styleCommon = "color: #fff; font-size: 1.3rem;"; const poweredBy = `Powered by: ─ Lots of ambition and dedication ─ My song metadata API: https://api.sv443.net/geniurl ─ My userscript utility library: https://github.com/Sv443-Network/UserUtils ─ This library for semver comparison: https://github.com/omichelsen/compare-versions ─ This TrustedTypes-compatible HTML sanitization library: https://github.com/cure53/DOMPurify ─ This markdown parser library: https://github.com/markedjs/marked ─ This tiny event listener library: https://github.com/ai/nanoevents ─ TypeScript and the tslib runtime: https://github.com/microsoft/TypeScript ─ The Cousine font: https://fonts.google.com/specimen/Cousine`; console.log( `\ %c${scriptInfo.name}%cv${scriptInfo.version}%c • ${scriptInfo.namespace}%c Build #${buildNumber}${mode === "development" ? " (dev mode)" : ""} %c${poweredBy}`, `${styleCommon} ${styleGradient} font-weight: bold; padding-left: 6px; padding-right: 6px;`, `${styleCommon} background-color: ${gradientContBg}; padding-left: 8px; padding-right: 8px;`, "color: #fff; font-size: 1.2rem;", "padding: initial; font-size: 0.9rem;", "padding: initial; font-size: 1rem;", ); } //#region preInit /** Stuff that needs to be called ASAP, before anything async happens */ function preInit() { try { const unsupportedHandlers = [ "FireMonkey", ]; if(unsupportedHandlers.includes(GM?.info?.scriptHandler ?? "_")) return showPrompt({ type: "alert", message: `BetterYTM does not work when using ${GM.info.scriptHandler} as the userscript manager extension and will be disabled.\nI recommend using either ViolentMonkey, TamperMonkey or GreaseMonkey.`, denyBtnText: "Close" }); log("Session ID:", getSessionId()); initInterface(); setLogLevel(defaultLogLevel); if(getDomain() === "ytm") initBeforeUnloadHook(); init(); } catch(err) { return error("Fatal pre-init error:", err); } } //#region init async function init() { try { const domain = getDomain(); const features = await initConfig(); setLogLevel(features.logLevel); await initLyricsCache(); await initTranslations(features.locale ?? "en-US"); setLocale(features.locale ?? "en-US"); try { initPlugins(); } catch(err) { error("Plugin loading error:", err); emitInterface("bytm:fatalError", "Error while loading plugins"); } if(features.disableBeforeUnloadPopup && domain === "ytm") enableDiscardBeforeUnload(); if(features.rememberSongTime) initRememberSongTime(); if(!isDomLoaded()) document.addEventListener("DOMContentLoaded", onDomLoad, { once: true }); else onDomLoad(); } catch(err) { error("Fatal error:", err); } } //#region onDomLoad /** Called when the DOM has finished loading and can be queried and altered by the userscript */ async function onDomLoad() { const domain = getDomain(); const feats = getFeatures(); const ftInit = [] as [string, Promise][]; // for being able to apply domain-specific styles (prefix any CSS selector with "body.bytm-dom-yt" or "body.bytm-dom-ytm") document.body.classList.add(`bytm-dom-${domain}`); try { initGlobalCss(); initObservers(); initSvgSpritesheet(); Promise.allSettled([ injectCssBundle(), initVersionCheck(), ]); } catch(err) { error("Encountered error in feature pre-init:", err); } log(`DOM loaded and feature pre-init finished, now initializing all features for domain "${domain}"...`); try { //#region welcome dlg if(typeof await GM.getValue("bytm-installed") !== "string") { // open welcome menu with language selector const dlg = await getWelcomeDialog(); dlg.on("close", () => GM.setValue("bytm-installed", JSON.stringify({ timestamp: Date.now(), version: scriptInfo.version }))); info("Showing welcome menu"); await dlg.open(); } if(domain === "ytm") { //#region (ytm) layout if(feats.watermarkEnabled) ftInit.push(["addWatermark", addWatermark()]); if(feats.fixSpacing) ftInit.push(["fixSpacing", fixSpacing()]); ftInit.push(["thumbnailOverlay", initThumbnailOverlay()]); if(feats.hideCursorOnIdle) ftInit.push(["hideCursorOnIdle", initHideCursorOnIdle()]); if(feats.fixHdrIssues) ftInit.push(["fixHdrIssues", fixHdrIssues()]); if(feats.showVotes) ftInit.push(["showVotes", initShowVotes()]); //#region (ytm) volume ftInit.push(["volumeFeatures", initVolumeFeatures()]); //#region (ytm) song lists if(feats.lyricsQueueButton || feats.deleteFromQueueButton) ftInit.push(["queueButtons", initQueueButtons()]); ftInit.push(["aboveQueueBtns", initAboveQueueBtns()]); //#region (ytm) behavior if(feats.closeToastsTimeout > 0) ftInit.push(["autoCloseToasts", initAutoCloseToasts()]); ftInit.push(["autoScrollToActiveSongMode", initAutoScrollToActiveSong()]); //#region (ytm) input ftInit.push(["arrowKeySkip", initArrowKeySkip()]); ftInit.push(["frameSkip", initFrameSkip()]); if(feats.anchorImprovements) ftInit.push(["anchorImprovements", addAnchorImprovements()]); ftInit.push(["numKeysSkip", initNumKeysSkip()]); //#region (ytm) lyrics if(feats.geniusLyrics) ftInit.push(["playerBarLyricsBtn", addPlayerBarLyricsBtn()]); // #region (ytm) integrations if(feats.sponsorBlockIntegration) ftInit.push(["sponsorBlockIntegration", fixSponsorBlock()]); const hideThemeSongLogo = addStyleFromResource("css-hide_themesong_logo"); if(feats.themeSongIntegration) ftInit.push(["themeSongIntegration", Promise.allSettled([fixThemeSong(), hideThemeSongLogo])]); else ftInit.push(["themeSongIntegration", Promise.allSettled([fixPlayerPageTheming(), hideThemeSongLogo])]); } //#region (ytm+yt) cfg menu try { if(domain === "ytm") { addSelectorListener("popupContainer", "tp-yt-iron-dropdown #contentWrapper ytmusic-multi-page-menu-renderer #container", { listener: addConfigMenuOptionYTM, }); } else if(domain === "yt") { addSelectorListener<0, "yt">("ytGuide", "#sections ytd-guide-section-renderer:nth-child(5) #items ytd-guide-entry-renderer:nth-child(1)", { listener: (el) => el.parentElement && addConfigMenuOptionYT(el.parentElement), }); } } catch(err) { error("Couldn't add config menu option:", err); } if(["ytm", "yt"].includes(domain)) { //#region general ftInit.push(["initSiteEvents", initSiteEvents()]); //#region (ytm+yt) layout if(feats.removeShareTrackingParamSites && (feats.removeShareTrackingParamSites === domain || feats.removeShareTrackingParamSites === "all")) ftInit.push(["initRemShareTrackParam", initRemShareTrackParam()]); //#region (ytm+yt) input ftInit.push(["siteSwitch", initSiteSwitch(domain)]); if(feats.autoLikeChannels) ftInit.push(["autoLikeChannels", initAutoLike()]); //#region (ytm+yt) integrations if(feats.disableDarkReaderSites !== "none") ftInit.push(["disableDarkReaderSites", disableDarkReader()]); } emitInterface("bytm:featureInitStarted"); const initStartTs = Date.now(); // wait for feature init or timeout (in case an init function is hung up on a promise) await Promise.race([ pauseFor(feats.initTimeout > 0 ? feats.initTimeout * 1000 : 8_000), Promise.allSettled( ftInit.map(([name, prom]) => new Promise(async (res) => { const v = await prom; emitInterface("bytm:featureInitialized", name); res(v); }) ) ), ]); emitInterface("bytm:ready"); info(`Done initializing ${ftInit.length} features after ${Math.floor(Date.now() - initStartTs)}ms`); try { registerDevCommands(); } catch(e) { warn("Couldn't register dev menu commands:", e); } try { runDevTreatments(); } catch(e) { warn("Couldn't run dev treatments:", e); } } catch(err) { error("Feature error:", err); emitInterface("bytm:fatalError", "Error while initializing features"); } } //#region css /** Inserts the bundled CSS files imported throughout the script into a