import { addGlobalStyle, compress, decompress, type Stringifiable } from "@sv443-network/userutils"; import { initOnSelector, warn } from "./utils"; import { clearConfig, getFeatures, initConfig } from "./config"; import { buildNumber, compressionFormat, defaultLogLevel, mode, scriptInfo } from "./constants"; import { error, getDomain, info, getSessionId, log, setLogLevel, initTranslations, setLocale } from "./utils"; import { initSiteEvents } from "./siteEvents"; import { emitInterface, initInterface, initPlugins } from "./interface"; import { addWelcomeMenu, showWelcomeMenu } from "./menu/welcomeMenu"; import { initObservers, observers } from "./observers"; import { // layout addWatermark, removeUpgradeTab, removeShareTrackingParam, fixSpacing, addScrollToActiveBtn, // volume initVolumeFeatures, // song lists initQueueButtons, // behavior initBeforeUnloadHook, disableBeforeUnload, initAutoCloseToasts, initRememberSongTime, disableDarkReader, // input initArrowKeySkip, initSiteSwitch, addAnchorImprovements, initNumKeysSkip, // lyrics addMediaCtrlLyricsBtn, // menu addConfigMenuOption, // other initVersionCheck, initLyricsCache, } from "./features/index"; { // 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.5em; padding-left: 6px; padding-right: 6px;"; console.log(); console.log( `%c${scriptInfo.name}%cv${scriptInfo.version}%c\n\nBuild #${buildNumber} ─ ${scriptInfo.namespace}`, `font-weight: bold; ${styleCommon} ${styleGradient}`, `background-color: #333; ${styleCommon}`, "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", "─ The fuse.js library: https://github.com/krisk/Fuse", "─ This markdown parser library: https://github.com/markedjs/marked", "─ This tiny event listener library: https://github.com/ai/nanoevents", ].join("\n")); console.log(); } let domLoaded = false; const domain = getDomain(); /** Stuff that needs to be called ASAP, before anything async happens */ function preInit() { log("Session ID:", getSessionId()); initInterface(); setLogLevel(defaultLogLevel); if(domain === "ytm") initBeforeUnloadHook(); init(); } async function init() { try { document.addEventListener("DOMContentLoaded", () => { domLoaded = true; }); const features = await initConfig(); setLogLevel(features.logLevel); await initLyricsCache(); await initTranslations(features.locale ?? "en_US"); setLocale(features.locale ?? "en_US"); emitInterface("bytm:initPlugins"); if(features.disableBeforeUnloadPopup && domain === "ytm") disableBeforeUnload(); if(!domLoaded) document.addEventListener("DOMContentLoaded", onDomLoad); else onDomLoad(); if(features.rememberSongTime) initRememberSongTime(); } catch(err) { error("General Error:", err); } // init menu separately from features try { void "TODO(v1.2):"; // initMenu(); } catch(err) { error("Couldn't initialize menu:", err); } } /** Called when the DOM has finished loading and can be queried and altered by the userscript */ async function onDomLoad() { insertGlobalStyle(); initObservers(); initOnSelector(); const features = getFeatures(); const ftInit = [] as Promise[]; await initVersionCheck(); log(`DOM loaded. Initializing features for domain "${domain}"...`); try { if(domain === "ytm") { disableDarkReader(); ftInit.push(initSiteEvents()); if(typeof await GM.getValue("bytm-installed") !== "string") { // open welcome menu with language selector await addWelcomeMenu(); info("Showing welcome menu"); await showWelcomeMenu(); await GM.setValue("bytm-installed", JSON.stringify({ timestamp: Date.now(), version: scriptInfo.version })); } observers.body.addListener("tp-yt-iron-dropdown #contentWrapper ytd-multi-page-menu-renderer #container.menu-container", { listener: addConfigMenuOption, }); if(features.arrowKeySupport) ftInit.push(initArrowKeySkip()); if(features.removeUpgradeTab) ftInit.push(removeUpgradeTab()); if(features.watermarkEnabled) ftInit.push(addWatermark()); if(features.geniusLyrics) ftInit.push(addMediaCtrlLyricsBtn()); if(features.deleteFromQueueButton || features.lyricsQueueButton) ftInit.push(initQueueButtons()); if(features.anchorImprovements) ftInit.push(addAnchorImprovements()); if(features.closeToastsTimeout > 0) ftInit.push(initAutoCloseToasts()); if(features.removeShareTrackingParam) ftInit.push(removeShareTrackingParam()); if(features.numKeysSkipToTime) ftInit.push(initNumKeysSkip()); if(features.fixSpacing) ftInit.push(fixSpacing()); if(features.scrollToActiveSongBtn) ftInit.push(addScrollToActiveBtn()); ftInit.push(initVolumeFeatures()); } if(["ytm", "yt"].includes(domain)) { if(features.switchBetweenSites) ftInit.push(initSiteSwitch(domain)); // TODO: for hot reloading features // ftInit.push(new Promise((resolve) => { // for(const [k, v] of Object.entries(featInfo)) { // try { // const featVal = features[k as keyof typeof featInfo]; // // @ts-ignore // if(v.enable && featVal === true) { // console.log("###> enable", k); // // @ts-ignore // v.enable(features); // console.log("###>> enable ok"); // } // // @ts-ignore // else if(v.disable && featVal === false) { // console.log("###> disable", k); // // @ts-ignore // v.disable(features); // console.log("###>> disable ok"); // } // } // catch(err) { // error(`Couldn't initialize feature "${k}" due to error:`, err); // } // } // console.log("###>>> done for loop"); // resolve(); // })); } await Promise.allSettled(ftInit); emitInterface("bytm:ready"); try { initPlugins(); } catch(err) { error("Plugin loading error:", err); } try { registerDevMenuCommands(); } catch(e) { warn("Couldn't register dev menu commands:", e); } } catch(err) { error("Feature error:", err); } } // TODO(v1.2): // async function initFeatures() { // const ftInit = [] as Promise[]; // log(`DOM loaded. Initializing features for domain "${domain}"...`); // for(const [ftKey, ftInfo] of Object.entries(featInfo)) { // try { // // @ts-ignore // const res = ftInfo?.enable?.() as undefined | Promise; // if(res instanceof Promise) // ftInit.push(res); // else // ftInit.push(Promise.resolve()); // } // catch(err) { // error(`Couldn't initialize feature "${ftKey}" due to error:`, err); // } // } // siteEvents.on("configOptionChanged", (ftKey, oldValue, newValue) => { // try { // // @ts-ignore // if(featInfo[ftKey].change) { // // @ts-ignore // featInfo[ftKey].change(oldValue, newValue); // } // // @ts-ignore // else if(featInfo[ftKey].disable) { // // @ts-ignore // const disableRes = featInfo[ftKey].disable(); // if(disableRes instanceof Promise) // @ts-ignore // disableRes.then(() => featInfo[ftKey]?.enable?.()); // else // @ts-ignore // featInfo[ftKey]?.enable?.(); // } // else { // // TODO: set "page reload required" flag in new menu // if(confirm("[Work in progress]\nYou changed an option that requires a page reload to be applied.\nReload the page now?")) { // disableBeforeUnload(); // location.reload(); // } // } // } // catch(err) { // error(`Couldn't change feature "${ftKey}" due to error:`, err); // } // }); // Promise.all(ftInit).then(() => { // emitInterface("bytm:ready"); // }); // } /** Inserts the bundled CSS files imported throughout the script into a