Explorar o código

ref: massive volume feature refactor

Sv443 hai 1 ano
pai
achega
829d8dcdf6

+ 58 - 16
assets/translations/README.md

@@ -20,15 +20,15 @@ To submit or edit a translation, please follow [this guide](../../contributing.m
 ### Translation progress:
 |   | Locale | Translated keys | Based on |
 | :----: | ------ | --------------- | :------: |
-| ─ | [`en_US`](./en_US.json) | 176 (default locale) |  |
-| ‼️ | [`de_DE`](./de_DE.json) | `164/176` (93.2%) | ─ |
-| ─ | [`en_UK`](./en_UK.json) | `176/176` (100%) | `en_US` |
-| ‼️ | [`es_ES`](./es_ES.json) | `164/176` (93.2%) | ─ |
-| ‼️ | [`fr_FR`](./fr_FR.json) | `164/176` (93.2%) | ─ |
-| ‼️ | [`hi_IN`](./hi_IN.json) | `164/176` (93.2%) | ─ |
-| ‼️ | [`ja_JA`](./ja_JA.json) | `164/176` (93.2%) | ─ |
-| ‼️ | [`pt_BR`](./pt_BR.json) | `164/176` (93.2%) | ─ |
-| ‼️ | [`zh_CN`](./zh_CN.json) | `164/176` (93.2%) | ─ |
+| ─ | [`en_US`](./en_US.json) | 180 (default locale) |  |
+| ‼️ | [`de_DE`](./de_DE.json) | `162/180` (90%) | ─ |
+| ─ | [`en_UK`](./en_UK.json) | `180/180` (100%) | `en_US` |
+| ‼️ | [`es_ES`](./es_ES.json) | `162/180` (90%) | ─ |
+| ‼️ | [`fr_FR`](./fr_FR.json) | `162/180` (90%) | ─ |
+| ‼️ | [`hi_IN`](./hi_IN.json) | `162/180` (90%) | ─ |
+| ‼️ | [`ja_JA`](./ja_JA.json) | `162/180` (90%) | ─ |
+| ‼️ | [`pt_BR`](./pt_BR.json) | `162/180` (90%) | ─ |
+| ‼️ | [`zh_CN`](./zh_CN.json) | `162/180` (90%) | ─ |
 
 <sub>
 ✅ - Fully translated
@@ -49,7 +49,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 ### Missing keys:
 
-<details><summary><code>de_DE</code> - 12 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>de_DE</code> - 18 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -61,6 +61,12 @@ This means to figure out which keys are untranslated, you will need to manually
 | `dev_mode` | `Developer mode` |
 | `dev_mode_short` | `Dev` |
 | `advanced_mode_short` | `Advanced` |
+| `feature_category_volume` | `Volume` |
+| `feature_desc_volumeSharedBetweenTabs` | `If the volume level is set in one tab, set it to the same value in others too?` |
+| `feature_helptext_volumeSharedBetweenTabs` | `If you change the volume in one tab, the volume level will be set to the same value in all other tabs that have this feature enabled.\nThis value will be remembered and restored across sessions, until disabled.` |
+| `feature_desc_setInitialTabVolume` | `Sets the volume level to a specific value once when opening the site` |
+| `feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible` | `This feature is incompatible with the "Volume level shared between tabs" feature and will be ignored while using the shared volume feature!` |
+| `feature_desc_initialTabVolumeLevel` | `The value to set the volume level to when opening the site` |
 | `feature_desc_rememberSongTimeDuration` | `How long in seconds to remember the song's time for after it was last played` |
 | `feature_desc_rememberSongTimeReduction` | `How many seconds to subtract when restoring the time of a remembered song` |
 | `feature_helptext_rememberSongTimeReduction` | `When restoring the time of a song that was remembered, this amount of seconds will be subtracted from the remembered time so you can re-listen to the part that was interrupted.` |
@@ -68,7 +74,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>es_ES</code> - 12 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>es_ES</code> - 18 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -80,6 +86,12 @@ This means to figure out which keys are untranslated, you will need to manually
 | `dev_mode` | `Developer mode` |
 | `dev_mode_short` | `Dev` |
 | `advanced_mode_short` | `Advanced` |
+| `feature_category_volume` | `Volume` |
+| `feature_desc_volumeSharedBetweenTabs` | `If the volume level is set in one tab, set it to the same value in others too?` |
+| `feature_helptext_volumeSharedBetweenTabs` | `If you change the volume in one tab, the volume level will be set to the same value in all other tabs that have this feature enabled.\nThis value will be remembered and restored across sessions, until disabled.` |
+| `feature_desc_setInitialTabVolume` | `Sets the volume level to a specific value once when opening the site` |
+| `feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible` | `This feature is incompatible with the "Volume level shared between tabs" feature and will be ignored while using the shared volume feature!` |
+| `feature_desc_initialTabVolumeLevel` | `The value to set the volume level to when opening the site` |
 | `feature_desc_rememberSongTimeDuration` | `How long in seconds to remember the song's time for after it was last played` |
 | `feature_desc_rememberSongTimeReduction` | `How many seconds to subtract when restoring the time of a remembered song` |
 | `feature_helptext_rememberSongTimeReduction` | `When restoring the time of a song that was remembered, this amount of seconds will be subtracted from the remembered time so you can re-listen to the part that was interrupted.` |
@@ -87,7 +99,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>fr_FR</code> - 12 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>fr_FR</code> - 18 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -99,6 +111,12 @@ This means to figure out which keys are untranslated, you will need to manually
 | `dev_mode` | `Developer mode` |
 | `dev_mode_short` | `Dev` |
 | `advanced_mode_short` | `Advanced` |
+| `feature_category_volume` | `Volume` |
+| `feature_desc_volumeSharedBetweenTabs` | `If the volume level is set in one tab, set it to the same value in others too?` |
+| `feature_helptext_volumeSharedBetweenTabs` | `If you change the volume in one tab, the volume level will be set to the same value in all other tabs that have this feature enabled.\nThis value will be remembered and restored across sessions, until disabled.` |
+| `feature_desc_setInitialTabVolume` | `Sets the volume level to a specific value once when opening the site` |
+| `feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible` | `This feature is incompatible with the "Volume level shared between tabs" feature and will be ignored while using the shared volume feature!` |
+| `feature_desc_initialTabVolumeLevel` | `The value to set the volume level to when opening the site` |
 | `feature_desc_rememberSongTimeDuration` | `How long in seconds to remember the song's time for after it was last played` |
 | `feature_desc_rememberSongTimeReduction` | `How many seconds to subtract when restoring the time of a remembered song` |
 | `feature_helptext_rememberSongTimeReduction` | `When restoring the time of a song that was remembered, this amount of seconds will be subtracted from the remembered time so you can re-listen to the part that was interrupted.` |
@@ -106,7 +124,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>hi_IN</code> - 12 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>hi_IN</code> - 18 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -118,6 +136,12 @@ This means to figure out which keys are untranslated, you will need to manually
 | `dev_mode` | `Developer mode` |
 | `dev_mode_short` | `Dev` |
 | `advanced_mode_short` | `Advanced` |
+| `feature_category_volume` | `Volume` |
+| `feature_desc_volumeSharedBetweenTabs` | `If the volume level is set in one tab, set it to the same value in others too?` |
+| `feature_helptext_volumeSharedBetweenTabs` | `If you change the volume in one tab, the volume level will be set to the same value in all other tabs that have this feature enabled.\nThis value will be remembered and restored across sessions, until disabled.` |
+| `feature_desc_setInitialTabVolume` | `Sets the volume level to a specific value once when opening the site` |
+| `feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible` | `This feature is incompatible with the "Volume level shared between tabs" feature and will be ignored while using the shared volume feature!` |
+| `feature_desc_initialTabVolumeLevel` | `The value to set the volume level to when opening the site` |
 | `feature_desc_rememberSongTimeDuration` | `How long in seconds to remember the song's time for after it was last played` |
 | `feature_desc_rememberSongTimeReduction` | `How many seconds to subtract when restoring the time of a remembered song` |
 | `feature_helptext_rememberSongTimeReduction` | `When restoring the time of a song that was remembered, this amount of seconds will be subtracted from the remembered time so you can re-listen to the part that was interrupted.` |
@@ -125,7 +149,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>ja_JA</code> - 12 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>ja_JA</code> - 18 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -137,6 +161,12 @@ This means to figure out which keys are untranslated, you will need to manually
 | `dev_mode` | `Developer mode` |
 | `dev_mode_short` | `Dev` |
 | `advanced_mode_short` | `Advanced` |
+| `feature_category_volume` | `Volume` |
+| `feature_desc_volumeSharedBetweenTabs` | `If the volume level is set in one tab, set it to the same value in others too?` |
+| `feature_helptext_volumeSharedBetweenTabs` | `If you change the volume in one tab, the volume level will be set to the same value in all other tabs that have this feature enabled.\nThis value will be remembered and restored across sessions, until disabled.` |
+| `feature_desc_setInitialTabVolume` | `Sets the volume level to a specific value once when opening the site` |
+| `feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible` | `This feature is incompatible with the "Volume level shared between tabs" feature and will be ignored while using the shared volume feature!` |
+| `feature_desc_initialTabVolumeLevel` | `The value to set the volume level to when opening the site` |
 | `feature_desc_rememberSongTimeDuration` | `How long in seconds to remember the song's time for after it was last played` |
 | `feature_desc_rememberSongTimeReduction` | `How many seconds to subtract when restoring the time of a remembered song` |
 | `feature_helptext_rememberSongTimeReduction` | `When restoring the time of a song that was remembered, this amount of seconds will be subtracted from the remembered time so you can re-listen to the part that was interrupted.` |
@@ -144,7 +174,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>pt_BR</code> - 12 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>pt_BR</code> - 18 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -156,6 +186,12 @@ This means to figure out which keys are untranslated, you will need to manually
 | `dev_mode` | `Developer mode` |
 | `dev_mode_short` | `Dev` |
 | `advanced_mode_short` | `Advanced` |
+| `feature_category_volume` | `Volume` |
+| `feature_desc_volumeSharedBetweenTabs` | `If the volume level is set in one tab, set it to the same value in others too?` |
+| `feature_helptext_volumeSharedBetweenTabs` | `If you change the volume in one tab, the volume level will be set to the same value in all other tabs that have this feature enabled.\nThis value will be remembered and restored across sessions, until disabled.` |
+| `feature_desc_setInitialTabVolume` | `Sets the volume level to a specific value once when opening the site` |
+| `feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible` | `This feature is incompatible with the "Volume level shared between tabs" feature and will be ignored while using the shared volume feature!` |
+| `feature_desc_initialTabVolumeLevel` | `The value to set the volume level to when opening the site` |
 | `feature_desc_rememberSongTimeDuration` | `How long in seconds to remember the song's time for after it was last played` |
 | `feature_desc_rememberSongTimeReduction` | `How many seconds to subtract when restoring the time of a remembered song` |
 | `feature_helptext_rememberSongTimeReduction` | `When restoring the time of a song that was remembered, this amount of seconds will be subtracted from the remembered time so you can re-listen to the part that was interrupted.` |
@@ -163,7 +199,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>zh_CN</code> - 12 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>zh_CN</code> - 18 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -175,6 +211,12 @@ This means to figure out which keys are untranslated, you will need to manually
 | `dev_mode` | `Developer mode` |
 | `dev_mode_short` | `Dev` |
 | `advanced_mode_short` | `Advanced` |
+| `feature_category_volume` | `Volume` |
+| `feature_desc_volumeSharedBetweenTabs` | `If the volume level is set in one tab, set it to the same value in others too?` |
+| `feature_helptext_volumeSharedBetweenTabs` | `If you change the volume in one tab, the volume level will be set to the same value in all other tabs that have this feature enabled.\nThis value will be remembered and restored across sessions, until disabled.` |
+| `feature_desc_setInitialTabVolume` | `Sets the volume level to a specific value once when opening the site` |
+| `feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible` | `This feature is incompatible with the "Volume level shared between tabs" feature and will be ignored while using the shared volume feature!` |
+| `feature_desc_initialTabVolumeLevel` | `The value to set the volume level to when opening the site` |
 | `feature_desc_rememberSongTimeDuration` | `How long in seconds to remember the song's time for after it was last played` |
 | `feature_desc_rememberSongTimeReduction` | `How many seconds to subtract when restoring the time of a remembered song` |
 | `feature_helptext_rememberSongTimeReduction` | `When restoring the time of a song that was remembered, this amount of seconds will be subtracted from the remembered time so you can re-listen to the part that was interrupted.` |

+ 0 - 2
assets/translations/de_DE.json

@@ -143,8 +143,6 @@
     "feature_helptext_rememberSongTime-1": "Manchmal möchtest du nach dem Neuladen der Seite oder nach dem versehentlichen Schließen an derselben Stelle weiterhören. Diese Funktion ermöglicht es dir, das zu tun.\nUm die Zeit des Songs zu speichern, musst du ihn %1 Sekunde lang abspielen, dann wird die Zeit gespeichert und für kurze Zeit wiederherstellbar sein.",
     "feature_helptext_rememberSongTime-n": "Manchmal möchtest du nach dem Neuladen der Seite oder nach dem versehentlichen Schließen an derselben Stelle weiterhören. Diese Funktion ermöglicht es dir, das zu tun.\nUm die Zeit des Songs zu speichern, musst du ihn %1 Sekunden lang abspielen, dann wird die Zeit gespeichert und für kurze Zeit wiederherstellbar sein.",
     "feature_desc_rememberSongTimeSites": "Auf welchen Seiten soll die Songzeit gespeichert und wiederhergestellt werden?",
-    "feature_desc_lockVolume": "Erzwinge, dass der Lautstärkeregler auf einem bestimmten Level bleibt",
-    "feature_desc_lockVolumeLevel": "Auf welchem Lautstärkelevel der Lautstärkeregler gesperrt werden soll",
 
     "feature_desc_arrowKeySupport": "Benutze die Pfeiltasten um vor- und zurückzuspulen",
     "feature_helptext_arrowKeySupport": "Normalerweise kannst du nur in 10 Sekunden Schritten vor- und zurückspulen, indem du die Tasten \"H\" und \"L\" benutzt. Diese Funktion ermöglicht es dir, auch die Pfeiltasten zu benutzen.\nUm die Anzahl der Sekunden zu ändern, um die gespult werden soll, benutze die Option unten.",

+ 13 - 8
assets/translations/en_US.json

@@ -117,18 +117,13 @@
     "unit_days-n": "days",
 
     "feature_category_layout": "Layout",
+    "feature_category_volume": "Volume",
     "feature_category_songLists": "Song Lists",
     "feature_category_behavior": "Behavior",
     "feature_category_input": "Input",
     "feature_category_lyrics": "Lyrics",
     "feature_category_general": "General",
 
-    "feature_desc_removeUpgradeTab": "Remove the Upgrade / Premium tab",
-    "feature_desc_volumeSliderLabel": "Add a percentage label next to the volume slider",
-    "feature_desc_volumeSliderSize": "The width of the volume slider in pixels",
-    "feature_desc_volumeSliderStep": "Volume slider sensitivity (by how little percent the volume can be changed at a time)",
-    "feature_desc_volumeSliderScrollStep": "Volume slider scroll wheel sensitivity in percent - snaps to the nearest sensitivity value from above",
-    "feature_helptext_volumeSliderScrollStep": "By how much percent the volume should be changed when scrolling the volume slider with the mouse wheel.\nThis should be a multiple of the volume slider sensitivity, otherwise there will be small irregular jumps in the volume when scrolling.",
     "feature_desc_watermarkEnabled": "Show a watermark under the site logo that opens this config menu",
     "feature_helptext_watermarkEnabled": "If this is disabled, you can still open the config menu by clicking the option in the menu that opens when you click your profile picture in the top right corner.\nHowever it will be harder to find the easter egg ;)",
     "feature_desc_removeShareTrackingParam": "Remove the tracking parameter \"&si\" from links in the share popup",
@@ -137,6 +132,18 @@
     "feature_desc_fixSpacing": "Fix spacing issues in the layout",
     "feature_helptext_fixSpacing": "There are various locations in the user interface where the spacing between elements is inconsistent. This feature fixes those issues.",
 
+    "feature_desc_removeUpgradeTab": "Remove the Upgrade / Premium tab",
+    "feature_desc_volumeSliderLabel": "Add a percentage label next to the volume slider",
+    "feature_desc_volumeSliderSize": "The width of the volume slider in pixels",
+    "feature_desc_volumeSliderStep": "Volume slider sensitivity (by how little percent the volume can be changed at a time)",
+    "feature_desc_volumeSliderScrollStep": "Volume slider scroll wheel sensitivity in percent - snaps to the nearest sensitivity value from above",
+    "feature_helptext_volumeSliderScrollStep": "By how much percent the volume should be changed when scrolling the volume slider with the mouse wheel.\nThis should be a multiple of the volume slider sensitivity, otherwise there will be small irregular jumps in the volume when scrolling.",
+    "feature_desc_volumeSharedBetweenTabs": "If the volume level is set in one tab, set it to the same value in others too?",
+    "feature_helptext_volumeSharedBetweenTabs": "If you change the volume in one tab, the volume level will be set to the same value in all other tabs that have this feature enabled.\nThis value will be remembered and restored across sessions, until disabled.",
+    "feature_desc_setInitialTabVolume": "Sets the volume level to a specific value once when opening the site",
+    "feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible": "This feature is incompatible with the \"Volume level shared between tabs\" feature and will be ignored while using the shared volume feature!",
+    "feature_desc_initialTabVolumeLevel": "The value to set the volume level to when opening the site",
+
     "feature_desc_lyricsQueueButton": "Add a button to each song in a queue to quickly open its lyrics page",
     "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?",
@@ -155,8 +162,6 @@
     "feature_desc_rememberSongTimeReduction": "How many seconds to subtract when restoring the time of a remembered song",
     "feature_helptext_rememberSongTimeReduction": "When restoring the time of a song that was remembered, this amount of seconds will be subtracted from the remembered time so you can re-listen to the part that was interrupted.",
     "feature_desc_rememberSongTimeMinPlayTime": "Minimum amount of seconds a song needs to be played for its time to be remembered",
-    "feature_desc_lockVolume": "Force the volume slider to stay at a specific level",
-    "feature_desc_lockVolumeLevel": "What volume level to lock the volume slider at",
 
     "feature_desc_arrowKeySupport": "Use arrow keys to skip forwards and backwards in the currently playing song",
     "feature_helptext_arrowKeySupport": "Normally you can only skip forwards and backwards by a fixed 10 second interval with the keys \"H\" and \"L\". This feature allows you to use the arrow keys too.\nTo change the amount of seconds to skip, use the option below.",

+ 0 - 2
assets/translations/es_ES.json

@@ -143,8 +143,6 @@
     "feature_helptext_rememberSongTime-1": "A veces, al volver a cargar la página o restaurarla después de cerrarla accidentalmente, desea reanudar la escucha en el mismo punto. Esta función le permite hacer eso.\nPara registrar el tiempo de la canción, debe reproducirla durante %1 segundo, luego su tiempo se recordará y se podrá restaurar por un corto tiempo.",
     "feature_helptext_rememberSongTime-n": "A veces, al volver a cargar la página o restaurarla después de cerrarla accidentalmente, desea reanudar la escucha en el mismo punto. Esta función le permite hacer eso.\nPara registrar el tiempo de la canción, debe reproducirla durante %1 segundos, luego su tiempo se recordará y se podrá restaurar por un corto tiempo.",
     "feature_desc_rememberSongTimeSites": "¿En qué sitios se debe recordar y restaurar el tiempo de la canción?",
-    "feature_desc_lockVolume": "Forzar el control deslizante de volumen para que se mantenga en un nivel específico",
-    "feature_desc_lockVolumeLevel": "A qué nivel de volumen bloquear el control deslizante de volumen",
 
     "feature_desc_arrowKeySupport": "Use las teclas de flecha para saltar hacia adelante y hacia atrás en la canción que se está reproduciendo actualmente",
     "feature_helptext_arrowKeySupport": "Normalmente solo puede saltar hacia adelante y hacia atrás en un intervalo fijo de 10 segundos con las teclas \"H\" y \"L\". Esta función le permite usar las teclas de flecha también.\nPara cambiar la cantidad de segundos para saltar, use la opción a continuación.",

+ 0 - 2
assets/translations/fr_FR.json

@@ -143,8 +143,6 @@
     "feature_helptext_rememberSongTime-1": "Parfois, lorsque vous rechargez la page ou la restaurez après l'avoir fermée accidentellement, vous voulez reprendre l'écoute au même point. Cette fonctionnalité vous permet de le faire.\nPour enregistrer le temps de la chanson, vous devez la lire pendant %1 seconde, puis son temps sera mémorisé et pourra être restauré pendant un court instant.",
     "feature_helptext_rememberSongTime-n": "Parfois, lorsque vous rechargez la page ou la restaurez après l'avoir fermée accidentellement, vous voulez reprendre l'écoute au même point. Cette fonctionnalité vous permet de le faire.\nPour enregistrer le temps de la chanson, vous devez la lire pendant %1 secondes, puis son temps sera mémorisé et pourra être restauré pendant un court instant.",
     "feature_desc_rememberSongTimeSites": "Sur quels sites le temps de la chanson doit-il être mémorisé et restauré?",
-    "feature_desc_lockVolume": "Forcer le curseur de volume à rester à un niveau spécifique",
-    "feature_desc_lockVolumeLevel": "À quel niveau de volume verrouiller le curseur de volume",
 
     "feature_desc_arrowKeySupport": "Utilisez les touches fléchées pour avancer et reculer dans la chanson en cours de lecture",
     "feature_helptext_arrowKeySupport": "Normalement, vous ne pouvez avancer et reculer que par intervalles fixes de 10 secondes avec les touches \"H\" et \"L\". Cette fonctionnalité vous permet d'utiliser aussi les touches fléchées.\nPour changer le nombre de secondes à sauter, utilisez l'option ci-dessous.",

+ 0 - 2
assets/translations/hi_IN.json

@@ -143,8 +143,6 @@
     "feature_helptext_rememberSongTime-1": "कभी-कभी पृष्ठ को फिर से लोड करने या उसे अनजाने में बंद करने के बाद, आप चाहते हैं कि आप वही समय पर सुनना जारी रखें। यह सुविधा आपको इसे करने की अनुमति देती है।\nगीत का समय याद करने और बहाल करने के लिए, आपको इसे %1 सेकंड तक चलाना होगा, फिर इसका समय याद किया जाएगा और थोड़ी देर के लिए बहाल किया जा सकता है।",
     "feature_helptext_rememberSongTime-n": "कभी-कभी पृष्ठ को फिर से लोड करने या उसे अनजाने में बंद करने के बाद, आप चाहते हैं कि आप वही समय पर सुनना जारी रखें। यह सुविधा आपको इसे करने की अनुमति देती है।\nगीत का समय याद करने और बहाल करने के लिए, आपको इसे %1 सेकंड तक चलाना होगा, फिर इसका समय याद किया जाएगा और थोड़ी देर के लिए बहाल किया जा सकता है।",
     "feature_desc_rememberSongTimeSites": "गीत का समय किन साइटों पर याद रखें और बहाल करें?",
-    "feature_desc_lockVolume": "वॉल्यूम स्लाइडर को एक निश्चित स्तर पर बंद करें",
-    "feature_desc_lockVolumeLevel": "वॉल्यूम स्लाइडर को किस स्तर पर बंद करना चाहिए",
 
     "feature_desc_arrowKeySupport": "वर्तमान में चल रहे गीत के मीडिया नियंत्रणों में एक बटन जो एरो कुंजियों का समर्थन करता है",
     "feature_helptext_arrowKeySupport": "सामान्य रूप से आप केवल बाएं और दाएं तीर कुंजियों का उपयोग करके एक निश्चित 10 सेकंड के अंतराल में छोड़ सकते हैं। इस सुविधा की मदद से आप तीर कुंजियों का उपयोग कर सकते हैं।\nछोड़ने के लिए सेकंडों की मात्रा बदलने के लिए, नीचे दिए गए विकल्प का उपयोग करें।",

+ 0 - 2
assets/translations/ja_JA.json

@@ -143,8 +143,6 @@
     "feature_helptext_rememberSongTime-1": "ページを再読み込みしたり、誤って閉じたりして復元したときに、同じ場所で聞き直したいことがあります。この機能を使用すると、それが可能になります。\n曲の時間を記録するには、%1 秒再生する必要があります。その後、その時間が記憶され、しばらくの間復元可能になります。",
     "feature_helptext_rememberSongTime-n": "ページを再読み込みしたり、誤って閉じたりして復元したときに、同じ場所で聞き直したいことがあります。この機能を使用すると、それが可能になります。\n曲の時間を記録するには、%1 秒再生する必要があります。その後、その時間が記憶され、しばらくの間復元可能になります。",
     "feature_desc_rememberSongTimeSites": "曲の時間を記憶して復元するサイトはどこですか?",
-    "feature_desc_lockVolume": "音量スライダーを特定のレベルにロックする",
-    "feature_desc_lockVolumeLevel": "音量スライダーをロックする音量レベル",
 
     "feature_desc_arrowKeySupport": "現在再生中の曲で前後にスキップするには矢印キーを使用する",
     "feature_helptext_arrowKeySupport": "通常、キー \"H\" と \"L\" を使用して 10 秒間隔で前後にスキップすることができます。この機能を使用すると、矢印キーも使用できます。\nスキップする秒数を変更するには、以下のオプションを使用してください。",

+ 0 - 2
assets/translations/pt_BR.json

@@ -143,8 +143,6 @@
     "feature_helptext_rememberSongTime-1": "Às vezes, ao recarregar a página ou restaurá-la após fechá-la acidentalmente, você deseja retomar a audição no mesmo ponto. Este recurso permite que você faça isso.\nPara registrar o tempo da música, você precisa reproduzi-la por %1 segundo, então seu tempo será lembrado e restaurável por um curto período.",
     "feature_helptext_rememberSongTime-n": "Às vezes, ao recarregar a página ou restaurá-la após fechá-la acidentalmente, você deseja retomar a audição no mesmo ponto. Este recurso permite que você faça isso.\nPara registrar o tempo da música, você precisa reproduzi-la por %1 segundos, então seu tempo será lembrado e restaurável por um curto período.",
     "feature_desc_rememberSongTimeSites": "Em quais sites o tempo da música deve ser lembrado e restaurado?",
-    "feature_desc_lockVolume": "Forçar o controle deslizante de volume a permanecer em um nível específico",
-    "feature_desc_lockVolumeLevel": "Em que nível de volume bloquear o controle deslizante?",
 
     "feature_desc_arrowKeySupport": "Use as teclas de seta para pular para a próxima ou anterior música na fila",
     "feature_helptext_arrowKeySupport": "Normalmente, você só pode pular para frente e para trás por um intervalo fixo de 10 segundos com as teclas \"H\" e \"L\". Este recurso permite que você use as teclas de seta também.\nPara alterar a quantidade de segundos a pular, use a opção abaixo.",

+ 0 - 2
assets/translations/zh_CN.json

@@ -143,8 +143,6 @@
     "feature_helptext_rememberSongTime-1": "有时在重新加载页面或意外关闭后恢复它时,您希望在相同的位置继续听歌。此功能允许您这样做。\n为了记录歌曲的时间,您需要播放它 %1 秒,然后它的时间将被记住,并在短时间内可以恢复。",
     "feature_helptext_rememberSongTime-n": "有时在重新加载页面或意外关闭后恢复它时,您希望在相同的位置继续听歌。此功能允许您这样做。\n为了记录歌曲的时间,您需要播放它 %1 秒,然后它的时间将被记住,并在短时间内可以恢复。",
     "feature_desc_rememberSongTimeSites": "在哪些网站上应该记住和恢复歌曲时间?",
-    "feature_desc_lockVolume": "强制音量滑块保持在特定级别",
-    "feature_desc_lockVolumeLevel": "将音量滑块锁定在哪个音量级别?",
 
     "feature_desc_arrowKeySupport": "使用箭头键在当前播放的歌曲中前进和后退",
     "feature_helptext_arrowKeySupport": "通常,您只能使用 \"H\" 和 \"L\" 键以固定的 10 秒间隔前进和后退。此功能允许您也使用箭头键。\n要更改要跳过的秒数,请使用下面的选项。",

+ 322 - 298
dist/BetterYTM.user.js

@@ -17,7 +17,7 @@
 // @license           AGPL-3.0-only
 // @author            Sv443
 // @copyright         Sv443 (https://github.com/Sv443)
-// @icon              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=d0ddcd5
+// @icon              http://localhost:8710/assets/images/logo/logo_48.png?b=e29c6443-e251-4862-b5b7-00defc20568b
 // @match             https://music.youtube.com/*
 // @match             https://www.youtube.com/*
 // @run-at            document-start
@@ -34,36 +34,36 @@
 // @grant             GM.xmlHttpRequest
 // @grant             unsafeWindow
 // @noframes
-// @resource          css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css?b=d0ddcd5
-// @resource          css-fix_spacing         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css?b=d0ddcd5
-// @resource          doc-changelog           https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md?b=d0ddcd5
-// @resource          icon-advanced_mode      https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/plus_circle_small.svg?b=d0ddcd5
-// @resource          icon-arrow_down         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg?b=d0ddcd5
-// @resource          icon-delete             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg?b=d0ddcd5
-// @resource          icon-error              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg?b=d0ddcd5
-// @resource          icon-experimental       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/beaker_small.svg?b=d0ddcd5
-// @resource          icon-globe              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg?b=d0ddcd5
-// @resource          icon-help               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg?b=d0ddcd5
-// @resource          icon-lock               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lock.svg?b=d0ddcd5
-// @resource          icon-lyrics             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg?b=d0ddcd5
-// @resource          icon-skip_to            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg?b=d0ddcd5
-// @resource          icon-spinner            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg?b=d0ddcd5
-// @resource          img-logo                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=d0ddcd5
-// @resource          img-close               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/close.png?b=d0ddcd5
-// @resource          img-discord             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/discord.png?b=d0ddcd5
-// @resource          img-github              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/github.png?b=d0ddcd5
-// @resource          img-greasyfork          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/greasyfork.png?b=d0ddcd5
-// @resource          img-openuserjs          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/openuserjs.png?b=d0ddcd5
-// @resource          trans-de_DE             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json?b=d0ddcd5
-// @resource          trans-en_US             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json?b=d0ddcd5
-// @resource          trans-en_UK             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json?b=d0ddcd5
-// @resource          trans-es_ES             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json?b=d0ddcd5
-// @resource          trans-fr_FR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json?b=d0ddcd5
-// @resource          trans-hi_IN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json?b=d0ddcd5
-// @resource          trans-ja_JA             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json?b=d0ddcd5
-// @resource          trans-pt_BR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json?b=d0ddcd5
-// @resource          trans-zh_CN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json?b=d0ddcd5
-// @require           https://cdn.jsdelivr.net/npm/@sv443-network/userutils@5.0.1/dist/index.global.js
+// @resource          css-anchor_improvements http://localhost:8710/assets/style/anchorImprovements.css?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          css-fix_spacing         http://localhost:8710/assets/style/fixSpacing.css?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          doc-changelog           http://localhost:8710/changelog.md?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-advanced_mode      http://localhost:8710/assets/icons/plus_circle_small.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-arrow_down         http://localhost:8710/assets/icons/arrow_down.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-delete             http://localhost:8710/assets/icons/delete.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-error              http://localhost:8710/assets/icons/error.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-experimental       http://localhost:8710/assets/icons/beaker_small.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-globe              http://localhost:8710/assets/icons/globe.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-help               http://localhost:8710/assets/icons/help.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-lock               http://localhost:8710/assets/icons/lock.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-lyrics             http://localhost:8710/assets/icons/lyrics.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-skip_to            http://localhost:8710/assets/icons/skip_to.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          icon-spinner            http://localhost:8710/assets/icons/spinner.svg?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          img-logo                http://localhost:8710/assets/images/logo/logo_48.png?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          img-close               http://localhost:8710/assets/images/close.png?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          img-discord             http://localhost:8710/assets/images/external/discord.png?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          img-github              http://localhost:8710/assets/images/external/github.png?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          img-greasyfork          http://localhost:8710/assets/images/external/greasyfork.png?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          img-openuserjs          http://localhost:8710/assets/images/external/openuserjs.png?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-de_DE             http://localhost:8710/assets/translations/de_DE.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-en_US             http://localhost:8710/assets/translations/en_US.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-en_UK             http://localhost:8710/assets/translations/en_UK.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-es_ES             http://localhost:8710/assets/translations/es_ES.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-fr_FR             http://localhost:8710/assets/translations/fr_FR.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-hi_IN             http://localhost:8710/assets/translations/hi_IN.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-ja_JA             http://localhost:8710/assets/translations/ja_JA.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-pt_BR             http://localhost:8710/assets/translations/pt_BR.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @resource          trans-zh_CN             http://localhost:8710/assets/translations/zh_CN.json?b=e29c6443-e251-4862-b5b7-00defc20568b
+// @require           https://cdn.jsdelivr.net/npm/@sv443-network/userutils@6.0.0/dist/index.global.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.basic.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.js
 // @grant             GM.registerMenuCommand
@@ -252,7 +252,7 @@ var LogLevel;
 })(LogLevel || (LogLevel = {}));const modeRaw = "development";
 const branchRaw = "develop";
 const hostRaw = "github";
-const buildNumberRaw = "d0ddcd5";
+const buildNumberRaw = "be7e60f";
 /** 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 */
@@ -265,6 +265,18 @@ const host = (hostRaw.match(/^#{{.+}}$/) ? "github" : hostRaw);
 const buildNumber = (buildNumberRaw.match(/^#{{.+}}$/) ? "BUILD_ERROR!" : buildNumberRaw); // asserted as generic string instead of literal
 /** Default compression format used throughout BYTM */
 const compressionFormat = "deflate-raw";
+typeof sessionStorage !== "undefined"
+    && (() => {
+        try {
+            const key = `_bytm_${UserUtils.randomId(4)}`;
+            sessionStorage.setItem(key, "test");
+            sessionStorage.removeItem(key);
+            return true;
+        }
+        catch (_a) {
+            return false;
+        }
+    })();
 /**
  * How much info should be logged to the devtools console
  * 0 = Debug (show everything) or 1 = Info (show only important stuff)
@@ -416,9 +428,9 @@ const maxViewedPenalty = 1000 * 60 * 60 * 24 * 5; // 5 days
 /** A fraction of this max value will be removed from the "added" timestamp when adding penalized cache entries */
 const maxAddedPenalty = 1000 * 60 * 60 * 24 * 15; // 15 days
 let canCompress$1 = true;
-const lyricsCacheMgr = new UserUtils.ConfigManager({
+const lyricsCacheMgr = new UserUtils.DataStore({
     id: "bytm-lyrics-cache",
-    defaultConfig: {
+    defaultData: {
         cache: [],
     },
     formatVersion: 1,
@@ -1208,7 +1220,7 @@ var updates = {
 	openuserjs: "https://openuserjs.org/scripts/Sv443/BetterYTM"
 };
 var dependencies = {
-	"@sv443-network/userutils": "^5.0.1",
+	"@sv443-network/userutils": "^6.0.0",
 	"fuse.js": "^7.0.0",
 	marked: "^12.0.0",
 	nanoevents: "^9.0.0"
@@ -1447,6 +1459,176 @@ function compareVersions(a, b) {
             return -1;
     }
     return 0;
+}//#MARKER init
+/** Initializes all volume-related features */
+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) => __awaiter(this, void 0, void 0, function* () {
+                const volSliderCont = document.createElement("div");
+                volSliderCont.id = "bytm-vol-slider-cont";
+                if (getFeatures().volumeSliderScrollStep !== featInfo.volumeSliderScrollStep.default)
+                    initScrollStep(volSliderCont, sliderElem);
+                UserUtils.addParent(sliderElem, volSliderCont);
+                if (typeof getFeatures().volumeSliderSize === "number")
+                    setVolSliderSize();
+                if (getFeatures().volumeSliderLabel)
+                    yield addVolumeSliderLabel(sliderElem, volSliderCont);
+                setVolSliderStep(sliderElem);
+                if (getFeatures().volumeSharedBetweenTabs) {
+                    sliderElem.addEventListener("change", () => sharedVolumeChanged(Number(sliderElem.value)));
+                    setInterval(checkSharedVolume, 500);
+                    checkSharedVolume();
+                }
+                if (getFeatures().setInitialTabVolume)
+                    setInitialTabVolume(sliderElem);
+            }),
+        });
+    });
+}
+//#MARKER scroll step
+/** Initializes the volume slider scroll step features */
+function initScrollStep(volSliderCont, sliderElem) {
+    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) + (getFeatures().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
+//#SECTION label
+/** Adds a percentage label to the volume slider and tooltip */
+function addVolumeSliderLabel(sliderElem, sliderContainer) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const labelContElem = document.createElement("div");
+        labelContElem.id = "bytm-vol-slider-label";
+        const getLabel = (value) => `${value}%`;
+        const labelElem = document.createElement("div");
+        labelElem.classList.add("label");
+        labelElem.textContent = getLabel(sliderElem.value);
+        // prevent video from minimizing
+        labelContElem.addEventListener("click", (e) => e.stopPropagation());
+        const getLabelText = (slider) => { var _a; return t("volume_tooltip", slider.value, (_a = getFeatures().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 = () => {
+            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 div.label");
+            if (labelElem2)
+                labelElem2.textContent = getLabel(sliderElem.value);
+        };
+        sliderElem.addEventListener("change", () => updateLabel());
+        siteEvents.on("configChanged", () => {
+            updateLabel();
+        });
+        onSelectorOld("#bytm-vol-slider-cont", {
+            listener: (volumeCont) => {
+                labelContElem.appendChild(labelElem);
+                volumeCont.appendChild(labelContElem);
+            },
+        });
+        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)
+                labelContElem.classList.add("bytm-visible");
+            else if (labelContElem.classList.contains("bytm-visible") || document.activeElement !== sliderElem)
+                labelContElem.classList.remove("bytm-visible");
+            if (Number(sliderElem.value) !== lastSliderVal) {
+                lastSliderVal = Number(sliderElem.value);
+                updateLabel();
+            }
+        });
+        sliderHoverObserver.observe(sliderElem, {
+            attributes: true,
+        });
+    });
+}
+//#SECTION size
+/** Sets the volume slider to a set size */
+function setVolSliderSize() {
+    const { volumeSliderSize: size } = getFeatures();
+    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";
+}
+//#SECTION step
+/** Sets the `step` attribute of the volume slider */
+function setVolSliderStep(sliderElem) {
+    sliderElem.setAttribute("step", String(getFeatures().volumeSliderStep));
+}
+//#MARKER shared volume
+/** Saves the shared volume level to persistent storage */
+function sharedVolumeChanged(vol) {
+    return __awaiter(this, void 0, void 0, function* () {
+        try {
+            yield GM.setValue("bytm-shared-volume", String(vol));
+        }
+        catch (err) {
+            error("Couldn't save shared volume level due to an error:", err);
+        }
+    });
+}
+let ignoreVal = -1;
+let lastCheckedSharedVolume = -1;
+/** Checks if the shared volume has changed and updates the volume slider accordingly */
+function checkSharedVolume() {
+    return __awaiter(this, void 0, void 0, function* () {
+        try {
+            const vol = yield GM.getValue("bytm-shared-volume");
+            console.log(">checkshared", vol, lastCheckedSharedVolume);
+            if (vol && lastCheckedSharedVolume !== Number(vol)) {
+                if (ignoreVal === Number(vol))
+                    return;
+                lastCheckedSharedVolume = Number(vol);
+                const sliderElem = document.querySelector("tp-yt-paper-slider#volume-slider");
+                if (sliderElem) {
+                    sliderElem.value = String(vol);
+                    sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
+                }
+            }
+        }
+        catch (err) {
+            error("Couldn't check for shared volume level due to an error:", err);
+        }
+    });
+}
+function volumeSharedBetweenTabsDisabled() {
+    return __awaiter(this, void 0, void 0, function* () {
+        yield GM.deleteValue("bytm-shared-volume");
+    });
+}
+//#MARKER initial volume
+/** Sets the volume slider to a set volume level when the session starts */
+function setInitialTabVolume(sliderElem) {
+    const initialVol = getFeatures().initialTabVolumeLevel;
+    if (getFeatures().volumeSharedBetweenTabs) {
+        lastCheckedSharedVolume = ignoreVal = initialVol;
+    }
+    sliderElem.value = String(initialVol);
+    sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
 }//#MARKER create menu elements
 let isCfgMenuAdded = false;
 let isCfgMenuOpen = false;
@@ -2618,11 +2800,7 @@ function openChangelogMenu(returnTo = "cfgMenu") {
         menuBg.style.visibility = "visible";
         menuBg.style.display = "block";
     });
-}let features$2;
-function setLayoutConfig(feats) {
-    features$2 = feats;
-}
-//#MARKER BYTM-Config buttons
+}//#MARKER BYTM-Config buttons
 let logoExchanged = false, improveLogoCalled = false;
 /** Adds a watermark beneath the logo */
 function addWatermark() {
@@ -2749,125 +2927,6 @@ function removeUpgradeTab() {
         });
     });
 }
-//#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) => __awaiter(this, void 0, void 0, function* () {
-                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)
-                    yield addVolumeSliderLabel(sliderElem, volSliderCont);
-                setVolSliderStep(sliderElem);
-            }),
-        });
-    });
-}
-/** Adds a percentage label to the volume slider and tooltip */
-function addVolumeSliderLabel(sliderElem, sliderContainer) {
-    return __awaiter(this, void 0, void 0, function* () {
-        const labelContElem = document.createElement("div");
-        labelContElem.id = "bytm-vol-slider-label";
-        const getLabel = (value) => `${getFeatures().lockVolume ? getFeatures().lockVolumeLevel : value}%`;
-        const labelElem = document.createElement("div");
-        labelElem.classList.add("label");
-        labelElem.textContent = getLabel(sliderElem.value);
-        // prevent video from minimizing
-        labelContElem.addEventListener("click", (e) => e.stopPropagation());
-        const getLabelText = (slider) => { var _a; return t("volume_tooltip", getFeatures().lockVolume ? getFeatures().lockVolumeLevel : 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 = () => {
-            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 div.label");
-            if (labelElem2)
-                labelElem2.textContent = getLabel(sliderElem.value);
-        };
-        let lockIconElem;
-        const lockIconHtml = yield resourceToHTMLString("icon-lock");
-        if (getFeatures().lockVolume && lockIconHtml) {
-            lockIconElem = document.createElement("span");
-            lockIconElem.title = lockIconElem.ariaLabel = t("volume_locked", getFeatures().lockVolumeLevel);
-            lockIconElem.innerHTML = lockIconHtml;
-        }
-        else {
-            lockIconElem = document.createElement("div");
-            lockIconElem.textContent = " ";
-            lockIconElem.style.minWidth = "32px";
-        }
-        sliderElem.addEventListener("change", () => updateLabel());
-        siteEvents.on("configChanged", () => {
-            updateLabel();
-            if (lockIconElem)
-                lockIconElem.title = lockIconElem.ariaLabel = t("volume_locked", getFeatures().lockVolumeLevel);
-        });
-        onSelectorOld("#bytm-vol-slider-cont", {
-            listener: (volumeCont) => {
-                lockIconElem && labelContElem.appendChild(lockIconElem);
-                labelContElem.appendChild(labelElem);
-                volumeCont.appendChild(labelContElem);
-            },
-        });
-        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)
-                labelContElem.classList.add("bytm-visible");
-            else if (labelContElem.classList.contains("bytm-visible") || document.activeElement !== sliderElem)
-                labelContElem.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() {
@@ -3248,58 +3307,6 @@ function disableDarkReader() {
         document.head.appendChild(metaElem);
         info("Sent hint to Dark Reader to disable itself");
     }
-}
-//#MARKER lock volume
-let volumeSliderObserverActive = false;
-let sliderElem;
-function overrideVolValues() {
-    if (!sliderElem || !getFeatures().lockVolume)
-        return;
-    volumeSliderObserverActive = false;
-    setTimeout(() => {
-        const vidElem = document.querySelector(videoSelector);
-        if (vidElem)
-            vidElem.volume = getFeatures().lockVolumeLevel / 100;
-        if (!sliderElem) {
-            volumeSliderObserverActive = true;
-            return;
-        }
-        sliderElem.value = String(getFeatures().lockVolumeLevel);
-        sliderElem.setAttribute("aria-valuenow", String(getFeatures().lockVolumeLevel));
-        const knobElem = document.querySelector("#volume-slider #sliderKnobContainer #sliderKnob");
-        if (knobElem)
-            knobElem.style.left = `${getFeatures().lockVolumeLevel}%`;
-        const labelElem = document.querySelector("#bytm-vol-slider-label .label");
-        const newLabelContent = `${getFeatures().lockVolumeLevel}%`;
-        if (labelElem && labelElem.textContent !== newLabelContent)
-            labelElem.textContent = newLabelContent;
-        volumeSliderObserverActive = true;
-    }, 10);
-}
-/** Locks the volume slider at a specific level */
-function enableLockVolume() {
-    return __awaiter(this, void 0, void 0, function* () {
-        const observer = new MutationObserver((mutations) => {
-            for (const mutation of mutations) {
-                if (!volumeSliderObserverActive)
-                    return;
-                if (mutation.target.id === "sliderBar" && mutation.type === "attributes") {
-                    if (mutation.attributeName === "value" || mutation.attributeName === "aria-valuenow")
-                        overrideVolValues();
-                }
-            }
-        });
-        onSelectorOld("#volume-slider tp-yt-paper-progress#sliderBar", {
-            listener: (elem) => {
-                sliderElem = elem;
-                overrideVolValues();
-                volumeSliderObserverActive = true;
-                observer.observe(elem, {
-                    attributeFilter: ["value", "aria-valuenow"],
-                });
-            }
-        });
-    });
 }const inputIgnoreTagNames = ["INPUT", "TEXTAREA", "SELECT", "BUTTON", "A"];
 let features$1;
 function setInputConfig(feats) {
@@ -3965,8 +3972,9 @@ const localeOptions = Object.entries(locales).reduce((a, [locale, { name }]) =>
 /** Decoration elements that can be added next to the label */
 const adornments = {
     advanced: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return `<span class="bytm-advanced-mode-icon bytm-adorn-icon" title="${t("advanced_mode")}">${(_a = yield resourceToHTMLString("icon-advanced_mode")) !== null && _a !== void 0 ? _a : ""}</span>`; }),
-    experimental: () => __awaiter(void 0, void 0, void 0, function* () { var _b; return `<span class="bytm-experimental-icon bytm-adorn-icon" title="${t("experimental_feature")}">${(_b = yield resourceToHTMLString("icon-experimental")) !== null && _b !== void 0 ? _b : ""}</span>`; }),
+    experimental: () => __awaiter(void 0, void 0, void 0, function* () { var _b; return `<span class="bytm-experimental-icon bytm-adorn-icon" title="${t("experimental_feature")}" aria-label="${t("experimental_feature")}" role="alert">${(_b = yield resourceToHTMLString("icon-experimental")) !== null && _b !== void 0 ? _b : ""}</span>`; }),
     globe: () => __awaiter(void 0, void 0, void 0, function* () { var _c; return (_c = yield resourceToHTMLString("icon-globe")) !== null && _c !== void 0 ? _c : ""; }),
+    warning: (text) => __awaiter(void 0, void 0, void 0, function* () { var _d; return `<span class="bytm-warning-icon bytm-adorn-icon" title="${text}" aria-label="${text}" role="alert">${(_d = yield resourceToHTMLString("icon-error")) !== null && _d !== void 0 ? _d : ""}</span>`; }),
 };
 //#MARKER features
 /**
@@ -4003,16 +4011,51 @@ const adornments = {
  */
 const featInfo = {
     //#SECTION layout
-    volumeSliderLabel: {
+    watermarkEnabled: {
+        type: "toggle",
+        category: "layout",
+        default: true,
+        enable: noopTODO,
+        disable: noopTODO,
+    },
+    removeShareTrackingParam: {
+        type: "toggle",
+        category: "layout",
+        default: true,
+        enable: noopTODO,
+        disable: noopTODO,
+    },
+    fixSpacing: {
+        type: "toggle",
+        category: "layout",
+        default: true,
+        enable: noopTODO,
+        disable: noopTODO,
+    },
+    scrollToActiveSongBtn: {
+        type: "toggle",
+        category: "layout",
+        default: true,
+        enable: noopTODO,
+        disable: noopTODO,
+    },
+    removeUpgradeTab: {
         type: "toggle",
         category: "layout",
         default: true,
         enable: noopTODO,
+    },
+    //#SECTION volume
+    volumeSliderLabel: {
+        type: "toggle",
+        category: "volume",
+        default: true,
+        enable: noopTODO,
         disable: noopTODO,
     },
     volumeSliderSize: {
         type: "number",
-        category: "layout",
+        category: "volume",
         min: 50,
         max: 500,
         step: 5,
@@ -4023,7 +4066,7 @@ const featInfo = {
     },
     volumeSliderStep: {
         type: "slider",
-        category: "layout",
+        category: "volume",
         min: 1,
         max: 25,
         default: 2,
@@ -4033,7 +4076,7 @@ const featInfo = {
     },
     volumeSliderScrollStep: {
         type: "slider",
-        category: "layout",
+        category: "volume",
         min: 1,
         max: 25,
         default: 10,
@@ -4041,39 +4084,32 @@ const featInfo = {
         enable: noopTODO,
         change: noopTODO,
     },
-    watermarkEnabled: {
+    volumeSharedBetweenTabs: {
         type: "toggle",
-        category: "layout",
-        default: true,
-        enable: noopTODO,
-        disable: noopTODO,
-    },
-    removeShareTrackingParam: {
-        type: "toggle",
-        category: "layout",
-        default: true,
-        enable: noopTODO,
-        disable: noopTODO,
-    },
-    fixSpacing: {
-        type: "toggle",
-        category: "layout",
-        default: true,
+        category: "volume",
+        default: false,
         enable: noopTODO,
-        disable: noopTODO,
+        disable: () => volumeSharedBetweenTabsDisabled,
     },
-    scrollToActiveSongBtn: {
+    setInitialTabVolume: {
         type: "toggle",
-        category: "layout",
-        default: true,
+        category: "volume",
+        default: false,
         enable: noopTODO,
         disable: noopTODO,
+        textAdornment: () => getFeatures().volumeSharedBetweenTabs ? adornments.warning(t("feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible").replace(/"/g, "'")) : undefined,
     },
-    removeUpgradeTab: {
-        type: "toggle",
-        category: "layout",
-        default: true,
+    initialTabVolumeLevel: {
+        type: "slider",
+        category: "volume",
+        min: 0,
+        max: 100,
+        step: 1,
+        default: 100,
+        unit: "%",
         enable: noopTODO,
+        change: noopTODO,
+        textAdornment: () => getFeatures().volumeSharedBetweenTabs ? adornments.warning(t("feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible").replace(/"/g, "'")) : undefined,
     },
     //#SECTION song lists
     lyricsQueueButton: {
@@ -4181,24 +4217,6 @@ const featInfo = {
         // TODO: to be reworked or removed in the big menu rework
         textAdornment: adornments.advanced,
     },
-    lockVolume: {
-        type: "toggle",
-        category: "behavior",
-        default: false,
-        enable: () => noopTODO,
-        disable: () => noopTODO,
-    },
-    lockVolumeLevel: {
-        type: "slider",
-        category: "behavior",
-        min: 0,
-        max: 100,
-        step: 1,
-        default: 100,
-        unit: "%",
-        enable: noop,
-        change: () => noopTODO,
-    },
     //#SECTION input
     arrowKeySupport: {
         type: "toggle",
@@ -4375,8 +4393,6 @@ const featInfo = {
         textAdornment: () => getFeatures().advancedMode ? adornments.advanced() : undefined,
     },
 };
-function noop() {
-}
 function noopTODO() {
 }/** If this number is incremented, the features object data will be migrated to the new format */
 const formatVersion = 5;
@@ -4412,10 +4428,10 @@ const migrations = {
         "geniUrlBase", "geniUrlToken",
         "lyricsCacheMaxSize", "lyricsCacheTTL",
         "clearLyricsCache", "advancedMode",
-        "lockVolume", "lockVolumeLevel",
         "checkVersionNow", "advancedLyricsFilter",
         "rememberSongTimeDuration", "rememberSongTimeReduction",
-        "rememberSongTimeMinPlayTime",
+        "rememberSongTimeMinPlayTime", "volumeSharedBetweenTabs",
+        "setInitialTabVolume", "initialTabVolumeLevel",
     ], oldData),
 };
 // TODO: once advanced filtering is fully implemented, clear cache on migration (to v6)
@@ -4430,57 +4446,57 @@ function useDefaultConfig(keys, oldData) {
 function getFeatureDefault(key) {
     return featInfo[key].default;
 }
-const defaultConfig = Object.keys(featInfo)
+const defaultData = Object.keys(featInfo)
     .reduce((acc, key) => {
     acc[key] = featInfo[key].default;
     return acc;
 }, {});
 let canCompress = true;
-const cfgMgr = new UserUtils.ConfigManager({
+const bytmCfgStore = new UserUtils.DataStore({
     id: "bytm-config",
     formatVersion,
-    defaultConfig,
+    defaultData,
     migrations,
     encodeData: (data) => canCompress ? UserUtils.compress(data, compressionFormat, "string") : data,
     decodeData: (data) => canCompress ? UserUtils.decompress(data, compressionFormat, "string") : data,
 });
-/** Initializes the ConfigManager instance and loads persistent data into memory */
+/** Initializes the DataStore instance and loads persistent data into memory */
 function initConfig() {
     return __awaiter(this, void 0, void 0, function* () {
         canCompress = yield compressionSupported();
-        const oldFmtVer = Number(yield GM.getValue(`_uucfgver-${cfgMgr.id}`, NaN));
-        const data = yield cfgMgr.loadData();
-        log(`Initialized ConfigManager (format version = ${cfgMgr.formatVersion})`);
+        const oldFmtVer = Number(yield GM.getValue(`_uucfgver-${bytmCfgStore.id}`, NaN));
+        const data = yield bytmCfgStore.loadData();
+        log(`Initialized DataStore (format version = ${bytmCfgStore.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}`);
+        else if (oldFmtVer !== bytmCfgStore.formatVersion)
+            info(`Config data migrated from version ${oldFmtVer} to ${bytmCfgStore.formatVersion}`);
         emitInterface("bytm:configReady", getFeaturesInterface());
         return Object.assign({}, data);
     });
 }
 /** Returns the current feature config from the in-memory cache as a copy */
 function getFeatures() {
-    return cfgMgr.getData();
+    return bytmCfgStore.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());
+    const res = bytmCfgStore.setData(featureConf);
+    emitSiteEvent("configChanged", bytmCfgStore.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());
+    const res = bytmCfgStore.saveDefaultData();
+    emitSiteEvent("configChanged", bytmCfgStore.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();
+        yield bytmCfgStore.deleteData();
         info("Deleted config from persistent storage");
     });
 }const { getUnsafeWindow } = UserUtils__namespace;
@@ -5067,7 +5083,6 @@ function init() {
             yield initTranslations((_a = features.locale) !== null && _a !== void 0 ? _a : "en_US");
             setLocale((_b = features.locale) !== null && _b !== void 0 ? _b : "en_US");
             // TODO(v1.2): remove these
-            setLayoutConfig(features);
             setInputConfig(features);
             setSongListsConfig(features);
             if (features.disableBeforeUnloadPopup && domain === "ytm")
@@ -5138,8 +5153,6 @@ function onDomLoad() {
                     ftInit.push(fixSpacing());
                 if (features.scrollToActiveSongBtn)
                     ftInit.push(addScrollToActiveBtn());
-                if (features.lockVolume)
-                    ftInit.push(enableLockVolume());
                 ftInit.push(initVolumeFeatures());
             }
             if (["ytm", "yt"].includes(domain)) {
@@ -5191,7 +5204,14 @@ function onDomLoad() {
 /** 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(`/* TODO(v1.2): leave only dialog */
+    UserUtils.addGlobalStyle(`:root {
+  --bytm-dialog-accent-col: #3683d4;
+  --bytm-advanced-mode-color: #c5a73b;
+  --bytm-experimental-col: #d07ff0;
+  --bytm-warning-col: #f27735;
+}
+
+/* TODO(v1.2): leave only dialog */
 #bytm-cfg-dialog-bg,
 #bytm-cfg-menu-bg
 {
@@ -5280,16 +5300,32 @@ function insertGlobalStyle() {
 .bytm-adorn-icon {
   display: inline-flex;
   align-items: center;
+  cursor: help;
 }
 
 .bytm-ftconf-adv-copy-btn {
   margin: 0px 10px;
 }
 
-:root {
-  --bytm-dialog-accent-col: #3683d4;
-  --bytm-advanced-mode-color: #c5a73b;
-  --bytm-experimental-col: #df543d;
+.bytm-ftitem-adornment svg path {
+  fill: var(--bytm-dialog-accent-col, #fff);
+}
+
+.bytm-advanced-mode-icon svg path {
+  fill: var(--bytm-advanced-mode-color, #fff);
+}
+
+.bytm-experimental-icon svg path {
+  fill: var(--bytm-experimental-col, #fff);
+}
+
+.bytm-warning-icon svg {
+  width: 24px;
+  height: 24px;
+}
+
+.bytm-warning-icon svg path {
+  fill: var(--bytm-warning-col, #fff);
 }
 
 .bytm-dialog-bg {
@@ -5682,18 +5718,6 @@ hr {
   margin-right: 6px;
 }
 
-.bytm-ftitem-adornment svg path {
-  fill: var(--bytm-dialog-accent-col, #fff);
-}
-
-.bytm-advanced-mode-icon svg path {
-  fill: var(--bytm-advanced-mode-color, #fff);
-}
-
-.bytm-experimental-icon svg path {
-  fill: var(--bytm-experimental-col, #fff);
-}
-
 .bytm-hotkey-wrapper {
   display: flex;
   flex-direction: row;

+ 0 - 18
src/components/BytmDialog.css

@@ -1,9 +1,3 @@
-:root {
-  --bytm-dialog-accent-col: #3683d4;
-  --bytm-advanced-mode-color: #c5a73b;
-  --bytm-experimental-col: #df543d;
-}
-
 .bytm-dialog-bg {
   --bytm-dialog-bg: #333333;
   --bytm-dialog-bg-highlight: #252525;
@@ -393,15 +387,3 @@ hr {
   align-items: center;
   margin-right: 6px;
 }
-
-.bytm-ftitem-adornment svg path {
-  fill: var(--bytm-dialog-accent-col, #fff);
-}
-
-.bytm-advanced-mode-icon svg path {
-  fill: var(--bytm-advanced-mode-color, #fff);
-}
-
-.bytm-experimental-icon svg path {
-  fill: var(--bytm-experimental-col, #fff);
-}

+ 3 - 3
src/config.ts

@@ -48,12 +48,12 @@ export const migrations: ConfigMigrationsDict = {
     "geniUrlBase", "geniUrlToken",
     "lyricsCacheMaxSize", "lyricsCacheTTL",
     "clearLyricsCache", "advancedMode",
-    "lockVolume", "lockVolumeLevel",
     "checkVersionNow", "advancedLyricsFilter",
     "rememberSongTimeDuration", "rememberSongTimeReduction",
-    "rememberSongTimeMinPlayTime",
+    "rememberSongTimeMinPlayTime", "volumeSharedBetweenTabs",
+    "setInitialTabVolume", "initialTabVolumeLevel",
   ], oldData),
-};
+} as const satisfies ConfigMigrationsDict;
 // TODO: once advanced filtering is fully implemented, clear cache on migration (to v6)
 
 /** Uses the passed {@linkcode oldData} as the base (if given) and sets all passed {@linkcode keys} to their feature default - returns a copy of the object */

+ 15 - 0
src/constants.ts

@@ -1,3 +1,4 @@
+import { randomId } from "@sv443-network/userutils";
 import { LogLevel } from "./types";
 
 const modeRaw = "#{{MODE}}";
@@ -26,6 +27,20 @@ export const platformNames: Record<typeof host, string> = {
 /** Default compression format used throughout BYTM */
 export const compressionFormat: CompressionFormat = "deflate-raw";
 
+export const sessionStorageAllowed =
+  typeof sessionStorage !== "undefined"
+  && (() => {
+    try {
+      const key = `_bytm_${randomId(4)}`;
+      sessionStorage.setItem(key, "test");
+      sessionStorage.removeItem(key);
+      return true;
+    }
+    catch {
+      return false;
+    }
+  })();
+
 /**
  * How much info should be logged to the devtools console  
  * 0 = Debug (show everything) or 1 = Info (show only important stuff)

+ 29 - 0
src/dialogs/dialogs.css

@@ -1,3 +1,10 @@
+:root {
+  --bytm-dialog-accent-col: #3683d4;
+  --bytm-advanced-mode-color: #c5a73b;
+  --bytm-experimental-col: #d07ff0;
+  --bytm-warning-col: #f27735;
+}
+
 /* TODO(v1.2): leave only dialog */
 #bytm-cfg-dialog-bg,
 #bytm-cfg-menu-bg
@@ -87,8 +94,30 @@
 .bytm-adorn-icon {
   display: inline-flex;
   align-items: center;
+  cursor: help;
 }
 
 .bytm-ftconf-adv-copy-btn {
   margin: 0px 10px;
 }
+
+.bytm-ftitem-adornment svg path {
+  fill: var(--bytm-dialog-accent-col, #fff);
+}
+
+.bytm-advanced-mode-icon svg path {
+  fill: var(--bytm-advanced-mode-color, #fff);
+}
+
+.bytm-experimental-icon svg path {
+  fill: var(--bytm-experimental-col, #fff);
+}
+
+.bytm-warning-icon svg {
+  width: 24px;
+  height: 24px;
+}
+
+.bytm-warning-icon svg path {
+  fill: var(--bytm-warning-col, #fff);
+}

+ 0 - 61
src/features/behavior.ts

@@ -222,64 +222,3 @@ export function disableDarkReader() {
     info("Sent hint to Dark Reader to disable itself");
   }
 }
-
-//#MARKER lock volume
-
-let volumeSliderObserverActive = false;
-
-let sliderElem: HTMLInputElement | undefined;
-function overrideVolValues() {
-  if(!sliderElem || !getFeatures().lockVolume)
-    return;
-
-  volumeSliderObserverActive = false;
-
-  setTimeout(() => {
-    const vidElem = document.querySelector<HTMLVideoElement>(videoSelector);
-    if(vidElem)
-      vidElem.volume = getFeatures().lockVolumeLevel / 100;
-  
-    if(!sliderElem) {
-      volumeSliderObserverActive = true;
-      return;
-    }
-    sliderElem.value = String(getFeatures().lockVolumeLevel);
-    sliderElem.setAttribute("aria-valuenow", String(getFeatures().lockVolumeLevel));
-
-    const knobElem = document.querySelector<HTMLElement>("#volume-slider #sliderKnobContainer #sliderKnob");
-    if(knobElem)
-      knobElem.style.left = `${getFeatures().lockVolumeLevel}%`;
-
-    const labelElem = document.querySelector<HTMLElement>("#bytm-vol-slider-label .label");
-    const newLabelContent = `${getFeatures().lockVolumeLevel}%`;
-    if(labelElem && labelElem.textContent !== newLabelContent)
-      labelElem.textContent = newLabelContent;
-
-    volumeSliderObserverActive = true;
-  }, 10);
-}
-
-/** Locks the volume slider at a specific level */
-export async function enableLockVolume() {
-  const observer = new MutationObserver((mutations) => {
-    for(const mutation of mutations) {
-      if(!volumeSliderObserverActive)
-        return;
-      if((mutation.target as HTMLElement).id === "sliderBar" && mutation.type === "attributes") {
-        if(mutation.attributeName === "value" || mutation.attributeName === "aria-valuenow")
-          overrideVolValues();
-      }
-    }
-  });
-
-  onSelectorOld<HTMLInputElement>("#volume-slider tp-yt-paper-progress#sliderBar", {
-    listener: (elem) => {
-      sliderElem = elem;
-      overrideVolValues();
-      volumeSliderObserverActive = true;
-      observer.observe(elem, {
-        attributeFilter: ["value", "aria-valuenow"],
-      });
-    }
-  });
-}

+ 64 - 48
src/features/index.ts

@@ -6,6 +6,7 @@ import { doVersionCheck } from "./versionCheck";
 import { mode } from "../constants";
 import { getFeatures } from "../config";
 import { FeatureInfo } from "../types";
+import { volumeSharedBetweenTabsDisabled } from "./volume";
 
 export * from "./layout";
 export * from "./behavior";
@@ -14,6 +15,7 @@ export * from "./lyrics";
 export * from "./lyricsCache";
 export * from "./songLists";
 export * from "./versionCheck";
+export * from "./volume";
 
 type SelectOption = { value: number | string, label: string };
 
@@ -31,8 +33,9 @@ const localeOptions = Object.entries(langMapping).reduce((a, [locale, { name }])
 /** Decoration elements that can be added next to the label */
 const adornments = {
   advanced: async () => `<span class="bytm-advanced-mode-icon bytm-adorn-icon" title="${t("advanced_mode")}">${await resourceToHTMLString("icon-advanced_mode") ?? ""}</span>`,
-  experimental: async () => `<span class="bytm-experimental-icon bytm-adorn-icon" title="${t("experimental_feature")}">${await resourceToHTMLString("icon-experimental") ?? ""}</span>`,
+  experimental: async () => `<span class="bytm-experimental-icon bytm-adorn-icon" title="${t("experimental_feature")}" aria-label="${t("experimental_feature")}" role="alert">${await resourceToHTMLString("icon-experimental") ?? ""}</span>`,
   globe: async () => await resourceToHTMLString("icon-globe") ?? "",
+  warning: async (text: string) => `<span class="bytm-warning-icon bytm-adorn-icon" title="${text}" aria-label="${text}" role="alert">${await resourceToHTMLString("icon-error") ?? ""}</span>`,
 };
 
 //#MARKER features
@@ -71,16 +74,52 @@ const adornments = {
  */
 export const featInfo = {
   //#SECTION layout
-  volumeSliderLabel: {
+  watermarkEnabled: {
+    type: "toggle",
+    category: "layout",
+    default: true,
+    enable: noopTODO,
+    disable: noopTODO,
+  },
+  removeShareTrackingParam: {
+    type: "toggle",
+    category: "layout",
+    default: true,
+    enable: noopTODO,
+    disable: noopTODO,
+  },
+  fixSpacing: {
     type: "toggle",
     category: "layout",
     default: true,
     enable: noopTODO,
     disable: noopTODO,
   },
+  scrollToActiveSongBtn: {
+    type: "toggle",
+    category: "layout",
+    default: true,
+    enable: noopTODO,
+    disable: noopTODO,
+  },
+  removeUpgradeTab: {
+    type: "toggle",
+    category: "layout",
+    default: true,
+    enable: noopTODO,
+  },
+
+  //#SECTION volume
+  volumeSliderLabel: {
+    type: "toggle",
+    category: "volume",
+    default: true,
+    enable: noopTODO,
+    disable: noopTODO,
+  },
   volumeSliderSize: {
     type: "number",
-    category: "layout",
+    category: "volume",
     min: 50,
     max: 500,
     step: 5,
@@ -91,7 +130,7 @@ export const featInfo = {
   },
   volumeSliderStep: {
     type: "slider",
-    category: "layout",
+    category: "volume",
     min: 1,
     max: 25,
     default: 2,
@@ -101,7 +140,7 @@ export const featInfo = {
   },
   volumeSliderScrollStep: {
     type: "slider",
-    category: "layout",
+    category: "volume",
     min: 1,
     max: 25,
     default: 10,
@@ -109,39 +148,32 @@ export const featInfo = {
     enable: noopTODO,
     change: noopTODO,
   },
-  watermarkEnabled: {
-    type: "toggle",
-    category: "layout",
-    default: true,
-    enable: noopTODO,
-    disable: noopTODO,
-  },
-  removeShareTrackingParam: {
-    type: "toggle",
-    category: "layout",
-    default: true,
-    enable: noopTODO,
-    disable: noopTODO,
-  },
-  fixSpacing: {
+  volumeSharedBetweenTabs: {
     type: "toggle",
-    category: "layout",
-    default: true,
+    category: "volume",
+    default: false,
     enable: noopTODO,
-    disable: noopTODO,
+    disable: () => volumeSharedBetweenTabsDisabled,
   },
-  scrollToActiveSongBtn: {
+  setInitialTabVolume: {
     type: "toggle",
-    category: "layout",
-    default: true,
+    category: "volume",
+    default: false,
     enable: noopTODO,
     disable: noopTODO,
+    textAdornment: () => getFeatures().volumeSharedBetweenTabs ? adornments.warning(t("feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible").replace(/"/g, "'")) : undefined,
   },
-  removeUpgradeTab: {
-    type: "toggle",
-    category: "layout",
-    default: true,
+  initialTabVolumeLevel: {
+    type: "slider",
+    category: "volume",
+    min: 0,
+    max: 100,
+    step: 1,
+    default: 100,
+    unit: "%",
     enable: noopTODO,
+    change: noopTODO,
+    textAdornment: () => getFeatures().volumeSharedBetweenTabs ? adornments.warning(t("feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible").replace(/"/g, "'")) : undefined,
   },
 
   //#SECTION song lists
@@ -251,24 +283,6 @@ export const featInfo = {
     // TODO: to be reworked or removed in the big menu rework
     textAdornment: adornments.advanced,
   },
-  lockVolume: {
-    type: "toggle",
-    category: "behavior",
-    default: false,
-    enable: () => noopTODO,
-    disable: () => noopTODO,
-  },
-  lockVolumeLevel: {
-    type: "slider",
-    category: "behavior",
-    min: 0,
-    max: 100,
-    step: 1,
-    default: 100,
-    unit: "%",
-    enable: noop,
-    change: () => noopTODO,
-  },
 
   //#SECTION input
   arrowKeySupport: {
@@ -456,3 +470,5 @@ function noop() {
 function noopTODO() {
   void 0;
 }
+
+void [noop, noopTODO];

+ 2 - 157
src/features/layout.ts

@@ -1,19 +1,9 @@
-import { addGlobalStyle, addParent, autoPlural, fetchAdvanced, insertAfter, pauseFor, type Stringifiable } from "@sv443-network/userutils";
-import type { FeatureConfig } from "../types";
+import { addGlobalStyle, addParent, autoPlural, fetchAdvanced, insertAfter, pauseFor } from "@sv443-network/userutils";
 import { scriptInfo } from "../constants";
-import { error, getResourceUrl, log, onSelectorOld, warn, t, resourceToHTMLString, onInteraction } from "../utils";
-import { siteEvents } from "../siteEvents";
+import { error, getResourceUrl, log, onSelectorOld, warn, t, onInteraction } from "../utils";
 import { openCfgMenu } from "../menu/menu_old";
-import { getFeatures } from "../config";
-import { featInfo } from ".";
 import "./layout.css";
 
-let features: FeatureConfig;
-
-export function setLayoutConfig(feats: FeatureConfig) {
-  features = feats;
-}
-
 //#MARKER BYTM-Config buttons
 
 let logoExchanged = false, improveLogoCalled = false;
@@ -168,151 +158,6 @@ export async function removeUpgradeTab() {
   });
 }
 
-//#MARKER volume slider
-
-export async function initVolumeFeatures() {
-  // not technically an input element but behaves pretty much the same
-  onSelectorOld<HTMLInputElement>("tp-yt-paper-slider#volume-slider", {
-    listener: async (sliderElem) => {
-      const volSliderCont = document.createElement("div");
-      volSliderCont.id = "bytm-vol-slider-cont";
-
-      if(features.volumeSliderScrollStep !== featInfo.volumeSliderScrollStep.default) {
-        for(const evtName of ["wheel", "scroll", "mousewheel", "DOMMouseScroll"]) {
-          volSliderCont.addEventListener(evtName, (e) => {
-            e.preventDefault();
-            // cancels all the other events that would be fired
-            e.stopImmediatePropagation();
-
-            const delta = (e as WheelEvent).deltaY ?? (e as CustomEvent<number | undefined>).detail ?? 1;
-            const volumeDir = -Math.sign(delta);
-            const newVolume = String(Number(sliderElem.value) + (features.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,
-          });
-        }
-      }
-
-      addParent(sliderElem, volSliderCont);
-
-      if(typeof features.volumeSliderSize === "number")
-        setVolSliderSize();
-
-      if(features.volumeSliderLabel)
-        await addVolumeSliderLabel(sliderElem, volSliderCont);
-
-      setVolSliderStep(sliderElem);
-    },
-  });
-}
-
-/** Adds a percentage label to the volume slider and tooltip */
-async function addVolumeSliderLabel(sliderElem: HTMLInputElement, sliderContainer: HTMLDivElement) {
-  const labelContElem = document.createElement("div");
-  labelContElem.id = "bytm-vol-slider-label";
-
-  const getLabel = (value: Stringifiable) => `${getFeatures().lockVolume ? getFeatures().lockVolumeLevel : value}%`;
-
-  const labelElem = document.createElement("div");
-  labelElem.classList.add("label");
-  labelElem.textContent = getLabel(sliderElem.value);
-
-  // prevent video from minimizing
-  labelContElem.addEventListener("click", (e) => e.stopPropagation());
-
-  const getLabelText = (slider: HTMLInputElement) =>
-    t("volume_tooltip", getFeatures().lockVolume ? getFeatures().lockVolumeLevel : slider.value, features.volumeSliderStep ?? slider.step);
-
-  const labelFull = getLabelText(sliderElem);
-  sliderContainer.setAttribute("title", labelFull);
-  sliderElem.setAttribute("title", labelFull);
-  sliderElem.setAttribute("aria-valuetext", labelFull);
-
-  const updateLabel = () => {
-    const labelFull = getLabelText(sliderElem);
-
-    sliderContainer.setAttribute("title", labelFull);
-    sliderElem.setAttribute("title", labelFull);
-    sliderElem.setAttribute("aria-valuetext", labelFull);
-
-    const labelElem2 = document.querySelector<HTMLDivElement>("#bytm-vol-slider-label div.label");
-    if(labelElem2)
-      labelElem2.textContent = getLabel(sliderElem.value);
-  };
-
-  let lockIconElem: HTMLElement | undefined;
-  const lockIconHtml = await resourceToHTMLString("icon-lock");
-  if(getFeatures().lockVolume && lockIconHtml) {
-    lockIconElem = document.createElement("span");
-    lockIconElem.title = lockIconElem.ariaLabel = t("volume_locked", getFeatures().lockVolumeLevel);
-    lockIconElem.innerHTML = lockIconHtml;
-  }
-  else {
-    lockIconElem = document.createElement("div");
-    lockIconElem.textContent = " ";
-    lockIconElem.style.minWidth = "32px";
-  }
-
-  sliderElem.addEventListener("change", () => updateLabel());
-  siteEvents.on("configChanged", () => {
-    updateLabel();
-    if(lockIconElem)
-      lockIconElem.title = lockIconElem.ariaLabel = t("volume_locked", getFeatures().lockVolumeLevel);
-  });
-
-  onSelectorOld("#bytm-vol-slider-cont", {
-    listener: (volumeCont) => {
-      lockIconElem && labelContElem.appendChild(lockIconElem);
-      labelContElem.appendChild(labelElem);
-
-      volumeCont.appendChild(labelContElem);
-    },
-  });
-
-  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)
-      labelContElem.classList.add("bytm-visible");
-    else if(labelContElem.classList.contains("bytm-visible") || document.activeElement !== sliderElem)
-      labelContElem.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;
-
-  if(typeof size !== "number" || isNaN(Number(size)))
-    return;
-
-  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: HTMLInputElement) {
-  sliderElem.setAttribute("step", String(features.volumeSliderStep));
-}
-
 //#MARKER anchor improvements
 
 /** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */

+ 210 - 0
src/features/volume.ts

@@ -0,0 +1,210 @@
+import { addGlobalStyle, addParent, type Stringifiable } from "@sv443-network/userutils";
+import { getFeatures } from "../config";
+import { error, onSelectorOld, t } from "../utils";
+import { siteEvents } from "../siteEvents";
+import { featInfo } from ".";
+
+//#MARKER init
+
+/** Initializes all volume-related features */
+export async function initVolumeFeatures() {
+  // not technically an input element but behaves pretty much the same
+  onSelectorOld<HTMLInputElement>("tp-yt-paper-slider#volume-slider", {
+    listener: async (sliderElem) => {
+      const volSliderCont = document.createElement("div");
+      volSliderCont.id = "bytm-vol-slider-cont";
+
+      if(getFeatures().volumeSliderScrollStep !== featInfo.volumeSliderScrollStep.default)
+        initScrollStep(volSliderCont, sliderElem);
+
+      addParent(sliderElem, volSliderCont);
+
+      if(typeof getFeatures().volumeSliderSize === "number")
+        setVolSliderSize();
+
+      if(getFeatures().volumeSliderLabel)
+        await addVolumeSliderLabel(sliderElem, volSliderCont);
+
+      setVolSliderStep(sliderElem);
+
+      if(getFeatures().volumeSharedBetweenTabs) {
+        sliderElem.addEventListener("change", () => sharedVolumeChanged(Number(sliderElem.value)));
+        setInterval(checkSharedVolume, 500);
+        checkSharedVolume();
+      }
+
+      if(getFeatures().setInitialTabVolume)
+        setInitialTabVolume(sliderElem);
+    },
+  });
+}
+
+//#MARKER scroll step
+
+/** Initializes the volume slider scroll step features */
+function initScrollStep(volSliderCont: HTMLDivElement, sliderElem: HTMLInputElement) {
+  for(const evtName of ["wheel", "scroll", "mousewheel", "DOMMouseScroll"]) {
+    volSliderCont.addEventListener(evtName, (e) => {
+      e.preventDefault();
+      // cancels all the other events that would be fired
+      e.stopImmediatePropagation();
+
+      const delta = (e as WheelEvent).deltaY ?? (e as CustomEvent<number | undefined>).detail ?? 1;
+      const volumeDir = -Math.sign(delta);
+      const newVolume = String(Number(sliderElem.value) + (getFeatures().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
+
+//#SECTION label
+
+/** Adds a percentage label to the volume slider and tooltip */
+async function addVolumeSliderLabel(sliderElem: HTMLInputElement, sliderContainer: HTMLDivElement) {
+  const labelContElem = document.createElement("div");
+  labelContElem.id = "bytm-vol-slider-label";
+
+  const getLabel = (value: Stringifiable) => `${value}%`;
+
+  const labelElem = document.createElement("div");
+  labelElem.classList.add("label");
+  labelElem.textContent = getLabel(sliderElem.value);
+
+  // prevent video from minimizing
+  labelContElem.addEventListener("click", (e) => e.stopPropagation());
+
+  const getLabelText = (slider: HTMLInputElement) =>
+    t("volume_tooltip", slider.value, getFeatures().volumeSliderStep ?? slider.step);
+
+  const labelFull = getLabelText(sliderElem);
+  sliderContainer.setAttribute("title", labelFull);
+  sliderElem.setAttribute("title", labelFull);
+  sliderElem.setAttribute("aria-valuetext", labelFull);
+
+  const updateLabel = () => {
+    const labelFull = getLabelText(sliderElem);
+
+    sliderContainer.setAttribute("title", labelFull);
+    sliderElem.setAttribute("title", labelFull);
+    sliderElem.setAttribute("aria-valuetext", labelFull);
+
+    const labelElem2 = document.querySelector<HTMLDivElement>("#bytm-vol-slider-label div.label");
+    if(labelElem2)
+      labelElem2.textContent = getLabel(sliderElem.value);
+  };
+
+  sliderElem.addEventListener("change", () => updateLabel());
+  siteEvents.on("configChanged", () => {
+    updateLabel();
+  });
+
+  onSelectorOld("#bytm-vol-slider-cont", {
+    listener: (volumeCont) => {
+      labelContElem.appendChild(labelElem);
+
+      volumeCont.appendChild(labelContElem);
+    },
+  });
+
+  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)
+      labelContElem.classList.add("bytm-visible");
+    else if(labelContElem.classList.contains("bytm-visible") || document.activeElement !== sliderElem)
+      labelContElem.classList.remove("bytm-visible");
+
+    if(Number(sliderElem.value) !== lastSliderVal) {
+      lastSliderVal = Number(sliderElem.value);
+      updateLabel();
+    }
+  });
+
+  sliderHoverObserver.observe(sliderElem, {
+    attributes: true,
+  });
+}
+
+//#SECTION size
+
+/** Sets the volume slider to a set size */
+function setVolSliderSize() {
+  const { volumeSliderSize: size } = getFeatures();
+
+  if(typeof size !== "number" || isNaN(Number(size)))
+    return;
+
+  addGlobalStyle(`\
+#bytm-vol-slider-cont tp-yt-paper-slider#volume-slider {
+  width: ${size}px !important;
+}`).id = "bytm-style-vol-slider-size";
+}
+
+//#SECTION step
+
+/** Sets the `step` attribute of the volume slider */
+function setVolSliderStep(sliderElem: HTMLInputElement) {
+  sliderElem.setAttribute("step", String(getFeatures().volumeSliderStep));
+}
+
+//#MARKER shared volume
+
+/** Saves the shared volume level to persistent storage */
+async function sharedVolumeChanged(vol: number) {
+  try {
+    await GM.setValue("bytm-shared-volume", String(vol));
+  }
+  catch(err) {
+    error("Couldn't save shared volume level due to an error:", err);
+  }
+}
+
+let ignoreVal = -1;
+let lastCheckedSharedVolume = -1;
+/** Checks if the shared volume has changed and updates the volume slider accordingly */
+async function checkSharedVolume() {
+  try {
+    const vol = await GM.getValue("bytm-shared-volume");
+    console.log(">checkshared", vol, lastCheckedSharedVolume);
+    if(vol && lastCheckedSharedVolume !== Number(vol)) {
+      if(ignoreVal === Number(vol))
+        return;
+      lastCheckedSharedVolume = Number(vol);
+
+      const sliderElem = document.querySelector<HTMLInputElement>("tp-yt-paper-slider#volume-slider");
+      if(sliderElem) {
+        sliderElem.value = String(vol);
+        sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
+      }
+    }
+  }
+  catch(err) {
+    error("Couldn't check for shared volume level due to an error:", err);
+  }
+}
+
+export async function volumeSharedBetweenTabsDisabled() {
+  await GM.deleteValue("bytm-shared-volume");
+}
+
+//#MARKER initial volume
+
+/** Sets the volume slider to a set volume level when the session starts */
+function setInitialTabVolume(sliderElem: HTMLInputElement) {
+  const initialVol = getFeatures().initialTabVolumeLevel;
+  if(getFeatures().volumeSharedBetweenTabs) {
+    lastCheckedSharedVolume = ignoreVal = initialVol;
+  }
+  sliderElem.value = String(initialVol);
+  sliderElem.dispatchEvent(new Event("change", { bubbles: true }));
+}

+ 4 - 7
src/index.ts

@@ -11,16 +11,17 @@ import {
   // features:
   featInfo,
   // layout
-  setLayoutConfig, addWatermark,
-  removeUpgradeTab, initVolumeFeatures,
+  addWatermark, removeUpgradeTab,
   removeShareTrackingParam, fixSpacing,
   addScrollToActiveBtn,
+  // volume
+  initVolumeFeatures,
   // song lists
   setSongListsConfig, initQueueButtons,
   // behavior
   initBeforeUnloadHook, disableBeforeUnload,
   initAutoCloseToasts, initRememberSongTime,
-  disableDarkReader, enableLockVolume,
+  disableDarkReader,
   // input
   setInputConfig, initArrowKeySkip,
   initSiteSwitch, addAnchorImprovements,
@@ -87,7 +88,6 @@ async function init() {
     setLocale(features.locale ?? "en_US");
 
     // TODO(v1.2): remove these
-    setLayoutConfig(features);
     setInputConfig(features);
     setSongListsConfig(features);
 
@@ -181,9 +181,6 @@ async function onDomLoad() {
       if(features.scrollToActiveSongBtn)
         ftInit.push(addScrollToActiveBtn());
 
-      if(features.lockVolume)
-        ftInit.push(enableLockVolume());
-
       ftInit.push(initVolumeFeatures());
     }
 

+ 17 - 12
src/types.ts

@@ -109,6 +109,7 @@ export type FeatureKey = keyof FeatureConfig;
 
 export type FeatureCategory =
   | "layout"
+  | "volume"
   | "songLists"
   | "behavior"
   | "input"
@@ -205,14 +206,6 @@ export type FeatureInfo = Record<
 /** Feature configuration */
 export interface FeatureConfig {
   //#SECTION layout
-  /** Add a percentage label to the volume slider */
-  volumeSliderLabel: boolean;
-  /** The width of the volume slider in pixels */
-  volumeSliderSize: number;
-  /** Volume slider sensitivity - the smaller this number, the finer the volume control */
-  volumeSliderStep: number;
-  /** Volume slider scroll wheel sensitivity */
-  volumeSliderScrollStep: number;
   /** Show a BetterYTM watermark under the YTM logo */
   watermarkEnabled: boolean;
   /** Remove the "si" tracking parameter from links in the share popup */
@@ -224,6 +217,22 @@ export interface FeatureConfig {
   /** Remove the \"Upgrade\" / YT Music Premium tab */
   removeUpgradeTab: boolean;
 
+  //#SECTION volume
+  /** Add a percentage label to the volume slider */
+  volumeSliderLabel: boolean;
+  /** The width of the volume slider in pixels */
+  volumeSliderSize: number;
+  /** Volume slider sensitivity - the smaller this number, the finer the volume control */
+  volumeSliderStep: number;
+  /** Volume slider scroll wheel sensitivity */
+  volumeSliderScrollStep: number;
+  /** Whether the volume should be locked to the same level across all tabs (changing in one changes in all others too) */
+  volumeSharedBetweenTabs: boolean;
+  /** Whether to set an initial volume level for each new session */
+  setInitialTabVolume: boolean;
+  /** The initial volume level to set for each new session */
+  initialTabVolumeLevel: number;
+
   //#SECTION song lists
   /** Add a button to each song in the queue to quickly open its lyrics page */
   lyricsQueueButton: boolean;
@@ -249,10 +258,6 @@ export interface FeatureConfig {
   rememberSongTimeReduction: number;
   /** Minimum time in seconds the song needs to be played before it is remembered */
   rememberSongTimeMinPlayTime: number;
-  /** Lock the volume slider at a specific level */
-  lockVolume: boolean;
-  /** The volume level to lock the slider at */
-  lockVolumeLevel: number;
 
   //#SECTION input
   /** Arrow keys skip forwards and backwards */