BetterYTM.user.js 108 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377
  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 unsafeWindow
  21. // ==/UserScript==
  22. /*
  23. ▄▄▄ ▄ ▄▄▄▄▄▄ ▄
  24. █ █ ▄▄▄ █ █ ▄▄▄ ▄ ▄█ █ █ █▀▄▀█
  25. █▀▀▄ █▄█ █▀ █▀ █▄█ █▀ █ █ █ █
  26. █▄▄▀ ▀▄▄ ▀▄▄ ▀▄▄ ▀▄▄ █ █ █ █ █
  27. Made with ❤️ by Sv443
  28. I welcome every contribution on GitHub!
  29. https://github.com/Sv443/BetterYTM
  30. */
  31. /* Disclaimer: I am not affiliated with YouTube, Google, Alphabet, Genius or anyone else */
  32. /* C&D this 🖕 */
  33. // ==UserScript==
  34. // @name BetterYTM
  35. // @homepageURL https://github.com/Sv443/BetterYTM#readme
  36. // @namespace https://github.com/Sv443/BetterYTM
  37. // @version 1.0.0
  38. // @description Configurable layout and UX improvements for YouTube Music
  39. // @description:de Konfigurierbares Layout und UX-Verbesserungen für YouTube Music
  40. // @license MIT
  41. // @author Sv443
  42. // @copyright Sv443 (https://github.com/Sv443)
  43. // @icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icon/icon.png
  44. // @match https://music.youtube.com/*
  45. // @match https://www.youtube.com/*
  46. // @run-at document-start
  47. // @downloadURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
  48. // @updateURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
  49. // @connect api.sv443.net
  50. // @grant GM.getValue
  51. // @grant GM.setValue
  52. // @grant unsafeWindow
  53. // ==/UserScript==
  54. /*
  55. ▄▄▄ ▄ ▄▄▄▄▄▄ ▄
  56. █ █ ▄▄▄ █ █ ▄▄▄ ▄ ▄█ █ █ █▀▄▀█
  57. █▀▀▄ █▄█ █▀ █▀ █▄█ █▀ █ █ █ █
  58. █▄▄▀ ▀▄▄ ▀▄▄ ▀▄▄ ▀▄▄ █ █ █ █ █
  59. Made with ❤️ by Sv443
  60. I welcome every contribution on GitHub!
  61. https://github.com/Sv443/BetterYTM
  62. */
  63. /* Disclaimer: I am not affiliated with YouTube, Google, Alphabet, Genius or anyone else */
  64. /* C&D this 🖕 */
  65. /******/ var __webpack_modules__ = ({
  66. /***/ "./node_modules/@billjs/event-emitter/lib/index.js":
  67. /*!*********************************************************!*\
  68. !*** ./node_modules/@billjs/event-emitter/lib/index.js ***!
  69. \*********************************************************/
  70. /***/ (function(__unused_webpack_module, exports) {
  71. /**
  72. * A simple and lightweight EventEmitter by TypeScript for Node.js or Browsers.
  73. *
  74. * @author billjs
  75. * @see https://github.com/billjs/event-emitter
  76. * @license MIT(https://opensource.org/licenses/MIT)
  77. */
  78. Object.defineProperty(exports, "__esModule", ({ value: true }));
  79. /**
  80. * It's a class for managing events.
  81. * It can be extended to provide event functionality for other classes or object.
  82. *
  83. * @export
  84. * @class EventEmitter
  85. */
  86. var EventEmitter = /** @class */ (function () {
  87. function EventEmitter() {
  88. /**
  89. * the all event handlers are added.
  90. * it's a Map data structure(key-value), the key is event type, and the value is event handler.
  91. *
  92. * @memberof EventEmitter
  93. */
  94. this._eventHandlers = {};
  95. }
  96. /**
  97. * event type validator.
  98. *
  99. * @param {string} type event type
  100. * @returns {boolean}
  101. * @memberof EventEmitter
  102. */
  103. EventEmitter.prototype.isValidType = function (type) {
  104. return typeof type === 'string';
  105. };
  106. /**
  107. * event handler validator.
  108. *
  109. * @param {EventHandler} handler event handler
  110. * @returns {boolean}
  111. * @memberof EventEmitter
  112. */
  113. EventEmitter.prototype.isValidHandler = function (handler) {
  114. return typeof handler === 'function';
  115. };
  116. /**
  117. * listen on a new event by type and handler.
  118. * if listen on, the true is returned, otherwise the false.
  119. * The handler will not be listen if it is a duplicate.
  120. *
  121. * @param {string} type event type, it must be a unique string.
  122. * @param {EventHandler} handler event handler, when if the same handler is passed, listen it by only once.
  123. * @returns {boolean}
  124. * @memberof EventEmitter
  125. * @example
  126. * const emitter = new EventEmitter();
  127. * emitter.on('change:name', evt => {
  128. * console.log(evt);
  129. * });
  130. */
  131. EventEmitter.prototype.on = function (type, handler) {
  132. if (!type || !handler)
  133. return false;
  134. if (!this.isValidType(type))
  135. return false;
  136. if (!this.isValidHandler(handler))
  137. return false;
  138. var handlers = this._eventHandlers[type];
  139. if (!handlers)
  140. handlers = this._eventHandlers[type] = [];
  141. // when the same handler is passed, listen it by only once.
  142. if (handlers.indexOf(handler) >= 0)
  143. return false;
  144. handler._once = false;
  145. handlers.push(handler);
  146. return true;
  147. };
  148. /**
  149. * listen on an once event by type and handler.
  150. * when the event is fired, that will be listen off immediately and automatically.
  151. * The handler will not be listen if it is a duplicate.
  152. *
  153. * @param {string} type event type, it must be a unique string.
  154. * @param {EventHandler} handler event handler, when if the same handler is passed, listen it by only once.
  155. * @returns {boolean}
  156. * @memberof EventEmitter
  157. * @example
  158. * const emitter = new EventEmitter();
  159. * emitter.once('change:name', evt => {
  160. * console.log(evt);
  161. * });
  162. */
  163. EventEmitter.prototype.once = function (type, handler) {
  164. if (!type || !handler)
  165. return false;
  166. if (!this.isValidType(type))
  167. return false;
  168. if (!this.isValidHandler(handler))
  169. return false;
  170. var ret = this.on(type, handler);
  171. if (ret) {
  172. // set `_once` private property after listened,
  173. // avoid to modify event handler that has been listened.
  174. handler._once = true;
  175. }
  176. return ret;
  177. };
  178. /**
  179. * listen off an event by type and handler.
  180. * or listen off events by type, when if only type argument is passed.
  181. * or listen off all events, when if no arguments are passed.
  182. *
  183. * @param {string} [type] event type
  184. * @param {EventHandler} [handler] event handler
  185. * @returns
  186. * @memberof EventEmitter
  187. * @example
  188. * const emitter = new EventEmitter();
  189. * // listen off the specified event
  190. * emitter.off('change:name', evt => {
  191. * console.log(evt);
  192. * });
  193. * // listen off events by type
  194. * emitter.off('change:name');
  195. * // listen off all events
  196. * emitter.off();
  197. */
  198. EventEmitter.prototype.off = function (type, handler) {
  199. // listen off all events, when if no arguments are passed.
  200. // it does samething as `offAll` method.
  201. if (!type)
  202. return this.offAll();
  203. // listen off events by type, when if only type argument is passed.
  204. if (!handler) {
  205. this._eventHandlers[type] = [];
  206. return;
  207. }
  208. if (!this.isValidType(type))
  209. return;
  210. if (!this.isValidHandler(handler))
  211. return;
  212. var handlers = this._eventHandlers[type];
  213. if (!handlers || !handlers.length)
  214. return;
  215. // otherwise, listen off the specified event.
  216. for (var i = 0; i < handlers.length; i++) {
  217. var fn = handlers[i];
  218. if (fn === handler) {
  219. handlers.splice(i, 1);
  220. break;
  221. }
  222. }
  223. };
  224. /**
  225. * listen off all events, that means every event will be emptied.
  226. *
  227. * @memberof EventEmitter
  228. * @example
  229. * const emitter = new EventEmitter();
  230. * emitter.offAll();
  231. */
  232. EventEmitter.prototype.offAll = function () {
  233. this._eventHandlers = {};
  234. };
  235. /**
  236. * fire the specified event, and you can to pass a data.
  237. * When fired, every handler attached to that event will be executed.
  238. * But, if it's an once event, listen off it immediately after called handler.
  239. *
  240. * @param {string} type event type
  241. * @param {*} [data] event data
  242. * @returns
  243. * @memberof EventEmitter
  244. * @example
  245. * const emitter = new EventEmitter();
  246. * emitter.fire('change:name', 'new name');
  247. */
  248. EventEmitter.prototype.fire = function (type, data) {
  249. if (!type || !this.isValidType(type))
  250. return;
  251. var handlers = this._eventHandlers[type];
  252. if (!handlers || !handlers.length)
  253. return;
  254. var event = this.createEvent(type, data);
  255. for (var _i = 0, handlers_1 = handlers; _i < handlers_1.length; _i++) {
  256. var handler = handlers_1[_i];
  257. if (!this.isValidHandler(handler))
  258. continue;
  259. if (handler._once)
  260. event.once = true;
  261. // call event handler, and pass the event argument.
  262. handler(event);
  263. // if it's an once event, listen off it immediately after called handler.
  264. if (event.once)
  265. this.off(type, handler);
  266. }
  267. };
  268. /**
  269. * check whether the specified event has been listen on.
  270. * or check whether the events by type has been listen on, when if only `type` argument is passed.
  271. *
  272. * @param {string} type event type
  273. * @param {EventHandler} [handler] event handler, optional
  274. * @returns {boolean}
  275. * @memberof EventEmitter
  276. * @example
  277. * const emitter = new EventEmitter();
  278. * const result = emitter.has('change:name');
  279. */
  280. EventEmitter.prototype.has = function (type, handler) {
  281. if (!type || !this.isValidType(type))
  282. return false;
  283. var handlers = this._eventHandlers[type];
  284. // if there are no any events, return false.
  285. if (!handlers || !handlers.length)
  286. return false;
  287. // at lest one event, and no pass `handler` argument, then return true.
  288. if (!handler || !this.isValidHandler(handler))
  289. return true;
  290. // otherwise, need to traverse the handlers.
  291. return handlers.indexOf(handler) >= 0;
  292. };
  293. /**
  294. * get the handlers for the specified event type.
  295. *
  296. * @param {string} type event type
  297. * @returns {EventHandler[]}
  298. * @memberof EventEmitter
  299. * @example
  300. * const emitter = new EventEmitter();
  301. * const handlers = emitter.getHandlers('change:name');
  302. * console.log(handlers);
  303. */
  304. EventEmitter.prototype.getHandlers = function (type) {
  305. if (!type || !this.isValidType(type))
  306. return [];
  307. return this._eventHandlers[type] || [];
  308. };
  309. /**
  310. * create event object.
  311. *
  312. * @param {string} type event type
  313. * @param {*} [data] event data
  314. * @param {boolean} [once=false] is it an once event?
  315. * @returns {Event}
  316. * @memberof EventEmitter
  317. */
  318. EventEmitter.prototype.createEvent = function (type, data, once) {
  319. if (once === void 0) { once = false; }
  320. var event = { type: type, data: data, timestamp: Date.now(), once: once };
  321. return event;
  322. };
  323. return EventEmitter;
  324. }());
  325. exports.EventEmitter = EventEmitter;
  326. /**
  327. * EventEmitter instance for global.
  328. * @type {EventEmitter}
  329. */
  330. exports.globalEvent = new EventEmitter();
  331. /***/ }),
  332. /***/ "./changelog.md":
  333. /*!**********************!*\
  334. !*** ./changelog.md ***!
  335. \**********************/
  336. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  337. __webpack_require__.r(__webpack_exports__);
  338. // Module
  339. 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<p><br><br><br><br><br><br><br><br><br></p>\n";
  340. // Exports
  341. /* harmony default export */ __webpack_exports__["default"] = (code);
  342. /***/ }),
  343. /***/ "./src/features/menu/menu.html":
  344. /*!*************************************!*\
  345. !*** ./src/features/menu/menu.html ***!
  346. \*************************************/
  347. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  348. __webpack_require__.r(__webpack_exports__);
  349. // Module
  350. 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";
  351. // Exports
  352. /* harmony default export */ __webpack_exports__["default"] = (code);
  353. /***/ }),
  354. /***/ "./src/features/layout.css":
  355. /*!*********************************!*\
  356. !*** ./src/features/layout.css ***!
  357. \*********************************/
  358. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  359. __webpack_require__.r(__webpack_exports__);
  360. // extracted by mini-css-extract-plugin
  361. /***/ }),
  362. /***/ "./src/features/menu/menu.css":
  363. /*!************************************!*\
  364. !*** ./src/features/menu/menu.css ***!
  365. \************************************/
  366. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  367. __webpack_require__.r(__webpack_exports__);
  368. // extracted by mini-css-extract-plugin
  369. /***/ }),
  370. /***/ "./src/config.ts":
  371. /*!***********************!*\
  372. !*** ./src/config.ts ***!
  373. \***********************/
  374. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  375. __webpack_require__.r(__webpack_exports__);
  376. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  377. /* harmony export */ defaultFeatures: function() { return /* binding */ defaultFeatures; },
  378. /* harmony export */ getFeatures: function() { return /* binding */ getFeatures; },
  379. /* harmony export */ loadFeatureConf: function() { return /* binding */ loadFeatureConf; },
  380. /* harmony export */ saveFeatureConf: function() { return /* binding */ saveFeatureConf; },
  381. /* harmony export */ setDefaultFeatConf: function() { return /* binding */ setDefaultFeatConf; }
  382. /* harmony export */ });
  383. /* harmony import */ var _features_index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./features/index */ "./src/features/index.ts");
  384. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
  385. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  386. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  387. return new (P || (P = Promise))(function (resolve, reject) {
  388. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  389. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  390. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  391. step((generator = generator.apply(thisArg, _arguments || [])).next());
  392. });
  393. };
  394. /** If this number is incremented, the features object needs to be migrated (TODO: migration not implemented yet) */
  395. const formatVersion = 1;
  396. const defaultFeatures = Object.keys(_features_index__WEBPACK_IMPORTED_MODULE_0__.featInfo)
  397. .reduce((acc, key) => {
  398. acc[key] = _features_index__WEBPACK_IMPORTED_MODULE_0__.featInfo[key].default;
  399. return acc;
  400. }, {});
  401. /** In-memory features object to save on a little bit of I/O */
  402. let featuresCache;
  403. /**
  404. * Returns the current FeatureConfig in memory or reads it from GM storage
  405. * Automatically applies defaults for non-existant keys
  406. * @param forceRead Set to true to force reading the config from GM storage
  407. */
  408. function getFeatures(forceRead = false) {
  409. return __awaiter(this, void 0, void 0, function* () {
  410. if (featuresCache === undefined || forceRead)
  411. yield saveFeatureConf(featuresCache = Object.assign(Object.assign({}, defaultFeatures), yield loadFeatureConf())); // look at this sexy one liner
  412. return featuresCache;
  413. });
  414. }
  415. /** Loads a feature configuration saved persistently, returns an empty object if no feature configuration was saved */
  416. function loadFeatureConf() {
  417. return __awaiter(this, void 0, void 0, function* () {
  418. const defConf = Object.assign({}, defaultFeatures);
  419. try {
  420. const featureConf = yield GM.getValue("betterytm-config");
  421. if (typeof featureConf !== "string") {
  422. yield setDefaultFeatConf();
  423. return featuresCache = defConf;
  424. }
  425. return featuresCache = Object.freeze(featureConf ? JSON.parse(featureConf) : defConf);
  426. }
  427. catch (err) {
  428. yield setDefaultFeatConf();
  429. return featuresCache = defConf;
  430. }
  431. });
  432. }
  433. /**
  434. * Saves the passed feature configuration persistently in GM storage and in the in-memory cache
  435. * @param featureConf
  436. */
  437. function saveFeatureConf(featureConf) {
  438. if (!featureConf || typeof featureConf != "object")
  439. throw new TypeError("Feature config not provided or invalid");
  440. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)("Saving new feature config:", featureConf);
  441. featuresCache = Object.assign({}, featureConf);
  442. GM.setValue("betterytm-config-ver", formatVersion);
  443. return GM.setValue("betterytm-config", JSON.stringify(featureConf));
  444. }
  445. /** Resets the featuresCache synchronously and the persistent features storage asynchronously to its default values */
  446. function setDefaultFeatConf() {
  447. featuresCache = Object.assign({}, defaultFeatures);
  448. GM.setValue("betterytm-config-ver", formatVersion);
  449. return GM.setValue("betterytm-config", JSON.stringify(defaultFeatures));
  450. }
  451. /***/ }),
  452. /***/ "./src/constants.ts":
  453. /*!**************************!*\
  454. !*** ./src/constants.ts ***!
  455. \**************************/
  456. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  457. __webpack_require__.r(__webpack_exports__);
  458. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  459. /* harmony export */ branch: function() { return /* binding */ branch; },
  460. /* harmony export */ logLevel: function() { return /* binding */ logLevel; },
  461. /* harmony export */ scriptInfo: function() { return /* binding */ scriptInfo; },
  462. /* harmony export */ triesInterval: function() { return /* binding */ triesInterval; },
  463. /* harmony export */ triesLimit: function() { return /* binding */ triesLimit; }
  464. /* harmony export */ });
  465. const branchRaw = "develop";
  466. /** The branch to use in various URLs that point to the GitHub repo */
  467. const branch = branchRaw.match(/^{{.+}}$/) ? "main" : branchRaw;
  468. /**
  469. * How much info should be logged to the devtools console?
  470. * 0 = Debug (show everything) or 1 = Info (show only important stuff)
  471. */
  472. const logLevel = 0; // TODO: change in prod.
  473. /** Specifies the hard limit for repetitive tasks */
  474. const triesLimit = 50;
  475. /** Specifies the interval in ms for repetitive tasks */
  476. const triesInterval = 200;
  477. /** Info about the userscript, parsed from the userscript header (tools/post-build.js) */
  478. const scriptInfo = Object.freeze({
  479. name: GM.info.script.name,
  480. version: GM.info.script.version,
  481. namespace: GM.info.script.namespace,
  482. lastCommit: "fae706c", // assert as generic string instead of union
  483. });
  484. /***/ }),
  485. /***/ "./src/events.ts":
  486. /*!***********************!*\
  487. !*** ./src/events.ts ***!
  488. \***********************/
  489. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  490. __webpack_require__.r(__webpack_exports__);
  491. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  492. /* harmony export */ getEvtData: function() { return /* binding */ getEvtData; },
  493. /* harmony export */ initSiteEvents: function() { return /* binding */ initSiteEvents; },
  494. /* harmony export */ removeAllObservers: function() { return /* binding */ removeAllObservers; },
  495. /* harmony export */ siteEvents: function() { return /* binding */ siteEvents; }
  496. /* harmony export */ });
  497. /* harmony import */ var _billjs_event_emitter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @billjs/event-emitter */ "./node_modules/@billjs/event-emitter/lib/index.js");
  498. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
  499. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  500. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  501. return new (P || (P = Promise))(function (resolve, reject) {
  502. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  503. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  504. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  505. step((generator = generator.apply(thisArg, _arguments || [])).next());
  506. });
  507. };
  508. /** EventEmitter instance that is used to detect changes to the site */
  509. const siteEvents = new _billjs_event_emitter__WEBPACK_IMPORTED_MODULE_0__.EventEmitter();
  510. /**
  511. * Returns the data of an event from the `@billjs/event-emitter` library.
  512. * This function is used as a shorthand to extract the data and assert it with the type passed in `<T>`
  513. * @param evt Event object from the `.on()` or `.once()` method
  514. * @template T Type of the data passed by `.fire(type: string, data: T)`
  515. */
  516. function getEvtData(evt) {
  517. return evt.data;
  518. }
  519. let observers = [];
  520. /** Disconnects and deletes all observers. Run `initSiteEvents()` again to create new ones. */
  521. function removeAllObservers() {
  522. observers.forEach((observer, i) => {
  523. observer.disconnect();
  524. delete observers[i];
  525. });
  526. observers = [];
  527. }
  528. /** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
  529. function initSiteEvents() {
  530. return __awaiter(this, void 0, void 0, function* () {
  531. try {
  532. //#SECTION queue
  533. // the queue container always exists so it doesn't need the extra init function
  534. const queueObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
  535. if (addedNodes.length > 0 || removedNodes.length > 0) {
  536. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
  537. siteEvents.fire("queueChanged", target);
  538. }
  539. });
  540. // only observe added or removed elements
  541. queueObs.observe(document.querySelector(".side-panel.modular #contents.ytmusic-player-queue"), {
  542. childList: true,
  543. });
  544. const autoplayObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
  545. if (addedNodes.length > 0 || removedNodes.length > 0) {
  546. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Detected autoplay queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
  547. siteEvents.fire("autoplayQueueChanged", target);
  548. }
  549. });
  550. // TODO: check if this works since autoplay seems to be lazy-loaded
  551. autoplayObs.observe(document.querySelector(".side-panel.modular ytmusic-player-queue #automix-contents"), {
  552. childList: true,
  553. });
  554. //#SECTION home page observers
  555. initHomeObservers();
  556. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Successfully initialized SiteEvents observers");
  557. observers = observers.concat([
  558. queueObs,
  559. autoplayObs,
  560. ]);
  561. }
  562. catch (err) {
  563. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't initialize SiteEvents observers due to an error:\n", err);
  564. }
  565. });
  566. }
  567. /**
  568. * The home page might not exist yet if the site was accessed through any path like /watch directly.
  569. * This function will keep waiting for when the home page exists, then create the necessary MutationObservers.
  570. */
  571. function initHomeObservers() {
  572. var _a;
  573. return __awaiter(this, void 0, void 0, function* () {
  574. let interval;
  575. // hidden="" attribute is only present if the content of the page doesn't exist yet
  576. // so this pauses execution until that attribute is removed
  577. if ((_a = document.querySelector("ytmusic-browse-response#browse-page")) === null || _a === void 0 ? void 0 : _a.hasAttribute("hidden")) {
  578. yield new Promise((res) => {
  579. interval = setInterval(() => {
  580. var _a;
  581. if (!((_a = document.querySelector("ytmusic-browse-response#browse-page")) === null || _a === void 0 ? void 0 : _a.hasAttribute("hidden"))) {
  582. clearInterval(interval);
  583. res();
  584. }
  585. }, 50);
  586. });
  587. }
  588. siteEvents.fire("homePageLoaded");
  589. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Initialized home page observers");
  590. //#SECTION carousel shelves
  591. const shelfContainerObs = new MutationObserver(([{ addedNodes, removedNodes }]) => {
  592. if (addedNodes.length > 0 || removedNodes.length > 0) {
  593. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)("Detected carousel shelf container change - added nodes:", addedNodes.length, "- removed nodes:", removedNodes.length);
  594. siteEvents.fire("carouselShelvesChanged", { addedNodes, removedNodes });
  595. }
  596. });
  597. shelfContainerObs.observe(document.querySelector("#contents.ytmusic-section-list-renderer"), {
  598. childList: true,
  599. });
  600. observers = observers.concat([shelfContainerObs]);
  601. });
  602. }
  603. /***/ }),
  604. /***/ "./src/features/index.ts":
  605. /*!*******************************!*\
  606. !*** ./src/features/index.ts ***!
  607. \*******************************/
  608. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  609. __webpack_require__.r(__webpack_exports__);
  610. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  611. /* harmony export */ addAnchorImprovements: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addAnchorImprovements; },
  612. /* harmony export */ addLyricsCacheEntry: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.addLyricsCacheEntry; },
  613. /* harmony export */ addMediaCtrlLyricsBtn: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.addMediaCtrlLyricsBtn; },
  614. /* harmony export */ addMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.addMenu; },
  615. /* harmony export */ addWatermark: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.addWatermark; },
  616. /* harmony export */ closeMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.closeMenu; },
  617. /* harmony export */ createLyricsBtn: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.createLyricsBtn; },
  618. /* harmony export */ disableBeforeUnload: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.disableBeforeUnload; },
  619. /* harmony export */ enableBeforeUnload: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.enableBeforeUnload; },
  620. /* harmony export */ featInfo: function() { return /* binding */ featInfo; },
  621. /* harmony export */ geniUrlBase: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.geniUrlBase; },
  622. /* harmony export */ getCurrentLyricsUrl: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getCurrentLyricsUrl; },
  623. /* harmony export */ getGeniusUrl: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getGeniusUrl; },
  624. /* harmony export */ getLyricsCacheEntry: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.getLyricsCacheEntry; },
  625. /* harmony export */ initArrowKeySkip: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initArrowKeySkip; },
  626. /* harmony export */ initBeforeUnloadHook: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initBeforeUnloadHook; },
  627. /* harmony export */ initMenu: function() { return /* reexport safe */ _menu_menu__WEBPACK_IMPORTED_MODULE_4__.initMenu; },
  628. /* harmony export */ initQueueButtons: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.initQueueButtons; },
  629. /* harmony export */ initSiteSwitch: function() { return /* reexport safe */ _input__WEBPACK_IMPORTED_MODULE_1__.initSiteSwitch; },
  630. /* harmony export */ openMenu: function() { return /* reexport safe */ _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__.openMenu; },
  631. /* harmony export */ preInitLayout: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.preInitLayout; },
  632. /* harmony export */ removeUpgradeTab: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.removeUpgradeTab; },
  633. /* harmony export */ sanitizeArtists: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.sanitizeArtists; },
  634. /* harmony export */ sanitizeSong: function() { return /* reexport safe */ _lyrics__WEBPACK_IMPORTED_MODULE_3__.sanitizeSong; },
  635. /* harmony export */ setVolSliderSize: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.setVolSliderSize; },
  636. /* harmony export */ setVolSliderStep: function() { return /* reexport safe */ _layout__WEBPACK_IMPORTED_MODULE_2__.setVolSliderStep; }
  637. /* harmony export */ });
  638. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
  639. /* harmony import */ var _input__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./input */ "./src/features/input.ts");
  640. /* harmony import */ var _layout__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./layout */ "./src/features/layout.ts");
  641. /* harmony import */ var _lyrics__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./lyrics */ "./src/features/lyrics.ts");
  642. /* harmony import */ var _menu_menu__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu/menu */ "./src/features/menu/menu.ts");
  643. /* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
  644. // TODO
  645. /** Contains all possible features with their default values and other config */
  646. const featInfo = {
  647. //#SECTION input
  648. arrowKeySupport: {
  649. desc: "Arrow keys skip forwards and backwards by 10 seconds",
  650. type: "toggle",
  651. category: "input",
  652. default: true,
  653. },
  654. switchBetweenSites: {
  655. desc: "Add F9 as a hotkey to switch between the YT and YTM sites on a video / song",
  656. type: "toggle",
  657. category: "input",
  658. default: true,
  659. },
  660. switchSitesHotkey: {
  661. desc: "TODO: Which hotkey needs to be pressed to switch sites?",
  662. type: "hotkey",
  663. category: "input",
  664. default: {
  665. key: "F9",
  666. shift: false,
  667. ctrl: false,
  668. meta: false,
  669. },
  670. visible: false,
  671. },
  672. disableBeforeUnloadPopup: {
  673. desc: "Completely disable the popup that sometimes appears before leaving the site",
  674. type: "toggle",
  675. category: "input",
  676. default: false,
  677. },
  678. anchorImprovements: {
  679. desc: "TODO: Make it so middle clicking a song to open it in a new tab is easier",
  680. type: "toggle",
  681. category: "input",
  682. default: true,
  683. visible: false,
  684. },
  685. //#SECTION layout
  686. removeUpgradeTab: {
  687. desc: "Remove the \"Upgrade\" / YT Music Premium tab",
  688. type: "toggle",
  689. category: "layout",
  690. default: true,
  691. },
  692. volumeSliderSize: {
  693. desc: "The width of the volume slider in pixels",
  694. type: "number",
  695. category: "layout",
  696. min: 10,
  697. max: 1000,
  698. step: 5,
  699. default: 160,
  700. unit: "px",
  701. },
  702. volumeSliderStep: {
  703. desc: "Volume slider sensitivity - the smaller this number, the finer the volume control",
  704. type: "slider",
  705. category: "layout",
  706. min: 1,
  707. max: 20,
  708. default: 2,
  709. },
  710. watermarkEnabled: {
  711. desc: `Show a ${_constants__WEBPACK_IMPORTED_MODULE_0__.scriptInfo.name} watermark under the YTM logo`,
  712. type: "toggle",
  713. category: "layout",
  714. default: true,
  715. visible: false,
  716. },
  717. queueButtons: {
  718. desc: "Add buttons while hovering over a song in a queue to quickly remove it (TODO) or open its lyrics",
  719. type: "toggle",
  720. category: "layout",
  721. default: true,
  722. },
  723. //#SECTION lyrics
  724. geniusLyrics: {
  725. desc: "Add a button to the media controls to open the current song's lyrics on genius.com in a new tab",
  726. type: "toggle",
  727. category: "lyrics",
  728. default: true,
  729. },
  730. };
  731. /***/ }),
  732. /***/ "./src/features/input.ts":
  733. /*!*******************************!*\
  734. !*** ./src/features/input.ts ***!
  735. \*******************************/
  736. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  737. __webpack_require__.r(__webpack_exports__);
  738. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  739. /* harmony export */ disableBeforeUnload: function() { return /* binding */ disableBeforeUnload; },
  740. /* harmony export */ enableBeforeUnload: function() { return /* binding */ enableBeforeUnload; },
  741. /* harmony export */ initArrowKeySkip: function() { return /* binding */ initArrowKeySkip; },
  742. /* harmony export */ initBeforeUnloadHook: function() { return /* binding */ initBeforeUnloadHook; },
  743. /* harmony export */ initSiteSwitch: function() { return /* binding */ initSiteSwitch; }
  744. /* harmony export */ });
  745. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
  746. /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../config */ "./src/config.ts");
  747. //#MARKER arrow key skip
  748. function initArrowKeySkip() {
  749. document.addEventListener("keydown", onKeyDown);
  750. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.log)("Added key press listener");
  751. }
  752. /** Called when the user presses any key, anywhere */
  753. function onKeyDown(evt) {
  754. var _a, _b;
  755. if (["ArrowLeft", "ArrowRight"].includes(evt.code)) {
  756. // discard the event when a (text) input is currently active, like when editing a playlist
  757. if (["INPUT", "TEXTAREA", "SELECT"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) !== null && _b !== void 0 ? _b : "_"))
  758. return (0,_utils__WEBPACK_IMPORTED_MODULE_0__.info)(`Captured valid key but the current active element is <${document.activeElement.tagName.toLowerCase()}>, so the keypress is ignored`);
  759. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.log)(`Captured key '${evt.code}' in proxy listener`);
  760. // 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
  761. const defaultProps = {
  762. altKey: false,
  763. ctrlKey: false,
  764. metaKey: false,
  765. shiftKey: false,
  766. target: document.body,
  767. currentTarget: document.body,
  768. originalTarget: document.body,
  769. explicitOriginalTarget: document.body,
  770. srcElement: document.body,
  771. type: "keydown",
  772. bubbles: true,
  773. cancelBubble: false,
  774. cancelable: true,
  775. isTrusted: true,
  776. repeat: false,
  777. // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
  778. view: (0,_utils__WEBPACK_IMPORTED_MODULE_0__.getUnsafeWindow)(),
  779. };
  780. let invalidKey = false;
  781. let keyProps = {};
  782. switch (evt.code) {
  783. case "ArrowLeft":
  784. keyProps = {
  785. code: "KeyH",
  786. key: "h",
  787. keyCode: 72,
  788. which: 72,
  789. };
  790. break;
  791. case "ArrowRight":
  792. keyProps = {
  793. code: "KeyL",
  794. key: "l",
  795. keyCode: 76,
  796. which: 76,
  797. };
  798. break;
  799. default:
  800. invalidKey = true;
  801. break;
  802. }
  803. if (!invalidKey) {
  804. const proxyProps = Object.assign(Object.assign({ code: "" }, defaultProps), keyProps);
  805. document.body.dispatchEvent(new KeyboardEvent("keydown", proxyProps));
  806. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.log)(`Dispatched proxy keydown event: [${evt.code}] -> [${proxyProps.code}]`);
  807. }
  808. else
  809. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.warn)(`Captured key '${evt.code}' has no defined behavior`);
  810. }
  811. }
  812. //#MARKER site switch
  813. /** Initializes the site switch feature */
  814. function initSiteSwitch(domain) {
  815. // TODO:
  816. // extra features:
  817. // - keep video time
  818. // - configurable hotkey
  819. document.addEventListener("keydown", (e) => {
  820. if (e.key === "F9")
  821. switchSite(domain === "yt" ? "ytm" : "yt");
  822. });
  823. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.log)("Initialized site switch listener");
  824. }
  825. /** Switches to the other site (between YT and YTM) */
  826. function switchSite(newDomain) {
  827. var _a;
  828. try {
  829. let subdomain;
  830. if (newDomain === "ytm")
  831. subdomain = "music";
  832. else if (newDomain === "yt")
  833. subdomain = "www";
  834. if (!subdomain)
  835. throw new Error(`Unrecognized domain '${newDomain}'`);
  836. const { pathname, search, hash } = new URL(location.href);
  837. const vt = (_a = (0,_utils__WEBPACK_IMPORTED_MODULE_0__.getVideoTime)()) !== null && _a !== void 0 ? _a : 0;
  838. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.log)(`Found video time of ${vt} seconds`);
  839. const newSearch = search.includes("?") ? `${search}&t=${vt}` : `?t=${vt}`;
  840. const url = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
  841. console.info(`BetterYTM - switching to domain '${newDomain}' at ${url}`);
  842. disableBeforeUnload();
  843. location.assign(url);
  844. }
  845. catch (err) {
  846. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.error)("Error while switching site:", err);
  847. }
  848. }
  849. //#MARKER beforeunload popup
  850. let beforeUnloadEnabled = true;
  851. /** Disables the popup before leaving the site */
  852. function disableBeforeUnload() {
  853. beforeUnloadEnabled = false;
  854. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.info)("Disabled popup before leaving the site");
  855. }
  856. /** (Re-)enables the popup before leaving the site */
  857. function enableBeforeUnload() {
  858. beforeUnloadEnabled = true;
  859. (0,_utils__WEBPACK_IMPORTED_MODULE_0__.info)("Enabled popup before leaving the site");
  860. }
  861. /**
  862. * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
  863. * event listeners before they can be attached by the site.
  864. */
  865. function initBeforeUnloadHook() {
  866. Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
  867. (function (original) {
  868. // @ts-ignore
  869. window.__proto__.addEventListener = function (...args) {
  870. if (beforeUnloadEnabled && args[0] === "beforeunload")
  871. return (0,_utils__WEBPACK_IMPORTED_MODULE_0__.log)("Prevented beforeunload event listener from attaching");
  872. else
  873. return original.apply(this, args);
  874. };
  875. // @ts-ignore
  876. })(window.__proto__.addEventListener);
  877. (0,_config__WEBPACK_IMPORTED_MODULE_1__.getFeatures)().then(feats => {
  878. if (feats.disableBeforeUnloadPopup)
  879. disableBeforeUnload();
  880. });
  881. }
  882. /***/ }),
  883. /***/ "./src/features/layout.ts":
  884. /*!********************************!*\
  885. !*** ./src/features/layout.ts ***!
  886. \********************************/
  887. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  888. __webpack_require__.r(__webpack_exports__);
  889. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  890. /* harmony export */ addAnchorImprovements: function() { return /* binding */ addAnchorImprovements; },
  891. /* harmony export */ addWatermark: function() { return /* binding */ addWatermark; },
  892. /* harmony export */ initQueueButtons: function() { return /* binding */ initQueueButtons; },
  893. /* harmony export */ preInitLayout: function() { return /* binding */ preInitLayout; },
  894. /* harmony export */ removeUpgradeTab: function() { return /* binding */ removeUpgradeTab; },
  895. /* harmony export */ setVolSliderSize: function() { return /* binding */ setVolSliderSize; },
  896. /* harmony export */ setVolSliderStep: function() { return /* binding */ setVolSliderStep; }
  897. /* harmony export */ });
  898. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
  899. /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../config */ "./src/config.ts");
  900. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
  901. /* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../events */ "./src/events.ts");
  902. /* harmony import */ var _menu_menu_old__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu/menu_old */ "./src/features/menu/menu_old.ts");
  903. /* harmony import */ var _lyrics__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./lyrics */ "./src/features/lyrics.ts");
  904. /* harmony import */ var _layout_css__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./layout.css */ "./src/features/layout.css");
  905. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  906. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  907. return new (P || (P = Promise))(function (resolve, reject) {
  908. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  909. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  910. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  911. step((generator = generator.apply(thisArg, _arguments || [])).next());
  912. });
  913. };
  914. let features;
  915. function preInitLayout() {
  916. return __awaiter(this, void 0, void 0, function* () {
  917. features = yield (0,_config__WEBPACK_IMPORTED_MODULE_1__.getFeatures)();
  918. });
  919. }
  920. //#MARKER watermark
  921. /** Adds a watermark beneath the logo */
  922. function addWatermark() {
  923. const watermark = document.createElement("a");
  924. watermark.role = "button";
  925. watermark.id = "betterytm-watermark";
  926. watermark.className = "style-scope ytmusic-nav-bar";
  927. watermark.innerText = _constants__WEBPACK_IMPORTED_MODULE_0__.scriptInfo.name;
  928. watermark.title = "Open menu";
  929. watermark.tabIndex = 1000;
  930. watermark.addEventListener("click", () => (0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_4__.openMenu)());
  931. // when using the tab key to navigate
  932. watermark.addEventListener("keydown", (e) => e.key === "Enter" && (0,_menu_menu_old__WEBPACK_IMPORTED_MODULE_4__.openMenu)());
  933. const logoElem = document.querySelector("#left-content");
  934. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.insertAfter)(logoElem, watermark);
  935. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)("Added watermark element:", watermark);
  936. }
  937. //#MARKER remove upgrade tab
  938. let removeUpgradeTries = 0;
  939. /** Removes the "Upgrade" / YT Music Premium tab from the title / nav bar */
  940. function removeUpgradeTab() {
  941. const tabElem = document.querySelector(".ytmusic-nav-bar ytmusic-pivot-bar-item-renderer[tab-id=\"SPunlimited\"]");
  942. if (tabElem) {
  943. tabElem.remove();
  944. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Removed upgrade tab after ${removeUpgradeTries} tries`);
  945. }
  946. else if (removeUpgradeTries < _constants__WEBPACK_IMPORTED_MODULE_0__.triesLimit) {
  947. setTimeout(removeUpgradeTab, _constants__WEBPACK_IMPORTED_MODULE_0__.triesInterval); // TODO: improve this
  948. removeUpgradeTries++;
  949. }
  950. else
  951. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)(`Couldn't find upgrade tab to remove after ${removeUpgradeTries} tries`);
  952. }
  953. //#MARKER volume slider
  954. /** Sets the volume slider to a set size */
  955. function setVolSliderSize() {
  956. const { volumeSliderSize: size } = features;
  957. if (typeof size !== "number" || isNaN(Number(size)))
  958. return;
  959. const style = `\
  960. .volume-slider.ytmusic-player-bar, .expand-volume-slider.ytmusic-player-bar {
  961. width: ${size}px !important;
  962. }`;
  963. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.addGlobalStyle)(style, "vol-slider");
  964. }
  965. /** Sets the `step` attribute of the volume slider */
  966. function setVolSliderStep() {
  967. const sliderElem = document.querySelector("tp-yt-paper-slider#volume-slider");
  968. sliderElem.setAttribute("step", String(features.volumeSliderStep));
  969. }
  970. //#MARKER queue buttons
  971. // TODO: account for the fact initially the elements might not exist, if the site was not opened directly with a video playing or via the /watch path
  972. function initQueueButtons() {
  973. const addQueueBtns = (evt) => {
  974. let amt = 0;
  975. for (const queueItm of (0,_events__WEBPACK_IMPORTED_MODULE_3__.getEvtData)(evt).childNodes) {
  976. if (!queueItm.classList.contains("bytm-has-queue-btns")) {
  977. addQueueButtons(queueItm);
  978. amt++;
  979. }
  980. }
  981. if (amt > 0)
  982. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Added buttons to ${amt} new queue ${(0,_utils__WEBPACK_IMPORTED_MODULE_2__.autoPlural)("item", amt)}`);
  983. };
  984. _events__WEBPACK_IMPORTED_MODULE_3__.siteEvents.on("queueChanged", addQueueBtns);
  985. _events__WEBPACK_IMPORTED_MODULE_3__.siteEvents.on("autoplayQueueChanged", addQueueBtns);
  986. const queueItems = document.querySelectorAll("#contents.ytmusic-player-queue > ytmusic-player-queue-item");
  987. if (queueItems.length === 0)
  988. return;
  989. queueItems.forEach(itm => addQueueButtons(itm));
  990. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Added buttons to ${queueItems.length} existing queue items`);
  991. }
  992. /**
  993. * Adds the buttons to each item in the current song queue.
  994. * Also observes for changes to add new buttons to new items in the queue.
  995. * @param queueItem The element with tagname `ytmusic-player-queue-item` to add queue buttons to
  996. */
  997. function addQueueButtons(queueItem) {
  998. return __awaiter(this, void 0, void 0, function* () {
  999. //#SECTION general queue item stuff
  1000. const queueBtnsCont = document.createElement("div");
  1001. queueBtnsCont.className = "bytm-queue-btn-container";
  1002. const songInfo = queueItem.querySelector(".song-info");
  1003. if (!songInfo)
  1004. return false;
  1005. const [songEl, artistEl] = songInfo.querySelectorAll("yt-formatted-string");
  1006. const song = songEl.innerText;
  1007. const artist = artistEl.innerText;
  1008. if (!song || !artist)
  1009. return false;
  1010. //#SECTION lyrics btn
  1011. const lyricsBtnElem = (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.createLyricsBtn)(undefined, false);
  1012. {
  1013. lyricsBtnElem.title = "Open this song's lyrics in a new tab";
  1014. lyricsBtnElem.style.display = "inline-flex";
  1015. lyricsBtnElem.style.visibility = "initial";
  1016. lyricsBtnElem.style.pointerEvents = "initial";
  1017. lyricsBtnElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
  1018. let lyricsUrl;
  1019. const artistsSan = (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.sanitizeArtists)(artist);
  1020. const songSan = (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.sanitizeSong)(song);
  1021. const cachedLyricsUrl = (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.getLyricsCacheEntry)(artistsSan, songSan);
  1022. if (cachedLyricsUrl)
  1023. lyricsUrl = cachedLyricsUrl;
  1024. else if (!songInfo.hasAttribute("data-bytm-loading")) {
  1025. if (!cachedLyricsUrl)
  1026. songInfo.setAttribute("data-bytm-loading", "");
  1027. const imgEl = lyricsBtnElem.querySelector("img");
  1028. imgEl.src = (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getAssetUrl)("loading.gif");
  1029. lyricsUrl = cachedLyricsUrl !== null && cachedLyricsUrl !== void 0 ? cachedLyricsUrl : yield (0,_lyrics__WEBPACK_IMPORTED_MODULE_5__.getGeniusUrl)(artistsSan, songSan);
  1030. if (!cachedLyricsUrl)
  1031. songInfo.removeAttribute("data-bytm-loading");
  1032. imgEl.src = (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getAssetUrl)("external/genius.png");
  1033. if (!lyricsUrl) {
  1034. if (confirm("Couldn't find a lyrics page for this song.\nDo you want to open genius.com to manually search for it?"))
  1035. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.openInNewTab)("https://genius.com/search");
  1036. return;
  1037. }
  1038. }
  1039. lyricsUrl && (0,_utils__WEBPACK_IMPORTED_MODULE_2__.openInNewTab)(lyricsUrl);
  1040. }));
  1041. }
  1042. //#SECTION delete from queue btn
  1043. const deleteBtnElem = document.createElement("a");
  1044. {
  1045. Object.assign(deleteBtnElem, {
  1046. title: "Remove this song from the queue",
  1047. className: "ytmusic-player-bar bytm-delete-from-queue bytm-generic-btn",
  1048. role: "button",
  1049. target: "_blank",
  1050. rel: "noopener noreferrer",
  1051. });
  1052. deleteBtnElem.style.visibility = "initial";
  1053. deleteBtnElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
  1054. // container of the queue item popup menu - element gets reused for every queue item
  1055. let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
  1056. try {
  1057. // three dots button to open the popup menu of a queue item
  1058. const dotsBtnElem = queueItem.querySelector("ytmusic-menu-renderer yt-button-shape button");
  1059. if (queuePopupCont)
  1060. queuePopupCont.setAttribute("data-bytm-hidden", "true");
  1061. dotsBtnElem.click();
  1062. yield (0,_utils__WEBPACK_IMPORTED_MODULE_2__.pauseFor)(25);
  1063. queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
  1064. if (!queuePopupCont.hasAttribute("data-bytm-hidden"))
  1065. queuePopupCont.setAttribute("data-bytm-hidden", "true");
  1066. // a little bit janky and unreliable but the only way afaik
  1067. const removeFromQueueBtn = queuePopupCont.querySelector("tp-yt-paper-listbox *[role=option]:nth-child(7)");
  1068. yield (0,_utils__WEBPACK_IMPORTED_MODULE_2__.pauseFor)(20);
  1069. removeFromQueueBtn.click();
  1070. }
  1071. catch (err) {
  1072. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't remove song from queue due to error:", err);
  1073. }
  1074. finally {
  1075. queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.removeAttribute("data-bytm-hidden");
  1076. }
  1077. }));
  1078. const imgElem = document.createElement("img");
  1079. imgElem.className = "bytm-generic-btn-img";
  1080. imgElem.src = (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getAssetUrl)("close.png"); // TODO: make own icon for this
  1081. deleteBtnElem.appendChild(imgElem);
  1082. }
  1083. //#SECTION append elements to DOM
  1084. queueBtnsCont.appendChild(lyricsBtnElem);
  1085. queueBtnsCont.appendChild(deleteBtnElem);
  1086. songInfo.appendChild(queueBtnsCont);
  1087. queueItem.classList.add("bytm-has-queue-btns");
  1088. return true;
  1089. });
  1090. }
  1091. //#MARKER better clickable stuff
  1092. // TODO: account for the fact initially the elements might not exist, if the site was opened directly with the /watch path
  1093. function addAnchorImprovements() {
  1094. void 0;
  1095. }
  1096. /***/ }),
  1097. /***/ "./src/features/lyrics.ts":
  1098. /*!********************************!*\
  1099. !*** ./src/features/lyrics.ts ***!
  1100. \********************************/
  1101. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  1102. __webpack_require__.r(__webpack_exports__);
  1103. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  1104. /* harmony export */ addLyricsCacheEntry: function() { return /* binding */ addLyricsCacheEntry; },
  1105. /* harmony export */ addMediaCtrlLyricsBtn: function() { return /* binding */ addMediaCtrlLyricsBtn; },
  1106. /* harmony export */ createLyricsBtn: function() { return /* binding */ createLyricsBtn; },
  1107. /* harmony export */ geniUrlBase: function() { return /* binding */ geniUrlBase; },
  1108. /* harmony export */ getCurrentLyricsUrl: function() { return /* binding */ getCurrentLyricsUrl; },
  1109. /* harmony export */ getGeniusUrl: function() { return /* binding */ getGeniusUrl; },
  1110. /* harmony export */ getLyricsCacheEntry: function() { return /* binding */ getLyricsCacheEntry; },
  1111. /* harmony export */ sanitizeArtists: function() { return /* binding */ sanitizeArtists; },
  1112. /* harmony export */ sanitizeSong: function() { return /* binding */ sanitizeSong; }
  1113. /* harmony export */ });
  1114. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../constants */ "./src/constants.ts");
  1115. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../utils */ "./src/utils.ts");
  1116. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  1117. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  1118. return new (P || (P = Promise))(function (resolve, reject) {
  1119. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  1120. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  1121. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  1122. step((generator = generator.apply(thisArg, _arguments || [])).next());
  1123. });
  1124. };
  1125. var __asyncValues = (undefined && undefined.__asyncValues) || function (o) {
  1126. if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
  1127. var m = o[Symbol.asyncIterator], i;
  1128. 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);
  1129. 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); }); }; }
  1130. function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
  1131. };
  1132. /** Base URL of geniURL */
  1133. const geniUrlBase = "https://api.sv443.net/geniurl";
  1134. /** GeniURL endpoint that gives song metadata when provided with a `?q` or `?artist` and `?song` parameter - [more info](https://api.sv443.net/geniurl) */
  1135. const geniURLSearchTopUrl = `${geniUrlBase}/search/top`;
  1136. /**
  1137. * The threshold to pass to geniURL's fuzzy filtering.
  1138. * 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.
  1139. * Set to undefined to use the default.
  1140. */
  1141. const threshold = 0.55;
  1142. /** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
  1143. const geniUrlRatelimitTimeframe = 30;
  1144. const thresholdParam = threshold ? `&threshold=${(0,_utils__WEBPACK_IMPORTED_MODULE_1__.clamp)(threshold, 0, 1)}` : "";
  1145. //#MARKER cache
  1146. /** Cache with key format `ARTIST - SONG` (sanitized) and lyrics URLs as values. Used to prevent extraneous requests to geniURL. */
  1147. const lyricsUrlCache = new Map();
  1148. /** How many cache entries can exist at a time - this is used to cap memory usage */
  1149. const maxLyricsCacheSize = 100;
  1150. // TODO: implement this
  1151. /**
  1152. * Returns the lyrics URL from the passed un-/sanitized artist and song name, or undefined if the entry doesn't exist yet.
  1153. * **The passed parameters need to be sanitized first!**
  1154. */
  1155. function getLyricsCacheEntry(artists, song) {
  1156. return lyricsUrlCache.get(`${artists} - ${song}`);
  1157. }
  1158. /** Adds the provided entry into the lyrics URL cache */
  1159. function addLyricsCacheEntry(artists, song, lyricsUrl) {
  1160. lyricsUrlCache.set(`${sanitizeArtists(artists)} - ${sanitizeSong(song)}`, lyricsUrl);
  1161. // delete oldest entry if cache gets too big
  1162. if (lyricsUrlCache.size > maxLyricsCacheSize)
  1163. lyricsUrlCache.delete([...lyricsUrlCache.keys()].at(-1));
  1164. }
  1165. //#MARKER media control bar
  1166. let mcCurrentSongTitle = "";
  1167. let mcLyricsButtonAddTries = 0;
  1168. /** Adds a lyrics button to the media controls bar */
  1169. function addMediaCtrlLyricsBtn() {
  1170. const likeContainer = document.querySelector(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer");
  1171. if (!likeContainer) {
  1172. mcLyricsButtonAddTries++;
  1173. if (mcLyricsButtonAddTries < _constants__WEBPACK_IMPORTED_MODULE_0__.triesLimit) {
  1174. setTimeout(addMediaCtrlLyricsBtn, _constants__WEBPACK_IMPORTED_MODULE_0__.triesInterval); // TODO: improve this
  1175. return;
  1176. }
  1177. return (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)(`Couldn't find element to append lyrics buttons to after ${mcLyricsButtonAddTries} tries`);
  1178. }
  1179. const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
  1180. // run parallel without awaiting so the MutationObserver below can observe the title element in time
  1181. (() => __awaiter(this, void 0, void 0, function* () {
  1182. const gUrl = yield getCurrentLyricsUrl();
  1183. const linkElem = createLyricsBtn(gUrl !== null && gUrl !== void 0 ? gUrl : undefined);
  1184. linkElem.id = "betterytm-lyrics-button";
  1185. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Inserted lyrics button after ${mcLyricsButtonAddTries} tries:`, linkElem);
  1186. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.insertAfter)(likeContainer, linkElem);
  1187. }))();
  1188. mcCurrentSongTitle = songTitleElem.title;
  1189. const onMutation = (mutations) => { var _a, mutations_1, mutations_1_1; return __awaiter(this, void 0, void 0, function* () {
  1190. var _b, e_1, _c, _d;
  1191. try {
  1192. for (_a = true, mutations_1 = __asyncValues(mutations); mutations_1_1 = yield mutations_1.next(), _b = mutations_1_1.done, !_b;) {
  1193. _d = mutations_1_1.value;
  1194. _a = false;
  1195. try {
  1196. const mut = _d;
  1197. const newTitle = mut.target.title;
  1198. if (newTitle !== mcCurrentSongTitle && newTitle.length > 0) {
  1199. const lyricsBtn = document.querySelector("#betterytm-lyrics-button");
  1200. if (!lyricsBtn)
  1201. return;
  1202. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Song title changed from '${mcCurrentSongTitle}' to '${newTitle}'`);
  1203. lyricsBtn.style.cursor = "wait";
  1204. lyricsBtn.style.pointerEvents = "none";
  1205. mcCurrentSongTitle = newTitle;
  1206. const url = yield getCurrentLyricsUrl(); // can take a second or two
  1207. if (!url)
  1208. continue;
  1209. lyricsBtn.href = url;
  1210. lyricsBtn.title = "Open the current song's lyrics in a new tab";
  1211. lyricsBtn.style.cursor = "pointer";
  1212. lyricsBtn.style.visibility = "initial";
  1213. lyricsBtn.style.display = "inline-flex";
  1214. lyricsBtn.style.pointerEvents = "initial";
  1215. }
  1216. }
  1217. finally {
  1218. _a = true;
  1219. }
  1220. }
  1221. }
  1222. catch (e_1_1) { e_1 = { error: e_1_1 }; }
  1223. finally {
  1224. try {
  1225. if (!_a && !_b && (_c = mutations_1.return)) yield _c.call(mutations_1);
  1226. }
  1227. finally { if (e_1) throw e_1.error; }
  1228. }
  1229. }); };
  1230. // 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
  1231. const obs = new MutationObserver(onMutation);
  1232. obs.observe(songTitleElem, { attributes: true, attributeFilter: ["title"] });
  1233. }
  1234. //#MARKER utils
  1235. /** Removes everything in parentheses from the passed song name */
  1236. function sanitizeSong(songName) {
  1237. const parensRegex = /\(.+\)/gmi;
  1238. const squareParensRegex = /\[.+\]/gmi;
  1239. // trim right after the song name:
  1240. const sanitized = songName
  1241. .replace(parensRegex, "")
  1242. .replace(squareParensRegex, "");
  1243. return sanitized.trim();
  1244. }
  1245. /** Removes the secondary artist (if it exists) from the passed artists string */
  1246. function sanitizeArtists(artists) {
  1247. artists = artists.split(/\s*\u2022\s*/gmiu)[0]; // split at &bull; [•] character
  1248. if (artists.match(/&/))
  1249. artists = artists.split(/\s*&\s*/gm)[0];
  1250. if (artists.match(/,/))
  1251. artists = artists.split(/,\s*/gm)[0];
  1252. return artists.trim();
  1253. }
  1254. /** Returns the lyrics URL from genius for the currently selected song */
  1255. function getCurrentLyricsUrl() {
  1256. var _a;
  1257. return __awaiter(this, void 0, void 0, function* () {
  1258. try {
  1259. // In videos the video title contains both artist and song title, in "regular" YTM songs, the video title only contains the song title
  1260. const isVideo = typeof ((_a = document.querySelector("ytmusic-player")) === null || _a === void 0 ? void 0 : _a.getAttribute("video-mode_")) === "string";
  1261. const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
  1262. const songMetaElem = document.querySelector("span.subtitle > yt-formatted-string:first-child");
  1263. if (!songTitleElem || !songMetaElem || !songTitleElem.title)
  1264. return null;
  1265. const songNameRaw = songTitleElem.title;
  1266. const songName = sanitizeSong(songNameRaw);
  1267. const artistName = sanitizeArtists(songMetaElem.title);
  1268. /** Use when the current song is not a "real YTM song" with a static background, but rather a music video */
  1269. const getGeniusUrlVideo = () => __awaiter(this, void 0, void 0, function* () {
  1270. if (!songName.includes("-")) // for some fucking reason some music videos have YTM-like song title and artist separation, some don't
  1271. return yield getGeniusUrl(artistName, songName);
  1272. const [artist, ...rest] = songName.split("-").map(v => v.trim());
  1273. return yield getGeniusUrl(artist, rest.join(" "));
  1274. });
  1275. // TODO: artist might need further splitting before comma or ampersand
  1276. const url = isVideo ? yield getGeniusUrlVideo() : yield getGeniusUrl(artistName, songName);
  1277. return url;
  1278. }
  1279. catch (err) {
  1280. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't resolve lyrics URL:", err);
  1281. return null;
  1282. }
  1283. });
  1284. }
  1285. /** Fetches the actual lyrics URL from geniURL - **the passed parameters need to be sanitized first!** */
  1286. function getGeniusUrl(artist, song) {
  1287. var _a, _b, _c;
  1288. return __awaiter(this, void 0, void 0, function* () {
  1289. try {
  1290. const cacheEntry = getLyricsCacheEntry(artist, song);
  1291. if (cacheEntry) {
  1292. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Found lyrics URL in cache: ${cacheEntry}`);
  1293. return cacheEntry;
  1294. }
  1295. const startTs = Date.now();
  1296. const fetchUrl = `${geniURLSearchTopUrl}?artist=${encodeURIComponent(artist)}&song=${encodeURIComponent(song)}${thresholdParam}`;
  1297. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.log)(`Requesting URL from geniURL at '${fetchUrl}'`);
  1298. const fetchRes = yield fetch(fetchUrl);
  1299. if (fetchRes.status === 429) {
  1300. 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.`);
  1301. return undefined;
  1302. }
  1303. else if (fetchRes.status < 200 || fetchRes.status >= 300) {
  1304. (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)"}`);
  1305. return undefined;
  1306. }
  1307. const result = yield fetchRes.json();
  1308. if (typeof result === "object" && result.error) {
  1309. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't fetch lyrics URL:", result.message);
  1310. return undefined;
  1311. }
  1312. const url = result.url;
  1313. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.info)(`Found lyrics URL (after ${Date.now() - startTs}ms): ${url}`);
  1314. addLyricsCacheEntry(artist, song, url);
  1315. return url;
  1316. }
  1317. catch (err) {
  1318. (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)("Couldn't get lyrics URL due to error:", err);
  1319. return undefined;
  1320. }
  1321. });
  1322. }
  1323. /** Creates the base lyrics button element */
  1324. function createLyricsBtn(geniusUrl, hideIfLoading = true) {
  1325. const linkElem = document.createElement("a");
  1326. linkElem.className = "ytmusic-player-bar bytm-generic-btn";
  1327. linkElem.title = geniusUrl ? "Click to open this song's lyrics in a new tab" : "Loading lyrics URL...";
  1328. if (geniusUrl)
  1329. linkElem.href = geniusUrl;
  1330. linkElem.role = "button";
  1331. linkElem.target = "_blank";
  1332. linkElem.rel = "noopener noreferrer";
  1333. linkElem.style.visibility = hideIfLoading && geniusUrl ? "initial" : "hidden";
  1334. linkElem.style.display = hideIfLoading && geniusUrl ? "inline-flex" : "none";
  1335. const imgElem = document.createElement("img");
  1336. imgElem.className = "bytm-generic-btn-img";
  1337. imgElem.src = (0,_utils__WEBPACK_IMPORTED_MODULE_1__.getAssetUrl)("external/genius.png");
  1338. linkElem.appendChild(imgElem);
  1339. return linkElem;
  1340. }
  1341. /***/ }),
  1342. /***/ "./src/features/menu/menu.ts":
  1343. /*!***********************************!*\
  1344. !*** ./src/features/menu/menu.ts ***!
  1345. \***********************************/
  1346. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  1347. __webpack_require__.r(__webpack_exports__);
  1348. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  1349. /* harmony export */ closeMenu: function() { return /* binding */ closeMenu; },
  1350. /* harmony export */ initMenu: function() { return /* binding */ initMenu; },
  1351. /* harmony export */ openMenu: function() { return /* binding */ openMenu; },
  1352. /* harmony export */ setActiveTab: function() { return /* binding */ setActiveTab; }
  1353. /* harmony export */ });
  1354. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../constants */ "./src/constants.ts");
  1355. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../utils */ "./src/utils.ts");
  1356. /* harmony import */ var _changelog_md__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../changelog.md */ "./changelog.md");
  1357. /* harmony import */ var _menu_html__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./menu.html */ "./src/features/menu/menu.html");
  1358. /* harmony import */ var _menu_css__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu.css */ "./src/features/menu/menu.css");
  1359. //#MARKER menu
  1360. /**
  1361. * These are the base selector values for the menu tabs
  1362. * Header selector format: `#${baseValue}-header`
  1363. * Content selector format: `#${baseValue}-content`
  1364. */
  1365. const tabsSelectors = {
  1366. options: "bytm-menu-tab-options",
  1367. info: "bytm-menu-tab-info",
  1368. changelog: "bytm-menu-tab-changelog",
  1369. };
  1370. function initMenu() {
  1371. document.addEventListener("DOMContentLoaded", () => {
  1372. // create menu container
  1373. const menuContainer = document.createElement("div");
  1374. menuContainer.id = "bytm-menu-container";
  1375. // add menu html
  1376. menuContainer.innerHTML = _menu_html__WEBPACK_IMPORTED_MODULE_3__["default"];
  1377. document.body.appendChild(menuContainer);
  1378. initMenuContents();
  1379. });
  1380. }
  1381. let menuContTries = 0;
  1382. function initMenuContents() {
  1383. var _a;
  1384. if (!document.querySelector("#bytm-menu-dialog"))
  1385. return menuContTries++ < _constants__WEBPACK_IMPORTED_MODULE_0__.triesLimit
  1386. ? setTimeout(initMenuContents, _constants__WEBPACK_IMPORTED_MODULE_0__.triesInterval)
  1387. : (0,_utils__WEBPACK_IMPORTED_MODULE_1__.error)(`couldn't create menu element after ${_constants__WEBPACK_IMPORTED_MODULE_0__.triesLimit} tries.`);
  1388. // hook events
  1389. for (const tab in tabsSelectors) {
  1390. const selector = tabsSelectors[tab];
  1391. (_a = document.querySelector(`#${selector}-header`)) === null || _a === void 0 ? void 0 : _a.addEventListener("click", () => {
  1392. setActiveTab(tab);
  1393. });
  1394. }
  1395. // init tab contents
  1396. initOptionsContent();
  1397. initInfoContent();
  1398. initChangelogContent();
  1399. }
  1400. /** Opens the specified tab */
  1401. function setActiveTab(tab) {
  1402. const tabs = Object.assign({}, tabsSelectors);
  1403. delete tabs[tab];
  1404. // disable all but new active tab
  1405. for (const disableTab of Object.keys(tabs)) {
  1406. document.querySelector(`#${tabs[disableTab]}-header`).dataset.active = "false";
  1407. document.querySelector(`#${tabs[disableTab]}-content`).dataset.active = "false";
  1408. }
  1409. // enable new active tab
  1410. document.querySelector(`#${tabsSelectors[tab]}-header`).dataset.active = "true";
  1411. document.querySelector(`#${tabsSelectors[tab]}-content`).dataset.active = "true";
  1412. }
  1413. /** Opens the modal menu dialog */
  1414. function openMenu() {
  1415. document.querySelector("#bytm-menu-dialog").showModal();
  1416. }
  1417. /** Closes the modal menu dialog */
  1418. function closeMenu() {
  1419. document.querySelector("#bytm-menu-dialog").close();
  1420. }
  1421. //#MARKER menu tab contents
  1422. function initOptionsContent() {
  1423. const tab = document.querySelector("#bytm-menu-tab-options-content");
  1424. void tab;
  1425. }
  1426. function initInfoContent() {
  1427. const tab = document.querySelector("#bytm-menu-tab-info-content");
  1428. void tab;
  1429. }
  1430. function initChangelogContent() {
  1431. const tab = document.querySelector("#bytm-menu-tab-changelog-content");
  1432. tab.innerHTML = _changelog_md__WEBPACK_IMPORTED_MODULE_2__["default"];
  1433. }
  1434. /***/ }),
  1435. /***/ "./src/features/menu/menu_old.ts":
  1436. /*!***************************************!*\
  1437. !*** ./src/features/menu/menu_old.ts ***!
  1438. \***************************************/
  1439. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  1440. __webpack_require__.r(__webpack_exports__);
  1441. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  1442. /* harmony export */ addMenu: function() { return /* binding */ addMenu; },
  1443. /* harmony export */ closeMenu: function() { return /* binding */ closeMenu; },
  1444. /* harmony export */ openMenu: function() { return /* binding */ openMenu; }
  1445. /* harmony export */ });
  1446. /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../config */ "./src/config.ts");
  1447. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../constants */ "./src/constants.ts");
  1448. /* harmony import */ var _index__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../index */ "./src/features/index.ts");
  1449. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../../utils */ "./src/utils.ts");
  1450. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  1451. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  1452. return new (P || (P = Promise))(function (resolve, reject) {
  1453. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  1454. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  1455. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  1456. step((generator = generator.apply(thisArg, _arguments || [])).next());
  1457. });
  1458. };
  1459. //#MARKER menu
  1460. /**
  1461. * Adds an element to open the BetterYTM menu
  1462. * @deprecated TODO: replace in v1.1.0 - see https://github.com/Sv443/BetterYTM/issues/23
  1463. */
  1464. function addMenu() {
  1465. var _a, _b;
  1466. return __awaiter(this, void 0, void 0, function* () {
  1467. // bg & menu
  1468. const backgroundElem = document.createElement("div");
  1469. backgroundElem.id = "betterytm-menu-bg";
  1470. backgroundElem.title = "Click here to close the menu";
  1471. backgroundElem.style.visibility = "hidden";
  1472. backgroundElem.style.display = "none";
  1473. backgroundElem.addEventListener("click", (e) => {
  1474. var _a;
  1475. if (((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "betterytm-menu-bg")
  1476. closeMenu();
  1477. });
  1478. document.body.addEventListener("keydown", ({ key }) => {
  1479. if (key === "Escape")
  1480. closeMenu();
  1481. });
  1482. const menuContainer = document.createElement("div");
  1483. menuContainer.title = "";
  1484. menuContainer.id = "betterytm-menu";
  1485. menuContainer.style.borderRadius = "15px";
  1486. menuContainer.style.display = "flex";
  1487. menuContainer.style.flexDirection = "column";
  1488. menuContainer.style.justifyContent = "space-between";
  1489. // title
  1490. const titleCont = document.createElement("div");
  1491. titleCont.style.padding = "8px 20px 15px 8px";
  1492. titleCont.style.display = "flex";
  1493. titleCont.style.justifyContent = "space-between";
  1494. titleCont.id = "betterytm-menu-titlecont";
  1495. const titleElem = document.createElement("h2");
  1496. titleElem.id = "betterytm-menu-title";
  1497. titleElem.innerText = `${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name} - Configuration`;
  1498. const linksCont = document.createElement("div");
  1499. linksCont.id = "betterytm-menu-linkscont";
  1500. const addLink = (imgSrc, href, title) => {
  1501. const anchorElem = document.createElement("a");
  1502. anchorElem.className = "betterytm-menu-link";
  1503. anchorElem.rel = "noopener noreferrer";
  1504. anchorElem.target = "_blank";
  1505. anchorElem.href = href;
  1506. anchorElem.title = title;
  1507. anchorElem.style.marginLeft = "10px";
  1508. const imgElem = document.createElement("img");
  1509. imgElem.className = "betterytm-menu-img";
  1510. imgElem.src = imgSrc;
  1511. imgElem.style.width = "32px";
  1512. imgElem.style.height = "32px";
  1513. anchorElem.appendChild(imgElem);
  1514. linksCont.appendChild(anchorElem);
  1515. };
  1516. addLink((0,_utils__WEBPACK_IMPORTED_MODULE_3__.getAssetUrl)("external/github.png"), _constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.namespace, `${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name} on GitHub`);
  1517. addLink((0,_utils__WEBPACK_IMPORTED_MODULE_3__.getAssetUrl)("external/greasyfork.png"), "https://greasyfork.org/xyz", `${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name} on GreasyFork`);
  1518. const closeElem = document.createElement("img");
  1519. closeElem.id = "betterytm-menu-close";
  1520. closeElem.src = (0,_utils__WEBPACK_IMPORTED_MODULE_3__.getAssetUrl)("close.png");
  1521. closeElem.title = "Click to close the menu";
  1522. closeElem.style.marginLeft = "50px";
  1523. closeElem.style.width = "32px";
  1524. closeElem.style.height = "32px";
  1525. closeElem.addEventListener("click", closeMenu);
  1526. linksCont.appendChild(closeElem);
  1527. titleCont.appendChild(titleElem);
  1528. titleCont.appendChild(linksCont);
  1529. // TODO: features
  1530. const featuresCont = document.createElement("div");
  1531. featuresCont.id = "betterytm-menu-opts";
  1532. featuresCont.style.display = "flex";
  1533. featuresCont.style.flexDirection = "column";
  1534. featuresCont.style.overflowY = "auto";
  1535. /** Gets called whenever the feature config is changed */
  1536. const confChanged = (key, initialVal, newVal) => __awaiter(this, void 0, void 0, function* () {
  1537. const fmt = (val) => typeof val === "object" ? JSON.stringify(val) : String(val);
  1538. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.info)(`Feature config changed, key '${key}' from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
  1539. const featConf = Object.assign({}, yield (0,_config__WEBPACK_IMPORTED_MODULE_0__.getFeatures)());
  1540. featConf[key] = newVal;
  1541. yield (0,_config__WEBPACK_IMPORTED_MODULE_0__.saveFeatureConf)(featConf);
  1542. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Saved feature config changes:\n", yield GM.getValue("betterytm-config"));
  1543. });
  1544. const features = yield (0,_config__WEBPACK_IMPORTED_MODULE_0__.getFeatures)();
  1545. for (const key in features) {
  1546. const ftInfo = _index__WEBPACK_IMPORTED_MODULE_2__.featInfo[key];
  1547. // @ts-ignore
  1548. if (!ftInfo || ftInfo.visible === false)
  1549. continue;
  1550. const { desc, type, default: ftDefault } = ftInfo;
  1551. // @ts-ignore
  1552. const step = (_a = ftInfo === null || ftInfo === void 0 ? void 0 : ftInfo.step) !== null && _a !== void 0 ? _a : undefined;
  1553. const val = features[key];
  1554. const initialVal = (_b = val !== null && val !== void 0 ? val : ftDefault) !== null && _b !== void 0 ? _b : undefined;
  1555. const ftConfElem = document.createElement("div");
  1556. ftConfElem.id = `betterytm-ftconf-${key}`;
  1557. ftConfElem.style.display = "flex";
  1558. ftConfElem.style.flexDirection = "row";
  1559. ftConfElem.style.justifyContent = "space-between";
  1560. ftConfElem.style.padding = "8px 20px";
  1561. {
  1562. const textElem = document.createElement("span");
  1563. textElem.style.display = "inline-block";
  1564. textElem.style.fontSize = "15px";
  1565. textElem.innerText = desc;
  1566. ftConfElem.appendChild(textElem);
  1567. }
  1568. {
  1569. let inputType = "text";
  1570. switch (type) {
  1571. case "toggle":
  1572. inputType = "checkbox";
  1573. break;
  1574. case "slider":
  1575. inputType = "range";
  1576. break;
  1577. case "number":
  1578. inputType = "number";
  1579. break;
  1580. }
  1581. const inputElemId = `betterytm-ftconf-${key}-input`;
  1582. const ctrlElem = document.createElement("span");
  1583. ctrlElem.style.display = "inline-block";
  1584. ctrlElem.style.whiteSpace = "nowrap";
  1585. const inputElem = document.createElement("input");
  1586. inputElem.id = inputElemId;
  1587. inputElem.style.marginRight = "37px";
  1588. inputElem.type = inputType;
  1589. if (type === "toggle")
  1590. inputElem.style.marginLeft = "5px";
  1591. if (typeof initialVal !== "undefined")
  1592. inputElem.value = String(initialVal);
  1593. if (type === "number" && step)
  1594. inputElem.step = step;
  1595. // @ts-ignore
  1596. if (ftInfo.min && ftInfo.max) {
  1597. // @ts-ignore
  1598. inputElem.min = ftInfo.min;
  1599. // @ts-ignore
  1600. inputElem.max = ftInfo.max;
  1601. }
  1602. if (type === "toggle" && typeof initialVal !== "undefined")
  1603. inputElem.checked = Boolean(initialVal);
  1604. // @ts-ignore
  1605. const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
  1606. const fmtVal = (v) => String(v).trim();
  1607. const toggleLabelText = (toggled) => toggled ? "On" : "Off";
  1608. let labelElem;
  1609. if (type === "slider") {
  1610. labelElem = document.createElement("label");
  1611. labelElem.classList.add("betterytm-ftconf-label");
  1612. labelElem.style.marginRight = "20px";
  1613. labelElem.style.fontSize = "16px";
  1614. labelElem.htmlFor = inputElemId;
  1615. labelElem.innerText = fmtVal(initialVal) + unitTxt;
  1616. inputElem.addEventListener("input", () => {
  1617. if (labelElem)
  1618. labelElem.innerText = fmtVal(parseInt(inputElem.value)) + unitTxt;
  1619. });
  1620. }
  1621. else if (type === "toggle") {
  1622. labelElem = document.createElement("label");
  1623. labelElem.classList.add("betterytm-ftconf-label");
  1624. labelElem.style.paddingLeft = "10px";
  1625. labelElem.style.paddingRight = "5px";
  1626. labelElem.style.fontSize = "16px";
  1627. labelElem.htmlFor = inputElemId;
  1628. labelElem.innerText = toggleLabelText(Boolean(initialVal)) + unitTxt;
  1629. inputElem.addEventListener("input", () => {
  1630. if (labelElem)
  1631. labelElem.innerText = toggleLabelText(inputElem.checked) + unitTxt;
  1632. });
  1633. }
  1634. inputElem.addEventListener("input", () => {
  1635. let v = parseInt(inputElem.value);
  1636. if (isNaN(v))
  1637. v = Number(inputElem.value);
  1638. if (typeof initialVal !== "undefined")
  1639. confChanged(key, initialVal, (type !== "toggle" ? v : inputElem.checked));
  1640. });
  1641. const resetElem = document.createElement("button");
  1642. resetElem.innerText = "Reset";
  1643. resetElem.addEventListener("click", () => {
  1644. inputElem[type !== "toggle" ? "value" : "checked"] = ftDefault;
  1645. if (labelElem) {
  1646. if (type === "toggle")
  1647. labelElem.innerText = toggleLabelText(inputElem.checked);
  1648. else
  1649. labelElem.innerText = fmtVal(parseInt(inputElem.value));
  1650. }
  1651. if (typeof initialVal !== "undefined")
  1652. confChanged(key, initialVal, ftDefault);
  1653. });
  1654. labelElem && ctrlElem.appendChild(labelElem);
  1655. ctrlElem.appendChild(inputElem);
  1656. ctrlElem.appendChild(resetElem);
  1657. ftConfElem.appendChild(ctrlElem);
  1658. }
  1659. featuresCont.appendChild(ftConfElem);
  1660. }
  1661. const footerElem = document.createElement("div");
  1662. footerElem.id = "betterytm-menu-footer";
  1663. footerElem.style.marginTop = "20px";
  1664. footerElem.style.fontSize = "17px";
  1665. footerElem.style.textDecoration = "underline";
  1666. footerElem.style.padding = "10px 20px";
  1667. footerElem.style.position = "sticky";
  1668. footerElem.style.backgroundColor = "var(--bytm-menu-bg)";
  1669. footerElem.style.bottom = "0";
  1670. footerElem.innerText = "You need to reload the page to apply changes.";
  1671. const reloadElem = document.createElement("button");
  1672. reloadElem.style.marginLeft = "20px";
  1673. reloadElem.innerText = "Reload now";
  1674. reloadElem.title = "Click to reload the page";
  1675. reloadElem.addEventListener("click", () => location.reload());
  1676. footerElem.appendChild(reloadElem);
  1677. featuresCont.appendChild(footerElem);
  1678. // finalize
  1679. const menuBody = document.createElement("div");
  1680. menuBody.id = "betterytm-menu-body";
  1681. menuBody.appendChild(titleCont);
  1682. menuBody.appendChild(featuresCont);
  1683. const versionCont = document.createElement("div");
  1684. versionCont.style.display = "flex";
  1685. versionCont.style.justifyContent = "space-around";
  1686. versionCont.style.fontSize = "1.15em";
  1687. versionCont.style.marginTop = "10px";
  1688. versionCont.style.marginBottom = "5px";
  1689. const versionElem = document.createElement("span");
  1690. versionElem.id = "betterytm-menu-version";
  1691. versionElem.innerText = `v${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.version}`;
  1692. versionCont.appendChild(versionElem);
  1693. featuresCont.appendChild(versionCont);
  1694. menuContainer.appendChild(menuBody);
  1695. menuContainer.appendChild(versionCont);
  1696. backgroundElem.appendChild(menuContainer);
  1697. document.body.appendChild(backgroundElem);
  1698. // add style
  1699. const menuStyle = `\
  1700. :root {
  1701. --bytm-menu-bg: #282828;
  1702. }
  1703. #betterytm-menu-bg {
  1704. display: block;
  1705. position: fixed;
  1706. width: 100vw;
  1707. height: 100vh;
  1708. top: 0;
  1709. left: 0;
  1710. z-index: 15;
  1711. background-color: rgba(0, 0, 0, 0.6);
  1712. }
  1713. /* TODO:FIXME: needs better positioning (vw and vh === "big no no") */
  1714. #betterytm-menu {
  1715. display: inline-block;
  1716. position: fixed;
  1717. width: 50vw;
  1718. height: auto;
  1719. left: 25vw;
  1720. top: 10vh;
  1721. z-index: 16;
  1722. padding: 8px;
  1723. color: #fff;
  1724. background-color: var(--bytm-menu-bg);
  1725. }
  1726. #betterytm-menu-opts {
  1727. max-height: 70vh;
  1728. overflow: auto;
  1729. }
  1730. #betterytm-menu-titlecont {
  1731. display: flex;
  1732. }
  1733. #betterytm-menu-title {
  1734. font-size: 20px;
  1735. margin-top: 5px;
  1736. margin-bottom: 8px;
  1737. }
  1738. #betterytm-menu-linkscont {
  1739. display: flex;
  1740. }
  1741. .betterytm-menu-link {
  1742. display: inline-block;
  1743. }
  1744. /*.betterytm-menu-img {
  1745. }*/
  1746. #betterytm-menu-close {
  1747. cursor: pointer;
  1748. }
  1749. .betterytm-ftconf-label {
  1750. user-select: none;
  1751. }`;
  1752. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.log)("Added menu elem:", backgroundElem);
  1753. (0,_utils__WEBPACK_IMPORTED_MODULE_3__.addGlobalStyle)(menuStyle, "menu");
  1754. });
  1755. }
  1756. function closeMenu() {
  1757. const menuBg = document.querySelector("#betterytm-menu-bg");
  1758. menuBg.style.visibility = "hidden";
  1759. menuBg.style.display = "none";
  1760. }
  1761. function openMenu() {
  1762. const menuBg = document.querySelector("#betterytm-menu-bg");
  1763. menuBg.style.visibility = "visible";
  1764. menuBg.style.display = "block";
  1765. }
  1766. /***/ }),
  1767. /***/ "./src/utils.ts":
  1768. /*!**********************!*\
  1769. !*** ./src/utils.ts ***!
  1770. \**********************/
  1771. /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
  1772. __webpack_require__.r(__webpack_exports__);
  1773. /* harmony export */ __webpack_require__.d(__webpack_exports__, {
  1774. /* harmony export */ addGlobalStyle: function() { return /* binding */ addGlobalStyle; },
  1775. /* harmony export */ autoPlural: function() { return /* binding */ autoPlural; },
  1776. /* harmony export */ clamp: function() { return /* binding */ clamp; },
  1777. /* harmony export */ dbg: function() { return /* binding */ dbg; },
  1778. /* harmony export */ error: function() { return /* binding */ error; },
  1779. /* harmony export */ getAssetUrl: function() { return /* binding */ getAssetUrl; },
  1780. /* harmony export */ getDomain: function() { return /* binding */ getDomain; },
  1781. /* harmony export */ getUnsafeWindow: function() { return /* binding */ getUnsafeWindow; },
  1782. /* harmony export */ getVideoTime: function() { return /* binding */ getVideoTime; },
  1783. /* harmony export */ info: function() { return /* binding */ info; },
  1784. /* harmony export */ insertAfter: function() { return /* binding */ insertAfter; },
  1785. /* harmony export */ log: function() { return /* binding */ log; },
  1786. /* harmony export */ openInNewTab: function() { return /* binding */ openInNewTab; },
  1787. /* harmony export */ pauseFor: function() { return /* binding */ pauseFor; },
  1788. /* harmony export */ setLogLevel: function() { return /* binding */ setLogLevel; },
  1789. /* harmony export */ warn: function() { return /* binding */ warn; }
  1790. /* harmony export */ });
  1791. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./constants */ "./src/constants.ts");
  1792. //#SECTION logging
  1793. let curLogLevel = 1;
  1794. /** Sets the current log level. 0 = Debug, 1 = Info */
  1795. function setLogLevel(level) {
  1796. curLogLevel = level;
  1797. }
  1798. /** 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 */
  1799. function getLogLevel(args) {
  1800. const minLogLvl = 0, maxLogLvl = 1;
  1801. if (typeof args.at(-1) === "number")
  1802. return clamp(args.splice(args.length - 1)[0], minLogLvl, maxLogLvl);
  1803. return 1;
  1804. }
  1805. /** Common prefix to be able to tell logged messages apart and filter them in devtools */
  1806. const consPrefix = `[${_constants__WEBPACK_IMPORTED_MODULE_0__.scriptInfo.name}]`;
  1807. const consPrefixDbg = `[${_constants__WEBPACK_IMPORTED_MODULE_0__.scriptInfo.name}/#DEBUG]`;
  1808. /**
  1809. * Logs string-compatible values to the console, as long as the log level is sufficient.
  1810. * @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.
  1811. */
  1812. function log(...args) {
  1813. if (curLogLevel <= getLogLevel(args))
  1814. console.log(consPrefix, ...args);
  1815. }
  1816. /**
  1817. * Logs string-compatible values to the console as info, as long as the log level is sufficient.
  1818. * @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.
  1819. */
  1820. function info(...args) {
  1821. if (curLogLevel <= getLogLevel(args))
  1822. console.info(consPrefix, ...args);
  1823. }
  1824. /**
  1825. * Logs string-compatible values to the console as a warning, as long as the log level is sufficient.
  1826. * @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.
  1827. */
  1828. function warn(...args) {
  1829. if (curLogLevel <= getLogLevel(args))
  1830. console.warn(consPrefix, ...args);
  1831. }
  1832. /** Logs string-compatible values to the console as an error, no matter the log level. */
  1833. function error(...args) {
  1834. console.error(consPrefix, ...args);
  1835. }
  1836. /** Logs string-compatible values to the console, intended for debugging only */
  1837. function dbg(...args) {
  1838. console.log(consPrefixDbg, ...args);
  1839. }
  1840. //#SECTION video time
  1841. /**
  1842. * Returns the current video time in seconds
  1843. * Dispatches mouse movement events in case the video time can't be estimated
  1844. * @returns Returns null if the video time is unavailable
  1845. */
  1846. function getVideoTime() {
  1847. const domain = getDomain();
  1848. try {
  1849. if (domain === "ytm") {
  1850. const pbEl = document.querySelector("#progress-bar");
  1851. return !isNaN(Number(pbEl.value)) ? Number(pbEl.value) : null;
  1852. }
  1853. else if (domain === "yt") {
  1854. // YT doesn't update the progress bar when it's hidden (YTM doesn't hide it) so TODO: come up with some solution here
  1855. // Possible solution:
  1856. // - Use MutationObserver to detect when attributes of progress bar (selector `div.ytp-progress-bar[role="slider"]`) change
  1857. // - Wait until the attribute increments, then save the value of `aria-valuenow` and the current system time to memory
  1858. // - When site switch hotkey is pressed, take saved `aria-valuenow` value and add the difference between saved system time and current system time
  1859. // - If no value is present, use the script from `dev/ytForceShowVideoTime.js` to simulate mouse movement to force the element to update
  1860. // - Subtract one or two seconds to make up for rounding errors
  1861. // - profit
  1862. // if(!ytCurrentVideoTime) {
  1863. // ytForceShowVideoTime();
  1864. // const videoTime = document.querySelector("#TODO")?.getAttribute("aria-valuenow") ?? null;
  1865. // }
  1866. void ytForceShowVideoTime;
  1867. return null;
  1868. }
  1869. return null;
  1870. }
  1871. catch (err) {
  1872. error("Couldn't get video time due to error:", err);
  1873. return null;
  1874. }
  1875. }
  1876. /** Sends events that force the video controls to become visible for about 3 seconds */
  1877. function ytForceShowVideoTime() {
  1878. const player = document.querySelector("#movie_player");
  1879. if (!player)
  1880. return false;
  1881. const defaultProps = {
  1882. // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
  1883. view: getUnsafeWindow(),
  1884. bubbles: true,
  1885. cancelable: false,
  1886. };
  1887. player.dispatchEvent(new MouseEvent("mouseenter", defaultProps));
  1888. const { x, y, width, height } = player.getBoundingClientRect();
  1889. const screenY = Math.round(y + height / 2);
  1890. const screenX = x + Math.min(50, Math.round(width / 3));
  1891. player.dispatchEvent(new MouseEvent("mousemove", Object.assign(Object.assign({}, defaultProps), { screenY,
  1892. screenX, movementX: 5, movementY: 0 })));
  1893. setTimeout(() => {
  1894. player.dispatchEvent(new MouseEvent("mouseleave", defaultProps));
  1895. }, 4000);
  1896. return true;
  1897. }
  1898. //#SECTION DOM
  1899. /**
  1900. * Inserts `afterNode` as a sibling just after the provided `beforeNode`
  1901. * @param beforeNode
  1902. * @param afterNode
  1903. * @returns Returns the `afterNode`
  1904. */
  1905. function insertAfter(beforeNode, afterNode) {
  1906. var _a;
  1907. (_a = beforeNode.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(afterNode, beforeNode.nextSibling);
  1908. return afterNode;
  1909. }
  1910. /**
  1911. * Adds global CSS style through a `<style>` element in the document's `<head>`
  1912. * @param style CSS string
  1913. * @param ref Reference name that is included in the `<style>`'s ID - prefixed with `betterytm-style-` - defaults to a random number if left undefined
  1914. */
  1915. function addGlobalStyle(style, ref) {
  1916. if (typeof ref !== "string" || ref.length === 0)
  1917. ref = String(Math.floor(Math.random() * 10000));
  1918. const styleElem = document.createElement("style");
  1919. styleElem.id = `betterytm-style-${ref}`;
  1920. styleElem.innerHTML = style;
  1921. document.head.appendChild(styleElem);
  1922. log(`Inserted global style with ref '${ref}':`, styleElem);
  1923. }
  1924. //#SECTION misc
  1925. /**
  1926. * Creates an invisible anchor with _blank target and clicks it.
  1927. * This has to be run in relatively quick succession to a user interaction event, else the browser rejects it.
  1928. */
  1929. function openInNewTab(href) {
  1930. try {
  1931. const openElem = document.createElement("a");
  1932. Object.assign(openElem, {
  1933. className: "betterytm-open-in-new-tab",
  1934. target: "_blank",
  1935. rel: "noopener noreferrer",
  1936. href,
  1937. });
  1938. openElem.style.visibility = "hidden";
  1939. document.body.appendChild(openElem);
  1940. openElem.click();
  1941. // timeout just to be safe
  1942. setTimeout(() => openElem.remove(), 200);
  1943. }
  1944. catch (err) {
  1945. error("Couldn't open URL in a new tab due to an error:", err);
  1946. }
  1947. }
  1948. /**
  1949. * Returns `unsafeWindow` if it is available, otherwise falls back to just `window`
  1950. * unsafeWindow is sometimes needed because otherwise YTM errors out - see [this issue](https://github.com/Sv443/BetterYTM/issues/18#show_issue)
  1951. */
  1952. function getUnsafeWindow() {
  1953. try {
  1954. // throws ReferenceError if the "@grant unsafeWindow" isn't present
  1955. return unsafeWindow;
  1956. }
  1957. catch (e) {
  1958. return window;
  1959. }
  1960. }
  1961. /**
  1962. * Returns the current domain as a constant string representation
  1963. * @throws Throws if script runs on an unexpected website
  1964. */
  1965. function getDomain() {
  1966. const { hostname } = new URL(location.href);
  1967. if (hostname.includes("music.youtube"))
  1968. return "ytm";
  1969. else if (hostname.includes("youtube"))
  1970. return "yt";
  1971. else
  1972. throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
  1973. }
  1974. /** Returns the URL of the asset hosted on GitHub at the specified relative `path` (starting at `ROOT/assets/`) */
  1975. function getAssetUrl(path) {
  1976. return `https://raw.githubusercontent.com/Sv443/BetterYTM/${_constants__WEBPACK_IMPORTED_MODULE_0__.branch}/assets/${path}`;
  1977. }
  1978. /**
  1979. * Automatically appends an `s` to the passed `word`, if `num` is not equal to 1
  1980. * @param word A word in singular form, to auto-convert to plural
  1981. * @param num If this is an array, the amount of items is used
  1982. */
  1983. function autoPlural(word, num) {
  1984. if (Array.isArray(num))
  1985. num = num.length;
  1986. return `${word}${num === 1 ? "" : "s"}`;
  1987. }
  1988. /** Ensures the passed `value` always stays between `min` and `max` */
  1989. function clamp(value, min, max) {
  1990. return Math.max(Math.min(value, max), min);
  1991. }
  1992. /** Pauses async execution for the specified time in ms */
  1993. function pauseFor(time) {
  1994. return new Promise((res) => {
  1995. setTimeout(res, time);
  1996. });
  1997. }
  1998. /***/ })
  1999. /******/ });
  2000. /************************************************************************/
  2001. /******/ // The module cache
  2002. /******/ var __webpack_module_cache__ = {};
  2003. /******/
  2004. /******/ // The require function
  2005. /******/ function __webpack_require__(moduleId) {
  2006. /******/ // Check if module is in cache
  2007. /******/ var cachedModule = __webpack_module_cache__[moduleId];
  2008. /******/ if (cachedModule !== undefined) {
  2009. /******/ return cachedModule.exports;
  2010. /******/ }
  2011. /******/ // Create a new module (and put it into the cache)
  2012. /******/ var module = __webpack_module_cache__[moduleId] = {
  2013. /******/ // no module.id needed
  2014. /******/ // no module.loaded needed
  2015. /******/ exports: {}
  2016. /******/ };
  2017. /******/
  2018. /******/ // Execute the module function
  2019. /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  2020. /******/
  2021. /******/ // Return the exports of the module
  2022. /******/ return module.exports;
  2023. /******/ }
  2024. /******/
  2025. /************************************************************************/
  2026. /******/ /* webpack/runtime/define property getters */
  2027. /******/ !function() {
  2028. /******/ // define getter functions for harmony exports
  2029. /******/ __webpack_require__.d = function(exports, definition) {
  2030. /******/ for(var key in definition) {
  2031. /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
  2032. /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
  2033. /******/ }
  2034. /******/ }
  2035. /******/ };
  2036. /******/ }();
  2037. /******/
  2038. /******/ /* webpack/runtime/hasOwnProperty shorthand */
  2039. /******/ !function() {
  2040. /******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
  2041. /******/ }();
  2042. /******/
  2043. /******/ /* webpack/runtime/make namespace object */
  2044. /******/ !function() {
  2045. /******/ // define __esModule on exports
  2046. /******/ __webpack_require__.r = function(exports) {
  2047. /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
  2048. /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
  2049. /******/ }
  2050. /******/ Object.defineProperty(exports, '__esModule', { value: true });
  2051. /******/ };
  2052. /******/ }();
  2053. /******/
  2054. /************************************************************************/
  2055. var __webpack_exports__ = {};
  2056. // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  2057. !function() {
  2058. /*!**********************!*\
  2059. !*** ./src/index.ts ***!
  2060. \**********************/
  2061. __webpack_require__.r(__webpack_exports__);
  2062. /* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./config */ "./src/config.ts");
  2063. /* harmony import */ var _constants__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./constants */ "./src/constants.ts");
  2064. /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./utils */ "./src/utils.ts");
  2065. /* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./events */ "./src/events.ts");
  2066. /* harmony import */ var _features_index__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./features/index */ "./src/features/index.ts");
  2067. var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
  2068. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  2069. return new (P || (P = Promise))(function (resolve, reject) {
  2070. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  2071. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  2072. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  2073. step((generator = generator.apply(thisArg, _arguments || [])).next());
  2074. });
  2075. };
  2076. // TODO: add some style
  2077. console.log(`${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name} v${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.version} (${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.lastCommit}) - ${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.namespace}`);
  2078. console.log(`Powered by lots of ambition and my song metadata API: ${_features_index__WEBPACK_IMPORTED_MODULE_4__.geniUrlBase}`);
  2079. const domain = (0,_utils__WEBPACK_IMPORTED_MODULE_2__.getDomain)();
  2080. /** Stuff that needs to be called ASAP, before anything async happens */
  2081. function preInit() {
  2082. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.setLogLevel)(_constants__WEBPACK_IMPORTED_MODULE_1__.logLevel);
  2083. if (domain === "ytm")
  2084. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.initBeforeUnloadHook)();
  2085. init();
  2086. }
  2087. function init() {
  2088. return __awaiter(this, void 0, void 0, function* () {
  2089. yield (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.preInitLayout)();
  2090. try {
  2091. document.addEventListener("DOMContentLoaded", onDomLoad);
  2092. }
  2093. catch (err) {
  2094. console.error(`${_constants__WEBPACK_IMPORTED_MODULE_1__.scriptInfo.name} - General Error:`, err);
  2095. }
  2096. try {
  2097. void ["TODO(v1.1):", _features_index__WEBPACK_IMPORTED_MODULE_4__.initMenu];
  2098. // initMenu();
  2099. }
  2100. catch (err) {
  2101. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't initialize menu:", err);
  2102. }
  2103. });
  2104. }
  2105. /** Called when the DOM has finished loading and can be queried and altered by the userscript */
  2106. function onDomLoad() {
  2107. return __awaiter(this, void 0, void 0, function* () {
  2108. // post-build these double quotes are replaced by backticks
  2109. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.addGlobalStyle)(`/*!***************************************************************************!*\
  2110. !*** css ./node_modules/css-loader/dist/cjs.js!./src/features/layout.css ***!
  2111. \***************************************************************************/
  2112. /* #MARKER misc */
  2113. .bytm-generic-btn {
  2114. display: inline-flex;
  2115. align-items: center;
  2116. justify-content: center;
  2117. position: relative;
  2118. vertical-align: middle;
  2119. cursor: pointer;
  2120. margin-left: 8px;
  2121. width: 40px;
  2122. height: 40px;
  2123. border-radius: 100%;
  2124. background-color: transparent;
  2125. }
  2126. .bytm-generic-btn:hover {
  2127. background-color: var(--yt-spec-10-percent-layer, #1d1d1d);
  2128. }
  2129. .bytm-generic-btn-img {
  2130. display: inline-block;
  2131. z-index: 10;
  2132. width: 24px;
  2133. height: 24px;
  2134. padding: 5px;
  2135. }
  2136. /* #MARKER watermark */
  2137. #betterytm-watermark {
  2138. font-size: 10px;
  2139. display: inline-block;
  2140. position: absolute;
  2141. left: 97px;
  2142. top: 46px;
  2143. z-index: 10;
  2144. color: white;
  2145. text-decoration: none;
  2146. cursor: pointer;
  2147. }
  2148. #betterytm-watermark:hover {
  2149. text-decoration: underline;
  2150. }
  2151. /* #MARKER queue buttons */
  2152. /* TODO: */
  2153. .side-panel.modular ytmusic-player-queue-item .song-info.ytmusic-player-queue-item {
  2154. position: relative;
  2155. }
  2156. .side-panel.modular ytmusic-player-queue-item .song-info .bytm-queue-btn-container {
  2157. display: none;
  2158. position: absolute;
  2159. right: 0;
  2160. }
  2161. .side-panel.modular ytmusic-player-queue-item:hover .song-info .bytm-queue-btn-container {
  2162. display: inline-block;
  2163. }
  2164. ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown[data-bytm-hidden=true] {
  2165. display: none !important;
  2166. }
  2167. /*!******************************************************************************!*\
  2168. !*** css ./node_modules/css-loader/dist/cjs.js!./src/features/menu/menu.css ***!
  2169. \******************************************************************************/
  2170. /* #bytm-menu-backdrop {
  2171. display: none;
  2172. flex-direction: column;
  2173. justify-content: center;
  2174. align-items: center;
  2175. }
  2176. #bytm-menu-backdrop[data-menu-open="true"] {
  2177. display: flex;
  2178. } */
  2179. #bytm-menu-header-container {
  2180. display: flex;
  2181. justify-content: flex-start;
  2182. align-items: center;
  2183. border-color: #ffffff;
  2184. border-style: none solid none none;
  2185. }
  2186. .bytm-menu-header-option {
  2187. display: "flex";
  2188. justify-content: center;
  2189. align-items: center;
  2190. border-color: #ffffff;
  2191. border-style: solid none solid none;
  2192. }
  2193. #bytm-menu-header-option h3 {
  2194. margin: 0;
  2195. }
  2196. .bytm-menu-tab[data-active="true"] {
  2197. display: none;
  2198. }
  2199. .bytm-menu-tab[data-active="false"] {
  2200. display: none;
  2201. }
  2202. /*# sourceMappingURL=main.css.map*/`, "global");
  2203. const features = yield (0,_config__WEBPACK_IMPORTED_MODULE_0__.loadFeatureConf)();
  2204. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.log)(`Initializing features for domain '${domain}'`);
  2205. try {
  2206. if (domain === "ytm") {
  2207. (0,_events__WEBPACK_IMPORTED_MODULE_3__.initSiteEvents)();
  2208. if (features.arrowKeySupport)
  2209. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.initArrowKeySkip)();
  2210. if (features.removeUpgradeTab)
  2211. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.removeUpgradeTab)();
  2212. if (features.watermarkEnabled)
  2213. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.addWatermark)();
  2214. if (features.geniusLyrics)
  2215. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.addMediaCtrlLyricsBtn)();
  2216. if (features.queueButtons)
  2217. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.initQueueButtons)();
  2218. if (typeof features.volumeSliderSize === "number")
  2219. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.setVolSliderSize)();
  2220. if (features.anchorImprovements)
  2221. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.addAnchorImprovements)();
  2222. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.setVolSliderStep)();
  2223. }
  2224. if (["ytm", "yt"].includes(domain)) {
  2225. if (features.switchBetweenSites)
  2226. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.initSiteSwitch)(domain);
  2227. try {
  2228. (0,_features_index__WEBPACK_IMPORTED_MODULE_4__.addMenu)(); // TODO(v1.1): remove
  2229. }
  2230. catch (err) {
  2231. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("Couldn't add menu:", err);
  2232. }
  2233. }
  2234. }
  2235. catch (err) {
  2236. (0,_utils__WEBPACK_IMPORTED_MODULE_2__.error)("General error while executing feature:", err);
  2237. }
  2238. });
  2239. }
  2240. preInit();
  2241. }();
  2242. //# sourceMappingURL=BetterYTM.user.js.map