Browse Source

feat: generic prompt dialog

Sv443 7 months ago
parent
commit
189c2007f5

+ 1 - 0
assets/icons/alert.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#ffffff"><path d="M440-280h80v-240h-80v240Zm40-320q17 0 28.5-11.5T520-640q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640q0 17 11.5 28.5T480-600Zm0 520q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>

+ 2 - 0
assets/resources.json

@@ -10,10 +10,12 @@
   "css-vol_slider_size": "style/volSliderSize.css",
   "doc-changelog": "/changelog.md",
   "icon-advanced_mode": "icons/plus_circle_small.svg",
+  "icon-alert": "icons/alert.svg",
   "icon-arrow_down": "icons/arrow_down.svg",
   "icon-auto_like_enabled": "icons/auto_like_enabled.svg",
   "icon-auto_like": "icons/auto_like.svg",
   "icon-clear_list": "icons/clear_list.svg",
+  "icon-confirm": "icons/help.svg",
   "icon-copy": "icons/copy.svg",
   "icon-delete": "icons/delete.svg",
   "icon-edit": "icons/edit.svg",

+ 43 - 15
assets/translations/README.md

@@ -16,15 +16,15 @@ To submit or edit a translation, please follow [this guide](../../contributing.m
 ### Translation progress:
 | &nbsp; | Locale | Translated keys | Based on |
 | :----: | ------ | --------------- | :------: |
-|  | [`en_US`](./en_US.json) | `296` (default locale) |  |
-| ✅ | [`de_DE`](./de_DE.json) | `296/296` (100%) | ─ |
-|  | [`en_UK`](./en_UK.json) | `296/296` (100%) | `en_US` |
-| ‼️ | [`es_ES`](./es_ES.json) | `202/296` (68.2%) | ─ |
-| ‼️ | [`fr_FR`](./fr_FR.json) | `202/296` (68.2%) | ─ |
-| ‼️ | [`hi_IN`](./hi_IN.json) | `202/296` (68.2%) | ─ |
-| ‼️ | [`ja_JA`](./ja_JA.json) | `202/296` (68.2%) | ─ |
-| ‼️ | [`pt_BR`](./pt_BR.json) | `202/296` (68.2%) | ─ |
-| ‼️ | [`zh_CN`](./zh_CN.json) | `202/296` (68.2%) | ─ |
+|  | [`en_US`](./en_US.json) | `299` (default locale) |  |
+| ⚠ | [`de_DE`](./de_DE.json) | `296/299` (99%) | ─ |
+|  | [`en_UK`](./en_UK.json) | `299/299` (100%) | `en_US` |
+| ‼️ | [`es_ES`](./es_ES.json) | `202/299` (67.6%) | ─ |
+| ‼️ | [`fr_FR`](./fr_FR.json) | `202/299` (67.6%) | ─ |
+| ‼️ | [`hi_IN`](./hi_IN.json) | `202/299` (67.6%) | ─ |
+| ‼️ | [`ja_JA`](./ja_JA.json) | `202/299` (67.6%) | ─ |
+| ‼️ | [`pt_BR`](./pt_BR.json) | `202/299` (67.6%) | ─ |
+| ‼️ | [`zh_CN`](./zh_CN.json) | `202/299` (67.6%) | ─ |
 
 <sub>
 ✅ - Fully translated
@@ -45,7 +45,17 @@ This means to figure out which keys are untranslated, you will need to manually
 
 ### Missing keys:
 
-<details><summary><code>es_ES</code> - 94 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>de_DE</code> - 3 missing keys <i>(click to show)</i></summary><br>
+
+| Key | English text |
+| --- | ------------ |
+| `prompt_confirm` | `Confirm` |
+| `prompt_close` | `Close` |
+| `prompt_cancel` | `Cancel` |
+
+<br></details>
+
+<details><summary><code>es_ES</code> - 97 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -97,6 +107,9 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_like_export_import_title` | `Export or Import Auto-liked Channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
+| `prompt_confirm` | `Confirm` |
+| `prompt_close` | `Close` |
+| `prompt_cancel` | `Cancel` |
 | `vote_label_likes-1` | `%1 like` |
 | `vote_label_likes-n` | `%1 likes` |
 | `vote_label_dislikes-1` | `%1 dislike` |
@@ -146,7 +159,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>fr_FR</code> - 94 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>fr_FR</code> - 97 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -198,6 +211,9 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_like_export_import_title` | `Export or Import Auto-liked Channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
+| `prompt_confirm` | `Confirm` |
+| `prompt_close` | `Close` |
+| `prompt_cancel` | `Cancel` |
 | `vote_label_likes-1` | `%1 like` |
 | `vote_label_likes-n` | `%1 likes` |
 | `vote_label_dislikes-1` | `%1 dislike` |
@@ -247,7 +263,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>hi_IN</code> - 94 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>hi_IN</code> - 97 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -299,6 +315,9 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_like_export_import_title` | `Export or Import Auto-liked Channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
+| `prompt_confirm` | `Confirm` |
+| `prompt_close` | `Close` |
+| `prompt_cancel` | `Cancel` |
 | `vote_label_likes-1` | `%1 like` |
 | `vote_label_likes-n` | `%1 likes` |
 | `vote_label_dislikes-1` | `%1 dislike` |
@@ -348,7 +367,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>ja_JA</code> - 94 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>ja_JA</code> - 97 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -400,6 +419,9 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_like_export_import_title` | `Export or Import Auto-liked Channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
+| `prompt_confirm` | `Confirm` |
+| `prompt_close` | `Close` |
+| `prompt_cancel` | `Cancel` |
 | `vote_label_likes-1` | `%1 like` |
 | `vote_label_likes-n` | `%1 likes` |
 | `vote_label_dislikes-1` | `%1 dislike` |
@@ -449,7 +471,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>pt_BR</code> - 94 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>pt_BR</code> - 97 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -501,6 +523,9 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_like_export_import_title` | `Export or Import Auto-liked Channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
+| `prompt_confirm` | `Confirm` |
+| `prompt_close` | `Close` |
+| `prompt_cancel` | `Cancel` |
 | `vote_label_likes-1` | `%1 like` |
 | `vote_label_likes-n` | `%1 likes` |
 | `vote_label_dislikes-1` | `%1 dislike` |
@@ -550,7 +575,7 @@ This means to figure out which keys are untranslated, you will need to manually
 
 <br></details>
 
-<details><summary><code>zh_CN</code> - 94 missing keys <i>(click to show)</i></summary><br>
+<details><summary><code>zh_CN</code> - 97 missing keys <i>(click to show)</i></summary><br>
 
 | Key | English text |
 | --- | ------------ |
@@ -602,6 +627,9 @@ This means to figure out which keys are untranslated, you will need to manually
 | `auto_like_export_import_title` | `Export or Import Auto-liked Channels` |
 | `auto_like_export_desc` | `Copy the following text to export your auto-liked channels.` |
 | `auto_like_import_desc` | `Paste the auto-liked channels you want to import into the field below, then click the import button:` |
+| `prompt_confirm` | `Confirm` |
+| `prompt_close` | `Close` |
+| `prompt_cancel` | `Cancel` |
 | `vote_label_likes-1` | `%1 like` |
 | `vote_label_likes-n` | `%1 likes` |
 | `vote_label_dislikes-1` | `%1 dislike` |

+ 4 - 0
assets/translations/en_US.json

@@ -171,6 +171,10 @@
     "auto_like_export_desc": "Copy the following text to export your auto-liked channels.",
     "auto_like_import_desc": "Paste the auto-liked channels you want to import into the field below, then click the import button:",
 
+    "prompt_confirm": "Confirm",
+    "prompt_close": "Close",
+    "prompt_cancel": "Cancel",
+
     "vote_label_likes-1": "%1 like",
     "vote_label_likes-n": "%1 likes",
     "vote_label_dislikes-1": "%1 dislike",

+ 22 - 0
dist/BetterYTM.css

@@ -878,6 +878,28 @@ body .bytm-ripple.slower {
   justify-content: space-between;
 }
 
+#bytm-prompt-dialog-header svg {
+  width: 24px;
+  height: 24px;
+}
+
+#bytm-prompt-dialog-message {
+  margin-bottom: 15px;
+}
+
+#bytm-prompt-dialog-button-wrapper {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: end;
+}
+
+#bytm-prompt-dialog-buttons-cont {
+  display: flex;
+  flex-direction: row;
+  gap: 15px;
+}
+
 :root {
   --bytm-menu-bg-highlight: #252525;
 }

+ 1 - 0
package.json

@@ -99,6 +99,7 @@
     "express": "^4.19.2",
     "globals": "^15.6.0",
     "knip": "^5.22.2",
+    "nanoevents": "^9.0.0",
     "nodemon": "^3.1.4",
     "open-cli": "^8.0.0",
     "pnpm": "^9.4.0",

+ 3 - 0
pnpm-lock.yaml

@@ -108,6 +108,9 @@ importers:
       knip:
         specifier: ^5.22.2
         version: 5.22.2(@types/[email protected])([email protected])
+      nanoevents:
+        specifier: ^9.0.0
+        version: 9.0.0
       nodemon:
         specifier: ^3.1.4
         version: 3.1.4

+ 1 - 0
src/dialogs/index.ts

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

+ 21 - 0
src/dialogs/prompt.css

@@ -0,0 +1,21 @@
+#bytm-prompt-dialog-header svg {
+  width: 24px;
+  height: 24px;
+}
+
+#bytm-prompt-dialog-message {
+  margin-bottom: 15px;
+}
+
+#bytm-prompt-dialog-button-wrapper {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: end;
+}
+
+#bytm-prompt-dialog-buttons-cont {
+  display: flex;
+  flex-direction: row;
+  gap: 15px;
+}

+ 139 - 0
src/dialogs/prompt.ts

@@ -0,0 +1,139 @@
+import type { Emitter } from "nanoevents";
+import type { Stringifiable } from "@sv443-network/userutils";
+import { getOS, resourceAsString, setInnerHtml, t } from "../utils/index.js";
+import { BytmDialog, type BytmDialogEvents } from "../components/index.js";
+import "./prompt.css";
+
+export type PromptDialogRenderProps = {
+  type: "confirm" | "alert";
+  message: Stringifiable;
+};
+
+export type ShowPromptProps = Partial<PromptDialogRenderProps> & Required<Pick<PromptDialogRenderProps, "message">>;
+
+export type PromptDialogEmitter = Emitter<BytmDialogEvents & {
+  resolve: (result: boolean) => void;
+}>;
+
+let promptDialog: PromptDialog | null = null;
+
+// TODO: implement prompt() equivalent for text input
+class PromptDialog extends BytmDialog {
+  constructor(props: PromptDialogRenderProps) {
+    super({
+      id: "prompt-dialog",
+      width: 400,
+      height: 400,
+      destroyOnClose: true,
+      closeBtnEnabled: true,
+      closeOnBgClick: props.type === "alert",
+      closeOnEscPress: true,
+      small: true,
+      renderHeader: () => this.renderHeader(props),
+      renderBody: () => this.renderBody(props),
+    });
+  }
+
+  async renderHeader({ type }: PromptDialogRenderProps) {
+    const headerEl = document.createElement("div");
+    headerEl.id = "bytm-prompt-dialog-header";
+    const iconSvg = await resourceAsString(type === "alert" ? "icon-alert" : "icon-confirm");
+    if(iconSvg)
+      setInnerHtml(headerEl, iconSvg);
+
+    return headerEl;
+  }
+
+  async renderBody({ type, message }: PromptDialogRenderProps) {
+    const resolve = (val: boolean) => (this.events as PromptDialogEmitter).emit("resolve", val);
+
+    const contElem = document.createElement("div");
+
+    const messageElem = document.createElement("h3");
+    messageElem.role = "subheading";
+    messageElem.tabIndex = 0;
+    messageElem.textContent = String(message);
+    messageElem.id = "bytm-prompt-dialog-message";
+    contElem.appendChild(messageElem);
+
+    const buttonsWrapper = document.createElement("div");
+    buttonsWrapper.id = "bytm-prompt-dialog-button-wrapper";
+
+    const buttonsCont = document.createElement("div");
+    buttonsCont.id = "bytm-prompt-dialog-buttons-cont";
+
+    let confirmBtn: HTMLButtonElement | undefined;
+    if(type === "confirm") {
+      confirmBtn = document.createElement("button");
+      confirmBtn.textContent = confirmBtn.ariaLabel = confirmBtn.title = t("prompt_confirm");
+      confirmBtn.id = "bytm-prompt-dialog-confirm";
+      confirmBtn.tabIndex = 0;
+      confirmBtn.addEventListener("click", () => {
+        resolve(true);
+        promptDialog?.close();
+      }, { once: true });
+    }
+
+    const closeBtn = document.createElement("button");
+    closeBtn.textContent = closeBtn.ariaLabel = closeBtn.title = t(type === "alert" ? "prompt_close" : "prompt_cancel");
+    closeBtn.id = "bytm-prompt-dialog-close";
+    closeBtn.tabIndex = 0;
+    closeBtn.addEventListener("click", () => {
+      resolve(type === "alert");
+      promptDialog?.close();
+    }, { once: true });
+
+    confirmBtn && getOS() !== "mac" && buttonsCont.appendChild(confirmBtn);
+    buttonsCont.appendChild(closeBtn);
+    confirmBtn && getOS() === "mac" && buttonsCont.appendChild(confirmBtn);
+
+    buttonsWrapper.appendChild(buttonsCont);
+    contElem.appendChild(buttonsWrapper);
+
+    return contElem;
+  }
+}
+
+/** Shows a prompt dialog of the specified type and resolves true if the user confirms it or false if they cancel it - always resolves true with type "alert" */
+export function showPrompt({
+  type = "alert",
+  message,
+}: ShowPromptProps) {
+  return new Promise<boolean>((resolve) => {
+    if(BytmDialog.getOpenDialogs().includes("prompt-dialog"))
+      promptDialog?.close();
+
+    promptDialog = new PromptDialog({
+      type,
+      message,
+    });
+
+    // make config menu inert while prompt dialog is open
+    promptDialog.once("open", () => document.querySelector("#bytm-cfg-menu")?.setAttribute("inert", "true"));
+    promptDialog.once("close", () => document.querySelector("#bytm-cfg-menu")?.removeAttribute("inert"));
+
+    let resolveVal: boolean | undefined;
+    const tryResolve = () => resolve(typeof resolveVal === "boolean" ? resolveVal : false);
+
+    const resolveUnsub = promptDialog.on("resolve" as "_", (val: boolean) => {
+      resolveUnsub();
+      if(resolveVal)
+        return;
+      resolveVal = val;
+      tryResolve();
+    });
+
+    const closeUnsub = promptDialog.on("close", () => {
+      closeUnsub();
+      if(resolveVal)
+        return;
+      resolveVal = type === "alert";
+      tryResolve();
+    });
+
+    promptDialog.open();
+  });
+}
+
+//@ts-ignore
+unsafeWindow.showPrompt = showPrompt;