123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186 |
- // ==UserScript==
- // @name BetterYTM
- // @homepageURL https://github.com/Sv443/BetterYTM#readme
- // @namespace https://github.com/Sv443/BetterYTM
- // @version 1.0.0
- // @description Configurable layout and UX improvements for YouTube Music
- // @description:de Konfigurierbares Layout und UX-Verbesserungen für YouTube Music
- // @license MIT
- // @author Sv443
- // @copyright Sv443 (https://github.com/Sv443)
- // @icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icon/icon.png
- // @match https://music.youtube.com/*
- // @match https://www.youtube.com/*
- // @run-at document-start
- // @downloadURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
- // @updateURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
- // @connect api.sv443.net
- // @grant GM.getValue
- // @grant GM.setValue
- // @grant GM.getResourceUrl
- // @grant unsafeWindow
- // @noframes
- // @resource icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icon/icon.png
- // @resource close https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/close.png
- // @resource delete https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/delete.svg
- // @resource error https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/error.svg
- // @resource lyrics https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/lyrics.svg
- // @resource spinner https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/spinner.svg
- // @resource github https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/github.png
- // @resource greasyfork https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/greasyfork.png
- // ==/UserScript==
- /*
- ▄▄▄ ▄ ▄▄▄▄▄▄ ▄
- █ █ ▄▄▄ █ █ ▄▄▄ ▄ ▄█ █ █ █▀▄▀█
- █▀▀▄ █▄█ █▀ █▀ █▄█ █▀ █ █ █ █
- █▄▄▀ ▀▄▄ ▀▄▄ ▀▄▄ ▀▄▄ █ █ █ █ █
- Made with ❤️ by Sv443
- I welcome every contribution on GitHub!
- https://github.com/Sv443/BetterYTM
- */
- /* Disclaimer: I am not affiliated with YouTube, Google, Alphabet, Genius or anyone else */
- /* C&D this 🖕 */
- /******/ var __webpack_modules__ = ({
- /***/ "./node_modules/@billjs/event-emitter/lib/index.js":
- /*!*********************************************************!*\
- !*** ./node_modules/@billjs/event-emitter/lib/index.js ***!
- \*********************************************************/
- /***/ (function(__unused_webpack_module, exports) {
- /**
- * A simple and lightweight EventEmitter by TypeScript for Node.js or Browsers.
- *
- * @author billjs
- * @see https://github.com/billjs/event-emitter
- * @license MIT(https://opensource.org/licenses/MIT)
- */
- Object.defineProperty(exports, "__esModule", ({ value: true }));
- /**
- * It's a class for managing events.
- * It can be extended to provide event functionality for other classes or object.
- *
- * @export
- * @class EventEmitter
- */
- var EventEmitter = /** @class */ (function () {
- function EventEmitter() {
- /**
- * the all event handlers are added.
- * it's a Map data structure(key-value), the key is event type, and the value is event handler.
- *
- * @memberof EventEmitter
- */
- this._eventHandlers = {};
- }
- /**
- * event type validator.
- *
- * @param {string} type event type
- * @returns {boolean}
- * @memberof EventEmitter
- */
- EventEmitter.prototype.isValidType = function (type) {
- return typeof type === 'string';
- };
- /**
- * event handler validator.
- *
- * @param {EventHandler} handler event handler
- * @returns {boolean}
- * @memberof EventEmitter
- */
- EventEmitter.prototype.isValidHandler = function (handler) {
- return typeof handler === 'function';
- };
- /**
- * listen on a new event by type and handler.
- * if listen on, the true is returned, otherwise the false.
- * The handler will not be listen if it is a duplicate.
- *
- * @param {string} type event type, it must be a unique string.
- * @param {EventHandler} handler event handler, when if the same handler is passed, listen it by only once.
- * @returns {boolean}
- * @memberof EventEmitter
- * @example
- * const emitter = new EventEmitter();
- * emitter.on('change:name', evt => {
- * console.log(evt);
- * });
- */
- EventEmitter.prototype.on = function (type, handler) {
- if (!type || !handler)
- return false;
- if (!this.isValidType(type))
- return false;
- if (!this.isValidHandler(handler))
- return false;
- var handlers = this._eventHandlers[type];
- if (!handlers)
- handlers = this._eventHandlers[type] = [];
- // when the same handler is passed, listen it by only once.
- if (handlers.indexOf(handler) >= 0)
- return false;
- handler._once = false;
- handlers.push(handler);
- return true;
- };
- /**
- * listen on an once event by type and handler.
- * when the event is fired, that will be listen off immediately and automatically.
- * The handler will not be listen if it is a duplicate.
- *
- * @param {string} type event type, it must be a unique string.
- * @param {EventHandler} handler event handler, when if the same handler is passed, listen it by only once.
- * @returns {boolean}
- * @memberof EventEmitter
- * @example
- * const emitter = new EventEmitter();
- * emitter.once('change:name', evt => {
- * console.log(evt);
- * });
- */
- EventEmitter.prototype.once = function (type, handler) {
- if (!type || !handler)
- return false;
- if (!this.isValidType(type))
- return false;
- if (!this.isValidHandler(handler))
- return false;
- var ret = this.on(type, handler);
- if (ret) {
- // set `_once` private property after listened,
- // avoid to modify event handler that has been listened.
- handler._once = true;
- }
- return ret;
- };
- /**
- * listen off an event by type and handler.
- * or listen off events by type, when if only type argument is passed.
- * or listen off all events, when if no arguments are passed.
- *
- * @param {string} [type] event type
- * @param {EventHandler} [handler] event handler
- * @returns
- * @memberof EventEmitter
- * @example
- * const emitter = new EventEmitter();
- * // listen off the specified event
- * emitter.off('change:name', evt => {
- * console.log(evt);
- * });
- * // listen off events by type
- * emitter.off('change:name');
- * // listen off all events
- * emitter.off();
- */
- EventEmitter.prototype.off = function (type, handler) {
- // listen off all events, when if no arguments are passed.
- // it does samething as `offAll` method.
- if (!type)
- return this.offAll();
- // listen off events by type, when if only type argument is passed.
- if (!handler) {
- this._eventHandlers[type] = [];
- return;
- }
- if (!this.isValidType(type))
- return;
- if (!this.isValidHandler(handler))
- return;
- var handlers = this._eventHandlers[type];
- if (!handlers || !handlers.length)
- return;
- // otherwise, listen off the specified event.
- for (var i = 0; i < handlers.length; i++) {
- var fn = handlers[i];
- if (fn === handler) {
- handlers.splice(i, 1);
- break;
- }
- }
- };
- /**
- * listen off all events, that means every event will be emptied.
- *
- * @memberof EventEmitter
- * @example
- * const emitter = new EventEmitter();
- * emitter.offAll();
- */
- EventEmitter.prototype.offAll = function () {
- this._eventHandlers = {};
- };
- /**
- * fire the specified event, and you can to pass a data.
- * When fired, every handler attached to that event will be executed.
- * But, if it's an once event, listen off it immediately after called handler.
- *
- * @param {string} type event type
- * @param {*} [data] event data
- * @returns
- * @memberof EventEmitter
- * @example
- * const emitter = new EventEmitter();
- * emitter.fire('change:name', 'new name');
- */
- EventEmitter.prototype.fire = function (type, data) {
- if (!type || !this.isValidType(type))
- return;
- var handlers = this._eventHandlers[type];
- if (!handlers || !handlers.length)
- return;
- var event = this.createEvent(type, data);
- for (var _i = 0, handlers_1 = handlers; _i < handlers_1.length; _i++) {
- var handler = handlers_1[_i];
- if (!this.isValidHandler(handler))
- continue;
- if (handler._once)
- event.once = true;
- // call event handler, and pass the event argument.
- handler(event);
- // if it's an once event, listen off it immediately after called handler.
- if (event.once)
- this.off(type, handler);
- }
- };
- /**
- * check whether the specified event has been listen on.
- * or check whether the events by type has been listen on, when if only `type` argument is passed.
- *
- * @param {string} type event type
- * @param {EventHandler} [handler] event handler, optional
- * @returns {boolean}
- * @memberof EventEmitter
- * @example
- * const emitter = new EventEmitter();
- * const result = emitter.has('change:name');
- */
- EventEmitter.prototype.has = function (type, handler) {
- if (!type || !this.isValidType(type))
- return false;
- var handlers = this._eventHandlers[type];
- // if there are no any events, return false.
- if (!handlers || !handlers.length)
- return false;
- // at lest one event, and no pass `handler` argument, then return true.
- if (!handler || !this.isValidHandler(handler))
- return true;
- // otherwise, need to traverse the handlers.
- return handlers.indexOf(handler) >= 0;
- };
- /**
- * get the handlers for the specified event type.
- *
- * @param {string} type event type
- * @returns {EventHandler[]}
- * @memberof EventEmitter
- * @example
- * const emitter = new EventEmitter();
- * const handlers = emitter.getHandlers('change:name');
- * console.log(handlers);
- */
- EventEmitter.prototype.getHandlers = function (type) {
- if (!type || !this.isValidType(type))
- return [];
- return this._eventHandlers[type] || [];
- };
- /**
- * create event object.
- *
- * @param {string} type event type
- * @param {*} [data] event data
- * @param {boolean} [once=false] is it an once event?
- * @returns {Event}
- * @memberof EventEmitter
- */
- EventEmitter.prototype.createEvent = function (type, data, once) {
- if (once === void 0) { once = false; }
- var event = { type: type, data: data, timestamp: Date.now(), once: once };
- return event;
- };
- return EventEmitter;
- }());
- exports.EventEmitter = EventEmitter;
- /**
- * EventEmitter instance for global.
- * @type {EventEmitter}
- */
- exports.globalEvent = new EventEmitter();
- /***/ }),
- /***/ "./changelog.md":
- /*!**********************!*\
- !*** ./changelog.md ***!
- \**********************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- // Module
- var code = "<h1 id=\"betterytm-changelog\">BetterYTM Changelog</h1>\n<br>\n\n<h2 id=\"history\">History:</h2>\n<ul>\n<li><strong><a href=\"#100\">v1.0.0</a></strong></li>\n<li><a href=\"#020\">v0.2.0</a></li>\n<li><a href=\"#010\">v0.1.0</a></li>\n</ul>\n<hr>\n<p><br><br></p>\n<h2 id=\"100\">1.0.0</h2>\n<p>TODO:</p>\n<ul>\n<li>Added menu to configure features</li>\n<li>New configurable features:<ul>\n<li>Make volume slider bigger</li>\n<li>Choose step of volume slider for finer control</li>\n<li>Add lyrics button to each song in a playlist</li>\n</ul>\n</li>\n<li>Changes / Fixes:<ul>\n<li>Now the lyrics button will directly link to the lyrics (using my API <a href=\"https://github.com/Sv443/geniURL\">geniURL</a>)</li>\n<li>Site switch with <kbd>F9</kbd> will now keep the video time</li>\n</ul>\n</li>\n</ul>\n<br>\n\n<h2 id=\"020\">0.2.0</h2>\n<ul>\n<li>Added Features:<ul>\n<li>Switch between YouTube and YT Music (with <kbd>F9</kbd> by default)</li>\n<li>Search for song lyrics with new button in media controls</li>\n<li>Remove "Upgrade to YTM Premium" tab</li>\n</ul>\n</li>\n</ul>\n<br>\n\n<h2 id=\"010\">0.1.0</h2>\n<ul>\n<li>Added support for arrow keys to skip forward or backward (currently only by fixed 10 second interval)</li>\n</ul>\n";
- // Exports
- /* harmony default export */ __webpack_exports__["default"] = (code);
- /***/ }),
- /***/ "./src/features/menu/menu.html":
- /*!*************************************!*\
- !*** ./src/features/menu/menu.html ***!
- \*************************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- // Module
- var code = "<dialog id=\"bytm-menu-dialog\">\n <div id=\"bytm-menu-header-container\">\n <div class=\"bytm-menu-header-option\" id=\"bytm-menu-tab-options-header\" data-active=\"true\">\n <h3>Options</h3>\n </div>\n <div class=\"bytm-menu-header-option\" id=\"bytm-menu-tab-info-header\" data-active=\"false\">\n <h3>Info</h3>\n </div>\n <div class=\"bytm-menu-header-option\" id=\"bytm-menu-tab-changelog-header\" data-active=\"false\">\n <h3>Changelog</h3>\n </div>\n </div>\n <div id=\"bytm-menu-body\">\n <div class=\"bytm-menu-tab-content\" id=\"bytm-menu-tab-options-content\" data-active=\"true\"></div>\n <div class=\"bytm-menu-tab-content\" id=\"bytm-menu-tab-info-content\" data-active=\"false\">\n ayo info\n </div>\n <div class=\"bytm-menu-tab-content\" id=\"bytm-menu-tab-changelog-content\" data-active=\"false\"></div>\n </div>\n</dialog>\n";
- // Exports
- /* harmony default export */ __webpack_exports__["default"] = (code);
- /***/ }),
- /***/ "./src/features/layout.css":
- /*!*********************************!*\
- !*** ./src/features/layout.css ***!
- \*********************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- // extracted by mini-css-extract-plugin
- /***/ }),
- /***/ "./src/features/menu/menu.css":
- /*!************************************!*\
- !*** ./src/features/menu/menu.css ***!
- \************************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- // extracted by mini-css-extract-plugin
- /***/ }),
- /***/ "./src/features/menu/menu_old.css":
- /*!****************************************!*\
- !*** ./src/features/menu/menu_old.css ***!
- \****************************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- // extracted by mini-css-extract-plugin
- /***/ }),
- /***/ "./src/config.ts":
- /*!***********************!*\
- !*** ./src/config.ts ***!
- \***********************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ defaultFeatures: function() { return /* binding */ defaultFeatures; },
- /* harmony export */ getFeatures: function() { return /* binding */ getFeatures; },
- /* harmony export */ loadFeatureConf: function() { return /* binding */ loadFeatureConf; },
- /* harmony export */ saveFeatureConf: function() { return /* binding */ saveFeatureConf; },
- /* harmony export */ setDefaultFeatConf: function() { return /* binding */ setDefaultFeatConf; }
- /* harmony export */ });
- /* harmony import */ var _features_index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./features/index */ "./src/features/index.ts");
- /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
- var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- };
- /** If this number is incremented, the features object needs to be migrated (TODO: migration not implemented yet) */
- const formatVersion = 1;
- const defaultFeatures = Object.keys(_features_index__WEBPACK_IMPORTED_MODULE_0__.featInfo)
- .reduce((acc, key) => {
- acc[key] = _features_index__WEBPACK_IMPORTED_MODULE_0__.featInfo[key].default;
- return acc;
- }, {});
- /** In-memory features object to save on a little bit of I/O */
- let featuresCache;
- /**
- * Returns the current FeatureConfig in memory or reads it from GM storage if undefined.
- * Automatically applies defaults for non-existant keys
- * @param forceRead Set to true to force reading the config from GM storage
- */
- function getFeatures(forceRead = false) {
- return __awaiter(this, void 0, void 0, function* () {
- if (!featuresCache || forceRead)
- featuresCache = yield loadFeatureConf();
- return featuresCache;
- });
- }
- /** Loads a feature configuration saved persistently, returns an empty object if no feature configuration was saved */
- function loadFeatureConf() {
- return __awaiter(this, void 0, void 0, function* () {
- const defConf = Object.assign({}, defaultFeatures);
- try {
- const featureConf = yield GM.getValue("betterytm-config");
- // empty object length is 2-3, so check for >3 to be sure
- if (typeof featureConf !== "string" || featureConf.length <= 3) {
- yield setDefaultFeatConf();
- return featuresCache = defConf;
- }
- return featuresCache = Object.assign(Object.assign({}, defConf), (featureConf ? JSON.parse(featureConf) : {}));
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Error loading feature configuration, resetting to default:", err);
- yield setDefaultFeatConf();
- return featuresCache = defConf;
- }
- });
- }
- /**
- * Saves the passed feature configuration persistently in GM storage and in the in-memory cache
- * @param featureConf
- */
- function saveFeatureConf(featureConf) {
- if (!featureConf || typeof featureConf != "object")
- throw new TypeError("Feature config not provided or invalid");
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Saving new feature config:", featureConf);
- featuresCache = Object.assign({}, featureConf);
- GM.setValue("betterytm-config-ver", formatVersion);
- return GM.setValue("betterytm-config", JSON.stringify(featureConf));
- }
- /** Resets the featuresCache synchronously and the persistent features storage asynchronously to their default values */
- function setDefaultFeatConf() {
- featuresCache = Object.assign({}, defaultFeatures);
- GM.setValue("betterytm-config-ver", formatVersion);
- return GM.setValue("betterytm-config", JSON.stringify(defaultFeatures));
- }
- /***/ }),
- /***/ "./src/constants.ts":
- /*!**************************!*\
- !*** ./src/constants.ts ***!
- \**************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ branch: function() { return /* binding */ branch; },
- /* harmony export */ logLevel: function() { return /* binding */ logLevel; },
- /* harmony export */ mode: function() { return /* binding */ mode; },
- /* harmony export */ scriptInfo: function() { return /* binding */ scriptInfo; }
- /* harmony export */ });
- const modeRaw = "development";
- const branchRaw = "develop";
- /** The mode in which the script was built (production or development) */
- const mode = modeRaw.match(/^{{.+}}$/) ? "production" : modeRaw;
- /** The branch to use in various URLs that point to the GitHub repo */
- const branch = branchRaw.match(/^{{.+}}$/) ? "main" : branchRaw;
- /**
- * How much info should be logged to the devtools console
- * 0 = Debug (show everything) or 1 = Info (show only important stuff)
- */
- const logLevel = mode === "production" ? 1 : 0;
- /** Info about the userscript, parsed from the userscript header (tools/post-build.js) */
- const scriptInfo = {
- name: GM.info.script.name,
- version: GM.info.script.version,
- namespace: GM.info.script.namespace,
- lastCommit: "fb62267", // assert as generic string instead of literal
- };
- /***/ }),
- /***/ "./src/events.ts":
- /*!***********************!*\
- !*** ./src/events.ts ***!
- \***********************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ getEvtData: function() { return /* binding */ getEvtData; },
- /* harmony export */ initSiteEvents: function() { return /* binding */ initSiteEvents; },
- /* harmony export */ removeAllObservers: function() { return /* binding */ removeAllObservers; },
- /* harmony export */ siteEvents: function() { return /* binding */ siteEvents; }
- /* harmony export */ });
- /* harmony import */ var _billjs_event_emitter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @billjs/event-emitter */ "./node_modules/@billjs/event-emitter/lib/index.js");
- /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
- var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- };
- /** EventEmitter instance that is used to detect changes to the site */
- const siteEvents = new _billjs_event_emitter__WEBPACK_IMPORTED_MODULE_0__.EventEmitter();
- /**
- * Returns the data of an event from the `@billjs/event-emitter` library.
- * This function is used as a shorthand to extract the data and assert it with the type passed in `<T>`
- * @param evt Event object from the `.on()` or `.once()` method
- * @template T Type of the data passed by `.fire(type: string, data: T)`
- */
- function getEvtData(evt) {
- return evt.data;
- }
- let observers = [];
- /** Disconnects and deletes all observers. Run `initSiteEvents()` again to create new ones. */
- function removeAllObservers() {
- observers.forEach((observer, i) => {
- observer.disconnect();
- delete observers[i];
- });
- observers = [];
- }
- /** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
- function initSiteEvents() {
- return __awaiter(this, void 0, void 0, function* () {
- try {
- //#SECTION queue
- // the queue container always exists so it doesn't need the extra init function
- const queueObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
- if (addedNodes.length > 0 || removedNodes.length > 0) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
- siteEvents.fire("queueChanged", target);
- }
- });
- // only observe added or removed elements
- queueObs.observe(document.querySelector(".side-panel.modular #contents.ytmusic-player-queue"), {
- childList: true,
- });
- const autoplayObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
- if (addedNodes.length > 0 || removedNodes.length > 0) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Detected autoplay queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
- siteEvents.fire("autoplayQueueChanged", target);
- }
- });
- autoplayObs.observe(document.querySelector(".side-panel.modular ytmusic-player-queue #automix-contents"), {
- childList: true,
- });
- //#SECTION home page observers
- initHomeObservers();
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Successfully initialized SiteEvents observers");
- observers = observers.concat([
- queueObs,
- autoplayObs,
- ]);
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't initialize SiteEvents observers due to an error:\n", err);
- }
- });
- }
- /**
- * The home page might not exist yet if the site was accessed through any path like /watch directly.
- * This function will keep waiting for when the home page exists, then create the necessary MutationObservers.
- */
- function initHomeObservers() {
- var _a;
- return __awaiter(this, void 0, void 0, function* () {
- let interval;
- // hidden="" attribute is only present if the content of the page doesn't exist yet
- // so this pauses execution until that attribute is removed
- if ((_a = document.querySelector("ytmusic-browse-response#browse-page")) === null || _a === void 0 ? void 0 : _a.hasAttribute("hidden")) {
- yield new Promise((res) => {
- interval = setInterval(() => {
- var _a;
- if (!((_a = document.querySelector("ytmusic-browse-response#browse-page")) === null || _a === void 0 ? void 0 : _a.hasAttribute("hidden"))) {
- clearInterval(interval);
- res();
- }
- }, 50);
- });
- }
- siteEvents.fire("homePageLoaded");
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Initialized home page observers");
- //#SECTION carousel shelves
- const shelfContainerObs = new MutationObserver(([{ addedNodes, removedNodes }]) => {
- if (addedNodes.length > 0 || removedNodes.length > 0) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Detected carousel shelf container change - added nodes:", addedNodes.length, "- removed nodes:", removedNodes.length);
- siteEvents.fire("carouselShelvesChanged", { addedNodes, removedNodes });
- }
- });
- shelfContainerObs.observe(document.querySelector("#contents.ytmusic-section-list-renderer"), {
- childList: true,
- });
- observers = observers.concat([shelfContainerObs]);
- });
- }
- /***/ }),
- /***/ "./src/features/index.ts":
- /*!*******************************!*\
- !*** ./src/features/index.ts ***!
- \*******************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ addAnchorImprovements: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addAnchorImprovements; },
- /* harmony export */ addConfigMenuOption: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addConfigMenuOption; },
- /* harmony export */ addLyricsCacheEntry: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.addLyricsCacheEntry; },
- /* harmony export */ addMediaCtrlLyricsBtn: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.addMediaCtrlLyricsBtn; },
- /* harmony export */ addMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.addMenu; },
- /* harmony export */ addWatermark: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addWatermark; },
- /* harmony export */ closeMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.closeMenu; },
- /* harmony export */ createLyricsBtn: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.createLyricsBtn; },
- /* harmony export */ disableBeforeUnload: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.disableBeforeUnload; },
- /* harmony export */ enableBeforeUnload: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.enableBeforeUnload; },
- /* harmony export */ featInfo: function() { return /* binding */ featInfo; },
- /* harmony export */ geniUrlBase: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.geniUrlBase; },
- /* harmony export */ getCurrentLyricsUrl: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getCurrentLyricsUrl; },
- /* harmony export */ getGeniusUrl: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getGeniusUrl; },
- /* harmony export */ getLyricsCacheEntry: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getLyricsCacheEntry; },
- /* harmony export */ initArrowKeySkip: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initArrowKeySkip; },
- /* harmony export */ initBeforeUnloadHook: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initBeforeUnloadHook; },
- /* harmony export */ initMenu: function() { return /* reexport safe */ _menu_menu__WEBPACK_IMPORTED_MODULE_4__.initMenu; },
- /* harmony export */ initQueueButtons: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.initQueueButtons; },
- /* harmony export */ initSiteSwitch: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initSiteSwitch; },
- /* harmony export */ initVolumeFeatures: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.initVolumeFeatures; },
- /* harmony export */ openMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu; },
- /* harmony export */ preInitLayout: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.preInitLayout; },
- /* harmony export */ removeUpgradeTab: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.removeUpgradeTab; },
- /* harmony export */ sanitizeArtists: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.sanitizeArtists; },
- /* harmony export */ sanitizeSong: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.sanitizeSong; }
- /* harmony export */ });
- /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
- /* harmony import */ var _input__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./input */ "./src/features/input.ts");
- /* harmony import */ var _layout__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./layout */ "./src/features/layout.ts");
- /* harmony import */ var _lyrics__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./lyrics */ "./src/features/lyrics.ts");
- /* harmony import */ var _menu_menu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu/menu */ "./src/features/menu/menu.ts");
- /* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
- /** Contains all possible features with their default values and other config */
- const featInfo = {
- //#SECTION input
- arrowKeySupport: {
- desc: "Arrow keys skip forwards and backwards by 10 seconds",
- type: "toggle",
- category: "input",
- default: true,
- },
- switchBetweenSites: {
- desc: "Add F9 as a hotkey to switch between the YT and YTM sites on a video / song",
- type: "toggle",
- category: "input",
- default: true,
- },
- switchSitesHotkey: {
- desc: "TODO(v1.1): Which hotkey needs to be pressed to switch sites?",
- type: "hotkey",
- category: "input",
- default: {
- key: "F9",
- shift: false,
- ctrl: false,
- meta: false,
- },
- visible: false,
- },
- disableBeforeUnloadPopup: {
- desc: "Completely disable the popup that sometimes appears before leaving the site",
- type: "toggle",
- category: "input",
- default: false,
- },
- anchorImprovements: {
- desc: "Add link elements all over the page so stuff can be opened in a new tab easier",
- type: "toggle",
- category: "input",
- default: true,
- },
- //#SECTION layout
- removeUpgradeTab: {
- desc: "Remove the \"Upgrade\" / YT Music Premium tab",
- type: "toggle",
- category: "layout",
- default: true,
- },
- volumeSliderLabel: {
- desc: "Add a percentage label to the volume slider",
- type: "toggle",
- category: "layout",
- default: true,
- },
- volumeSliderSize: {
- desc: "Width of the volume slider in pixels",
- type: "number",
- category: "layout",
- min: 50,
- max: 500,
- step: 5,
- default: 160,
- unit: "px",
- },
- volumeSliderStep: {
- desc: "Volume slider sensitivity - the smaller this number, the finer the volume control",
- type: "slider",
- category: "layout",
- min: 1,
- max: 20,
- default: 2,
- },
- watermarkEnabled: {
- desc: `Show a ${_constants__WEBPACK_IMPORTED_MODULE_0__.scriptInfo.name} watermark under the YTM logo`,
- type: "toggle",
- category: "layout",
- default: true,
- },
- queueButtons: {
- desc: "Add buttons to each song in the queue to quickly open their lyrics or remove them from the queue",
- type: "toggle",
- category: "layout",
- default: true,
- },
- //#SECTION lyrics
- geniusLyrics: {
- desc: "Add a button to the media controls of the currently playing song to open its lyrics on genius.com",
- type: "toggle",
- category: "lyrics",
- default: true,
- },
- };
- /***/ }),
- /***/ "./src/features/input.ts":
- /*!*******************************!*\
- !*** ./src/features/input.ts ***!
- \*******************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ disableBeforeUnload: function() { return /* binding */ disableBeforeUnload; },
- /* harmony export */ enableBeforeUnload: function() { return /* binding */ enableBeforeUnload; },
- /* harmony export */ initArrowKeySkip: function() { return /* binding */ initArrowKeySkip; },
- /* harmony export */ initBeforeUnloadHook: function() { return /* binding */ initBeforeUnloadHook; },
- /* harmony export */ initSiteSwitch: function() { return /* binding */ initSiteSwitch; }
- /* harmony export */ });
- /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
- /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
- /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../config */ "./src/config.ts");
- var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- };
- //#MARKER arrow key skip
- function initArrowKeySkip() {
- document.addEventListener("keydown", onKeyDown);
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Added key press listener");
- }
- /** Called when the user presses any key, anywhere */
- function onKeyDown(evt) {
- var _a, _b;
- if (["ArrowLeft", "ArrowRight"].includes(evt.code)) {
- // discard the event when a (text) input is currently active, like when editing a playlist
- if (["INPUT", "TEXTAREA", "SELECT"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) !== null && _b !== void 0 ? _b : "_"))
- return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Captured valid key but the current active element is <${document.activeElement.tagName.toLowerCase()}>, so the keypress is ignored`);
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Captured key '${evt.code}' in proxy listener`);
- // ripped this stuff from the console, most of these are probably unnecessary but this was finnicky af and I am sick and tired of trial and error
- const defaultProps = {
- altKey: false,
- ctrlKey: false,
- metaKey: false,
- shiftKey: false,
- target: document.body,
- currentTarget: document.body,
- originalTarget: document.body,
- explicitOriginalTarget: document.body,
- srcElement: document.body,
- type: "keydown",
- bubbles: true,
- cancelBubble: false,
- cancelable: true,
- isTrusted: true,
- repeat: false,
- // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
- view: (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.getUnsafeWindow)(),
- };
- let invalidKey = false;
- let keyProps = {};
- switch (evt.code) {
- case "ArrowLeft":
- keyProps = {
- code: "KeyH",
- key: "h",
- keyCode: 72,
- which: 72,
- };
- break;
- case "ArrowRight":
- keyProps = {
- code: "KeyL",
- key: "l",
- keyCode: 76,
- which: 76,
- };
- break;
- default:
- invalidKey = true;
- break;
- }
- if (!invalidKey) {
- const proxyProps = Object.assign(Object.assign({ code: "" }, defaultProps), keyProps);
- document.body.dispatchEvent(new KeyboardEvent("keydown", proxyProps));
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Dispatched proxy keydown event: [${evt.code}] -> [${proxyProps.code}]`);
- }
- else
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.warn)(`Captured key '${evt.code}' has no defined behavior`);
- }
- }
- //#MARKER site switch
- /** Initializes the site switch feature */
- function initSiteSwitch(domain) {
- document.addEventListener("keydown", (e) => {
- if (e.key === "F9")
- switchSite(domain === "yt" ? "ytm" : "yt");
- });
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Initialized site switch listener");
- }
- /** Switches to the other site (between YT and YTM) */
- function switchSite(newDomain) {
- var _a;
- return __awaiter(this, void 0, void 0, function* () {
- try {
- if (newDomain === "ytm" && !location.href.includes("/watch"))
- return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.warn)("Not on a video page, so the site switch is ignored");
- let subdomain;
- if (newDomain === "ytm")
- subdomain = "music";
- else if (newDomain === "yt")
- subdomain = "www";
- if (!subdomain)
- throw new Error(`Unrecognized domain '${newDomain}'`);
- disableBeforeUnload();
- const { pathname, search, hash } = new URL(location.href);
- const vt = (_a = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getVideoTime)()) !== null && _a !== void 0 ? _a : 0;
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Found video time of ${vt} seconds`);
- const cleanSearch = search.split("&")
- .filter((param) => !param.match(/^\??t=/))
- .join("&");
- const newSearch = cleanSearch.includes("?") ? `${cleanSearch.startsWith("?") ? cleanSearch : "?" + cleanSearch}&t=${vt}` : `?t=${vt}`;
- const newUrl = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Switching to domain '${newDomain}' at ${newUrl}`);
- location.assign(newUrl);
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Error while switching site:", err);
- }
- });
- }
- //#MARKER beforeunload popup
- let beforeUnloadEnabled = true;
- /** Disables the popup before leaving the site */
- function disableBeforeUnload() {
- beforeUnloadEnabled = false;
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Disabled popup before leaving the site");
- }
- /** (Re-)enables the popup before leaving the site */
- function enableBeforeUnload() {
- beforeUnloadEnabled = true;
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Enabled popup before leaving the site");
- }
- /**
- * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
- * event listeners before they can be called by the site.
- */
- function initBeforeUnloadHook() {
- Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
- (function (original) {
- // @ts-ignore
- window.__proto__.addEventListener = function (...args) {
- if (!beforeUnloadEnabled && args[0] === "beforeunload")
- return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Prevented beforeunload event listener from being called");
- else
- return original.apply(this, args);
- };
- // @ts-ignore
- })(window.__proto__.addEventListener);
- (0,_config__WEBPACK_IMPORTED_MODULE_2__.getFeatures)().then(feats => {
- if (feats.disableBeforeUnloadPopup)
- disableBeforeUnload();
- });
- }
- /***/ }),
- /***/ "./src/features/layout.ts":
- /*!********************************!*\
- !*** ./src/features/layout.ts ***!
- \********************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ addAnchorImprovements: function() { return /* binding */ addAnchorImprovements; },
- /* harmony export */ addConfigMenuOption: function() { return /* binding */ addConfigMenuOption; },
- /* harmony export */ addWatermark: function() { return /* binding */ addWatermark; },
- /* harmony export */ initQueueButtons: function() { return /* binding */ initQueueButtons; },
- /* harmony export */ initVolumeFeatures: function() { return /* binding */ initVolumeFeatures; },
- /* harmony export */ preInitLayout: function() { return /* binding */ preInitLayout; },
- /* harmony export */ removeUpgradeTab: function() { return /* binding */ removeUpgradeTab; }
- /* harmony export */ });
- /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
- /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
- /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../config */ "./src/config.ts");
- /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
- /* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../events */ "./src/events.ts");
- /* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
- /* harmony import */ var _lyrics__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./lyrics */ "./src/features/lyrics.ts");
- /* harmony import */ var _layout_css__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./layout.css */ "./src/features/layout.css");
- /* harmony import */ var ___WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! . */ "./src/features/index.ts");
- var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- };
- let features;
- function preInitLayout() {
- return __awaiter(this, void 0, void 0, function* () {
- features = yield (0,_config__WEBPACK_IMPORTED_MODULE_2__.getFeatures)();
- });
- }
- //#MARKER BYTM-Config buttons
- let menuOpenAmt = 0, logoExchanged = false;
- /** Adds a watermark beneath the logo */
- function addWatermark() {
- const watermark = document.createElement("a");
- watermark.role = "button";
- watermark.id = "bytm-watermark";
- watermark.className = "style-scope ytmusic-nav-bar bytm-no-select";
- watermark.innerText = _constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name;
- watermark.title = "Open menu";
- watermark.tabIndex = 1000;
- improveLogo();
- watermark.addEventListener("click", (e) => {
- e.stopPropagation();
- menuOpenAmt++;
- if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
- (0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu)();
- if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
- exchangeLogo();
- });
- // when using the tab key to navigate
- watermark.addEventListener("keydown", (e) => {
- if (e.key === "Enter") {
- e.stopPropagation();
- menuOpenAmt++;
- if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
- (0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu)();
- if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
- exchangeLogo();
- }
- });
- const logoElem = document.querySelector("#left-content");
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.insertAfter)(logoElem, watermark);
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Added watermark element", watermark);
- }
- /** Turns the regular `<img>`-based logo into inline SVG to be able to animate and modify parts of it */
- function improveLogo() {
- return __awaiter(this, void 0, void 0, function* () {
- try {
- const res = yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.fetchAdvanced)("https://music.youtube.com/img/on_platform_logo_dark.svg");
- const svg = yield res.text();
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-logo a", {
- listener: (logoElem) => {
- var _a;
- logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
- logoElem.innerHTML = svg;
- logoElem.querySelectorAll("ellipse").forEach((e) => {
- e.classList.add("bytm-mod-logo-ellipse");
- });
- (_a = logoElem.querySelector("path")) === null || _a === void 0 ? void 0 : _a.classList.add("bytm-mod-logo-path");
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Swapped logo to inline SVG");
- },
- });
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.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() {
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(".bytm-mod-logo", {
- listener: (logoElem) => __awaiter(this, void 0, void 0, function* () {
- if (logoElem.classList.contains("bytm-logo-exchanged"))
- return;
- logoExchanged = true;
- logoElem.classList.add("bytm-logo-exchanged");
- const iconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("icon");
- const newLogo = document.createElement("img");
- newLogo.className = "bytm-mod-logo-img";
- newLogo.src = iconUrl;
- logoElem.insertBefore(newLogo, logoElem.querySelector("svg"));
- document.head.querySelectorAll("link[rel=\"icon\"]").forEach((e) => {
- e.href = iconUrl;
- });
- setTimeout(() => {
- logoElem.querySelectorAll(".bytm-mod-logo-ellipse").forEach(e => e.remove());
- }, 1000);
- }),
- });
- }
- /** Called whenever the menu exists to add a BYTM-Configuration button */
- function addConfigMenuOption(container) {
- return __awaiter(this, void 0, void 0, function* () {
- const cfgOptElem = document.createElement("a");
- cfgOptElem.role = "button";
- cfgOptElem.className = "bytm-cfg-menu-option bytm-anchor";
- cfgOptElem.ariaLabel = "Click to open BetterYTM's configuration menu";
- const cfgOptItemElem = document.createElement("div");
- cfgOptItemElem.className = "bytm-cfg-menu-option-item";
- cfgOptItemElem.addEventListener("click", (e) => {
- const settingsBtnElem = document.querySelector("ytmusic-nav-bar ytmusic-settings-button tp-yt-paper-icon-button");
- settingsBtnElem === null || settingsBtnElem === void 0 ? void 0 : settingsBtnElem.click();
- menuOpenAmt++;
- if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
- (0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu)();
- if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
- exchangeLogo();
- });
- const cfgOptIconElem = document.createElement("img");
- cfgOptIconElem.className = "bytm-cfg-menu-option-icon";
- cfgOptIconElem.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("icon");
- const cfgOptTextElem = document.createElement("div");
- cfgOptTextElem.className = "bytm-cfg-menu-option-text";
- cfgOptTextElem.innerText = "BetterYTM Configuration";
- cfgOptItemElem.appendChild(cfgOptIconElem);
- cfgOptItemElem.appendChild(cfgOptTextElem);
- cfgOptElem.appendChild(cfgOptItemElem);
- container.appendChild(cfgOptElem);
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Added BYTM-Configuration button to menu popover", cfgOptElem);
- });
- }
- //#MARKER remove upgrade tab
- /** Removes the "Upgrade" / YT Music Premium tab from the sidebar */
- function removeUpgradeTab() {
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-child(4)", {
- listener: (tabElemLarge) => {
- tabElemLarge.remove();
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Removed large upgrade tab");
- },
- });
- // TODO:FIXME: doesn't work fsr
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout #mini-guide ytmusic-guide-renderer #sections ytmusic-guide-section-renderer[is-primary] #items ytmusic-guide-entry-renderer:nth-child(4)", {
- listener: (tabElemSmall) => {
- tabElemSmall.remove();
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Removed small upgrade tab");
- },
- });
- }
- //#MARKER volume slider
- function initVolumeFeatures() {
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("tp-yt-paper-slider#volume-slider", {
- listener: (sliderElem) => {
- const volSliderCont = document.createElement("div");
- volSliderCont.id = "bytm-vol-slider-cont";
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(sliderElem, volSliderCont);
- if (typeof features.volumeSliderSize === "number")
- setVolSliderSize();
- if (features.volumeSliderLabel)
- addVolumeSliderLabel();
- setVolSliderStep();
- },
- });
- }
- const volSliderSelector = "tp-yt-paper-slider#volume-slider";
- /** Adds a percentage label to the volume slider and tooltip */
- function addVolumeSliderLabel() {
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(volSliderSelector, {
- listener: (sliderElem) => {
- const labelElem = document.createElement("div");
- labelElem.className = "bytm-vol-slider-label";
- labelElem.innerText = `${sliderElem.value}%`;
- sliderElem.addEventListener("change", () => {
- const label = `${sliderElem.value}%`;
- const sensText = features.volumeSliderStep !== ___WEBPACK_IMPORTED_MODULE_8__.featInfo.volumeSliderStep["default"] ? ` (Sensitivity: ${sliderElem.step})` : "";
- const labelFull = `Volume: ${label}${sensText}`;
- sliderElem.setAttribute("title", labelFull); // TODO: probably needs to be on the parent
- sliderElem.setAttribute("aria-valuetext", labelFull);
- const labelElem2 = document.querySelector(".bytm-vol-slider-label");
- if (labelElem2)
- labelElem2.innerText = label;
- });
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("#bytm-vol-slider-cont", {
- listener: (volumeCont) => {
- volumeCont.appendChild(labelElem);
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Added volume slider label", labelElem);
- },
- });
- },
- });
- }
- /** Sets the volume slider to a set size */
- function setVolSliderSize() {
- const { volumeSliderSize: size } = features;
- if (typeof size !== "number" || isNaN(Number(size)))
- return;
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addGlobalStyle)(`\
- /* BetterYTM - set volume slider size */
- #bytm-vol-slider-cont tp-yt-paper-slider#volume-slider {
- width: ${size}px !important;
- }`);
- }
- /** Sets the `step` attribute of the volume slider */
- function setVolSliderStep() {
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(volSliderSelector, {
- listener: (sliderElem) => {
- sliderElem.setAttribute("step", String(features.volumeSliderStep));
- },
- });
- }
- //#MARKER queue buttons
- function initQueueButtons() {
- const addQueueBtns = (evt) => {
- let amt = 0;
- for (const queueItm of (0,_events__WEBPACK_IMPORTED_MODULE_4__.getEvtData)(evt).childNodes) {
- if (!queueItm.classList.contains("bytm-has-queue-btns")) {
- addQueueButtons(queueItm);
- amt++;
- }
- }
- if (amt > 0)
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Added buttons to ${amt} new queue ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", amt)}`);
- };
- _events__WEBPACK_IMPORTED_MODULE_4__.siteEvents.on("queueChanged", addQueueBtns);
- _events__WEBPACK_IMPORTED_MODULE_4__.siteEvents.on("autoplayQueueChanged", addQueueBtns);
- const queueItems = document.querySelectorAll("#contents.ytmusic-player-queue > ytmusic-player-queue-item");
- if (queueItems.length === 0)
- return;
- queueItems.forEach(itm => addQueueButtons(itm));
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Added buttons to ${queueItems.length} existing queue ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", queueItems)}`);
- }
- /**
- * Adds the buttons to each item in the current song queue.
- * Also observes for changes to add new buttons to new items in the queue.
- * TODO:FIXME: deleting an element from the queue shifts the lyrics buttons
- * @param queueItem The element with tagname `ytmusic-player-queue-item` to add queue buttons to
- */
- function addQueueButtons(queueItem) {
- return __awaiter(this, void 0, void 0, function* () {
- //#SECTION general queue item stuff
- const queueBtnsCont = document.createElement("div");
- queueBtnsCont.className = "bytm-queue-btn-container";
- const songInfo = queueItem.querySelector(".song-info");
- if (!songInfo)
- return false;
- const [songEl, artistEl] = songInfo.querySelectorAll("yt-formatted-string");
- const song = songEl.innerText;
- const artist = artistEl.innerText;
- if (!song || !artist)
- return false;
- const lyricsIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("lyrics");
- const deleteIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("delete");
- //#SECTION lyrics btn
- const lyricsBtnElem = yield (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.createLyricsBtn)(undefined, false);
- {
- lyricsBtnElem.title = "Open this song's lyrics in a new tab";
- lyricsBtnElem.style.display = "inline-flex";
- lyricsBtnElem.style.visibility = "initial";
- lyricsBtnElem.style.pointerEvents = "initial";
- lyricsBtnElem.addEventListener("click", (e) => __awaiter(this, void 0, void 0, function* () {
- e.stopPropagation();
- let lyricsUrl;
- const artistsSan = (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.sanitizeArtists)(artist);
- const songSan = (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.sanitizeSong)(song);
- const cachedLyricsUrl = (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.getLyricsCacheEntry)(artistsSan, songSan);
- if (cachedLyricsUrl)
- lyricsUrl = cachedLyricsUrl;
- else if (!songInfo.hasAttribute("data-bytm-loading")) {
- const imgEl = lyricsBtnElem.querySelector("img");
- if (!cachedLyricsUrl) {
- songInfo.setAttribute("data-bytm-loading", "");
- imgEl.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("spinner");
- imgEl.classList.add("bytm-spinner");
- }
- lyricsUrl = cachedLyricsUrl !== null && cachedLyricsUrl !== void 0 ? cachedLyricsUrl : yield (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.getGeniusUrl)(artistsSan, songSan);
- const resetImgElem = () => {
- imgEl.src = lyricsIconUrl;
- imgEl.classList.remove("bytm-spinner");
- };
- if (!cachedLyricsUrl) {
- songInfo.removeAttribute("data-bytm-loading");
- // so the new image doesn't "blink"
- setTimeout(resetImgElem, 100);
- }
- if (!lyricsUrl) {
- resetImgElem();
- if (confirm("Couldn't find a lyrics page for this song.\nDo you want to open genius.com to manually search for it?"))
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.openInNewTab)("https://genius.com/search");
- return;
- }
- }
- lyricsUrl && (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.openInNewTab)(lyricsUrl);
- }));
- }
- //#SECTION delete from queue btn
- const deleteBtnElem = document.createElement("a");
- {
- Object.assign(deleteBtnElem, {
- title: "Remove this song from the queue",
- className: "ytmusic-player-bar bytm-delete-from-queue bytm-generic-btn",
- role: "button",
- });
- deleteBtnElem.style.visibility = "initial";
- deleteBtnElem.addEventListener("click", (e) => __awaiter(this, void 0, void 0, function* () {
- e.stopPropagation();
- // container of the queue item popup menu - element gets reused for every queue item
- let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
- try {
- // three dots button to open the popup menu of a queue item
- const dotsBtnElem = queueItem.querySelector("ytmusic-menu-renderer yt-button-shape button");
- if (queuePopupCont)
- queuePopupCont.setAttribute("data-bytm-hidden", "true");
- dotsBtnElem.click();
- yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(25);
- queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
- if (!queuePopupCont.hasAttribute("data-bytm-hidden"))
- queuePopupCont.setAttribute("data-bytm-hidden", "true");
- // a little bit janky and unreliable but the only way afaik
- const removeFromQueueBtn = queuePopupCont.querySelector("tp-yt-paper-listbox *[role=option]:nth-child(7)");
- yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(20);
- removeFromQueueBtn.click();
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't remove song from queue due to error:", err);
- }
- finally {
- queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.removeAttribute("data-bytm-hidden");
- }
- }));
- const imgElem = document.createElement("img");
- imgElem.className = "bytm-generic-btn-img";
- imgElem.src = deleteIconUrl;
- deleteBtnElem.appendChild(imgElem);
- }
- //#SECTION append elements to DOM
- queueBtnsCont.appendChild(lyricsBtnElem);
- queueBtnsCont.appendChild(deleteBtnElem);
- songInfo.appendChild(queueBtnsCont);
- queueItem.classList.add("bytm-has-queue-btns");
- return true;
- });
- }
- //#MARKER better clickable stuff
- // TODO: add to thumbnails in "songs" list on channel pages (/channel/$id)
- // TODO: add to thumbnails in playlists (/playlist?list=$id)
- /** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
- function addAnchorImprovements() {
- //#SECTION carousel shelves
- try {
- // home page
- /** Only adds anchor improvements for carousel shelves that contain the regular list-item-renderer, not the two-row-item-renderer */
- const condCarouselImprovements = (el) => {
- const listItemRenderer = el.querySelector("ytmusic-responsive-list-item-renderer");
- if (listItemRenderer) {
- const itemsElem = el.querySelector("ul#items");
- if (itemsElem) {
- const improvedElems = improveCarouselAnchors(itemsElem);
- improvedElems > 0 && (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Added anchor improvements to ${improvedElems} carousel shelf ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", improvedElems)}`);
- }
- }
- };
- // initial three shelves aren't included in the event fire
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-carousel-shelf-renderer", {
- listener: () => {
- const carouselShelves = document.body.querySelectorAll("ytmusic-carousel-shelf-renderer");
- carouselShelves.forEach(condCarouselImprovements);
- },
- });
- // every shelf that's loaded by scrolling:
- _events__WEBPACK_IMPORTED_MODULE_4__.siteEvents.on("carouselShelvesChanged", (evt) => {
- const { addedNodes, removedNodes } = (0,_events__WEBPACK_IMPORTED_MODULE_4__.getEvtData)(evt);
- void removedNodes;
- if (addedNodes.length > 0)
- addedNodes.forEach(condCarouselImprovements);
- });
- // related tab in /watch
- // TODO: items are lazy-loaded so this needs to be done differently
- // maybe the onSelectorExists feature can be expanded to conditionally support continuous checking & querySelectorAll
- const relatedTabAnchorImprovements = (tabElem) => {
- const relatedCarouselShelves = tabElem === null || tabElem === void 0 ? void 0 : tabElem.querySelectorAll("ytmusic-carousel-shelf-renderer");
- if (relatedCarouselShelves)
- relatedCarouselShelves.forEach(condCarouselImprovements);
- };
- const relatedTabContentsSelector = "ytmusic-section-list-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] #contents";
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"]", {
- listener: (relatedTabContainer) => {
- const relatedTabObserver = new MutationObserver(([{ addedNodes, removedNodes }]) => {
- if (addedNodes.length > 0 || removedNodes.length > 0)
- relatedTabAnchorImprovements(document.querySelector(relatedTabContentsSelector));
- });
- relatedTabObserver.observe(relatedTabContainer, {
- childList: true,
- });
- },
- });
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(relatedTabContentsSelector, {
- listener: (relatedTabContents) => {
- relatedTabAnchorImprovements(relatedTabContents);
- },
- });
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't improve carousel shelf anchors due to an error:", err);
- }
- //#SECTION sidebar
- try {
- const addSidebarAnchors = (sidebarCont) => {
- const items = sidebarCont.parentNode.querySelectorAll("ytmusic-guide-entry-renderer tp-yt-paper-item");
- improveSidebarAnchors(items);
- return items.length;
- };
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
- listener: (sidebarCont) => {
- const itemsAmt = addSidebarAnchors(sidebarCont);
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Added anchors around ${itemsAmt} sidebar ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", itemsAmt)}`);
- },
- });
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout #mini-guide ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
- listener: (miniSidebarCont) => {
- const itemsAmt = addSidebarAnchors(miniSidebarCont);
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Added anchors around ${itemsAmt} mini sidebar ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", itemsAmt)}`);
- },
- });
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.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) {
- sidebarItems.forEach((item, i) => {
- var _a;
- const anchorElem = document.createElement("a");
- anchorElem.classList.add("bytm-anchor", "bytm-no-select");
- anchorElem.role = "button";
- anchorElem.target = "_self";
- anchorElem.href = (_a = sidebarPaths[i]) !== null && _a !== void 0 ? _a : "#";
- anchorElem.title = "Middle click to open in a new tab";
- anchorElem.addEventListener("click", (e) => {
- e.preventDefault();
- });
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(item, anchorElem);
- });
- }
- /**
- * 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) {
- if (itemsElement.classList.contains("bytm-anchors-improved"))
- return 0;
- let improvedElems = 0;
- try {
- const allListItems = itemsElement.querySelectorAll("ytmusic-responsive-list-item-renderer");
- for (const listItem of allListItems) {
- const thumbnailElem = listItem.querySelector(".left-items");
- const titleElem = listItem.querySelector(".title-column yt-formatted-string.title a");
- if (!thumbnailElem || !titleElem) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.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 bytm-anchor";
- thumbnailAnchor.href = titleElem.href;
- thumbnailAnchor.target = "_self";
- thumbnailAnchor.role = "button";
- thumbnailAnchor.addEventListener("click", (e) => {
- e.preventDefault();
- });
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(thumbnailElem, thumbnailAnchor);
- improvedElems++;
- }
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't add anchor improvements due to error:", err);
- }
- finally {
- itemsElement.classList.add("bytm-anchors-improved");
- }
- return improvedElems;
- }
- /***/ }),
- /***/ "./src/features/lyrics.ts":
- /*!********************************!*\
- !*** ./src/features/lyrics.ts ***!
- \********************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ addLyricsCacheEntry: function() { return /* binding */ addLyricsCacheEntry; },
- /* harmony export */ addMediaCtrlLyricsBtn: function() { return /* binding */ addMediaCtrlLyricsBtn; },
- /* harmony export */ createLyricsBtn: function() { return /* binding */ createLyricsBtn; },
- /* harmony export */ geniUrlBase: function() { return /* binding */ geniUrlBase; },
- /* harmony export */ getCurrentLyricsUrl: function() { return /* binding */ getCurrentLyricsUrl; },
- /* harmony export */ getGeniusUrl: function() { return /* binding */ getGeniusUrl; },
- /* harmony export */ getLyricsCacheEntry: function() { return /* binding */ getLyricsCacheEntry; },
- /* harmony export */ sanitizeArtists: function() { return /* binding */ sanitizeArtists; },
- /* harmony export */ sanitizeSong: function() { return /* binding */ sanitizeSong; }
- /* harmony export */ });
- /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
- /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
- var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- };
- var __asyncValues = (undefined && undefined.__asyncValues) || function (o) {
- if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
- var m = o[Symbol.asyncIterator], i;
- return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
- function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
- function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
- };
- /** Base URL of geniURL */
- const geniUrlBase = "https://api.sv443.net/geniurl";
- /** GeniURL endpoint that gives song metadata when provided with a `?q` or `?artist` and `?song` parameter - [more info](https://api.sv443.net/geniurl) */
- const geniURLSearchTopUrl = `${geniUrlBase}/search/top`;
- /**
- * The threshold to pass to geniURL's fuzzy filtering.
- * From fuse.js docs: At what point does the match algorithm give up. A threshold of 0.0 requires a perfect match (of both letters and location), a threshold of 1.0 would match anything.
- * Set to undefined to use the default.
- */
- const threshold = 0.55;
- /** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
- const geniUrlRatelimitTimeframe = 30;
- const thresholdParam = threshold ? `&threshold=${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.clamp)(threshold, 0, 1)}` : "";
- //#MARKER cache
- /** Cache with key format `ARTIST - SONG` (sanitized) and lyrics URLs as values. Used to prevent extraneous requests to geniURL. */
- const lyricsUrlCache = new Map();
- /** How many cache entries can exist at a time - this is used to cap memory usage */
- const maxLyricsCacheSize = 100;
- /**
- * Returns the lyrics URL from the passed un-/sanitized artist and song name, or undefined if the entry doesn't exist yet.
- * **The passed parameters need to be sanitized first!**
- */
- function getLyricsCacheEntry(artists, song) {
- return lyricsUrlCache.get(`${artists} - ${song}`);
- }
- /** Adds the provided entry into the lyrics URL cache */
- function addLyricsCacheEntry(artists, song, lyricsUrl) {
- lyricsUrlCache.set(`${sanitizeArtists(artists)} - ${sanitizeSong(song)}`, lyricsUrl);
- // delete oldest entry if cache gets too big
- if (lyricsUrlCache.size > maxLyricsCacheSize)
- lyricsUrlCache.delete([...lyricsUrlCache.keys()].at(-1));
- }
- //#MARKER media control bar
- let mcCurrentSongTitle = "";
- /** Adds a lyrics button to the media controls bar */
- function addMediaCtrlLyricsBtn() {
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer", { listener: addActualMediaCtrlLyricsBtn });
- }
- // TODO: add error.svg if the request fails
- /** Actually adds the lyrics button after the like button renderer has been verified to exist */
- function addActualMediaCtrlLyricsBtn(likeContainer) {
- return __awaiter(this, void 0, void 0, function* () {
- const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
- // run parallel without awaiting so the MutationObserver below can observe the title element in time
- (() => __awaiter(this, void 0, void 0, function* () {
- const gUrl = yield getCurrentLyricsUrl();
- const linkElem = yield createLyricsBtn(gUrl !== null && gUrl !== void 0 ? gUrl : undefined);
- linkElem.id = "betterytm-lyrics-button";
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Inserted lyrics button into media controls bar");
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.insertAfter)(likeContainer, linkElem);
- }))();
- mcCurrentSongTitle = songTitleElem.title;
- const spinnerIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("spinner");
- const lyricsIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("lyrics");
- const onMutation = (mutations) => { var _a, mutations_1, mutations_1_1; return __awaiter(this, void 0, void 0, function* () {
- var _b, e_1, _c, _d;
- try {
- for (_a = true, mutations_1 = __asyncValues(mutations); mutations_1_1 = yield mutations_1.next(), _b = mutations_1_1.done, !_b;) {
- _d = mutations_1_1.value;
- _a = false;
- try {
- const mut = _d;
- const newTitle = mut.target.title;
- if (newTitle !== mcCurrentSongTitle && newTitle.length > 0) {
- const lyricsBtn = document.querySelector("#betterytm-lyrics-button");
- if (!lyricsBtn)
- return;
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Song title changed from '${mcCurrentSongTitle}' to '${newTitle}'`);
- lyricsBtn.style.cursor = "wait";
- lyricsBtn.style.pointerEvents = "none";
- const imgElem = lyricsBtn.querySelector("img");
- imgElem.src = spinnerIconUrl;
- imgElem.classList.add("bytm-spinner");
- mcCurrentSongTitle = newTitle;
- const url = yield getCurrentLyricsUrl(); // can take a second or two
- imgElem.src = lyricsIconUrl;
- imgElem.classList.remove("bytm-spinner");
- if (!url)
- continue;
- lyricsBtn.href = url;
- lyricsBtn.title = "Open the current song's lyrics in a new tab";
- lyricsBtn.style.cursor = "pointer";
- lyricsBtn.style.visibility = "initial";
- lyricsBtn.style.display = "inline-flex";
- lyricsBtn.style.pointerEvents = "initial";
- }
- }
- finally {
- _a = true;
- }
- }
- }
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
- finally {
- try {
- if (!_a && !_b && (_c = mutations_1.return)) yield _c.call(mutations_1);
- }
- finally { if (e_1) throw e_1.error; }
- }
- }); };
- // since YT and YTM don't reload the page on video change, MutationObserver needs to be used to watch for changes in the video title
- const obs = new MutationObserver(onMutation);
- obs.observe(songTitleElem, { attributes: true, attributeFilter: ["title"] });
- });
- }
- //#MARKER utils
- /** Removes everything in parentheses from the passed song name */
- function sanitizeSong(songName) {
- const parensRegex = /\(.+\)/gmi;
- const squareParensRegex = /\[.+\]/gmi;
- // trim right after the song name:
- const sanitized = songName
- .replace(parensRegex, "")
- .replace(squareParensRegex, "");
- return sanitized.trim();
- }
- /** Removes the secondary artist (if it exists) from the passed artists string */
- function sanitizeArtists(artists) {
- artists = artists.split(/\s*\u2022\s*/gmiu)[0]; // split at • [•] character
- if (artists.match(/&/))
- artists = artists.split(/\s*&\s*/gm)[0];
- if (artists.match(/,/))
- artists = artists.split(/,\s*/gm)[0];
- return artists.trim();
- }
- /** Returns the lyrics URL from genius for the currently selected song */
- function getCurrentLyricsUrl() {
- var _a;
- return __awaiter(this, void 0, void 0, function* () {
- try {
- // In videos the video title contains both artist and song title, in "regular" YTM songs, the video title only contains the song title
- const isVideo = typeof ((_a = document.querySelector("ytmusic-player")) === null || _a === void 0 ? void 0 : _a.getAttribute("video-mode_")) === "string";
- const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
- const songMetaElem = document.querySelector("span.subtitle > yt-formatted-string:first-child");
- if (!songTitleElem || !songMetaElem || !songTitleElem.title)
- return undefined;
- const songNameRaw = songTitleElem.title;
- const songName = sanitizeSong(songNameRaw);
- const artistName = sanitizeArtists(songMetaElem.title);
- /** Use when the current song is not a "real YTM song" with a static background, but rather a music video */
- const getGeniusUrlVideo = () => __awaiter(this, void 0, void 0, function* () {
- if (!songName.includes("-")) // for some fucking reason some music videos have YTM-like song title and artist separation, some don't
- return yield getGeniusUrl(artistName, songName);
- const [artist, ...rest] = songName.split("-").map(v => v.trim());
- return yield getGeniusUrl(artist, rest.join(" "));
- });
- // TODO: artist might need further splitting before comma or ampersand
- const url = isVideo ? yield getGeniusUrlVideo() : yield getGeniusUrl(artistName, songName);
- return url;
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't resolve lyrics URL:", err);
- return undefined;
- }
- });
- }
- /** Fetches the actual lyrics URL from geniURL - **the passed parameters need to be sanitized first!** */
- function getGeniusUrl(artist, song) {
- var _a, _b, _c;
- return __awaiter(this, void 0, void 0, function* () {
- try {
- const cacheEntry = getLyricsCacheEntry(artist, song);
- if (cacheEntry) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Found lyrics URL in cache: ${cacheEntry}`);
- return cacheEntry;
- }
- const startTs = Date.now();
- const fetchUrl = `${geniURLSearchTopUrl}?artist=${encodeURIComponent(artist)}&song=${encodeURIComponent(song)}${thresholdParam}`;
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Requesting URL from geniURL at '${fetchUrl}'`);
- const fetchRes = yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.fetchAdvanced)(fetchUrl);
- if (fetchRes.status === 429) {
- alert(`You are being rate limited.\nPlease wait ${(_a = fetchRes.headers.get("retry-after")) !== null && _a !== void 0 ? _a : geniUrlRatelimitTimeframe} seconds before requesting more lyrics.`);
- return undefined;
- }
- else if (fetchRes.status < 200 || fetchRes.status >= 300) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)(`Couldn't fetch lyrics URL from geniURL - status: ${fetchRes.status} - response: ${(_c = (_b = (yield fetchRes.json()).message) !== null && _b !== void 0 ? _b : yield fetchRes.text()) !== null && _c !== void 0 ? _c : "(none)"}`);
- return undefined;
- }
- const result = yield fetchRes.json();
- if (typeof result === "object" && result.error) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't fetch lyrics URL:", result.message);
- return undefined;
- }
- const url = result.url;
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Found lyrics URL (after ${Date.now() - startTs}ms): ${url}`);
- addLyricsCacheEntry(artist, song, url);
- return url;
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't get lyrics URL due to error:", err);
- return undefined;
- }
- });
- }
- /** Creates the base lyrics button element */
- function createLyricsBtn(geniusUrl, hideIfLoading = true) {
- return __awaiter(this, void 0, void 0, function* () {
- const linkElem = document.createElement("a");
- linkElem.className = "ytmusic-player-bar bytm-generic-btn";
- linkElem.title = geniusUrl ? "Click to open this song's lyrics in a new tab" : "Loading lyrics URL...";
- if (geniusUrl)
- linkElem.href = geniusUrl;
- linkElem.role = "button";
- linkElem.target = "_blank";
- linkElem.rel = "noopener noreferrer";
- linkElem.style.visibility = hideIfLoading && geniusUrl ? "initial" : "hidden";
- linkElem.style.display = hideIfLoading && geniusUrl ? "inline-flex" : "none";
- const imgElem = document.createElement("img");
- imgElem.className = "bytm-generic-btn-img";
- imgElem.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("lyrics");
- linkElem.appendChild(imgElem);
- return linkElem;
- });
- }
- /***/ }),
- /***/ "./src/features/menu/menu.ts":
- /*!***********************************!*\
- !*** ./src/features/menu/menu.ts ***!
- \***********************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ closeMenu: function() { return /* binding */ closeMenu; },
- /* harmony export */ initMenu: function() { return /* binding */ initMenu; },
- /* harmony export */ openMenu: function() { return /* binding */ openMenu; },
- /* harmony export */ setActiveTab: function() { return /* binding */ setActiveTab; }
- /* harmony export */ });
- /* harmony import */ var _changelog_md__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../../changelog.md */ "./changelog.md");
- /* harmony import */ var _menu_html__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./menu.html */ "./src/features/menu/menu.html");
- /* harmony import */ var _menu_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./menu.css */ "./src/features/menu/menu.css");
- // REQUIREMENTS:
- // - modal using the <dialog> element
- // - sections with headers
- // - support for "custom widgets"
- // - debounce or save on button press to store new configuration
- // - much better scaling including no vw and vh units
- //#MARKER menu
- /**
- * These are the base selector values for the menu tabs
- * Header selector format: `#${baseValue}-header`
- * Content selector format: `#${baseValue}-content`
- */
- const tabsSelectors = {
- options: "bytm-menu-tab-options",
- info: "bytm-menu-tab-info",
- changelog: "bytm-menu-tab-changelog",
- };
- /** Called from init(), before DOMContentLoaded is fired */
- function initMenu() {
- document.addEventListener("DOMContentLoaded", () => {
- // create menu container
- const menuContainer = document.createElement("div");
- menuContainer.id = "bytm-menu-container";
- // add menu html
- menuContainer.innerHTML = _menu_html__WEBPACK_IMPORTED_MODULE_1__["default"];
- document.body.appendChild(menuContainer);
- initMenuContents();
- });
- }
- function initMenuContents() {
- var _a;
- // hook events
- for (const tab in tabsSelectors) {
- const selector = tabsSelectors[tab];
- (_a = document.querySelector(`#${selector}-header`)) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => {
- setActiveTab(tab);
- });
- }
- // init tab contents
- initOptionsContent();
- initInfoContent();
- initChangelogContent();
- }
- /** Opens the specified tab */
- function setActiveTab(tab) {
- const tabs = Object.assign({}, tabsSelectors);
- delete tabs[tab];
- // disable all but new active tab
- for (const [, val] of Object.entries(tabs)) {
- document.querySelector(`#${val}-header`).dataset.active = "false";
- document.querySelector(`#${val}-content`).dataset.active = "false";
- }
- // enable new active tab
- document.querySelector(`#${tabsSelectors[tab]}-header`).dataset.active = "true";
- document.querySelector(`#${tabsSelectors[tab]}-content`).dataset.active = "true";
- }
- /** Opens the modal menu dialog */
- function openMenu() {
- var _a;
- (_a = document.querySelector("#bytm-menu-dialog")) === null || _a === void 0 ? void 0 : _a.showModal();
- }
- /** Closes the modal menu dialog */
- function closeMenu() {
- var _a;
- (_a = document.querySelector("#bytm-menu-dialog")) === null || _a === void 0 ? void 0 : _a.close();
- }
- //#MARKER menu tab contents
- function initOptionsContent() {
- const tab = document.querySelector("#bytm-menu-tab-options-content");
- void tab;
- }
- function initInfoContent() {
- const tab = document.querySelector("#bytm-menu-tab-info-content");
- void tab;
- }
- function initChangelogContent() {
- const tab = document.querySelector("#bytm-menu-tab-changelog-content");
- tab.innerHTML = _changelog_md__WEBPACK_IMPORTED_MODULE_0__["default"];
- }
- /***/ }),
- /***/ "./src/features/menu/menu_old.ts":
- /*!***************************************!*\
- !*** ./src/features/menu/menu_old.ts ***!
- \***************************************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ addMenu: function() { return /* binding */ addMenu; },
- /* harmony export */ closeMenu: function() { return /* binding */ closeMenu; },
- /* harmony export */ openMenu: function() { return /* binding */ openMenu; }
- /* harmony export */ });
- /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
- /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../config */ "./src/config.ts");
- /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../constants */ "./src/constants.ts");
- /* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../index */ "./src/features/index.ts");
- /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../utils */ "./src/utils.ts");
- /* harmony import */ var _menu_old_css__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu_old.css */ "./src/features/menu/menu_old.css");
- var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- };
- //#MARKER create menu elements
- let isMenuOpen = false;
- /**
- * Adds an element to open the BetterYTM menu
- * @deprecated to be replaced with new menu - see https://github.com/Sv443/BetterYTM/issues/23
- */
- function addMenu() {
- var _a, _b;
- return __awaiter(this, void 0, void 0, function* () {
- //#SECTION backdrop & menu container
- const backgroundElem = document.createElement("div");
- backgroundElem.id = "betterytm-menu-bg";
- backgroundElem.title = "Click here to close the menu";
- backgroundElem.style.visibility = "hidden";
- backgroundElem.style.display = "none";
- backgroundElem.addEventListener("click", (e) => {
- var _a;
- if (isMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "betterytm-menu-bg")
- closeMenu(e);
- });
- document.body.addEventListener("keydown", (e) => {
- if (isMenuOpen && e.key === "Escape")
- closeMenu(e);
- });
- const menuContainer = document.createElement("div");
- menuContainer.title = "";
- menuContainer.id = "betterytm-menu";
- menuContainer.style.borderRadius = "15px";
- menuContainer.style.display = "flex";
- menuContainer.style.flexDirection = "column";
- menuContainer.style.justifyContent = "space-between";
- //#SECTION title bar
- const titleCont = document.createElement("div");
- titleCont.style.padding = "8px 20px 15px 20px";
- titleCont.style.display = "flex";
- titleCont.style.justifyContent = "space-between";
- titleCont.id = "betterytm-menu-titlecont";
- const titleElem = document.createElement("h2");
- titleElem.id = "betterytm-menu-title";
- titleElem.classList.add("bytm-no-select");
- titleElem.innerText = `${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.name} - Configuration`;
- const linksCont = document.createElement("div");
- linksCont.id = "betterytm-menu-linkscont";
- const addLink = (imgSrc, href, title) => {
- const anchorElem = document.createElement("a");
- anchorElem.className = "betterytm-menu-link";
- anchorElem.rel = "noopener noreferrer";
- anchorElem.target = "_blank";
- anchorElem.href = href;
- anchorElem.title = title;
- anchorElem.style.marginLeft = "10px";
- const imgElem = document.createElement("img");
- imgElem.className = "betterytm-menu-img bytm-no-select";
- imgElem.src = imgSrc;
- imgElem.style.width = "32px";
- imgElem.style.height = "32px";
- anchorElem.appendChild(imgElem);
- linksCont.appendChild(anchorElem);
- };
- addLink(yield (0,_utils__WEBPACK_IMPORTED_MODULE_4__.getResourceUrl)("github"), _constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.namespace, `${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.name} on GitHub`);
- addLink(yield (0,_utils__WEBPACK_IMPORTED_MODULE_4__.getResourceUrl)("greasyfork"), "https://greasyfork.org/TODO", `${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.name} on GreasyFork`);
- const closeElem = document.createElement("img");
- closeElem.id = "betterytm-menu-close";
- closeElem.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_4__.getResourceUrl)("close");
- closeElem.title = "Click to close the menu";
- closeElem.style.marginLeft = "50px";
- closeElem.style.width = "32px";
- closeElem.style.height = "32px";
- closeElem.addEventListener("click", closeMenu);
- linksCont.appendChild(closeElem);
- titleCont.appendChild(titleElem);
- titleCont.appendChild(linksCont);
- //#SECTION feature list
- const featuresCont = document.createElement("div");
- featuresCont.id = "betterytm-menu-opts";
- featuresCont.style.display = "flex";
- featuresCont.style.flexDirection = "column";
- featuresCont.style.overflowY = "auto";
- /** Gets called whenever the feature config is changed */
- const confChanged = (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.debounce)((key, initialVal, newVal) => __awaiter(this, void 0, void 0, function* () {
- const fmt = (val) => typeof val === "object" ? JSON.stringify(val) : String(val);
- (0,_utils__WEBPACK_IMPORTED_MODULE_4__.info)(`Feature config changed, key '${key}' from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
- const featConf = Object.assign({}, yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.getFeatures)());
- featConf[key] = newVal;
- yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.saveFeatureConf)(featConf);
- (0,_utils__WEBPACK_IMPORTED_MODULE_4__.log)("Saved feature config changes:\n", yield GM.getValue("betterytm-config"));
- }));
- const features = yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.getFeatures)();
- for (const key in features) {
- const ftInfo = _index__WEBPACK_IMPORTED_MODULE_3__.featInfo[key];
- // @ts-ignore
- if (!ftInfo || ftInfo.visible === false)
- continue;
- const { desc, type, default: ftDefault } = ftInfo;
- // @ts-ignore
- const step = (_a = ftInfo === null || ftInfo === void 0 ? void 0 : ftInfo.step) !== null && _a !== void 0 ? _a : undefined;
- const val = features[key];
- const initialVal = (_b = val !== null && val !== void 0 ? val : ftDefault) !== null && _b !== void 0 ? _b : undefined;
- const ftConfElem = document.createElement("div");
- ftConfElem.id = `betterytm-ftconf-${key}`;
- ftConfElem.style.display = "flex";
- ftConfElem.style.flexDirection = "row";
- ftConfElem.style.justifyContent = "space-between";
- ftConfElem.style.padding = "8px 20px";
- {
- const textElem = document.createElement("span");
- textElem.style.display = "inline-block";
- textElem.style.fontSize = "15px";
- textElem.innerText = desc;
- ftConfElem.appendChild(textElem);
- }
- {
- let inputType = "text";
- switch (type) {
- case "toggle":
- inputType = "checkbox";
- break;
- case "slider":
- inputType = "range";
- break;
- case "number":
- inputType = "number";
- break;
- }
- const inputElemId = `betterytm-ftconf-${key}-input`;
- const ctrlElem = document.createElement("span");
- ctrlElem.style.display = "inline-flex";
- ctrlElem.style.alignItems = "center";
- ctrlElem.style.whiteSpace = "nowrap";
- const inputElem = document.createElement("input");
- inputElem.id = inputElemId;
- inputElem.type = inputType;
- if (type === "toggle")
- inputElem.style.marginLeft = "5px";
- if (typeof initialVal !== "undefined")
- inputElem.value = String(initialVal);
- if (type === "number" && step)
- inputElem.step = step;
- // @ts-ignore
- if (ftInfo.min && ftInfo.max) {
- // @ts-ignore
- inputElem.min = ftInfo.min;
- // @ts-ignore
- inputElem.max = ftInfo.max;
- }
- if (type === "toggle" && typeof initialVal !== "undefined")
- inputElem.checked = Boolean(initialVal);
- // @ts-ignore
- const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
- const fmtVal = (v) => String(v).trim();
- const toggleLabelText = (toggled) => toggled ? "On" : "Off";
- let labelElem;
- if (type === "slider") {
- labelElem = document.createElement("label");
- labelElem.classList.add("betterytm-ftconf-label");
- labelElem.style.marginRight = "20px";
- labelElem.style.fontSize = "16px";
- labelElem.htmlFor = inputElemId;
- labelElem.innerText = fmtVal(initialVal) + unitTxt;
- inputElem.addEventListener("input", () => {
- if (labelElem)
- labelElem.innerText = fmtVal(parseInt(inputElem.value)) + unitTxt;
- });
- }
- else if (type === "toggle") {
- labelElem = document.createElement("label");
- labelElem.classList.add("betterytm-ftconf-label");
- labelElem.style.paddingLeft = "10px";
- labelElem.style.paddingRight = "5px";
- labelElem.style.fontSize = "16px";
- labelElem.htmlFor = inputElemId;
- labelElem.innerText = toggleLabelText(Boolean(initialVal)) + unitTxt;
- inputElem.addEventListener("input", () => {
- if (labelElem)
- labelElem.innerText = toggleLabelText(inputElem.checked) + unitTxt;
- });
- }
- inputElem.addEventListener("input", () => {
- let v = Number(String(inputElem.value).trim());
- if (isNaN(v))
- v = Number(inputElem.value);
- if (typeof initialVal !== "undefined")
- confChanged(key, initialVal, (type !== "toggle" ? v : inputElem.checked));
- });
- labelElem && ctrlElem.appendChild(labelElem);
- ctrlElem.appendChild(inputElem);
- ftConfElem.appendChild(ctrlElem);
- }
- featuresCont.appendChild(ftConfElem);
- }
- //#SECTION footer
- const footerCont = document.createElement("div");
- footerCont.id = "betterytm-menu-footer-cont";
- footerCont.style.display = "flex";
- footerCont.style.flexDirection = "row";
- footerCont.style.justifyContent = "space-between";
- footerCont.style.padding = "10px 20px";
- footerCont.style.marginTop = "20px";
- footerCont.style.position = "sticky";
- footerCont.style.bottom = "0";
- footerCont.style.backgroundColor = "var(--bytm-menu-bg)";
- const footerElem = document.createElement("div");
- footerElem.id = "betterytm-menu-footer";
- footerElem.style.fontSize = "17px";
- footerElem.style.textDecoration = "underline";
- footerElem.innerText = "You need to reload the page to apply changes.";
- const reloadElem = document.createElement("button");
- reloadElem.style.marginLeft = "20px";
- reloadElem.innerText = "Reload now";
- reloadElem.title = "Click to reload the page";
- reloadElem.addEventListener("click", () => location.reload());
- const resetElem = document.createElement("button");
- resetElem.className = "bytm-cfg-reset-btn";
- resetElem.title = "Click to reset all settings to their default value";
- resetElem.innerText = "Reset";
- resetElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
- if (confirm("Do you really want to reset all settings to their default value?\nThe page will automatically reload if you proceed.")) {
- yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.setDefaultFeatConf)();
- location.reload();
- }
- }));
- footerElem.appendChild(reloadElem);
- footerCont.appendChild(footerElem);
- footerCont.appendChild(resetElem);
- featuresCont.appendChild(footerCont);
- //#SECTION finalize
- const menuBody = document.createElement("div");
- menuBody.id = "betterytm-menu-body";
- menuBody.appendChild(titleCont);
- menuBody.appendChild(featuresCont);
- const versionCont = document.createElement("div");
- versionCont.style.display = "flex";
- versionCont.style.justifyContent = "space-around";
- versionCont.style.fontSize = "1.15em";
- versionCont.style.marginTop = "10px";
- versionCont.style.marginBottom = "5px";
- const versionElem = document.createElement("span");
- versionElem.id = "betterytm-menu-version";
- versionElem.innerText = `v${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.version}`;
- versionCont.appendChild(versionElem);
- featuresCont.appendChild(versionCont);
- menuContainer.appendChild(menuBody);
- menuContainer.appendChild(versionCont);
- backgroundElem.appendChild(menuContainer);
- document.body.appendChild(backgroundElem);
- (0,_utils__WEBPACK_IMPORTED_MODULE_4__.log)("Added menu element:", backgroundElem);
- });
- }
- //#MARKER utilities
- function closeMenu(e) {
- if (!isMenuOpen)
- return;
- isMenuOpen = false;
- (e === null || e === void 0 ? void 0 : e.bubbles) && e.stopPropagation();
- document.body.classList.remove("bytm-disable-scroll");
- const menuBg = document.querySelector("#betterytm-menu-bg");
- menuBg.style.visibility = "hidden";
- menuBg.style.display = "none";
- }
- // function that opens the menu, it should do the inverse of closeMenu()
- function openMenu() {
- if (isMenuOpen)
- return;
- isMenuOpen = true;
- document.body.classList.add("bytm-disable-scroll");
- const menuBg = document.querySelector("#betterytm-menu-bg");
- menuBg.style.visibility = "visible";
- menuBg.style.display = "block";
- }
- /***/ }),
- /***/ "./src/utils.ts":
- /*!**********************!*\
- !*** ./src/utils.ts ***!
- \**********************/
- /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ dbg: function() { return /* binding */ dbg; },
- /* harmony export */ error: function() { return /* binding */ error; },
- /* harmony export */ getDomain: function() { return /* binding */ getDomain; },
- /* harmony export */ getResourceUrl: function() { return /* binding */ getResourceUrl; },
- /* harmony export */ getVideoTime: function() { return /* binding */ getVideoTime; },
- /* harmony export */ info: function() { return /* binding */ info; },
- /* harmony export */ log: function() { return /* binding */ log; },
- /* harmony export */ setLogLevel: function() { return /* binding */ setLogLevel; },
- /* harmony export */ warn: function() { return /* binding */ warn; }
- /* harmony export */ });
- /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
- /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./constants */ "./src/constants.ts");
- //#SECTION logging
- let curLogLevel = 1;
- /** Sets the current log level. 0 = Debug, 1 = Info */
- function setLogLevel(level) {
- curLogLevel = level;
- }
- /** Extracts the log level from the last item from spread arguments - returns 1 if the last item is not a number or too low or high */
- function getLogLevel(args) {
- const minLogLvl = 0, maxLogLvl = 1;
- if (typeof args.at(-1) === "number")
- return (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.clamp)(args.splice(args.length - 1)[0], minLogLvl, maxLogLvl);
- return 0;
- }
- /** Common prefix to be able to tell logged messages apart and filter them in devtools */
- const consPrefix = `[${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name}]`;
- const consPrefixDbg = `[${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name}/#DEBUG]`;
- /**
- * Logs string-compatible values to the console, as long as the log level is sufficient.
- * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
- */
- function log(...args) {
- if (curLogLevel <= getLogLevel(args))
- console.log(consPrefix, ...args);
- }
- /**
- * Logs string-compatible values to the console as info, as long as the log level is sufficient.
- * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
- */
- function info(...args) {
- if (curLogLevel <= getLogLevel(args))
- console.info(consPrefix, ...args);
- }
- /**
- * Logs string-compatible values to the console as a warning, as long as the log level is sufficient.
- * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
- */
- function warn(...args) {
- if (curLogLevel <= getLogLevel(args))
- console.warn(consPrefix, ...args);
- }
- /** Logs string-compatible values to the console as an error, no matter the log level. */
- function error(...args) {
- console.error(consPrefix, ...args);
- }
- /** Logs string-compatible values to the console, intended for debugging only */
- function dbg(...args) {
- console.log(consPrefixDbg, ...args);
- }
- //#SECTION video time
- /**
- * Returns the current video time in seconds
- * Dispatches mouse movement events in case the video time can't be estimated
- * @returns Returns null if the video time is unavailable
- */
- function getVideoTime() {
- return new Promise((res) => {
- const domain = getDomain();
- try {
- if (domain === "ytm") {
- const pbEl = document.querySelector("#progress-bar");
- return res(!isNaN(Number(pbEl.value)) ? Number(pbEl.value) : null);
- }
- else if (domain === "yt") {
- // YT doesn't update the progress bar when it's hidden (contrary to YTM which never hides it)
- ytForceShowVideoTime();
- const pbSelector = ".ytp-chrome-bottom div.ytp-progress-bar[role=\"slider\"]";
- const progElem = document.querySelector(pbSelector);
- let videoTime = progElem ? Number(progElem.getAttribute("aria-valuenow")) : -1;
- const mut = new MutationObserver(() => {
- // .observe() is only called when the element exists - no need to check for null
- videoTime = Number(document.querySelector(pbSelector).getAttribute("aria-valuenow"));
- });
- const observe = (progElem) => {
- mut.observe(progElem, {
- attributes: true,
- attributeFilter: ["aria-valuenow"],
- });
- setTimeout(() => {
- res(videoTime >= 0 && !isNaN(videoTime) ? videoTime : null);
- }, 500);
- };
- if (!progElem)
- return (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(pbSelector, { listener: observe });
- else
- return observe(progElem);
- }
- }
- catch (err) {
- error("Couldn't get video time due to error:", err);
- res(null);
- }
- });
- }
- /**
- * Sends events that force the video controls to become visible for about 3 seconds.
- * This only works once, then the page needs to be reloaded!
- */
- function ytForceShowVideoTime() {
- const player = document.querySelector("#movie_player");
- if (!player)
- return false;
- const defaultProps = {
- // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
- view: (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.getUnsafeWindow)(),
- bubbles: true,
- cancelable: false,
- };
- player.dispatchEvent(new MouseEvent("mouseenter", defaultProps));
- const { x, y, width, height } = player.getBoundingClientRect();
- const screenY = Math.round(y + height / 2);
- const screenX = x + Math.min(50, Math.round(width / 3));
- player.dispatchEvent(new MouseEvent("mousemove", Object.assign(Object.assign({}, defaultProps), { screenY,
- screenX, movementX: 5, movementY: 0 })));
- return true;
- }
- // /** Parses a video time string in the format `[hh:m]m:ss` to the equivalent number of seconds - returns 0 if input couldn't be parsed */
- // function parseVideoTime(videoTime: string) {
- // const matches = /^((\d{1,2}):)?(\d{1,2}):(\d{2})$/.exec(videoTime);
- // if(!matches)
- // return 0;
- // const [, , hrs, min, sec] = matches as unknown as [string, string | undefined, string | undefined, string, string];
- // let finalTime = 0;
- // if(hrs)
- // finalTime += Number(hrs) * 60 * 60;
- // finalTime += Number(min) * 60 + Number(sec);
- // return isNaN(finalTime) ? 0 : finalTime;
- // }
- // const selectorExistsMap = new Map<string, Array<(element: HTMLElement) => void>>();
- // /**
- // * Calls the `listener` as soon as the `selector` exists in the DOM.
- // * Listeners are deleted as soon as they are called once.
- // * Multiple listeners with the same selector may be registered.
- // */
- // export function onSelectorExists(selector: string, listener: (element: HTMLElement) => void) {
- // const el = document.querySelector<HTMLElement>(selector);
- // if(el)
- // listener(el);
- // else {
- // if(selectorExistsMap.get(selector))
- // selectorExistsMap.set(selector, [...selectorExistsMap.get(selector)!, listener]);
- // else
- // selectorExistsMap.set(selector, [listener]);
- // }
- // }
- // /** Initializes the MutationObserver responsible for checking selectors registered in `onSelectorExists()` */
- // export function initSelectorExistsCheck() {
- // const observer = new MutationObserver(() => {
- // for(const [selector, listeners] of selectorExistsMap.entries()) {
- // const el = document.querySelector<HTMLElement>(selector);
- // if(el) {
- // listeners.forEach(listener => listener(el));
- // selectorExistsMap.delete(selector);
- // }
- // }
- // });
- // observer.observe(document.body, {
- // subtree: true,
- // childList: true,
- // });
- // log("Initialized \"selector exists\" MutationObserver");
- // }
- /**
- * Returns the current domain as a constant string representation
- * @throws Throws if script runs on an unexpected website
- */
- function getDomain() {
- const { hostname } = new URL(location.href);
- if (hostname.includes("music.youtube"))
- return "ytm";
- else if (hostname.includes("youtube"))
- return "yt";
- else
- throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
- }
- /** Returns the URL of a resource by its name, as defined in `assets/resources.json`, from GM resource cache - [see GM.getResourceUrl docs](https://wiki.greasespot.net/GM.getResourceUrl) */
- function getResourceUrl(name) {
- return GM.getResourceUrl(name);
- }
- /***/ }),
- /***/ "../../svn/UserUtils/dist/index.mjs":
- /*!******************************************!*\
- !*** ../../svn/UserUtils/dist/index.mjs ***!
- \******************************************/
- /***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ Config: function() { return /* binding */ Config; },
- /* harmony export */ addGlobalStyle: function() { return /* binding */ addGlobalStyle; },
- /* harmony export */ addParent: function() { return /* binding */ addParent; },
- /* harmony export */ amplifyMedia: function() { return /* binding */ amplifyMedia; },
- /* harmony export */ autoPlural: function() { return /* binding */ autoPlural; },
- /* harmony export */ clamp: function() { return /* binding */ clamp; },
- /* harmony export */ debounce: function() { return /* binding */ debounce; },
- /* harmony export */ fetchAdvanced: function() { return /* binding */ fetchAdvanced; },
- /* harmony export */ getSelectorMap: function() { return /* binding */ getSelectorMap; },
- /* harmony export */ getUnsafeWindow: function() { return /* binding */ getUnsafeWindow; },
- /* harmony export */ initOnSelector: function() { return /* binding */ initOnSelector; },
- /* harmony export */ insertAfter: function() { return /* binding */ insertAfter; },
- /* harmony export */ interceptEvent: function() { return /* binding */ interceptEvent; },
- /* harmony export */ interceptWindowEvent: function() { return /* binding */ interceptWindowEvent; },
- /* harmony export */ mapRange: function() { return /* binding */ mapRange; },
- /* harmony export */ onSelector: function() { return /* binding */ onSelector; },
- /* harmony export */ openInNewTab: function() { return /* binding */ openInNewTab; },
- /* harmony export */ pauseFor: function() { return /* binding */ pauseFor; },
- /* harmony export */ preloadImages: function() { return /* binding */ preloadImages; },
- /* harmony export */ randRange: function() { return /* binding */ randRange; },
- /* harmony export */ randomItem: function() { return /* binding */ randomItem; },
- /* harmony export */ randomItemIndex: function() { return /* binding */ randomItemIndex; },
- /* harmony export */ randomizeArray: function() { return /* binding */ randomizeArray; },
- /* harmony export */ removeOnSelector: function() { return /* binding */ removeOnSelector; },
- /* harmony export */ takeRandomItem: function() { return /* binding */ takeRandomItem; }
- /* harmony export */ });
- var __defProp = Object.defineProperty;
- var __defProps = Object.defineProperties;
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
- var __hasOwnProp = Object.prototype.hasOwnProperty;
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
- var __spreadValues = (a, b) => {
- for (var prop in b || (b = {}))
- if (__hasOwnProp.call(b, prop))
- __defNormalProp(a, prop, b[prop]);
- if (__getOwnPropSymbols)
- for (var prop of __getOwnPropSymbols(b)) {
- if (__propIsEnum.call(b, prop))
- __defNormalProp(a, prop, b[prop]);
- }
- return a;
- };
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
- var __publicField = (obj, key, value) => {
- __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
- return value;
- };
- var __async = (__this, __arguments, generator) => {
- return new Promise((resolve, reject) => {
- var fulfilled = (value) => {
- try {
- step(generator.next(value));
- } catch (e) {
- reject(e);
- }
- };
- var rejected = (value) => {
- try {
- step(generator.throw(value));
- } catch (e) {
- reject(e);
- }
- };
- var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
- step((generator = generator.apply(__this, __arguments)).next());
- });
- };
- // lib/math.ts
- function clamp(value, min, max) {
- return Math.max(Math.min(value, max), min);
- }
- function mapRange(value, range_1_min, range_1_max, range_2_min, range_2_max) {
- if (Number(range_1_min) === 0 && Number(range_2_min) === 0)
- return value * (range_2_max / range_1_max);
- return (value - range_1_min) * ((range_2_max - range_2_min) / (range_1_max - range_1_min)) + range_2_min;
- }
- function randRange(...args) {
- let min, max;
- if (typeof args[0] === "number" && typeof args[1] === "number") {
- [min, max] = args;
- } else if (typeof args[0] === "number" && typeof args[1] !== "number") {
- min = 0;
- max = args[0];
- } else
- throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
- min = Number(min);
- max = Number(max);
- if (isNaN(min) || isNaN(max))
- throw new TypeError(`Parameters "min" and "max" can't be NaN`);
- if (min > max)
- throw new TypeError(`Parameter "min" can't be bigger than "max"`);
- return Math.floor(Math.random() * (max - min + 1)) + min;
- }
- // lib/array.ts
- function randomItem(array) {
- return randomItemIndex(array)[0];
- }
- function randomItemIndex(array) {
- if (array.length === 0)
- return [void 0, void 0];
- const idx = randRange(array.length - 1);
- return [array[idx], idx];
- }
- function takeRandomItem(arr) {
- const [itm, idx] = randomItemIndex(arr);
- if (idx === void 0)
- return void 0;
- arr.splice(idx, 1);
- return itm;
- }
- function randomizeArray(array) {
- const retArray = [...array];
- if (array.length === 0)
- return array;
- for (let i = retArray.length - 1; i > 0; i--) {
- const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
- [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
- }
- return retArray;
- }
- // lib/config.ts
- var Config = class {
- /**
- * Creates an instance of Config.
- * @param id A unique ID for this configuration
- * @param defaultData The default data to use if no data is saved yet. Until the data is loaded from persistent storage, this will be the data returned by `getData()`
- * @param formatVersion An incremental version of the data format. If the format of the data is changed, this number should be incremented, in which case all necessary functions of the migrations dictionary will be run consecutively. Never decrement this number, but you may skip numbers if you need to for some reason.
- * @param migrations A dictionary of functions that can be used to migrate data from older versions of the configuration to newer ones. The keys of the dictionary should be the format version that the functions can migrate to, from the previous whole integer value. The values should be functions that take the data in the old format and return the data in the new format. The functions will be run in order from the oldest to the newest version. If the current format version is not in the dictionary, no migrations will be run.
- */
- constructor(id, formatVersion, defaultData, migrations) {
- __publicField(this, "id");
- __publicField(this, "formatVersion");
- __publicField(this, "defaultData");
- __publicField(this, "migrations");
- __publicField(this, "cachedData");
- this.id = id;
- this.formatVersion = formatVersion;
- this.defaultData = this.cachedData = defaultData;
- this.migrations = migrations;
- this.loadData();
- }
- /** Loads the data saved in persistent storage into the in-memory cache and also returns it */
- loadData() {
- return __async(this, null, function* () {
- try {
- const gmData = yield GM.getValue(this.id, this.defaultData);
- const gmFmtVer = yield GM.getValue(`_uufmtver-${this.id}`);
- if (typeof gmData !== "string" || typeof gmFmtVer !== "number")
- return void 0;
- let parsed = JSON.parse(gmData);
- if (this.formatVersion > gmFmtVer)
- parsed = yield this.runMigrations(parsed, gmFmtVer);
- return this.cachedData = typeof parsed === "object" ? parsed : void 0;
- } catch (err) {
- return void 0;
- }
- });
- }
- /** Returns the data from the in-memory cache. Use `loadData()` to get fresh data from persistent storage (usually not necessary). */
- getData() {
- return this.cachedData;
- }
- /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
- setData(data) {
- this.cachedData = data;
- return new Promise((resolve) => __async(this, null, function* () {
- yield GM.setValue(this.id, JSON.stringify(data));
- yield GM.setValue(`_uufmtver-${this.id}`, this.formatVersion);
- resolve();
- }));
- }
- /** Runs all necessary migration functions consecutively */
- runMigrations(oldData, oldFmtVer) {
- return __async(this, null, function* () {
- return new Promise((resolve) => __async(this, null, function* () {
- if (!this.migrations)
- return resolve(oldData);
- let newData = oldData;
- const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
- for (const [fmtVer, migrationFunc] of sortedMigrations) {
- const ver = Number(fmtVer);
- if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
- const migRes = migrationFunc(newData);
- newData = migRes instanceof Promise ? yield migRes : migRes;
- oldFmtVer = ver;
- }
- }
- yield GM.setValue(`_uufmtver-${this.id}`, this.formatVersion);
- resolve(newData);
- }));
- });
- }
- };
- // lib/dom.ts
- function getUnsafeWindow() {
- try {
- return unsafeWindow;
- } catch (e) {
- return window;
- }
- }
- function insertAfter(beforeElement, afterElement) {
- var _a;
- (_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
- return afterElement;
- }
- function addParent(element2, newParent) {
- const oldParent = element2.parentNode;
- if (!oldParent)
- throw new Error("Element doesn't have a parent node");
- oldParent.replaceChild(newParent, element2);
- newParent.appendChild(element2);
- return newParent;
- }
- function addGlobalStyle(style) {
- const styleElem = document.createElement("style");
- styleElem.innerHTML = style;
- document.head.appendChild(styleElem);
- }
- function preloadImages(srcUrls, 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", (evt) => rejects && rej(evt));
- }));
- return Promise.allSettled(promises);
- }
- function openInNewTab(href) {
- const openElem = document.createElement("a");
- Object.assign(openElem, {
- className: "userutils-open-in-new-tab",
- target: "_blank",
- rel: "noopener noreferrer",
- href
- });
- openElem.style.display = "none";
- document.body.appendChild(openElem);
- openElem.click();
- setTimeout(openElem.remove, 50);
- }
- function interceptEvent(eventObject, eventName, predicate) {
- if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
- Error.stackTraceLimit = 1e3;
- }
- (function(original) {
- element.__proto__.addEventListener = function(...args) {
- if (args[0] === eventName && predicate())
- return;
- else
- return original.apply(this, args);
- };
- })(eventObject.__proto__.addEventListener);
- }
- function interceptWindowEvent(eventName, predicate) {
- return interceptEvent(getUnsafeWindow(), eventName, predicate);
- }
- function amplifyMedia(mediaElement, multiplier = 1) {
- const context = new (window.AudioContext || window.webkitAudioContext)();
- const result = {
- mediaElement,
- amplify: (multiplier2) => {
- result.gain.gain.value = multiplier2;
- },
- getAmpLevel: () => result.gain.gain.value,
- context,
- source: context.createMediaElementSource(mediaElement),
- gain: context.createGain()
- };
- result.source.connect(result.gain);
- result.gain.connect(context.destination);
- result.amplify(multiplier);
- return result;
- }
- // lib/misc.ts
- function autoPlural(word, num) {
- if (Array.isArray(num) || num instanceof NodeList)
- num = num.length;
- return `${word}${num === 1 ? "" : "s"}`;
- }
- function pauseFor(time) {
- return new Promise((res) => {
- setTimeout(res, time);
- });
- }
- function debounce(func, timeout = 300) {
- let timer;
- return function(...args) {
- clearTimeout(timer);
- timer = setTimeout(() => func.apply(this, args), timeout);
- };
- }
- function fetchAdvanced(_0) {
- return __async(this, arguments, function* (url, options = {}) {
- const { timeout = 1e4 } = options;
- const controller = new AbortController();
- const id = setTimeout(() => controller.abort(), timeout);
- const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
- signal: controller.signal
- }));
- clearTimeout(id);
- return res;
- });
- }
- // lib/onSelector.ts
- var selectorMap = /* @__PURE__ */ new Map();
- function onSelector(selector, options) {
- let selectorMapItems = [];
- if (selectorMap.has(selector))
- selectorMapItems = selectorMap.get(selector);
- selectorMapItems.push(options);
- selectorMap.set(selector, selectorMapItems);
- checkSelectorExists(selector, selectorMapItems);
- }
- function removeOnSelector(selector) {
- return selectorMap.delete(selector);
- }
- function checkSelectorExists(selector, options) {
- const deleteIndices = [];
- options.forEach((option, i) => {
- try {
- const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
- if (elements !== null && elements instanceof NodeList && elements.length > 0 || elements !== null) {
- option.listener(elements);
- if (!option.continuous)
- deleteIndices.push(i);
- }
- } catch (err) {
- console.error(`Couldn't call listener for selector '${selector}'`, err);
- }
- });
- if (deleteIndices.length > 0) {
- const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
- if (newOptsArray.length === 0)
- selectorMap.delete(selector);
- else {
- selectorMap.set(selector, newOptsArray);
- }
- }
- }
- function initOnSelector(options = {}) {
- const observer = new MutationObserver(() => {
- for (const [selector, options2] of selectorMap.entries())
- checkSelectorExists(selector, options2);
- });
- observer.observe(document.body, __spreadValues({
- subtree: true,
- childList: true
- }, options));
- }
- function getSelectorMap() {
- return selectorMap;
- }
- //# sourceMappingURL=http://localhost:8710/out.js.map
- //# sourceMappingURL=http://localhost:8710/index.mjs.map
- /***/ })
- /******/ });
- /************************************************************************/
- /******/ // The module cache
- /******/ var __webpack_module_cache__ = {};
- /******/
- /******/ // The require function
- /******/ function __webpack_require__(moduleId) {
- /******/ // Check if module is in cache
- /******/ var cachedModule = __webpack_module_cache__[moduleId];
- /******/ if (cachedModule !== undefined) {
- /******/ return cachedModule.exports;
- /******/ }
- /******/ // Create a new module (and put it into the cache)
- /******/ var module = __webpack_module_cache__[moduleId] = {
- /******/ // no module.id needed
- /******/ // no module.loaded needed
- /******/ exports: {}
- /******/ };
- /******/
- /******/ // Execute the module function
- /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
- /******/
- /******/ // Return the exports of the module
- /******/ return module.exports;
- /******/ }
- /******/
- /************************************************************************/
- /******/ /* webpack/runtime/define property getters */
- /******/ !function() {
- /******/ // define getter functions for harmony exports
- /******/ __webpack_require__.d = function(exports, definition) {
- /******/ for(var key in definition) {
- /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
- /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
- /******/ }
- /******/ }
- /******/ };
- /******/ }();
- /******/
- /******/ /* webpack/runtime/hasOwnProperty shorthand */
- /******/ !function() {
- /******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
- /******/ }();
- /******/
- /******/ /* webpack/runtime/make namespace object */
- /******/ !function() {
- /******/ // define __esModule on exports
- /******/ __webpack_require__.r = function(exports) {
- /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
- /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
- /******/ }
- /******/ Object.defineProperty(exports, '__esModule', { value: true });
- /******/ };
- /******/ }();
- /******/
- /************************************************************************/
- var __webpack_exports__ = {};
- // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
- !function() {
- /*!**********************!*\
- !*** ./src/index.ts ***!
- \**********************/
- __webpack_require__.r(__webpack_exports__);
- /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
- /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./config */ "./src/config.ts");
- /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./constants */ "./src/constants.ts");
- /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
- /* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./events */ "./src/events.ts");
- /* harmony import */ var _features_index__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./features/index */ "./src/features/index.ts");
- var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
- return new (P || (P = Promise))(function (resolve, reject) {
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
- step((generator = generator.apply(thisArg, _arguments || [])).next());
- });
- };
- {
- // 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${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.name}%cv${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.version}%c\n\nBuild #${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.lastCommit} ─ ${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.namespace}`, `font-weight: bold; ${styleCommon} ${styleGradient}`, `background-color: #333; ${styleCommon}`, "padding: initial;");
- console.log([
- "Powered by:",
- "─ lots of ambition",
- `─ my song metadata API: ${_features_index__WEBPACK_IMPORTED_MODULE_5__.geniUrlBase}`,
- "─ my userscript utility library: https://github.com/Sv443-Network/UserUtils",
- "─ this tiny event listener library: https://github.com/billjs/event-emitter",
- ].join("\n"));
- console.log();
- }
- const domain = (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getDomain)();
- /** Stuff that needs to be called ASAP, before anything async happens */
- function preInit() {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.setLogLevel)(_constants__WEBPACK_IMPORTED_MODULE_2__.logLevel);
- if (domain === "ytm")
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.initBeforeUnloadHook)();
- init();
- }
- function init() {
- return __awaiter(this, void 0, void 0, function* () {
- yield (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.preInitLayout)();
- try {
- document.addEventListener("DOMContentLoaded", onDomLoad);
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("General Error:", err);
- }
- try {
- void ["TODO(v1.1):", _features_index__WEBPACK_IMPORTED_MODULE_5__.initMenu];
- // initMenu();
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't initialize menu:", err);
- }
- });
- }
- /** Called when the DOM has finished loading and can be queried and altered by the userscript */
- function onDomLoad() {
- return __awaiter(this, void 0, void 0, function* () {
- // post-build these double quotes are replaced by backticks (because if backticks are used here, webpack converts them to double quotes)
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addGlobalStyle)(/* BetterYTM - global style */
- `/*!**********************************************************************************!*\
- !*** css ./node_modules/css-loader/dist/cjs.js!./src/features/menu/menu_old.css ***!
- \**********************************************************************************/
- :root {
- --bytm-menu-bg: #212121;
- }
- #betterytm-menu-bg {
- display: block;
- position: fixed;
- width: 100vw;
- height: 100vh;
- top: 0;
- left: 0;
- z-index: 15;
- background-color: rgba(0, 0, 0, 0.6);
- }
- #betterytm-menu {
- display: inline-block;
- position: fixed;
- width: 50vw;
- height: auto;
- left: 25vw;
- top: 10vh;
- z-index: 16;
- padding: 10px 25px;
- color: #fff;
- background-color: var(--bytm-menu-bg);
- }
- #betterytm-menu-opts {
- max-height: 70vh;
- overflow: auto;
- }
- #betterytm-menu-titlecont {
- display: flex;
- }
- #betterytm-menu-title {
- font-size: 20px;
- margin-top: 5px;
- margin-bottom: 8px;
- }
- #betterytm-menu-linkscont {
- display: flex;
- }
- .betterytm-menu-link {
- cursor: pointer;
- display: inline-block;
- }
- #betterytm-menu-close {
- cursor: pointer;
- }
- .betterytm-ftconf-label {
- user-select: none;
- }
- body.bytm-disable-scroll {
- overflow-y: hidden !important;
- }
- /*!***************************************************************************!*\
- !*** css ./node_modules/css-loader/dist/cjs.js!./src/features/layout.css ***!
- \***************************************************************************/
- /* #MARKER misc */
- .bytm-generic-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- position: relative;
- vertical-align: middle;
- cursor: pointer;
- margin-left: 8px;
- width: 40px;
- height: 40px;
- border-radius: 100%;
- background-color: transparent;
- }
- .bytm-generic-btn:hover {
- background-color: var(--yt-spec-10-percent-layer, #1d1d1d);
- }
- .bytm-generic-btn-img {
- display: inline-block;
- z-index: 10;
- width: 24px;
- height: 24px;
- padding: 5px;
- }
- .bytm-spinner {
- animation: rotate 1.5s linear infinite;
- }
- @keyframes rotate {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
- }
- .bytm-anchor {
- all: unset;
- cursor: pointer;
- }
- /* ytmusic-logo a[bytm-animated="true"] .bytm-mod-logo-ellipse {
- transform-origin: 12px 12px;
- animation: rotate 1s ease-in-out infinite;
- } */
- ytmusic-logo a.bytm-logo-exchanged .bytm-mod-logo-path {
- transform-origin: 12px 12px;
- animation: rotate 1s ease-in-out;
- }
- ytmusic-logo a.bytm-logo-exchanged .bytm-mod-logo-img {
- width: 24px;
- height: 24px;
- z-index: 1000;
- position: absolute;
- animation: rotate-fade-in 1s ease-in-out;
- }
- @keyframes rotate-fade-in {
- 0% {
- opacity: 0;
- transform: rotate(0deg);
- }
- 30% {
- opacity: 0;
- }
- 90% {
- opacity: 1;
- }
- 100% {
- transform: rotate(360deg);
- }
- }
- .bytm-no-select {
- user-select: none;
- -ms-user-select: none;
- -moz-user-select: none;
- -webkit-user-select: none;
- }
- /* #MARKER menu */
- .bytm-cfg-menu-option {
- display: block;
- padding: 8px 0;
- }
- .bytm-cfg-menu-option-item {
- display: flex;
- flex-direction: row;
- align-items: center;
- font-size: 16px;
- font-weight: 400;
- line-height: 24px;
- padding: var(--yt-compact-link-paper-item-padding, 0px 36px 0 16px);
- min-height: var(--paper-item-min-height, 40px);
- white-space: nowrap;
- cursor: pointer;
- }
- .bytm-cfg-menu-option-item:hover {
- background-color: var(--yt-spec-badge-chip-background, #3e3e3e);
- }
- .bytm-cfg-menu-option-icon {
- width: 24px;
- height: 24px;
- margin-right: 16px;
- display: flex;
- align-items: center;
- flex-direction: row;
- flex: none;
- }
- .bytm-cfg-menu-option-text {
- font-size: 1.4rem;
- line-height: 2rem;
- }
- yt-multi-page-menu-section-renderer.ytd-multi-page-menu-renderer {
- border-bottom: 1px solid var(--yt-spec-10-percent-layer, #3e3e3e);
- }
- /* #MARKER watermark */
- #bytm-watermark {
- font-size: 10px;
- display: inline-block;
- position: absolute;
- left: 97px;
- top: 46px;
- z-index: 10;
- color: white;
- text-decoration: none;
- cursor: pointer;
- }
- #bytm-watermark:hover {
- text-decoration: underline;
- }
- /* #MARKER queue buttons */
- .side-panel.modular ytmusic-player-queue-item .song-info.ytmusic-player-queue-item {
- position: relative;
- }
- .side-panel.modular ytmusic-player-queue-item .bytm-queue-btn-container {
- background: rgb(0, 0, 0);
- background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 15%);
- display: none;
- position: absolute;
- right: 0;
- padding-left: 25px;
- height: 100%;
- }
- .side-panel.modular ytmusic-player-queue-item:hover .bytm-queue-btn-container {
- display: inline-block;
- }
- .side-panel.modular ytmusic-player-queue-item[play-button-state="loading"] .bytm-queue-btn-container,
- .side-panel.modular ytmusic-player-queue-item[play-button-state="playing"] .bytm-queue-btn-container,
- .side-panel.modular ytmusic-player-queue-item[play-button-state="paused"] .bytm-queue-btn-container {
- /* using a var() is not viable since the nesting changes the actual value of the variable */
- background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(29, 29, 29, 1) 15%);
- }
- 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;
- }
- /*!******************************************************************************!*\
- !*** css ./node_modules/css-loader/dist/cjs.js!./src/features/menu/menu.css ***!
- \******************************************************************************/
- /* #bytm-menu-backdrop {
- display: none;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- }
- #bytm-menu-backdrop[data-menu-open="true"] {
- display: flex;
- } */
- #bytm-menu-header-container {
- display: flex;
- justify-content: flex-start;
- align-items: center;
- border-color: #ffffff;
- border-style: none solid none none;
- }
- .bytm-menu-header-option {
- display: "flex";
- justify-content: center;
- align-items: center;
- border-color: #ffffff;
- border-style: solid none solid none;
- }
- #bytm-menu-header-option h3 {
- margin: 0;
- }
- .bytm-menu-tab[data-active="true"] {
- display: none;
- }
- .bytm-menu-tab[data-active="false"] {
- display: none;
- }
- /*# sourceMappingURL=http://localhost:8710/global.css.map*/`);
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.initOnSelector)();
- const features = yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.loadFeatureConf)();
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Initializing features for domain "${domain}"...`);
- try {
- if (domain === "ytm") {
- try {
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.addMenu)(); // TODO(v1.1): remove
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't add menu:", err);
- }
- (0,_events__WEBPACK_IMPORTED_MODULE_4__.initSiteEvents)();
- (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("tp-yt-iron-dropdown #contentWrapper ytd-multi-page-menu-renderer #container.menu-container", { listener: _features_index__WEBPACK_IMPORTED_MODULE_5__.addConfigMenuOption });
- if (features.arrowKeySupport)
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.initArrowKeySkip)();
- if (features.removeUpgradeTab)
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.removeUpgradeTab)();
- if (features.watermarkEnabled)
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.addWatermark)();
- if (features.geniusLyrics)
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.addMediaCtrlLyricsBtn)();
- if (features.queueButtons)
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.initQueueButtons)();
- if (features.anchorImprovements)
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.addAnchorImprovements)();
- // TODO:
- void _features_index__WEBPACK_IMPORTED_MODULE_5__.initVolumeFeatures;
- }
- if (["ytm", "yt"].includes(domain)) {
- if (features.switchBetweenSites)
- (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.initSiteSwitch)(domain);
- }
- }
- catch (err) {
- (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("General error while executing feature:", err);
- }
- });
- }
- preInit();
- }();
- //# sourceMappingURL=http://localhost:8710/BetterYTM.user.js.map
|