ソースを参照

fix: queue btns overhaul

Sven 11 ヶ月 前
コミット
df21050045

+ 4 - 4
.vscode/settings.json

@@ -18,10 +18,10 @@
       "color": "black",
       "overviewRulerColor": "#ed0",
     },
-    "((//\\s?)?#region ([^\\S\\r\\n]*[\\(\\)\\w,.\\-_&+#*'\"]+)*[:]*)": { //#region test (abc):
-      "backgroundColor": "#069",
-      "color": "#fff",
-      "overviewRulerColor": "#069",
+    "((//\\s*|/\\*\\s*)?#region ([^\\S\\r\\n]*[\\(\\)\\w,.\\-_&+#*'\"/]+)*[:]*)": { //#region test (abc):
+      "backgroundColor": "#5df",
+      "color": "#000",
+      "overviewRulerColor": "#5df",
     },
     "((<!--\\s*)?</?\\{\\{[A-Z_-]+\\}\\}>(\\s*-->)?)": { // <!-- <{{FOO}}> --> and <!-- </{{FOO}}> --> or <{{BAR}}> and </{{BAR}}>
       "backgroundColor": "#9af",

+ 2 - 1
assets/resources.json

@@ -1,7 +1,8 @@
 {
+  "css-above_queue_btns": "style/aboveQueueBtns.css",
   "css-anchor_improvements": "style/anchorImprovements.css",
-  "css-fix_spacing": "style/fixSpacing.css",
   "css-fix_hdr": "style/fixHDR.css",
+  "css-fix_spacing": "style/fixSpacing.css",
   "doc-changelog": "/changelog.md",
   "icon-advanced_mode": "icons/plus_circle_small.svg",
   "icon-arrow_down": "icons/arrow_down.svg",

+ 14 - 0
assets/style/aboveQueueBtns.css

@@ -0,0 +1,14 @@
+#side-panel ytmusic-tab-renderer ytmusic-queue-header-renderer {
+  position: sticky;
+  align-items: center;
+  top: 0;
+  z-index: 2;
+  padding: 16px 8px;
+  margin: unset;
+  background-color: #030303;
+  border-bottom: 1px solid var(--ytmusic-divider);
+}
+
+#side-panel ytmusic-tab-renderer ytmusic-queue-header-renderer.hidden {
+  z-index: 0;
+}

+ 47 - 40
assets/translations/README.md

@@ -1,9 +1,9 @@
 <!--
-    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-    !!             THIS IS A GENERATED FILE             !!
-    !!    all changes will be overwritten next build    !!
-    !! only edit in `src/tools/tr-progress-template.md` !!
-    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+  !!!             THIS IS A GENERATED FILE             !!!
+  !!!    all changes will be overwritten next build    !!!
+  !!! only edit in `src/tools/tr-progress-template.md` !!!
+  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 -->
 
 
@@ -21,14 +21,14 @@ To submit or edit a translation, please follow [this guide](../../contributing.m
 | &nbsp; | Locale | Translated keys | Based on |
 | :----: | ------ | --------------- | :------: |
 | ─ | [`en_US`](./en_US.json) | 209 (default locale) |  |
-| ⚠ | [`de_DE`](./de_DE.json) | `205/209` (98.1%) | ─ |
+| ⚠ | [`de_DE`](./de_DE.json) | `204/209` (97.6%) | ─ |
 | ─ | [`en_UK`](./en_UK.json) | `209/209` (100%) | `en_US` |
-| ⚠ | [`es_ES`](./es_ES.json) | `205/209` (98.1%) | ─ |
-| ⚠ | [`fr_FR`](./fr_FR.json) | `205/209` (98.1%) | ─ |
-| ⚠ | [`hi_IN`](./hi_IN.json) | `205/209` (98.1%) | ─ |
-| ⚠ | [`ja_JA`](./ja_JA.json) | `205/209` (98.1%) | ─ |
-| ⚠ | [`pt_BR`](./pt_BR.json) | `205/209` (98.1%) | ─ |
-| ⚠ | [`zh_CN`](./zh_CN.json) | `205/209` (98.1%) | ─ |
+| ⚠ | [`es_ES`](./es_ES.json) | `204/209` (97.6%) | ─ |
+| ⚠ | [`fr_FR`](./fr_FR.json) | `204/209` (97.6%) | ─ |
+| ⚠ | [`hi_IN`](./hi_IN.json) | `204/209` (97.6%) | ─ |
+| ⚠ | [`ja_JA`](./ja_JA.json) | `204/209` (97.6%) | ─ |
+| ⚠ | [`pt_BR`](./pt_BR.json) | `204/209` (97.6%) | ─ |
+| ⚠ | [`zh_CN`](./zh_CN.json) | `204/209` (97.6%) | ─ |
 
 <sub>
 ✅ - Fully translated
@@ -49,79 +49,86 @@ This means to figure out which keys are untranslated, you will need to manually
 
 ### Missing keys:
 
-<details><summary><code>de_DE</code> - 4 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>de_DE</code> - 5 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
-| `clear_queue` | `Clear the queue` |
-| `clear_queue_confirm` | `Do you really want to clear the queue and leave only the currently playing song?` |
+| `clear_list` | `Clear the list` |
+| `clear_list_confirm` | `Do you really want to clear the list and leave only the currently playing song?` |
 | `feature_desc_fixHdrIssues` | `Fix rendering issues when using an HDR-compatible GPU and monitor` |
-| `feature_desc_clearQueueBtn` | `Add a button to the currently playing queue (or playlist) to quickly clear it` |
+| `feature_desc_scrollToActiveSongBtn` | `Add a button above the queue to scroll to the currently playing song` |
+| `feature_desc_clearQueueBtn` | `Add a button above the currently playing queue or playlist to quickly clear it` |
 
 <br></details>
 
-<details><summary><code>es_ES</code> - 4 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>es_ES</code> - 5 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
-| `clear_queue` | `Clear the queue` |
-| `clear_queue_confirm` | `Do you really want to clear the queue and leave only the currently playing song?` |
+| `clear_list` | `Clear the list` |
+| `clear_list_confirm` | `Do you really want to clear the list and leave only the currently playing song?` |
 | `feature_desc_fixHdrIssues` | `Fix rendering issues when using an HDR-compatible GPU and monitor` |
-| `feature_desc_clearQueueBtn` | `Add a button to the currently playing queue (or playlist) to quickly clear it` |
+| `feature_desc_scrollToActiveSongBtn` | `Add a button above the queue to scroll to the currently playing song` |
+| `feature_desc_clearQueueBtn` | `Add a button above the currently playing queue or playlist to quickly clear it` |
 
 <br></details>
 
-<details><summary><code>fr_FR</code> - 4 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>fr_FR</code> - 5 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
-| `clear_queue` | `Clear the queue` |
-| `clear_queue_confirm` | `Do you really want to clear the queue and leave only the currently playing song?` |
+| `clear_list` | `Clear the list` |
+| `clear_list_confirm` | `Do you really want to clear the list and leave only the currently playing song?` |
 | `feature_desc_fixHdrIssues` | `Fix rendering issues when using an HDR-compatible GPU and monitor` |
-| `feature_desc_clearQueueBtn` | `Add a button to the currently playing queue (or playlist) to quickly clear it` |
+| `feature_desc_scrollToActiveSongBtn` | `Add a button above the queue to scroll to the currently playing song` |
+| `feature_desc_clearQueueBtn` | `Add a button above the currently playing queue or playlist to quickly clear it` |
 
 <br></details>
 
-<details><summary><code>hi_IN</code> - 4 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>hi_IN</code> - 5 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
-| `clear_queue` | `Clear the queue` |
-| `clear_queue_confirm` | `Do you really want to clear the queue and leave only the currently playing song?` |
+| `clear_list` | `Clear the list` |
+| `clear_list_confirm` | `Do you really want to clear the list and leave only the currently playing song?` |
 | `feature_desc_fixHdrIssues` | `Fix rendering issues when using an HDR-compatible GPU and monitor` |
-| `feature_desc_clearQueueBtn` | `Add a button to the currently playing queue (or playlist) to quickly clear it` |
+| `feature_desc_scrollToActiveSongBtn` | `Add a button above the queue to scroll to the currently playing song` |
+| `feature_desc_clearQueueBtn` | `Add a button above the currently playing queue or playlist to quickly clear it` |
 
 <br></details>
 
-<details><summary><code>ja_JA</code> - 4 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>ja_JA</code> - 5 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
-| `clear_queue` | `Clear the queue` |
-| `clear_queue_confirm` | `Do you really want to clear the queue and leave only the currently playing song?` |
+| `clear_list` | `Clear the list` |
+| `clear_list_confirm` | `Do you really want to clear the list and leave only the currently playing song?` |
 | `feature_desc_fixHdrIssues` | `Fix rendering issues when using an HDR-compatible GPU and monitor` |
-| `feature_desc_clearQueueBtn` | `Add a button to the currently playing queue (or playlist) to quickly clear it` |
+| `feature_desc_scrollToActiveSongBtn` | `Add a button above the queue to scroll to the currently playing song` |
+| `feature_desc_clearQueueBtn` | `Add a button above the currently playing queue or playlist to quickly clear it` |
 
 <br></details>
 
-<details><summary><code>pt_BR</code> - 4 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>pt_BR</code> - 5 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
-| `clear_queue` | `Clear the queue` |
-| `clear_queue_confirm` | `Do you really want to clear the queue and leave only the currently playing song?` |
+| `clear_list` | `Clear the list` |
+| `clear_list_confirm` | `Do you really want to clear the list and leave only the currently playing song?` |
 | `feature_desc_fixHdrIssues` | `Fix rendering issues when using an HDR-compatible GPU and monitor` |
-| `feature_desc_clearQueueBtn` | `Add a button to the currently playing queue (or playlist) to quickly clear it` |
+| `feature_desc_scrollToActiveSongBtn` | `Add a button above the queue to scroll to the currently playing song` |
+| `feature_desc_clearQueueBtn` | `Add a button above the currently playing queue or playlist to quickly clear it` |
 
 <br></details>
 
-<details><summary><code>zh_CN</code> - 4 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>zh_CN</code> - 5 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
-| `clear_queue` | `Clear the queue` |
-| `clear_queue_confirm` | `Do you really want to clear the queue and leave only the currently playing song?` |
+| `clear_list` | `Clear the list` |
+| `clear_list_confirm` | `Do you really want to clear the list and leave only the currently playing song?` |
 | `feature_desc_fixHdrIssues` | `Fix rendering issues when using an HDR-compatible GPU and monitor` |
-| `feature_desc_clearQueueBtn` | `Add a button to the currently playing queue (or playlist) to quickly clear it` |
+| `feature_desc_scrollToActiveSongBtn` | `Add a button above the queue to scroll to the currently playing song` |
+| `feature_desc_clearQueueBtn` | `Add a button above the currently playing queue or playlist to quickly clear it` |
 
 <br></details>

+ 0 - 1
assets/translations/de_DE.json

@@ -171,7 +171,6 @@
     "feature_desc_deleteFromQueueButton": "Füge jedem Song in der Wiedergabeliste einen Knopf hinzu, um ihn schnell zu entfernen",
     "feature_desc_listButtonsPlacement": "Wo sollen die Wiedergabelisten-Knöpfe angezeigt werden?",
     "feature_helptext_listButtonsPlacement": "Es gibt verschiedene Songlisten auf der Seite, wie z.B. Albumseiten, Playlists und die aktuelle Wiedergabeliste. Mit dieser Option kannst du auswählen, wo die Wiedergabelisten-Knöpfe angezeigt werden sollen.",
-    "feature_desc_scrollToActiveSongBtn": "Füge einen Knopf zur Wiedergabeliste hinzu, um zum aktiven Song zu scrollen",
 
     "feature_desc_disableBeforeUnloadPopup": "Verhindere das Erscheinen des Bestätigungs-Popup beim Verlassen der Seite, während ein Song läuft",
     "feature_helptext_disableBeforeUnloadPopup": "Wenn du versuchst, die Seite zu verlassen, während ein Song läuft, erscheint ein Popup, das dich fragt, ob du die Seite wirklich verlassen möchtest. Es könnte etwas in der Art von \"Du hast ungespeicherte Daten\" oder \"Diese Seite fragt, ob du sie schließen möchtest\" sein.\nDiese Funktion deaktiviert dieses Popup vollständig.",

+ 4 - 4
assets/translations/en_US.json

@@ -50,8 +50,8 @@
     "delete_from_list": "Delete this song from the list",
     "couldnt_remove_from_queue": "Couldn't remove this song from the queue",
     "couldnt_delete_from_list": "Couldn't delete this song from the list",
-    "clear_queue": "Clear the queue",
-    "clear_queue_confirm": "Do you really want to clear the queue and leave only the currently playing song?",
+    "clear_list": "Clear the list",
+    "clear_list_confirm": "Do you really want to clear the list and leave only the currently playing song?",
     "scroll_to_playing": "Scroll to the currently playing song",
     "scroll_to_bottom": "Click to scroll to the bottom",
     "volume_tooltip": "Volume: %1% (Sensitivity: %2%)",
@@ -174,8 +174,8 @@
     "feature_desc_deleteFromQueueButton": "Add a button to each song in a queue to quickly remove it",
     "feature_desc_listButtonsPlacement": "Where should the queue buttons show up?",
     "feature_helptext_listButtonsPlacement": "There are various song lists on the site like album pages, playlists and the currently playing queue. With this option you can choose where the queue buttons should show up.",
-    "feature_desc_scrollToActiveSongBtn": "Add a button to the queue to scroll to the currently playing song",
-    "feature_desc_clearQueueBtn": "Add a button to the currently playing queue (or playlist) to quickly clear it",
+    "feature_desc_scrollToActiveSongBtn": "Add a button above the queue to scroll to the currently playing song",
+    "feature_desc_clearQueueBtn": "Add a button above the currently playing queue or playlist to quickly clear it",
 
     "feature_desc_disableBeforeUnloadPopup": "Prevent the confirmation popup that appears when trying to leave the site while a song is playing",
     "feature_helptext_disableBeforeUnloadPopup": "When trying to leave the site while a few seconds into a song that is actively playing, a popup will appear asking you to confirm that you want to leave the site. It might say something along the lines of \"you have unsaved data\" or \"this site is asking if you want to close it\".\nThis feature disables that popup entirely.",

+ 0 - 1
assets/translations/es_ES.json

@@ -171,7 +171,6 @@
     "feature_desc_deleteFromQueueButton": "Agregue un botón a cada canción en la cola para eliminarla rápidamente",
     "feature_desc_listButtonsPlacement": "¿Dónde deberían aparecer los botones de la cola?",
     "feature_helptext_listButtonsPlacement": "Hay varias listas de canciones en el sitio, como páginas de álbumes, listas de reproducción y la cola de reproducción actual. Con esta opción, puede elegir dónde deben aparecer los botones de la cola.",
-    "feature_desc_scrollToActiveSongBtn": "Agregue un botón a la cola para desplazarse hasta la canción que se está reproduciendo actualmente",
 
     "feature_desc_disableBeforeUnloadPopup": "Evite la ventana emergente de confirmación que aparece al intentar salir del sitio mientras se reproduce una canción",
     "feature_helptext_disableBeforeUnloadPopup": "Cuando intenta salir del sitio mientras está reproduciendo una canción que lleva unos segundos, aparecerá una ventana emergente que le pedirá que confirme que desea salir del sitio. Podría decir algo así como \"tiene datos no guardados\" o \"este sitio está preguntando si desea cerrarlo\".\nEsta función deshabilita completamente esa ventana emergente.",

+ 0 - 1
assets/translations/fr_FR.json

@@ -171,7 +171,6 @@
     "feature_desc_deleteFromQueueButton": "Ajouter un bouton à chaque chanson de la file d'attente pour la supprimer rapidement",
     "feature_desc_listButtonsPlacement": "Où les boutons de file d'attente doivent-ils apparaître?",
     "feature_helptext_listButtonsPlacement": "Il existe diverses listes de chansons sur le site comme les pages d'album, les listes de lecture et la file d'attente de lecture actuelle. Avec cette option, vous pouvez choisir où les boutons de file d'attente doivent apparaître.",
-    "feature_desc_scrollToActiveSongBtn": "Ajouter un bouton à la file d'attente pour faire défiler jusqu'à la chanson en cours de lecture",
 
     "feature_desc_disableBeforeUnloadPopup": "Empêcher la fenêtre contextuelle de confirmation qui apparaît lors de la tentative de quitter le site pendant qu'une chanson est en cours de lecture",
     "feature_helptext_disableBeforeUnloadPopup": "Lorsque vous essayez de quitter le site alors que vous êtes quelques secondes dans une chanson qui est en cours de lecture, une fenêtre contextuelle apparaîtra pour vous demander de confirmer que vous voulez quitter le site. Elle pourrait dire quelque chose comme \"vous avez des données non enregistrées\" ou \"ce site demande si vous voulez le fermer\".\nCette fonctionnalité désactive complètement cette fenêtre contextuelle.",

+ 0 - 1
assets/translations/hi_IN.json

@@ -171,7 +171,6 @@
     "feature_desc_deleteFromQueueButton": "कतार में प्रत्येक गीत में एक बटन जो इसे त्वरित रूप से हटा देता है",
     "feature_desc_listButtonsPlacement": "कतार बटन कहाँ दिखाएं?",
     "feature_helptext_listButtonsPlacement": "साइट पर विभिन्न गीत सूचियाँ हैं जैसे एल्बम पेज, प्लेलिस्ट और वर्तमान में चल रहे कतार। इस विकल्प के साथ आप चुन सकते हैं कि कतार बटन कहाँ दिखाएं।",
-    "feature_desc_scrollToActiveSongBtn": "कतार में एक बटन जो वर्तमान में चल रहे गीत पर स्क्रॉल करता है",
 
     "feature_desc_disableBeforeUnloadPopup": "एक गीत चल रहे होने पर साइट छोड़ने का प्रयास करने पर आने वाली पुष्टि पॉपअप को रोकें",
     "feature_helptext_disableBeforeUnloadPopup": "जब आप वेबसाइट छोड़ने की कोशिश करते हैं जब आप एक गीत को थोड़े समय के लिए सुन रहे होते हैं, तो एक पॉपअप आता है जो आपसे पुष्टि करता है कि क्या आप वाकई साइट छोड़ना चाहते हैं। यह कुछ इस प्रकार का हो सकता है \"आपके पास असहेज डेटा है\" या \"यह साइट आपसे पूछ रही है कि क्या आप इसे बंद करना चाहते हैं\"।\nयह सुविधा इस पॉपअप को पूरी तरह से अक्षम करती है।",

+ 0 - 1
assets/translations/ja_JA.json

@@ -171,7 +171,6 @@
     "feature_desc_deleteFromQueueButton": "キュー内の各曲にボタンを追加して、すばやく削除できるようにする",
     "feature_desc_listButtonsPlacement": "キューボタンの表示場所を選択する",
     "feature_helptext_listButtonsPlacement": "アルバムページ、プレイリスト、現在再生中のキューなど、サイトにはさまざまな曲リストがあります。このオプションを使用して、キューボタンを表示する場所を選択できます。",
-    "feature_desc_scrollToActiveSongBtn": "キューに現在再生中の曲までスクロールするボタンを追加する",
 
     "feature_desc_disableBeforeUnloadPopup": "曲が再生されている間にサイトを離れようとすると表示される確認ポップアップを防止する",
     "feature_helptext_disableBeforeUnloadPopup": "曲が再生されている間にサイトを離れようとすると、数秒後にポップアップが表示され、サイトを離れるかどうかを確認するように求められます。それは「保存されていないデータがあります」とか「このサイトを閉じるかどうかを尋ねています」とかのようなことが書かれているかもしれません。\nこの機能はそのポップアップを完全に無効にします。",

+ 0 - 1
assets/translations/pt_BR.json

@@ -171,7 +171,6 @@
     "feature_desc_deleteFromQueueButton": "Adicionar um botão a cada música na fila para removê-la rapidamente",
     "feature_desc_listButtonsPlacement": "Onde os botões da fila devem aparecer?",
     "feature_helptext_listButtonsPlacement": "Existem várias listas de músicas no site, como páginas de álbuns, listas de reprodução e a fila de reprodução atual. Com esta opção, você pode escolher onde os botões da fila devem aparecer.",
-    "feature_desc_scrollToActiveSongBtn": "Adicionar um botão à fila para rolar até a música que está tocando atualmente",
 
     "feature_desc_disableBeforeUnloadPopup": "Evite a janela de confirmação que aparece ao tentar sair do site enquanto uma música está tocando",
     "feature_helptext_disableBeforeUnloadPopup": "Ao tentar sair do site enquanto uma música está tocando, uma janela de confirmação pode aparecer, pedindo que você confirme que deseja sair do site. Pode dizer algo como \"você tem dados não salvos\" ou \"este site está perguntando se você deseja fechá-lo\".\nEste recurso desativa completamente essa janela de confirmação.",

+ 0 - 1
assets/translations/zh_CN.json

@@ -171,7 +171,6 @@
     "feature_desc_deleteFromQueueButton": "在队列中的每首歌曲旁边添加一个按钮,以快速删除它",
     "feature_desc_listButtonsPlacement": "队列按钮应该显示在哪里?",
     "feature_helptext_listButtonsPlacement": "网站上有各种歌曲列表,如专辑页面、播放列表和当前播放的队列。使用此选项,您可以选择队列按钮应该显示在哪里。",
-    "feature_desc_scrollToActiveSongBtn": "在队列中添加一个按钮,以滚动到当前播放的歌曲",
 
     "feature_desc_disableBeforeUnloadPopup": "防止在播放歌曲时尝试离开网站时出现的确认弹出窗口",
     "feature_helptext_disableBeforeUnloadPopup": "当尝试在正在播放的歌曲中几秒钟后离开网站时,将出现一个弹出窗口,询问您是否要离开网站。它可能会说类似于 \"您有未保存的数据\" 或 \"此网站正在询问您是否要关闭它\"。\n此功能完全禁用了该弹出窗口。",

+ 8 - 8
src/components/circularButton.ts

@@ -17,10 +17,8 @@ type CircularBtnOptions = (
   | {
     /** URL to navigate to when the button is clicked */
     href: string;
-    onClick?: undefined;
   }
   | {
-    href?: undefined;
     /** Callback function to execute when the button is clicked */
     onClick: (event: MouseEvent | KeyboardEvent) => void;
   }
@@ -34,25 +32,27 @@ type CircularBtnOptions = (
  */
 export async function createCircularBtn({
   title,
-  href,
-  onClick,
   ...rest
 }: CircularBtnOptions) {
   let btnElem: HTMLElement;
-  if(href) {
+  if("href" in rest && rest.href) {
     btnElem = document.createElement("a");
-    (btnElem as HTMLAnchorElement).href = href;
+    (btnElem as HTMLAnchorElement).href = rest.href;
     btnElem.role = "button";
     (btnElem as HTMLAnchorElement).target = "_blank";
     (btnElem as HTMLAnchorElement).rel = "noopener noreferrer";
   }
-  else {
+  else if("onClick" in rest && rest.onClick) {
     btnElem = document.createElement("div");
-    onClick && onInteraction(btnElem, onClick);
+    rest.onClick && onInteraction(btnElem, rest.onClick);
   }
+  else
+    throw new TypeError("Either 'href' or 'onClick' must be provided");
 
   btnElem.classList.add("bytm-generic-btn");
   btnElem.ariaLabel = btnElem.title = title;
+  btnElem.tabIndex = 0;
+  btnElem.role = "button";
 
   const imgElem = document.createElement("img");
   imgElem.classList.add("bytm-generic-btn-img");

+ 2 - 2
src/features/behavior.ts

@@ -1,5 +1,5 @@
 import { clamp, interceptWindowEvent, pauseFor } from "@sv443-network/userutils";
-import { domLoaded, error, getDomain, getVideoTime, getWatchId, info, log, videoSelector, waitVideoElementReady } from "../utils";
+import { domLoaded, error, getDomain, getVideoTime, getWatchId, info, log, getVideoSelector, waitVideoElementReady } from "../utils";
 import { LogLevel } from "../types";
 import { getFeatures } from "src/config";
 import { addSelectorListener } from "src/observers";
@@ -145,7 +145,7 @@ async function remSongUpdateEntry() {
 
     const songTime = await getVideoTime() ?? 0;
 
-    const paused = document.querySelector<HTMLVideoElement>(videoSelector)?.paused ?? false;
+    const paused = document.querySelector<HTMLVideoElement>(getVideoSelector())?.paused ?? 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

+ 12 - 12
src/features/index.ts

@@ -129,18 +129,6 @@ export const featInfo = {
     default: true,
     textAdornment: adornments.reloadRequired,
   },
-  scrollToActiveSongBtn: {
-    type: "toggle",
-    category: "layout",
-    default: true,
-    textAdornment: adornments.reloadRequired,
-  },
-  clearQueueBtn: {
-    type: "toggle",
-    category: "layout",
-    default: true,
-    textAdornment: adornments.reloadRequired,
-  },
   removeUpgradeTab: {
     type: "toggle",
     category: "layout",
@@ -311,6 +299,18 @@ export const featInfo = {
     default: "everywhere",
     textAdornment: adornments.reloadRequired,
   },
+  scrollToActiveSongBtn: {
+    type: "toggle",
+    category: "songLists",
+    default: true,
+    textAdornment: adornments.reloadRequired,
+  },
+  clearQueueBtn: {
+    type: "toggle",
+    category: "songLists",
+    default: true,
+    textAdornment: adornments.reloadRequired,
+  },
 
   //#region behavior
   disableBeforeUnloadPopup: {

+ 3 - 3
src/features/input.ts

@@ -1,5 +1,5 @@
 import { clamp } from "@sv443-network/userutils";
-import { error, getVideoTime, info, log, warn, videoSelector } from "../utils";
+import { error, getVideoTime, info, log, warn, getVideoSelector } from "../utils";
 import type { Domain } from "../types";
 import { isCfgMenuOpen } from "../menu/menu_old";
 import { disableBeforeUnload } from "./behavior";
@@ -31,7 +31,7 @@ export async function initArrowKeySkip() {
 
     log(`Captured arrow key '${evt.code}' - skipping by ${skipBy} seconds`);
     
-    const vidElem = document.querySelector<HTMLVideoElement>(videoSelector);
+    const vidElem = document.querySelector<HTMLVideoElement>(getVideoSelector());
     
     if(vidElem)
       vidElem.currentTime = clamp(vidElem.currentTime + skipBy, 0, vidElem.duration);
@@ -129,7 +129,7 @@ export async function initNumKeysSkip() {
     )
       return info("Captured valid key to skip video to, but ignored it since an unexpected element is active:", document.activeElement);
 
-    const vidElem = document.querySelector<HTMLVideoElement>(videoSelector);
+    const vidElem = document.querySelector<HTMLVideoElement>(getVideoSelector());
     if(!vidElem)
       return warn("Could not find video element, so the keypress is ignored");
 

+ 20 - 1
src/features/layout.css

@@ -141,6 +141,13 @@ button[disabled].bytm-busy {
   cursor: progress;
 }
 
+/* #region random fixes */
+
+.sponsorSkipNoticeContainer,
+.sponsorSkipObject {
+  z-index: 4;
+}
+
 /* #region menu */
 
 .bytm-cfg-menu-option {
@@ -240,9 +247,21 @@ yt-multi-page-menu-section-renderer.ytd-multi-page-menu-renderer {
   flex-direction: row;
   justify-content: center;
   align-items: center;
+  flex-wrap: nowrap;
   height: 100%;
 }
 
+#bytm-above-queue-btn-wrapper {
+  white-space: nowrap;
+}
+
+.bytm-above-queue-btn {
+  width: 32px;
+  height: 32px;
+  margin-left: 0;
+  margin-right: 8px;
+}
+
 /* #region scroll to active */
 
 #bytm-scroll-to-active-btn-cont {
@@ -274,7 +293,7 @@ yt-multi-page-menu-section-renderer.ytd-multi-page-menu-renderer {
   padding: 4px;
 }
 
-/** #region thumbnail */
+/* #region thumbnail */
 
 #bytm-thumbnail-overlay {
   position: absolute;

+ 74 - 96
src/features/layout.ts

@@ -2,9 +2,11 @@ import { addParent, autoPlural, debounce, fetchAdvanced, insertAfter, pauseFor }
 import { getFeatures } from "../config";
 import { siteEvents } from "../siteEvents";
 import { addSelectorListener } from "../observers";
-import { error, getResourceUrl, log, warn, t, onInteraction, openInTab, getBestThumbnailUrl, getDomain, addStyle, currentMediaType, domLoaded, waitVideoElementReady, hdrEnabled, insertBefore, getVideoTime } from "../utils";
+import { error, getResourceUrl, log, warn, t, onInteraction, openInTab, getBestThumbnailUrl, getDomain, addStyle, currentMediaType, domLoaded, waitVideoElementReady, hdrEnabled, getVideoTime, insertBefore } from "../utils";
 import { scriptInfo } from "../constants";
 import { openCfgMenu } from "../menu/menu_old";
+import { createCircularBtn } from "../components";
+import type { ResourceKey } from "../types";
 import "./layout.css";
 
 //#region cfg menu buttons
@@ -234,8 +236,6 @@ export async function addAnchorImprovements() {
       }
     };
 
-    // TODO: needs to be optimized
-
     // home page
 
     addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-carousel-shelf-renderer ytmusic-responsive-list-item-renderer", {
@@ -398,102 +398,80 @@ export async function addAboveQueueBtns() {
 
       addParent(rightBtnsEl, aboveQueueBtnCont);
 
-      if(scrollToActiveSongBtn)
-        await addScrollToActiveBtn(rightBtnsEl);
-      if(clearQueueBtn)
-        await addClearQueueBtn(rightBtnsEl);
-    },
-  });
-}
-
-/** Adds a button above the queue to scroll to the active song */
-export async function addScrollToActiveBtn(rightBtnsEl: HTMLElement) {
-  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.tabIndex = 0;
-  linkElem.classList.add("ytmusic-player-bar", "bytm-generic-btn");
-  linkElem.ariaLabel = linkElem.title = t("scroll_to_playing");
-  linkElem.role = "button";
-
-  const imgElem = document.createElement("img");
-  imgElem.classList.add("bytm-generic-btn-img");
-  imgElem.src = await getResourceUrl("icon-skip_to");
-
-  const scrollToActiveInteraction = () => {
-    const activeItem = document.querySelector<HTMLElement>("#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;
-
-    activeItem.scrollIntoView({
-      behavior: "smooth",
-      block: "center",
-      inline: "center",
-    });
-  };
-
-  siteEvents.on("fullscreenToggled", (isFullscreen) => {
-    if(isFullscreen)
-      containerElem.classList.add("hidden");
-    else
-      containerElem.classList.remove("hidden");
-  });
-
-  onInteraction(linkElem, scrollToActiveInteraction, { capture: true });
-
-  linkElem.appendChild(imgElem);
-  containerElem.appendChild(linkElem);
-  insertBefore(rightBtnsEl, containerElem);
-}
+      const headerEl = rightBtnsEl.closest<HTMLElement>("ytmusic-queue-header-renderer");
+      if(!headerEl)
+        return error("Couldn't find queue header element while adding above queue buttons");
 
-/** Adds a button above the queue to clear it */
-export async function addClearQueueBtn(rightBtnsEl: HTMLElement) {
-  const containerElem = document.createElement("div");
-  containerElem.id = "bytm-clear-queue-btn-cont";
-
-  const linkElem = document.createElement("div");
-  linkElem.id = "bytm-clear-queue-btn";
-  linkElem.tabIndex = 0;
-  linkElem.classList.add("ytmusic-player-bar", "bytm-generic-btn");
-  linkElem.ariaLabel = linkElem.title = t("clear_queue");
-  linkElem.role = "button";
-
-  const imgElem = document.createElement("img");
-  imgElem.classList.add("bytm-generic-btn-img");
-  imgElem.src = await getResourceUrl("icon-clear_list");
-
-  siteEvents.on("fullscreenToggled", (isFullscreen) => {
-    if(isFullscreen)
-      containerElem.classList.add("hidden");
-    else
-      containerElem.classList.remove("hidden");
-  });
+      siteEvents.on("fullscreenToggled", (isFullscreen) => {
+        headerEl.classList[isFullscreen ? "add" : "remove"]("hidden");
+      });
 
-  onInteraction(
-    linkElem,
-    async () => {
-      try {
-        // TODO: better confirmation dialog?
-        if(!confirm(t("clear_queue_confirm")))
-          return;
-        const url = new URL(location.href);
-        url.searchParams.delete("list");
-        url.searchParams.set("t", String(await getVideoTime(0)));
-        location.href = String(url);
+      const contBtns = [
+        {
+          condition: scrollToActiveSongBtn,
+          id: "scroll-to-active",
+          resourceName: "icon-skip_to",
+          titleKey: "scroll_to_playing",
+          async interaction() {
+            const activeItem = document.querySelector<HTMLElement>("#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;
+
+            activeItem.scrollIntoView({
+              behavior: "smooth",
+              block: "center",
+              inline: "center",
+            });
+          },
+        },
+        {
+          condition: clearQueueBtn,
+          id: "clear-queue",
+          resourceName: "icon-clear_list",
+          titleKey: "clear_list",
+          async interaction() {
+            try {
+              // TODO: better confirmation dialog?
+              if(!confirm(t("clear_list_confirm")))
+                return;
+              const url = new URL(location.href);
+              url.searchParams.delete("list");
+              url.searchParams.set("t", String(await getVideoTime(0)));
+              location.href = String(url);
+            }
+            catch(err) {
+              error("Couldn't clear queue due to an error:", err);
+            }
+          },
+        },
+      ];
+
+      if(contBtns.some(b => Boolean(b.condition))) {
+        const css = await (await fetchAdvanced(await getResourceUrl("css-above_queue_btns"))).text();
+        css && addStyle(css, "above-queue-btns");
+
+        const wrapperElem = document.createElement("div");
+        wrapperElem.id = "bytm-above-queue-btn-wrapper";
+
+        for(const item of contBtns) {
+          if(Boolean(item.condition) === false)
+            continue;
+
+          const btnElem = await createCircularBtn({
+            resourceName: item.resourceName as ResourceKey,
+            onClick: item.interaction,
+            title: t(item.titleKey),
+          });
+          btnElem.id = `bytm-${item.id}-btn`;
+          btnElem.classList.add("ytmusic-player-bar", "bytm-generic-btn", "bytm-above-queue-btn");
+
+          wrapperElem.appendChild(btnElem);
+        }
+
+        insertBefore(rightBtnsEl, wrapperElem);
       }
-      catch(err) {
-        error("Couldn't clear queue due to an error:", err);
-      }
-    }, {
-      capture: true,
-    }
-  );
-
-  linkElem.appendChild(imgElem);
-  containerElem.appendChild(linkElem);
-  insertBefore(rightBtnsEl, containerElem);
+    },
+  });
 }
 
 //#region thumbnail overlay

+ 1 - 1
src/features/songLists.ts

@@ -85,7 +85,7 @@ async function addQueueButtons(
   classes: string[] = [],
 ) {
   const queueBtnsCont = document.createElement("div");
-  queueBtnsCont.classList.add("bytm-queue-btn-container", ...classes);
+  queueBtnsCont.classList.add(...["bytm-queue-btn-container", ...classes]);
 
   const lyricsIconUrl = await getResourceUrl("icon-lyrics");
   const deleteIconUrl = await getResourceUrl("icon-delete");

+ 4 - 2
src/index.ts

@@ -56,11 +56,10 @@ import {
   console.log();
 }
 
-const domain = getDomain();
-
 /** Stuff that needs to be called ASAP, before anything async happens */
 function preInit() {
   try {
+    const domain = getDomain();
     log("Session ID:", getSessionId());
     initInterface();
     setLogLevel(defaultLogLevel);
@@ -77,6 +76,8 @@ function preInit() {
 
 async function init() {
   try {
+    const domain = getDomain();
+
     const features = await initConfig();
     setLogLevel(features.logLevel);
 
@@ -114,6 +115,7 @@ async function init() {
 
 /** Called when the DOM has finished loading and can be queried and altered by the userscript */
 async function onDomLoad() {
+  const domain = getDomain();
   const features = getFeatures();
   const ftInit = [] as Promise<void>[];
 

+ 5 - 5
src/tools/tr-progress.ts

@@ -98,11 +98,11 @@ ${lines.join("\n")}\n
 
   const banner = `\
 <!--
-    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-    !!             THIS IS A GENERATED FILE             !!
-    !!    all changes will be overwritten next build    !!
-    !! only edit in \`src/tools/tr-progress-template.md\` !!
-    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+  !!!             THIS IS A GENERATED FILE             !!!
+  !!!    all changes will be overwritten next build    !!!
+  !!! only edit in \`src/tools/tr-progress-template.md\` !!!
+  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 -->`;
 
   const progTableHeader = `\

+ 4 - 4
src/utils/dom.ts

@@ -4,7 +4,7 @@ import { addSelectorListener } from "src/observers";
 
 //#region video time, volume
 
-export const videoSelector = getDomain() === "ytm" ? "ytmusic-player video" : "#player-container ytd-player video";
+export const getVideoSelector = () => getDomain() === "ytm" ? "ytmusic-player video" : "#player-container ytd-player video";
 
 /**
  * Returns the current video time in seconds, with the given {@linkcode precision} (2 decimal digits by default).  
@@ -20,7 +20,7 @@ export function getVideoTime(precision = 2) {
 
     try {
       if(domain === "ytm") {
-        const vidElem = document.querySelector<HTMLVideoElement>(videoSelector);
+        const vidElem = document.querySelector<HTMLVideoElement>(getVideoSelector());
         if(vidElem)
           return res(Number(precision <= 0 ? Math.floor(vidElem.currentTime) : vidElem.currentTime.toFixed(precision)));
 
@@ -30,7 +30,7 @@ export function getVideoTime(precision = 2) {
         });
       }
       else if(domain === "yt") {
-        const vidElem = document.querySelector<HTMLVideoElement>(videoSelector);
+        const vidElem = document.querySelector<HTMLVideoElement>(getVideoSelector());
         if(vidElem)
           return res(Number(precision <= 0 ? Math.floor(vidElem.currentTime) : vidElem.currentTime.toFixed(precision)));
 
@@ -108,7 +108,7 @@ function ytForceShowVideoTime() {
 /** Waits for the video element to be in its readyState 4 / canplay state and returns it - resolves immediately if the video is already ready */
 export function waitVideoElementReady(): Promise<HTMLVideoElement> {
   return new Promise((res) => {
-    addSelectorListener<HTMLVideoElement>("body", videoSelector, {
+    addSelectorListener<HTMLVideoElement>("body", getVideoSelector(), {
       listener: async (vidElem) => {
         if(vidElem) {
           // this is just after YT has finished doing their own shenanigans with the video time and volume

+ 5 - 5
src/utils/misc.ts

@@ -7,19 +7,19 @@ import langMapping from "../../assets/locales.json" assert { type: "json" };
 
 //#region misc
 
-let domain: Domain; 
+let cachedDomain: Domain; 
 
 /**
  * Returns the current domain as a constant string representation
  * @throws Throws if script runs on an unexpected website
  */
 export function getDomain(): Domain {
-  if(domain)
-    return domain;
+  if(cachedDomain)
+    return cachedDomain;
   if(location.hostname.match(/^music\.youtube/))
-    return domain = "ytm";
+    return cachedDomain = "ytm";
   else if(location.hostname.match(/youtube\./))
-    return domain = "yt";
+    return cachedDomain = "yt";
   else
     throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
 }