|
@@ -1,4312 +1,5716 @@
|
|
|
-(function(UserUtils,marked){'use strict';function _interopNamespaceDefault(e){var n=Object.create(null);if(e){Object.keys(e).forEach(function(k){if(k!=='default'){var d=Object.getOwnPropertyDescriptor(e,k);Object.defineProperty(n,k,d.get?d:{enumerable:true,get:function(){return e[k]}});}})}n.default=e;return Object.freeze(n)}var UserUtils__namespace=/*#__PURE__*/_interopNamespaceDefault(UserUtils);/******************************************************************************
|
|
|
-Copyright (c) Microsoft Corporation.
|
|
|
+// ==UserScript==
|
|
|
+// @name BetterYTM
|
|
|
+// @namespace https://github.com/Sv443/BetterYTM
|
|
|
+// @version 1.1.0
|
|
|
+// @description Lots of configurable layout and user experience improvements for YouTube Music
|
|
|
+// @description:de-DE Konfigurierbare Layout- und Benutzererfahrungs-Verbesserungen für YouTube Music
|
|
|
+// @description:en-US Configurable layout and user experience improvements for YouTube Music
|
|
|
+// @description:en-UK Configurable layout and user experience improvements for YouTube Music
|
|
|
+// @description:es-ES Mejoras de diseño y experiencia de usuario configurables para YouTube Music
|
|
|
+// @description:fr-FR Améliorations de la mise en page et de l'expérience utilisateur configurables pour YouTube Music
|
|
|
+// @description:hi-IN YouTube Music के लिए विन्यास और यूजर अनुभव में सुधार करने योग्य लेआउट और यूजर अनुभव सुधार
|
|
|
+// @description:ja-JA YouTube Musicのレイアウトとユーザーエクスペリエンスの改善を設定可能にする
|
|
|
+// @description:pt-BR Melhorias configuráveis no layout e na experiência do usuário para o YouTube Music
|
|
|
+// @description:zh-CN 可配置的布局和YouTube Music的用户体验改进
|
|
|
+// @homepageURL https://github.com/Sv443/BetterYTM#readme
|
|
|
+// @supportURL https://github.com/Sv443/BetterYTM/issues
|
|
|
+// @license AGPL-3.0-only
|
|
|
+// @author Sv443
|
|
|
+// @copyright Sv443 (https://github.com/Sv443)
|
|
|
+// @icon https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/logo/logo_48.png
|
|
|
+// @match https://music.youtube.com/*
|
|
|
+// @match https://www.youtube.com/*
|
|
|
+// @run-at document-start
|
|
|
+// @downloadURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
|
|
|
+// @updateURL https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.user.js
|
|
|
+// @connect api.sv443.net
|
|
|
+// @connect github.com
|
|
|
+// @connect raw.githubusercontent.com
|
|
|
+// @grant GM.getValue
|
|
|
+// @grant GM.setValue
|
|
|
+// @grant GM.deleteValue
|
|
|
+// @grant GM.getResourceUrl
|
|
|
+// @grant GM.setClipboard
|
|
|
+// @grant GM.xmlHttpRequest
|
|
|
+// @grant unsafeWindow
|
|
|
+// @noframes
|
|
|
+// @resource img-arrow_down https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg
|
|
|
+// @resource img-delete https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg
|
|
|
+// @resource img-error https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg
|
|
|
+// @resource img-globe https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg
|
|
|
+// @resource img-help https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg
|
|
|
+// @resource img-lyrics https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg
|
|
|
+// @resource img-skip_to https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg
|
|
|
+// @resource img-spinner https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg
|
|
|
+// @resource img-logo https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/logo/logo_48.png
|
|
|
+// @resource img-close https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/close.png
|
|
|
+// @resource img-discord https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/discord.png
|
|
|
+// @resource img-github https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/github.png
|
|
|
+// @resource img-greasyfork https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/greasyfork.png
|
|
|
+// @resource img-openuserjs https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/external/openuserjs.png
|
|
|
+// @resource css-fix_spacing https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css
|
|
|
+// @resource css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css
|
|
|
+// @resource doc-changelog https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md
|
|
|
+// @resource trans-de_DE https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json
|
|
|
+// @resource trans-en_US https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json
|
|
|
+// @resource trans-en_UK https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json
|
|
|
+// @resource trans-es_ES https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json
|
|
|
+// @resource trans-fr_FR https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json
|
|
|
+// @resource trans-hi_IN https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json
|
|
|
+// @resource trans-ja_JA https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json
|
|
|
+// @resource trans-pt_BR https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json
|
|
|
+// @resource trans-zh_CN https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json
|
|
|
+// @require https://cdn.jsdelivr.net/npm/@sv443-network/[email protected]/dist/index.global.js
|
|
|
+// @require https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.js
|
|
|
+// ==/UserScript==
|
|
|
+/*
|
|
|
+▄▄▄ ▄ ▄▄▄▄▄▄ ▄
|
|
|
+█ █ ▄▄▄ █ █ ▄▄▄ ▄ ▄█ █ █ █▀▄▀█
|
|
|
+█▀▀▄ █▄█ █▀ █▀ █▄█ █▀ █ █ █ █
|
|
|
+█▄▄▀ ▀▄▄ ▀▄▄ ▀▄▄ ▀▄▄ █ █ █ █ █
|
|
|
+
|
|
|
+ Made with ❤️ by Sv443
|
|
|
+I welcome every contribution on GitHub!
|
|
|
+ https://github.com/Sv443/BetterYTM
|
|
|
+*/
|
|
|
+
|
|
|
+/* Disclaimer: I am not affiliated with or endorsed by YouTube, Google, Alphabet, Genius or anyone else */
|
|
|
+/* C&D this 🖕 */
|
|
|
+
|
|
|
+(function (UserUtils, marked) {
|
|
|
+ 'use strict';
|
|
|
+
|
|
|
+ function _interopNamespaceDefault(e) {
|
|
|
+ var n = Object.create(null);
|
|
|
+ if (e) {
|
|
|
+ Object.keys(e).forEach(function (k) {
|
|
|
+ if (k !== 'default') {
|
|
|
+ var d = Object.getOwnPropertyDescriptor(e, k);
|
|
|
+ Object.defineProperty(n, k, d.get ? d : {
|
|
|
+ enumerable: true,
|
|
|
+ get: function () { return e[k]; }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ n.default = e;
|
|
|
+ return Object.freeze(n);
|
|
|
+ }
|
|
|
+
|
|
|
+ var UserUtils__namespace = /*#__PURE__*/_interopNamespaceDefault(UserUtils);
|
|
|
+
|
|
|
+ /******************************************************************************
|
|
|
+ Copyright (c) Microsoft Corporation.
|
|
|
|
|
|
-Permission to use, copy, modify, and/or distribute this software for any
|
|
|
-purpose with or without fee is hereby granted.
|
|
|
+ Permission to use, copy, modify, and/or distribute this software for any
|
|
|
+ purpose with or without fee is hereby granted.
|
|
|
|
|
|
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
|
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
|
-AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
|
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
|
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
|
-OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
|
-PERFORMANCE OF THIS SOFTWARE.
|
|
|
-***************************************************************************** */
|
|
|
-/* global Reflect, Promise, SuppressedError, Symbol */
|
|
|
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
|
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
|
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
|
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
|
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
|
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
|
+ PERFORMANCE OF THIS SOFTWARE.
|
|
|
+ ***************************************************************************** */
|
|
|
+ /* global Reflect, Promise, SuppressedError, Symbol */
|
|
|
|
|
|
|
|
|
-function __awaiter(thisArg, _arguments, P, generator) {
|
|
|
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
|
- return new (P || (P = Promise))(function (resolve, reject) {
|
|
|
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
|
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
|
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
|
- step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
|
- });
|
|
|
-}
|
|
|
+ function __awaiter(thisArg, _arguments, P, generator) {
|
|
|
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
|
+ return new (P || (P = Promise))(function (resolve, reject) {
|
|
|
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
|
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
|
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
|
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
-function __values(o) {
|
|
|
- var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
|
- if (m) return m.call(o);
|
|
|
- if (o && typeof o.length === "number") return {
|
|
|
- next: function () {
|
|
|
- if (o && i >= o.length) o = void 0;
|
|
|
- return { value: o && o[i++], done: !o };
|
|
|
- }
|
|
|
- };
|
|
|
- throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
|
-}
|
|
|
+ function __values(o) {
|
|
|
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
|
+ if (m) return m.call(o);
|
|
|
+ if (o && typeof o.length === "number") return {
|
|
|
+ next: function () {
|
|
|
+ if (o && i >= o.length) o = void 0;
|
|
|
+ return { value: o && o[i++], done: !o };
|
|
|
+ }
|
|
|
+ };
|
|
|
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
|
+ }
|
|
|
|
|
|
-function __asyncValues(o) {
|
|
|
- if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
|
- var m = o[Symbol.asyncIterator], i;
|
|
|
- return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
|
- function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
|
- function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
|
-}
|
|
|
+ function __asyncValues(o) {
|
|
|
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
|
+ var m = o[Symbol.asyncIterator], i;
|
|
|
+ return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
|
+ function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
|
+ function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
|
+ }
|
|
|
|
|
|
-typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
|
- var e = new Error(message);
|
|
|
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
|
-};let createNanoEvents = () => ({
|
|
|
- emit(event, ...args) {
|
|
|
- for (
|
|
|
- let i = 0,
|
|
|
- callbacks = this.events[event] || [],
|
|
|
- length = callbacks.length;
|
|
|
- i < length;
|
|
|
- i++
|
|
|
- ) {
|
|
|
- callbacks[i](...args);
|
|
|
- }
|
|
|
- },
|
|
|
- events: {},
|
|
|
- on(event, cb) {
|
|
|
-(this.events[event] ||= []).push(cb);
|
|
|
- return () => {
|
|
|
- this.events[event] = this.events[event]?.filter(i => cb !== i);
|
|
|
- }
|
|
|
- }
|
|
|
-});/** Abstract class that can be extended to create an event emitter with helper methods and a strongly typed event map */
|
|
|
-class NanoEmitter {
|
|
|
- constructor() {
|
|
|
- Object.defineProperty(this, "events", {
|
|
|
- enumerable: true,
|
|
|
- configurable: true,
|
|
|
- writable: true,
|
|
|
- value: createNanoEvents()
|
|
|
- });
|
|
|
- Object.defineProperty(this, "unsubscribers", {
|
|
|
- enumerable: true,
|
|
|
- configurable: true,
|
|
|
- writable: true,
|
|
|
- value: []
|
|
|
- });
|
|
|
- }
|
|
|
- /** Subscribes to an event - returns a function that unsubscribes the event listener */
|
|
|
- on(event, cb) {
|
|
|
- // eslint-disable-next-line prefer-const
|
|
|
- let unsub;
|
|
|
- const unsubProxy = () => {
|
|
|
- if (!unsub)
|
|
|
- return;
|
|
|
- unsub();
|
|
|
- this.unsubscribers = this.unsubscribers.filter(u => u !== unsub);
|
|
|
- };
|
|
|
- unsub = this.events.on(event, cb);
|
|
|
- this.unsubscribers.push(unsub);
|
|
|
- return unsubProxy;
|
|
|
- }
|
|
|
- /** Unsubscribes all event listeners */
|
|
|
- unsubscribeAll() {
|
|
|
- for (const unsub of this.unsubscribers)
|
|
|
- unsub();
|
|
|
- this.unsubscribers = [];
|
|
|
- }
|
|
|
-}// I know TS enums are impure but it doesn't really matter here, plus they look cooler
|
|
|
-var LogLevel;
|
|
|
-(function (LogLevel) {
|
|
|
- LogLevel[LogLevel["Debug"] = 0] = "Debug";
|
|
|
- LogLevel[LogLevel["Info"] = 1] = "Info";
|
|
|
-})(LogLevel || (LogLevel = {}));const modeRaw = "#{{MODE}}";
|
|
|
-const branchRaw = "#{{BRANCH}}";
|
|
|
-const hostRaw = "#{{HOST}}";
|
|
|
-/** The mode in which the script was built (production or development) */
|
|
|
-const mode = (modeRaw.match(/^#{{.+}}$/) ? "production" : modeRaw);
|
|
|
-/** The branch to use in various URLs that point to the GitHub repo */
|
|
|
-const branch = (branchRaw.match(/^#{{.+}}$/) ? "main" : branchRaw);
|
|
|
-/** Path to the GitHub repo */
|
|
|
-const repo = "Sv443/BetterYTM";
|
|
|
-/** Which host the userscript was installed from */
|
|
|
-const host = (hostRaw.match(/^#{{.+}}$/) ? "github" : hostRaw);
|
|
|
-/**
|
|
|
- * How much info should be logged to the devtools console
|
|
|
- * 0 = Debug (show everything) or 1 = Info (show only important stuff)
|
|
|
- */
|
|
|
-const defaultLogLevel = mode === "production" ? LogLevel.Info : LogLevel.Debug;
|
|
|
-/** Info about the userscript, parsed from the userscript header (tools/post-build.js) */
|
|
|
-const scriptInfo = {
|
|
|
- name: GM.info.script.name,
|
|
|
- version: GM.info.script.version,
|
|
|
- namespace: GM.info.script.namespace,
|
|
|
- buildNumber: "#{{BUILD_NUMBER}}", // asserted as generic string instead of literal
|
|
|
-};/** Options that are applied to every SelectorObserver instance */
|
|
|
-const defaultObserverOptions = {
|
|
|
- defaultDebounce: 100,
|
|
|
-};
|
|
|
-const observers$1 = {};
|
|
|
-/** Call after DOM load to initialize all SelectorObserver instances */
|
|
|
-function initObservers() {
|
|
|
- try {
|
|
|
- // #SECTION body = the entire <body> element - use sparingly due to performance impacts!
|
|
|
- observers$1.body = new UserUtils.SelectorObserver(document.body, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: false }));
|
|
|
- observers$1.body.enable();
|
|
|
- // #SECTION playerBar = media controls bar at the bottom of the page
|
|
|
- const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
|
|
|
- observers$1.playerBar = new UserUtils.SelectorObserver(playerBarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { defaultDebounce: 200 }));
|
|
|
- observers$1.body.addListener(playerBarSelector, {
|
|
|
- listener: () => {
|
|
|
- console.log("#DBG-UU enabling playerBar observer");
|
|
|
- observers$1.playerBar.enable();
|
|
|
- },
|
|
|
- });
|
|
|
- // #SECTION playerBarInfo = song title, artist, album, etc. inside the player bar
|
|
|
- const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
|
|
|
- observers$1.playerBarInfo = new UserUtils.SelectorObserver(playerBarInfoSelector, Object.assign(Object.assign({}, defaultObserverOptions), { attributes: true, attributeFilter: ["title"] }));
|
|
|
- observers$1.playerBarInfo.addListener(playerBarInfoSelector, {
|
|
|
- listener: () => {
|
|
|
- console.log("#DBG-UU enabling playerBarTitle observer");
|
|
|
- observers$1.playerBarInfo.enable();
|
|
|
- },
|
|
|
- });
|
|
|
- // #DEBUG example: listen for title change:
|
|
|
- observers$1.playerBarInfo.addListener("yt-formatted-string.title", {
|
|
|
- continuous: true,
|
|
|
- listener: (titleElem) => {
|
|
|
- console.log("#DBG-UU >>>>> title changed", titleElem.title);
|
|
|
- },
|
|
|
- });
|
|
|
- emitInterface("bytm:observersReady");
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Failed to initialize observers:", err);
|
|
|
- }
|
|
|
-}
|
|
|
-/** Interface function for adding listeners to the already present observers */
|
|
|
-function addSelectorListener(observerName, selector, options) {
|
|
|
- observers$1[observerName].addListener(selector, options);
|
|
|
-}var de_DE = {
|
|
|
- name: "Deutsch (Deutschland)",
|
|
|
- nameEnglish: "German",
|
|
|
- emoji: "🇩🇪",
|
|
|
- userscriptDesc: "Konfigurierbare Layout- und Benutzererfahrungs-Verbesserungen für YouTube Music",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var en_US = {
|
|
|
- name: "English (United States)",
|
|
|
- nameEnglish: "English",
|
|
|
- emoji: "🇺🇸",
|
|
|
- userscriptDesc: "Configurable layout and user experience improvements for YouTube Music",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var en_UK = {
|
|
|
- name: "English (United Kingdom)",
|
|
|
- nameEnglish: "German",
|
|
|
- emoji: "🇩🇪",
|
|
|
- userscriptDesc: "Configurable layout and user experience improvements for YouTube Music",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var es_ES = {
|
|
|
- name: "Español (España)",
|
|
|
- nameEnglish: "Spanish",
|
|
|
- emoji: "🇪🇸",
|
|
|
- userscriptDesc: "Mejoras de diseño y experiencia de usuario configurables para YouTube Music",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var fr_FR = {
|
|
|
- name: "Français (France)",
|
|
|
- nameEnglish: "French",
|
|
|
- emoji: "🇫🇷",
|
|
|
- userscriptDesc: "Améliorations de la mise en page et de l'expérience utilisateur configurables pour YouTube Music",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var hi_IN = {
|
|
|
- name: "हिंदी (भारत)",
|
|
|
- nameEnglish: "Hindi",
|
|
|
- emoji: "🇮🇳",
|
|
|
- userscriptDesc: "YouTube Music के लिए विन्यास और यूजर अनुभव में सुधार करने योग्य लेआउट और यूजर अनुभव सुधार",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var ja_JA = {
|
|
|
- name: "日本語 (日本)",
|
|
|
- nameEnglish: "Japanese",
|
|
|
- emoji: "🇯🇵",
|
|
|
- userscriptDesc: "YouTube Musicのレイアウトとユーザーエクスペリエンスの改善を設定可能にする",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var pt_BR = {
|
|
|
- name: "Português (Brasil)",
|
|
|
- nameEnglish: "Portuguese",
|
|
|
- emoji: "🇵🇹",
|
|
|
- userscriptDesc: "Melhorias configuráveis no layout e na experiência do usuário para o YouTube Music",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var zh_CN = {
|
|
|
- name: "中文(简化,中国)",
|
|
|
- nameEnglish: "Chinese (simpl.)",
|
|
|
- emoji: "🇨🇳",
|
|
|
- userscriptDesc: "可配置的布局和YouTube Music的用户体验改进",
|
|
|
- authors: [
|
|
|
- "Sv443"
|
|
|
- ]
|
|
|
-};
|
|
|
-var locales = {
|
|
|
- de_DE: de_DE,
|
|
|
- en_US: en_US,
|
|
|
- en_UK: en_UK,
|
|
|
- es_ES: es_ES,
|
|
|
- fr_FR: fr_FR,
|
|
|
- hi_IN: hi_IN,
|
|
|
- ja_JA: ja_JA,
|
|
|
- pt_BR: pt_BR,
|
|
|
- zh_CN: zh_CN
|
|
|
-};let features$3;
|
|
|
-function setBehaviorConfig(feats) {
|
|
|
- features$3 = feats;
|
|
|
-}
|
|
|
-//#MARKER beforeunload popup
|
|
|
-let beforeUnloadEnabled = true;
|
|
|
-/** Disables the popup before leaving the site */
|
|
|
-function disableBeforeUnload() {
|
|
|
- beforeUnloadEnabled = false;
|
|
|
- info("Disabled popup before leaving the site");
|
|
|
-}
|
|
|
-/**
|
|
|
- * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
|
|
|
- * event listeners before they can be called by the site.
|
|
|
- */
|
|
|
-function initBeforeUnloadHook() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
|
|
|
- (function (original) {
|
|
|
- // @ts-ignore
|
|
|
- window.__proto__.addEventListener = function (...args) {
|
|
|
- const origListener = typeof args[1] === "function" ? args[1] : args[1].handleEvent;
|
|
|
- args[1] = function (...a) {
|
|
|
- if (!beforeUnloadEnabled && args[0] === "beforeunload") {
|
|
|
- info("Prevented beforeunload event listener from being called");
|
|
|
- return false;
|
|
|
- }
|
|
|
- else
|
|
|
- return origListener.apply(this, a);
|
|
|
- };
|
|
|
- original.apply(this, args);
|
|
|
- };
|
|
|
- // @ts-ignore
|
|
|
- })(window.__proto__.addEventListener);
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER auto close toasts
|
|
|
-/** Closes toasts after a set amount of time */
|
|
|
-function initAutoCloseToasts() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- const animTimeout = 300;
|
|
|
- const closeTimeout = Math.max(features$3.closeToastsTimeout * 1000 + animTimeout, animTimeout);
|
|
|
- onSelectorOld("tp-yt-paper-toast#toast", {
|
|
|
- all: true,
|
|
|
- continuous: true,
|
|
|
- listener: (toastElems) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- var _a;
|
|
|
- for (const toastElem of toastElems) {
|
|
|
- if (!toastElem.hasAttribute("allow-click-through"))
|
|
|
- continue;
|
|
|
- if (toastElem.classList.contains("bytm-closing"))
|
|
|
- continue;
|
|
|
- toastElem.classList.add("bytm-closing");
|
|
|
- yield UserUtils.pauseFor(closeTimeout);
|
|
|
- toastElem.classList.remove("paper-toast-open");
|
|
|
- log(`Automatically closed toast '${(_a = toastElem.querySelector("#text-container yt-formatted-string")) === null || _a === void 0 ? void 0 : _a.textContent}' after ${features$3.closeToastsTimeout * 1000}ms`);
|
|
|
- // wait for the transition to finish
|
|
|
- yield UserUtils.pauseFor(animTimeout);
|
|
|
- toastElem.style.display = "none";
|
|
|
- }
|
|
|
- }),
|
|
|
- });
|
|
|
- log("Initialized automatic toast closing");
|
|
|
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
|
+ var e = new Error(message);
|
|
|
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
|
+ };
|
|
|
+
|
|
|
+ let createNanoEvents = () => ({
|
|
|
+ emit(event, ...args) {
|
|
|
+ for (
|
|
|
+ let i = 0,
|
|
|
+ callbacks = this.events[event] || [],
|
|
|
+ length = callbacks.length;
|
|
|
+ i < length;
|
|
|
+ i++
|
|
|
+ ) {
|
|
|
+ callbacks[i](...args);
|
|
|
}
|
|
|
- catch (err) {
|
|
|
- error("Error in automatic toast closing:", err);
|
|
|
+ },
|
|
|
+ events: {},
|
|
|
+ on(event, cb) {
|
|
|
+ (this.events[event] ||= []).push(cb);
|
|
|
+ return () => {
|
|
|
+ this.events[event] = this.events[event]?.filter(i => cb !== i);
|
|
|
}
|
|
|
+ }
|
|
|
});
|
|
|
-}
|
|
|
-/** After how many milliseconds a remembered entry should expire */
|
|
|
-const remSongEntryExpiry = 1000 * 60 * 1;
|
|
|
-/** Minimum time a song has to be played before it is committed to GM storage */
|
|
|
-const remSongMinPlayTime = 10;
|
|
|
-let remSongsCache = [];
|
|
|
-/** Remembers the time of the last played song and resumes playback from that time */
|
|
|
-function initRememberSongTime() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (features$3.rememberSongTimeSites !== "all" && features$3.rememberSongTimeSites !== getDomain())
|
|
|
- return;
|
|
|
- const storedDataRaw = yield GM.getValue("bytm-rem-songs");
|
|
|
- if (!storedDataRaw)
|
|
|
- yield GM.setValue("bytm-rem-songs", "[]");
|
|
|
- remSongsCache = JSON.parse(String(storedDataRaw !== null && storedDataRaw !== void 0 ? storedDataRaw : "[]"));
|
|
|
- log(`Initialized song time remembering with ${remSongsCache.length} initial entries`);
|
|
|
- if (location.pathname.startsWith("/watch"))
|
|
|
- yield restoreSongTime();
|
|
|
- remSongUpdateEntry();
|
|
|
- setInterval(remSongUpdateEntry, 1000);
|
|
|
- });
|
|
|
-}
|
|
|
-/** Tries to restore the time of the currently playing song */
|
|
|
-function restoreSongTime() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (location.pathname.startsWith("/watch")) {
|
|
|
- const { searchParams } = new URL(location.href);
|
|
|
- const watchID = searchParams.get("v");
|
|
|
- if (!watchID)
|
|
|
- return;
|
|
|
- const entry = remSongsCache.find(entry => entry.watchID === watchID);
|
|
|
- if (entry) {
|
|
|
- if (Date.now() - entry.updateTimestamp > remSongEntryExpiry) {
|
|
|
- yield delRemSongData(entry.watchID);
|
|
|
+
|
|
|
+ /** Abstract class that can be extended to create an event emitter with helper methods and a strongly typed event map */
|
|
|
+ class NanoEmitter {
|
|
|
+ constructor() {
|
|
|
+ Object.defineProperty(this, "events", {
|
|
|
+ enumerable: true,
|
|
|
+ configurable: true,
|
|
|
+ writable: true,
|
|
|
+ value: createNanoEvents()
|
|
|
+ });
|
|
|
+ Object.defineProperty(this, "unsubscribers", {
|
|
|
+ enumerable: true,
|
|
|
+ configurable: true,
|
|
|
+ writable: true,
|
|
|
+ value: []
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Subscribes to an event - returns a function that unsubscribes the event listener */
|
|
|
+ on(event, cb) {
|
|
|
+ // eslint-disable-next-line prefer-const
|
|
|
+ let unsub;
|
|
|
+ const unsubProxy = () => {
|
|
|
+ if (!unsub)
|
|
|
return;
|
|
|
- }
|
|
|
- else {
|
|
|
- onSelectorOld(videoSelector, {
|
|
|
- listener: (vidElem) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (vidElem) {
|
|
|
- const applyTime = () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (isNaN(entry.songTime))
|
|
|
- return;
|
|
|
- vidElem.currentTime = UserUtils.clamp(Math.max(entry.songTime, 0), 0, vidElem.duration);
|
|
|
- yield delRemSongData(entry.watchID);
|
|
|
- info(`Restored song time to ${Math.floor(entry.songTime / 60)}m, ${(entry.songTime % 60).toFixed(1)}s`, LogLevel.Info);
|
|
|
- });
|
|
|
- if (vidElem.readyState === 4)
|
|
|
- applyTime();
|
|
|
- else
|
|
|
- vidElem.addEventListener("canplay", applyTime, { once: true });
|
|
|
- }
|
|
|
- }),
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
+ unsub();
|
|
|
+ this.unsubscribers = this.unsubscribers.filter(u => u !== unsub);
|
|
|
+ };
|
|
|
+ unsub = this.events.on(event, cb);
|
|
|
+ this.unsubscribers.push(unsub);
|
|
|
+ return unsubProxy;
|
|
|
}
|
|
|
- });
|
|
|
-}
|
|
|
-/** Updates the currently playing song's entry in GM storage */
|
|
|
-function remSongUpdateEntry() {
|
|
|
- var _a, _b, _c;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (location.pathname.startsWith("/watch")) {
|
|
|
- const { searchParams } = new URL(location.href);
|
|
|
- const watchID = searchParams.get("v");
|
|
|
- if (!watchID)
|
|
|
- return;
|
|
|
- const songTime = (_a = yield getVideoTime()) !== null && _a !== void 0 ? _a : 0;
|
|
|
- const paused = (_c = (_b = document.querySelector(videoSelector)) === null || _b === void 0 ? void 0 : _b.paused) !== null && _c !== void 0 ? _c : false;
|
|
|
- // don't immediately update to reduce race conditions and only update if the video is playing
|
|
|
- // also it just sounds better if the song starts at the beginning if only a couple seconds have passed
|
|
|
- if (songTime > remSongMinPlayTime && !paused) {
|
|
|
- const entry = {
|
|
|
- watchID,
|
|
|
- songTime,
|
|
|
- updateTimestamp: Date.now(),
|
|
|
- };
|
|
|
- yield setRemSongData(entry);
|
|
|
- }
|
|
|
- // if the song is rewound to the beginning, delete the entry
|
|
|
- else {
|
|
|
- const entry = remSongsCache.find(entry => entry.watchID === watchID);
|
|
|
- if (entry && songTime <= remSongMinPlayTime)
|
|
|
- yield delRemSongData(entry.watchID);
|
|
|
- }
|
|
|
+ /** Unsubscribes all event listeners */
|
|
|
+ unsubscribeAll() {
|
|
|
+ for (const unsub of this.unsubscribers)
|
|
|
+ unsub();
|
|
|
+ this.unsubscribers = [];
|
|
|
}
|
|
|
- const expiredEntries = remSongsCache.filter(entry => Date.now() - entry.updateTimestamp > remSongEntryExpiry);
|
|
|
- for (const entry of expiredEntries)
|
|
|
- yield delRemSongData(entry.watchID);
|
|
|
- });
|
|
|
-}
|
|
|
-/** Adds an entry or updates it if it already exists */
|
|
|
-function setRemSongData(data) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const foundIdx = remSongsCache.findIndex(entry => entry.watchID === data.watchID);
|
|
|
- if (foundIdx >= 0)
|
|
|
- remSongsCache[foundIdx] = data;
|
|
|
- else
|
|
|
- remSongsCache.push(data);
|
|
|
- yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
|
|
|
- });
|
|
|
-}
|
|
|
-/** Deletes an entry */
|
|
|
-function delRemSongData(watchID) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- remSongsCache = [...remSongsCache.filter(entry => entry.watchID !== watchID)];
|
|
|
- yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER disable darkreader
|
|
|
-/** Disables Dark Reader if it is enabled */
|
|
|
-function disableDarkReader() {
|
|
|
- if (document.querySelector(".darkreader")) {
|
|
|
- const metaElem = document.createElement("meta");
|
|
|
- metaElem.name = "darkreader-lock";
|
|
|
- metaElem.classList.add("bytm-disable-darkreader");
|
|
|
- document.head.appendChild(metaElem);
|
|
|
- info("Sent hint to Dark Reader to disable itself");
|
|
|
}
|
|
|
-}/** EventEmitter instance that is used to detect changes to the site */
|
|
|
-const siteEvents = createNanoEvents();
|
|
|
-let observers = [];
|
|
|
-/** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
|
|
|
-function initSiteEvents() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
+
|
|
|
+ // I know TS enums are impure but it doesn't really matter here, plus they look cooler
|
|
|
+ var LogLevel;
|
|
|
+ (function (LogLevel) {
|
|
|
+ LogLevel[LogLevel["Debug"] = 0] = "Debug";
|
|
|
+ LogLevel[LogLevel["Info"] = 1] = "Info";
|
|
|
+ })(LogLevel || (LogLevel = {}));
|
|
|
+
|
|
|
+ const modeRaw = "production";
|
|
|
+ const branchRaw = "develop";
|
|
|
+ const hostRaw = "github";
|
|
|
+ /** The mode in which the script was built (production or development) */
|
|
|
+ const mode = (modeRaw.match(/^#{{.+}}$/) ? "production" : modeRaw);
|
|
|
+ /** The branch to use in various URLs that point to the GitHub repo */
|
|
|
+ const branch = (branchRaw.match(/^#{{.+}}$/) ? "main" : branchRaw);
|
|
|
+ /** Path to the GitHub repo */
|
|
|
+ const repo = "Sv443/BetterYTM";
|
|
|
+ /** Which host the userscript was installed from */
|
|
|
+ const host = (hostRaw.match(/^#{{.+}}$/) ? "github" : hostRaw);
|
|
|
+ /**
|
|
|
+ * How much info should be logged to the devtools console
|
|
|
+ * 0 = Debug (show everything) or 1 = Info (show only important stuff)
|
|
|
+ */
|
|
|
+ const defaultLogLevel = mode === "production" ? LogLevel.Info : LogLevel.Debug;
|
|
|
+ /** Info about the userscript, parsed from the userscript header (tools/post-build.js) */
|
|
|
+ const scriptInfo = {
|
|
|
+ name: GM.info.script.name,
|
|
|
+ version: GM.info.script.version,
|
|
|
+ namespace: GM.info.script.namespace,
|
|
|
+ buildNumber: "1ac3d61", // asserted as generic string instead of literal
|
|
|
+ };
|
|
|
+
|
|
|
+ /** Options that are applied to every SelectorObserver instance */
|
|
|
+ const defaultObserverOptions = {
|
|
|
+ defaultDebounce: 100,
|
|
|
+ };
|
|
|
+ const observers$1 = {};
|
|
|
+ /** Call after DOM load to initialize all SelectorObserver instances */
|
|
|
+ function initObservers() {
|
|
|
try {
|
|
|
- //#SECTION queue
|
|
|
- // the queue container always exists so it doesn't need an extra init function
|
|
|
- const queueObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
|
|
|
- if (addedNodes.length > 0 || removedNodes.length > 0) {
|
|
|
- info(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
|
|
|
- emitSiteEvent("queueChanged", target);
|
|
|
- }
|
|
|
- });
|
|
|
- // only observe added or removed elements
|
|
|
- queueObs.observe(document.querySelector("#side-panel #contents.ytmusic-player-queue"), {
|
|
|
- childList: true,
|
|
|
+ // #SECTION body = the entire <body> element - use sparingly due to performance impacts!
|
|
|
+ observers$1.body = new UserUtils.SelectorObserver(document.body, Object.assign(Object.assign({}, defaultObserverOptions), { subtree: false }));
|
|
|
+ observers$1.body.enable();
|
|
|
+ // #SECTION playerBar = media controls bar at the bottom of the page
|
|
|
+ const playerBarSelector = "ytmusic-app-layout ytmusic-player-bar.ytmusic-app";
|
|
|
+ observers$1.playerBar = new UserUtils.SelectorObserver(playerBarSelector, Object.assign(Object.assign({}, defaultObserverOptions), { defaultDebounce: 200 }));
|
|
|
+ observers$1.body.addListener(playerBarSelector, {
|
|
|
+ listener: () => {
|
|
|
+ console.log("#DBG-UU enabling playerBar observer");
|
|
|
+ observers$1.playerBar.enable();
|
|
|
+ },
|
|
|
});
|
|
|
- const autoplayObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
|
|
|
- if (addedNodes.length > 0 || removedNodes.length > 0) {
|
|
|
- info(`Detected autoplay queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
|
|
|
- emitSiteEvent("autoplayQueueChanged", target);
|
|
|
- }
|
|
|
+ // #SECTION playerBarInfo = song title, artist, album, etc. inside the player bar
|
|
|
+ const playerBarInfoSelector = `${playerBarSelector} .middle-controls .content-info-wrapper`;
|
|
|
+ observers$1.playerBarInfo = new UserUtils.SelectorObserver(playerBarInfoSelector, Object.assign(Object.assign({}, defaultObserverOptions), { attributes: true, attributeFilter: ["title"] }));
|
|
|
+ observers$1.playerBarInfo.addListener(playerBarInfoSelector, {
|
|
|
+ listener: () => {
|
|
|
+ console.log("#DBG-UU enabling playerBarTitle observer");
|
|
|
+ observers$1.playerBarInfo.enable();
|
|
|
+ },
|
|
|
});
|
|
|
- autoplayObs.observe(document.querySelector("#side-panel ytmusic-player-queue #automix-contents"), {
|
|
|
- childList: true,
|
|
|
+ // #DEBUG example: listen for title change:
|
|
|
+ observers$1.playerBarInfo.addListener("yt-formatted-string.title", {
|
|
|
+ continuous: true,
|
|
|
+ listener: (titleElem) => {
|
|
|
+ console.log("#DBG-UU >>>>> title changed", titleElem.title);
|
|
|
+ },
|
|
|
});
|
|
|
- info("Successfully initialized SiteEvents observers");
|
|
|
- observers = observers.concat([
|
|
|
- queueObs,
|
|
|
- autoplayObs,
|
|
|
- ]);
|
|
|
+ emitInterface("bytm:observersReady");
|
|
|
}
|
|
|
catch (err) {
|
|
|
- error("Couldn't initialize SiteEvents observers due to an error:\n", err);
|
|
|
+ error("Failed to initialize observers:", err);
|
|
|
}
|
|
|
- });
|
|
|
-}
|
|
|
-/** Emits a site event with the given key and arguments */
|
|
|
-function emitSiteEvent(key, ...args) {
|
|
|
- siteEvents.emit(key, ...args);
|
|
|
- emitInterface(`bytm:siteEvent:${key}`, args);
|
|
|
-}/** Creates a hotkey input element */
|
|
|
-function createHotkeyInput({ initialValue, resetValue, onChange }) {
|
|
|
- var _a;
|
|
|
- const wrapperElem = document.createElement("div");
|
|
|
- wrapperElem.classList.add("bytm-hotkey-wrapper");
|
|
|
- const infoElem = document.createElement("span");
|
|
|
- infoElem.classList.add("bytm-hotkey-info");
|
|
|
- const inputElem = document.createElement("input");
|
|
|
- inputElem.type = "button";
|
|
|
- inputElem.classList.add("bytm-ftconf-input", "bytm-hotkey-input", "bytm-btn");
|
|
|
- inputElem.dataset.state = "inactive";
|
|
|
- inputElem.value = (_a = initialValue === null || initialValue === void 0 ? void 0 : initialValue.code) !== null && _a !== void 0 ? _a : t("hotkey_input_click_to_change");
|
|
|
- inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_change_tooltip");
|
|
|
- const resetElem = document.createElement("span");
|
|
|
- resetElem.classList.add("bytm-hotkey-reset", "bytm-link");
|
|
|
- resetElem.role = "button";
|
|
|
- resetElem.tabIndex = 0;
|
|
|
- resetElem.textContent = `(${t("reset")})`;
|
|
|
- const resetClicked = (e) => {
|
|
|
- e.preventDefault();
|
|
|
- e.stopImmediatePropagation();
|
|
|
- onChange(resetValue);
|
|
|
- inputElem.value = resetValue.code;
|
|
|
- inputElem.dataset.state = "inactive";
|
|
|
- infoElem.textContent = getHotkeyInfo(resetValue);
|
|
|
+ }
|
|
|
+ /** Interface function for adding listeners to the already present observers */
|
|
|
+ function addSelectorListener(observerName, selector, options) {
|
|
|
+ observers$1[observerName].addListener(selector, options);
|
|
|
+ }
|
|
|
+
|
|
|
+ var de_DE = {
|
|
|
+ name: "Deutsch (Deutschland)",
|
|
|
+ nameEnglish: "German",
|
|
|
+ emoji: "🇩🇪",
|
|
|
+ userscriptDesc: "Konfigurierbare Layout- und Benutzererfahrungs-Verbesserungen für YouTube Music",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
};
|
|
|
- resetElem.addEventListener("click", resetClicked);
|
|
|
- resetElem.addEventListener("keydown", (e) => e.key === "Enter" && resetClicked(e));
|
|
|
- if (initialValue)
|
|
|
- infoElem.textContent = getHotkeyInfo(initialValue);
|
|
|
- let lastKeyDown;
|
|
|
- document.addEventListener("keypress", (e) => {
|
|
|
- if (inputElem.dataset.state !== "active")
|
|
|
- return;
|
|
|
- if ((lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.code) === e.code && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.shift) === e.shiftKey && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.ctrl) === e.ctrlKey && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.alt) === e.altKey)
|
|
|
- return;
|
|
|
- e.preventDefault();
|
|
|
- e.stopImmediatePropagation();
|
|
|
- const hotkey = {
|
|
|
- code: e.code,
|
|
|
- shift: e.shiftKey,
|
|
|
- ctrl: e.ctrlKey,
|
|
|
- alt: e.altKey,
|
|
|
- };
|
|
|
- inputElem.value = hotkey.code;
|
|
|
- inputElem.dataset.state = "inactive";
|
|
|
- infoElem.textContent = getHotkeyInfo(hotkey);
|
|
|
- onChange(hotkey);
|
|
|
- });
|
|
|
- document.addEventListener("keydown", (e) => {
|
|
|
- if (inputElem.dataset.state !== "active")
|
|
|
- return;
|
|
|
- if (["ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight"].includes(e.code))
|
|
|
- return;
|
|
|
- e.preventDefault();
|
|
|
- e.stopImmediatePropagation();
|
|
|
- const hotkey = {
|
|
|
- code: e.code,
|
|
|
- shift: e.shiftKey,
|
|
|
- ctrl: e.ctrlKey,
|
|
|
- alt: e.altKey,
|
|
|
- };
|
|
|
- lastKeyDown = hotkey;
|
|
|
- inputElem.value = hotkey.code;
|
|
|
- inputElem.dataset.state = "inactive";
|
|
|
- infoElem.textContent = getHotkeyInfo(hotkey);
|
|
|
- inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_cancel_tooltip");
|
|
|
- onChange(hotkey);
|
|
|
- });
|
|
|
- const deactivate = () => {
|
|
|
- var _a, _b;
|
|
|
- siteEvents.emit("hotkeyInputActive", false);
|
|
|
- const curVal = (_a = getFeatures().switchSitesHotkey) !== null && _a !== void 0 ? _a : initialValue;
|
|
|
- inputElem.value = (_b = curVal === null || curVal === void 0 ? void 0 : curVal.code) !== null && _b !== void 0 ? _b : t("hotkey_input_click_to_change");
|
|
|
- inputElem.dataset.state = "inactive";
|
|
|
- inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_change_tooltip");
|
|
|
- infoElem.textContent = curVal ? getHotkeyInfo(curVal) : "";
|
|
|
+ var en_US = {
|
|
|
+ name: "English (United States)",
|
|
|
+ nameEnglish: "English",
|
|
|
+ emoji: "🇺🇸",
|
|
|
+ userscriptDesc: "Configurable layout and user experience improvements for YouTube Music",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
};
|
|
|
- const activate = () => {
|
|
|
- siteEvents.emit("hotkeyInputActive", true);
|
|
|
- inputElem.value = "< ... >";
|
|
|
- inputElem.dataset.state = "active";
|
|
|
- inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_cancel_tooltip");
|
|
|
+ var en_UK = {
|
|
|
+ name: "English (United Kingdom)",
|
|
|
+ nameEnglish: "German",
|
|
|
+ emoji: "🇩🇪",
|
|
|
+ userscriptDesc: "Configurable layout and user experience improvements for YouTube Music",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
};
|
|
|
- siteEvents.on("cfgMenuClosed", deactivate);
|
|
|
- inputElem.addEventListener("click", () => {
|
|
|
- if (inputElem.dataset.state === "active")
|
|
|
- deactivate();
|
|
|
- else
|
|
|
- activate();
|
|
|
- });
|
|
|
- wrapperElem.appendChild(infoElem);
|
|
|
- wrapperElem.appendChild(inputElem);
|
|
|
- resetValue && wrapperElem.appendChild(resetElem);
|
|
|
- return wrapperElem;
|
|
|
-}
|
|
|
-function getHotkeyInfo(hotkey) {
|
|
|
- const modifiers = [];
|
|
|
- hotkey.ctrl && modifiers.push(t("hotkey_key_ctrl"));
|
|
|
- hotkey.shift && modifiers.push(t("hotkey_key_shift"));
|
|
|
- hotkey.alt && modifiers.push(getOS() === "mac" ? t("hotkey_key_mac_option") : t("hotkey_key_alt"));
|
|
|
- return modifiers.reduce((a, c) => a += `${c} + `, "");
|
|
|
-}
|
|
|
-/** Crude OS detection for keyboard layout purposes */
|
|
|
-function getOS() {
|
|
|
- if (navigator.userAgent.match(/mac(\s?os|intel)/i))
|
|
|
- return "mac";
|
|
|
- return "other";
|
|
|
-}var name = "betterytm";
|
|
|
-var userscriptName = "BetterYTM";
|
|
|
-var version = "1.1.0";
|
|
|
-var description = "Lots of configurable layout and user experience improvements for YouTube Music";
|
|
|
-var homepage = "https://github.com/Sv443/BetterYTM";
|
|
|
-var main = "./src/index.ts";
|
|
|
-var type = "module";
|
|
|
-var scripts = {
|
|
|
- dev: "concurrently \"nodemon --exec npm run build-watch\" \"npm run serve\"",
|
|
|
- serve: "npm run node-ts -- ./src/tools/serve.ts",
|
|
|
- lint: "tsc --noEmit && eslint .",
|
|
|
- build: "rollup -c",
|
|
|
- "build-watch": "rollup -c --config-mode development --config-host github --config-branch develop",
|
|
|
- "build-develop": "rollup -c --config-mode production --config-host github --config-branch develop",
|
|
|
- "build-prod": "npm run build-prod-gh && npm run build-prod-gf && npm run build-prod-oujs",
|
|
|
- "build-prod-base": "rollup -c --config-mode production --config-branch main",
|
|
|
- "build-prod-gh": "npm run build-prod-base -- --config-host github",
|
|
|
- "build-prod-gf": "npm run build-prod-base -- --config-host greasyfork --config-suffix _gf",
|
|
|
- "build-prod-oujs": "npm run build-prod-base -- --config-host openuserjs --config-suffix _oujs",
|
|
|
- "post-build": "npm run node-ts -- ./src/tools/post-build.ts",
|
|
|
- "tr-progress": "npm run node-ts -- ./src/tools/tr-progress.ts",
|
|
|
- "tr-format": "npm run node-ts -- ./src/tools/tr-format.ts",
|
|
|
- "gen-readme": "npm run node-ts -- ./src/tools/gen-readme.ts",
|
|
|
- "node-ts": "node --no-warnings=ExperimentalWarning --enable-source-maps --loader ts-node/esm",
|
|
|
- invisible: "node src/tools/run-invisible.mjs",
|
|
|
- test: "npm run node-ts -- ./test.ts"
|
|
|
-};
|
|
|
-var engines = {
|
|
|
- node: ">=18",
|
|
|
- npm: ">=8"
|
|
|
-};
|
|
|
-var repository = {
|
|
|
- type: "git",
|
|
|
- url: "git+https://github.com/Sv443/BetterYTM.git"
|
|
|
-};
|
|
|
-var author = {
|
|
|
- name: "Sv443",
|
|
|
- url: "https://github.com/Sv443"
|
|
|
-};
|
|
|
-var license = "AGPL-3.0-only";
|
|
|
-var bugs = {
|
|
|
- url: "https://github.com/Sv443/BetterYTM/issues"
|
|
|
-};
|
|
|
-var funding = {
|
|
|
- type: "github",
|
|
|
- url: "https://github.com/sponsors/Sv443"
|
|
|
-};
|
|
|
-var hosts = {
|
|
|
- github: "https://github.com/Sv443/BetterYTM",
|
|
|
- greasyfork: "https://greasyfork.org/en/scripts/475682-betterytm",
|
|
|
- openuserjs: "https://openuserjs.org/scripts/Sv443/BetterYTM"
|
|
|
-};
|
|
|
-var updates = {
|
|
|
- github: "https://github.com/Sv443/BetterYTM/releases",
|
|
|
- greasyfork: "https://greasyfork.org/en/scripts/475682-betterytm",
|
|
|
- openuserjs: "https://openuserjs.org/scripts/Sv443/BetterYTM"
|
|
|
-};
|
|
|
-var dependencies = {
|
|
|
- nanoevents: "^9.0.0"
|
|
|
-};
|
|
|
-var devDependencies = {
|
|
|
- "@rollup/plugin-json": "^6.0.1",
|
|
|
- "@rollup/plugin-node-resolve": "^15.2.3",
|
|
|
- "@rollup/plugin-terser": "^0.4.4",
|
|
|
- "@rollup/plugin-typescript": "^11.1.5",
|
|
|
- "@sv443-network/userutils": "^4.2.1",
|
|
|
- "@types/express": "^4.17.17",
|
|
|
- "@types/greasemonkey": "^4.0.4",
|
|
|
- "@types/node": "^20.2.4",
|
|
|
- "@typescript-eslint/eslint-plugin": "^6.7.4",
|
|
|
- "@typescript-eslint/parser": "^6.7.4",
|
|
|
- concurrently: "^8.1.0",
|
|
|
- dotenv: "^16.4.1",
|
|
|
- eslint: "^8.51.0",
|
|
|
- express: "^4.18.2",
|
|
|
- marked: "^12.0.0",
|
|
|
- nodemon: "^3.0.1",
|
|
|
- rollup: "^4.6.0",
|
|
|
- "rollup-plugin-execute": "^1.1.1",
|
|
|
- "rollup-plugin-html": "^0.2.1",
|
|
|
- "rollup-plugin-import-css": "^3.3.5",
|
|
|
- "ts-node": "^10.9.1",
|
|
|
- tslib: "^2.5.2",
|
|
|
- typescript: "^5.0.4"
|
|
|
-};
|
|
|
-var browserslist = [
|
|
|
- "last 1 version",
|
|
|
- "> 1%",
|
|
|
- "not dead"
|
|
|
-];
|
|
|
-var nodemonConfig = {
|
|
|
- watch: [
|
|
|
- "src/**",
|
|
|
- "assets/**",
|
|
|
- "rollup.config.mjs",
|
|
|
- ".env",
|
|
|
- "changelog.md",
|
|
|
- "package.json"
|
|
|
- ],
|
|
|
- ext: "ts,mts,js,jsx,mjs,json,html,css,svg,png",
|
|
|
- ignore: [
|
|
|
- "dist/*",
|
|
|
- "dev/*"
|
|
|
- ]
|
|
|
-};
|
|
|
-var pkg = {
|
|
|
- name: name,
|
|
|
- userscriptName: userscriptName,
|
|
|
- version: version,
|
|
|
- description: description,
|
|
|
- homepage: homepage,
|
|
|
- main: main,
|
|
|
- type: type,
|
|
|
- scripts: scripts,
|
|
|
- engines: engines,
|
|
|
- repository: repository,
|
|
|
- author: author,
|
|
|
- license: license,
|
|
|
- bugs: bugs,
|
|
|
- funding: funding,
|
|
|
- hosts: hosts,
|
|
|
- updates: updates,
|
|
|
- dependencies: dependencies,
|
|
|
- devDependencies: devDependencies,
|
|
|
- browserslist: browserslist,
|
|
|
- nodemonConfig: nodemonConfig
|
|
|
-};//#MARKER create menu elements
|
|
|
-let isCfgMenuAdded = false;
|
|
|
-let isCfgMenuOpen = false;
|
|
|
-const compressionFormat = "deflate-raw";
|
|
|
-function compressionSupported() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- yield UserUtils.compress(".", compressionFormat);
|
|
|
- return true;
|
|
|
- }
|
|
|
- catch (e) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-/** Threshold in pixels from the top of the options container that dictates for how long the scroll indicator is shown */
|
|
|
-const scrollIndicatorOffsetThreshold = 30;
|
|
|
-let scrollIndicatorEnabled = true;
|
|
|
-/** Locale at the point of initializing the config menu */
|
|
|
-let initLocale;
|
|
|
-/** Stringified config at the point of initializing the config menu */
|
|
|
-let initConfig$1;
|
|
|
-/**
|
|
|
- * Adds an element to open the BetterYTM menu
|
|
|
- * @deprecated to be replaced with new menu - see https://github.com/Sv443/BetterYTM/issues/23
|
|
|
- */
|
|
|
-function addCfgMenu() {
|
|
|
- var _a, _b, _c, _d, _e;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (isCfgMenuAdded)
|
|
|
- return;
|
|
|
- isCfgMenuAdded = true;
|
|
|
- initLocale = getFeatures().locale;
|
|
|
- initConfig$1 = JSON.stringify(getFeatures());
|
|
|
- const initLangReloadText = t("lang_changed_prompt_reload");
|
|
|
- const toggled_on = t("toggled_on");
|
|
|
- const toggled_off = t("toggled_off");
|
|
|
- //#SECTION backdrop & menu container
|
|
|
- const backgroundElem = document.createElement("div");
|
|
|
- backgroundElem.id = "bytm-cfg-menu-bg";
|
|
|
- backgroundElem.classList.add("bytm-menu-bg");
|
|
|
- backgroundElem.ariaLabel = backgroundElem.title = t("close_menu_tooltip");
|
|
|
- backgroundElem.style.visibility = "hidden";
|
|
|
- backgroundElem.style.display = "none";
|
|
|
- backgroundElem.addEventListener("click", (e) => {
|
|
|
- var _a;
|
|
|
- if (isCfgMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-cfg-menu-bg")
|
|
|
- closeCfgMenu(e);
|
|
|
- });
|
|
|
- document.body.addEventListener("keydown", (e) => {
|
|
|
- if (isCfgMenuOpen && e.key === "Escape")
|
|
|
- closeCfgMenu(e);
|
|
|
- });
|
|
|
- const menuContainer = document.createElement("div");
|
|
|
- menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
- menuContainer.classList.add("bytm-menu");
|
|
|
- menuContainer.id = "bytm-cfg-menu";
|
|
|
- //#SECTION title bar
|
|
|
- const headerElem = document.createElement("div");
|
|
|
- headerElem.classList.add("bytm-menu-header");
|
|
|
- const titleCont = document.createElement("div");
|
|
|
- titleCont.className = "bytm-menu-titlecont";
|
|
|
- titleCont.role = "heading";
|
|
|
- titleCont.ariaLevel = "1";
|
|
|
- const titleElem = document.createElement("h2");
|
|
|
- titleElem.className = "bytm-menu-title";
|
|
|
- const titleTextElem = document.createElement("div");
|
|
|
- titleTextElem.textContent = t("config_menu_title", scriptInfo.name);
|
|
|
- titleElem.appendChild(titleTextElem);
|
|
|
- const linksCont = document.createElement("div");
|
|
|
- linksCont.id = "bytm-menu-linkscont";
|
|
|
- linksCont.role = "navigation";
|
|
|
- const addLink = (imgSrc, href, title) => {
|
|
|
- const anchorElem = document.createElement("a");
|
|
|
- anchorElem.className = "bytm-menu-link bytm-no-select";
|
|
|
- anchorElem.rel = "noopener noreferrer";
|
|
|
- anchorElem.href = href;
|
|
|
- anchorElem.target = "_blank";
|
|
|
- anchorElem.tabIndex = 0;
|
|
|
- anchorElem.role = "button";
|
|
|
- anchorElem.ariaLabel = anchorElem.title = title;
|
|
|
- const imgElem = document.createElement("img");
|
|
|
- imgElem.className = "bytm-menu-img";
|
|
|
- imgElem.src = imgSrc;
|
|
|
- imgElem.style.width = "32px";
|
|
|
- imgElem.style.height = "32px";
|
|
|
- anchorElem.appendChild(imgElem);
|
|
|
- linksCont.appendChild(anchorElem);
|
|
|
- };
|
|
|
- addLink(yield getResourceUrl("img-discord"), "https://dc.sv443.net/", t("open_discord"));
|
|
|
- const links = [
|
|
|
- ["github", yield getResourceUrl("img-github"), scriptInfo.namespace, t("open_github", scriptInfo.name)],
|
|
|
- ["greasyfork", yield getResourceUrl("img-greasyfork"), pkg.hosts.greasyfork, t("open_greasyfork", scriptInfo.name)],
|
|
|
- ["openuserjs", yield getResourceUrl("img-openuserjs"), pkg.hosts.openuserjs, t("open_openuserjs", scriptInfo.name)],
|
|
|
- ];
|
|
|
- const hostLink = links.find(([name]) => name === host);
|
|
|
- const otherLinks = links.filter(([name]) => name !== host);
|
|
|
- const reorderedLinks = hostLink ? [hostLink, ...otherLinks] : links;
|
|
|
- for (const [, ...args] of reorderedLinks)
|
|
|
- addLink(...args);
|
|
|
- const closeElem = document.createElement("img");
|
|
|
- closeElem.classList.add("bytm-menu-close");
|
|
|
- closeElem.role = "button";
|
|
|
- closeElem.tabIndex = 0;
|
|
|
- closeElem.src = yield getResourceUrl("img-close");
|
|
|
- closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
- closeElem.addEventListener("click", closeCfgMenu);
|
|
|
- closeElem.addEventListener("keydown", ({ key }) => key === "Enter" && closeCfgMenu());
|
|
|
- titleCont.appendChild(titleElem);
|
|
|
- titleCont.appendChild(linksCont);
|
|
|
- headerElem.appendChild(titleCont);
|
|
|
- headerElem.appendChild(closeElem);
|
|
|
- //#SECTION footer
|
|
|
- const footerCont = document.createElement("div");
|
|
|
- footerCont.className = "bytm-menu-footer-cont";
|
|
|
- const footerElemCont = document.createElement("div");
|
|
|
- const footerElem = document.createElement("div");
|
|
|
- footerElem.classList.add("bytm-menu-footer", "hidden");
|
|
|
- footerElem.textContent = t("reload_hint");
|
|
|
- const reloadElem = document.createElement("button");
|
|
|
- reloadElem.classList.add("bytm-btn");
|
|
|
- reloadElem.style.marginLeft = "10px";
|
|
|
- reloadElem.textContent = t("reload_now");
|
|
|
- reloadElem.ariaLabel = reloadElem.title = t("reload_tooltip");
|
|
|
- reloadElem.addEventListener("click", () => {
|
|
|
- closeCfgMenu();
|
|
|
- disableBeforeUnload();
|
|
|
- location.reload();
|
|
|
- });
|
|
|
- footerElem.appendChild(reloadElem);
|
|
|
- footerElemCont.appendChild(footerElem);
|
|
|
- const resetElem = document.createElement("button");
|
|
|
- resetElem.classList.add("bytm-btn");
|
|
|
- resetElem.ariaLabel = resetElem.title = t("reset_tooltip");
|
|
|
- resetElem.textContent = t("reset");
|
|
|
- resetElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (confirm(t("reset_confirm"))) {
|
|
|
- yield setDefaultFeatures();
|
|
|
- closeCfgMenu();
|
|
|
- disableBeforeUnload();
|
|
|
- location.reload();
|
|
|
- }
|
|
|
- }));
|
|
|
- const exportElem = document.createElement("button");
|
|
|
- exportElem.classList.add("bytm-btn");
|
|
|
- exportElem.ariaLabel = exportElem.title = t("export_tooltip");
|
|
|
- exportElem.textContent = t("export");
|
|
|
- exportElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- yield openExportMenu();
|
|
|
- closeCfgMenu(undefined, false);
|
|
|
- }));
|
|
|
- const importElem = document.createElement("button");
|
|
|
- importElem.classList.add("bytm-btn");
|
|
|
- importElem.ariaLabel = importElem.title = t("import_tooltip");
|
|
|
- importElem.textContent = t("import");
|
|
|
- importElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- yield openImportMenu();
|
|
|
- closeCfgMenu(undefined, false);
|
|
|
- }));
|
|
|
- const buttonsCont = document.createElement("div");
|
|
|
- buttonsCont.id = "bytm-menu-footer-buttons-cont";
|
|
|
- buttonsCont.appendChild(exportElem);
|
|
|
- buttonsCont.appendChild(importElem);
|
|
|
- buttonsCont.appendChild(resetElem);
|
|
|
- footerCont.appendChild(footerElemCont);
|
|
|
- footerCont.appendChild(buttonsCont);
|
|
|
- //#SECTION feature list
|
|
|
- const featuresCont = document.createElement("div");
|
|
|
- featuresCont.id = "bytm-menu-opts";
|
|
|
- /** Gets called whenever the feature config is changed */
|
|
|
- const confChanged = UserUtils.debounce((key, initialVal, newVal) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const fmt = (val) => typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
|
- info(`Feature config changed at key '${key}', from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
|
|
|
- const featConf = JSON.parse(JSON.stringify(getFeatures()));
|
|
|
- featConf[key] = newVal;
|
|
|
- yield saveFeatures(featConf);
|
|
|
- if (initConfig$1 !== JSON.stringify(featConf))
|
|
|
- footerElem.classList.remove("hidden");
|
|
|
- else
|
|
|
- footerElem.classList.add("hidden");
|
|
|
- if (initLocale !== featConf.locale) {
|
|
|
- yield initTranslations(featConf.locale);
|
|
|
- setLocale(featConf.locale);
|
|
|
- const newText = t("lang_changed_prompt_reload");
|
|
|
- const confirmText = newText !== initLangReloadText ? `${newText}\n\n────────────────────────────────\n\n${initLangReloadText}` : newText;
|
|
|
- if (confirm(confirmText)) {
|
|
|
- closeCfgMenu();
|
|
|
- disableBeforeUnload();
|
|
|
- location.reload();
|
|
|
- }
|
|
|
- }
|
|
|
- else if (getLocale() !== featConf.locale)
|
|
|
- setLocale(featConf.locale);
|
|
|
- }));
|
|
|
- const featureCfg = getFeatures();
|
|
|
- const featureCfgWithCategories = Object.entries(featInfo)
|
|
|
- .reduce((acc, [key, { category }]) => {
|
|
|
- if (!acc[category])
|
|
|
- acc[category] = {};
|
|
|
- acc[category][key] = featureCfg[key];
|
|
|
- return acc;
|
|
|
- }, {});
|
|
|
- const fmtVal = (v) => String(v).trim();
|
|
|
- const toggleLabelText = (toggled) => toggled ? toggled_on : toggled_off;
|
|
|
- for (const category in featureCfgWithCategories) {
|
|
|
- const featObj = featureCfgWithCategories[category];
|
|
|
- const catHeaderElem = document.createElement("h3");
|
|
|
- catHeaderElem.classList.add("bytm-ftconf-category-header");
|
|
|
- catHeaderElem.role = "heading";
|
|
|
- catHeaderElem.ariaLevel = "2";
|
|
|
- catHeaderElem.textContent = `${t(`feature_category_${category}`)}:`;
|
|
|
- featuresCont.appendChild(catHeaderElem);
|
|
|
- for (const featKey in featObj) {
|
|
|
- const ftInfo = featInfo[featKey];
|
|
|
- // @ts-ignore
|
|
|
- if (!ftInfo || ftInfo.hidden === true)
|
|
|
- continue;
|
|
|
- const { type, default: ftDefault } = ftInfo;
|
|
|
+ var es_ES = {
|
|
|
+ name: "Español (España)",
|
|
|
+ nameEnglish: "Spanish",
|
|
|
+ emoji: "🇪🇸",
|
|
|
+ userscriptDesc: "Mejoras de diseño y experiencia de usuario configurables para YouTube Music",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ var fr_FR = {
|
|
|
+ name: "Français (France)",
|
|
|
+ nameEnglish: "French",
|
|
|
+ emoji: "🇫🇷",
|
|
|
+ userscriptDesc: "Améliorations de la mise en page et de l'expérience utilisateur configurables pour YouTube Music",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ var hi_IN = {
|
|
|
+ name: "हिंदी (भारत)",
|
|
|
+ nameEnglish: "Hindi",
|
|
|
+ emoji: "🇮🇳",
|
|
|
+ userscriptDesc: "YouTube Music के लिए विन्यास और यूजर अनुभव में सुधार करने योग्य लेआउट और यूजर अनुभव सुधार",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ var ja_JA = {
|
|
|
+ name: "日本語 (日本)",
|
|
|
+ nameEnglish: "Japanese",
|
|
|
+ emoji: "🇯🇵",
|
|
|
+ userscriptDesc: "YouTube Musicのレイアウトとユーザーエクスペリエンスの改善を設定可能にする",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ var pt_BR = {
|
|
|
+ name: "Português (Brasil)",
|
|
|
+ nameEnglish: "Portuguese",
|
|
|
+ emoji: "🇵🇹",
|
|
|
+ userscriptDesc: "Melhorias configuráveis no layout e na experiência do usuário para o YouTube Music",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ var zh_CN = {
|
|
|
+ name: "中文(简化,中国)",
|
|
|
+ nameEnglish: "Chinese (simpl.)",
|
|
|
+ emoji: "🇨🇳",
|
|
|
+ userscriptDesc: "可配置的布局和YouTube Music的用户体验改进",
|
|
|
+ authors: [
|
|
|
+ "Sv443"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ var locales = {
|
|
|
+ de_DE: de_DE,
|
|
|
+ en_US: en_US,
|
|
|
+ en_UK: en_UK,
|
|
|
+ es_ES: es_ES,
|
|
|
+ fr_FR: fr_FR,
|
|
|
+ hi_IN: hi_IN,
|
|
|
+ ja_JA: ja_JA,
|
|
|
+ pt_BR: pt_BR,
|
|
|
+ zh_CN: zh_CN
|
|
|
+ };
|
|
|
+
|
|
|
+ let features$3;
|
|
|
+ function setBehaviorConfig(feats) {
|
|
|
+ features$3 = feats;
|
|
|
+ }
|
|
|
+ //#MARKER beforeunload popup
|
|
|
+ let beforeUnloadEnabled = true;
|
|
|
+ /** Disables the popup before leaving the site */
|
|
|
+ function disableBeforeUnload() {
|
|
|
+ beforeUnloadEnabled = false;
|
|
|
+ info("Disabled popup before leaving the site");
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
|
|
|
+ * event listeners before they can be called by the site.
|
|
|
+ */
|
|
|
+ function initBeforeUnloadHook() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
|
|
|
+ (function (original) {
|
|
|
// @ts-ignore
|
|
|
- const step = (_a = ftInfo === null || ftInfo === void 0 ? void 0 : ftInfo.step) !== null && _a !== void 0 ? _a : undefined;
|
|
|
- const val = featureCfg[featKey];
|
|
|
- const initialVal = (_b = val !== null && val !== void 0 ? val : ftDefault) !== null && _b !== void 0 ? _b : undefined;
|
|
|
- const ftConfElem = document.createElement("div");
|
|
|
- ftConfElem.classList.add("bytm-ftitem");
|
|
|
- {
|
|
|
- const featLeftSideElem = document.createElement("div");
|
|
|
- featLeftSideElem.classList.add("bytm-ftitem-leftside");
|
|
|
- const textElem = document.createElement("span");
|
|
|
- textElem.textContent = t(`feature_desc_${featKey}`);
|
|
|
- let adornmentElem;
|
|
|
- const adornContent = (_c = ftInfo.textAdornment) === null || _c === void 0 ? void 0 : _c.call(ftInfo);
|
|
|
- if (typeof adornContent === "string" || adornContent instanceof Promise) {
|
|
|
- adornmentElem = document.createElement("span");
|
|
|
- adornmentElem.id = `bytm-ftitem-${featKey}-adornment`;
|
|
|
- adornmentElem.classList.add("bytm-ftitem-adornment");
|
|
|
- adornmentElem.innerHTML = adornContent instanceof Promise ? yield adornContent : adornContent;
|
|
|
- }
|
|
|
- let helpElem;
|
|
|
- // @ts-ignore
|
|
|
- const hasHelpTextFunc = typeof ((_d = featInfo[featKey]) === null || _d === void 0 ? void 0 : _d.helpText) === "function";
|
|
|
- // @ts-ignore
|
|
|
- const helpTextVal = hasHelpTextFunc && featInfo[featKey].helpText();
|
|
|
- if (hasKey(`feature_helptext_${featKey}`) || (helpTextVal && hasKey(helpTextVal))) {
|
|
|
- const helpElemImgHtml = yield resourceToHTMLString("img-help");
|
|
|
- if (helpElemImgHtml) {
|
|
|
- helpElem = document.createElement("div");
|
|
|
- helpElem.classList.add("bytm-ftitem-help-btn", "bytm-generic-btn");
|
|
|
- helpElem.ariaLabel = helpElem.title = t("feature_help_button_tooltip");
|
|
|
- helpElem.role = "button";
|
|
|
- helpElem.tabIndex = 0;
|
|
|
- helpElem.innerHTML = helpElemImgHtml;
|
|
|
- const helpElemClicked = (e) => {
|
|
|
- e.preventDefault();
|
|
|
- e.stopPropagation();
|
|
|
- openHelpDialog(featKey);
|
|
|
- };
|
|
|
- helpElem.addEventListener("click", helpElemClicked);
|
|
|
- helpElem.addEventListener("keydown", (e) => e.key === "Enter" && helpElemClicked(e));
|
|
|
- }
|
|
|
- else {
|
|
|
- error(`Couldn't create help button SVG element for feature '${featKey}'`);
|
|
|
- }
|
|
|
- }
|
|
|
- featLeftSideElem.appendChild(textElem);
|
|
|
- adornmentElem && featLeftSideElem.appendChild(adornmentElem);
|
|
|
- helpElem && featLeftSideElem.appendChild(helpElem);
|
|
|
- ftConfElem.appendChild(featLeftSideElem);
|
|
|
- }
|
|
|
- {
|
|
|
- let inputType = "text";
|
|
|
- let inputTag = "input";
|
|
|
- switch (type) {
|
|
|
- case "toggle":
|
|
|
- inputType = "checkbox";
|
|
|
- break;
|
|
|
- case "slider":
|
|
|
- inputType = "range";
|
|
|
- break;
|
|
|
- case "number":
|
|
|
- inputType = "number";
|
|
|
- break;
|
|
|
- case "select":
|
|
|
- inputTag = "select";
|
|
|
- inputType = undefined;
|
|
|
- break;
|
|
|
- case "hotkey":
|
|
|
- inputTag = undefined;
|
|
|
- inputType = undefined;
|
|
|
- break;
|
|
|
- }
|
|
|
- const inputElemId = `bytm-ftconf-${featKey}-input`;
|
|
|
- const ctrlElem = document.createElement("span");
|
|
|
- ctrlElem.classList.add("bytm-ftconf-ctrl");
|
|
|
- if (inputTag) {
|
|
|
- // standard input element:
|
|
|
- const inputElem = document.createElement(inputTag);
|
|
|
- inputElem.classList.add("bytm-ftconf-input");
|
|
|
- inputElem.id = inputElemId;
|
|
|
- if (inputType)
|
|
|
- inputElem.type = inputType;
|
|
|
- // @ts-ignore
|
|
|
- if (typeof ftInfo.min !== "undefined" && ftInfo.max !== "undefined") {
|
|
|
- // @ts-ignore
|
|
|
- inputElem.min = ftInfo.min;
|
|
|
- // @ts-ignore
|
|
|
- inputElem.max = ftInfo.max;
|
|
|
- }
|
|
|
- if (typeof initialVal !== "undefined")
|
|
|
- inputElem.value = String(initialVal);
|
|
|
- if (type === "number" || type === "slider" && step)
|
|
|
- inputElem.step = String(step);
|
|
|
- if (type === "toggle" && typeof initialVal !== "undefined")
|
|
|
- inputElem.checked = Boolean(initialVal);
|
|
|
- // @ts-ignore
|
|
|
- const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
|
|
|
- let labelElem;
|
|
|
- if (type === "slider") {
|
|
|
- labelElem = document.createElement("label");
|
|
|
- labelElem.classList.add("bytm-ftconf-label", "bytm-slider-label");
|
|
|
- labelElem.textContent = fmtVal(initialVal) + unitTxt;
|
|
|
- inputElem.addEventListener("input", () => {
|
|
|
- if (labelElem)
|
|
|
- labelElem.textContent = fmtVal(Number(inputElem.value)) + unitTxt;
|
|
|
- });
|
|
|
- }
|
|
|
- else if (type === "toggle") {
|
|
|
- labelElem = document.createElement("label");
|
|
|
- labelElem.classList.add("bytm-ftconf-label", "bytm-toggle-label");
|
|
|
- labelElem.textContent = toggleLabelText(Boolean(initialVal)) + unitTxt;
|
|
|
- inputElem.addEventListener("input", () => {
|
|
|
- if (labelElem)
|
|
|
- labelElem.textContent = toggleLabelText(inputElem.checked) + unitTxt;
|
|
|
- });
|
|
|
+ window.__proto__.addEventListener = function (...args) {
|
|
|
+ const origListener = typeof args[1] === "function" ? args[1] : args[1].handleEvent;
|
|
|
+ args[1] = function (...a) {
|
|
|
+ if (!beforeUnloadEnabled && args[0] === "beforeunload") {
|
|
|
+ info("Prevented beforeunload event listener from being called");
|
|
|
+ return false;
|
|
|
}
|
|
|
- else if (type === "select") {
|
|
|
- const ftOpts = typeof ftInfo.options === "function"
|
|
|
- ? ftInfo.options()
|
|
|
- : ftInfo.options;
|
|
|
- for (const { value, label } of ftOpts) {
|
|
|
- const optionElem = document.createElement("option");
|
|
|
- optionElem.value = String(value);
|
|
|
- optionElem.textContent = label;
|
|
|
- if (value === initialVal)
|
|
|
- optionElem.selected = true;
|
|
|
- inputElem.appendChild(optionElem);
|
|
|
- }
|
|
|
- }
|
|
|
- inputElem.addEventListener("input", () => {
|
|
|
- let v = String(inputElem.value).trim();
|
|
|
- if (["number", "slider"].includes(type) || v.match(/^-?\d+$/))
|
|
|
- v = Number(v);
|
|
|
- if (typeof initialVal !== "undefined")
|
|
|
- confChanged(featKey, initialVal, (type !== "toggle" ? v : inputElem.checked));
|
|
|
- });
|
|
|
- if (labelElem) {
|
|
|
- labelElem.id = `bytm-ftconf-${featKey}-label`;
|
|
|
- labelElem.htmlFor = inputElemId;
|
|
|
- ctrlElem.appendChild(labelElem);
|
|
|
+ else
|
|
|
+ return origListener.apply(this, a);
|
|
|
+ };
|
|
|
+ original.apply(this, args);
|
|
|
+ };
|
|
|
+ // @ts-ignore
|
|
|
+ })(window.__proto__.addEventListener);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ //#MARKER auto close toasts
|
|
|
+ /** Closes toasts after a set amount of time */
|
|
|
+ function initAutoCloseToasts() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ const animTimeout = 300;
|
|
|
+ const closeTimeout = Math.max(features$3.closeToastsTimeout * 1000 + animTimeout, animTimeout);
|
|
|
+ onSelectorOld("tp-yt-paper-toast#toast", {
|
|
|
+ all: true,
|
|
|
+ continuous: true,
|
|
|
+ listener: (toastElems) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ var _a;
|
|
|
+ for (const toastElem of toastElems) {
|
|
|
+ if (!toastElem.hasAttribute("allow-click-through"))
|
|
|
+ continue;
|
|
|
+ if (toastElem.classList.contains("bytm-closing"))
|
|
|
+ continue;
|
|
|
+ toastElem.classList.add("bytm-closing");
|
|
|
+ yield UserUtils.pauseFor(closeTimeout);
|
|
|
+ toastElem.classList.remove("paper-toast-open");
|
|
|
+ log(`Automatically closed toast '${(_a = toastElem.querySelector("#text-container yt-formatted-string")) === null || _a === void 0 ? void 0 : _a.textContent}' after ${features$3.closeToastsTimeout * 1000}ms`);
|
|
|
+ // wait for the transition to finish
|
|
|
+ yield UserUtils.pauseFor(animTimeout);
|
|
|
+ toastElem.style.display = "none";
|
|
|
}
|
|
|
- ctrlElem.appendChild(inputElem);
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ log("Initialized automatic toast closing");
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Error in automatic toast closing:", err);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** After how many milliseconds a remembered entry should expire */
|
|
|
+ const remSongEntryExpiry = 1000 * 60 * 1;
|
|
|
+ /** Minimum time a song has to be played before it is committed to GM storage */
|
|
|
+ const remSongMinPlayTime = 10;
|
|
|
+ let remSongsCache = [];
|
|
|
+ /** Remembers the time of the last played song and resumes playback from that time */
|
|
|
+ function initRememberSongTime() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (features$3.rememberSongTimeSites !== "all" && features$3.rememberSongTimeSites !== getDomain())
|
|
|
+ return;
|
|
|
+ const storedDataRaw = yield GM.getValue("bytm-rem-songs");
|
|
|
+ if (!storedDataRaw)
|
|
|
+ yield GM.setValue("bytm-rem-songs", "[]");
|
|
|
+ remSongsCache = JSON.parse(String(storedDataRaw !== null && storedDataRaw !== void 0 ? storedDataRaw : "[]"));
|
|
|
+ log(`Initialized song time remembering with ${remSongsCache.length} initial entries`);
|
|
|
+ if (location.pathname.startsWith("/watch"))
|
|
|
+ yield restoreSongTime();
|
|
|
+ remSongUpdateEntry();
|
|
|
+ setInterval(remSongUpdateEntry, 1000);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Tries to restore the time of the currently playing song */
|
|
|
+ function restoreSongTime() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (location.pathname.startsWith("/watch")) {
|
|
|
+ const { searchParams } = new URL(location.href);
|
|
|
+ const watchID = searchParams.get("v");
|
|
|
+ if (!watchID)
|
|
|
+ return;
|
|
|
+ const entry = remSongsCache.find(entry => entry.watchID === watchID);
|
|
|
+ if (entry) {
|
|
|
+ if (Date.now() - entry.updateTimestamp > remSongEntryExpiry) {
|
|
|
+ yield delRemSongData(entry.watchID);
|
|
|
+ return;
|
|
|
}
|
|
|
else {
|
|
|
- // custom input element:
|
|
|
- let wrapperElem;
|
|
|
- switch (type) {
|
|
|
- case "hotkey":
|
|
|
- wrapperElem = createHotkeyInput({
|
|
|
- initialValue: initialVal,
|
|
|
- resetValue: featInfo.switchSitesHotkey.default,
|
|
|
- onChange: (hotkey) => {
|
|
|
- confChanged(featKey, initialVal, hotkey);
|
|
|
- },
|
|
|
- });
|
|
|
- break;
|
|
|
- }
|
|
|
- ctrlElem.appendChild(wrapperElem);
|
|
|
+ onSelectorOld(videoSelector, {
|
|
|
+ listener: (vidElem) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (vidElem) {
|
|
|
+ const applyTime = () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (isNaN(entry.songTime))
|
|
|
+ return;
|
|
|
+ vidElem.currentTime = UserUtils.clamp(Math.max(entry.songTime, 0), 0, vidElem.duration);
|
|
|
+ yield delRemSongData(entry.watchID);
|
|
|
+ info(`Restored song time to ${Math.floor(entry.songTime / 60)}m, ${(entry.songTime % 60).toFixed(1)}s`, LogLevel.Info);
|
|
|
+ });
|
|
|
+ if (vidElem.readyState === 4)
|
|
|
+ applyTime();
|
|
|
+ else
|
|
|
+ vidElem.addEventListener("canplay", applyTime, { once: true });
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ });
|
|
|
}
|
|
|
- ftConfElem.appendChild(ctrlElem);
|
|
|
}
|
|
|
- featuresCont.appendChild(ftConfElem);
|
|
|
}
|
|
|
- }
|
|
|
- //#SECTION set values of inputs on external change
|
|
|
- siteEvents.on("rebuildCfgMenu", (newConfig) => {
|
|
|
- for (const ftKey in featInfo) {
|
|
|
- const ftElem = document.querySelector(`#bytm-ftconf-${ftKey}-input`);
|
|
|
- const labelElem = document.querySelector(`#bytm-ftconf-${ftKey}-label`);
|
|
|
- if (!ftElem)
|
|
|
- continue;
|
|
|
- const ftInfo = featInfo[ftKey];
|
|
|
- const value = newConfig[ftKey];
|
|
|
- if (ftInfo.type === "toggle")
|
|
|
- ftElem.checked = Boolean(value);
|
|
|
- else
|
|
|
- ftElem.value = String(value);
|
|
|
- if (!labelElem)
|
|
|
- continue;
|
|
|
- // @ts-ignore
|
|
|
- const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
|
|
|
- if (ftInfo.type === "slider")
|
|
|
- labelElem.textContent = fmtVal(Number(value)) + unitTxt;
|
|
|
- else if (ftInfo.type === "toggle")
|
|
|
- labelElem.textContent = toggleLabelText(Boolean(value)) + unitTxt;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Updates the currently playing song's entry in GM storage */
|
|
|
+ function remSongUpdateEntry() {
|
|
|
+ var _a, _b, _c;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (location.pathname.startsWith("/watch")) {
|
|
|
+ const { searchParams } = new URL(location.href);
|
|
|
+ const watchID = searchParams.get("v");
|
|
|
+ if (!watchID)
|
|
|
+ return;
|
|
|
+ const songTime = (_a = yield getVideoTime()) !== null && _a !== void 0 ? _a : 0;
|
|
|
+ const paused = (_c = (_b = document.querySelector(videoSelector)) === null || _b === void 0 ? void 0 : _b.paused) !== null && _c !== void 0 ? _c : false;
|
|
|
+ // don't immediately update to reduce race conditions and only update if the video is playing
|
|
|
+ // also it just sounds better if the song starts at the beginning if only a couple seconds have passed
|
|
|
+ if (songTime > remSongMinPlayTime && !paused) {
|
|
|
+ const entry = {
|
|
|
+ watchID,
|
|
|
+ songTime,
|
|
|
+ updateTimestamp: Date.now(),
|
|
|
+ };
|
|
|
+ yield setRemSongData(entry);
|
|
|
+ }
|
|
|
+ // if the song is rewound to the beginning, delete the entry
|
|
|
+ else {
|
|
|
+ const entry = remSongsCache.find(entry => entry.watchID === watchID);
|
|
|
+ if (entry && songTime <= remSongMinPlayTime)
|
|
|
+ yield delRemSongData(entry.watchID);
|
|
|
+ }
|
|
|
}
|
|
|
- info("Rebuilt config menu");
|
|
|
+ const expiredEntries = remSongsCache.filter(entry => Date.now() - entry.updateTimestamp > remSongEntryExpiry);
|
|
|
+ for (const entry of expiredEntries)
|
|
|
+ yield delRemSongData(entry.watchID);
|
|
|
});
|
|
|
- //#SECTION scroll indicator
|
|
|
- const scrollIndicator = document.createElement("img");
|
|
|
- scrollIndicator.id = "bytm-menu-scroll-indicator";
|
|
|
- scrollIndicator.src = yield getResourceUrl("img-arrow_down");
|
|
|
- scrollIndicator.role = "button";
|
|
|
- scrollIndicator.ariaLabel = scrollIndicator.title = t("scroll_to_bottom");
|
|
|
- featuresCont.appendChild(scrollIndicator);
|
|
|
- scrollIndicator.addEventListener("click", () => {
|
|
|
- const bottomAnchor = document.querySelector("#bytm-menu-bottom-anchor");
|
|
|
- bottomAnchor === null || bottomAnchor === void 0 ? void 0 : bottomAnchor.scrollIntoView({
|
|
|
- behavior: "smooth",
|
|
|
- });
|
|
|
+ }
|
|
|
+ /** Adds an entry or updates it if it already exists */
|
|
|
+ function setRemSongData(data) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const foundIdx = remSongsCache.findIndex(entry => entry.watchID === data.watchID);
|
|
|
+ if (foundIdx >= 0)
|
|
|
+ remSongsCache[foundIdx] = data;
|
|
|
+ else
|
|
|
+ remSongsCache.push(data);
|
|
|
+ yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
|
|
|
});
|
|
|
- featuresCont.addEventListener("scroll", (evt) => {
|
|
|
- var _a, _b;
|
|
|
- const scrollPos = (_b = (_a = evt.target) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0;
|
|
|
- const scrollIndicator = document.querySelector("#bytm-menu-scroll-indicator");
|
|
|
- if (!scrollIndicator)
|
|
|
- return;
|
|
|
- if (scrollIndicatorEnabled && scrollPos > scrollIndicatorOffsetThreshold && !scrollIndicator.classList.contains("bytm-hidden")) {
|
|
|
- scrollIndicator.classList.add("bytm-hidden");
|
|
|
+ }
|
|
|
+ /** Deletes an entry */
|
|
|
+ function delRemSongData(watchID) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ remSongsCache = [...remSongsCache.filter(entry => entry.watchID !== watchID)];
|
|
|
+ yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
|
|
|
+ });
|
|
|
+ }
|
|
|
+ //#MARKER disable darkreader
|
|
|
+ /** Disables Dark Reader if it is enabled */
|
|
|
+ function disableDarkReader() {
|
|
|
+ if (document.querySelector(".darkreader")) {
|
|
|
+ const metaElem = document.createElement("meta");
|
|
|
+ metaElem.name = "darkreader-lock";
|
|
|
+ metaElem.classList.add("bytm-disable-darkreader");
|
|
|
+ document.head.appendChild(metaElem);
|
|
|
+ info("Sent hint to Dark Reader to disable itself");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** EventEmitter instance that is used to detect changes to the site */
|
|
|
+ const siteEvents = createNanoEvents();
|
|
|
+ let observers = [];
|
|
|
+ /** Creates MutationObservers that check if parts of the site have changed, then emit an event on the `siteEvents` instance. */
|
|
|
+ function initSiteEvents() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ //#SECTION queue
|
|
|
+ // the queue container always exists so it doesn't need an extra init function
|
|
|
+ const queueObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
|
|
|
+ if (addedNodes.length > 0 || removedNodes.length > 0) {
|
|
|
+ info(`Detected queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
|
|
|
+ emitSiteEvent("queueChanged", target);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // only observe added or removed elements
|
|
|
+ queueObs.observe(document.querySelector("#side-panel #contents.ytmusic-player-queue"), {
|
|
|
+ childList: true,
|
|
|
+ });
|
|
|
+ const autoplayObs = new MutationObserver(([{ addedNodes, removedNodes, target }]) => {
|
|
|
+ if (addedNodes.length > 0 || removedNodes.length > 0) {
|
|
|
+ info(`Detected autoplay queue change - added nodes: ${[...addedNodes.values()].length} - removed nodes: ${[...removedNodes.values()].length}`);
|
|
|
+ emitSiteEvent("autoplayQueueChanged", target);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ autoplayObs.observe(document.querySelector("#side-panel ytmusic-player-queue #automix-contents"), {
|
|
|
+ childList: true,
|
|
|
+ });
|
|
|
+ info("Successfully initialized SiteEvents observers");
|
|
|
+ observers = observers.concat([
|
|
|
+ queueObs,
|
|
|
+ autoplayObs,
|
|
|
+ ]);
|
|
|
}
|
|
|
- else if (scrollIndicatorEnabled && scrollPos <= scrollIndicatorOffsetThreshold && scrollIndicator.classList.contains("bytm-hidden")) {
|
|
|
- scrollIndicator.classList.remove("bytm-hidden");
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't initialize SiteEvents observers due to an error:\n", err);
|
|
|
}
|
|
|
});
|
|
|
- const bottomAnchor = document.createElement("div");
|
|
|
- bottomAnchor.id = "bytm-menu-bottom-anchor";
|
|
|
- featuresCont.appendChild(bottomAnchor);
|
|
|
- //#SECTION finalize
|
|
|
- menuContainer.appendChild(headerElem);
|
|
|
- menuContainer.appendChild(featuresCont);
|
|
|
- const versionElemCont = document.createElement("div");
|
|
|
- versionElemCont.id = "bytm-menu-version";
|
|
|
- const versionElem = document.createElement("a");
|
|
|
- versionElem.classList.add("bytm-link");
|
|
|
- versionElem.role = "button";
|
|
|
- versionElem.tabIndex = 0;
|
|
|
- versionElem.ariaLabel = versionElem.title = t("version_tooltip", scriptInfo.version, scriptInfo.buildNumber);
|
|
|
- versionElem.textContent = `v${scriptInfo.version} (${scriptInfo.buildNumber})`;
|
|
|
- const versionElemClicked = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- e.preventDefault();
|
|
|
- e.stopPropagation();
|
|
|
- yield openChangelogMenu("cfgMenu");
|
|
|
- closeCfgMenu(undefined, false);
|
|
|
- });
|
|
|
- versionElem.addEventListener("click", versionElemClicked);
|
|
|
- versionElem.addEventListener("keydown", (e) => e.key === "Enter" && versionElemClicked(e));
|
|
|
- menuContainer.appendChild(footerCont);
|
|
|
- versionElemCont.appendChild(versionElem);
|
|
|
- titleElem.appendChild(versionElemCont);
|
|
|
- backgroundElem.appendChild(menuContainer);
|
|
|
- document.body.appendChild(backgroundElem);
|
|
|
- window.addEventListener("resize", UserUtils.debounce(checkToggleScrollIndicator, 150));
|
|
|
- log("Added menu element");
|
|
|
- // ensure stuff is reset if menu was opened before being added
|
|
|
- isCfgMenuOpen = false;
|
|
|
- document.body.classList.remove("bytm-disable-scroll");
|
|
|
- (_e = document.querySelector("ytmusic-app")) === null || _e === void 0 ? void 0 : _e.removeAttribute("inert");
|
|
|
- backgroundElem.style.visibility = "hidden";
|
|
|
- backgroundElem.style.display = "none";
|
|
|
- });
|
|
|
-}
|
|
|
-/** Closes the config menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
-function closeCfgMenu(evt, enableScroll = true) {
|
|
|
- var _a;
|
|
|
- if (!isCfgMenuOpen)
|
|
|
- return;
|
|
|
- isCfgMenuOpen = false;
|
|
|
- (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
- if (enableScroll) {
|
|
|
- document.body.classList.remove("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
|
|
|
}
|
|
|
- const menuBg = document.querySelector("#bytm-cfg-menu-bg");
|
|
|
- siteEvents.emit("cfgMenuClosed");
|
|
|
- if (!menuBg)
|
|
|
- return;
|
|
|
- menuBg.style.visibility = "hidden";
|
|
|
- menuBg.style.display = "none";
|
|
|
-}
|
|
|
-/** Opens the config menu if it is closed */
|
|
|
-function openCfgMenu() {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (!isCfgMenuAdded)
|
|
|
- yield addCfgMenu();
|
|
|
- if (isCfgMenuOpen)
|
|
|
- return;
|
|
|
- isCfgMenuOpen = true;
|
|
|
- document.body.classList.add("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
- const menuBg = document.querySelector("#bytm-cfg-menu-bg");
|
|
|
- if (!menuBg)
|
|
|
- return;
|
|
|
- menuBg.style.visibility = "visible";
|
|
|
- menuBg.style.display = "block";
|
|
|
- checkToggleScrollIndicator();
|
|
|
- });
|
|
|
-}
|
|
|
-/** Checks if the features container is scrollable and toggles the scroll indicator accordingly */
|
|
|
-function checkToggleScrollIndicator() {
|
|
|
- const featuresCont = document.querySelector("#bytm-menu-opts");
|
|
|
- const scrollIndicator = document.querySelector("#bytm-menu-scroll-indicator");
|
|
|
- // disable scroll indicator if container doesn't scroll
|
|
|
- if (featuresCont && scrollIndicator) {
|
|
|
- const verticalScroll = UserUtils.isScrollable(featuresCont).vertical;
|
|
|
- /** If true, the indicator's threshold is under the available scrollable space and so it should be disabled */
|
|
|
- const underThreshold = featuresCont.scrollHeight - featuresCont.clientHeight <= scrollIndicatorOffsetThreshold;
|
|
|
- if (!underThreshold && verticalScroll && !scrollIndicatorEnabled) {
|
|
|
- scrollIndicatorEnabled = true;
|
|
|
- scrollIndicator.classList.remove("bytm-hidden");
|
|
|
- }
|
|
|
- if ((!verticalScroll && scrollIndicatorEnabled) || underThreshold) {
|
|
|
- scrollIndicatorEnabled = false;
|
|
|
- scrollIndicator.classList.add("bytm-hidden");
|
|
|
- }
|
|
|
+ /** Emits a site event with the given key and arguments */
|
|
|
+ function emitSiteEvent(key, ...args) {
|
|
|
+ siteEvents.emit(key, ...args);
|
|
|
+ emitInterface(`bytm:siteEvent:${key}`, args);
|
|
|
}
|
|
|
-}
|
|
|
-//#MARKER help dialog
|
|
|
-let isHelpDialogOpen = false;
|
|
|
-/** Key of the feature currently loaded in the help dialog */
|
|
|
-let helpDialogCurFeature;
|
|
|
-/** Opens the feature help dialog for the given feature */
|
|
|
-function openHelpDialog(featureKey) {
|
|
|
- var _a, _b, _c;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (isHelpDialogOpen)
|
|
|
- return;
|
|
|
- isHelpDialogOpen = true;
|
|
|
- let menuBgElem;
|
|
|
- if (!helpDialogCurFeature) {
|
|
|
- // create menu
|
|
|
- const headerElem = document.createElement("div");
|
|
|
- headerElem.classList.add("bytm-menu-header", "small");
|
|
|
+
|
|
|
+ /** Creates a hotkey input element */
|
|
|
+ function createHotkeyInput({ initialValue, resetValue, onChange }) {
|
|
|
+ var _a;
|
|
|
+ const wrapperElem = document.createElement("div");
|
|
|
+ wrapperElem.classList.add("bytm-hotkey-wrapper");
|
|
|
+ const infoElem = document.createElement("span");
|
|
|
+ infoElem.classList.add("bytm-hotkey-info");
|
|
|
+ const inputElem = document.createElement("input");
|
|
|
+ inputElem.type = "button";
|
|
|
+ inputElem.classList.add("bytm-ftconf-input", "bytm-hotkey-input", "bytm-btn");
|
|
|
+ inputElem.dataset.state = "inactive";
|
|
|
+ inputElem.value = (_a = initialValue === null || initialValue === void 0 ? void 0 : initialValue.code) !== null && _a !== void 0 ? _a : t("hotkey_input_click_to_change");
|
|
|
+ inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_change_tooltip");
|
|
|
+ const resetElem = document.createElement("span");
|
|
|
+ resetElem.classList.add("bytm-hotkey-reset", "bytm-link");
|
|
|
+ resetElem.role = "button";
|
|
|
+ resetElem.tabIndex = 0;
|
|
|
+ resetElem.textContent = `(${t("reset")})`;
|
|
|
+ const resetClicked = (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ onChange(resetValue);
|
|
|
+ inputElem.value = resetValue.code;
|
|
|
+ inputElem.dataset.state = "inactive";
|
|
|
+ infoElem.textContent = getHotkeyInfo(resetValue);
|
|
|
+ };
|
|
|
+ resetElem.addEventListener("click", resetClicked);
|
|
|
+ resetElem.addEventListener("keydown", (e) => e.key === "Enter" && resetClicked(e));
|
|
|
+ if (initialValue)
|
|
|
+ infoElem.textContent = getHotkeyInfo(initialValue);
|
|
|
+ let lastKeyDown;
|
|
|
+ document.addEventListener("keypress", (e) => {
|
|
|
+ if (inputElem.dataset.state !== "active")
|
|
|
+ return;
|
|
|
+ if ((lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.code) === e.code && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.shift) === e.shiftKey && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.ctrl) === e.ctrlKey && (lastKeyDown === null || lastKeyDown === void 0 ? void 0 : lastKeyDown.alt) === e.altKey)
|
|
|
+ return;
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ const hotkey = {
|
|
|
+ code: e.code,
|
|
|
+ shift: e.shiftKey,
|
|
|
+ ctrl: e.ctrlKey,
|
|
|
+ alt: e.altKey,
|
|
|
+ };
|
|
|
+ inputElem.value = hotkey.code;
|
|
|
+ inputElem.dataset.state = "inactive";
|
|
|
+ infoElem.textContent = getHotkeyInfo(hotkey);
|
|
|
+ onChange(hotkey);
|
|
|
+ });
|
|
|
+ document.addEventListener("keydown", (e) => {
|
|
|
+ if (inputElem.dataset.state !== "active")
|
|
|
+ return;
|
|
|
+ if (["ShiftLeft", "ShiftRight", "ControlLeft", "ControlRight", "AltLeft", "AltRight"].includes(e.code))
|
|
|
+ return;
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ const hotkey = {
|
|
|
+ code: e.code,
|
|
|
+ shift: e.shiftKey,
|
|
|
+ ctrl: e.ctrlKey,
|
|
|
+ alt: e.altKey,
|
|
|
+ };
|
|
|
+ lastKeyDown = hotkey;
|
|
|
+ inputElem.value = hotkey.code;
|
|
|
+ inputElem.dataset.state = "inactive";
|
|
|
+ infoElem.textContent = getHotkeyInfo(hotkey);
|
|
|
+ inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_cancel_tooltip");
|
|
|
+ onChange(hotkey);
|
|
|
+ });
|
|
|
+ const deactivate = () => {
|
|
|
+ var _a, _b;
|
|
|
+ siteEvents.emit("hotkeyInputActive", false);
|
|
|
+ const curVal = (_a = getFeatures().switchSitesHotkey) !== null && _a !== void 0 ? _a : initialValue;
|
|
|
+ inputElem.value = (_b = curVal === null || curVal === void 0 ? void 0 : curVal.code) !== null && _b !== void 0 ? _b : t("hotkey_input_click_to_change");
|
|
|
+ inputElem.dataset.state = "inactive";
|
|
|
+ inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_change_tooltip");
|
|
|
+ infoElem.textContent = curVal ? getHotkeyInfo(curVal) : "";
|
|
|
+ };
|
|
|
+ const activate = () => {
|
|
|
+ siteEvents.emit("hotkeyInputActive", true);
|
|
|
+ inputElem.value = "< ... >";
|
|
|
+ inputElem.dataset.state = "active";
|
|
|
+ inputElem.ariaLabel = inputElem.title = t("hotkey_input_click_to_cancel_tooltip");
|
|
|
+ };
|
|
|
+ siteEvents.on("cfgMenuClosed", deactivate);
|
|
|
+ inputElem.addEventListener("click", () => {
|
|
|
+ if (inputElem.dataset.state === "active")
|
|
|
+ deactivate();
|
|
|
+ else
|
|
|
+ activate();
|
|
|
+ });
|
|
|
+ wrapperElem.appendChild(infoElem);
|
|
|
+ wrapperElem.appendChild(inputElem);
|
|
|
+ resetValue && wrapperElem.appendChild(resetElem);
|
|
|
+ return wrapperElem;
|
|
|
+ }
|
|
|
+ function getHotkeyInfo(hotkey) {
|
|
|
+ const modifiers = [];
|
|
|
+ hotkey.ctrl && modifiers.push(t("hotkey_key_ctrl"));
|
|
|
+ hotkey.shift && modifiers.push(t("hotkey_key_shift"));
|
|
|
+ hotkey.alt && modifiers.push(getOS() === "mac" ? t("hotkey_key_mac_option") : t("hotkey_key_alt"));
|
|
|
+ return modifiers.reduce((a, c) => a += `${c} + `, "");
|
|
|
+ }
|
|
|
+ /** Crude OS detection for keyboard layout purposes */
|
|
|
+ function getOS() {
|
|
|
+ if (navigator.userAgent.match(/mac(\s?os|intel)/i))
|
|
|
+ return "mac";
|
|
|
+ return "other";
|
|
|
+ }
|
|
|
+
|
|
|
+ var name = "betterytm";
|
|
|
+ var userscriptName = "BetterYTM";
|
|
|
+ var version = "1.1.0";
|
|
|
+ var description = "Lots of configurable layout and user experience improvements for YouTube Music";
|
|
|
+ var homepage = "https://github.com/Sv443/BetterYTM";
|
|
|
+ var main = "./src/index.ts";
|
|
|
+ var type = "module";
|
|
|
+ var scripts = {
|
|
|
+ dev: "concurrently \"nodemon --exec npm run build-watch\" \"npm run serve\"",
|
|
|
+ serve: "npm run node-ts -- ./src/tools/serve.ts",
|
|
|
+ lint: "tsc --noEmit && eslint .",
|
|
|
+ build: "rollup -c",
|
|
|
+ "build-watch": "rollup -c --config-mode development --config-host github --config-branch develop",
|
|
|
+ "build-develop": "rollup -c --config-mode production --config-host github --config-branch develop",
|
|
|
+ "build-prod": "npm run build-prod-gh && npm run build-prod-gf && npm run build-prod-oujs",
|
|
|
+ "build-prod-base": "rollup -c --config-mode production --config-branch main",
|
|
|
+ "build-prod-gh": "npm run build-prod-base -- --config-host github",
|
|
|
+ "build-prod-gf": "npm run build-prod-base -- --config-host greasyfork --config-suffix _gf",
|
|
|
+ "build-prod-oujs": "npm run build-prod-base -- --config-host openuserjs --config-suffix _oujs",
|
|
|
+ "post-build": "npm run node-ts -- ./src/tools/post-build.ts",
|
|
|
+ "tr-progress": "npm run node-ts -- ./src/tools/tr-progress.ts",
|
|
|
+ "tr-format": "npm run node-ts -- ./src/tools/tr-format.ts",
|
|
|
+ "gen-readme": "npm run node-ts -- ./src/tools/gen-readme.ts",
|
|
|
+ "node-ts": "node --no-warnings=ExperimentalWarning --enable-source-maps --loader ts-node/esm",
|
|
|
+ invisible: "node src/tools/run-invisible.mjs",
|
|
|
+ test: "npm run node-ts -- ./test.ts"
|
|
|
+ };
|
|
|
+ var engines = {
|
|
|
+ node: ">=18",
|
|
|
+ npm: ">=8"
|
|
|
+ };
|
|
|
+ var repository = {
|
|
|
+ type: "git",
|
|
|
+ url: "git+https://github.com/Sv443/BetterYTM.git"
|
|
|
+ };
|
|
|
+ var author = {
|
|
|
+ name: "Sv443",
|
|
|
+ url: "https://github.com/Sv443"
|
|
|
+ };
|
|
|
+ var license = "AGPL-3.0-only";
|
|
|
+ var bugs = {
|
|
|
+ url: "https://github.com/Sv443/BetterYTM/issues"
|
|
|
+ };
|
|
|
+ var funding = {
|
|
|
+ type: "github",
|
|
|
+ url: "https://github.com/sponsors/Sv443"
|
|
|
+ };
|
|
|
+ var hosts = {
|
|
|
+ github: "https://github.com/Sv443/BetterYTM",
|
|
|
+ greasyfork: "https://greasyfork.org/en/scripts/475682-betterytm",
|
|
|
+ openuserjs: "https://openuserjs.org/scripts/Sv443/BetterYTM"
|
|
|
+ };
|
|
|
+ var updates = {
|
|
|
+ github: "https://github.com/Sv443/BetterYTM/releases",
|
|
|
+ greasyfork: "https://greasyfork.org/en/scripts/475682-betterytm",
|
|
|
+ openuserjs: "https://openuserjs.org/scripts/Sv443/BetterYTM"
|
|
|
+ };
|
|
|
+ var dependencies = {
|
|
|
+ nanoevents: "^9.0.0"
|
|
|
+ };
|
|
|
+ var devDependencies = {
|
|
|
+ "@rollup/plugin-json": "^6.0.1",
|
|
|
+ "@rollup/plugin-node-resolve": "^15.2.3",
|
|
|
+ "@rollup/plugin-terser": "^0.4.4",
|
|
|
+ "@rollup/plugin-typescript": "^11.1.5",
|
|
|
+ "@sv443-network/userutils": "^4.2.1",
|
|
|
+ "@types/express": "^4.17.17",
|
|
|
+ "@types/greasemonkey": "^4.0.4",
|
|
|
+ "@types/node": "^20.2.4",
|
|
|
+ "@typescript-eslint/eslint-plugin": "^6.7.4",
|
|
|
+ "@typescript-eslint/parser": "^6.7.4",
|
|
|
+ concurrently: "^8.1.0",
|
|
|
+ dotenv: "^16.4.1",
|
|
|
+ eslint: "^8.51.0",
|
|
|
+ express: "^4.18.2",
|
|
|
+ marked: "^12.0.0",
|
|
|
+ nodemon: "^3.0.1",
|
|
|
+ rollup: "^4.6.0",
|
|
|
+ "rollup-plugin-execute": "^1.1.1",
|
|
|
+ "rollup-plugin-html": "^0.2.1",
|
|
|
+ "rollup-plugin-import-css": "^3.3.5",
|
|
|
+ "ts-node": "^10.9.1",
|
|
|
+ tslib: "^2.5.2",
|
|
|
+ typescript: "^5.0.4"
|
|
|
+ };
|
|
|
+ var browserslist = [
|
|
|
+ "last 1 version",
|
|
|
+ "> 1%",
|
|
|
+ "not dead"
|
|
|
+ ];
|
|
|
+ var nodemonConfig = {
|
|
|
+ watch: [
|
|
|
+ "src/**",
|
|
|
+ "assets/**",
|
|
|
+ "rollup.config.mjs",
|
|
|
+ ".env",
|
|
|
+ "changelog.md",
|
|
|
+ "package.json"
|
|
|
+ ],
|
|
|
+ ext: "ts,mts,js,jsx,mjs,json,html,css,svg,png",
|
|
|
+ ignore: [
|
|
|
+ "dist/*",
|
|
|
+ "dev/*"
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ var pkg = {
|
|
|
+ name: name,
|
|
|
+ userscriptName: userscriptName,
|
|
|
+ version: version,
|
|
|
+ description: description,
|
|
|
+ homepage: homepage,
|
|
|
+ main: main,
|
|
|
+ type: type,
|
|
|
+ scripts: scripts,
|
|
|
+ engines: engines,
|
|
|
+ repository: repository,
|
|
|
+ author: author,
|
|
|
+ license: license,
|
|
|
+ bugs: bugs,
|
|
|
+ funding: funding,
|
|
|
+ hosts: hosts,
|
|
|
+ updates: updates,
|
|
|
+ dependencies: dependencies,
|
|
|
+ devDependencies: devDependencies,
|
|
|
+ browserslist: browserslist,
|
|
|
+ nodemonConfig: nodemonConfig
|
|
|
+ };
|
|
|
+
|
|
|
+ //#MARKER create menu elements
|
|
|
+ let isCfgMenuAdded = false;
|
|
|
+ let isCfgMenuOpen = false;
|
|
|
+ const compressionFormat = "deflate-raw";
|
|
|
+ function compressionSupported() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ yield UserUtils.compress(".", compressionFormat);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Threshold in pixels from the top of the options container that dictates for how long the scroll indicator is shown */
|
|
|
+ const scrollIndicatorOffsetThreshold = 30;
|
|
|
+ let scrollIndicatorEnabled = true;
|
|
|
+ /** Locale at the point of initializing the config menu */
|
|
|
+ let initLocale;
|
|
|
+ /** Stringified config at the point of initializing the config menu */
|
|
|
+ let initConfig$1;
|
|
|
+ /**
|
|
|
+ * Adds an element to open the BetterYTM menu
|
|
|
+ * @deprecated to be replaced with new menu - see https://github.com/Sv443/BetterYTM/issues/23
|
|
|
+ */
|
|
|
+ function addCfgMenu() {
|
|
|
+ var _a, _b, _c, _d, _e;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (isCfgMenuAdded)
|
|
|
+ return;
|
|
|
+ isCfgMenuAdded = true;
|
|
|
+ initLocale = getFeatures().locale;
|
|
|
+ initConfig$1 = JSON.stringify(getFeatures());
|
|
|
+ const initLangReloadText = t("lang_changed_prompt_reload");
|
|
|
+ const toggled_on = t("toggled_on");
|
|
|
+ const toggled_off = t("toggled_off");
|
|
|
+ //#SECTION backdrop & menu container
|
|
|
+ const backgroundElem = document.createElement("div");
|
|
|
+ backgroundElem.id = "bytm-cfg-menu-bg";
|
|
|
+ backgroundElem.classList.add("bytm-menu-bg");
|
|
|
+ backgroundElem.ariaLabel = backgroundElem.title = t("close_menu_tooltip");
|
|
|
+ backgroundElem.style.visibility = "hidden";
|
|
|
+ backgroundElem.style.display = "none";
|
|
|
+ backgroundElem.addEventListener("click", (e) => {
|
|
|
+ var _a;
|
|
|
+ if (isCfgMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-cfg-menu-bg")
|
|
|
+ closeCfgMenu(e);
|
|
|
+ });
|
|
|
+ document.body.addEventListener("keydown", (e) => {
|
|
|
+ if (isCfgMenuOpen && e.key === "Escape")
|
|
|
+ closeCfgMenu(e);
|
|
|
+ });
|
|
|
+ const menuContainer = document.createElement("div");
|
|
|
+ menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
+ menuContainer.classList.add("bytm-menu");
|
|
|
+ menuContainer.id = "bytm-cfg-menu";
|
|
|
+ //#SECTION title bar
|
|
|
+ const headerElem = document.createElement("div");
|
|
|
+ headerElem.classList.add("bytm-menu-header");
|
|
|
const titleCont = document.createElement("div");
|
|
|
- titleCont.className = "bytm-menu-titlecont-no-title";
|
|
|
+ titleCont.className = "bytm-menu-titlecont";
|
|
|
titleCont.role = "heading";
|
|
|
titleCont.ariaLevel = "1";
|
|
|
- const helpIconSvg = yield resourceToHTMLString("img-help");
|
|
|
- if (helpIconSvg)
|
|
|
- titleCont.innerHTML = helpIconSvg;
|
|
|
+ const titleElem = document.createElement("h2");
|
|
|
+ titleElem.className = "bytm-menu-title";
|
|
|
+ const titleTextElem = document.createElement("div");
|
|
|
+ titleTextElem.textContent = t("config_menu_title", scriptInfo.name);
|
|
|
+ titleElem.appendChild(titleTextElem);
|
|
|
+ const linksCont = document.createElement("div");
|
|
|
+ linksCont.id = "bytm-menu-linkscont";
|
|
|
+ linksCont.role = "navigation";
|
|
|
+ const addLink = (imgSrc, href, title) => {
|
|
|
+ const anchorElem = document.createElement("a");
|
|
|
+ anchorElem.className = "bytm-menu-link bytm-no-select";
|
|
|
+ anchorElem.rel = "noopener noreferrer";
|
|
|
+ anchorElem.href = href;
|
|
|
+ anchorElem.target = "_blank";
|
|
|
+ anchorElem.tabIndex = 0;
|
|
|
+ anchorElem.role = "button";
|
|
|
+ anchorElem.ariaLabel = anchorElem.title = title;
|
|
|
+ const imgElem = document.createElement("img");
|
|
|
+ imgElem.className = "bytm-menu-img";
|
|
|
+ imgElem.src = imgSrc;
|
|
|
+ imgElem.style.width = "32px";
|
|
|
+ imgElem.style.height = "32px";
|
|
|
+ anchorElem.appendChild(imgElem);
|
|
|
+ linksCont.appendChild(anchorElem);
|
|
|
+ };
|
|
|
+ addLink(yield getResourceUrl("img-discord"), "https://dc.sv443.net/", t("open_discord"));
|
|
|
+ const links = [
|
|
|
+ ["github", yield getResourceUrl("img-github"), scriptInfo.namespace, t("open_github", scriptInfo.name)],
|
|
|
+ ["greasyfork", yield getResourceUrl("img-greasyfork"), pkg.hosts.greasyfork, t("open_greasyfork", scriptInfo.name)],
|
|
|
+ ["openuserjs", yield getResourceUrl("img-openuserjs"), pkg.hosts.openuserjs, t("open_openuserjs", scriptInfo.name)],
|
|
|
+ ];
|
|
|
+ const hostLink = links.find(([name]) => name === host);
|
|
|
+ const otherLinks = links.filter(([name]) => name !== host);
|
|
|
+ const reorderedLinks = hostLink ? [hostLink, ...otherLinks] : links;
|
|
|
+ for (const [, ...args] of reorderedLinks)
|
|
|
+ addLink(...args);
|
|
|
const closeElem = document.createElement("img");
|
|
|
- closeElem.classList.add("bytm-menu-close", "small");
|
|
|
+ closeElem.classList.add("bytm-menu-close");
|
|
|
closeElem.role = "button";
|
|
|
closeElem.tabIndex = 0;
|
|
|
closeElem.src = yield getResourceUrl("img-close");
|
|
|
closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
- closeElem.addEventListener("click", (e) => closeHelpDialog(e));
|
|
|
- closeElem.addEventListener("keydown", (e) => e.key === "Enter" && closeHelpDialog(e));
|
|
|
+ closeElem.addEventListener("click", closeCfgMenu);
|
|
|
+ closeElem.addEventListener("keydown", ({ key }) => key === "Enter" && closeCfgMenu());
|
|
|
+ titleCont.appendChild(titleElem);
|
|
|
+ titleCont.appendChild(linksCont);
|
|
|
headerElem.appendChild(titleCont);
|
|
|
headerElem.appendChild(closeElem);
|
|
|
- menuBgElem = document.createElement("div");
|
|
|
- menuBgElem.id = "bytm-feat-help-menu-bg";
|
|
|
+ //#SECTION footer
|
|
|
+ const footerCont = document.createElement("div");
|
|
|
+ footerCont.className = "bytm-menu-footer-cont";
|
|
|
+ const footerElemCont = document.createElement("div");
|
|
|
+ const footerElem = document.createElement("div");
|
|
|
+ footerElem.classList.add("bytm-menu-footer", "hidden");
|
|
|
+ footerElem.textContent = t("reload_hint");
|
|
|
+ const reloadElem = document.createElement("button");
|
|
|
+ reloadElem.classList.add("bytm-btn");
|
|
|
+ reloadElem.style.marginLeft = "10px";
|
|
|
+ reloadElem.textContent = t("reload_now");
|
|
|
+ reloadElem.ariaLabel = reloadElem.title = t("reload_tooltip");
|
|
|
+ reloadElem.addEventListener("click", () => {
|
|
|
+ closeCfgMenu();
|
|
|
+ disableBeforeUnload();
|
|
|
+ location.reload();
|
|
|
+ });
|
|
|
+ footerElem.appendChild(reloadElem);
|
|
|
+ footerElemCont.appendChild(footerElem);
|
|
|
+ const resetElem = document.createElement("button");
|
|
|
+ resetElem.classList.add("bytm-btn");
|
|
|
+ resetElem.ariaLabel = resetElem.title = t("reset_tooltip");
|
|
|
+ resetElem.textContent = t("reset");
|
|
|
+ resetElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (confirm(t("reset_confirm"))) {
|
|
|
+ yield setDefaultFeatures();
|
|
|
+ closeCfgMenu();
|
|
|
+ disableBeforeUnload();
|
|
|
+ location.reload();
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ const exportElem = document.createElement("button");
|
|
|
+ exportElem.classList.add("bytm-btn");
|
|
|
+ exportElem.ariaLabel = exportElem.title = t("export_tooltip");
|
|
|
+ exportElem.textContent = t("export");
|
|
|
+ exportElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ yield openExportMenu();
|
|
|
+ closeCfgMenu(undefined, false);
|
|
|
+ }));
|
|
|
+ const importElem = document.createElement("button");
|
|
|
+ importElem.classList.add("bytm-btn");
|
|
|
+ importElem.ariaLabel = importElem.title = t("import_tooltip");
|
|
|
+ importElem.textContent = t("import");
|
|
|
+ importElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ yield openImportMenu();
|
|
|
+ closeCfgMenu(undefined, false);
|
|
|
+ }));
|
|
|
+ const buttonsCont = document.createElement("div");
|
|
|
+ buttonsCont.id = "bytm-menu-footer-buttons-cont";
|
|
|
+ buttonsCont.appendChild(exportElem);
|
|
|
+ buttonsCont.appendChild(importElem);
|
|
|
+ buttonsCont.appendChild(resetElem);
|
|
|
+ footerCont.appendChild(footerElemCont);
|
|
|
+ footerCont.appendChild(buttonsCont);
|
|
|
+ //#SECTION feature list
|
|
|
+ const featuresCont = document.createElement("div");
|
|
|
+ featuresCont.id = "bytm-menu-opts";
|
|
|
+ /** Gets called whenever the feature config is changed */
|
|
|
+ const confChanged = UserUtils.debounce((key, initialVal, newVal) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const fmt = (val) => typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
|
+ info(`Feature config changed at key '${key}', from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
|
|
|
+ const featConf = JSON.parse(JSON.stringify(getFeatures()));
|
|
|
+ featConf[key] = newVal;
|
|
|
+ yield saveFeatures(featConf);
|
|
|
+ if (initConfig$1 !== JSON.stringify(featConf))
|
|
|
+ footerElem.classList.remove("hidden");
|
|
|
+ else
|
|
|
+ footerElem.classList.add("hidden");
|
|
|
+ if (initLocale !== featConf.locale) {
|
|
|
+ yield initTranslations(featConf.locale);
|
|
|
+ setLocale(featConf.locale);
|
|
|
+ const newText = t("lang_changed_prompt_reload");
|
|
|
+ const confirmText = newText !== initLangReloadText ? `${newText}\n\n────────────────────────────────\n\n${initLangReloadText}` : newText;
|
|
|
+ if (confirm(confirmText)) {
|
|
|
+ closeCfgMenu();
|
|
|
+ disableBeforeUnload();
|
|
|
+ location.reload();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (getLocale() !== featConf.locale)
|
|
|
+ setLocale(featConf.locale);
|
|
|
+ }));
|
|
|
+ const featureCfg = getFeatures();
|
|
|
+ const featureCfgWithCategories = Object.entries(featInfo)
|
|
|
+ .reduce((acc, [key, { category }]) => {
|
|
|
+ if (!acc[category])
|
|
|
+ acc[category] = {};
|
|
|
+ acc[category][key] = featureCfg[key];
|
|
|
+ return acc;
|
|
|
+ }, {});
|
|
|
+ const fmtVal = (v) => String(v).trim();
|
|
|
+ const toggleLabelText = (toggled) => toggled ? toggled_on : toggled_off;
|
|
|
+ for (const category in featureCfgWithCategories) {
|
|
|
+ const featObj = featureCfgWithCategories[category];
|
|
|
+ const catHeaderElem = document.createElement("h3");
|
|
|
+ catHeaderElem.classList.add("bytm-ftconf-category-header");
|
|
|
+ catHeaderElem.role = "heading";
|
|
|
+ catHeaderElem.ariaLevel = "2";
|
|
|
+ catHeaderElem.textContent = `${t(`feature_category_${category}`)}:`;
|
|
|
+ featuresCont.appendChild(catHeaderElem);
|
|
|
+ for (const featKey in featObj) {
|
|
|
+ const ftInfo = featInfo[featKey];
|
|
|
+ // @ts-ignore
|
|
|
+ if (!ftInfo || ftInfo.hidden === true)
|
|
|
+ continue;
|
|
|
+ const { type, default: ftDefault } = ftInfo;
|
|
|
+ // @ts-ignore
|
|
|
+ const step = (_a = ftInfo === null || ftInfo === void 0 ? void 0 : ftInfo.step) !== null && _a !== void 0 ? _a : undefined;
|
|
|
+ const val = featureCfg[featKey];
|
|
|
+ const initialVal = (_b = val !== null && val !== void 0 ? val : ftDefault) !== null && _b !== void 0 ? _b : undefined;
|
|
|
+ const ftConfElem = document.createElement("div");
|
|
|
+ ftConfElem.classList.add("bytm-ftitem");
|
|
|
+ {
|
|
|
+ const featLeftSideElem = document.createElement("div");
|
|
|
+ featLeftSideElem.classList.add("bytm-ftitem-leftside");
|
|
|
+ const textElem = document.createElement("span");
|
|
|
+ textElem.textContent = t(`feature_desc_${featKey}`);
|
|
|
+ let adornmentElem;
|
|
|
+ const adornContent = (_c = ftInfo.textAdornment) === null || _c === void 0 ? void 0 : _c.call(ftInfo);
|
|
|
+ if (typeof adornContent === "string" || adornContent instanceof Promise) {
|
|
|
+ adornmentElem = document.createElement("span");
|
|
|
+ adornmentElem.id = `bytm-ftitem-${featKey}-adornment`;
|
|
|
+ adornmentElem.classList.add("bytm-ftitem-adornment");
|
|
|
+ adornmentElem.innerHTML = adornContent instanceof Promise ? yield adornContent : adornContent;
|
|
|
+ }
|
|
|
+ let helpElem;
|
|
|
+ // @ts-ignore
|
|
|
+ const hasHelpTextFunc = typeof ((_d = featInfo[featKey]) === null || _d === void 0 ? void 0 : _d.helpText) === "function";
|
|
|
+ // @ts-ignore
|
|
|
+ const helpTextVal = hasHelpTextFunc && featInfo[featKey].helpText();
|
|
|
+ if (hasKey(`feature_helptext_${featKey}`) || (helpTextVal && hasKey(helpTextVal))) {
|
|
|
+ const helpElemImgHtml = yield resourceToHTMLString("img-help");
|
|
|
+ if (helpElemImgHtml) {
|
|
|
+ helpElem = document.createElement("div");
|
|
|
+ helpElem.classList.add("bytm-ftitem-help-btn", "bytm-generic-btn");
|
|
|
+ helpElem.ariaLabel = helpElem.title = t("feature_help_button_tooltip");
|
|
|
+ helpElem.role = "button";
|
|
|
+ helpElem.tabIndex = 0;
|
|
|
+ helpElem.innerHTML = helpElemImgHtml;
|
|
|
+ const helpElemClicked = (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ openHelpDialog(featKey);
|
|
|
+ };
|
|
|
+ helpElem.addEventListener("click", helpElemClicked);
|
|
|
+ helpElem.addEventListener("keydown", (e) => e.key === "Enter" && helpElemClicked(e));
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ error(`Couldn't create help button SVG element for feature '${featKey}'`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ featLeftSideElem.appendChild(textElem);
|
|
|
+ adornmentElem && featLeftSideElem.appendChild(adornmentElem);
|
|
|
+ helpElem && featLeftSideElem.appendChild(helpElem);
|
|
|
+ ftConfElem.appendChild(featLeftSideElem);
|
|
|
+ }
|
|
|
+ {
|
|
|
+ let inputType = "text";
|
|
|
+ let inputTag = "input";
|
|
|
+ switch (type) {
|
|
|
+ case "toggle":
|
|
|
+ inputType = "checkbox";
|
|
|
+ break;
|
|
|
+ case "slider":
|
|
|
+ inputType = "range";
|
|
|
+ break;
|
|
|
+ case "number":
|
|
|
+ inputType = "number";
|
|
|
+ break;
|
|
|
+ case "select":
|
|
|
+ inputTag = "select";
|
|
|
+ inputType = undefined;
|
|
|
+ break;
|
|
|
+ case "hotkey":
|
|
|
+ inputTag = undefined;
|
|
|
+ inputType = undefined;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ const inputElemId = `bytm-ftconf-${featKey}-input`;
|
|
|
+ const ctrlElem = document.createElement("span");
|
|
|
+ ctrlElem.classList.add("bytm-ftconf-ctrl");
|
|
|
+ if (inputTag) {
|
|
|
+ // standard input element:
|
|
|
+ const inputElem = document.createElement(inputTag);
|
|
|
+ inputElem.classList.add("bytm-ftconf-input");
|
|
|
+ inputElem.id = inputElemId;
|
|
|
+ if (inputType)
|
|
|
+ inputElem.type = inputType;
|
|
|
+ // @ts-ignore
|
|
|
+ if (typeof ftInfo.min !== "undefined" && ftInfo.max !== "undefined") {
|
|
|
+ // @ts-ignore
|
|
|
+ inputElem.min = ftInfo.min;
|
|
|
+ // @ts-ignore
|
|
|
+ inputElem.max = ftInfo.max;
|
|
|
+ }
|
|
|
+ if (typeof initialVal !== "undefined")
|
|
|
+ inputElem.value = String(initialVal);
|
|
|
+ if (type === "number" || type === "slider" && step)
|
|
|
+ inputElem.step = String(step);
|
|
|
+ if (type === "toggle" && typeof initialVal !== "undefined")
|
|
|
+ inputElem.checked = Boolean(initialVal);
|
|
|
+ // @ts-ignore
|
|
|
+ const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
|
|
|
+ let labelElem;
|
|
|
+ if (type === "slider") {
|
|
|
+ labelElem = document.createElement("label");
|
|
|
+ labelElem.classList.add("bytm-ftconf-label", "bytm-slider-label");
|
|
|
+ labelElem.textContent = fmtVal(initialVal) + unitTxt;
|
|
|
+ inputElem.addEventListener("input", () => {
|
|
|
+ if (labelElem)
|
|
|
+ labelElem.textContent = fmtVal(Number(inputElem.value)) + unitTxt;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ else if (type === "toggle") {
|
|
|
+ labelElem = document.createElement("label");
|
|
|
+ labelElem.classList.add("bytm-ftconf-label", "bytm-toggle-label");
|
|
|
+ labelElem.textContent = toggleLabelText(Boolean(initialVal)) + unitTxt;
|
|
|
+ inputElem.addEventListener("input", () => {
|
|
|
+ if (labelElem)
|
|
|
+ labelElem.textContent = toggleLabelText(inputElem.checked) + unitTxt;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ else if (type === "select") {
|
|
|
+ const ftOpts = typeof ftInfo.options === "function"
|
|
|
+ ? ftInfo.options()
|
|
|
+ : ftInfo.options;
|
|
|
+ for (const { value, label } of ftOpts) {
|
|
|
+ const optionElem = document.createElement("option");
|
|
|
+ optionElem.value = String(value);
|
|
|
+ optionElem.textContent = label;
|
|
|
+ if (value === initialVal)
|
|
|
+ optionElem.selected = true;
|
|
|
+ inputElem.appendChild(optionElem);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ inputElem.addEventListener("input", () => {
|
|
|
+ let v = String(inputElem.value).trim();
|
|
|
+ if (["number", "slider"].includes(type) || v.match(/^-?\d+$/))
|
|
|
+ v = Number(v);
|
|
|
+ if (typeof initialVal !== "undefined")
|
|
|
+ confChanged(featKey, initialVal, (type !== "toggle" ? v : inputElem.checked));
|
|
|
+ });
|
|
|
+ if (labelElem) {
|
|
|
+ labelElem.id = `bytm-ftconf-${featKey}-label`;
|
|
|
+ labelElem.htmlFor = inputElemId;
|
|
|
+ ctrlElem.appendChild(labelElem);
|
|
|
+ }
|
|
|
+ ctrlElem.appendChild(inputElem);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // custom input element:
|
|
|
+ let wrapperElem;
|
|
|
+ switch (type) {
|
|
|
+ case "hotkey":
|
|
|
+ wrapperElem = createHotkeyInput({
|
|
|
+ initialValue: initialVal,
|
|
|
+ resetValue: featInfo.switchSitesHotkey.default,
|
|
|
+ onChange: (hotkey) => {
|
|
|
+ confChanged(featKey, initialVal, hotkey);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ ctrlElem.appendChild(wrapperElem);
|
|
|
+ }
|
|
|
+ ftConfElem.appendChild(ctrlElem);
|
|
|
+ }
|
|
|
+ featuresCont.appendChild(ftConfElem);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //#SECTION set values of inputs on external change
|
|
|
+ siteEvents.on("rebuildCfgMenu", (newConfig) => {
|
|
|
+ for (const ftKey in featInfo) {
|
|
|
+ const ftElem = document.querySelector(`#bytm-ftconf-${ftKey}-input`);
|
|
|
+ const labelElem = document.querySelector(`#bytm-ftconf-${ftKey}-label`);
|
|
|
+ if (!ftElem)
|
|
|
+ continue;
|
|
|
+ const ftInfo = featInfo[ftKey];
|
|
|
+ const value = newConfig[ftKey];
|
|
|
+ if (ftInfo.type === "toggle")
|
|
|
+ ftElem.checked = Boolean(value);
|
|
|
+ else
|
|
|
+ ftElem.value = String(value);
|
|
|
+ if (!labelElem)
|
|
|
+ continue;
|
|
|
+ // @ts-ignore
|
|
|
+ const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
|
|
|
+ if (ftInfo.type === "slider")
|
|
|
+ labelElem.textContent = fmtVal(Number(value)) + unitTxt;
|
|
|
+ else if (ftInfo.type === "toggle")
|
|
|
+ labelElem.textContent = toggleLabelText(Boolean(value)) + unitTxt;
|
|
|
+ }
|
|
|
+ info("Rebuilt config menu");
|
|
|
+ });
|
|
|
+ //#SECTION scroll indicator
|
|
|
+ const scrollIndicator = document.createElement("img");
|
|
|
+ scrollIndicator.id = "bytm-menu-scroll-indicator";
|
|
|
+ scrollIndicator.src = yield getResourceUrl("img-arrow_down");
|
|
|
+ scrollIndicator.role = "button";
|
|
|
+ scrollIndicator.ariaLabel = scrollIndicator.title = t("scroll_to_bottom");
|
|
|
+ featuresCont.appendChild(scrollIndicator);
|
|
|
+ scrollIndicator.addEventListener("click", () => {
|
|
|
+ const bottomAnchor = document.querySelector("#bytm-menu-bottom-anchor");
|
|
|
+ bottomAnchor === null || bottomAnchor === void 0 ? void 0 : bottomAnchor.scrollIntoView({
|
|
|
+ behavior: "smooth",
|
|
|
+ });
|
|
|
+ });
|
|
|
+ featuresCont.addEventListener("scroll", (evt) => {
|
|
|
+ var _a, _b;
|
|
|
+ const scrollPos = (_b = (_a = evt.target) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0;
|
|
|
+ const scrollIndicator = document.querySelector("#bytm-menu-scroll-indicator");
|
|
|
+ if (!scrollIndicator)
|
|
|
+ return;
|
|
|
+ if (scrollIndicatorEnabled && scrollPos > scrollIndicatorOffsetThreshold && !scrollIndicator.classList.contains("bytm-hidden")) {
|
|
|
+ scrollIndicator.classList.add("bytm-hidden");
|
|
|
+ }
|
|
|
+ else if (scrollIndicatorEnabled && scrollPos <= scrollIndicatorOffsetThreshold && scrollIndicator.classList.contains("bytm-hidden")) {
|
|
|
+ scrollIndicator.classList.remove("bytm-hidden");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const bottomAnchor = document.createElement("div");
|
|
|
+ bottomAnchor.id = "bytm-menu-bottom-anchor";
|
|
|
+ featuresCont.appendChild(bottomAnchor);
|
|
|
+ //#SECTION finalize
|
|
|
+ menuContainer.appendChild(headerElem);
|
|
|
+ menuContainer.appendChild(featuresCont);
|
|
|
+ const versionElemCont = document.createElement("div");
|
|
|
+ versionElemCont.id = "bytm-menu-version";
|
|
|
+ const versionElem = document.createElement("a");
|
|
|
+ versionElem.classList.add("bytm-link");
|
|
|
+ versionElem.role = "button";
|
|
|
+ versionElem.tabIndex = 0;
|
|
|
+ versionElem.ariaLabel = versionElem.title = t("version_tooltip", scriptInfo.version, scriptInfo.buildNumber);
|
|
|
+ versionElem.textContent = `v${scriptInfo.version} (${scriptInfo.buildNumber})`;
|
|
|
+ const versionElemClicked = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ yield openChangelogMenu("cfgMenu");
|
|
|
+ closeCfgMenu(undefined, false);
|
|
|
+ });
|
|
|
+ versionElem.addEventListener("click", versionElemClicked);
|
|
|
+ versionElem.addEventListener("keydown", (e) => e.key === "Enter" && versionElemClicked(e));
|
|
|
+ menuContainer.appendChild(footerCont);
|
|
|
+ versionElemCont.appendChild(versionElem);
|
|
|
+ titleElem.appendChild(versionElemCont);
|
|
|
+ backgroundElem.appendChild(menuContainer);
|
|
|
+ document.body.appendChild(backgroundElem);
|
|
|
+ window.addEventListener("resize", UserUtils.debounce(checkToggleScrollIndicator, 150));
|
|
|
+ log("Added menu element");
|
|
|
+ // ensure stuff is reset if menu was opened before being added
|
|
|
+ isCfgMenuOpen = false;
|
|
|
+ document.body.classList.remove("bytm-disable-scroll");
|
|
|
+ (_e = document.querySelector("ytmusic-app")) === null || _e === void 0 ? void 0 : _e.removeAttribute("inert");
|
|
|
+ backgroundElem.style.visibility = "hidden";
|
|
|
+ backgroundElem.style.display = "none";
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Closes the config menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
+ function closeCfgMenu(evt, enableScroll = true) {
|
|
|
+ var _a;
|
|
|
+ if (!isCfgMenuOpen)
|
|
|
+ return;
|
|
|
+ isCfgMenuOpen = false;
|
|
|
+ (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
+ if (enableScroll) {
|
|
|
+ document.body.classList.remove("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
|
|
|
+ }
|
|
|
+ const menuBg = document.querySelector("#bytm-cfg-menu-bg");
|
|
|
+ siteEvents.emit("cfgMenuClosed");
|
|
|
+ if (!menuBg)
|
|
|
+ return;
|
|
|
+ menuBg.style.visibility = "hidden";
|
|
|
+ menuBg.style.display = "none";
|
|
|
+ }
|
|
|
+ /** Opens the config menu if it is closed */
|
|
|
+ function openCfgMenu() {
|
|
|
+ var _a;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (!isCfgMenuAdded)
|
|
|
+ yield addCfgMenu();
|
|
|
+ if (isCfgMenuOpen)
|
|
|
+ return;
|
|
|
+ isCfgMenuOpen = true;
|
|
|
+ document.body.classList.add("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
+ const menuBg = document.querySelector("#bytm-cfg-menu-bg");
|
|
|
+ if (!menuBg)
|
|
|
+ return;
|
|
|
+ menuBg.style.visibility = "visible";
|
|
|
+ menuBg.style.display = "block";
|
|
|
+ checkToggleScrollIndicator();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Checks if the features container is scrollable and toggles the scroll indicator accordingly */
|
|
|
+ function checkToggleScrollIndicator() {
|
|
|
+ const featuresCont = document.querySelector("#bytm-menu-opts");
|
|
|
+ const scrollIndicator = document.querySelector("#bytm-menu-scroll-indicator");
|
|
|
+ // disable scroll indicator if container doesn't scroll
|
|
|
+ if (featuresCont && scrollIndicator) {
|
|
|
+ const verticalScroll = UserUtils.isScrollable(featuresCont).vertical;
|
|
|
+ /** If true, the indicator's threshold is under the available scrollable space and so it should be disabled */
|
|
|
+ const underThreshold = featuresCont.scrollHeight - featuresCont.clientHeight <= scrollIndicatorOffsetThreshold;
|
|
|
+ if (!underThreshold && verticalScroll && !scrollIndicatorEnabled) {
|
|
|
+ scrollIndicatorEnabled = true;
|
|
|
+ scrollIndicator.classList.remove("bytm-hidden");
|
|
|
+ }
|
|
|
+ if ((!verticalScroll && scrollIndicatorEnabled) || underThreshold) {
|
|
|
+ scrollIndicatorEnabled = false;
|
|
|
+ scrollIndicator.classList.add("bytm-hidden");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //#MARKER help dialog
|
|
|
+ let isHelpDialogOpen = false;
|
|
|
+ /** Key of the feature currently loaded in the help dialog */
|
|
|
+ let helpDialogCurFeature;
|
|
|
+ /** Opens the feature help dialog for the given feature */
|
|
|
+ function openHelpDialog(featureKey) {
|
|
|
+ var _a, _b, _c;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (isHelpDialogOpen)
|
|
|
+ return;
|
|
|
+ isHelpDialogOpen = true;
|
|
|
+ let menuBgElem;
|
|
|
+ if (!helpDialogCurFeature) {
|
|
|
+ // create menu
|
|
|
+ const headerElem = document.createElement("div");
|
|
|
+ headerElem.classList.add("bytm-menu-header", "small");
|
|
|
+ const titleCont = document.createElement("div");
|
|
|
+ titleCont.className = "bytm-menu-titlecont-no-title";
|
|
|
+ titleCont.role = "heading";
|
|
|
+ titleCont.ariaLevel = "1";
|
|
|
+ const helpIconSvg = yield resourceToHTMLString("img-help");
|
|
|
+ if (helpIconSvg)
|
|
|
+ titleCont.innerHTML = helpIconSvg;
|
|
|
+ const closeElem = document.createElement("img");
|
|
|
+ closeElem.classList.add("bytm-menu-close", "small");
|
|
|
+ closeElem.role = "button";
|
|
|
+ closeElem.tabIndex = 0;
|
|
|
+ closeElem.src = yield getResourceUrl("img-close");
|
|
|
+ closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
+ closeElem.addEventListener("click", (e) => closeHelpDialog(e));
|
|
|
+ closeElem.addEventListener("keydown", (e) => e.key === "Enter" && closeHelpDialog(e));
|
|
|
+ headerElem.appendChild(titleCont);
|
|
|
+ headerElem.appendChild(closeElem);
|
|
|
+ menuBgElem = document.createElement("div");
|
|
|
+ menuBgElem.id = "bytm-feat-help-menu-bg";
|
|
|
+ menuBgElem.classList.add("bytm-menu-bg");
|
|
|
+ menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
|
|
|
+ menuBgElem.style.visibility = "hidden";
|
|
|
+ menuBgElem.style.display = "none";
|
|
|
+ menuBgElem.addEventListener("click", (e) => {
|
|
|
+ var _a;
|
|
|
+ if (isHelpDialogOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-feat-help-menu-bg")
|
|
|
+ closeHelpDialog(e);
|
|
|
+ });
|
|
|
+ document.body.addEventListener("keydown", (e) => {
|
|
|
+ if (isHelpDialogOpen && e.key === "Escape")
|
|
|
+ closeHelpDialog(e);
|
|
|
+ });
|
|
|
+ const menuContainer = document.createElement("div");
|
|
|
+ menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
+ menuContainer.classList.add("bytm-menu");
|
|
|
+ menuContainer.id = "bytm-feat-help-menu";
|
|
|
+ const featDescElem = document.createElement("h3");
|
|
|
+ featDescElem.id = "bytm-feat-help-menu-desc";
|
|
|
+ const helpTextElem = document.createElement("div");
|
|
|
+ helpTextElem.id = "bytm-feat-help-menu-text";
|
|
|
+ menuContainer.appendChild(headerElem);
|
|
|
+ menuContainer.appendChild(featDescElem);
|
|
|
+ menuContainer.appendChild(helpTextElem);
|
|
|
+ menuBgElem.appendChild(menuContainer);
|
|
|
+ document.body.appendChild(menuBgElem);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ menuBgElem = document.querySelector("#bytm-feat-help-menu-bg");
|
|
|
+ if (helpDialogCurFeature !== featureKey) {
|
|
|
+ // update help text
|
|
|
+ const featDescElem = menuBgElem.querySelector("#bytm-feat-help-menu-desc");
|
|
|
+ const helpTextElem = menuBgElem.querySelector("#bytm-feat-help-menu-text");
|
|
|
+ featDescElem.textContent = t(`feature_desc_${featureKey}`);
|
|
|
+ // @ts-ignore
|
|
|
+ const helpText = (_b = (_a = featInfo[featureKey]) === null || _a === void 0 ? void 0 : _a.helpText) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
|
+ helpTextElem.textContent = helpText !== null && helpText !== void 0 ? helpText : t(`feature_helptext_${featureKey}`);
|
|
|
+ }
|
|
|
+ // show menu
|
|
|
+ const menuBg = document.querySelector("#bytm-feat-help-menu-bg");
|
|
|
+ if (!menuBg)
|
|
|
+ return warn("Couldn't find feature help dialog background element");
|
|
|
+ helpDialogCurFeature = featureKey;
|
|
|
+ menuBg.style.visibility = "visible";
|
|
|
+ menuBg.style.display = "block";
|
|
|
+ (_c = document.querySelector("#bytm-cfg-menu")) === null || _c === void 0 ? void 0 : _c.setAttribute("inert", "true");
|
|
|
+ });
|
|
|
+ }
|
|
|
+ function closeHelpDialog(evt) {
|
|
|
+ var _a;
|
|
|
+ if (!isHelpDialogOpen)
|
|
|
+ return;
|
|
|
+ isHelpDialogOpen = false;
|
|
|
+ (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
+ const menuBg = document.querySelector("#bytm-feat-help-menu-bg");
|
|
|
+ if (!menuBg)
|
|
|
+ return warn("Couldn't find feature help dialog background element");
|
|
|
+ menuBg.style.visibility = "hidden";
|
|
|
+ menuBg.style.display = "none";
|
|
|
+ (_a = document.querySelector("#bytm-cfg-menu")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
|
|
|
+ }
|
|
|
+ //#MARKER export menu
|
|
|
+ let isExportMenuAdded = false;
|
|
|
+ let isExportMenuOpen = false;
|
|
|
+ let copiedTxtTimeout = undefined;
|
|
|
+ let lastUncompressedCfgString;
|
|
|
+ /** Adds a menu to copy the current configuration as compressed (if supported) or uncompressed JSON (hidden by default) */
|
|
|
+ function addExportMenu() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const canCompress = yield compressionSupported();
|
|
|
+ const menuBgElem = document.createElement("div");
|
|
|
+ menuBgElem.id = "bytm-export-menu-bg";
|
|
|
menuBgElem.classList.add("bytm-menu-bg");
|
|
|
menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
|
|
|
menuBgElem.style.visibility = "hidden";
|
|
|
menuBgElem.style.display = "none";
|
|
|
menuBgElem.addEventListener("click", (e) => {
|
|
|
var _a;
|
|
|
- if (isHelpDialogOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-feat-help-menu-bg")
|
|
|
- closeHelpDialog(e);
|
|
|
+ if (isExportMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-export-menu-bg") {
|
|
|
+ closeExportMenu(e);
|
|
|
+ openCfgMenu();
|
|
|
+ }
|
|
|
});
|
|
|
document.body.addEventListener("keydown", (e) => {
|
|
|
- if (isHelpDialogOpen && e.key === "Escape")
|
|
|
- closeHelpDialog(e);
|
|
|
+ if (isExportMenuOpen && e.key === "Escape") {
|
|
|
+ closeExportMenu(e);
|
|
|
+ openCfgMenu();
|
|
|
+ }
|
|
|
});
|
|
|
const menuContainer = document.createElement("div");
|
|
|
menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
menuContainer.classList.add("bytm-menu");
|
|
|
- menuContainer.id = "bytm-feat-help-menu";
|
|
|
- const featDescElem = document.createElement("h3");
|
|
|
- featDescElem.id = "bytm-feat-help-menu-desc";
|
|
|
- const helpTextElem = document.createElement("div");
|
|
|
- helpTextElem.id = "bytm-feat-help-menu-text";
|
|
|
- menuContainer.appendChild(headerElem);
|
|
|
- menuContainer.appendChild(featDescElem);
|
|
|
- menuContainer.appendChild(helpTextElem);
|
|
|
- menuBgElem.appendChild(menuContainer);
|
|
|
- document.body.appendChild(menuBgElem);
|
|
|
- }
|
|
|
- else
|
|
|
- menuBgElem = document.querySelector("#bytm-feat-help-menu-bg");
|
|
|
- if (helpDialogCurFeature !== featureKey) {
|
|
|
- // update help text
|
|
|
- const featDescElem = menuBgElem.querySelector("#bytm-feat-help-menu-desc");
|
|
|
- const helpTextElem = menuBgElem.querySelector("#bytm-feat-help-menu-text");
|
|
|
- featDescElem.textContent = t(`feature_desc_${featureKey}`);
|
|
|
- // @ts-ignore
|
|
|
- const helpText = (_b = (_a = featInfo[featureKey]) === null || _a === void 0 ? void 0 : _a.helpText) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
|
- helpTextElem.textContent = helpText !== null && helpText !== void 0 ? helpText : t(`feature_helptext_${featureKey}`);
|
|
|
- }
|
|
|
- // show menu
|
|
|
- const menuBg = document.querySelector("#bytm-feat-help-menu-bg");
|
|
|
- if (!menuBg)
|
|
|
- return warn("Couldn't find feature help dialog background element");
|
|
|
- helpDialogCurFeature = featureKey;
|
|
|
- menuBg.style.visibility = "visible";
|
|
|
- menuBg.style.display = "block";
|
|
|
- (_c = document.querySelector("#bytm-cfg-menu")) === null || _c === void 0 ? void 0 : _c.setAttribute("inert", "true");
|
|
|
- });
|
|
|
-}
|
|
|
-function closeHelpDialog(evt) {
|
|
|
- var _a;
|
|
|
- if (!isHelpDialogOpen)
|
|
|
- return;
|
|
|
- isHelpDialogOpen = false;
|
|
|
- (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
- const menuBg = document.querySelector("#bytm-feat-help-menu-bg");
|
|
|
- if (!menuBg)
|
|
|
- return warn("Couldn't find feature help dialog background element");
|
|
|
- menuBg.style.visibility = "hidden";
|
|
|
- menuBg.style.display = "none";
|
|
|
- (_a = document.querySelector("#bytm-cfg-menu")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
|
|
|
-}
|
|
|
-//#MARKER export menu
|
|
|
-let isExportMenuAdded = false;
|
|
|
-let isExportMenuOpen = false;
|
|
|
-let copiedTxtTimeout = undefined;
|
|
|
-let lastUncompressedCfgString;
|
|
|
-/** Adds a menu to copy the current configuration as compressed (if supported) or uncompressed JSON (hidden by default) */
|
|
|
-function addExportMenu() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const canCompress = yield compressionSupported();
|
|
|
- const menuBgElem = document.createElement("div");
|
|
|
- menuBgElem.id = "bytm-export-menu-bg";
|
|
|
- menuBgElem.classList.add("bytm-menu-bg");
|
|
|
- menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
|
|
|
- menuBgElem.style.visibility = "hidden";
|
|
|
- menuBgElem.style.display = "none";
|
|
|
- menuBgElem.addEventListener("click", (e) => {
|
|
|
- var _a;
|
|
|
- if (isExportMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-export-menu-bg") {
|
|
|
- closeExportMenu(e);
|
|
|
- openCfgMenu();
|
|
|
- }
|
|
|
- });
|
|
|
- document.body.addEventListener("keydown", (e) => {
|
|
|
- if (isExportMenuOpen && e.key === "Escape") {
|
|
|
+ menuContainer.id = "bytm-export-menu";
|
|
|
+ //#SECTION title bar
|
|
|
+ const headerElem = document.createElement("div");
|
|
|
+ headerElem.classList.add("bytm-menu-header");
|
|
|
+ const titleCont = document.createElement("div");
|
|
|
+ titleCont.className = "bytm-menu-titlecont";
|
|
|
+ titleCont.role = "heading";
|
|
|
+ titleCont.ariaLevel = "1";
|
|
|
+ const titleElem = document.createElement("h2");
|
|
|
+ titleElem.className = "bytm-menu-title";
|
|
|
+ titleElem.textContent = t("export_menu_title", scriptInfo.name);
|
|
|
+ const closeElem = document.createElement("img");
|
|
|
+ closeElem.classList.add("bytm-menu-close");
|
|
|
+ closeElem.role = "button";
|
|
|
+ closeElem.tabIndex = 0;
|
|
|
+ closeElem.src = yield getResourceUrl("img-close");
|
|
|
+ closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
+ const closeExportMenuClicked = (e) => {
|
|
|
closeExportMenu(e);
|
|
|
openCfgMenu();
|
|
|
- }
|
|
|
- });
|
|
|
- const menuContainer = document.createElement("div");
|
|
|
- menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
- menuContainer.classList.add("bytm-menu");
|
|
|
- menuContainer.id = "bytm-export-menu";
|
|
|
- //#SECTION title bar
|
|
|
- const headerElem = document.createElement("div");
|
|
|
- headerElem.classList.add("bytm-menu-header");
|
|
|
- const titleCont = document.createElement("div");
|
|
|
- titleCont.className = "bytm-menu-titlecont";
|
|
|
- titleCont.role = "heading";
|
|
|
- titleCont.ariaLevel = "1";
|
|
|
- const titleElem = document.createElement("h2");
|
|
|
- titleElem.className = "bytm-menu-title";
|
|
|
- titleElem.textContent = t("export_menu_title", scriptInfo.name);
|
|
|
- const closeElem = document.createElement("img");
|
|
|
- closeElem.classList.add("bytm-menu-close");
|
|
|
- closeElem.role = "button";
|
|
|
- closeElem.tabIndex = 0;
|
|
|
- closeElem.src = yield getResourceUrl("img-close");
|
|
|
- closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
- const closeExportMenuClicked = (e) => {
|
|
|
- closeExportMenu(e);
|
|
|
- openCfgMenu();
|
|
|
- };
|
|
|
- closeElem.addEventListener("click", (e) => closeExportMenuClicked(e));
|
|
|
- closeElem.addEventListener("keydown", (e) => e.key === "Enter" && closeExportMenuClicked(e));
|
|
|
- titleCont.appendChild(titleElem);
|
|
|
- headerElem.appendChild(titleCont);
|
|
|
- headerElem.appendChild(closeElem);
|
|
|
- //#SECTION body
|
|
|
- const menuBodyElem = document.createElement("div");
|
|
|
- menuBodyElem.classList.add("bytm-menu-body");
|
|
|
- const textElem = document.createElement("div");
|
|
|
- textElem.id = "bytm-export-menu-text";
|
|
|
- textElem.textContent = t("export_hint");
|
|
|
- const textAreaElem = document.createElement("textarea");
|
|
|
- textAreaElem.id = "bytm-export-menu-textarea";
|
|
|
- textAreaElem.readOnly = true;
|
|
|
- const cfgString = JSON.stringify({ formatVersion, data: getFeatures() });
|
|
|
- lastUncompressedCfgString = JSON.stringify({ formatVersion, data: getFeatures() }, undefined, 2);
|
|
|
- textAreaElem.value = canCompress ? yield UserUtils.compress(cfgString, compressionFormat) : cfgString;
|
|
|
- siteEvents.on("configChanged", (data) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const textAreaElem = document.querySelector("#bytm-export-menu-textarea");
|
|
|
- const cfgString = JSON.stringify({ formatVersion, data });
|
|
|
- lastUncompressedCfgString = JSON.stringify({ formatVersion, data }, undefined, 2);
|
|
|
- if (textAreaElem)
|
|
|
- textAreaElem.value = canCompress ? yield UserUtils.compress(cfgString, compressionFormat) : cfgString;
|
|
|
- }));
|
|
|
- //#SECTION footer
|
|
|
- const footerElem = document.createElement("div");
|
|
|
- footerElem.classList.add("bytm-menu-footer-right");
|
|
|
- const copyBtnElem = document.createElement("button");
|
|
|
- copyBtnElem.classList.add("bytm-btn");
|
|
|
- copyBtnElem.textContent = t("copy_to_clipboard");
|
|
|
- copyBtnElem.ariaLabel = copyBtnElem.title = t("copy_config_tooltip");
|
|
|
- const copiedTextElem = document.createElement("span");
|
|
|
- copiedTextElem.id = "bytm-export-menu-copied-txt";
|
|
|
- copiedTextElem.classList.add("bytm-menu-footer-copied");
|
|
|
- copiedTextElem.textContent = t("copied_notice");
|
|
|
- copiedTextElem.style.display = "none";
|
|
|
- const copyBtnClicked = (evt) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
- const textAreaElem = document.querySelector("#bytm-export-menu-textarea");
|
|
|
- if (textAreaElem) {
|
|
|
- GM.setClipboard(String((evt === null || evt === void 0 ? void 0 : evt.shiftKey) || (evt === null || evt === void 0 ? void 0 : evt.ctrlKey) ? lastUncompressedCfgString : textAreaElem.value));
|
|
|
- copiedTextElem.style.display = "inline-block";
|
|
|
- if (typeof copiedTxtTimeout === "undefined") {
|
|
|
- copiedTxtTimeout = setTimeout(() => {
|
|
|
- copiedTextElem.style.display = "none";
|
|
|
- copiedTxtTimeout = undefined;
|
|
|
- }, 3000);
|
|
|
+ };
|
|
|
+ closeElem.addEventListener("click", (e) => closeExportMenuClicked(e));
|
|
|
+ closeElem.addEventListener("keydown", (e) => e.key === "Enter" && closeExportMenuClicked(e));
|
|
|
+ titleCont.appendChild(titleElem);
|
|
|
+ headerElem.appendChild(titleCont);
|
|
|
+ headerElem.appendChild(closeElem);
|
|
|
+ //#SECTION body
|
|
|
+ const menuBodyElem = document.createElement("div");
|
|
|
+ menuBodyElem.classList.add("bytm-menu-body");
|
|
|
+ const textElem = document.createElement("div");
|
|
|
+ textElem.id = "bytm-export-menu-text";
|
|
|
+ textElem.textContent = t("export_hint");
|
|
|
+ const textAreaElem = document.createElement("textarea");
|
|
|
+ textAreaElem.id = "bytm-export-menu-textarea";
|
|
|
+ textAreaElem.readOnly = true;
|
|
|
+ const cfgString = JSON.stringify({ formatVersion, data: getFeatures() });
|
|
|
+ lastUncompressedCfgString = JSON.stringify({ formatVersion, data: getFeatures() }, undefined, 2);
|
|
|
+ textAreaElem.value = canCompress ? yield UserUtils.compress(cfgString, compressionFormat) : cfgString;
|
|
|
+ siteEvents.on("configChanged", (data) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const textAreaElem = document.querySelector("#bytm-export-menu-textarea");
|
|
|
+ const cfgString = JSON.stringify({ formatVersion, data });
|
|
|
+ lastUncompressedCfgString = JSON.stringify({ formatVersion, data }, undefined, 2);
|
|
|
+ if (textAreaElem)
|
|
|
+ textAreaElem.value = canCompress ? yield UserUtils.compress(cfgString, compressionFormat) : cfgString;
|
|
|
+ }));
|
|
|
+ //#SECTION footer
|
|
|
+ const footerElem = document.createElement("div");
|
|
|
+ footerElem.classList.add("bytm-menu-footer-right");
|
|
|
+ const copyBtnElem = document.createElement("button");
|
|
|
+ copyBtnElem.classList.add("bytm-btn");
|
|
|
+ copyBtnElem.textContent = t("copy_to_clipboard");
|
|
|
+ copyBtnElem.ariaLabel = copyBtnElem.title = t("copy_config_tooltip");
|
|
|
+ const copiedTextElem = document.createElement("span");
|
|
|
+ copiedTextElem.id = "bytm-export-menu-copied-txt";
|
|
|
+ copiedTextElem.classList.add("bytm-menu-footer-copied");
|
|
|
+ copiedTextElem.textContent = t("copied_notice");
|
|
|
+ copiedTextElem.style.display = "none";
|
|
|
+ const copyBtnClicked = (evt) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
+ const textAreaElem = document.querySelector("#bytm-export-menu-textarea");
|
|
|
+ if (textAreaElem) {
|
|
|
+ GM.setClipboard(String((evt === null || evt === void 0 ? void 0 : evt.shiftKey) || (evt === null || evt === void 0 ? void 0 : evt.ctrlKey) ? lastUncompressedCfgString : textAreaElem.value));
|
|
|
+ copiedTextElem.style.display = "inline-block";
|
|
|
+ if (typeof copiedTxtTimeout === "undefined") {
|
|
|
+ copiedTxtTimeout = setTimeout(() => {
|
|
|
+ copiedTextElem.style.display = "none";
|
|
|
+ copiedTxtTimeout = undefined;
|
|
|
+ }, 3000);
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
+ });
|
|
|
+ copyBtnElem.addEventListener("click", copyBtnClicked);
|
|
|
+ copyBtnElem.addEventListener("keydown", (e) => e.key === "Enter" && copyBtnClicked(e));
|
|
|
+ // flex-direction is row-reverse
|
|
|
+ footerElem.appendChild(copyBtnElem);
|
|
|
+ footerElem.appendChild(copiedTextElem);
|
|
|
+ //#SECTION finalize
|
|
|
+ menuBodyElem.appendChild(textElem);
|
|
|
+ menuBodyElem.appendChild(textAreaElem);
|
|
|
+ menuBodyElem.appendChild(footerElem);
|
|
|
+ menuContainer.appendChild(headerElem);
|
|
|
+ menuContainer.appendChild(menuBodyElem);
|
|
|
+ menuBgElem.appendChild(menuContainer);
|
|
|
+ document.body.appendChild(menuBgElem);
|
|
|
});
|
|
|
- copyBtnElem.addEventListener("click", copyBtnClicked);
|
|
|
- copyBtnElem.addEventListener("keydown", (e) => e.key === "Enter" && copyBtnClicked(e));
|
|
|
- // flex-direction is row-reverse
|
|
|
- footerElem.appendChild(copyBtnElem);
|
|
|
- footerElem.appendChild(copiedTextElem);
|
|
|
- //#SECTION finalize
|
|
|
- menuBodyElem.appendChild(textElem);
|
|
|
- menuBodyElem.appendChild(textAreaElem);
|
|
|
- menuBodyElem.appendChild(footerElem);
|
|
|
- menuContainer.appendChild(headerElem);
|
|
|
- menuContainer.appendChild(menuBodyElem);
|
|
|
- menuBgElem.appendChild(menuContainer);
|
|
|
- document.body.appendChild(menuBgElem);
|
|
|
- });
|
|
|
-}
|
|
|
-/** Closes the export menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
-function closeExportMenu(evt) {
|
|
|
- if (!isExportMenuOpen)
|
|
|
- return;
|
|
|
- isExportMenuOpen = false;
|
|
|
- (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
- const menuBg = document.querySelector("#bytm-export-menu-bg");
|
|
|
- if (!menuBg)
|
|
|
- return warn("Couldn't find export menu background element");
|
|
|
- menuBg.style.visibility = "hidden";
|
|
|
- menuBg.style.display = "none";
|
|
|
- const copiedTxt = document.querySelector("#bytm-export-menu-copied-txt");
|
|
|
- if (copiedTxt) {
|
|
|
- copiedTxt.style.display = "none";
|
|
|
- if (typeof copiedTxtTimeout === "number") {
|
|
|
- clearTimeout(copiedTxtTimeout);
|
|
|
- copiedTxtTimeout = undefined;
|
|
|
- }
|
|
|
}
|
|
|
-}
|
|
|
-/** Opens the export menu if it is closed */
|
|
|
-function openExportMenu() {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (!isExportMenuAdded)
|
|
|
- yield addExportMenu();
|
|
|
- isExportMenuAdded = true;
|
|
|
- if (isExportMenuOpen)
|
|
|
+ /** Closes the export menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
+ function closeExportMenu(evt) {
|
|
|
+ if (!isExportMenuOpen)
|
|
|
return;
|
|
|
- isExportMenuOpen = true;
|
|
|
- document.body.classList.add("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
+ isExportMenuOpen = false;
|
|
|
+ (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
const menuBg = document.querySelector("#bytm-export-menu-bg");
|
|
|
if (!menuBg)
|
|
|
return warn("Couldn't find export menu background element");
|
|
|
- menuBg.style.visibility = "visible";
|
|
|
- menuBg.style.display = "block";
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER import menu
|
|
|
-let isImportMenuAdded = false;
|
|
|
-let isImportMenuOpen = false;
|
|
|
-/** Adds a menu to import a configuration from compressed or uncompressed JSON (hidden by default) */
|
|
|
-function addImportMenu() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const menuBgElem = document.createElement("div");
|
|
|
- menuBgElem.id = "bytm-import-menu-bg";
|
|
|
- menuBgElem.classList.add("bytm-menu-bg");
|
|
|
- menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
|
|
|
- menuBgElem.style.visibility = "hidden";
|
|
|
- menuBgElem.style.display = "none";
|
|
|
- menuBgElem.addEventListener("click", (e) => {
|
|
|
- var _a;
|
|
|
- if (isImportMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-import-menu-bg") {
|
|
|
- closeImportMenu(e);
|
|
|
- openCfgMenu();
|
|
|
- }
|
|
|
- });
|
|
|
- document.body.addEventListener("keydown", (e) => {
|
|
|
- if (isImportMenuOpen && e.key === "Escape") {
|
|
|
- closeImportMenu(e);
|
|
|
- openCfgMenu();
|
|
|
+ menuBg.style.visibility = "hidden";
|
|
|
+ menuBg.style.display = "none";
|
|
|
+ const copiedTxt = document.querySelector("#bytm-export-menu-copied-txt");
|
|
|
+ if (copiedTxt) {
|
|
|
+ copiedTxt.style.display = "none";
|
|
|
+ if (typeof copiedTxtTimeout === "number") {
|
|
|
+ clearTimeout(copiedTxtTimeout);
|
|
|
+ copiedTxtTimeout = undefined;
|
|
|
}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /** Opens the export menu if it is closed */
|
|
|
+ function openExportMenu() {
|
|
|
+ var _a;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (!isExportMenuAdded)
|
|
|
+ yield addExportMenu();
|
|
|
+ isExportMenuAdded = true;
|
|
|
+ if (isExportMenuOpen)
|
|
|
+ return;
|
|
|
+ isExportMenuOpen = true;
|
|
|
+ document.body.classList.add("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
+ const menuBg = document.querySelector("#bytm-export-menu-bg");
|
|
|
+ if (!menuBg)
|
|
|
+ return warn("Couldn't find export menu background element");
|
|
|
+ menuBg.style.visibility = "visible";
|
|
|
+ menuBg.style.display = "block";
|
|
|
});
|
|
|
- const menuContainer = document.createElement("div");
|
|
|
- menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
- menuContainer.classList.add("bytm-menu");
|
|
|
- menuContainer.id = "bytm-import-menu";
|
|
|
- //#SECTION title bar
|
|
|
- const headerElem = document.createElement("div");
|
|
|
- headerElem.classList.add("bytm-menu-header");
|
|
|
- const titleCont = document.createElement("div");
|
|
|
- titleCont.className = "bytm-menu-titlecont";
|
|
|
- titleCont.role = "heading";
|
|
|
- titleCont.ariaLevel = "1";
|
|
|
- const titleElem = document.createElement("h2");
|
|
|
- titleElem.className = "bytm-menu-title";
|
|
|
- titleElem.textContent = t("import_menu_title", scriptInfo.name);
|
|
|
- const closeElem = document.createElement("img");
|
|
|
- closeElem.classList.add("bytm-menu-close");
|
|
|
- closeElem.role = "button";
|
|
|
- closeElem.tabIndex = 0;
|
|
|
- closeElem.src = yield getResourceUrl("img-close");
|
|
|
- closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
- const closeImportMenuClicked = (e) => {
|
|
|
- closeImportMenu(e);
|
|
|
- openCfgMenu();
|
|
|
- };
|
|
|
- closeElem.addEventListener("click", closeImportMenuClicked);
|
|
|
- closeElem.addEventListener("keydown", (e) => e.key === "Enter" && closeImportMenuClicked(e));
|
|
|
- titleCont.appendChild(titleElem);
|
|
|
- headerElem.appendChild(titleCont);
|
|
|
- headerElem.appendChild(closeElem);
|
|
|
- //#SECTION body
|
|
|
- const menuBodyElem = document.createElement("div");
|
|
|
- menuBodyElem.classList.add("bytm-menu-body");
|
|
|
- const textElem = document.createElement("div");
|
|
|
- textElem.id = "bytm-import-menu-text";
|
|
|
- textElem.textContent = t("import_hint");
|
|
|
- const textAreaElem = document.createElement("textarea");
|
|
|
- textAreaElem.id = "bytm-import-menu-textarea";
|
|
|
- //#SECTION footer
|
|
|
- const footerElem = document.createElement("div");
|
|
|
- footerElem.classList.add("bytm-menu-footer-right");
|
|
|
- const importBtnElem = document.createElement("button");
|
|
|
- importBtnElem.classList.add("bytm-btn");
|
|
|
- importBtnElem.textContent = t("import");
|
|
|
- importBtnElem.ariaLabel = importBtnElem.title = t("start_import_tooltip");
|
|
|
- importBtnElem.addEventListener("click", (evt) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
- const textAreaElem = document.querySelector("#bytm-import-menu-textarea");
|
|
|
- if (!textAreaElem)
|
|
|
- return warn("Couldn't find import menu textarea element");
|
|
|
- try {
|
|
|
- /** Tries to parse an uncompressed or compressed input string as a JSON object */
|
|
|
- const decode = (input) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- return JSON.parse(input);
|
|
|
- }
|
|
|
- catch (_a) {
|
|
|
- try {
|
|
|
- return JSON.parse(yield UserUtils.decompress(input, compressionFormat));
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- warn("Couldn't import configuration:", err);
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
- const parsed = yield decode(textAreaElem.value.trim());
|
|
|
- if (typeof parsed !== "object")
|
|
|
- return alert(t("import_error_invalid"));
|
|
|
- if (typeof parsed.formatVersion !== "number")
|
|
|
- return alert(t("import_error_no_format_version"));
|
|
|
- if (typeof parsed.data !== "object" || parsed.data === null || Object.keys(parsed.data).length === 0)
|
|
|
- return alert(t("import_error_no_data"));
|
|
|
- if (parsed.formatVersion < formatVersion) {
|
|
|
- let newData = JSON.parse(JSON.stringify(parsed.data));
|
|
|
- const sortedMigrations = Object.entries(migrations)
|
|
|
- .sort(([a], [b]) => Number(a) - Number(b));
|
|
|
- let curFmtVer = Number(parsed.formatVersion);
|
|
|
- for (const [fmtVer, migrationFunc] of sortedMigrations) {
|
|
|
- const ver = Number(fmtVer);
|
|
|
- if (curFmtVer < formatVersion && curFmtVer < ver) {
|
|
|
- try {
|
|
|
- const migRes = JSON.parse(JSON.stringify(migrationFunc(newData)));
|
|
|
- newData = migRes instanceof Promise ? yield migRes : migRes;
|
|
|
- curFmtVer = ver;
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- console.error(`Error while running migration function for format version ${fmtVer}:`, err);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- parsed.formatVersion = curFmtVer;
|
|
|
- parsed.data = newData;
|
|
|
- }
|
|
|
- else if (parsed.formatVersion !== formatVersion)
|
|
|
- return alert(t("import_error_wrong_format_version", formatVersion, parsed.formatVersion));
|
|
|
- yield saveFeatures(Object.assign(Object.assign({}, getFeatures()), parsed.data));
|
|
|
- if (confirm(t("import_success_confirm_reload"))) {
|
|
|
- disableBeforeUnload();
|
|
|
- return location.reload();
|
|
|
- }
|
|
|
- emitSiteEvent("rebuildCfgMenu", parsed.data);
|
|
|
- closeImportMenu();
|
|
|
- openCfgMenu();
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- warn("Couldn't import configuration:", err);
|
|
|
- alert(t("import_error_invalid"));
|
|
|
- }
|
|
|
- }));
|
|
|
- footerElem.appendChild(importBtnElem);
|
|
|
- //#SECTION finalize
|
|
|
- menuBodyElem.appendChild(textElem);
|
|
|
- menuBodyElem.appendChild(textAreaElem);
|
|
|
- menuBodyElem.appendChild(footerElem);
|
|
|
- menuContainer.appendChild(headerElem);
|
|
|
- menuContainer.appendChild(menuBodyElem);
|
|
|
- menuBgElem.appendChild(menuContainer);
|
|
|
- document.body.appendChild(menuBgElem);
|
|
|
- });
|
|
|
-}
|
|
|
-/** Closes the import menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
-function closeImportMenu(evt) {
|
|
|
- if (!isImportMenuOpen)
|
|
|
- return;
|
|
|
- isImportMenuOpen = false;
|
|
|
- (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
- const menuBg = document.querySelector("#bytm-import-menu-bg");
|
|
|
- const textAreaElem = document.querySelector("#bytm-import-menu-textarea");
|
|
|
- if (textAreaElem)
|
|
|
- textAreaElem.value = "";
|
|
|
- if (!menuBg)
|
|
|
- return warn("Couldn't find import menu background element");
|
|
|
- menuBg.style.visibility = "hidden";
|
|
|
- menuBg.style.display = "none";
|
|
|
-}
|
|
|
-/** Opens the import menu if it is closed */
|
|
|
-function openImportMenu() {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (!isImportMenuAdded)
|
|
|
- yield addImportMenu();
|
|
|
- isImportMenuAdded = true;
|
|
|
- if (isImportMenuOpen)
|
|
|
+ }
|
|
|
+ //#MARKER import menu
|
|
|
+ let isImportMenuAdded = false;
|
|
|
+ let isImportMenuOpen = false;
|
|
|
+ /** Adds a menu to import a configuration from compressed or uncompressed JSON (hidden by default) */
|
|
|
+ function addImportMenu() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const menuBgElem = document.createElement("div");
|
|
|
+ menuBgElem.id = "bytm-import-menu-bg";
|
|
|
+ menuBgElem.classList.add("bytm-menu-bg");
|
|
|
+ menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
|
|
|
+ menuBgElem.style.visibility = "hidden";
|
|
|
+ menuBgElem.style.display = "none";
|
|
|
+ menuBgElem.addEventListener("click", (e) => {
|
|
|
+ var _a;
|
|
|
+ if (isImportMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-import-menu-bg") {
|
|
|
+ closeImportMenu(e);
|
|
|
+ openCfgMenu();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ document.body.addEventListener("keydown", (e) => {
|
|
|
+ if (isImportMenuOpen && e.key === "Escape") {
|
|
|
+ closeImportMenu(e);
|
|
|
+ openCfgMenu();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const menuContainer = document.createElement("div");
|
|
|
+ menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
+ menuContainer.classList.add("bytm-menu");
|
|
|
+ menuContainer.id = "bytm-import-menu";
|
|
|
+ //#SECTION title bar
|
|
|
+ const headerElem = document.createElement("div");
|
|
|
+ headerElem.classList.add("bytm-menu-header");
|
|
|
+ const titleCont = document.createElement("div");
|
|
|
+ titleCont.className = "bytm-menu-titlecont";
|
|
|
+ titleCont.role = "heading";
|
|
|
+ titleCont.ariaLevel = "1";
|
|
|
+ const titleElem = document.createElement("h2");
|
|
|
+ titleElem.className = "bytm-menu-title";
|
|
|
+ titleElem.textContent = t("import_menu_title", scriptInfo.name);
|
|
|
+ const closeElem = document.createElement("img");
|
|
|
+ closeElem.classList.add("bytm-menu-close");
|
|
|
+ closeElem.role = "button";
|
|
|
+ closeElem.tabIndex = 0;
|
|
|
+ closeElem.src = yield getResourceUrl("img-close");
|
|
|
+ closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
+ const closeImportMenuClicked = (e) => {
|
|
|
+ closeImportMenu(e);
|
|
|
+ openCfgMenu();
|
|
|
+ };
|
|
|
+ closeElem.addEventListener("click", closeImportMenuClicked);
|
|
|
+ closeElem.addEventListener("keydown", (e) => e.key === "Enter" && closeImportMenuClicked(e));
|
|
|
+ titleCont.appendChild(titleElem);
|
|
|
+ headerElem.appendChild(titleCont);
|
|
|
+ headerElem.appendChild(closeElem);
|
|
|
+ //#SECTION body
|
|
|
+ const menuBodyElem = document.createElement("div");
|
|
|
+ menuBodyElem.classList.add("bytm-menu-body");
|
|
|
+ const textElem = document.createElement("div");
|
|
|
+ textElem.id = "bytm-import-menu-text";
|
|
|
+ textElem.textContent = t("import_hint");
|
|
|
+ const textAreaElem = document.createElement("textarea");
|
|
|
+ textAreaElem.id = "bytm-import-menu-textarea";
|
|
|
+ //#SECTION footer
|
|
|
+ const footerElem = document.createElement("div");
|
|
|
+ footerElem.classList.add("bytm-menu-footer-right");
|
|
|
+ const importBtnElem = document.createElement("button");
|
|
|
+ importBtnElem.classList.add("bytm-btn");
|
|
|
+ importBtnElem.textContent = t("import");
|
|
|
+ importBtnElem.ariaLabel = importBtnElem.title = t("start_import_tooltip");
|
|
|
+ importBtnElem.addEventListener("click", (evt) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
+ const textAreaElem = document.querySelector("#bytm-import-menu-textarea");
|
|
|
+ if (!textAreaElem)
|
|
|
+ return warn("Couldn't find import menu textarea element");
|
|
|
+ try {
|
|
|
+ /** Tries to parse an uncompressed or compressed input string as a JSON object */
|
|
|
+ const decode = (input) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ return JSON.parse(input);
|
|
|
+ }
|
|
|
+ catch (_a) {
|
|
|
+ try {
|
|
|
+ return JSON.parse(yield UserUtils.decompress(input, compressionFormat));
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ warn("Couldn't import configuration:", err);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const parsed = yield decode(textAreaElem.value.trim());
|
|
|
+ if (typeof parsed !== "object")
|
|
|
+ return alert(t("import_error_invalid"));
|
|
|
+ if (typeof parsed.formatVersion !== "number")
|
|
|
+ return alert(t("import_error_no_format_version"));
|
|
|
+ if (typeof parsed.data !== "object" || parsed.data === null || Object.keys(parsed.data).length === 0)
|
|
|
+ return alert(t("import_error_no_data"));
|
|
|
+ if (parsed.formatVersion < formatVersion) {
|
|
|
+ let newData = JSON.parse(JSON.stringify(parsed.data));
|
|
|
+ const sortedMigrations = Object.entries(migrations)
|
|
|
+ .sort(([a], [b]) => Number(a) - Number(b));
|
|
|
+ let curFmtVer = Number(parsed.formatVersion);
|
|
|
+ for (const [fmtVer, migrationFunc] of sortedMigrations) {
|
|
|
+ const ver = Number(fmtVer);
|
|
|
+ if (curFmtVer < formatVersion && curFmtVer < ver) {
|
|
|
+ try {
|
|
|
+ const migRes = JSON.parse(JSON.stringify(migrationFunc(newData)));
|
|
|
+ newData = migRes instanceof Promise ? yield migRes : migRes;
|
|
|
+ curFmtVer = ver;
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ console.error(`Error while running migration function for format version ${fmtVer}:`, err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ parsed.formatVersion = curFmtVer;
|
|
|
+ parsed.data = newData;
|
|
|
+ }
|
|
|
+ else if (parsed.formatVersion !== formatVersion)
|
|
|
+ return alert(t("import_error_wrong_format_version", formatVersion, parsed.formatVersion));
|
|
|
+ yield saveFeatures(Object.assign(Object.assign({}, getFeatures()), parsed.data));
|
|
|
+ if (confirm(t("import_success_confirm_reload"))) {
|
|
|
+ disableBeforeUnload();
|
|
|
+ return location.reload();
|
|
|
+ }
|
|
|
+ emitSiteEvent("rebuildCfgMenu", parsed.data);
|
|
|
+ closeImportMenu();
|
|
|
+ openCfgMenu();
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ warn("Couldn't import configuration:", err);
|
|
|
+ alert(t("import_error_invalid"));
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ footerElem.appendChild(importBtnElem);
|
|
|
+ //#SECTION finalize
|
|
|
+ menuBodyElem.appendChild(textElem);
|
|
|
+ menuBodyElem.appendChild(textAreaElem);
|
|
|
+ menuBodyElem.appendChild(footerElem);
|
|
|
+ menuContainer.appendChild(headerElem);
|
|
|
+ menuContainer.appendChild(menuBodyElem);
|
|
|
+ menuBgElem.appendChild(menuContainer);
|
|
|
+ document.body.appendChild(menuBgElem);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Closes the import menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
+ function closeImportMenu(evt) {
|
|
|
+ if (!isImportMenuOpen)
|
|
|
return;
|
|
|
- isImportMenuOpen = true;
|
|
|
- document.body.classList.add("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
+ isImportMenuOpen = false;
|
|
|
+ (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
const menuBg = document.querySelector("#bytm-import-menu-bg");
|
|
|
+ const textAreaElem = document.querySelector("#bytm-import-menu-textarea");
|
|
|
+ if (textAreaElem)
|
|
|
+ textAreaElem.value = "";
|
|
|
if (!menuBg)
|
|
|
return warn("Couldn't find import menu background element");
|
|
|
- menuBg.style.visibility = "visible";
|
|
|
- menuBg.style.display = "block";
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER changelog menu
|
|
|
-let isChangelogMenuAdded = false;
|
|
|
-let isChangelogMenuOpen = false;
|
|
|
-/** Adds a changelog menu (hidden by default) */
|
|
|
-function addChangelogMenu() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const menuBgElem = document.createElement("div");
|
|
|
- menuBgElem.id = "bytm-changelog-menu-bg";
|
|
|
- menuBgElem.classList.add("bytm-menu-bg");
|
|
|
- menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
|
|
|
- menuBgElem.style.visibility = "hidden";
|
|
|
- menuBgElem.style.display = "none";
|
|
|
- menuBgElem.addEventListener("click", (e) => {
|
|
|
- var _a;
|
|
|
- if (isChangelogMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-changelog-menu-bg") {
|
|
|
+ menuBg.style.visibility = "hidden";
|
|
|
+ menuBg.style.display = "none";
|
|
|
+ }
|
|
|
+ /** Opens the import menu if it is closed */
|
|
|
+ function openImportMenu() {
|
|
|
+ var _a;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (!isImportMenuAdded)
|
|
|
+ yield addImportMenu();
|
|
|
+ isImportMenuAdded = true;
|
|
|
+ if (isImportMenuOpen)
|
|
|
+ return;
|
|
|
+ isImportMenuOpen = true;
|
|
|
+ document.body.classList.add("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
+ const menuBg = document.querySelector("#bytm-import-menu-bg");
|
|
|
+ if (!menuBg)
|
|
|
+ return warn("Couldn't find import menu background element");
|
|
|
+ menuBg.style.visibility = "visible";
|
|
|
+ menuBg.style.display = "block";
|
|
|
+ });
|
|
|
+ }
|
|
|
+ //#MARKER changelog menu
|
|
|
+ let isChangelogMenuAdded = false;
|
|
|
+ let isChangelogMenuOpen = false;
|
|
|
+ /** Adds a changelog menu (hidden by default) */
|
|
|
+ function addChangelogMenu() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const menuBgElem = document.createElement("div");
|
|
|
+ menuBgElem.id = "bytm-changelog-menu-bg";
|
|
|
+ menuBgElem.classList.add("bytm-menu-bg");
|
|
|
+ menuBgElem.ariaLabel = menuBgElem.title = t("close_menu_tooltip");
|
|
|
+ menuBgElem.style.visibility = "hidden";
|
|
|
+ menuBgElem.style.display = "none";
|
|
|
+ menuBgElem.addEventListener("click", (e) => {
|
|
|
+ var _a;
|
|
|
+ if (isChangelogMenuOpen && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === "bytm-changelog-menu-bg") {
|
|
|
+ closeChangelogMenu(e);
|
|
|
+ if (menuBgElem.dataset.returnTo === "cfgMenu")
|
|
|
+ openCfgMenu();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ document.body.addEventListener("keydown", (e) => {
|
|
|
+ if (isChangelogMenuOpen && e.key === "Escape") {
|
|
|
+ closeChangelogMenu(e);
|
|
|
+ if (menuBgElem.dataset.returnTo === "cfgMenu")
|
|
|
+ openCfgMenu();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const menuContainer = document.createElement("div");
|
|
|
+ menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
+ menuContainer.classList.add("bytm-menu");
|
|
|
+ menuContainer.id = "bytm-changelog-menu";
|
|
|
+ //#SECTION title bar
|
|
|
+ const headerElem = document.createElement("div");
|
|
|
+ headerElem.classList.add("bytm-menu-header");
|
|
|
+ const titleCont = document.createElement("div");
|
|
|
+ titleCont.className = "bytm-menu-titlecont";
|
|
|
+ titleCont.role = "heading";
|
|
|
+ titleCont.ariaLevel = "1";
|
|
|
+ const titleElem = document.createElement("h2");
|
|
|
+ titleElem.className = "bytm-menu-title";
|
|
|
+ titleElem.textContent = t("changelog_menu_title", scriptInfo.name);
|
|
|
+ const closeElem = document.createElement("img");
|
|
|
+ closeElem.classList.add("bytm-menu-close");
|
|
|
+ closeElem.role = "button";
|
|
|
+ closeElem.tabIndex = 0;
|
|
|
+ closeElem.src = yield getResourceUrl("img-close");
|
|
|
+ closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
+ const closeChangelogMenuClicked = (e) => {
|
|
|
closeChangelogMenu(e);
|
|
|
if (menuBgElem.dataset.returnTo === "cfgMenu")
|
|
|
openCfgMenu();
|
|
|
+ };
|
|
|
+ closeElem.addEventListener("click", closeChangelogMenuClicked);
|
|
|
+ closeElem.addEventListener("keydown", (e) => e.key === "Enter" && closeChangelogMenuClicked(e));
|
|
|
+ titleCont.appendChild(titleElem);
|
|
|
+ headerElem.appendChild(titleCont);
|
|
|
+ headerElem.appendChild(closeElem);
|
|
|
+ //#SECTION body
|
|
|
+ const getChangelogHtml = (() => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ const changelogMd = yield getChangelogMd();
|
|
|
+ return yield parseMarkdown(changelogMd);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ return `Error: ${err}`;
|
|
|
+ }
|
|
|
+ }));
|
|
|
+ const menuBodyElem = document.createElement("div");
|
|
|
+ menuBodyElem.id = "bytm-changelog-menu-body";
|
|
|
+ menuBodyElem.classList.add("bytm-menu-body");
|
|
|
+ const textElem = document.createElement("div");
|
|
|
+ textElem.id = "bytm-changelog-menu-text";
|
|
|
+ textElem.classList.add("bytm-markdown-container");
|
|
|
+ textElem.innerHTML = yield getChangelogHtml();
|
|
|
+ //#SECTION finalize
|
|
|
+ menuBodyElem.appendChild(textElem);
|
|
|
+ menuContainer.appendChild(headerElem);
|
|
|
+ menuContainer.appendChild(menuBodyElem);
|
|
|
+ menuBgElem.appendChild(menuContainer);
|
|
|
+ document.body.appendChild(menuBgElem);
|
|
|
+ const anchors = document.querySelectorAll("#bytm-changelog-menu-text a");
|
|
|
+ for (const anchor of anchors) {
|
|
|
+ anchor.ariaLabel = anchor.title = anchor.href;
|
|
|
+ anchor.target = "_blank";
|
|
|
}
|
|
|
});
|
|
|
- document.body.addEventListener("keydown", (e) => {
|
|
|
- if (isChangelogMenuOpen && e.key === "Escape") {
|
|
|
- closeChangelogMenu(e);
|
|
|
- if (menuBgElem.dataset.returnTo === "cfgMenu")
|
|
|
+ }
|
|
|
+ /** Closes the changelog menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
+ function closeChangelogMenu(evt) {
|
|
|
+ if (!isChangelogMenuOpen)
|
|
|
+ return;
|
|
|
+ isChangelogMenuOpen = false;
|
|
|
+ (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
+ const menuBg = document.querySelector("#bytm-changelog-menu-bg");
|
|
|
+ if (!menuBg)
|
|
|
+ return warn("Couldn't find changelog menu background element");
|
|
|
+ menuBg.style.visibility = "hidden";
|
|
|
+ menuBg.style.display = "none";
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Opens the changelog menu if it is closed
|
|
|
+ * @param returnTo What menu to open after the changelog menu is closed
|
|
|
+ */
|
|
|
+ function openChangelogMenu(returnTo = "cfgMenu") {
|
|
|
+ var _a;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (!isChangelogMenuAdded)
|
|
|
+ yield addChangelogMenu();
|
|
|
+ isChangelogMenuAdded = true;
|
|
|
+ if (isChangelogMenuOpen)
|
|
|
+ return;
|
|
|
+ isChangelogMenuOpen = true;
|
|
|
+ document.body.classList.add("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
+ const menuBg = document.querySelector("#bytm-changelog-menu-bg");
|
|
|
+ if (!menuBg)
|
|
|
+ return warn("Couldn't find changelog menu background element");
|
|
|
+ menuBg.dataset.returnTo = returnTo;
|
|
|
+ menuBg.style.visibility = "visible";
|
|
|
+ menuBg.style.display = "block";
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ let features$2;
|
|
|
+ function setLayoutConfig(feats) {
|
|
|
+ features$2 = feats;
|
|
|
+ }
|
|
|
+ //#MARKER BYTM-Config buttons
|
|
|
+ let logoExchanged = false, improveLogoCalled = false;
|
|
|
+ /** Adds a watermark beneath the logo */
|
|
|
+ function addWatermark() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const watermark = document.createElement("a");
|
|
|
+ watermark.role = "button";
|
|
|
+ watermark.id = "bytm-watermark";
|
|
|
+ watermark.className = "style-scope ytmusic-nav-bar bytm-no-select";
|
|
|
+ watermark.textContent = scriptInfo.name;
|
|
|
+ watermark.ariaLabel = watermark.title = t("open_menu_tooltip", scriptInfo.name);
|
|
|
+ watermark.tabIndex = 0;
|
|
|
+ improveLogo();
|
|
|
+ const watermarkOpenMenu = (e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ if ((!e.shiftKey && !e.ctrlKey) || logoExchanged)
|
|
|
openCfgMenu();
|
|
|
- }
|
|
|
+ if (!logoExchanged && (e.shiftKey || e.ctrlKey))
|
|
|
+ exchangeLogo();
|
|
|
+ };
|
|
|
+ watermark.addEventListener("click", watermarkOpenMenu);
|
|
|
+ watermark.addEventListener("keydown", (e) => e.key === "Enter" && watermarkOpenMenu(e));
|
|
|
+ onSelectorOld("ytmusic-nav-bar #left-content", {
|
|
|
+ listener: (logoElem) => UserUtils.insertAfter(logoElem, watermark),
|
|
|
+ });
|
|
|
+ log("Added watermark element");
|
|
|
});
|
|
|
- const menuContainer = document.createElement("div");
|
|
|
- menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
- menuContainer.classList.add("bytm-menu");
|
|
|
- menuContainer.id = "bytm-changelog-menu";
|
|
|
- //#SECTION title bar
|
|
|
- const headerElem = document.createElement("div");
|
|
|
- headerElem.classList.add("bytm-menu-header");
|
|
|
- const titleCont = document.createElement("div");
|
|
|
- titleCont.className = "bytm-menu-titlecont";
|
|
|
- titleCont.role = "heading";
|
|
|
- titleCont.ariaLevel = "1";
|
|
|
- const titleElem = document.createElement("h2");
|
|
|
- titleElem.className = "bytm-menu-title";
|
|
|
- titleElem.textContent = t("changelog_menu_title", scriptInfo.name);
|
|
|
- const closeElem = document.createElement("img");
|
|
|
- closeElem.classList.add("bytm-menu-close");
|
|
|
- closeElem.role = "button";
|
|
|
- closeElem.tabIndex = 0;
|
|
|
- closeElem.src = yield getResourceUrl("img-close");
|
|
|
- closeElem.ariaLabel = closeElem.title = t("close_menu_tooltip");
|
|
|
- const closeChangelogMenuClicked = (e) => {
|
|
|
- closeChangelogMenu(e);
|
|
|
- if (menuBgElem.dataset.returnTo === "cfgMenu")
|
|
|
- openCfgMenu();
|
|
|
- };
|
|
|
- closeElem.addEventListener("click", closeChangelogMenuClicked);
|
|
|
- closeElem.addEventListener("keydown", (e) => e.key === "Enter" && closeChangelogMenuClicked(e));
|
|
|
- titleCont.appendChild(titleElem);
|
|
|
- headerElem.appendChild(titleCont);
|
|
|
- headerElem.appendChild(closeElem);
|
|
|
- //#SECTION body
|
|
|
- const getChangelogHtml = (() => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ }
|
|
|
+ /** Turns the regular `<img>`-based logo into inline SVG to be able to animate and modify parts of it */
|
|
|
+ function improveLogo() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
try {
|
|
|
- const changelogMd = yield getChangelogMd();
|
|
|
- return yield parseMarkdown(changelogMd);
|
|
|
+ if (improveLogoCalled)
|
|
|
+ return;
|
|
|
+ improveLogoCalled = true;
|
|
|
+ const res = yield UserUtils.fetchAdvanced("https://music.youtube.com/img/on_platform_logo_dark.svg");
|
|
|
+ const svg = yield res.text();
|
|
|
+ onSelectorOld("ytmusic-logo a", {
|
|
|
+ listener: (logoElem) => {
|
|
|
+ var _a;
|
|
|
+ logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
|
|
|
+ logoElem.innerHTML = svg;
|
|
|
+ logoElem.querySelectorAll("ellipse").forEach((e) => {
|
|
|
+ e.classList.add("bytm-mod-logo-ellipse");
|
|
|
+ });
|
|
|
+ (_a = logoElem.querySelector("path")) === null || _a === void 0 ? void 0 : _a.classList.add("bytm-mod-logo-path");
|
|
|
+ log("Swapped logo to inline SVG");
|
|
|
+ },
|
|
|
+ });
|
|
|
}
|
|
|
catch (err) {
|
|
|
- return `Error: ${err}`;
|
|
|
+ error("Couldn't improve logo due to an error:", err);
|
|
|
}
|
|
|
- }));
|
|
|
- const menuBodyElem = document.createElement("div");
|
|
|
- menuBodyElem.id = "bytm-changelog-menu-body";
|
|
|
- menuBodyElem.classList.add("bytm-menu-body");
|
|
|
- const textElem = document.createElement("div");
|
|
|
- textElem.id = "bytm-changelog-menu-text";
|
|
|
- textElem.classList.add("bytm-markdown-container");
|
|
|
- textElem.innerHTML = yield getChangelogHtml();
|
|
|
- //#SECTION finalize
|
|
|
- menuBodyElem.appendChild(textElem);
|
|
|
- menuContainer.appendChild(headerElem);
|
|
|
- menuContainer.appendChild(menuBodyElem);
|
|
|
- menuBgElem.appendChild(menuContainer);
|
|
|
- document.body.appendChild(menuBgElem);
|
|
|
- const anchors = document.querySelectorAll("#bytm-changelog-menu-text a");
|
|
|
- for (const anchor of anchors) {
|
|
|
- anchor.ariaLabel = anchor.title = anchor.href;
|
|
|
- anchor.target = "_blank";
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-/** Closes the changelog menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
-function closeChangelogMenu(evt) {
|
|
|
- if (!isChangelogMenuOpen)
|
|
|
- return;
|
|
|
- isChangelogMenuOpen = false;
|
|
|
- (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
- const menuBg = document.querySelector("#bytm-changelog-menu-bg");
|
|
|
- if (!menuBg)
|
|
|
- return warn("Couldn't find changelog menu background element");
|
|
|
- menuBg.style.visibility = "hidden";
|
|
|
- menuBg.style.display = "none";
|
|
|
-}
|
|
|
-/**
|
|
|
- * Opens the changelog menu if it is closed
|
|
|
- * @param returnTo What menu to open after the changelog menu is closed
|
|
|
- */
|
|
|
-function openChangelogMenu(returnTo = "cfgMenu") {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (!isChangelogMenuAdded)
|
|
|
- yield addChangelogMenu();
|
|
|
- isChangelogMenuAdded = true;
|
|
|
- if (isChangelogMenuOpen)
|
|
|
- return;
|
|
|
- isChangelogMenuOpen = true;
|
|
|
- document.body.classList.add("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
- const menuBg = document.querySelector("#bytm-changelog-menu-bg");
|
|
|
- if (!menuBg)
|
|
|
- return warn("Couldn't find changelog menu background element");
|
|
|
- menuBg.dataset.returnTo = returnTo;
|
|
|
- menuBg.style.visibility = "visible";
|
|
|
- menuBg.style.display = "block";
|
|
|
- });
|
|
|
-}let features$2;
|
|
|
-function setLayoutConfig(feats) {
|
|
|
- features$2 = feats;
|
|
|
-}
|
|
|
-//#MARKER BYTM-Config buttons
|
|
|
-let logoExchanged = false, improveLogoCalled = false;
|
|
|
-/** Adds a watermark beneath the logo */
|
|
|
-function addWatermark() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const watermark = document.createElement("a");
|
|
|
- watermark.role = "button";
|
|
|
- watermark.id = "bytm-watermark";
|
|
|
- watermark.className = "style-scope ytmusic-nav-bar bytm-no-select";
|
|
|
- watermark.textContent = scriptInfo.name;
|
|
|
- watermark.ariaLabel = watermark.title = t("open_menu_tooltip", scriptInfo.name);
|
|
|
- watermark.tabIndex = 0;
|
|
|
- improveLogo();
|
|
|
- const watermarkOpenMenu = (e) => {
|
|
|
- e.stopPropagation();
|
|
|
- if ((!e.shiftKey && !e.ctrlKey) || logoExchanged)
|
|
|
- openCfgMenu();
|
|
|
- if (!logoExchanged && (e.shiftKey || e.ctrlKey))
|
|
|
- exchangeLogo();
|
|
|
- };
|
|
|
- watermark.addEventListener("click", watermarkOpenMenu);
|
|
|
- watermark.addEventListener("keydown", (e) => e.key === "Enter" && watermarkOpenMenu(e));
|
|
|
- onSelectorOld("ytmusic-nav-bar #left-content", {
|
|
|
- listener: (logoElem) => UserUtils.insertAfter(logoElem, watermark),
|
|
|
});
|
|
|
- log("Added watermark element");
|
|
|
- });
|
|
|
-}
|
|
|
-/** Turns the regular `<img>`-based logo into inline SVG to be able to animate and modify parts of it */
|
|
|
-function improveLogo() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- if (improveLogoCalled)
|
|
|
- return;
|
|
|
- improveLogoCalled = true;
|
|
|
- const res = yield UserUtils.fetchAdvanced("https://music.youtube.com/img/on_platform_logo_dark.svg");
|
|
|
- const svg = yield res.text();
|
|
|
- onSelectorOld("ytmusic-logo a", {
|
|
|
- listener: (logoElem) => {
|
|
|
- var _a;
|
|
|
- logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
|
|
|
- logoElem.innerHTML = svg;
|
|
|
- logoElem.querySelectorAll("ellipse").forEach((e) => {
|
|
|
- e.classList.add("bytm-mod-logo-ellipse");
|
|
|
- });
|
|
|
- (_a = logoElem.querySelector("path")) === null || _a === void 0 ? void 0 : _a.classList.add("bytm-mod-logo-path");
|
|
|
- log("Swapped logo to inline SVG");
|
|
|
+ }
|
|
|
+ /** Exchanges the default YTM logo into BetterYTM's logo with a sick ass animation */
|
|
|
+ function exchangeLogo() {
|
|
|
+ onSelectorOld(".bytm-mod-logo", {
|
|
|
+ listener: (logoElem) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (logoElem.classList.contains("bytm-logo-exchanged"))
|
|
|
+ return;
|
|
|
+ logoExchanged = true;
|
|
|
+ logoElem.classList.add("bytm-logo-exchanged");
|
|
|
+ const iconUrl = yield getResourceUrl("img-logo");
|
|
|
+ const newLogo = document.createElement("img");
|
|
|
+ newLogo.className = "bytm-mod-logo-img";
|
|
|
+ newLogo.src = iconUrl;
|
|
|
+ logoElem.insertBefore(newLogo, logoElem.querySelector("svg"));
|
|
|
+ document.head.querySelectorAll("link[rel=\"icon\"]").forEach((e) => {
|
|
|
+ e.href = iconUrl;
|
|
|
+ });
|
|
|
+ setTimeout(() => {
|
|
|
+ logoElem.querySelectorAll(".bytm-mod-logo-ellipse").forEach(e => e.remove());
|
|
|
+ }, 1000);
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Called whenever the avatar popover menu exists to add a BYTM-Configuration button to the user menu popover */
|
|
|
+ function addConfigMenuOption(container) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const cfgOptElem = document.createElement("div");
|
|
|
+ cfgOptElem.className = "bytm-cfg-menu-option";
|
|
|
+ const cfgOptItemElem = document.createElement("div");
|
|
|
+ cfgOptItemElem.className = "bytm-cfg-menu-option-item";
|
|
|
+ cfgOptItemElem.role = "button";
|
|
|
+ cfgOptItemElem.tabIndex = 0;
|
|
|
+ cfgOptItemElem.ariaLabel = cfgOptItemElem.title = t("open_menu_tooltip", scriptInfo.name);
|
|
|
+ const cfgOptItemClicked = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const settingsBtnElem = document.querySelector("ytmusic-nav-bar ytmusic-settings-button tp-yt-paper-icon-button");
|
|
|
+ settingsBtnElem === null || settingsBtnElem === void 0 ? void 0 : settingsBtnElem.click();
|
|
|
+ yield UserUtils.pauseFor(20);
|
|
|
+ if ((!e.shiftKey && !e.ctrlKey) || logoExchanged)
|
|
|
+ openCfgMenu();
|
|
|
+ if (!logoExchanged && (e.shiftKey || e.ctrlKey))
|
|
|
+ exchangeLogo();
|
|
|
+ });
|
|
|
+ cfgOptItemElem.addEventListener("click", cfgOptItemClicked);
|
|
|
+ cfgOptItemElem.addEventListener("keydown", (e) => e.key === "Enter" && cfgOptItemClicked(e));
|
|
|
+ const cfgOptIconElem = document.createElement("img");
|
|
|
+ cfgOptIconElem.className = "bytm-cfg-menu-option-icon";
|
|
|
+ cfgOptIconElem.src = yield getResourceUrl("img-logo");
|
|
|
+ const cfgOptTextElem = document.createElement("div");
|
|
|
+ cfgOptTextElem.className = "bytm-cfg-menu-option-text";
|
|
|
+ cfgOptTextElem.textContent = t("config_menu_option", scriptInfo.name);
|
|
|
+ cfgOptItemElem.appendChild(cfgOptIconElem);
|
|
|
+ cfgOptItemElem.appendChild(cfgOptTextElem);
|
|
|
+ cfgOptElem.appendChild(cfgOptItemElem);
|
|
|
+ container.appendChild(cfgOptElem);
|
|
|
+ improveLogo();
|
|
|
+ log("Added BYTM-Configuration button to menu popover");
|
|
|
+ });
|
|
|
+ }
|
|
|
+ //#MARKER remove upgrade tab
|
|
|
+ /** Removes the "Upgrade" / YT Music Premium tab from the sidebar */
|
|
|
+ function removeUpgradeTab() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ onSelectorOld("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
|
|
|
+ listener: (tabElemLarge) => {
|
|
|
+ tabElemLarge.remove();
|
|
|
+ log("Removed large upgrade tab");
|
|
|
},
|
|
|
});
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't improve logo due to an error:", err);
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-/** Exchanges the default YTM logo into BetterYTM's logo with a sick ass animation */
|
|
|
-function exchangeLogo() {
|
|
|
- onSelectorOld(".bytm-mod-logo", {
|
|
|
- listener: (logoElem) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (logoElem.classList.contains("bytm-logo-exchanged"))
|
|
|
- return;
|
|
|
- logoExchanged = true;
|
|
|
- logoElem.classList.add("bytm-logo-exchanged");
|
|
|
- const iconUrl = yield getResourceUrl("img-logo");
|
|
|
- const newLogo = document.createElement("img");
|
|
|
- newLogo.className = "bytm-mod-logo-img";
|
|
|
- newLogo.src = iconUrl;
|
|
|
- logoElem.insertBefore(newLogo, logoElem.querySelector("svg"));
|
|
|
- document.head.querySelectorAll("link[rel=\"icon\"]").forEach((e) => {
|
|
|
- e.href = iconUrl;
|
|
|
+ onSelectorOld("ytmusic-app-layout #mini-guide ytmusic-guide-renderer #sections ytmusic-guide-section-renderer[is-primary] #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
|
|
|
+ listener: (tabElemSmall) => {
|
|
|
+ tabElemSmall.remove();
|
|
|
+ log("Removed small upgrade tab");
|
|
|
+ },
|
|
|
});
|
|
|
- setTimeout(() => {
|
|
|
- logoElem.querySelectorAll(".bytm-mod-logo-ellipse").forEach(e => e.remove());
|
|
|
- }, 1000);
|
|
|
- }),
|
|
|
- });
|
|
|
-}
|
|
|
-/** Called whenever the avatar popover menu exists to add a BYTM-Configuration button to the user menu popover */
|
|
|
-function addConfigMenuOption(container) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const cfgOptElem = document.createElement("div");
|
|
|
- cfgOptElem.className = "bytm-cfg-menu-option";
|
|
|
- const cfgOptItemElem = document.createElement("div");
|
|
|
- cfgOptItemElem.className = "bytm-cfg-menu-option-item";
|
|
|
- cfgOptItemElem.role = "button";
|
|
|
- cfgOptItemElem.tabIndex = 0;
|
|
|
- cfgOptItemElem.ariaLabel = cfgOptItemElem.title = t("open_menu_tooltip", scriptInfo.name);
|
|
|
- const cfgOptItemClicked = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const settingsBtnElem = document.querySelector("ytmusic-nav-bar ytmusic-settings-button tp-yt-paper-icon-button");
|
|
|
- settingsBtnElem === null || settingsBtnElem === void 0 ? void 0 : settingsBtnElem.click();
|
|
|
- yield UserUtils.pauseFor(20);
|
|
|
- if ((!e.shiftKey && !e.ctrlKey) || logoExchanged)
|
|
|
- openCfgMenu();
|
|
|
- if (!logoExchanged && (e.shiftKey || e.ctrlKey))
|
|
|
- exchangeLogo();
|
|
|
});
|
|
|
- cfgOptItemElem.addEventListener("click", cfgOptItemClicked);
|
|
|
- cfgOptItemElem.addEventListener("keydown", (e) => e.key === "Enter" && cfgOptItemClicked(e));
|
|
|
- const cfgOptIconElem = document.createElement("img");
|
|
|
- cfgOptIconElem.className = "bytm-cfg-menu-option-icon";
|
|
|
- cfgOptIconElem.src = yield getResourceUrl("img-logo");
|
|
|
- const cfgOptTextElem = document.createElement("div");
|
|
|
- cfgOptTextElem.className = "bytm-cfg-menu-option-text";
|
|
|
- cfgOptTextElem.textContent = t("config_menu_option", scriptInfo.name);
|
|
|
- cfgOptItemElem.appendChild(cfgOptIconElem);
|
|
|
- cfgOptItemElem.appendChild(cfgOptTextElem);
|
|
|
- cfgOptElem.appendChild(cfgOptItemElem);
|
|
|
- container.appendChild(cfgOptElem);
|
|
|
- improveLogo();
|
|
|
- log("Added BYTM-Configuration button to menu popover");
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER remove upgrade tab
|
|
|
-/** Removes the "Upgrade" / YT Music Premium tab from the sidebar */
|
|
|
-function removeUpgradeTab() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- onSelectorOld("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
|
|
|
- listener: (tabElemLarge) => {
|
|
|
- tabElemLarge.remove();
|
|
|
- log("Removed large upgrade tab");
|
|
|
- },
|
|
|
- });
|
|
|
- onSelectorOld("ytmusic-app-layout #mini-guide ytmusic-guide-renderer #sections ytmusic-guide-section-renderer[is-primary] #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
|
|
|
- listener: (tabElemSmall) => {
|
|
|
- tabElemSmall.remove();
|
|
|
- log("Removed small upgrade tab");
|
|
|
- },
|
|
|
- });
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER volume slider
|
|
|
-function initVolumeFeatures() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- // not technically an input element but behaves pretty much the same
|
|
|
- onSelectorOld("tp-yt-paper-slider#volume-slider", {
|
|
|
- listener: (sliderElem) => {
|
|
|
- const volSliderCont = document.createElement("div");
|
|
|
- volSliderCont.id = "bytm-vol-slider-cont";
|
|
|
- if (features$2.volumeSliderScrollStep !== featInfo.volumeSliderScrollStep.default) {
|
|
|
- for (const evtName of ["wheel", "scroll", "mousewheel", "DOMMouseScroll"]) {
|
|
|
- volSliderCont.addEventListener(evtName, (e) => {
|
|
|
- var _a, _b;
|
|
|
- e.preventDefault();
|
|
|
- // cancels all the other events that would be fired
|
|
|
- e.stopImmediatePropagation();
|
|
|
- const delta = (_b = (_a = e.deltaY) !== null && _a !== void 0 ? _a : e.detail) !== null && _b !== void 0 ? _b : 1;
|
|
|
- const volumeDir = -Math.sign(delta);
|
|
|
- const newVolume = String(Number(sliderElem.value) + (features$2.volumeSliderScrollStep * volumeDir));
|
|
|
- sliderElem.value = newVolume;
|
|
|
- sliderElem.setAttribute("aria-valuenow", newVolume);
|
|
|
- // make the site actually change the volume
|
|
|
- sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
|
|
|
- }, {
|
|
|
- // takes precedence over the slider's own event listener
|
|
|
- capture: true,
|
|
|
- });
|
|
|
+ }
|
|
|
+ //#MARKER volume slider
|
|
|
+ function initVolumeFeatures() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ // not technically an input element but behaves pretty much the same
|
|
|
+ onSelectorOld("tp-yt-paper-slider#volume-slider", {
|
|
|
+ listener: (sliderElem) => {
|
|
|
+ const volSliderCont = document.createElement("div");
|
|
|
+ volSliderCont.id = "bytm-vol-slider-cont";
|
|
|
+ if (features$2.volumeSliderScrollStep !== featInfo.volumeSliderScrollStep.default) {
|
|
|
+ for (const evtName of ["wheel", "scroll", "mousewheel", "DOMMouseScroll"]) {
|
|
|
+ volSliderCont.addEventListener(evtName, (e) => {
|
|
|
+ var _a, _b;
|
|
|
+ e.preventDefault();
|
|
|
+ // cancels all the other events that would be fired
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ const delta = (_b = (_a = e.deltaY) !== null && _a !== void 0 ? _a : e.detail) !== null && _b !== void 0 ? _b : 1;
|
|
|
+ const volumeDir = -Math.sign(delta);
|
|
|
+ const newVolume = String(Number(sliderElem.value) + (features$2.volumeSliderScrollStep * volumeDir));
|
|
|
+ sliderElem.value = newVolume;
|
|
|
+ sliderElem.setAttribute("aria-valuenow", newVolume);
|
|
|
+ // make the site actually change the volume
|
|
|
+ sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
|
|
|
+ }, {
|
|
|
+ // takes precedence over the slider's own event listener
|
|
|
+ capture: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
- UserUtils.addParent(sliderElem, volSliderCont);
|
|
|
- if (typeof features$2.volumeSliderSize === "number")
|
|
|
- setVolSliderSize();
|
|
|
- if (features$2.volumeSliderLabel)
|
|
|
- addVolumeSliderLabel(sliderElem, volSliderCont);
|
|
|
- setVolSliderStep(sliderElem);
|
|
|
- },
|
|
|
+ UserUtils.addParent(sliderElem, volSliderCont);
|
|
|
+ if (typeof features$2.volumeSliderSize === "number")
|
|
|
+ setVolSliderSize();
|
|
|
+ if (features$2.volumeSliderLabel)
|
|
|
+ addVolumeSliderLabel(sliderElem, volSliderCont);
|
|
|
+ setVolSliderStep(sliderElem);
|
|
|
+ },
|
|
|
+ });
|
|
|
});
|
|
|
- });
|
|
|
-}
|
|
|
-/** Adds a percentage label to the volume slider and tooltip */
|
|
|
-function addVolumeSliderLabel(sliderElem, sliderContainer) {
|
|
|
- const labelElem = document.createElement("div");
|
|
|
- labelElem.id = "bytm-vol-slider-label";
|
|
|
- labelElem.textContent = `${sliderElem.value}%`;
|
|
|
- // prevent video from minimizing
|
|
|
- labelElem.addEventListener("click", (e) => e.stopPropagation());
|
|
|
- const getLabelText = (slider) => { var _a; return t("volume_tooltip", slider.value, (_a = features$2.volumeSliderStep) !== null && _a !== void 0 ? _a : slider.step); };
|
|
|
- const labelFull = getLabelText(sliderElem);
|
|
|
- sliderContainer.setAttribute("title", labelFull);
|
|
|
- sliderElem.setAttribute("title", labelFull);
|
|
|
- sliderElem.setAttribute("aria-valuetext", labelFull);
|
|
|
- const updateLabel = () => {
|
|
|
+ }
|
|
|
+ /** Adds a percentage label to the volume slider and tooltip */
|
|
|
+ function addVolumeSliderLabel(sliderElem, sliderContainer) {
|
|
|
+ const labelElem = document.createElement("div");
|
|
|
+ labelElem.id = "bytm-vol-slider-label";
|
|
|
+ labelElem.textContent = `${sliderElem.value}%`;
|
|
|
+ // prevent video from minimizing
|
|
|
+ labelElem.addEventListener("click", (e) => e.stopPropagation());
|
|
|
+ const getLabelText = (slider) => { var _a; return t("volume_tooltip", slider.value, (_a = features$2.volumeSliderStep) !== null && _a !== void 0 ? _a : slider.step); };
|
|
|
const labelFull = getLabelText(sliderElem);
|
|
|
sliderContainer.setAttribute("title", labelFull);
|
|
|
sliderElem.setAttribute("title", labelFull);
|
|
|
sliderElem.setAttribute("aria-valuetext", labelFull);
|
|
|
- const labelElem2 = document.querySelector("#bytm-vol-slider-label");
|
|
|
- if (labelElem2)
|
|
|
- labelElem2.textContent = `${sliderElem.value}%`;
|
|
|
- };
|
|
|
- sliderElem.addEventListener("change", () => updateLabel());
|
|
|
- onSelectorOld("#bytm-vol-slider-cont", {
|
|
|
- listener: (volumeCont) => {
|
|
|
- volumeCont.appendChild(labelElem);
|
|
|
- },
|
|
|
- });
|
|
|
- let lastSliderVal = Number(sliderElem.value);
|
|
|
- // show label if hovering over slider or slider is focused
|
|
|
- const sliderHoverObserver = new MutationObserver(() => {
|
|
|
- if (sliderElem.classList.contains("on-hover") || document.activeElement === sliderElem)
|
|
|
- labelElem.classList.add("bytm-visible");
|
|
|
- else if (labelElem.classList.contains("bytm-visible") || document.activeElement !== sliderElem)
|
|
|
- labelElem.classList.remove("bytm-visible");
|
|
|
- if (Number(sliderElem.value) !== lastSliderVal) {
|
|
|
- lastSliderVal = Number(sliderElem.value);
|
|
|
- updateLabel();
|
|
|
- }
|
|
|
- });
|
|
|
- sliderHoverObserver.observe(sliderElem, {
|
|
|
- attributes: true,
|
|
|
- });
|
|
|
-}
|
|
|
-/** Sets the volume slider to a set size */
|
|
|
-function setVolSliderSize() {
|
|
|
- const { volumeSliderSize: size } = features$2;
|
|
|
- if (typeof size !== "number" || isNaN(Number(size)))
|
|
|
- return;
|
|
|
- UserUtils.addGlobalStyle(`\
|
|
|
+ const updateLabel = () => {
|
|
|
+ const labelFull = getLabelText(sliderElem);
|
|
|
+ sliderContainer.setAttribute("title", labelFull);
|
|
|
+ sliderElem.setAttribute("title", labelFull);
|
|
|
+ sliderElem.setAttribute("aria-valuetext", labelFull);
|
|
|
+ const labelElem2 = document.querySelector("#bytm-vol-slider-label");
|
|
|
+ if (labelElem2)
|
|
|
+ labelElem2.textContent = `${sliderElem.value}%`;
|
|
|
+ };
|
|
|
+ sliderElem.addEventListener("change", () => updateLabel());
|
|
|
+ onSelectorOld("#bytm-vol-slider-cont", {
|
|
|
+ listener: (volumeCont) => {
|
|
|
+ volumeCont.appendChild(labelElem);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ let lastSliderVal = Number(sliderElem.value);
|
|
|
+ // show label if hovering over slider or slider is focused
|
|
|
+ const sliderHoverObserver = new MutationObserver(() => {
|
|
|
+ if (sliderElem.classList.contains("on-hover") || document.activeElement === sliderElem)
|
|
|
+ labelElem.classList.add("bytm-visible");
|
|
|
+ else if (labelElem.classList.contains("bytm-visible") || document.activeElement !== sliderElem)
|
|
|
+ labelElem.classList.remove("bytm-visible");
|
|
|
+ if (Number(sliderElem.value) !== lastSliderVal) {
|
|
|
+ lastSliderVal = Number(sliderElem.value);
|
|
|
+ updateLabel();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ sliderHoverObserver.observe(sliderElem, {
|
|
|
+ attributes: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Sets the volume slider to a set size */
|
|
|
+ function setVolSliderSize() {
|
|
|
+ const { volumeSliderSize: size } = features$2;
|
|
|
+ if (typeof size !== "number" || isNaN(Number(size)))
|
|
|
+ return;
|
|
|
+ UserUtils.addGlobalStyle(`\
|
|
|
#bytm-vol-slider-cont tp-yt-paper-slider#volume-slider {
|
|
|
width: ${size}px !important;
|
|
|
}`).id = "bytm-style-vol-slider-size";
|
|
|
-}
|
|
|
-/** Sets the `step` attribute of the volume slider */
|
|
|
-function setVolSliderStep(sliderElem) {
|
|
|
- sliderElem.setAttribute("step", String(features$2.volumeSliderStep));
|
|
|
-}
|
|
|
-//#MARKER anchor improvements
|
|
|
-/** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
|
|
|
-function addAnchorImprovements() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- const css = yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("css-anchor_improvements"))).text();
|
|
|
- if (css)
|
|
|
- UserUtils.addGlobalStyle(css).id = "bytm-style-anchor-improvements";
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't add anchor improvements CSS due to an error:", err);
|
|
|
- }
|
|
|
- //#SECTION carousel shelves
|
|
|
- try {
|
|
|
- const preventDefault = (e) => e.preventDefault();
|
|
|
- /** Adds anchor improvements to <ytmusic-responsive-list-item-renderer> */
|
|
|
- const addListItemAnchors = (items) => {
|
|
|
- var _a;
|
|
|
- for (const item of items) {
|
|
|
- if (item.classList.contains("bytm-anchor-improved"))
|
|
|
- continue;
|
|
|
- item.classList.add("bytm-anchor-improved");
|
|
|
- const thumbnailElem = item.querySelector(".left-items");
|
|
|
- const titleElem = item.querySelector(".title-column .title a");
|
|
|
- if (!thumbnailElem || !titleElem)
|
|
|
- continue;
|
|
|
- const anchorElem = document.createElement("a");
|
|
|
- anchorElem.classList.add("bytm-anchor", "bytm-carousel-shelf-anchor");
|
|
|
- anchorElem.href = (_a = titleElem === null || titleElem === void 0 ? void 0 : titleElem.href) !== null && _a !== void 0 ? _a : "#";
|
|
|
- anchorElem.target = "_self";
|
|
|
- anchorElem.role = "button";
|
|
|
- anchorElem.addEventListener("click", preventDefault);
|
|
|
- UserUtils.addParent(thumbnailElem, anchorElem);
|
|
|
- }
|
|
|
- };
|
|
|
- // home page
|
|
|
- onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-carousel-shelf-renderer ytmusic-responsive-list-item-renderer", {
|
|
|
- continuous: true,
|
|
|
- all: true,
|
|
|
- listener: addListItemAnchors,
|
|
|
- });
|
|
|
- // related tab in /watch
|
|
|
- onSelectorOld("ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] ytmusic-responsive-list-item-renderer", {
|
|
|
- continuous: true,
|
|
|
- all: true,
|
|
|
- listener: addListItemAnchors,
|
|
|
- });
|
|
|
- // playlists
|
|
|
- onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer", {
|
|
|
- continuous: true,
|
|
|
- all: true,
|
|
|
- listener: addListItemAnchors,
|
|
|
- });
|
|
|
- // generic shelves
|
|
|
- onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer", {
|
|
|
- continuous: true,
|
|
|
- all: true,
|
|
|
- listener: addListItemAnchors,
|
|
|
- });
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't improve carousel shelf anchors due to an error:", err);
|
|
|
- }
|
|
|
- //#SECTION sidebar
|
|
|
- try {
|
|
|
- const addSidebarAnchors = (sidebarCont) => {
|
|
|
- const items = sidebarCont.parentNode.querySelectorAll("ytmusic-guide-entry-renderer tp-yt-paper-item");
|
|
|
- improveSidebarAnchors(items);
|
|
|
- return items.length;
|
|
|
- };
|
|
|
- onSelectorOld("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
|
|
|
- listener: (sidebarCont) => {
|
|
|
- const itemsAmt = addSidebarAnchors(sidebarCont);
|
|
|
- log(`Added anchors around ${itemsAmt} sidebar ${UserUtils.autoPlural("item", itemsAmt)}`);
|
|
|
- },
|
|
|
- });
|
|
|
- onSelectorOld("ytmusic-app-layout #mini-guide ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
|
|
|
- listener: (miniSidebarCont) => {
|
|
|
- const itemsAmt = addSidebarAnchors(miniSidebarCont);
|
|
|
- log(`Added anchors around ${itemsAmt} mini sidebar ${UserUtils.autoPlural("item", itemsAmt)}`);
|
|
|
- },
|
|
|
- });
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't add anchors to sidebar items due to an error:", err);
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-const sidebarPaths = [
|
|
|
- "/",
|
|
|
- "/explore",
|
|
|
- "/library",
|
|
|
-];
|
|
|
-/**
|
|
|
- * Adds anchors to the sidebar items so they can be opened in a new tab
|
|
|
- * @param sidebarItem
|
|
|
- */
|
|
|
-function improveSidebarAnchors(sidebarItems) {
|
|
|
- sidebarItems.forEach((item, i) => {
|
|
|
- var _a;
|
|
|
- const anchorElem = document.createElement("a");
|
|
|
- anchorElem.classList.add("bytm-anchor", "bytm-no-select");
|
|
|
- anchorElem.role = "button";
|
|
|
- anchorElem.target = "_self";
|
|
|
- anchorElem.href = (_a = sidebarPaths[i]) !== null && _a !== void 0 ? _a : "#";
|
|
|
- anchorElem.ariaLabel = anchorElem.title = t("middle_click_open_tab");
|
|
|
- anchorElem.addEventListener("click", (e) => {
|
|
|
- e.preventDefault();
|
|
|
- });
|
|
|
- UserUtils.addParent(item, anchorElem);
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER remove share tracking param
|
|
|
-let lastShareVal = "";
|
|
|
-/** Removes the ?si tracking parameter from share URLs */
|
|
|
-function removeShareTrackingParam() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const removeSiParam = (inputElem) => {
|
|
|
+ }
|
|
|
+ /** Sets the `step` attribute of the volume slider */
|
|
|
+ function setVolSliderStep(sliderElem) {
|
|
|
+ sliderElem.setAttribute("step", String(features$2.volumeSliderStep));
|
|
|
+ }
|
|
|
+ //#MARKER anchor improvements
|
|
|
+ /** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
|
|
|
+ function addAnchorImprovements() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
try {
|
|
|
- if (lastShareVal === inputElem.value)
|
|
|
- return;
|
|
|
- const url = new URL(inputElem.value);
|
|
|
- if (!url.searchParams.has("si"))
|
|
|
- return;
|
|
|
- lastShareVal = inputElem.value;
|
|
|
- url.searchParams.delete("si");
|
|
|
- inputElem.value = String(url);
|
|
|
- log(`Removed tracking parameter from share link: ${url}`);
|
|
|
+ const css = yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("css-anchor_improvements"))).text();
|
|
|
+ if (css)
|
|
|
+ UserUtils.addGlobalStyle(css).id = "bytm-style-anchor-improvements";
|
|
|
}
|
|
|
catch (err) {
|
|
|
- warn("Couldn't remove tracking parameter from share link due to error:", err);
|
|
|
+ error("Couldn't add anchor improvements CSS due to an error:", err);
|
|
|
}
|
|
|
- };
|
|
|
- onSelectorOld("tp-yt-paper-dialog ytmusic-unified-share-panel-renderer", {
|
|
|
- listener: (sharePanelEl) => {
|
|
|
- const obs = new MutationObserver(() => {
|
|
|
- const inputElem = sharePanelEl.querySelector("input#share-url");
|
|
|
- inputElem && removeSiParam(inputElem);
|
|
|
+ //#SECTION carousel shelves
|
|
|
+ try {
|
|
|
+ const preventDefault = (e) => e.preventDefault();
|
|
|
+ /** Adds anchor improvements to <ytmusic-responsive-list-item-renderer> */
|
|
|
+ const addListItemAnchors = (items) => {
|
|
|
+ var _a;
|
|
|
+ for (const item of items) {
|
|
|
+ if (item.classList.contains("bytm-anchor-improved"))
|
|
|
+ continue;
|
|
|
+ item.classList.add("bytm-anchor-improved");
|
|
|
+ const thumbnailElem = item.querySelector(".left-items");
|
|
|
+ const titleElem = item.querySelector(".title-column .title a");
|
|
|
+ if (!thumbnailElem || !titleElem)
|
|
|
+ continue;
|
|
|
+ const anchorElem = document.createElement("a");
|
|
|
+ anchorElem.classList.add("bytm-anchor", "bytm-carousel-shelf-anchor");
|
|
|
+ anchorElem.href = (_a = titleElem === null || titleElem === void 0 ? void 0 : titleElem.href) !== null && _a !== void 0 ? _a : "#";
|
|
|
+ anchorElem.target = "_self";
|
|
|
+ anchorElem.role = "button";
|
|
|
+ anchorElem.addEventListener("click", preventDefault);
|
|
|
+ UserUtils.addParent(thumbnailElem, anchorElem);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ // home page
|
|
|
+ onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-carousel-shelf-renderer ytmusic-responsive-list-item-renderer", {
|
|
|
+ continuous: true,
|
|
|
+ all: true,
|
|
|
+ listener: addListItemAnchors,
|
|
|
});
|
|
|
- obs.observe(sharePanelEl, {
|
|
|
- childList: true,
|
|
|
- subtree: true,
|
|
|
- attributeFilter: ["aria-hidden", "checked"],
|
|
|
+ // related tab in /watch
|
|
|
+ onSelectorOld("ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] ytmusic-responsive-list-item-renderer", {
|
|
|
+ continuous: true,
|
|
|
+ all: true,
|
|
|
+ listener: addListItemAnchors,
|
|
|
});
|
|
|
- },
|
|
|
+ // playlists
|
|
|
+ onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer", {
|
|
|
+ continuous: true,
|
|
|
+ all: true,
|
|
|
+ listener: addListItemAnchors,
|
|
|
+ });
|
|
|
+ // generic shelves
|
|
|
+ onSelectorOld("#contents.ytmusic-section-list-renderer ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer", {
|
|
|
+ continuous: true,
|
|
|
+ all: true,
|
|
|
+ listener: addListItemAnchors,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't improve carousel shelf anchors due to an error:", err);
|
|
|
+ }
|
|
|
+ //#SECTION sidebar
|
|
|
+ try {
|
|
|
+ const addSidebarAnchors = (sidebarCont) => {
|
|
|
+ const items = sidebarCont.parentNode.querySelectorAll("ytmusic-guide-entry-renderer tp-yt-paper-item");
|
|
|
+ improveSidebarAnchors(items);
|
|
|
+ return items.length;
|
|
|
+ };
|
|
|
+ onSelectorOld("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
|
|
|
+ listener: (sidebarCont) => {
|
|
|
+ const itemsAmt = addSidebarAnchors(sidebarCont);
|
|
|
+ log(`Added anchors around ${itemsAmt} sidebar ${UserUtils.autoPlural("item", itemsAmt)}`);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ onSelectorOld("ytmusic-app-layout #mini-guide ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
|
|
|
+ listener: (miniSidebarCont) => {
|
|
|
+ const itemsAmt = addSidebarAnchors(miniSidebarCont);
|
|
|
+ log(`Added anchors around ${itemsAmt} mini sidebar ${UserUtils.autoPlural("item", itemsAmt)}`);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't add anchors to sidebar items due to an error:", err);
|
|
|
+ }
|
|
|
});
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER fix margins
|
|
|
-/** Applies global CSS to fix various spacings */
|
|
|
-function fixSpacing() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- const css = yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("css-fix_spacing"))).text();
|
|
|
- if (css)
|
|
|
- UserUtils.addGlobalStyle(css).id = "bytm-style-fix-spacing";
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't fix spacing due to an error:", err);
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER scroll to active song
|
|
|
-/** Adds a button to the queue to scroll to the active song */
|
|
|
-function addScrollToActiveBtn() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- onSelectorOld("#side-panel #tabsContent tp-yt-paper-tab:nth-of-type(1)", {
|
|
|
- listener: (tabElem) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const containerElem = document.createElement("div");
|
|
|
- containerElem.id = "bytm-scroll-to-active-btn-cont";
|
|
|
- const linkElem = document.createElement("div");
|
|
|
- linkElem.id = "bytm-scroll-to-active-btn";
|
|
|
- linkElem.className = "ytmusic-player-bar bytm-generic-btn";
|
|
|
- linkElem.ariaLabel = linkElem.title = t("scroll_to_playing");
|
|
|
- linkElem.role = "button";
|
|
|
- const imgElem = document.createElement("img");
|
|
|
- imgElem.className = "bytm-generic-btn-img";
|
|
|
- imgElem.src = yield getResourceUrl("img-skip_to");
|
|
|
- linkElem.addEventListener("click", (e) => {
|
|
|
- const activeItem = document.querySelector("#side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"loading\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"playing\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"paused\"]");
|
|
|
- if (!activeItem)
|
|
|
+ }
|
|
|
+ const sidebarPaths = [
|
|
|
+ "/",
|
|
|
+ "/explore",
|
|
|
+ "/library",
|
|
|
+ ];
|
|
|
+ /**
|
|
|
+ * Adds anchors to the sidebar items so they can be opened in a new tab
|
|
|
+ * @param sidebarItem
|
|
|
+ */
|
|
|
+ function improveSidebarAnchors(sidebarItems) {
|
|
|
+ sidebarItems.forEach((item, i) => {
|
|
|
+ var _a;
|
|
|
+ const anchorElem = document.createElement("a");
|
|
|
+ anchorElem.classList.add("bytm-anchor", "bytm-no-select");
|
|
|
+ anchorElem.role = "button";
|
|
|
+ anchorElem.target = "_self";
|
|
|
+ anchorElem.href = (_a = sidebarPaths[i]) !== null && _a !== void 0 ? _a : "#";
|
|
|
+ anchorElem.ariaLabel = anchorElem.title = t("middle_click_open_tab");
|
|
|
+ anchorElem.addEventListener("click", (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ });
|
|
|
+ UserUtils.addParent(item, anchorElem);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ //#MARKER remove share tracking param
|
|
|
+ let lastShareVal = "";
|
|
|
+ /** Removes the ?si tracking parameter from share URLs */
|
|
|
+ function removeShareTrackingParam() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const removeSiParam = (inputElem) => {
|
|
|
+ try {
|
|
|
+ if (lastShareVal === inputElem.value)
|
|
|
return;
|
|
|
- e.preventDefault();
|
|
|
- e.stopImmediatePropagation();
|
|
|
- activeItem.scrollIntoView({
|
|
|
- behavior: "smooth",
|
|
|
- block: "center",
|
|
|
- inline: "center",
|
|
|
+ const url = new URL(inputElem.value);
|
|
|
+ if (!url.searchParams.has("si"))
|
|
|
+ return;
|
|
|
+ lastShareVal = inputElem.value;
|
|
|
+ url.searchParams.delete("si");
|
|
|
+ inputElem.value = String(url);
|
|
|
+ log(`Removed tracking parameter from share link: ${url}`);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ warn("Couldn't remove tracking parameter from share link due to error:", err);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ onSelectorOld("tp-yt-paper-dialog ytmusic-unified-share-panel-renderer", {
|
|
|
+ listener: (sharePanelEl) => {
|
|
|
+ const obs = new MutationObserver(() => {
|
|
|
+ const inputElem = sharePanelEl.querySelector("input#share-url");
|
|
|
+ inputElem && removeSiParam(inputElem);
|
|
|
});
|
|
|
- });
|
|
|
- linkElem.appendChild(imgElem);
|
|
|
- containerElem.appendChild(linkElem);
|
|
|
- tabElem.appendChild(containerElem);
|
|
|
- }),
|
|
|
+ obs.observe(sharePanelEl, {
|
|
|
+ childList: true,
|
|
|
+ subtree: true,
|
|
|
+ attributeFilter: ["aria-hidden", "checked"],
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
});
|
|
|
- });
|
|
|
-}let features$1;
|
|
|
-function setInputConfig(feats) {
|
|
|
- features$1 = feats;
|
|
|
-}
|
|
|
-//#MARKER arrow key skip
|
|
|
-function initArrowKeySkip() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- document.addEventListener("keydown", (evt) => {
|
|
|
- var _a, _b, _c, _d;
|
|
|
- if (!["ArrowLeft", "ArrowRight"].includes(evt.code))
|
|
|
- return;
|
|
|
- // discard the event when a (text) input is currently active, like when editing a playlist
|
|
|
- if (["INPUT", "TEXTAREA", "SELECT"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) !== null && _b !== void 0 ? _b : "_"))
|
|
|
- return info(`Captured valid key to skip forward or backward but the current active element is <${(_c = document.activeElement) === null || _c === void 0 ? void 0 : _c.tagName.toLowerCase()}>, so the keypress is ignored`);
|
|
|
- evt.preventDefault();
|
|
|
- evt.stopImmediatePropagation();
|
|
|
- let skipBy = (_d = features$1.arrowKeySkipBy) !== null && _d !== void 0 ? _d : featInfo.arrowKeySkipBy.default;
|
|
|
- if (evt.code === "ArrowLeft")
|
|
|
- skipBy *= -1;
|
|
|
- log(`Captured arrow key '${evt.code}' - skipping by ${skipBy} seconds`);
|
|
|
- const vidElem = document.querySelector(videoSelector);
|
|
|
- if (vidElem)
|
|
|
- vidElem.currentTime = UserUtils.clamp(vidElem.currentTime + skipBy, 0, vidElem.duration);
|
|
|
+ }
|
|
|
+ //#MARKER fix margins
|
|
|
+ /** Applies global CSS to fix various spacings */
|
|
|
+ function fixSpacing() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ const css = yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("css-fix_spacing"))).text();
|
|
|
+ if (css)
|
|
|
+ UserUtils.addGlobalStyle(css).id = "bytm-style-fix-spacing";
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't fix spacing due to an error:", err);
|
|
|
+ }
|
|
|
});
|
|
|
- log("Added arrow key press listener");
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER site switch
|
|
|
-/** switch sites only if current video time is greater than this value */
|
|
|
-const videoTimeThreshold = 3;
|
|
|
-let siteSwitchEnabled = true;
|
|
|
-/** Initializes the site switch feature */
|
|
|
-function initSiteSwitch(domain) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- document.addEventListener("keydown", (e) => {
|
|
|
- const hotkey = features$1.switchSitesHotkey;
|
|
|
- if (siteSwitchEnabled && e.code === hotkey.code && e.shiftKey === hotkey.shift && e.ctrlKey === hotkey.ctrl && e.altKey === hotkey.alt)
|
|
|
- switchSite(domain === "yt" ? "ytm" : "yt");
|
|
|
+ }
|
|
|
+ //#MARKER scroll to active song
|
|
|
+ /** Adds a button to the queue to scroll to the active song */
|
|
|
+ function addScrollToActiveBtn() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ onSelectorOld("#side-panel #tabsContent tp-yt-paper-tab:nth-of-type(1)", {
|
|
|
+ listener: (tabElem) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const containerElem = document.createElement("div");
|
|
|
+ containerElem.id = "bytm-scroll-to-active-btn-cont";
|
|
|
+ const linkElem = document.createElement("div");
|
|
|
+ linkElem.id = "bytm-scroll-to-active-btn";
|
|
|
+ linkElem.className = "ytmusic-player-bar bytm-generic-btn";
|
|
|
+ linkElem.ariaLabel = linkElem.title = t("scroll_to_playing");
|
|
|
+ linkElem.role = "button";
|
|
|
+ const imgElem = document.createElement("img");
|
|
|
+ imgElem.className = "bytm-generic-btn-img";
|
|
|
+ imgElem.src = yield getResourceUrl("img-skip_to");
|
|
|
+ linkElem.addEventListener("click", (e) => {
|
|
|
+ const activeItem = document.querySelector("#side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"loading\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"playing\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"paused\"]");
|
|
|
+ if (!activeItem)
|
|
|
+ return;
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ activeItem.scrollIntoView({
|
|
|
+ behavior: "smooth",
|
|
|
+ block: "center",
|
|
|
+ inline: "center",
|
|
|
+ });
|
|
|
+ });
|
|
|
+ linkElem.appendChild(imgElem);
|
|
|
+ containerElem.appendChild(linkElem);
|
|
|
+ tabElem.appendChild(containerElem);
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ let features$1;
|
|
|
+ function setInputConfig(feats) {
|
|
|
+ features$1 = feats;
|
|
|
+ }
|
|
|
+ //#MARKER arrow key skip
|
|
|
+ function initArrowKeySkip() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ document.addEventListener("keydown", (evt) => {
|
|
|
+ var _a, _b, _c, _d;
|
|
|
+ if (!["ArrowLeft", "ArrowRight"].includes(evt.code))
|
|
|
+ return;
|
|
|
+ // discard the event when a (text) input is currently active, like when editing a playlist
|
|
|
+ if (["INPUT", "TEXTAREA", "SELECT"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.tagName) !== null && _b !== void 0 ? _b : "_"))
|
|
|
+ return info(`Captured valid key to skip forward or backward but the current active element is <${(_c = document.activeElement) === null || _c === void 0 ? void 0 : _c.tagName.toLowerCase()}>, so the keypress is ignored`);
|
|
|
+ evt.preventDefault();
|
|
|
+ evt.stopImmediatePropagation();
|
|
|
+ let skipBy = (_d = features$1.arrowKeySkipBy) !== null && _d !== void 0 ? _d : featInfo.arrowKeySkipBy.default;
|
|
|
+ if (evt.code === "ArrowLeft")
|
|
|
+ skipBy *= -1;
|
|
|
+ log(`Captured arrow key '${evt.code}' - skipping by ${skipBy} seconds`);
|
|
|
+ const vidElem = document.querySelector(videoSelector);
|
|
|
+ if (vidElem)
|
|
|
+ vidElem.currentTime = UserUtils.clamp(vidElem.currentTime + skipBy, 0, vidElem.duration);
|
|
|
+ });
|
|
|
+ log("Added arrow key press listener");
|
|
|
});
|
|
|
- siteEvents.on("hotkeyInputActive", (state) => {
|
|
|
- siteSwitchEnabled = !state;
|
|
|
+ }
|
|
|
+ //#MARKER site switch
|
|
|
+ /** switch sites only if current video time is greater than this value */
|
|
|
+ const videoTimeThreshold = 3;
|
|
|
+ let siteSwitchEnabled = true;
|
|
|
+ /** Initializes the site switch feature */
|
|
|
+ function initSiteSwitch(domain) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ document.addEventListener("keydown", (e) => {
|
|
|
+ const hotkey = features$1.switchSitesHotkey;
|
|
|
+ if (siteSwitchEnabled && e.code === hotkey.code && e.shiftKey === hotkey.shift && e.ctrlKey === hotkey.ctrl && e.altKey === hotkey.alt)
|
|
|
+ switchSite(domain === "yt" ? "ytm" : "yt");
|
|
|
+ });
|
|
|
+ siteEvents.on("hotkeyInputActive", (state) => {
|
|
|
+ siteSwitchEnabled = !state;
|
|
|
+ });
|
|
|
+ log("Initialized site switch listener");
|
|
|
});
|
|
|
- log("Initialized site switch listener");
|
|
|
- });
|
|
|
-}
|
|
|
-/** Switches to the other site (between YT and YTM) */
|
|
|
-function switchSite(newDomain) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- if (!(["/watch", "/playlist"].some(v => location.pathname.startsWith(v))))
|
|
|
- return warn("Not on a supported page, so the site switch is ignored");
|
|
|
- let subdomain;
|
|
|
- if (newDomain === "ytm")
|
|
|
- subdomain = "music";
|
|
|
- else if (newDomain === "yt")
|
|
|
- subdomain = "www";
|
|
|
- if (!subdomain)
|
|
|
- throw new Error(`Unrecognized domain '${newDomain}'`);
|
|
|
- disableBeforeUnload();
|
|
|
- const { pathname, search, hash } = new URL(location.href);
|
|
|
- const vt = yield getVideoTime();
|
|
|
- log(`Found video time of ${vt} seconds`);
|
|
|
- const cleanSearch = search.split("&")
|
|
|
- .filter((param) => !param.match(/^\??t=/))
|
|
|
- .join("&");
|
|
|
- const newSearch = typeof vt === "number" && vt > videoTimeThreshold ?
|
|
|
- cleanSearch.includes("?")
|
|
|
- ? `${cleanSearch.startsWith("?")
|
|
|
+ }
|
|
|
+ /** Switches to the other site (between YT and YTM) */
|
|
|
+ function switchSite(newDomain) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ if (!(["/watch", "/playlist"].some(v => location.pathname.startsWith(v))))
|
|
|
+ return warn("Not on a supported page, so the site switch is ignored");
|
|
|
+ let subdomain;
|
|
|
+ if (newDomain === "ytm")
|
|
|
+ subdomain = "music";
|
|
|
+ else if (newDomain === "yt")
|
|
|
+ subdomain = "www";
|
|
|
+ if (!subdomain)
|
|
|
+ throw new Error(`Unrecognized domain '${newDomain}'`);
|
|
|
+ disableBeforeUnload();
|
|
|
+ const { pathname, search, hash } = new URL(location.href);
|
|
|
+ const vt = yield getVideoTime();
|
|
|
+ log(`Found video time of ${vt} seconds`);
|
|
|
+ const cleanSearch = search.split("&")
|
|
|
+ .filter((param) => !param.match(/^\??t=/))
|
|
|
+ .join("&");
|
|
|
+ const newSearch = typeof vt === "number" && vt > videoTimeThreshold ?
|
|
|
+ cleanSearch.includes("?")
|
|
|
+ ? `${cleanSearch.startsWith("?")
|
|
|
? cleanSearch
|
|
|
: "?" + cleanSearch}&t=${vt}`
|
|
|
- : `?t=${vt}`
|
|
|
- : cleanSearch;
|
|
|
- const newUrl = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
|
|
|
- info(`Switching to domain '${newDomain}' at ${newUrl}`);
|
|
|
- location.assign(newUrl);
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Error while switching site:", err);
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER number keys skip to time
|
|
|
-/** Adds the ability to skip to a certain time in the video by pressing a number key (0-9) */
|
|
|
-function initNumKeysSkip() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- document.addEventListener("keydown", (e) => {
|
|
|
- var _a, _b, _c, _d;
|
|
|
- if (!e.key.trim().match(/^[0-9]$/))
|
|
|
- return;
|
|
|
- if (isCfgMenuOpen)
|
|
|
- return;
|
|
|
- // discard the event when a (text) input is currently active, like when editing a playlist or when the search bar is focused
|
|
|
- if (document.activeElement !== document.body
|
|
|
- && !["progress-bar"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : "_")
|
|
|
- && !["BUTTON", "A"].includes((_d = (_c = document.activeElement) === null || _c === void 0 ? void 0 : _c.tagName) !== null && _d !== void 0 ? _d : "_"))
|
|
|
- return info("Captured valid key to skip video to but an unexpected element is focused, so the keypress is ignored");
|
|
|
- const vidElem = document.querySelector(videoSelector);
|
|
|
- if (!vidElem)
|
|
|
- return warn("Could not find video element, so the keypress is ignored");
|
|
|
- const newVidTime = vidElem.duration / (10 / Number(e.key));
|
|
|
- if (!isNaN(newVidTime)) {
|
|
|
- log(`Captured number key [${e.key}], skipping to ${Math.floor(newVidTime / 60)}m ${(newVidTime % 60).toFixed(1)}s`);
|
|
|
- vidElem.currentTime = newVidTime;
|
|
|
+ : `?t=${vt}`
|
|
|
+ : cleanSearch;
|
|
|
+ const newUrl = `https://${subdomain}.youtube.com${pathname}${newSearch}${hash}`;
|
|
|
+ info(`Switching to domain '${newDomain}' at ${newUrl}`);
|
|
|
+ location.assign(newUrl);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Error while switching site:", err);
|
|
|
}
|
|
|
});
|
|
|
- log("Added number key press listener");
|
|
|
- });
|
|
|
-}/** Base URL of geniURL */
|
|
|
-const geniUrlBase = "https://api.sv443.net/geniurl";
|
|
|
-/** GeniURL endpoint that gives song metadata when provided with a `?q` or `?artist` and `?song` parameter - [more info](https://api.sv443.net/geniurl) */
|
|
|
-const geniURLSearchTopUrl = `${geniUrlBase}/search/top`;
|
|
|
-/**
|
|
|
- * The threshold to pass to geniURL's fuzzy filtering.
|
|
|
- * From fuse.js docs: At what point does the match algorithm give up. A threshold of 0.0 requires a perfect match (of both letters and location), a threshold of 1.0 would match anything.
|
|
|
- * Set to undefined to use the default.
|
|
|
- */
|
|
|
-const threshold = 0.55;
|
|
|
-/** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
|
|
|
-const geniUrlRatelimitTimeframe = 30;
|
|
|
-`&threshold=${UserUtils.clamp(threshold, 0, 1)}` ;
|
|
|
-//#MARKER cache
|
|
|
-/** Cache with key format `ARTIST - SONG` (sanitized) and lyrics URLs as values. Used to prevent extraneous requests to geniURL. */
|
|
|
-const lyricsUrlCache = new Map();
|
|
|
-/** How many cache entries can exist at a time - this is used to cap memory usage */
|
|
|
-const maxLyricsCacheSize = 100;
|
|
|
-/**
|
|
|
- * Returns the lyrics URL from the passed un-/sanitized artist and song name, or undefined if the entry doesn't exist yet.
|
|
|
- * **The passed parameters need to be sanitized first!**
|
|
|
- */
|
|
|
-function getLyricsCacheEntry(artists, song) {
|
|
|
- return lyricsUrlCache.get(`${artists} - ${song}`);
|
|
|
-}
|
|
|
-/** Adds the provided entry into the lyrics URL cache */
|
|
|
-function addLyricsCacheEntry(artists, song, lyricsUrl) {
|
|
|
- lyricsUrlCache.set(`${sanitizeArtists(artists)} - ${sanitizeSong(song)}`, lyricsUrl);
|
|
|
- // delete oldest entry if cache gets too big
|
|
|
- if (lyricsUrlCache.size > maxLyricsCacheSize)
|
|
|
- lyricsUrlCache.delete([...lyricsUrlCache.keys()].at(-1));
|
|
|
-}
|
|
|
-//#MARKER media control bar
|
|
|
-let currentSongTitle = "";
|
|
|
-/** Adds a lyrics button to the media controls bar */
|
|
|
-function addMediaCtrlLyricsBtn() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- onSelectorOld(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer", { listener: addActualMediaCtrlLyricsBtn });
|
|
|
- });
|
|
|
-}
|
|
|
-/** Actually adds the lyrics button after the like button renderer has been verified to exist */
|
|
|
-function addActualMediaCtrlLyricsBtn(likeContainer) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
|
|
|
- if (!songTitleElem)
|
|
|
- return warn("Couldn't find song title element");
|
|
|
- // run parallel without awaiting so the MutationObserver below can observe the title element in time
|
|
|
- (() => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const gUrl = yield getCurrentLyricsUrl();
|
|
|
- const linkElem = yield createLyricsBtn(gUrl !== null && gUrl !== void 0 ? gUrl : undefined);
|
|
|
- linkElem.id = "betterytm-lyrics-button";
|
|
|
- log("Inserted lyrics button into media controls bar");
|
|
|
- UserUtils.insertAfter(likeContainer, linkElem);
|
|
|
- }))();
|
|
|
- currentSongTitle = songTitleElem.title;
|
|
|
- const spinnerIconUrl = yield getResourceUrl("img-spinner");
|
|
|
- const lyricsIconUrl = yield getResourceUrl("img-lyrics");
|
|
|
- const errorIconUrl = yield getResourceUrl("img-error");
|
|
|
- const onMutation = (mutations) => { var _a, mutations_1, mutations_1_1; return __awaiter(this, void 0, void 0, function* () {
|
|
|
- var _b, e_1, _c, _d;
|
|
|
- try {
|
|
|
- for (_a = true, mutations_1 = __asyncValues(mutations); mutations_1_1 = yield mutations_1.next(), _b = mutations_1_1.done, !_b; _a = true) {
|
|
|
- _d = mutations_1_1.value;
|
|
|
- _a = false;
|
|
|
- const mut = _d;
|
|
|
- const newTitle = mut.target.title;
|
|
|
- if (newTitle !== currentSongTitle && newTitle.length > 0) {
|
|
|
- const lyricsBtn = document.querySelector("#betterytm-lyrics-button");
|
|
|
- if (!lyricsBtn)
|
|
|
- continue;
|
|
|
- info(`Song title changed from '${currentSongTitle}' to '${newTitle}'`);
|
|
|
- lyricsBtn.style.cursor = "wait";
|
|
|
- lyricsBtn.style.pointerEvents = "none";
|
|
|
- const imgElem = lyricsBtn.querySelector("img");
|
|
|
- imgElem.src = spinnerIconUrl;
|
|
|
- imgElem.classList.add("bytm-spinner");
|
|
|
- currentSongTitle = newTitle;
|
|
|
- const url = yield getCurrentLyricsUrl(); // can take a second or two
|
|
|
- imgElem.src = lyricsIconUrl;
|
|
|
- imgElem.classList.remove("bytm-spinner");
|
|
|
- if (!url) {
|
|
|
- let artist, song;
|
|
|
- if ("mediaSession" in navigator && navigator.mediaSession.metadata) {
|
|
|
- artist = navigator.mediaSession.metadata.artist;
|
|
|
- song = navigator.mediaSession.metadata.title;
|
|
|
+ }
|
|
|
+ //#MARKER number keys skip to time
|
|
|
+ /** Adds the ability to skip to a certain time in the video by pressing a number key (0-9) */
|
|
|
+ function initNumKeysSkip() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ document.addEventListener("keydown", (e) => {
|
|
|
+ var _a, _b, _c, _d;
|
|
|
+ if (!e.key.trim().match(/^[0-9]$/))
|
|
|
+ return;
|
|
|
+ if (isCfgMenuOpen)
|
|
|
+ return;
|
|
|
+ // discard the event when a (text) input is currently active, like when editing a playlist or when the search bar is focused
|
|
|
+ if (document.activeElement !== document.body
|
|
|
+ && !["progress-bar"].includes((_b = (_a = document.activeElement) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : "_")
|
|
|
+ && !["BUTTON", "A"].includes((_d = (_c = document.activeElement) === null || _c === void 0 ? void 0 : _c.tagName) !== null && _d !== void 0 ? _d : "_"))
|
|
|
+ return info("Captured valid key to skip video to but an unexpected element is focused, so the keypress is ignored");
|
|
|
+ const vidElem = document.querySelector(videoSelector);
|
|
|
+ if (!vidElem)
|
|
|
+ return warn("Could not find video element, so the keypress is ignored");
|
|
|
+ const newVidTime = vidElem.duration / (10 / Number(e.key));
|
|
|
+ if (!isNaN(newVidTime)) {
|
|
|
+ log(`Captured number key [${e.key}], skipping to ${Math.floor(newVidTime / 60)}m ${(newVidTime % 60).toFixed(1)}s`);
|
|
|
+ vidElem.currentTime = newVidTime;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ log("Added number key press listener");
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Base URL of geniURL */
|
|
|
+ const geniUrlBase = "https://api.sv443.net/geniurl";
|
|
|
+ /** GeniURL endpoint that gives song metadata when provided with a `?q` or `?artist` and `?song` parameter - [more info](https://api.sv443.net/geniurl) */
|
|
|
+ const geniURLSearchTopUrl = `${geniUrlBase}/search/top`;
|
|
|
+ /**
|
|
|
+ * The threshold to pass to geniURL's fuzzy filtering.
|
|
|
+ * From fuse.js docs: At what point does the match algorithm give up. A threshold of 0.0 requires a perfect match (of both letters and location), a threshold of 1.0 would match anything.
|
|
|
+ * Set to undefined to use the default.
|
|
|
+ */
|
|
|
+ const threshold = 0.55;
|
|
|
+ /** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
|
|
|
+ const geniUrlRatelimitTimeframe = 30;
|
|
|
+`&threshold=${UserUtils.clamp(threshold, 0, 1)}` ;
|
|
|
+ //#MARKER cache
|
|
|
+ /** Cache with key format `ARTIST - SONG` (sanitized) and lyrics URLs as values. Used to prevent extraneous requests to geniURL. */
|
|
|
+ const lyricsUrlCache = new Map();
|
|
|
+ /** How many cache entries can exist at a time - this is used to cap memory usage */
|
|
|
+ const maxLyricsCacheSize = 100;
|
|
|
+ /**
|
|
|
+ * Returns the lyrics URL from the passed un-/sanitized artist and song name, or undefined if the entry doesn't exist yet.
|
|
|
+ * **The passed parameters need to be sanitized first!**
|
|
|
+ */
|
|
|
+ function getLyricsCacheEntry(artists, song) {
|
|
|
+ return lyricsUrlCache.get(`${artists} - ${song}`);
|
|
|
+ }
|
|
|
+ /** Adds the provided entry into the lyrics URL cache */
|
|
|
+ function addLyricsCacheEntry(artists, song, lyricsUrl) {
|
|
|
+ lyricsUrlCache.set(`${sanitizeArtists(artists)} - ${sanitizeSong(song)}`, lyricsUrl);
|
|
|
+ // delete oldest entry if cache gets too big
|
|
|
+ if (lyricsUrlCache.size > maxLyricsCacheSize)
|
|
|
+ lyricsUrlCache.delete([...lyricsUrlCache.keys()].at(-1));
|
|
|
+ }
|
|
|
+ //#MARKER media control bar
|
|
|
+ let currentSongTitle = "";
|
|
|
+ /** Adds a lyrics button to the media controls bar */
|
|
|
+ function addMediaCtrlLyricsBtn() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ onSelectorOld(".middle-controls-buttons ytmusic-like-button-renderer#like-button-renderer", { listener: addActualMediaCtrlLyricsBtn });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Actually adds the lyrics button after the like button renderer has been verified to exist */
|
|
|
+ function addActualMediaCtrlLyricsBtn(likeContainer) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
|
|
|
+ if (!songTitleElem)
|
|
|
+ return warn("Couldn't find song title element");
|
|
|
+ // run parallel without awaiting so the MutationObserver below can observe the title element in time
|
|
|
+ (() => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const gUrl = yield getCurrentLyricsUrl();
|
|
|
+ const linkElem = yield createLyricsBtn(gUrl !== null && gUrl !== void 0 ? gUrl : undefined);
|
|
|
+ linkElem.id = "betterytm-lyrics-button";
|
|
|
+ log("Inserted lyrics button into media controls bar");
|
|
|
+ UserUtils.insertAfter(likeContainer, linkElem);
|
|
|
+ }))();
|
|
|
+ currentSongTitle = songTitleElem.title;
|
|
|
+ const spinnerIconUrl = yield getResourceUrl("img-spinner");
|
|
|
+ const lyricsIconUrl = yield getResourceUrl("img-lyrics");
|
|
|
+ const errorIconUrl = yield getResourceUrl("img-error");
|
|
|
+ const onMutation = (mutations) => { var _a, mutations_1, mutations_1_1; return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ var _b, e_1, _c, _d;
|
|
|
+ try {
|
|
|
+ for (_a = true, mutations_1 = __asyncValues(mutations); mutations_1_1 = yield mutations_1.next(), _b = mutations_1_1.done, !_b; _a = true) {
|
|
|
+ _d = mutations_1_1.value;
|
|
|
+ _a = false;
|
|
|
+ const mut = _d;
|
|
|
+ const newTitle = mut.target.title;
|
|
|
+ if (newTitle !== currentSongTitle && newTitle.length > 0) {
|
|
|
+ const lyricsBtn = document.querySelector("#betterytm-lyrics-button");
|
|
|
+ if (!lyricsBtn)
|
|
|
+ continue;
|
|
|
+ info(`Song title changed from '${currentSongTitle}' to '${newTitle}'`);
|
|
|
+ lyricsBtn.style.cursor = "wait";
|
|
|
+ lyricsBtn.style.pointerEvents = "none";
|
|
|
+ const imgElem = lyricsBtn.querySelector("img");
|
|
|
+ imgElem.src = spinnerIconUrl;
|
|
|
+ imgElem.classList.add("bytm-spinner");
|
|
|
+ currentSongTitle = newTitle;
|
|
|
+ const url = yield getCurrentLyricsUrl(); // can take a second or two
|
|
|
+ imgElem.src = lyricsIconUrl;
|
|
|
+ imgElem.classList.remove("bytm-spinner");
|
|
|
+ if (!url) {
|
|
|
+ let artist, song;
|
|
|
+ if ("mediaSession" in navigator && navigator.mediaSession.metadata) {
|
|
|
+ artist = navigator.mediaSession.metadata.artist;
|
|
|
+ song = navigator.mediaSession.metadata.title;
|
|
|
+ }
|
|
|
+ const query = artist && song ? "?q=" + encodeURIComponent(sanitizeArtists(artist) + " - " + sanitizeSong(song)) : "";
|
|
|
+ imgElem.src = errorIconUrl;
|
|
|
+ imgElem.ariaLabel = imgElem.title = t("lyrics_not_found_click_open_search");
|
|
|
+ lyricsBtn.style.cursor = "pointer";
|
|
|
+ lyricsBtn.style.pointerEvents = "all";
|
|
|
+ lyricsBtn.style.display = "inline-flex";
|
|
|
+ lyricsBtn.style.visibility = "visible";
|
|
|
+ lyricsBtn.href = `https://genius.com/search${query}`;
|
|
|
+ continue;
|
|
|
}
|
|
|
- const query = artist && song ? "?q=" + encodeURIComponent(sanitizeArtists(artist) + " - " + sanitizeSong(song)) : "";
|
|
|
- imgElem.src = errorIconUrl;
|
|
|
- imgElem.ariaLabel = imgElem.title = t("lyrics_not_found_click_open_search");
|
|
|
+ lyricsBtn.href = url;
|
|
|
+ lyricsBtn.ariaLabel = lyricsBtn.title = t("open_current_lyrics");
|
|
|
lyricsBtn.style.cursor = "pointer";
|
|
|
- lyricsBtn.style.pointerEvents = "all";
|
|
|
- lyricsBtn.style.display = "inline-flex";
|
|
|
lyricsBtn.style.visibility = "visible";
|
|
|
- lyricsBtn.href = `https://genius.com/search${query}`;
|
|
|
- continue;
|
|
|
+ lyricsBtn.style.display = "inline-flex";
|
|
|
+ lyricsBtn.style.pointerEvents = "initial";
|
|
|
}
|
|
|
- lyricsBtn.href = url;
|
|
|
- lyricsBtn.ariaLabel = lyricsBtn.title = t("open_current_lyrics");
|
|
|
- lyricsBtn.style.cursor = "pointer";
|
|
|
- lyricsBtn.style.visibility = "visible";
|
|
|
- lyricsBtn.style.display = "inline-flex";
|
|
|
- lyricsBtn.style.pointerEvents = "initial";
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
|
- finally {
|
|
|
- try {
|
|
|
- if (!_a && !_b && (_c = mutations_1.return)) yield _c.call(mutations_1);
|
|
|
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
|
+ finally {
|
|
|
+ try {
|
|
|
+ if (!_a && !_b && (_c = mutations_1.return)) yield _c.call(mutations_1);
|
|
|
+ }
|
|
|
+ finally { if (e_1) throw e_1.error; }
|
|
|
}
|
|
|
- finally { if (e_1) throw e_1.error; }
|
|
|
- }
|
|
|
- }); };
|
|
|
- // since YT and YTM don't reload the page on video change, MutationObserver needs to be used to watch for changes in the video title
|
|
|
- const obs = new MutationObserver(onMutation);
|
|
|
- obs.observe(songTitleElem, { attributes: true, attributeFilter: ["title"] });
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER utils
|
|
|
-/** Removes everything in parentheses from the passed song name */
|
|
|
-function sanitizeSong(songName) {
|
|
|
- const parensRegex = /\(.+\)/gmi;
|
|
|
- const squareParensRegex = /\[.+\]/gmi;
|
|
|
- // trim right after the song name:
|
|
|
- const sanitized = songName
|
|
|
- .replace(parensRegex, "")
|
|
|
- .replace(squareParensRegex, "");
|
|
|
- return sanitized.trim();
|
|
|
-}
|
|
|
-/** Removes the secondary artist (if it exists) from the passed artists string */
|
|
|
-function sanitizeArtists(artists) {
|
|
|
- artists = artists.split(/\s*\u2022\s*/gmiu)[0]; // split at • [•] character
|
|
|
- if (artists.match(/&/))
|
|
|
- artists = artists.split(/\s*&\s*/gm)[0];
|
|
|
- if (artists.match(/,/))
|
|
|
- artists = artists.split(/,\s*/gm)[0];
|
|
|
- return artists.trim();
|
|
|
-}
|
|
|
-/** Returns the lyrics URL from genius for the currently selected song */
|
|
|
-function getCurrentLyricsUrl() {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- // In videos the video title contains both artist and song title, in "regular" YTM songs, the video title only contains the song title
|
|
|
- const isVideo = typeof ((_a = document.querySelector("ytmusic-player")) === null || _a === void 0 ? void 0 : _a.hasAttribute("video-mode"));
|
|
|
- const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
|
|
|
- const songMetaElem = document.querySelector("span.subtitle > yt-formatted-string :first-child");
|
|
|
- if (!songTitleElem || !songMetaElem)
|
|
|
- return undefined;
|
|
|
- const songNameRaw = songTitleElem.title;
|
|
|
- let songName = songNameRaw;
|
|
|
- let artistName = songMetaElem.textContent;
|
|
|
- if (isVideo) {
|
|
|
- // for some fucking reason some music videos have YTM-like song title and artist separation, some don't
|
|
|
- if (songName.includes("-")) {
|
|
|
- const split = splitVideoTitle(songName);
|
|
|
- songName = split.song;
|
|
|
- artistName = split.artist;
|
|
|
+ }); };
|
|
|
+ // since YT and YTM don't reload the page on video change, MutationObserver needs to be used to watch for changes in the video title
|
|
|
+ const obs = new MutationObserver(onMutation);
|
|
|
+ obs.observe(songTitleElem, { attributes: true, attributeFilter: ["title"] });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ //#MARKER utils
|
|
|
+ /** Removes everything in parentheses from the passed song name */
|
|
|
+ function sanitizeSong(songName) {
|
|
|
+ const parensRegex = /\(.+\)/gmi;
|
|
|
+ const squareParensRegex = /\[.+\]/gmi;
|
|
|
+ // trim right after the song name:
|
|
|
+ const sanitized = songName
|
|
|
+ .replace(parensRegex, "")
|
|
|
+ .replace(squareParensRegex, "");
|
|
|
+ return sanitized.trim();
|
|
|
+ }
|
|
|
+ /** Removes the secondary artist (if it exists) from the passed artists string */
|
|
|
+ function sanitizeArtists(artists) {
|
|
|
+ artists = artists.split(/\s*\u2022\s*/gmiu)[0]; // split at • [•] character
|
|
|
+ if (artists.match(/&/))
|
|
|
+ artists = artists.split(/\s*&\s*/gm)[0];
|
|
|
+ if (artists.match(/,/))
|
|
|
+ artists = artists.split(/,\s*/gm)[0];
|
|
|
+ return artists.trim();
|
|
|
+ }
|
|
|
+ /** Returns the lyrics URL from genius for the currently selected song */
|
|
|
+ function getCurrentLyricsUrl() {
|
|
|
+ var _a;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ // In videos the video title contains both artist and song title, in "regular" YTM songs, the video title only contains the song title
|
|
|
+ const isVideo = typeof ((_a = document.querySelector("ytmusic-player")) === null || _a === void 0 ? void 0 : _a.hasAttribute("video-mode"));
|
|
|
+ const songTitleElem = document.querySelector(".content-info-wrapper > yt-formatted-string");
|
|
|
+ const songMetaElem = document.querySelector("span.subtitle > yt-formatted-string :first-child");
|
|
|
+ if (!songTitleElem || !songMetaElem)
|
|
|
+ return undefined;
|
|
|
+ const songNameRaw = songTitleElem.title;
|
|
|
+ let songName = songNameRaw;
|
|
|
+ let artistName = songMetaElem.textContent;
|
|
|
+ if (isVideo) {
|
|
|
+ // for some fucking reason some music videos have YTM-like song title and artist separation, some don't
|
|
|
+ if (songName.includes("-")) {
|
|
|
+ const split = splitVideoTitle(songName);
|
|
|
+ songName = split.song;
|
|
|
+ artistName = split.artist;
|
|
|
+ }
|
|
|
}
|
|
|
+ if (!artistName)
|
|
|
+ return undefined;
|
|
|
+ const url = yield fetchLyricsUrl(sanitizeArtists(artistName), sanitizeSong(songName));
|
|
|
+ if (url) {
|
|
|
+ emitInterface("bytm:lyricsLoaded", {
|
|
|
+ type: "current",
|
|
|
+ artists: artistName,
|
|
|
+ title: songName,
|
|
|
+ url,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return url;
|
|
|
}
|
|
|
- if (!artistName)
|
|
|
- return undefined;
|
|
|
- const url = yield fetchLyricsUrl(sanitizeArtists(artistName), sanitizeSong(songName));
|
|
|
- if (url) {
|
|
|
- emitInterface("bytm:lyricsLoaded", {
|
|
|
- type: "current",
|
|
|
- artists: artistName,
|
|
|
- title: songName,
|
|
|
- url,
|
|
|
- });
|
|
|
- }
|
|
|
- return url;
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't resolve lyrics URL:", err);
|
|
|
- return undefined;
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-/** Fetches the actual lyrics URL from geniURL - **the passed parameters need to be sanitized first!** */
|
|
|
-function fetchLyricsUrl(artist, song) {
|
|
|
- var _a, _b, _c;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- const cacheEntry = getLyricsCacheEntry(artist, song);
|
|
|
- if (cacheEntry) {
|
|
|
- info(`Found lyrics URL in cache: ${cacheEntry}`);
|
|
|
- return cacheEntry;
|
|
|
- }
|
|
|
- const startTs = Date.now();
|
|
|
- const fetchUrl = constructUrlString(geniURLSearchTopUrl, {
|
|
|
- disableFuzzy: null,
|
|
|
- utm_source: "BetterYTM",
|
|
|
- utm_content: `v${scriptInfo.version}`,
|
|
|
- artist,
|
|
|
- song,
|
|
|
- });
|
|
|
- log(`Requesting URL from geniURL at '${fetchUrl}'`);
|
|
|
- const fetchRes = yield UserUtils.fetchAdvanced(fetchUrl);
|
|
|
- if (fetchRes.status === 429) {
|
|
|
- const waitSeconds = Number((_a = fetchRes.headers.get("retry-after")) !== null && _a !== void 0 ? _a : geniUrlRatelimitTimeframe);
|
|
|
- alert(tp("lyrics_rate_limited", waitSeconds, waitSeconds));
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't resolve lyrics URL:", err);
|
|
|
return undefined;
|
|
|
}
|
|
|
- else if (fetchRes.status < 200 || fetchRes.status >= 300) {
|
|
|
- error(`Couldn't fetch lyrics URL from geniURL - status: ${fetchRes.status} - response: ${(_c = (_b = (yield fetchRes.json()).message) !== null && _b !== void 0 ? _b : yield fetchRes.text()) !== null && _c !== void 0 ? _c : "(none)"}`);
|
|
|
- return undefined;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Fetches the actual lyrics URL from geniURL - **the passed parameters need to be sanitized first!** */
|
|
|
+ function fetchLyricsUrl(artist, song) {
|
|
|
+ var _a, _b, _c;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ const cacheEntry = getLyricsCacheEntry(artist, song);
|
|
|
+ if (cacheEntry) {
|
|
|
+ info(`Found lyrics URL in cache: ${cacheEntry}`);
|
|
|
+ return cacheEntry;
|
|
|
+ }
|
|
|
+ const startTs = Date.now();
|
|
|
+ const fetchUrl = constructUrlString(geniURLSearchTopUrl, {
|
|
|
+ disableFuzzy: null,
|
|
|
+ utm_source: "BetterYTM",
|
|
|
+ utm_content: `v${scriptInfo.version}`,
|
|
|
+ artist,
|
|
|
+ song,
|
|
|
+ });
|
|
|
+ log(`Requesting URL from geniURL at '${fetchUrl}'`);
|
|
|
+ const fetchRes = yield UserUtils.fetchAdvanced(fetchUrl);
|
|
|
+ if (fetchRes.status === 429) {
|
|
|
+ const waitSeconds = Number((_a = fetchRes.headers.get("retry-after")) !== null && _a !== void 0 ? _a : geniUrlRatelimitTimeframe);
|
|
|
+ alert(tp("lyrics_rate_limited", waitSeconds, waitSeconds));
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ else if (fetchRes.status < 200 || fetchRes.status >= 300) {
|
|
|
+ error(`Couldn't fetch lyrics URL from geniURL - status: ${fetchRes.status} - response: ${(_c = (_b = (yield fetchRes.json()).message) !== null && _b !== void 0 ? _b : yield fetchRes.text()) !== null && _c !== void 0 ? _c : "(none)"}`);
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ const result = yield fetchRes.json();
|
|
|
+ if (typeof result === "object" && result.error) {
|
|
|
+ error("Couldn't fetch lyrics URL:", result.message);
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ const url = result.url;
|
|
|
+ info(`Found lyrics URL (after ${Date.now() - startTs}ms): ${url}`);
|
|
|
+ addLyricsCacheEntry(artist, song, url);
|
|
|
+ return url;
|
|
|
}
|
|
|
- const result = yield fetchRes.json();
|
|
|
- if (typeof result === "object" && result.error) {
|
|
|
- error("Couldn't fetch lyrics URL:", result.message);
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't get lyrics URL due to error:", err);
|
|
|
return undefined;
|
|
|
}
|
|
|
- const url = result.url;
|
|
|
- info(`Found lyrics URL (after ${Date.now() - startTs}ms): ${url}`);
|
|
|
- addLyricsCacheEntry(artist, song, url);
|
|
|
- return url;
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't get lyrics URL due to error:", err);
|
|
|
- return undefined;
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-/** Creates the base lyrics button element */
|
|
|
-function createLyricsBtn(geniusUrl, hideIfLoading = true) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const linkElem = document.createElement("a");
|
|
|
- linkElem.className = "ytmusic-player-bar bytm-generic-btn";
|
|
|
- linkElem.ariaLabel = linkElem.title = geniusUrl ? t("open_lyrics") : t("lyrics_loading");
|
|
|
- if (geniusUrl)
|
|
|
- linkElem.href = geniusUrl;
|
|
|
- linkElem.role = "button";
|
|
|
- linkElem.target = "_blank";
|
|
|
- linkElem.rel = "noopener noreferrer";
|
|
|
- linkElem.style.visibility = hideIfLoading && geniusUrl ? "initial" : "hidden";
|
|
|
- linkElem.style.display = hideIfLoading && geniusUrl ? "inline-flex" : "none";
|
|
|
- const imgElem = document.createElement("img");
|
|
|
- imgElem.className = "bytm-generic-btn-img";
|
|
|
- imgElem.src = yield getResourceUrl("img-lyrics");
|
|
|
- linkElem.appendChild(imgElem);
|
|
|
- return linkElem;
|
|
|
- });
|
|
|
-}
|
|
|
-/** Splits a video title that contains a hyphen into an artist and song */
|
|
|
-function splitVideoTitle(title) {
|
|
|
- const [artist, ...rest] = title.split("-").map((v, i) => i < 2 ? v.trim() : v);
|
|
|
- return { artist, song: rest.join("-") };
|
|
|
-}let features;
|
|
|
-function setSongListsConfig(feats) {
|
|
|
- features = feats;
|
|
|
-}
|
|
|
-/** Initializes the queue buttons */
|
|
|
-function initQueueButtons() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const addCurrentQueueBtns = (evt) => {
|
|
|
- let amt = 0;
|
|
|
- for (const queueItm of evt.childNodes) {
|
|
|
- if (!queueItm.classList.contains("bytm-has-queue-btns")) {
|
|
|
- addQueueButtons(queueItm, undefined, "currentQueue");
|
|
|
- amt++;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Creates the base lyrics button element */
|
|
|
+ function createLyricsBtn(geniusUrl, hideIfLoading = true) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const linkElem = document.createElement("a");
|
|
|
+ linkElem.className = "ytmusic-player-bar bytm-generic-btn";
|
|
|
+ linkElem.ariaLabel = linkElem.title = geniusUrl ? t("open_lyrics") : t("lyrics_loading");
|
|
|
+ if (geniusUrl)
|
|
|
+ linkElem.href = geniusUrl;
|
|
|
+ linkElem.role = "button";
|
|
|
+ linkElem.target = "_blank";
|
|
|
+ linkElem.rel = "noopener noreferrer";
|
|
|
+ linkElem.style.visibility = hideIfLoading && geniusUrl ? "initial" : "hidden";
|
|
|
+ linkElem.style.display = hideIfLoading && geniusUrl ? "inline-flex" : "none";
|
|
|
+ const imgElem = document.createElement("img");
|
|
|
+ imgElem.className = "bytm-generic-btn-img";
|
|
|
+ imgElem.src = yield getResourceUrl("img-lyrics");
|
|
|
+ linkElem.appendChild(imgElem);
|
|
|
+ return linkElem;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Splits a video title that contains a hyphen into an artist and song */
|
|
|
+ function splitVideoTitle(title) {
|
|
|
+ const [artist, ...rest] = title.split("-").map((v, i) => i < 2 ? v.trim() : v);
|
|
|
+ return { artist, song: rest.join("-") };
|
|
|
+ }
|
|
|
+
|
|
|
+ let features;
|
|
|
+ function setSongListsConfig(feats) {
|
|
|
+ features = feats;
|
|
|
+ }
|
|
|
+ /** Initializes the queue buttons */
|
|
|
+ function initQueueButtons() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const addCurrentQueueBtns = (evt) => {
|
|
|
+ let amt = 0;
|
|
|
+ for (const queueItm of evt.childNodes) {
|
|
|
+ if (!queueItm.classList.contains("bytm-has-queue-btns")) {
|
|
|
+ addQueueButtons(queueItm, undefined, "currentQueue");
|
|
|
+ amt++;
|
|
|
+ }
|
|
|
}
|
|
|
+ if (amt > 0)
|
|
|
+ log(`Added buttons to ${amt} new queue ${UserUtils.autoPlural("item", amt)}`);
|
|
|
+ };
|
|
|
+ // current queue
|
|
|
+ siteEvents.on("queueChanged", addCurrentQueueBtns);
|
|
|
+ siteEvents.on("autoplayQueueChanged", addCurrentQueueBtns);
|
|
|
+ const queueItems = document.querySelectorAll("#contents.ytmusic-player-queue > ytmusic-player-queue-item");
|
|
|
+ if (queueItems.length > 0) {
|
|
|
+ queueItems.forEach(itm => addQueueButtons(itm, undefined, "currentQueue"));
|
|
|
+ log(`Added buttons to ${queueItems.length} existing "current song queue" ${UserUtils.autoPlural("item", queueItems)}`);
|
|
|
}
|
|
|
- if (amt > 0)
|
|
|
- log(`Added buttons to ${amt} new queue ${UserUtils.autoPlural("item", amt)}`);
|
|
|
- };
|
|
|
- // current queue
|
|
|
- siteEvents.on("queueChanged", addCurrentQueueBtns);
|
|
|
- siteEvents.on("autoplayQueueChanged", addCurrentQueueBtns);
|
|
|
- const queueItems = document.querySelectorAll("#contents.ytmusic-player-queue > ytmusic-player-queue-item");
|
|
|
- if (queueItems.length > 0) {
|
|
|
- queueItems.forEach(itm => addQueueButtons(itm, undefined, "currentQueue"));
|
|
|
- log(`Added buttons to ${queueItems.length} existing "current song queue" ${UserUtils.autoPlural("item", queueItems)}`);
|
|
|
- }
|
|
|
- // generic lists
|
|
|
- // TODO:FIXME: dragging the items around removes the queue buttons
|
|
|
- const addGenericListQueueBtns = (listElem) => {
|
|
|
- if (listElem.classList.contains("bytm-list-has-queue-btns"))
|
|
|
- return;
|
|
|
- const queueItems = listElem.querySelectorAll("ytmusic-responsive-list-item-renderer");
|
|
|
- if (queueItems.length === 0)
|
|
|
- return;
|
|
|
- listElem.classList.add("bytm-list-has-queue-btns");
|
|
|
- queueItems.forEach(itm => addQueueButtons(itm, ".flex-columns", "genericQueue", ["bytm-generic-list-queue-btn-container"]));
|
|
|
- log(`Added buttons to ${queueItems.length} new "generic song list" ${UserUtils.autoPlural("item", queueItems)}`);
|
|
|
- };
|
|
|
- const listSelectors = [
|
|
|
- "ytmusic-playlist-shelf-renderer #contents",
|
|
|
- "ytmusic-section-list-renderer[main-page-type=\"MUSIC_PAGE_TYPE_ALBUM\"] ytmusic-shelf-renderer #contents",
|
|
|
- "ytmusic-section-list-renderer[main-page-type=\"MUSIC_PAGE_TYPE_ARTIST\"] ytmusic-shelf-renderer #contents",
|
|
|
- ];
|
|
|
- if (features.listButtonsPlacement === "everywhere") {
|
|
|
- for (const selector of listSelectors) {
|
|
|
- onSelectorOld(selector, {
|
|
|
- all: true,
|
|
|
- continuous: true,
|
|
|
- listener: (songLists) => {
|
|
|
- for (const list of songLists)
|
|
|
- addGenericListQueueBtns(list);
|
|
|
- },
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
- // TODO: support grids?
|
|
|
- });
|
|
|
-}
|
|
|
-/**
|
|
|
- * Adds the buttons to each item in the current song queue.
|
|
|
- * Also observes for changes to add new buttons to new items in the queue.
|
|
|
- * @param queueItem The element with tagname `ytmusic-player-queue-item` to add queue buttons to
|
|
|
- * @param listType The type of list the queue item is in
|
|
|
- * @param classes Extra CSS classes to apply to the container
|
|
|
- */
|
|
|
-function addQueueButtons(queueItem, containerParentSelector = ".song-info", listType = "currentQueue", classes = []) {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- //#SECTION general queue item stuff
|
|
|
- const queueBtnsCont = document.createElement("div");
|
|
|
- queueBtnsCont.classList.add("bytm-queue-btn-container", ...classes);
|
|
|
- const lyricsIconUrl = yield getResourceUrl("img-lyrics");
|
|
|
- const deleteIconUrl = yield getResourceUrl("img-delete");
|
|
|
- //#SECTION lyrics btn
|
|
|
- let lyricsBtnElem;
|
|
|
- if (features.lyricsQueueButton) {
|
|
|
- lyricsBtnElem = yield createLyricsBtn(undefined, false);
|
|
|
- lyricsBtnElem.ariaLabel = lyricsBtnElem.title = t("open_lyrics");
|
|
|
- lyricsBtnElem.style.display = "inline-flex";
|
|
|
- lyricsBtnElem.style.visibility = "initial";
|
|
|
- lyricsBtnElem.style.pointerEvents = "initial";
|
|
|
- lyricsBtnElem.role = "link";
|
|
|
- lyricsBtnElem.tabIndex = 0;
|
|
|
- const lyricsBtnClicked = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- e.preventDefault();
|
|
|
- e.stopImmediatePropagation();
|
|
|
- let song, artist;
|
|
|
- if (listType === "currentQueue") {
|
|
|
- const songInfo = queueItem.querySelector(".song-info");
|
|
|
- if (!songInfo)
|
|
|
- return;
|
|
|
- const [songEl, artistEl] = songInfo.querySelectorAll("yt-formatted-string");
|
|
|
- song = songEl === null || songEl === void 0 ? void 0 : songEl.textContent;
|
|
|
- artist = artistEl === null || artistEl === void 0 ? void 0 : artistEl.textContent;
|
|
|
- }
|
|
|
- else if (listType === "genericQueue") {
|
|
|
- const songEl = queueItem.querySelector(".title-column yt-formatted-string a");
|
|
|
- let artistEl = null;
|
|
|
- if (location.pathname.startsWith("/playlist"))
|
|
|
- artistEl = document.querySelector("ytmusic-detail-header-renderer .metadata .subtitle-container yt-formatted-string a");
|
|
|
- else
|
|
|
- artistEl = queueItem.querySelector(".secondary-flex-columns yt-formatted-string:first-child a");
|
|
|
- song = songEl === null || songEl === void 0 ? void 0 : songEl.textContent;
|
|
|
- artist = artistEl === null || artistEl === void 0 ? void 0 : artistEl.textContent;
|
|
|
- }
|
|
|
- else
|
|
|
+ // generic lists
|
|
|
+ // TODO:FIXME: dragging the items around removes the queue buttons
|
|
|
+ const addGenericListQueueBtns = (listElem) => {
|
|
|
+ if (listElem.classList.contains("bytm-list-has-queue-btns"))
|
|
|
return;
|
|
|
- if (!song || !artist)
|
|
|
- return error("Couldn't get song or artist name from queue item - song:", song, "- artist:", artist);
|
|
|
- let lyricsUrl;
|
|
|
- const artistsSan = sanitizeArtists(artist);
|
|
|
- const songSan = sanitizeSong(song);
|
|
|
- const splitTitle = splitVideoTitle(songSan);
|
|
|
- const cachedLyricsUrl = songSan.includes("-")
|
|
|
- ? getLyricsCacheEntry(splitTitle.artist, splitTitle.song)
|
|
|
- : getLyricsCacheEntry(artistsSan, songSan);
|
|
|
- if (cachedLyricsUrl)
|
|
|
- lyricsUrl = cachedLyricsUrl;
|
|
|
- else if (!queueItem.hasAttribute("data-bytm-loading")) {
|
|
|
- const imgEl = lyricsBtnElem === null || lyricsBtnElem === void 0 ? void 0 : lyricsBtnElem.querySelector("img");
|
|
|
- if (!imgEl)
|
|
|
- return;
|
|
|
- if (!cachedLyricsUrl) {
|
|
|
- queueItem.setAttribute("data-bytm-loading", "");
|
|
|
- imgEl.src = yield getResourceUrl("img-spinner");
|
|
|
- imgEl.classList.add("bytm-spinner");
|
|
|
- }
|
|
|
- lyricsUrl = cachedLyricsUrl !== null && cachedLyricsUrl !== void 0 ? cachedLyricsUrl : yield fetchLyricsUrl(artistsSan, songSan);
|
|
|
- if (lyricsUrl) {
|
|
|
- emitInterface("bytm:lyricsLoaded", {
|
|
|
- type: "queue",
|
|
|
- artists: artist,
|
|
|
- title: song,
|
|
|
- url: lyricsUrl,
|
|
|
- });
|
|
|
+ const queueItems = listElem.querySelectorAll("ytmusic-responsive-list-item-renderer");
|
|
|
+ if (queueItems.length === 0)
|
|
|
+ return;
|
|
|
+ listElem.classList.add("bytm-list-has-queue-btns");
|
|
|
+ queueItems.forEach(itm => addQueueButtons(itm, ".flex-columns", "genericQueue", ["bytm-generic-list-queue-btn-container"]));
|
|
|
+ log(`Added buttons to ${queueItems.length} new "generic song list" ${UserUtils.autoPlural("item", queueItems)}`);
|
|
|
+ };
|
|
|
+ const listSelectors = [
|
|
|
+ "ytmusic-playlist-shelf-renderer #contents",
|
|
|
+ "ytmusic-section-list-renderer[main-page-type=\"MUSIC_PAGE_TYPE_ALBUM\"] ytmusic-shelf-renderer #contents",
|
|
|
+ "ytmusic-section-list-renderer[main-page-type=\"MUSIC_PAGE_TYPE_ARTIST\"] ytmusic-shelf-renderer #contents",
|
|
|
+ ];
|
|
|
+ if (features.listButtonsPlacement === "everywhere") {
|
|
|
+ for (const selector of listSelectors) {
|
|
|
+ onSelectorOld(selector, {
|
|
|
+ all: true,
|
|
|
+ continuous: true,
|
|
|
+ listener: (songLists) => {
|
|
|
+ for (const list of songLists)
|
|
|
+ addGenericListQueueBtns(list);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // TODO: support grids?
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Adds the buttons to each item in the current song queue.
|
|
|
+ * Also observes for changes to add new buttons to new items in the queue.
|
|
|
+ * @param queueItem The element with tagname `ytmusic-player-queue-item` to add queue buttons to
|
|
|
+ * @param listType The type of list the queue item is in
|
|
|
+ * @param classes Extra CSS classes to apply to the container
|
|
|
+ */
|
|
|
+ function addQueueButtons(queueItem, containerParentSelector = ".song-info", listType = "currentQueue", classes = []) {
|
|
|
+ var _a;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ //#SECTION general queue item stuff
|
|
|
+ const queueBtnsCont = document.createElement("div");
|
|
|
+ queueBtnsCont.classList.add("bytm-queue-btn-container", ...classes);
|
|
|
+ const lyricsIconUrl = yield getResourceUrl("img-lyrics");
|
|
|
+ const deleteIconUrl = yield getResourceUrl("img-delete");
|
|
|
+ //#SECTION lyrics btn
|
|
|
+ let lyricsBtnElem;
|
|
|
+ if (features.lyricsQueueButton) {
|
|
|
+ lyricsBtnElem = yield createLyricsBtn(undefined, false);
|
|
|
+ lyricsBtnElem.ariaLabel = lyricsBtnElem.title = t("open_lyrics");
|
|
|
+ lyricsBtnElem.style.display = "inline-flex";
|
|
|
+ lyricsBtnElem.style.visibility = "initial";
|
|
|
+ lyricsBtnElem.style.pointerEvents = "initial";
|
|
|
+ lyricsBtnElem.role = "link";
|
|
|
+ lyricsBtnElem.tabIndex = 0;
|
|
|
+ const lyricsBtnClicked = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ let song, artist;
|
|
|
+ if (listType === "currentQueue") {
|
|
|
+ const songInfo = queueItem.querySelector(".song-info");
|
|
|
+ if (!songInfo)
|
|
|
+ return;
|
|
|
+ const [songEl, artistEl] = songInfo.querySelectorAll("yt-formatted-string");
|
|
|
+ song = songEl === null || songEl === void 0 ? void 0 : songEl.textContent;
|
|
|
+ artist = artistEl === null || artistEl === void 0 ? void 0 : artistEl.textContent;
|
|
|
}
|
|
|
- const resetImgElem = () => {
|
|
|
- imgEl.src = lyricsIconUrl;
|
|
|
- imgEl.classList.remove("bytm-spinner");
|
|
|
- };
|
|
|
- if (!cachedLyricsUrl) {
|
|
|
- queueItem.removeAttribute("data-bytm-loading");
|
|
|
- // so the new image doesn't "blink"
|
|
|
- setTimeout(resetImgElem, 100);
|
|
|
+ else if (listType === "genericQueue") {
|
|
|
+ const songEl = queueItem.querySelector(".title-column yt-formatted-string a");
|
|
|
+ let artistEl = null;
|
|
|
+ if (location.pathname.startsWith("/playlist"))
|
|
|
+ artistEl = document.querySelector("ytmusic-detail-header-renderer .metadata .subtitle-container yt-formatted-string a");
|
|
|
+ else
|
|
|
+ artistEl = queueItem.querySelector(".secondary-flex-columns yt-formatted-string:first-child a");
|
|
|
+ song = songEl === null || songEl === void 0 ? void 0 : songEl.textContent;
|
|
|
+ artist = artistEl === null || artistEl === void 0 ? void 0 : artistEl.textContent;
|
|
|
}
|
|
|
- if (!lyricsUrl) {
|
|
|
- resetImgElem();
|
|
|
- if (confirm(t("lyrics_not_found_confirm_open_search")))
|
|
|
- UserUtils.openInNewTab(`https://genius.com/search?q=${encodeURIComponent(`${artistsSan} - ${songSan}`)}`);
|
|
|
+ else
|
|
|
return;
|
|
|
+ if (!song || !artist)
|
|
|
+ return error("Couldn't get song or artist name from queue item - song:", song, "- artist:", artist);
|
|
|
+ let lyricsUrl;
|
|
|
+ const artistsSan = sanitizeArtists(artist);
|
|
|
+ const songSan = sanitizeSong(song);
|
|
|
+ const splitTitle = splitVideoTitle(songSan);
|
|
|
+ const cachedLyricsUrl = songSan.includes("-")
|
|
|
+ ? getLyricsCacheEntry(splitTitle.artist, splitTitle.song)
|
|
|
+ : getLyricsCacheEntry(artistsSan, songSan);
|
|
|
+ if (cachedLyricsUrl)
|
|
|
+ lyricsUrl = cachedLyricsUrl;
|
|
|
+ else if (!queueItem.hasAttribute("data-bytm-loading")) {
|
|
|
+ const imgEl = lyricsBtnElem === null || lyricsBtnElem === void 0 ? void 0 : lyricsBtnElem.querySelector("img");
|
|
|
+ if (!imgEl)
|
|
|
+ return;
|
|
|
+ if (!cachedLyricsUrl) {
|
|
|
+ queueItem.setAttribute("data-bytm-loading", "");
|
|
|
+ imgEl.src = yield getResourceUrl("img-spinner");
|
|
|
+ imgEl.classList.add("bytm-spinner");
|
|
|
+ }
|
|
|
+ lyricsUrl = cachedLyricsUrl !== null && cachedLyricsUrl !== void 0 ? cachedLyricsUrl : yield fetchLyricsUrl(artistsSan, songSan);
|
|
|
+ if (lyricsUrl) {
|
|
|
+ emitInterface("bytm:lyricsLoaded", {
|
|
|
+ type: "queue",
|
|
|
+ artists: artist,
|
|
|
+ title: song,
|
|
|
+ url: lyricsUrl,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const resetImgElem = () => {
|
|
|
+ imgEl.src = lyricsIconUrl;
|
|
|
+ imgEl.classList.remove("bytm-spinner");
|
|
|
+ };
|
|
|
+ if (!cachedLyricsUrl) {
|
|
|
+ queueItem.removeAttribute("data-bytm-loading");
|
|
|
+ // so the new image doesn't "blink"
|
|
|
+ setTimeout(resetImgElem, 100);
|
|
|
+ }
|
|
|
+ if (!lyricsUrl) {
|
|
|
+ resetImgElem();
|
|
|
+ if (confirm(t("lyrics_not_found_confirm_open_search")))
|
|
|
+ UserUtils.openInNewTab(`https://genius.com/search?q=${encodeURIComponent(`${artistsSan} - ${songSan}`)}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
- lyricsUrl && UserUtils.openInNewTab(lyricsUrl);
|
|
|
- });
|
|
|
- lyricsBtnElem.addEventListener("click", lyricsBtnClicked);
|
|
|
- lyricsBtnElem.addEventListener("keydown", (e) => e.key === "Enter" && lyricsBtnClicked(e));
|
|
|
- }
|
|
|
- //#SECTION delete from queue btn
|
|
|
- let deleteBtnElem;
|
|
|
- if (features.deleteFromQueueButton) {
|
|
|
- deleteBtnElem = document.createElement("a");
|
|
|
- deleteBtnElem.ariaLabel = deleteBtnElem.title = (listType === "currentQueue" ? t("remove_from_queue") : t("delete_from_list"));
|
|
|
- deleteBtnElem.classList.add("ytmusic-player-bar", "bytm-delete-from-queue", "bytm-generic-btn");
|
|
|
- deleteBtnElem.role = "button";
|
|
|
- deleteBtnElem.tabIndex = 0;
|
|
|
- deleteBtnElem.style.visibility = "initial";
|
|
|
- const imgElem = document.createElement("img");
|
|
|
- imgElem.classList.add("bytm-generic-btn-img");
|
|
|
- imgElem.src = deleteIconUrl;
|
|
|
- const deleteBtnClicked = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
|
- e.preventDefault();
|
|
|
- e.stopImmediatePropagation();
|
|
|
- // container of the queue item popup menu - element gets reused for every queue item
|
|
|
- let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
|
|
|
- try {
|
|
|
- // three dots button to open the popup menu of a queue item
|
|
|
- const dotsBtnElem = queueItem.querySelector("ytmusic-menu-renderer yt-button-shape[id=\"button-shape\"] button");
|
|
|
- if (dotsBtnElem) {
|
|
|
- if (queuePopupCont)
|
|
|
- queuePopupCont.setAttribute("data-bytm-hidden", "true");
|
|
|
- dotsBtnElem.click();
|
|
|
- yield UserUtils.pauseFor(10);
|
|
|
- queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
|
|
|
- queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.setAttribute("data-bytm-hidden", "true");
|
|
|
- // a little bit janky and unreliable but the only way afaik
|
|
|
- const removeFromQueueBtn = queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.querySelector("tp-yt-paper-listbox ytmusic-menu-service-item-renderer:nth-of-type(3)");
|
|
|
- yield UserUtils.pauseFor(10);
|
|
|
- removeFromQueueBtn === null || removeFromQueueBtn === void 0 ? void 0 : removeFromQueueBtn.click();
|
|
|
- // queue items aren't removed automatically outside of the current queue
|
|
|
- if (removeFromQueueBtn && listType === "genericQueue") {
|
|
|
- yield UserUtils.pauseFor(500);
|
|
|
- clearInner(queueItem);
|
|
|
- queueItem.remove();
|
|
|
- }
|
|
|
- if (!removeFromQueueBtn) {
|
|
|
- warn("Couldn't find 'remove from queue' button in queue item three dots menu");
|
|
|
+ lyricsUrl && UserUtils.openInNewTab(lyricsUrl);
|
|
|
+ });
|
|
|
+ lyricsBtnElem.addEventListener("click", lyricsBtnClicked);
|
|
|
+ lyricsBtnElem.addEventListener("keydown", (e) => e.key === "Enter" && lyricsBtnClicked(e));
|
|
|
+ }
|
|
|
+ //#SECTION delete from queue btn
|
|
|
+ let deleteBtnElem;
|
|
|
+ if (features.deleteFromQueueButton) {
|
|
|
+ deleteBtnElem = document.createElement("a");
|
|
|
+ deleteBtnElem.ariaLabel = deleteBtnElem.title = (listType === "currentQueue" ? t("remove_from_queue") : t("delete_from_list"));
|
|
|
+ deleteBtnElem.classList.add("ytmusic-player-bar", "bytm-delete-from-queue", "bytm-generic-btn");
|
|
|
+ deleteBtnElem.role = "button";
|
|
|
+ deleteBtnElem.tabIndex = 0;
|
|
|
+ deleteBtnElem.style.visibility = "initial";
|
|
|
+ const imgElem = document.createElement("img");
|
|
|
+ imgElem.classList.add("bytm-generic-btn-img");
|
|
|
+ imgElem.src = deleteIconUrl;
|
|
|
+ const deleteBtnClicked = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ // container of the queue item popup menu - element gets reused for every queue item
|
|
|
+ let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
|
|
|
+ try {
|
|
|
+ // three dots button to open the popup menu of a queue item
|
|
|
+ const dotsBtnElem = queueItem.querySelector("ytmusic-menu-renderer yt-button-shape[id=\"button-shape\"] button");
|
|
|
+ if (dotsBtnElem) {
|
|
|
+ if (queuePopupCont)
|
|
|
+ queuePopupCont.setAttribute("data-bytm-hidden", "true");
|
|
|
dotsBtnElem.click();
|
|
|
- imgElem.src = yield getResourceUrl("img-error");
|
|
|
- if (deleteBtnElem)
|
|
|
- deleteBtnElem.ariaLabel = deleteBtnElem.title = (listType === "currentQueue" ? t("couldnt_remove_from_queue") : t("couldnt_delete_from_list"));
|
|
|
+ yield UserUtils.pauseFor(10);
|
|
|
+ queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown");
|
|
|
+ queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.setAttribute("data-bytm-hidden", "true");
|
|
|
+ // a little bit janky and unreliable but the only way afaik
|
|
|
+ const removeFromQueueBtn = queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.querySelector("tp-yt-paper-listbox ytmusic-menu-service-item-renderer:nth-of-type(3)");
|
|
|
+ yield UserUtils.pauseFor(10);
|
|
|
+ removeFromQueueBtn === null || removeFromQueueBtn === void 0 ? void 0 : removeFromQueueBtn.click();
|
|
|
+ // queue items aren't removed automatically outside of the current queue
|
|
|
+ if (removeFromQueueBtn && listType === "genericQueue") {
|
|
|
+ yield UserUtils.pauseFor(500);
|
|
|
+ clearInner(queueItem);
|
|
|
+ queueItem.remove();
|
|
|
+ }
|
|
|
+ if (!removeFromQueueBtn) {
|
|
|
+ warn("Couldn't find 'remove from queue' button in queue item three dots menu");
|
|
|
+ dotsBtnElem.click();
|
|
|
+ imgElem.src = yield getResourceUrl("img-error");
|
|
|
+ if (deleteBtnElem)
|
|
|
+ deleteBtnElem.ariaLabel = deleteBtnElem.title = (listType === "currentQueue" ? t("couldnt_remove_from_queue") : t("couldnt_delete_from_list"));
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't remove song from queue due to error:", err);
|
|
|
- }
|
|
|
- finally {
|
|
|
- queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.removeAttribute("data-bytm-hidden");
|
|
|
- }
|
|
|
- });
|
|
|
- deleteBtnElem.addEventListener("click", deleteBtnClicked);
|
|
|
- deleteBtnElem.addEventListener("keydown", (e) => e.key === "Enter" && deleteBtnClicked(e));
|
|
|
- deleteBtnElem.appendChild(imgElem);
|
|
|
- }
|
|
|
- //#SECTION append elements to DOM
|
|
|
- lyricsBtnElem && queueBtnsCont.appendChild(lyricsBtnElem);
|
|
|
- deleteBtnElem && queueBtnsCont.appendChild(deleteBtnElem);
|
|
|
- (_a = queueItem.querySelector(containerParentSelector)) === null || _a === void 0 ? void 0 : _a.appendChild(queueBtnsCont);
|
|
|
- queueItem.classList.add("bytm-has-queue-btns");
|
|
|
- });
|
|
|
-}let verNotifDialog = null;
|
|
|
-/** Creates and/or returns the dialog to be shown when a new version is available */
|
|
|
-function getVersionNotifDialog({ latestTag, }) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (!verNotifDialog) {
|
|
|
- const changelogMdFull = yield getChangelogMd();
|
|
|
- const changelogMd = changelogMdFull.split("<div class=\"split\">")[1];
|
|
|
- const changelogHtml = yield parseMarkdown(changelogMd);
|
|
|
- verNotifDialog = new BytmDialog({
|
|
|
- id: "version-notif",
|
|
|
- maxWidth: 600,
|
|
|
- maxHeight: 800,
|
|
|
- closeOnBgClick: false,
|
|
|
- closeOnEscPress: true,
|
|
|
- destroyOnClose: true,
|
|
|
- smallMenu: true,
|
|
|
- renderBody: () => renderBody({ latestTag, changelogHtml }),
|
|
|
- });
|
|
|
- }
|
|
|
- return verNotifDialog;
|
|
|
- });
|
|
|
-}
|
|
|
-function renderBody({ latestTag, changelogHtml, }) {
|
|
|
- const hostPlatformNames = {
|
|
|
- github: "GitHub",
|
|
|
- greasyfork: "GreasyFork",
|
|
|
- openuserjs: "OpenUserJS",
|
|
|
- };
|
|
|
- const wrapperEl = document.createElement("div");
|
|
|
- const pEl = document.createElement("p");
|
|
|
- pEl.textContent = t("new_version_available", scriptInfo.name, scriptInfo.version, latestTag, hostPlatformNames[host]);
|
|
|
- wrapperEl.appendChild(pEl);
|
|
|
- const disableUpdCheckEl = document.createElement("div");
|
|
|
- disableUpdCheckEl.id = "bytm-disable-update-check-wrapper";
|
|
|
- const checkboxEl = document.createElement("input");
|
|
|
- checkboxEl.type = "checkbox";
|
|
|
- checkboxEl.id = "bytm-disable-update-check-chkbox";
|
|
|
- checkboxEl.checked = false;
|
|
|
- const labelEl = document.createElement("label");
|
|
|
- labelEl.htmlFor = "bytm-disable-update-check-chkbox";
|
|
|
- labelEl.textContent = t("disable_update_check");
|
|
|
- disableUpdCheckEl.appendChild(checkboxEl);
|
|
|
- disableUpdCheckEl.appendChild(labelEl);
|
|
|
- wrapperEl.appendChild(disableUpdCheckEl);
|
|
|
- verNotifDialog.on("close", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const config = getFeatures();
|
|
|
- if (checkboxEl.checked)
|
|
|
- config.versionCheck = false;
|
|
|
- yield saveFeatures(config);
|
|
|
- }));
|
|
|
- const btnWrapper = document.createElement("div");
|
|
|
- btnWrapper.id = "bytm-version-notif-dialog-btns";
|
|
|
- const btnUpdate = document.createElement("button");
|
|
|
- btnUpdate.className = "bytm-btn";
|
|
|
- btnUpdate.textContent = t("open_update_page", hostPlatformNames[host]);
|
|
|
- btnUpdate.addEventListener("click", () => {
|
|
|
- window.open(pkg.updates[host]);
|
|
|
- verNotifDialog.close();
|
|
|
- });
|
|
|
- const btnIgnore = document.createElement("button");
|
|
|
- btnIgnore.className = "bytm-btn";
|
|
|
- btnIgnore.textContent = t("ignore_for_24h");
|
|
|
- btnIgnore.addEventListener("click", () => verNotifDialog.close());
|
|
|
- btnWrapper.appendChild(btnUpdate);
|
|
|
- btnWrapper.appendChild(btnIgnore);
|
|
|
- wrapperEl.appendChild(btnWrapper);
|
|
|
- return wrapperEl;
|
|
|
-}const releaseURL = "https://github.com/Sv443/BetterYTM/releases/latest";
|
|
|
-function checkVersion() {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- if (getFeatures().versionCheck === false)
|
|
|
- return info("Version check is disabled");
|
|
|
- const lastCheck = yield GM.getValue("bytm-version-check", 0);
|
|
|
- if (Date.now() - lastCheck < 1000 * 60 * 60 * 24)
|
|
|
- return;
|
|
|
- yield GM.setValue("bytm-version-check", Date.now());
|
|
|
- const res = yield sendRequest({
|
|
|
- method: "GET",
|
|
|
- url: releaseURL,
|
|
|
- });
|
|
|
- const latestTag = (_a = res.finalUrl.split("/").pop()) === null || _a === void 0 ? void 0 : _a.replace(/[a-zA-Z]/g, "");
|
|
|
- if (!latestTag)
|
|
|
- return;
|
|
|
- const versionComp = compareVersions(scriptInfo.version, latestTag);
|
|
|
- info("Version check - current version:", scriptInfo.version, "- latest version:", latestTag);
|
|
|
- if (versionComp < 0) {
|
|
|
- const dialog = yield getVersionNotifDialog({ latestTag });
|
|
|
- yield dialog.open();
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't remove song from queue due to error:", err);
|
|
|
+ }
|
|
|
+ finally {
|
|
|
+ queuePopupCont === null || queuePopupCont === void 0 ? void 0 : queuePopupCont.removeAttribute("data-bytm-hidden");
|
|
|
+ }
|
|
|
+ });
|
|
|
+ deleteBtnElem.addEventListener("click", deleteBtnClicked);
|
|
|
+ deleteBtnElem.addEventListener("keydown", (e) => e.key === "Enter" && deleteBtnClicked(e));
|
|
|
+ deleteBtnElem.appendChild(imgElem);
|
|
|
}
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Version check failed:", err);
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-/**
|
|
|
- * Crudely compares two semver version strings.
|
|
|
- * @returns Returns 1 if a > b or -1 if a < b or 0 if a == b
|
|
|
- */
|
|
|
-function compareVersions(a, b) {
|
|
|
- const pa = a.split(".");
|
|
|
- const pb = b.split(".");
|
|
|
- for (let i = 0; i < 3; i++) {
|
|
|
- const na = Number(pa[i]);
|
|
|
- const nb = Number(pb[i]);
|
|
|
- if (na > nb)
|
|
|
- return 1;
|
|
|
- if (nb > na)
|
|
|
- return -1;
|
|
|
- if (!isNaN(na) && isNaN(nb))
|
|
|
- return 1;
|
|
|
- if (isNaN(na) && !isNaN(nb))
|
|
|
- return -1;
|
|
|
- }
|
|
|
- return 0;
|
|
|
-}//#MARKER feature dependencies
|
|
|
-const localeOptions = Object.entries(locales).reduce((a, [locale, { name }]) => {
|
|
|
- return [...a, {
|
|
|
- value: locale,
|
|
|
- label: name,
|
|
|
- }];
|
|
|
-}, [])
|
|
|
- .sort((a, b) => a.label.localeCompare(b.label));
|
|
|
-//#MARKER features
|
|
|
-/**
|
|
|
- * Contains all possible features with their default values and other configuration.
|
|
|
- *
|
|
|
- * **Required props:**
|
|
|
- * | Property | Description |
|
|
|
- * | :-- | :-- |
|
|
|
- * | `type` | type of the feature configuration element - use autocomplete or check `FeatureTypeProps` in `src/types.ts` |
|
|
|
- * | `category` | category of the feature - use autocomplete or check `FeatureCategory` in `src/types.ts` |
|
|
|
- * | `default` | default value of the feature - type of the value depends on the given `type` |
|
|
|
- * | `enable(value: any)` | function that will be called when the feature is enabled / initialized for the first time |
|
|
|
- *
|
|
|
- * **Optional props:**
|
|
|
- * | Property | Description |
|
|
|
- * | :-- | :-- |
|
|
|
- * | `disable(newValue: any)` | for type `toggle` only - function that will be called when the feature is disabled - can be a synchronous or asynchronous function |
|
|
|
- * | `change(prevValue: any, newValue: any)` | for types `number`, `select`, `slider` and `hotkey` only - function that will be called when the value is changed |
|
|
|
- * | `helpText(): string / () => string` | function that returns an HTML string or the literal string itself that will be the help text for this feature - writing as function is useful for pluralizing or inserting values into the translation at runtime - if not set, translation with key `feature_helptext_featureKey` will be used instead, if available |
|
|
|
- * | `textAdornment(): string / Promise<string>` | function that returns an HTML string that will be appended to the text in the config menu as an adornment element - TODO: to be replaced in the big menu rework |
|
|
|
- * | `hidden` | if true, the feature will not be shown in the settings - default is undefined (false) |
|
|
|
- * | `min` | Only if type is `number` or `slider` - Overwrites the default of the `min` property of the HTML input element |
|
|
|
- * | `max` | Only if type is `number` or `slider` - Overwrites the default of the `max` property of the HTML input element |
|
|
|
- * | `step` | Only if type is `number` or `slider` - Overwrites the default of the `step` property of the HTML input element |
|
|
|
- * | `unit` | Only if type is `number` or `slider` - The unit text that is displayed next to the input element, i.e. "px" |
|
|
|
- *
|
|
|
- * **Notes:**
|
|
|
- * - If no `disable()` or `change()` function is present, the page needs to be reloaded for the changes to take effect
|
|
|
- */
|
|
|
-const featInfo = {
|
|
|
- //#SECTION layout
|
|
|
- removeUpgradeTab: {
|
|
|
- type: "toggle",
|
|
|
- category: "layout",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- },
|
|
|
- volumeSliderLabel: {
|
|
|
- type: "toggle",
|
|
|
- category: "layout",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- volumeSliderSize: {
|
|
|
- type: "number",
|
|
|
- category: "layout",
|
|
|
- min: 50,
|
|
|
- max: 500,
|
|
|
- step: 5,
|
|
|
- default: 150,
|
|
|
- unit: "px",
|
|
|
- enable: () => void "TODO",
|
|
|
- change: () => void "TODO",
|
|
|
- },
|
|
|
- volumeSliderStep: {
|
|
|
- type: "slider",
|
|
|
- category: "layout",
|
|
|
- min: 1,
|
|
|
- max: 25,
|
|
|
- default: 2,
|
|
|
- unit: "%",
|
|
|
- enable: () => void "TODO",
|
|
|
- change: () => void "TODO",
|
|
|
- },
|
|
|
- volumeSliderScrollStep: {
|
|
|
- type: "slider",
|
|
|
- category: "layout",
|
|
|
- min: 1,
|
|
|
- max: 25,
|
|
|
- default: 10,
|
|
|
- unit: "%",
|
|
|
- enable: () => void "TODO",
|
|
|
- change: () => void "TODO",
|
|
|
- },
|
|
|
- watermarkEnabled: {
|
|
|
- type: "toggle",
|
|
|
- category: "layout",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- removeShareTrackingParam: {
|
|
|
- type: "toggle",
|
|
|
- category: "layout",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- fixSpacing: {
|
|
|
- type: "toggle",
|
|
|
- category: "layout",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- scrollToActiveSongBtn: {
|
|
|
- type: "toggle",
|
|
|
- category: "layout",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- //#SECTION song lists
|
|
|
- lyricsQueueButton: {
|
|
|
- type: "toggle",
|
|
|
- category: "songLists",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- deleteFromQueueButton: {
|
|
|
- type: "toggle",
|
|
|
- category: "songLists",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- listButtonsPlacement: {
|
|
|
- type: "select",
|
|
|
- category: "songLists",
|
|
|
- options: () => [
|
|
|
- { value: "queueOnly", label: t("list_button_placement_queue_only") },
|
|
|
- { value: "everywhere", label: t("list_button_placement_everywhere") },
|
|
|
- ],
|
|
|
- default: "everywhere",
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- //#SECTION behavior
|
|
|
- disableBeforeUnloadPopup: {
|
|
|
- type: "toggle",
|
|
|
- category: "behavior",
|
|
|
- default: false,
|
|
|
- enable: () => void "TODO",
|
|
|
- },
|
|
|
- closeToastsTimeout: {
|
|
|
- type: "number",
|
|
|
- category: "behavior",
|
|
|
- min: 0,
|
|
|
- max: 30,
|
|
|
- step: 0.5,
|
|
|
- default: 0,
|
|
|
- unit: "s",
|
|
|
- enable: () => void "TODO",
|
|
|
- change: () => void "TODO",
|
|
|
- },
|
|
|
- rememberSongTime: {
|
|
|
- type: "toggle",
|
|
|
- category: "behavior",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO", // TODO: feasible?
|
|
|
- helpText: () => tp("feature_helptext_rememberSongTime", remSongMinPlayTime, remSongMinPlayTime)
|
|
|
- },
|
|
|
- rememberSongTimeSites: {
|
|
|
- type: "select",
|
|
|
- category: "behavior",
|
|
|
- options: () => [
|
|
|
- { value: "all", label: t("remember_song_time_sites_all") },
|
|
|
- { value: "yt", label: t("remember_song_time_sites_yt") },
|
|
|
- { value: "ytm", label: t("remember_song_time_sites_ytm") },
|
|
|
- ],
|
|
|
- default: "ytm",
|
|
|
- enable: () => void "TODO",
|
|
|
- change: () => void "TODO",
|
|
|
- },
|
|
|
- //#SECTION input
|
|
|
- arrowKeySupport: {
|
|
|
- type: "toggle",
|
|
|
- category: "input",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- arrowKeySkipBy: {
|
|
|
- type: "number",
|
|
|
- category: "input",
|
|
|
- min: 0.5,
|
|
|
- max: 60,
|
|
|
- step: 0.5,
|
|
|
- default: 5,
|
|
|
- enable: () => void "TODO",
|
|
|
- change: () => void "TODO",
|
|
|
- },
|
|
|
- switchBetweenSites: {
|
|
|
- type: "toggle",
|
|
|
- category: "input",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- switchSitesHotkey: {
|
|
|
- type: "hotkey",
|
|
|
- category: "input",
|
|
|
- default: {
|
|
|
- code: "F9",
|
|
|
- shift: false,
|
|
|
- ctrl: false,
|
|
|
- alt: false,
|
|
|
- },
|
|
|
- enable: () => void "TODO",
|
|
|
- change: () => void "TODO",
|
|
|
- },
|
|
|
- anchorImprovements: {
|
|
|
- type: "toggle",
|
|
|
- category: "input",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- numKeysSkipToTime: {
|
|
|
- type: "toggle",
|
|
|
- category: "input",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- //#SECTION lyrics
|
|
|
- geniusLyrics: {
|
|
|
- type: "toggle",
|
|
|
- category: "lyrics",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- //#SECTION general
|
|
|
- locale: {
|
|
|
- type: "select",
|
|
|
- category: "general",
|
|
|
- options: localeOptions,
|
|
|
- default: getPreferredLocale(),
|
|
|
- enable: () => void "TODO",
|
|
|
- // TODO: to be reworked or removed in the big menu rework
|
|
|
- textAdornment: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = yield resourceToHTMLString("img-globe")) !== null && _a !== void 0 ? _a : ""; }),
|
|
|
- },
|
|
|
- versionCheck: {
|
|
|
- type: "toggle",
|
|
|
- category: "general",
|
|
|
- default: true,
|
|
|
- enable: () => void "TODO",
|
|
|
- disable: () => void "TODO",
|
|
|
- },
|
|
|
- logLevel: {
|
|
|
- type: "select",
|
|
|
- category: "general",
|
|
|
- options: () => [
|
|
|
- { value: 0, label: t("log_level_debug") },
|
|
|
- { value: 1, label: t("log_level_info") },
|
|
|
- ],
|
|
|
- default: 1,
|
|
|
- enable: () => void "TODO",
|
|
|
- },
|
|
|
-};/** If this number is incremented, the features object data will be migrated to the new format */
|
|
|
-const formatVersion = 4;
|
|
|
-/** Config data format migration dictionary */
|
|
|
-const migrations = {
|
|
|
- // 1 -> 2
|
|
|
- 2: (oldData) => {
|
|
|
- const queueBtnsEnabled = Boolean(oldData.queueButtons);
|
|
|
- delete oldData.queueButtons;
|
|
|
- return Object.assign(Object.assign({}, oldData), { deleteFromQueueButton: queueBtnsEnabled, lyricsQueueButton: queueBtnsEnabled });
|
|
|
- },
|
|
|
- // 2 -> 3
|
|
|
- 3: (oldData) => (Object.assign(Object.assign({}, oldData), { removeShareTrackingParam: getFeatureDefault("removeShareTrackingParam"), numKeysSkipToTime: getFeatureDefault("numKeysSkipToTime"), fixSpacing: getFeatureDefault("fixSpacing"), scrollToActiveSongBtn: getFeatureDefault("scrollToActiveSongBtn"), logLevel: getFeatureDefault("logLevel") })),
|
|
|
- // 3 -> 4
|
|
|
- 4: (oldData) => {
|
|
|
- var _a, _b, _c, _d;
|
|
|
- const oldSwitchSitesHotkey = oldData.switchSitesHotkey;
|
|
|
- return Object.assign(Object.assign({}, oldData), { rememberSongTime: getFeatureDefault("rememberSongTime"), rememberSongTimeSites: getFeatureDefault("rememberSongTimeSites"), arrowKeySkipBy: 10, switchSitesHotkey: {
|
|
|
- code: (_a = oldSwitchSitesHotkey.key) !== null && _a !== void 0 ? _a : "F9",
|
|
|
- shift: Boolean((_b = oldSwitchSitesHotkey.shift) !== null && _b !== void 0 ? _b : false),
|
|
|
- ctrl: Boolean((_c = oldSwitchSitesHotkey.ctrl) !== null && _c !== void 0 ? _c : false),
|
|
|
- alt: Boolean((_d = oldSwitchSitesHotkey.meta) !== null && _d !== void 0 ? _d : false),
|
|
|
- }, listButtonsPlacement: "queueOnly", volumeSliderScrollStep: getFeatureDefault("volumeSliderScrollStep"), locale: getFeatureDefault("locale"), versionCheck: getFeatureDefault("versionCheck") });
|
|
|
- },
|
|
|
-};
|
|
|
-function getFeatureDefault(key) {
|
|
|
- return featInfo[key].default;
|
|
|
-}
|
|
|
-const defaultConfig = Object.keys(featInfo)
|
|
|
- .reduce((acc, key) => {
|
|
|
- acc[key] = featInfo[key].default;
|
|
|
- return acc;
|
|
|
-}, {});
|
|
|
-const cfgMgr = new UserUtils.ConfigManager({
|
|
|
- id: "bytm-config",
|
|
|
- formatVersion,
|
|
|
- defaultConfig,
|
|
|
- migrations,
|
|
|
-});
|
|
|
-/** Initializes the ConfigManager instance and loads persistent data into memory */
|
|
|
-function initConfig() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const oldFmtVer = Number(yield GM.getValue(`_uucfgver-${cfgMgr.id}`, NaN));
|
|
|
- const data = yield cfgMgr.loadData();
|
|
|
- log(`Initialized ConfigManager (format version = ${cfgMgr.formatVersion})`);
|
|
|
- if (isNaN(oldFmtVer))
|
|
|
- info("Config data initialized with default values");
|
|
|
- else if (oldFmtVer !== cfgMgr.formatVersion)
|
|
|
- info(`Config data migrated from version ${oldFmtVer} to ${cfgMgr.formatVersion}`);
|
|
|
- return data;
|
|
|
- });
|
|
|
-}
|
|
|
-/** Returns the current feature config from the in-memory cache */
|
|
|
-function getFeatures() {
|
|
|
- return cfgMgr.getData();
|
|
|
-}
|
|
|
-/** Saves the feature config synchronously to the in-memory cache and asynchronously to the persistent storage */
|
|
|
-function saveFeatures(featureConf) {
|
|
|
- const res = cfgMgr.setData(featureConf);
|
|
|
- emitSiteEvent("configChanged", cfgMgr.getData());
|
|
|
- info("Saved new feature config:", featureConf);
|
|
|
- return res;
|
|
|
-}
|
|
|
-/** Saves the default feature config synchronously to the in-memory cache and asynchronously to persistent storage */
|
|
|
-function setDefaultFeatures() {
|
|
|
- const res = cfgMgr.saveDefaultData();
|
|
|
- emitSiteEvent("configChanged", cfgMgr.getData());
|
|
|
- info("Reset feature config to its default values");
|
|
|
- return res;
|
|
|
-}
|
|
|
-/** Clears the feature config from the persistent storage - since the cache will be out of whack, this should only be run before a site re-/unload */
|
|
|
-function clearConfig() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- yield cfgMgr.deleteConfig();
|
|
|
- info("Deleted config from persistent storage");
|
|
|
- });
|
|
|
-}const { getUnsafeWindow } = UserUtils__namespace;
|
|
|
-const globalFuncs = {
|
|
|
- addSelectorListener,
|
|
|
- getResourceUrl,
|
|
|
- getSessionId,
|
|
|
- getVideoTime,
|
|
|
- setLocale,
|
|
|
- getLocale,
|
|
|
- hasKey,
|
|
|
- hasKeyFor,
|
|
|
- t,
|
|
|
- tp,
|
|
|
- getFeatures,
|
|
|
- saveFeatures,
|
|
|
- fetchLyricsUrl,
|
|
|
- getLyricsCacheEntry,
|
|
|
- sanitizeArtists,
|
|
|
- sanitizeSong,
|
|
|
-};
|
|
|
-/** Initializes the BYTM interface */
|
|
|
-function initInterface() {
|
|
|
- const props = Object.assign(Object.assign(Object.assign({ mode,
|
|
|
- branch }, scriptInfo), globalFuncs), { UserUtils: UserUtils__namespace });
|
|
|
- for (const [key, value] of Object.entries(props))
|
|
|
- setGlobalProp(key, value);
|
|
|
- log("Initialized BYTM interface");
|
|
|
-}
|
|
|
-/** Sets a global property on the window.BYTM object */
|
|
|
-function setGlobalProp(key, value) {
|
|
|
- // use unsafeWindow so the properties are available outside of the userscript's scope
|
|
|
- const win = getUnsafeWindow();
|
|
|
- if (!win.BYTM)
|
|
|
- win.BYTM = {};
|
|
|
- win.BYTM[key] = value;
|
|
|
-}
|
|
|
-/** Emits an event on the BYTM interface */
|
|
|
-function emitInterface(type, ...data) {
|
|
|
- getUnsafeWindow().dispatchEvent(new CustomEvent(type, { detail: data[0] }));
|
|
|
-}const fetchOpts = {
|
|
|
- timeout: 10000,
|
|
|
-};
|
|
|
-/** Contains all translation keys of all initialized and loaded translations */
|
|
|
-const allTrKeys = new Map();
|
|
|
-/** Contains the identifiers of all initialized and loaded translation locales */
|
|
|
-const initializedLocales = new Set();
|
|
|
-/** Initializes the translations */
|
|
|
-function initTranslations(locale) {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (initializedLocales.has(locale))
|
|
|
- return;
|
|
|
- initializedLocales.add(locale);
|
|
|
- try {
|
|
|
- const transUrl = yield getResourceUrl(`trans-${locale}`);
|
|
|
- const transFile = yield (yield UserUtils.fetchAdvanced(transUrl, fetchOpts)).json();
|
|
|
- // merge with base translations if specified
|
|
|
- const baseTransUrl = transFile.base ? yield getResourceUrl(`trans-${transFile.base}`) : undefined;
|
|
|
- const baseTransFile = baseTransUrl ? yield (yield UserUtils.fetchAdvanced(baseTransUrl, fetchOpts)).json() : undefined;
|
|
|
- const translations = Object.assign(Object.assign({}, ((_a = baseTransFile === null || baseTransFile === void 0 ? void 0 : baseTransFile.translations) !== null && _a !== void 0 ? _a : {})), transFile.translations);
|
|
|
- UserUtils.tr.addLanguage(locale, translations);
|
|
|
- allTrKeys.set(locale, new Set(Object.keys(translations)));
|
|
|
- info(`Loaded translations for locale '${locale}'`);
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- const errStr = `Couldn't load translations for locale '${locale}'`;
|
|
|
- error(errStr, err);
|
|
|
- throw new Error(errStr);
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-/** Sets the current language for translations */
|
|
|
-function setLocale(locale) {
|
|
|
- UserUtils.tr.setLanguage(locale);
|
|
|
- setGlobalProp("locale", locale);
|
|
|
- emitInterface("bytm:setLocale", { locale });
|
|
|
-}
|
|
|
-/** Returns the currently set language */
|
|
|
-function getLocale() {
|
|
|
- return UserUtils.tr.getLanguage();
|
|
|
-}
|
|
|
-/** Returns whether the given translation key exists in the current locale */
|
|
|
-function hasKey(key) {
|
|
|
- return hasKeyFor(getLocale(), key);
|
|
|
-}
|
|
|
-/** Returns whether the given translation key exists in the given locale */
|
|
|
-function hasKeyFor(locale, key) {
|
|
|
- var _a, _b;
|
|
|
- return (_b = (_a = allTrKeys.get(locale)) === null || _a === void 0 ? void 0 : _a.has(key)) !== null && _b !== void 0 ? _b : false;
|
|
|
-}
|
|
|
-/** Returns the translated string for the given key, after optionally inserting values */
|
|
|
-function t(key, ...values) {
|
|
|
- return UserUtils.tr(key, ...values);
|
|
|
-}
|
|
|
-/**
|
|
|
- * Returns the translated string for the given key with an added pluralization identifier based on the passed `num`
|
|
|
- * Tries to fall back to the non-pluralized syntax if no translation was found
|
|
|
- */
|
|
|
-function tp(key, num, ...values) {
|
|
|
- if (typeof num !== "number")
|
|
|
- num = num.length;
|
|
|
- const plNum = num === 1 ? "1" : "n";
|
|
|
- const trans = t(`${key}-${plNum}`, ...values);
|
|
|
- if (trans === key)
|
|
|
- return t(key, ...values);
|
|
|
- return trans;
|
|
|
-}/** ID of the last opened (top-most) dialog */
|
|
|
-let lastDialogId = null;
|
|
|
-/** Creates and manages a modal dialog element */
|
|
|
-class BytmDialog extends NanoEmitter {
|
|
|
- constructor(options) {
|
|
|
- super();
|
|
|
- Object.defineProperty(this, "options", {
|
|
|
- enumerable: true,
|
|
|
- configurable: true,
|
|
|
- writable: true,
|
|
|
- value: void 0
|
|
|
- });
|
|
|
- Object.defineProperty(this, "id", {
|
|
|
- enumerable: true,
|
|
|
- configurable: true,
|
|
|
- writable: true,
|
|
|
- value: void 0
|
|
|
+ //#SECTION append elements to DOM
|
|
|
+ lyricsBtnElem && queueBtnsCont.appendChild(lyricsBtnElem);
|
|
|
+ deleteBtnElem && queueBtnsCont.appendChild(deleteBtnElem);
|
|
|
+ (_a = queueItem.querySelector(containerParentSelector)) === null || _a === void 0 ? void 0 : _a.appendChild(queueBtnsCont);
|
|
|
+ queueItem.classList.add("bytm-has-queue-btns");
|
|
|
});
|
|
|
- Object.defineProperty(this, "dialogOpen", {
|
|
|
- enumerable: true,
|
|
|
- configurable: true,
|
|
|
- writable: true,
|
|
|
- value: false
|
|
|
+ }
|
|
|
+
|
|
|
+ let verNotifDialog = null;
|
|
|
+ /** Creates and/or returns the dialog to be shown when a new version is available */
|
|
|
+ function getVersionNotifDialog({ latestTag, }) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (!verNotifDialog) {
|
|
|
+ const changelogMdFull = yield getChangelogMd();
|
|
|
+ const changelogMd = changelogMdFull.split("<div class=\"split\">")[1];
|
|
|
+ const changelogHtml = yield parseMarkdown(changelogMd);
|
|
|
+ verNotifDialog = new BytmDialog({
|
|
|
+ id: "version-notif",
|
|
|
+ maxWidth: 600,
|
|
|
+ maxHeight: 800,
|
|
|
+ closeOnBgClick: false,
|
|
|
+ closeOnEscPress: true,
|
|
|
+ destroyOnClose: true,
|
|
|
+ smallMenu: true,
|
|
|
+ renderBody: () => renderBody({
|
|
|
+ latestTag,
|
|
|
+ changelogHtml,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return verNotifDialog;
|
|
|
});
|
|
|
- Object.defineProperty(this, "dialogRendered", {
|
|
|
- enumerable: true,
|
|
|
- configurable: true,
|
|
|
- writable: true,
|
|
|
- value: false
|
|
|
+ }
|
|
|
+ function renderBody({ latestTag, changelogHtml, }) {
|
|
|
+ const hostPlatformNames = {
|
|
|
+ github: "GitHub",
|
|
|
+ greasyfork: "GreasyFork",
|
|
|
+ openuserjs: "OpenUserJS",
|
|
|
+ };
|
|
|
+ const wrapperEl = document.createElement("div");
|
|
|
+ const pEl = document.createElement("p");
|
|
|
+ pEl.textContent = t("new_version_available", scriptInfo.name, scriptInfo.version, latestTag, hostPlatformNames[host]);
|
|
|
+ wrapperEl.appendChild(pEl);
|
|
|
+ const changelogDetailsEl = document.createElement("details");
|
|
|
+ changelogDetailsEl.id = "bytm-version-notif-changelog-details";
|
|
|
+ changelogDetailsEl.open = false;
|
|
|
+ const changelogSummaryEl = document.createElement("summary");
|
|
|
+ changelogSummaryEl.ariaLabel = changelogSummaryEl.title = changelogSummaryEl.textContent = t("expand_release_notes");
|
|
|
+ changelogDetailsEl.appendChild(changelogSummaryEl);
|
|
|
+ const changelogEl = document.createElement("p");
|
|
|
+ changelogEl.id = "bytm-version-notif-changelog-cont";
|
|
|
+ changelogEl.classList.add("bytm-markdown-container");
|
|
|
+ changelogEl.innerHTML = changelogHtml;
|
|
|
+ changelogEl.querySelectorAll("a").forEach((a) => {
|
|
|
+ a.target = "_blank";
|
|
|
+ a.rel = "noopener noreferrer";
|
|
|
});
|
|
|
- Object.defineProperty(this, "listenersAttached", {
|
|
|
- enumerable: true,
|
|
|
- configurable: true,
|
|
|
- writable: true,
|
|
|
- value: false
|
|
|
+ changelogDetailsEl.appendChild(changelogEl);
|
|
|
+ wrapperEl.appendChild(changelogDetailsEl);
|
|
|
+ const disableUpdCheckEl = document.createElement("div");
|
|
|
+ disableUpdCheckEl.id = "bytm-disable-update-check-wrapper";
|
|
|
+ const checkboxEl = document.createElement("input");
|
|
|
+ checkboxEl.type = "checkbox";
|
|
|
+ checkboxEl.id = "bytm-disable-update-check-chkbox";
|
|
|
+ checkboxEl.tabIndex = 0;
|
|
|
+ checkboxEl.checked = false;
|
|
|
+ const labelEl = document.createElement("label");
|
|
|
+ labelEl.htmlFor = "bytm-disable-update-check-chkbox";
|
|
|
+ labelEl.textContent = t("disable_update_check");
|
|
|
+ disableUpdCheckEl.appendChild(checkboxEl);
|
|
|
+ disableUpdCheckEl.appendChild(labelEl);
|
|
|
+ wrapperEl.appendChild(disableUpdCheckEl);
|
|
|
+ verNotifDialog === null || verNotifDialog === void 0 ? void 0 : verNotifDialog.on("close", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const config = getFeatures();
|
|
|
+ if (checkboxEl.checked)
|
|
|
+ config.versionCheck = false;
|
|
|
+ yield saveFeatures(config);
|
|
|
+ }));
|
|
|
+ const btnWrapper = document.createElement("div");
|
|
|
+ btnWrapper.id = "bytm-version-notif-dialog-btns";
|
|
|
+ const btnUpdate = document.createElement("button");
|
|
|
+ btnUpdate.className = "bytm-btn";
|
|
|
+ btnUpdate.tabIndex = 0;
|
|
|
+ btnUpdate.textContent = t("open_update_page_install_manually", hostPlatformNames[host]);
|
|
|
+ const btnUpdateClicked = () => {
|
|
|
+ window.open(pkg.updates[host]);
|
|
|
+ verNotifDialog === null || verNotifDialog === void 0 ? void 0 : verNotifDialog.close();
|
|
|
+ };
|
|
|
+ btnUpdate.addEventListener("click", btnUpdateClicked);
|
|
|
+ btnUpdate.addEventListener("keydown", (e) => e.key === "Enter" && btnUpdateClicked());
|
|
|
+ const btnClose = document.createElement("button");
|
|
|
+ btnClose.className = "bytm-btn";
|
|
|
+ btnClose.tabIndex = 0;
|
|
|
+ btnClose.textContent = t("ignore_for_24h");
|
|
|
+ checkboxEl.addEventListener("change", () => {
|
|
|
+ if (checkboxEl.checked)
|
|
|
+ btnClose.textContent = t("close");
|
|
|
+ else
|
|
|
+ btnClose.textContent = t("ignore_for_24h");
|
|
|
});
|
|
|
- this.options = Object.assign({ closeOnBgClick: true, closeOnEscPress: true, closeBtnEnabled: true, destroyOnClose: false, smallHeader: false }, options);
|
|
|
- this.id = options.id;
|
|
|
+ btnClose.addEventListener("click", () => verNotifDialog === null || verNotifDialog === void 0 ? void 0 : verNotifDialog.close());
|
|
|
+ btnClose.addEventListener("keydown", (e) => e.key === "Enter" && (verNotifDialog === null || verNotifDialog === void 0 ? void 0 : verNotifDialog.close()));
|
|
|
+ btnWrapper.appendChild(btnUpdate);
|
|
|
+ btnWrapper.appendChild(btnClose);
|
|
|
+ wrapperEl.appendChild(btnWrapper);
|
|
|
+ return wrapperEl;
|
|
|
}
|
|
|
- /** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
|
|
|
- mount() {
|
|
|
+
|
|
|
+ const releaseURL = "https://github.com/Sv443/BetterYTM/releases/latest";
|
|
|
+ function checkVersion() {
|
|
|
+ var _a;
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (this.dialogRendered)
|
|
|
- return;
|
|
|
- this.dialogRendered = true;
|
|
|
- const bgElem = document.createElement("div");
|
|
|
- bgElem.id = `bytm-${this.id}-dialog-bg`;
|
|
|
- bgElem.classList.add("bytm-dialog-bg");
|
|
|
- if (this.options.closeOnBgClick)
|
|
|
- bgElem.ariaLabel = bgElem.title = t("close_menu_tooltip");
|
|
|
- bgElem.style.visibility = "hidden";
|
|
|
- bgElem.style.display = "none";
|
|
|
- bgElem.inert = true;
|
|
|
- bgElem.appendChild(yield this.getDialogContent());
|
|
|
- document.body.appendChild(bgElem);
|
|
|
- this.attachListeners(bgElem);
|
|
|
- UserUtils.addGlobalStyle(`\
|
|
|
-#bytm-${this.id}-dialog-bg {
|
|
|
- --bytm-dialog-width-max: ${this.options.maxWidth}px;
|
|
|
- --bytm-dialog-height-max: ${this.options.maxHeight}px;
|
|
|
-}`).id = `bytm-style-dialog-${this.id}`;
|
|
|
- this.events.emit("render");
|
|
|
- return bgElem;
|
|
|
+ try {
|
|
|
+ if (getFeatures().versionCheck === false)
|
|
|
+ return info("Version check is disabled");
|
|
|
+ const lastCheck = yield GM.getValue("bytm-version-check", 0);
|
|
|
+ if (Date.now() - lastCheck < 1000 * 60 * 60 * 24)
|
|
|
+ return;
|
|
|
+ yield GM.setValue("bytm-version-check", Date.now());
|
|
|
+ const res = yield sendRequest({
|
|
|
+ method: "GET",
|
|
|
+ url: releaseURL,
|
|
|
+ });
|
|
|
+ const latestTag = (_a = res.finalUrl.split("/").pop()) === null || _a === void 0 ? void 0 : _a.replace(/[a-zA-Z]/g, "");
|
|
|
+ if (!latestTag)
|
|
|
+ return;
|
|
|
+ const versionComp = compareVersions(scriptInfo.version, latestTag);
|
|
|
+ info("Version check - current version:", scriptInfo.version, "- latest version:", latestTag);
|
|
|
+ if (versionComp < 0) {
|
|
|
+ const dialog = yield getVersionNotifDialog({ latestTag });
|
|
|
+ yield dialog.open();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Version check failed:", err);
|
|
|
+ }
|
|
|
});
|
|
|
}
|
|
|
- /** Clears all dialog contents (unmounts them from the DOM) in preparation for a new rendering call */
|
|
|
- unmount() {
|
|
|
- var _a;
|
|
|
- this.dialogRendered = false;
|
|
|
- const clearSelectors = [
|
|
|
- `#bytm-${this.id}-dialog-bg`,
|
|
|
- `#bytm-style-dialog-${this.id}`,
|
|
|
- ];
|
|
|
- for (const sel of clearSelectors) {
|
|
|
- const elem = document.querySelector(sel);
|
|
|
- (elem === null || elem === void 0 ? void 0 : elem.hasChildNodes()) && clearInner(elem);
|
|
|
- (_a = document.querySelector(sel)) === null || _a === void 0 ? void 0 : _a.remove();
|
|
|
+ /**
|
|
|
+ * Crudely compares two semver version strings.
|
|
|
+ * @returns Returns 1 if a > b or -1 if a < b or 0 if a == b
|
|
|
+ */
|
|
|
+ function compareVersions(a, b) {
|
|
|
+ const pa = a.split(".");
|
|
|
+ const pb = b.split(".");
|
|
|
+ for (let i = 0; i < 3; i++) {
|
|
|
+ const na = Number(pa[i]);
|
|
|
+ const nb = Number(pb[i]);
|
|
|
+ if (na > nb)
|
|
|
+ return 1;
|
|
|
+ if (nb > na)
|
|
|
+ return -1;
|
|
|
+ if (!isNaN(na) && isNaN(nb))
|
|
|
+ return 1;
|
|
|
+ if (isNaN(na) && !isNaN(nb))
|
|
|
+ return -1;
|
|
|
}
|
|
|
- this.events.emit("clear");
|
|
|
- }
|
|
|
- /** Clears and then re-renders the dialog */
|
|
|
- rerender() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- this.unmount();
|
|
|
- yield this.mount();
|
|
|
- });
|
|
|
+ return 0;
|
|
|
}
|
|
|
+
|
|
|
+ //#MARKER feature dependencies
|
|
|
+ const localeOptions = Object.entries(locales).reduce((a, [locale, { name }]) => {
|
|
|
+ return [...a, {
|
|
|
+ value: locale,
|
|
|
+ label: name,
|
|
|
+ }];
|
|
|
+ }, [])
|
|
|
+ .sort((a, b) => a.label.localeCompare(b.label));
|
|
|
+ //#MARKER features
|
|
|
/**
|
|
|
- * Opens the dialog - also mounts it if it hasn't been mounted yet
|
|
|
- * Prevents default action and immediate propagation of the passed event
|
|
|
+ * Contains all possible features with their default values and other configuration.
|
|
|
+ *
|
|
|
+ * **Required props:**
|
|
|
+ * | Property | Description |
|
|
|
+ * | :-- | :-- |
|
|
|
+ * | `type` | type of the feature configuration element - use autocomplete or check `FeatureTypeProps` in `src/types.ts` |
|
|
|
+ * | `category` | category of the feature - use autocomplete or check `FeatureCategory` in `src/types.ts` |
|
|
|
+ * | `default` | default value of the feature - type of the value depends on the given `type` |
|
|
|
+ * | `enable(value: any)` | function that will be called when the feature is enabled / initialized for the first time |
|
|
|
+ *
|
|
|
+ * **Optional props:**
|
|
|
+ * | Property | Description |
|
|
|
+ * | :-- | :-- |
|
|
|
+ * | `disable(newValue: any)` | for type `toggle` only - function that will be called when the feature is disabled - can be a synchronous or asynchronous function |
|
|
|
+ * | `change(prevValue: any, newValue: any)` | for types `number`, `select`, `slider` and `hotkey` only - function that will be called when the value is changed |
|
|
|
+ * | `helpText(): string / () => string` | function that returns an HTML string or the literal string itself that will be the help text for this feature - writing as function is useful for pluralizing or inserting values into the translation at runtime - if not set, translation with key `feature_helptext_featureKey` will be used instead, if available |
|
|
|
+ * | `textAdornment(): string / Promise<string>` | function that returns an HTML string that will be appended to the text in the config menu as an adornment element - TODO: to be replaced in the big menu rework |
|
|
|
+ * | `hidden` | if true, the feature will not be shown in the settings - default is undefined (false) |
|
|
|
+ * | `min` | Only if type is `number` or `slider` - Overwrites the default of the `min` property of the HTML input element |
|
|
|
+ * | `max` | Only if type is `number` or `slider` - Overwrites the default of the `max` property of the HTML input element |
|
|
|
+ * | `step` | Only if type is `number` or `slider` - Overwrites the default of the `step` property of the HTML input element |
|
|
|
+ * | `unit` | Only if type is `number` or `slider` - The unit text that is displayed next to the input element, i.e. "px" |
|
|
|
+ *
|
|
|
+ * **Notes:**
|
|
|
+ * - If no `disable()` or `change()` function is present, the page needs to be reloaded for the changes to take effect
|
|
|
*/
|
|
|
- open(e) {
|
|
|
- var _a;
|
|
|
+ const featInfo = {
|
|
|
+ //#SECTION layout
|
|
|
+ removeUpgradeTab: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "layout",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ },
|
|
|
+ volumeSliderLabel: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "layout",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ volumeSliderSize: {
|
|
|
+ type: "number",
|
|
|
+ category: "layout",
|
|
|
+ min: 50,
|
|
|
+ max: 500,
|
|
|
+ step: 5,
|
|
|
+ default: 150,
|
|
|
+ unit: "px",
|
|
|
+ enable: () => void "TODO",
|
|
|
+ change: () => void "TODO",
|
|
|
+ },
|
|
|
+ volumeSliderStep: {
|
|
|
+ type: "slider",
|
|
|
+ category: "layout",
|
|
|
+ min: 1,
|
|
|
+ max: 25,
|
|
|
+ default: 2,
|
|
|
+ unit: "%",
|
|
|
+ enable: () => void "TODO",
|
|
|
+ change: () => void "TODO",
|
|
|
+ },
|
|
|
+ volumeSliderScrollStep: {
|
|
|
+ type: "slider",
|
|
|
+ category: "layout",
|
|
|
+ min: 1,
|
|
|
+ max: 25,
|
|
|
+ default: 10,
|
|
|
+ unit: "%",
|
|
|
+ enable: () => void "TODO",
|
|
|
+ change: () => void "TODO",
|
|
|
+ },
|
|
|
+ watermarkEnabled: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "layout",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ removeShareTrackingParam: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "layout",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ fixSpacing: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "layout",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ scrollToActiveSongBtn: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "layout",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ //#SECTION song lists
|
|
|
+ lyricsQueueButton: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "songLists",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ deleteFromQueueButton: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "songLists",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ listButtonsPlacement: {
|
|
|
+ type: "select",
|
|
|
+ category: "songLists",
|
|
|
+ options: () => [
|
|
|
+ { value: "queueOnly", label: t("list_button_placement_queue_only") },
|
|
|
+ { value: "everywhere", label: t("list_button_placement_everywhere") },
|
|
|
+ ],
|
|
|
+ default: "everywhere",
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ //#SECTION behavior
|
|
|
+ disableBeforeUnloadPopup: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "behavior",
|
|
|
+ default: false,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ },
|
|
|
+ closeToastsTimeout: {
|
|
|
+ type: "number",
|
|
|
+ category: "behavior",
|
|
|
+ min: 0,
|
|
|
+ max: 30,
|
|
|
+ step: 0.5,
|
|
|
+ default: 0,
|
|
|
+ unit: "s",
|
|
|
+ enable: () => void "TODO",
|
|
|
+ change: () => void "TODO",
|
|
|
+ },
|
|
|
+ rememberSongTime: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "behavior",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO", // TODO: feasible?
|
|
|
+ helpText: () => tp("feature_helptext_rememberSongTime", remSongMinPlayTime, remSongMinPlayTime)
|
|
|
+ },
|
|
|
+ rememberSongTimeSites: {
|
|
|
+ type: "select",
|
|
|
+ category: "behavior",
|
|
|
+ options: () => [
|
|
|
+ { value: "all", label: t("remember_song_time_sites_all") },
|
|
|
+ { value: "yt", label: t("remember_song_time_sites_yt") },
|
|
|
+ { value: "ytm", label: t("remember_song_time_sites_ytm") },
|
|
|
+ ],
|
|
|
+ default: "ytm",
|
|
|
+ enable: () => void "TODO",
|
|
|
+ change: () => void "TODO",
|
|
|
+ },
|
|
|
+ //#SECTION input
|
|
|
+ arrowKeySupport: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "input",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ arrowKeySkipBy: {
|
|
|
+ type: "number",
|
|
|
+ category: "input",
|
|
|
+ min: 0.5,
|
|
|
+ max: 60,
|
|
|
+ step: 0.5,
|
|
|
+ default: 5,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ change: () => void "TODO",
|
|
|
+ },
|
|
|
+ switchBetweenSites: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "input",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ switchSitesHotkey: {
|
|
|
+ type: "hotkey",
|
|
|
+ category: "input",
|
|
|
+ default: {
|
|
|
+ code: "F9",
|
|
|
+ shift: false,
|
|
|
+ ctrl: false,
|
|
|
+ alt: false,
|
|
|
+ },
|
|
|
+ enable: () => void "TODO",
|
|
|
+ change: () => void "TODO",
|
|
|
+ },
|
|
|
+ anchorImprovements: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "input",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ numKeysSkipToTime: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "input",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ //#SECTION lyrics
|
|
|
+ geniusLyrics: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "lyrics",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ //#SECTION general
|
|
|
+ locale: {
|
|
|
+ type: "select",
|
|
|
+ category: "general",
|
|
|
+ options: localeOptions,
|
|
|
+ default: getPreferredLocale(),
|
|
|
+ enable: () => void "TODO",
|
|
|
+ // TODO: to be reworked or removed in the big menu rework
|
|
|
+ textAdornment: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = yield resourceToHTMLString("img-globe")) !== null && _a !== void 0 ? _a : ""; }),
|
|
|
+ },
|
|
|
+ versionCheck: {
|
|
|
+ type: "toggle",
|
|
|
+ category: "general",
|
|
|
+ default: true,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ disable: () => void "TODO",
|
|
|
+ },
|
|
|
+ logLevel: {
|
|
|
+ type: "select",
|
|
|
+ category: "general",
|
|
|
+ options: () => [
|
|
|
+ { value: 0, label: t("log_level_debug") },
|
|
|
+ { value: 1, label: t("log_level_info") },
|
|
|
+ ],
|
|
|
+ default: 1,
|
|
|
+ enable: () => void "TODO",
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ /** If this number is incremented, the features object data will be migrated to the new format */
|
|
|
+ const formatVersion = 4;
|
|
|
+ /** Config data format migration dictionary */
|
|
|
+ const migrations = {
|
|
|
+ // 1 -> 2
|
|
|
+ 2: (oldData) => {
|
|
|
+ const queueBtnsEnabled = Boolean(oldData.queueButtons);
|
|
|
+ delete oldData.queueButtons;
|
|
|
+ return Object.assign(Object.assign({}, oldData), { deleteFromQueueButton: queueBtnsEnabled, lyricsQueueButton: queueBtnsEnabled });
|
|
|
+ },
|
|
|
+ // 2 -> 3
|
|
|
+ 3: (oldData) => (Object.assign(Object.assign({}, oldData), { removeShareTrackingParam: getFeatureDefault("removeShareTrackingParam"), numKeysSkipToTime: getFeatureDefault("numKeysSkipToTime"), fixSpacing: getFeatureDefault("fixSpacing"), scrollToActiveSongBtn: getFeatureDefault("scrollToActiveSongBtn"), logLevel: getFeatureDefault("logLevel") })),
|
|
|
+ // 3 -> 4
|
|
|
+ 4: (oldData) => {
|
|
|
+ var _a, _b, _c, _d;
|
|
|
+ const oldSwitchSitesHotkey = oldData.switchSitesHotkey;
|
|
|
+ return Object.assign(Object.assign({}, oldData), { rememberSongTime: getFeatureDefault("rememberSongTime"), rememberSongTimeSites: getFeatureDefault("rememberSongTimeSites"), arrowKeySkipBy: 10, switchSitesHotkey: {
|
|
|
+ code: (_a = oldSwitchSitesHotkey.key) !== null && _a !== void 0 ? _a : "F9",
|
|
|
+ shift: Boolean((_b = oldSwitchSitesHotkey.shift) !== null && _b !== void 0 ? _b : false),
|
|
|
+ ctrl: Boolean((_c = oldSwitchSitesHotkey.ctrl) !== null && _c !== void 0 ? _c : false),
|
|
|
+ alt: Boolean((_d = oldSwitchSitesHotkey.meta) !== null && _d !== void 0 ? _d : false),
|
|
|
+ }, listButtonsPlacement: "queueOnly", volumeSliderScrollStep: getFeatureDefault("volumeSliderScrollStep"), locale: getFeatureDefault("locale"), versionCheck: getFeatureDefault("versionCheck") });
|
|
|
+ },
|
|
|
+ };
|
|
|
+ function getFeatureDefault(key) {
|
|
|
+ return featInfo[key].default;
|
|
|
+ }
|
|
|
+ const defaultConfig = Object.keys(featInfo)
|
|
|
+ .reduce((acc, key) => {
|
|
|
+ acc[key] = featInfo[key].default;
|
|
|
+ return acc;
|
|
|
+ }, {});
|
|
|
+ const cfgMgr = new UserUtils.ConfigManager({
|
|
|
+ id: "bytm-config",
|
|
|
+ formatVersion,
|
|
|
+ defaultConfig,
|
|
|
+ migrations,
|
|
|
+ });
|
|
|
+ /** Initializes the ConfigManager instance and loads persistent data into memory */
|
|
|
+ function initConfig() {
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
- e === null || e === void 0 ? void 0 : e.preventDefault();
|
|
|
- e === null || e === void 0 ? void 0 : e.stopImmediatePropagation();
|
|
|
- if (this.isOpen())
|
|
|
- return;
|
|
|
- this.dialogOpen = true;
|
|
|
- if (!this.isRendered())
|
|
|
- yield this.mount();
|
|
|
- document.body.classList.add("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
- const dialogBg = document.querySelector(`#bytm-${this.id}-dialog-bg`);
|
|
|
- if (!dialogBg)
|
|
|
- return warn(`Couldn't find background element for dialog with ID '${this.id}'`);
|
|
|
- dialogBg.style.visibility = "visible";
|
|
|
- dialogBg.style.display = "block";
|
|
|
- dialogBg.inert = false;
|
|
|
- lastDialogId = this.id;
|
|
|
- this.events.emit("open");
|
|
|
- return dialogBg;
|
|
|
+ const oldFmtVer = Number(yield GM.getValue(`_uucfgver-${cfgMgr.id}`, NaN));
|
|
|
+ const data = yield cfgMgr.loadData();
|
|
|
+ log(`Initialized ConfigManager (format version = ${cfgMgr.formatVersion})`);
|
|
|
+ if (isNaN(oldFmtVer))
|
|
|
+ info("Config data initialized with default values");
|
|
|
+ else if (oldFmtVer !== cfgMgr.formatVersion)
|
|
|
+ info(`Config data migrated from version ${oldFmtVer} to ${cfgMgr.formatVersion}`);
|
|
|
+ return data;
|
|
|
});
|
|
|
}
|
|
|
- /** Closes the dialog - prevents default action and immediate propagation of the passed event */
|
|
|
- close(e) {
|
|
|
- var _a;
|
|
|
- e === null || e === void 0 ? void 0 : e.preventDefault();
|
|
|
- e === null || e === void 0 ? void 0 : e.stopImmediatePropagation();
|
|
|
- if (!this.isOpen())
|
|
|
- return;
|
|
|
- this.dialogOpen = false;
|
|
|
- document.body.classList.remove("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
|
|
|
- const dialogBg = document.querySelector(`#bytm-${this.id}-dialog-bg`);
|
|
|
- if (!dialogBg)
|
|
|
- return warn(`Couldn't find background element for dialog with ID '${this.id}'`);
|
|
|
- dialogBg.style.visibility = "hidden";
|
|
|
- dialogBg.style.display = "none";
|
|
|
- dialogBg.inert = true;
|
|
|
- if (BytmDialog.getLastDialogId() === this.id)
|
|
|
- lastDialogId = null;
|
|
|
- this.events.emit("close");
|
|
|
- if (this.options.destroyOnClose)
|
|
|
- this.destroy();
|
|
|
+ /** Returns the current feature config from the in-memory cache */
|
|
|
+ function getFeatures() {
|
|
|
+ return cfgMgr.getData();
|
|
|
}
|
|
|
- /** Returns true if the dialog is open */
|
|
|
- isOpen() {
|
|
|
- return this.dialogOpen;
|
|
|
+ /** Saves the feature config synchronously to the in-memory cache and asynchronously to the persistent storage */
|
|
|
+ function saveFeatures(featureConf) {
|
|
|
+ const res = cfgMgr.setData(featureConf);
|
|
|
+ emitSiteEvent("configChanged", cfgMgr.getData());
|
|
|
+ info("Saved new feature config:", featureConf);
|
|
|
+ return res;
|
|
|
}
|
|
|
- /** Returns true if the dialog has been rendered */
|
|
|
- isRendered() {
|
|
|
- return this.dialogRendered;
|
|
|
+ /** Saves the default feature config synchronously to the in-memory cache and asynchronously to persistent storage */
|
|
|
+ function setDefaultFeatures() {
|
|
|
+ const res = cfgMgr.saveDefaultData();
|
|
|
+ emitSiteEvent("configChanged", cfgMgr.getData());
|
|
|
+ info("Reset feature config to its default values");
|
|
|
+ return res;
|
|
|
}
|
|
|
- /** Clears the dialog and removes all event listeners */
|
|
|
- destroy() {
|
|
|
- this.events.emit("destroy");
|
|
|
- this.unmount();
|
|
|
- this.unsubscribeAll();
|
|
|
+ /** Clears the feature config from the persistent storage - since the cache will be out of whack, this should only be run before a site re-/unload */
|
|
|
+ function clearConfig() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ yield cfgMgr.deleteConfig();
|
|
|
+ info("Deleted config from persistent storage");
|
|
|
+ });
|
|
|
}
|
|
|
- /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
|
|
|
- static getLastDialogId() {
|
|
|
- return lastDialogId;
|
|
|
+
|
|
|
+ const { getUnsafeWindow } = UserUtils__namespace;
|
|
|
+ const globalFuncs = {
|
|
|
+ addSelectorListener,
|
|
|
+ getResourceUrl,
|
|
|
+ getSessionId,
|
|
|
+ getVideoTime,
|
|
|
+ setLocale,
|
|
|
+ getLocale,
|
|
|
+ hasKey,
|
|
|
+ hasKeyFor,
|
|
|
+ t,
|
|
|
+ tp,
|
|
|
+ getFeatures,
|
|
|
+ saveFeatures,
|
|
|
+ fetchLyricsUrl,
|
|
|
+ getLyricsCacheEntry,
|
|
|
+ sanitizeArtists,
|
|
|
+ sanitizeSong,
|
|
|
+ };
|
|
|
+ /** Initializes the BYTM interface */
|
|
|
+ function initInterface() {
|
|
|
+ const props = Object.assign(Object.assign(Object.assign({ mode,
|
|
|
+ branch }, scriptInfo), globalFuncs), { UserUtils: UserUtils__namespace });
|
|
|
+ for (const [key, value] of Object.entries(props))
|
|
|
+ setGlobalProp(key, value);
|
|
|
+ log("Initialized BYTM interface");
|
|
|
}
|
|
|
- /** Called once to attach all generic event listeners */
|
|
|
- attachListeners(bgElem) {
|
|
|
- if (this.listenersAttached)
|
|
|
- return;
|
|
|
- this.listenersAttached = true;
|
|
|
- if (this.options.closeOnBgClick) {
|
|
|
- bgElem.addEventListener("click", (e) => {
|
|
|
- var _a;
|
|
|
- if (this.isOpen() && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === `bytm-${this.id}-dialog-bg`)
|
|
|
- this.close(e);
|
|
|
- });
|
|
|
- }
|
|
|
- if (this.options.closeOnEscPress) {
|
|
|
- document.body.addEventListener("keydown", (e) => {
|
|
|
- if (e.key === "Escape" && this.isOpen() && BytmDialog.getLastDialogId() === this.id)
|
|
|
- this.close(e);
|
|
|
- });
|
|
|
- }
|
|
|
+ /** Sets a global property on the window.BYTM object */
|
|
|
+ function setGlobalProp(key, value) {
|
|
|
+ // use unsafeWindow so the properties are available outside of the userscript's scope
|
|
|
+ const win = getUnsafeWindow();
|
|
|
+ if (!win.BYTM)
|
|
|
+ win.BYTM = {};
|
|
|
+ win.BYTM[key] = value;
|
|
|
}
|
|
|
- getDialogContent() {
|
|
|
- var _a, _b, _c, _d;
|
|
|
+ /** Emits an event on the BYTM interface */
|
|
|
+ function emitInterface(type, ...data) {
|
|
|
+ getUnsafeWindow().dispatchEvent(new CustomEvent(type, { detail: data[0] }));
|
|
|
+ }
|
|
|
+
|
|
|
+ const fetchOpts = {
|
|
|
+ timeout: 10000,
|
|
|
+ };
|
|
|
+ /** Contains all translation keys of all initialized and loaded translations */
|
|
|
+ const allTrKeys = new Map();
|
|
|
+ /** Contains the identifiers of all initialized and loaded translation locales */
|
|
|
+ const initializedLocales = new Set();
|
|
|
+ /** Initializes the translations */
|
|
|
+ function initTranslations(locale) {
|
|
|
+ var _a;
|
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
|
- const header = (_b = (_a = this.options).renderHeader) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
|
- const footer = (_d = (_c = this.options).renderFooter) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
|
- const dialogWrapperEl = document.createElement("div");
|
|
|
- dialogWrapperEl.id = `bytm-${this.id}-dialog`;
|
|
|
- dialogWrapperEl.classList.add("bytm-dialog");
|
|
|
- dialogWrapperEl.ariaLabel = dialogWrapperEl.title = "";
|
|
|
- //#SECTION header
|
|
|
- const headerWrapperEl = document.createElement("div");
|
|
|
- headerWrapperEl.classList.add("bytm-dialog-header");
|
|
|
- this.options.smallMenu && headerWrapperEl.classList.add("small");
|
|
|
- if (header) {
|
|
|
- const headerTitleWrapperEl = document.createElement("div");
|
|
|
- headerTitleWrapperEl.classList.add("bytm-dialog-title-wrapper");
|
|
|
- headerTitleWrapperEl.role = "heading";
|
|
|
- headerTitleWrapperEl.ariaLevel = "1";
|
|
|
- headerTitleWrapperEl.appendChild(header);
|
|
|
- headerWrapperEl.appendChild(headerTitleWrapperEl);
|
|
|
- }
|
|
|
- else
|
|
|
- headerWrapperEl.appendChild(document.createElement("div"));
|
|
|
- if (this.options.closeBtnEnabled) {
|
|
|
- const closeBtnEl = document.createElement("img");
|
|
|
- closeBtnEl.classList.add("bytm-dialog-close");
|
|
|
- this.options.smallMenu && closeBtnEl.classList.add("small");
|
|
|
- closeBtnEl.src = yield getResourceUrl("img-close");
|
|
|
- closeBtnEl.role = "button";
|
|
|
- closeBtnEl.tabIndex = 0;
|
|
|
- closeBtnEl.addEventListener("click", () => this.close());
|
|
|
- headerWrapperEl.appendChild(closeBtnEl);
|
|
|
+ if (initializedLocales.has(locale))
|
|
|
+ return;
|
|
|
+ initializedLocales.add(locale);
|
|
|
+ try {
|
|
|
+ const transUrl = yield getResourceUrl(`trans-${locale}`);
|
|
|
+ const transFile = yield (yield UserUtils.fetchAdvanced(transUrl, fetchOpts)).json();
|
|
|
+ // merge with base translations if specified
|
|
|
+ const baseTransUrl = transFile.base ? yield getResourceUrl(`trans-${transFile.base}`) : undefined;
|
|
|
+ const baseTransFile = baseTransUrl ? yield (yield UserUtils.fetchAdvanced(baseTransUrl, fetchOpts)).json() : undefined;
|
|
|
+ const translations = Object.assign(Object.assign({}, ((_a = baseTransFile === null || baseTransFile === void 0 ? void 0 : baseTransFile.translations) !== null && _a !== void 0 ? _a : {})), transFile.translations);
|
|
|
+ UserUtils.tr.addLanguage(locale, translations);
|
|
|
+ allTrKeys.set(locale, new Set(Object.keys(translations)));
|
|
|
+ info(`Loaded translations for locale '${locale}'`);
|
|
|
}
|
|
|
- dialogWrapperEl.appendChild(headerWrapperEl);
|
|
|
- //#SECTION body
|
|
|
- const menuBodyElem = document.createElement("div");
|
|
|
- menuBodyElem.id = `bytm-${this.id}-dialog-body`;
|
|
|
- menuBodyElem.classList.add("bytm-dialog-body");
|
|
|
- this.options.smallMenu && menuBodyElem.classList.add("small");
|
|
|
- menuBodyElem.appendChild(this.options.renderBody());
|
|
|
- dialogWrapperEl.appendChild(menuBodyElem);
|
|
|
- //#SECTION footer
|
|
|
- if (footer) {
|
|
|
- const footerWrapper = document.createElement("div");
|
|
|
- footerWrapper.classList.add("bytm-dialog-footer-cont");
|
|
|
- dialogWrapperEl.appendChild(footerWrapper);
|
|
|
- footerWrapper.appendChild(footer);
|
|
|
+ catch (err) {
|
|
|
+ const errStr = `Couldn't load translations for locale '${locale}'`;
|
|
|
+ error(errStr, err);
|
|
|
+ throw new Error(errStr);
|
|
|
}
|
|
|
- return dialogWrapperEl;
|
|
|
});
|
|
|
}
|
|
|
-}//#SECTION video time
|
|
|
-const videoSelector = getDomain() === "ytm" ? "ytmusic-player video" : "#content ytd-player video";
|
|
|
-/**
|
|
|
- * Returns the current video time in seconds
|
|
|
- * Dispatches mouse movement events in case the video time can't be read from the video or progress bar elements (needs a prior user interaction to work)
|
|
|
- * @returns Returns null if the video time is unavailable or no user interaction has happened prior to calling in case of the fallback behavior being used
|
|
|
- */
|
|
|
-function getVideoTime() {
|
|
|
- return new Promise((res) => {
|
|
|
- const domain = getDomain();
|
|
|
- try {
|
|
|
- if (domain === "ytm") {
|
|
|
- const vidElem = document.querySelector(videoSelector);
|
|
|
- if (vidElem)
|
|
|
- return res(Math.floor(vidElem.currentTime));
|
|
|
- onSelectorOld("tp-yt-paper-slider#progress-bar tp-yt-paper-progress#sliderBar", {
|
|
|
- listener: (pbEl) => res(!isNaN(Number(pbEl.value)) ? Math.floor(Number(pbEl.value)) : null)
|
|
|
- });
|
|
|
- }
|
|
|
- else if (domain === "yt") {
|
|
|
- const vidElem = document.querySelector(videoSelector);
|
|
|
- if (vidElem)
|
|
|
- return res(Math.floor(vidElem.currentTime));
|
|
|
- // YT doesn't update the progress bar when it's hidden (contrary to YTM which never hides it)
|
|
|
- ytForceShowVideoTime();
|
|
|
- const pbSelector = ".ytp-chrome-bottom div.ytp-progress-bar[role=\"slider\"]";
|
|
|
- let videoTime = -1;
|
|
|
- const mut = new MutationObserver(() => {
|
|
|
- // .observe() is only called when the element exists - no need to check for null
|
|
|
- videoTime = Number(document.querySelector(pbSelector).getAttribute("aria-valuenow"));
|
|
|
- });
|
|
|
- const observe = (progElem) => {
|
|
|
- mut.observe(progElem, {
|
|
|
- attributes: true,
|
|
|
- attributeFilter: ["aria-valuenow"],
|
|
|
- });
|
|
|
- if (videoTime >= 0 && !isNaN(videoTime)) {
|
|
|
- res(Math.floor(videoTime));
|
|
|
- mut.disconnect();
|
|
|
- }
|
|
|
- else
|
|
|
- setTimeout(() => {
|
|
|
- res(videoTime >= 0 && !isNaN(videoTime) ? Math.floor(videoTime) : null);
|
|
|
- mut.disconnect();
|
|
|
- }, 500);
|
|
|
- };
|
|
|
- onSelectorOld(pbSelector, { listener: observe });
|
|
|
- }
|
|
|
- }
|
|
|
- catch (err) {
|
|
|
- error("Couldn't get video time due to error:", err);
|
|
|
- res(null);
|
|
|
- }
|
|
|
- });
|
|
|
-}
|
|
|
-/**
|
|
|
- * Sends events that force the video controls to become visible for about 3 seconds.
|
|
|
- * This only works once (for some reason), then the page needs to be reloaded!
|
|
|
- */
|
|
|
-function ytForceShowVideoTime() {
|
|
|
- const player = document.querySelector("#movie_player");
|
|
|
- if (!player)
|
|
|
- return false;
|
|
|
- const defaultProps = {
|
|
|
- // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
|
|
|
- view: UserUtils.getUnsafeWindow(),
|
|
|
- bubbles: true,
|
|
|
- cancelable: false,
|
|
|
- };
|
|
|
- player.dispatchEvent(new MouseEvent("mouseenter", defaultProps));
|
|
|
- const { x, y, width, height } = player.getBoundingClientRect();
|
|
|
- const screenY = Math.round(y + height / 2);
|
|
|
- const screenX = x + Math.min(50, Math.round(width / 3));
|
|
|
- player.dispatchEvent(new MouseEvent("mousemove", Object.assign(Object.assign({}, defaultProps), { screenY,
|
|
|
- screenX, movementX: 5, movementY: 0 })));
|
|
|
- return true;
|
|
|
-}
|
|
|
-/** Removes all child nodes of an element without invoking the slow-ish HTML parser */
|
|
|
-function clearInner(element) {
|
|
|
- while (element.hasChildNodes())
|
|
|
- clearNode(element.firstChild);
|
|
|
-}
|
|
|
-function clearNode(element) {
|
|
|
- while (element.hasChildNodes())
|
|
|
- clearNode(element.firstChild);
|
|
|
- element.parentNode.removeChild(element);
|
|
|
-}let curLogLevel = LogLevel.Info;
|
|
|
-/** Common prefix to be able to tell logged messages apart and filter them in devtools */
|
|
|
-const consPrefix = `[${scriptInfo.name}]`;
|
|
|
-`[${scriptInfo.name}/#DEBUG]`;
|
|
|
-/** Sets the current log level. 0 = Debug, 1 = Info */
|
|
|
-function setLogLevel(level) {
|
|
|
- if (curLogLevel !== level)
|
|
|
- console.log(consPrefix, "Setting log level to", level === 0 ? "Debug" : "Info");
|
|
|
- curLogLevel = level;
|
|
|
- setGlobalProp("logLevel", level);
|
|
|
-}
|
|
|
-/** Extracts the log level from the last item from spread arguments - returns 0 if the last item is not a number or too low or high */
|
|
|
-function getLogLevel(args) {
|
|
|
- const minLogLvl = 0, maxLogLvl = 1;
|
|
|
- if (typeof args.at(-1) === "number")
|
|
|
- return UserUtils.clamp(args.splice(args.length - 1)[0], minLogLvl, maxLogLvl);
|
|
|
- return LogLevel.Debug;
|
|
|
-}
|
|
|
-/**
|
|
|
- * Logs all passed values to the console, as long as the log level is sufficient.
|
|
|
- * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
|
|
|
- */
|
|
|
-function log(...args) {
|
|
|
- if (curLogLevel <= getLogLevel(args))
|
|
|
- console.log(consPrefix, ...args);
|
|
|
-}
|
|
|
-/**
|
|
|
- * Logs all passed values to the console as info, as long as the log level is sufficient.
|
|
|
- * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
|
|
|
- */
|
|
|
-function info(...args) {
|
|
|
- if (curLogLevel <= getLogLevel(args))
|
|
|
- console.info(consPrefix, ...args);
|
|
|
-}
|
|
|
-/** Logs all passed values to the console as a warning, no matter the log level. */
|
|
|
-function warn(...args) {
|
|
|
- console.warn(consPrefix, ...args);
|
|
|
-}
|
|
|
-/** Logs all passed values to the console as an error, no matter the log level. */
|
|
|
-function error(...args) {
|
|
|
- console.error(consPrefix, ...args);
|
|
|
-}//#SECTION misc
|
|
|
-/**
|
|
|
- * Returns the current domain as a constant string representation
|
|
|
- * @throws Throws if script runs on an unexpected website
|
|
|
- */
|
|
|
-function getDomain() {
|
|
|
- if (location.hostname.match(/^music\.youtube/))
|
|
|
- return "ytm";
|
|
|
- else if (location.hostname.match(/youtube\./))
|
|
|
- return "yt";
|
|
|
- else
|
|
|
- throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
|
|
|
-}
|
|
|
-/** Returns a pseudo-random ID unique to each session - returns null if sessionStorage is unavailable */
|
|
|
-function getSessionId() {
|
|
|
- try {
|
|
|
- let sesId = window.sessionStorage.getItem("_bytm-session-id");
|
|
|
- if (!sesId)
|
|
|
- window.sessionStorage.setItem("_bytm-session-id", sesId = UserUtils.randomId(8, 36));
|
|
|
- return sesId;
|
|
|
+ /** Sets the current language for translations */
|
|
|
+ function setLocale(locale) {
|
|
|
+ UserUtils.tr.setLanguage(locale);
|
|
|
+ setGlobalProp("locale", locale);
|
|
|
+ emitInterface("bytm:setLocale", { locale });
|
|
|
}
|
|
|
- catch (err) {
|
|
|
- warn("Couldn't get session ID, sessionStorage / cookies might be disabled:", err);
|
|
|
- return null;
|
|
|
+ /** Returns the currently set language */
|
|
|
+ function getLocale() {
|
|
|
+ return UserUtils.tr.getLanguage();
|
|
|
}
|
|
|
-}
|
|
|
-//#SECTION resources
|
|
|
-/**
|
|
|
- * Returns the URL of a resource by its name, as defined in `assets/resources.json`, from GM resource cache - [see GM.getResourceUrl docs](https://wiki.greasespot.net/GM.getResourceUrl)
|
|
|
- * Falls back to a `raw.githubusercontent.com` URL or base64-encoded data URI if the resource is not available in the GM resource cache
|
|
|
- */
|
|
|
-function getResourceUrl(name) {
|
|
|
- var _a;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- let url = yield GM.getResourceUrl(name);
|
|
|
- if (!url || url.length === 0) {
|
|
|
- const resource = (_a = GM.info.script.resources) === null || _a === void 0 ? void 0 : _a[name].url;
|
|
|
- if (typeof resource === "string") {
|
|
|
- const resourceUrl = new URL(resource);
|
|
|
- const resourcePath = resourceUrl.pathname;
|
|
|
- if (resourcePath)
|
|
|
- return `https://raw.githubusercontent.com/${repo}/${branch}${resourcePath}`;
|
|
|
- }
|
|
|
- warn(`Couldn't get blob URL nor external URL for @resource '${name}', trying to use base64-encoded fallback`);
|
|
|
- // @ts-ignore
|
|
|
- url = yield GM.getResourceUrl(name, false);
|
|
|
- }
|
|
|
- return url;
|
|
|
- });
|
|
|
-}
|
|
|
-/**
|
|
|
- * Returns the preferred locale of the user, provided it is supported by the userscript.
|
|
|
- * Prioritizes `navigator.language`, then `navigator.languages`, then `"en_US"` as a fallback.
|
|
|
- */
|
|
|
-function getPreferredLocale() {
|
|
|
- var _a;
|
|
|
- const navLang = navigator.language.replace(/-/g, "_");
|
|
|
- const navLangs = navigator.languages
|
|
|
- .filter(lang => lang.match(/^[a-z]{2}(-|_)[A-Z]$/) !== null)
|
|
|
- .map(lang => lang.replace(/-/g, "_"));
|
|
|
- if (Object.entries(locales).find(([key]) => key === navLang))
|
|
|
- return navLang;
|
|
|
- for (const loc of navLangs) {
|
|
|
- if (Object.entries(locales).find(([key]) => key === loc))
|
|
|
- return loc;
|
|
|
+ /** Returns whether the given translation key exists in the current locale */
|
|
|
+ function hasKey(key) {
|
|
|
+ return hasKeyFor(getLocale(), key);
|
|
|
}
|
|
|
- // if navigator.languages has entries that aren't locale codes in the format xx_XX
|
|
|
- if (navigator.languages.some(lang => lang.match(/^[a-z]{2}$/))) {
|
|
|
- for (const lang of navLangs) {
|
|
|
- const foundLoc = (_a = Object.entries(locales).find(([key]) => key.startsWith(lang))) === null || _a === void 0 ? void 0 : _a[0];
|
|
|
- if (foundLoc)
|
|
|
- return foundLoc;
|
|
|
- }
|
|
|
+ /** Returns whether the given translation key exists in the given locale */
|
|
|
+ function hasKeyFor(locale, key) {
|
|
|
+ var _a, _b;
|
|
|
+ return (_b = (_a = allTrKeys.get(locale)) === null || _a === void 0 ? void 0 : _a.has(key)) !== null && _b !== void 0 ? _b : false;
|
|
|
}
|
|
|
- return "en_US";
|
|
|
-}
|
|
|
-/** Returns the content behind the passed resource identifier to be assigned to an element's innerHTML property */
|
|
|
-function resourceToHTMLString(resource) {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- const resourceUrl = yield getResourceUrl(resource);
|
|
|
- if (!resourceUrl)
|
|
|
- throw new Error(`Couldn't find URL for resource '${resource}'`);
|
|
|
- return yield (yield UserUtils.fetchAdvanced(resourceUrl)).text();
|
|
|
+ /** Returns the translated string for the given key, after optionally inserting values */
|
|
|
+ function t(key, ...values) {
|
|
|
+ return UserUtils.tr(key, ...values);
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Returns the translated string for the given key with an added pluralization identifier based on the passed `num`
|
|
|
+ * Tries to fall back to the non-pluralized syntax if no translation was found
|
|
|
+ */
|
|
|
+ function tp(key, num, ...values) {
|
|
|
+ if (typeof num !== "number")
|
|
|
+ num = num.length;
|
|
|
+ const plNum = num === 1 ? "1" : "n";
|
|
|
+ const trans = t(`${key}-${plNum}`, ...values);
|
|
|
+ if (trans === key)
|
|
|
+ return t(key, ...values);
|
|
|
+ return trans;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** ID of the last opened (top-most) dialog */
|
|
|
+ let lastDialogId = null;
|
|
|
+ /** Creates and manages a modal dialog element */
|
|
|
+ class BytmDialog extends NanoEmitter {
|
|
|
+ constructor(options) {
|
|
|
+ super();
|
|
|
+ Object.defineProperty(this, "options", {
|
|
|
+ enumerable: true,
|
|
|
+ configurable: true,
|
|
|
+ writable: true,
|
|
|
+ value: void 0
|
|
|
+ });
|
|
|
+ Object.defineProperty(this, "id", {
|
|
|
+ enumerable: true,
|
|
|
+ configurable: true,
|
|
|
+ writable: true,
|
|
|
+ value: void 0
|
|
|
+ });
|
|
|
+ Object.defineProperty(this, "dialogOpen", {
|
|
|
+ enumerable: true,
|
|
|
+ configurable: true,
|
|
|
+ writable: true,
|
|
|
+ value: false
|
|
|
+ });
|
|
|
+ Object.defineProperty(this, "dialogRendered", {
|
|
|
+ enumerable: true,
|
|
|
+ configurable: true,
|
|
|
+ writable: true,
|
|
|
+ value: false
|
|
|
+ });
|
|
|
+ Object.defineProperty(this, "listenersAttached", {
|
|
|
+ enumerable: true,
|
|
|
+ configurable: true,
|
|
|
+ writable: true,
|
|
|
+ value: false
|
|
|
+ });
|
|
|
+ this.options = Object.assign({ closeOnBgClick: true, closeOnEscPress: true, closeBtnEnabled: true, destroyOnClose: false, smallHeader: false }, options);
|
|
|
+ this.id = options.id;
|
|
|
}
|
|
|
- catch (err) {
|
|
|
- error("Couldn't get SVG element from resource:", err);
|
|
|
- return null;
|
|
|
+ /** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
|
|
|
+ mount() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (this.dialogRendered)
|
|
|
+ return;
|
|
|
+ this.dialogRendered = true;
|
|
|
+ const bgElem = document.createElement("div");
|
|
|
+ bgElem.id = `bytm-${this.id}-dialog-bg`;
|
|
|
+ bgElem.classList.add("bytm-dialog-bg");
|
|
|
+ if (this.options.closeOnBgClick)
|
|
|
+ bgElem.ariaLabel = bgElem.title = t("close_menu_tooltip");
|
|
|
+ bgElem.style.visibility = "hidden";
|
|
|
+ bgElem.style.display = "none";
|
|
|
+ bgElem.inert = true;
|
|
|
+ bgElem.appendChild(yield this.getDialogContent());
|
|
|
+ document.body.appendChild(bgElem);
|
|
|
+ this.attachListeners(bgElem);
|
|
|
+ UserUtils.addGlobalStyle(`\
|
|
|
+#bytm-${this.id}-dialog-bg {
|
|
|
+ --bytm-dialog-width-max: ${this.options.maxWidth}px;
|
|
|
+ --bytm-dialog-height-max: ${this.options.maxHeight}px;
|
|
|
+}`).id = `bytm-style-dialog-${this.id}`;
|
|
|
+ this.events.emit("render");
|
|
|
+ return bgElem;
|
|
|
+ });
|
|
|
}
|
|
|
- });
|
|
|
-}
|
|
|
-/** Parses a markdown string and turns it into an HTML string - doesn't sanitize against XSS! */
|
|
|
-function parseMarkdown(md) {
|
|
|
- return marked.marked.parse(md, {
|
|
|
- async: true,
|
|
|
- gfm: true,
|
|
|
- });
|
|
|
-}
|
|
|
-/** Returns the content of the changelog markdown file */
|
|
|
-function getChangelogMd() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- return yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("doc-changelog"))).text();
|
|
|
- });
|
|
|
-}const selectorMap = new Map();
|
|
|
-/**
|
|
|
- * Calls the {@linkcode listener} as soon as the {@linkcode selector} exists in the DOM.
|
|
|
- * Listeners are deleted when they are called once, unless `options.continuous` is set.
|
|
|
- * Multiple listeners with the same selector may be registered.
|
|
|
- * @param selector The selector to listen for
|
|
|
- * @param options Used for switching to `querySelectorAll()` and for calling the listener continuously
|
|
|
- * @template TElem The type of element that the listener will return as its argument (defaults to the generic type HTMLElement)
|
|
|
- * @deprecated To be replaced with UserUtils' SelectorObserver class
|
|
|
- */
|
|
|
-function onSelectorOld(selector, options) {
|
|
|
- let selectorMapItems = [];
|
|
|
- if (selectorMap.has(selector))
|
|
|
- selectorMapItems = selectorMap.get(selector);
|
|
|
- // I don't feel like dealing with intersecting types, this should work just fine at runtime
|
|
|
- // @ts-ignore
|
|
|
- selectorMapItems.push(options);
|
|
|
- selectorMap.set(selector, selectorMapItems);
|
|
|
- checkSelectorExists(selector, selectorMapItems);
|
|
|
-}
|
|
|
-function checkSelectorExists(selector, options) {
|
|
|
- const deleteIndices = [];
|
|
|
- options.forEach((option, i) => {
|
|
|
- try {
|
|
|
- const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
|
|
|
- if ((elements !== null && elements instanceof NodeList && elements.length > 0) || elements !== null) {
|
|
|
- // I don't feel like dealing with intersecting types, this should work just fine at runtime
|
|
|
- // @ts-ignore
|
|
|
- option.listener(elements);
|
|
|
- if (!option.continuous)
|
|
|
- deleteIndices.push(i);
|
|
|
+ /** Clears all dialog contents (unmounts them from the DOM) in preparation for a new rendering call */
|
|
|
+ unmount() {
|
|
|
+ var _a;
|
|
|
+ this.dialogRendered = false;
|
|
|
+ const clearSelectors = [
|
|
|
+ `#bytm-${this.id}-dialog-bg`,
|
|
|
+ `#bytm-style-dialog-${this.id}`,
|
|
|
+ ];
|
|
|
+ for (const sel of clearSelectors) {
|
|
|
+ const elem = document.querySelector(sel);
|
|
|
+ (elem === null || elem === void 0 ? void 0 : elem.hasChildNodes()) && clearInner(elem);
|
|
|
+ (_a = document.querySelector(sel)) === null || _a === void 0 ? void 0 : _a.remove();
|
|
|
}
|
|
|
+ this.events.emit("clear");
|
|
|
}
|
|
|
- catch (err) {
|
|
|
- console.error(`Couldn't call listener for selector '${selector}'`, err);
|
|
|
+ /** Clears and then re-renders the dialog */
|
|
|
+ rerender() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ this.unmount();
|
|
|
+ yield this.mount();
|
|
|
+ });
|
|
|
}
|
|
|
- });
|
|
|
- if (deleteIndices.length > 0) {
|
|
|
- const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
|
|
|
- if (newOptsArray.length === 0)
|
|
|
- selectorMap.delete(selector);
|
|
|
- else {
|
|
|
- // once again laziness strikes
|
|
|
- // @ts-ignore
|
|
|
- selectorMap.set(selector, newOptsArray);
|
|
|
+ /**
|
|
|
+ * Opens the dialog - also mounts it if it hasn't been mounted yet
|
|
|
+ * Prevents default action and immediate propagation of the passed event
|
|
|
+ */
|
|
|
+ open(e) {
|
|
|
+ var _a;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ e === null || e === void 0 ? void 0 : e.preventDefault();
|
|
|
+ e === null || e === void 0 ? void 0 : e.stopImmediatePropagation();
|
|
|
+ if (this.isOpen())
|
|
|
+ return;
|
|
|
+ this.dialogOpen = true;
|
|
|
+ if (!this.isRendered())
|
|
|
+ yield this.mount();
|
|
|
+ document.body.classList.add("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
+ const dialogBg = document.querySelector(`#bytm-${this.id}-dialog-bg`);
|
|
|
+ if (!dialogBg)
|
|
|
+ return warn(`Couldn't find background element for dialog with ID '${this.id}'`);
|
|
|
+ dialogBg.style.visibility = "visible";
|
|
|
+ dialogBg.style.display = "block";
|
|
|
+ dialogBg.inert = false;
|
|
|
+ lastDialogId = this.id;
|
|
|
+ this.events.emit("open");
|
|
|
+ return dialogBg;
|
|
|
+ });
|
|
|
}
|
|
|
- }
|
|
|
-}
|
|
|
-/**
|
|
|
- * Initializes a MutationObserver that checks for all registered selectors whenever an element is added to or removed from the `<body>`
|
|
|
- * @param options For fine-tuning what triggers the MutationObserver's checking function - `subtree` and `childList` are set to true by default
|
|
|
- */
|
|
|
-function initOnSelector(options = {}) {
|
|
|
- const observer = new MutationObserver(() => {
|
|
|
- for (const [selector, options] of selectorMap.entries())
|
|
|
- checkSelectorExists(selector, options);
|
|
|
- });
|
|
|
- observer.observe(document.body, Object.assign({ subtree: true, childList: true }, options));
|
|
|
-}/**
|
|
|
- * Constructs a URL from a base URL and a record of query parameters.
|
|
|
- * If a value is null, the parameter will be valueless.
|
|
|
- * All values will be stringified using their `toString()` method and then URI-encoded.
|
|
|
- * @returns Returns a string instead of a URL object
|
|
|
- */
|
|
|
-function constructUrlString(baseUrl, params) {
|
|
|
- return `${baseUrl}?${Object.entries(params).map(([key, val]) => `${key}${val === null ? "" : `=${encodeURIComponent(String(val))}`}`).join("&")}`;
|
|
|
-}
|
|
|
-/**
|
|
|
- * Sends a request with the specified parameters and returns the response as a Promise.
|
|
|
- * Ignores the CORS policy, contrary to fetch and fetchAdvanced.
|
|
|
- */
|
|
|
-function sendRequest(details) {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- GM.xmlHttpRequest(Object.assign(Object.assign({}, details), { onload: resolve, onerror: reject, ontimeout: reject, onabort: reject }));
|
|
|
- });
|
|
|
-}//#MARKER menu
|
|
|
-let isWelcomeMenuOpen = false;
|
|
|
-/** Adds the welcome menu to the DOM */
|
|
|
-function addWelcomeMenu() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- //#SECTION backdrop & menu container
|
|
|
- const backgroundElem = document.createElement("div");
|
|
|
- backgroundElem.id = "bytm-welcome-menu-bg";
|
|
|
- backgroundElem.classList.add("bytm-menu-bg");
|
|
|
- backgroundElem.style.visibility = "hidden";
|
|
|
- backgroundElem.style.display = "none";
|
|
|
- const menuContainer = document.createElement("div");
|
|
|
- menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
- menuContainer.classList.add("bytm-menu");
|
|
|
- menuContainer.id = "bytm-welcome-menu";
|
|
|
- //#SECTION title bar
|
|
|
- const headerElem = document.createElement("div");
|
|
|
- headerElem.classList.add("bytm-menu-header");
|
|
|
- const titleWrapperElem = document.createElement("div");
|
|
|
- titleWrapperElem.id = "bytm-welcome-menu-title-wrapper";
|
|
|
- const titleLogoElem = document.createElement("img");
|
|
|
- titleLogoElem.id = "bytm-welcome-menu-title-logo";
|
|
|
- titleLogoElem.classList.add("bytm-no-select");
|
|
|
- titleLogoElem.src = yield getResourceUrl("img-logo");
|
|
|
- const titleElem = document.createElement("h2");
|
|
|
- titleElem.id = "bytm-welcome-menu-title";
|
|
|
- titleElem.className = "bytm-menu-title";
|
|
|
- titleElem.role = "heading";
|
|
|
- titleElem.ariaLevel = "1";
|
|
|
- titleWrapperElem.appendChild(titleLogoElem);
|
|
|
- titleWrapperElem.appendChild(titleElem);
|
|
|
- headerElem.appendChild(titleWrapperElem);
|
|
|
- //#SECTION footer
|
|
|
- const footerCont = document.createElement("div");
|
|
|
- footerCont.id = "bytm-welcome-menu-footer-cont";
|
|
|
- footerCont.className = "bytm-menu-footer-cont";
|
|
|
- const openCfgElem = document.createElement("button");
|
|
|
- openCfgElem.id = "bytm-welcome-menu-open-cfg";
|
|
|
- openCfgElem.classList.add("bytm-btn");
|
|
|
- openCfgElem.addEventListener("click", () => {
|
|
|
- closeWelcomeMenu();
|
|
|
- openCfgMenu();
|
|
|
- });
|
|
|
- const openChangelogElem = document.createElement("button");
|
|
|
- openChangelogElem.id = "bytm-welcome-menu-open-changelog";
|
|
|
- openChangelogElem.classList.add("bytm-btn");
|
|
|
- openChangelogElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- yield openChangelogMenu("exit");
|
|
|
- closeWelcomeMenu();
|
|
|
- }));
|
|
|
- const closeBtnElem = document.createElement("button");
|
|
|
- closeBtnElem.id = "bytm-welcome-menu-footer-close";
|
|
|
- closeBtnElem.classList.add("bytm-btn");
|
|
|
- closeBtnElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- closeWelcomeMenu();
|
|
|
- }));
|
|
|
- const leftButtonsCont = document.createElement("div");
|
|
|
- leftButtonsCont.id = "bytm-menu-footer-left-buttons-cont";
|
|
|
- leftButtonsCont.appendChild(openCfgElem);
|
|
|
- leftButtonsCont.appendChild(openChangelogElem);
|
|
|
- footerCont.appendChild(leftButtonsCont);
|
|
|
- footerCont.appendChild(closeBtnElem);
|
|
|
- //#SECTION content
|
|
|
- const contentWrapper = document.createElement("div");
|
|
|
- contentWrapper.id = "bytm-welcome-menu-content-wrapper";
|
|
|
- // locale switcher
|
|
|
- const localeCont = document.createElement("div");
|
|
|
- localeCont.id = "bytm-welcome-menu-locale-cont";
|
|
|
- const localeImg = document.createElement("img");
|
|
|
- localeImg.id = "bytm-welcome-menu-locale-img";
|
|
|
- localeImg.classList.add("bytm-no-select");
|
|
|
- localeImg.src = yield getResourceUrl("img-globe");
|
|
|
- const localeSelectElem = document.createElement("select");
|
|
|
- localeSelectElem.id = "bytm-welcome-menu-locale-select";
|
|
|
- for (const [locale, { name }] of Object.entries(locales)) {
|
|
|
- const localeOptionElem = document.createElement("option");
|
|
|
- localeOptionElem.value = locale;
|
|
|
- localeOptionElem.textContent = name;
|
|
|
- localeSelectElem.appendChild(localeOptionElem);
|
|
|
+ /** Closes the dialog - prevents default action and immediate propagation of the passed event */
|
|
|
+ close(e) {
|
|
|
+ var _a;
|
|
|
+ e === null || e === void 0 ? void 0 : e.preventDefault();
|
|
|
+ e === null || e === void 0 ? void 0 : e.stopImmediatePropagation();
|
|
|
+ if (!this.isOpen())
|
|
|
+ return;
|
|
|
+ this.dialogOpen = false;
|
|
|
+ document.body.classList.remove("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
|
|
|
+ const dialogBg = document.querySelector(`#bytm-${this.id}-dialog-bg`);
|
|
|
+ if (!dialogBg)
|
|
|
+ return warn(`Couldn't find background element for dialog with ID '${this.id}'`);
|
|
|
+ dialogBg.style.visibility = "hidden";
|
|
|
+ dialogBg.style.display = "none";
|
|
|
+ dialogBg.inert = true;
|
|
|
+ if (BytmDialog.getLastDialogId() === this.id)
|
|
|
+ lastDialogId = null;
|
|
|
+ this.events.emit("close");
|
|
|
+ if (this.options.destroyOnClose)
|
|
|
+ this.destroy();
|
|
|
}
|
|
|
- localeSelectElem.value = getFeatures().locale;
|
|
|
- localeSelectElem.addEventListener("change", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const selectedLocale = localeSelectElem.value;
|
|
|
- const feats = Object.assign({}, getFeatures());
|
|
|
- feats.locale = selectedLocale;
|
|
|
- saveFeatures(feats);
|
|
|
- yield initTranslations(selectedLocale);
|
|
|
- setLocale(selectedLocale);
|
|
|
- retranslateWelcomeMenu();
|
|
|
- }));
|
|
|
- localeCont.appendChild(localeImg);
|
|
|
- localeCont.appendChild(localeSelectElem);
|
|
|
- contentWrapper.appendChild(localeCont);
|
|
|
- // text
|
|
|
- const textCont = document.createElement("div");
|
|
|
- textCont.id = "bytm-welcome-menu-text-cont";
|
|
|
- const textElem = document.createElement("p");
|
|
|
- textElem.id = "bytm-welcome-menu-text";
|
|
|
- const textElems = [];
|
|
|
- const line1Elem = document.createElement("span");
|
|
|
- line1Elem.id = "bytm-welcome-text-line1";
|
|
|
- textElems.push(line1Elem);
|
|
|
- const br1Elem = document.createElement("br");
|
|
|
- textElems.push(br1Elem);
|
|
|
- const line2Elem = document.createElement("span");
|
|
|
- line2Elem.id = "bytm-welcome-text-line2";
|
|
|
- textElems.push(line2Elem);
|
|
|
- const br2Elem = document.createElement("br");
|
|
|
- textElems.push(br2Elem);
|
|
|
- const br3Elem = document.createElement("br");
|
|
|
- textElems.push(br3Elem);
|
|
|
- const line3Elem = document.createElement("span");
|
|
|
- line3Elem.id = "bytm-welcome-text-line3";
|
|
|
- textElems.push(line3Elem);
|
|
|
- const br4Elem = document.createElement("br");
|
|
|
- textElems.push(br4Elem);
|
|
|
- const line4Elem = document.createElement("span");
|
|
|
- line4Elem.id = "bytm-welcome-text-line4";
|
|
|
- textElems.push(line4Elem);
|
|
|
- const br5Elem = document.createElement("br");
|
|
|
- textElems.push(br5Elem);
|
|
|
- const br6Elem = document.createElement("br");
|
|
|
- textElems.push(br6Elem);
|
|
|
- const line5Elem = document.createElement("span");
|
|
|
- line5Elem.id = "bytm-welcome-text-line5";
|
|
|
- textElems.push(line5Elem);
|
|
|
- textElems.forEach((elem) => textElem.appendChild(elem));
|
|
|
- textCont.appendChild(textElem);
|
|
|
- contentWrapper.appendChild(textCont);
|
|
|
- //#SECTION finalize
|
|
|
- menuContainer.appendChild(headerElem);
|
|
|
- menuContainer.appendChild(contentWrapper);
|
|
|
- menuContainer.appendChild(footerCont);
|
|
|
- backgroundElem.appendChild(menuContainer);
|
|
|
- document.body.appendChild(backgroundElem);
|
|
|
- retranslateWelcomeMenu();
|
|
|
- });
|
|
|
-}
|
|
|
-//#MARKER (re-)translate
|
|
|
-/** Retranslates all elements inside the welcome menu */
|
|
|
-function retranslateWelcomeMenu() {
|
|
|
- const getLink = (href) => {
|
|
|
- return [`<a href="${href}" class="bytm-link" target="_blank" rel="noopener noreferrer">`, "</a>"];
|
|
|
- };
|
|
|
- const changes = {
|
|
|
- "#bytm-welcome-menu-title": (e) => e.textContent = t("welcome_menu_title", scriptInfo.name),
|
|
|
- "#bytm-welcome-menu-title-close": (e) => e.ariaLabel = e.title = t("close_menu_tooltip"),
|
|
|
- "#bytm-welcome-menu-open-cfg": (e) => {
|
|
|
- e.textContent = t("config_menu");
|
|
|
- e.ariaLabel = e.title = t("open_config_menu_tooltip");
|
|
|
- },
|
|
|
- "#bytm-welcome-menu-open-changelog": (e) => {
|
|
|
- e.textContent = t("open_changelog");
|
|
|
- e.ariaLabel = e.title = t("open_changelog_tooltip");
|
|
|
- },
|
|
|
- "#bytm-welcome-menu-footer-close": (e) => {
|
|
|
- e.textContent = t("close");
|
|
|
- e.ariaLabel = e.title = t("close_menu_tooltip");
|
|
|
- },
|
|
|
- "#bytm-welcome-text-line1": (e) => e.innerHTML = t("welcome_text_line_1"),
|
|
|
- "#bytm-welcome-text-line2": (e) => e.innerHTML = t("welcome_text_line_2", scriptInfo.name),
|
|
|
- "#bytm-welcome-text-line3": (e) => e.innerHTML = t("welcome_text_line_3", scriptInfo.name, ...getLink(`${pkg.hosts.greasyfork}/feedback`), ...getLink(pkg.hosts.openuserjs)),
|
|
|
- "#bytm-welcome-text-line4": (e) => e.innerHTML = t("welcome_text_line_4", ...getLink(pkg.funding.url)),
|
|
|
- "#bytm-welcome-text-line5": (e) => e.innerHTML = t("welcome_text_line_5", ...getLink(pkg.bugs.url)),
|
|
|
- };
|
|
|
- for (const [selector, cb] of Object.entries(changes)) {
|
|
|
- const elem = document.querySelector(selector);
|
|
|
- if (!elem) {
|
|
|
- warn(`Couldn't find element ${selector} in welcome menu`);
|
|
|
- continue;
|
|
|
+ /** Returns true if the dialog is open */
|
|
|
+ isOpen() {
|
|
|
+ return this.dialogOpen;
|
|
|
}
|
|
|
- cb(elem);
|
|
|
- }
|
|
|
-}
|
|
|
-/** Closes the welcome menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
-function closeWelcomeMenu(evt) {
|
|
|
- var _a;
|
|
|
- if (!isWelcomeMenuOpen)
|
|
|
- return;
|
|
|
- isWelcomeMenuOpen = false;
|
|
|
- (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
- document.body.classList.remove("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
|
|
|
- const menuBg = document.querySelector("#bytm-welcome-menu-bg");
|
|
|
- siteEvents.emit("welcomeMenuClosed");
|
|
|
- if (!menuBg)
|
|
|
- return warn("Couldn't find welcome menu background element");
|
|
|
- menuBg.style.visibility = "hidden";
|
|
|
- menuBg.style.display = "none";
|
|
|
-}
|
|
|
-//#MARKER open, show & close
|
|
|
-/** Opens the welcome menu if it is closed */
|
|
|
-function openWelcomeMenu() {
|
|
|
- var _a;
|
|
|
- if (isWelcomeMenuOpen)
|
|
|
- return;
|
|
|
- isWelcomeMenuOpen = true;
|
|
|
- document.body.classList.add("bytm-disable-scroll");
|
|
|
- (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
- const menuBg = document.querySelector("#bytm-welcome-menu-bg");
|
|
|
- if (!menuBg)
|
|
|
- return warn("Couldn't find welcome menu background element");
|
|
|
- menuBg.style.visibility = "visible";
|
|
|
- menuBg.style.display = "block";
|
|
|
-}
|
|
|
-/** Shows the welcome menu and returns a promise that resolves when the menu is closed */
|
|
|
-function showWelcomeMenu() {
|
|
|
- return new Promise((resolve) => {
|
|
|
- const unsub = siteEvents.on("welcomeMenuClosed", () => {
|
|
|
- unsub();
|
|
|
- resolve();
|
|
|
- });
|
|
|
- openWelcomeMenu();
|
|
|
- });
|
|
|
-}{
|
|
|
- // console watermark with sexy gradient
|
|
|
- const styleGradient = "background: rgba(165, 38, 38, 1); background: linear-gradient(90deg, rgb(154, 31, 103) 0%, rgb(135, 31, 31) 40%, rgb(184, 64, 41) 100%);";
|
|
|
- const styleCommon = "color: #fff; font-size: 1.5em; padding-left: 6px; padding-right: 6px;";
|
|
|
- console.log();
|
|
|
- console.log(`%c${scriptInfo.name}%cv${scriptInfo.version}%c\n\nBuild ${scriptInfo.buildNumber} ─ ${scriptInfo.namespace}`, `font-weight: bold; ${styleCommon} ${styleGradient}`, `background-color: #333; ${styleCommon}`, "padding: initial;");
|
|
|
- console.log([
|
|
|
- "Powered by:",
|
|
|
- "─ Lots of ambition and dedication",
|
|
|
- `─ My song metadata API: ${geniUrlBase}`,
|
|
|
- "─ My userscript utility library: https://github.com/Sv443-Network/UserUtils",
|
|
|
- "─ This tiny event listener library: https://github.com/ai/nanoevents",
|
|
|
- "─ This markdown parser library: https://github.com/markedjs/marked",
|
|
|
- ].join("\n"));
|
|
|
- console.log();
|
|
|
-}
|
|
|
-let domLoaded = false;
|
|
|
-const domain = getDomain();
|
|
|
-/** Stuff that needs to be called ASAP, before anything async happens */
|
|
|
-function preInit() {
|
|
|
- log("Session ID:", getSessionId());
|
|
|
- initInterface();
|
|
|
- setLogLevel(defaultLogLevel);
|
|
|
- if (domain === "ytm")
|
|
|
- initBeforeUnloadHook();
|
|
|
- init();
|
|
|
-}
|
|
|
-function init() {
|
|
|
- var _a, _b;
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- try {
|
|
|
- registerMenuCommands();
|
|
|
+ /** Returns true if the dialog has been rendered */
|
|
|
+ isRendered() {
|
|
|
+ return this.dialogRendered;
|
|
|
}
|
|
|
- catch (e) {
|
|
|
+ /** Clears the dialog and removes all event listeners */
|
|
|
+ destroy() {
|
|
|
+ this.events.emit("destroy");
|
|
|
+ this.unmount();
|
|
|
+ this.unsubscribeAll();
|
|
|
}
|
|
|
- try {
|
|
|
- document.addEventListener("DOMContentLoaded", () => {
|
|
|
- domLoaded = true;
|
|
|
- });
|
|
|
- const features = yield initConfig();
|
|
|
- yield initTranslations((_a = features.locale) !== null && _a !== void 0 ? _a : "en_US");
|
|
|
- setLocale((_b = features.locale) !== null && _b !== void 0 ? _b : "en_US");
|
|
|
- setLogLevel(features.logLevel);
|
|
|
- setLayoutConfig(features);
|
|
|
- setBehaviorConfig(features);
|
|
|
- setInputConfig(features);
|
|
|
- setSongListsConfig(features);
|
|
|
- if (features.disableBeforeUnloadPopup && domain === "ytm")
|
|
|
- disableBeforeUnload();
|
|
|
- if (!domLoaded)
|
|
|
- document.addEventListener("DOMContentLoaded", onDomLoad);
|
|
|
- else
|
|
|
- onDomLoad();
|
|
|
- if (features.rememberSongTime)
|
|
|
- initRememberSongTime();
|
|
|
+ /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
|
|
|
+ static getLastDialogId() {
|
|
|
+ return lastDialogId;
|
|
|
}
|
|
|
- catch (err) {
|
|
|
- error("General Error:", err);
|
|
|
+ /** Called once to attach all generic event listeners */
|
|
|
+ attachListeners(bgElem) {
|
|
|
+ if (this.listenersAttached)
|
|
|
+ return;
|
|
|
+ this.listenersAttached = true;
|
|
|
+ if (this.options.closeOnBgClick) {
|
|
|
+ bgElem.addEventListener("click", (e) => {
|
|
|
+ var _a;
|
|
|
+ if (this.isOpen() && ((_a = e.target) === null || _a === void 0 ? void 0 : _a.id) === `bytm-${this.id}-dialog-bg`)
|
|
|
+ this.close(e);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (this.options.closeOnEscPress) {
|
|
|
+ document.body.addEventListener("keydown", (e) => {
|
|
|
+ if (e.key === "Escape" && this.isOpen() && BytmDialog.getLastDialogId() === this.id)
|
|
|
+ this.close(e);
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
- // init menu separately from features
|
|
|
- try {
|
|
|
- void "TODO(v1.2):";
|
|
|
- // initMenu();
|
|
|
+ getDialogContent() {
|
|
|
+ var _a, _b, _c, _d;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const header = (_b = (_a = this.options).renderHeader) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
|
+ const footer = (_d = (_c = this.options).renderFooter) === null || _d === void 0 ? void 0 : _d.call(_c);
|
|
|
+ const dialogWrapperEl = document.createElement("div");
|
|
|
+ dialogWrapperEl.id = `bytm-${this.id}-dialog`;
|
|
|
+ dialogWrapperEl.classList.add("bytm-dialog");
|
|
|
+ dialogWrapperEl.ariaLabel = dialogWrapperEl.title = "";
|
|
|
+ //#SECTION header
|
|
|
+ const headerWrapperEl = document.createElement("div");
|
|
|
+ headerWrapperEl.classList.add("bytm-dialog-header");
|
|
|
+ this.options.smallMenu && headerWrapperEl.classList.add("small");
|
|
|
+ if (header) {
|
|
|
+ const headerTitleWrapperEl = document.createElement("div");
|
|
|
+ headerTitleWrapperEl.classList.add("bytm-dialog-title-wrapper");
|
|
|
+ headerTitleWrapperEl.role = "heading";
|
|
|
+ headerTitleWrapperEl.ariaLevel = "1";
|
|
|
+ headerTitleWrapperEl.appendChild(header);
|
|
|
+ headerWrapperEl.appendChild(headerTitleWrapperEl);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ headerWrapperEl.appendChild(document.createElement("div"));
|
|
|
+ if (this.options.closeBtnEnabled) {
|
|
|
+ const closeBtnEl = document.createElement("img");
|
|
|
+ closeBtnEl.classList.add("bytm-dialog-close");
|
|
|
+ this.options.smallMenu && closeBtnEl.classList.add("small");
|
|
|
+ closeBtnEl.src = yield getResourceUrl("img-close");
|
|
|
+ closeBtnEl.role = "button";
|
|
|
+ closeBtnEl.tabIndex = 0;
|
|
|
+ closeBtnEl.addEventListener("click", () => this.close());
|
|
|
+ headerWrapperEl.appendChild(closeBtnEl);
|
|
|
+ }
|
|
|
+ dialogWrapperEl.appendChild(headerWrapperEl);
|
|
|
+ //#SECTION body
|
|
|
+ const menuBodyElem = document.createElement("div");
|
|
|
+ menuBodyElem.id = `bytm-${this.id}-dialog-body`;
|
|
|
+ menuBodyElem.classList.add("bytm-dialog-body");
|
|
|
+ this.options.smallMenu && menuBodyElem.classList.add("small");
|
|
|
+ menuBodyElem.appendChild(this.options.renderBody());
|
|
|
+ dialogWrapperEl.appendChild(menuBodyElem);
|
|
|
+ //#SECTION footer
|
|
|
+ if (footer) {
|
|
|
+ const footerWrapper = document.createElement("div");
|
|
|
+ footerWrapper.classList.add("bytm-dialog-footer-cont");
|
|
|
+ dialogWrapperEl.appendChild(footerWrapper);
|
|
|
+ footerWrapper.appendChild(footer);
|
|
|
+ }
|
|
|
+ return dialogWrapperEl;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //#SECTION video time
|
|
|
+ const videoSelector = getDomain() === "ytm" ? "ytmusic-player video" : "#content ytd-player video";
|
|
|
+ /**
|
|
|
+ * Returns the current video time in seconds
|
|
|
+ * Dispatches mouse movement events in case the video time can't be read from the video or progress bar elements (needs a prior user interaction to work)
|
|
|
+ * @returns Returns null if the video time is unavailable or no user interaction has happened prior to calling in case of the fallback behavior being used
|
|
|
+ */
|
|
|
+ function getVideoTime() {
|
|
|
+ return new Promise((res) => {
|
|
|
+ const domain = getDomain();
|
|
|
+ try {
|
|
|
+ if (domain === "ytm") {
|
|
|
+ const vidElem = document.querySelector(videoSelector);
|
|
|
+ if (vidElem)
|
|
|
+ return res(Math.floor(vidElem.currentTime));
|
|
|
+ onSelectorOld("tp-yt-paper-slider#progress-bar tp-yt-paper-progress#sliderBar", {
|
|
|
+ listener: (pbEl) => res(!isNaN(Number(pbEl.value)) ? Math.floor(Number(pbEl.value)) : null)
|
|
|
+ });
|
|
|
+ }
|
|
|
+ else if (domain === "yt") {
|
|
|
+ const vidElem = document.querySelector(videoSelector);
|
|
|
+ if (vidElem)
|
|
|
+ return res(Math.floor(vidElem.currentTime));
|
|
|
+ // YT doesn't update the progress bar when it's hidden (contrary to YTM which never hides it)
|
|
|
+ ytForceShowVideoTime();
|
|
|
+ const pbSelector = ".ytp-chrome-bottom div.ytp-progress-bar[role=\"slider\"]";
|
|
|
+ let videoTime = -1;
|
|
|
+ const mut = new MutationObserver(() => {
|
|
|
+ // .observe() is only called when the element exists - no need to check for null
|
|
|
+ videoTime = Number(document.querySelector(pbSelector).getAttribute("aria-valuenow"));
|
|
|
+ });
|
|
|
+ const observe = (progElem) => {
|
|
|
+ mut.observe(progElem, {
|
|
|
+ attributes: true,
|
|
|
+ attributeFilter: ["aria-valuenow"],
|
|
|
+ });
|
|
|
+ if (videoTime >= 0 && !isNaN(videoTime)) {
|
|
|
+ res(Math.floor(videoTime));
|
|
|
+ mut.disconnect();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ setTimeout(() => {
|
|
|
+ res(videoTime >= 0 && !isNaN(videoTime) ? Math.floor(videoTime) : null);
|
|
|
+ mut.disconnect();
|
|
|
+ }, 500);
|
|
|
+ };
|
|
|
+ onSelectorOld(pbSelector, { listener: observe });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't get video time due to error:", err);
|
|
|
+ res(null);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Sends events that force the video controls to become visible for about 3 seconds.
|
|
|
+ * This only works once (for some reason), then the page needs to be reloaded!
|
|
|
+ */
|
|
|
+ function ytForceShowVideoTime() {
|
|
|
+ const player = document.querySelector("#movie_player");
|
|
|
+ if (!player)
|
|
|
+ return false;
|
|
|
+ const defaultProps = {
|
|
|
+ // needed because otherwise YTM errors out - see https://github.com/Sv443/BetterYTM/issues/18#show_issue
|
|
|
+ view: UserUtils.getUnsafeWindow(),
|
|
|
+ bubbles: true,
|
|
|
+ cancelable: false,
|
|
|
+ };
|
|
|
+ player.dispatchEvent(new MouseEvent("mouseenter", defaultProps));
|
|
|
+ const { x, y, width, height } = player.getBoundingClientRect();
|
|
|
+ const screenY = Math.round(y + height / 2);
|
|
|
+ const screenX = x + Math.min(50, Math.round(width / 3));
|
|
|
+ player.dispatchEvent(new MouseEvent("mousemove", Object.assign(Object.assign({}, defaultProps), { screenY,
|
|
|
+ screenX, movementX: 5, movementY: 0 })));
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ /** Removes all child nodes of an element without invoking the slow-ish HTML parser */
|
|
|
+ function clearInner(element) {
|
|
|
+ while (element.hasChildNodes())
|
|
|
+ clearNode(element.firstChild);
|
|
|
+ }
|
|
|
+ function clearNode(element) {
|
|
|
+ while (element.hasChildNodes())
|
|
|
+ clearNode(element.firstChild);
|
|
|
+ element.parentNode.removeChild(element);
|
|
|
+ }
|
|
|
+
|
|
|
+ let curLogLevel = LogLevel.Info;
|
|
|
+ /** Common prefix to be able to tell logged messages apart and filter them in devtools */
|
|
|
+ const consPrefix = `[${scriptInfo.name}]`;
|
|
|
+`[${scriptInfo.name}/#DEBUG]`;
|
|
|
+ /** Sets the current log level. 0 = Debug, 1 = Info */
|
|
|
+ function setLogLevel(level) {
|
|
|
+ if (curLogLevel !== level)
|
|
|
+ console.log(consPrefix, "Setting log level to", level === 0 ? "Debug" : "Info");
|
|
|
+ curLogLevel = level;
|
|
|
+ setGlobalProp("logLevel", level);
|
|
|
+ }
|
|
|
+ /** Extracts the log level from the last item from spread arguments - returns 0 if the last item is not a number or too low or high */
|
|
|
+ function getLogLevel(args) {
|
|
|
+ const minLogLvl = 0, maxLogLvl = 1;
|
|
|
+ if (typeof args.at(-1) === "number")
|
|
|
+ return UserUtils.clamp(args.splice(args.length - 1)[0], minLogLvl, maxLogLvl);
|
|
|
+ return LogLevel.Debug;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Logs all passed values to the console, as long as the log level is sufficient.
|
|
|
+ * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
|
|
|
+ */
|
|
|
+ function log(...args) {
|
|
|
+ if (curLogLevel <= getLogLevel(args))
|
|
|
+ console.log(consPrefix, ...args);
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Logs all passed values to the console as info, as long as the log level is sufficient.
|
|
|
+ * @param args Last parameter is log level (0 = Debug, 1/undefined = Info) - any number as the last parameter will be stripped out! Convert to string if they shouldn't be.
|
|
|
+ */
|
|
|
+ function info(...args) {
|
|
|
+ if (curLogLevel <= getLogLevel(args))
|
|
|
+ console.info(consPrefix, ...args);
|
|
|
+ }
|
|
|
+ /** Logs all passed values to the console as a warning, no matter the log level. */
|
|
|
+ function warn(...args) {
|
|
|
+ console.warn(consPrefix, ...args);
|
|
|
+ }
|
|
|
+ /** Logs all passed values to the console as an error, no matter the log level. */
|
|
|
+ function error(...args) {
|
|
|
+ console.error(consPrefix, ...args);
|
|
|
+ }
|
|
|
+
|
|
|
+ //#SECTION misc
|
|
|
+ /**
|
|
|
+ * Returns the current domain as a constant string representation
|
|
|
+ * @throws Throws if script runs on an unexpected website
|
|
|
+ */
|
|
|
+ function getDomain() {
|
|
|
+ if (location.hostname.match(/^music\.youtube/))
|
|
|
+ return "ytm";
|
|
|
+ else if (location.hostname.match(/youtube\./))
|
|
|
+ return "yt";
|
|
|
+ else
|
|
|
+ throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
|
|
|
+ }
|
|
|
+ /** Returns a pseudo-random ID unique to each session - returns null if sessionStorage is unavailable */
|
|
|
+ function getSessionId() {
|
|
|
+ try {
|
|
|
+ let sesId = window.sessionStorage.getItem("_bytm-session-id");
|
|
|
+ if (!sesId)
|
|
|
+ window.sessionStorage.setItem("_bytm-session-id", sesId = UserUtils.randomId(8, 36));
|
|
|
+ return sesId;
|
|
|
}
|
|
|
catch (err) {
|
|
|
- error("Couldn't initialize menu:", err);
|
|
|
+ warn("Couldn't get session ID, sessionStorage / cookies might be disabled:", err);
|
|
|
+ return null;
|
|
|
}
|
|
|
- });
|
|
|
-}
|
|
|
-/** Called when the DOM has finished loading and can be queried and altered by the userscript */
|
|
|
-function onDomLoad() {
|
|
|
- return __awaiter(this, void 0, void 0, function* () {
|
|
|
- insertGlobalStyle();
|
|
|
- initObservers();
|
|
|
- initOnSelector();
|
|
|
- const features = getFeatures();
|
|
|
- const ftInit = [];
|
|
|
- yield checkVersion();
|
|
|
- log(`DOM loaded. Initializing features for domain "${domain}"...`);
|
|
|
- try {
|
|
|
- if (domain === "ytm") {
|
|
|
- disableDarkReader();
|
|
|
- ftInit.push(initSiteEvents());
|
|
|
- if (typeof (yield GM.getValue("bytm-installed")) !== "string") {
|
|
|
- // open welcome menu with language selector
|
|
|
- yield addWelcomeMenu();
|
|
|
- info("Showing welcome menu");
|
|
|
- yield showWelcomeMenu();
|
|
|
- yield GM.setValue("bytm-installed", JSON.stringify({ timestamp: Date.now(), version: scriptInfo.version }));
|
|
|
+ }
|
|
|
+ //#SECTION resources
|
|
|
+ /**
|
|
|
+ * Returns the URL of a resource by its name, as defined in `assets/resources.json`, from GM resource cache - [see GM.getResourceUrl docs](https://wiki.greasespot.net/GM.getResourceUrl)
|
|
|
+ * Falls back to a `raw.githubusercontent.com` URL or base64-encoded data URI if the resource is not available in the GM resource cache
|
|
|
+ */
|
|
|
+ function getResourceUrl(name) {
|
|
|
+ var _a;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ let url = yield GM.getResourceUrl(name);
|
|
|
+ if (!url || url.length === 0) {
|
|
|
+ const resource = (_a = GM.info.script.resources) === null || _a === void 0 ? void 0 : _a[name].url;
|
|
|
+ if (typeof resource === "string") {
|
|
|
+ const resourceUrl = new URL(resource);
|
|
|
+ const resourcePath = resourceUrl.pathname;
|
|
|
+ if (resourcePath)
|
|
|
+ return `https://raw.githubusercontent.com/${repo}/${branch}${resourcePath}`;
|
|
|
}
|
|
|
- observers$1.body.addListener("tp-yt-iron-dropdown #contentWrapper ytd-multi-page-menu-renderer #container.menu-container", {
|
|
|
- listener: addConfigMenuOption,
|
|
|
- });
|
|
|
- if (features.arrowKeySupport)
|
|
|
- ftInit.push(initArrowKeySkip());
|
|
|
- if (features.removeUpgradeTab)
|
|
|
- ftInit.push(removeUpgradeTab());
|
|
|
- if (features.watermarkEnabled)
|
|
|
- ftInit.push(addWatermark());
|
|
|
- if (features.geniusLyrics)
|
|
|
- ftInit.push(addMediaCtrlLyricsBtn());
|
|
|
- if (features.deleteFromQueueButton || features.lyricsQueueButton)
|
|
|
- ftInit.push(initQueueButtons());
|
|
|
- if (features.anchorImprovements)
|
|
|
- ftInit.push(addAnchorImprovements());
|
|
|
- if (features.closeToastsTimeout > 0)
|
|
|
- ftInit.push(initAutoCloseToasts());
|
|
|
- if (features.removeShareTrackingParam)
|
|
|
- ftInit.push(removeShareTrackingParam());
|
|
|
- if (features.numKeysSkipToTime)
|
|
|
- ftInit.push(initNumKeysSkip());
|
|
|
- if (features.fixSpacing)
|
|
|
- ftInit.push(fixSpacing());
|
|
|
- if (features.scrollToActiveSongBtn)
|
|
|
- ftInit.push(addScrollToActiveBtn());
|
|
|
- ftInit.push(initVolumeFeatures());
|
|
|
+ warn(`Couldn't get blob URL nor external URL for @resource '${name}', trying to use base64-encoded fallback`);
|
|
|
+ // @ts-ignore
|
|
|
+ url = yield GM.getResourceUrl(name, false);
|
|
|
}
|
|
|
- if (["ytm", "yt"].includes(domain)) {
|
|
|
- if (features.switchBetweenSites)
|
|
|
- ftInit.push(initSiteSwitch(domain));
|
|
|
+ return url;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Returns the preferred locale of the user, provided it is supported by the userscript.
|
|
|
+ * Prioritizes `navigator.language`, then `navigator.languages`, then `"en_US"` as a fallback.
|
|
|
+ */
|
|
|
+ function getPreferredLocale() {
|
|
|
+ var _a;
|
|
|
+ const navLang = navigator.language.replace(/-/g, "_");
|
|
|
+ const navLangs = navigator.languages
|
|
|
+ .filter(lang => lang.match(/^[a-z]{2}(-|_)[A-Z]$/) !== null)
|
|
|
+ .map(lang => lang.replace(/-/g, "_"));
|
|
|
+ if (Object.entries(locales).find(([key]) => key === navLang))
|
|
|
+ return navLang;
|
|
|
+ for (const loc of navLangs) {
|
|
|
+ if (Object.entries(locales).find(([key]) => key === loc))
|
|
|
+ return loc;
|
|
|
+ }
|
|
|
+ // if navigator.languages has entries that aren't locale codes in the format xx_XX
|
|
|
+ if (navigator.languages.some(lang => lang.match(/^[a-z]{2}$/))) {
|
|
|
+ for (const lang of navLangs) {
|
|
|
+ const foundLoc = (_a = Object.entries(locales).find(([key]) => key.startsWith(lang))) === null || _a === void 0 ? void 0 : _a[0];
|
|
|
+ if (foundLoc)
|
|
|
+ return foundLoc;
|
|
|
}
|
|
|
- Promise.allSettled(ftInit).then(() => {
|
|
|
- emitInterface("bytm:ready");
|
|
|
- });
|
|
|
}
|
|
|
- catch (err) {
|
|
|
- error("Feature error:", err);
|
|
|
+ return "en_US";
|
|
|
+ }
|
|
|
+ /** Returns the content behind the passed resource identifier to be assigned to an element's innerHTML property */
|
|
|
+ function resourceToHTMLString(resource) {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ const resourceUrl = yield getResourceUrl(resource);
|
|
|
+ if (!resourceUrl)
|
|
|
+ throw new Error(`Couldn't find URL for resource '${resource}'`);
|
|
|
+ return yield (yield UserUtils.fetchAdvanced(resourceUrl)).text();
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't get SVG element from resource:", err);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Parses a markdown string and turns it into an HTML string - doesn't sanitize against XSS! */
|
|
|
+ function parseMarkdown(md) {
|
|
|
+ return marked.marked.parse(md, {
|
|
|
+ async: true,
|
|
|
+ gfm: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Returns the content of the changelog markdown file */
|
|
|
+ function getChangelogMd() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ return yield (yield UserUtils.fetchAdvanced(yield getResourceUrl("doc-changelog"))).text();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const selectorMap = new Map();
|
|
|
+ /**
|
|
|
+ * Calls the {@linkcode listener} as soon as the {@linkcode selector} exists in the DOM.
|
|
|
+ * Listeners are deleted when they are called once, unless `options.continuous` is set.
|
|
|
+ * Multiple listeners with the same selector may be registered.
|
|
|
+ * @param selector The selector to listen for
|
|
|
+ * @param options Used for switching to `querySelectorAll()` and for calling the listener continuously
|
|
|
+ * @template TElem The type of element that the listener will return as its argument (defaults to the generic type HTMLElement)
|
|
|
+ * @deprecated To be replaced with UserUtils' SelectorObserver class
|
|
|
+ */
|
|
|
+ function onSelectorOld(selector, options) {
|
|
|
+ let selectorMapItems = [];
|
|
|
+ if (selectorMap.has(selector))
|
|
|
+ selectorMapItems = selectorMap.get(selector);
|
|
|
+ // I don't feel like dealing with intersecting types, this should work just fine at runtime
|
|
|
+ // @ts-ignore
|
|
|
+ selectorMapItems.push(options);
|
|
|
+ selectorMap.set(selector, selectorMapItems);
|
|
|
+ checkSelectorExists(selector, selectorMapItems);
|
|
|
+ }
|
|
|
+ function checkSelectorExists(selector, options) {
|
|
|
+ const deleteIndices = [];
|
|
|
+ options.forEach((option, i) => {
|
|
|
+ try {
|
|
|
+ const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
|
|
|
+ if ((elements !== null && elements instanceof NodeList && elements.length > 0) || elements !== null) {
|
|
|
+ // I don't feel like dealing with intersecting types, this should work just fine at runtime
|
|
|
+ // @ts-ignore
|
|
|
+ option.listener(elements);
|
|
|
+ if (!option.continuous)
|
|
|
+ deleteIndices.push(i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ console.error(`Couldn't call listener for selector '${selector}'`, err);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (deleteIndices.length > 0) {
|
|
|
+ const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
|
|
|
+ if (newOptsArray.length === 0)
|
|
|
+ selectorMap.delete(selector);
|
|
|
+ else {
|
|
|
+ // once again laziness strikes
|
|
|
+ // @ts-ignore
|
|
|
+ selectorMap.set(selector, newOptsArray);
|
|
|
+ }
|
|
|
}
|
|
|
- });
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Initializes a MutationObserver that checks for all registered selectors whenever an element is added to or removed from the `<body>`
|
|
|
+ * @param options For fine-tuning what triggers the MutationObserver's checking function - `subtree` and `childList` are set to true by default
|
|
|
+ */
|
|
|
+ function initOnSelector(options = {}) {
|
|
|
+ const observer = new MutationObserver(() => {
|
|
|
+ for (const [selector, options] of selectorMap.entries())
|
|
|
+ checkSelectorExists(selector, options);
|
|
|
+ });
|
|
|
+ observer.observe(document.body, Object.assign({ subtree: true, childList: true }, options));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Constructs a URL from a base URL and a record of query parameters.
|
|
|
+ * If a value is null, the parameter will be valueless.
|
|
|
+ * All values will be stringified using their `toString()` method and then URI-encoded.
|
|
|
+ * @returns Returns a string instead of a URL object
|
|
|
+ */
|
|
|
+ function constructUrlString(baseUrl, params) {
|
|
|
+ return `${baseUrl}?${Object.entries(params).map(([key, val]) => `${key}${val === null ? "" : `=${encodeURIComponent(String(val))}`}`).join("&")}`;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Sends a request with the specified parameters and returns the response as a Promise.
|
|
|
+ * Ignores the CORS policy, contrary to fetch and fetchAdvanced.
|
|
|
+ */
|
|
|
+ function sendRequest(details) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ GM.xmlHttpRequest(Object.assign(Object.assign({}, details), { onload: resolve, onerror: reject, ontimeout: reject, onabort: reject }));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ //#MARKER menu
|
|
|
+ let isWelcomeMenuOpen = false;
|
|
|
+ /** Adds the welcome menu to the DOM */
|
|
|
+ function addWelcomeMenu() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ //#SECTION backdrop & menu container
|
|
|
+ const backgroundElem = document.createElement("div");
|
|
|
+ backgroundElem.id = "bytm-welcome-menu-bg";
|
|
|
+ backgroundElem.classList.add("bytm-menu-bg");
|
|
|
+ backgroundElem.style.visibility = "hidden";
|
|
|
+ backgroundElem.style.display = "none";
|
|
|
+ const menuContainer = document.createElement("div");
|
|
|
+ menuContainer.ariaLabel = menuContainer.title = ""; // prevent bg title from propagating downwards
|
|
|
+ menuContainer.classList.add("bytm-menu");
|
|
|
+ menuContainer.id = "bytm-welcome-menu";
|
|
|
+ //#SECTION title bar
|
|
|
+ const headerElem = document.createElement("div");
|
|
|
+ headerElem.classList.add("bytm-menu-header");
|
|
|
+ const titleWrapperElem = document.createElement("div");
|
|
|
+ titleWrapperElem.id = "bytm-welcome-menu-title-wrapper";
|
|
|
+ const titleLogoElem = document.createElement("img");
|
|
|
+ titleLogoElem.id = "bytm-welcome-menu-title-logo";
|
|
|
+ titleLogoElem.classList.add("bytm-no-select");
|
|
|
+ titleLogoElem.src = yield getResourceUrl("img-logo");
|
|
|
+ const titleElem = document.createElement("h2");
|
|
|
+ titleElem.id = "bytm-welcome-menu-title";
|
|
|
+ titleElem.className = "bytm-menu-title";
|
|
|
+ titleElem.role = "heading";
|
|
|
+ titleElem.ariaLevel = "1";
|
|
|
+ titleWrapperElem.appendChild(titleLogoElem);
|
|
|
+ titleWrapperElem.appendChild(titleElem);
|
|
|
+ headerElem.appendChild(titleWrapperElem);
|
|
|
+ //#SECTION footer
|
|
|
+ const footerCont = document.createElement("div");
|
|
|
+ footerCont.id = "bytm-welcome-menu-footer-cont";
|
|
|
+ footerCont.className = "bytm-menu-footer-cont";
|
|
|
+ const openCfgElem = document.createElement("button");
|
|
|
+ openCfgElem.id = "bytm-welcome-menu-open-cfg";
|
|
|
+ openCfgElem.classList.add("bytm-btn");
|
|
|
+ openCfgElem.addEventListener("click", () => {
|
|
|
+ closeWelcomeMenu();
|
|
|
+ openCfgMenu();
|
|
|
+ });
|
|
|
+ const openChangelogElem = document.createElement("button");
|
|
|
+ openChangelogElem.id = "bytm-welcome-menu-open-changelog";
|
|
|
+ openChangelogElem.classList.add("bytm-btn");
|
|
|
+ openChangelogElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ yield openChangelogMenu("exit");
|
|
|
+ closeWelcomeMenu();
|
|
|
+ }));
|
|
|
+ const closeBtnElem = document.createElement("button");
|
|
|
+ closeBtnElem.id = "bytm-welcome-menu-footer-close";
|
|
|
+ closeBtnElem.classList.add("bytm-btn");
|
|
|
+ closeBtnElem.addEventListener("click", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ closeWelcomeMenu();
|
|
|
+ }));
|
|
|
+ const leftButtonsCont = document.createElement("div");
|
|
|
+ leftButtonsCont.id = "bytm-menu-footer-left-buttons-cont";
|
|
|
+ leftButtonsCont.appendChild(openCfgElem);
|
|
|
+ leftButtonsCont.appendChild(openChangelogElem);
|
|
|
+ footerCont.appendChild(leftButtonsCont);
|
|
|
+ footerCont.appendChild(closeBtnElem);
|
|
|
+ //#SECTION content
|
|
|
+ const contentWrapper = document.createElement("div");
|
|
|
+ contentWrapper.id = "bytm-welcome-menu-content-wrapper";
|
|
|
+ // locale switcher
|
|
|
+ const localeCont = document.createElement("div");
|
|
|
+ localeCont.id = "bytm-welcome-menu-locale-cont";
|
|
|
+ const localeImg = document.createElement("img");
|
|
|
+ localeImg.id = "bytm-welcome-menu-locale-img";
|
|
|
+ localeImg.classList.add("bytm-no-select");
|
|
|
+ localeImg.src = yield getResourceUrl("img-globe");
|
|
|
+ const localeSelectElem = document.createElement("select");
|
|
|
+ localeSelectElem.id = "bytm-welcome-menu-locale-select";
|
|
|
+ for (const [locale, { name }] of Object.entries(locales)) {
|
|
|
+ const localeOptionElem = document.createElement("option");
|
|
|
+ localeOptionElem.value = locale;
|
|
|
+ localeOptionElem.textContent = name;
|
|
|
+ localeSelectElem.appendChild(localeOptionElem);
|
|
|
+ }
|
|
|
+ localeSelectElem.value = getFeatures().locale;
|
|
|
+ localeSelectElem.addEventListener("change", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const selectedLocale = localeSelectElem.value;
|
|
|
+ const feats = Object.assign({}, getFeatures());
|
|
|
+ feats.locale = selectedLocale;
|
|
|
+ saveFeatures(feats);
|
|
|
+ yield initTranslations(selectedLocale);
|
|
|
+ setLocale(selectedLocale);
|
|
|
+ retranslateWelcomeMenu();
|
|
|
+ }));
|
|
|
+ localeCont.appendChild(localeImg);
|
|
|
+ localeCont.appendChild(localeSelectElem);
|
|
|
+ contentWrapper.appendChild(localeCont);
|
|
|
+ // text
|
|
|
+ const textCont = document.createElement("div");
|
|
|
+ textCont.id = "bytm-welcome-menu-text-cont";
|
|
|
+ const textElem = document.createElement("p");
|
|
|
+ textElem.id = "bytm-welcome-menu-text";
|
|
|
+ const textElems = [];
|
|
|
+ const line1Elem = document.createElement("span");
|
|
|
+ line1Elem.id = "bytm-welcome-text-line1";
|
|
|
+ textElems.push(line1Elem);
|
|
|
+ const br1Elem = document.createElement("br");
|
|
|
+ textElems.push(br1Elem);
|
|
|
+ const line2Elem = document.createElement("span");
|
|
|
+ line2Elem.id = "bytm-welcome-text-line2";
|
|
|
+ textElems.push(line2Elem);
|
|
|
+ const br2Elem = document.createElement("br");
|
|
|
+ textElems.push(br2Elem);
|
|
|
+ const br3Elem = document.createElement("br");
|
|
|
+ textElems.push(br3Elem);
|
|
|
+ const line3Elem = document.createElement("span");
|
|
|
+ line3Elem.id = "bytm-welcome-text-line3";
|
|
|
+ textElems.push(line3Elem);
|
|
|
+ const br4Elem = document.createElement("br");
|
|
|
+ textElems.push(br4Elem);
|
|
|
+ const line4Elem = document.createElement("span");
|
|
|
+ line4Elem.id = "bytm-welcome-text-line4";
|
|
|
+ textElems.push(line4Elem);
|
|
|
+ const br5Elem = document.createElement("br");
|
|
|
+ textElems.push(br5Elem);
|
|
|
+ const br6Elem = document.createElement("br");
|
|
|
+ textElems.push(br6Elem);
|
|
|
+ const line5Elem = document.createElement("span");
|
|
|
+ line5Elem.id = "bytm-welcome-text-line5";
|
|
|
+ textElems.push(line5Elem);
|
|
|
+ textElems.forEach((elem) => textElem.appendChild(elem));
|
|
|
+ textCont.appendChild(textElem);
|
|
|
+ contentWrapper.appendChild(textCont);
|
|
|
+ //#SECTION finalize
|
|
|
+ menuContainer.appendChild(headerElem);
|
|
|
+ menuContainer.appendChild(contentWrapper);
|
|
|
+ menuContainer.appendChild(footerCont);
|
|
|
+ backgroundElem.appendChild(menuContainer);
|
|
|
+ document.body.appendChild(backgroundElem);
|
|
|
+ retranslateWelcomeMenu();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ //#MARKER (re-)translate
|
|
|
+ /** Retranslates all elements inside the welcome menu */
|
|
|
+ function retranslateWelcomeMenu() {
|
|
|
+ const getLink = (href) => {
|
|
|
+ return [`<a href="${href}" class="bytm-link" target="_blank" rel="noopener noreferrer">`, "</a>"];
|
|
|
+ };
|
|
|
+ const changes = {
|
|
|
+ "#bytm-welcome-menu-title": (e) => e.textContent = t("welcome_menu_title", scriptInfo.name),
|
|
|
+ "#bytm-welcome-menu-title-close": (e) => e.ariaLabel = e.title = t("close_menu_tooltip"),
|
|
|
+ "#bytm-welcome-menu-open-cfg": (e) => {
|
|
|
+ e.textContent = t("config_menu");
|
|
|
+ e.ariaLabel = e.title = t("open_config_menu_tooltip");
|
|
|
+ },
|
|
|
+ "#bytm-welcome-menu-open-changelog": (e) => {
|
|
|
+ e.textContent = t("open_changelog");
|
|
|
+ e.ariaLabel = e.title = t("open_changelog_tooltip");
|
|
|
+ },
|
|
|
+ "#bytm-welcome-menu-footer-close": (e) => {
|
|
|
+ e.textContent = t("close");
|
|
|
+ e.ariaLabel = e.title = t("close_menu_tooltip");
|
|
|
+ },
|
|
|
+ "#bytm-welcome-text-line1": (e) => e.innerHTML = t("welcome_text_line_1"),
|
|
|
+ "#bytm-welcome-text-line2": (e) => e.innerHTML = t("welcome_text_line_2", scriptInfo.name),
|
|
|
+ "#bytm-welcome-text-line3": (e) => e.innerHTML = t("welcome_text_line_3", scriptInfo.name, ...getLink(`${pkg.hosts.greasyfork}/feedback`), ...getLink(pkg.hosts.openuserjs)),
|
|
|
+ "#bytm-welcome-text-line4": (e) => e.innerHTML = t("welcome_text_line_4", ...getLink(pkg.funding.url)),
|
|
|
+ "#bytm-welcome-text-line5": (e) => e.innerHTML = t("welcome_text_line_5", ...getLink(pkg.bugs.url)),
|
|
|
+ };
|
|
|
+ for (const [selector, cb] of Object.entries(changes)) {
|
|
|
+ const elem = document.querySelector(selector);
|
|
|
+ if (!elem) {
|
|
|
+ warn(`Couldn't find element ${selector} in welcome menu`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ cb(elem);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /** Closes the welcome menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
|
|
|
+ function closeWelcomeMenu(evt) {
|
|
|
+ var _a;
|
|
|
+ if (!isWelcomeMenuOpen)
|
|
|
+ return;
|
|
|
+ isWelcomeMenuOpen = false;
|
|
|
+ (evt === null || evt === void 0 ? void 0 : evt.bubbles) && evt.stopPropagation();
|
|
|
+ document.body.classList.remove("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.removeAttribute("inert");
|
|
|
+ const menuBg = document.querySelector("#bytm-welcome-menu-bg");
|
|
|
+ siteEvents.emit("welcomeMenuClosed");
|
|
|
+ if (!menuBg)
|
|
|
+ return warn("Couldn't find welcome menu background element");
|
|
|
+ menuBg.style.visibility = "hidden";
|
|
|
+ menuBg.style.display = "none";
|
|
|
+ }
|
|
|
+ //#MARKER open, show & close
|
|
|
+ /** Opens the welcome menu if it is closed */
|
|
|
+ function openWelcomeMenu() {
|
|
|
+ var _a;
|
|
|
+ if (isWelcomeMenuOpen)
|
|
|
+ return;
|
|
|
+ isWelcomeMenuOpen = true;
|
|
|
+ document.body.classList.add("bytm-disable-scroll");
|
|
|
+ (_a = document.querySelector("ytmusic-app")) === null || _a === void 0 ? void 0 : _a.setAttribute("inert", "true");
|
|
|
+ const menuBg = document.querySelector("#bytm-welcome-menu-bg");
|
|
|
+ if (!menuBg)
|
|
|
+ return warn("Couldn't find welcome menu background element");
|
|
|
+ menuBg.style.visibility = "visible";
|
|
|
+ menuBg.style.display = "block";
|
|
|
+ }
|
|
|
+ /** Shows the welcome menu and returns a promise that resolves when the menu is closed */
|
|
|
+ function showWelcomeMenu() {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const unsub = siteEvents.on("welcomeMenuClosed", () => {
|
|
|
+ unsub();
|
|
|
+ resolve();
|
|
|
+ });
|
|
|
+ openWelcomeMenu();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ {
|
|
|
+ // console watermark with sexy gradient
|
|
|
+ const styleGradient = "background: rgba(165, 38, 38, 1); background: linear-gradient(90deg, rgb(154, 31, 103) 0%, rgb(135, 31, 31) 40%, rgb(184, 64, 41) 100%);";
|
|
|
+ const styleCommon = "color: #fff; font-size: 1.5em; padding-left: 6px; padding-right: 6px;";
|
|
|
+ console.log();
|
|
|
+ console.log(`%c${scriptInfo.name}%cv${scriptInfo.version}%c\n\nBuild ${scriptInfo.buildNumber} ─ ${scriptInfo.namespace}`, `font-weight: bold; ${styleCommon} ${styleGradient}`, `background-color: #333; ${styleCommon}`, "padding: initial;");
|
|
|
+ console.log([
|
|
|
+ "Powered by:",
|
|
|
+ "─ Lots of ambition and dedication",
|
|
|
+ `─ My song metadata API: ${geniUrlBase}`,
|
|
|
+ "─ My userscript utility library: https://github.com/Sv443-Network/UserUtils",
|
|
|
+ "─ This tiny event listener library: https://github.com/ai/nanoevents",
|
|
|
+ "─ This markdown parser library: https://github.com/markedjs/marked",
|
|
|
+ ].join("\n"));
|
|
|
+ console.log();
|
|
|
+ }
|
|
|
+ let domLoaded = false;
|
|
|
+ const domain = getDomain();
|
|
|
+ /** Stuff that needs to be called ASAP, before anything async happens */
|
|
|
+ function preInit() {
|
|
|
+ log("Session ID:", getSessionId());
|
|
|
+ initInterface();
|
|
|
+ setLogLevel(defaultLogLevel);
|
|
|
+ if (domain === "ytm")
|
|
|
+ initBeforeUnloadHook();
|
|
|
+ init();
|
|
|
+ }
|
|
|
+ function init() {
|
|
|
+ var _a, _b;
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ try {
|
|
|
+ registerMenuCommands();
|
|
|
+ }
|
|
|
+ catch (e) {
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ document.addEventListener("DOMContentLoaded", () => {
|
|
|
+ domLoaded = true;
|
|
|
+ });
|
|
|
+ const features = yield initConfig();
|
|
|
+ yield initTranslations((_a = features.locale) !== null && _a !== void 0 ? _a : "en_US");
|
|
|
+ setLocale((_b = features.locale) !== null && _b !== void 0 ? _b : "en_US");
|
|
|
+ setLogLevel(features.logLevel);
|
|
|
+ setLayoutConfig(features);
|
|
|
+ setBehaviorConfig(features);
|
|
|
+ setInputConfig(features);
|
|
|
+ setSongListsConfig(features);
|
|
|
+ if (features.disableBeforeUnloadPopup && domain === "ytm")
|
|
|
+ disableBeforeUnload();
|
|
|
+ if (!domLoaded)
|
|
|
+ document.addEventListener("DOMContentLoaded", onDomLoad);
|
|
|
+ else
|
|
|
+ onDomLoad();
|
|
|
+ if (features.rememberSongTime)
|
|
|
+ initRememberSongTime();
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("General Error:", err);
|
|
|
+ }
|
|
|
+ // init menu separately from features
|
|
|
+ try {
|
|
|
+ void "TODO(v1.2):";
|
|
|
+ // initMenu();
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Couldn't initialize menu:", err);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Called when the DOM has finished loading and can be queried and altered by the userscript */
|
|
|
+ function onDomLoad() {
|
|
|
+ return __awaiter(this, void 0, void 0, function* () {
|
|
|
+ insertGlobalStyle();
|
|
|
+ initObservers();
|
|
|
+ initOnSelector();
|
|
|
+ const features = getFeatures();
|
|
|
+ const ftInit = [];
|
|
|
+ yield checkVersion();
|
|
|
+ log(`DOM loaded. Initializing features for domain "${domain}"...`);
|
|
|
+ try {
|
|
|
+ if (domain === "ytm") {
|
|
|
+ disableDarkReader();
|
|
|
+ ftInit.push(initSiteEvents());
|
|
|
+ if (typeof (yield GM.getValue("bytm-installed")) !== "string") {
|
|
|
+ // open welcome menu with language selector
|
|
|
+ yield addWelcomeMenu();
|
|
|
+ info("Showing welcome menu");
|
|
|
+ yield showWelcomeMenu();
|
|
|
+ yield GM.setValue("bytm-installed", JSON.stringify({ timestamp: Date.now(), version: scriptInfo.version }));
|
|
|
+ }
|
|
|
+ observers$1.body.addListener("tp-yt-iron-dropdown #contentWrapper ytd-multi-page-menu-renderer #container.menu-container", {
|
|
|
+ listener: addConfigMenuOption,
|
|
|
+ });
|
|
|
+ if (features.arrowKeySupport)
|
|
|
+ ftInit.push(initArrowKeySkip());
|
|
|
+ if (features.removeUpgradeTab)
|
|
|
+ ftInit.push(removeUpgradeTab());
|
|
|
+ if (features.watermarkEnabled)
|
|
|
+ ftInit.push(addWatermark());
|
|
|
+ if (features.geniusLyrics)
|
|
|
+ ftInit.push(addMediaCtrlLyricsBtn());
|
|
|
+ if (features.deleteFromQueueButton || features.lyricsQueueButton)
|
|
|
+ ftInit.push(initQueueButtons());
|
|
|
+ if (features.anchorImprovements)
|
|
|
+ ftInit.push(addAnchorImprovements());
|
|
|
+ if (features.closeToastsTimeout > 0)
|
|
|
+ ftInit.push(initAutoCloseToasts());
|
|
|
+ if (features.removeShareTrackingParam)
|
|
|
+ ftInit.push(removeShareTrackingParam());
|
|
|
+ if (features.numKeysSkipToTime)
|
|
|
+ ftInit.push(initNumKeysSkip());
|
|
|
+ if (features.fixSpacing)
|
|
|
+ ftInit.push(fixSpacing());
|
|
|
+ if (features.scrollToActiveSongBtn)
|
|
|
+ ftInit.push(addScrollToActiveBtn());
|
|
|
+ ftInit.push(initVolumeFeatures());
|
|
|
+ }
|
|
|
+ if (["ytm", "yt"].includes(domain)) {
|
|
|
+ if (features.switchBetweenSites)
|
|
|
+ ftInit.push(initSiteSwitch(domain));
|
|
|
+ }
|
|
|
+ Promise.allSettled(ftInit).then(() => {
|
|
|
+ emitInterface("bytm:ready");
|
|
|
+ });
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ error("Feature error:", err);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ /** Inserts the bundled CSS files imported throughout the script into a <style> element in the <head> */
|
|
|
+ function insertGlobalStyle() {
|
|
|
+ // post-build these double quotes are replaced by backticks (because if backticks are used here, the bundler converts them to double quotes)
|
|
|
+ UserUtils.addGlobalStyle(`.bytm-menu-bg {
|
|
|
+ --bytm-menu-bg: #333333;
|
|
|
+ --bytm-menu-bg-highlight: #252525;
|
|
|
+ --bytm-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
|
|
|
+ --bytm-menu-separator-color: #797979;
|
|
|
+ --bytm-menu-border-radius: 10px;
|
|
|
}
|
|
|
-/** Inserts the bundled CSS files imported throughout the script into a <style> element in the <head> */
|
|
|
-function insertGlobalStyle() {
|
|
|
- // post-build these double quotes are replaced by backticks (because if backticks are used here, the bundler converts them to double quotes)
|
|
|
- UserUtils.addGlobalStyle("#{{GLOBAL_STYLE}}").id = "bytm-style-global";
|
|
|
+
|
|
|
+#bytm-cfg-menu-bg {
|
|
|
+ --bytm-menu-height-max: 750px;
|
|
|
+ --bytm-menu-width-max: 1000px;
|
|
|
}
|
|
|
-function registerMenuCommands() {
|
|
|
- if (mode === "development") {
|
|
|
- GM.registerMenuCommand("Reset config", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (confirm("Reset the configuration to its default values?\nThis will automatically reload the page.")) {
|
|
|
- yield clearConfig();
|
|
|
- disableBeforeUnload();
|
|
|
- location.reload();
|
|
|
- }
|
|
|
- }), "r");
|
|
|
- GM.registerMenuCommand("List GM values", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const keys = yield GM.listValues();
|
|
|
- console.log("GM values:");
|
|
|
- if (keys.length === 0)
|
|
|
- console.log(" No values found.");
|
|
|
- for (const key of keys)
|
|
|
- console.log(` ${key} -> ${yield GM.getValue(key)}`);
|
|
|
- alert("See console.");
|
|
|
- }), "l");
|
|
|
- GM.registerMenuCommand("Delete all GM values", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- if (confirm("Clear all GM values?\nSee console for details.")) {
|
|
|
+
|
|
|
+#bytm-changelog-menu-bg {
|
|
|
+ --bytm-menu-height-max: 800px;
|
|
|
+ --bytm-menu-width-max: 800px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-export-menu-bg, #bytm-import-menu-bg {
|
|
|
+ --bytm-menu-height-max: 500px;
|
|
|
+ --bytm-menu-width-max: 600px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-feat-help-menu-bg {
|
|
|
+ --bytm-menu-height-max: 400px;
|
|
|
+ --bytm-menu-width-max: 600px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-bg {
|
|
|
+ display: block;
|
|
|
+ position: fixed;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ z-index: 5;
|
|
|
+ background-color: rgba(0, 0, 0, 0.6);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu {
|
|
|
+ position: fixed;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: calc(min(100% - 60px, var(--bytm-menu-width-max)));
|
|
|
+ border-radius: var(--bytm-menu-border-radius);
|
|
|
+ height: auto;
|
|
|
+ max-height: calc(min(100% - 40px, var(--bytm-menu-height-max)));
|
|
|
+ left: 50%;
|
|
|
+ top: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ z-index: 6;
|
|
|
+ color: #fff;
|
|
|
+ background-color: var(--bytm-menu-bg);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-body {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-menu-opts {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ position: relative;
|
|
|
+ padding: 30px 0px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ padding: 15px 20px 15px 20px;
|
|
|
+ background-color: var(--bytm-menu-bg);
|
|
|
+ border: 2px solid var(--bytm-menu-separator-color);
|
|
|
+ border-style: none none solid none;
|
|
|
+ border-radius: var(--bytm-menu-border-radius) var(--bytm-menu-border-radius) 0px 0px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-header.small {
|
|
|
+ padding: 10px 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-titlecont {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-titlecont-no-title {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-title {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+ font-size: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-menu-version {
|
|
|
+ position: absolute;
|
|
|
+ width: 100%;
|
|
|
+ bottom: -10px;
|
|
|
+ left: 0;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: normal;
|
|
|
+ z-index: 7;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-menu-version .bytm-link {
|
|
|
+ color: #c6d2db;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-menu-linkscont {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-left: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-link {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-link:not(:last-of-type) {
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-link .bytm-menu-img {
|
|
|
+ position: relative;
|
|
|
+ border-radius: 50%;
|
|
|
+ bottom: 0px;
|
|
|
+ transition: bottom 0.15s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-link:hover .bytm-menu-img {
|
|
|
+ bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-close {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-close.small {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-footer {
|
|
|
+ font-size: 17px;
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-footer.hidden {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-footer-cont {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 6px;
|
|
|
+ padding: 15px 20px;
|
|
|
+ background: var(--bytm-menu-bg);
|
|
|
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bytm-menu-bg) 30%, var(--bytm-menu-bg) 100%);
|
|
|
+ border: 2px solid var(--bytm-menu-separator-color);
|
|
|
+ border-style: solid none none none;
|
|
|
+ border-radius: 0px 0px var(--bytm-menu-border-radius) var(--bytm-menu-border-radius);
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-menu-footer-buttons-cont button:not(:last-of-type) {
|
|
|
+ margin-right: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-footer-right {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row-reverse;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-menu-footer-left-buttons-cont button:not(:last-of-type) {
|
|
|
+ margin-right: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-menu-scroll-indicator {
|
|
|
+ --bytm-scroll-indicator-padding: 5px;
|
|
|
+ position: sticky;
|
|
|
+ bottom: -15px;
|
|
|
+ left: 50%;
|
|
|
+ margin-top: calc(-32px - var(--bytm-scroll-indicator-padding) * 2);
|
|
|
+ padding: var(--bytm-scroll-indicator-padding);
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ z-index: 7;
|
|
|
+ background-color: var(--bytm-scroll-indicator-bg);
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-hidden {
|
|
|
+ visibility: hidden !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-category-header {
|
|
|
+ font-size: 18px;
|
|
|
+ margin-top: 32px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ padding: 0px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-category-header:first-of-type {
|
|
|
+ margin-top: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 1.4rem;
|
|
|
+ padding: 8px 20px;
|
|
|
+ transition: background-color 0.15s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem:hover {
|
|
|
+ background-color: var(--bytm-menu-bg-highlight);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-leftside {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-ctrl {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-left: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-label {
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-slider-label {
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-toggle-label {
|
|
|
+ padding-left: 10px;
|
|
|
+ padding-right: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-input.bytm-hotkey-input {
|
|
|
+ cursor: pointer;
|
|
|
+ min-width: 50px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-input[type=number] {
|
|
|
+ width: 75px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-input[type=checkbox] {
|
|
|
+ margin-left: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-export-menu-text, #bytm-import-menu-text {
|
|
|
+ font-size: 1.6em;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-menu-footer-copied {
|
|
|
+ font-size: 1.6em;
|
|
|
+ margin-right: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-changelog-menu-body {
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-export-menu-textarea, #bytm-import-menu-textarea {
|
|
|
+ width: 100%;
|
|
|
+ height: 150px;
|
|
|
+ resize: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow-y: auto;
|
|
|
+ font-size: 1.5em;
|
|
|
+ line-height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Markdown stuff */
|
|
|
+
|
|
|
+.bytm-markdown-container kbd {
|
|
|
+ --bytm-easing: cubic-bezier(0.31, 0.58, 0.24, 1.15);
|
|
|
+ display: inline-block;
|
|
|
+ vertical-align: bottom;
|
|
|
+ padding: 4px;
|
|
|
+ padding-top: 2px;
|
|
|
+ font-size: 0.95em;
|
|
|
+ line-height: 11px;
|
|
|
+ background-color: #222;
|
|
|
+ border: 1px solid #777;
|
|
|
+ border-radius: 5px;
|
|
|
+ box-shadow: inset 0 -2px 0 #515559;
|
|
|
+ transition: padding 0.1s var(--bytm-easing), box-shadow 0.1s var(--bytm-easing);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container kbd:active {
|
|
|
+ padding-bottom: 2px;
|
|
|
+ box-shadow: inset 0 0 0 initial;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container kbd::selection {
|
|
|
+ background: rgba(0, 0, 0, 0);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container code {
|
|
|
+ background-color: #222;
|
|
|
+ border-radius: 3px;
|
|
|
+ padding: 1px 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container h2 {
|
|
|
+ margin-bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container h2:not(:first-of-type) {
|
|
|
+ margin-top: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container ul li::before {
|
|
|
+ content: "• ";
|
|
|
+ font-weight: bolder;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container ul li > ul li::before {
|
|
|
+ white-space: pre;
|
|
|
+ content: " • ";
|
|
|
+ font-weight: bolder;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-feat-help-menu-desc, #bytm-feat-help-menu-text {
|
|
|
+ overflow-wrap: break-word;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ padding: 10px 10px 15px 20px;
|
|
|
+ font-size: 1.5em;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-feat-help-menu-desc {
|
|
|
+ font-size: 1.65em;
|
|
|
+ padding-bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-help-btn {
|
|
|
+ width: 24px !important;
|
|
|
+ height: 24px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-help-btn svg {
|
|
|
+ width: 18px !important;
|
|
|
+ height: 18px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-help-btn svg > path {
|
|
|
+ fill: #b3bec7 !important;
|
|
|
+}
|
|
|
+
|
|
|
+hr {
|
|
|
+ display: block;
|
|
|
+ margin: 8px 0px 12px 0px;
|
|
|
+ border: revert;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-adornment {
|
|
|
+ display: inline-flex;
|
|
|
+ justify-content: flex-start;
|
|
|
+ align-items: center;
|
|
|
+ margin-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-ftitem-locale-adornment svg path {
|
|
|
+ fill: #4595c7;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-hotkey-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-hotkey-reset {
|
|
|
+ font-size: 0.9em;
|
|
|
+ margin-left: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-hotkey-info {
|
|
|
+ font-size: 0.9em;
|
|
|
+ margin-right: 5px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* #MARKER misc */
|
|
|
+
|
|
|
+.bytm-disable-scroll {
|
|
|
+ overflow: hidden !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-generic-btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ position: relative;
|
|
|
+ vertical-align: middle;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-left: 8px;
|
|
|
+
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+
|
|
|
+ border: 1px solid transparent;
|
|
|
+ border-radius: 100%;
|
|
|
+ background-color: transparent;
|
|
|
+
|
|
|
+ transition: background-color 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-generic-btn:hover {
|
|
|
+ background-color: rgba(255, 255, 255, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-generic-btn:active {
|
|
|
+ background-color: #5f5f5f;
|
|
|
+ animation: flashBorder 0.4s ease 1;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes flashBorder {
|
|
|
+ 0% {
|
|
|
+ border: 1px solid transparent;
|
|
|
+ }
|
|
|
+ 20% {
|
|
|
+ border: 1px solid #727272;
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ border: 1px solid transparent;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-generic-btn-img {
|
|
|
+ display: inline-block;
|
|
|
+ z-index: 10;
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-spinner {
|
|
|
+ animation: rotate 1.2s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes rotate {
|
|
|
+ from {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-anchor {
|
|
|
+ all: unset;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+/* ytmusic-logo a[bytm-animated="true"] .bytm-mod-logo-ellipse {
|
|
|
+ transform-origin: 12px 12px;
|
|
|
+ animation: rotate 1s ease-in-out infinite;
|
|
|
+} */
|
|
|
+
|
|
|
+ytmusic-logo a.bytm-logo-exchanged .bytm-mod-logo-path {
|
|
|
+ transform-origin: 12px 12px;
|
|
|
+ animation: rotate 1s ease-in-out;
|
|
|
+}
|
|
|
+
|
|
|
+ytmusic-logo a.bytm-logo-exchanged .bytm-mod-logo-img {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ z-index: 1000;
|
|
|
+ position: absolute;
|
|
|
+ animation: rotate-fade-in 1s ease-in-out;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes rotate-fade-in {
|
|
|
+ 0% {
|
|
|
+ opacity: 0;
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ 30% {
|
|
|
+ opacity: 0;
|
|
|
+ }
|
|
|
+ 90% {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-no-select {
|
|
|
+ user-select: none;
|
|
|
+ -ms-user-select: none;
|
|
|
+ -moz-user-select: none;
|
|
|
+ -webkit-user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+/* YTM does some weird styling that breaks everything, so this reverts all of BYTM's buttons to the browser default style */
|
|
|
+button.bytm-btn {
|
|
|
+ padding: revert;
|
|
|
+ border: revert;
|
|
|
+ outline: revert;
|
|
|
+ font: revert;
|
|
|
+ text-transform: revert;
|
|
|
+ color: revert;
|
|
|
+ background: revert;
|
|
|
+ line-height: 1.4em;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-link, .bytm-markdown-container a {
|
|
|
+ color: #369bff;
|
|
|
+ text-decoration: none;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-link:hover, .bytm-markdown-container a:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+/* #MARKER menu */
|
|
|
+
|
|
|
+.bytm-cfg-menu-option {
|
|
|
+ display: block;
|
|
|
+ padding: 8px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-cfg-menu-option-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 1.4rem;
|
|
|
+ font-weight: 400;
|
|
|
+ line-height: 24px;
|
|
|
+ padding: var(--yt-compact-link-paper-item-padding, 0px 36px 0 16px);
|
|
|
+ min-height: var(--paper-item-min-height, 40px);
|
|
|
+ white-space: nowrap;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-cfg-menu-option-item:hover {
|
|
|
+ background-color: var(--yt-spec-badge-chip-background, #3e3e3e);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-cfg-menu-option-icon {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ margin-right: 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-direction: row;
|
|
|
+ flex: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-cfg-menu-option-text {
|
|
|
+ font-size: 1.4rem;
|
|
|
+ line-height: 2rem;
|
|
|
+}
|
|
|
+
|
|
|
+yt-multi-page-menu-section-renderer.ytd-multi-page-menu-renderer {
|
|
|
+ border-bottom: 1px solid var(--yt-spec-10-percent-layer, #3e3e3e);
|
|
|
+}
|
|
|
+
|
|
|
+/* #MARKER watermark */
|
|
|
+
|
|
|
+#bytm-watermark {
|
|
|
+ font-size: 10px;
|
|
|
+ display: inline-block;
|
|
|
+ position: absolute;
|
|
|
+ left: 97px;
|
|
|
+ top: 45px;
|
|
|
+ z-index: 10;
|
|
|
+ color: white;
|
|
|
+ text-decoration: none;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-watermark:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+/* #MARKER volume slider */
|
|
|
+
|
|
|
+#bytm-vol-slider-cont {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-vol-slider-label {
|
|
|
+ opacity: 0.000001;
|
|
|
+ position: absolute;
|
|
|
+ font-size: 15px;
|
|
|
+ top: 50%;
|
|
|
+ left: 0;
|
|
|
+ transform: translate(calc(-50% - 10px), -50%);
|
|
|
+ text-align: right;
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-vol-slider-label.bytm-visible {
|
|
|
+ opacity: 1;
|
|
|
+}
|
|
|
+
|
|
|
+/* #MARKER scroll to active */
|
|
|
+
|
|
|
+#bytm-scroll-to-active-btn-cont {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ position: absolute;
|
|
|
+ right: 5px;
|
|
|
+ top: 0;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-scroll-to-active-btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-scroll-to-active-btn {
|
|
|
+ width: revert;
|
|
|
+ height: revert;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-scroll-to-active-btn .bytm-generic-btn-img {
|
|
|
+ padding: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+/* #MARKER queue buttons */
|
|
|
+
|
|
|
+#side-panel ytmusic-player-queue-item .song-info.ytmusic-player-queue-item {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+#side-panel ytmusic-player-queue-item .bytm-queue-btn-container {
|
|
|
+ background: rgb(0, 0, 0);
|
|
|
+ background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #030303 15%);
|
|
|
+ display: none;
|
|
|
+ position: absolute;
|
|
|
+ right: 0;
|
|
|
+ padding-left: 25px;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+#side-panel ytmusic-player-queue-item[selected] .bytm-queue-btn-container {
|
|
|
+ background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #1D1D1D 15%);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-generic-list-queue-btn-container {
|
|
|
+ /* otherwise the queue buttons render over the currently playing song page */
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+#side-panel ytmusic-player-queue-item:hover .bytm-queue-btn-container,
|
|
|
+ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer:hover .bytm-queue-btn-container,
|
|
|
+ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer:hover .bytm-queue-btn-container {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+ytmusic-responsive-list-item-renderer .title-column {
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+#side-panel ytmusic-player-queue-item[play-button-state="loading"] .bytm-queue-btn-container,
|
|
|
+#side-panel ytmusic-player-queue-item[play-button-state="playing"] .bytm-queue-btn-container,
|
|
|
+#side-panel ytmusic-player-queue-item[play-button-state="paused"] .bytm-queue-btn-container {
|
|
|
+ /* using a var() with predefined value from YTM is not viable since the nesting changes the actual value of the variable */
|
|
|
+ background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #030303 15%);
|
|
|
+}
|
|
|
+
|
|
|
+#side-panel ytmusic-player-queue-item[selected][play-button-state="loading"] .bytm-queue-btn-container,
|
|
|
+#side-panel ytmusic-player-queue-item[selected][play-button-state="playing"] .bytm-queue-btn-container,
|
|
|
+#side-panel ytmusic-player-queue-item[selected][play-button-state="paused"] .bytm-queue-btn-container {
|
|
|
+ /* using a var() with predefined value from YTM is not viable since the nesting changes the actual value of the variable */
|
|
|
+ background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #1D1D1D 15%);
|
|
|
+}
|
|
|
+
|
|
|
+ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown[data-bytm-hidden=true] {
|
|
|
+ display: none !important;
|
|
|
+}
|
|
|
+
|
|
|
+ytmusic-responsive-list-item-renderer.bytm-has-queue-btns .bytm-generic-list-queue-btn-container {
|
|
|
+ visibility: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+ytmusic-responsive-list-item-renderer.bytm-has-queue-btns .bytm-generic-list-queue-btn-container a.bytm-generic-btn {
|
|
|
+ visibility: hidden !important;
|
|
|
+}
|
|
|
+
|
|
|
+ytmusic-responsive-list-item-renderer.bytm-has-queue-btns:hover .bytm-generic-list-queue-btn-container {
|
|
|
+ visibility: visible;
|
|
|
+}
|
|
|
+
|
|
|
+ytmusic-responsive-list-item-renderer.bytm-has-queue-btns:hover .bytm-generic-list-queue-btn-container a.bytm-generic-btn {
|
|
|
+ visibility: visible !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-body p {
|
|
|
+ overflow-wrap: break-word;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-body details summary {
|
|
|
+ cursor: pointer;
|
|
|
+ font-style: italic;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-version-notif-dialog-btns {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-disable-update-check-wrapper {
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-disable-update-check-wrapper label {
|
|
|
+ padding-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-version-notif-changelog-cont {
|
|
|
+ max-height: 400px;
|
|
|
+ overflow-y: auto;
|
|
|
+ margin: 10px 0px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-version-notif-changelog-details {
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-bg {
|
|
|
+ --bytm-dialog-bg: #333333;
|
|
|
+ --bytm-dialog-bg-highlight: #252525;
|
|
|
+ --bytm-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
|
|
|
+ --bytm-dialog-separator-color: #797979;
|
|
|
+ --bytm-dialog-border-radius: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-cfg-dialog-bg {
|
|
|
+ --bytm-dialog-height-max: 750px;
|
|
|
+ --bytm-dialog-width-max: 1000px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-changelog-dialog-bg {
|
|
|
+ --bytm-dialog-height-max: 800px;
|
|
|
+ --bytm-dialog-width-max: 800px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-export-dialog-bg, #bytm-import-dialog-bg {
|
|
|
+ --bytm-dialog-height-max: 500px;
|
|
|
+ --bytm-dialog-width-max: 600px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-feat-help-dialog-bg {
|
|
|
+ --bytm-dialog-height-max: 400px;
|
|
|
+ --bytm-dialog-width-max: 600px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-bg {
|
|
|
+ display: block;
|
|
|
+ position: fixed;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ z-index: 5;
|
|
|
+ background-color: rgba(0, 0, 0, 0.6);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog {
|
|
|
+ position: fixed;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ width: calc(min(100% - 60px, var(--bytm-dialog-width-max)));
|
|
|
+ border-radius: var(--bytm-dialog-border-radius);
|
|
|
+ height: auto;
|
|
|
+ max-height: calc(min(100% - 40px, var(--bytm-dialog-height-max)));
|
|
|
+ left: 50%;
|
|
|
+ top: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ z-index: 6;
|
|
|
+ color: #fff;
|
|
|
+ background-color: var(--bytm-dialog-bg);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-body {
|
|
|
+ font-size: 1.4rem;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-body.small {
|
|
|
+ padding: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-dialog-opts {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ position: relative;
|
|
|
+ padding: 30px 0px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 6px;
|
|
|
+ padding: 15px 20px 15px 20px;
|
|
|
+ background-color: var(--bytm-dialog-bg);
|
|
|
+ border: 2px solid var(--bytm-dialog-separator-color);
|
|
|
+ border-style: none none solid none;
|
|
|
+ border-radius: var(--bytm-dialog-border-radius) var(--bytm-dialog-border-radius) 0px 0px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-header.small {
|
|
|
+ padding: 10px 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-titlecont {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-titlecont-no-title {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-title {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+ font-size: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-dialog-version {
|
|
|
+ position: absolute;
|
|
|
+ width: 100%;
|
|
|
+ bottom: -10px;
|
|
|
+ left: 0;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: normal;
|
|
|
+ z-index: 7;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-dialog-version .bytm-link {
|
|
|
+ color: #c6d2db;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-dialog-linkscont {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-left: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-link {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-link:not(:last-of-type) {
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-link .bytm-dialog-img {
|
|
|
+ position: relative;
|
|
|
+ border-radius: 50%;
|
|
|
+ bottom: 0px;
|
|
|
+ transition: bottom 0.15s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-link:hover .bytm-dialog-img {
|
|
|
+ bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-close {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-close.small {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-footer {
|
|
|
+ font-size: 17px;
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-footer.hidden {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-footer-cont {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 6px;
|
|
|
+ padding: 15px 20px;
|
|
|
+ background: var(--bytm-dialog-bg);
|
|
|
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--bytm-dialog-bg) 30%, var(--bytm-dialog-bg) 100%);
|
|
|
+ border: 2px solid var(--bytm-dialog-separator-color);
|
|
|
+ border-style: solid none none none;
|
|
|
+ border-radius: 0px 0px var(--bytm-dialog-border-radius) var(--bytm-dialog-border-radius);
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-dialog-footer-buttons-cont button:not(:last-of-type) {
|
|
|
+ margin-right: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-footer-right {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row-reverse;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-dialog-footer-left-buttons-cont button:not(:last-of-type) {
|
|
|
+ margin-right: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-dialog-scroll-indicator {
|
|
|
+ --bytm-scroll-indicator-padding: 5px;
|
|
|
+ position: sticky;
|
|
|
+ bottom: -15px;
|
|
|
+ left: 50%;
|
|
|
+ margin-top: calc(-32px - var(--bytm-scroll-indicator-padding) * 2);
|
|
|
+ padding: var(--bytm-scroll-indicator-padding);
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ z-index: 7;
|
|
|
+ background-color: var(--bytm-scroll-indicator-bg);
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-hidden {
|
|
|
+ visibility: hidden !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-category-header {
|
|
|
+ font-size: 18px;
|
|
|
+ margin-top: 32px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ padding: 0px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-category-header:first-of-type {
|
|
|
+ margin-top: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 1.4em;
|
|
|
+ padding: 8px 20px;
|
|
|
+ transition: background-color 0.15s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem:hover {
|
|
|
+ background-color: var(--bytm-dialog-bg-highlight);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-leftside {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-ctrl {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-left: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-label {
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-slider-label {
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-toggle-label {
|
|
|
+ padding-left: 10px;
|
|
|
+ padding-right: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-input.bytm-hotkey-input {
|
|
|
+ cursor: pointer;
|
|
|
+ min-width: 50px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-input[type=number] {
|
|
|
+ width: 75px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftconf-input[type=checkbox] {
|
|
|
+ margin-left: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-export-dialog-text, #bytm-import-dialog-text {
|
|
|
+ font-size: 1.6em;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-dialog-footer-copied {
|
|
|
+ font-size: 1.6em;
|
|
|
+ margin-right: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-changelog-dialog-body {
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-export-dialog-textarea, #bytm-import-dialog-textarea {
|
|
|
+ width: 100%;
|
|
|
+ height: 150px;
|
|
|
+ resize: none;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow-y: auto;
|
|
|
+ font-size: 1.4rem;
|
|
|
+ line-height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Markdown stuff */
|
|
|
+
|
|
|
+.bytm-markdown-container kbd {
|
|
|
+ --bytm-easing: cubic-bezier(0.31, 0.58, 0.24, 1.15);
|
|
|
+ display: inline-block;
|
|
|
+ vertical-align: bottom;
|
|
|
+ padding: 4px;
|
|
|
+ padding-top: 2px;
|
|
|
+ font-size: 0.95em;
|
|
|
+ line-height: 11px;
|
|
|
+ background-color: #222;
|
|
|
+ border: 1px solid #777;
|
|
|
+ border-radius: 5px;
|
|
|
+ box-shadow: inset 0 -2px 0 #515559;
|
|
|
+ transition: padding 0.1s var(--bytm-easing), box-shadow 0.1s var(--bytm-easing);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container kbd:active {
|
|
|
+ padding-bottom: 2px;
|
|
|
+ box-shadow: inset 0 0 0 initial;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container kbd::selection {
|
|
|
+ background: rgba(0, 0, 0, 0);
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container code {
|
|
|
+ background-color: #222;
|
|
|
+ border-radius: 3px;
|
|
|
+ padding: 1px 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container h2 {
|
|
|
+ margin-bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container h2:not(:first-of-type) {
|
|
|
+ margin-top: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container ul li::before {
|
|
|
+ content: "• ";
|
|
|
+ font-weight: bolder;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-markdown-container ul li > ul li::before {
|
|
|
+ white-space: pre;
|
|
|
+ content: " • ";
|
|
|
+ font-weight: bolder;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-feat-help-dialog-desc, #bytm-feat-help-dialog-text {
|
|
|
+ overflow-wrap: break-word;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ padding: 10px 10px 15px 20px;
|
|
|
+ font-size: 1.5em;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-feat-help-dialog-desc {
|
|
|
+ font-size: 1.65em;
|
|
|
+ padding-bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-help-btn {
|
|
|
+ width: 24px !important;
|
|
|
+ height: 24px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-help-btn svg {
|
|
|
+ width: 18px !important;
|
|
|
+ height: 18px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-help-btn svg > path {
|
|
|
+ fill: #b3bec7 !important;
|
|
|
+}
|
|
|
+
|
|
|
+hr {
|
|
|
+ display: block;
|
|
|
+ margin: 8px 0px 12px 0px;
|
|
|
+ border: revert;
|
|
|
+}
|
|
|
+
|
|
|
+.bytm-ftitem-adornment {
|
|
|
+ display: inline-flex;
|
|
|
+ justify-content: flex-start;
|
|
|
+ align-items: center;
|
|
|
+ margin-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-ftitem-locale-adornment svg path {
|
|
|
+ fill: #4595c7;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-bg {
|
|
|
+ --bytm-menu-height-max: 500px;
|
|
|
+ --bytm-menu-width-max: 700px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-title-wrapper {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-title-logo {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ margin-right: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-content-wrapper {
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-locale-cont {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-start;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-locale-img {
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-text {
|
|
|
+ font-size: 1.6em;
|
|
|
+ padding: 8px 20px;
|
|
|
+ margin: 10px 0px;
|
|
|
+ line-height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-locale-select {
|
|
|
+ font-size: 1.6em;
|
|
|
+}
|
|
|
+
|
|
|
+#bytm-welcome-menu-footer-cont {
|
|
|
+ border-radius: 0px 0px var(--bytm-menu-border-radius) var(--bytm-menu-border-radius);
|
|
|
+ padding: 20px;
|
|
|
+}`).id = "bytm-style-global";
|
|
|
+ }
|
|
|
+ function registerMenuCommands() {
|
|
|
+ if (mode === "development") {
|
|
|
+ GM.registerMenuCommand("Reset config", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (confirm("Reset the configuration to its default values?\nThis will automatically reload the page.")) {
|
|
|
+ yield clearConfig();
|
|
|
+ disableBeforeUnload();
|
|
|
+ location.reload();
|
|
|
+ }
|
|
|
+ }), "r");
|
|
|
+ GM.registerMenuCommand("List GM values", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
const keys = yield GM.listValues();
|
|
|
- console.log("Clearing GM values:");
|
|
|
+ console.log("GM values:");
|
|
|
if (keys.length === 0)
|
|
|
console.log(" No values found.");
|
|
|
- for (const key of keys) {
|
|
|
+ for (const key of keys)
|
|
|
+ console.log(` ${key} -> ${yield GM.getValue(key)}`);
|
|
|
+ alert("See console.");
|
|
|
+ }), "l");
|
|
|
+ GM.registerMenuCommand("Delete all GM values", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ if (confirm("Clear all GM values?\nSee console for details.")) {
|
|
|
+ const keys = yield GM.listValues();
|
|
|
+ console.log("Clearing GM values:");
|
|
|
+ if (keys.length === 0)
|
|
|
+ console.log(" No values found.");
|
|
|
+ for (const key of keys) {
|
|
|
+ yield GM.deleteValue(key);
|
|
|
+ console.log(` Deleted ${key}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }), "d");
|
|
|
+ GM.registerMenuCommand("Delete GM value by name", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const key = prompt("Enter the name of the GM value to delete.\nEmpty input cancels the operation.");
|
|
|
+ if (key && key.length > 0) {
|
|
|
+ const oldVal = yield GM.getValue(key);
|
|
|
yield GM.deleteValue(key);
|
|
|
- console.log(` Deleted ${key}`);
|
|
|
+ console.log(`Deleted GM value '${key}' with previous value '${oldVal}'`);
|
|
|
}
|
|
|
- }
|
|
|
- }), "d");
|
|
|
- GM.registerMenuCommand("Delete GM value by name", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const key = prompt("Enter the name of the GM value to delete.\nEmpty input cancels the operation.");
|
|
|
- if (key && key.length > 0) {
|
|
|
- const oldVal = yield GM.getValue(key);
|
|
|
- yield GM.deleteValue(key);
|
|
|
- console.log(`Deleted GM value '${key}' with previous value '${oldVal}'`);
|
|
|
- }
|
|
|
- }), "n");
|
|
|
- GM.registerMenuCommand("Reset install timestamp", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- yield GM.deleteValue("bytm-installed");
|
|
|
- console.log("Reset install time.");
|
|
|
- }), "t");
|
|
|
- GM.registerMenuCommand("Reset version check timestamp", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- yield GM.deleteValue("bytm-version-check");
|
|
|
- console.log("Reset version check time.");
|
|
|
- }), "v");
|
|
|
- GM.registerMenuCommand("List active selector listeners", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
- const lines = [];
|
|
|
- let listenersAmt = 0;
|
|
|
- for (const [obsName, obs] of Object.entries(observers$1)) {
|
|
|
- const listeners = obs.getAllListeners();
|
|
|
- lines.push(`- "${obsName}" (${listeners.size} listeners):`);
|
|
|
- [...listeners].forEach(([k, v]) => {
|
|
|
- listenersAmt += v.length;
|
|
|
- lines.push(` [${v.length}] ${k}`);
|
|
|
- v.forEach(({ all, continuous }, i) => {
|
|
|
- lines.push(` ${v.length > 1 && i !== v.length - 1 ? "├" : "└"}> ${continuous ? "continuous" : "single-shot"}, ${all ? "select multiple" : "select single"}`);
|
|
|
+ }), "n");
|
|
|
+ GM.registerMenuCommand("Reset install timestamp", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ yield GM.deleteValue("bytm-installed");
|
|
|
+ console.log("Reset install time.");
|
|
|
+ }), "t");
|
|
|
+ GM.registerMenuCommand("Reset version check timestamp", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ yield GM.deleteValue("bytm-version-check");
|
|
|
+ console.log("Reset version check time.");
|
|
|
+ }), "v");
|
|
|
+ GM.registerMenuCommand("List active selector listeners", () => __awaiter(this, void 0, void 0, function* () {
|
|
|
+ const lines = [];
|
|
|
+ let listenersAmt = 0;
|
|
|
+ for (const [obsName, obs] of Object.entries(observers$1)) {
|
|
|
+ const listeners = obs.getAllListeners();
|
|
|
+ lines.push(`- "${obsName}" (${listeners.size} listeners):`);
|
|
|
+ [...listeners].forEach(([k, v]) => {
|
|
|
+ listenersAmt += v.length;
|
|
|
+ lines.push(` [${v.length}] ${k}`);
|
|
|
+ v.forEach(({ all, continuous }, i) => {
|
|
|
+ lines.push(` ${v.length > 1 && i !== v.length - 1 ? "├" : "└"}> ${continuous ? "continuous" : "single-shot"}, ${all ? "select multiple" : "select single"}`);
|
|
|
+ });
|
|
|
});
|
|
|
- });
|
|
|
- }
|
|
|
- console.log(`Showing currently active listeners for ${Object.keys(observers$1).length} observers with ${listenersAmt} total listeners:\n${lines.join("\n")}`);
|
|
|
- alert("See console.");
|
|
|
- }), "s");
|
|
|
+ }
|
|
|
+ console.log(`Showing currently active listeners for ${Object.keys(observers$1).length} observers with ${listenersAmt} total listeners:\n${lines.join("\n")}`);
|
|
|
+ alert("See console.");
|
|
|
+ }), "s");
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
-preInit();})(UserUtils,marked);//# sourceMappingURL=BetterYTM.user.js.map
|
|
|
+ preInit();
|
|
|
+
|
|
|
+})(UserUtils, marked);
|