Browse Source

feat: add plugin list dialog

Sv443 7 months ago
parent
commit
08e12e9c09

+ 191 - 10
assets/translations/README.md

@@ -16,15 +16,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) | `301` (default locale) |  |
-| ✅ | [`de_DE`](./de_DE.json) | `301/301` (100%) | ─ |
-|  | [`en_UK`](./en_UK.json) | `301/301` (100%) | `en_US` |
-| ✅ | [`es_ES`](./es_ES.json) | `301/301` (100%) | ─ |
-| ✅ | [`fr_FR`](./fr_FR.json) | `301/301` (100%) | ─ |
-| ✅ | [`hi_IN`](./hi_IN.json) | `301/301` (100%) | ─ |
-| ✅ | [`ja_JA`](./ja_JA.json) | `301/301` (100%) | ─ |
-| ✅ | [`pt_BR`](./pt_BR.json) | `301/301` (100%) | ─ |
-| ✅ | [`zh_CN`](./zh_CN.json) | `301/301` (100%) | ─ |
+|  | [`en_US`](./en_US.json) | `320` (default locale) |  |
+| ‼️ | [`de_DE`](./de_DE.json) | `301/320` (94.1%) | ─ |
+|  | [`en_UK`](./en_UK.json) | `320/320` (100%) | `en_US` |
+| ‼️ | [`es_ES`](./es_ES.json) | `301/320` (94.1%) | ─ |
+| ‼️ | [`fr_FR`](./fr_FR.json) | `301/320` (94.1%) | ─ |
+| ‼️ | [`hi_IN`](./hi_IN.json) | `301/320` (94.1%) | ─ |
+| ‼️ | [`ja_JA`](./ja_JA.json) | `301/320` (94.1%) | ─ |
+| ‼️ | [`pt_BR`](./pt_BR.json) | `301/320` (94.1%) | ─ |
+| ‼️ | [`zh_CN`](./zh_CN.json) | `301/320` (94.1%) | ─ |
 
 <sub>
 ✅ - Fully translated
@@ -44,4 +44,185 @@ This means to figure out which keys are untranslated, you will need to manually
 <br>
 
 ### Missing keys:
-No missing keys
+
+<details><summary><code>de_DE</code> - 19 missing keys <i>(click to show)</i></summary><br>
+
+| Key | English text |
+| --- | ------------ |
+| `plugin_list_title` | `Plugin List` |
+| `plugin_list_permissions_header` | `Permissions:` |
+| `plugin_link_type_source` | `Repository` |
+| `plugin_link_type_other` | `Other / Homepage` |
+| `plugin_link_type_bug` | `Report a bug` |
+| `plugin_link_type_greasyfork` | `GreasyFork` |
+| `plugin_link_type_openuserjs` | `OpenUserJS` |
+| `plugin_intent_description_ReadFeatureConfig` | `This plugin can read the feature configuration` |
+| `plugin_intent_description_WriteFeatureConfig` | `This plugin can write to the feature configuration` |
+| `plugin_intent_description_SeeHiddenConfigValues` | `This plugin has access to hidden config values` |
+| `plugin_intent_description_WriteLyricsCache` | `This plugin can write to the lyrics cache` |
+| `plugin_intent_description_WriteTranslations` | `This plugin can add new translations and overwrite existing ones` |
+| `plugin_intent_description_CreateModalDialogs` | `This plugin can create modal dialogs` |
+| `plugin_intent_description_ReadAutoLikeData` | `This plugin can read auto-like data` |
+| `plugin_intent_description_WriteAutoLikeData` | `This plugin can write to auto-like data` |
+| `feature_category_plugins` | `Plugins` |
+| `feature_desc_openPluginList` | `Open the list of plugins you have installed` |
+| `feature_btn_openPluginList` | `Open list` |
+| `feature_btn_openPluginList_running` | `Opening...` |
+
+<br></details>
+
+<details><summary><code>es_ES</code> - 19 missing keys <i>(click to show)</i></summary><br>
+
+| Key | English text |
+| --- | ------------ |
+| `plugin_list_title` | `Plugin List` |
+| `plugin_list_permissions_header` | `Permissions:` |
+| `plugin_link_type_source` | `Repository` |
+| `plugin_link_type_other` | `Other / Homepage` |
+| `plugin_link_type_bug` | `Report a bug` |
+| `plugin_link_type_greasyfork` | `GreasyFork` |
+| `plugin_link_type_openuserjs` | `OpenUserJS` |
+| `plugin_intent_description_ReadFeatureConfig` | `This plugin can read the feature configuration` |
+| `plugin_intent_description_WriteFeatureConfig` | `This plugin can write to the feature configuration` |
+| `plugin_intent_description_SeeHiddenConfigValues` | `This plugin has access to hidden config values` |
+| `plugin_intent_description_WriteLyricsCache` | `This plugin can write to the lyrics cache` |
+| `plugin_intent_description_WriteTranslations` | `This plugin can add new translations and overwrite existing ones` |
+| `plugin_intent_description_CreateModalDialogs` | `This plugin can create modal dialogs` |
+| `plugin_intent_description_ReadAutoLikeData` | `This plugin can read auto-like data` |
+| `plugin_intent_description_WriteAutoLikeData` | `This plugin can write to auto-like data` |
+| `feature_category_plugins` | `Plugins` |
+| `feature_desc_openPluginList` | `Open the list of plugins you have installed` |
+| `feature_btn_openPluginList` | `Open list` |
+| `feature_btn_openPluginList_running` | `Opening...` |
+
+<br></details>
+
+<details><summary><code>fr_FR</code> - 19 missing keys <i>(click to show)</i></summary><br>
+
+| Key | English text |
+| --- | ------------ |
+| `plugin_list_title` | `Plugin List` |
+| `plugin_list_permissions_header` | `Permissions:` |
+| `plugin_link_type_source` | `Repository` |
+| `plugin_link_type_other` | `Other / Homepage` |
+| `plugin_link_type_bug` | `Report a bug` |
+| `plugin_link_type_greasyfork` | `GreasyFork` |
+| `plugin_link_type_openuserjs` | `OpenUserJS` |
+| `plugin_intent_description_ReadFeatureConfig` | `This plugin can read the feature configuration` |
+| `plugin_intent_description_WriteFeatureConfig` | `This plugin can write to the feature configuration` |
+| `plugin_intent_description_SeeHiddenConfigValues` | `This plugin has access to hidden config values` |
+| `plugin_intent_description_WriteLyricsCache` | `This plugin can write to the lyrics cache` |
+| `plugin_intent_description_WriteTranslations` | `This plugin can add new translations and overwrite existing ones` |
+| `plugin_intent_description_CreateModalDialogs` | `This plugin can create modal dialogs` |
+| `plugin_intent_description_ReadAutoLikeData` | `This plugin can read auto-like data` |
+| `plugin_intent_description_WriteAutoLikeData` | `This plugin can write to auto-like data` |
+| `feature_category_plugins` | `Plugins` |
+| `feature_desc_openPluginList` | `Open the list of plugins you have installed` |
+| `feature_btn_openPluginList` | `Open list` |
+| `feature_btn_openPluginList_running` | `Opening...` |
+
+<br></details>
+
+<details><summary><code>hi_IN</code> - 19 missing keys <i>(click to show)</i></summary><br>
+
+| Key | English text |
+| --- | ------------ |
+| `plugin_list_title` | `Plugin List` |
+| `plugin_list_permissions_header` | `Permissions:` |
+| `plugin_link_type_source` | `Repository` |
+| `plugin_link_type_other` | `Other / Homepage` |
+| `plugin_link_type_bug` | `Report a bug` |
+| `plugin_link_type_greasyfork` | `GreasyFork` |
+| `plugin_link_type_openuserjs` | `OpenUserJS` |
+| `plugin_intent_description_ReadFeatureConfig` | `This plugin can read the feature configuration` |
+| `plugin_intent_description_WriteFeatureConfig` | `This plugin can write to the feature configuration` |
+| `plugin_intent_description_SeeHiddenConfigValues` | `This plugin has access to hidden config values` |
+| `plugin_intent_description_WriteLyricsCache` | `This plugin can write to the lyrics cache` |
+| `plugin_intent_description_WriteTranslations` | `This plugin can add new translations and overwrite existing ones` |
+| `plugin_intent_description_CreateModalDialogs` | `This plugin can create modal dialogs` |
+| `plugin_intent_description_ReadAutoLikeData` | `This plugin can read auto-like data` |
+| `plugin_intent_description_WriteAutoLikeData` | `This plugin can write to auto-like data` |
+| `feature_category_plugins` | `Plugins` |
+| `feature_desc_openPluginList` | `Open the list of plugins you have installed` |
+| `feature_btn_openPluginList` | `Open list` |
+| `feature_btn_openPluginList_running` | `Opening...` |
+
+<br></details>
+
+<details><summary><code>ja_JA</code> - 19 missing keys <i>(click to show)</i></summary><br>
+
+| Key | English text |
+| --- | ------------ |
+| `plugin_list_title` | `Plugin List` |
+| `plugin_list_permissions_header` | `Permissions:` |
+| `plugin_link_type_source` | `Repository` |
+| `plugin_link_type_other` | `Other / Homepage` |
+| `plugin_link_type_bug` | `Report a bug` |
+| `plugin_link_type_greasyfork` | `GreasyFork` |
+| `plugin_link_type_openuserjs` | `OpenUserJS` |
+| `plugin_intent_description_ReadFeatureConfig` | `This plugin can read the feature configuration` |
+| `plugin_intent_description_WriteFeatureConfig` | `This plugin can write to the feature configuration` |
+| `plugin_intent_description_SeeHiddenConfigValues` | `This plugin has access to hidden config values` |
+| `plugin_intent_description_WriteLyricsCache` | `This plugin can write to the lyrics cache` |
+| `plugin_intent_description_WriteTranslations` | `This plugin can add new translations and overwrite existing ones` |
+| `plugin_intent_description_CreateModalDialogs` | `This plugin can create modal dialogs` |
+| `plugin_intent_description_ReadAutoLikeData` | `This plugin can read auto-like data` |
+| `plugin_intent_description_WriteAutoLikeData` | `This plugin can write to auto-like data` |
+| `feature_category_plugins` | `Plugins` |
+| `feature_desc_openPluginList` | `Open the list of plugins you have installed` |
+| `feature_btn_openPluginList` | `Open list` |
+| `feature_btn_openPluginList_running` | `Opening...` |
+
+<br></details>
+
+<details><summary><code>pt_BR</code> - 19 missing keys <i>(click to show)</i></summary><br>
+
+| Key | English text |
+| --- | ------------ |
+| `plugin_list_title` | `Plugin List` |
+| `plugin_list_permissions_header` | `Permissions:` |
+| `plugin_link_type_source` | `Repository` |
+| `plugin_link_type_other` | `Other / Homepage` |
+| `plugin_link_type_bug` | `Report a bug` |
+| `plugin_link_type_greasyfork` | `GreasyFork` |
+| `plugin_link_type_openuserjs` | `OpenUserJS` |
+| `plugin_intent_description_ReadFeatureConfig` | `This plugin can read the feature configuration` |
+| `plugin_intent_description_WriteFeatureConfig` | `This plugin can write to the feature configuration` |
+| `plugin_intent_description_SeeHiddenConfigValues` | `This plugin has access to hidden config values` |
+| `plugin_intent_description_WriteLyricsCache` | `This plugin can write to the lyrics cache` |
+| `plugin_intent_description_WriteTranslations` | `This plugin can add new translations and overwrite existing ones` |
+| `plugin_intent_description_CreateModalDialogs` | `This plugin can create modal dialogs` |
+| `plugin_intent_description_ReadAutoLikeData` | `This plugin can read auto-like data` |
+| `plugin_intent_description_WriteAutoLikeData` | `This plugin can write to auto-like data` |
+| `feature_category_plugins` | `Plugins` |
+| `feature_desc_openPluginList` | `Open the list of plugins you have installed` |
+| `feature_btn_openPluginList` | `Open list` |
+| `feature_btn_openPluginList_running` | `Opening...` |
+
+<br></details>
+
+<details><summary><code>zh_CN</code> - 19 missing keys <i>(click to show)</i></summary><br>
+
+| Key | English text |
+| --- | ------------ |
+| `plugin_list_title` | `Plugin List` |
+| `plugin_list_permissions_header` | `Permissions:` |
+| `plugin_link_type_source` | `Repository` |
+| `plugin_link_type_other` | `Other / Homepage` |
+| `plugin_link_type_bug` | `Report a bug` |
+| `plugin_link_type_greasyfork` | `GreasyFork` |
+| `plugin_link_type_openuserjs` | `OpenUserJS` |
+| `plugin_intent_description_ReadFeatureConfig` | `This plugin can read the feature configuration` |
+| `plugin_intent_description_WriteFeatureConfig` | `This plugin can write to the feature configuration` |
+| `plugin_intent_description_SeeHiddenConfigValues` | `This plugin has access to hidden config values` |
+| `plugin_intent_description_WriteLyricsCache` | `This plugin can write to the lyrics cache` |
+| `plugin_intent_description_WriteTranslations` | `This plugin can add new translations and overwrite existing ones` |
+| `plugin_intent_description_CreateModalDialogs` | `This plugin can create modal dialogs` |
+| `plugin_intent_description_ReadAutoLikeData` | `This plugin can read auto-like data` |
+| `plugin_intent_description_WriteAutoLikeData` | `This plugin can write to auto-like data` |
+| `feature_category_plugins` | `Plugins` |
+| `feature_desc_openPluginList` | `Open the list of plugins you have installed` |
+| `feature_btn_openPluginList` | `Open list` |
+| `feature_btn_openPluginList_running` | `Opening...` |
+
+<br></details>

+ 25 - 2
assets/translations/en_US.json

@@ -199,6 +199,24 @@
     "color_lightness_normal": "Normal",
     "color_lightness_lighter": "Lighter",
 
+    "plugin_list_title": "Plugin List",
+    "plugin_list_permissions_header": "Permissions:",
+
+    "plugin_link_type_source": "Repository",
+    "plugin_link_type_other": "Other / Homepage",
+    "plugin_link_type_bug": "Report a bug",
+    "plugin_link_type_greasyfork": "GreasyFork",
+    "plugin_link_type_openuserjs": "OpenUserJS",
+
+    "plugin_intent_description_ReadFeatureConfig": "This plugin can read the feature configuration",
+    "plugin_intent_description_WriteFeatureConfig": "This plugin can write to the feature configuration",
+    "plugin_intent_description_SeeHiddenConfigValues": "This plugin has access to hidden config values",
+    "plugin_intent_description_WriteLyricsCache": "This plugin can write to the lyrics cache",
+    "plugin_intent_description_WriteTranslations": "This plugin can add new translations and overwrite existing ones",
+    "plugin_intent_description_CreateModalDialogs": "This plugin can create modal dialogs",
+    "plugin_intent_description_ReadAutoLikeData": "This plugin can read auto-like data",
+    "plugin_intent_description_WriteAutoLikeData": "This plugin can write to auto-like data",
+
     "plugin_validation_error_no_property": "No property '%1' with type '%2'",
     "plugin_validation_error_invalid_property-1": "Property '%1' with value '%2' is invalid. Example value: %3",
     "plugin_validation_error_invalid_property-n": "Property '%1' with value '%2' is invalid. Example values: %3",
@@ -210,6 +228,7 @@
     "feature_category_input": "Input",
     "feature_category_lyrics": "Lyrics",
     "feature_category_integrations": "Integrations",
+    "feature_category_plugins": "Plugins",
     "feature_category_general": "General",
 
     "feature_desc_watermarkEnabled": "Show a watermark under the site logo that opens this config menu",
@@ -309,6 +328,12 @@
     "feature_desc_themeSongLightness": "How light the accent colors should be that are derived from the current ThemeSong theme",
     "feature_helptext_themeSongLightness": "Depending on the settings you chose for the ThemeSong extension, this feature allows you to adjust the lightness of the accent colors that are derived from the current theme.\n\nThis feature will have no effect if the ThemeSong extension is not installed.",
 
+    "feature_desc_openPluginList": "Open the list of plugins you have installed",
+    "feature_btn_openPluginList": "Open list",
+    "feature_btn_openPluginList_running": "Opening...",
+    "feature_desc_initTimeout": "How many seconds to wait for features to initialize before considering them to likely be in an errored state",
+    "feature_helptext_initTimeout": "This is the amount of time in seconds that the script will wait for features to initialize before considering them to likely be in an errored state.\nThis will not affect the script's behavior in a significant way, but if one of your plugins can't initialize in time, you should try increasing this value.",
+
     "feature_desc_locale": "Language",
     "feature_desc_localeFallback": "Use English as a fallback for missing translations (disable if you are contributing translations)",
     "feature_desc_versionCheck": "Check for updates every 24 hours",
@@ -318,8 +343,6 @@
     "feature_btn_checkVersionNow_running": "Checking...",
     "feature_desc_logLevel": "How much information to log to the console",
     "feature_helptext_logLevel": "Changing this is really only needed for debugging purposes as a result of experiencing a problem.\nShould you have one, you can increase the log level here, open your browser's JavaScript console (usually with Ctrl + Shift + K) and attach screenshots of that log in a GitHub issue.",
-    "feature_desc_initTimeout": "How many seconds to wait for features to initialize before considering them to likely be in an errored state",
-    "feature_helptext_initTimeout": "This is the amount of time in seconds that the script will wait for features to initialize before considering them to likely be in an errored state.\nThis will not affect the script's behavior in a significant way, but if one of your plugins can't initialize in time, you should try increasing this value.",
     "feature_desc_toastDuration": "For how many seconds custom toast notifications should be shown - 0 to disable them entirely",
     "feature_desc_showToastOnGenericError": "Show a notification when an error occurs?",
     "feature_helptext_showToastOnGenericError": "Should an error occur in the script that prevents parts of it from working correctly, a notification will be shown to inform you about it.\nIf you encounter a problem often, please copy the error from the JavaScript console (usually in the F12 menu) and please open an issue on GitHub.",

+ 79 - 0
dist/BetterYTM.css

@@ -930,6 +930,85 @@ body .bytm-ripple.slower {
   font-size: 1.45rem;
 }
 
+.bytm-plugin-list-row {
+  --bytm-plugin-list-row-max-height: 120px;
+
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  justify-content: space-between;
+  border-bottom: 1px solid var(--bytm-dialog-separator-color, #ccc);
+  padding: 8px 0px;
+  gap: 20px;
+  max-height: var(--bytm-plugin-list-row-max-height);
+}
+
+.bytm-plugin-list-row:first-child {
+  padding-top: 0;
+}
+
+.bytm-plugin-list-row:last-child {
+  border-bottom: none;
+  padding-bottom: 0;
+}
+
+.bytm-plugin-list-row-left,
+.bytm-plugin-list-row-right {
+  display: flex;
+  flex-direction: column;
+}
+
+.bytm-plugin-list-row-title {
+  display: inline-flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 1.6rem;
+  font-weight: bold;
+  margin-bottom: 4px;
+}
+
+.bytm-plugin-list-row-namespace {
+  font-size: 1rem;
+  font-weight: 400;
+  color: #ccc;
+}
+
+.bytm-plugin-list-row-links-list {
+  margin-top: 2px;
+}
+
+.bytm-plugin-list-row-links-list-bullet {
+  margin: 0 5px;
+}
+
+.bytm-plugin-list-row-right {
+  overflow-x: hidden;
+  overflow-y: auto;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  min-width: 25%;
+  max-height: var(--bytm-plugin-list-row-max-height);
+}
+
+.bytm-plugin-list-row-permissions-header {
+  position: sticky;
+  top: 0;
+  background-color: var(--bytm-dialog-bg);
+  font-size: 1.4rem;
+  font-weight: bold;
+  padding-bottom: 5px;
+}
+
+.bytm-plugin-list-row-intent-item::before {
+  content: "•";
+  margin-right: 3px;
+}
+
+.bytm-plugin-list-row-intent-item {
+  cursor: help;
+}
+
 :root {
   --bytm-menu-bg-highlight: #252525;
 }

+ 1 - 1
src/config.ts

@@ -100,7 +100,7 @@ export const migrations: DataMigrationsDict = {
       useDefaultConfig(oldData, [
         "showToastOnGenericError", "sponsorBlockIntegration",
         "themeSongIntegration", "themeSongLightness",
-        "errorOnLyricsNotFound",
+        "errorOnLyricsNotFound", "openPluginList",
       ]), [
         { key: "toastDuration", oldDefault: 3 },
       ]

+ 1 - 0
src/dialogs/index.ts

@@ -4,6 +4,7 @@ export * from "./autoLike.js";
 export * from "./changelog.js";
 export * from "./featConfig.js";
 export * from "./featHelp.js";
+export * from "./pluginList.js";
 export * from "./prompt.js";
 export * from "./versionNotif.js";
 export * from "./welcome.js";

+ 78 - 0
src/dialogs/pluginList.css

@@ -0,0 +1,78 @@
+.bytm-plugin-list-row {
+  --bytm-plugin-list-row-max-height: 120px;
+
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  justify-content: space-between;
+  border-bottom: 1px solid var(--bytm-dialog-separator-color, #ccc);
+  padding: 8px 0px;
+  gap: 20px;
+  max-height: var(--bytm-plugin-list-row-max-height);
+}
+
+.bytm-plugin-list-row:first-child {
+  padding-top: 0;
+}
+
+.bytm-plugin-list-row:last-child {
+  border-bottom: none;
+  padding-bottom: 0;
+}
+
+.bytm-plugin-list-row-left,
+.bytm-plugin-list-row-right {
+  display: flex;
+  flex-direction: column;
+}
+
+.bytm-plugin-list-row-title {
+  display: inline-flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 1.6rem;
+  font-weight: bold;
+  margin-bottom: 4px;
+}
+
+.bytm-plugin-list-row-namespace {
+  font-size: 1rem;
+  font-weight: 400;
+  color: #ccc;
+}
+
+.bytm-plugin-list-row-links-list {
+  margin-top: 2px;
+}
+
+.bytm-plugin-list-row-links-list-bullet {
+  margin: 0 5px;
+}
+
+.bytm-plugin-list-row-right {
+  overflow-x: hidden;
+  overflow-y: auto;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  min-width: 25%;
+  max-height: var(--bytm-plugin-list-row-max-height);
+}
+
+.bytm-plugin-list-row-permissions-header {
+  position: sticky;
+  top: 0;
+  background-color: var(--bytm-dialog-bg);
+  font-size: 1.4rem;
+  font-weight: bold;
+  padding-bottom: 5px;
+}
+
+.bytm-plugin-list-row-intent-item::before {
+  content: "•";
+  margin-right: 3px;
+}
+
+.bytm-plugin-list-row-intent-item {
+  cursor: help;
+}

+ 132 - 0
src/dialogs/pluginList.ts

@@ -0,0 +1,132 @@
+import { BytmDialog } from "../components/index.js";
+import { registeredPlugins } from "../interface.js";
+import { getLocale, t } from "../utils/translations.js";
+import { PluginIntent } from "../types.js";
+import "./pluginList.css";
+
+let pluginListDialog: BytmDialog | null = null;
+
+/** Creates and/or returns the import dialog */
+export async function getPluginListDialog() {
+  if(!pluginListDialog) {
+    pluginListDialog = new BytmDialog({
+      id: "welcome",
+      width: 900,
+      height: 1000,
+      verticalAlign: "top",
+      closeBtnEnabled: true,
+      closeOnBgClick: true,
+      closeOnEscPress: true,
+      destroyOnClose: true,
+      small: true,
+      renderHeader,
+      renderBody,
+    });
+  }
+  return pluginListDialog;
+}
+
+async function renderHeader() {
+  const titleElem = document.createElement("h2");
+  titleElem.id = "bytm-plugin-list-title";
+  titleElem.classList.add("bytm-dialog-title");
+  titleElem.role = "heading";
+  titleElem.ariaLevel = "1";
+  titleElem.tabIndex = 0;
+  titleElem.textContent = t("plugin_list_title");
+
+  return titleElem;
+}
+
+async function renderBody() {
+  const listContainerEl = document.createElement("div");
+  listContainerEl.id = "bytm-plugin-list-container";
+
+  for(const [, { def: { plugin, intents } }] of registeredPlugins.entries()) {
+    const rowEl = document.createElement("div");
+    rowEl.classList.add("bytm-plugin-list-row");
+
+    const leftEl = document.createElement("div");
+    leftEl.classList.add("bytm-plugin-list-row-left");
+    rowEl.appendChild(leftEl);
+
+    const titleEl = document.createElement("div");
+    titleEl.classList.add("bytm-plugin-list-row-title");
+    titleEl.tabIndex = 0;
+    titleEl.textContent = titleEl.title = titleEl.ariaLabel = `${plugin.name} - ${plugin.version}`;
+    leftEl.appendChild(titleEl);
+
+    const namespaceEl = document.createElement("div");
+    namespaceEl.classList.add("bytm-plugin-list-row-namespace");
+    namespaceEl.tabIndex = 0;
+    namespaceEl.textContent = namespaceEl.title = namespaceEl.ariaLabel = plugin.namespace;
+    titleEl.appendChild(namespaceEl);
+
+    const descEl = document.createElement("p");
+    descEl.classList.add("bytm-plugin-list-row-desc");
+    descEl.tabIndex = 0;
+    descEl.textContent = descEl.title = descEl.ariaLabel = plugin.description[getLocale()] ?? plugin.description.en_US;
+    leftEl.appendChild(descEl);
+
+    const linksList = document.createElement("div");
+    linksList.classList.add("bytm-plugin-list-row-links-list");
+    linksList.tabIndex = 0;
+    leftEl.appendChild(linksList);
+
+    let linkElCreated = false;
+    for(const key in plugin.homepage) {
+      const url = plugin.homepage[key as keyof typeof plugin.homepage];
+      if(!url)
+        continue;
+
+      if(linkElCreated) {
+        const bulletEl = document.createElement("span");
+        bulletEl.classList.add("bytm-plugin-list-row-links-list-bullet");
+        bulletEl.textContent = "•";
+        linksList.appendChild(bulletEl);
+      }
+      linkElCreated = true;
+
+      const linkEl = document.createElement("a");
+      linkEl.classList.add("bytm-plugin-list-row-link", "bytm-link");
+      linkEl.href = url;
+      linkEl.tabIndex = 0;
+      linkEl.target = "_blank";
+      linkEl.rel = "noopener noreferrer";
+      linkEl.textContent = t(`plugin_link_type_${key}`);
+      linkEl.title = linkEl.ariaLabel = url;
+      linksList.appendChild(linkEl);
+    }
+
+    const rightEl = document.createElement("div");
+    rightEl.classList.add("bytm-plugin-list-row-right");
+    rowEl.appendChild(rightEl);
+
+    const intentsAmount = Object.keys(PluginIntent).length / 2;
+    const intentsArr = typeof intents === "number" && intents > 0 ? (() => {
+      const arr = [];
+      for(let i = 0; i < intentsAmount; i++)
+        if(intents & (2 ** i)) arr.push(2 ** i);
+      return arr;
+    })() : [];
+
+    const permissionsHeaderEl = document.createElement("div");
+    permissionsHeaderEl.classList.add("bytm-plugin-list-row-permissions-header");
+    permissionsHeaderEl.tabIndex = 0;
+    permissionsHeaderEl.textContent = permissionsHeaderEl.title = permissionsHeaderEl.ariaLabel = t("plugin_list_permissions_header");
+    rightEl.appendChild(permissionsHeaderEl);
+
+    for(const intent of intentsArr) {
+      const intentEl = document.createElement("div");
+      intentEl.classList.add("bytm-plugin-list-row-intent-item");
+      intentEl.tabIndex = 0;
+      intentEl.textContent = PluginIntent[intent];
+      intentEl.title = intentEl.ariaLabel = t(`plugin_intent_description_${PluginIntent[intent]}`);
+      rightEl.appendChild(intentEl);
+    }
+
+    listContainerEl.appendChild(rowEl);
+  }
+
+  return listContainerEl;
+}

+ 22 - 13
src/features/index.ts

@@ -5,7 +5,7 @@ import { getFeature, promptResetConfig } from "../config.js";
 import { FeatureInfo, type ColorLightness, type ResourceKey, type SiteSelection, type SiteSelectionOrNone } from "../types.js";
 import { emitSiteEvent } from "../siteEvents.js";
 import langMapping from "../../assets/locales.json" with { type: "json" };
-import { getAutoLikeDialog, showPrompt } from "../dialogs/index.js";
+import { getAutoLikeDialog, getPluginListDialog, showPrompt } from "../dialogs/index.js";
 import { showIconToast } from "../components/index.js";
 import { mode } from "../constants.js";
 
@@ -666,7 +666,8 @@ export const featInfo = {
     type: "toggle",
     category: "integrations",
     default: true,
-    textAdornment: adornments.reloadRequired,
+    advanced: true,
+    textAdornment: () => combineAdornments([adornments.advanced, adornments.reloadRequired]),
   },
   themeSongIntegration: {
     type: "toggle",
@@ -682,6 +683,25 @@ export const featInfo = {
     textAdornment: adornments.reloadRequired,
   },
 
+  //#region cat:plugins
+  openPluginList: {
+    type: "button",
+    category: "plugins",
+    default: undefined,
+    click: () => getPluginListDialog().then(d => d.open()),
+  },
+  initTimeout: {
+    type: "number",
+    category: "plugins",
+    min: 3,
+    max: 30,
+    default: 8,
+    step: 0.1,
+    unit: "s",
+    advanced: true,
+    textAdornment: () => combineAdornments([adornments.advanced, adornments.reloadRequired]),
+  },
+
   //#region cat:general
   locale: {
     type: "select",
@@ -718,17 +738,6 @@ export const featInfo = {
     default: 1,
     textAdornment: adornments.reloadRequired,
   },
-  initTimeout: {
-    type: "number",
-    category: "general",
-    min: 3,
-    max: 30,
-    default: 8,
-    step: 0.1,
-    unit: "s",
-    advanced: true,
-    textAdornment: () => combineAdornments([adornments.advanced, adornments.reloadRequired]),
-  },
   toastDuration: {
     type: "slider",
     category: "general",

+ 4 - 2
src/index.ts

@@ -6,7 +6,7 @@ import { dbg, error, getDomain, info, getSessionId, log, setLogLevel, initTransl
 import { initSiteEvents } from "./siteEvents.js";
 import { emitInterface, initInterface, initPlugins } from "./interface.js";
 import { initObservers, addSelectorListener, globservers } from "./observers.js";
-import { getWelcomeDialog, showPrompt } from "./dialogs/index.js";
+import { getPluginListDialog, showPrompt } from "./dialogs/index.js";
 import type { FeatureConfig } from "./types.js";
 import {
   // layout
@@ -164,7 +164,7 @@ async function onDomLoad() {
 
     if(typeof await GM.getValue("bytm-installed") !== "string") {
       // open welcome menu with language selector
-      const dlg = await getWelcomeDialog();
+      const dlg = await getPluginListDialog();
       dlg.on("close", () => GM.setValue("bytm-installed", JSON.stringify({ timestamp: Date.now(), version: scriptInfo.version })));
       info("Showing welcome menu");
       await dlg.open();
@@ -340,6 +340,8 @@ function registerDevCommands() {
   if(mode !== "development")
     return;
 
+  GM.registerMenuCommand("Open plugin list", () => getPluginListDialog().then(dlg => dlg.open()));
+
   GM.registerMenuCommand("Reset config", async () => {
     if(confirm("Reset the configuration to its default values?\nThis will automatically reload the page.")) {
       await clearConfig();

+ 30 - 33
src/interface.ts

@@ -222,7 +222,7 @@ export function emitInterface<
 //#region register plugins
 
 /** Map of plugin ID and all registered plugins */
-const registeredPlugins = new Map<string, PluginItem>();
+export const registeredPlugins = new Map<string, PluginItem>();
 
 /** Map of plugin ID to auth token for plugins that have been registered */
 const registeredPluginTokens = new Map<string, string>();
@@ -231,43 +231,40 @@ const registeredPluginTokens = new Map<string, string>();
 export function initPlugins() {
   // TODO(v1.3): check perms and ask user for initial activation
 
-  /** Map of plugin ID and plugins that are queued up for registration */
-  const queuedPlugins = new Map<string, PluginItem>();
-
   const registerPlugin = (def: PluginDef): PluginRegisterResult => {
-    const validationErrors = validatePluginDef(def);
-    if(validationErrors)
-      throw new Error(`Failed to register plugin${def?.plugin?.name ? ` '${def?.plugin?.name}'` : ""} with invalid definition:\n- ${validationErrors.join("\n- ")}`);
-
-    const events = new NanoEmitter<PluginEventMap>({ publicEmit: true });
-    const token = randomId(32, 36);
-
-    queuedPlugins.set(getPluginKey(def), {
-      def: def,
-      events,
-    });
-    registeredPluginTokens.set(getPluginKey(def), token);
-
-    return {
-      info: getPluginInfo(token, def)!,
-      events,
-      token,
-    };
-  };
-
-  emitInterface("bytm:registerPlugin", (def: PluginDef) => registerPlugin(def));
-
-  for(const [key, { def, events }] of queuedPlugins) {
     try {
-      registeredPlugins.set(key, { def, events });
-      queuedPlugins.delete(key);
-      emitOnPlugins("pluginRegistered", (d) => sameDef(d, def), pluginDefToInfo(def)!);
-      info(`Initialized plugin '${getPluginKey(def)}'`);
+      if(registeredPlugins.has(getPluginKey(def)))
+        throw new Error(`Failed to register plugin '${getPluginKey(def)}': Plugin with the same name and namespace is already registered`);
+
+      const validationErrors = validatePluginDef(def);
+      if(validationErrors)
+        throw new Error(`Failed to register plugin${def?.plugin?.name ? ` '${def?.plugin?.name}'` : ""} with invalid definition:\n- ${validationErrors.join("\n- ")}`);
+
+      const events = new NanoEmitter<PluginEventMap>({ publicEmit: true });
+      const token = randomId(32, 36, true);
+
+      registeredPlugins.set(getPluginKey(def), {
+        def: def,
+        events,
+      });
+      registeredPluginTokens.set(getPluginKey(def), token);
+
+      info(`Successfully registered plugin '${getPluginKey(def)}'`);
+      setTimeout(() => emitOnPlugins("pluginRegistered", (d) => sameDef(d, def), pluginDefToInfo(def)!), 1);
+
+      return {
+        info: getPluginInfo(token, def)!,
+        events,
+        token,
+      };
     }
     catch(err) {
-      error(`Failed to initialize plugin '${getPluginKey(def)}':`, err);
+      error(`Failed to register plugin '${getPluginKey(def)}':`, err);
+      throw err;
     }
-  }
+  };
+
+  emitInterface("bytm:registerPlugin", (def: PluginDef) => registerPlugin(def));
 
   if(registeredPlugins.size > 0)
     log(`Registered ${registeredPlugins.size} ${autoPlural("plugin", registeredPlugins.size)}`);

+ 7 - 2
src/types.ts

@@ -379,6 +379,7 @@ export type FeatureCategory =
   | "input"
   | "lyrics"
   | "integrations"
+  | "plugins"
   | "general";
 
 type SelectOption = {
@@ -610,6 +611,12 @@ export interface FeatureConfig {
   /** Lightness of the color used when ThemeSong is enabled */
   themeSongLightness: ColorLightness;
 
+  //#region plugins
+  /** Button that opens the plugin list dialog */
+  openPluginList: undefined;
+  /** Amount of seconds until the feature initialization times out */
+  initTimeout: number;
+
   //#region misc
   /** The locale to use for translations */
   locale: TrLocale;
@@ -621,8 +628,6 @@ export interface FeatureConfig {
   checkVersionNow: undefined;
   /** The console log level - 0 = Debug, 1 = Info */
   logLevel: LogLevel;
-  /** Amount of seconds until the feature initialization times out */
-  initTimeout: number;
   /** Amount of seconds to show BYTM's toasts for */
   toastDuration: number;
   /** Whether to show a toast on generic errors */