Roler's Bookmarklets

Various simple bookmarklets

// ==UserScript==
// @name         Roler's Bookmarklets
// @namespace    https://github.com/rRoler/bookmarklets
// @version      1.2.7
// @description  Various simple bookmarklets
// @author       Roler
// @match        http*://mangadex.org/*
// @match        http*://www.amazon.co.jp/*
// @match        http*://www.amazon.com/*
// @match        http*://bookwalker.jp/*
// @match        http*://r18.bookwalker.jp/*
// @match        http*://global.bookwalker.jp/*
// @match        http*://viewer-trial.bookwalker.jp/*
// @match        http*://booklive.jp/*
// @supportURL   https://github.com/rRoler/bookmarklets/issues
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @connect      wsrv.nl
// @connect      c.roler.dev
// @connect      www.amazon.co.jp
// @connect      www.amazon.com
// @connect      res.booklive.jp
// @connect      kitsu.app
// @connect      c.bookwalker.jp
// @connect      *
// @run-at       document-end
// ==/UserScript==

(() => {
/*!
 * Licensed under MIT: https://github.com/rRoler/bookmarklets/raw/main/LICENSE
 * Third party licenses: https://github.com/rRoler/bookmarklets/raw/main/dist/userscript.dependencies.txt
 */

// User agents
const DESKTOP_USER_AGENT =
	'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0';

// URLs
const ROLER_CDN_URL =
	/* @__PURE__ */
	new URL('https://c.roler.dev');

const WSRV_URL =
	/* @__PURE__ */
	new URL('https://wsrv.nl');

// MangaDex URLs
const MANGADEX_URL =
	/* @__PURE__ */
	new URL('https://mangadex.org');

const MANGADEX_CANARY_URL =
	/* @__PURE__ */
	new URL('https://canary.mangadex.dev');

const MANGADEX_SANDBOX_URL =
	/* @__PURE__ */
	new URL('https://sandbox.mangadex.dev');

const MANGADEX_API_URL =
	/* @__PURE__ */
	new URL('https://api.mangadex.org');

const MANGADEX_DEV_API_URL =
	/* @__PURE__ */
	new URL('https://api.mangadex.dev');

const MANGADEX_AUTH_URL =
	/* @__PURE__ */
	new URL('https://auth.mangadex.org');

const MANGADEX_DEV_AUTH_URL =
	/* @__PURE__ */
	new URL('https://auth.mangadex.dev');

const AMAZON_JAPAN_URL =
	/* @__PURE__ */
	new URL('https://www.amazon.co.jp');

const BOOKWALKER_URL =
	/* @__PURE__ */
	new URL('https://bookwalker.jp');

const BOOKWALKER_GLOBAL_URL =
	/* @__PURE__ */
	new URL('https://global.bookwalker.jp');

const BOOKWALKER_VIEWER_TRIAL_URL =
	/* @__PURE__ */
	new URL('https://viewer-trial.bookwalker.jp');

const BOOKWALKER_R18_URL =
	/* @__PURE__ */
	new URL('https://r18.bookwalker.jp');

const BOOKLIVE_URL =
	/* @__PURE__ */
	new URL('https://booklive.jp');

const BOOKLIVE_CDN_URL =
	/* @__PURE__ */
	new URL('https://res.booklive.jp');

const EBOOKJAPAN_URL =
	/* @__PURE__ */
	new URL('https://ebookjapan.yahoo.co.jp');

const CDJAPAN_URL =
	/* @__PURE__ */
	new URL('https://www.cdjapan.co.jp');

// Tracking site URLs
const ANILIST_URL =
	/* @__PURE__ */
	new URL('https://anilist.co');

const ANIME_PLANET_URL =
	/* @__PURE__ */
	new URL('https://www.anime-planet.com');

const KITSU_URL =
	/* @__PURE__ */
	new URL('https://kitsu.app');

const MANGAUPDATES_URL =
	/* @__PURE__ */
	new URL('https://www.mangaupdates.com');

const MYANIMELIST_URL =
	/* @__PURE__ */
	new URL('https://myanimelist.net');

const NOVELUPDATES_URL =
	/* @__PURE__ */
	new URL('https://www.novelupdates.com');

const wsrvUrl = WSRV_URL.origin;
function getWsrvUrl(options) {
  const cdnURL = new URL(options.cdnUrl || wsrvUrl);
  cdnURL.searchParams.set('url', options.url);
  if (options.defaultUrl) cdnURL.searchParams.set('default', options.defaultUrl);
  if (options.output) cdnURL.searchParams.set('output', options.output);
  if (options.quality && ['jpeg', 'tiff', 'webp'].includes(options.output || 'jpeg')) {
    if (options.quality < 1) options.quality = 1;
    if (options.quality > 100) options.quality = 100;
    cdnURL.searchParams.set('q', options.quality.toString());
  }
  if (options.width) cdnURL.searchParams.set('width', options.width.toString());
  if (options.height) cdnURL.searchParams.set('height', options.height.toString());
  if (options.cx) cdnURL.searchParams.set('cx', options.cx.toString());
  if (options.cy) cdnURL.searchParams.set('cy', options.cy.toString());
  if (options.cw) cdnURL.searchParams.set('cw', options.cw.toString());
  if (options.ch) cdnURL.searchParams.set('ch', options.ch.toString());
  return cdnURL;
}
async function getWsrvResource(options) {
  const errorPrefix = 'WSRV Error: ';
  if (!options.url.startsWith('http')) throw new Error(errorPrefix + 'Invalid URL');
  const url = getWsrvUrl(options);
  const response = await fetch(url.toString());
  if (!response.ok) throw new Error(errorPrefix + response.statusText);
  return await (options.output === 'json' ? response.json() : response.blob());
}
function getWsrvImage(options) {
  return getWsrvResource(options);
}
function getWsrvData(options) {
  if (!options.output) options.output = 'json';
  return getWsrvResource(options);
}

var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};

function getDefaultExportFromCjs (x) {
	return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}

var FileSaver_min$1 = {exports: {}};

var FileSaver_min = FileSaver_min$1.exports;

var hasRequiredFileSaver_min;

function requireFileSaver_min () {
	if (hasRequiredFileSaver_min) return FileSaver_min$1.exports;
	hasRequiredFileSaver_min = 1;
	(function (module, exports) {
		(function(a,b){b();})(FileSaver_min,function(){function b(a,b){return "undefined"==typeof b?b={autoBom:false}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c);},d.onerror=function(){console.error("could not download file");},d.send();}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,false);try{b.send();}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"));}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",true,true,window,0,0,0,80,20,false,false,false,false,0,null),a.dispatchEvent(b);}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof commonjsGlobal&&commonjsGlobal.global===commonjsGlobal?commonjsGlobal:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href);},4E4),setTimeout(function(){e(j);},0));}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else {var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i);});}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null;},k.readAsDataURL(b);}else {var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m);},4E4);}});f.saveAs=g.saveAs=g,(module.exports=g);});

		
	} (FileSaver_min$1));
	return FileSaver_min$1.exports;
}

var FileSaver_minExports = /*@__PURE__*/ requireFileSaver_min();
var fileSaver = /*@__PURE__*/getDefaultExportFromCjs(FileSaver_minExports);

// DEFLATE is a complex format; to read this code, you should probably check the RFC first:
// https://tools.ietf.org/html/rfc1951
// You may also wish to take a look at the guide I made about this program:
// https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
// Some of the following code is similar to that of UZIP.js:
// https://github.com/photopea/UZIP.js
// However, the vast majority of the codebase has diverged from UZIP.js to increase performance and reduce bundle size.
// Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint
// is better for memory in most engines (I *think*).

// aliases for shorter compressed code (most minifers don't do this)
var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
// fixed length extra bits
var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
// fixed distance extra bits
var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
// get base, reverse index map from extra bits
var freb = function (eb, start) {
    var b = new u16(31);
    for (var i = 0; i < 31; ++i) {
        b[i] = start += 1 << eb[i - 1];
    }
    // numbers here are at max 18 bits
    var r = new i32(b[30]);
    for (var i = 1; i < 30; ++i) {
        for (var j = b[i]; j < b[i + 1]; ++j) {
            r[j] = ((j - b[i]) << 5) | i;
        }
    }
    return { b: b, r: r };
};
var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
// we can ignore the fact that the other numbers are wrong; they never happen anyway
fl[28] = 258, revfl[258] = 28;
freb(fdeb, 0);
// map of value to reverse (assuming 16 bits)
var rev = new u16(32768);
for (var i = 0; i < 32768; ++i) {
    // reverse table algorithm from SO
    var x = ((i & 0xAAAA) >> 1) | ((i & 0x5555) << 1);
    x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2);
    x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4);
    rev[i] = (((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8)) >> 1;
}
// fixed length tree
var flt = new u8(288);
for (var i = 0; i < 144; ++i)
    flt[i] = 8;
for (var i = 144; i < 256; ++i)
    flt[i] = 9;
for (var i = 256; i < 280; ++i)
    flt[i] = 7;
for (var i = 280; i < 288; ++i)
    flt[i] = 8;
// fixed distance tree
var fdt = new u8(32);
for (var i = 0; i < 32; ++i)
    fdt[i] = 5;
// typed array slice - allows garbage collector to free original reference,
// while being more compatible than .slice
var slc = function (v, s, e) {
    if (e == null || e > v.length)
        e = v.length;
    // can't use .constructor in case user-supplied
    return new u8(v.subarray(s, e));
};
// error codes
var ec = [
    'unexpected EOF',
    'invalid block type',
    'invalid length/literal',
    'invalid distance',
    'stream finished',
    'no stream handler',
    ,
    'no callback',
    'invalid UTF-8 data',
    'extra field too long',
    'date not in range 1980-2099',
    'filename too long',
    'stream finishing',
    'invalid zip data'
    // determined by unknown compression method
];
var err = function (ind, msg, nt) {
    var e = new Error(msg || ec[ind]);
    e.code = ind;
    if (Error.captureStackTrace)
        Error.captureStackTrace(e, err);
    if (!nt)
        throw e;
    return e;
};
// empty
var et = /*#__PURE__*/ new u8(0);
// CRC32 table
var crct = /*#__PURE__*/ (function () {
    var t = new Int32Array(256);
    for (var i = 0; i < 256; ++i) {
        var c = i, k = 9;
        while (--k)
            c = ((c & 1) && -306674912) ^ (c >>> 1);
        t[i] = c;
    }
    return t;
})();
// CRC32
var crc = function () {
    var c = -1;
    return {
        p: function (d) {
            // closures have awful performance
            var cr = c;
            for (var i = 0; i < d.length; ++i)
                cr = crct[(cr & 255) ^ d[i]] ^ (cr >>> 8);
            c = cr;
        },
        d: function () { return ~c; }
    };
};
// Walmart object spread
var mrg = function (a, b) {
    var o = {};
    for (var k in a)
        o[k] = a[k];
    for (var k in b)
        o[k] = b[k];
    return o;
};
// write bytes
var wbytes = function (d, b, v) {
    for (; v; ++b)
        d[b] = v, v >>>= 8;
};
// text encoder
var te = typeof TextEncoder != 'undefined' && /*#__PURE__*/ new TextEncoder();
// text decoder
var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder();
try {
    td.decode(et, { stream: true });
}
catch (e) { }
/**
 * Converts a string into a Uint8Array for use with compression/decompression methods
 * @param str The string to encode
 * @param latin1 Whether or not to interpret the data as Latin-1. This should
 *               not need to be true unless decoding a binary string.
 * @returns The string encoded in UTF-8/Latin-1 binary
 */
function strToU8(str, latin1) {
    var i; 
    if (te)
        return te.encode(str);
    var l = str.length;
    var ar = new u8(str.length + (str.length >> 1));
    var ai = 0;
    var w = function (v) { ar[ai++] = v; };
    for (var i = 0; i < l; ++i) {
        if (ai + 5 > ar.length) {
            var n = new u8(ai + 8 + ((l - i) << 1));
            n.set(ar);
            ar = n;
        }
        var c = str.charCodeAt(i);
        if (c < 128 || latin1)
            w(c);
        else if (c < 2048)
            w(192 | (c >> 6)), w(128 | (c & 63));
        else if (c > 55295 && c < 57344)
            c = 65536 + (c & 1023 << 10) | (str.charCodeAt(++i) & 1023),
                w(240 | (c >> 18)), w(128 | ((c >> 12) & 63)), w(128 | ((c >> 6) & 63)), w(128 | (c & 63));
        else
            w(224 | (c >> 12)), w(128 | ((c >> 6) & 63)), w(128 | (c & 63));
    }
    return slc(ar, 0, ai);
}
// extra field length
var exfl = function (ex) {
    var le = 0;
    if (ex) {
        for (var k in ex) {
            var l = ex[k].length;
            if (l > 65535)
                err(9);
            le += l + 4;
        }
    }
    return le;
};
// write zip header
var wzh = function (d, b, f, fn, u, c, ce, co) {
    var fl = fn.length, ex = f.extra, col = co && co.length;
    var exl = exfl(ex);
    wbytes(d, b, ce != null ? 0x2014B50 : 0x4034B50), b += 4;
    if (ce != null)
        d[b++] = 20, d[b++] = f.os;
    d[b] = 20, b += 2; // spec compliance? what's that?
    d[b++] = (f.flag << 1) | (c < 0 && 8), d[b++] = u && 8;
    d[b++] = f.compression & 255, d[b++] = f.compression >> 8;
    var dt = new Date(f.mtime == null ? Date.now() : f.mtime), y = dt.getFullYear() - 1980;
    if (y < 0 || y > 119)
        err(10);
    wbytes(d, b, (y << 25) | ((dt.getMonth() + 1) << 21) | (dt.getDate() << 16) | (dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() >> 1)), b += 4;
    if (c != -1) {
        wbytes(d, b, f.crc);
        wbytes(d, b + 4, c < 0 ? -c - 2 : c);
        wbytes(d, b + 8, f.size);
    }
    wbytes(d, b + 12, fl);
    wbytes(d, b + 14, exl), b += 16;
    if (ce != null) {
        wbytes(d, b, col);
        wbytes(d, b + 6, f.attrs);
        wbytes(d, b + 10, ce), b += 14;
    }
    d.set(fn, b);
    b += fl;
    if (exl) {
        for (var k in ex) {
            var exf = ex[k], l = exf.length;
            wbytes(d, b, +k);
            wbytes(d, b + 2, l);
            d.set(exf, b + 4), b += 4 + l;
        }
    }
    if (col)
        d.set(co, b), b += col;
    return b;
};
// write zip footer (end of central directory)
var wzf = function (o, b, c, d, e) {
    wbytes(o, b, 0x6054B50); // skip disk
    wbytes(o, b + 8, c);
    wbytes(o, b + 10, c);
    wbytes(o, b + 12, d);
    wbytes(o, b + 16, e);
};
/**
 * A pass-through stream to keep data uncompressed in a ZIP archive.
 */
var ZipPassThrough = /*#__PURE__*/ (function () {
    /**
     * Creates a pass-through stream that can be added to ZIP archives
     * @param filename The filename to associate with this data stream
     */
    function ZipPassThrough(filename) {
        this.filename = filename;
        this.c = crc();
        this.size = 0;
        this.compression = 0;
    }
    /**
     * Processes a chunk and pushes to the output stream. You can override this
     * method in a subclass for custom behavior, but by default this passes
     * the data through. You must call this.ondata(err, chunk, final) at some
     * point in this method.
     * @param chunk The chunk to process
     * @param final Whether this is the last chunk
     */
    ZipPassThrough.prototype.process = function (chunk, final) {
        this.ondata(null, chunk, final);
    };
    /**
     * Pushes a chunk to be added. If you are subclassing this with a custom
     * compression algorithm, note that you must push data from the source
     * file only, pre-compression.
     * @param chunk The chunk to push
     * @param final Whether this is the last chunk
     */
    ZipPassThrough.prototype.push = function (chunk, final) {
        if (!this.ondata)
            err(5);
        this.c.p(chunk);
        this.size += chunk.length;
        if (final)
            this.crc = this.c.d();
        this.process(chunk, final || false);
    };
    return ZipPassThrough;
}());
// TODO: Better tree shaking
/**
 * A zippable archive to which files can incrementally be added
 */
var Zip = /*#__PURE__*/ (function () {
    /**
     * Creates an empty ZIP archive to which files can be added
     * @param cb The callback to call whenever data for the generated ZIP archive
     *           is available
     */
    function Zip(cb) {
        this.ondata = cb;
        this.u = [];
        this.d = 1;
    }
    /**
     * Adds a file to the ZIP archive
     * @param file The file stream to add
     */
    Zip.prototype.add = function (file) {
        var _this = this;
        if (!this.ondata)
            err(5);
        // finishing or finished
        if (this.d & 2)
            this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, false);
        else {
            var f = strToU8(file.filename), fl_1 = f.length;
            var com = file.comment, o = com && strToU8(com);
            var u = fl_1 != file.filename.length || (o && (com.length != o.length));
            var hl_1 = fl_1 + exfl(file.extra) + 30;
            if (fl_1 > 65535)
                this.ondata(err(11, 0, 1), null, false);
            var header = new u8(hl_1);
            wzh(header, 0, file, f, u, -1);
            var chks_1 = [header];
            var pAll_1 = function () {
                for (var _i = 0, chks_2 = chks_1; _i < chks_2.length; _i++) {
                    var chk = chks_2[_i];
                    _this.ondata(null, chk, false);
                }
                chks_1 = [];
            };
            var tr_1 = this.d;
            this.d = 0;
            var ind_1 = this.u.length;
            var uf_1 = mrg(file, {
                f: f,
                u: u,
                o: o,
                t: function () {
                    if (file.terminate)
                        file.terminate();
                },
                r: function () {
                    pAll_1();
                    if (tr_1) {
                        var nxt = _this.u[ind_1 + 1];
                        if (nxt)
                            nxt.r();
                        else
                            _this.d = 1;
                    }
                    tr_1 = 1;
                }
            });
            var cl_1 = 0;
            file.ondata = function (err, dat, final) {
                if (err) {
                    _this.ondata(err, dat, final);
                    _this.terminate();
                }
                else {
                    cl_1 += dat.length;
                    chks_1.push(dat);
                    if (final) {
                        var dd = new u8(16);
                        wbytes(dd, 0, 0x8074B50);
                        wbytes(dd, 4, file.crc);
                        wbytes(dd, 8, cl_1);
                        wbytes(dd, 12, file.size);
                        chks_1.push(dd);
                        uf_1.c = cl_1, uf_1.b = hl_1 + cl_1 + 16, uf_1.crc = file.crc, uf_1.size = file.size;
                        if (tr_1)
                            uf_1.r();
                        tr_1 = 1;
                    }
                    else if (tr_1)
                        pAll_1();
                }
            };
            this.u.push(uf_1);
        }
    };
    /**
     * Ends the process of adding files and prepares to emit the final chunks.
     * This *must* be called after adding all desired files for the resulting
     * ZIP file to work properly.
     */
    Zip.prototype.end = function () {
        var _this = this;
        if (this.d & 2) {
            this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, true);
            return;
        }
        if (this.d)
            this.e();
        else
            this.u.push({
                r: function () {
                    if (!(_this.d & 1))
                        return;
                    _this.u.splice(-1, 1);
                    _this.e();
                },
                t: function () { }
            });
        this.d = 3;
    };
    Zip.prototype.e = function () {
        var bt = 0, l = 0, tl = 0;
        for (var _i = 0, _a = this.u; _i < _a.length; _i++) {
            var f = _a[_i];
            tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0);
        }
        var out = new u8(tl + 22);
        for (var _b = 0, _c = this.u; _b < _c.length; _b++) {
            var f = _c[_b];
            wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o);
            bt += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b;
        }
        wzf(out, bt, this.u.length, tl, l);
        this.ondata(null, out, true);
        this.d = 2;
    };
    /**
     * A method to terminate any internal workers used by the stream. Subsequent
     * calls to add() will fail.
     */
    Zip.prototype.terminate = function () {
        for (var _i = 0, _a = this.u; _i < _a.length; _i++) {
            var f = _a[_i];
            f.t();
        }
        this.d = 2;
    };
    return Zip;
}());

let isUserScript = false;
const userAgentDesktop = DESKTOP_USER_AGENT;
function enableUserScriptFeatures() {
  isUserScript = true;
}
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
function isUrlLocallyStored(url) {
  return url.startsWith('blob:') || url.startsWith('data:');
}
function openNewTab(url) {
  window.open(url, '_blank', 'noopener,noreferrer');
}
function formatRegexText(text) {
  const toReplace = [['uuid', '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'], ['numid', '[0-9]+']];
  let newText = text;
  toReplace.forEach(strings => newText = newText.replaceAll(`:${strings[0]}`, strings[1]));
  return newText;
}
function getMatch(string, regex, index = 0) {
  const regexMatches = string.match(regex);
  if (regexMatches && regexMatches[index]) return regexMatches[index];
}
function splitArray(array, chunkSize = 100) {
  const arrayCopy = [...array];
  const resArray = [];
  while (arrayCopy.length) resArray.push(arrayCopy.splice(0, chunkSize));
  return resArray;
}
function waitForElement(reference, noElement = false) {
  const getElement = () => typeof reference === 'string' ? document.body.querySelector(reference) : document.body.contains(reference) ? reference : null;
  let element = getElement();
  return new Promise(resolve => {
    if (noElement ? !element : element) return resolve(element);
    const observer = new MutationObserver(() => {
      element = getElement();
      if (noElement ? !element : element) {
        resolve(element);
        observer.disconnect();
      }
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  });
}
function parseStorage(key) {
  const value = localStorage.getItem(key);
  if (value) return JSON.parse(value);
}
function saveStorage(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}
function createSVG(options) {
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  if (options.svg.attributes) setAttributes(svg, options.svg.attributes);
  if (options.svg.styles) setStyles(svg, options.svg.styles);
  for (const pathOptions of options.paths) {
    const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    if (pathOptions.attributes) setAttributes(path, pathOptions.attributes);
    if (pathOptions.styles) setStyles(path, pathOptions.styles);
    svg.append(path);
  }
  return svg;
}
function setStyles(element, styles) {
  for (const style in styles) {
    if (styles[style].endsWith('!important')) element.style.setProperty(style, styles[style].slice(0, -10), 'important');else element.style.setProperty(style, styles[style]);
  }
}
function getStyles(element, styles) {
  const resStyles = {};
  for (const style of styles || element.style) {
    const value = element.style.getPropertyValue(style);
    const priority = element.style.getPropertyPriority(style);
    resStyles[style] = priority ? `${value} !${priority}` : value;
  }
  return resStyles;
}
function removeStyles(element, styles) {
  for (const style of styles || element.style) element.style.removeProperty(style);
}
function setAttributes(element, attributes) {
  for (const attribute in attributes) element.setAttribute(attribute, attributes[attribute]);
}
function createUrl(base, path = '/', query = {}) {
  const url = new URL(base);
  url.pathname = path;
  for (const key in query) {
    const value = query[key];
    if (Array.isArray(value)) {
      for (const item of value) url.searchParams.append(key, item);
    } else url.searchParams.set(key, value.toString());
  }
  return url;
}
async function copyText(text) {
  let copied = false;
  await navigator.clipboard.writeText(text).then(() => {
    console.debug('Copied to clipboard:\n' + text);
    copied = true;
  }, () => {
    console.error('Failed to copy to clipboard:\n' + text);
    copied = false;
  });
  return copied;
}
function filterFilename(name, options) {
  const replaceString = options?.replaceString || '_';
  const isPath = !!options?.isPath;
  const extensionRegex = /\.[a-z0-9]+$/i;
  const extension = getMatch(name.trim(), extensionRegex) || '';
  const filter = (str, removeExtension = true) => str.trim().replace(removeExtension ? extensionRegex : '', '').normalize('NFKC').replace(/[\\/:"*?<>|]/g, replaceString).trim().slice(0, 255 - extension.length).trim();
  const pathParts = name.split(/[\\/]/g);
  const filename = isPath ? pathParts.map((p, i) => filter(p, i === pathParts.length - 1)).join('/') : filter(name);
  return filename + extension;
}
async function saveFile(data, filename, options) {
  const name = filterFilename(filename || 'rbm-file');
  const path = filterFilename(options?.path || '', {
    isPath: true
  });
  const isString = typeof data === 'string';
  const url = isString ? data : URL.createObjectURL(data);
  const save = () => new Promise((resolve, reject) => {
    const useFileSaver = () => {
      try {
        fileSaver.saveAs(url, name);
        resolve();
      } catch (error) {
        reject(error);
      }
    };
    if (!isUserScript || !options?.path) return useFileSaver();
    const useFallback = error => {
      console.warn(error);
      useFileSaver();
    };
    try {
      GM_download({
        url,
        name: filterFilename(`${path ? path + '/' : ''}${name}`, {
          isPath: true
        }),
        // @ts-ignore
        saveAs: !!options?.saveAs,
        headers: {
          Origin: window.location.origin,
          Referer: window.location.href
        },
        onload: () => resolve(),
        onerror: useFallback
      });
    } catch (error) {
      useFallback(error);
    }
  });
  try {
    await save();
  } finally {
    if (!isString) URL.revokeObjectURL(url);
  }
  await sleep(100);
}
async function zipFiles(files, options = {}) {
  const {
    filename = 'archive.zip',
    onProgress,
    signal
  } = options;
  return new Promise((resolve, reject) => {
    const chunks = [];
    const zip = new Zip((error, chunk, final) => {
      if (error) return reject(error);
      chunks.push(chunk);
      if (final) {
        const blob = new Blob(chunks, {
          type: 'application/zip'
        });
        resolve(new File([blob], filename, {
          type: blob.type
        }));
      }
    });
    onProgress?.(0, files.length);
    const processFiles = async () => {
      for (const [index, file] of files.entries()) {
        const fileIndex = index + 1;
        if (signal?.aborted) {
          zip.end();
          reject(new DOMException('Zipping aborted', 'AbortError'));
          return;
        }
        try {
          const zipData = new Uint8Array(await file.arrayBuffer());
          const zipFile = new ZipPassThrough(file.name);
          zip.add(zipFile);
          zipFile.push(zipData, true);
          onProgress?.(fileIndex, files.length);
        } catch (error) {
          zip.end();
          reject(error);
          return;
        }
        if (fileIndex === files.length) zip.end();
      }
    };
    processFiles();
  });
}
function formatStringVariable(name) {
  return `%${name}%`;
}
function replaceStringVariable(string, variables) {
  let newString = string;
  variables.forEach(variable => {
    newString = newString.replaceAll(variable[0], variable[1]);
  });
  return newString;
}
function addKeyShortcutListener(keys, callback, parent = document.body) {
  let pressedKeys = [];
  parent.addEventListener('keydown', e => {
    const pressedKey = e.code || e.key.toUpperCase();
    if (!pressedKeys.includes(pressedKey)) pressedKeys.push(pressedKey);
  });
  parent.addEventListener('keyup', () => {
    if (keys.length === pressedKeys.length && keys.every(key => pressedKeys.includes(key))) {
      callback();
    }
    pressedKeys = [];
  });
}
function hideImageElement(element) {
  setStyles(element, {
    width: 'fit-content',
    height: 'fit-content',
    opacity: '0',
    position: 'absolute',
    top: '-10000px',
    'z-index': '-10000',
    'pointer-events': 'none'
  });
  return element;
}
async function getImageDimensions(url, options) {
  if (!isUrlLocallyStored(url)) {
    const cdnData = await getWsrvData({
      url
    }).catch(console.warn);
    if (cdnData && cdnData.width > 0 && cdnData.height > 0) return {
      width: cdnData.width,
      height: cdnData.height
    };
  }
  const imageUrl = options?.localUrl || url;
  const replacementUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQIW2NgAAIAAAUAAR4f7BQAAAAASUVORK5CYII=';
  const imageElement = new Image();
  hideImageElement(imageElement);
  document.body.append(imageElement);
  try {
    return await new Promise((resolve, reject) => {
      function fallbackMethod() {
        imageElement.onerror = e => {
          reject(e);
        };
        imageElement.onload = () => {
          resolve({
            width: imageElement.naturalWidth,
            height: imageElement.naturalHeight
          });
        };
        imageElement.src = imageUrl;
      }
      try {
        const observer = new ResizeObserver(_entries => {
          const imageWidth = imageElement.naturalWidth;
          const imageHeight = imageElement.naturalHeight;
          if (imageWidth > 0 && imageHeight > 0) {
            observer.disconnect();
            imageElement.src = replacementUrl;
            resolve({
              width: imageWidth,
              height: imageHeight
            });
          }
        });
        imageElement.onerror = e => {
          console.warn(e);
          observer.disconnect();
          fallbackMethod();
        };
        observer.observe(imageElement);
      } catch (error) {
        fallbackMethod();
      }
      imageElement.src = imageUrl;
    });
  } finally {
    imageElement.remove();
  }
}
async function cropImage(options) {
  options.output = options.output || 'png';
  options.quality = options.quality || 98;
  if (options.quality < 1) options.quality = 1;
  if (options.quality > 100) options.quality = 100;
  const isLocal = isUrlLocallyStored(options.url);
  let croppedImage = !isLocal ? await getWsrvImage(options).catch(console.warn) : null;
  if (!croppedImage && isLocal || !croppedImage && options.localUrl) {
    const imageElement = new Image();
    hideImageElement(imageElement);
    document.body.append(imageElement);
    try {
      await new Promise((resolve, reject) => {
        imageElement.onerror = e => reject(e);
        imageElement.onload = () => resolve();
        imageElement.src = options.localUrl || options.url;
      });
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      const croppedWidth = options.cw || (options.width || imageElement.naturalWidth) - (options.cx || 0);
      const croppedHeight = options.ch || (options.height || imageElement.naturalHeight) - (options.cy || 0);
      canvas.width = croppedWidth;
      canvas.height = croppedHeight;
      ctx?.drawImage(imageElement, options.cx || 0, options.cy || 0, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight);
      croppedImage = await new Promise(resolve => canvas?.toBlob(blob => resolve(blob), `image/${options.output}`, options.quality / 100));
    } finally {
      imageElement.remove();
    }
  }
  if (!croppedImage) throw new Error('Failed to crop image.');
  return croppedImage;
}
function formatCSV(data) {
  return data.map(row => row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',')).join('\n');
}
function capitalizeFirstLetter(string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}
function formatXMLTag(name, value, indent = 0) {
  return `${'\t'.repeat(indent)}<${name}>${value}</${name}>\n`;
}
async function gmFetch(input, init) {
  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
  const {
    method = 'GET',
    headers = {},
    body,
    signal,
    anonymous,
    ...otherOptions
  } = init || {};
  const headersObj = {};
  if (!anonymous) {
    headersObj['Origin'] = window.location.origin;
    headersObj['Referer'] = window.location.href;
  }
  if (headers) {
    if (headers instanceof Headers) {
      headers.forEach((value, key) => {
        headersObj[key] = value;
      });
    } else if (Array.isArray(headers)) {
      headers.forEach(([key, value]) => {
        headersObj[key] = value;
      });
    } else {
      Object.assign(headersObj, headers);
    }
  }
  const abortError = new DOMException('The operation was aborted', 'AbortError');
  if (signal?.aborted) throw abortError;
  let response;
  try {
    let gmRequest;
    response = await new Promise((resolve, reject) => {
      const abortHandler = () => {
        if (gmRequest) gmRequest.abort();
        reject(abortError);
      };
      const addAbortHandler = () => {
        if (signal) signal.addEventListener('abort', abortHandler);
      };
      const removeAbortHandler = () => {
        if (signal) signal.removeEventListener('abort', abortHandler);
      };
      addAbortHandler();
      gmRequest = GM_xmlhttpRequest({
        method: method.toUpperCase(),
        url: url,
        headers: headersObj,
        anonymous,
        data: body,
        responseType: 'blob',
        onload: response => {
          try {
            removeAbortHandler();
            const responseHeaders = new Headers();
            if (response.responseHeaders) {
              const headerLines = response.responseHeaders.trim().split('\n');
              headerLines.forEach(line => {
                const colonIndex = line.indexOf(':');
                if (colonIndex > 0) {
                  const key = line.substring(0, colonIndex).trim();
                  const value = line.substring(colonIndex + 1).trim();
                  responseHeaders.set(key, value);
                }
              });
            }
            const fetchResponse = new Response(response.response, {
              status: response.status,
              statusText: response.statusText,
              headers: responseHeaders
            });
            resolve(fetchResponse);
          } catch (error) {
            reject(error);
          }
        },
        onerror: error => {
          removeAbortHandler();
          reject(error);
        },
        ontimeout: () => {
          removeAbortHandler();
          reject(new Error('GM_xmlhttpRequest timed out'));
        },
        onabort: () => {
          removeAbortHandler();
          reject(new Error('GM_xmlhttpRequest was aborted'));
        }
      });
    });
  } catch (gmError) {
    console.warn('GM_xmlhttpRequest failed, falling back to native fetch:', gmError);
    response = await fetch(input, {
      method,
      headers,
      body,
      signal,
      ...otherOptions
    });
  }
  return response;
}
async function detectImageType(image) {
  const buffer = await image.arrayBuffer();
  if (!buffer || buffer.byteLength < 12) return null;
  const bytes = new Uint8Array(buffer);
  const header = Array.from(bytes.slice(0, 12)).map(b => b.toString(16).padStart(2, '0')).join('');
  if (header.startsWith('89504e47')) return 'png';
  if (header.startsWith('ffd8ff')) return 'jpeg';
  if (header.startsWith('47494638')) return 'gif';
  if (header.startsWith('52494646') && header.substring(16, 24) === '57454250') {
    return 'webp';
  }
  return null;
}
async function urlToFile(url, options) {
  const res = await gmFetch(url, options?.fetchOptions);
  const blob = await res.blob();
  const imageType = await detectImageType(blob);
  const name = url.split('/').pop();
  const extension = imageType || blob.type.split('/')[1];
  return new File([blob], filterFilename(options?.fileName || `${name}.${extension}`), {
    type: imageType ? `image/${imageType}` : blob.type
  });
}

class Component {
  constructor(componentElement = document.createElement('div'), {
    defaultStyles = true,
    defaultEvents = true
  } = {}) {
    this.componentElement = componentElement;
    if (defaultStyles) this.setDefaultStyles();
    if (defaultEvents) this.addDefaultEvents();
  }
  setDefaultStyles(element = this.componentElement) {
    setStyles(element, {
      color: componentColors.text,
      'font-family': 'Poppins,Verdana,sans-serif !important',
      'font-size': '16px',
      'font-weight': 'normal',
      'line-height': '20px'
    });
  }
  addDefaultEvents(element = this.componentElement) {
    waitForElement(element).then(() => {
      element.dispatchEvent(new CustomEvent('componentadded'));
      waitForElement(element, true).then(() => {
        element.dispatchEvent(new CustomEvent('componentremoved'));
        this.addDefaultEvents();
      });
    });
  }
  add(parent = document.body) {
    return parent.appendChild(this.componentElement);
  }
  remove() {
    this.componentElement.remove();
  }
  replace(withElement) {
    this.componentElement.replaceWith(withElement);
  }
  hidden = false;
  displayStyles = {};
  hide() {
    if (this.hidden) return;
    this.hidden = true;
    this.displayStyles = getStyles(this.componentElement, ['display']);
    setStyles(this.componentElement, {
      display: 'none !important'
    });
  }
  show() {
    if (!this.hidden) return;
    this.hidden = false;
    setStyles(this.componentElement, this.displayStyles);
  }
  disabled = false;
  opacityStyles = {};
  disable() {
    if (this.disabled) return;
    this.disabled = true;
    this.opacityStyles = getStyles(this.componentElement, ['opacity', 'pointer-events']);
    setStyles(this.componentElement, {
      opacity: '0.5 !important',
      'pointer-events': 'none !important'
    });
  }
  enable() {
    if (!this.disabled) return;
    this.disabled = false;
    setStyles(this.componentElement, this.opacityStyles);
  }
  generateId() {
    return `bm-component-${Math.random().toString(36).substring(2, 15)}`;
  }
}
let componentColors = {
  text: '#000',
  primary: '#b5e853',
  secondary: '#cccccc',
  background: '#fff',
  accent: '#3c3c3c',
  warning: '#ffcf0e',
  error: '#FF4040'
};
function setComponentColors(colors) {
  componentColors = {
    ...componentColors,
    ...colors
  };
}

class Button extends Component {
  constructor(text, callback) {
    super(document.createElement('button'));
    setStyles(this.componentElement, {
      'font-size': '20px',
      'font-weight': 'bold',
      'line-height': '24px',
      border: 'none',
      'border-radius': '8px',
      cursor: 'pointer',
      padding: '4px 8px'
    });
    this.componentElement.innerText = text;
    this.componentElement.addEventListener('click', callback);
  }
}
class PrimaryButton extends Button {
  constructor(text, callback) {
    super(text, callback);
    setStyles(this.componentElement, {
      'background-color': componentColors.primary
    });
  }
}
class SecondaryButton extends Button {
  constructor(text, callback) {
    super(text, callback);
    setStyles(this.componentElement, {
      'background-color': componentColors.secondary
    });
  }
}

class TextInput extends Component {
  constructor(defaultValue = '', {
    labelText
  } = {}) {
    super(document.createElement('span'), {
      defaultStyles: false
    });
    const inputId = this.generateId();
    const listId = this.generateId();
    if (labelText) {
      const label = document.createElement('label');
      label.innerText = labelText;
      this.setDefaultStyles(label);
      label.setAttribute('for', inputId);
      this.componentElement.append(label);
    }
    const input = document.createElement('input');
    if (typeof defaultValue === 'string') input.value = defaultValue;else input.value = defaultValue[0];
    this.setDefaultStyles(input);
    setStyles(input, {
      'font-size': '18px',
      border: `1px solid ${componentColors.secondary}`,
      'border-radius': '4px',
      'background-color': componentColors.background,
      padding: '2px 8px'
    });
    input.setAttribute('id', inputId);
    this.componentElement.append(input);
    this.inputElement = input;
    if (Array.isArray(defaultValue)) {
      input.setAttribute('list', listId);
      const dataList = document.createElement('datalist');
      dataList.setAttribute('id', listId);
      defaultValue.forEach(value => {
        const option = document.createElement('option');
        option.value = value;
        option.innerText = value;
        dataList.append(option);
      });
      this.componentElement.append(dataList);
    }
  }
}
class TextArea extends Component {
  constructor(defaultValue = '', {
    rows,
    cols,
    labelText
  } = {}) {
    super(document.createElement('span'), {
      defaultStyles: false
    });
    const textareaId = this.generateId();
    if (labelText) {
      const label = document.createElement('label');
      label.innerText = labelText;
      this.setDefaultStyles(label);
      label.setAttribute('for', textareaId);
      this.componentElement.append(label);
    }
    const textarea = document.createElement('textarea');
    textarea.value = defaultValue;
    this.setDefaultStyles(textarea);
    setStyles(textarea, {
      'font-size': '18px',
      border: `1px solid ${componentColors.secondary}`,
      'border-radius': '4px',
      'background-color': componentColors.background,
      padding: '2px 8px'
    });
    textarea.setAttribute('id', textareaId);
    if (rows) textarea.setAttribute('rows', rows.toString());
    if (cols) textarea.setAttribute('cols', cols.toString());
    this.componentElement.append(textarea);
    this.textareaElement = textarea;
  }
}

class Select extends Component {
  constructor(values, {
    labelText
  } = {}) {
    super(document.createElement('span'), {
      defaultStyles: false
    });
    const selectId = this.generateId();
    if (labelText) {
      const label = document.createElement('label');
      label.innerText = labelText;
      this.setDefaultStyles(label);
      label.setAttribute('for', selectId);
      this.componentElement.append(label);
    }
    const select = document.createElement('select');
    this.setDefaultStyles(select);
    setStyles(select, {
      'font-size': '18px',
      border: `1px solid ${componentColors.secondary}`,
      'border-radius': '4px',
      'background-color': componentColors.background,
      padding: '2px 8px'
    });
    select.setAttribute('id', selectId);
    this.componentElement.append(select);
    this.selectElement = select;
    values.forEach(value => {
      const option = document.createElement('option');
      option.value = value;
      option.innerText = value;
      select.append(option);
    });
  }
}

class Checkbox extends Component {
  constructor(callback = () => {}, {
    labelText
  } = {}) {
    super(document.createElement('span'), {
      defaultStyles: false
    });
    setStyles(this.componentElement, {
      gap: '8px',
      display: 'flex',
      'align-items': 'center',
      'justify-content': 'center'
    });
    const inputId = this.generateId();
    const input = document.createElement('input');
    this.setDefaultStyles(input);
    setStyles(input, {
      appearance: 'checkbox',
      width: '18px',
      height: '18px',
      margin: '0',
      'accent-color': componentColors.primary,
      border: `1px solid ${componentColors.secondary}`,
      'border-radius': '2px',
      cursor: 'pointer'
    });
    input.setAttribute('id', inputId);
    input.setAttribute('type', 'checkbox');
    input.addEventListener('change', () => callback(input.checked));
    this.componentElement.append(input);
    this.checkboxElement = input;
    if (labelText) {
      const label = document.createElement('label');
      label.innerText = labelText;
      this.setDefaultStyles(label);
      setStyles(label, {
        cursor: 'pointer'
      });
      label.setAttribute('for', inputId);
      this.componentElement.append(label);
    }
  }
}

var name = "heroicons";

console.debug(name, 'included');
const outlineIconOptions = {
  svg: {
    attributes: {
      fill: 'none',
      viewBox: '0 0 24 24',
      'stroke-width': '1.5',
      stroke: 'currentColor'
    },
    styles: {
      width: '24px',
      height: '24px'
    }
  },
  paths: [{
    attributes: {
      'stroke-linecap': 'round',
      'stroke-linejoin': 'round'
    }
  }]
};
const solidIconOptions = {
  svg: {
    attributes: {
      fill: 'currentColor',
      viewBox: '0 0 24 24'
    },
    styles: {
      width: '24px',
      height: '24px'
    }
  },
  paths: [{
    attributes: {
      'fill-rule': 'evenodd',
      'clip-rule': 'evenodd'
    }
  }]
};
const miniIconOptions = {
  svg: {
    attributes: {
      fill: 'currentColor',
      viewBox: '0 0 20 20'
    },
    styles: {
      width: '20px',
      height: '20px'
    }
  },
  paths: [{
    attributes: {
      'fill-rule': 'evenodd',
      'clip-rule': 'evenodd'
    }
  }]
};

/**
 * <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
 *   <path fill-rule="evenodd" d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
 * </svg>
 * **/
const xMarkSolid = () => {
  const options = solidIconOptions;
  options.paths[0].attributes.d = 'M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z';
  return createSVG(options);
};

/**
 * <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
 *   <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
 * </svg>
 **/
const informationCircleOutline = () => {
  const options = outlineIconOptions;
  options.paths[0].attributes.d = 'M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z';
  return createSVG(options);
};

/**
 * <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
 *   <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
 * </svg>
 **/
const informationCircleMini = () => {
  const options = miniIconOptions;
  options.paths[0].attributes.d = 'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z';
  return createSVG(options);
};

/**
 * <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
 *   <path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
 * </svg>
 * **/
const ellipsisHorizontalCircleOutline = () => {
  const options = outlineIconOptions;
  options.paths[0].attributes.d = 'M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z';
  return createSVG(options);
};

/**
 * <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
 *   <path fill-rule="evenodd" d="M2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-3-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
 * </svg>
 * **/
const ellipsisHorizontalCircleMini = () => {
  const options = miniIconOptions;
  options.paths[0].attributes.d = 'M2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm8 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-3-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm7 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z';
  return createSVG(options);
};

class Modal extends Component {
  constructor({
    title,
    content,
    buttons
  }) {
    super();
    setStyles(this.componentElement, {
      'z-index': '1000000',
      position: 'fixed',
      top: '0',
      left: '0',
      width: '100%',
      height: '100%',
      display: 'flex',
      'align-items': 'center',
      'justify-content': 'center'
    });
    const background = document.createElement('div');
    setStyles(background, {
      position: 'fixed',
      top: '0',
      left: '0',
      height: '100%',
      width: '100%',
      'background-color': 'rgba(0, 0, 0, 0.4)',
      'backdrop-filter': 'blur(4px)'
    });
    background.addEventListener('click', () => this.remove());
    this.componentElement.append(background);
    const box = document.createElement('div');
    setStyles(box, {
      'z-index': '1',
      'min-width': '300px',
      'max-width': '80vw',
      'max-height': '100vh',
      'background-color': componentColors.background,
      'box-shadow': '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.4)',
      'border-radius': '8px',
      margin: '8px',
      padding: '8px'
    });
    this.componentElement.append(box);
    const headerContainer = document.createElement('div');
    setStyles(headerContainer, {
      'max-height': '32px',
      display: 'flex',
      'justify-content': 'space-between',
      'align-items': 'center',
      gap: '8px',
      'padding-bottom': '8px'
    });
    box.append(headerContainer);
    const titleContainer = document.createElement('span');
    if (title) titleContainer.innerText = title;
    this.setDefaultStyles(titleContainer);
    setStyles(titleContainer, {
      'font-size': '24px',
      'line-height': '32px',
      'font-weight': 'bold',
      overflow: 'hidden',
      'text-overflow': 'ellipsis',
      'white-space': 'nowrap'
    });
    headerContainer.append(titleContainer);
    const close = document.createElement('button');
    const closeIcon = xMarkSolid();
    setStyles(close, {
      width: '32px',
      height: '32px',
      'flex-shrink': '0',
      cursor: 'pointer',
      border: 'none',
      background: 'none',
      padding: '0'
    });
    setStyles(closeIcon, {
      width: '100%',
      height: '100%',
      cursor: 'pointer'
    });
    close.addEventListener('click', () => this.remove());
    close.append(closeIcon);
    headerContainer.append(close);
    const contentContainer = document.createElement('div');
    if (typeof content === 'string') contentContainer.innerText = content;else contentContainer.append(content);
    this.setDefaultStyles(contentContainer);
    setStyles(contentContainer, {
      'text-align': 'center',
      'max-height': '75vh',
      'overflow-y': 'auto',
      padding: '4px'
    });
    box.append(contentContainer);
    if (buttons) {
      const footerContainer = document.createElement('div');
      this.setDefaultStyles(footerContainer);
      setStyles(footerContainer, {
        'max-height': '50px',
        display: 'flex',
        'align-items': 'center',
        gap: '8px',
        'padding-top': '8px',
        'overflow-x': 'auto'
      });
      const footerMargin = document.createElement('div');
      setStyles(footerMargin, {
        'margin-left': 'auto'
      });
      footerContainer.append(footerMargin);
      buttons.forEach(button => {
        setStyles(button.componentElement, {
          'flex-shrink': '0'
        });
        button.add(footerContainer);
      });
      box.append(footerContainer);
    }
    let isAdded = false;
    let bodyOverflows = {};
    this.componentElement.addEventListener('componentadded', () => {
      if (isAdded) return;
      isAdded = true;
      bodyOverflows = getStyles(document.body, ['overflow', 'overflow-y', 'overflow-x']);
      setStyles(document.body, {
        overflow: 'hidden !important'
      });
    });
    this.componentElement.addEventListener('componentremoved', () => {
      if (!isAdded) return;
      isAdded = false;
      setStyles(document.body, bodyOverflows);
    });
  }
}
async function alertModal(text, level) {
  switch (level) {
    case 'warning':
      console.warn(text);
      break;
    case 'error':
      console.error(text);
      break;
    default:
      console.log(text);
      break;
  }
  try {
    const okButton = new PrimaryButton('OK', () => {
      modal.remove();
    });
    const modal = new Modal({
      title: level?.toUpperCase().concat('!'),
      content: text.toString(),
      buttons: [okButton]
    });
    modal.add();
    okButton.componentElement.focus();
    return await new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve()));
  } catch (error) {
    console.error(error);
    return alert(text);
  }
}
async function promptModal(text, defaultValue = '') {
  try {
    const input = new TextInput(defaultValue, {
      labelText: text
    });
    setStyles(input.inputElement, {
      width: '90%'
    });
    let value;
    const okButton = new PrimaryButton('OK', () => {
      value = input.inputElement.value;
      modal.remove();
    });
    const cancelButton = new SecondaryButton('Cancel', () => {
      value = null;
      modal.remove();
    });
    const modal = new Modal({
      content: input.componentElement,
      buttons: [okButton, cancelButton]
    });
    input.inputElement.addEventListener('keydown', event => {
      if (event.key === 'Enter') okButton.componentElement.click();
    });
    modal.add();
    input.inputElement.focus();
    return await new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(value)));
  } catch (error) {
    console.error(error);
    return prompt(text, Array.isArray(defaultValue) ? defaultValue[0] : defaultValue);
  }
}
function promptAreaModal(text, defaultValue = '') {
  const contentContainer = document.createElement('div');
  setStyles(contentContainer, {
    width: '100vw',
    'max-width': '100%'
  });
  const textarea = new TextArea(defaultValue, {
    rows: 20,
    labelText: text
  });
  setStyles(textarea.textareaElement, {
    width: '100%'
  });
  contentContainer.append(textarea.componentElement);
  let value;
  const okButton = new PrimaryButton('OK', () => {
    value = textarea.textareaElement.value;
    modal.remove();
  });
  const cancelButton = new SecondaryButton('Cancel', () => {
    value = null;
    modal.remove();
  });
  const modal = new Modal({
    content: contentContainer,
    buttons: [okButton, cancelButton]
  });
  modal.add();
  textarea.textareaElement.focus();
  return new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(value)));
}
function selectModal(text, options) {
  const select = new Select(options, {
    labelText: text
  });
  setStyles(select.selectElement, {
    width: '90%'
  });
  let value;
  const okButton = new PrimaryButton('OK', () => {
    value = select.selectElement.value;
    modal.remove();
  });
  const cancelButton = new SecondaryButton('Cancel', () => {
    value = null;
    modal.remove();
  });
  const modal = new Modal({
    content: select.componentElement,
    buttons: [okButton, cancelButton]
  });
  select.selectElement.addEventListener('keydown', event => {
    if (event.key === 'Enter') okButton.componentElement.click();
  });
  modal.add();
  select.selectElement.focus();
  return new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(value)));
}
function checkboxModal(text, options, defaultValues = []) {
  const selectedOptions = [];
  const checkboxes = options.map(option => {
    const checkbox = new Checkbox(checked => {
      if (checked) {
        selectedOptions.push(option);
      } else {
        selectedOptions.splice(selectedOptions.indexOf(option), 1);
      }
      updateSelectAllCheckbox();
    }, {
      labelText: option
    });
    return {
      value: option,
      element: checkbox.componentElement,
      checkboxElement: checkbox.checkboxElement
    };
  });
  const selectAllCheckbox = new Checkbox(checked => {
    if (checked) {
      checkboxes.forEach(checkbox => {
        if (!checkbox.checkboxElement.checked) checkbox.checkboxElement.click();
      });
    } else {
      checkboxes.forEach(checkbox => {
        if (checkbox.checkboxElement.checked) checkbox.checkboxElement.click();
      });
    }
  });
  const updateSelectAllCheckbox = () => {
    const allChecked = checkboxes.every(checkbox => checkbox.checkboxElement.checked);
    const allUnchecked = checkboxes.every(checkbox => !checkbox.checkboxElement.checked);
    if (allChecked) {
      selectAllCheckbox.checkboxElement.checked = true;
      selectAllCheckbox.checkboxElement.indeterminate = false;
    } else if (allUnchecked) {
      selectAllCheckbox.checkboxElement.checked = false;
      selectAllCheckbox.checkboxElement.indeterminate = false;
    } else {
      selectAllCheckbox.checkboxElement.checked = false;
      selectAllCheckbox.checkboxElement.indeterminate = true;
    }
  };
  let values;
  const okButton = new PrimaryButton('OK', () => {
    values = selectedOptions;
    modal.remove();
  });
  const cancelButton = new SecondaryButton('Cancel', () => {
    values = null;
    modal.remove();
  });
  const contentContainer = document.createElement('div');
  setStyles(contentContainer, {
    display: 'flex',
    'flex-direction': 'column',
    'align-items': 'start',
    gap: '8px'
  });
  contentContainer.append(selectAllCheckbox.componentElement, ...checkboxes.map(checkbox => checkbox.element));
  const modal = new Modal({
    title: text,
    content: contentContainer,
    buttons: [okButton, cancelButton]
  });
  modal.add();
  checkboxes.forEach(checkbox => {
    if (defaultValues.includes(checkbox.value)) checkbox.checkboxElement.click();
  });
  okButton.componentElement.focus();
  return new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(values)));
}
function multiOptionModal(options, title) {
  const inputs = {};
  const elements = [];
  Object.entries(options).forEach(([key, config]) => {
    let getValue;
    let optionElement;
    const inputStyle = {
      width: '90%'
    };
    switch (config.type) {
      case 'text':
      case 'textarea':
        {
          const defaultVal = typeof config.defaultValue === 'string' ? config.defaultValue : '';
          const defaultExpandedVal = typeof config.defaultValue === 'string' ? [defaultVal, ...(config.options || [])] : config.options || defaultVal;
          const textComponent = config.type === 'textarea' ? new TextArea(defaultVal, {
            rows: 5,
            labelText: config.label
          }) : new TextInput(Array.isArray(defaultExpandedVal) && defaultExpandedVal.length > 0 ? defaultExpandedVal : defaultVal, {
            labelText: config.label
          });
          const textInputElement = textComponent.inputElement || textComponent.textareaElement;
          setStyles(textInputElement, inputStyle);
          getValue = () => textInputElement.value;
          optionElement = textComponent.componentElement;
          break;
        }
      case 'select':
        {
          const selectOptions = config.options || ['Yes', 'No'];
          const defaultVal = typeof config.defaultValue === 'string' ? config.defaultValue : selectOptions[0];
          const selectComponent = new Select(selectOptions, {
            labelText: config.label
          });
          setStyles(selectComponent.selectElement, inputStyle);
          selectComponent.selectElement.value = defaultVal;
          getValue = () => selectComponent.selectElement.value;
          optionElement = selectComponent.componentElement;
          break;
        }
      case 'checkbox':
        {
          const defaultChecked = typeof config.defaultValue === 'boolean' ? config.defaultValue : false;
          const checkboxComponent = new Checkbox(undefined, {
            labelText: config.label
          });
          setStyles(checkboxComponent.componentElement, {
            ...inputStyle,
            display: 'flex',
            'align-items': 'center',
            'justify-content': 'flex-start'
          });
          checkboxComponent.checkboxElement.checked = defaultChecked;
          getValue = () => checkboxComponent.checkboxElement.checked;
          optionElement = checkboxComponent.componentElement;
          break;
        }
      default:
        return;
    }
    inputs[key] = getValue;
    elements.push(optionElement);
  });
  let values;
  const okButton = new PrimaryButton('OK', () => {
    const result = {};
    Object.keys(inputs).forEach(key => {
      result[key] = inputs[key]();
    });
    values = result;
    modal.remove();
  });
  const cancelButton = new SecondaryButton('Cancel', () => {
    values = null;
    modal.remove();
  });
  const contentContainer = document.createElement('div');
  setStyles(contentContainer, {
    width: '100%',
    display: 'flex',
    'flex-direction': 'column',
    'align-items': 'center',
    'justify-content': 'center',
    gap: '8px'
  });
  contentContainer.append(...elements);
  const modal = new Modal({
    title,
    content: contentContainer,
    buttons: [okButton, cancelButton]
  });
  modal.add();
  okButton.componentElement.focus();
  return new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(values)));
}

class KeyRecorder extends Component {
  keys = [];
  isRecording = false;
  recordButton = new PrimaryButton('+', () => this.isRecording ? this.stop() : this.record());
  keyButtonsElement = document.createElement('span');
  keyListeners = [];
  constructor({
    keys,
    onChange
  }) {
    super();
    const flexStyles = {
      display: 'flex',
      'flex-direction': 'row',
      'flex-wrap': 'wrap',
      gap: '4px'
    };
    setStyles(this.componentElement, {
      ...flexStyles,
      border: `1px solid ${componentColors.accent}`,
      'border-radius': '4px',
      padding: '4px'
    });
    setStyles(this.keyButtonsElement, flexStyles);
    this.componentElement.append(this.keyButtonsElement, this.recordButton.componentElement);
    if (keys) this.addKeys(keys);
    this.onChange = onChange;
    this.componentElement.addEventListener('componentremoved', () => this.stop());
  }
  addKey(key) {
    if (this.keys.includes(key)) return;
    this.keys.push(key);
    const button = new SecondaryButton(key, () => this.removeKey(key));
    button.componentElement.setAttribute('data-key', key);
    this.keyButtonsElement.append(button.componentElement);
    if (this.onChange) this.onChange(this.keys);
  }
  removeKey(key) {
    const keyIndex = this.keys.indexOf(key);
    if (keyIndex <= -1) return;
    this.keys.splice(keyIndex, 1);
    const buttonElement = this.keyButtonsElement.querySelector(`[data-key="${key}"]`);
    if (buttonElement) buttonElement.remove();
    if (this.onChange) this.onChange(this.keys);
  }
  addKeyListeners() {
    this.keyListeners.forEach(listener => document.body.addEventListener(listener.type, listener.callback));
  }
  clearKeyListeners() {
    this.keyListeners.forEach(listener => document.body.removeEventListener(listener.type, listener.callback));
    this.keyListeners = [];
  }
  record() {
    if (this.isRecording) return;
    this.isRecording = true;
    this.recordButton.componentElement.innerText = '+ Hold keys...';
    this.keyListeners.push({
      type: 'keydown',
      callback: e => {
        if (this.isRecording) {
          e.preventDefault();
          e.stopPropagation();
          this.addKey(e.code || e.key.toUpperCase());
        }
      }
    }, {
      type: 'keyup',
      callback: e => {
        if (this.isRecording) {
          e.preventDefault();
          e.stopPropagation();
          this.stop();
        }
      }
    });
    this.addKeyListeners();
  }
  stop = () => {
    this.isRecording = false;
    this.recordButton.componentElement.innerText = '+';
    this.clearKeyListeners();
  };
  clear() {
    [...this.keys].forEach(key => this.removeKey(key));
  }
  addKeys(keys) {
    keys.forEach(key => this.addKey(key));
  }
}

const storageKey = 'rbm-settings-4abbd04d-2504-4a5a-8cf2-c96bc68bbdea';
function getSavedField(fieldId) {
  const savedFields = parseStorage(storageKey) || [];
  return savedFields.find(f => f.id === fieldId);
}
function setSavedField(field) {
  const savedFields = parseStorage(storageKey) || [];
  const fieldIndex = savedFields.findIndex(f => f.id === field.id);
  if (fieldIndex === -1) {
    savedFields.push(field);
  } else {
    savedFields[fieldIndex] = field;
  }
  saveStorage(storageKey, savedFields);
}
class SettingsField {
  constructor(props) {
    this.id = props.id;
    this.name = props.name;
    this.description = props.description;
    this.userScriptOnly = props.userScriptOnly;
    this.settings = props.settings;
    this.savedSettings = this.settings.map(setting => ({
      ...setting
    }));
    this.newSettings = this.settings.map(setting => ({
      ...setting
    }));
    this.load();
  }
  getValue(id) {
    const setting = this.savedSettings.find(s => s.id === id);
    if (!setting || setting.userScriptOnly && !isUserScript) return;
    switch (setting.type) {
      case 'text':
      case 'textarea':
        {
          if (setting.value || setting.value?.trim() === '') return setting.value;
          return setting.defaultValue;
        }
      case 'checkbox':
      case 'keys':
        {
          if (setting.value === undefined) return setting.defaultValue;
          return setting.value;
        }
      default:
        {
          return setting.value || setting.defaultValue;
        }
    }
  }
  setValue(id, value) {
    const setting = this.newSettings.find(s => s.id === id);
    if (setting) setting.value = value;
  }
  load() {
    const loadedSettings = [];
    const savedField = getSavedField(this.id);
    if (savedField?.settings) {
      for (const setting of this.settings) {
        const loadedSetting = {
          ...setting
        };
        const savedSetting = savedField.settings.find(s => s.id === setting.id && s.type === setting.type && !(setting.type === 'select' && !s.options?.includes(setting.value || setting.defaultValue)));
        if (savedSetting) {
          loadedSetting.value = savedSetting.value;
        }
        loadedSettings.push(loadedSetting);
      }
    } else {
      loadedSettings.push(...this.settings.map(setting => ({
        ...setting
      })));
    }
    this.savedSettings = loadedSettings;
    this.newSettings = loadedSettings.map(setting => ({
      ...setting
    }));
  }
  save() {
    const newSettings = this.newSettings.map(setting => ({
      ...setting,
      value: Array.isArray(setting.value) ? [...setting.value] : setting.value
    }));
    setSavedField({
      id: this.id,
      name: this.name,
      description: this.description,
      settings: newSettings
    });
    this.savedSettings = newSettings;
  }
}
class Settings extends Modal {
  constructor(fields) {
    const cancelButton = new SecondaryButton('Cancel', () => this.remove());
    const saveButton = new PrimaryButton('Save', () => this.save());
    const content = document.createElement('div');
    setStyles(content, {
      width: '100%',
      display: 'flex',
      gap: '12px',
      'flex-wrap': 'wrap',
      'align-items': 'center',
      'justify-content': 'center'
    });
    super({
      title: 'SETTINGS',
      content: content,
      buttons: [cancelButton, saveButton]
    });
    this.contentContainer = content;
    this.cancelButton = cancelButton;
    this.saveButton = saveButton;
    this.fields = fields;
  }
  load() {
    this.fields.forEach(field => field.load());
    this.updateButtons();
  }
  save() {
    this.fields.forEach(field => field.save());
    this.updateButtons();
  }
  add(parent) {
    this.load();
    this.render();
    return super.add(parent);
  }
  render() {
    while (this.contentContainer.firstChild) {
      this.contentContainer.removeChild(this.contentContainer.firstChild);
    }
    let renderedSettingCount = 0;
    for (const field of this.fields) {
      if (field.userScriptOnly && !isUserScript) continue;
      const fieldElement = document.createElement('div');
      setStyles(fieldElement, {
        width: '100%',
        display: 'flex',
        'flex-wrap': 'wrap',
        'align-items': 'flex-start',
        gap: '4px',
        padding: '8px',
        'background-color': componentColors.secondary,
        'border-radius': '8px',
        'box-shadow': '0 4px 8px 0 rgba(0, 0, 0, 0.40)'
      });
      const fieldNameElement = document.createElement('span');
      fieldNameElement.innerText = field.name;
      setStyles(fieldNameElement, {
        width: '100%',
        'text-align': 'center',
        'font-weight': 'bold',
        'font-size': '20px',
        'line-height': '24px'
      });
      const fieldDescriptionElement = document.createElement('span');
      if (field.description) {
        fieldDescriptionElement.innerText = field.description;
        setStyles(fieldDescriptionElement, {
          width: '100%',
          'text-align': 'center'
        });
      }
      fieldElement.append(fieldNameElement, fieldDescriptionElement);
      for (const setting of field.savedSettings) {
        if (setting.userScriptOnly && !isUserScript) continue;
        const settingElement = document.createElement('div');
        setStyles(settingElement, {
          width: '100%',
          display: 'flex',
          'flex-direction': 'column',
          'align-items': 'flex-start',
          gap: '4px',
          padding: '8px',
          'box-shadow': '0 2px 4px 0 rgba(0, 0, 0, 0.25)',
          'background-color': componentColors.background,
          'border-radius': '8px'
        });
        const settingNameElement = document.createElement('span');
        settingNameElement.innerText = setting.name;
        setStyles(settingNameElement, {
          'font-weight': 'bold',
          'font-size': '18px',
          'line-height': '22px',
          'text-align': 'left'
        });
        const settingDescriptionElement = document.createElement('span');
        if (setting.description) {
          settingDescriptionElement.innerText = setting.description;
          setStyles(settingDescriptionElement, {
            'text-align': 'left'
          });
        }
        const settingInputElements = document.createElement('div');
        setStyles(settingInputElements, {
          width: '100%',
          display: 'flex',
          'flex-direction': 'row',
          'justify-content': 'space-between',
          gap: '4px'
        });
        settingElement.append(settingNameElement, settingDescriptionElement, settingInputElements);
        const inputComponentStyle = {
          'flex-grow': '1',
          display: 'flex'
        };
        const inputStyle = {
          width: '50%',
          'flex-grow': '1'
        };
        const resetButtonText = 'Reset';
        switch (setting.type) {
          case 'text':
          case 'textarea':
            {
              const textSettingValue = field.getValue(setting.id);
              const textComponent = setting.type === 'textarea' ? new TextArea(textSettingValue, {
                rows: 5
              }) : new TextInput(textSettingValue);
              const textInputElement = textComponent.inputElement || textComponent.textareaElement;
              setStyles(textComponent.componentElement, inputComponentStyle);
              setStyles(textInputElement, inputStyle);
              const onTextInput = () => {
                field.setValue(setting.id, textInputElement.value);
                updateTextResetButton();
                this.updateButtons();
              };
              textInputElement.addEventListener('input', () => onTextInput());
              const textResetButton = new SecondaryButton(resetButtonText, () => {
                textInputElement.value = setting.defaultValue;
                onTextInput();
              });
              const updateTextResetButton = () => {
                if (textInputElement.value === setting.defaultValue) {
                  textResetButton.disable();
                } else {
                  textResetButton.enable();
                }
              };
              updateTextResetButton();
              settingInputElements.append(textComponent.componentElement, textResetButton.componentElement);
              break;
            }
          case 'checkbox':
            {
              const checkboxSettingValue = field.getValue(setting.id);
              const onCheck = () => {
                field.setValue(setting.id, checkboxComponent.checkboxElement.checked);
                updateCheckboxResetButton();
                this.updateButtons();
              };
              const checkboxComponent = new Checkbox(() => onCheck());
              checkboxComponent.checkboxElement.checked = !!checkboxSettingValue;
              const checkboxResetButton = new SecondaryButton(resetButtonText, () => {
                checkboxComponent.checkboxElement.checked = setting.defaultValue;
                onCheck();
              });
              const updateCheckboxResetButton = () => {
                if (checkboxComponent.checkboxElement.checked === setting.defaultValue) {
                  checkboxResetButton.disable();
                } else {
                  checkboxResetButton.enable();
                }
              };
              updateCheckboxResetButton();
              settingInputElements.append(checkboxComponent.componentElement, checkboxResetButton.componentElement);
              break;
            }
          case 'select':
            {
              const selectSettingValue = field.getValue(setting.id);
              const onSelect = () => {
                field.setValue(setting.id, selectComponent.selectElement.value);
                updateSelectResetButton();
                this.updateButtons();
              };
              const selectComponent = new Select(setting.options);
              setStyles(selectComponent.componentElement, inputComponentStyle);
              setStyles(selectComponent.selectElement, inputStyle);
              selectComponent.selectElement.addEventListener('change', () => onSelect());
              selectComponent.selectElement.value = selectSettingValue || setting.defaultValue;
              const selectResetButton = new SecondaryButton(resetButtonText, () => {
                selectComponent.selectElement.value = setting.defaultValue;
                onSelect();
              });
              const updateSelectResetButton = () => {
                if (selectComponent.selectElement.value === setting.defaultValue) {
                  selectResetButton.disable();
                } else {
                  selectResetButton.enable();
                }
              };
              updateSelectResetButton();
              settingInputElements.append(selectComponent.componentElement, selectResetButton.componentElement);
              break;
            }
          case 'keys':
            {
              const keysSettingValue = field.getValue(setting.id) || [];
              const keyRecorderComponent = new KeyRecorder({
                keys: keysSettingValue,
                onChange: keys => {
                  field.setValue(setting.id, keys);
                  updateKeysResetButton();
                  this.updateButtons();
                }
              });
              setStyles(keyRecorderComponent.componentElement, inputComponentStyle);
              const keysResetButton = new SecondaryButton(resetButtonText, () => {
                keyRecorderComponent.clear();
                keyRecorderComponent.addKeys(setting.defaultValue);
              });
              const updateKeysResetButton = () => {
                const defaultKeys = setting.defaultValue;
                const currentKeys = keyRecorderComponent.keys;
                if (defaultKeys.length === currentKeys.length && defaultKeys.every(key => currentKeys.includes(key))) {
                  keysResetButton.disable();
                } else {
                  keysResetButton.enable();
                }
              };
              updateKeysResetButton();
              settingInputElements.append(keyRecorderComponent.componentElement, keysResetButton.componentElement);
              break;
            }
        }
        fieldElement.append(settingElement);
        renderedSettingCount++;
      }
      this.contentContainer.append(fieldElement);
    }
    if (renderedSettingCount <= 0) {
      const noSettingsElement = document.createElement('p');
      noSettingsElement.innerText = 'No settings available';
      setStyles(noSettingsElement, {
        width: '100%',
        'text-align': 'center',
        'font-size': '20px',
        'line-height': '24px',
        'font-weight': 'semibold'
      });
      this.contentContainer.append(noSettingsElement);
      return;
    }
  }
  updateButtons() {
    const hasChanges = this.fields.some(field => field.savedSettings.some(saved => {
      const newSetting = field.newSettings.find(n => n.id === saved.id);
      if (Array.isArray(newSetting?.value) && Array.isArray(saved.value)) {
        return newSetting.value.length !== saved.value.length || saved.value.some(v => !newSetting.value?.includes(v));
      }
      return newSetting && newSetting.value !== saved.value;
    }));
    if (hasChanges) this.saveButton.enable();else this.saveButton.disable();
  }
}

class Bookmarklet {
  website = 'bookmarklets.roler.dev';
  main = () => {
    alert('Bookmarklet successfully executed!');
  };
  isWebsite = () => new RegExp(this.website).test(window.location.hostname);
  isRoute = () => {
    if (this.routes) {
      const routes = this.routes.map(route => {
        route = formatRegexText(route);
        route = `^${route}`;
        return route;
      });
      return routes.some(route => new RegExp(route).test(window.location.pathname + window.location.search));
    }
    return true;
  };
  execute() {
    let notice;
    if (!this.isWebsite()) notice = 'Bookmarklet executed on the wrong website!\n' + `Allowed website: ${this.website}`;
    if (!this.isRoute() && !notice) notice = 'Bookmarklet executed on the wrong route!\n' + `Allowed routes: ${this.routes.join(', ')}`;
    if (notice) {
      console.error(notice);
      alert(notice);
      return;
    }
    this.main();
  }
}

class UniversalBookmarklet extends Bookmarklet {
  website = '.*';
}

const anonymousM = () => createSVG({
  svg: {
    attributes: {
      width: '100',
      height: '100',
      stroke: 'currentColor'
    }
  },
  paths: [{
    attributes: {
      d: 'M96.64 56.72c-3.18-6.34-6.04-13.9-7.46-19.72-.21-.87-.36-1.8-.53-2.88-.42-2.65-.95-5.95-2.81-10.52-1.15-2.82-4.4-7.12-8.6-7.78l-2.51-7.26c-.11-.26-.33-.46-.6-.53l-3.46-.87c-.01 0-.03 0-.04-.01-.02 0-.04-.01-.06-.01h-.05-.06-.05-.05c-.02 0-.04.01-.05.01-.02 0-.04.01-.05.01-.02 0-.03.01-.05.02s-.03.01-.05.02-.03.02-.05.02c-.02.01-.03.02-.05.03-.03.01-.05.02-.06.03-.02.01-.03.02-.05.03-.01.01-.03.02-.04.04-.01.01-.02.02-.03.02l-4.97 4.85s-.01.01-.01.02c-.01.01-.02.02-.02.03-.02.02-.04.04-.05.07-.01.01-.01.02-.02.03-.02.03-.04.06-.06.1-.02.03-.03.07-.04.1 0 .01-.01.02-.01.03-.01.03-.01.05-.02.08 0 .01 0 .02-.01.03-.01.04-.01.08-.01.11v.09c0 .03 0 .04.01.05.01.03.01.06.02.09l.79 2.55c-1.62-1.26-3.47-2.31-5.58-3.02-2.87-.96-5.94-1.28-9.14-.95-3.59-.5-10.16-1.39-15.84 2.33-.22.14-.43.29-.65.44l.23-1.38c0-.04.01-.06.01-.09v-.06-.08-.03c0-.03-.01-.07-.02-.1-.01-.05-.02-.08-.04-.11 0-.01-.01-.02-.01-.03-.01-.02-.02-.05-.04-.07-.01-.01-.01-.02-.02-.03-.01-.02-.03-.04-.05-.06-.01-.01-.01-.02-.02-.02-.02-.03-.05-.05-.07-.07l-5.18-4.63c-.01-.01-.02-.01-.03-.02-.02-.03-.03-.04-.05-.05s-.03-.02-.05-.03-.03-.02-.05-.03-.03-.02-.05-.02c-.02-.01-.03-.01-.05-.02s-.04-.01-.05-.02c-.02 0-.03-.01-.05-.01s-.04-.01-.06-.01-.03-.01-.05-.01h-.06-.05c-.02 0-.04 0-.06.01-.02 0-.03 0-.05.01-.02 0-.04.01-.06.01-.01 0-.03.01-.04.01l-3.42 1.02a.83.83 0 0 0-.56.56l-2.06 6.92c-4.49 1.01-7.67 6.51-8.58 8.73-1.86 4.57-2.39 7.86-2.81 10.52-.17 1.08-.32 2.01-.53 2.88-1.65 6.72-4.17 13.72-6.92 19.22-.1.2-.12.43-.04.65 2.31 6.72 4.89 10.62 8.28 15.38a.83.83 0 0 0 .77.35.83.83 0 0 0 .68-.5c.49-1.15.6-1.56.85-2.48l.26-.97-.04 1.27c-.04 1.69-.08 3.03-.44 4.57-.06.27.01.56.21.77l2.07 2.19a.83.83 0 0 0 .61.26c.06 0 .12-.01.18-.02a.82.82 0 0 0 .61-.54c2.43-7.01 2.58-11.98 2.73-16.8.13-4.11.25-8 1.7-13.09a135.84 135.84 0 0 1 2-6.42c-.02.71-.02 1.42 0 2.13l.29 7.92a8.31 8.31 0 0 1-.29 2.51c-.23.83-.55 1.63-.94 2.4-.58 1.14-1.32 2.19-2.19 3.13-.23.25-.29.61-.15.92s.46.5.8.48c.74-.04 2.18-.26 3.46-1.35.15-.13.3-.27.44-.41.46.91 1.14 1.9 2 3.16l.1.15c.75 1.1 1.6 1.9 2.45 2.47-.05 1.49.61 2.95 1.8 3.87.83.64 1.83.96 2.84.96a4.48 4.48 0 0 0 .89-.09l.01.15c.02.22.13.42.3.56a.86.86 0 0 0 .53.19h.08l3.03-.3a.84.84 0 0 0 .75-.91l-.17-1.7c-.02-.22-.13-.42-.3-.56s-.39-.21-.61-.19l-3.03.3a.84.84 0 0 0-.75.91l.02.16c-.95.22-1.96.01-2.74-.59-.64-.5-1.07-1.21-1.21-1.99a8.66 8.66 0 0 0 4.17.6c.28-.03.52-.2.65-.45a.85.85 0 0 0-.01-.79c-.31-.55-.58-1.07-.84-1.57.92.96 1.95 1.81 3.09 2.53l3.34 2.12c-.02.64-.05 1.69-.15 2.89l-.79-.59a.97.97 0 0 0-.87-.15c-.3.09-.53.32-.63.62l-1.18 3.54-2.34 2.34a.97.97 0 0 0-.14 1.2c-.07.08-.13.16-.17.25-.66.43-3.15 1.21-4.83 1.74-4.76 1.5-6.32 2.08-6.6 3.25-.11.46.03.95.37 1.3 1.36 1.36 13.7 7.84 25.29 7.84.19 0 .37 0 .56-.01 10.57-.19 22.17-5.06 25.7-7.73.27-.21.44-.53.44-.87.01-.34-.15-.67-.41-.88-1.41-1.14-3.63-1.9-5.98-2.69-1.98-.67-4.02-1.37-5.15-2.21a.97.97 0 0 0-.15-1.18l-2.34-2.34-1.18-3.54c-.1-.3-.33-.53-.63-.62s-.62-.04-.87.15l-.83.63c-.07-1.19-.1-2.22-.1-2.74l3.64-2.32c1.29-.82 2.39-1.82 3.33-3.05-.27.79-.6 1.61-1.01 2.54a.82.82 0 0 0 .06.79c.15.24.42.38.7.38 1.08 0 4.82-.28 7.36-3.9a15.69 15.69 0 0 0 1.83-3.43c.1.14.21.27.32.4 1.25 1.44 2.77 2.07 3.82 2.34.35.09.72-.06.92-.36.19-.31.17-.7-.07-.98-.89-1.05-1.63-2.22-2.21-3.47-.66-1.42-1.1-2.93-1.31-4.49V38.2c1.41 5.08 2.49 13.09 3.61 21.44.86 6.4 1.75 13.01 2.86 18.82a.81.81 0 0 0 .58.64c.08.02.16.03.24.03.22 0 .44-.09.6-.26l2.43-2.54c.2-.2.27-.49.21-.77-.52-2.18-.89-4.4-1.11-6.63a37.61 37.61 0 0 0 1.65 4.6.83.83 0 0 0 .68.5.84.84 0 0 0 .77-.35c3.53-4.96 5.98-9.84 8.2-16.31.02-.21.01-.45-.09-.65zM69.37 19.14l-1.25-5.03 2.56-2.83 1.76 5.33c-1.18.59-2.24 1.45-3.07 2.53zm3.6-.92l2.64 8-1.45 1.74c-.9-2.26-2.16-4.82-3.87-7.24a7.36 7.36 0 0 1 2.68-2.5zM29.08 19c-.63-.81-1.41-1.47-2.2-1.98l1.54-5.44 2.68 2.71-.67 3.32c-.46.45-.91.91-1.35 1.39zm-2.13 10.12l-1.07-.95c.83-.97 1.65-1.83 2.28-2.35.08-.07.16-.13.25-.2l-1.46 3.5zm-.54-10.41c.56.41 1.11.93 1.54 1.56-1.15 1.33-2.21 2.73-3.14 4.1l1.6-5.66zm-6.23 29.16c-1.51 5.29-1.64 9.46-1.76 13.5-.14 4.38-.27 8.9-2.21 14.99l-.81-.86c.31-1.52.35-2.84.39-4.47.02-.83.04-1.78.11-2.86a52.74 52.74 0 0 0-.29-9.65.83.83 0 0 0-.88-.73.84.84 0 0 0-.78.84c.04 3.6-.43 7.17-1.39 10.59l-.28 1.05-.22.81c-2.89-4.12-5.03-7.61-7.04-13.35 2.75-5.55 5.25-12.55 6.9-19.25.23-.94.39-1.95.56-3.01.41-2.57.92-5.76 2.71-10.15.69-1.69 3.15-6.06 6.46-7.43l-2.71 9.08c-.02.04-.04.09-.05.13a.82.82 0 0 0 .27.88l2.45 2.01c-.68 1.53-1.1 2.87-1.17 3.87a.83.83 0 0 0 .76.89.83.83 0 0 0 .9-.75c.05-.5.56-1.46 1.29-2.55l1.93 1.58c-1.99 4.84-3.71 9.82-5.14 14.84zm19.8-8.5c.64 2 1.49 3.75 2.43 5.27h-6.77c.85-1.01 1.64-2.07 2.35-3.18.63-.99 1.2-2.02 1.71-3.07.08.32.18.65.28.98zm13.37-2.15l-.1-1.68a37.79 37.79 0 0 0 2.97 4.04c1.58 1.86 3.34 3.55 5.25 5.06h-7.29c-.38-2.46-.66-4.94-.83-7.42zM39.5 76.16a1.02 1.02 0 0 0 .24-.38l.82-2.47 8.24 6.18a65.07 65.07 0 0 1-1.75 2.78c-1.47 2.2-2.31 3.08-2.72 3.42-1.45-2.28-5.12-6.13-6.66-7.71l1.83-1.82zm-12.72 7.93c1.12-.47 3-1.06 4.37-1.49 3.51-1.1 5.25-1.69 5.92-2.45 2.09 2.19 5.12 5.5 5.88 7.02a1.14 1.14 0 0 0 .18.25 1.36 1.36 0 0 0 .98.41c.07 0 .14 0 .2-.01.4-.05.91-.26 1.71-1.06l.8 1.6c.1.2.28.35.49.42-.02.13-.06.27-.12.46-.15.46-.38.91-.58 1.25-4.68-.43-9.19-1.8-12.41-3-3.44-1.27-6.07-2.6-7.42-3.4zm46.26.18c-3.81 2.21-11.74 5.27-19.54 6.14a6.59 6.59 0 0 1-.4-.95 5.91 5.91 0 0 1-.19-.69c.18-.08.33-.21.43-.39l.8-1.6c.79.8 1.31 1 1.71 1.06a1.41 1.41 0 0 0 .2.01c.37 0 .72-.14.98-.41.07-.07.14-.16.18-.25.77-1.53 3.84-4.9 5.94-7.09 1.41 1.14 3.63 1.9 5.98 2.69 1.39.47 2.8.95 3.91 1.48zM59.61 73.31l.82 2.47a1.02 1.02 0 0 0 .24.38l1.82 1.82c-1.54 1.57-5.21 5.42-6.66 7.71-.41-.34-1.24-1.22-2.72-3.42a65.07 65.07 0 0 1-1.75-2.78l8.25-6.18zm-2.78-.36l-6.75 5.06-6.81-5.11c.12-1.14.18-2.2.22-3.05l2.94 1.87c1.15.73 2.47 1.1 3.79 1.1s2.64-.37 3.79-1.1l2.64-1.68.18 2.91zm4.2-8.32l-8.2 5.22c-1.58 1-3.62 1-5.2 0l-8.18-5.21c-2.67-1.7-4.69-4.21-5.78-7.15h32.46c-1.14 3.41-2.73 5.63-5.1 7.14zm15.09-5.43l-.04-.04a5.52 5.52 0 0 1-1.02-1.72c-.13-.34-.47-.56-.83-.54a.83.83 0 0 0-.76.63c-.43 1.73-1.16 3.34-2.17 4.78-1.45 2.06-3.35 2.8-4.68 3.05 1.06-2.64 1.44-4.64 1.64-7.25.07-.2.13-.41.2-.62h1.6c.38 0 .7-.31.7-.7V45.34c0-.38-.31-.7-.7-.7h-2.05c-.58-5.04-1.7-10.04-3.36-14.96a.83.83 0 0 0-1.58.54c1.6 4.74 2.69 9.56 3.26 14.42h-2.05c-2.53-1.75-4.8-3.8-6.79-6.14-1.68-1.97-3.13-4.14-4.34-6.45a84.54 84.54 0 0 1 .73-11.58.97.97 0 0 0-.84-1.09c-.54-.07-1.02.3-1.09.84a86.3 86.3 0 0 0-.55 17.15 85.51 85.51 0 0 0 .81 7.29h-2.87a15.22 15.22 0 0 1-1.58-3.87c-.12-.46-.56-.77-1.03-.73-.48.04-.85.43-.88.91a22.86 22.86 0 0 0 .05 3.7h-1.16c-1.13-1.62-2.17-3.57-2.9-5.87-2.56-8.02.31-14.97 1.66-17.58.25-.48.06-1.07-.42-1.31-.48-.25-1.07-.06-1.31.42-1.24 2.4-3.67 8.2-2.66 15.23-.67 1.76-1.52 3.45-2.52 5.03-.93 1.46-1.99 2.82-3.17 4.08h-.93a76.44 76.44 0 0 1 2.35-15.47c.12-.45-.15-.9-.6-1.02s-.9.15-1.02.6c-1.35 5.21-2.15 10.53-2.41 15.89h-2.69c-.38 0-.7.31-.7.7v11.46c0 .38.31.7.7.7h3.14c.43 2.69 1.21 4.94 2.63 7.67-.76-.07-1.73-.28-2.71-.82-.09-.17-.25-.31-.45-.35-.05-.01-.1-.02-.14-.02-.66-.46-1.31-1.09-1.89-1.95l-.1-.15c-1.26-1.84-2.09-3.06-2.35-4.17a.84.84 0 0 0-1.57-.18 3.97 3.97 0 0 1-.98 1.27c.23-.38.45-.76.65-1.16a14.52 14.52 0 0 0 1.05-2.7 9.8 9.8 0 0 0 .35-3.03l-.29-7.92c-.15-4.22.59-8.34 2.21-12.24l3.44-8.27a.85.85 0 0 0-.24-.97.84.84 0 0 0-1-.04c-.95.65-1.88 1.35-2.77 2.07-.29.24-.64.56-1.02.94l.27-.4c1.02-1.47 2.1-2.86 3.22-4.12.03-.03.05-.06.08-.09 2.01-2.27 4.12-4.16 6.19-5.51 5.22-3.41 11.45-2.53 14.8-2.06a.74.74 0 0 0 .21 0c2.99-.33 5.85-.04 8.53.86 2.87.96 5.22 2.63 7.15 4.6.04.05.09.1.14.14.48.5.93 1.02 1.36 1.55.03.06.08.11.12.15.15.19.29.38.43.57.04.06.08.12.13.18 3.44 4.71 5.04 10.16 5.59 12.46v19.53a.41.41 0 0 0 .01.11 17.21 17.21 0 0 0 1.46 5.03c.1.21.22.45.35.69zm11.87 12.25c-.42-1.09-.78-2.19-1.09-3.31-.96-3.42-1.42-6.98-1.39-10.59a.84.84 0 0 0-.78-.84.83.83 0 0 0-.88.73 52.74 52.74 0 0 0-.29 9.65 52.97 52.97 0 0 0 1.21 8.4l-.99 1.04c-.96-5.37-1.77-11.34-2.55-17.13-1.51-11.24-2.95-21.86-5.32-26.01-.1-.42-.23-.93-.4-1.52l5.16-4.61a.83.83 0 0 0 .23-.89c-.02-.05-.04-.09-.06-.13l-2.96-8.55c3.2 1.03 5.62 4.63 6.4 6.54 1.79 4.39 2.3 7.58 2.71 10.15.17 1.07.33 2.08.56 3.01 1.43 5.84 4.27 13.38 7.44 19.75-1.95 5.62-4.09 9.99-7 14.31z'
    }
  }]
});
class LoadingCircle extends Component {
  constructor() {
    super(anonymousM(), {
      defaultStyles: false
    });
    setStyles(this.componentElement, {
      width: '100px',
      height: '100px'
    });
    this.componentElement.animate({
      transform: ['rotate(0deg)', 'rotate(360deg)']
    }, {
      duration: 1000,
      iterations: Infinity,
      easing: 'linear'
    });
  }
}

class Skeleton extends Component {
  constructor() {
    super(document.createElement('div'), {
      defaultStyles: false
    });
    setStyles(this.componentElement, {
      width: '100%',
      height: '100%',
      'background-color': componentColors.secondary,
      opacity: '0.4',
      'border-radius': '4px'
    });
    this.componentElement.animate({
      opacity: [0.2, 0.4, 0.2]
    }, {
      duration: 2000,
      iterations: Infinity,
      easing: 'ease-in-out'
    });
  }
}

class SimpleProgressBar extends Component {
  maxValue = 100;
  minValue = 0;
  currentValue = this.minValue;
  constructor(maxValue, minValue) {
    super(document.createElement('div'), {
      defaultStyles: false
    });
    setStyles(this.componentElement, {
      'z-index': '1000000',
      position: 'fixed',
      bottom: '0',
      left: '0',
      width: '100%',
      height: '24px',
      'background-color': componentColors.accent,
      cursor: 'pointer'
    });
    const progress = document.createElement('div');
    setStyles(progress, {
      width: '0%',
      height: '100%',
      'background-color': componentColors.primary,
      transition: 'width 200ms'
    });
    this.barElement = progress;
    this.componentElement.append(progress);
    this.componentElement.addEventListener('click', () => this.remove());
    this.reset({
      maxValue,
      minValue
    });
  }
  start({
    maxValue,
    minValue,
    currentValue
  } = {}) {
    this.reset({
      maxValue,
      minValue,
      currentValue
    });
    this.add();
  }
  update(currentValue = this.currentValue + 1) {
    if (currentValue > this.maxValue) currentValue = this.maxValue;else if (currentValue < this.minValue) currentValue = this.minValue;
    const currentPercentageRounded = Math.ceil(this.currentValue / this.maxValue * 100);
    const percentageRounded = Math.ceil(currentValue / this.maxValue * 100);
    if (percentageRounded >= 100) this.remove();else if (currentPercentageRounded !== percentageRounded && percentageRounded >= 0) setStyles(this.barElement, {
      width: `${percentageRounded}%`
    });
    this.currentValue = currentValue;
  }
  reset({
    maxValue,
    minValue,
    currentValue
  } = {}) {
    if (maxValue) this.maxValue = maxValue;
    if (minValue) this.minValue = minValue;
    this.update(currentValue || this.minValue);
  }
}

const settingIds = {
  cropFormat: 'crop_format',
  cropQuality: 'crop_quality',
  imageFilename: 'image_filename',
  zipFilename: 'zip_filename',
  enableSavePaths: 'enable_save_paths',
  userscriptImageSavePath: 'userscript_image_save_path',
  userscriptZipSavePath: 'userscript_zip_save_path',
  copyFormat: 'copy_format'
};
const settingGlobalStringVariableNames = {
  seriesTitle: formatStringVariable('SERIES_TITLE'),
  hostname: formatStringVariable('HOSTNAME')
};
const settingPathStringVariableNames = {
  volumeRanges: formatStringVariable('VOLUME_RANGES'),
  firstVolumeNumber: formatStringVariable('FIRST_VOLUME_NUMBER'),
  lastVolumeNumber: formatStringVariable('LAST_VOLUME_NUMBER'),
  coverCount: formatStringVariable('COVER_COUNT'),
  totalCoverCount: formatStringVariable('TOTAL_COVER_COUNT'),
  ...settingGlobalStringVariableNames
};
const settingImageStringVariableNames = {
  volumeName: formatStringVariable('VOLUME_NAME'),
  volumeNumber: formatStringVariable('VOLUME_NUMBER'),
  bookTitle: formatStringVariable('BOOK_TITLE'),
  ...settingGlobalStringVariableNames
};
const settingZipStringVariableNames = {
  ...settingPathStringVariableNames
};
const settingCopyStringVariableNames = {
  coverUrl: formatStringVariable('COVER_URL'),
  ...settingImageStringVariableNames,
  ...settingPathStringVariableNames
};
const settingStringVariableNames = {
  ...settingGlobalStringVariableNames,
  ...settingPathStringVariableNames,
  ...settingImageStringVariableNames,
  ...settingZipStringVariableNames,
  ...settingCopyStringVariableNames
};
const savePathSettingNotice = 'This setting may only work with Tampermonkey!\n';
const getFileSavingSettingDescription = variableIds => 'Available variables: ' + Object.values(variableIds).join(', ');
const getFileSavingSettingDefaultValue = (nameParts, isPath = false) => nameParts.join(isPath ? '/' : ' - ');
const defaultCropFormat = 'jpeg';
const defaultCropQuality = 98;
const coverDownloaderSettings = new SettingsField({
  id: '29f4b713-8ccd-4a4e-97ac-5a34d48ac5d7',
  name: 'Cover Downloader',
  settings: [{
    id: settingIds.cropFormat,
    type: 'select',
    name: 'Crop Format',
    description: 'Select the output format of the cropped images.',
    options: ['PNG', 'JPEG'],
    defaultValue: defaultCropFormat.toUpperCase()
  }, {
    id: settingIds.cropQuality,
    type: 'text',
    name: 'Crop Quality',
    description: 'Specify the output quality of the cropped images.\n' + 'Only used if the format is JPEG.\n' + 'Quality range: 1 - 100',
    defaultValue: defaultCropQuality.toString()
  }, {
    id: settingIds.imageFilename,
    type: 'text',
    name: 'Image Filename',
    description: getFileSavingSettingDescription(settingImageStringVariableNames),
    defaultValue: getFileSavingSettingDefaultValue([settingImageStringVariableNames.hostname, settingImageStringVariableNames.seriesTitle, settingImageStringVariableNames.volumeName])
  }, {
    id: settingIds.zipFilename,
    type: 'text',
    name: 'Zip Filename',
    description: getFileSavingSettingDescription(settingZipStringVariableNames),
    defaultValue: getFileSavingSettingDefaultValue([settingZipStringVariableNames.hostname, settingZipStringVariableNames.seriesTitle, 'covers', `[${settingZipStringVariableNames.volumeRanges}]`, `(${settingZipStringVariableNames.coverCount})`])
  }, {
    id: settingIds.enableSavePaths,
    type: 'checkbox',
    name: 'Enable Save Paths',
    userScriptOnly: true,
    description: savePathSettingNotice + 'Enables the use of save paths for images and zips.',
    defaultValue: false
  }, {
    id: settingIds.userscriptImageSavePath,
    type: 'text',
    name: 'Image Save Path',
    userScriptOnly: true,
    description: savePathSettingNotice + getFileSavingSettingDescription(settingPathStringVariableNames),
    defaultValue: getFileSavingSettingDefaultValue(['covers', 'image', settingPathStringVariableNames.hostname, settingPathStringVariableNames.seriesTitle], true)
  }, {
    id: settingIds.userscriptZipSavePath,
    type: 'text',
    name: 'Zip Save Path',
    userScriptOnly: true,
    description: savePathSettingNotice + getFileSavingSettingDescription(settingPathStringVariableNames),
    defaultValue: getFileSavingSettingDefaultValue(['covers', 'zip'], true)
  }, {
    id: settingIds.copyFormat,
    type: 'textarea',
    name: 'Copy Format',
    description: getFileSavingSettingDescription(settingCopyStringVariableNames),
    defaultValue: `[${settingCopyStringVariableNames.volumeName}][ja]{${settingCopyStringVariableNames.volumeName} cover from ${settingCopyStringVariableNames.hostname}}(${settingCopyStringVariableNames.coverUrl})\n`
  }]
});
class CoverDownloader extends Modal {
  knownFileNames = {};
  aborted = false;
  loadMax = 1;
  currentLoad = 0;
  covers = [];
  busy = false;
  stats = {};
  thumbnail = {
    width: 280,
    output: 'jpeg'
  };
  settings = {
    crop: {
      format: defaultCropFormat,
      quality: defaultCropQuality
    }
  };
  constructor(getCovers, {
    loadMax = 1,
    title,
    fileNamePrefix = 'Volume',
    disableCropping = false
  } = {}) {
    const resultsContainer = document.createElement('div');
    setStyles(resultsContainer, {
      width: '100%',
      'min-width': '200px',
      height: '100%',
      'min-height': '200px',
      display: 'flex',
      'flex-wrap': 'wrap',
      gap: '8px',
      'justify-content': 'center',
      'align-items': 'center'
    });
    const loadContainer = document.createElement('div');
    setStyles(loadContainer, {
      width: '90%',
      'flex-shrink': '0',
      'margin-top': '2px'
    });
    const loadButton = new SecondaryButton('LOAD MORE', () => this.loadCovers());
    setStyles(loadButton.componentElement, {
      width: '100%'
    });
    loadButton.add(loadContainer);
    const buttons = {
      selectAll: new PrimaryButton('Select All', () => this.selectAll()),
      crop: new PrimaryButton('Crop', () => this.crop()),
      open: new PrimaryButton('Open', () => this.open()),
      copy: new PrimaryButton('Copy', () => this.copy()),
      zip: new PrimaryButton('Zip', () => this.zip()),
      save: new PrimaryButton('Save', () => this.save())
    };
    setStyles(buttons.selectAll.componentElement, {
      'min-width': '150px'
    });
    setStyles(buttons.crop.componentElement, {
      'min-width': '100px'
    });
    super({
      title: 'Cover Downloader',
      content: resultsContainer,
      buttons: Object.values(buttons)
    });
    this.resultsContainer = resultsContainer;
    this.loadContainer = loadContainer;
    this.loadButton = loadButton;
    this.loadCircle = new LoadingCircle();
    this.buttons = buttons;
    this.loadingCircle = new LoadingCircle();
    this.loadMax = loadMax;
    this.getCovers = getCovers;
    this.title = title?.trim();
    this.fileNamePrefix = fileNamePrefix;
    this.croppingDisabled = disableCropping;
    this.loadSettings();
    this.componentElement.addEventListener('componentadded', () => {
      this.aborted = false;
      Object.values(buttons).forEach(button => button.hide());
      this.loadingCircle.add(this.resultsContainer);
      this.loadSettings();
      this.loadCovers();
    });
    this.componentElement.addEventListener('componentremoved', () => {
      this.aborted = true;
      this.clearCovers();
    });
  }
  loadSettings() {
    const enablePathsSetting = !!coverDownloaderSettings.getValue(settingIds.enableSavePaths);
    const imagePathSetting = coverDownloaderSettings.getValue(settingIds.userscriptImageSavePath);
    const zipPathSetting = coverDownloaderSettings.getValue(settingIds.userscriptZipSavePath);
    this.settings.paths = {
      enabled: enablePathsSetting,
      image: imagePathSetting?.trim(),
      zip: zipPathSetting?.trim()
    };
    const imageFilenameSetting = coverDownloaderSettings.getValue(settingIds.imageFilename);
    const zipFilenameSetting = coverDownloaderSettings.getValue(settingIds.zipFilename);
    this.settings.filenames = {
      image: imageFilenameSetting?.trim(),
      zip: zipFilenameSetting?.trim()
    };
    this.settings.copyFormat = coverDownloaderSettings.getValue(settingIds.copyFormat);
    const cropFormatSetting = coverDownloaderSettings.getValue(settingIds.cropFormat);
    this.settings.crop.format = cropFormatSetting?.toLowerCase() || this.settings.crop.format;
    const cropQualitySetting = coverDownloaderSettings.getValue(settingIds.cropQuality);
    this.settings.crop.quality = cropQualitySetting ? parseInt(cropQualitySetting) : this.settings.crop.quality;
  }
  loadCovers() {
    if (this.currentLoad >= this.loadMax) this.currentLoad = 0;
    ++this.currentLoad;
    this.loadButton.replace(this.loadCircle.componentElement);
    const progressBar = new SimpleProgressBar();
    this.getCovers(this.currentLoad).then(covers => {
      if (this.aborted) return;
      const coverUrls = this.covers.map(cover => cover.url);
      covers = covers.filter(cover => !coverUrls.includes(cover.url));
      if (covers.length <= 0) throw new Error('No covers found');
      covers.forEach(cover => cover.title = cover.title || `${this.covers.length + 1}`);
      covers.forEach(cover => this.parseTitle(cover));
      covers.sort((a, b) => {
        return a.parsedTitle.localeCompare(b.parsedTitle, undefined, {
          numeric: true,
          sensitivity: 'base'
        });
      });
      covers.forEach(cover => this.setCoverFilename(cover));
      this.covers.push(...covers);
      this.updateStats();
      progressBar.start({
        maxValue: covers.length
      });
      const afterLoad = () => {
        progressBar.update();
        if (progressBar.currentValue >= progressBar.maxValue) {
          progressBar.remove();
          if (covers.some(cover => cover.cropAmount && !cover.cropped)) this.crop(covers, true).catch(console.error);
        }
      };
      covers.forEach(cover => this.loadCover(cover).then(afterLoad).catch(afterLoad));
    }).catch(error => {
      console.error(error);
      progressBar.remove();
      this.remove();
      alertModal('Failed to load covers!\n' + error, 'error').catch(console.error);
    });
  }
  async loadCover(cover) {
    const result = document.createElement('div');
    setStyles(result, {
      'min-width': '134px',
      'max-width': '140px',
      'min-height': '234px',
      'max-height': '240px',
      'flex-grow': '1',
      'background-color': componentColors.background,
      border: `1px solid ${componentColors.secondary}`,
      'border-radius': '4px',
      'box-shadow': '0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 3px 5px 0 rgba(0, 0, 0, 0.2)',
      overflow: 'hidden',
      display: 'flex',
      'flex-direction': 'column',
      cursor: 'pointer',
      'user-select': 'none'
    });
    if (cover.element) cover.element.replaceWith(result);
    cover.element = result;
    const headerContainer = document.createElement('div');
    this.setDefaultStyles(headerContainer);
    setStyles(headerContainer, {
      'font-size': '14px',
      'line-height': '14px',
      display: 'flex',
      'justify-content': 'space-between',
      'align-items': 'center',
      gap: '2px',
      padding: '4px'
    });
    result.append(headerContainer);
    const dimensionsElementPlaceholder = new Skeleton();
    setStyles(dimensionsElementPlaceholder.componentElement, {
      height: '14px'
    });
    dimensionsElementPlaceholder.add(headerContainer);
    const checkboxElementPlaceholder = new Skeleton();
    setStyles(checkboxElementPlaceholder.componentElement, {
      height: '14px',
      width: '14px',
      'flex-shrink': '0'
    });
    checkboxElementPlaceholder.add(headerContainer);
    const imageContainer = document.createElement('div');
    setStyles(imageContainer, {
      position: 'relative',
      'flex-grow': '1'
    });
    result.append(imageContainer);
    const imageElementPlaceholder = new Skeleton();
    setStyles(imageElementPlaceholder.componentElement, {
      position: 'absolute',
      top: '0',
      left: '0'
    });
    imageContainer.append(imageElementPlaceholder.componentElement);
    const footerContainer = document.createElement('div');
    this.setDefaultStyles(footerContainer);
    setStyles(footerContainer, {
      'font-size': '14px',
      'line-height': '14px',
      'text-align': 'center',
      padding: '4px',
      overflow: 'hidden',
      'text-overflow': 'ellipsis',
      'white-space': 'nowrap'
    });
    result.append(footerContainer);
    const titleElementPlaceholder = new Skeleton();
    setStyles(titleElementPlaceholder.componentElement, {
      height: '14px'
    });
    titleElementPlaceholder.add(footerContainer);
    if (this.covers.every(c => c.element)) {
      this.loadingCircle.remove();
      this.covers.forEach(cover => {
        if (!this.resultsContainer.contains(cover.element)) this.resultsContainer.append(cover.element);
      });
      this.loadCircle.replace(this.loadButton.componentElement);
      if (this.currentLoad < this.loadMax) this.resultsContainer.append(this.loadContainer);else this.loadContainer.remove();
    }
    const titleElement = document.createElement('span');
    titleElement.innerText = cover.parsedTitle;
    titleElement.setAttribute('title', cover.parsedTitle);
    titleElementPlaceholder.replace(titleElement);
    await this.download(cover).catch(e => console.warn('Failed to download cover', cover.url, e));
    if (this.aborted) return;
    const imageElement = document.createElement('img');
    imageElement.alt = cover.filename;
    setStyles(imageElement, {
      height: '100%',
      width: '100%',
      position: 'absolute',
      top: '0',
      left: '0',
      'object-fit': 'cover',
      'object-position': 'center'
    });
    imageElementPlaceholder.replace(imageElement);
    const checkbox = new Checkbox();
    setStyles(checkbox.checkboxElement, {
      width: '14px',
      height: '14px',
      position: 'unset',
      'vertical-align': 'unset'
    });
    checkboxElementPlaceholder.replace(checkbox.componentElement);
    cover.select = (select = true) => {
      cover.selected = select;
      checkbox.checkboxElement.checked = cover.selected;
      this.lastSelected = cover;
      let borderColor = componentColors.secondary;
      if (cover.selected) {
        if (cover.errored) borderColor = componentColors.error;else if (!cover.blobUrl) borderColor = componentColors.warning;else borderColor = componentColors.primary;
      }
      let backgroundColor = componentColors.background;
      if (cover.selected) {
        if (cover.errored) backgroundColor = componentColors.error;else if (!cover.blobUrl) backgroundColor = componentColors.warning;else backgroundColor = componentColors.primary;
      }
      setStyles(result, {
        'border-color': borderColor,
        'background-color': backgroundColor
      });
      if (cover.errored) setStyles(checkbox.checkboxElement, {
        'accent-color': componentColors.error
      });else if (!cover.blobUrl) setStyles(checkbox.checkboxElement, {
        'accent-color': componentColors.warning
      });
      this.updateButtons();
      this.updateStats();
    };
    result.addEventListener('click', event => {
      if (!cover.select) return;
      if (event.shiftKey && this.lastSelected) {
        this.selectRange(this.lastSelected, cover, !cover.selected);
      } else cover.select(!cover.selected);
    });
    const thumbnailUrl = cover.thumbnailUrl || getWsrvUrl({
      url: cover.url,
      ...this.thumbnail
    }).href;
    new Promise((resolve, reject) => {
      imageElement.onerror = e => reject(e);
      imageElement.onload = () => resolve();
      imageElement.src = thumbnailUrl;
    }).catch(e => {
      console.warn(e);
      const fallbackThumbnailUrl = cover.blobUrl || cover.url;
      imageElement.onerror = e => console.error('Failed to load thumbnail:', fallbackThumbnailUrl, e);
      imageElement.src = fallbackThumbnailUrl;
    });
    const coverDimensions = !cover.cropped ? await getImageDimensions(cover.url, {
      localUrl: cover.blobUrl || undefined
    }).catch(console.error) : null;
    if (coverDimensions || cover.cropped) {
      cover.width = coverDimensions?.width || cover.width;
      cover.height = coverDimensions?.height || cover.height;
      cover.cropAmount = this.getCropMethod(cover);
      const dimensionsElement = document.createElement('span');
      dimensionsElement.innerText = `${cover.width}x${cover.height}${cover.cropped ? 'c' : ''}`;
      dimensionsElementPlaceholder.replace(dimensionsElement);
    } else {
      console.error('Failed to load cover:', cover.editedUrl || cover.url);
      cover.errored = true;
      const errorElement = document.createElement('span');
      this.setDefaultStyles(errorElement);
      setStyles(errorElement, {
        'font-size': '32px',
        'font-weight': 'bold',
        width: '100%',
        height: '100%',
        position: 'absolute',
        top: '0',
        left: '0',
        'background-color': componentColors.error,
        display: 'flex',
        'justify-content': 'center',
        'align-items': 'center'
      });
      errorElement.innerText = 'ERROR';
      imageElement.replaceWith(errorElement);
      if (cover.selected && cover.select) cover.select();
    }
    if (cover.selected || this.covers.length === 1) cover.select();
    cover.loaded = true;
    this.updateButtons();
  }
  clearCovers() {
    this.removeBlobs();
    this.covers.forEach(cover => cover.element?.remove());
    this.loadContainer.remove();
    this.covers = [];
    this.currentLoad = 0;
    this.knownFileNames = {};
  }
  createBlobUrl(cover) {
    if (!cover.blob) return;
    if (!cover.blobUrl) cover.blobUrl = URL.createObjectURL(cover.blob);
  }
  removeBlob(cover) {
    if (cover.blobUrl) {
      URL.revokeObjectURL(cover.blobUrl);
      delete cover.blobUrl;
    }
    if (cover.blob) delete cover.blob;
  }
  removeBlobs() {
    this.covers.forEach(cover => this.removeBlob(cover));
  }
  setBlob(cover, blob) {
    if (this.aborted) {
      this.removeBlob(cover);
      throw new Error('aborted');
    } else {
      cover.blob = cover.blob || blob;
      this.createBlobUrl(cover);
      return cover.blob;
    }
  }
  parseTitle(cover) {
    let volumeString = cover.title;
    const japaneseCharacters = '0123456789'.split('');
    japaneseCharacters.forEach((character, i) => volumeString = volumeString.replaceAll(character, i.toString()));
    const spaceMatch = volumeString.match(/\((\d+)(\.\d+)?\)| (\d+)(\.\d+)? /);
    if (spaceMatch && spaceMatch[0]) volumeString = spaceMatch[0];
    const volumeNumbers = volumeString.match(/\d+(?:\.\d+)?/g);
    if (volumeNumbers) {
      const volumeNumberString = volumeNumbers.pop();
      if (volumeNumberString) cover.volumeNumber = parseFloat(volumeNumberString);
      cover.parsedTitle = `${this.fileNamePrefix} ${cover.volumeNumber}`.trim();
    } else cover.parsedTitle = cover.title.trim();
  }
  updateStats() {
    const selectedCovers = this.covers.filter(c => c.selected);
    const volumeNumbers = selectedCovers.filter(c => c.volumeNumber !== undefined && c.volumeNumber !== null).map(c => c.volumeNumber).sort((a, b) => a - b);
    const firstVolume = volumeNumbers.length > 0 ? volumeNumbers[0] : undefined;
    const lastVolume = volumeNumbers.length > 0 ? volumeNumbers[volumeNumbers.length - 1] : undefined;
    const volumeRanges = [];
    if (volumeNumbers.length > 0) {
      let rangeStart = volumeNumbers[0];
      let rangeEnd = volumeNumbers[0];
      for (let i = 1; i <= volumeNumbers.length; i++) {
        const current = volumeNumbers[i];
        const prev = volumeNumbers[i - 1];
        if (i === volumeNumbers.length || current !== prev + 1) {
          volumeRanges.push([rangeStart, rangeEnd]);
          if (i < volumeNumbers.length) {
            rangeStart = current;
            rangeEnd = current;
          }
        } else {
          rangeEnd = current;
        }
      }
    }
    this.stats = {
      firstVolume,
      lastVolume,
      volumeRanges,
      totalCovers: this.covers.length,
      selectedCovers: selectedCovers.length
    };
  }
  parseFilename(filename, cover) {
    if (!filename) return;
    return replaceStringVariable(filename, [[settingStringVariableNames.volumeName, cover?.parsedTitle || 'Unknown Volume'], [settingStringVariableNames.volumeNumber, cover?.volumeNumber?.toString() || '1'], [settingStringVariableNames.bookTitle, cover?.title || 'Unknown Book'], [settingStringVariableNames.seriesTitle, this.title || 'Unknown Series'], [settingStringVariableNames.volumeRanges, this.stats.volumeRanges?.map(([start, end]) => start === end ? `${start}` : `${start}-${end}`).join(', ') || '1'], [settingStringVariableNames.firstVolumeNumber, this.stats.firstVolume?.toString() || '1'], [settingStringVariableNames.lastVolumeNumber, this.stats.lastVolume?.toString() || '1'], [settingStringVariableNames.totalCoverCount, this.stats.totalCovers?.toString() || '1'], [settingStringVariableNames.coverCount, this.stats.selectedCovers?.toString() || '1'], [settingStringVariableNames.hostname, window.location.hostname || 'Unknown Hostname'], [settingStringVariableNames.coverUrl, cover?.editedUrl || cover?.url || 'Unknown Cover URL']]);
  }
  setCoverFilename(cover) {
    const name = this.parseFilename(this.settings.filenames?.image, cover) || cover.parsedTitle || 'cover';
    const extension = cover.blob?.type.split('/')[1]?.replace('jpeg', 'jpg') || getMatch(cover.url, /\.(\w+)$/, 1) || 'jpg';
    if (this.knownFileNames[name] === undefined) this.knownFileNames[name] = 0;else ++this.knownFileNames[name];
    if (this.knownFileNames[name] === 0) cover.filename = name;else cover.filename = `${name} (${this.knownFileNames[name]})`;
    cover.extension = extension;
  }
  async download(cover) {
    if (cover.blob) return this.setBlob(cover, cover.blob);
    return await gmFetch(cover.url).then(response => {
      if (!response.ok) throw new Error(response.statusText);
      return response.blob();
    }).then(blob => this.setBlob(cover, blob));
  }
  selectRange(rangeStartCover, rangeEndCover, select = true) {
    if (!this.covers) return;
    let rangeStart = this.covers.indexOf(rangeStartCover);
    let rangeEnd = this.covers.indexOf(rangeEndCover);
    if (rangeStart > rangeEnd) [rangeStart, rangeEnd] = [rangeEnd, rangeStart];
    for (let i = rangeStart; i <= rangeEnd; i++) {
      const cover = this.covers[i];
      if (select && cover.errored) continue;
      if (cover.select) cover.select(select);
    }
  }
  isSelectAll = () => !this.covers.some(cover => cover.selected);
  isCropped = () => this.covers.some(cover => cover.selected && cover.cropped);
  updateButtons() {
    const select = this.isSelectAll();
    const cropped = this.isCropped();
    if (select) this.buttons.selectAll.componentElement.innerText = 'Select All';else this.buttons.selectAll.componentElement.innerText = 'Deselect All';
    this.buttons.selectAll.show();
    if (select && this.covers.every(cover => cover.errored) || select && this.covers.some(cover => !cover.loaded && !cover.errored)) this.buttons.selectAll.disable();else this.buttons.selectAll.enable();
    if (!cropped) this.buttons.crop.componentElement.innerText = 'Crop';else this.buttons.crop.componentElement.innerText = 'Uncrop';
    if (this.croppingDisabled || this.covers.every(cover => !cover.cropAmount)) this.buttons.crop.hide();else this.buttons.crop.show();
    if (this.busy || select) this.buttons.crop.disable();else this.buttons.crop.enable();
    if (!this.covers.some(cover => cover.selected)) {
      this.buttons.open.disable();
      this.buttons.copy.disable();
    } else {
      this.buttons.open.enable();
      this.buttons.copy.enable();
    }
    this.buttons.open.show();
    this.buttons.copy.show();
    if (this.covers.every(cover => !cover.blob)) this.buttons.zip.hide();else this.buttons.zip.show();
    if (this.busy || select || this.covers.some(cover => cover.selected && !cover.blob)) this.buttons.zip.disable();else this.buttons.zip.enable();
    if (this.covers.every(cover => !cover.blobUrl)) this.buttons.save.hide();else this.buttons.save.show();
    if (this.busy || select || this.covers.some(cover => cover.selected && !cover.blobUrl)) this.buttons.save.disable();else this.buttons.save.enable();
  }
  selectAll() {
    if (!this.covers) return;
    this.selectRange(this.covers[0], this.covers[this.covers.length - 1], this.isSelectAll());
    delete this.lastSelected;
  }
  getCropMethod(cover) {
    if (cover.cropAmount) return cover.cropAmount;
    if (cover.cropped || !cover.width || !cover.height) return;
    const aspect = Math.floor(cover.width / cover.height * 100) / 100;
    if (cover.width >= 880 && cover.width <= 964 && cover.height === 1200) return 120;
    if (cover.width >= 220 && cover.width <= 241 && cover.height === 300) return 30;
    if (cover.height > 4000 && aspect >= 0.73 && aspect < 0.8) return -355;
    if (cover.width > 2000 && cover.height > 2000 && aspect >= 0.73 && aspect < 0.8) return -211;
    if (cover.width < 2000 && cover.height > 2000 && aspect >= 0.73 && aspect < 0.8) return -224;
  }
  async crop(covers = this.covers, force = false) {
    if (this.busy || !covers || this.croppingDisabled) return;
    this.busy = true;
    this.updateButtons();
    const cropped = this.isCropped();
    const coversToCrop = covers.filter(cover => force && cover.cropAmount || cover.selected && cover.cropAmount);
    const progressBar = new SimpleProgressBar(coversToCrop.length);
    progressBar.start();
    await Promise.all(coversToCrop.map(async cover => {
      if (force && cover.cropped) {
        progressBar.update();
        return;
      } else if (cropped && !force) {
        if (cover.cropped) {
          cover.loaded = false;
          this.removeBlob(cover);
          delete cover.editedUrl;
          delete cover.thumbnailUrl;
          cover.extension = cover.croppedExtension;
          cover.cropped = false;
          await this.loadCover(cover).catch(console.error);
        }
        progressBar.update();
        return;
      }
      const cropExtension = this.settings.crop.format?.replace('jpeg', 'jpg');
      const width = cover.width;
      const height = cover.height;
      const cropAmount = cover.cropAmount;
      const absoluteCropAmount = Math.abs(cropAmount);
      const croppedWidth = width - absoluteCropAmount;
      const cropSettings = {
        url: cover.url,
        output: this.settings.crop.format,
        quality: this.settings.crop.quality,
        width,
        height,
        cw: cropAmount > 0 ? croppedWidth : undefined,
        cx: cropAmount < 0 ? absoluteCropAmount : undefined
      };
      let croppedImage = await getWsrvImage(cropSettings).catch(console.warn);
      if (croppedImage) {
        const croppedShareUrl = getWsrvUrl(cropSettings);
        cover.editedUrl = croppedShareUrl.href;
      } else if (cover.blobUrl) {
        croppedImage = await cropImage({
          ...cropSettings,
          url: cover.blobUrl
        }).catch(console.error);
      }
      if (croppedImage) {
        const thumbnailAbsoluteCropAmount = Math.round(absoluteCropAmount * (this.thumbnail.width / width));
        const thumbnailCroppedWidth = this.thumbnail.width - thumbnailAbsoluteCropAmount;
        const thumbnailUrl = getWsrvUrl({
          url: cover.url,
          ...this.thumbnail,
          cw: cropAmount > 0 ? thumbnailCroppedWidth : undefined,
          cx: cropAmount < 0 ? thumbnailAbsoluteCropAmount : undefined
        });
        cover.loaded = false;
        this.removeBlob(cover);
        this.setBlob(cover, croppedImage);
        cover.width = croppedWidth;
        cover.height = height;
        cover.thumbnailUrl = thumbnailUrl.href;
        cover.croppedExtension = cover.extension;
        cover.extension = cropExtension;
        cover.cropped = true;
        await this.loadCover(cover).catch(console.error);
      } else {
        console.error('Failed to crop cover:', cover.url);
        cover.loaded = false;
        this.removeBlob(cover);
        if (cover.croppedExtension) cover.extension = cover.croppedExtension;
        cover.cropped = false;
        await this.loadCover(cover).catch(console.error);
      }
      progressBar.update();
    }));
    progressBar.remove();
    this.busy = false;
    this.updateButtons();
  }
  open() {
    this.covers.forEach(cover => {
      if (!cover.selected) return;
      openNewTab(cover.cropped ? cover.editedUrl || cover.blobUrl || cover.url : cover.url);
    });
  }
  async copy() {
    let clipboardText = '';
    this.covers.forEach(cover => {
      if (!cover.selected) return;
      clipboardText += this.parseFilename(this.settings.copyFormat, cover) || '';
    });
    const copied = await copyText(clipboardText);
    if (!copied) await alertModal('Failed to copy to clipboard!\n' + clipboardText, 'error');
  }
  async save() {
    if (this.busy) return;
    const path = this.parseFilename(this.settings.paths?.image);
    this.busy = true;
    this.updateButtons();
    for (const cover of this.covers) {
      if (!cover.selected) continue;
      const filename = `${cover.filename}.${cover.extension}`;
      await saveFile(isUserScript && this.settings.paths?.enabled ? cover.cropped ? cover.editedUrl || cover.blobUrl || cover.url : cover.url : cover.blobUrl || cover.editedUrl || cover.url, filename, {
        path: this.settings.paths?.enabled ? path : undefined
      });
    }
    this.busy = false;
    this.updateButtons();
  }
  async zip() {
    if (this.busy) return;
    const path = this.parseFilename(this.settings.paths?.zip);
    const filename = `${this.parseFilename(this.settings.filenames?.zip) || 'covers'}.zip`;
    this.busy = true;
    this.updateButtons();
    const selectedCoverFiles = this.covers.filter(cover => cover.selected && cover.blob).map(cover => new File([cover.blob], `${cover.filename}.${cover.extension}`, {
      type: cover.blob.type
    }));
    const progressBar = new SimpleProgressBar();
    const abortController = new AbortController();
    if (this.aborted) abortController.abort();
    progressBar.start({
      maxValue: selectedCoverFiles.length
    });
    let zipFile;
    try {
      zipFile = await zipFiles(selectedCoverFiles, {
        filename,
        signal: abortController.signal,
        onProgress: () => {
          if (this.aborted) abortController.abort();
          progressBar.update();
        }
      });
    } catch (error) {
      console.error(error);
      progressBar.remove();
      this.busy = false;
      this.updateButtons();
      if (!this.aborted) await alertModal('Failed to zip covers!\n' + error, 'error');
    }
    this.busy = false;
    this.updateButtons();
    progressBar.remove();
    if (zipFile) await saveFile(zipFile, filename, {
      path: this.settings.paths?.enabled ? path : undefined
    });
  }
}

class MangadexBookmarklet extends Bookmarklet {
  website = `^(${MANGADEX_URL.hostname}|${MANGADEX_CANARY_URL.hostname}|${MANGADEX_SANDBOX_URL.hostname})`;
}

var data;
var hasRequiredData;

function requireData () {
	if (hasRequiredData) return data;
	hasRequiredData = 1;
	const LANGUAGES_LIST = {
	  aa: {
	    name: 'Afar',
	    nativeName: 'Afaraf',
	  },
	  ab: {
	    name: 'Abkhaz',
	    nativeName: 'аҧсуа бызшәа',
	  },
	  ae: {
	    name: 'Avestan',
	    nativeName: 'avesta',
	  },
	  af: {
	    name: 'Afrikaans',
	    nativeName: 'Afrikaans',
	  },
	  ak: {
	    name: 'Akan',
	    nativeName: 'Akan',
	  },
	  am: {
	    name: 'Amharic',
	    nativeName: 'አማርኛ',
	  },
	  an: {
	    name: 'Aragonese',
	    nativeName: 'aragonés',
	  },
	  ar: {
	    name: 'Arabic',
	    nativeName: 'العربية',
	  },
	  as: {
	    name: 'Assamese',
	    nativeName: 'অসমীয়া',
	  },
	  av: {
	    name: 'Avaric',
	    nativeName: 'авар мацӀ',
	  },
	  ay: {
	    name: 'Aymara',
	    nativeName: 'aymar aru',
	  },
	  az: {
	    name: 'Azerbaijani',
	    nativeName: 'azərbaycan dili',
	  },
	  ba: {
	    name: 'Bashkir',
	    nativeName: 'башҡорт теле',
	  },
	  be: {
	    name: 'Belarusian',
	    nativeName: 'беларуская мова',
	  },
	  bg: {
	    name: 'Bulgarian',
	    nativeName: 'български език',
	  },
	  bi: {
	    name: 'Bislama',
	    nativeName: 'Bislama',
	  },
	  bm: {
	    name: 'Bambara',
	    nativeName: 'bamanankan',
	  },
	  bn: {
	    name: 'Bengali',
	    nativeName: 'বাংলা',
	  },
	  bo: {
	    name: 'Tibetan',
	    nativeName: 'བོད་ཡིག',
	  },
	  br: {
	    name: 'Breton',
	    nativeName: 'brezhoneg',
	  },
	  bs: {
	    name: 'Bosnian',
	    nativeName: 'bosanski jezik',
	  },
	  ca: {
	    name: 'Catalan',
	    nativeName: 'Català',
	  },
	  ce: {
	    name: 'Chechen',
	    nativeName: 'нохчийн мотт',
	  },
	  ch: {
	    name: 'Chamorro',
	    nativeName: 'Chamoru',
	  },
	  co: {
	    name: 'Corsican',
	    nativeName: 'corsu',
	  },
	  cr: {
	    name: 'Cree',
	    nativeName: 'ᓀᐦᐃᔭᐍᐏᐣ',
	  },
	  cs: {
	    name: 'Czech',
	    nativeName: 'Čeština',
	  },
	  cu: {
	    name: 'Old Church Slavonic',
	    nativeName: 'ѩзыкъ словѣньскъ',
	  },
	  cv: {
	    name: 'Chuvash',
	    nativeName: 'чӑваш чӗлхи',
	  },
	  cy: {
	    name: 'Welsh',
	    nativeName: 'Cymraeg',
	  },
	  da: {
	    name: 'Danish',
	    nativeName: 'Dansk',
	  },
	  de: {
	    name: 'German',
	    nativeName: 'Deutsch',
	  },
	  dv: {
	    name: 'Divehi',
	    nativeName: 'ދިވެހި',
	  },
	  dz: {
	    name: 'Dzongkha',
	    nativeName: 'རྫོང་ཁ',
	  },
	  ee: {
	    name: 'Ewe',
	    nativeName: 'Eʋegbe',
	  },
	  el: {
	    name: 'Greek',
	    nativeName: 'Ελληνικά',
	  },
	  en: {
	    name: 'English',
	    nativeName: 'English',
	  },
	  eo: {
	    name: 'Esperanto',
	    nativeName: 'Esperanto',
	  },
	  es: {
	    name: 'Spanish',
	    nativeName: 'Español',
	  },
	  et: {
	    name: 'Estonian',
	    nativeName: 'eesti',
	  },
	  eu: {
	    name: 'Basque',
	    nativeName: 'euskara',
	  },
	  fa: {
	    name: 'Persian',
	    nativeName: 'فارسی',
	  },
	  ff: {
	    name: 'Fula',
	    nativeName: 'Fulfulde',
	  },
	  fi: {
	    name: 'Finnish',
	    nativeName: 'suomi',
	  },
	  fj: {
	    name: 'Fijian',
	    nativeName: 'vosa Vakaviti',
	  },
	  fo: {
	    name: 'Faroese',
	    nativeName: 'Føroyskt',
	  },
	  fr: {
	    name: 'French',
	    nativeName: 'Français',
	  },
	  fy: {
	    name: 'Western Frisian',
	    nativeName: 'Frysk',
	  },
	  ga: {
	    name: 'Irish',
	    nativeName: 'Gaeilge',
	  },
	  gd: {
	    name: 'Scottish Gaelic',
	    nativeName: 'Gàidhlig',
	  },
	  gl: {
	    name: 'Galician',
	    nativeName: 'galego',
	  },
	  gn: {
	    name: 'Guaraní',
	    nativeName: "Avañe'ẽ",
	  },
	  gu: {
	    name: 'Gujarati',
	    nativeName: 'ગુજરાતી',
	  },
	  gv: {
	    name: 'Manx',
	    nativeName: 'Gaelg',
	  },
	  ha: {
	    name: 'Hausa',
	    nativeName: 'هَوُسَ',
	  },
	  he: {
	    name: 'Hebrew',
	    nativeName: 'עברית',
	  },
	  hi: {
	    name: 'Hindi',
	    nativeName: 'हिन्दी',
	  },
	  ho: {
	    name: 'Hiri Motu',
	    nativeName: 'Hiri Motu',
	  },
	  hr: {
	    name: 'Croatian',
	    nativeName: 'Hrvatski',
	  },
	  ht: {
	    name: 'Haitian',
	    nativeName: 'Kreyòl ayisyen',
	  },
	  hu: {
	    name: 'Hungarian',
	    nativeName: 'magyar',
	  },
	  hy: {
	    name: 'Armenian',
	    nativeName: 'Հայերեն',
	  },
	  hz: {
	    name: 'Herero',
	    nativeName: 'Otjiherero',
	  },
	  ia: {
	    name: 'Interlingua',
	    nativeName: 'Interlingua',
	  },
	  id: {
	    name: 'Indonesian',
	    nativeName: 'Bahasa Indonesia',
	  },
	  ie: {
	    name: 'Interlingue',
	    nativeName: 'Interlingue',
	  },
	  ig: {
	    name: 'Igbo',
	    nativeName: 'Asụsụ Igbo',
	  },
	  ii: {
	    name: 'Nuosu',
	    nativeName: 'ꆈꌠ꒿ Nuosuhxop',
	  },
	  ik: {
	    name: 'Inupiaq',
	    nativeName: 'Iñupiaq',
	  },
	  io: {
	    name: 'Ido',
	    nativeName: 'Ido',
	  },
	  is: {
	    name: 'Icelandic',
	    nativeName: 'Íslenska',
	  },
	  it: {
	    name: 'Italian',
	    nativeName: 'Italiano',
	  },
	  iu: {
	    name: 'Inuktitut',
	    nativeName: 'ᐃᓄᒃᑎᑐᑦ',
	  },
	  ja: {
	    name: 'Japanese',
	    nativeName: '日本語',
	  },
	  jv: {
	    name: 'Javanese',
	    nativeName: 'basa Jawa',
	  },
	  ka: {
	    name: 'Georgian',
	    nativeName: 'ქართული',
	  },
	  kg: {
	    name: 'Kongo',
	    nativeName: 'Kikongo',
	  },
	  ki: {
	    name: 'Kikuyu',
	    nativeName: 'Gĩkũyũ',
	  },
	  kj: {
	    name: 'Kwanyama',
	    nativeName: 'Kuanyama',
	  },
	  kk: {
	    name: 'Kazakh',
	    nativeName: 'қазақ тілі',
	  },
	  kl: {
	    name: 'Kalaallisut',
	    nativeName: 'kalaallisut',
	  },
	  km: {
	    name: 'Khmer',
	    nativeName: 'ខេមរភាសា',
	  },
	  kn: {
	    name: 'Kannada',
	    nativeName: 'ಕನ್ನಡ',
	  },
	  ko: {
	    name: 'Korean',
	    nativeName: '한국어',
	  },
	  kr: {
	    name: 'Kanuri',
	    nativeName: 'Kanuri',
	  },
	  ks: {
	    name: 'Kashmiri',
	    nativeName: 'कश्मीरी',
	  },
	  ku: {
	    name: 'Kurdish',
	    nativeName: 'Kurdî',
	  },
	  kv: {
	    name: 'Komi',
	    nativeName: 'коми кыв',
	  },
	  kw: {
	    name: 'Cornish',
	    nativeName: 'Kernewek',
	  },
	  ky: {
	    name: 'Kyrgyz',
	    nativeName: 'Кыргызча',
	  },
	  la: {
	    name: 'Latin',
	    nativeName: 'latine',
	  },
	  lb: {
	    name: 'Luxembourgish',
	    nativeName: 'Lëtzebuergesch',
	  },
	  lg: {
	    name: 'Ganda',
	    nativeName: 'Luganda',
	  },
	  li: {
	    name: 'Limburgish',
	    nativeName: 'Limburgs',
	  },
	  ln: {
	    name: 'Lingala',
	    nativeName: 'Lingála',
	  },
	  lo: {
	    name: 'Lao',
	    nativeName: 'ພາສາລາວ',
	  },
	  lt: {
	    name: 'Lithuanian',
	    nativeName: 'lietuvių kalba',
	  },
	  lu: {
	    name: 'Luba-Katanga',
	    nativeName: 'Kiluba',
	  },
	  lv: {
	    name: 'Latvian',
	    nativeName: 'latviešu valoda',
	  },
	  mg: {
	    name: 'Malagasy',
	    nativeName: 'fiteny malagasy',
	  },
	  mh: {
	    name: 'Marshallese',
	    nativeName: 'Kajin M̧ajeļ',
	  },
	  mi: {
	    name: 'Māori',
	    nativeName: 'te reo Māori',
	  },
	  mk: {
	    name: 'Macedonian',
	    nativeName: 'македонски јазик',
	  },
	  ml: {
	    name: 'Malayalam',
	    nativeName: 'മലയാളം',
	  },
	  mn: {
	    name: 'Mongolian',
	    nativeName: 'Монгол хэл',
	  },
	  mr: {
	    name: 'Marathi',
	    nativeName: 'मराठी',
	  },
	  ms: {
	    name: 'Malay',
	    nativeName: 'Bahasa Melayu',
	  },
	  mt: {
	    name: 'Maltese',
	    nativeName: 'Malti',
	  },
	  my: {
	    name: 'Burmese',
	    nativeName: 'ဗမာစာ',
	  },
	  na: {
	    name: 'Nauru',
	    nativeName: 'Dorerin Naoero',
	  },
	  nb: {
	    name: 'Norwegian Bokmål',
	    nativeName: 'Norsk bokmål',
	  },
	  nd: {
	    name: 'Northern Ndebele',
	    nativeName: 'isiNdebele',
	  },
	  ne: {
	    name: 'Nepali',
	    nativeName: 'नेपाली',
	  },
	  ng: {
	    name: 'Ndonga',
	    nativeName: 'Owambo',
	  },
	  nl: {
	    name: 'Dutch',
	    nativeName: 'Nederlands',
	  },
	  nn: {
	    name: 'Norwegian Nynorsk',
	    nativeName: 'Norsk nynorsk',
	  },
	  no: {
	    name: 'Norwegian',
	    nativeName: 'Norsk',
	  },
	  nr: {
	    name: 'Southern Ndebele',
	    nativeName: 'isiNdebele',
	  },
	  nv: {
	    name: 'Navajo',
	    nativeName: 'Diné bizaad',
	  },
	  ny: {
	    name: 'Chichewa',
	    nativeName: 'chiCheŵa',
	  },
	  oc: {
	    name: 'Occitan',
	    nativeName: 'occitan',
	  },
	  oj: {
	    name: 'Ojibwe',
	    nativeName: 'ᐊᓂᔑᓈᐯᒧᐎᓐ',
	  },
	  om: {
	    name: 'Oromo',
	    nativeName: 'Afaan Oromoo',
	  },
	  or: {
	    name: 'Oriya',
	    nativeName: 'ଓଡ଼ିଆ',
	  },
	  os: {
	    name: 'Ossetian',
	    nativeName: 'ирон æвзаг',
	  },
	  pa: {
	    name: 'Panjabi',
	    nativeName: 'ਪੰਜਾਬੀ',
	  },
	  pi: {
	    name: 'Pāli',
	    nativeName: 'पाऴि',
	  },
	  pl: {
	    name: 'Polish',
	    nativeName: 'Polski',
	  },
	  ps: {
	    name: 'Pashto',
	    nativeName: 'پښتو',
	  },
	  pt: {
	    name: 'Portuguese',
	    nativeName: 'Português',
	  },
	  qu: {
	    name: 'Quechua',
	    nativeName: 'Runa Simi',
	  },
	  rm: {
	    name: 'Romansh',
	    nativeName: 'rumantsch grischun',
	  },
	  rn: {
	    name: 'Kirundi',
	    nativeName: 'Ikirundi',
	  },
	  ro: {
	    name: 'Romanian',
	    nativeName: 'Română',
	  },
	  ru: {
	    name: 'Russian',
	    nativeName: 'Русский',
	  },
	  rw: {
	    name: 'Kinyarwanda',
	    nativeName: 'Ikinyarwanda',
	  },
	  sa: {
	    name: 'Sanskrit',
	    nativeName: 'संस्कृतम्',
	  },
	  sc: {
	    name: 'Sardinian',
	    nativeName: 'sardu',
	  },
	  sd: {
	    name: 'Sindhi',
	    nativeName: 'सिन्धी',
	  },
	  se: {
	    name: 'Northern Sami',
	    nativeName: 'Davvisámegiella',
	  },
	  sg: {
	    name: 'Sango',
	    nativeName: 'yângâ tî sängö',
	  },
	  si: {
	    name: 'Sinhala',
	    nativeName: 'සිංහල',
	  },
	  sk: {
	    name: 'Slovak',
	    nativeName: 'Slovenčina',
	  },
	  sl: {
	    name: 'Slovenian',
	    nativeName: 'slovenščina',
	  },
	  sm: {
	    name: 'Samoan',
	    nativeName: "gagana fa'a Samoa",
	  },
	  sn: {
	    name: 'Shona',
	    nativeName: 'chiShona',
	  },
	  so: {
	    name: 'Somali',
	    nativeName: 'Soomaaliga',
	  },
	  sq: {
	    name: 'Albanian',
	    nativeName: 'Shqip',
	  },
	  sr: {
	    name: 'Serbian',
	    nativeName: 'српски језик',
	  },
	  ss: {
	    name: 'Swati',
	    nativeName: 'SiSwati',
	  },
	  st: {
	    name: 'Southern Sotho',
	    nativeName: 'Sesotho',
	  },
	  su: {
	    name: 'Sundanese',
	    nativeName: 'Basa Sunda',
	  },
	  sv: {
	    name: 'Swedish',
	    nativeName: 'Svenska',
	  },
	  sw: {
	    name: 'Swahili',
	    nativeName: 'Kiswahili',
	  },
	  ta: {
	    name: 'Tamil',
	    nativeName: 'தமிழ்',
	  },
	  te: {
	    name: 'Telugu',
	    nativeName: 'తెలుగు',
	  },
	  tg: {
	    name: 'Tajik',
	    nativeName: 'тоҷикӣ',
	  },
	  th: {
	    name: 'Thai',
	    nativeName: 'ไทย',
	  },
	  ti: {
	    name: 'Tigrinya',
	    nativeName: 'ትግርኛ',
	  },
	  tk: {
	    name: 'Turkmen',
	    nativeName: 'Türkmençe',
	  },
	  tl: {
	    name: 'Tagalog',
	    nativeName: 'Wikang Tagalog',
	  },
	  tn: {
	    name: 'Tswana',
	    nativeName: 'Setswana',
	  },
	  to: {
	    name: 'Tonga',
	    nativeName: 'faka Tonga',
	  },
	  tr: {
	    name: 'Turkish',
	    nativeName: 'Türkçe',
	  },
	  ts: {
	    name: 'Tsonga',
	    nativeName: 'Xitsonga',
	  },
	  tt: {
	    name: 'Tatar',
	    nativeName: 'татар теле',
	  },
	  tw: {
	    name: 'Twi',
	    nativeName: 'Twi',
	  },
	  ty: {
	    name: 'Tahitian',
	    nativeName: 'Reo Tahiti',
	  },
	  ug: {
	    name: 'Uyghur',
	    nativeName: 'ئۇيغۇرچە‎',
	  },
	  uk: {
	    name: 'Ukrainian',
	    nativeName: 'Українська',
	  },
	  ur: {
	    name: 'Urdu',
	    nativeName: 'اردو',
	  },
	  uz: {
	    name: 'Uzbek',
	    nativeName: 'Ўзбек',
	  },
	  ve: {
	    name: 'Venda',
	    nativeName: 'Tshivenḓa',
	  },
	  vi: {
	    name: 'Vietnamese',
	    nativeName: 'Tiếng Việt',
	  },
	  vo: {
	    name: 'Volapük',
	    nativeName: 'Volapük',
	  },
	  wa: {
	    name: 'Walloon',
	    nativeName: 'walon',
	  },
	  wo: {
	    name: 'Wolof',
	    nativeName: 'Wollof',
	  },
	  xh: {
	    name: 'Xhosa',
	    nativeName: 'isiXhosa',
	  },
	  yi: {
	    name: 'Yiddish',
	    nativeName: 'ייִדיש',
	  },
	  yo: {
	    name: 'Yoruba',
	    nativeName: 'Yorùbá',
	  },
	  za: {
	    name: 'Zhuang',
	    nativeName: 'Saɯ cueŋƅ',
	  },
	  zh: {
	    name: 'Chinese',
	    nativeName: '中文',
	  },
	  zu: {
	    name: 'Zulu',
	    nativeName: 'isiZulu',
	  },
	};

	data = LANGUAGES_LIST;
	return data;
}

var src;
var hasRequiredSrc;

function requireSrc () {
	if (hasRequiredSrc) return src;
	hasRequiredSrc = 1;
	const LANGUAGES_LIST = /*@__PURE__*/ requireData();

	const LANGUAGES = {};
	const LANGUAGES_BY_NAME = {};
	const LANGUAGE_CODES = [];
	const LANGUAGE_NAMES = [];
	const LANGUAGE_NATIVE_NAMES = [];

	for (const code in LANGUAGES_LIST) {
	  const { name, nativeName } = LANGUAGES_LIST[code];
	  LANGUAGES[code] =
	    LANGUAGES_BY_NAME[name.toLowerCase()] =
	    LANGUAGES_BY_NAME[nativeName.toLowerCase()] =
	      { code, name, nativeName };
	  LANGUAGE_CODES.push(code);
	  LANGUAGE_NAMES.push(name);
	  LANGUAGE_NATIVE_NAMES.push(nativeName);
	}

	src = class ISO6391 {
	  static getLanguages(codes = []) {
	    return codes.map(code =>
	      ISO6391.validate(code)
	        ? Object.assign({}, LANGUAGES[code])
	        : { code, name: '', nativeName: '' }
	    );
	  }

	  static getName(code) {
	    return ISO6391.validate(code) ? LANGUAGES_LIST[code].name : '';
	  }

	  static getAllNames() {
	    return LANGUAGE_NAMES.slice();
	  }

	  static getNativeName(code) {
	    return ISO6391.validate(code) ? LANGUAGES_LIST[code].nativeName : '';
	  }

	  static getAllNativeNames() {
	    return LANGUAGE_NATIVE_NAMES.slice();
	  }

	  static getCode(name) {
	    name = name.toLowerCase();
	    return LANGUAGES_BY_NAME.hasOwnProperty(name)
	      ? LANGUAGES_BY_NAME[name].code
	      : '';
	  }

	  static getAllCodes() {
	    return LANGUAGE_CODES.slice();
	  }

	  static validate(code) {
	    return LANGUAGES_LIST.hasOwnProperty(code);
	  }
	};
	return src;
}

var srcExports = /*@__PURE__*/ requireSrc();
var ISO6391 = /*@__PURE__*/getDefaultExportFromCjs(srcExports);

const titleRoute = '/title/:uuid';
const titleEditRoute = '/title/edit/:uuid';
const titleDraftRoute = '/title/:uuid\\?draft=true';
const titleEditDraftRoute = '/title/edit/:uuid\\?draft=true';
const titleCreateRoute = '/create/title';
const titleEditRoutes = [titleEditRoute, titleEditDraftRoute];
const allSiteLangs = ['ja-ro',
// Japanese (Romanized)
'ko-ro',
// Korean (Romanized)
'zh-ro',
// Chinese (Romanized)
'zh-hk',
// Chinese (Traditional)
'es-la',
// Spanish (LATAM)
'pt-br',
// Portuguese (Brazil)
... /* @__PURE__ */ISO6391.getAllCodes()];
const langToFlagMap = {
  en: 'gb',
  // English -> Great Britain
  ja: 'jp',
  // Japanese -> Japan
  'ja-ro': 'jp',
  // Japanese (Romanized) -> Japan
  ko: 'kr',
  // Korean -> South Korea
  'ko-ro': 'kr',
  // Korean (Romanized) -> South Korea
  zh: 'cn',
  // Chinese (Simplified) -> China
  'zh-hk': 'hk',
  // Chinese (Traditional) -> Hong Kong
  'zh-ro': 'cn',
  // Chinese (Romanized) -> China
  af: 'za',
  // Afrikaans -> South Africa
  sq: 'sq',
  // Albanian -> Albania
  ar: 'sa',
  // Arabic -> Saudi Arabia
  az: 'az',
  // Azerbaijani -> Azerbaijan
  eu: 'eu',
  // Basque -> Basque
  be: 'by',
  // Belarusian -> Belarus
  bn: 'bd',
  // Bengali -> Bangladesh
  bg: 'bg',
  // Bulgarian -> Bulgaria
  my: 'mm',
  // Burmese -> Myanmar
  ca: 'ad',
  // Catalan -> Andorra
  cv: 'ru-cu',
  // Chuvash -> Russia
  hr: 'hr',
  // Croatian -> Croatia
  cs: 'cz',
  // Czech -> Czech Republic
  da: 'dk',
  // Danish -> Denmark
  nl: 'nl',
  // Dutch -> Netherlands
  eo: 'eo',
  // Esperanto -> Esperanto
  et: 'et',
  // Estonian -> Estonia
  tl: 'ph',
  // Filipino/Tagalog -> Philippines
  fi: 'fi',
  // Finnish -> Finland
  fr: 'fr',
  // French -> France
  ka: 'ka',
  // Georgian -> Georgia
  de: 'de',
  // German -> Germany
  el: 'gr',
  // Greek -> Greece
  he: 'il',
  // Hebrew -> Israel
  hi: 'in',
  // Hindi -> India
  hu: 'hu',
  // Hungarian -> Hungary
  id: 'id',
  // Indonesian -> Indonesia
  ga: 'ie',
  // Irish -> Ireland
  it: 'it',
  // Italian -> Italy
  jv: 'id',
  // Javanese -> Indonesia
  kk: 'kz',
  // Kazakh -> Kazakhstan
  la: 'ri',
  // Latin -> Vatican City
  lt: 'lt',
  // Lithuanian -> Lithuania
  ms: 'my',
  // Malay -> Malaysia
  mn: 'mn',
  // Mongolian -> Mongolia
  ne: 'np',
  // Nepali -> Nepal
  no: 'no',
  // Norwegian -> Norway
  fa: 'ir',
  // Persian/Farsi -> Iran
  pl: 'pl',
  // Polish -> Poland
  pt: 'pt',
  // Portuguese -> Portugal
  'pt-br': 'br',
  // Portuguese (Brazil) -> Brazil
  ro: 'ro',
  // Romanian -> Romania
  ru: 'ru',
  // Russian -> Russia
  sr: 'rs',
  // Serbian -> Serbia
  sk: 'sk',
  // Slovak -> Slovakia
  sl: 'si',
  // Slovenian -> Slovenia
  es: 'es',
  // Spanish -> Spain
  'es-la': 'mx',
  // Spanish (LATAM) -> Mexico
  sv: 'se',
  // Swedish -> Sweden
  ta: 'tam',
  // Tamil -> Tamil
  te: 'tel',
  // Telugu -> Telugu
  th: 'th',
  // Thai -> Thailand
  tr: 'tr',
  // Turkish -> Turkey
  uk: 'ua',
  // Ukrainian -> Ukraine
  ur: 'pk',
  // Urdu -> Pakistan
  uz: 'uz',
  // Uzbek -> Uzbekistan
  vi: 'vn' // Vietnamese -> Vietnam
};
const baseTitleSiteLinks = {
  al: `${ANILIST_URL.origin}/manga/`,
  ap: `${ANIME_PLANET_URL.origin}/manga/`,
  kt: `${KITSU_URL.origin}/manga/`,
  mu: `${MANGAUPDATES_URL.origin}/series/`,
  mu_num: `${MANGAUPDATES_URL.origin}/series.html?id=`,
  mal: `${MYANIMELIST_URL.origin}/manga/`,
  nu: `${NOVELUPDATES_URL.origin}/series/`,
  bw: `${BOOKWALKER_URL.origin}/`,
  amz: '',
  ebj: '',
  cdj: ''
};
const mdComponentColors = {
  color: 'rgb(var(--md-color))',
  primary: 'rgb(var(--md-primary))',
  background: 'rgb(var(--md-background))',
  accent: 'rgb(var(--md-accent))',
  accent20: 'rgb(var(--md-accent-20))',
  buttonAccent: 'rgb(var(--md-button-accent))',
  statusYellow: 'rgb(var(--md-status-yellow))',
  statusRed: 'rgb(var(--md-status-red))'
};
const roleColors = {
  ROLE_ADMIN: 'rgb(155, 89, 182)',
  ROLE_DEVELOPER: 'rgb(255, 110, 233)',
  ROLE_GLOBAL_MODERATOR: 'rgb(233, 30, 99)',
  ROLE_FORUM_MODERATOR: 'rgb(233, 30, 99)',
  ROLE_PUBLIC_RELATIONS: 'rgb(230, 126, 34)',
  ROLE_DESIGNER: 'rgb(254, 110, 171)',
  ROLE_STAFF: 'rgb(233, 30, 99)',
  ROLE_VIP: 'rgb(241, 196, 15)',
  ROLE_POWER_UPLOADER: 'rgb(46, 204, 113)',
  ROLE_CONTRIBUTOR: 'rgb(32, 102, 148)',
  ROLE_GROUP_LEADER: 'rgb(52, 152, 219)',
  ROLE_SUPPORTER: 'rgb(93, 93, 180)',
  ROLE_MD_AT_HOME: 'rgb(26, 121, 57)',
  ROLE_GROUP_MEMBER: 'rgb(250, 250, 250)',
  ROLE_MEMBER: 'rgb(250, 250, 250)',
  ROLE_USER: 'rgb(250, 250, 250)',
  ROLE_UNVERIFIED: 'rgb(250, 250, 250)',
  ROLE_GUEST: 'rgb(250, 250, 250)',
  ROLE_BANNED: 'rgb(0, 0, 0)'
};
const titleId = (path = window.location.pathname) => getMatch(path, new RegExp(formatRegexText('/title/(?:edit/)?(:uuid)')), 1);
const titleIsCreate = (path = window.location.pathname) => new RegExp(formatRegexText(titleCreateRoute)).test(path);
const titleIsEdit = (path = window.location.pathname) => new RegExp(formatRegexText(titleEditRoute)).test(path);
const titleIsDraft = (href = window.location.href) => new RegExp(formatRegexText(titleDraftRoute)).test(href) || new RegExp(formatRegexText(titleEditDraftRoute)).test(href);
const listId = (path = window.location.pathname) => getMatch(path, new RegExp(formatRegexText('/list/(:uuid)')), 1);
const chapterId = (path = window.location.pathname) => getMatch(path, new RegExp(formatRegexText('/chapter/(:uuid)')), 1);
const useComponents = () => setComponentColors({
  text: mdComponentColors.color,
  primary: mdComponentColors.primary,
  secondary: mdComponentColors.buttonAccent,
  background: mdComponentColors.background,
  accent: mdComponentColors.accent,
  warning: mdComponentColors.statusYellow,
  error: mdComponentColors.statusRed
});
const getUserRoleColor = roles => {
  for (const role in roleColors) {
    if (roles.includes(role)) return roleColors[role];
  }
  return roleColors.ROLE_USER;
};
const authToken = () => parseStorage(`oidc.user:${MANGADEX_AUTH_URL.origin}/realms/mangadex:mangadex-frontend-stable`) || parseStorage(`oidc.user:${MANGADEX_AUTH_URL.origin}/realms/mangadex:mangadex-frontend-canary`) || parseStorage(`oidc.user:${MANGADEX_DEV_AUTH_URL.origin}/realms/mangadex:mangadex-frontend-sandbox`);
const storage = () => parseStorage('md');
const locale = () => storage()?.userPreferences?.interfaceLocale || storage()?.userPreferences?.locale || 'en';
const localTime = (date = Date.now()) => new Date(date).toLocaleString(locale(), {
  hour12: false
});
const langDisplayName = () => new Intl.DisplayNames([locale()], {
  type: 'language'
});
const linkIdToURL = (siteId, seriesId) => {
  if (!siteId || !seriesId) return '';
  let baseURL = baseTitleSiteLinks[siteId] || '';
  if (siteId === 'mu' && !/[A-Za-z]/.test(seriesId)) baseURL = baseTitleSiteLinks['mu_num'];
  return baseURL + seriesId;
};

class FetchClient {
  queue = [];
  processing = false;
  abortControllers = new Map();
  bucketLastRefill = Date.now();
  activeRequests = 0;
  constructor(options = {}) {
    const {
      rateLimitRequests = Infinity,
      rateLimitTime = 1000
    } = options;
    this.rateLimitRequests = rateLimitRequests;
    this.rateLimitTime = rateLimitTime;
    this.bucketTokens = rateLimitRequests;
    this.fetchFunction = options.fetchFunction || fetch;
  }
  async processQueue() {
    if (this.processing) return;
    this.processing = true;
    while (this.queue.length > 0 && this.activeRequests < this.rateLimitRequests) {
      await this.refillBucket();
      if (this.bucketTokens > 0) {
        const queueItem = this.queue.shift();
        if (queueItem) {
          this.bucketTokens--;
          this.activeRequests++;
          queueItem.request().finally(() => {
            this.activeRequests--;
            this.processQueue();
          });
        }
      } else {
        const waitTime = this.calculateWaitTime();
        await new Promise(resolve => setTimeout(resolve, waitTime));
      }
    }
    this.processing = false;
  }
  calculateWaitTime() {
    const now = Date.now();
    const timeSinceLastRefill = now - this.bucketLastRefill;
    const timeUntilNextRefill = this.rateLimitTime - timeSinceLastRefill % this.rateLimitTime;
    return Math.max(timeUntilNextRefill, 100);
  }
  async refillBucket() {
    const now = Date.now();
    const timePassed = now - this.bucketLastRefill;
    const tokensToAdd = Math.floor(timePassed / this.rateLimitTime) * this.rateLimitRequests;
    if (tokensToAdd > 0) {
      this.bucketTokens = Math.min(this.bucketTokens + tokensToAdd, this.rateLimitRequests);
      this.bucketLastRefill = now;
    }
  }
  getRetryAfterValue(headers) {
    for (const [key, value] of headers.entries()) {
      if (key.toLowerCase().endsWith('retry-after')) {
        return value;
      }
    }
    return null;
  }
  async fetch(input, init) {
    const fetchFunction = init?.fetchFunction || this.fetchFunction || fetch;
    const requestId = init?.requestId || crypto.randomUUID();
    const abortController = new AbortController();
    this.abortControllers.set(requestId, abortController);
    const _request = async () => {
      try {
        const response = await fetchFunction(input, {
          signal: abortController.signal,
          ...init
        });
        if (response.status === 429) {
          const retryAfter = this.getRetryAfterValue(response.headers);
          throw new Error(`Rate limit exceeded. Retry after: ${retryAfter} seconds`);
        }
        return response;
      } finally {
        this.abortControllers.delete(requestId);
      }
    };
    return new Promise((resolve, reject) => {
      this.queue.push({
        id: requestId,
        request: async () => {
          try {
            const response = await _request();
            resolve(response);
          } catch (error) {
            reject(error);
          }
        },
        abort: () => {
          abortController.abort();
          this.abortControllers.delete(requestId);
          reject(new DOMException('The operation was aborted.', 'AbortError'));
        }
      });
      this.processQueue();
    });
  }
  abort(requestId) {
    const index = this.queue.findIndex(item => item.id === requestId);
    if (index > -1) {
      const [queueItem] = this.queue.splice(index, 1);
      queueItem.abort();
    }
  }
  abortAll() {
    this.queue.forEach(queueItem => this.abort(queueItem.id));
  }
}

const baseUrlSetting = {
  id: 'base_url',
  type: 'text',
  name: 'Base URL',
  description: 'The base URL of the MangaDex API.',
  defaultValue: window.location.hostname.startsWith('sandbox') ? MANGADEX_DEV_API_URL.origin : MANGADEX_API_URL.origin
};
const moreInfoString = `\nMore information at ${MANGADEX_API_URL.origin}/docs/2-limitations/#general-rate-limit`;
const rateLimitRequestsSetting = {
  id: 'rate_limit_requests',
  type: 'text',
  name: 'Rate Limit Requests',
  description: 'The number of requests allowed per the time specified in the Rate Limit Time field.' + moreInfoString,
  defaultValue: '5'
};
const rateLimitTimeSetting = {
  id: 'rate_limit_time',
  type: 'text',
  name: 'Rate Limit Time (ms)',
  description: 'The time in milliseconds to wait between requests.' + moreInfoString,
  defaultValue: '1000'
};
const mangadexAPISettings = new SettingsField({
  id: '8993eca2-7906-43ff-a0b2-9716b67ea229',
  name: 'API',
  settings: [baseUrlSetting, rateLimitRequestsSetting, rateLimitTimeSetting]
});
const rateLimitRequestsValue = mangadexAPISettings.getValue(rateLimitRequestsSetting.id);
let rateLimitRequestsNumber = rateLimitRequestsValue ? parseInt(rateLimitRequestsValue) : parseInt(rateLimitRequestsSetting.defaultValue);
rateLimitRequestsNumber = Math.max(1, rateLimitRequestsNumber);
const rateLimitTimeValue = mangadexAPISettings.getValue(rateLimitTimeSetting.id);
let rateLimitTimeNumber = rateLimitTimeValue ? parseInt(rateLimitTimeValue) : parseInt(rateLimitTimeSetting.defaultValue);
rateLimitTimeNumber = Math.max(0, rateLimitTimeNumber);
const baseUrl = mangadexAPISettings.getValue(baseUrlSetting.id);
const fetchClient = new FetchClient({
  rateLimitRequests: rateLimitRequestsNumber,
  rateLimitTime: rateLimitTimeNumber
});
const contentRatings = ['safe', 'suggestive', 'erotica', 'pornographic'];
const trackingSites = ['al', 'ap', 'kt', 'mu', 'mal', 'nu'];
const retailSites = ['bw', 'amz', 'ebj', 'cdj'];
const checkResourceId = id => {
  if (!id) throw new Error('Invalid ID');
};
async function responsePromise({
  path,
  query,
  method = 'GET',
  body,
  useAuth = false,
  contentType
}) {
  return await new Promise((resolve, reject) => {
    if (query?.offset) if (query?.offset + query?.limit > 10000) reject(new Error('Collection size limit reached'));
    const headers = {};
    if (useAuth) {
      const authToken$1 = authToken();
      if (!authToken$1) reject(new Error('Not logged in'));else headers.Authorization = `${authToken$1.token_type} ${authToken$1.access_token}`;
    }
    if (contentType) headers['Content-Type'] = contentType;
    fetchClient.fetch(createUrl(baseUrl, path, query), {
      method: method,
      body: body,
      headers: headers
    }).then(response => response.json()).then(responseJson => {
      let error;
      if (responseJson.result !== 'ok') {
        if (Array.isArray(responseJson.errors)) error = JSON.stringify(responseJson.errors) || 'Unknown error';else error = 'Unknown error';
      } else if (!responseJson) {
        error = 'Response is empty';
      }
      if (error) reject(new Error(error));else resolve(responseJson);
    }).catch(reject);
  });
}
async function collectionResponsePromise({
  options,
  offset = 0,
  limit = 10000,
  collectionLimit = 100,
  callback
}) {
  const responseCollectionLimit = Math.min(collectionLimit, limit);
  let allResponses;
  let responseOffset = offset;
  let responseTotal = Math.min(10000, offset + limit);
  while (responseOffset < responseTotal) {
    const response = await responsePromise({
      ...options,
      query: {
        ...options.query,
        offset: responseOffset,
        limit: responseCollectionLimit
      }
    });
    if (!response.data.length) break;
    responseTotal = Math.min(responseTotal, response.total);
    responseOffset += responseCollectionLimit;
    if (!allResponses) {
      allResponses = {
        result: response.result,
        response: response.response,
        data: response.data,
        limit: response.limit,
        offset: response.offset,
        total: response.total
      };
    } else allResponses.data.push(...response.data);
    if (callback) callback(response);
  }
  if (!allResponses) throw new Error('All responses are empty');
  return allResponses;
}
async function getManga(id = titleId(), isDraft = titleIsDraft()) {
  checkResourceId(id);
  return await responsePromise({
    path: `/manga${isDraft ? '/draft/' : '/'}${id}`,
    useAuth: isDraft
  });
}
async function createManga(data) {
  return await responsePromise({
    path: '/manga',
    method: 'POST',
    body: JSON.stringify(data),
    useAuth: true,
    contentType: 'application/json'
  });
}
async function updateManga(data, id = titleId()) {
  checkResourceId(id);
  return await responsePromise({
    path: `/manga/${id}`,
    method: 'PUT',
    body: JSON.stringify(data),
    useAuth: true,
    contentType: 'application/json'
  });
}
async function getMangaList({
  title,
  ids = [titleId()],
  includes = [],
  contentRating = contentRatings,
  offset,
  limit,
  callback
} = {}) {
  const query = {
    'includes[]': includes,
    'contentRating[]': contentRating
  };
  if (title) query['title'] = title;
  if (ids) query['ids[]'] = ids;
  return await collectionResponsePromise({
    options: {
      path: '/manga',
      query
    },
    offset,
    limit,
    callback: callback
  });
}
async function createMangaRelation(data, id = titleId()) {
  checkResourceId(id);
  return await responsePromise({
    path: `/manga/${id}/relation`,
    method: 'POST',
    body: JSON.stringify(data),
    useAuth: true,
    contentType: 'application/json'
  });
}
async function uploadCover(data, id = titleId()) {
  checkResourceId(id);
  const formData = new FormData();
  formData.append('file', data.file);
  if (data.volume) formData.append('volume', data.volume);
  if (data.description) formData.append('description', data.description);
  if (data.locale) formData.append('locale', data.locale);
  return await responsePromise({
    path: `/cover/${id}`,
    method: 'POST',
    body: formData,
    useAuth: true
  });
}
async function getCoverList({
  mangaIds = [titleId()],
  order = {},
  includes = [],
  offset,
  limit,
  callback
} = {}) {
  const query = {
    'manga[]': mangaIds,
    'includes[]': includes
  };
  if (order?.volume) query['order[volume]'] = order.volume;
  return await collectionResponsePromise({
    options: {
      path: '/cover',
      query
    },
    offset,
    limit,
    callback: callback
  });
}
async function getChapterList({
  title,
  ids = [titleId()],
  manga,
  includes = [],
  contentRating = contentRatings,
  includeUnavailable,
  offset,
  limit,
  callback
} = {}) {
  const query = {
    'includes[]': includes,
    'contentRating[]': contentRating
  };
  if (title) query['title'] = title;
  if (ids) query['ids[]'] = ids;
  if (manga) query['manga'] = manga;
  if (includeUnavailable !== undefined) query['includeUnavailable'] = includeUnavailable ? '1' : '0';
  return await collectionResponsePromise({
    options: {
      path: '/chapter',
      query
    },
    offset,
    limit,
    callback: callback
  });
}
async function getMangaStatuses({
  status
} = {}) {
  const query = {};
  if (status) query['status'] = status;
  return await responsePromise({
    path: '/manga/status',
    query,
    useAuth: true
  });
}
async function getReadMarkers({
  mangaIds = [titleId()],
  grouped = true
} = {}) {
  const query = {
    'ids[]': mangaIds,
    grouped: grouped
  };
  return await responsePromise({
    path: '/manga/read',
    query,
    useAuth: true
  });
}
async function getCustomList({
  id = listId(),
  visibility
} = {}) {
  checkResourceId(id);
  return await responsePromise({
    path: `/list/${id}`,
    useAuth: visibility !== 'public'
  });
}
async function getMangaStatistics({
  mangaIds = [titleId()]
} = {}) {
  const query = {
    'manga[]': mangaIds
  };
  return await responsePromise({
    path: '/statistics/manga',
    query
  });
}
async function getMangaRatings({
  mangaIds = [titleId()]
} = {}) {
  const query = {
    'manga[]': mangaIds
  };
  return await responsePromise({
    path: '/rating',
    query,
    useAuth: true
  });
}
async function getChapterStatistics({
  chapterIds = [chapterId()]
} = {}) {
  const query = {
    'chapter[]': chapterIds
  };
  return await responsePromise({
    path: '/statistics/chapter',
    query
  });
}
async function getLoggedUser() {
  return await responsePromise({
    path: '/user/me',
    useAuth: true
  });
}

const mdOverlaySelector = '.md-overlay';
const textInputContainerSelector = 'div.input-container';
const textInputSelector = 'input.inline-input';
function getTextInputs(indexes) {
  return Array.from(document.querySelectorAll(textInputContainerSelector)).flatMap((div, index) => indexes && indexes.includes(index) ? Array.from(div.querySelectorAll(textInputSelector)) : []);
}
function getTextInputValues(inputs) {
  return inputs.map(input => input.value.trim()).filter(value => value);
}
function getTitleInputs() {
  return getTextInputs([0, 1]);
}
function getTitleInputValues() {
  return getTextInputValues(getTitleInputs());
}
function getLinkInputs() {
  return getTextInputs([3, 4, 5]);
}
function getLinkInputValues() {
  return getTextInputValues(getLinkInputs());
}
const newCoverFileInputSelector = 'input[type="file"]#file';
const coverPageSelector = 'div.page-sizer > .page';
const coverVolumeInputSelector = '.volume-num input';
const coverLocaleImageSelector = 'img[src^="/img/flags/"]';
const coverSelectedLocaleImageSelector = 'img.volume-flag';
const coverCloseSelector = '.close';
const coverDescriptionEditButtonSelector = 'button.volume-edit';
const coverDescriptionInputSelector = '.md-modal__box textarea.md-textarea__input';
const coverDescriptionSaveButtonSelector = 'button.primary';
function getCovers(imageUrlFilter) {
  let filtered = Array.from(document.querySelectorAll(coverPageSelector));
  if (imageUrlFilter) {
    filtered = filtered.filter(element => {
      const backgroundImageUrl = element.style.getPropertyValue('background-image').replace(/url\("|"\)$/g, '').trim();
      return backgroundImageUrl.match(imageUrlFilter);
    });
  }
  return filtered.map(element => element.parentElement?.parentElement).filter(element => !!element);
}
async function addCover(file) {
  const inputElement = document.querySelector(newCoverFileInputSelector);
  if (!inputElement) throw new Error('No input element found!');
  const existingCoverElements = getCovers();
  const dt = new DataTransfer();
  dt.items.add(file);
  inputElement.files = dt.files;
  inputElement.dispatchEvent(new Event('change', {
    bubbles: true
  }));
  await sleep(200);
  const newCoverElements = getCovers();
  const newCoverElement = newCoverElements.find(element => !existingCoverElements.includes(element));
  if (!newCoverElement) throw new Error('No new cover element found!');
  return newCoverElement;
}
async function deleteCover(coverElement) {
  const closeElement = coverElement.querySelector(coverCloseSelector);
  if (!closeElement) return false;
  closeElement.click();
  await sleep(2);
  return !document.contains(coverElement);
}
async function setCoverVolume(coverElement, volume, overwrite = false) {
  const inputElement = coverElement.querySelector(coverVolumeInputSelector);
  if (!inputElement) return false;
  let valueChanged = false;
  if (inputElement.value !== volume || overwrite) {
    if (!inputElement.value || overwrite) {
      inputElement.value = volume;
      inputElement.dispatchEvent(new InputEvent('input'));
    }
    if (inputElement.value === volume) valueChanged = true;
  }
  return valueChanged;
}
async function setCoverLocale(coverElement, locale) {
  const flagCode = langToFlagMap[locale] || locale;
  const countryFlagElements = Array.from(coverElement.querySelectorAll(coverLocaleImageSelector));
  const countryFlagOptionElement = countryFlagElements.find(flagImage => flagImage.src.includes(flagCode))?.parentElement;
  if (!countryFlagOptionElement) return false;
  countryFlagOptionElement.click();
  await sleep(2);
  const overlayElement = document.querySelector(mdOverlaySelector);
  overlayElement?.click();
  const selectedCountryFlagElement = coverElement.querySelector(coverSelectedLocaleImageSelector);
  if (!selectedCountryFlagElement) return false;
  return selectedCountryFlagElement.src.includes(flagCode);
}
async function setCoverDescription(coverElement, description, overwrite = false) {
  const volumeEditButton = coverElement.querySelector(coverDescriptionEditButtonSelector);
  if (!volumeEditButton) return false;
  volumeEditButton.click();
  const coverDescriptionInputElement = await waitForElement(coverDescriptionInputSelector);
  if (!coverDescriptionInputElement) return false;
  let valueChanged = false;
  if (coverDescriptionInputElement.value !== description || overwrite) {
    if (!coverDescriptionInputElement.value || overwrite) {
      coverDescriptionInputElement.value = description;
      coverDescriptionInputElement.dispatchEvent(new InputEvent('input'));
    }
    if (coverDescriptionInputElement.value === description) valueChanged = true;
  }
  await sleep(2);
  const saveButtonElement = coverDescriptionInputElement?.parentElement?.parentElement?.parentElement?.parentElement?.querySelector(coverDescriptionSaveButtonSelector);
  saveButtonElement?.click();
  await waitForElement(coverDescriptionInputSelector, true);
  return valueChanged;
}

const defaultDescriptionId = 'default_description';
const mangadexAddCoverDescriptionsSettings = new SettingsField({
  id: 'e99c3210-1c08-4756-b4f0-565e329569e3',
  name: 'Cover Descriptions',
  settings: [{
    id: defaultDescriptionId,
    type: 'textarea',
    name: 'Default Description',
    defaultValue: 'Volume %VOLUME% cover from BookLive'
  }]
});
class MangadexAddCoverDescriptions extends MangadexBookmarklet {
  routes = [...titleEditRoutes, titleCreateRoute];
  main = async () => {
    useComponents();
    const defaultDescription = await promptAreaModal('Enter a description', mangadexAddCoverDescriptionsSettings.getValue(defaultDescriptionId));
    if (!defaultDescription) return;
    const changedDescriptions = [];
    const newCoverElements = getCovers(/^blob:/);
    for (const newCoverElement of newCoverElements) {
      const volumeElement = newCoverElement.querySelector(coverVolumeInputSelector);
      const languageElement = newCoverElement.querySelector(coverSelectedLocaleImageSelector);
      const volumeStringVariable = formatStringVariable('VOLUME');
      const languageStringVariable = formatStringVariable('LANGUAGE');
      const volume = volumeElement?.value.trim() || 'No Volume';
      const language = languageElement?.title.trim() || 'No Language';
      const changed = await setCoverDescription(newCoverElement, replaceStringVariable(defaultDescription, [[volumeStringVariable, volume], [languageStringVariable, language]]), false);
      if (changed) changedDescriptions.push(newCoverElement);
    }
    if (changedDescriptions.length <= 0) return alertModal('No newly added covers with empty descriptions found!');
    console.log('Added descriptions:', changedDescriptions);
  };
}

const mangadexSearchMissingLinksSettings = new SettingsField({
  id: '6d4e9694-c62a-498b-bf8c-aa4c61ea6155',
  name: 'Missing Links to Search',
  settings: [{
    id: 'al_enabled',
    type: 'checkbox',
    name: 'Anilist',
    defaultValue: true
  }, {
    id: 'ap_enabled',
    type: 'checkbox',
    name: 'Anime-Planet',
    defaultValue: true
  }, {
    id: 'kt_enabled',
    type: 'checkbox',
    name: 'Kitsu',
    defaultValue: true
  }, {
    id: 'mu_enabled',
    type: 'checkbox',
    name: 'MangaUpdates',
    defaultValue: true
  }, {
    id: 'mal_enabled',
    type: 'checkbox',
    name: 'MyAnimeList',
    defaultValue: true
  }, {
    id: 'nu_enabled',
    type: 'checkbox',
    name: 'NovelUpdates',
    defaultValue: true
  }, {
    id: 'bw_enabled',
    type: 'checkbox',
    name: 'BookWalker Japan',
    defaultValue: true
  }, {
    id: 'amz_enabled',
    type: 'checkbox',
    name: 'Amazon Japan',
    defaultValue: true
  }, {
    id: 'ebj_enabled',
    type: 'checkbox',
    name: 'eBookJapan',
    defaultValue: true
  }, {
    id: 'cdj_enabled',
    type: 'checkbox',
    name: 'CDJapan',
    defaultValue: true
  }]
});
class MangadexSearchMissingLinks extends MangadexBookmarklet {
  routes = [titleRoute, ...titleEditRoutes, titleCreateRoute];
  main = async () => {
    useComponents();
    const websites = {
      al: `${ANILIST_URL.origin}/search/manga?search=`,
      ap: `${ANIME_PLANET_URL.origin}/manga/all?name=`,
      kt: `${KITSU_URL.origin}/manga?subtype=manga&text=`,
      mu: `${MANGAUPDATES_URL.origin}/search.html?search=`,
      mal: `${MYANIMELIST_URL.origin}/manga.php?q=`,
      nu: `${NOVELUPDATES_URL.origin}/series-finder/?sf=1&sh=`,
      bw: `${BOOKWALKER_URL.origin}/search/?qcat=2&word=`,
      amz: `${AMAZON_JAPAN_URL.origin}/s?rh=n:466280&k=`,
      ebj: `${EBOOKJAPAN_URL.origin}/search/?keyword=`,
      cdj: `${CDJAPAN_URL.origin}/searchuni?term.media_format=BOOK&q=`
    };
    for (const website of Object.keys(websites)) {
      if (!mangadexSearchMissingLinksSettings.getValue(website + '_enabled')) delete websites[website];
    }
    const modalText = 'Search Options';
    const modalOptions = {
      title: {
        label: 'Enter a title to search for',
        type: 'text',
        defaultValue: undefined,
        options: []
      },
      searchTracking: {
        label: 'Search tracking sites',
        type: 'checkbox',
        defaultValue: true
      },
      searchRetail: {
        label: 'Search retail sites',
        type: 'checkbox',
        defaultValue: true
      }
    };
    const openSearchWindows = (sitesToOpen, options) => sitesToOpen.forEach(website => {
      if (trackingSites.includes(website) && options.searchTracking || retailSites.includes(website) && options.searchRetail) {
        openNewTab(websites[website] + options.title);
      }
    });
    for (const website of Object.keys(websites)) {
      if (!mangadexSearchMissingLinksSettings.getValue(website + '_enabled')) delete websites[website];
    }
    if (titleIsCreate()) {
      modalOptions.title.options = getTitleInputValues();
      const options = await multiOptionModal(modalOptions, modalText);
      if (!options || !options.title) return;
      openSearchWindows(Object.keys(websites), options);
      return;
    }
    getManga().then(async titleInfo => {
      if (!titleInfo.data.attributes.tags.some(tag => tag.attributes.name.en === 'Adaptation')) delete websites.nu;
      const missingWebsites = Object.keys(websites).filter(website => titleInfo.data.attributes.links && !titleInfo.data.attributes.links[website]);
      if (missingWebsites.length <= 0) return alertModal('All links are already added!');
      const originalLang = titleInfo.data.attributes.originalLanguage;
      let originalTitle = undefined;
      const altTitles = Array.isArray(titleInfo.data.attributes.altTitles) ? titleInfo.data.attributes.altTitles : undefined;
      if (altTitles) originalTitle = altTitles.find(title => title[originalLang]);else console.debug('No alt titles found');
      const mainTitleLang = Object.keys(titleInfo.data.attributes.title)[0];
      modalOptions.title.defaultValue = originalTitle ? originalTitle[originalLang] : titleInfo.data.attributes.title[mainTitleLang] || '';
      modalOptions.title.options = [titleInfo.data.attributes.title[mainTitleLang], ...(altTitles?.map(_title => _title[Object.keys(_title)[0]]) || [])].filter(_title => _title !== modalOptions.title.defaultValue);
      const options = await multiOptionModal(modalOptions, modalText);
      if (!options || !options.title) return;
      openSearchWindows(missingWebsites, options);
    });
  };
}

const alEnabledId = 'al_enabled';
const apEnabledId = 'ap_enabled';
const ktEnabledId = 'kt_enabled';
const ktIdEnabledId = 'kt_id_enabled';
const muEnabledId = 'mu_enabled';
const malEnabledId = 'mal_enabled';
const nuEnabledId = 'nu_enabled';
const bwEnabledId = 'bw_enabled';
const amzEnabledId = 'amz_enabled';
const ebjEnabledId = 'ebj_enabled';
const cdjEnabledId = 'cdj_enabled';
const genericCleanupEnabledId = 'generic_cleanup_enabled';
const mangadexShortenLinksSettings = new SettingsField({
  id: '1686bebe-5013-427e-b40f-1216677e2626',
  name: 'Links to Shorten',
  settings: [{
    id: alEnabledId,
    type: 'checkbox',
    name: 'Anilist',
    defaultValue: true
  }, {
    id: apEnabledId,
    type: 'checkbox',
    name: 'Anime-Planet',
    defaultValue: true
  }, {
    id: ktEnabledId,
    type: 'checkbox',
    name: 'Kitsu',
    defaultValue: true
  }, {
    id: ktIdEnabledId,
    type: 'checkbox',
    name: 'Kitsu Slug to ID',
    description: 'Replace the slug with the ID if possible. Requires Kitsu to be enabled.',
    defaultValue: true
  }, {
    id: muEnabledId,
    type: 'checkbox',
    name: 'MangaUpdates',
    defaultValue: true
  }, {
    id: malEnabledId,
    type: 'checkbox',
    name: 'MyAnimeList',
    defaultValue: true
  }, {
    id: nuEnabledId,
    type: 'checkbox',
    name: 'NovelUpdates',
    defaultValue: true
  }, {
    id: bwEnabledId,
    type: 'checkbox',
    name: 'BookWalker Japan',
    defaultValue: true
  }, {
    id: amzEnabledId,
    type: 'checkbox',
    name: 'Amazon Japan',
    defaultValue: true
  }, {
    id: ebjEnabledId,
    type: 'checkbox',
    name: 'eBookJapan',
    defaultValue: true
  }, {
    id: cdjEnabledId,
    type: 'checkbox',
    name: 'CDJapan',
    defaultValue: true
  }, {
    id: genericCleanupEnabledId,
    type: 'checkbox',
    name: 'Generic URL Cleanup',
    description: 'Remove unnecessary parts of any URL.',
    defaultValue: false
  }]
});
class MangadexShortenLinks extends MangadexBookmarklet {
  routes = [...titleEditRoutes, titleCreateRoute];
  main = async () => {
    useComponents();
    const inputs = getLinkInputs();
    const changedLinks = {};
    const progressBar = new SimpleProgressBar(inputs.length);
    const numIdRegex = '[0-9]+';
    const numAndLetterIdRegex = '[A-Za-z0-9-%]+';
    const asinRegex = '[A-Z0-9]{10}';
    const regexPatterns = [{
      regex: `(${ANILIST_URL.hostname}/manga/)(${numIdRegex})`,
      settingId: alEnabledId
    }, {
      regex: `(${ANIME_PLANET_URL.hostname}/manga/)(${numAndLetterIdRegex})`,
      settingId: apEnabledId
    }, {
      regex: `(kitsu.(?:io|app)/manga/)(${numAndLetterIdRegex})`,
      settingId: ktEnabledId
    }, {
      regex: `(${MANGAUPDATES_URL.hostname}/series/)(${numAndLetterIdRegex})`,
      settingId: muEnabledId
    }, {
      regex: `(${MYANIMELIST_URL.hostname}/manga/)(${numIdRegex})`,
      settingId: malEnabledId
    }, {
      regex: `(${NOVELUPDATES_URL.hostname}/series/)(${numAndLetterIdRegex})`,
      settingId: nuEnabledId
    }, {
      regex: `(${BOOKWALKER_URL.hostname}/series/)(${numIdRegex}(?:/list)?)`,
      settingId: bwEnabledId
    }, {
      regex: `(${BOOKWALKER_URL.hostname}/)(${numAndLetterIdRegex})`,
      settingId: bwEnabledId
    }, {
      regex: `(www.amazon[a-z.]+/).*((?:dp/|gp/product/|kindle-dbs/product/)${asinRegex})`,
      settingId: amzEnabledId
    }, {
      regex: `(www.amazon[a-z.]+/gp/product).*(/${asinRegex})`,
      settingId: amzEnabledId
    }, {
      regex: `(${EBOOKJAPAN_URL.hostname}/books/)(${numIdRegex})`,
      settingId: ebjEnabledId
    }, {
      regex: `(${CDJAPAN_URL.hostname}/product/)(NEOBK-${numIdRegex})`,
      settingId: cdjEnabledId
    }, {
      regex: '(.*/)(.*)/$',
      settingId: genericCleanupEnabledId
    }];
    progressBar.start();
    await Promise.all(inputs.map(async element => {
      const link = element.value.trim();
      let shortLink = link;
      for (const pattern of regexPatterns) {
        if (pattern.settingId && !mangadexShortenLinksSettings.getValue(pattern.settingId)) {
          continue;
        }
        const regex = new RegExp(`(?:https?://${pattern.regex}.*)$`);
        let websiteUrl = getMatch(link, regex, 1);
        let id = getMatch(link, regex, 2);
        if (websiteUrl && id) {
          if (/^kitsu.(io|app)\/manga\/$/.test(websiteUrl)) {
            websiteUrl = websiteUrl.replace('kitsu.io', KITSU_URL.hostname);
            if (!new RegExp(`^${numIdRegex}$`).test(id) && mangadexShortenLinksSettings.getValue(ktIdEnabledId)) {
              try {
                const slugResponse = await fetch(`${KITSU_URL.origin}/api/edge/manga?filter[slug]=${id}`);
                const {
                  data
                } = await slugResponse.json();
                id = data[0].id;
              } catch (error) {
                console.warn('Failed to find kitsu id:', error);
              }
            }
          }
          shortLink = `https://${websiteUrl}${id}`;
          break;
        }
      }
      if (shortLink !== link) {
        element.value = shortLink;
        element.dispatchEvent(new InputEvent('input'));
        changedLinks[link] = shortLink;
      }
      progressBar.update();
    }));
    progressBar.remove();
    if (Object.keys(changedLinks).length <= 0) return alertModal('No links changed!');
    console.log('Changed links:', changedLinks);
  };
}

class AmazonBookmarklet extends Bookmarklet {
  website = '^www.amazon[a-z.]+$';
}

class BookliveBookmarklet extends Bookmarklet {
  website = `^${BOOKLIVE_URL.hostname}$`;
}

class BookwalkerBookmarklet extends Bookmarklet {
  website = `^(${BOOKWALKER_URL.hostname}|${BOOKWALKER_R18_URL.hostname}|${BOOKWALKER_GLOBAL_URL.hostname}|${BOOKWALKER_VIEWER_TRIAL_URL.hostname})$`;
}

class UniversalSettings extends UniversalBookmarklet {
  additionalFields = [];
  main = () => {
    const fields = [];
    if (new MangadexBookmarklet().isWebsite()) {
      useComponents();
      fields.push(mangadexAPISettings, mangadexAddCoverDescriptionsSettings, mangadexShortenLinksSettings, mangadexSearchMissingLinksSettings);
    } else if (new AmazonBookmarklet().isWebsite()) {
      fields.push(coverDownloaderSettings);
    } else if (new BookliveBookmarklet().isWebsite()) {
      fields.push(coverDownloaderSettings);
    } else if (new BookwalkerBookmarklet().isWebsite()) {
      fields.push(coverDownloaderSettings);
    }
    new Settings([...fields, ...this.additionalFields]).add();
  };
}

class MangadexExportTitleList extends MangadexBookmarklet {
  main = async () => {
    useComponents();
    const errors = [];
    const onError = e => {
      console.error(e);
      errors.push(e);
    };
    const listId$1 = listId();
    let mangaList = [];
    const exportFormatOptions = {
      xml: 'MyAnimeList XML',
      csv: 'CSV',
      json: 'JSON'
    };
    const exportFormat = await selectModal('Export format', Object.values(exportFormatOptions));
    if (!exportFormat) return;
    const csvDataColumns = {
      title: 'Title',
      originalTitle: 'Original Title',
      originalLanguage: 'Original Language',
      author: 'Authors',
      year: 'Publication Year',
      publication: 'Publication Status',
      contentRating: 'Content Rating',
      demographic: 'Demographic',
      tags: 'Tags',
      description: 'Description',
      mangaId: 'Manga ID',
      mangaThread: 'Manga Forum Thread ID',
      myRating: 'My Rating',
      readingStatus: listId$1 ? 'List Name' : 'My Reading Status',
      isOneshot: 'Is Oneshot',
      lastVolume: 'Last Published Volume',
      lastChapter: 'Last Published Chapter',
      readVolume: 'Latest Read Volume',
      readChapter: 'Latest Read Chapter',
      readChapterScans: 'Latest Read Chapter Scanlation Groups',
      readChapterId: 'Latest Read Chapter ID',
      readChapterThread: 'Latest Read Chapter Forum Thread ID',
      anilist: 'Anilist',
      animePlanet: 'Anime Planet',
      kitsu: 'Kitsu',
      mangaUpdates: 'MangaUpdates',
      myAnimeList: 'MyAnimeList',
      novelUpdates: 'NovelUpdates',
      bookWalker: 'BookWalker',
      amazon: 'Amazon',
      ebookJapan: 'Ebook Japan',
      cdJapan: 'CD Japan',
      officialRaw: 'Official Raw',
      officialEnglish: 'Official English'
    };
    const allIncludeDataOptions = {
      ...csvDataColumns,
      askForPreferredLang: 'Ask for Preferred Language',
      includeUnavailableChapters: 'Include Unavailable Chapters',
      updateOnImport: 'Update on Import',
      excludeNoMal: 'Exclude Titles with no MyAnimeList ID'
    };
    const includeDataOptions = [allIncludeDataOptions.askForPreferredLang, allIncludeDataOptions.includeUnavailableChapters];
    const defaultIncludeDataOptions = [allIncludeDataOptions.askForPreferredLang, allIncludeDataOptions.includeUnavailableChapters];
    switch (exportFormat) {
      case exportFormatOptions.xml:
        {
          includeDataOptions.push(allIncludeDataOptions.updateOnImport, allIncludeDataOptions.excludeNoMal, allIncludeDataOptions.myRating, allIncludeDataOptions.readingStatus, allIncludeDataOptions.lastVolume, allIncludeDataOptions.lastChapter, allIncludeDataOptions.readVolume, allIncludeDataOptions.readChapter, allIncludeDataOptions.readChapterScans);
          defaultIncludeDataOptions.push(allIncludeDataOptions.updateOnImport, allIncludeDataOptions.excludeNoMal, allIncludeDataOptions.myRating, allIncludeDataOptions.readingStatus, allIncludeDataOptions.lastVolume, allIncludeDataOptions.lastChapter, allIncludeDataOptions.readVolume, allIncludeDataOptions.readChapter);
          break;
        }
      case exportFormatOptions.csv:
        {
          includeDataOptions.push(...Object.values(csvDataColumns));
          defaultIncludeDataOptions.push(allIncludeDataOptions.title, allIncludeDataOptions.originalTitle, allIncludeDataOptions.originalLanguage, allIncludeDataOptions.year, allIncludeDataOptions.readingStatus, allIncludeDataOptions.readVolume, allIncludeDataOptions.readChapter);
          break;
        }
    }
    const dataToInclude = await checkboxModal('Options', includeDataOptions, defaultIncludeDataOptions);
    if (dataToInclude === null || dataToInclude === undefined) return;
    switch (exportFormat) {
      case exportFormatOptions.xml:
        {
          dataToInclude.push(...[allIncludeDataOptions.title, allIncludeDataOptions.isOneshot, allIncludeDataOptions.myAnimeList]);
          break;
        }
      case exportFormatOptions.json:
        {
          dataToInclude.push(...Object.values(csvDataColumns));
          break;
        }
    }
    const progressBar = new SimpleProgressBar();
    progressBar.start({
      maxValue: 1
    });
    if (listId$1) {
      mangaList = await getCustomList({
        id: listId$1
      }).then(response => response.data.relationships.filter(rel => rel.type === 'manga').map(rel => ({
        id: rel.id,
        listName: response.data.attributes.name
      }))).catch(onError);
    } else {
      mangaList = await getMangaStatuses().then(response => Object.entries(response.statuses).map(([id, status]) => ({
        id,
        status
      }))).catch(onError);
    }
    const mangaIds = mangaList?.map(status => status.id);
    progressBar.update();
    if (!mangaIds?.length) return alertModal('This list seems empty!', 'error');
    const splitMangaIds = splitArray(mangaIds, 100);
    progressBar.start({
      maxValue: splitMangaIds.length
    });
    const splitMangaData = await Promise.all(splitMangaIds.flatMap(async ids => {
      const data = await getMangaList({
        ids: ids,
        includes: dataToInclude.includes(allIncludeDataOptions.author) ? ['artist', 'author'] : undefined
      }).then(response => response.data).catch(onError);
      progressBar.update();
      return data;
    }));
    progressBar.remove();
    if (!splitMangaData) return alertModal('Failed to fetch manga data!', 'error');
    const mangaData = splitMangaData.flat().filter(m => !!m);
    let preferredLang;
    if (dataToInclude.includes(allIncludeDataOptions.askForPreferredLang)) {
      const allTitleLangs = dataToInclude.includes(allIncludeDataOptions.title) ? mangaData.filter(m => Array.isArray(m.attributes.altTitles)).flatMap(m => m.attributes.altTitles.flatMap(altTitle => Object.keys(altTitle))) : [];
      const allDescriptionLangs = dataToInclude.includes(allIncludeDataOptions.description) ? mangaData.flatMap(m => Object.keys(m.attributes.description)) : [];
      const allLangs = {};
      [...allTitleLangs, ...allDescriptionLangs].forEach(lang => {
        let displayName = lang;
        try {
          displayName = langDisplayName().of(lang) || lang;
        } catch (e) {
          console.warn(e);
        }
        if (!allLangs[displayName]) allLangs[displayName] = lang;
      });
      if (Object.keys(allLangs).length > 0) {
        const siteLocaleName = langDisplayName().of(locale());
        const preferredLangName = await selectModal('Preferred language', Object.keys(allLangs).sort((a, b) => a === siteLocaleName ? -1 : b === siteLocaleName ? 1 : a.localeCompare(b)));
        if (preferredLangName) preferredLang = allLangs[preferredLangName];
      }
    }
    const mangaDataSplitIds = splitArray(mangaData.map(m => m.id), 100);
    progressBar.start({
      maxValue: mangaDataSplitIds.length
    });
    let mangaStatistics = {};
    if (dataToInclude.includes(allIncludeDataOptions.mangaThread)) {
      const splitMangaStatistics = await Promise.all(mangaDataSplitIds.map(async ids => {
        const data = await getMangaStatistics({
          mangaIds: ids
        }).then(response => response.statistics).catch(onError);
        progressBar.update();
        return data;
      }));
      splitMangaStatistics.forEach(data => {
        mangaStatistics = {
          ...mangaStatistics,
          ...data
        };
      });
    }
    progressBar.remove();
    progressBar.start();
    let mangaRatings = {};
    if (dataToInclude.includes(allIncludeDataOptions.myRating)) {
      const splitMangaRatings = await Promise.all(mangaDataSplitIds.map(async ids => {
        const data = await getMangaRatings({
          mangaIds: ids
        }).then(response => response.ratings).catch(onError);
        progressBar.update();
        return data;
      }));
      splitMangaRatings.forEach(data => {
        mangaRatings = {
          ...mangaRatings,
          ...data
        };
      });
    }
    progressBar.remove();
    progressBar.start();
    let readChapterMarkers = {};
    let chapterStatistics = {};
    if (dataToInclude.includes(allIncludeDataOptions.readChapter) || dataToInclude.includes(allIncludeDataOptions.readVolume)) {
      const splitReadChapterMarkers = await Promise.all(mangaDataSplitIds.map(async ids => {
        const data = await getReadMarkers({
          mangaIds: ids,
          grouped: true
        }).then(response => response.data).catch(onError);
        progressBar.update();
        return data;
      }));
      progressBar.remove();
      splitReadChapterMarkers.forEach(data => {
        readChapterMarkers = {
          ...readChapterMarkers,
          ...data
        };
      });
    }
    const readChapterIds = Object.values(readChapterMarkers).flat();
    const splitReadChapterIds = splitArray(readChapterIds, 100);
    progressBar.start({
      maxValue: splitReadChapterIds.length
    });
    let chapterData = [];
    if (readChapterIds.length > 0) {
      const splitReadChapterData = await Promise.all(splitReadChapterIds.flatMap(async ids => {
        const data = await getChapterList({
          ids,
          includes: dataToInclude.includes(allIncludeDataOptions.readChapterScans) ? ['scanlation_group'] : undefined,
          includeUnavailable: dataToInclude.includes(allIncludeDataOptions.includeUnavailableChapters)
        }).then(response => response.data).catch(onError);
        progressBar.update();
        return data;
      }));
      progressBar.remove();
      chapterData = splitReadChapterData.flat().filter(c => !!c);
      const splitChapterDataIds = splitArray(chapterData.map(c => c.id), 100);
      if (dataToInclude.includes(allIncludeDataOptions.readChapterThread)) {
        progressBar.start({
          maxValue: splitChapterDataIds.length
        });
        const splitChapterStatistics = await Promise.all(splitChapterDataIds.map(async ids => {
          const data = await getChapterStatistics({
            chapterIds: ids
          }).then(response => response.statistics).catch(onError);
          progressBar.update();
          return data;
        }));
        splitChapterStatistics.forEach(data => {
          chapterStatistics = {
            ...chapterStatistics,
            ...data
          };
        });
      }
    }
    progressBar.remove();
    const mergedData = mangaData.map(manga => {
      const mainTitle = manga.attributes.title[Object.keys(manga.attributes.title)[0]];
      const altTitles = Array.isArray(manga.attributes.altTitles) ? manga.attributes.altTitles : undefined;
      const preferredTitle = preferredLang ? altTitles?.find(t => t[preferredLang])?.[preferredLang] || mainTitle : mainTitle;
      const fallbackDescription = manga.attributes.description.en || manga.attributes.description[manga.attributes.originalLanguage] || manga.attributes.description[Object.keys(manga.attributes.description)[0]];
      const preferredDescription = preferredLang ? manga.attributes.description[preferredLang] || fallbackDescription : fallbackDescription;
      const originalTitle = altTitles?.find(t => t[manga.attributes.originalLanguage])?.[manga.attributes.originalLanguage];
      const readChapters = chapterData.map(c => {
        if (readChapterMarkers[manga.id]?.includes(c.id)) return {
          ...c,
          ...chapterStatistics[c.id]
        };
      }).filter(c => !!c).sort((a, b) => {
        const aDigits = a.attributes.chapter?.split('.').map(d => parseInt(d));
        const bDigits = b.attributes.chapter?.split('.').map(d => parseInt(d));
        if (aDigits && bDigits) {
          const aNum = aDigits.reduce((acc, cur) => acc * 10 + cur, 0);
          const bNum = bDigits.reduce((acc, cur) => acc * 10 + cur, 0);
          return aNum - bNum;
        }
        return 0;
      });
      const list = mangaList?.find(s => s.id === manga.id);
      const allAuthorNames = manga.relationships.filter(rel => ['author', 'artist'].includes(rel.type) && rel.attributes?.name).filter((rel, i, arr) => arr.findIndex(r => r.id === rel.id) === i).map(a => a.attributes?.name).filter(a => !!a).join(', ');
      const allTags = manga.attributes.tags.map(t => t.attributes.name.en).join(', ');
      const latestReadChapter = readChapters[readChapters.length - 1];
      const scanlationGroups = latestReadChapter?.relationships.map(rel => {
        if (rel.type === 'scanlation_group') return rel.attributes?.name;
      }).filter(s => !!s).join(', ');
      const isOneshot = manga.attributes.tags.some(t => t.id === '0234a31e-a729-4e28-9d6a-3f87c4966b9e');
      const lastVolume = manga.attributes.lastVolume;
      const lastChapter = isOneshot ? manga.attributes.lastChapter || '0' : manga.attributes.lastChapter;
      return {
        ...manga,
        preferredTitle,
        preferredDescription,
        originalTitle,
        allAuthorNames,
        allTags,
        isOneshot,
        lastVolume,
        lastChapter,
        mangaThreadId: mangaStatistics[manga.id]?.comments?.threadId,
        listName: list?.listName,
        readingStatus: list?.status,
        readChapters,
        latestReadVolume: latestReadChapter?.attributes.volume,
        latestReadChapter: latestReadChapter?.attributes.chapter === null ? '0' : latestReadChapter?.attributes.chapter,
        latestReadChapterScans: scanlationGroups,
        latestReadChapterId: latestReadChapter?.id,
        latestReadChapterThreadId: readChapters[readChapters.length - 1]?.comments?.threadId,
        myRating: mangaRatings[manga.id]?.rating
      };
    }).sort((a, b) => a.preferredTitle.localeCompare(b.preferredTitle));
    const filename = `MangaDex ${mangaList?.[0]?.listName || (listId$1 ? 'List' : 'Library')} ${localTime().replaceAll(/[:/]/g, '-')}`;
    switch (exportFormat) {
      case exportFormatOptions.xml:
        {
          const malStatuses = {
            reading: 'Reading',
            completed: 'Completed',
            onHold: 'On Hold',
            dropped: 'Dropped',
            planToRead: 'Plan to Read'
          };
          const statusToMal = status => {
            let malStatus = malStatuses.reading;
            if (status) {
              switch (status.toLowerCase()) {
                case 'reading':
                case 're_reading':
                  malStatus = malStatuses.reading;
                  break;
                case 'completed':
                  malStatus = malStatuses.completed;
                  break;
                case 'on_hold':
                  malStatus = malStatuses.onHold;
                  break;
                case 'dropped':
                  malStatus = malStatuses.dropped;
                  break;
                case 'plan_to_read':
                  malStatus = malStatuses.planToRead;
                  break;
              }
            }
            return malStatus;
          };
          const excludedMangaIds = [];
          const mergedMalData = mergedData.filter(d => {
            if (dataToInclude.includes(allIncludeDataOptions.excludeNoMal) && !d.attributes.links?.mal) {
              excludedMangaIds.push(d.id);
              return false;
            }
            return true;
          }).map(d => ({
            ...d,
            readingStatus: statusToMal(d.readingStatus)
          }));
          let xmlContent = '<?xml version="1.0" encoding="UTF-8"?>\n';
          let xmlMyAnimeListSection = '';
          const user = await getLoggedUser().catch(onError);
          const allMalStatuses = mergedMalData.map(d => d.readingStatus);
          let xmlMyInfoSection = '';
          xmlMyInfoSection += formatXMLTag('user_id', '', 2);
          xmlMyInfoSection += formatXMLTag('user_name', user?.data.attributes?.username || 'MangaDex User', 2);
          xmlMyInfoSection += formatXMLTag('user_export_type', '2', 2);
          xmlMyInfoSection += formatXMLTag('user_total_manga', mergedMalData.length.toString(), 2);
          xmlMyInfoSection += formatXMLTag('user_total_reading', allMalStatuses.filter(s => s === malStatuses.reading).length.toString(), 2);
          xmlMyInfoSection += formatXMLTag('user_total_completed', allMalStatuses.filter(s => s === malStatuses.completed).length.toString(), 2);
          xmlMyInfoSection += formatXMLTag('user_total_onhold', allMalStatuses.filter(s => s === malStatuses.onHold).length.toString(), 2);
          xmlMyInfoSection += formatXMLTag('user_total_dropped', allMalStatuses.filter(s => s === malStatuses.dropped).length.toString(), 2);
          xmlMyInfoSection += formatXMLTag('user_total_plantoread', allMalStatuses.filter(s => s === malStatuses.planToRead).length.toString(), 2);
          xmlMyAnimeListSection += formatXMLTag('myinfo', '\n' + xmlMyInfoSection + '\t', 1) + '\n';
          const formatChapterNumber = chapter => {
            if (!chapter) return '0';
            const chapterNum = parseInt(chapter.split('.')[0]);
            return chapterNum ? chapterNum.toString() : '0';
          };
          const reReadingMangaIds = mergedData.filter(d => d.readingStatus === 're_reading').map(d => d.id);
          mergedMalData.forEach(manga => {
            const mangaVolumes = dataToInclude.includes(allIncludeDataOptions.lastVolume) ? formatChapterNumber(manga.lastVolume) : '0';
            const mangaChapters = dataToInclude.includes(allIncludeDataOptions.lastChapter) ? manga.lastChapter === '0' && manga.isOneshot ? '1' : formatChapterNumber(manga.lastChapter) : '0';
            const myReadVolumes = dataToInclude.includes(allIncludeDataOptions.readVolume) ? formatChapterNumber(manga.latestReadVolume) : '0';
            const myReadChapters = dataToInclude.includes(allIncludeDataOptions.readChapter) ? manga.latestReadChapter === '0' && manga.isOneshot ? '1' : formatChapterNumber(manga.latestReadChapter) : '0';
            const myScanalationGroup = dataToInclude.includes(allIncludeDataOptions.readChapterScans) ? manga.latestReadChapterScans : '';
            const myScore = dataToInclude.includes(allIncludeDataOptions.myRating) ? manga.myRating?.toString() : '0';
            const myStatus = dataToInclude.includes(allIncludeDataOptions.readingStatus) ? manga.readingStatus : '';
            const myTimesRead = (myStatus === malStatuses.completed || reReadingMangaIds.includes(manga.id) ? '1' : null) || (myReadChapters !== '0' && mangaChapters !== '0' ? parseInt(myReadChapters) >= parseInt(mangaChapters) ? '1' : '0' : '0');
            const updateOnImport = dataToInclude.includes(allIncludeDataOptions.updateOnImport) ? '1' : '0';
            let xmlMangaSection = '';
            xmlMangaSection += formatXMLTag('manga_mangadb_id', manga.attributes.links?.mal || '', 2);
            xmlMangaSection += formatXMLTag('manga_title', `<![CDATA[${manga.preferredTitle || ''}]]>`, 2);
            xmlMangaSection += formatXMLTag('manga_volumes', mangaVolumes || '0', 2);
            xmlMangaSection += formatXMLTag('manga_chapters', mangaChapters || '0', 2);
            xmlMangaSection += formatXMLTag('my_id', '', 2);
            xmlMangaSection += formatXMLTag('my_read_volumes', myReadVolumes || '0', 2);
            xmlMangaSection += formatXMLTag('my_read_chapters', myReadChapters || '0', 2);
            xmlMangaSection += formatXMLTag('my_start_date', '0000-00-00', 2);
            xmlMangaSection += formatXMLTag('my_finish_date', '0000-00-00', 2);
            xmlMangaSection += formatXMLTag('my_scanalation_group', `<![CDATA[${myScanalationGroup || ''}]]>`, 2);
            xmlMangaSection += formatXMLTag('my_score', myScore || '0', 2);
            xmlMangaSection += formatXMLTag('my_storage', '', 2);
            xmlMangaSection += formatXMLTag('my_retail_volumes', '0', 2);
            xmlMangaSection += formatXMLTag('my_status', myStatus || malStatuses.reading, 2);
            xmlMangaSection += formatXMLTag('my_comments', '<![CDATA[]]>', 2);
            xmlMangaSection += formatXMLTag('my_times_read', myTimesRead, 2);
            xmlMangaSection += formatXMLTag('my_tags', '<![CDATA[]]>', 2);
            xmlMangaSection += formatXMLTag('my_reread_value', '', 2);
            xmlMangaSection += formatXMLTag('update_on_import', updateOnImport, 2);
            xmlMyAnimeListSection += formatXMLTag('manga', '\n' + xmlMangaSection + '\t', 1);
          });
          xmlContent += formatXMLTag('myanimelist', '\n' + xmlMyAnimeListSection, 0);
          await saveFile(new Blob([xmlContent.trim()], {
            type: 'application/xml'
          }), `${filename}.xml`);
          if (excludedMangaIds.length > 0) await alertModal(`You have enabled the "${allIncludeDataOptions.excludeNoMal}" option!\n` + 'The following titles were excluded because they lack a MyAnimeList ID:\n\n' + excludedMangaIds.map(id => `https://${window.location.host}/title/${id}`).join('\n'), 'warning');
          break;
        }
      case exportFormatOptions.csv:
        {
          const csvData = [Object.values(csvDataColumns).filter(d => dataToInclude.includes(d))];
          csvData.push(...mergedData.map(manga => [[allIncludeDataOptions.title, manga.preferredTitle], [allIncludeDataOptions.originalTitle, manga.originalTitle], [allIncludeDataOptions.originalLanguage, langDisplayName().of(manga.attributes.originalLanguage) || manga.attributes.originalLanguage], [allIncludeDataOptions.author, manga.allAuthorNames], [allIncludeDataOptions.year, manga.attributes.year], [allIncludeDataOptions.publication, capitalizeFirstLetter(manga.attributes.status)], [allIncludeDataOptions.contentRating, capitalizeFirstLetter(manga.attributes.contentRating)], [allIncludeDataOptions.demographic, manga.attributes.publicationDemographic ? capitalizeFirstLetter(manga.attributes.publicationDemographic) : ''], [allIncludeDataOptions.tags, manga.allTags], [allIncludeDataOptions.description, manga.preferredDescription], [allIncludeDataOptions.mangaId, manga.id], [allIncludeDataOptions.mangaThread, manga.mangaThreadId], [allIncludeDataOptions.myRating, manga.myRating], [allIncludeDataOptions.readingStatus, listId$1 ? manga.listName : manga.readingStatus ? capitalizeFirstLetter(manga.readingStatus).replaceAll('_', ' ') : ''], [allIncludeDataOptions.isOneshot, manga.isOneshot], [allIncludeDataOptions.lastVolume, manga.lastVolume], [allIncludeDataOptions.lastChapter, manga.lastChapter], [allIncludeDataOptions.readVolume, manga.latestReadVolume], [allIncludeDataOptions.readChapter, manga.latestReadChapter], [allIncludeDataOptions.readChapterScans, manga.latestReadChapterScans], [allIncludeDataOptions.readChapterId, manga.latestReadChapterId], [allIncludeDataOptions.readChapterThread, manga.latestReadChapterThreadId], [allIncludeDataOptions.anilist, linkIdToURL('al', manga.attributes.links?.al)], [allIncludeDataOptions.animePlanet, linkIdToURL('ap', manga.attributes.links?.ap)], [allIncludeDataOptions.kitsu, linkIdToURL('kt', manga.attributes.links?.kt)], [allIncludeDataOptions.mangaUpdates, linkIdToURL('mu', manga.attributes.links?.mu)], [allIncludeDataOptions.myAnimeList, linkIdToURL('mal', manga.attributes.links?.mal)], [allIncludeDataOptions.novelUpdates, linkIdToURL('nu', manga.attributes.links?.nu)], [allIncludeDataOptions.bookWalker, linkIdToURL('bw', manga.attributes.links?.bw)], [allIncludeDataOptions.amazon, linkIdToURL('amz', manga.attributes.links?.amz)], [allIncludeDataOptions.ebookJapan, linkIdToURL('ebj', manga.attributes.links?.ebj)], [allIncludeDataOptions.cdJapan, linkIdToURL('cdj', manga.attributes.links?.cdj)], [allIncludeDataOptions.officialRaw, manga.attributes.links?.raw], [allIncludeDataOptions.officialEnglish, manga.attributes.links?.engtl]].flatMap(d => {
            if (dataToInclude.includes(d[0])) return d[1] ? d[1].toString() : '';
          }).filter(d => d !== undefined)));
          const csv = formatCSV(csvData);
          await saveFile(new Blob([csv], {
            type: 'text/csv'
          }), `${filename}.csv`);
          break;
        }
      case exportFormatOptions.json:
        {
          await saveFile(new Blob([JSON.stringify(mergedData, null, 2)], {
            type: 'application/json'
          }), `${filename}.json`);
          break;
        }
    }
    if (errors.length > 0) return alertModal('Failed to fetch some data:\n\n' + errors.join('\n'), 'warning');
  };
}

class MangadexShowCoverData extends MangadexBookmarklet {
  main = () => {
    useComponents();
    const maxCoverRetry = 4;
    const requestLimit = 100;
    const maxRequestOffset = 1000;
    const coverElements = [];
    const coverFileNames = new Map();
    const skippedCoverFileNames = new Map();
    const mangaIdsForQuery = {
      manga: [],
      cover: []
    };
    const progressBar = new SimpleProgressBar();
    document.querySelectorAll('img, div').forEach(element => {
      const imageSource = element.src || element.style.getPropertyValue('background-image');
      if (!/\/covers\/+[-0-9a-f]{20,}\/+[-0-9a-f]{20,}[^/]+(?:[?#].*)?$/.test(imageSource) || element.classList.contains('banner-image') || element.parentElement?.classList.contains('banner-bg')) return;
      const mangaId = getMatch(imageSource, /[-0-9a-f]{20,}/);
      const coverFileName = getMatch(imageSource, /([-0-9a-f]{20,}\.[^/.]*)\.[0-9]+\.[^/.?#]*([?#].*)?$/, 1) || getMatch(imageSource, /[-0-9a-f]{20,}\.[^/.]*?$/);
      if (!mangaId || !coverFileName) return;
      const addCoverFileName = fileNames => {
        if (fileNames.has(mangaId)) fileNames.get(mangaId)?.add(coverFileName);else fileNames.set(mangaId, new Set([coverFileName]));
      };
      if (element.getAttribute('cover-data-bookmarklet') === 'executed') {
        addCoverFileName(skippedCoverFileNames);
        return;
      }
      coverElements.push(element);
      element.setAttribute('cover-data-bookmarklet', 'executed');
      addCoverFileName(coverFileNames);
    });
    if (coverFileNames.size <= 0) {
      if (document.querySelector('[cover-data-bookmarklet="executed"]')) return alertModal('No new covers were found on this page since the last time this bookmarklet was executed!');
      return alertModal('No covers were found on this page!');
    }
    progressBar.start({
      maxValue: coverElements.length
    });
    coverFileNames.forEach((fileNames, mangaId) => {
      const skippedCoversSize = skippedCoverFileNames.get(mangaId)?.size || 0;
      if (fileNames.size + skippedCoversSize > 1 || titleId() === mangaId) mangaIdsForQuery.cover.push(mangaId);else mangaIdsForQuery.manga.push(mangaId);
    });
    getAllCoverData().then(covers => {
      let addedCoverData = 0;
      let failedCoverData = 0;
      const coverImagesContainer = document.createElement('div');
      setStyles(coverImagesContainer, {
        width: 'fit-content',
        height: 'fit-content',
        opacity: '0',
        position: 'absolute',
        top: '-10000px',
        'z-index': '-10000',
        'pointer-events': 'none'
      });
      document.body.append(coverImagesContainer);
      coverElements.forEach(element => {
        const imageSource = element.src || element.style.getPropertyValue('background-image');
        let coverManga;
        const cover = covers.find(cover => {
          coverManga = cover.relationships.find(relationship => relationship.type === 'manga');
          if (coverManga && new RegExp(`${coverManga.id}/${cover.attributes.fileName}`).test(imageSource)) return cover;
        });
        if (!cover || !coverManga) {
          console.error(`Element changed primary cover image: ${element}`);
          ++failedCoverData;
          reportFailed();
          return;
        }
        let coverRetry = 0;
        const coverUrl = `${window.location.origin}/covers/${coverManga.id}/${cover.attributes.fileName}`;
        const replacementCoverUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQIW2NgAAIAAAUAAR4f7BQAAAAASUVORK5CYII=';
        const fullSizeImage = new Image();
        fullSizeImage.setAttribute('cover-data-bookmarklet', 'executed');
        coverImagesContainer.append(fullSizeImage);
        function reportFailed() {
          if (addedCoverData + failedCoverData >= coverElements.length) {
            progressBar.remove();
            if (failedCoverData > 0) alertModal(`${failedCoverData} cover images failed to load.\n\nReload the page and execute the bookmarklet again!`, 'error').catch(console.error);
          }
        }
        function fallbackMethod() {
          fullSizeImage.onerror = () => {
            console.error(`Cover image failed to load: ${coverUrl}`);
            ++failedCoverData;
            reportFailed();
          };
          fullSizeImage.onload = () => {
            fullSizeImage.remove();
            if (coverImagesContainer.children.length <= 0) coverImagesContainer.remove();
            displayCoverData(element, fullSizeImage.naturalWidth, fullSizeImage.naturalHeight, cover);
            progressBar.update(++addedCoverData);
            reportFailed();
          };
        }
        try {
          fullSizeImage.onerror = () => {
            console.warn(`Cover image failed to load: ${coverUrl}.\nRetrying...`);
            fullSizeImage.removeAttribute('src');
            if (++coverRetry >= maxCoverRetry) fallbackMethod();
            fullSizeImage.setAttribute('src', coverUrl);
          };
          new ResizeObserver((_entries, observer) => {
            if (coverRetry >= maxCoverRetry) return observer.disconnect();
            const fullSizeImageWidth = fullSizeImage.naturalWidth;
            const fullSizeImageHeight = fullSizeImage.naturalHeight;
            if (fullSizeImageWidth > 0 && fullSizeImageHeight > 0) {
              observer.disconnect();
              fullSizeImage.remove();
              fullSizeImage.src = replacementCoverUrl;
              if (coverImagesContainer.children.length <= 0) coverImagesContainer.remove();
              displayCoverData(element, fullSizeImageWidth, fullSizeImageHeight, cover);
              progressBar.update(++addedCoverData);
              reportFailed();
            }
          }).observe(fullSizeImage);
        } catch (error) {
          fallbackMethod();
        }
        fullSizeImage.src = coverUrl;
      });
    }).catch(e => {
      console.error(e);
      alertModal('Failed to fetch cover data!\n' + e.message, 'error').catch(console.error);
    });
    function displayCoverData(element, fullSizeImageWidth, fullSizeImageHeight, cover) {
      element.setAttribute('cover-data-cover-id', cover.id);
      const showAllInformation = (event, show = true) => {
        const showInformation = element => setStyles(element, {
          display: show ? 'flex' : 'none'
        });
        event.stopPropagation();
        event.preventDefault();
        if (event.shiftKey) document.querySelectorAll('.cover-data-bookmarklet-information').forEach(element => showInformation(element));else showInformation(informationElement);
      };
      const user = cover.relationships.find(relationship => relationship.type === 'user' && relationship.id !== 'f8cc4f8a-e596-4618-ab05-ef6572980bbf');
      const information = {
        Dimensions: `${fullSizeImageWidth}x${fullSizeImageHeight}`,
        Version: cover.attributes.version,
        Description: cover.attributes.description,
        Language: cover.attributes.locale && langDisplayName().of(cover.attributes.locale),
        Volume: cover.attributes.volume,
        User: user?.attributes?.username,
        'Created at': localTime(cover.attributes.createdAt),
        'Updated at': localTime(cover.attributes.updatedAt),
        ID: cover.id
      };
      const informationShowElement = document.createElement('span');
      setStyles(informationShowElement, {
        position: 'absolute',
        top: '0',
        'z-index': '1'
      });
      const informationShowElementContent = document.createElement('span');
      setStyles(informationShowElementContent, {
        width: 'fit-content',
        display: 'flex',
        gap: '0.1rem',
        'align-items': 'center'
      });
      informationShowElementContent.addEventListener('click', showAllInformation);
      informationShowElement.append(informationShowElementContent);
      const informationShowElementText = document.createElement('span');
      informationShowElementText.innerText = information['Dimensions'];
      setStyles(informationShowElementText, {
        'padding-top': '0.25px'
      });
      informationShowElementContent.append(informationShowElementText);
      const informationElement = document.createElement('span');
      informationElement.classList.add('cover-data-bookmarklet-information');
      setStyles(informationElement, {
        display: 'none',
        position: 'absolute',
        width: '100%',
        height: '100%',
        padding: '0.4rem',
        gap: '0.2rem',
        overflow: 'auto',
        'flex-wrap': 'wrap',
        'align-content': 'baseline',
        'background-color': mdComponentColors.accent,
        'z-index': '2'
      });
      informationElement.addEventListener('click', e => showAllInformation(e, false));
      const informationItemElements = {};
      for (const info in information) {
        const value = information[info];
        if (!value) {
          delete information[info];
          continue;
        }
        informationItemElements[info] = document.createElement('small');
        informationItemElements[info].innerText = value;
        informationItemElements[info].setAttribute('title', `${info}: ${value}`);
        setStyles(informationItemElements[info], {
          height: 'fit-content',
          'max-width': '100%',
          'flex-grow': '1',
          'text-align': 'center',
          'background-color': mdComponentColors.accent20,
          padding: '0.2rem 0.4rem',
          'border-radius': '0.25rem'
        });
        informationElement.append(informationItemElements[info]);
      }
      informationShowElementContent.setAttribute('title', Object.entries(information).map(([key, value]) => `${key}: ${value}`).join('\n'));
      if (informationItemElements['Volume']) informationItemElements['Volume'].innerText = `Volume ${information['Volume']}`;
      if (informationItemElements['Description']) {
        setStyles(informationItemElements['Description'], {
          width: '100%',
          border: `1px solid ${mdComponentColors.primary}`
        });
      }
      if (informationItemElements['User']) {
        const roleColor = getUserRoleColor(user.attributes.roles);
        setStyles(informationItemElements['User'], {
          width: '100%',
          color: roleColor,
          border: `1px solid ${roleColor}`,
          'background-color': roleColor.replace(')', ',0.1)')
        });
        const padding = getStyles(informationItemElements['User'], ['padding'])?.padding;
        removeStyles(informationItemElements['User'], ['padding']);
        const userLinkElement = document.createElement('a');
        setStyles(userLinkElement, {
          display: 'block',
          width: '100%',
          height: '100%',
          padding: padding,
          overflow: 'hidden',
          'text-overflow': 'ellipsis',
          'white-space': 'nowrap'
        });
        userLinkElement.href = `/user/${user.id}`;
        userLinkElement.target = '_blank';
        userLinkElement.innerText = informationItemElements['User'].innerText;
        informationItemElements['User'].innerText = '';
        informationItemElements['User'].append(userLinkElement);
        informationItemElements['User'].addEventListener('click', event => {
          event.stopPropagation();
          event.preventDefault();
          openNewTab(`/user/${user.id}`);
        });
      }
      informationItemElements['Version'].innerText = `Version ${information['Version']}`;
      informationItemElements['Created at'].innerText = `Created at ${information['Created at']}`;
      informationItemElements['Updated at'].innerText = `Updated at ${information['Updated at']}`;
      informationItemElements['ID'].innerText = 'Copy Cover ID';
      informationItemElements['ID'].addEventListener('click', event => {
        const copyId = ids => {
          navigator.clipboard.writeText(ids).then(() => console.debug(`Copied cover ids: ${ids}`), () => console.error(`Failed to copy cover ids: ${ids}`)).catch(console.error);
        };
        event.stopPropagation();
        event.preventDefault();
        if (event.shiftKey) {
          const coverIds = [];
          document.querySelectorAll('[cover-data-cover-id]').forEach(element => {
            const coverId = element.getAttribute('cover-data-cover-id');
            if (coverId && !coverIds.includes(coverId)) coverIds.push(coverId);
          });
          copyId(coverIds.join(' '));
        } else copyId(cover.id);
      });
      if (element instanceof HTMLImageElement) {
        setStyles(informationShowElement, {
          padding: '0.2rem 0.4rem 0.5rem',
          color: '#fff',
          left: '0',
          width: '100%',
          background: 'linear-gradient(0deg,transparent,rgba(0,0,0,0.8))',
          'border-top-right-radius': '0.25rem',
          'border-top-left-radius': '0.25rem'
        });
        if (information['Description']) informationShowElementContent.append(informationCircleOutline());else informationShowElementContent.append(ellipsisHorizontalCircleOutline());
        setStyles(informationElement, {
          'border-radius': '0.25rem'
        });
        element.parentElement?.append(informationShowElement, informationElement);
      } else {
        setStyles(informationShowElement, {
          padding: '0 0.2rem',
          'background-color': mdComponentColors.accent,
          'border-bottom-left-radius': '4px',
          'border-bottom-right-radius': '4px'
        });
        setStyles(informationShowElementText, {
          'max-height': '1.5rem'
        });
        if (information['Description']) informationShowElementContent.append(informationCircleMini());else informationShowElementContent.append(ellipsisHorizontalCircleMini());
        element.append(informationShowElement, informationElement);
      }
    }
    function getAllCoverData() {
      const covers = [];
      async function awaitAllCoverData() {
        for (const endpoint in mangaIdsForQuery) {
          const isCoverEndpoint = endpoint === 'cover';
          const mangaIdsForQuerySplit = splitArray(mangaIdsForQuery[endpoint]);
          for (const ids of mangaIdsForQuerySplit) {
            const rsp = await getCoverData(ids, isCoverEndpoint);
            if (isCoverEndpoint) {
              covers.push(...rsp.data);
              for (let i = rsp.limit; i < rsp.total; i += rsp.limit) {
                const rsp = await getCoverData(ids, isCoverEndpoint, i);
                covers.push(...rsp.data);
              }
            } else {
              rsp.data.forEach(manga => {
                const cover = manga.relationships.find(relationship => relationship.type === 'cover_art');
                if (cover) {
                  cover.relationships = [{
                    type: manga.type,
                    id: manga.id
                  }];
                  covers.push(cover);
                }
              });
            }
          }
        }
        return covers;
      }
      return new Promise((resolve, reject) => awaitAllCoverData().then(resolve).catch(reject));
    }
    function getCoverData(ids, isCoverEndpoint, offset = 0) {
      return new Promise((resolve, reject) => {
        if (offset > maxRequestOffset) return reject(new Error(`Offset is bigger than ${maxRequestOffset}!`));
        if (isCoverEndpoint) getCoverList({
          mangaIds: ids,
          order: {
            volume: 'asc'
          },
          includes: ['user'],
          offset: offset,
          limit: requestLimit
        }).then(resolve).catch(reject);else getMangaList({
          ids: ids,
          includes: ['cover_art'],
          contentRating: contentRatings,
          offset: offset,
          limit: requestLimit
        }).then(resolve).catch(reject);
      });
    }
  };
}

class MangadexUploadCoversFromLinks extends MangadexBookmarklet {
  routes = [titleRoute, ...titleEditRoutes, titleCreateRoute];
  main = async () => {
    useComponents();
    const progressBar = new SimpleProgressBar();
    const errors = [];
    const onError = error => {
      console.error(error);
      errors.push(error);
      progressBar.update();
    };
    const coverDataLinksString = await promptAreaModal('Enter cover image links\n(separated by newlines)');
    if (!coverDataLinksString) return;
    const coverDataLinks = coverDataLinksString.split('\n');
    const parseDataLink = async link => {
      if (!link.trim()) return;
      const matched = link.match(/(?:\[\s*(?:[^\]\d]*?)?(?<volume_num>\d+)?(?:[^\]\d]*?)?\s*]\s*)?(?:\[\s*(?<locale>[a-z]{2}(?:-[a-z0-9]{2,8})?)?\s*]\s*)?(?:\{\s*(?<description>[^}]*)?\s*}\s*)?(?:\(\s*(?<url_1>https?:\/\/\S+)\s*\)|(?<url_2>https?:\/\/\S+))/i);
      if (!matched) throw new Error(`Invalid link "${link}"`);
      const {
        volume_num,
        locale,
        description,
        url_1,
        url_2
      } = matched.groups;
      const url = url_1 || url_2;
      if (!url) throw new Error(`No URL found in link "${link}"`);
      let coverFile;
      try {
        coverFile = await urlToFile(url, {
          fetchOptions: {
            anonymous: true
          }
        });
        if (!coverFile.type.startsWith('image/')) throw new Error(`"${url}" is not an image!`);
      } catch (error) {
        throw new Error(`Failed to fetch cover from link "${link}": ${error}`);
      }
      let formattedDescription = description;
      if (formattedDescription) {
        const newLineStringVariable = formatStringVariable('NL');
        formattedDescription = replaceStringVariable(formattedDescription, [[newLineStringVariable, '\n']]);
      }
      return {
        file: coverFile,
        locale: locale,
        volume: volume_num,
        description: formattedDescription
      };
    };
    progressBar.start({
      maxValue: coverDataLinks.length
    });
    const parsedData = await Promise.all(coverDataLinks.map(async coverDataLink => {
      let parsedData;
      try {
        parsedData = await parseDataLink(coverDataLink);
      } catch (error) {
        onError(error);
        return;
      }
      progressBar.update();
      return parsedData;
    }));
    const covers = parsedData.filter(data => !!data);
    progressBar.start({
      maxValue: covers.length
    });
    if (titleIsEdit() || titleIsCreate()) {
      for (const cover of covers) {
        let coverElement;
        try {
          coverElement = await addCover(cover.file);
        } catch (error) {
          onError(error);
          continue;
        }
        if (cover.volume) {
          const changed = await setCoverVolume(coverElement, cover.volume, true);
          console.debug('Changed cover volume:', changed, coverElement);
        }
        if (cover.locale) {
          const changed = await setCoverLocale(coverElement, cover.locale);
          console.debug('Changed cover locale:', changed, coverElement);
        }
        if (cover.description) {
          const changed = await setCoverDescription(coverElement, cover.description, true);
          console.debug('Changed cover description:', changed, coverElement);
        }
        progressBar.update();
      }
    } else {
      await Promise.all(covers.map(async cover => {
        try {
          await uploadCover(cover);
        } catch (error) {
          onError(error);
          return;
        }
        progressBar.update();
      }));
    }
    progressBar.remove();
    if (errors.length > 0) {
      await alertModal('Failed to upload some covers!\n\n\n' + errors.join('\n\n'), 'error');
    }
  };
}

class MangadexOpenLinks extends MangadexBookmarklet {
  routes = [titleRoute, ...titleEditRoutes, titleCreateRoute];
  main = async () => {
    const titleId$1 = titleId();
    const inputLinks = getLinkInputValues();
    const links = [];
    if (inputLinks.length <= 0 && titleId$1) {
      const titleInfo = await getManga(titleId$1);
      if (titleInfo.data.attributes.links) {
        for (const site in titleInfo.data.attributes.links) links.push(linkIdToURL(site, titleInfo.data.attributes.links[site]));
      }
    } else links.push(...inputLinks);
    links.forEach(link => openNewTab(link));
  };
}

class MangadexDelCoversByLang extends MangadexBookmarklet {
  routes = [...titleEditRoutes, titleCreateRoute];
  main = async () => {
    useComponents();
    const coverElements = getCovers();
    const languages = Array.from(new Set(coverElements.map(element => {
      const language = element.querySelector(coverSelectedLocaleImageSelector);
      if (!language) return;
      return language.title.trim();
    }).filter(language => !!language)));
    if (languages.length <= 0) return alertModal('No covers found!');
    const selectedLanguage = await selectModal('Select language', languages);
    if (!selectedLanguage) return;
    const deletedCovers = [];
    for (const element of coverElements) {
      const language = element.querySelector(coverSelectedLocaleImageSelector);
      if (!language) continue;
      if (selectedLanguage === language.title.trim()) {
        if (await deleteCover(element)) deletedCovers.push(element);else console.error('Failed to delete cover:', element);
      }
    }
    if (deletedCovers.length <= 0) return alertModal('No covers in given language found!');
    console.log('Deleted covers:', deletedCovers);
  };
}

class MangadexSearchAllTitles extends MangadexBookmarklet {
  routes = [titleRoute, ...titleEditRoutes, titleCreateRoute];
  main = async () => {
    useComponents();
    const titleId$1 = titleId();
    const inputTitles = getTitleInputValues();
    const titles = [];
    const titlesToSearch = [];
    const progressBar = new SimpleProgressBar();
    const foundTitleIds = titleId$1 ? [titleId$1] : [];
    if (inputTitles.length <= 0 && titleId$1) {
      const titleInfo = await getManga(titleId$1);
      const mainTitleLang = Object.keys(titleInfo.data.attributes.title)[0];
      const mainTitle = titleInfo.data.attributes.title[mainTitleLang];
      const altTitles = titleInfo.data.attributes.altTitles;
      titles.push(mainTitle);
      if (Array.isArray(altTitles)) titles.push(...altTitles.map(title => title[Object.keys(title)[0]]));
    } else titles.push(...inputTitles);
    progressBar.start({
      maxValue: titles.length
    });
    await Promise.all(titles.map(async title => {
      if (!title || titlesToSearch.length > 10) return progressBar.update();
      const titleList = await getMangaList({
        title: title,
        offset: 0,
        limit: 100,
        contentRating: contentRatings
      }).catch(console.debug);
      if (titleList) {
        for (const manga of titleList.data) {
          if (foundTitleIds.includes(manga.id)) continue;
          foundTitleIds.push(manga.id);
          if (!titlesToSearch.includes(title)) titlesToSearch.push(title);
        }
        if (titleList.total > 100 && !titlesToSearch.includes(title)) titlesToSearch.push(title);
      }
      progressBar.update();
    }));
    progressBar.remove();
    titlesToSearch.forEach(title => openNewTab(createUrl(window.location.origin, '/titles', {
      q: title,
      content: contentRatings.join(',')
    })));
  };
}

class MangadexCloneTitle extends MangadexBookmarklet {
  routes = [titleRoute, ...titleEditRoutes];
  main = async () => {
    useComponents();
    const dataMap = {
      title: 'Title',
      altTitles: 'Alternative Titles',
      description: 'Synopsis',
      authors: 'Authors',
      artists: 'Artists',
      originalLanguage: 'Original Language',
      contentRating: 'Content Rating',
      publicationDemographic: 'Magazine Demographic',
      status: 'Publication Status',
      lastVolume: 'Final Chapter',
      lastChapter: 'Final Chapter',
      year: 'Publication Year',
      tags: 'Tags',
      links: 'Sites',
      relations: 'Relations',
      covers: 'Covers',
      chapterNumbersResetOnNewVolume: 'Chapter Numbers Reset On New Volume'
    };
    const dataMapNames = Object.values(dataMap).reduce((acc, current) => acc.includes(current) ? acc : [...acc, current], []);
    const dataToClone = await checkboxModal('Data to clone', dataMapNames, dataMapNames.filter(name => name !== dataMap.relations && name !== dataMap.covers));
    if (!dataToClone) return;
    if (!dataToClone.length) {
      await alertModal('You must select some data to clone!', 'error');
      return;
    }
    const progressBar = new SimpleProgressBar(1, 0);
    progressBar.start();
    const titleInfo = await getManga().catch(error => alertModal('Failed to fetch title data!\n\n' + error, 'error'));
    if (!titleInfo) {
      progressBar.remove();
      return;
    }
    progressBar.update();
    const isSelected = name => dataToClone.includes(name);
    const getRelationshipIds = (type, data = titleInfo.data.relationships) => data?.map(rel => rel.type === type && rel.id).filter(id => id);
    const newTitleData = {
      title: isSelected(dataMap.title) ? titleInfo.data.attributes.title : {
        en: 'Untitled'
      },
      altTitles: isSelected(dataMap.altTitles) && Array.isArray(titleInfo.data.attributes.altTitles) ? titleInfo.data.attributes.altTitles : [],
      description: isSelected(dataMap.description) ? titleInfo.data.attributes.description : {},
      authors: isSelected(dataMap.authors) ? getRelationshipIds('author') : [],
      artists: isSelected(dataMap.artists) ? getRelationshipIds('artist') : [],
      links: isSelected(dataMap.links) && titleInfo.data.attributes.links ? titleInfo.data.attributes.links : {},
      originalLanguage: isSelected(dataMap.originalLanguage) ? titleInfo.data.attributes.originalLanguage : 'ja',
      lastVolume: isSelected(dataMap.lastVolume) ? titleInfo.data.attributes.lastVolume : null,
      lastChapter: isSelected(dataMap.lastChapter) ? titleInfo.data.attributes.lastChapter : null,
      publicationDemographic: isSelected(dataMap.publicationDemographic) ? titleInfo.data.attributes.publicationDemographic : null,
      status: isSelected(dataMap.status) ? titleInfo.data.attributes.status : 'ongoing',
      year: isSelected(dataMap.year) ? titleInfo.data.attributes.year : null,
      contentRating: isSelected(dataMap.contentRating) ? titleInfo.data.attributes.contentRating : 'safe',
      chapterNumbersResetOnNewVolume: isSelected(dataMap.chapterNumbersResetOnNewVolume) ? titleInfo.data.attributes.chapterNumbersResetOnNewVolume : false,
      tags: isSelected(dataMap.tags) ? getRelationshipIds('tag', titleInfo.data.attributes.tags) : []
    };
    const createdTitleURLPrompt = (await promptModal('Leave empty to create a new title\nor\nEnter a URL of an existing title to merge', ''))?.trim();
    if (createdTitleURLPrompt === null || createdTitleURLPrompt === undefined) return;
    progressBar.start();
    let createdTitle;
    if (createdTitleURLPrompt) {
      let createdTitleURL;
      try {
        createdTitleURL = new URL(createdTitleURLPrompt);
      } catch (error) {
        progressBar.remove();
        await alertModal('Invalid title URL!', 'error');
        return;
      }
      const createdTitleId = titleId(createdTitleURL.pathname);
      if (!createdTitleId) {
        progressBar.remove();
        await alertModal('Invalid title UUID!', 'error');
        return;
      }
      const createdTitleIsDraft = titleIsDraft(createdTitleURL.href);
      createdTitle = await getManga(createdTitleId, createdTitleIsDraft).catch(error => alertModal('Failed to fetch created title!\n\n' + error, 'error'));
    } else {
      createdTitle = await createManga(newTitleData).catch(error => alertModal('Failed to create new title!\n\n' + error, 'error'));
    }
    if (!createdTitle) {
      progressBar.remove();
      return;
    }
    progressBar.update();
    if (createdTitleURLPrompt) {
      const createdTitleAltTitles = Array.isArray(createdTitle.data.attributes.altTitles) ? createdTitle.data.attributes.altTitles : [];
      const dedupedNewTitleAltTitles = newTitleData.altTitles?.filter(altTitle => !createdTitleAltTitles.some(title => altTitle[Object.keys(altTitle)[0]] === title[Object.keys(title)[0]])) || [];
      const createdTitleAuthors = getRelationshipIds('author', createdTitle.data.relationships);
      const dedupedNewTitleAuthors = newTitleData.authors?.filter(author => !createdTitleAuthors.includes(author)) || [];
      const createdTitleArtists = getRelationshipIds('artist', createdTitle.data.relationships);
      const dedupedNewTitleArtists = newTitleData.artists?.filter(artist => !createdTitleArtists.includes(artist)) || [];
      const createdTitleTags = getRelationshipIds('tag', createdTitle.data.attributes.tags);
      const dedupedNewTitleTags = newTitleData.tags?.filter(tag => !createdTitleTags.includes(tag)) || [];
      const softMergeType = 'Copy missing only';
      const moderateMergeType = 'Overwrite and copy missing';
      const hardMergeType = 'Overwrite all';
      const mergeType = await selectModal("Choose how to merge the title data\n(doesn't affect relations or covers)", [softMergeType, moderateMergeType, hardMergeType]);
      if (mergeType === null || mergeType === undefined) return;
      progressBar.start();
      const isModerateMerge = mergeType === moderateMergeType;
      const isHardMerge = mergeType === hardMergeType;
      const mergedTitleData = isHardMerge ? {
        ...newTitleData,
        version: createdTitle.data.attributes.version
      } : {
        title: isModerateMerge && isSelected(dataMap.title) ? newTitleData.title : createdTitle.data.attributes.title || newTitleData.title,
        altTitles: [...createdTitleAltTitles, ...dedupedNewTitleAltTitles],
        description: isModerateMerge ? {
          ...(createdTitle.data.attributes.description || {}),
          ...(newTitleData.description || {})
        } : {
          ...(newTitleData.description || {}),
          ...(createdTitle.data.attributes.description || {})
        },
        authors: [...createdTitleAuthors, ...dedupedNewTitleAuthors],
        artists: [...createdTitleArtists, ...dedupedNewTitleArtists],
        links: isModerateMerge ? {
          ...(createdTitle.data.attributes.links || {}),
          ...(newTitleData.links || {})
        } : {
          ...(newTitleData.links || {}),
          ...(createdTitle.data.attributes.links || {})
        },
        originalLanguage: isModerateMerge && isSelected(dataMap.originalLanguage) ? newTitleData.originalLanguage : createdTitle.data.attributes.originalLanguage || newTitleData.originalLanguage,
        lastVolume: isModerateMerge && isSelected(dataMap.lastVolume) ? newTitleData.lastVolume : createdTitle.data.attributes.lastVolume || newTitleData.lastVolume,
        lastChapter: isModerateMerge && isSelected(dataMap.lastChapter) ? newTitleData.lastChapter : createdTitle.data.attributes.lastChapter || newTitleData.lastChapter,
        publicationDemographic: isModerateMerge && isSelected(dataMap.publicationDemographic) ? newTitleData.publicationDemographic : createdTitle.data.attributes.publicationDemographic || newTitleData.publicationDemographic,
        status: isModerateMerge && isSelected(dataMap.status) ? newTitleData.status : createdTitle.data.attributes.status || newTitleData.status,
        year: isModerateMerge && isSelected(dataMap.year) ? newTitleData.year : createdTitle.data.attributes.year || newTitleData.year,
        contentRating: isModerateMerge && isSelected(dataMap.contentRating) ? newTitleData.contentRating : createdTitle.data.attributes.contentRating || newTitleData.contentRating,
        chapterNumbersResetOnNewVolume: isModerateMerge && isSelected(dataMap.chapterNumbersResetOnNewVolume) ? newTitleData.chapterNumbersResetOnNewVolume : createdTitle.data.attributes.chapterNumbersResetOnNewVolume || newTitleData.chapterNumbersResetOnNewVolume,
        tags: [...createdTitleTags, ...dedupedNewTitleTags],
        version: createdTitle.data.attributes.version
      };
      createdTitle = await updateManga(mergedTitleData, createdTitle.data.id).catch(error => alertModal('Failed to update title data!\n\n' + error, 'error'));
      if (!createdTitle) {
        progressBar.remove();
        return;
      }
      progressBar.update();
    }
    const errors = [];
    if (isSelected(dataMap.relations)) {
      const getMangaRelations = (relations = titleInfo.data.relationships) => relations.filter(rel => rel.type === 'manga' && rel.related).map(rel => ({
        targetManga: rel.id,
        relation: rel.related
      }));
      const relations = getMangaRelations();
      let dedupedRelations = relations;
      if (createdTitleURLPrompt) {
        const createdTitleRelations = getMangaRelations(createdTitle.data.relationships);
        dedupedRelations = relations.filter(relation => !createdTitleRelations.some(createdRelation => createdRelation.targetManga === relation.targetManga && createdRelation.relation === relation.relation));
      }
      progressBar.start({
        maxValue: dedupedRelations.length
      });
      await Promise.all(dedupedRelations.map(async relation => {
        await createMangaRelation(relation, createdTitle.data.id).catch(error => {
          fetchClient.abortAll();
          if (error.name !== 'AbortError') errors.push('Failed to create relations: ' + error);
        });
        progressBar.update();
      }));
    }
    if (isSelected(dataMap.covers)) {
      progressBar.start();
      const getTitleCovers = async (mangaId = titleInfo.data.id) => await getCoverList({
        mangaIds: [mangaId],
        callback: () => progressBar.update()
      }).then(data => data.data).catch(error => {
        fetchClient.abortAll();
        if (error.name !== 'AbortError') errors.push('Failed to fetch cover data lists: ' + error);
      });
      const allCovers = await getTitleCovers();
      let dedupedCovers = allCovers;
      if (allCovers && createdTitleURLPrompt) {
        progressBar.start();
        const createdTitleCovers = await getTitleCovers(createdTitle.data.id);
        if (createdTitleCovers) {
          dedupedCovers = allCovers.filter(cover => !createdTitleCovers.some(createdCover => createdCover.attributes.volume === cover.attributes.volume && createdCover.attributes.locale === cover.attributes.locale));
        }
      }
      if (dedupedCovers) {
        progressBar.start({
          maxValue: dedupedCovers.length
        });
        await Promise.all(dedupedCovers.map(async cover => {
          const coverImageResponse = await fetch(`https://mangadex.org/covers/${titleInfo.data.id}/${cover.attributes.fileName}`).catch(error => {
            errors.push('Failed to fetch cover image: ' + error);
          });
          if (!coverImageResponse) return;
          const coverBlob = await coverImageResponse.blob();
          await uploadCover({
            file: new File([coverBlob], cover.attributes.fileName, {
              type: coverBlob.type
            }),
            volume: cover.attributes.volume || null,
            description: cover.attributes.description || '',
            locale: cover.attributes.locale || titleInfo.data.attributes.originalLanguage
          }, createdTitle.data.id).catch(error => {
            fetchClient.abortAll();
            if (error.name !== 'AbortError') errors.push('Failed to upload covers: ' + error);
          });
          progressBar.update();
        }));
      }
    }
    progressBar.remove();
    if (errors.length) {
      await alertModal('Failed to clone all title data!\n\n' + errors.join('\n\n'), 'error');
    }
    openNewTab(`/title/edit/${createdTitle.data.id}${createdTitle.data.attributes.state === 'draft' ? '?draft=true' : ''}`);
  };
}

class MangadexChangeMainTitleLang extends MangadexBookmarklet {
  routes = [titleRoute, ...titleEditRoutes];
  main = async () => {
    useComponents();
    const titleData = await getManga();
    const mainTitleLangCode = Object.keys(titleData.data.attributes.title)[0];
    const allLangs = {};
    const allLangCodes = [mainTitleLangCode, 'en', ...allSiteLangs.filter(lang => lang !== mainTitleLangCode && lang !== 'en')];
    allLangCodes.forEach(langCode => {
      let langDisplayName$1;
      try {
        langDisplayName$1 = langDisplayName().of(langCode);
      } catch (error) {
        console.warn(error);
      }
      if (langDisplayName$1) allLangs[langDisplayName$1] = langCode;
    });
    const selectedLanguage = await selectModal("Select the new main title's language\n(current one is selected by default)", Object.keys(allLangs));
    if (!selectedLanguage) return;
    const selectedLangCode = allLangs[selectedLanguage];
    try {
      await updateManga({
        title: {
          [selectedLangCode]: titleData.data.attributes.title[mainTitleLangCode]
        },
        version: titleData.data.attributes.version
      });
    } catch (error) {
      console.error(error);
      await alertModal("Failed to update main title's language!\n\n" + error, 'error');
    }
  };
}

const asinRegex = '(?:[/dp]|$)([A-Z0-9]{10})';
class AmazonDownloadCovers extends AmazonBookmarklet {
  routes = [`.*${asinRegex}`];
  main = () => {
    const getAsin = url => getMatch(url, new RegExp(asinRegex), 1);
    const getCoverUrl = asin => `${window.location.origin}/images/P/${asin}.01.MAIN._SCRM_.jpg`;
    const books = (element = document) => element.querySelectorAll('a.itemImageLink');
    let downloader;
    const covers = [];
    const locationAsin = getAsin(window.location.pathname);
    const followButtons = document.querySelectorAll('#follow-button');
    const followButtonAsins = [];
    let followButtonAsin;
    followButtons.forEach(button => {
      const asin = button.parentElement?.getAttribute('data-asin');
      if (asin) followButtonAsins.push(asin);
    });
    if (followButtonAsins.length > 0) {
      followButtonAsin = followButtonAsins[0];
    }
    const dataAsin = followButtonAsin || locationAsin;
    if (!dataAsin) {
      const error = new Error('Asin not found!');
      console.error(error);
      alertModal(error, 'error').catch(console.error);
      return;
    }
    if (books().length > 0) {
      const pageSize = 100;
      const itemsElement = document.querySelector('#seriesAsinListPagination, #seriesAsinListPagination_volume');
      const maxItems = parseInt(itemsElement?.getAttribute('data-number_of_items') || books().length.toString());
      const maxPage = Math.ceil(maxItems / pageSize);
      downloader = new CoverDownloader(async loadIndex => {
        let seriesPage = await fetch(`https://${window.location.host}/kindle-dbs/productPage/ajax/seriesAsinList?asin=${dataAsin}&pageNumber=${loadIndex}&pageSize=${pageSize}`, {
          headers: {
            'User-Agent': userAgentDesktop
          }
        }).then(response => response.text()).then(html => new DOMParser().parseFromString(html, 'text/html')).catch(console.error);
        if (!seriesPage || books(seriesPage).length < 1) {
          if (loadIndex !== 1) throw new Error('Failed to fetch series page!');
          seriesPage = document;
        }
        books(seriesPage).forEach(element => {
          const asin = getAsin(element.href);
          if (!asin) return;
          covers.push({
            url: getCoverUrl(asin),
            title: element.getAttribute('title')
          });
        });
        return covers;
      }, {
        loadMax: maxPage,
        title: document.querySelector('#collection-title, #collection-masthead__title, #title-sdp-aw')?.textContent
      });
    } else {
      const bookTitle = document.querySelector('#productTitle, #ebooksTitle, #title')?.textContent?.split('     ')[0];
      downloader = new CoverDownloader(async () => {
        covers.push({
          url: getCoverUrl(dataAsin),
          title: bookTitle
        });
        return covers;
      }, {
        title: (document.querySelector('#seriesBulletWidget_feature_div > .a-link-normal') || document.querySelector('#mobile_productTitleGroup_inner_feature_div > .a-row > .a-row > .a-link-normal'))?.textContent?.replace(/.*: /, '')
      });
    }
    downloader.add();
  };
}

class BookwalkerDownloadCovers extends BookwalkerBookmarklet {
  routes = ['/de:uuid', '/series/:numid', '/:numid/:numid/viewer.html'];
  main = () => {
    const getSeriesId = link => getMatch(link, /series\/(\d+)/, 1);
    const getBookId = link => getMatch(link, /(?:de|cid=)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/, 1);
    const getLastPage = elements => {
      let lastPage = 1;
      elements.forEach(element => {
        const url = element.getAttribute('href') || element.getAttribute('value');
        if (!url) return;
        const page = getMatch(url, /page=(\d+)/, 1);
        if (!page) return;
        const pageNum = parseInt(page);
        if (lastPage < pageNum) lastPage = pageNum;
      });
      return lastPage;
    };
    const cdnBaseUrl = `${ROLER_CDN_URL.origin}/bw`;
    const getCdnBookUrl = bookId => `${cdnBaseUrl}/${bookId}?crop=false`;
    let downloader;
    const covers = [];
    if (window.location.hostname === BOOKWALKER_VIEWER_TRIAL_URL.hostname) {
      const bookId = getBookId(window.location.search);
      downloader = new CoverDownloader(async () => {
        const pagesJson = await fetch(`${cdnBaseUrl}/pages?ids[]=${bookId}`).then(response => response.json());
        const pages = pagesJson.data[0].pages;
        pages.forEach((page, i) => {
          covers.push({
            url: page.url,
            title: i.toString()
          });
        });
        return covers;
      }, {
        fileNamePrefix: 'Page',
        title: document.querySelector('title')?.textContent,
        disableCropping: true
      });
    } else if (getBookId(window.location.pathname)) {
      const bookId = getBookId(window.location.pathname);
      const bookTitle = document.querySelector('.detail-book-title')?.textContent || document.querySelector('meta[property="og:title"]')?.getAttribute('content');
      downloader = new CoverDownloader(async () => {
        if (bookId) covers.push({
          url: getCdnBookUrl(bookId),
          title: bookTitle
        });
        return covers;
      }, {
        title: document.querySelector(`a[href^="${window.location.origin}/series/"]`)?.textContent
      });
    } else if (/series\/\d+/.test(window.location.pathname)) {
      const seriesId = getSeriesId(window.location.pathname);
      const lastPage = (element = document) => getLastPage(element.querySelectorAll('a[href*="page="], option[value*="page="]'));
      const seriesTitle = document.querySelector('.o-contents-section__title, .o-headline-ttl')?.textContent;
      const wayomiSeriesTitle = document.querySelector('.o-ttsk-card__title')?.textContent;
      const globalSeriesTitle = document.querySelector('.title-main-inner')?.textContent?.split('\n').find(title => title) || document.querySelector('.title-main')?.textContent;
      downloader = new CoverDownloader(async loadIndex => {
        let seriesPage = document;
        if (wayomiSeriesTitle) {
          seriesPage.querySelectorAll('.o-ttsk-list-item > a').forEach(element => {
            const bookId = element.getAttribute('data-book-uuid');
            if (!bookId) return;
            covers.push({
              url: getCdnBookUrl(bookId),
              title: element.getAttribute('data-book-title')
            });
          });
          return covers;
        }
        if (downloader.loadMax > 1 || !/\/list/.test(window.location.pathname)) {
          seriesPage = await fetch(`https://${window.location.host}/series/${seriesId}/list/?order=title&page=${loadIndex}`, {
            headers: {
              'User-Agent': userAgentDesktop
            }
          }).then(response => response.text()).then(html => new DOMParser().parseFromString(html, 'text/html'));
        }
        if (!/\/list/.test(window.location.pathname) && loadIndex === 1) downloader.loadMax = lastPage(seriesPage);
        seriesPage.querySelectorAll('a.m-thumb__image > img, a.a-thumb-img > img, a.a-tile-thumb-img > img').forEach(element => {
          const bookId = getBookId(element.parentElement.href);
          if (!bookId) return;
          covers.push({
            url: getCdnBookUrl(bookId),
            title: element.alt
          });
        });
        return covers;
      }, {
        loadMax: wayomiSeriesTitle ? 1 : lastPage(),
        title: wayomiSeriesTitle || seriesTitle || globalSeriesTitle,
        fileNamePrefix: wayomiSeriesTitle ? 'Chapter' : 'Volume'
      });
    }
    try {
      downloader.add();
    } catch (error) {
      console.error(error);
      alertModal('Failed to initialize cover downloader!\n' + error, 'error').catch(console.error);
    }
  };
}

class BookliveDownloadCovers extends BookliveBookmarklet {
  routes = ['/product/index/title_id/:numid/vol_no/:numid'];
  main = () => {
    const getTitleId = link => getMatch(link, /title_id\/(\d+)/, 1);
    const getVolumeId = link => getMatch(link, /vol_no\/(\d+)/, 1);
    const downloader = new CoverDownloader(async () => {
      const covers = [];
      const titleId = getTitleId(window.location.pathname);
      document.querySelectorAll(`a[href^="/product/index/title_id/${titleId}/vol_no/"] > img`).forEach(element => {
        const volumeId = getVolumeId(element.parentElement.href);
        if (!volumeId) return;
        const cover = {
          title: element.alt,
          url: `${BOOKLIVE_CDN_URL.origin}/${titleId}/${volumeId}/thumbnail/X.jpg`
        };
        if (covers.some(c => c.url === cover.url)) return;
        covers.push(cover);
      });
      if (!covers.length) {
        const volumeId = getVolumeId(window.location.pathname);
        covers.push({
          title: document.querySelector('#product_display_1')?.textContent,
          url: `${BOOKLIVE_CDN_URL.origin}/${titleId}/${volumeId}/thumbnail/X.jpg`
        });
      }
      return covers;
    }, {
      title: document.querySelector('.heading_title')?.textContent
    });
    downloader.add();
  };
}

enableUserScriptFeatures();const settings = [];const universalSettings = new UniversalSettings();if (universalSettings.isWebsite()) {GM_registerMenuCommand('[Any Website] Settings Manager v1.6', () =>universalSettings.execute());settings.push({id: 'universal-settings_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Settings Manager',description: 'Keys to press to execute the Settings Manager bookmarklet.',defaultValue: ['ControlLeft', 'ShiftLeft', 'AltLeft', 'KeyS']});}const mangadexExportTitleList = new MangadexExportTitleList();if (mangadexExportTitleList.isWebsite()) {GM_registerMenuCommand('[MangaDex] Export Title List v1.8', () =>mangadexExportTitleList.execute());settings.push({id: 'mangadex-export_title_list_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Export Title List',description: 'Keys to press to execute the Export Title List bookmarklet.',defaultValue: []});}const mangadexShowCoverData = new MangadexShowCoverData();if (mangadexShowCoverData.isWebsite()) {GM_registerMenuCommand('[MangaDex] Show Cover Data v4.5', () =>mangadexShowCoverData.execute());settings.push({id: 'mangadex-show_cover_data_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Show Cover Data',description: 'Keys to press to execute the Show Cover Data bookmarklet.',defaultValue: []});}const mangadexUploadCoversFromLinks = new MangadexUploadCoversFromLinks();if (mangadexUploadCoversFromLinks.isWebsite()) {GM_registerMenuCommand('[MangaDex] Upload Covers from Links v1.1', () =>mangadexUploadCoversFromLinks.execute());settings.push({id: 'mangadex-upload_covers_from_links_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Upload Covers from Links',description: 'Keys to press to execute the Upload Covers from Links bookmarklet.',defaultValue: []});}const mangadexAddCoverDescriptions = new MangadexAddCoverDescriptions();if (mangadexAddCoverDescriptions.isWebsite()) {GM_registerMenuCommand('[MangaDex] Add Cover Descriptions v3.3', () =>mangadexAddCoverDescriptions.execute());settings.push({id: 'mangadex-add_cover_descriptions_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Add Cover Descriptions',description: 'Keys to press to execute the Add Cover Descriptions bookmarklet.',defaultValue: []});}const mangadexSearchMissingLinks = new MangadexSearchMissingLinks();if (mangadexSearchMissingLinks.isWebsite()) {GM_registerMenuCommand('[MangaDex] Search Missing Links v3.3', () =>mangadexSearchMissingLinks.execute());settings.push({id: 'mangadex-search_missing_links_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Search Missing Links',description: 'Keys to press to execute the Search Missing Links bookmarklet.',defaultValue: []});}const mangadexShortenLinks = new MangadexShortenLinks();if (mangadexShortenLinks.isWebsite()) {GM_registerMenuCommand('[MangaDex] Shorten Links v3.3', () =>mangadexShortenLinks.execute());settings.push({id: 'mangadex-shorten_links_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Shorten Links',description: 'Keys to press to execute the Shorten Links bookmarklet.',defaultValue: []});}const mangadexOpenLinks = new MangadexOpenLinks();if (mangadexOpenLinks.isWebsite()) {GM_registerMenuCommand('[MangaDex] Open Links v2.6', () =>mangadexOpenLinks.execute());settings.push({id: 'mangadex-open_links_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Open Links',description: 'Keys to press to execute the Open Links bookmarklet.',defaultValue: []});}const mangadexDelCoversByLang = new MangadexDelCoversByLang();if (mangadexDelCoversByLang.isWebsite()) {GM_registerMenuCommand('[MangaDex] Delete Covers by Language v2.7', () =>mangadexDelCoversByLang.execute());settings.push({id: 'mangadex-del_covers_by_lang_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Delete Covers by Language',description: 'Keys to press to execute the Delete Covers by Language bookmarklet.',defaultValue: []});}const mangadexSearchAllTitles = new MangadexSearchAllTitles();if (mangadexSearchAllTitles.isWebsite()) {GM_registerMenuCommand('[MangaDex] Search All Titles v1.6', () =>mangadexSearchAllTitles.execute());settings.push({id: 'mangadex-search_all_titles_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Search All Titles',description: 'Keys to press to execute the Search All Titles bookmarklet.',defaultValue: []});}const mangadexCloneTitle = new MangadexCloneTitle();if (mangadexCloneTitle.isWebsite()) {GM_registerMenuCommand('[MangaDex] Clone/Merge Title v2.0', () =>mangadexCloneTitle.execute());settings.push({id: 'mangadex-clone_title_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Clone/Merge Title',description: 'Keys to press to execute the Clone/Merge Title bookmarklet.',defaultValue: []});}const mangadexChangeMainTitleLang = new MangadexChangeMainTitleLang();if (mangadexChangeMainTitleLang.isWebsite()) {GM_registerMenuCommand('[MangaDex] Change Main Title Language v1.1', () =>mangadexChangeMainTitleLang.execute());settings.push({id: 'mangadex-change_main_title_lang_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Change Main Title Language',description: 'Keys to press to execute the Change Main Title Language bookmarklet.',defaultValue: []});}const amazonDownloadCovers = new AmazonDownloadCovers();if (amazonDownloadCovers.isWebsite()) {GM_registerMenuCommand('[Amazon] Download Covers v3.8', () =>amazonDownloadCovers.execute());settings.push({id: 'amazon-download_covers_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Download Covers',description: 'Keys to press to execute the Download Covers bookmarklet.',defaultValue: []});}const bookwalkerDownloadCovers = new BookwalkerDownloadCovers();if (bookwalkerDownloadCovers.isWebsite()) {GM_registerMenuCommand('[BookWalker] Download Covers v2.9', () =>bookwalkerDownloadCovers.execute());settings.push({id: 'bookwalker-download_covers_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Download Covers',description: 'Keys to press to execute the Download Covers bookmarklet.',defaultValue: []});}const bookliveDownloadCovers = new BookliveDownloadCovers();if (bookliveDownloadCovers.isWebsite()) {GM_registerMenuCommand('[BookLive] Download Covers v2.2', () =>bookliveDownloadCovers.execute());settings.push({id: 'booklive-download_covers_key_shortcut',type: 'keys',name: 'Keyboard Shortcut for Download Covers',description: 'Keys to press to execute the Download Covers bookmarklet.',defaultValue: []});}const settingsField = new SettingsField({id: '1ed69755-08c1-4d22-8a7d-6c4377102cc7',name: 'UserScript',description: 'Settings only available when using the UserScript (reload the page to apply changes).',settings});universalSettings.additionalFields.push(settingsField);settingsField.load();const universalSettingsKeyShortcut = settingsField.getValue('universal-settings_key_shortcut');if (universalSettingsKeyShortcut && universalSettingsKeyShortcut.length > 0) {addKeyShortcutListener(universalSettingsKeyShortcut, () => universalSettings.execute());}const mangadexExportTitleListKeyShortcut = settingsField.getValue('mangadex-export_title_list_key_shortcut');if (mangadexExportTitleListKeyShortcut && mangadexExportTitleListKeyShortcut.length > 0) {addKeyShortcutListener(mangadexExportTitleListKeyShortcut, () => mangadexExportTitleList.execute());}const mangadexShowCoverDataKeyShortcut = settingsField.getValue('mangadex-show_cover_data_key_shortcut');if (mangadexShowCoverDataKeyShortcut && mangadexShowCoverDataKeyShortcut.length > 0) {addKeyShortcutListener(mangadexShowCoverDataKeyShortcut, () => mangadexShowCoverData.execute());}const mangadexUploadCoversFromLinksKeyShortcut = settingsField.getValue('mangadex-upload_covers_from_links_key_shortcut');if (mangadexUploadCoversFromLinksKeyShortcut && mangadexUploadCoversFromLinksKeyShortcut.length > 0) {addKeyShortcutListener(mangadexUploadCoversFromLinksKeyShortcut, () => mangadexUploadCoversFromLinks.execute());}const mangadexAddCoverDescriptionsKeyShortcut = settingsField.getValue('mangadex-add_cover_descriptions_key_shortcut');if (mangadexAddCoverDescriptionsKeyShortcut && mangadexAddCoverDescriptionsKeyShortcut.length > 0) {addKeyShortcutListener(mangadexAddCoverDescriptionsKeyShortcut, () => mangadexAddCoverDescriptions.execute());}const mangadexSearchMissingLinksKeyShortcut = settingsField.getValue('mangadex-search_missing_links_key_shortcut');if (mangadexSearchMissingLinksKeyShortcut && mangadexSearchMissingLinksKeyShortcut.length > 0) {addKeyShortcutListener(mangadexSearchMissingLinksKeyShortcut, () => mangadexSearchMissingLinks.execute());}const mangadexShortenLinksKeyShortcut = settingsField.getValue('mangadex-shorten_links_key_shortcut');if (mangadexShortenLinksKeyShortcut && mangadexShortenLinksKeyShortcut.length > 0) {addKeyShortcutListener(mangadexShortenLinksKeyShortcut, () => mangadexShortenLinks.execute());}const mangadexOpenLinksKeyShortcut = settingsField.getValue('mangadex-open_links_key_shortcut');if (mangadexOpenLinksKeyShortcut && mangadexOpenLinksKeyShortcut.length > 0) {addKeyShortcutListener(mangadexOpenLinksKeyShortcut, () => mangadexOpenLinks.execute());}const mangadexDelCoversByLangKeyShortcut = settingsField.getValue('mangadex-del_covers_by_lang_key_shortcut');if (mangadexDelCoversByLangKeyShortcut && mangadexDelCoversByLangKeyShortcut.length > 0) {addKeyShortcutListener(mangadexDelCoversByLangKeyShortcut, () => mangadexDelCoversByLang.execute());}const mangadexSearchAllTitlesKeyShortcut = settingsField.getValue('mangadex-search_all_titles_key_shortcut');if (mangadexSearchAllTitlesKeyShortcut && mangadexSearchAllTitlesKeyShortcut.length > 0) {addKeyShortcutListener(mangadexSearchAllTitlesKeyShortcut, () => mangadexSearchAllTitles.execute());}const mangadexCloneTitleKeyShortcut = settingsField.getValue('mangadex-clone_title_key_shortcut');if (mangadexCloneTitleKeyShortcut && mangadexCloneTitleKeyShortcut.length > 0) {addKeyShortcutListener(mangadexCloneTitleKeyShortcut, () => mangadexCloneTitle.execute());}const mangadexChangeMainTitleLangKeyShortcut = settingsField.getValue('mangadex-change_main_title_lang_key_shortcut');if (mangadexChangeMainTitleLangKeyShortcut && mangadexChangeMainTitleLangKeyShortcut.length > 0) {addKeyShortcutListener(mangadexChangeMainTitleLangKeyShortcut, () => mangadexChangeMainTitleLang.execute());}const amazonDownloadCoversKeyShortcut = settingsField.getValue('amazon-download_covers_key_shortcut');if (amazonDownloadCoversKeyShortcut && amazonDownloadCoversKeyShortcut.length > 0) {addKeyShortcutListener(amazonDownloadCoversKeyShortcut, () => amazonDownloadCovers.execute());}const bookwalkerDownloadCoversKeyShortcut = settingsField.getValue('bookwalker-download_covers_key_shortcut');if (bookwalkerDownloadCoversKeyShortcut && bookwalkerDownloadCoversKeyShortcut.length > 0) {addKeyShortcutListener(bookwalkerDownloadCoversKeyShortcut, () => bookwalkerDownloadCovers.execute());}const bookliveDownloadCoversKeyShortcut = settingsField.getValue('booklive-download_covers_key_shortcut');if (bookliveDownloadCoversKeyShortcut && bookliveDownloadCoversKeyShortcut.length > 0) {addKeyShortcutListener(bookliveDownloadCoversKeyShortcut, () => bookliveDownloadCovers.execute());}
})();