BetterYTM.user.js 144 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186
  1. // ==UserScript==
  2. // @name BetterYTM
  3. // @homepageURL https://github.com/Sv443/BetterYTM#readme
  4. // @namespace https://github.com/Sv443/BetterYTM
  5. // @version 1.0.0
  6. // @description Configurable layout and UX improvements for YouTube Music
  7. // @description:de Konfigurierbares Layout und UX-Verbesserungen für YouTube Music
  8. // @license MIT
  9. // @author Sv443
  10. // @copyright Sv443 (https://github.com/Sv443)
  11. // @icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icon/icon.png
  12. // @match https://music.youtube.com/*
  13. // @match https://www.youtube.com/*
  14. // @run-at document-start
  15. // @downloadURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
  16. // @updateURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
  17. // @connect api.sv443.net
  18. // @grant GM.getValue
  19. // @grant GM.setValue
  20. // @grant GM.getResourceUrl
  21. // @grant unsafeWindow
  22. // @noframes
  23. // @resource icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icon/icon.png
  24. // @resource close https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/close.png
  25. // @resource delete https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/delete.svg
  26. // @resource error https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/error.svg
  27. // @resource lyrics https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/lyrics.svg
  28. // @resource spinner https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/spinner.svg
  29. // @resource github https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/github.png
  30. // @resource greasyfork https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/greasyfork.png
  31. // ==/UserScript==
  32. /*
  33. ▄▄▄ ▄ ▄▄▄▄▄▄ ▄
  34. █ █ ▄▄▄ █ █ ▄▄▄ ▄ ▄█ █ █ █▀▄▀█
  35. █▀▀▄ █▄█ █▀ █▀ █▄█ █▀ █ █ █ █
  36. █▄▄▀ ▀▄▄ ▀▄▄ ▀▄▄ ▀▄▄ █ █ █ █ █
  37. Made with ❤️ by Sv443
  38. I welcome every contribution on GitHub!
  39. https://github.com/Sv443/BetterYTM
  40. */
  41. /* Disclaimer: I am not affiliated with YouTube, Google, Alphabet, Genius or anyone else */
  42. /* C&D this 🖕 */
  43. /******/ var __webpack_modules__ = ({
  44. /***/ "./node_modules/@billjs/event-emitter/lib/index.js":
  45. /*!*********************************************************!*\
  46. !*** ./node_modules/@billjs/event-emitter/lib/index.js ***!
  47. \*********************************************************/
  48. /***/ (function(__unused_webpack_module, exports) {
  49. /**
  50. * A simple and lightweight EventEmitter by TypeScript for Node.js or Browsers.
  51. *
  52. * @author billjs
  53. * @see https://github.com/billjs/event-emitter
  54. * @license MIT(https://opensource.org/licenses/MIT)
  55. */
  56. Object.defineProperty(exports, "__esModule", ({ value: true }));
  57. /**
  58. * It's a class for managing events.
  59. * It can be extended to provide event functionality for other classes or object.
  60. *
  61. * @export
  62. * @class EventEmitter
  63. */
  64. var EventEmitter = /** @class */ (function () {
  65. function EventEmitter() {
  66. /**
  67. * the all event handlers are added.
  68. * it's a Map data structure(key-value), the key is event type, and the value is event handler.
  69. *
  70. * @memberof EventEmitter
  71. */
  72. this._eventHandlers = {};
  73. }
  74. /**
  75. * event type validator.
  76. *
  77. * @param {string} type event type
  78. * @returns {boolean}
  79. * @memberof EventEmitter
  80. */
  81. EventEmitter.prototype.isValidType = function (type) {
  82. return typeof type === 'string';
  83. };
  84. /**
  85. * event handler validator.
  86. *
  87. * @param {EventHandler} handler event handler
  88. * @returns {boolean}
  89. * @memberof EventEmitter
  90. */
  91. EventEmitter.prototype.isValidHandler = function (handler) {
  92. return typeof handler === 'function';
  93. };
  94. /**
  95. * listen on a new event by type and handler.
  96. * if listen on, the true is returned, otherwise the false.
  97. * The handler will not be listen if it is a duplicate.
  98. *
  99. * @param {string} type event type, it must be a unique string.
  100. * @param {EventHandler} handler event handler, when if the same handler is passed, listen it by only once.
  101. * @returns {boolean}
  102. * @memberof EventEmitter
  103. * @example
  104. * const emitter = new EventEmitter();
  105. * emitter.on('change:name', evt => {
  106. * console.log(evt);
  107. * });
  108. */
  109. EventEmitter.prototype.on = function (type, handler) {
  110. if (!type || !handler)
  111. return false;
  112. if (!this.isValidType(type))
  113. return false;
  114. if (!this.isValidHandler(handler))
  115. return false;
  116. var handlers = this._eventHandlers[type];
  117. if (!handlers)
  118. handlers = this._eventHandlers[type] = [];
  119. // when the same handler is passed, listen it by only once.
  120. if (handlers.indexOf(handler) >= 0)
  121. return false;
  122. handler._once = false;
  123. handlers.push(handler);
  124. return true;
  125. };
  126. /**
  127. * listen on an once event by type and handler.
  128. * when the event is fired, that will be listen off immediately and automatically.
  129. * The handler will not be listen if it is a duplicate.
  130. *
  131. * @param {string} type event type, it must be a unique string.
  132. * @param {EventHandler} handler event handler, when if the same handler is passed, listen it by only once.
  133. * @returns {boolean}
  134. * @memberof EventEmitter
  135. * @example
  136. * const emitter = new EventEmitter();
  137. * emitter.once('change:name', evt => {
  138. * console.log(evt);
  139. * });
  140. */
  141. EventEmitter.prototype.once = function (type, handler) {
  142. if (!type || !handler)
  143. return false;
  144. if (!this.isValidType(type))
  145. return false;
  146. if (!this.isValidHandler(handler))
  147. return false;
  148. var ret = this.on(type, handler);
  149. if (ret) {
  150. // set `_once` private property after listened,
  151. // avoid to modify event handler that has been listened.
  152. handler._once = true;
  153. }
  154. return ret;
  155. };
  156. /**
  157. * listen off an event by type and handler.
  158. * or listen off events by type, when if only type argument is passed.
  159. * or listen off all events, when if no arguments are passed.
  160. *
  161. * @param {string} [type] event type
  162. * @param {EventHandler} [handler] event handler
  163. * @returns
  164. * @memberof EventEmitter
  165. * @example
  166. * const emitter = new EventEmitter();
  167. * // listen off the specified event
  168. * emitter.off('change:name', evt => {
  169. * console.log(evt);
  170. * });
  171. * // listen off events by type
  172. * emitter.off('change:name');
  173. * // listen off all events
  174. * emitter.off();
  175. */
  176. EventEmitter.prototype.off = function (type, handler) {
  177. // listen off all events, when if no arguments are passed.
  178. // it does samething as `offAll` method.
  179. if (!type)
  180. return this.offAll();
  181. // listen off events by type, when if only type argument is passed.
  182. if (!handler) {
  183. this._eventHandlers[type] = [];
  184. return;
  185. }
  186. if (!this.isValidType(type))
  187. return;
  188. if (!this.isValidHandler(handler))
  189. return;
  190. var handlers = this._eventHandlers[type];
  191. if (!handlers || !handlers.length)
  192. return;
  193. // otherwise, listen off the specified event.
  194. for (var i = 0; i < handlers.length; i++) {
  195. var fn = handlers[i];
  196. if (fn === handler) {
  197. handlers.splice(i, 1);
  198. break;
  199. }
  200. }
  201. };
  202. /**
  203. * listen off all events, that means every event will be emptied.
  204. *
  205. * @memberof EventEmitter
  206. * @example
  207. * const emitter = new EventEmitter();
  208. * emitter.offAll();
  209. */
  210. EventEmitter.prototype.offAll = function () {
  211. this._eventHandlers = {};
  212. };
  213. /**
  214. * fire the specified event, and you can to pass a data.
  215. * When fired, every handler attached to that event will be executed.
  216. * But, if it's an once event, listen off it immediately after called handler.
  217. *
  218. * @param {string} type event type
  219. * @param {*} [data] event data
  220. * @returns
  221. * @memberof EventEmitter
  222. * @example
  223. * const emitter = new EventEmitter();
  224. * emitter.fire('change:name', 'new name');
  225. */
  226. EventEmitter.prototype.fire = function (type, data) {
  227. if (!type || !this.isValidType(type))
  228. return;
  229. var handlers = this._eventHandlers[type];
  230. if (!handlers || !handlers.length)
  231. return;
  232. var event = this.createEvent(type, data);
  233. for (var _i = 0, handlers_1 = handlers; _i < handlers_1.length; _i++) {
  234. var handler = handlers_1[_i];
  235. if (!this.isValidHandler(handler))
  236. continue;
  237. if (handler._once)
  238. event.once = true;
  239. // call event handler, and pass the event argument.
  240. handler(event);
  241. // if it's an once event, listen off it immediately after called handler.
  242. if (event.once)
  243. this.off(type, handler);
  244. }
  245. };
  246. /**
  247. * check whether the specified event has been listen on.
  248. * or check whether the events by type has been listen on, when if only `type` argument is passed.
  249. *
  250. * @param {string} type event type
  251. * @param {EventHandler} [handler] event handler, optional
  252. * @returns {boolean}
  253. * @memberof EventEmitter
  254. * @example
  255. * const emitter = new EventEmitter();
  256. * const result = emitter.has('change:name');
  257. */
  258. EventEmitter.prototype.has = function (type, handler) {
  259. if (!type || !this.isValidType(type))
  260. return false;
  261. var handlers = this._eventHandlers[type];
  262. // if there are no any events, return false.
  263. if (!handlers || !handlers.length)
  264. return false;
  265. // at lest one event, and no pass `handler` argument, then return true.
  266. if (!handler || !this.isValidHandler(handler))
  267. return true;
  268. // otherwise, need to traverse the handlers.
  269. return handlers.indexOf(handler) >= 0;
  270. };
  271. /**
  272. * get the handlers for the specified event type.
  273. *
  274. * @param {string} type event type
  275. * @returns {EventHandler[]}
  276. * @memberof EventEmitter
  277. * @example
  278. * const emitter = new EventEmitter();
  279. * const handlers = emitter.getHandlers('change:name');
  280. * console.log(handlers);
  281. */
  282. EventEmitter.prototype.getHandlers = function (type) {
  283. if (!type || !this.isValidType(type))
  284. return [];
  285. return this._eventHandlers[type] || [];
  286. };
  287. /**
  288. * create event object.
  289. *
  290. * @param {string} type event type
  291. * @param {*} [data] event data
  292. * @param {boolean} [once=false] is it an once event?
  293. * @returns {Event}
  294. * @memberof EventEmitter
  295. */
  296. EventEmitter.prototype.createEvent = function (type, data, once) {
  297. if (once === void 0) { once = false; }
  298. var event = { type: type, data: data, timestamp: Date.now(), once: once };
  299. return event;
  300. };
  301. return EventEmitter;
  302. }());
  303. exports.EventEmitter = EventEmitter;
  304. /**
  305. * EventEmitter instance for global.
  306. * @type {EventEmitter}
  307. */
  308. exports.globalEvent = new EventEmitter();
  309. /***/ }),
  310. /***/ "./changelog.md":
  311. /*!**********************!*\
  312. !*** ./changelog.md ***!
  313. \**********************/
  314. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  315. __webpack_require__.r(__webpack_exports__);
  316. // Module
  317. 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 &quot;Upgrade to YTM Premium&quot; 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";
  318. // Exports
  319. /* harmony default export */ __webpack_exports__["default"] = (code);
  320. /***/ }),
  321. /***/ "./src/features/menu/menu.html":
  322. /*!*************************************!*\
  323. !*** ./src/features/menu/menu.html ***!
  324. \*************************************/
  325. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  326. __webpack_require__.r(__webpack_exports__);
  327. // Module
  328. 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";
  329. // Exports
  330. /* harmony default export */ __webpack_exports__["default"] = (code);
  331. /***/ }),
  332. /***/ "./src/features/layout.css":
  333. /*!*********************************!*\
  334. !*** ./src/features/layout.css ***!
  335. \*********************************/
  336. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  337. __webpack_require__.r(__webpack_exports__);
  338. // extracted by mini-css-extract-plugin
  339. /***/ }),
  340. /***/ "./src/features/menu/menu.css":
  341. /*!************************************!*\
  342. !*** ./src/features/menu/menu.css ***!
  343. \************************************/
  344. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  345. __webpack_require__.r(__webpack_exports__);
  346. // extracted by mini-css-extract-plugin
  347. /***/ }),
  348. /***/ "./src/features/menu/menu_old.css":
  349. /*!****************************************!*\
  350. !*** ./src/features/menu/menu_old.css ***!
  351. \****************************************/
  352. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  353. __webpack_require__.r(__webpack_exports__);
  354. // extracted by mini-css-extract-plugin
  355. /***/ }),
  356. /***/ "./src/config.ts":
  357. /*!***********************!*\
  358. !*** ./src/config.ts ***!
  359. \***********************/
  360. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  361. __webpack_require__.r(__webpack_exports__);
  362. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  363. /* harmony export */ defaultFeatures: function() { return /* binding */ defaultFeatures; },
  364. /* harmony export */ getFeatures: function() { return /* binding */ getFeatures; },
  365. /* harmony export */ loadFeatureConf: function() { return /* binding */ loadFeatureConf; },
  366. /* harmony export */ saveFeatureConf: function() { return /* binding */ saveFeatureConf; },
  367. /* harmony export */ setDefaultFeatConf: function() { return /* binding */ setDefaultFeatConf; }
  368. /* harmony export */ });
  369. /* harmony import */ var _features_index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./features/index */ "./src/features/index.ts");
  370. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
  371. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  372. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  373. return new (P || (P = Promise))(function (resolve, reject) {
  374. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  375. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  376. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  377. step((generator = generator.apply(thisArg, _arguments || [])).next());
  378. });
  379. };
  380. /** If this number is incremented, the features object needs to be migrated (TODO: migration not implemented yet) */
  381. const formatVersion = 1;
  382. const defaultFeatures = Object.keys(_features_index__WEBPACK_IMPORTED_MODULE_0__.featInfo)
  383. .reduce((acc, key) => {
  384. acc[key] = _features_index__WEBPACK_IMPORTED_MODULE_0__.featInfo[key].default;
  385. return acc;
  386. }, {});
  387. /** In-memory features object to save on a little bit of I/O */
  388. let featuresCache;
  389. /**
  390. * Returns the current FeatureConfig in memory or reads it from GM storage if undefined.
  391. * Automatically applies defaults for non-existant keys
  392. * @param forceRead Set to true to force reading the config from GM storage
  393. */
  394. function getFeatures(forceRead = false) {
  395. return __awaiter(this, void 0, void 0, function* () {
  396. if (!featuresCache || forceRead)
  397. featuresCache = yield loadFeatureConf();
  398. return featuresCache;
  399. });
  400. }
  401. /** Loads a feature configuration saved persistently, returns an empty object if no feature configuration was saved */
  402. function loadFeatureConf() {
  403. return __awaiter(this, void 0, void 0, function* () {
  404. const defConf = Object.assign({}, defaultFeatures);
  405. try {
  406. const featureConf = yield GM.getValue("betterytm-config");
  407. // empty object length is 2-3, so check for >3 to be sure
  408. if (typeof featureConf !== "string" || featureConf.length <= 3) {
  409. yield setDefaultFeatConf();
  410. return featuresCache = defConf;
  411. }
  412. return featuresCache = Object.assign(Object.assign({}, defConf), (featureConf ? JSON.parse(featureConf) : {}));
  413. }
  414. catch (err) {
  415. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Error loading feature configuration, resetting to default:", err);
  416. yield setDefaultFeatConf();
  417. return featuresCache = defConf;
  418. }
  419. });
  420. }
  421. /**
  422. * Saves the passed feature configuration persistently in GM storage and in the in-memory cache
  423. * @param featureConf
  424. */
  425. function saveFeatureConf(featureConf) {
  426. if (!featureConf || typeof featureConf != "object")
  427. throw new TypeError("Feature config not provided or invalid");
  428. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Saving new feature config:", featureConf);
  429. featuresCache = Object.assign({}, featureConf);
  430. GM.setValue("betterytm-config-ver", formatVersion);
  431. return GM.setValue("betterytm-config", JSON.stringify(featureConf));
  432. }
  433. /** Resets the featuresCache synchronously and the persistent features storage asynchronously to their default values */
  434. function setDefaultFeatConf() {
  435. featuresCache = Object.assign({}, defaultFeatures);
  436. GM.setValue("betterytm-config-ver", formatVersion);
  437. return GM.setValue("betterytm-config", JSON.stringify(defaultFeatures));
  438. }
  439. /***/ }),
  440. /***/ "./src/constants.ts":
  441. /*!**************************!*\
  442. !*** ./src/constants.ts ***!
  443. \**************************/
  444. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  445. __webpack_require__.r(__webpack_exports__);
  446. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  447. /* harmony export */ branch: function() { return /* binding */ branch; },
  448. /* harmony export */ logLevel: function() { return /* binding */ logLevel; },
  449. /* harmony export */ mode: function() { return /* binding */ mode; },
  450. /* harmony export */ scriptInfo: function() { return /* binding */ scriptInfo; }
  451. /* harmony export */ });
  452. const modeRaw = "development";
  453. const branchRaw = "develop";
  454. /** The mode in which the script was built (production or development) */
  455. const mode = modeRaw.match(/^{{.+}}$/) ? "production" : modeRaw;
  456. /** The branch to use in various URLs that point to the GitHub repo */
  457. const branch = branchRaw.match(/^{{.+}}$/) ? "main" : branchRaw;
  458. /**
  459. * How much info should be logged to the devtools console
  460. * 0 = Debug (show everything) or 1 = Info (show only important stuff)
  461. */
  462. const logLevel = mode === "production" ? 1 : 0;
  463. /** Info about the userscript, parsed from the userscript header (tools/post-build.js) */
  464. const scriptInfo = {
  465. name: GM.info.script.name,
  466. version: GM.info.script.version,
  467. namespace: GM.info.script.namespace,
  468. lastCommit: "fb62267", // assert as generic string instead of literal
  469. };
  470. /***/ }),
  471. /***/ "./src/events.ts":
  472. /*!***********************!*\
  473. !*** ./src/events.ts ***!
  474. \***********************/
  475. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  476. __webpack_require__.r(__webpack_exports__);
  477. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  478. /* harmony export */ getEvtData: function() { return /* binding */ getEvtData; },
  479. /* harmony export */ initSiteEvents: function() { return /* binding */ initSiteEvents; },
  480. /* harmony export */ removeAllObservers: function() { return /* binding */ removeAllObservers; },
  481. /* harmony export */ siteEvents: function() { return /* binding */ siteEvents; }
  482. /* harmony export */ });
  483. /* harmony import */ var _billjs_event_emitter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @billjs/event-emitter */ "./node_modules/@billjs/event-emitter/lib/index.js");
  484. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
  485. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  486. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  487. return new (P || (P = Promise))(function (resolve, reject) {
  488. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  489. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  490. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  491. step((generator = generator.apply(thisArg, _arguments || [])).next());
  492. });
  493. };
  494. /** EventEmitter instance that is used to detect changes to the site */
  495. const siteEvents = new _billjs_event_emitter__WEBPACK_IMPORTED_MODULE_0__.EventEmitter();
  496. /**
  497. * Returns the data of an event from the `@billjs/event-emitter` library.
  498. * This function is used as a shorthand to extract the data and assert it with the type passed in `<T>`
  499. * @param evt Event object from the `.on()` or `.once()` method
  500. * @template T Type of the data passed by `.fire(type: string, data: T)`
  501. */
  502. function getEvtData(evt) {
  503. return evt.data;
  504. }
  505. let observers = [];
  506. /** Disconnects and deletes all observers. Run `initSiteEvents()` again to create new ones. */
  507. function removeAllObservers() {
  508. observers.forEach((observer, i) => {
  509. observer.disconnect();
  510. delete observers[i];
  511. });
  512. observers = [];
  513. }
  514. /** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
  515. function initSiteEvents() {
  516. return __awaiter(this, void 0, void 0, function* () {
  517. try {
  518. //#SECTION queue
  519. // the queue container always exists so it doesn't need the extra init function
  520. const queueObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
  521. if (addedNodes.length > 0 || removedNodes.length > 0) {
  522. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
  523. siteEvents.fire("queueChanged", target);
  524. }
  525. });
  526. // only observe added or removed elements
  527. queueObs.observe(document.querySelector(".side-panel.modular #contents.ytmusic-player-queue"), {
  528. childList: true,
  529. });
  530. const autoplayObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
  531. if (addedNodes.length > 0 || removedNodes.length > 0) {
  532. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Detected autoplay queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
  533. siteEvents.fire("autoplayQueueChanged", target);
  534. }
  535. });
  536. autoplayObs.observe(document.querySelector(".side-panel.modular ytmusic-player-queue #automix-contents"), {
  537. childList: true,
  538. });
  539. //#SECTION home page observers
  540. initHomeObservers();
  541. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Successfully initialized SiteEvents observers");
  542. observers = observers.concat([
  543. queueObs,
  544. autoplayObs,
  545. ]);
  546. }
  547. catch (err) {
  548. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't initialize SiteEvents observers due to an error:\n", err);
  549. }
  550. });
  551. }
  552. /**
  553. * The home page might not exist yet if the site was accessed through any path like /watch directly.
  554. * This function will keep waiting for when the home page exists, then create the necessary MutationObservers.
  555. */
  556. function initHomeObservers() {
  557. var _a;
  558. return __awaiter(this, void 0, void 0, function* () {
  559. let interval;
  560. // hidden="" attribute is only present if the content of the page doesn't exist yet
  561. // so this pauses execution until that attribute is removed
  562. if ((_a = document.querySelector("ytmusic-browse-response#browse-page")) === null || _a === void 0 ? void 0 : _a.hasAttribute("hidden")) {
  563. yield new Promise((res) => {
  564. interval = setInterval(() => {
  565. var _a;
  566. if (!((_a = document.querySelector("ytmusic-browse-response#browse-page")) === null || _a === void 0 ? void 0 : _a.hasAttribute("hidden"))) {
  567. clearInterval(interval);
  568. res();
  569. }
  570. }, 50);
  571. });
  572. }
  573. siteEvents.fire("homePageLoaded");
  574. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Initialized home page observers");
  575. //#SECTION carousel shelves
  576. const shelfContainerObs = new MutationObserver(([{ addedNodes, removedNodes }]) => {
  577. if (addedNodes.length > 0 || removedNodes.length > 0) {
  578. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Detected carousel shelf container change - added nodes:", addedNodes.length, "- removed nodes:", removedNodes.length);
  579. siteEvents.fire("carouselShelvesChanged", { addedNodes, removedNodes });
  580. }
  581. });
  582. shelfContainerObs.observe(document.querySelector("#contents.ytmusic-section-list-renderer"), {
  583. childList: true,
  584. });
  585. observers = observers.concat([shelfContainerObs]);
  586. });
  587. }
  588. /***/ }),
  589. /***/ "./src/features/index.ts":
  590. /*!*******************************!*\
  591. !*** ./src/features/index.ts ***!
  592. \*******************************/
  593. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  594. __webpack_require__.r(__webpack_exports__);
  595. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  596. /* harmony export */ addAnchorImprovements: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addAnchorImprovements; },
  597. /* harmony export */ addConfigMenuOption: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addConfigMenuOption; },
  598. /* harmony export */ addLyricsCacheEntry: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.addLyricsCacheEntry; },
  599. /* harmony export */ addMediaCtrlLyricsBtn: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.addMediaCtrlLyricsBtn; },
  600. /* harmony export */ addMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.addMenu; },
  601. /* harmony export */ addWatermark: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addWatermark; },
  602. /* harmony export */ closeMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.closeMenu; },
  603. /* harmony export */ createLyricsBtn: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.createLyricsBtn; },
  604. /* harmony export */ disableBeforeUnload: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.disableBeforeUnload; },
  605. /* harmony export */ enableBeforeUnload: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.enableBeforeUnload; },
  606. /* harmony export */ featInfo: function() { return /* binding */ featInfo; },
  607. /* harmony export */ geniUrlBase: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.geniUrlBase; },
  608. /* harmony export */ getCurrentLyricsUrl: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getCurrentLyricsUrl; },
  609. /* harmony export */ getGeniusUrl: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getGeniusUrl; },
  610. /* harmony export */ getLyricsCacheEntry: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getLyricsCacheEntry; },
  611. /* harmony export */ initArrowKeySkip: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initArrowKeySkip; },
  612. /* harmony export */ initBeforeUnloadHook: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initBeforeUnloadHook; },
  613. /* harmony export */ initMenu: function() { return /* reexport safe */ _menu_menu__WEBPACK_IMPORTED_MODULE_4__.initMenu; },
  614. /* harmony export */ initQueueButtons: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.initQueueButtons; },
  615. /* harmony export */ initSiteSwitch: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initSiteSwitch; },
  616. /* harmony export */ initVolumeFeatures: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.initVolumeFeatures; },
  617. /* harmony export */ openMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu; },
  618. /* harmony export */ preInitLayout: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.preInitLayout; },
  619. /* harmony export */ removeUpgradeTab: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.removeUpgradeTab; },
  620. /* harmony export */ sanitizeArtists: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.sanitizeArtists; },
  621. /* harmony export */ sanitizeSong: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.sanitizeSong; }
  622. /* harmony export */ });
  623. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
  624. /* harmony import */ var _input__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./input */ "./src/features/input.ts");
  625. /* harmony import */ var _layout__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./layout */ "./src/features/layout.ts");
  626. /* harmony import */ var _lyrics__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./lyrics */ "./src/features/lyrics.ts");
  627. /* harmony import */ var _menu_menu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu/menu */ "./src/features/menu/menu.ts");
  628. /* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
  629. /** Contains all possible features with their default values and other config */
  630. const featInfo = {
  631. //#SECTION input
  632. arrowKeySupport: {
  633. desc: "Arrow keys skip forwards and backwards by 10 seconds",
  634. type: "toggle",
  635. category: "input",
  636. default: true,
  637. },
  638. switchBetweenSites: {
  639. desc: "Add F9 as a hotkey to switch between the YT and YTM sites on a video / song",
  640. type: "toggle",
  641. category: "input",
  642. default: true,
  643. },
  644. switchSitesHotkey: {
  645. desc: "TODO(v1.1): Which hotkey needs to be pressed to switch sites?",
  646. type: "hotkey",
  647. category: "input",
  648. default: {
  649. key: "F9",
  650. shift: false,
  651. ctrl: false,
  652. meta: false,
  653. },
  654. visible: false,
  655. },
  656. disableBeforeUnloadPopup: {
  657. desc: "Completely disable the popup that sometimes appears before leaving the site",
  658. type: "toggle",
  659. category: "input",
  660. default: false,
  661. },
  662. anchorImprovements: {
  663. desc: "Add link elements all over the page so stuff can be opened in a new tab easier",
  664. type: "toggle",
  665. category: "input",
  666. default: true,
  667. },
  668. //#SECTION layout
  669. removeUpgradeTab: {
  670. desc: "Remove the \"Upgrade\" / YT Music Premium tab",
  671. type: "toggle",
  672. category: "layout",
  673. default: true,
  674. },
  675. volumeSliderLabel: {
  676. desc: "Add a percentage label to the volume slider",
  677. type: "toggle",
  678. category: "layout",
  679. default: true,
  680. },
  681. volumeSliderSize: {
  682. desc: "Width of the volume slider in pixels",
  683. type: "number",
  684. category: "layout",
  685. min: 50,
  686. max: 500,
  687. step: 5,
  688. default: 160,
  689. unit: "px",
  690. },
  691. volumeSliderStep: {
  692. desc: "Volume slider sensitivity - the smaller this number, the finer the volume control",
  693. type: "slider",
  694. category: "layout",
  695. min: 1,
  696. max: 20,
  697. default: 2,
  698. },
  699. watermarkEnabled: {
  700. desc: `Show a ${_constants__WEBPACK_IMPORTED_MODULE_0__.scriptInfo.name} watermark under the YTM logo`,
  701. type: "toggle",
  702. category: "layout",
  703. default: true,
  704. },
  705. queueButtons: {
  706. desc: "Add buttons to each song in the queue to quickly open their lyrics or remove them from the queue",
  707. type: "toggle",
  708. category: "layout",
  709. default: true,
  710. },
  711. //#SECTION lyrics
  712. geniusLyrics: {
  713. desc: "Add a button to the media controls of the currently playing song to open its lyrics on genius.com",
  714. type: "toggle",
  715. category: "lyrics",
  716. default: true,
  717. },
  718. };
  719. /***/ }),
  720. /***/ "./src/features/input.ts":
  721. /*!*******************************!*\
  722. !*** ./src/features/input.ts ***!
  723. \*******************************/
  724. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  725. __webpack_require__.r(__webpack_exports__);
  726. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  727. /* harmony export */ disableBeforeUnload: function() { return /* binding */ disableBeforeUnload; },
  728. /* harmony export */ enableBeforeUnload: function() { return /* binding */ enableBeforeUnload; },
  729. /* harmony export */ initArrowKeySkip: function() { return /* binding */ initArrowKeySkip; },
  730. /* harmony export */ initBeforeUnloadHook: function() { return /* binding */ initBeforeUnloadHook; },
  731. /* harmony export */ initSiteSwitch: function() { return /* binding */ initSiteSwitch; }
  732. /* harmony export */ });
  733. /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
  734. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
  735. /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../config */ "./src/config.ts");
  736. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  737. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  738. return new (P || (P = Promise))(function (resolve, reject) {
  739. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  740. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  741. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  742. step((generator = generator.apply(thisArg, _arguments || [])).next());
  743. });
  744. };
  745. //#MARKER arrow key skip
  746. function initArrowKeySkip() {
  747. document.addEventListener("keydown", onKeyDown);
  748. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Added key press listener");
  749. }
  750. /** Called when the user presses any key, anywhere */
  751. function onKeyDown(evt) {
  752. var _a, _b;
  753. if (["ArrowLeft", "ArrowRight"].includes(evt.code)) {
  754. // discard the event when a (text) input is currently active, like when editing a playlist
  755. if (["INPUT", "TEXTAREA", "SELECT"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) !== null && _b !== void 0 ? _b : "_"))
  756. 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`);
  757. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Captured key '${evt.code}' in proxy listener`);
  758. // 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
  759. const defaultProps = {
  760. altKey: false,
  761. ctrlKey: false,
  762. metaKey: false,
  763. shiftKey: false,
  764. target: document.body,
  765. currentTarget: document.body,
  766. originalTarget: document.body,
  767. explicitOriginalTarget: document.body,
  768. srcElement: document.body,
  769. type: "keydown",
  770. bubbles: true,
  771. cancelBubble: false,
  772. cancelable: true,
  773. isTrusted: true,
  774. repeat: false,
  775. // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
  776. view: (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.getUnsafeWindow)(),
  777. };
  778. let invalidKey = false;
  779. let keyProps = {};
  780. switch (evt.code) {
  781. case "ArrowLeft":
  782. keyProps = {
  783. code: "KeyH",
  784. key: "h",
  785. keyCode: 72,
  786. which: 72,
  787. };
  788. break;
  789. case "ArrowRight":
  790. keyProps = {
  791. code: "KeyL",
  792. key: "l",
  793. keyCode: 76,
  794. which: 76,
  795. };
  796. break;
  797. default:
  798. invalidKey = true;
  799. break;
  800. }
  801. if (!invalidKey) {
  802. const proxyProps = Object.assign(Object.assign({ code: "" }, defaultProps), keyProps);
  803. document.body.dispatchEvent(new KeyboardEvent("keydown", proxyProps));
  804. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Dispatched proxy keydown event: [${evt.code}] -> [${proxyProps.code}]`);
  805. }
  806. else
  807. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.warn)(`Captured key '${evt.code}' has no defined behavior`);
  808. }
  809. }
  810. //#MARKER site switch
  811. /** Initializes the site switch feature */
  812. function initSiteSwitch(domain) {
  813. document.addEventListener("keydown", (e) => {
  814. if (e.key === "F9")
  815. switchSite(domain === "yt" ? "ytm" : "yt");
  816. });
  817. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Initialized site switch listener");
  818. }
  819. /** Switches to the other site (between YT and YTM) */
  820. function switchSite(newDomain) {
  821. var _a;
  822. return __awaiter(this, void 0, void 0, function* () {
  823. try {
  824. if (newDomain === "ytm" && !location.href.includes("/watch"))
  825. return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.warn)("Not on a video page, so the site switch is ignored");
  826. let subdomain;
  827. if (newDomain === "ytm")
  828. subdomain = "music";
  829. else if (newDomain === "yt")
  830. subdomain = "www";
  831. if (!subdomain)
  832. throw new Error(`Unrecognized domain '${newDomain}'`);
  833. disableBeforeUnload();
  834. const { pathname, search, hash } = new URL(location.href);
  835. const vt = (_a = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getVideoTime)()) !== null && _a !== void 0 ? _a : 0;
  836. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Found video time of ${vt} seconds`);
  837. const cleanSearch = search.split("&")
  838. .filter((param) => !param.match(/^\??t=/))
  839. .join("&");
  840. const newSearch = cleanSearch.includes("?") ? `${cleanSearch.startsWith("?") ? cleanSearch : "?" + cleanSearch}&t=${vt}` : `?t=${vt}`;
  841. const newUrl = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
  842. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Switching to domain '${newDomain}' at ${newUrl}`);
  843. location.assign(newUrl);
  844. }
  845. catch (err) {
  846. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Error while switching site:", err);
  847. }
  848. });
  849. }
  850. //#MARKER beforeunload popup
  851. let beforeUnloadEnabled = true;
  852. /** Disables the popup before leaving the site */
  853. function disableBeforeUnload() {
  854. beforeUnloadEnabled = false;
  855. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Disabled popup before leaving the site");
  856. }
  857. /** (Re-)enables the popup before leaving the site */
  858. function enableBeforeUnload() {
  859. beforeUnloadEnabled = true;
  860. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Enabled popup before leaving the site");
  861. }
  862. /**
  863. * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
  864. * event listeners before they can be called by the site.
  865. */
  866. function initBeforeUnloadHook() {
  867. Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
  868. (function (original) {
  869. // @ts-ignore
  870. window.__proto__.addEventListener = function (...args) {
  871. if (!beforeUnloadEnabled && args[0] === "beforeunload")
  872. return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Prevented beforeunload event listener from being called");
  873. else
  874. return original.apply(this, args);
  875. };
  876. // @ts-ignore
  877. })(window.__proto__.addEventListener);
  878. (0,_config__WEBPACK_IMPORTED_MODULE_2__.getFeatures)().then(feats => {
  879. if (feats.disableBeforeUnloadPopup)
  880. disableBeforeUnload();
  881. });
  882. }
  883. /***/ }),
  884. /***/ "./src/features/layout.ts":
  885. /*!********************************!*\
  886. !*** ./src/features/layout.ts ***!
  887. \********************************/
  888. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  889. __webpack_require__.r(__webpack_exports__);
  890. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  891. /* harmony export */ addAnchorImprovements: function() { return /* binding */ addAnchorImprovements; },
  892. /* harmony export */ addConfigMenuOption: function() { return /* binding */ addConfigMenuOption; },
  893. /* harmony export */ addWatermark: function() { return /* binding */ addWatermark; },
  894. /* harmony export */ initQueueButtons: function() { return /* binding */ initQueueButtons; },
  895. /* harmony export */ initVolumeFeatures: function() { return /* binding */ initVolumeFeatures; },
  896. /* harmony export */ preInitLayout: function() { return /* binding */ preInitLayout; },
  897. /* harmony export */ removeUpgradeTab: function() { return /* binding */ removeUpgradeTab; }
  898. /* harmony export */ });
  899. /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
  900. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
  901. /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../config */ "./src/config.ts");
  902. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
  903. /* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../events */ "./src/events.ts");
  904. /* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
  905. /* harmony import */ var _lyrics__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./lyrics */ "./src/features/lyrics.ts");
  906. /* harmony import */ var _layout_css__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./layout.css */ "./src/features/layout.css");
  907. /* harmony import */ var ___WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! . */ "./src/features/index.ts");
  908. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  909. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  910. return new (P || (P = Promise))(function (resolve, reject) {
  911. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  912. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  913. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  914. step((generator = generator.apply(thisArg, _arguments || [])).next());
  915. });
  916. };
  917. let features;
  918. function preInitLayout() {
  919. return __awaiter(this, void 0, void 0, function* () {
  920. features = yield (0,_config__WEBPACK_IMPORTED_MODULE_2__.getFeatures)();
  921. });
  922. }
  923. //#MARKER BYTM-Config buttons
  924. let menuOpenAmt = 0, logoExchanged = false;
  925. /** Adds a watermark beneath the logo */
  926. function addWatermark() {
  927. const watermark = document.createElement("a");
  928. watermark.role = "button";
  929. watermark.id = "bytm-watermark";
  930. watermark.className = "style-scope ytmusic-nav-bar bytm-no-select";
  931. watermark.innerText = _constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name;
  932. watermark.title = "Open menu";
  933. watermark.tabIndex = 1000;
  934. improveLogo();
  935. watermark.addEventListener("click", (e) => {
  936. e.stopPropagation();
  937. menuOpenAmt++;
  938. if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
  939. (0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu)();
  940. if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
  941. exchangeLogo();
  942. });
  943. // when using the tab key to navigate
  944. watermark.addEventListener("keydown", (e) => {
  945. if (e.key === "Enter") {
  946. e.stopPropagation();
  947. menuOpenAmt++;
  948. if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
  949. (0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu)();
  950. if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
  951. exchangeLogo();
  952. }
  953. });
  954. const logoElem = document.querySelector("#left-content");
  955. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.insertAfter)(logoElem, watermark);
  956. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Added watermark element", watermark);
  957. }
  958. /** Turns the regular `<img>`-based logo into inline SVG to be able to animate and modify parts of it */
  959. function improveLogo() {
  960. return __awaiter(this, void 0, void 0, function* () {
  961. try {
  962. const res = yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.fetchAdvanced)("https://music.youtube.com/img/on_platform_logo_dark.svg");
  963. const svg = yield res.text();
  964. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-logo a", {
  965. listener: (logoElem) => {
  966. var _a;
  967. logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
  968. logoElem.innerHTML = svg;
  969. logoElem.querySelectorAll("ellipse").forEach((e) => {
  970. e.classList.add("bytm-mod-logo-ellipse");
  971. });
  972. (_a = logoElem.querySelector("path")) === null || _a === void 0 ? void 0 : _a.classList.add("bytm-mod-logo-path");
  973. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Swapped logo to inline SVG");
  974. },
  975. });
  976. }
  977. catch (err) {
  978. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't improve logo due to an error:", err);
  979. }
  980. });
  981. }
  982. /** Exchanges the default YTM logo into BetterYTM's logo with a sick ass animation */
  983. function exchangeLogo() {
  984. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(".bytm-mod-logo", {
  985. listener: (logoElem) => __awaiter(this, void 0, void 0, function* () {
  986. if (logoElem.classList.contains("bytm-logo-exchanged"))
  987. return;
  988. logoExchanged = true;
  989. logoElem.classList.add("bytm-logo-exchanged");
  990. const iconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("icon");
  991. const newLogo = document.createElement("img");
  992. newLogo.className = "bytm-mod-logo-img";
  993. newLogo.src = iconUrl;
  994. logoElem.insertBefore(newLogo, logoElem.querySelector("svg"));
  995. document.head.querySelectorAll("link[rel=\"icon\"]").forEach((e) => {
  996. e.href = iconUrl;
  997. });
  998. setTimeout(() => {
  999. logoElem.querySelectorAll(".bytm-mod-logo-ellipse").forEach(e => e.remove());
  1000. }, 1000);
  1001. }),
  1002. });
  1003. }
  1004. /** Called whenever the menu exists to add a BYTM-Configuration button */
  1005. function addConfigMenuOption(container) {
  1006. return __awaiter(this, void 0, void 0, function* () {
  1007. const cfgOptElem = document.createElement("a");
  1008. cfgOptElem.role = "button";
  1009. cfgOptElem.className = "bytm-cfg-menu-option bytm-anchor";
  1010. cfgOptElem.ariaLabel = "Click to open BetterYTM's configuration menu";
  1011. const cfgOptItemElem = document.createElement("div");
  1012. cfgOptItemElem.className = "bytm-cfg-menu-option-item";
  1013. cfgOptItemElem.addEventListener("click", (e) => {
  1014. const settingsBtnElem = document.querySelector("ytmusic-nav-bar ytmusic-settings-button tp-yt-paper-icon-button");
  1015. settingsBtnElem === null || settingsBtnElem === void 0 ? void 0 : settingsBtnElem.click();
  1016. menuOpenAmt++;
  1017. if ((!e.shiftKey || logoExchanged) && menuOpenAmt !== 5)
  1018. (0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu)();
  1019. if ((!logoExchanged && e.shiftKey) || menuOpenAmt === 5)
  1020. exchangeLogo();
  1021. });
  1022. const cfgOptIconElem = document.createElement("img");
  1023. cfgOptIconElem.className = "bytm-cfg-menu-option-icon";
  1024. cfgOptIconElem.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("icon");
  1025. const cfgOptTextElem = document.createElement("div");
  1026. cfgOptTextElem.className = "bytm-cfg-menu-option-text";
  1027. cfgOptTextElem.innerText = "BetterYTM Configuration";
  1028. cfgOptItemElem.appendChild(cfgOptIconElem);
  1029. cfgOptItemElem.appendChild(cfgOptTextElem);
  1030. cfgOptElem.appendChild(cfgOptItemElem);
  1031. container.appendChild(cfgOptElem);
  1032. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Added BYTM-Configuration button to menu popover", cfgOptElem);
  1033. });
  1034. }
  1035. //#MARKER remove upgrade tab
  1036. /** Removes the "Upgrade" / YT Music Premium tab from the sidebar */
  1037. function removeUpgradeTab() {
  1038. (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)", {
  1039. listener: (tabElemLarge) => {
  1040. tabElemLarge.remove();
  1041. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Removed large upgrade tab");
  1042. },
  1043. });
  1044. // TODO:FIXME: doesn't work fsr
  1045. (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)", {
  1046. listener: (tabElemSmall) => {
  1047. tabElemSmall.remove();
  1048. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Removed small upgrade tab");
  1049. },
  1050. });
  1051. }
  1052. //#MARKER volume slider
  1053. function initVolumeFeatures() {
  1054. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("tp-yt-paper-slider#volume-slider", {
  1055. listener: (sliderElem) => {
  1056. const volSliderCont = document.createElement("div");
  1057. volSliderCont.id = "bytm-vol-slider-cont";
  1058. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(sliderElem, volSliderCont);
  1059. if (typeof features.volumeSliderSize === "number")
  1060. setVolSliderSize();
  1061. if (features.volumeSliderLabel)
  1062. addVolumeSliderLabel();
  1063. setVolSliderStep();
  1064. },
  1065. });
  1066. }
  1067. const volSliderSelector = "tp-yt-paper-slider#volume-slider";
  1068. /** Adds a percentage label to the volume slider and tooltip */
  1069. function addVolumeSliderLabel() {
  1070. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(volSliderSelector, {
  1071. listener: (sliderElem) => {
  1072. const labelElem = document.createElement("div");
  1073. labelElem.className = "bytm-vol-slider-label";
  1074. labelElem.innerText = `${sliderElem.value}%`;
  1075. sliderElem.addEventListener("change", () => {
  1076. const label = `${sliderElem.value}%`;
  1077. const sensText = features.volumeSliderStep !== ___WEBPACK_IMPORTED_MODULE_8__.featInfo.volumeSliderStep["default"] ? ` (Sensitivity: ${sliderElem.step})` : "";
  1078. const labelFull = `Volume: ${label}${sensText}`;
  1079. sliderElem.setAttribute("title", labelFull); // TODO: probably needs to be on the parent
  1080. sliderElem.setAttribute("aria-valuetext", labelFull);
  1081. const labelElem2 = document.querySelector(".bytm-vol-slider-label");
  1082. if (labelElem2)
  1083. labelElem2.innerText = label;
  1084. });
  1085. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("#bytm-vol-slider-cont", {
  1086. listener: (volumeCont) => {
  1087. volumeCont.appendChild(labelElem);
  1088. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Added volume slider label", labelElem);
  1089. },
  1090. });
  1091. },
  1092. });
  1093. }
  1094. /** Sets the volume slider to a set size */
  1095. function setVolSliderSize() {
  1096. const { volumeSliderSize: size } = features;
  1097. if (typeof size !== "number" || isNaN(Number(size)))
  1098. return;
  1099. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addGlobalStyle)(`\
  1100. /* BetterYTM - set volume slider size */
  1101. #bytm-vol-slider-cont tp-yt-paper-slider#volume-slider {
  1102. width: ${size}px !important;
  1103. }`);
  1104. }
  1105. /** Sets the `step` attribute of the volume slider */
  1106. function setVolSliderStep() {
  1107. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(volSliderSelector, {
  1108. listener: (sliderElem) => {
  1109. sliderElem.setAttribute("step", String(features.volumeSliderStep));
  1110. },
  1111. });
  1112. }
  1113. //#MARKER queue buttons
  1114. function initQueueButtons() {
  1115. const addQueueBtns = (evt) => {
  1116. let amt = 0;
  1117. for (const queueItm of (0,_events__WEBPACK_IMPORTED_MODULE_4__.getEvtData)(evt).childNodes) {
  1118. if (!queueItm.classList.contains("bytm-has-queue-btns")) {
  1119. addQueueButtons(queueItm);
  1120. amt++;
  1121. }
  1122. }
  1123. if (amt > 0)
  1124. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Added buttons to ${amt} new queue ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", amt)}`);
  1125. };
  1126. _events__WEBPACK_IMPORTED_MODULE_4__.siteEvents.on("queueChanged", addQueueBtns);
  1127. _events__WEBPACK_IMPORTED_MODULE_4__.siteEvents.on("autoplayQueueChanged", addQueueBtns);
  1128. const queueItems = document.querySelectorAll("#contents.ytmusic-player-queue > ytmusic-player-queue-item");
  1129. if (queueItems.length === 0)
  1130. return;
  1131. queueItems.forEach(itm => addQueueButtons(itm));
  1132. (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)}`);
  1133. }
  1134. /**
  1135. * Adds the buttons to each item in the current song queue.
  1136. * Also observes for changes to add new buttons to new items in the queue.
  1137. * TODO:FIXME: deleting an element from the queue shifts the lyrics buttons
  1138. * @param queueItem The element with tagname `ytmusic-player-queue-item` to add queue buttons to
  1139. */
  1140. function addQueueButtons(queueItem) {
  1141. return __awaiter(this, void 0, void 0, function* () {
  1142. //#SECTION general queue item stuff
  1143. const queueBtnsCont = document.createElement("div");
  1144. queueBtnsCont.className = "bytm-queue-btn-container";
  1145. const songInfo = queueItem.querySelector(".song-info");
  1146. if (!songInfo)
  1147. return false;
  1148. const [songEl, artistEl] = songInfo.querySelectorAll("yt-formatted-string");
  1149. const song = songEl.innerText;
  1150. const artist = artistEl.innerText;
  1151. if (!song || !artist)
  1152. return false;
  1153. const lyricsIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("lyrics");
  1154. const deleteIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("delete");
  1155. //#SECTION lyrics btn
  1156. const lyricsBtnElem = yield (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.createLyricsBtn)(undefined, false);
  1157. {
  1158. lyricsBtnElem.title = "Open this song's lyrics in a new tab";
  1159. lyricsBtnElem.style.display = "inline-flex";
  1160. lyricsBtnElem.style.visibility = "initial";
  1161. lyricsBtnElem.style.pointerEvents = "initial";
  1162. lyricsBtnElem.addEventListener("click", (e) => __awaiter(this, void 0, void 0, function* () {
  1163. e.stopPropagation();
  1164. let lyricsUrl;
  1165. const artistsSan = (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.sanitizeArtists)(artist);
  1166. const songSan = (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.sanitizeSong)(song);
  1167. const cachedLyricsUrl = (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.getLyricsCacheEntry)(artistsSan, songSan);
  1168. if (cachedLyricsUrl)
  1169. lyricsUrl = cachedLyricsUrl;
  1170. else if (!songInfo.hasAttribute("data-bytm-loading")) {
  1171. const imgEl = lyricsBtnElem.querySelector("img");
  1172. if (!cachedLyricsUrl) {
  1173. songInfo.setAttribute("data-bytm-loading", "");
  1174. imgEl.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getResourceUrl)("spinner");
  1175. imgEl.classList.add("bytm-spinner");
  1176. }
  1177. lyricsUrl = cachedLyricsUrl !== null && cachedLyricsUrl !== void 0 ? cachedLyricsUrl : yield (0,_lyrics__WEBPACK_IMPORTED_MODULE_6__.getGeniusUrl)(artistsSan, songSan);
  1178. const resetImgElem = () => {
  1179. imgEl.src = lyricsIconUrl;
  1180. imgEl.classList.remove("bytm-spinner");
  1181. };
  1182. if (!cachedLyricsUrl) {
  1183. songInfo.removeAttribute("data-bytm-loading");
  1184. // so the new image doesn't "blink"
  1185. setTimeout(resetImgElem, 100);
  1186. }
  1187. if (!lyricsUrl) {
  1188. resetImgElem();
  1189. if (confirm("Couldn't find a lyrics page for this song.\nDo you want to open genius.com to manually search for it?"))
  1190. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.openInNewTab)("https://genius.com/search");
  1191. return;
  1192. }
  1193. }
  1194. lyricsUrl && (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.openInNewTab)(lyricsUrl);
  1195. }));
  1196. }
  1197. //#SECTION delete from queue btn
  1198. const deleteBtnElem = document.createElement("a");
  1199. {
  1200. Object.assign(deleteBtnElem, {
  1201. title: "Remove this song from the queue",
  1202. className: "ytmusic-player-bar bytm-delete-from-queue bytm-generic-btn",
  1203. role: "button",
  1204. });
  1205. deleteBtnElem.style.visibility = "initial";
  1206. deleteBtnElem.addEventListener("click", (e) => __awaiter(this, void 0, void 0, function* () {
  1207. e.stopPropagation();
  1208. // container of the queue item popup menu - element gets reused for every queue item
  1209. let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
  1210. try {
  1211. // three dots button to open the popup menu of a queue item
  1212. const dotsBtnElem = queueItem.querySelector("ytmusic-menu-renderer yt-button-shape button");
  1213. if (queuePopupCont)
  1214. queuePopupCont.setAttribute("data-bytm-hidden", "true");
  1215. dotsBtnElem.click();
  1216. yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(25);
  1217. queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
  1218. if (!queuePopupCont.hasAttribute("data-bytm-hidden"))
  1219. queuePopupCont.setAttribute("data-bytm-hidden", "true");
  1220. // a little bit janky and unreliable but the only way afaik
  1221. const removeFromQueueBtn = queuePopupCont.querySelector("tp-yt-paper-listbox *[role=option]:nth-child(7)");
  1222. yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.pauseFor)(20);
  1223. removeFromQueueBtn.click();
  1224. }
  1225. catch (err) {
  1226. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't remove song from queue due to error:", err);
  1227. }
  1228. finally {
  1229. queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.removeAttribute("data-bytm-hidden");
  1230. }
  1231. }));
  1232. const imgElem = document.createElement("img");
  1233. imgElem.className = "bytm-generic-btn-img";
  1234. imgElem.src = deleteIconUrl;
  1235. deleteBtnElem.appendChild(imgElem);
  1236. }
  1237. //#SECTION append elements to DOM
  1238. queueBtnsCont.appendChild(lyricsBtnElem);
  1239. queueBtnsCont.appendChild(deleteBtnElem);
  1240. songInfo.appendChild(queueBtnsCont);
  1241. queueItem.classList.add("bytm-has-queue-btns");
  1242. return true;
  1243. });
  1244. }
  1245. //#MARKER better clickable stuff
  1246. // TODO: add to thumbnails in "songs" list on channel pages (/channel/$id)
  1247. // TODO: add to thumbnails in playlists (/playlist?list=$id)
  1248. /** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
  1249. function addAnchorImprovements() {
  1250. //#SECTION carousel shelves
  1251. try {
  1252. // home page
  1253. /** Only adds anchor improvements for carousel shelves that contain the regular list-item-renderer, not the two-row-item-renderer */
  1254. const condCarouselImprovements = (el) => {
  1255. const listItemRenderer = el.querySelector("ytmusic-responsive-list-item-renderer");
  1256. if (listItemRenderer) {
  1257. const itemsElem = el.querySelector("ul#items");
  1258. if (itemsElem) {
  1259. const improvedElems = improveCarouselAnchors(itemsElem);
  1260. 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)}`);
  1261. }
  1262. }
  1263. };
  1264. // initial three shelves aren't included in the event fire
  1265. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-carousel-shelf-renderer", {
  1266. listener: () => {
  1267. const carouselShelves = document.body.querySelectorAll("ytmusic-carousel-shelf-renderer");
  1268. carouselShelves.forEach(condCarouselImprovements);
  1269. },
  1270. });
  1271. // every shelf that's loaded by scrolling:
  1272. _events__WEBPACK_IMPORTED_MODULE_4__.siteEvents.on("carouselShelvesChanged", (evt) => {
  1273. const { addedNodes, removedNodes } = (0,_events__WEBPACK_IMPORTED_MODULE_4__.getEvtData)(evt);
  1274. void removedNodes;
  1275. if (addedNodes.length > 0)
  1276. addedNodes.forEach(condCarouselImprovements);
  1277. });
  1278. // related tab in /watch
  1279. // TODO: items are lazy-loaded so this needs to be done differently
  1280. // maybe the onSelectorExists feature can be expanded to conditionally support continuous checking & querySelectorAll
  1281. const relatedTabAnchorImprovements = (tabElem) => {
  1282. const relatedCarouselShelves = tabElem === null || tabElem === void 0 ? void 0 : tabElem.querySelectorAll("ytmusic-carousel-shelf-renderer");
  1283. if (relatedCarouselShelves)
  1284. relatedCarouselShelves.forEach(condCarouselImprovements);
  1285. };
  1286. const relatedTabContentsSelector = "ytmusic-section-list-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] #contents";
  1287. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"]", {
  1288. listener: (relatedTabContainer) => {
  1289. const relatedTabObserver = new MutationObserver(([{ addedNodes, removedNodes }]) => {
  1290. if (addedNodes.length > 0 || removedNodes.length > 0)
  1291. relatedTabAnchorImprovements(document.querySelector(relatedTabContentsSelector));
  1292. });
  1293. relatedTabObserver.observe(relatedTabContainer, {
  1294. childList: true,
  1295. });
  1296. },
  1297. });
  1298. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(relatedTabContentsSelector, {
  1299. listener: (relatedTabContents) => {
  1300. relatedTabAnchorImprovements(relatedTabContents);
  1301. },
  1302. });
  1303. }
  1304. catch (err) {
  1305. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't improve carousel shelf anchors due to an error:", err);
  1306. }
  1307. //#SECTION sidebar
  1308. try {
  1309. const addSidebarAnchors = (sidebarCont) => {
  1310. const items = sidebarCont.parentNode.querySelectorAll("ytmusic-guide-entry-renderer tp-yt-paper-item");
  1311. improveSidebarAnchors(items);
  1312. return items.length;
  1313. };
  1314. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
  1315. listener: (sidebarCont) => {
  1316. const itemsAmt = addSidebarAnchors(sidebarCont);
  1317. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Added anchors around ${itemsAmt} sidebar ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", itemsAmt)}`);
  1318. },
  1319. });
  1320. (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", {
  1321. listener: (miniSidebarCont) => {
  1322. const itemsAmt = addSidebarAnchors(miniSidebarCont);
  1323. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Added anchors around ${itemsAmt} mini sidebar ${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.autoPlural)("item", itemsAmt)}`);
  1324. },
  1325. });
  1326. }
  1327. catch (err) {
  1328. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't add anchors to sidebar items due to an error:", err);
  1329. }
  1330. }
  1331. const sidebarPaths = [
  1332. "/",
  1333. "/explore",
  1334. "/library",
  1335. ];
  1336. /**
  1337. * Adds anchors to the sidebar items so they can be opened in a new tab
  1338. * @param sidebarItem
  1339. */
  1340. function improveSidebarAnchors(sidebarItems) {
  1341. sidebarItems.forEach((item, i) => {
  1342. var _a;
  1343. const anchorElem = document.createElement("a");
  1344. anchorElem.classList.add("bytm-anchor", "bytm-no-select");
  1345. anchorElem.role = "button";
  1346. anchorElem.target = "_self";
  1347. anchorElem.href = (_a = sidebarPaths[i]) !== null && _a !== void 0 ? _a : "#";
  1348. anchorElem.title = "Middle click to open in a new tab";
  1349. anchorElem.addEventListener("click", (e) => {
  1350. e.preventDefault();
  1351. });
  1352. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(item, anchorElem);
  1353. });
  1354. }
  1355. /**
  1356. * Actually adds the anchor improvements to carousel shelf items
  1357. * @param itemsElement The container with the selector `ul#items` inside of each `ytmusic-carousel`
  1358. */
  1359. function improveCarouselAnchors(itemsElement) {
  1360. if (itemsElement.classList.contains("bytm-anchors-improved"))
  1361. return 0;
  1362. let improvedElems = 0;
  1363. try {
  1364. const allListItems = itemsElement.querySelectorAll("ytmusic-responsive-list-item-renderer");
  1365. for (const listItem of allListItems) {
  1366. const thumbnailElem = listItem.querySelector(".left-items");
  1367. const titleElem = listItem.querySelector(".title-column yt-formatted-string.title a");
  1368. if (!thumbnailElem || !titleElem) {
  1369. (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");
  1370. continue;
  1371. }
  1372. const thumbnailAnchor = document.createElement("a");
  1373. thumbnailAnchor.className = "bytm-carousel-shelf-anchor bytm-anchor";
  1374. thumbnailAnchor.href = titleElem.href;
  1375. thumbnailAnchor.target = "_self";
  1376. thumbnailAnchor.role = "button";
  1377. thumbnailAnchor.addEventListener("click", (e) => {
  1378. e.preventDefault();
  1379. });
  1380. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addParent)(thumbnailElem, thumbnailAnchor);
  1381. improvedElems++;
  1382. }
  1383. }
  1384. catch (err) {
  1385. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't add anchor improvements due to error:", err);
  1386. }
  1387. finally {
  1388. itemsElement.classList.add("bytm-anchors-improved");
  1389. }
  1390. return improvedElems;
  1391. }
  1392. /***/ }),
  1393. /***/ "./src/features/lyrics.ts":
  1394. /*!********************************!*\
  1395. !*** ./src/features/lyrics.ts ***!
  1396. \********************************/
  1397. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  1398. __webpack_require__.r(__webpack_exports__);
  1399. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  1400. /* harmony export */ addLyricsCacheEntry: function() { return /* binding */ addLyricsCacheEntry; },
  1401. /* harmony export */ addMediaCtrlLyricsBtn: function() { return /* binding */ addMediaCtrlLyricsBtn; },
  1402. /* harmony export */ createLyricsBtn: function() { return /* binding */ createLyricsBtn; },
  1403. /* harmony export */ geniUrlBase: function() { return /* binding */ geniUrlBase; },
  1404. /* harmony export */ getCurrentLyricsUrl: function() { return /* binding */ getCurrentLyricsUrl; },
  1405. /* harmony export */ getGeniusUrl: function() { return /* binding */ getGeniusUrl; },
  1406. /* harmony export */ getLyricsCacheEntry: function() { return /* binding */ getLyricsCacheEntry; },
  1407. /* harmony export */ sanitizeArtists: function() { return /* binding */ sanitizeArtists; },
  1408. /* harmony export */ sanitizeSong: function() { return /* binding */ sanitizeSong; }
  1409. /* harmony export */ });
  1410. /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
  1411. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
  1412. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  1413. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  1414. return new (P || (P = Promise))(function (resolve, reject) {
  1415. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  1416. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  1417. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  1418. step((generator = generator.apply(thisArg, _arguments || [])).next());
  1419. });
  1420. };
  1421. var __asyncValues = (undefined && undefined.__asyncValues) || function (o) {
  1422. if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
  1423. var m = o[Symbol.asyncIterator], i;
  1424. 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);
  1425. 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); }); }; }
  1426. function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
  1427. };
  1428. /** Base URL of geniURL */
  1429. const geniUrlBase = "https://api.sv443.net/geniurl";
  1430. /** GeniURL endpoint that gives song metadata when provided with a `?q` or `?artist` and `?song` parameter - [more info](https://api.sv443.net/geniurl) */
  1431. const geniURLSearchTopUrl = `${geniUrlBase}/search/top`;
  1432. /**
  1433. * The threshold to pass to geniURL's fuzzy filtering.
  1434. * 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.
  1435. * Set to undefined to use the default.
  1436. */
  1437. const threshold = 0.55;
  1438. /** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
  1439. const geniUrlRatelimitTimeframe = 30;
  1440. const thresholdParam = threshold ? `&threshold=${(0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.clamp)(threshold, 0, 1)}` : "";
  1441. //#MARKER cache
  1442. /** Cache with key format `ARTIST - SONG` (sanitized) and lyrics URLs as values. Used to prevent extraneous requests to geniURL. */
  1443. const lyricsUrlCache = new Map();
  1444. /** How many cache entries can exist at a time - this is used to cap memory usage */
  1445. const maxLyricsCacheSize = 100;
  1446. /**
  1447. * Returns the lyrics URL from the passed un-/sanitized artist and song name, or undefined if the entry doesn't exist yet.
  1448. * **The passed parameters need to be sanitized first!**
  1449. */
  1450. function getLyricsCacheEntry(artists, song) {
  1451. return lyricsUrlCache.get(`${artists} - ${song}`);
  1452. }
  1453. /** Adds the provided entry into the lyrics URL cache */
  1454. function addLyricsCacheEntry(artists, song, lyricsUrl) {
  1455. lyricsUrlCache.set(`${sanitizeArtists(artists)} - ${sanitizeSong(song)}`, lyricsUrl);
  1456. // delete oldest entry if cache gets too big
  1457. if (lyricsUrlCache.size > maxLyricsCacheSize)
  1458. lyricsUrlCache.delete([...lyricsUrlCache.keys()].at(-1));
  1459. }
  1460. //#MARKER media control bar
  1461. let mcCurrentSongTitle = "";
  1462. /** Adds a lyrics button to the media controls bar */
  1463. function addMediaCtrlLyricsBtn() {
  1464. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer", { listener: addActualMediaCtrlLyricsBtn });
  1465. }
  1466. // TODO: add error.svg if the request fails
  1467. /** Actually adds the lyrics button after the like button renderer has been verified to exist */
  1468. function addActualMediaCtrlLyricsBtn(likeContainer) {
  1469. return __awaiter(this, void 0, void 0, function* () {
  1470. const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
  1471. // run parallel without awaiting so the MutationObserver below can observe the title element in time
  1472. (() => __awaiter(this, void 0, void 0, function* () {
  1473. const gUrl = yield getCurrentLyricsUrl();
  1474. const linkElem = yield createLyricsBtn(gUrl !== null && gUrl !== void 0 ? gUrl : undefined);
  1475. linkElem.id = "betterytm-lyrics-button";
  1476. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Inserted lyrics button into media controls bar");
  1477. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.insertAfter)(likeContainer, linkElem);
  1478. }))();
  1479. mcCurrentSongTitle = songTitleElem.title;
  1480. const spinnerIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("spinner");
  1481. const lyricsIconUrl = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("lyrics");
  1482. const onMutation = (mutations) => { var _a, mutations_1, mutations_1_1; return __awaiter(this, void 0, void 0, function* () {
  1483. var _b, e_1, _c, _d;
  1484. try {
  1485. for (_a = true, mutations_1 = __asyncValues(mutations); mutations_1_1 = yield mutations_1.next(), _b = mutations_1_1.done, !_b;) {
  1486. _d = mutations_1_1.value;
  1487. _a = false;
  1488. try {
  1489. const mut = _d;
  1490. const newTitle = mut.target.title;
  1491. if (newTitle !== mcCurrentSongTitle && newTitle.length > 0) {
  1492. const lyricsBtn = document.querySelector("#betterytm-lyrics-button");
  1493. if (!lyricsBtn)
  1494. return;
  1495. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Song title changed from '${mcCurrentSongTitle}' to '${newTitle}'`);
  1496. lyricsBtn.style.cursor = "wait";
  1497. lyricsBtn.style.pointerEvents = "none";
  1498. const imgElem = lyricsBtn.querySelector("img");
  1499. imgElem.src = spinnerIconUrl;
  1500. imgElem.classList.add("bytm-spinner");
  1501. mcCurrentSongTitle = newTitle;
  1502. const url = yield getCurrentLyricsUrl(); // can take a second or two
  1503. imgElem.src = lyricsIconUrl;
  1504. imgElem.classList.remove("bytm-spinner");
  1505. if (!url)
  1506. continue;
  1507. lyricsBtn.href = url;
  1508. lyricsBtn.title = "Open the current song's lyrics in a new tab";
  1509. lyricsBtn.style.cursor = "pointer";
  1510. lyricsBtn.style.visibility = "initial";
  1511. lyricsBtn.style.display = "inline-flex";
  1512. lyricsBtn.style.pointerEvents = "initial";
  1513. }
  1514. }
  1515. finally {
  1516. _a = true;
  1517. }
  1518. }
  1519. }
  1520. catch (e_1_1) { e_1 = { error: e_1_1 }; }
  1521. finally {
  1522. try {
  1523. if (!_a && !_b && (_c = mutations_1.return)) yield _c.call(mutations_1);
  1524. }
  1525. finally { if (e_1) throw e_1.error; }
  1526. }
  1527. }); };
  1528. // 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
  1529. const obs = new MutationObserver(onMutation);
  1530. obs.observe(songTitleElem, { attributes: true, attributeFilter: ["title"] });
  1531. });
  1532. }
  1533. //#MARKER utils
  1534. /** Removes everything in parentheses from the passed song name */
  1535. function sanitizeSong(songName) {
  1536. const parensRegex = /\(.+\)/gmi;
  1537. const squareParensRegex = /\[.+\]/gmi;
  1538. // trim right after the song name:
  1539. const sanitized = songName
  1540. .replace(parensRegex, "")
  1541. .replace(squareParensRegex, "");
  1542. return sanitized.trim();
  1543. }
  1544. /** Removes the secondary artist (if it exists) from the passed artists string */
  1545. function sanitizeArtists(artists) {
  1546. artists = artists.split(/\s*\u2022\s*/gmiu)[0]; // split at &bull; [•] character
  1547. if (artists.match(/&/))
  1548. artists = artists.split(/\s*&\s*/gm)[0];
  1549. if (artists.match(/,/))
  1550. artists = artists.split(/,\s*/gm)[0];
  1551. return artists.trim();
  1552. }
  1553. /** Returns the lyrics URL from genius for the currently selected song */
  1554. function getCurrentLyricsUrl() {
  1555. var _a;
  1556. return __awaiter(this, void 0, void 0, function* () {
  1557. try {
  1558. // In videos the video title contains both artist and song title, in "regular" YTM songs, the video title only contains the song title
  1559. const isVideo = typeof ((_a = document.querySelector("ytmusic-player")) === null || _a === void 0 ? void 0 : _a.getAttribute("video-mode_")) === "string";
  1560. const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
  1561. const songMetaElem = document.querySelector("span.subtitle > yt-formatted-string:first-child");
  1562. if (!songTitleElem || !songMetaElem || !songTitleElem.title)
  1563. return undefined;
  1564. const songNameRaw = songTitleElem.title;
  1565. const songName = sanitizeSong(songNameRaw);
  1566. const artistName = sanitizeArtists(songMetaElem.title);
  1567. /** Use when the current song is not a "real YTM song" with a static background, but rather a music video */
  1568. const getGeniusUrlVideo = () => __awaiter(this, void 0, void 0, function* () {
  1569. if (!songName.includes("-")) // for some fucking reason some music videos have YTM-like song title and artist separation, some don't
  1570. return yield getGeniusUrl(artistName, songName);
  1571. const [artist, ...rest] = songName.split("-").map(v => v.trim());
  1572. return yield getGeniusUrl(artist, rest.join(" "));
  1573. });
  1574. // TODO: artist might need further splitting before comma or ampersand
  1575. const url = isVideo ? yield getGeniusUrlVideo() : yield getGeniusUrl(artistName, songName);
  1576. return url;
  1577. }
  1578. catch (err) {
  1579. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't resolve lyrics URL:", err);
  1580. return undefined;
  1581. }
  1582. });
  1583. }
  1584. /** Fetches the actual lyrics URL from geniURL - **the passed parameters need to be sanitized first!** */
  1585. function getGeniusUrl(artist, song) {
  1586. var _a, _b, _c;
  1587. return __awaiter(this, void 0, void 0, function* () {
  1588. try {
  1589. const cacheEntry = getLyricsCacheEntry(artist, song);
  1590. if (cacheEntry) {
  1591. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Found lyrics URL in cache: ${cacheEntry}`);
  1592. return cacheEntry;
  1593. }
  1594. const startTs = Date.now();
  1595. const fetchUrl = `${geniURLSearchTopUrl}?artist=${encodeURIComponent(artist)}&song=${encodeURIComponent(song)}${thresholdParam}`;
  1596. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Requesting URL from geniURL at '${fetchUrl}'`);
  1597. const fetchRes = yield (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.fetchAdvanced)(fetchUrl);
  1598. if (fetchRes.status === 429) {
  1599. 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.`);
  1600. return undefined;
  1601. }
  1602. else if (fetchRes.status < 200 || fetchRes.status >= 300) {
  1603. (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)"}`);
  1604. return undefined;
  1605. }
  1606. const result = yield fetchRes.json();
  1607. if (typeof result === "object" && result.error) {
  1608. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't fetch lyrics URL:", result.message);
  1609. return undefined;
  1610. }
  1611. const url = result.url;
  1612. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Found lyrics URL (after ${Date.now() - startTs}ms): ${url}`);
  1613. addLyricsCacheEntry(artist, song, url);
  1614. return url;
  1615. }
  1616. catch (err) {
  1617. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't get lyrics URL due to error:", err);
  1618. return undefined;
  1619. }
  1620. });
  1621. }
  1622. /** Creates the base lyrics button element */
  1623. function createLyricsBtn(geniusUrl, hideIfLoading = true) {
  1624. return __awaiter(this, void 0, void 0, function* () {
  1625. const linkElem = document.createElement("a");
  1626. linkElem.className = "ytmusic-player-bar bytm-generic-btn";
  1627. linkElem.title = geniusUrl ? "Click to open this song's lyrics in a new tab" : "Loading lyrics URL...";
  1628. if (geniusUrl)
  1629. linkElem.href = geniusUrl;
  1630. linkElem.role = "button";
  1631. linkElem.target = "_blank";
  1632. linkElem.rel = "noopener noreferrer";
  1633. linkElem.style.visibility = hideIfLoading && geniusUrl ? "initial" : "hidden";
  1634. linkElem.style.display = hideIfLoading && geniusUrl ? "inline-flex" : "none";
  1635. const imgElem = document.createElement("img");
  1636. imgElem.className = "bytm-generic-btn-img";
  1637. imgElem.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getResourceUrl)("lyrics");
  1638. linkElem.appendChild(imgElem);
  1639. return linkElem;
  1640. });
  1641. }
  1642. /***/ }),
  1643. /***/ "./src/features/menu/menu.ts":
  1644. /*!***********************************!*\
  1645. !*** ./src/features/menu/menu.ts ***!
  1646. \***********************************/
  1647. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  1648. __webpack_require__.r(__webpack_exports__);
  1649. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  1650. /* harmony export */ closeMenu: function() { return /* binding */ closeMenu; },
  1651. /* harmony export */ initMenu: function() { return /* binding */ initMenu; },
  1652. /* harmony export */ openMenu: function() { return /* binding */ openMenu; },
  1653. /* harmony export */ setActiveTab: function() { return /* binding */ setActiveTab; }
  1654. /* harmony export */ });
  1655. /* harmony import */ var _changelog_md__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../../changelog.md */ "./changelog.md");
  1656. /* harmony import */ var _menu_html__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./menu.html */ "./src/features/menu/menu.html");
  1657. /* harmony import */ var _menu_css__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./menu.css */ "./src/features/menu/menu.css");
  1658. // REQUIREMENTS:
  1659. // - modal using the <dialog> element
  1660. // - sections with headers
  1661. // - support for "custom widgets"
  1662. // - debounce or save on button press to store new configuration
  1663. // - much better scaling including no vw and vh units
  1664. //#MARKER menu
  1665. /**
  1666. * These are the base selector values for the menu tabs
  1667. * Header selector format: `#${baseValue}-header`
  1668. * Content selector format: `#${baseValue}-content`
  1669. */
  1670. const tabsSelectors = {
  1671. options: "bytm-menu-tab-options",
  1672. info: "bytm-menu-tab-info",
  1673. changelog: "bytm-menu-tab-changelog",
  1674. };
  1675. /** Called from init(), before DOMContentLoaded is fired */
  1676. function initMenu() {
  1677. document.addEventListener("DOMContentLoaded", () => {
  1678. // create menu container
  1679. const menuContainer = document.createElement("div");
  1680. menuContainer.id = "bytm-menu-container";
  1681. // add menu html
  1682. menuContainer.innerHTML = _menu_html__WEBPACK_IMPORTED_MODULE_1__["default"];
  1683. document.body.appendChild(menuContainer);
  1684. initMenuContents();
  1685. });
  1686. }
  1687. function initMenuContents() {
  1688. var _a;
  1689. // hook events
  1690. for (const tab in tabsSelectors) {
  1691. const selector = tabsSelectors[tab];
  1692. (_a = document.querySelector(`#${selector}-header`)) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => {
  1693. setActiveTab(tab);
  1694. });
  1695. }
  1696. // init tab contents
  1697. initOptionsContent();
  1698. initInfoContent();
  1699. initChangelogContent();
  1700. }
  1701. /** Opens the specified tab */
  1702. function setActiveTab(tab) {
  1703. const tabs = Object.assign({}, tabsSelectors);
  1704. delete tabs[tab];
  1705. // disable all but new active tab
  1706. for (const [, val] of Object.entries(tabs)) {
  1707. document.querySelector(`#${val}-header`).dataset.active = "false";
  1708. document.querySelector(`#${val}-content`).dataset.active = "false";
  1709. }
  1710. // enable new active tab
  1711. document.querySelector(`#${tabsSelectors[tab]}-header`).dataset.active = "true";
  1712. document.querySelector(`#${tabsSelectors[tab]}-content`).dataset.active = "true";
  1713. }
  1714. /** Opens the modal menu dialog */
  1715. function openMenu() {
  1716. var _a;
  1717. (_a = document.querySelector("#bytm-menu-dialog")) === null || _a === void 0 ? void 0 : _a.showModal();
  1718. }
  1719. /** Closes the modal menu dialog */
  1720. function closeMenu() {
  1721. var _a;
  1722. (_a = document.querySelector("#bytm-menu-dialog")) === null || _a === void 0 ? void 0 : _a.close();
  1723. }
  1724. //#MARKER menu tab contents
  1725. function initOptionsContent() {
  1726. const tab = document.querySelector("#bytm-menu-tab-options-content");
  1727. void tab;
  1728. }
  1729. function initInfoContent() {
  1730. const tab = document.querySelector("#bytm-menu-tab-info-content");
  1731. void tab;
  1732. }
  1733. function initChangelogContent() {
  1734. const tab = document.querySelector("#bytm-menu-tab-changelog-content");
  1735. tab.innerHTML = _changelog_md__WEBPACK_IMPORTED_MODULE_0__["default"];
  1736. }
  1737. /***/ }),
  1738. /***/ "./src/features/menu/menu_old.ts":
  1739. /*!***************************************!*\
  1740. !*** ./src/features/menu/menu_old.ts ***!
  1741. \***************************************/
  1742. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  1743. __webpack_require__.r(__webpack_exports__);
  1744. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  1745. /* harmony export */ addMenu: function() { return /* binding */ addMenu; },
  1746. /* harmony export */ closeMenu: function() { return /* binding */ closeMenu; },
  1747. /* harmony export */ openMenu: function() { return /* binding */ openMenu; }
  1748. /* harmony export */ });
  1749. /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
  1750. /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../config */ "./src/config.ts");
  1751. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../constants */ "./src/constants.ts");
  1752. /* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../index */ "./src/features/index.ts");
  1753. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../../utils */ "./src/utils.ts");
  1754. /* harmony import */ var _menu_old_css__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu_old.css */ "./src/features/menu/menu_old.css");
  1755. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  1756. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  1757. return new (P || (P = Promise))(function (resolve, reject) {
  1758. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  1759. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  1760. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  1761. step((generator = generator.apply(thisArg, _arguments || [])).next());
  1762. });
  1763. };
  1764. //#MARKER create menu elements
  1765. let isMenuOpen = false;
  1766. /**
  1767. * Adds an element to open the BetterYTM menu
  1768. * @deprecated to be replaced with new menu - see https://github.com/Sv443/BetterYTM/issues/23
  1769. */
  1770. function addMenu() {
  1771. var _a, _b;
  1772. return __awaiter(this, void 0, void 0, function* () {
  1773. //#SECTION backdrop & menu container
  1774. const backgroundElem = document.createElement("div");
  1775. backgroundElem.id = "betterytm-menu-bg";
  1776. backgroundElem.title = "Click here to close the menu";
  1777. backgroundElem.style.visibility = "hidden";
  1778. backgroundElem.style.display = "none";
  1779. backgroundElem.addEventListener("click", (e) => {
  1780. var _a;
  1781. if (isMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "betterytm-menu-bg")
  1782. closeMenu(e);
  1783. });
  1784. document.body.addEventListener("keydown", (e) => {
  1785. if (isMenuOpen && e.key === "Escape")
  1786. closeMenu(e);
  1787. });
  1788. const menuContainer = document.createElement("div");
  1789. menuContainer.title = "";
  1790. menuContainer.id = "betterytm-menu";
  1791. menuContainer.style.borderRadius = "15px";
  1792. menuContainer.style.display = "flex";
  1793. menuContainer.style.flexDirection = "column";
  1794. menuContainer.style.justifyContent = "space-between";
  1795. //#SECTION title bar
  1796. const titleCont = document.createElement("div");
  1797. titleCont.style.padding = "8px 20px 15px 20px";
  1798. titleCont.style.display = "flex";
  1799. titleCont.style.justifyContent = "space-between";
  1800. titleCont.id = "betterytm-menu-titlecont";
  1801. const titleElem = document.createElement("h2");
  1802. titleElem.id = "betterytm-menu-title";
  1803. titleElem.classList.add("bytm-no-select");
  1804. titleElem.innerText = `${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.name} - Configuration`;
  1805. const linksCont = document.createElement("div");
  1806. linksCont.id = "betterytm-menu-linkscont";
  1807. const addLink = (imgSrc, href, title) => {
  1808. const anchorElem = document.createElement("a");
  1809. anchorElem.className = "betterytm-menu-link";
  1810. anchorElem.rel = "noopener noreferrer";
  1811. anchorElem.target = "_blank";
  1812. anchorElem.href = href;
  1813. anchorElem.title = title;
  1814. anchorElem.style.marginLeft = "10px";
  1815. const imgElem = document.createElement("img");
  1816. imgElem.className = "betterytm-menu-img bytm-no-select";
  1817. imgElem.src = imgSrc;
  1818. imgElem.style.width = "32px";
  1819. imgElem.style.height = "32px";
  1820. anchorElem.appendChild(imgElem);
  1821. linksCont.appendChild(anchorElem);
  1822. };
  1823. 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`);
  1824. addLink(yield (0,_utils__WEBPACK_IMPORTED_MODULE_4__.getResourceUrl)("greasyfork"), "https://greasyfork.org/TODO", `${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.name} on GreasyFork`);
  1825. const closeElem = document.createElement("img");
  1826. closeElem.id = "betterytm-menu-close";
  1827. closeElem.src = yield (0,_utils__WEBPACK_IMPORTED_MODULE_4__.getResourceUrl)("close");
  1828. closeElem.title = "Click to close the menu";
  1829. closeElem.style.marginLeft = "50px";
  1830. closeElem.style.width = "32px";
  1831. closeElem.style.height = "32px";
  1832. closeElem.addEventListener("click", closeMenu);
  1833. linksCont.appendChild(closeElem);
  1834. titleCont.appendChild(titleElem);
  1835. titleCont.appendChild(linksCont);
  1836. //#SECTION feature list
  1837. const featuresCont = document.createElement("div");
  1838. featuresCont.id = "betterytm-menu-opts";
  1839. featuresCont.style.display = "flex";
  1840. featuresCont.style.flexDirection = "column";
  1841. featuresCont.style.overflowY = "auto";
  1842. /** Gets called whenever the feature config is changed */
  1843. const confChanged = (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.debounce)((key, initialVal, newVal) => __awaiter(this, void 0, void 0, function* () {
  1844. const fmt = (val) => typeof val === "object" ? JSON.stringify(val) : String(val);
  1845. (0,_utils__WEBPACK_IMPORTED_MODULE_4__.info)(`Feature config changed, key '${key}' from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
  1846. const featConf = Object.assign({}, yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.getFeatures)());
  1847. featConf[key] = newVal;
  1848. yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.saveFeatureConf)(featConf);
  1849. (0,_utils__WEBPACK_IMPORTED_MODULE_4__.log)("Saved feature config changes:\n", yield GM.getValue("betterytm-config"));
  1850. }));
  1851. const features = yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.getFeatures)();
  1852. for (const key in features) {
  1853. const ftInfo = _index__WEBPACK_IMPORTED_MODULE_3__.featInfo[key];
  1854. // @ts-ignore
  1855. if (!ftInfo || ftInfo.visible === false)
  1856. continue;
  1857. const { desc, type, default: ftDefault } = ftInfo;
  1858. // @ts-ignore
  1859. const step = (_a = ftInfo === null || ftInfo === void 0 ? void 0 : ftInfo.step) !== null && _a !== void 0 ? _a : undefined;
  1860. const val = features[key];
  1861. const initialVal = (_b = val !== null && val !== void 0 ? val : ftDefault) !== null && _b !== void 0 ? _b : undefined;
  1862. const ftConfElem = document.createElement("div");
  1863. ftConfElem.id = `betterytm-ftconf-${key}`;
  1864. ftConfElem.style.display = "flex";
  1865. ftConfElem.style.flexDirection = "row";
  1866. ftConfElem.style.justifyContent = "space-between";
  1867. ftConfElem.style.padding = "8px 20px";
  1868. {
  1869. const textElem = document.createElement("span");
  1870. textElem.style.display = "inline-block";
  1871. textElem.style.fontSize = "15px";
  1872. textElem.innerText = desc;
  1873. ftConfElem.appendChild(textElem);
  1874. }
  1875. {
  1876. let inputType = "text";
  1877. switch (type) {
  1878. case "toggle":
  1879. inputType = "checkbox";
  1880. break;
  1881. case "slider":
  1882. inputType = "range";
  1883. break;
  1884. case "number":
  1885. inputType = "number";
  1886. break;
  1887. }
  1888. const inputElemId = `betterytm-ftconf-${key}-input`;
  1889. const ctrlElem = document.createElement("span");
  1890. ctrlElem.style.display = "inline-flex";
  1891. ctrlElem.style.alignItems = "center";
  1892. ctrlElem.style.whiteSpace = "nowrap";
  1893. const inputElem = document.createElement("input");
  1894. inputElem.id = inputElemId;
  1895. inputElem.type = inputType;
  1896. if (type === "toggle")
  1897. inputElem.style.marginLeft = "5px";
  1898. if (typeof initialVal !== "undefined")
  1899. inputElem.value = String(initialVal);
  1900. if (type === "number" && step)
  1901. inputElem.step = step;
  1902. // @ts-ignore
  1903. if (ftInfo.min && ftInfo.max) {
  1904. // @ts-ignore
  1905. inputElem.min = ftInfo.min;
  1906. // @ts-ignore
  1907. inputElem.max = ftInfo.max;
  1908. }
  1909. if (type === "toggle" && typeof initialVal !== "undefined")
  1910. inputElem.checked = Boolean(initialVal);
  1911. // @ts-ignore
  1912. const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
  1913. const fmtVal = (v) => String(v).trim();
  1914. const toggleLabelText = (toggled) => toggled ? "On" : "Off";
  1915. let labelElem;
  1916. if (type === "slider") {
  1917. labelElem = document.createElement("label");
  1918. labelElem.classList.add("betterytm-ftconf-label");
  1919. labelElem.style.marginRight = "20px";
  1920. labelElem.style.fontSize = "16px";
  1921. labelElem.htmlFor = inputElemId;
  1922. labelElem.innerText = fmtVal(initialVal) + unitTxt;
  1923. inputElem.addEventListener("input", () => {
  1924. if (labelElem)
  1925. labelElem.innerText = fmtVal(parseInt(inputElem.value)) + unitTxt;
  1926. });
  1927. }
  1928. else if (type === "toggle") {
  1929. labelElem = document.createElement("label");
  1930. labelElem.classList.add("betterytm-ftconf-label");
  1931. labelElem.style.paddingLeft = "10px";
  1932. labelElem.style.paddingRight = "5px";
  1933. labelElem.style.fontSize = "16px";
  1934. labelElem.htmlFor = inputElemId;
  1935. labelElem.innerText = toggleLabelText(Boolean(initialVal)) + unitTxt;
  1936. inputElem.addEventListener("input", () => {
  1937. if (labelElem)
  1938. labelElem.innerText = toggleLabelText(inputElem.checked) + unitTxt;
  1939. });
  1940. }
  1941. inputElem.addEventListener("input", () => {
  1942. let v = Number(String(inputElem.value).trim());
  1943. if (isNaN(v))
  1944. v = Number(inputElem.value);
  1945. if (typeof initialVal !== "undefined")
  1946. confChanged(key, initialVal, (type !== "toggle" ? v : inputElem.checked));
  1947. });
  1948. labelElem && ctrlElem.appendChild(labelElem);
  1949. ctrlElem.appendChild(inputElem);
  1950. ftConfElem.appendChild(ctrlElem);
  1951. }
  1952. featuresCont.appendChild(ftConfElem);
  1953. }
  1954. //#SECTION footer
  1955. const footerCont = document.createElement("div");
  1956. footerCont.id = "betterytm-menu-footer-cont";
  1957. footerCont.style.display = "flex";
  1958. footerCont.style.flexDirection = "row";
  1959. footerCont.style.justifyContent = "space-between";
  1960. footerCont.style.padding = "10px 20px";
  1961. footerCont.style.marginTop = "20px";
  1962. footerCont.style.position = "sticky";
  1963. footerCont.style.bottom = "0";
  1964. footerCont.style.backgroundColor = "var(--bytm-menu-bg)";
  1965. const footerElem = document.createElement("div");
  1966. footerElem.id = "betterytm-menu-footer";
  1967. footerElem.style.fontSize = "17px";
  1968. footerElem.style.textDecoration = "underline";
  1969. footerElem.innerText = "You need to reload the page to apply changes.";
  1970. const reloadElem = document.createElement("button");
  1971. reloadElem.style.marginLeft = "20px";
  1972. reloadElem.innerText = "Reload now";
  1973. reloadElem.title = "Click to reload the page";
  1974. reloadElem.addEventListener("click", () => location.reload());
  1975. const resetElem = document.createElement("button");
  1976. resetElem.className = "bytm-cfg-reset-btn";
  1977. resetElem.title = "Click to reset all settings to their default value";
  1978. resetElem.innerText = "Reset";
  1979. resetElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
  1980. if (confirm("Do you really want to reset all settings to their default value?\nThe page will automatically reload if you proceed.")) {
  1981. yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.setDefaultFeatConf)();
  1982. location.reload();
  1983. }
  1984. }));
  1985. footerElem.appendChild(reloadElem);
  1986. footerCont.appendChild(footerElem);
  1987. footerCont.appendChild(resetElem);
  1988. featuresCont.appendChild(footerCont);
  1989. //#SECTION finalize
  1990. const menuBody = document.createElement("div");
  1991. menuBody.id = "betterytm-menu-body";
  1992. menuBody.appendChild(titleCont);
  1993. menuBody.appendChild(featuresCont);
  1994. const versionCont = document.createElement("div");
  1995. versionCont.style.display = "flex";
  1996. versionCont.style.justifyContent = "space-around";
  1997. versionCont.style.fontSize = "1.15em";
  1998. versionCont.style.marginTop = "10px";
  1999. versionCont.style.marginBottom = "5px";
  2000. const versionElem = document.createElement("span");
  2001. versionElem.id = "betterytm-menu-version";
  2002. versionElem.innerText = `v${_constants__WEBPACK_IMPORTED_MODULE_2__.scriptInfo.version}`;
  2003. versionCont.appendChild(versionElem);
  2004. featuresCont.appendChild(versionCont);
  2005. menuContainer.appendChild(menuBody);
  2006. menuContainer.appendChild(versionCont);
  2007. backgroundElem.appendChild(menuContainer);
  2008. document.body.appendChild(backgroundElem);
  2009. (0,_utils__WEBPACK_IMPORTED_MODULE_4__.log)("Added menu element:", backgroundElem);
  2010. });
  2011. }
  2012. //#MARKER utilities
  2013. function closeMenu(e) {
  2014. if (!isMenuOpen)
  2015. return;
  2016. isMenuOpen = false;
  2017. (e === null || e === void 0 ? void 0 : e.bubbles) && e.stopPropagation();
  2018. document.body.classList.remove("bytm-disable-scroll");
  2019. const menuBg = document.querySelector("#betterytm-menu-bg");
  2020. menuBg.style.visibility = "hidden";
  2021. menuBg.style.display = "none";
  2022. }
  2023. // function that opens the menu, it should do the inverse of closeMenu()
  2024. function openMenu() {
  2025. if (isMenuOpen)
  2026. return;
  2027. isMenuOpen = true;
  2028. document.body.classList.add("bytm-disable-scroll");
  2029. const menuBg = document.querySelector("#betterytm-menu-bg");
  2030. menuBg.style.visibility = "visible";
  2031. menuBg.style.display = "block";
  2032. }
  2033. /***/ }),
  2034. /***/ "./src/utils.ts":
  2035. /*!**********************!*\
  2036. !*** ./src/utils.ts ***!
  2037. \**********************/
  2038. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  2039. __webpack_require__.r(__webpack_exports__);
  2040. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  2041. /* harmony export */ dbg: function() { return /* binding */ dbg; },
  2042. /* harmony export */ error: function() { return /* binding */ error; },
  2043. /* harmony export */ getDomain: function() { return /* binding */ getDomain; },
  2044. /* harmony export */ getResourceUrl: function() { return /* binding */ getResourceUrl; },
  2045. /* harmony export */ getVideoTime: function() { return /* binding */ getVideoTime; },
  2046. /* harmony export */ info: function() { return /* binding */ info; },
  2047. /* harmony export */ log: function() { return /* binding */ log; },
  2048. /* harmony export */ setLogLevel: function() { return /* binding */ setLogLevel; },
  2049. /* harmony export */ warn: function() { return /* binding */ warn; }
  2050. /* harmony export */ });
  2051. /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
  2052. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./constants */ "./src/constants.ts");
  2053. //#SECTION logging
  2054. let curLogLevel = 1;
  2055. /** Sets the current log level. 0 = Debug, 1 = Info */
  2056. function setLogLevel(level) {
  2057. curLogLevel = level;
  2058. }
  2059. /** 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 */
  2060. function getLogLevel(args) {
  2061. const minLogLvl = 0, maxLogLvl = 1;
  2062. if (typeof args.at(-1) === "number")
  2063. return (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.clamp)(args.splice(args.length - 1)[0], minLogLvl, maxLogLvl);
  2064. return 0;
  2065. }
  2066. /** Common prefix to be able to tell logged messages apart and filter them in devtools */
  2067. const consPrefix = `[${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name}]`;
  2068. const consPrefixDbg = `[${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name}/#DEBUG]`;
  2069. /**
  2070. * Logs string-compatible values to the console, as long as the log level is sufficient.
  2071. * @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.
  2072. */
  2073. function log(...args) {
  2074. if (curLogLevel <= getLogLevel(args))
  2075. console.log(consPrefix, ...args);
  2076. }
  2077. /**
  2078. * Logs string-compatible values to the console as info, as long as the log level is sufficient.
  2079. * @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.
  2080. */
  2081. function info(...args) {
  2082. if (curLogLevel <= getLogLevel(args))
  2083. console.info(consPrefix, ...args);
  2084. }
  2085. /**
  2086. * Logs string-compatible values to the console as a warning, as long as the log level is sufficient.
  2087. * @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.
  2088. */
  2089. function warn(...args) {
  2090. if (curLogLevel <= getLogLevel(args))
  2091. console.warn(consPrefix, ...args);
  2092. }
  2093. /** Logs string-compatible values to the console as an error, no matter the log level. */
  2094. function error(...args) {
  2095. console.error(consPrefix, ...args);
  2096. }
  2097. /** Logs string-compatible values to the console, intended for debugging only */
  2098. function dbg(...args) {
  2099. console.log(consPrefixDbg, ...args);
  2100. }
  2101. //#SECTION video time
  2102. /**
  2103. * Returns the current video time in seconds
  2104. * Dispatches mouse movement events in case the video time can't be estimated
  2105. * @returns Returns null if the video time is unavailable
  2106. */
  2107. function getVideoTime() {
  2108. return new Promise((res) => {
  2109. const domain = getDomain();
  2110. try {
  2111. if (domain === "ytm") {
  2112. const pbEl = document.querySelector("#progress-bar");
  2113. return res(!isNaN(Number(pbEl.value)) ? Number(pbEl.value) : null);
  2114. }
  2115. else if (domain === "yt") {
  2116. // YT doesn't update the progress bar when it's hidden (contrary to YTM which never hides it)
  2117. ytForceShowVideoTime();
  2118. const pbSelector = ".ytp-chrome-bottom div.ytp-progress-bar[role=\"slider\"]";
  2119. const progElem = document.querySelector(pbSelector);
  2120. let videoTime = progElem ? Number(progElem.getAttribute("aria-valuenow")) : -1;
  2121. const mut = new MutationObserver(() => {
  2122. // .observe() is only called when the element exists - no need to check for null
  2123. videoTime = Number(document.querySelector(pbSelector).getAttribute("aria-valuenow"));
  2124. });
  2125. const observe = (progElem) => {
  2126. mut.observe(progElem, {
  2127. attributes: true,
  2128. attributeFilter: ["aria-valuenow"],
  2129. });
  2130. setTimeout(() => {
  2131. res(videoTime >= 0 && !isNaN(videoTime) ? videoTime : null);
  2132. }, 500);
  2133. };
  2134. if (!progElem)
  2135. return (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.onSelector)(pbSelector, { listener: observe });
  2136. else
  2137. return observe(progElem);
  2138. }
  2139. }
  2140. catch (err) {
  2141. error("Couldn't get video time due to error:", err);
  2142. res(null);
  2143. }
  2144. });
  2145. }
  2146. /**
  2147. * Sends events that force the video controls to become visible for about 3 seconds.
  2148. * This only works once, then the page needs to be reloaded!
  2149. */
  2150. function ytForceShowVideoTime() {
  2151. const player = document.querySelector("#movie_player");
  2152. if (!player)
  2153. return false;
  2154. const defaultProps = {
  2155. // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
  2156. view: (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.getUnsafeWindow)(),
  2157. bubbles: true,
  2158. cancelable: false,
  2159. };
  2160. player.dispatchEvent(new MouseEvent("mouseenter", defaultProps));
  2161. const { x, y, width, height } = player.getBoundingClientRect();
  2162. const screenY = Math.round(y + height / 2);
  2163. const screenX = x + Math.min(50, Math.round(width / 3));
  2164. player.dispatchEvent(new MouseEvent("mousemove", Object.assign(Object.assign({}, defaultProps), { screenY,
  2165. screenX, movementX: 5, movementY: 0 })));
  2166. return true;
  2167. }
  2168. // /** 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 */
  2169. // function parseVideoTime(videoTime: string) {
  2170. // const matches = /^((\d{1,2}):)?(\d{1,2}):(\d{2})$/.exec(videoTime);
  2171. // if(!matches)
  2172. // return 0;
  2173. // const [, , hrs, min, sec] = matches as unknown as [string, string | undefined, string | undefined, string, string];
  2174. // let finalTime = 0;
  2175. // if(hrs)
  2176. // finalTime += Number(hrs) * 60 * 60;
  2177. // finalTime += Number(min) * 60 + Number(sec);
  2178. // return isNaN(finalTime) ? 0 : finalTime;
  2179. // }
  2180. // const selectorExistsMap = new Map<string, Array<(element: HTMLElement) => void>>();
  2181. // /**
  2182. // * Calls the `listener` as soon as the `selector` exists in the DOM.
  2183. // * Listeners are deleted as soon as they are called once.
  2184. // * Multiple listeners with the same selector may be registered.
  2185. // */
  2186. // export function onSelectorExists(selector: string, listener: (element: HTMLElement) => void) {
  2187. // const el = document.querySelector<HTMLElement>(selector);
  2188. // if(el)
  2189. // listener(el);
  2190. // else {
  2191. // if(selectorExistsMap.get(selector))
  2192. // selectorExistsMap.set(selector, [...selectorExistsMap.get(selector)!, listener]);
  2193. // else
  2194. // selectorExistsMap.set(selector, [listener]);
  2195. // }
  2196. // }
  2197. // /** Initializes the MutationObserver responsible for checking selectors registered in `onSelectorExists()` */
  2198. // export function initSelectorExistsCheck() {
  2199. // const observer = new MutationObserver(() => {
  2200. // for(const [selector, listeners] of selectorExistsMap.entries()) {
  2201. // const el = document.querySelector<HTMLElement>(selector);
  2202. // if(el) {
  2203. // listeners.forEach(listener => listener(el));
  2204. // selectorExistsMap.delete(selector);
  2205. // }
  2206. // }
  2207. // });
  2208. // observer.observe(document.body, {
  2209. // subtree: true,
  2210. // childList: true,
  2211. // });
  2212. // log("Initialized \"selector exists\" MutationObserver");
  2213. // }
  2214. /**
  2215. * Returns the current domain as a constant string representation
  2216. * @throws Throws if script runs on an unexpected website
  2217. */
  2218. function getDomain() {
  2219. const { hostname } = new URL(location.href);
  2220. if (hostname.includes("music.youtube"))
  2221. return "ytm";
  2222. else if (hostname.includes("youtube"))
  2223. return "yt";
  2224. else
  2225. throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
  2226. }
  2227. /** 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) */
  2228. function getResourceUrl(name) {
  2229. return GM.getResourceUrl(name);
  2230. }
  2231. /***/ }),
  2232. /***/ "../../svn/UserUtils/dist/index.mjs":
  2233. /*!******************************************!*\
  2234. !*** ../../svn/UserUtils/dist/index.mjs ***!
  2235. \******************************************/
  2236. /***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
  2237. __webpack_require__.r(__webpack_exports__);
  2238. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  2239. /* harmony export */ Config: function() { return /* binding */ Config; },
  2240. /* harmony export */ addGlobalStyle: function() { return /* binding */ addGlobalStyle; },
  2241. /* harmony export */ addParent: function() { return /* binding */ addParent; },
  2242. /* harmony export */ amplifyMedia: function() { return /* binding */ amplifyMedia; },
  2243. /* harmony export */ autoPlural: function() { return /* binding */ autoPlural; },
  2244. /* harmony export */ clamp: function() { return /* binding */ clamp; },
  2245. /* harmony export */ debounce: function() { return /* binding */ debounce; },
  2246. /* harmony export */ fetchAdvanced: function() { return /* binding */ fetchAdvanced; },
  2247. /* harmony export */ getSelectorMap: function() { return /* binding */ getSelectorMap; },
  2248. /* harmony export */ getUnsafeWindow: function() { return /* binding */ getUnsafeWindow; },
  2249. /* harmony export */ initOnSelector: function() { return /* binding */ initOnSelector; },
  2250. /* harmony export */ insertAfter: function() { return /* binding */ insertAfter; },
  2251. /* harmony export */ interceptEvent: function() { return /* binding */ interceptEvent; },
  2252. /* harmony export */ interceptWindowEvent: function() { return /* binding */ interceptWindowEvent; },
  2253. /* harmony export */ mapRange: function() { return /* binding */ mapRange; },
  2254. /* harmony export */ onSelector: function() { return /* binding */ onSelector; },
  2255. /* harmony export */ openInNewTab: function() { return /* binding */ openInNewTab; },
  2256. /* harmony export */ pauseFor: function() { return /* binding */ pauseFor; },
  2257. /* harmony export */ preloadImages: function() { return /* binding */ preloadImages; },
  2258. /* harmony export */ randRange: function() { return /* binding */ randRange; },
  2259. /* harmony export */ randomItem: function() { return /* binding */ randomItem; },
  2260. /* harmony export */ randomItemIndex: function() { return /* binding */ randomItemIndex; },
  2261. /* harmony export */ randomizeArray: function() { return /* binding */ randomizeArray; },
  2262. /* harmony export */ removeOnSelector: function() { return /* binding */ removeOnSelector; },
  2263. /* harmony export */ takeRandomItem: function() { return /* binding */ takeRandomItem; }
  2264. /* harmony export */ });
  2265. var __defProp = Object.defineProperty;
  2266. var __defProps = Object.defineProperties;
  2267. var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  2268. var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  2269. var __hasOwnProp = Object.prototype.hasOwnProperty;
  2270. var __propIsEnum = Object.prototype.propertyIsEnumerable;
  2271. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  2272. var __spreadValues = (a, b) => {
  2273. for (var prop in b || (b = {}))
  2274. if (__hasOwnProp.call(b, prop))
  2275. __defNormalProp(a, prop, b[prop]);
  2276. if (__getOwnPropSymbols)
  2277. for (var prop of __getOwnPropSymbols(b)) {
  2278. if (__propIsEnum.call(b, prop))
  2279. __defNormalProp(a, prop, b[prop]);
  2280. }
  2281. return a;
  2282. };
  2283. var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
  2284. var __publicField = (obj, key, value) => {
  2285. __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  2286. return value;
  2287. };
  2288. var __async = (__this, __arguments, generator) => {
  2289. return new Promise((resolve, reject) => {
  2290. var fulfilled = (value) => {
  2291. try {
  2292. step(generator.next(value));
  2293. } catch (e) {
  2294. reject(e);
  2295. }
  2296. };
  2297. var rejected = (value) => {
  2298. try {
  2299. step(generator.throw(value));
  2300. } catch (e) {
  2301. reject(e);
  2302. }
  2303. };
  2304. var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
  2305. step((generator = generator.apply(__this, __arguments)).next());
  2306. });
  2307. };
  2308. // lib/math.ts
  2309. function clamp(value, min, max) {
  2310. return Math.max(Math.min(value, max), min);
  2311. }
  2312. function mapRange(value, range_1_min, range_1_max, range_2_min, range_2_max) {
  2313. if (Number(range_1_min) === 0 && Number(range_2_min) === 0)
  2314. return value * (range_2_max / range_1_max);
  2315. return (value - range_1_min) * ((range_2_max - range_2_min) / (range_1_max - range_1_min)) + range_2_min;
  2316. }
  2317. function randRange(...args) {
  2318. let min, max;
  2319. if (typeof args[0] === "number" && typeof args[1] === "number") {
  2320. [min, max] = args;
  2321. } else if (typeof args[0] === "number" && typeof args[1] !== "number") {
  2322. min = 0;
  2323. max = args[0];
  2324. } else
  2325. throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
  2326. min = Number(min);
  2327. max = Number(max);
  2328. if (isNaN(min) || isNaN(max))
  2329. throw new TypeError(`Parameters "min" and "max" can't be NaN`);
  2330. if (min > max)
  2331. throw new TypeError(`Parameter "min" can't be bigger than "max"`);
  2332. return Math.floor(Math.random() * (max - min + 1)) + min;
  2333. }
  2334. // lib/array.ts
  2335. function randomItem(array) {
  2336. return randomItemIndex(array)[0];
  2337. }
  2338. function randomItemIndex(array) {
  2339. if (array.length === 0)
  2340. return [void 0, void 0];
  2341. const idx = randRange(array.length - 1);
  2342. return [array[idx], idx];
  2343. }
  2344. function takeRandomItem(arr) {
  2345. const [itm, idx] = randomItemIndex(arr);
  2346. if (idx === void 0)
  2347. return void 0;
  2348. arr.splice(idx, 1);
  2349. return itm;
  2350. }
  2351. function randomizeArray(array) {
  2352. const retArray = [...array];
  2353. if (array.length === 0)
  2354. return array;
  2355. for (let i = retArray.length - 1; i > 0; i--) {
  2356. const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
  2357. [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
  2358. }
  2359. return retArray;
  2360. }
  2361. // lib/config.ts
  2362. var Config = class {
  2363. /**
  2364. * Creates an instance of Config.
  2365. * @param id A unique ID for this configuration
  2366. * @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()`
  2367. * @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.
  2368. * @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.
  2369. */
  2370. constructor(id, formatVersion, defaultData, migrations) {
  2371. __publicField(this, "id");
  2372. __publicField(this, "formatVersion");
  2373. __publicField(this, "defaultData");
  2374. __publicField(this, "migrations");
  2375. __publicField(this, "cachedData");
  2376. this.id = id;
  2377. this.formatVersion = formatVersion;
  2378. this.defaultData = this.cachedData = defaultData;
  2379. this.migrations = migrations;
  2380. this.loadData();
  2381. }
  2382. /** Loads the data saved in persistent storage into the in-memory cache and also returns it */
  2383. loadData() {
  2384. return __async(this, null, function* () {
  2385. try {
  2386. const gmData = yield GM.getValue(this.id, this.defaultData);
  2387. const gmFmtVer = yield GM.getValue(`_uufmtver-${this.id}`);
  2388. if (typeof gmData !== "string" || typeof gmFmtVer !== "number")
  2389. return void 0;
  2390. let parsed = JSON.parse(gmData);
  2391. if (this.formatVersion > gmFmtVer)
  2392. parsed = yield this.runMigrations(parsed, gmFmtVer);
  2393. return this.cachedData = typeof parsed === "object" ? parsed : void 0;
  2394. } catch (err) {
  2395. return void 0;
  2396. }
  2397. });
  2398. }
  2399. /** Returns the data from the in-memory cache. Use `loadData()` to get fresh data from persistent storage (usually not necessary). */
  2400. getData() {
  2401. return this.cachedData;
  2402. }
  2403. /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
  2404. setData(data) {
  2405. this.cachedData = data;
  2406. return new Promise((resolve) => __async(this, null, function* () {
  2407. yield GM.setValue(this.id, JSON.stringify(data));
  2408. yield GM.setValue(`_uufmtver-${this.id}`, this.formatVersion);
  2409. resolve();
  2410. }));
  2411. }
  2412. /** Runs all necessary migration functions consecutively */
  2413. runMigrations(oldData, oldFmtVer) {
  2414. return __async(this, null, function* () {
  2415. return new Promise((resolve) => __async(this, null, function* () {
  2416. if (!this.migrations)
  2417. return resolve(oldData);
  2418. let newData = oldData;
  2419. const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
  2420. for (const [fmtVer, migrationFunc] of sortedMigrations) {
  2421. const ver = Number(fmtVer);
  2422. if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
  2423. const migRes = migrationFunc(newData);
  2424. newData = migRes instanceof Promise ? yield migRes : migRes;
  2425. oldFmtVer = ver;
  2426. }
  2427. }
  2428. yield GM.setValue(`_uufmtver-${this.id}`, this.formatVersion);
  2429. resolve(newData);
  2430. }));
  2431. });
  2432. }
  2433. };
  2434. // lib/dom.ts
  2435. function getUnsafeWindow() {
  2436. try {
  2437. return unsafeWindow;
  2438. } catch (e) {
  2439. return window;
  2440. }
  2441. }
  2442. function insertAfter(beforeElement, afterElement) {
  2443. var _a;
  2444. (_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
  2445. return afterElement;
  2446. }
  2447. function addParent(element2, newParent) {
  2448. const oldParent = element2.parentNode;
  2449. if (!oldParent)
  2450. throw new Error("Element doesn't have a parent node");
  2451. oldParent.replaceChild(newParent, element2);
  2452. newParent.appendChild(element2);
  2453. return newParent;
  2454. }
  2455. function addGlobalStyle(style) {
  2456. const styleElem = document.createElement("style");
  2457. styleElem.innerHTML = style;
  2458. document.head.appendChild(styleElem);
  2459. }
  2460. function preloadImages(srcUrls, rejects = false) {
  2461. const promises = srcUrls.map((src) => new Promise((res, rej) => {
  2462. const image = new Image();
  2463. image.src = src;
  2464. image.addEventListener("load", () => res(image));
  2465. image.addEventListener("error", (evt) => rejects && rej(evt));
  2466. }));
  2467. return Promise.allSettled(promises);
  2468. }
  2469. function openInNewTab(href) {
  2470. const openElem = document.createElement("a");
  2471. Object.assign(openElem, {
  2472. className: "userutils-open-in-new-tab",
  2473. target: "_blank",
  2474. rel: "noopener noreferrer",
  2475. href
  2476. });
  2477. openElem.style.display = "none";
  2478. document.body.appendChild(openElem);
  2479. openElem.click();
  2480. setTimeout(openElem.remove, 50);
  2481. }
  2482. function interceptEvent(eventObject, eventName, predicate) {
  2483. if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
  2484. Error.stackTraceLimit = 1e3;
  2485. }
  2486. (function(original) {
  2487. element.__proto__.addEventListener = function(...args) {
  2488. if (args[0] === eventName && predicate())
  2489. return;
  2490. else
  2491. return original.apply(this, args);
  2492. };
  2493. })(eventObject.__proto__.addEventListener);
  2494. }
  2495. function interceptWindowEvent(eventName, predicate) {
  2496. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  2497. }
  2498. function amplifyMedia(mediaElement, multiplier = 1) {
  2499. const context = new (window.AudioContext || window.webkitAudioContext)();
  2500. const result = {
  2501. mediaElement,
  2502. amplify: (multiplier2) => {
  2503. result.gain.gain.value = multiplier2;
  2504. },
  2505. getAmpLevel: () => result.gain.gain.value,
  2506. context,
  2507. source: context.createMediaElementSource(mediaElement),
  2508. gain: context.createGain()
  2509. };
  2510. result.source.connect(result.gain);
  2511. result.gain.connect(context.destination);
  2512. result.amplify(multiplier);
  2513. return result;
  2514. }
  2515. // lib/misc.ts
  2516. function autoPlural(word, num) {
  2517. if (Array.isArray(num) || num instanceof NodeList)
  2518. num = num.length;
  2519. return `${word}${num === 1 ? "" : "s"}`;
  2520. }
  2521. function pauseFor(time) {
  2522. return new Promise((res) => {
  2523. setTimeout(res, time);
  2524. });
  2525. }
  2526. function debounce(func, timeout = 300) {
  2527. let timer;
  2528. return function(...args) {
  2529. clearTimeout(timer);
  2530. timer = setTimeout(() => func.apply(this, args), timeout);
  2531. };
  2532. }
  2533. function fetchAdvanced(_0) {
  2534. return __async(this, arguments, function* (url, options = {}) {
  2535. const { timeout = 1e4 } = options;
  2536. const controller = new AbortController();
  2537. const id = setTimeout(() => controller.abort(), timeout);
  2538. const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
  2539. signal: controller.signal
  2540. }));
  2541. clearTimeout(id);
  2542. return res;
  2543. });
  2544. }
  2545. // lib/onSelector.ts
  2546. var selectorMap = /* @__PURE__ */ new Map();
  2547. function onSelector(selector, options) {
  2548. let selectorMapItems = [];
  2549. if (selectorMap.has(selector))
  2550. selectorMapItems = selectorMap.get(selector);
  2551. selectorMapItems.push(options);
  2552. selectorMap.set(selector, selectorMapItems);
  2553. checkSelectorExists(selector, selectorMapItems);
  2554. }
  2555. function removeOnSelector(selector) {
  2556. return selectorMap.delete(selector);
  2557. }
  2558. function checkSelectorExists(selector, options) {
  2559. const deleteIndices = [];
  2560. options.forEach((option, i) => {
  2561. try {
  2562. const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
  2563. if (elements !== null && elements instanceof NodeList && elements.length > 0 || elements !== null) {
  2564. option.listener(elements);
  2565. if (!option.continuous)
  2566. deleteIndices.push(i);
  2567. }
  2568. } catch (err) {
  2569. console.error(`Couldn't call listener for selector '${selector}'`, err);
  2570. }
  2571. });
  2572. if (deleteIndices.length > 0) {
  2573. const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
  2574. if (newOptsArray.length === 0)
  2575. selectorMap.delete(selector);
  2576. else {
  2577. selectorMap.set(selector, newOptsArray);
  2578. }
  2579. }
  2580. }
  2581. function initOnSelector(options = {}) {
  2582. const observer = new MutationObserver(() => {
  2583. for (const [selector, options2] of selectorMap.entries())
  2584. checkSelectorExists(selector, options2);
  2585. });
  2586. observer.observe(document.body, __spreadValues({
  2587. subtree: true,
  2588. childList: true
  2589. }, options));
  2590. }
  2591. function getSelectorMap() {
  2592. return selectorMap;
  2593. }
  2594. //# sourceMappingURL=http://localhost:8710/out.js.map
  2595. //# sourceMappingURL=http://localhost:8710/index.mjs.map
  2596. /***/ })
  2597. /******/ });
  2598. /************************************************************************/
  2599. /******/ // The module cache
  2600. /******/ var __webpack_module_cache__ = {};
  2601. /******/
  2602. /******/ // The require function
  2603. /******/ function __webpack_require__(moduleId) {
  2604. /******/ // Check if module is in cache
  2605. /******/ var cachedModule = __webpack_module_cache__[moduleId];
  2606. /******/ if (cachedModule !== undefined) {
  2607. /******/ return cachedModule.exports;
  2608. /******/ }
  2609. /******/ // Create a new module (and put it into the cache)
  2610. /******/ var module = __webpack_module_cache__[moduleId] = {
  2611. /******/ // no module.id needed
  2612. /******/ // no module.loaded needed
  2613. /******/ exports: {}
  2614. /******/ };
  2615. /******/
  2616. /******/ // Execute the module function
  2617. /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  2618. /******/
  2619. /******/ // Return the exports of the module
  2620. /******/ return module.exports;
  2621. /******/ }
  2622. /******/
  2623. /************************************************************************/
  2624. /******/ /* webpack/runtime/define property getters */
  2625. /******/ !function() {
  2626. /******/ // define getter functions for harmony exports
  2627. /******/ __webpack_require__.d = function(exports, definition) {
  2628. /******/ for(var key in definition) {
  2629. /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
  2630. /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
  2631. /******/ }
  2632. /******/ }
  2633. /******/ };
  2634. /******/ }();
  2635. /******/
  2636. /******/ /* webpack/runtime/hasOwnProperty shorthand */
  2637. /******/ !function() {
  2638. /******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
  2639. /******/ }();
  2640. /******/
  2641. /******/ /* webpack/runtime/make namespace object */
  2642. /******/ !function() {
  2643. /******/ // define __esModule on exports
  2644. /******/ __webpack_require__.r = function(exports) {
  2645. /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
  2646. /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  2647. /******/ }
  2648. /******/ Object.defineProperty(exports, '__esModule', { value: true });
  2649. /******/ };
  2650. /******/ }();
  2651. /******/
  2652. /************************************************************************/
  2653. var __webpack_exports__ = {};
  2654. // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  2655. !function() {
  2656. /*!**********************!*\
  2657. !*** ./src/index.ts ***!
  2658. \**********************/
  2659. __webpack_require__.r(__webpack_exports__);
  2660. /* harmony import */ var _sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @sv443-network/userutils */ "../../svn/UserUtils/dist/index.mjs");
  2661. /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./config */ "./src/config.ts");
  2662. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./constants */ "./src/constants.ts");
  2663. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
  2664. /* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./events */ "./src/events.ts");
  2665. /* harmony import */ var _features_index__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./features/index */ "./src/features/index.ts");
  2666. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  2667. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  2668. return new (P || (P = Promise))(function (resolve, reject) {
  2669. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  2670. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  2671. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  2672. step((generator = generator.apply(thisArg, _arguments || [])).next());
  2673. });
  2674. };
  2675. {
  2676. // console watermark with sexy gradient
  2677. 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%);";
  2678. const styleCommon = "color: #fff; font-size: 1.5em; padding-left: 6px; padding-right: 6px;";
  2679. console.log();
  2680. 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;");
  2681. console.log([
  2682. "Powered by:",
  2683. "─ lots of ambition",
  2684. `─ my song metadata API: ${_features_index__WEBPACK_IMPORTED_MODULE_5__.geniUrlBase}`,
  2685. "─ my userscript utility library: https://github.com/Sv443-Network/UserUtils",
  2686. "─ this tiny event listener library: https://github.com/billjs/event-emitter",
  2687. ].join("\n"));
  2688. console.log();
  2689. }
  2690. const domain = (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getDomain)();
  2691. /** Stuff that needs to be called ASAP, before anything async happens */
  2692. function preInit() {
  2693. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.setLogLevel)(_constants__WEBPACK_IMPORTED_MODULE_2__.logLevel);
  2694. if (domain === "ytm")
  2695. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.initBeforeUnloadHook)();
  2696. init();
  2697. }
  2698. function init() {
  2699. return __awaiter(this, void 0, void 0, function* () {
  2700. yield (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.preInitLayout)();
  2701. try {
  2702. document.addEventListener("DOMContentLoaded", onDomLoad);
  2703. }
  2704. catch (err) {
  2705. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("General Error:", err);
  2706. }
  2707. try {
  2708. void ["TODO(v1.1):", _features_index__WEBPACK_IMPORTED_MODULE_5__.initMenu];
  2709. // initMenu();
  2710. }
  2711. catch (err) {
  2712. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't initialize menu:", err);
  2713. }
  2714. });
  2715. }
  2716. /** Called when the DOM has finished loading and can be queried and altered by the userscript */
  2717. function onDomLoad() {
  2718. return __awaiter(this, void 0, void 0, function* () {
  2719. // post-build these double quotes are replaced by backticks (because if backticks are used here, webpack converts them to double quotes)
  2720. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.addGlobalStyle)(/* BetterYTM - global style */
  2721. `/*!**********************************************************************************!*\
  2722. !*** css ./node_modules/css-loader/dist/cjs.js!./src/features/menu/menu_old.css ***!
  2723. \**********************************************************************************/
  2724. :root {
  2725. --bytm-menu-bg: #212121;
  2726. }
  2727. #betterytm-menu-bg {
  2728. display: block;
  2729. position: fixed;
  2730. width: 100vw;
  2731. height: 100vh;
  2732. top: 0;
  2733. left: 0;
  2734. z-index: 15;
  2735. background-color: rgba(0, 0, 0, 0.6);
  2736. }
  2737. #betterytm-menu {
  2738. display: inline-block;
  2739. position: fixed;
  2740. width: 50vw;
  2741. height: auto;
  2742. left: 25vw;
  2743. top: 10vh;
  2744. z-index: 16;
  2745. padding: 10px 25px;
  2746. color: #fff;
  2747. background-color: var(--bytm-menu-bg);
  2748. }
  2749. #betterytm-menu-opts {
  2750. max-height: 70vh;
  2751. overflow: auto;
  2752. }
  2753. #betterytm-menu-titlecont {
  2754. display: flex;
  2755. }
  2756. #betterytm-menu-title {
  2757. font-size: 20px;
  2758. margin-top: 5px;
  2759. margin-bottom: 8px;
  2760. }
  2761. #betterytm-menu-linkscont {
  2762. display: flex;
  2763. }
  2764. .betterytm-menu-link {
  2765. cursor: pointer;
  2766. display: inline-block;
  2767. }
  2768. #betterytm-menu-close {
  2769. cursor: pointer;
  2770. }
  2771. .betterytm-ftconf-label {
  2772. user-select: none;
  2773. }
  2774. body.bytm-disable-scroll {
  2775. overflow-y: hidden !important;
  2776. }
  2777. /*!***************************************************************************!*\
  2778. !*** css ./node_modules/css-loader/dist/cjs.js!./src/features/layout.css ***!
  2779. \***************************************************************************/
  2780. /* #MARKER misc */
  2781. .bytm-generic-btn {
  2782. display: inline-flex;
  2783. align-items: center;
  2784. justify-content: center;
  2785. position: relative;
  2786. vertical-align: middle;
  2787. cursor: pointer;
  2788. margin-left: 8px;
  2789. width: 40px;
  2790. height: 40px;
  2791. border-radius: 100%;
  2792. background-color: transparent;
  2793. }
  2794. .bytm-generic-btn:hover {
  2795. background-color: var(--yt-spec-10-percent-layer, #1d1d1d);
  2796. }
  2797. .bytm-generic-btn-img {
  2798. display: inline-block;
  2799. z-index: 10;
  2800. width: 24px;
  2801. height: 24px;
  2802. padding: 5px;
  2803. }
  2804. .bytm-spinner {
  2805. animation: rotate 1.5s linear infinite;
  2806. }
  2807. @keyframes rotate {
  2808. from {
  2809. transform: rotate(0deg);
  2810. }
  2811. to {
  2812. transform: rotate(360deg);
  2813. }
  2814. }
  2815. .bytm-anchor {
  2816. all: unset;
  2817. cursor: pointer;
  2818. }
  2819. /* ytmusic-logo a[bytm-animated="true"] .bytm-mod-logo-ellipse {
  2820. transform-origin: 12px 12px;
  2821. animation: rotate 1s ease-in-out infinite;
  2822. } */
  2823. ytmusic-logo a.bytm-logo-exchanged .bytm-mod-logo-path {
  2824. transform-origin: 12px 12px;
  2825. animation: rotate 1s ease-in-out;
  2826. }
  2827. ytmusic-logo a.bytm-logo-exchanged .bytm-mod-logo-img {
  2828. width: 24px;
  2829. height: 24px;
  2830. z-index: 1000;
  2831. position: absolute;
  2832. animation: rotate-fade-in 1s ease-in-out;
  2833. }
  2834. @keyframes rotate-fade-in {
  2835. 0% {
  2836. opacity: 0;
  2837. transform: rotate(0deg);
  2838. }
  2839. 30% {
  2840. opacity: 0;
  2841. }
  2842. 90% {
  2843. opacity: 1;
  2844. }
  2845. 100% {
  2846. transform: rotate(360deg);
  2847. }
  2848. }
  2849. .bytm-no-select {
  2850. user-select: none;
  2851. -ms-user-select: none;
  2852. -moz-user-select: none;
  2853. -webkit-user-select: none;
  2854. }
  2855. /* #MARKER menu */
  2856. .bytm-cfg-menu-option {
  2857. display: block;
  2858. padding: 8px 0;
  2859. }
  2860. .bytm-cfg-menu-option-item {
  2861. display: flex;
  2862. flex-direction: row;
  2863. align-items: center;
  2864. font-size: 16px;
  2865. font-weight: 400;
  2866. line-height: 24px;
  2867. padding: var(--yt-compact-link-paper-item-padding, 0px 36px 0 16px);
  2868. min-height: var(--paper-item-min-height, 40px);
  2869. white-space: nowrap;
  2870. cursor: pointer;
  2871. }
  2872. .bytm-cfg-menu-option-item:hover {
  2873. background-color: var(--yt-spec-badge-chip-background, #3e3e3e);
  2874. }
  2875. .bytm-cfg-menu-option-icon {
  2876. width: 24px;
  2877. height: 24px;
  2878. margin-right: 16px;
  2879. display: flex;
  2880. align-items: center;
  2881. flex-direction: row;
  2882. flex: none;
  2883. }
  2884. .bytm-cfg-menu-option-text {
  2885. font-size: 1.4rem;
  2886. line-height: 2rem;
  2887. }
  2888. yt-multi-page-menu-section-renderer.ytd-multi-page-menu-renderer {
  2889. border-bottom: 1px solid var(--yt-spec-10-percent-layer, #3e3e3e);
  2890. }
  2891. /* #MARKER watermark */
  2892. #bytm-watermark {
  2893. font-size: 10px;
  2894. display: inline-block;
  2895. position: absolute;
  2896. left: 97px;
  2897. top: 46px;
  2898. z-index: 10;
  2899. color: white;
  2900. text-decoration: none;
  2901. cursor: pointer;
  2902. }
  2903. #bytm-watermark:hover {
  2904. text-decoration: underline;
  2905. }
  2906. /* #MARKER queue buttons */
  2907. .side-panel.modular ytmusic-player-queue-item .song-info.ytmusic-player-queue-item {
  2908. position: relative;
  2909. }
  2910. .side-panel.modular ytmusic-player-queue-item .bytm-queue-btn-container {
  2911. background: rgb(0, 0, 0);
  2912. background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 15%);
  2913. display: none;
  2914. position: absolute;
  2915. right: 0;
  2916. padding-left: 25px;
  2917. height: 100%;
  2918. }
  2919. .side-panel.modular ytmusic-player-queue-item:hover .bytm-queue-btn-container {
  2920. display: inline-block;
  2921. }
  2922. .side-panel.modular ytmusic-player-queue-item[play-button-state="loading"] .bytm-queue-btn-container,
  2923. .side-panel.modular ytmusic-player-queue-item[play-button-state="playing"] .bytm-queue-btn-container,
  2924. .side-panel.modular ytmusic-player-queue-item[play-button-state="paused"] .bytm-queue-btn-container {
  2925. /* using a var() is not viable since the nesting changes the actual value of the variable */
  2926. background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(29, 29, 29, 1) 15%);
  2927. }
  2928. ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown[data-bytm-hidden=true] {
  2929. display: none !important;
  2930. }
  2931. /* #MARKER anchor improvements */
  2932. ytmusic-responsive-list-item-renderer .left-items {
  2933. margin-right: 0 !important;
  2934. }
  2935. .bytm-carousel-shelf-anchor {
  2936. margin: 0 var(--ytmusic-responsive-list-item-thumbnail-margin-right, 16px) 0 0;
  2937. }
  2938. /*!******************************************************************************!*\
  2939. !*** css ./node_modules/css-loader/dist/cjs.js!./src/features/menu/menu.css ***!
  2940. \******************************************************************************/
  2941. /* #bytm-menu-backdrop {
  2942. display: none;
  2943. flex-direction: column;
  2944. justify-content: center;
  2945. align-items: center;
  2946. }
  2947. #bytm-menu-backdrop[data-menu-open="true"] {
  2948. display: flex;
  2949. } */
  2950. #bytm-menu-header-container {
  2951. display: flex;
  2952. justify-content: flex-start;
  2953. align-items: center;
  2954. border-color: #ffffff;
  2955. border-style: none solid none none;
  2956. }
  2957. .bytm-menu-header-option {
  2958. display: "flex";
  2959. justify-content: center;
  2960. align-items: center;
  2961. border-color: #ffffff;
  2962. border-style: solid none solid none;
  2963. }
  2964. #bytm-menu-header-option h3 {
  2965. margin: 0;
  2966. }
  2967. .bytm-menu-tab[data-active="true"] {
  2968. display: none;
  2969. }
  2970. .bytm-menu-tab[data-active="false"] {
  2971. display: none;
  2972. }
  2973. /*# sourceMappingURL=http://localhost:8710/global.css.map*/`);
  2974. (0,_sv443_network_userutils__WEBPACK_IMPORTED_MODULE_0__.initOnSelector)();
  2975. const features = yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.loadFeatureConf)();
  2976. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)(`Initializing features for domain "${domain}"...`);
  2977. try {
  2978. if (domain === "ytm") {
  2979. try {
  2980. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.addMenu)(); // TODO(v1.1): remove
  2981. }
  2982. catch (err) {
  2983. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("Couldn't add menu:", err);
  2984. }
  2985. (0,_events__WEBPACK_IMPORTED_MODULE_4__.initSiteEvents)();
  2986. (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 });
  2987. if (features.arrowKeySupport)
  2988. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.initArrowKeySkip)();
  2989. if (features.removeUpgradeTab)
  2990. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.removeUpgradeTab)();
  2991. if (features.watermarkEnabled)
  2992. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.addWatermark)();
  2993. if (features.geniusLyrics)
  2994. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.addMediaCtrlLyricsBtn)();
  2995. if (features.queueButtons)
  2996. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.initQueueButtons)();
  2997. if (features.anchorImprovements)
  2998. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.addAnchorImprovements)();
  2999. // TODO:
  3000. void _features_index__WEBPACK_IMPORTED_MODULE_5__.initVolumeFeatures;
  3001. }
  3002. if (["ytm", "yt"].includes(domain)) {
  3003. if (features.switchBetweenSites)
  3004. (0,_features_index__WEBPACK_IMPORTED_MODULE_5__.initSiteSwitch)(domain);
  3005. }
  3006. }
  3007. catch (err) {
  3008. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.error)("General error while executing feature:", err);
  3009. }
  3010. });
  3011. }
  3012. preInit();
  3013. }();
  3014. //# sourceMappingURL=http://localhost:8710/BetterYTM.user.js.map