Переглянути джерело

feat: replace all confirm() and alert() uses with new prompt dialog

Sv443 7 місяців тому
батько
коміт
119793609e

+ 14 - 11
src/components/BytmDialog.ts

@@ -4,6 +4,7 @@ import { clearInner, error, getDomain, getResourceUrl, onInteraction, warn } fro
 import { t } from "../utils/translations.js";
 import { emitInterface } from "../interface.js";
 import "./BytmDialog.css";
+import type { EventsMap } from "nanoevents";
 
 export interface BytmDialogOptions {
   /** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
@@ -36,16 +37,7 @@ export interface BytmDialogOptions {
   renderFooter?: () => HTMLElement | Promise<HTMLElement>;
 }
 
-// TODO: remove export as soon as config menu is migrated to use BytmDialog
-/** ID of the last opened (top-most) dialog */
-export let currentDialogId: string | null = null;
-/** IDs of all currently open dialogs, top-most first */
-export const openDialogs: string[] = [];
-/** TODO: remove as soon as config menu is migrated to use BytmDialog */
-export const setCurrentDialogId = (id: string | null) => currentDialogId = id;
-
-/** Creates and manages a modal dialog element */
-export class BytmDialog extends NanoEmitter<{
+export interface BytmDialogEvents extends EventsMap {
   /** Emitted just **after** the dialog is closed */
   close: () => void;
   /** Emitted just **after** the dialog is opened */
@@ -56,7 +48,18 @@ export class BytmDialog extends NanoEmitter<{
   clear: () => void;
   /** Emitted just **after** the dialog is destroyed and **before** all listeners are removed */
   destroy: () => void;
-}> {
+};
+
+// TODO: remove export as soon as config menu is migrated to use BytmDialog
+/** ID of the last opened (top-most) dialog */
+export let currentDialogId: string | null = null;
+/** IDs of all currently open dialogs, top-most first */
+export const openDialogs: string[] = [];
+/** TODO: remove as soon as config menu is migrated to use BytmDialog */
+export const setCurrentDialogId = (id: string | null) => currentDialogId = id;
+
+/** Creates and manages a modal dialog element */
+export class BytmDialog extends NanoEmitter<BytmDialogEvents> {
   public readonly options;
   public readonly id;
 

+ 1 - 8
src/components/hotkeyInput.ts

@@ -1,5 +1,5 @@
 import { emitSiteEvent, siteEvents } from "../siteEvents.js";
-import { onInteraction, setInnerHtml, t } from "../utils/index.js";
+import { getOS, onInteraction, setInnerHtml, t } from "../utils/index.js";
 import type { HotkeyObj } from "../types.js";
 import "./hotkeyInput.css";
 
@@ -186,13 +186,6 @@ function getHotkeyInfoHtml(hotkey: HotkeyObj) {
 </div>`;
 }
 
-/** Crude OS detection for keyboard layout purposes */
-function getOS() {
-  if(navigator.userAgent.match(/mac(\s?os|intel)/i))
-    return "mac";
-  return "other";
-}
-
 /** Converts a hotkey object to a string */
 function hotkeyToString(hotkey: HotkeyObj | undefined) {
   if(!hotkey)

+ 2 - 1
src/config.ts

@@ -6,6 +6,7 @@ import { compressionFormat, mode } from "./constants.js";
 import { emitInterface } from "./interface.js";
 import { closeCfgMenu } from "./menu/menu_old.js";
 import type { FeatureConfig, FeatureKey } from "./types.js";
+import { showPrompt } from "./dialogs/prompt.js";
 
 /** If this number is incremented, the features object data will be migrated to the new format */
 export const formatVersion = 7;
@@ -225,7 +226,7 @@ export function setDefaultFeatures() {
 }
 
 export async function promptResetConfig() {
-  if(confirm(t("reset_config_confirm"))) {
+  if(await showPrompt({ type: "confirm", message: t("reset_config_confirm") })) {
     closeCfgMenu();
     disableBeforeUnload();
     await setDefaultFeatures();

+ 5 - 4
src/dialogs/autoLike.ts

@@ -7,6 +7,7 @@ import { ExImDialog } from "../components/ExImDialog.js";
 import { compressionFormat } from "../constants.js";
 import type { AutoLikeData } from "../types.js";
 import "./autoLike.css";
+import { showPrompt } from "./prompt.js";
 
 let autoLikeDialog: BytmDialog | null = null;
 let autoLikeImExDialog: ExImDialog | null = null;
@@ -67,9 +68,9 @@ export async function getAutoLikeDialog() {
           log("Trying to import auto-like data:", parsed);
 
           if(!parsed || typeof parsed !== "object")
-            return alert(t("import_error_invalid"));
+            return await showPrompt({ message: t("import_error_invalid") });
           if(!parsed.channels || typeof parsed.channels !== "object" || Object.keys(parsed.channels).length === 0)
-            return alert(t("import_error_no_data"));
+            return await showPrompt({ message: t("import_error_no_data") });
 
           await autoLikeStore.setData(parsed);
           emitSiteEvent("autoLikeChannelsUpdated");
@@ -297,14 +298,14 @@ async function addAutoLikeEntryPrompts() {
   const id = parseChannelIdFromUrl(idPrompt) ?? (isValidChannelId(idPrompt) ? idPrompt : null);
 
   if(!id || id.length <= 0)
-    return alert(t("add_auto_like_channel_invalid_id"));
+    return await showPrompt({ message: t("add_auto_like_channel_invalid_id") });
 
   let overwriteName = false;
 
   const hasChannelEntry = autoLikeStore.getData().channels.find((ch) => ch.id === id);
 
   if(hasChannelEntry) {
-    if(!confirm(t("add_auto_like_channel_already_exists_prompt_new_name")))
+    if(!await showPrompt({ type: "confirm", message: t("add_auto_like_channel_already_exists_prompt_new_name") }))
       return;
     overwriteName = true;
   }

+ 7 - 6
src/dialogs/importCfg.ts

@@ -5,6 +5,7 @@ import { emitSiteEvent } from "../siteEvents.js";
 import { formatVersion, getFeatures, migrations, setFeatures } from "../config.js";
 import { disableBeforeUnload } from "../features/index.js";
 import { FeatureConfig } from "src/types.js";
+import { showPrompt } from "./prompt.js";
 
 let importDialog: BytmDialog | null = null;
 
@@ -73,11 +74,11 @@ async function renderFooter() {
       log("Trying to import config object:", parsed);
 
       if(!parsed || typeof parsed !== "object")
-        return alert(t("import_error_invalid"));
+        return await showPrompt({ message: t("import_error_invalid") });
       if(typeof parsed.formatVersion !== "number")
-        return alert(t("import_error_no_format_version"));
+        return await showPrompt({ message: t("import_error_no_format_version") });
       if(typeof parsed.data !== "object" || parsed.data === null || Object.keys(parsed.data).length === 0)
-        return alert(t("import_error_no_data"));
+        return await showPrompt({ message: t("import_error_no_data") });
       if(parsed.formatVersion < formatVersion) {
         let newData = JSON.parse(JSON.stringify(parsed.data));
         const sortedMigrations = Object.entries(migrations)
@@ -102,11 +103,11 @@ async function renderFooter() {
         parsed.data = newData;
       }
       else if(parsed.formatVersion !== formatVersion)
-        return alert(t("import_error_wrong_format_version", formatVersion, parsed.formatVersion));
+        return await showPrompt({ message: t("import_error_wrong_format_version", formatVersion, parsed.formatVersion) });
 
       await setFeatures({ ...getFeatures(), ...parsed.data });
 
-      if(confirm(t("import_success_confirm_reload"))) {
+      if(await showPrompt({ type: "confirm", message: t("import_success_confirm_reload") })) {
         disableBeforeUnload();
         return location.reload();
       }
@@ -117,7 +118,7 @@ async function renderFooter() {
     }
     catch(err) {
       warn("Couldn't import configuration:", err);
-      alert(t("import_error_invalid"));
+      await showPrompt({ message: t("import_error_invalid") });
     }
   });
 

+ 5 - 4
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 } from "../dialogs/index.js";
+import { getAutoLikeDialog, showPrompt } from "../dialogs/index.js";
 import { showIconToast } from "../components/index.js";
 import { mode } from "../constants.js";
 
@@ -625,9 +625,10 @@ export const featInfo = {
     category: "lyrics",
     async click() {
       const entries = getLyricsCache().length;
-      if(confirm(tp("lyrics_clear_cache_confirm_prompt", entries, entries))) {
+      const formattedEntries = entries.toLocaleString(getLocale().replace(/_/g, "-"), { style: "decimal", maximumFractionDigits: 0 });
+      if(await showPrompt({ type: "confirm", message: tp("lyrics_clear_cache_confirm_prompt", entries, formattedEntries) })) {
         await clearLyricsCache();
-        alert(t("lyrics_clear_cache_success"));
+        await showPrompt({ message: t("lyrics_clear_cache_success") });
       }
     },
     advanced: true,
@@ -637,7 +638,7 @@ export const featInfo = {
   //   type: "toggle",
   //   category: "lyrics",
   //   default: false,
-  //   change: () => setTimeout(() => confirm(t("lyrics_cache_changed_clear_confirm")) && clearLyricsCache(), 200),
+  //   change: () => setTimeout(async () => await showPrompt({ type: "confirm", message: t("lyrics_cache_changed_clear_confirm") }) && clearLyricsCache(), 200),
   //   advanced: true,
   //   textAdornment: adornments.experimental,
   //   reloadRequired: false,

+ 2 - 2
src/features/layout.ts

@@ -8,6 +8,7 @@ import { openCfgMenu } from "../menu/menu_old.js";
 import { createCircularBtn, createRipple } from "../components/index.js";
 import type { NumberNotation, ResourceKey, VideoVotesObj } from "../types.js";
 import "./layout.css";
+import { showPrompt } from "src/dialogs/prompt.js";
 
 //#region cfg menu btns
 
@@ -395,8 +396,7 @@ export async function initAboveQueueBtns() {
       titleKey: "clear_list",
       async interaction(evt: KeyboardEvent | MouseEvent) {
         try {
-          // TODO: better confirmation dialog?
-          if(evt.shiftKey || confirm(t("clear_list_confirm"))) {
+          if(evt.shiftKey || await showPrompt({ type: "confirm", message: t("clear_list_confirm") })) {
             const url = new URL(location.href);
             url.searchParams.delete("list");
             url.searchParams.set("time_continue", String(await getVideoTime(0)));

+ 2 - 1
src/features/lyrics.ts

@@ -6,6 +6,7 @@ import { getFeature } from "../config.js";
 import { addLyricsCacheEntryBest, getLyricsCacheEntry } from "./lyricsCache.js";
 import type { LyricsCacheEntry } from "../types.js";
 import { addSelectorListener } from "../observers.js";
+import { showPrompt } from "../dialogs/prompt.js";
 
 /** Ratelimit budget timeframe in seconds - should reflect what's in geniURL's docs */
 const geniUrlRatelimitTimeframe = 30;
@@ -238,7 +239,7 @@ export async function fetchLyricsUrls(artist: string, song: string): Promise<Omi
 
     if(fetchRes.status === 429) {
       const waitSeconds = Number(fetchRes.headers.get("retry-after") ?? geniUrlRatelimitTimeframe);
-      alert(tp("lyrics_rate_limited", waitSeconds, waitSeconds));
+      await showPrompt({ message: tp("lyrics_rate_limited", waitSeconds, waitSeconds) });
       return undefined;
     }
     else if(fetchRes.status < 200 || fetchRes.status >= 300) {

+ 2 - 1
src/features/songLists.ts

@@ -6,6 +6,7 @@ import { fetchLyricsUrlTop, createLyricsBtn, sanitizeArtists, sanitizeSong, spli
 import { getLyricsCacheEntry } from "./lyricsCache.js";
 import { addSelectorListener } from "../observers.js";
 import { createRipple } from "../components/ripple.js";
+import { showPrompt } from "../dialogs/prompt.js";
 import { getFeature } from "../config.js";
 import type { LyricsCacheEntry } from "../types.js";
 import "./songLists.css";
@@ -212,7 +213,7 @@ async function addQueueButtons(
 
         if(!lyricsUrl) {
           resetImgElem();
-          if(confirm(t("lyrics_not_found_confirm_open_search")))
+          if(await showPrompt({ type: "confirm", message: t("lyrics_not_found_confirm_open_search") }))
             openInTab(`https://genius.com/search?q=${encodeURIComponent(`${artistsSan} - ${songSan}`)}`);
           return;
         }

+ 4 - 4
src/features/versionCheck.ts

@@ -1,7 +1,7 @@
 import { scriptInfo } from "../constants.js";
 import { getFeature } from "../config.js";
 import { error, info, sendRequest, t } from "../utils/index.js";
-import { getVersionNotifDialog } from "../dialogs/index.js";
+import { getVersionNotifDialog, showPrompt } from "../dialogs/index.js";
 import { compare } from "compare-versions";
 import { LogLevel } from "../types.js";
 
@@ -37,12 +37,12 @@ export async function doVersionCheck(notifyNoUpdatesFound = false) {
   });
 
   // TODO: small dialog for "no update found" message?
-  const noUpdateFound = () => notifyNoUpdatesFound ? alert(t("no_updates_found")) : undefined;
+  const noUpdateFound = () => notifyNoUpdatesFound ? showPrompt({ message: t("no_updates_found") }) : undefined;
 
   const latestTag = res.finalUrl.split("/").pop()?.replace(/[a-zA-Z]/g, "");
 
   if(!latestTag)
-    return noUpdateFound();
+    return await noUpdateFound();
 
   info("Version check - current version:", scriptInfo.version, "- latest version:", latestTag, LogLevel.Info);
 
@@ -51,5 +51,5 @@ export async function doVersionCheck(notifyNoUpdatesFound = false) {
     await dialog.open();
     return;
   }
-  return noUpdateFound();
+  return await noUpdateFound();
 }

+ 8 - 8
src/menu/menu_old.ts

@@ -4,7 +4,7 @@ import { buildNumber, compressionFormat, host, mode, scriptInfo } from "../const
 import { featInfo, disableBeforeUnload } from "../features/index.js";
 import { error, getResourceUrl, info, log, resourceAsString, getLocale, hasKey, initTranslations, setLocale, t, arrayWithSeparators, tp, type TrKey, onInteraction, getDomain, copyToClipboard, warn, compressionSupported, tryToDecompressAndParse, setInnerHtml } from "../utils/index.js";
 import { emitSiteEvent, siteEvents } from "../siteEvents.js";
-import { getChangelogDialog, getFeatHelpDialog } from "../dialogs/index.js";
+import { getChangelogDialog, getFeatHelpDialog, showPrompt } from "../dialogs/index.js";
 import type { FeatureCategory, FeatureKey, FeatureConfig, HotkeyObj, FeatureInfo } from "../types.js";
 import { BytmDialog, ExImDialog, createHotkeyInput, createToggleInput, openDialogs, setCurrentDialogId } from "../components/index.js";
 import { emitInterface } from "../interface.js";
@@ -204,11 +204,11 @@ async function mountCfgMenu() {
         log("Trying to import configuration:", parsed);
 
         if(!parsed || typeof parsed !== "object")
-          return alert(t("import_error_invalid"));
+          return await showPrompt({ message: t("import_error_invalid") });
         if(typeof parsed.formatVersion !== "number")
-          return alert(t("import_error_no_format_version"));
+          return await showPrompt({ message: t("import_error_no_format_version") });
         if(typeof parsed.data !== "object" || parsed.data === null || Object.keys(parsed.data).length === 0)
-          return alert(t("import_error_no_data"));
+          return await showPrompt({ message: t("import_error_no_data") });
         if(parsed.formatVersion < formatVersion) {
           let newData = JSON.parse(JSON.stringify(parsed.data));
           const sortedMigrations = Object.entries(migrations)
@@ -233,11 +233,11 @@ async function mountCfgMenu() {
           parsed.data = newData;
         }
         else if(parsed.formatVersion !== formatVersion)
-          return alert(t("import_error_wrong_format_version", formatVersion, parsed.formatVersion));
+          return await showPrompt({ message: t("import_error_wrong_format_version", formatVersion, parsed.formatVersion) });
   
         await setFeatures({ ...getFeatures(), ...parsed.data });
   
-        if(confirm(t("import_success_confirm_reload"))) {
+        if(await showPrompt({ type: "confirm", message: t("import_success_confirm_reload") })) {
           disableBeforeUnload();
           return location.reload();
         }
@@ -247,7 +247,7 @@ async function mountCfgMenu() {
       }
       catch(err) {
         warn("Couldn't import configuration:", err);
-        alert(t("import_error_invalid"));
+        await showPrompt({ message: t("import_error_invalid") });
       }
     },
     title: () => t("bytm_config_export_import_title"),
@@ -314,7 +314,7 @@ async function mountCfgMenu() {
 
       const confirmText = newText !== initLangReloadText ? `${newText}\n\n────────────────────────────────\n\n${initLangReloadText}` : newText;
 
-      if(confirm(confirmText)) {
+      if(await showPrompt({ type: "confirm", message: confirmText })) {
         closeCfgMenu();
         disableBeforeUnload();
         location.reload();

+ 5 - 9
src/utils/dom.ts

@@ -1,9 +1,10 @@
-import { addGlobalStyle, debounce, getUnsafeWindow, randomId, type Stringifiable } from "@sv443-network/userutils";
-import { error, fetchCss, getDomain, t, warn } from "./index.js";
+import { addGlobalStyle, getUnsafeWindow, randomId, type Stringifiable } from "@sv443-network/userutils";
+import { error, fetchCss, getDomain, t } from "./index.js";
 import { addSelectorListener } from "../observers.js";
 import type { ResourceKey, TTPolicy } from "../types.js";
 import { siteEvents } from "../siteEvents.js";
 import DOMPurify from "dompurify";
+import { showPrompt } from "src/dialogs/prompt.js";
 
 /** Whether the DOM has finished loading and elements can be added or modified */
 export let domLoaded = false;
@@ -232,7 +233,7 @@ export function copyToClipboard(text: Stringifiable) {
     GM.setClipboard(String(text));
   }
   catch {
-    alert(t("copy_to_clipboard_error", String(text)));
+    showPrompt({ message: t("copy_to_clipboard_error", String(text)) });
   }
 }
 
@@ -248,10 +249,5 @@ export function setInnerHtml(element: HTMLElement, html: string) {
     });
   }
 
-  if(ttPolicy)
-    element.innerHTML = ttPolicy.createHTML(html);
-  else {
-    debounce(() => warn("Trusted Types policy not available, using innerHTML directly"), 1000, "rising")();
-    element.innerHTML = html;
-  }
+  element.innerHTML = ttPolicy?.createHTML(html) ?? html;
 }

+ 7 - 0
src/utils/misc.ts

@@ -184,6 +184,13 @@ export async function tryToDecompressAndParse<TData = Record<string, unknown>>(i
   return parsed;
 }
 
+/** Very crude OS detection */
+export function getOS() {
+  if(navigator.userAgent.match(/mac(\s?os|intel)/i))
+    return "mac";
+  return "other";
+}
+
 //#region resources
 
 /**