Greasy Fork is available in English.

UserUtils

Various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent & synchronous JSON databases, modify the DOM more easily, useful math and array functions and more

Questo script non dovrebbe essere installato direttamente. È una libreria per altri script da includere con la chiave // @require https://update.greasyfork.org/scripts/472956/1482087/UserUtils.js

// ==UserScript==
// @namespace    https://github.com/Sv443-Network/UserUtils
// @exclude      *
// @author       Sv443
// @supportURL   https://github.com/Sv443-Network/UserUtils/issues
// @homepageURL  https://github.com/Sv443-Network/UserUtils

// ==UserLibrary==
// @name         UserUtils
// @description  Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and more
// @version      8.3.2
// @license      MIT
// @copyright    Sv443 (https://github.com/Sv443)

// ==/UserScript==
// ==/UserLibrary==

// ==OpenUserJS==
// @author       Sv443
// ==/OpenUserJS==

var UserUtils = (function (exports) {
  var __defProp = Object.defineProperty;
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __spreadValues = (a, b) => {
    for (var prop in b || (b = {}))
      if (__hasOwnProp.call(b, prop))
        __defNormalProp(a, prop, b[prop]);
    if (__getOwnPropSymbols)
      for (var prop of __getOwnPropSymbols(b)) {
        if (__propIsEnum.call(b, prop))
          __defNormalProp(a, prop, b[prop]);
      }
    return a;
  };
  var __objRest = (source, exclude) => {
    var target = {};
    for (var prop in source)
      if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
        target[prop] = source[prop];
    if (source != null && __getOwnPropSymbols)
      for (var prop of __getOwnPropSymbols(source)) {
        if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
          target[prop] = source[prop];
      }
    return target;
  };
  var __publicField = (obj, key, value) => {
    __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
    return value;
  };
  var __async = (__this, __arguments, generator) => {
    return new Promise((resolve, reject) => {
      var fulfilled = (value) => {
        try {
          step(generator.next(value));
        } catch (e) {
          reject(e);
        }
      };
      var rejected = (value) => {
        try {
          step(generator.throw(value));
        } catch (e) {
          reject(e);
        }
      };
      var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
      step((generator = generator.apply(__this, __arguments)).next());
    });
  };

  // lib/math.ts
  function clamp(value, min, max) {
    return Math.max(Math.min(value, max), min);
  }
  function mapRange(value, range1min, range1max, range2min, range2max) {
    if (typeof range2min === "undefined" || range2max === void 0) {
      range2max = range1max;
      range2min = 0;
      range1max = range1min;
      range1min = 0;
    }
    if (Number(range1min) === 0 && Number(range2min) === 0)
      return value * (range2max / range1max);
    return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min;
  }
  function randRange(...args) {
    let min, max, enhancedEntropy = false;
    if (typeof args[0] === "number" && typeof args[1] === "number")
      [min, max] = args;
    else if (typeof args[0] === "number" && typeof args[1] !== "number") {
      min = 0;
      [max] = args;
    } else
      throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
    if (typeof args[2] === "boolean")
      enhancedEntropy = args[2];
    else if (typeof args[1] === "boolean")
      enhancedEntropy = args[1];
    min = Number(min);
    max = Number(max);
    if (isNaN(min) || isNaN(max))
      return NaN;
    if (min > max)
      throw new TypeError(`Parameter "min" can't be bigger than "max"`);
    if (enhancedEntropy) {
      const uintArr = new Uint8Array(1);
      crypto.getRandomValues(uintArr);
      return Number(Array.from(
        uintArr,
        (v) => Math.round(mapRange(v, 0, 255, min, max)).toString(10).substring(0, 1)
      ).join(""));
    } else
      return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  // lib/array.ts
  function randomItem(array) {
    return randomItemIndex(array)[0];
  }
  function randomItemIndex(array) {
    if (array.length === 0)
      return [void 0, void 0];
    const idx = randRange(array.length - 1);
    return [array[idx], idx];
  }
  function takeRandomItem(arr) {
    const [itm, idx] = randomItemIndex(arr);
    if (idx === void 0)
      return void 0;
    arr.splice(idx, 1);
    return itm;
  }
  function randomizeArray(array) {
    const retArray = [...array];
    if (array.length === 0)
      return retArray;
    for (let i = retArray.length - 1; i > 0; i--) {
      const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
      [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
    }
    return retArray;
  }

  // lib/colors.ts
  function hexToRgb(hex) {
    hex = (hex.startsWith("#") ? hex.slice(1) : hex).trim();
    const a = hex.length === 8 || hex.length === 4 ? parseInt(hex.slice(-(hex.length / 4)), 16) / (hex.length === 8 ? 255 : 15) : void 0;
    if (!isNaN(Number(a)))
      hex = hex.slice(0, -(hex.length / 4));
    if (hex.length === 3 || hex.length === 4)
      hex = hex.split("").map((c) => c + c).join("");
    const bigint = parseInt(hex, 16);
    const r = bigint >> 16 & 255;
    const g = bigint >> 8 & 255;
    const b = bigint & 255;
    return [clamp(r, 0, 255), clamp(g, 0, 255), clamp(b, 0, 255), typeof a === "number" ? clamp(a, 0, 1) : void 0];
  }
  function rgbToHex(red, green, blue, alpha, withHash = true, upperCase = false) {
    const toHexVal = (n) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0")[upperCase ? "toUpperCase" : "toLowerCase"]();
    return `${withHash ? "#" : ""}${toHexVal(red)}${toHexVal(green)}${toHexVal(blue)}${alpha ? toHexVal(alpha * 255) : ""}`;
  }
  function lightenColor(color, percent, upperCase = false) {
    return darkenColor(color, percent * -1, upperCase);
  }
  function darkenColor(color, percent, upperCase = false) {
    var _a;
    color = color.trim();
    const darkenRgb = (r2, g2, b2, percent2) => {
      r2 = Math.max(0, Math.min(255, r2 - r2 * percent2 / 100));
      g2 = Math.max(0, Math.min(255, g2 - g2 * percent2 / 100));
      b2 = Math.max(0, Math.min(255, b2 - b2 * percent2 / 100));
      return [r2, g2, b2];
    };
    let r, g, b, a;
    const isHexCol = color.match(/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/);
    if (isHexCol)
      [r, g, b, a] = hexToRgb(color);
    else if (color.startsWith("rgb")) {
      const rgbValues = (_a = color.match(/\d+(\.\d+)?/g)) == null ? void 0 : _a.map(Number);
      if (!rgbValues)
        throw new Error("Invalid RGB/RGBA color format");
      [r, g, b, a] = rgbValues;
    } else
      throw new Error("Unsupported color format");
    [r, g, b] = darkenRgb(r, g, b, percent);
    if (isHexCol)
      return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
    else if (color.startsWith("rgba"))
      return `rgba(${r}, ${g}, ${b}, ${a != null ? a : NaN})`;
    else if (color.startsWith("rgb"))
      return `rgb(${r}, ${g}, ${b})`;
    else
      throw new Error("Unsupported color format");
  }

  // lib/dom.ts
  function getUnsafeWindow() {
    try {
      return unsafeWindow;
    } catch (e) {
      return window;
    }
  }
  function addParent(element, newParent) {
    const oldParent = element.parentNode;
    if (!oldParent)
      throw new Error("Element doesn't have a parent node");
    oldParent.replaceChild(newParent, element);
    newParent.appendChild(element);
    return newParent;
  }
  function addGlobalStyle(style) {
    const styleElem = document.createElement("style");
    setInnerHtmlUnsafe(styleElem, style);
    document.head.appendChild(styleElem);
    return styleElem;
  }
  function preloadImages(srcUrls, rejects = false) {
    const promises = srcUrls.map((src) => new Promise((res, rej) => {
      const image = new Image();
      image.src = src;
      image.addEventListener("load", () => res(image));
      image.addEventListener("error", (evt) => rejects && rej(evt));
    }));
    return Promise.allSettled(promises);
  }
  function openInNewTab(href, background) {
    try {
      GM.openInTab(href, background);
    } catch (e) {
      const openElem = document.createElement("a");
      Object.assign(openElem, {
        className: "userutils-open-in-new-tab",
        target: "_blank",
        rel: "noopener noreferrer",
        href
      });
      openElem.style.display = "none";
      document.body.appendChild(openElem);
      openElem.click();
      setTimeout(openElem.remove, 50);
    }
  }
  function interceptEvent(eventObject, eventName, predicate = () => true) {
    var _a;
    if ((eventObject === window || eventObject === getUnsafeWindow()) && ((_a = GM == null ? void 0 : GM.info) == null ? void 0 : _a.scriptHandler) && GM.info.scriptHandler === "FireMonkey")
      throw new Error("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
    Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 100);
    if (isNaN(Error.stackTraceLimit))
      Error.stackTraceLimit = 100;
    (function(original) {
      eventObject.__proto__.addEventListener = function(...args) {
        var _a2, _b;
        const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a2 = args[1]) == null ? void 0 : _a2.handleEvent) != null ? _b : () => void 0;
        args[1] = function(...a) {
          if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
            return;
          else
            return origListener.apply(this, a);
        };
        original.apply(this, args);
      };
    })(eventObject.__proto__.addEventListener);
  }
  function interceptWindowEvent(eventName, predicate = () => true) {
    return interceptEvent(getUnsafeWindow(), eventName, predicate);
  }
  function isScrollable(element) {
    const { overflowX, overflowY } = getComputedStyle(element);
    return {
      vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
      horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
    };
  }
  function observeElementProp(element, property, callback) {
    const elementPrototype = Object.getPrototypeOf(element);
    if (elementPrototype.hasOwnProperty(property)) {
      const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property);
      Object.defineProperty(element, property, {
        get: function() {
          var _a;
          return (_a = descriptor == null ? void 0 : descriptor.get) == null ? void 0 : _a.apply(this, arguments);
        },
        set: function() {
          var _a;
          const oldValue = this[property];
          (_a = descriptor == null ? void 0 : descriptor.set) == null ? void 0 : _a.apply(this, arguments);
          const newValue = this[property];
          if (typeof callback === "function") {
            callback.bind(this, oldValue, newValue);
          }
          return newValue;
        }
      });
    }
  }
  function getSiblingsFrame(refElement, siblingAmount, refElementAlignment = "center-top", includeRef = true) {
    var _a, _b;
    const siblings = [...(_b = (_a = refElement.parentNode) == null ? void 0 : _a.childNodes) != null ? _b : []];
    const elemSiblIdx = siblings.indexOf(refElement);
    if (elemSiblIdx === -1)
      throw new Error("Element doesn't have a parent node");
    if (refElementAlignment === "top")
      return [...siblings.slice(elemSiblIdx + Number(!includeRef), elemSiblIdx + siblingAmount + Number(!includeRef))];
    else if (refElementAlignment.startsWith("center-")) {
      const halfAmount = (refElementAlignment === "center-bottom" ? Math.ceil : Math.floor)(siblingAmount / 2);
      const startIdx = Math.max(0, elemSiblIdx - halfAmount);
      const topOffset = Number(refElementAlignment === "center-top" && siblingAmount % 2 === 0 && includeRef);
      const btmOffset = Number(refElementAlignment === "center-bottom" && siblingAmount % 2 !== 0 && includeRef);
      const startIdxWithOffset = startIdx + topOffset + btmOffset;
      return [
        ...siblings.filter((_, idx) => includeRef || idx !== elemSiblIdx).slice(startIdxWithOffset, startIdxWithOffset + siblingAmount)
      ];
    } else if (refElementAlignment === "bottom")
      return [...siblings.slice(elemSiblIdx - siblingAmount + Number(includeRef), elemSiblIdx + Number(includeRef))];
    return [];
  }
  var ttPolicy;
  function setInnerHtmlUnsafe(element, html) {
    var _a, _b, _c;
    if (!ttPolicy && typeof ((_a = window == null ? void 0 : window.trustedTypes) == null ? void 0 : _a.createPolicy) === "function") {
      ttPolicy = window.trustedTypes.createPolicy("_uu_set_innerhtml_unsafe", {
        createHTML: (unsafeHtml) => unsafeHtml
      });
    }
    element.innerHTML = (_c = (_b = ttPolicy == null ? void 0 : ttPolicy.createHTML) == null ? void 0 : _b.call(ttPolicy, html)) != null ? _c : html;
    return element;
  }

  // lib/crypto.ts
  function compress(input, compressionFormat, outputType = "string") {
    return __async(this, null, function* () {
      const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input;
      const comp = new CompressionStream(compressionFormat);
      const writer = comp.writable.getWriter();
      writer.write(byteArray);
      writer.close();
      const buf = yield new Response(comp.readable).arrayBuffer();
      return outputType === "arrayBuffer" ? buf : ab2str(buf);
    });
  }
  function decompress(input, compressionFormat, outputType = "string") {
    return __async(this, null, function* () {
      const byteArray = typeof input === "string" ? str2ab(input) : input;
      const decomp = new DecompressionStream(compressionFormat);
      const writer = decomp.writable.getWriter();
      writer.write(byteArray);
      writer.close();
      const buf = yield new Response(decomp.readable).arrayBuffer();
      return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf);
    });
  }
  function ab2str(buf) {
    return getUnsafeWindow().btoa(
      new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "")
    );
  }
  function str2ab(str) {
    return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0));
  }
  function computeHash(input, algorithm = "SHA-256") {
    return __async(this, null, function* () {
      let data;
      if (typeof input === "string") {
        const encoder = new TextEncoder();
        data = encoder.encode(input);
      } else
        data = input;
      const hashBuffer = yield crypto.subtle.digest(algorithm, data);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
      return hashHex;
    });
  }
  function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true) {
    let arr = [];
    const caseArr = randomCase ? [0, 1] : [0];
    if (enhancedEntropy) {
      const uintArr = new Uint8Array(length);
      crypto.getRandomValues(uintArr);
      arr = Array.from(
        uintArr,
        (v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1)
      );
    } else {
      arr = Array.from(
        { length },
        () => Math.floor(Math.random() * radix).toString(radix)
      );
    }
    if (!arr.some((v) => /[a-zA-Z]/.test(v)))
      return arr.join("");
    return arr.map((v) => caseArr[randRange(0, caseArr.length - 1, enhancedEntropy)] === 1 ? v.toUpperCase() : v).join("");
  }

  // lib/DataStore.ts
  var DataStore = class {
    /**
     * Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions.  
     * Supports migrating data from older versions to newer ones and populating the cache with default data if no persistent data is found.  
     *   
     * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue` if the storageMethod is left as the default of `"GM"`  
     * ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData`
     * 
     * @template TData The type of the data that is saved in persistent storage for the currently set format version (will be automatically inferred from `defaultData` if not provided) - **This has to be a JSON-compatible object!** (no undefined, circular references, etc.)
     * @param options The options for this DataStore instance
     */
    constructor(options) {
      __publicField(this, "id");
      __publicField(this, "formatVersion");
      __publicField(this, "defaultData");
      __publicField(this, "encodeData");
      __publicField(this, "decodeData");
      __publicField(this, "storageMethod");
      __publicField(this, "cachedData");
      __publicField(this, "migrations");
      __publicField(this, "migrateIds", []);
      var _a;
      this.id = options.id;
      this.formatVersion = options.formatVersion;
      this.defaultData = options.defaultData;
      this.cachedData = options.defaultData;
      this.migrations = options.migrations;
      if (options.migrateIds)
        this.migrateIds = Array.isArray(options.migrateIds) ? options.migrateIds : [options.migrateIds];
      this.storageMethod = (_a = options.storageMethod) != null ? _a : "GM";
      this.encodeData = options.encodeData;
      this.decodeData = options.decodeData;
    }
    //#region public
    /**
     * Loads the data saved in persistent storage into the in-memory cache and also returns it.  
     * Automatically populates persistent storage with default data if it doesn't contain any data yet.  
     * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
     */
    loadData() {
      return __async(this, null, function* () {
        try {
          if (this.migrateIds.length > 0) {
            yield this.migrateId(this.migrateIds);
            this.migrateIds = [];
          }
          const gmData = yield this.getValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultData));
          let gmFmtVer = Number(yield this.getValue(`_uucfgver-${this.id}`, NaN));
          if (typeof gmData !== "string") {
            yield this.saveDefaultData();
            return __spreadValues({}, this.defaultData);
          }
          const isEncoded = Boolean(yield this.getValue(`_uucfgenc-${this.id}`, false));
          let saveData = false;
          if (isNaN(gmFmtVer)) {
            yield this.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
            saveData = true;
          }
          let parsed = yield this.deserializeData(gmData, isEncoded);
          if (gmFmtVer < this.formatVersion && this.migrations)
            parsed = yield this.runMigrations(parsed, gmFmtVer);
          if (saveData)
            yield this.setData(parsed);
          this.cachedData = __spreadValues({}, parsed);
          return this.cachedData;
        } catch (err) {
          console.warn("Error while parsing JSON data, resetting it to the default value.", err);
          yield this.saveDefaultData();
          return this.defaultData;
        }
      });
    }
    /**
     * Returns a copy of the data from the in-memory cache.  
     * Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
     * @param deepCopy Whether to return a deep copy of the data (default: `false`) - only necessary if your data object is nested and may have a bigger performance impact if enabled
     */
    getData(deepCopy = false) {
      return deepCopy ? this.deepCopy(this.cachedData) : __spreadValues({}, this.cachedData);
    }
    /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
    setData(data) {
      this.cachedData = data;
      const useEncoding = this.encodingEnabled();
      return new Promise((resolve) => __async(this, null, function* () {
        yield Promise.all([
          this.setValue(`_uucfg-${this.id}`, yield this.serializeData(data, useEncoding)),
          this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
          this.setValue(`_uucfgenc-${this.id}`, useEncoding)
        ]);
        resolve();
      }));
    }
    /** Saves the default data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
    saveDefaultData() {
      return __async(this, null, function* () {
        this.cachedData = this.defaultData;
        const useEncoding = this.encodingEnabled();
        return new Promise((resolve) => __async(this, null, function* () {
          yield Promise.all([
            this.setValue(`_uucfg-${this.id}`, yield this.serializeData(this.defaultData, useEncoding)),
            this.setValue(`_uucfgver-${this.id}`, this.formatVersion),
            this.setValue(`_uucfgenc-${this.id}`, useEncoding)
          ]);
          resolve();
        }));
      });
    }
    /**
     * Call this method to clear all persistently stored data associated with this DataStore instance.  
     * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}  
     * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.  
     *   
     * ⚠️ This requires the additional directive `@grant GM.deleteValue` if the storageMethod is left as the default of `"GM"`
     */
    deleteData() {
      return __async(this, null, function* () {
        yield Promise.all([
          this.deleteValue(`_uucfg-${this.id}`),
          this.deleteValue(`_uucfgver-${this.id}`),
          this.deleteValue(`_uucfgenc-${this.id}`)
        ]);
      });
    }
    /** Returns whether encoding and decoding are enabled for this DataStore instance */
    encodingEnabled() {
      return Boolean(this.encodeData && this.decodeData);
    }
    //#region migrations
    /**
     * Runs all necessary migration functions consecutively and saves the result to the in-memory cache and persistent storage and also returns it.  
     * This method is automatically called by {@linkcode loadData()} if the data format has changed since the last time the data was saved.  
     * Though calling this method manually is not necessary, it can be useful if you want to run migrations for special occasions like a user importing potentially outdated data that has been previously exported.  
     *   
     * If one of the migrations fails, the data will be reset to the default value if `resetOnError` is set to `true` (default). Otherwise, an error will be thrown and no data will be saved.
     */
    runMigrations(oldData, oldFmtVer, resetOnError = true) {
      return __async(this, null, function* () {
        if (!this.migrations)
          return oldData;
        let newData = oldData;
        const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
        let lastFmtVer = oldFmtVer;
        for (const [fmtVer, migrationFunc] of sortedMigrations) {
          const ver = Number(fmtVer);
          if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
            try {
              const migRes = migrationFunc(newData);
              newData = migRes instanceof Promise ? yield migRes : migRes;
              lastFmtVer = oldFmtVer = ver;
            } catch (err) {
              if (!resetOnError)
                throw new Error(`Error while running migration function for format version '${fmtVer}'`);
              console.error(`Error while running migration function for format version '${fmtVer}' - resetting to the default value.`, err);
              yield this.saveDefaultData();
              return this.getData();
            }
          }
        }
        yield Promise.all([
          this.setValue(`_uucfg-${this.id}`, yield this.serializeData(newData)),
          this.setValue(`_uucfgver-${this.id}`, lastFmtVer),
          this.setValue(`_uucfgenc-${this.id}`, this.encodingEnabled())
        ]);
        return this.cachedData = __spreadValues({}, newData);
      });
    }
    /**
     * Tries to migrate the currently saved persistent data from one or more old IDs to the ID set in the constructor.  
     * If no data exist for the old ID(s), nothing will be done, but some time may still pass trying to fetch the non-existent data.
     */
    migrateId(oldIds) {
      return __async(this, null, function* () {
        const ids = Array.isArray(oldIds) ? oldIds : [oldIds];
        yield Promise.all(ids.map((id) => __async(this, null, function* () {
          const data = yield this.getValue(`_uucfg-${id}`, JSON.stringify(this.defaultData));
          const fmtVer = Number(yield this.getValue(`_uucfgver-${id}`, NaN));
          const isEncoded = Boolean(yield this.getValue(`_uucfgenc-${id}`, false));
          if (data === void 0 || isNaN(fmtVer))
            return;
          const parsed = yield this.deserializeData(data, isEncoded);
          yield Promise.allSettled([
            this.setValue(`_uucfg-${this.id}`, yield this.serializeData(parsed)),
            this.setValue(`_uucfgver-${this.id}`, fmtVer),
            this.setValue(`_uucfgenc-${this.id}`, isEncoded),
            this.deleteValue(`_uucfg-${id}`),
            this.deleteValue(`_uucfgver-${id}`),
            this.deleteValue(`_uucfgenc-${id}`)
          ]);
        })));
      });
    }
    //#region serialization
    /** Serializes the data using the optional this.encodeData() and returns it as a string */
    serializeData(data, useEncoding = true) {
      return __async(this, null, function* () {
        const stringData = JSON.stringify(data);
        if (!this.encodingEnabled() || !useEncoding)
          return stringData;
        const encRes = this.encodeData(stringData);
        if (encRes instanceof Promise)
          return yield encRes;
        return encRes;
      });
    }
    /** Deserializes the data using the optional this.decodeData() and returns it as a JSON object */
    deserializeData(data, useEncoding = true) {
      return __async(this, null, function* () {
        let decRes = this.encodingEnabled() && useEncoding ? this.decodeData(data) : void 0;
        if (decRes instanceof Promise)
          decRes = yield decRes;
        return JSON.parse(decRes != null ? decRes : data);
      });
    }
    //#region misc
    /** Copies a JSON-compatible object and loses all its internal references in the process */
    deepCopy(obj) {
      return JSON.parse(JSON.stringify(obj));
    }
    //#region storage
    /** Gets a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
    getValue(name, defaultValue) {
      return __async(this, null, function* () {
        var _a, _b;
        switch (this.storageMethod) {
          case "localStorage":
            return (_a = localStorage.getItem(name)) != null ? _a : defaultValue;
          case "sessionStorage":
            return (_b = sessionStorage.getItem(name)) != null ? _b : defaultValue;
          default:
            return GM.getValue(name, defaultValue);
        }
      });
    }
    /**
     * Sets a value in persistent storage - can be overwritten in a subclass if you want to use something other than GM storage.  
     * The default storage engines will stringify all passed values like numbers or booleans, so be aware of that.
     */
    setValue(name, value) {
      return __async(this, null, function* () {
        switch (this.storageMethod) {
          case "localStorage":
            return localStorage.setItem(name, String(value));
          case "sessionStorage":
            return sessionStorage.setItem(name, String(value));
          default:
            return GM.setValue(name, String(value));
        }
      });
    }
    /** Deletes a value from persistent storage - can be overwritten in a subclass if you want to use something other than GM storage */
    deleteValue(name) {
      return __async(this, null, function* () {
        switch (this.storageMethod) {
          case "localStorage":
            return localStorage.removeItem(name);
          case "sessionStorage":
            return sessionStorage.removeItem(name);
          default:
            return GM.deleteValue(name);
        }
      });
    }
  };

  // lib/DataStoreSerializer.ts
  var DataStoreSerializer = class {
    constructor(stores, options = {}) {
      __publicField(this, "stores");
      __publicField(this, "options");
      if (!getUnsafeWindow().crypto || !getUnsafeWindow().crypto.subtle)
        throw new Error("DataStoreSerializer has to run in a secure context (HTTPS)!");
      this.stores = stores;
      this.options = __spreadValues({
        addChecksum: true,
        ensureIntegrity: true
      }, options);
    }
    /** Calculates the checksum of a string */
    calcChecksum(input) {
      return __async(this, null, function* () {
        return computeHash(input, "SHA-256");
      });
    }
    /** Serializes a DataStore instance */
    serializeStore(storeInst) {
      return __async(this, null, function* () {
        const data = storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
        const checksum = this.options.addChecksum ? yield this.calcChecksum(data) : void 0;
        return {
          id: storeInst.id,
          data,
          formatVersion: storeInst.formatVersion,
          encoded: storeInst.encodingEnabled(),
          checksum
        };
      });
    }
    /** Serializes the data stores into a string */
    serialize() {
      return __async(this, null, function* () {
        const serData = [];
        for (const store of this.stores)
          serData.push(yield this.serializeStore(store));
        return JSON.stringify(serData);
      });
    }
    /**
     * Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.  
     * Also triggers the migration process if the data format has changed.
     */
    deserialize(serializedData) {
      return __async(this, null, function* () {
        const deserStores = JSON.parse(serializedData);
        for (const storeData of deserStores) {
          const storeInst = this.stores.find((s) => s.id === storeData.id);
          if (!storeInst)
            throw new Error(`DataStore instance with ID "${storeData.id}" not found! Make sure to provide it in the DataStoreSerializer constructor.`);
          if (this.options.ensureIntegrity && typeof storeData.checksum === "string") {
            const checksum = yield this.calcChecksum(storeData.data);
            if (checksum !== storeData.checksum)
              throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!
Expected: ${storeData.checksum}
Has: ${checksum}`);
          }
          const decodedData = storeData.encoded && storeInst.encodingEnabled() ? yield storeInst.decodeData(storeData.data) : storeData.data;
          if (storeData.formatVersion && !isNaN(Number(storeData.formatVersion)) && Number(storeData.formatVersion) < storeInst.formatVersion)
            yield storeInst.runMigrations(JSON.parse(decodedData), Number(storeData.formatVersion), false);
          else
            yield storeInst.setData(JSON.parse(decodedData));
        }
      });
    }
    /**
     * Loads the persistent data of the DataStore instances into the in-memory cache.  
     * Also triggers the migration process if the data format has changed.
     * @returns Returns a PromiseSettledResult array with the results of each DataStore instance in the format `{ id: string, data: object }`
     */
    loadStoresData() {
      return __async(this, null, function* () {
        return Promise.allSettled(this.stores.map(
          (store) => __async(this, null, function* () {
            return {
              id: store.id,
              data: yield store.loadData()
            };
          })
        ));
      });
    }
    /** Resets the persistent data of the DataStore instances to their default values. */
    resetStoresData() {
      return __async(this, null, function* () {
        return Promise.allSettled(this.stores.map((store) => store.saveDefaultData()));
      });
    }
    /**
     * Deletes the persistent data of the DataStore instances.  
     * Leaves the in-memory data untouched.
     */
    deleteStoresData() {
      return __async(this, null, function* () {
        return Promise.allSettled(this.stores.map((store) => store.deleteData()));
      });
    }
  };

  // node_modules/nanoevents/index.js
  var createNanoEvents = () => ({
    emit(event, ...args) {
      for (let i = 0, callbacks = this.events[event] || [], length = callbacks.length; i < length; i++) {
        callbacks[i](...args);
      }
    },
    events: {},
    on(event, cb) {
      var _a;
      ((_a = this.events)[event] || (_a[event] = [])).push(cb);
      return () => {
        var _a2;
        this.events[event] = (_a2 = this.events[event]) == null ? void 0 : _a2.filter((i) => cb !== i);
      };
    }
  });

  // lib/NanoEmitter.ts
  var NanoEmitter = class {
    constructor(options = {}) {
      __publicField(this, "events", createNanoEvents());
      __publicField(this, "eventUnsubscribes", []);
      __publicField(this, "emitterOptions");
      this.emitterOptions = __spreadValues({
        publicEmit: false
      }, options);
    }
    /** Subscribes to an event - returns a function that unsubscribes the event listener */
    on(event, cb) {
      let unsub;
      const unsubProxy = () => {
        if (!unsub)
          return;
        unsub();
        this.eventUnsubscribes = this.eventUnsubscribes.filter((u) => u !== unsub);
      };
      unsub = this.events.on(event, cb);
      this.eventUnsubscribes.push(unsub);
      return unsubProxy;
    }
    /** Subscribes to an event and calls the callback or resolves the Promise only once */
    once(event, cb) {
      return new Promise((resolve) => {
        let unsub;
        const onceProxy = (...args) => {
          unsub();
          cb == null ? void 0 : cb(...args);
          resolve(args);
        };
        unsub = this.on(event, onceProxy);
      });
    }
    /** Emits an event on this instance - Needs `publicEmit` to be set to true in the constructor! */
    emit(event, ...args) {
      if (this.emitterOptions.publicEmit) {
        this.events.emit(event, ...args);
        return true;
      }
      return false;
    }
    /** Unsubscribes all event listeners */
    unsubscribeAll() {
      for (const unsub of this.eventUnsubscribes)
        unsub();
      this.eventUnsubscribes = [];
    }
  };

  // lib/Dialog.ts
  var defaultDialogCss = `.uu-no-select {
  user-select: none;
}

.uu-dialog-bg {
  --uu-dialog-bg: #333333;
  --uu-dialog-bg-highlight: #252525;
  --uu-scroll-indicator-bg: rgba(10, 10, 10, 0.7);
  --uu-dialog-separator-color: #797979;
  --uu-dialog-border-radius: 10px;
}

.uu-dialog-bg {
  display: block;
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
  z-index: 5;
  background-color: rgba(0, 0, 0, 0.6);
}

.uu-dialog {
  --uu-calc-dialog-height: calc(min(100vh - 40px, var(--uu-dialog-height-max)));
  position: absolute;
  display: flex;
  flex-direction: column;
  width: calc(min(100% - 60px, var(--uu-dialog-width-max)));
  border-radius: var(--uu-dialog-border-radius);
  height: auto;
  max-height: var(--uu-calc-dialog-height);
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 6;
  color: #fff;
  background-color: var(--uu-dialog-bg);
}

.uu-dialog.align-top {
  top: 0;
  transform: translate(-50%, 40px);
}

.uu-dialog.align-bottom {
  top: 100%;
  transform: translate(-50%, -100%);
}

.uu-dialog-body {
  font-size: 1.5rem;
  padding: 20px;
}

.uu-dialog-body.small {
  padding: 15px;
}

#uu-dialog-opts {
  display: flex;
  flex-direction: column;
  position: relative;
  padding: 30px 0px;
  overflow-y: auto;
}

.uu-dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 6px;
  padding: 15px 20px 15px 20px;
  background-color: var(--uu-dialog-bg);
  border: 2px solid var(--uu-dialog-separator-color);
  border-style: none none solid none !important;
  border-radius: var(--uu-dialog-border-radius) var(--uu-dialog-border-radius) 0px 0px;
}

.uu-dialog-header.small {
  padding: 10px 15px;
  border-style: none none solid none !important;
}

.uu-dialog-header-pad {
  content: " ";
  min-height: 32px;
}

.uu-dialog-header-pad.small {
  min-height: 24px;
}

.uu-dialog-titlecont {
  display: flex;
  align-items: center;
}

.uu-dialog-titlecont-no-title {
  display: flex;
  justify-content: flex-end;
  align-items: center;
}

.uu-dialog-title {
  position: relative;
  display: inline-block;
  font-size: 22px;
}

.uu-dialog-close {
  cursor: pointer;
}

.uu-dialog-header-img,
.uu-dialog-close
{
  width: 32px;
  height: 32px;
}

.uu-dialog-header-img.small,
.uu-dialog-close.small
{
  width: 24px;
  height: 24px;
}

.uu-dialog-footer {
  font-size: 17px;
  text-decoration: underline;
}

.uu-dialog-footer.hidden {
  display: none;
}

.uu-dialog-footer-cont {
  margin-top: 6px;
  padding: 15px 20px;
  background: var(--uu-dialog-bg);
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, var(--uu-dialog-bg) 30%, var(--uu-dialog-bg) 100%);
  border: 2px solid var(--uu-dialog-separator-color);
  border-style: solid none none none !important;
  border-radius: 0px 0px var(--uu-dialog-border-radius) var(--uu-dialog-border-radius);
}

.uu-dialog-footer-buttons-cont button:not(:last-of-type) {
  margin-right: 15px;
}`;
  exports.currentDialogId = null;
  var openDialogs = [];
  var defaultStrings = {
    closeDialogTooltip: "Click to close the dialog"
  };
  var Dialog = class _Dialog extends NanoEmitter {
    constructor(options) {
      super();
      /** Options passed to the dialog in the constructor */
      __publicField(this, "options");
      /** ID that gets added to child element IDs - has to be unique and conform to HTML ID naming rules! */
      __publicField(this, "id");
      /** Strings used in the dialog (used for translations) */
      __publicField(this, "strings");
      __publicField(this, "dialogOpen", false);
      __publicField(this, "dialogMounted", false);
      const _a = options, { strings } = _a, opts = __objRest(_a, ["strings"]);
      this.strings = __spreadValues(__spreadValues({}, defaultStrings), strings != null ? strings : {});
      this.options = __spreadValues({
        closeOnBgClick: true,
        closeOnEscPress: true,
        destroyOnClose: false,
        unmountOnClose: true,
        removeListenersOnDestroy: true,
        small: false,
        verticalAlign: "center"
      }, opts);
      this.id = opts.id;
    }
    //#region public
    /** Call after DOMContentLoaded to pre-render the dialog and invisibly mount it in the DOM */
    mount() {
      return __async(this, null, function* () {
        var _a;
        if (this.dialogMounted)
          return;
        this.dialogMounted = true;
        if (!document.querySelector("style.uu-dialog-css"))
          addGlobalStyle((_a = this.options.dialogCss) != null ? _a : defaultDialogCss).classList.add("uu-dialog-css");
        const bgElem = document.createElement("div");
        bgElem.id = `uu-${this.id}-dialog-bg`;
        bgElem.classList.add("uu-dialog-bg");
        if (this.options.closeOnBgClick)
          bgElem.ariaLabel = bgElem.title = this.getString("closeDialogTooltip");
        bgElem.style.setProperty("--uu-dialog-width-max", `${this.options.width}px`);
        bgElem.style.setProperty("--uu-dialog-height-max", `${this.options.height}px`);
        bgElem.style.visibility = "hidden";
        bgElem.style.display = "none";
        bgElem.inert = true;
        bgElem.appendChild(yield this.getDialogContent());
        document.body.appendChild(bgElem);
        this.attachListeners(bgElem);
        this.events.emit("render");
        return bgElem;
      });
    }
    /** Closes the dialog and clears all its contents (unmounts elements from the DOM) in preparation for a new rendering call */
    unmount() {
      var _a;
      this.close();
      this.dialogMounted = false;
      const clearSelectors = [
        `#uu-${this.id}-dialog-bg`,
        `#uu-style-dialog-${this.id}`
      ];
      for (const sel of clearSelectors)
        (_a = document.querySelector(sel)) == null ? void 0 : _a.remove();
      this.events.emit("clear");
    }
    /** Clears the DOM of the dialog and then renders it again */
    remount() {
      return __async(this, null, function* () {
        this.unmount();
        yield this.mount();
      });
    }
    /**
     * Opens the dialog - also mounts it if it hasn't been mounted yet  
     * Prevents default action and immediate propagation of the passed event
     */
    open(e) {
      return __async(this, null, function* () {
        var _a;
        e == null ? void 0 : e.preventDefault();
        e == null ? void 0 : e.stopImmediatePropagation();
        if (this.isOpen())
          return;
        this.dialogOpen = true;
        if (openDialogs.includes(this.id))
          throw new Error(`A dialog with the same ID of '${this.id}' already exists and is open!`);
        if (!this.isMounted())
          yield this.mount();
        const dialogBg = document.querySelector(`#uu-${this.id}-dialog-bg`);
        if (!dialogBg)
          return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
        dialogBg.style.visibility = "visible";
        dialogBg.style.display = "block";
        dialogBg.inert = false;
        exports.currentDialogId = this.id;
        openDialogs.unshift(this.id);
        for (const dialogId of openDialogs)
          if (dialogId !== this.id)
            (_a = document.querySelector(`#uu-${dialogId}-dialog-bg`)) == null ? void 0 : _a.setAttribute("inert", "true");
        document.body.classList.remove("uu-no-select");
        document.body.setAttribute("inert", "true");
        this.events.emit("open");
        return dialogBg;
      });
    }
    /** Closes the dialog - prevents default action and immediate propagation of the passed event */
    close(e) {
      var _a, _b;
      e == null ? void 0 : e.preventDefault();
      e == null ? void 0 : e.stopImmediatePropagation();
      if (!this.isOpen())
        return;
      this.dialogOpen = false;
      const dialogBg = document.querySelector(`#uu-${this.id}-dialog-bg`);
      if (!dialogBg)
        return console.warn(`Couldn't find background element for dialog with ID '${this.id}'`);
      dialogBg.style.visibility = "hidden";
      dialogBg.style.display = "none";
      dialogBg.inert = true;
      openDialogs.splice(openDialogs.indexOf(this.id), 1);
      exports.currentDialogId = (_a = openDialogs[0]) != null ? _a : null;
      if (exports.currentDialogId)
        (_b = document.querySelector(`#uu-${exports.currentDialogId}-dialog-bg`)) == null ? void 0 : _b.removeAttribute("inert");
      if (openDialogs.length === 0) {
        document.body.classList.add("uu-no-select");
        document.body.removeAttribute("inert");
      }
      this.events.emit("close");
      if (this.options.destroyOnClose)
        this.destroy();
      else if (this.options.unmountOnClose)
        this.unmount();
    }
    /** Returns true if the dialog is currently open */
    isOpen() {
      return this.dialogOpen;
    }
    /** Returns true if the dialog is currently mounted */
    isMounted() {
      return this.dialogMounted;
    }
    /** Clears the DOM of the dialog and removes all event listeners */
    destroy() {
      this.unmount();
      this.events.emit("destroy");
      this.options.removeListenersOnDestroy && this.unsubscribeAll();
    }
    //#region static
    /** Returns the ID of the top-most dialog (the dialog that has been opened last) */
    static getCurrentDialogId() {
      return exports.currentDialogId;
    }
    /** Returns the IDs of all currently open dialogs, top-most first */
    static getOpenDialogs() {
      return openDialogs;
    }
    //#region protected
    getString(key) {
      var _a;
      return (_a = this.strings[key]) != null ? _a : defaultStrings[key];
    }
    /** Called once to attach all generic event listeners */
    attachListeners(bgElem) {
      if (this.options.closeOnBgClick) {
        bgElem.addEventListener("click", (e) => {
          var _a;
          if (this.isOpen() && ((_a = e.target) == null ? void 0 : _a.id) === `uu-${this.id}-dialog-bg`)
            this.close(e);
        });
      }
      if (this.options.closeOnEscPress) {
        document.body.addEventListener("keydown", (e) => {
          if (e.key === "Escape" && this.isOpen() && _Dialog.getCurrentDialogId() === this.id)
            this.close(e);
        });
      }
    }
    //#region protected
    /**
     * Adds generic, accessible interaction listeners to the passed element.  
     * All listeners have the default behavior prevented and stop propagation (for keyboard events only as long as the captured key is valid).
     * @param listenerOptions Provide a {@linkcode listenerOptions} object to configure the listeners
     */
    onInteraction(elem, listener, listenerOptions) {
      const _a = listenerOptions != null ? listenerOptions : {}, { preventDefault = true, stopPropagation = true } = _a, listenerOpts = __objRest(_a, ["preventDefault", "stopPropagation"]);
      const interactionKeys = ["Enter", " ", "Space"];
      const proxListener = (e) => {
        if (e instanceof KeyboardEvent) {
          if (interactionKeys.includes(e.key)) {
            preventDefault && e.preventDefault();
            stopPropagation && e.stopPropagation();
          } else
            return;
        } else if (e instanceof MouseEvent) {
          preventDefault && e.preventDefault();
          stopPropagation && e.stopPropagation();
        }
        (listenerOpts == null ? void 0 : listenerOpts.once) && e.type === "keydown" && elem.removeEventListener("click", proxListener, listenerOpts);
        (listenerOpts == null ? void 0 : listenerOpts.once) && e.type === "click" && elem.removeEventListener("keydown", proxListener, listenerOpts);
        listener(e);
      };
      elem.addEventListener("click", proxListener, listenerOpts);
      elem.addEventListener("keydown", proxListener, listenerOpts);
    }
    /** Returns the dialog content element and all its children */
    getDialogContent() {
      return __async(this, null, function* () {
        var _a, _b, _c, _d;
        const header = (_b = (_a = this.options).renderHeader) == null ? void 0 : _b.call(_a);
        const footer = (_d = (_c = this.options).renderFooter) == null ? void 0 : _d.call(_c);
        const dialogWrapperEl = document.createElement("div");
        dialogWrapperEl.id = `uu-${this.id}-dialog`;
        dialogWrapperEl.classList.add("uu-dialog");
        dialogWrapperEl.ariaLabel = dialogWrapperEl.title = "";
        dialogWrapperEl.role = "dialog";
        dialogWrapperEl.setAttribute("aria-labelledby", `uu-${this.id}-dialog-title`);
        dialogWrapperEl.setAttribute("aria-describedby", `uu-${this.id}-dialog-body`);
        if (this.options.verticalAlign !== "center")
          dialogWrapperEl.classList.add(`align-${this.options.verticalAlign}`);
        const headerWrapperEl = document.createElement("div");
        headerWrapperEl.classList.add("uu-dialog-header");
        this.options.small && headerWrapperEl.classList.add("small");
        if (header) {
          const headerTitleWrapperEl = document.createElement("div");
          headerTitleWrapperEl.id = `uu-${this.id}-dialog-title`;
          headerTitleWrapperEl.classList.add("uu-dialog-title-wrapper");
          headerTitleWrapperEl.role = "heading";
          headerTitleWrapperEl.ariaLevel = "1";
          headerTitleWrapperEl.appendChild(header instanceof Promise ? yield header : header);
          headerWrapperEl.appendChild(headerTitleWrapperEl);
        } else {
          const padEl = document.createElement("div");
          padEl.classList.add("uu-dialog-header-pad", this.options.small ? "small" : "");
          headerWrapperEl.appendChild(padEl);
        }
        if (this.options.renderCloseBtn) {
          const closeBtnEl = yield this.options.renderCloseBtn();
          closeBtnEl.classList.add("uu-dialog-close");
          this.options.small && closeBtnEl.classList.add("small");
          closeBtnEl.tabIndex = 0;
          if (closeBtnEl.hasAttribute("alt"))
            closeBtnEl.setAttribute("alt", this.getString("closeDialogTooltip"));
          closeBtnEl.title = closeBtnEl.ariaLabel = this.getString("closeDialogTooltip");
          this.onInteraction(closeBtnEl, () => this.close());
          headerWrapperEl.appendChild(closeBtnEl);
        }
        dialogWrapperEl.appendChild(headerWrapperEl);
        const dialogBodyElem = document.createElement("div");
        dialogBodyElem.id = `uu-${this.id}-dialog-body`;
        dialogBodyElem.classList.add("uu-dialog-body");
        this.options.small && dialogBodyElem.classList.add("small");
        const body = this.options.renderBody();
        dialogBodyElem.appendChild(body instanceof Promise ? yield body : body);
        dialogWrapperEl.appendChild(dialogBodyElem);
        if (footer) {
          const footerWrapper = document.createElement("div");
          footerWrapper.classList.add("uu-dialog-footer-cont");
          dialogWrapperEl.appendChild(footerWrapper);
          footerWrapper.appendChild(footer instanceof Promise ? yield footer : footer);
        }
        return dialogWrapperEl;
      });
    }
  };

  // lib/misc.ts
  function autoPlural(word, num) {
    if (Array.isArray(num) || num instanceof NodeList)
      num = num.length;
    return `${word}${num === 1 ? "" : "s"}`;
  }
  function insertValues(input, ...values) {
    return input.replace(/%\d/gm, (match) => {
      var _a, _b;
      const argIndex = Number(match.substring(1)) - 1;
      return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
    });
  }
  function pauseFor(time) {
    return new Promise((res) => {
      setTimeout(() => res(), time);
    });
  }
  function debounce(func, timeout = 300, edge = "falling") {
    let id;
    return function(...args) {
      if (edge === "rising") {
        if (!id) {
          func.apply(this, args);
          id = setTimeout(() => id = void 0, timeout);
        }
      } else {
        clearTimeout(id);
        id = setTimeout(() => func.apply(this, args), timeout);
      }
    };
  }
  function fetchAdvanced(_0) {
    return __async(this, arguments, function* (input, options = {}) {
      var _a;
      const { timeout = 1e4 } = options;
      const { signal, abort } = new AbortController();
      (_a = options.signal) == null ? void 0 : _a.addEventListener("abort", abort);
      let signalOpts = {}, id = void 0;
      if (timeout >= 0) {
        id = setTimeout(() => abort(), timeout);
        signalOpts = { signal };
      }
      try {
        const res = yield fetch(input, __spreadValues(__spreadValues({}, options), signalOpts));
        id && clearTimeout(id);
        return res;
      } catch (err) {
        id && clearTimeout(id);
        throw err;
      }
    });
  }

  // lib/SelectorObserver.ts
  var domLoaded = false;
  document.addEventListener("DOMContentLoaded", () => domLoaded = true);
  var SelectorObserver = class {
    constructor(baseElement, options = {}) {
      __publicField(this, "enabled", false);
      __publicField(this, "baseElement");
      __publicField(this, "observer");
      __publicField(this, "observerOptions");
      __publicField(this, "customOptions");
      __publicField(this, "listenerMap");
      this.baseElement = baseElement;
      this.listenerMap = /* @__PURE__ */ new Map();
      const _a = options, {
        defaultDebounce,
        defaultDebounceEdge,
        disableOnNoListeners,
        enableOnAddListener
      } = _a, observerOptions = __objRest(_a, [
        "defaultDebounce",
        "defaultDebounceEdge",
        "disableOnNoListeners",
        "enableOnAddListener"
      ]);
      this.observerOptions = __spreadValues({
        childList: true,
        subtree: true
      }, observerOptions);
      this.customOptions = {
        defaultDebounce: defaultDebounce != null ? defaultDebounce : 0,
        defaultDebounceEdge: defaultDebounceEdge != null ? defaultDebounceEdge : "rising",
        disableOnNoListeners: disableOnNoListeners != null ? disableOnNoListeners : false,
        enableOnAddListener: enableOnAddListener != null ? enableOnAddListener : true
      };
      if (typeof this.customOptions.checkInterval !== "number") {
        this.observer = new MutationObserver(() => this.checkAllSelectors());
      } else {
        this.checkAllSelectors();
        setInterval(() => this.checkAllSelectors(), this.customOptions.checkInterval);
      }
    }
    /** Call to check all selectors in the {@linkcode listenerMap} using {@linkcode checkSelector()} */
    checkAllSelectors() {
      if (!this.enabled || !domLoaded)
        return;
      for (const [selector, listeners] of this.listenerMap.entries())
        this.checkSelector(selector, listeners);
    }
    /** Checks if the element(s) with the given {@linkcode selector} exist in the DOM and calls the respective {@linkcode listeners} accordingly */
    checkSelector(selector, listeners) {
      var _a;
      if (!this.enabled)
        return;
      const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
      if (!baseElement)
        return;
      const all = listeners.some((listener) => listener.all);
      const one = listeners.some((listener) => !listener.all);
      const allElements = all ? baseElement.querySelectorAll(selector) : null;
      const oneElement = one ? baseElement.querySelector(selector) : null;
      for (const options of listeners) {
        if (options.all) {
          if (allElements && allElements.length > 0) {
            options.listener(allElements);
            if (!options.continuous)
              this.removeListener(selector, options);
          }
        } else {
          if (oneElement) {
            options.listener(oneElement);
            if (!options.continuous)
              this.removeListener(selector, options);
          }
        }
        if (((_a = this.listenerMap.get(selector)) == null ? void 0 : _a.length) === 0)
          this.listenerMap.delete(selector);
        if (this.listenerMap.size === 0 && this.customOptions.disableOnNoListeners)
          this.disable();
      }
    }
    /**
     * Starts observing the children of the base element for changes to the given {@linkcode selector} according to the set {@linkcode options}
     * @param selector The selector to observe
     * @param options Options for the selector observation
     * @param options.listener Gets called whenever the selector was found in the DOM
     * @param [options.all] Whether to use `querySelectorAll()` instead - default is false
     * @param [options.continuous] Whether to call the listener continuously instead of just once - default is false
     * @param [options.debounce] Whether to debounce the listener to reduce calls to `querySelector` or `querySelectorAll` - set undefined or <=0 to disable (default)
     * @returns Returns a function that can be called to remove this listener more easily
     */
    addListener(selector, options) {
      options = __spreadValues({
        all: false,
        continuous: false,
        debounce: 0
      }, options);
      if (options.debounce && options.debounce > 0 || this.customOptions.defaultDebounce && this.customOptions.defaultDebounce > 0) {
        options.listener = debounce(
          options.listener,
          options.debounce || this.customOptions.defaultDebounce,
          options.debounceEdge || this.customOptions.defaultDebounceEdge
        );
      }
      if (this.listenerMap.has(selector))
        this.listenerMap.get(selector).push(options);
      else
        this.listenerMap.set(selector, [options]);
      if (this.enabled === false && this.customOptions.enableOnAddListener)
        this.enable();
      this.checkSelector(selector, [options]);
      return () => this.removeListener(selector, options);
    }
    /** Disables the observation of the child elements */
    disable() {
      var _a;
      if (!this.enabled)
        return;
      this.enabled = false;
      (_a = this.observer) == null ? void 0 : _a.disconnect();
    }
    /**
     * Enables or reenables the observation of the child elements.
     * @param immediatelyCheckSelectors Whether to immediately check if all previously registered selectors exist (default is true)
     * @returns Returns true when the observation was enabled, false otherwise (e.g. when the base element wasn't found)
     */
    enable(immediatelyCheckSelectors = true) {
      var _a;
      const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
      if (this.enabled || !baseElement)
        return false;
      this.enabled = true;
      (_a = this.observer) == null ? void 0 : _a.observe(baseElement, this.observerOptions);
      if (immediatelyCheckSelectors)
        this.checkAllSelectors();
      return true;
    }
    /** Returns whether the observation of the child elements is currently enabled */
    isEnabled() {
      return this.enabled;
    }
    /** Removes all listeners that have been registered with {@linkcode addListener()} */
    clearListeners() {
      this.listenerMap.clear();
    }
    /**
     * Removes all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()}
     * @returns Returns true when all listeners for the associated selector were found and removed, false otherwise
     */
    removeAllListeners(selector) {
      return this.listenerMap.delete(selector);
    }
    /**
     * Removes a single listener for the given {@linkcode selector} and {@linkcode options} that has been registered with {@linkcode addListener()}
     * @returns Returns true when the listener was found and removed, false otherwise
     */
    removeListener(selector, options) {
      const listeners = this.listenerMap.get(selector);
      if (!listeners)
        return false;
      const index = listeners.indexOf(options);
      if (index > -1) {
        listeners.splice(index, 1);
        return true;
      }
      return false;
    }
    /** Returns all listeners that have been registered with {@linkcode addListener()} */
    getAllListeners() {
      return this.listenerMap;
    }
    /** Returns all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()} */
    getListeners(selector) {
      return this.listenerMap.get(selector);
    }
  };

  // lib/translation.ts
  var trans = {};
  var curLang;
  var trLang = (language, key, ...args) => {
    var _a;
    if (!language)
      return key;
    const trText = (_a = trans[language]) == null ? void 0 : _a[key];
    if (!trText)
      return key;
    if (args.length > 0 && trText.match(/%\d/)) {
      return insertValues(trText, ...args);
    }
    return trText;
  };
  function tr(key, ...args) {
    return trLang(curLang, key, ...args);
  }
  tr.forLang = trLang;
  tr.addLanguage = (language, translations) => {
    trans[language] = translations;
  };
  tr.setLanguage = (language) => {
    curLang = language;
  };
  tr.getLanguage = () => {
    return curLang;
  };
  tr.getTranslations = (language) => {
    return trans[language != null ? language : curLang];
  };

  exports.DataStore = DataStore;
  exports.DataStoreSerializer = DataStoreSerializer;
  exports.Dialog = Dialog;
  exports.NanoEmitter = NanoEmitter;
  exports.SelectorObserver = SelectorObserver;
  exports.addGlobalStyle = addGlobalStyle;
  exports.addParent = addParent;
  exports.autoPlural = autoPlural;
  exports.clamp = clamp;
  exports.compress = compress;
  exports.computeHash = computeHash;
  exports.darkenColor = darkenColor;
  exports.debounce = debounce;
  exports.decompress = decompress;
  exports.defaultDialogCss = defaultDialogCss;
  exports.defaultStrings = defaultStrings;
  exports.fetchAdvanced = fetchAdvanced;
  exports.getSiblingsFrame = getSiblingsFrame;
  exports.getUnsafeWindow = getUnsafeWindow;
  exports.hexToRgb = hexToRgb;
  exports.insertValues = insertValues;
  exports.interceptEvent = interceptEvent;
  exports.interceptWindowEvent = interceptWindowEvent;
  exports.isScrollable = isScrollable;
  exports.lightenColor = lightenColor;
  exports.mapRange = mapRange;
  exports.observeElementProp = observeElementProp;
  exports.openDialogs = openDialogs;
  exports.openInNewTab = openInNewTab;
  exports.pauseFor = pauseFor;
  exports.preloadImages = preloadImages;
  exports.randRange = randRange;
  exports.randomId = randomId;
  exports.randomItem = randomItem;
  exports.randomItemIndex = randomItemIndex;
  exports.randomizeArray = randomizeArray;
  exports.rgbToHex = rgbToHex;
  exports.setInnerHtmlUnsafe = setInnerHtmlUnsafe;
  exports.takeRandomItem = takeRandomItem;
  exports.tr = tr;

  return exports;

})({});