import { compress, decompress, pauseFor, type Stringifiable } from "@sv443-network/userutils"; import { addStyleFromResource, domLoaded, warn } from "./utils/index.js"; import { clearConfig, fixMissingCfgKeys, getFeatures, initConfig, setFeatures } from "./config.js"; import { buildNumber, compressionFormat, defaultLogLevel, mode, scriptInfo } from "./constants.js"; import { 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 { getWelcomeDialog } from "./dialogs/index.js"; import type { FeatureConfig } from "./types.js"; import { // layout addWatermark, removeUpgradeTab, initRemShareTrackParam, fixSpacing, initThumbnailOverlay, initHideCursorOnIdle, fixHdrIssues, // volume initVolumeFeatures, // song lists initQueueButtons, initAboveQueueBtns, // behavior initBeforeUnloadHook, disableBeforeUnload, initAutoCloseToasts, initRememberSongTime, disableDarkReader, // input initArrowKeySkip, initSiteSwitch, addAnchorImprovements, initNumKeysSkip, initAutoLike, // lyrics addPlayerBarLyricsBtn, initLyricsCache, // menu addConfigMenuOptionYT, addConfigMenuOptionYTM, // general initVersionCheck, } from "./features/index.js"; //#region console watermark { // console watermark with sexy gradient const styleGradient = "background: rgba(165, 38, 38, 1); background: linear-gradient(90deg, rgb(154, 31, 103) 0%, rgb(135, 31, 31) 40%, rgb(184, 64, 41) 100%);"; const styleCommon = "color: #fff; font-size: 1.3rem;"; console.log(); console.log( `%c${scriptInfo.name}%c${scriptInfo.version}%c • ${scriptInfo.namespace}%c\n\nBuild #${buildNumber}`, `${styleCommon} ${styleGradient} font-weight: bold; padding-left: 6px; padding-right: 6px;`, `${styleCommon} background-color: #333; padding-left: 8px; padding-right: 8px;`, "color: #fff; font-size: 1.2rem;", "padding: initial;", ); console.log([ "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 tiny event listener library: https://github.com/ai/nanoevents", "─ This markdown parser library: https://github.com/markedjs/marked", "─ This fuzzy search library: https://github.com/krisk/Fuse", ].join("\n")); console.log(); } //#region preInit /** Stuff that needs to be called ASAP, before anything async happens */ function preInit() { try { 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"); emitInterface("bytm:registerPlugins"); if(features.disableBeforeUnloadPopup && domain === "ytm") disableBeforeUnload(); if(!domLoaded) document.addEventListener("DOMContentLoaded", onDomLoad, { once: true }); else onDomLoad(); if(features.rememberSongTime) initRememberSongTime(); } 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 features = 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 { initObservers(); await Promise.allSettled([ insertGlobalStyle(), initVersionCheck(), ]); } catch(err) { error("Fatal error in feature pre-init:", err); return; } 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(features.watermarkEnabled) ftInit.push(["addWatermark", addWatermark()]); if(features.fixSpacing) ftInit.push(["fixSpacing", fixSpacing()]); if(features.removeUpgradeTab) ftInit.push(["removeUpgradeTab", removeUpgradeTab()]); ftInit.push(["thumbnailOverlay", initThumbnailOverlay()]); if(features.hideCursorOnIdle) ftInit.push(["hideCursorOnIdle", initHideCursorOnIdle()]); if(features.fixHdrIssues) ftInit.push(["fixHdrIssues", fixHdrIssues()]); //#region (ytm) volume ftInit.push(["volumeFeatures", initVolumeFeatures()]); //#region (ytm) song lists if(features.lyricsQueueButton || features.deleteFromQueueButton) ftInit.push(["queueButtons", initQueueButtons()]); ftInit.push(["aboveQueueBtns", initAboveQueueBtns()]); //#region (ytm) behavior if(features.closeToastsTimeout > 0) ftInit.push(["autoCloseToasts", initAutoCloseToasts()]); //#region (ytm) input ftInit.push(["arrowKeySkip", initArrowKeySkip()]); if(features.anchorImprovements) ftInit.push(["anchorImprovements", addAnchorImprovements()]); ftInit.push(["numKeysSkip", initNumKeysSkip()]); //#region (ytm) lyrics if(features.geniusLyrics) ftInit.push(["playerBarLyricsBtn", addPlayerBarLyricsBtn()]); } //#region (ytm+yt) cfg menu option try { if(domain === "ytm") { addSelectorListener("body", "tp-yt-iron-dropdown #contentWrapper ytd-multi-page-menu-renderer #container.menu-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(features.disableDarkReaderSites !== "none") disableDarkReader(); if(features.removeShareTrackingParamSites && (features.removeShareTrackingParamSites === domain || features.removeShareTrackingParamSites === "all")) ftInit.push(["initRemShareTrackParam", initRemShareTrackParam()]); //#region (ytm+yt) input ftInit.push(["siteSwitch", initSiteSwitch(domain)]); if(getFeatures().autoLikeChannels) ftInit.push(["autoLikeChannels", initAutoLike()]); } emitInterface("bytm:featureInitStarted"); try { initPlugins(); } catch(err) { error("Plugin loading error:", err); emitInterface("bytm:fatalError", "Error while loading plugins"); } 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(getFeatures().initTimeout > 0 ? getFeatures().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 all ${ftInit.length} features after ${Math.floor(Date.now() - initStartTs)}ms`); try { registerDevMenuCommands(); } catch(e) { warn("Couldn't register dev menu commands:", e); } } catch(err) { error("Feature error:", err); emitInterface("bytm:fatalError", "Error while initializing features"); } } //#region insert css bundle /** Inserts the bundled CSS files imported throughout the script into a