Roler's Bookmarklets

Various simple bookmarklets

// ==UserScript==
// @name         Roler's Bookmarklets
// @namespace    https://github.com/rRoler/bookmarklets
// @version      1.1.8
// @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      viewer-epubs-trial.bookwalker.jp
// @connect      res.booklive.jp
// @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
 */

const wsrvUrl = 'https://wsrv.nl';
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);

let isUserScript = false;
const userAgentDesktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0';
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 getMatch(string, regex) {
  let index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
  const regexMatches = string.match(regex);
  if (regexMatches && regexMatches[index]) return regexMatches[index];
}
function splitArray(array) {
  let chunkSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100;
  const arrayCopy = [...array];
  const resArray = [];
  while (arrayCopy.length) resArray.push(arrayCopy.splice(0, chunkSize));
  return resArray;
}
function waitForElement(reference) {
  let noElement = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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) {
  let path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '/';
  let query = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
  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;
}
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 = function (str) {
    let removeExtension = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
    return 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);
}
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) {
  let parent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 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) {
  let indent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
  return `${'\t'.repeat(indent)}<${name}>${value}</${name}>\n`;
}

class Component {
  constructor() {
    let componentElement = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document.createElement('div');
    let {
      defaultStyles = true,
      defaultEvents = true
    } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    this.componentElement = componentElement;
    if (defaultStyles) this.setDefaultStyles();
    if (defaultEvents) this.addDefaultEvents();
  }
  setDefaultStyles() {
    let element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.componentElement;
    setStyles(element, {
      color: componentColors.text,
      'font-family': 'Poppins,Verdana,sans-serif !important',
      'font-size': '16px',
      'font-weight': 'normal',
      'line-height': '20px'
    });
  }
  addDefaultEvents() {
    let element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.componentElement;
    waitForElement(element).then(() => {
      element.dispatchEvent(new CustomEvent('componentadded'));
      waitForElement(element, true).then(() => {
        element.dispatchEvent(new CustomEvent('componentremoved'));
        this.addDefaultEvents();
      });
    });
  }
  add() {
    let parent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 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() {
    let defaultValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
    let {
      labelText
    } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    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];
    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() {
    let defaultValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
    let {
      rows = 5,
      cols = 10,
      labelText
    } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    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;
    setStyles(textarea, {
      'font-size': '18px',
      border: `1px solid ${componentColors.secondary}`,
      'border-radius': '4px',
      'background-color': componentColors.background,
      padding: '2px 8px'
    });
    textarea.setAttribute('id', textareaId);
    textarea.setAttribute('rows', rows.toString());
    textarea.setAttribute('cols', cols.toString());
    this.componentElement.append(textarea);
    this.textareaElement = textarea;
  }
}

class Select extends Component {
  constructor(values) {
    let {
      labelText
    } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    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() {
    let callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : () => {};
    let {
      labelText
    } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    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);
};

class Modal extends Component {
  constructor(_ref) {
    let {
      title,
      content,
      buttons
    } = _ref;
    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': '24px',
      '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) {
  let defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  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) {
  let defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
  const textarea = new TextArea(defaultValue, {
    labelText: text
  });
  setStyles(textarea.textareaElement, {
    width: '90%'
  });
  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: textarea.componentElement,
    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) {
  let defaultValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
  const selectedOptions = [];
  const checkboxes = options.map(option => {
    const checkbox = new Checkbox(checked => {
      if (checked) {
        selectedOptions.push(option);
      } else {
        selectedOptions.splice(selectedOptions.indexOf(option), 1);
      }
    }, {
      labelText: option
    });
    return {
      value: option,
      element: checkbox.componentElement,
      checkboxElement: checkbox.checkboxElement
    };
  });
  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(...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)));
}

class KeyRecorder extends Component {
  keys = [];
  isRecording = false;
  recordButton = (() => new PrimaryButton('+', () => this.isRecording ? this.stop() : this.record()))();
  keyButtonsElement = (() => document.createElement('span'))();
  keyListeners = [];
  constructor(_ref) {
    let {
      keys,
      onChange
    } = _ref;
    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) : 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 => {
        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]+']];
        toReplace.forEach(strings => route = route.replaceAll(`:${strings[0]}`, strings[1]));
        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'
    });
  }
}

// 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;
}());

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() {
    let {
      maxValue,
      minValue,
      currentValue
    } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    this.reset({
      maxValue,
      minValue,
      currentValue
    });
    this.add();
  }
  update() {
    let currentValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 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() {
    let {
      maxValue,
      minValue,
      currentValue
    } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    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 = function (nameParts) {
  let isPath = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  return 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}](${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) {
    let {
      loadMax = 1,
      title,
      fileNamePrefix = 'Volume',
      disableCropping = false
    } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    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) {
    var _this = this;
    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 = function () {
      let select = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 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(_ref => {
      let [start, end] = _ref;
      return 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 new Promise((resolve, reject) => {
      try {
        GM_xmlhttpRequest({
          url: cover.url,
          method: 'GET',
          responseType: 'blob',
          anonymous: true,
          headers: {
            Origin: window.location.origin,
            Referer: window.location.href
          },
          onload: response => {
            if (response.status < 200 || response.status > 299) return reject(response.statusText);
            try {
              resolve(this.setBlob(cover, response.response));
            } catch (error) {
              reject(error);
            }
          },
          onerror: reject,
          onabort: reject,
          ontimeout: reject
        });
      } catch (error) {
        fetch(cover.url).then(response => {
          if (!response.ok) throw new Error(response.statusText);
          return response.blob();
        }).then(blob => resolve(this.setBlob(cover, blob))).catch(reject);
      }
    });
  }
  selectRange(rangeStartCover, rangeEndCover) {
    let select = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 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() {
    let covers = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.covers;
    let force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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;
      window.open(cover.cropped ? cover.editedUrl || cover.blobUrl || cover.url : cover.url, '_blank');
    });
  }
  copy() {
    let clipboardText = '';
    this.covers.forEach(cover => {
      if (!cover.selected) return;
      clipboardText += this.parseFilename(this.settings.copyFormat, cover) || '';
    });
    navigator.clipboard.writeText(clipboardText).then(() => console.debug('Copied to clipboard:', clipboardText), () => {
      console.error('Failed to copy to clipboard:', clipboardText);
      alertModal('Failed to copy to clipboard!\n' + clipboardText, 'error').catch(console.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 progressBar = new SimpleProgressBar();
    const onError = error => {
      console.error(error);
      progressBar.remove();
      this.busy = false;
      this.updateButtons();
      alertModal('Failed to zip covers!\n' + error, 'error').catch(console.error);
    };
    const chunks = [];
    const zip = new Zip((error, chunk, final) => {
      if (error) onError(error);else chunks.push(chunk);
      if (final) {
        progressBar.remove();
        this.busy = false;
        this.updateButtons();
        if (this.aborted) return;
        saveFile(new Blob(chunks, {
          type: 'application/zip'
        }), filename, {
          path: this.settings.paths?.enabled ? path : undefined
        });
      }
    });
    const covers = this.covers.filter(cover => cover.selected && cover.blob);
    progressBar.start({
      maxValue: covers.length
    });
    for (const cover of covers) {
      if (this.aborted) {
        zip.end();
        break;
      }
      try {
        await this.zipCover(zip, cover);
        progressBar.update();
      } catch (error) {
        zip.end();
        onError(error);
        break;
      }
      if (progressBar.currentValue >= progressBar.maxValue) zip.end();
    }
  }
  async zipCover(zip, cover) {
    return await new Promise((resolve, reject) => {
      if (!cover.blob) throw new Error('No blob');
      const reader = new FileReader();
      reader.addEventListener('load', event => {
        if (!event.target) return reject('No target');
        const data = new Uint8Array(event.target.result);
        const file = new ZipPassThrough(`${cover.filename}.${cover.extension}`);
        zip.add(file);
        file.push(data, true);
        resolve();
      });
      reader.addEventListener('error', reject);
      reader.readAsArrayBuffer(cover.blob);
    });
  }
}

class MangadexBookmarklet extends Bookmarklet {
  website = '^mangadex.org|canary.mangadex.dev';
}

const titleRoute = '/title/:uuid';
const titleEditRoute = '/title/edit/:uuid';
const titleEditDraftRoute = '/title/edit/:uuid?draft=true';
const titleCreateRoute = '/create/title';
const titleEditRoutes = [titleEditRoute, titleEditDraftRoute];
const titleId = function () {
  let path = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.pathname;
  return getMatch(path, /\/title\/(?:edit\/)?([-0-9a-f]{20,})/, 1);
};
const titleIsDraft = function () {
  let search = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.search;
  return /draft=true/.test(search);
};
const titleEditInputs = indexes => Array.from(document.querySelectorAll('div.input-container')).flatMap((div, index) => indexes && indexes.includes(index) ? Array.from(div.querySelectorAll('input.inline-input')) : []);
const titleEditInputValues = indexes => titleEditInputs(indexes).map(input => input.value.trim()).filter(value => value);
const listId = function () {
  let path = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.pathname;
  return getMatch(path, /\/list\/([-0-9a-f]{20,})/, 1);
};
const chapterId = function () {
  let path = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.pathname;
  return getMatch(path, /\/chapter\/([-0-9a-f]{20,})/, 1);
};
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 useComponents = () => setComponentColors({
  text: mdComponentColors.color,
  primary: mdComponentColors.primary,
  secondary: mdComponentColors.buttonAccent,
  background: mdComponentColors.background,
  accent: mdComponentColors.accent,
  warning: mdComponentColors.statusYellow,
  error: mdComponentColors.statusRed
});
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 getUserRoleColor = roles => {
  for (const role in roleColors) {
    if (roles.includes(role)) return roleColors[role];
  }
  return roleColors.ROLE_USER;
};
const authToken = () => parseStorage('oidc.user:https://auth.mangadex.org/realms/mangadex:mangadex-frontend-stable') || parseStorage('oidc.user:https://auth.mangadex.org/realms/mangadex:mangadex-frontend-canary');
const storage = () => parseStorage('md');
const locale = () => storage()?.userPreferences?.interfaceLocale || storage()?.userPreferences?.locale || 'en';
const localTime = function () {
  let date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Date.now();
  return new Date(date).toLocaleString(locale(), {
    hour12: false
  });
};
const langDisplayName = () => new Intl.DisplayNames([locale()], {
  type: 'language'
});

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 elements = Array.from(document.querySelectorAll('div.page-sizer'));
    for (const element of elements) {
      if (/blob:https?:\/\/.*mangadex.*\/+[-0-9a-f]{20,}/.test(element.querySelector('.page').style.getPropertyValue('background-image'))) {
        const coverDescription = parseDescription(element, defaultDescription);
        const edit = element.parentElement?.querySelector('.volume-edit');
        edit?.dispatchEvent(new MouseEvent('click'));
        const changed = await setDescription(coverDescription);
        if (changed) changedDescriptions.push(element);
      }
    }
    if (changedDescriptions.length <= 0) return alertModal('No newly added covers with empty descriptions found!');
    console.log('Added descriptions:', changedDescriptions);
    function parseDescription(element, description) {
      const volumeElement = element.parentElement?.querySelector('.volume-num input');
      const volume = volumeElement?.value;
      const languageElement = element.parentElement?.querySelector('.md-select .md-select-inner-wrap .placeholder-text');
      const language = languageElement?.innerText;
      const masks = {
        volume: volume || 'No Volume',
        language: language || 'No Language'
      };
      for (const mask in masks) {
        const maskValue = masks[mask];
        if (maskValue) {
          description = description.replaceAll(`$${mask}`, maskValue);
        }
      }
      return description;
    }
    function setDescription(description) {
      return new Promise(resolve => {
        const selectors = '.md-modal__box .md-textarea__input';
        waitForElement(selectors).then(element => {
          let changed = true;
          const save = element?.parentElement?.parentElement?.parentElement?.parentElement?.querySelector('button.primary');
          if (!element.value) element.value = description;else changed = false;
          element?.dispatchEvent(new InputEvent('input'));
          setTimeout(() => {
            save?.dispatchEvent(new MouseEvent('click'));
            waitForElement(selectors, true).then(() => resolve(changed));
          }, 2);
        });
      });
    }
  };
}

class AmazonBookmarklet extends Bookmarklet {
  website = 'www.amazon.*';
}

class BookliveBookmarklet extends Bookmarklet {
  website = 'booklive.jp';
}

class BookwalkerBookmarklet extends Bookmarklet {
  website = '^((r18|global|viewer-trial).)?bookwalker.jp';
}

class UniversalSettings extends UniversalBookmarklet {
  additionalFields = [];
  main = () => {
    const fields = [];
    if (new MangadexBookmarklet().isWebsite()) {
      useComponents();
      fields.push(mangadexAddCoverDescriptionsSettings);
    } 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 FetchClient {
  queue = [];
  processing = false;
  abortControllers = (() => new Map())();
  bucketLastRefill = (() => Date.now())();
  activeRequests = 0;
  constructor() {
    let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    const {
      rateLimitRequests = Infinity,
      rateLimitTime = 1000
    } = options;
    this.rateLimitRequests = rateLimitRequests;
    this.rateLimitTime = rateLimitTime;
    this.bucketTokens = rateLimitRequests;
  }
  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 requestId = init?.requestId || crypto.randomUUID();
    const abortController = new AbortController();
    this.abortControllers.set(requestId, abortController);
    const _request = async () => {
      try {
        const response = await fetch(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 fetchClient = new FetchClient({
  rateLimitRequests: 5,
  rateLimitTime: 1000
});
const baseUrl = 'https://api.mangadex.org';
async function responsePromise(_ref) {
  let {
    path,
    query,
    method = 'GET',
    body,
    useAuth = false,
    contentType
  } = _ref;
  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(_ref2) {
  let {
    options,
    offset = 0,
    limit = 10000,
    collectionLimit = 100,
    callback
  } = _ref2;
  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() {
  let id = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : titleId();
  let isDraft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleIsDraft();
  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) {
  let id = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleId();
  return await responsePromise({
    path: `/manga/${id}`,
    method: 'PUT',
    body: JSON.stringify(data),
    useAuth: true,
    contentType: 'application/json'
  });
}
async function getMangaList() {
  let {
    title,
    ids = [titleId()],
    includes = [],
    contentRating = ['safe', 'suggestive', 'erotica', 'pornographic'],
    offset,
    limit,
    callback
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  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) {
  let id = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleId();
  return await responsePromise({
    path: `/manga/${id}/relation`,
    method: 'POST',
    body: JSON.stringify(data),
    useAuth: true,
    contentType: 'application/json'
  });
}
async function uploadCover(data) {
  let id = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleId();
  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() {
  let {
    mangaIds = [titleId()],
    order = {},
    includes = [],
    offset,
    limit,
    callback
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  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 getChapter() {
  let id = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : chapterId();
  let includes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
  const query = {
    'includes[]': includes
  };
  return await responsePromise({
    path: `/chapter/${id}`,
    query
  });
}
async function getChapterList() {
  let {
    title,
    ids = [titleId()],
    manga,
    includes = [],
    contentRating = ['safe', 'suggestive', 'erotica', 'pornographic'],
    offset,
    limit,
    callback
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  const query = {
    'includes[]': includes,
    'contentRating[]': contentRating
  };
  if (title) query['title'] = title;
  if (ids) query['ids[]'] = ids;
  if (manga) query['manga'] = manga;
  return await collectionResponsePromise({
    options: {
      path: '/chapter',
      query
    },
    offset,
    limit,
    callback: callback
  });
}
async function getMangaStatuses() {
  let {
    status
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  const query = {};
  if (status) query['status'] = status;
  return await responsePromise({
    path: '/manga/status',
    query,
    useAuth: true
  });
}
async function getReadMarkers() {
  let {
    mangaIds = [titleId()],
    grouped = true
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  const query = {
    'ids[]': mangaIds,
    grouped: grouped
  };
  return await responsePromise({
    path: '/manga/read',
    query,
    useAuth: true
  });
}
async function getCustomList() {
  let {
    id = listId(),
    visibility
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  return await responsePromise({
    path: `/list/${id}`,
    useAuth: visibility !== 'public'
  });
}
async function getMangaStatistics() {
  let {
    mangaIds = [titleId()]
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  const query = {
    'manga[]': mangaIds
  };
  return await responsePromise({
    path: '/statistics/manga',
    query
  });
}
async function getMangaRatings() {
  let {
    mangaIds = [titleId()]
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  const query = {
    'manga[]': mangaIds
  };
  return await responsePromise({
    path: '/rating',
    query,
    useAuth: true
  });
}
async function getChapterStatistics() {
  let {
    chapterIds = [chapterId()]
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  const query = {
    'chapter[]': chapterIds
  };
  return await responsePromise({
    path: '/statistics/chapter',
    query
  });
}
async function getLoggedUser() {
  return await responsePromise({
    path: '/user/me',
    useAuth: true
  });
}

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',
      author: 'Authors',
      year: 'Publication Year',
      publication: 'Publication Status',
      mangaThread: 'Manga Forum Thread ID',
      isOneshot: 'Is Oneshot',
      lastVolume: 'Last Published Volume',
      lastChapter: 'Last Published Chapter',
      readingStatus: listId$1 ? 'List Name' : 'Reading Status',
      readVolume: 'Latest Read Volume',
      readChapter: 'Latest Read Chapter',
      readChapterScans: 'Latest Read Chapter Scanlation Groups',
      myRating: 'My Rating',
      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,
      askForTitleLang: 'Ask for Preferred Title Language',
      missingChapters: 'Include Missing Chapters (slow)',
      updateOnImport: 'Update on Import',
      excludeNoMal: 'Exclude Titles with no MyAnimeList ID'
    };
    const includeDataOptions = [allIncludeDataOptions.askForTitleLang, allIncludeDataOptions.missingChapters];
    const defaultIncludeDataOptions = [allIncludeDataOptions.askForTitleLang];
    switch (exportFormat) {
      case exportFormatOptions.xml:
        {
          includeDataOptions.push(allIncludeDataOptions.updateOnImport, allIncludeDataOptions.excludeNoMal, allIncludeDataOptions.lastVolume, allIncludeDataOptions.lastChapter, allIncludeDataOptions.readingStatus, allIncludeDataOptions.readVolume, allIncludeDataOptions.readChapter, allIncludeDataOptions.readChapterScans, allIncludeDataOptions.myRating);
          defaultIncludeDataOptions.push(allIncludeDataOptions.updateOnImport, allIncludeDataOptions.excludeNoMal, allIncludeDataOptions.lastVolume, allIncludeDataOptions.lastChapter, allIncludeDataOptions.readingStatus, allIncludeDataOptions.readVolume, allIncludeDataOptions.readChapter, allIncludeDataOptions.myRating);
          break;
        }
      case exportFormatOptions.csv:
        {
          includeDataOptions.push(...Object.values(csvDataColumns));
          defaultIncludeDataOptions.push(allIncludeDataOptions.title, allIncludeDataOptions.originalTitle, 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(_ref => {
        let [id, status] = _ref;
        return {
          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 preferredTitleLang;
    if (dataToInclude.includes(allIncludeDataOptions.askForTitleLang) && dataToInclude.includes(allIncludeDataOptions.title)) {
      const allTitleLangs = mangaData.filter(m => Array.isArray(m.attributes.altTitles)).flatMap(m => m.attributes.altTitles.flatMap(altTitle => Object.keys(altTitle))).filter((lang, i, arr) => arr.indexOf(lang) === i);
      preferredTitleLang = await selectModal('Preferred title language', allTitleLangs);
    }
    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
        }).then(response => response.data).catch(onError);
        progressBar.update();
        return data;
      }));
      progressBar.remove();
      chapterData = splitReadChapterData.flat().filter(c => !!c);
      if (dataToInclude.includes(allIncludeDataOptions.missingChapters)) {
        const missingChapterIds = [];
        readChapterIds.forEach(chapterId => {
          const isChapter = chapterData.some(c => c.id === chapterId);
          if (!isChapter) missingChapterIds.push(chapterId);
        });
        progressBar.start({
          maxValue: missingChapterIds.length
        });
        const missingChaptersData = await Promise.all(missingChapterIds.map(async id => {
          const data = await getChapter(id, allIncludeDataOptions.readChapterScans ? ['scanlation_group'] : undefined).then(response => response.data).catch(onError);
          progressBar.update();
          return data;
        }));
        chapterData = [...chapterData, ...missingChaptersData.filter(c => !!c)];
      }
      progressBar.remove();
      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 = preferredTitleLang ? altTitles?.find(t => t[preferredTitleLang])?.[preferredTitleLang] || mainTitle : mainTitle;
      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 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,
        originalTitle: originalTitle,
        allAuthorNames: allAuthorNames,
        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,
        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 || '0', 2);
            xmlMangaSection += formatXMLTag('my_tags', '<![CDATA[]]>', 2);
            xmlMangaSection += formatXMLTag('my_reread_value', '', 2);
            xmlMangaSection += formatXMLTag('update_on_import', updateOnImport || '0', 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('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.author, manga.allAuthorNames], [allIncludeDataOptions.year, manga.attributes.year], [allIncludeDataOptions.publication, capitalizeFirstLetter(manga.attributes.status)], [allIncludeDataOptions.mangaThread, manga.mangaThreadId], [allIncludeDataOptions.isOneshot, manga.isOneshot], [allIncludeDataOptions.lastVolume, manga.lastVolume], [allIncludeDataOptions.lastChapter, manga.lastChapter], [allIncludeDataOptions.readingStatus, listId$1 ? manga.listName : manga.readingStatus ? capitalizeFirstLetter(manga.readingStatus).replaceAll('_', ' ') : ''], [allIncludeDataOptions.readVolume, manga.latestReadVolume], [allIncludeDataOptions.readChapter, manga.latestReadChapter], [allIncludeDataOptions.readChapterScans, manga.latestReadChapterScans], [allIncludeDataOptions.myRating, manga.myRating], [allIncludeDataOptions.readChapterThread, manga.latestReadChapterThreadId], [allIncludeDataOptions.anilist, manga.attributes.links?.al], [allIncludeDataOptions.animePlanet, manga.attributes.links?.ap], [allIncludeDataOptions.kitsu, manga.attributes.links?.kt], [allIncludeDataOptions.mangaUpdates, manga.attributes.links?.mu], [allIncludeDataOptions.myAnimeList, manga.attributes.links?.mal], [allIncludeDataOptions.novelUpdates, manga.attributes.links?.nu], [allIncludeDataOptions.bookWalker, manga.attributes.links?.bw], [allIncludeDataOptions.amazon, manga.attributes.links?.amz], [allIncludeDataOptions.ebookJapan, manga.attributes.links?.ebj], [allIncludeDataOptions.cdJapan, 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 = `https://mangadex.org/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 = function (event) {
        let show = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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(_ref => {
        let [key, value] = _ref;
        return `${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();
          window.open(`/user/${user.id}`, '_blank');
        });
      }
      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());
        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());
        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) {
      let offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 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: ['safe', 'suggestive', 'erotica', 'pornographic'],
          offset: offset,
          limit: requestLimit
        }).then(resolve).catch(reject);
      });
    }
  };
}

class MangadexSearchMissingLinks extends MangadexBookmarklet {
  routes = (() => [titleRoute, ...titleEditRoutes, titleCreateRoute])();
  main = async () => {
    useComponents();
    const websites = {
      al: 'https://anilist.co/search/manga?search=',
      ap: 'https://www.anime-planet.com/manga/all?name=',
      kt: 'https://kitsu.io/manga?subtype=manga&text=',
      mu: 'https://www.mangaupdates.com/search.html?search=',
      mal: 'https://myanimelist.net/manga.php?q=',
      nu: 'https://www.novelupdates.com/series-finder/?sf=1&sh=',
      bw: 'https://bookwalker.jp/search/?qcat=2&word=',
      amz: 'https://www.amazon.co.jp/s?rh=n:466280&k=',
      ebj: 'https://ebookjapan.yahoo.co.jp/search/?keyword=',
      cdj: 'https://www.cdjapan.co.jp/searchuni?term.media_format=BOOK&q='
    };
    if (/\/create\/title/.test(window.location.pathname)) {
      const inputTitles = titleEditInputValues([0, 1]);
      const title = await promptModal('Enter a title to search for', inputTitles.length > 0 ? inputTitles : '');
      if (!title) return;
      for (const website in websites) window.open(websites[website] + title, '_blank', 'noopener,noreferrer');
      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];
      let title = originalTitle ? originalTitle[originalLang] : titleInfo.data.attributes.title[mainTitleLang] || '';
      title = await promptModal('Enter a title to search for', [title, ...(altTitles?.map(_title => _title[Object.keys(_title)[0]]).filter(_title => _title !== title) || [])]);
      if (!title) return;
      missingWebsites.forEach(website => window.open(websites[website] + title, '_blank', 'noopener,noreferrer'));
    });
  };
}

class MangadexShortenLinks extends MangadexBookmarklet {
  routes = (() => [...titleEditRoutes, titleCreateRoute])();
  main = async () => {
    useComponents();
    const inputs = titleEditInputs([3, 4, 5]);
    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 regexes = [`(anilist.co/manga/)(${numIdRegex})`, `(www.anime-planet.com/manga/)(${numAndLetterIdRegex})`, `(kitsu.(?:io|app)/manga/)(${numAndLetterIdRegex})`, `(www.mangaupdates.com/series/)(${numAndLetterIdRegex})`, `(myanimelist.net/manga/)(${numIdRegex})`, `(bookwalker.jp/series/)(${numIdRegex}(?:/list)?)`, `(bookwalker.jp/)(${numAndLetterIdRegex})`, `(www.amazon[a-z.]+/).*((?:dp/|gp/product/|kindle-dbs/product/)${asinRegex})`, `(www.amazon[a-z.]+/gp/product).*(/${asinRegex})`, `(ebookjapan.yahoo.co.jp/books/)(${numIdRegex})`, `(www.cdjapan.co.jp/product/)(NEOBK-${numIdRegex})`, '(.*/)(.*)/$'];
    progressBar.start();
    await Promise.all(inputs.map(async element => {
      const link = element.value.trim();
      let shortLink = link;
      for (const regexPattern of regexes) {
        const regex = new RegExp(`(?:https?://${regexPattern}.*)$`);
        const websiteUrl = getMatch(link, regex, 1);
        let id = getMatch(link, regex, 2);
        if (websiteUrl && id) {
          if (/^kitsu.(io|app)\/manga\/$/.test(websiteUrl) && !new RegExp(`^${numIdRegex}$`).test(id)) {
            try {
              const slugResponse = await fetch(`https://${websiteUrl.replace('/manga/', '')}/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 MangadexOpenLinks extends MangadexBookmarklet {
  routes = (() => [titleRoute, ...titleEditRoutes, titleCreateRoute])();
  main = async () => {
    const titleId$1 = titleId();
    const inputLinks = titleEditInputValues([3, 4, 5]);
    const links = [];
    if (inputLinks.length <= 0 && titleId$1) {
      const titleInfo = await getManga(titleId$1);
      if (titleInfo.data.attributes.links) {
        const websites = {
          al: 'https://anilist.co/manga/',
          ap: 'https://www.anime-planet.com/manga/',
          kt: 'https://kitsu.io/manga/',
          mu: /[A-Za-z]/.test(titleInfo.data.attributes.links.mu) ? 'https://www.mangaupdates.com/series/' : 'https://www.mangaupdates.com/series.html?id=',
          mal: 'https://myanimelist.net/manga/',
          nu: 'https://www.novelupdates.com/series/',
          bw: 'https://bookwalker.jp/',
          amz: '',
          ebj: '',
          cdj: ''
        };
        for (const website in titleInfo.data.attributes.links) {
          const websiteUrl = websites[website] || '';
          const link = websiteUrl + titleInfo.data.attributes.links[website];
          links.push(link);
        }
      }
    } else links.push(...inputLinks);
    links.forEach(link => window.open(link, '_blank', 'noopener,noreferrer'));
  };
}

class MangadexDelCoversByLang extends MangadexBookmarklet {
  routes = (() => [...titleEditRoutes, titleCreateRoute])();
  main = async () => {
    useComponents();
    const languages = Array.from(new Set(Array.from(document.querySelectorAll('div.page-sizer')).map(element => {
      const parent = element.parentElement;
      if (!parent) return;
      const language = parent.querySelector('.placeholder-text.with-label');
      if (!language) return;
      return language.innerText.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 = [];
    document.querySelectorAll('div.page-sizer').forEach(element => {
      const parent = element.parentElement;
      if (!parent) return;
      const close = parent.querySelector('.close');
      const language = parent.querySelector('.placeholder-text.with-label');
      if (!close || !language) return;
      if (selectedLanguage === language.innerText.trim()) {
        close.dispatchEvent(new MouseEvent('click'));
        deletedCovers.push(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 () => {
    const titleId$1 = titleId();
    const inputTitles = titleEditInputValues([0, 1]);
    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: ['safe', 'suggestive', 'erotica', 'pornographic']
      });
      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 => window.open(createUrl(`https://${window.location.hostname}`, '/titles', {
      q: title,
      content: 'safe,suggestive,erotica,pornographic'
    }), '_blank'));
  };
}

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 = function (type) {
      let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleInfo.data.relationships;
      return 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.search);
      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 = function () {
        let relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : titleInfo.data.relationships;
        return 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 function () {
        let mangaId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : titleInfo.data.id;
        return 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');
    }
    window.open(`/title/edit/${createdTitle.data.id}${createdTitle.data.attributes.state === 'draft' ? '?draft=true' : ''}`, '_blank');
  };
}

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 => `https://${window.location.host}/images/P/${asin}.01.MAIN._SCRM_.jpg`;
    const books = function () {
      let element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
      return element.querySelectorAll('a.itemImageLink');
    };
    let downloader;
    const covers = [];
    const locationAsin = getAsin(window.location.pathname);
    if (!locationAsin) {
      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=${locationAsin}&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(locationAsin),
          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 = 'https://c.roler.dev/bw';
    const getCdnBookUrl = bookId => `${cdnBaseUrl}/${bookId}?crop=false`;
    let downloader;
    const covers = [];
    if (window.location.hostname === 'viewer-trial.bookwalker.jp') {
      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^="https://${window.location.host}/series/"]`)?.textContent
      });
    } else if (/series\/\d+/.test(window.location.pathname)) {
      const seriesId = getSeriesId(window.location.pathname);
      const lastPage = function () {
        let element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
        return 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: `https://res.booklive.jp/${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: `https://res.booklive.jp/${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.2', () =>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.2', () =>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.2', () =>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 mangadexAddCoverDescriptions = new MangadexAddCoverDescriptions();if (mangadexAddCoverDescriptions.isWebsite()) {GM_registerMenuCommand('[MangaDex] Add Cover Descriptions v3.0', () =>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 v2.8', () =>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.0', () =>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.2', () =>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.4', () =>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.3', () =>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 v1.7', () =>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 amazonDownloadCovers = new AmazonDownloadCovers();if (amazonDownloadCovers.isWebsite()) {GM_registerMenuCommand('[Amazon] Download Covers v3.4', () =>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.6', () =>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 v1.9', () =>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 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 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());}
})();