Greasy Fork

Roler's Bookmarklets

Various simple bookmarklets

// ==UserScript==
// @name         Roler's Bookmarklets
// @namespace    https://github.com/rRoler/bookmarklets
// @version      1.1.3
// @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      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 userAgentDesktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0';
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;
}

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

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.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) return;
    switch (setting.type) {
      case 'text':
      case 'textarea':
        {
          if (setting.value || setting.value?.trim() === '') return setting.value;
          return setting.defaultValue;
        }
      case 'checkbox':
        {
          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
    }));
    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-direction': 'column',
      '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);
    }
    if (!this.fields.length) {
      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;
    }
    for (const field of this.fields) {
      const fieldElement = document.createElement('div');
      setStyles(fieldElement, {
        width: '100%',
        display: 'flex',
        'flex-direction': 'column',
        '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) {
        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'
        };
        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('Reset', () => {
                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('Reset', () => {
                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('Reset', () => {
                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;
            }
        }
        fieldElement.append(settingElement);
      }
      this.contentContainer.append(fieldElement);
    }
  }
  updateButtons() {
    const hasChanges = this.fields.some(field => field.savedSettings.some(saved => {
      const newSetting = field.newSettings.find(n => n.id === saved.id);
      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 = '.*';
}

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 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 UniversalSettings extends UniversalBookmarklet {
  additionalFields = [];
  main = () => {
    const fields = [];
    if (new MangadexBookmarklet().isWebsite()) {
      fields.push(mangadexAddCoverDescriptionsSettings);
    }
    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
  });
}

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

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');
  };
}

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

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'
    });
  }
}

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 = requireFileSaver_min();
var fileSaver = /*@__PURE__*/getDefaultExportFromCjs(FileSaver_minExports);

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

// aliases for shorter compressed code (most minifers don't do this)
var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
// fixed length extra bits
var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
// fixed distance extra bits
var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
// get base, reverse index map from extra bits
var freb = function (eb, start) {
    var b = new u16(31);
    for (var i = 0; i < 31; ++i) {
        b[i] = start += 1 << eb[i - 1];
    }
    // numbers here are at max 18 bits
    var r = new i32(b[30]);
    for (var i = 1; i < 30; ++i) {
        for (var j = b[i]; j < b[i + 1]; ++j) {
            r[j] = ((j - b[i]) << 5) | i;
        }
    }
    return { b: b, r: r };
};
var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
// we can ignore the fact that the other numbers are wrong; they never happen anyway
fl[28] = 258, revfl[258] = 28;
freb(fdeb, 0);
// map of value to reverse (assuming 16 bits)
var rev = new u16(32768);
for (var i = 0; i < 32768; ++i) {
    // reverse table algorithm from SO
    var x = ((i & 0xAAAA) >> 1) | ((i & 0x5555) << 1);
    x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2);
    x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4);
    rev[i] = (((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8)) >> 1;
}
// fixed length tree
var flt = new u8(288);
for (var i = 0; i < 144; ++i)
    flt[i] = 8;
for (var i = 144; i < 256; ++i)
    flt[i] = 9;
for (var i = 256; i < 280; ++i)
    flt[i] = 7;
for (var i = 280; i < 288; ++i)
    flt[i] = 8;
// fixed distance tree
var fdt = new u8(32);
for (var i = 0; i < 32; ++i)
    fdt[i] = 5;
// typed array slice - allows garbage collector to free original reference,
// while being more compatible than .slice
var slc = function (v, s, e) {
    if (e == null || e > v.length)
        e = v.length;
    // can't use .constructor in case user-supplied
    return new u8(v.subarray(s, e));
};
// error codes
var ec = [
    'unexpected EOF',
    'invalid block type',
    'invalid length/literal',
    'invalid distance',
    'stream finished',
    'no stream handler',
    ,
    'no callback',
    'invalid UTF-8 data',
    'extra field too long',
    'date not in range 1980-2099',
    'filename too long',
    'stream finishing',
    'invalid zip data'
    // determined by unknown compression method
];
var err = function (ind, msg, nt) {
    var e = new Error(msg || ec[ind]);
    e.code = ind;
    if (Error.captureStackTrace)
        Error.captureStackTrace(e, err);
    if (!nt)
        throw e;
    return e;
};
// empty
var et = /*#__PURE__*/ new u8(0);
// CRC32 table
var crct = /*#__PURE__*/ (function () {
    var t = new Int32Array(256);
    for (var i = 0; i < 256; ++i) {
        var c = i, k = 9;
        while (--k)
            c = ((c & 1) && -306674912) ^ (c >>> 1);
        t[i] = c;
    }
    return t;
})();
// CRC32
var crc = function () {
    var c = -1;
    return {
        p: function (d) {
            // closures have awful performance
            var cr = c;
            for (var i = 0; i < d.length; ++i)
                cr = crct[(cr & 255) ^ d[i]] ^ (cr >>> 8);
            c = cr;
        },
        d: function () { return ~c; }
    };
};
// Walmart object spread
var mrg = function (a, b) {
    var o = {};
    for (var k in a)
        o[k] = a[k];
    for (var k in b)
        o[k] = b[k];
    return o;
};
// write bytes
var wbytes = function (d, b, v) {
    for (; v; ++b)
        d[b] = v, v >>>= 8;
};
// text encoder
var te = typeof TextEncoder != 'undefined' && /*#__PURE__*/ new TextEncoder();
// text decoder
var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder();
// text decoder stream
var tds = 0;
try {
    td.decode(et, { stream: true });
    tds = 1;
}
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 CoverDownloader extends Modal {
  knownFileNames = {};
  aborted = false;
  loadMax = 1;
  currentLoad = 0;
  covers = [];
  busy = false;
  constructor(getCovers) {
    let {
      loadMax = 1,
      title,
      fileNamePrefix = 'Volume'
    } = 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.componentElement.addEventListener('componentadded', () => {
      this.aborted = false;
      Object.values(buttons).forEach(button => button.hide());
      this.loadingCircle.add(this.resultsContainer);
      this.loadCovers();
    });
    this.componentElement.addEventListener('componentremoved', () => {
      this.aborted = true;
      this.clearCovers();
    });
  }
  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);
      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);
    let imageUrl = cover.url;
    try {
      await this.download(cover);
      if (cover.blobUrl) imageUrl = cover.blobUrl;
    } catch (error) {
      console.warn('Failed to download cover', cover.url, error);
    }
    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();
    };
    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 imageElementLoaded = await new Promise(resolve => {
      imageElement.onerror = () => resolve(false);
      imageElement.onload = () => resolve(true);
      imageElement.src = imageUrl;
    });
    if (imageElementLoaded) {
      cover.imageElement = imageElement;
      cover.width = imageElement.naturalWidth;
      cover.height = imageElement.naturalHeight;
      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:', imageUrl);
      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) cover.parsedTitle = `${this.fileNamePrefix} ${volumeNumbers.pop()}`.trim();else cover.parsedTitle = cover.title.trim();
  }
  setCoverFilename(cover) {
    const name = 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.covers.every(cover => !cover.cropAmount || !cover.blob || !cover.blobUrl)) this.buttons.crop.hide();else this.buttons.crop.show();
    if (this.busy || select || this.covers.filter(cover => cover.selected && cover.cropAmount && cover.blob && cover.blobUrl).length <= 0) 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) return;
    this.busy = true;
    this.updateButtons();
    const cropped = this.isCropped();
    const coversToCrop = covers.filter(cover => force && cover.imageElement && cover.cropAmount && cover.blob && cover.blobUrl || cover.selected && cover.imageElement && cover.cropAmount && cover.blob && cover.blobUrl);
    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);
          cover.extension = cover.croppedExtension;
          cover.cropped = false;
          await this.loadCover(cover).catch(console.error);
        }
        progressBar.update();
        return;
      }
      try {
        const img = cover.imageElement;
        const width = cover.width;
        const height = cover.height;
        const cropAmount = cover.cropAmount;
        const absoluteCropAmount = Math.abs(cropAmount);
        const croppedWidth = width - absoluteCropAmount;
        const canvas = document.createElement('canvas');
        canvas.width = croppedWidth;
        canvas.height = height;
        const ctx = canvas.getContext('2d');
        if (cropAmount > 0) ctx?.drawImage(img, 0, 0, croppedWidth, height, 0, 0, croppedWidth, height);else if (cropAmount < 0) ctx?.drawImage(img, absoluteCropAmount, 0, croppedWidth, height, 0, 0, croppedWidth, height);
        const blob = await new Promise(resolve => canvas?.toBlob(blob => resolve(blob), 'image/png'));
        if (blob) {
          cover.loaded = false;
          this.removeBlob(cover);
          this.setBlob(cover, blob);
          cover.croppedExtension = cover.extension;
          cover.extension = 'png';
          cover.cropped = true;
          await this.loadCover(cover).catch(console.error);
        }
      } catch (error) {
        console.error('Failed to crop cover:', cover.url, error);
        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.blobUrl && cover.cropped ? cover.blobUrl : cover.url, '_blank');
    });
  }
  copy() {
    let clipboardText = '';
    this.covers.forEach(cover => {
      if (!cover.selected) return;
      clipboardText += cover.url + '\n';
    });
    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);
    });
  }
  filterFileName = name => name.replace(/[\\/:"*?<>|]/g, '_');
  save() {
    this.covers.forEach(cover => {
      if (this.busy || !cover.selected) return;
      const saveName = `${window.location.hostname}/${this.title ? this.filterFileName(this.title) : 'unknown title'}/${this.filterFileName(cover.filename)}.${cover.extension}`;
      const saveFile = () => fileSaver.saveAs(cover.blobUrl || cover.url, saveName.replaceAll('/', ' - '));
      try {
        GM_download({
          url: cover.url,
          name: `covers/${saveName}`,
          // @ts-ignore
          saveAs: false,
          headers: {
            Origin: window.location.origin,
            Referer: window.location.href
          },
          onerror: saveFile
        });
      } catch (error) {
        saveFile();
      }
    });
  }
  async zip() {
    if (this.busy) return;
    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;
        fileSaver.saveAs(new Blob(chunks, {
          type: 'application/zip'
        }), `${window.location.hostname} - ${this.title ? this.filterFileName(this.title) : 'unknown title'} - covers.zip`);
      }
    });
    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);
    });
  }
}

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-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 BookwalkerBookmarklet extends Bookmarklet {
  website = '^((r18|global|viewer-trial).)?bookwalker.jp';
}

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;
    };
    let downloader;
    const covers = [];
    if (window.location.hostname === 'viewer-trial.bookwalker.jp') {
      const bookId = getBookId(window.location.search);
      downloader = new CoverDownloader(async () => {
        const readerInfoUrl = `https://${window.location.host}/trial-page/c?cid=${bookId}&BID=0`;
        const readerInfoResponse = await fetch(readerInfoUrl);
        const readerInfo = await readerInfoResponse.json();
        const readerUrl = readerInfo.cty === 0 ? readerInfo.url + 'normal_default/' : readerInfo.url;
        const authString = `?pfCd=${readerInfo.auth_info.pfCd}&Policy=${readerInfo.auth_info.Policy}&Signature=${readerInfo.auth_info.Signature}&Key-Pair-Id=${readerInfo.auth_info['Key-Pair-Id']}`;
        const configurationUrl = readerUrl + 'configuration_pack.json' + authString;
        const configurationResponse = await fetch(configurationUrl);
        const readerConfiguration = await configurationResponse.json();
        const pages = getPages(readerConfiguration);
        pages.forEach((page, i) => {
          const imageUrl = readerUrl + page.chapter.file + '/' + page.page.No + '.' + page.chapter.type + authString;
          covers.push({
            url: imageUrl,
            title: i.toString()
          });
        });
        return covers;
        function getPages(readerConfiguration) {
          const pages = [];
          for (const chapterInfo of readerConfiguration.configuration.contents) {
            for (const pageInfo of readerConfiguration[chapterInfo.file].FileLinkInfo.PageLinkInfoList) {
              const newPage = {
                page: pageInfo.Page,
                chapter: chapterInfo
              };
              pages.push(newPage);
            }
          }
          return pages;
        }
      }, {
        fileNamePrefix: 'Page',
        title: document.querySelector('title')?.textContent
      });
    } 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 () => {
        covers.push({
          url: `https://c.roler.dev/bw/${bookId}`,
          title: bookTitle
        });
        return covers;
      }, {
        title: document.querySelector(`a[href^="https://${window.location.host}/series/"]`)?.firstChild?.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: `https://c.roler.dev/bw/${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: `https://c.roler.dev/bw/${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 BookliveBookmarklet extends Bookmarklet {
  website = 'booklive.jp';
}

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

const settings = [];const universalSettings = new UniversalSettings();if (universalSettings.isWebsite()) {GM_registerMenuCommand('[Any Website] Settings Manager v1.0', () =>universalSettings.execute());settings.push({id: 'universal-settings_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Settings Manager',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Settings Manager bookmarklet (only the first letter will be used).',defaultValue: 's'});}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: 'text',name: 'Keyboard Shortcut for Show Cover Data',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Show Cover Data bookmarklet (only the first letter will be used).',defaultValue: ''});}const mangadexAddCoverDescriptions = new MangadexAddCoverDescriptions();if (mangadexAddCoverDescriptions.isWebsite()) {GM_registerMenuCommand('[MangaDex] Add Cover Descriptions v2.9', () =>mangadexAddCoverDescriptions.execute());settings.push({id: 'mangadex-add_cover_descriptions_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Add Cover Descriptions',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Add Cover Descriptions bookmarklet (only the first letter will be used).',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: 'text',name: 'Keyboard Shortcut for Search Missing Links',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Search Missing Links bookmarklet (only the first letter will be used).',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: 'text',name: 'Keyboard Shortcut for Shorten Links',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Shorten Links bookmarklet (only the first letter will be used).',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: 'text',name: 'Keyboard Shortcut for Open Links',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Open Links bookmarklet (only the first letter will be used).',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: 'text',name: 'Keyboard Shortcut for Delete Covers by Language',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Delete Covers by Language bookmarklet (only the first letter will be used).',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: 'text',name: 'Keyboard Shortcut for Search All Titles',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Search All Titles bookmarklet (only the first letter will be used).',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: 'text',name: 'Keyboard Shortcut for Clone/Merge Title',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Clone/Merge Title bookmarklet (only the first letter will be used).',defaultValue: ''});}const amazonDownloadCovers = new AmazonDownloadCovers();if (amazonDownloadCovers.isWebsite()) {GM_registerMenuCommand('[Amazon] Download Covers v3.2', () =>amazonDownloadCovers.execute());settings.push({id: 'amazon-download_covers_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Download Covers',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Download Covers bookmarklet (only the first letter will be used).',defaultValue: ''});}const bookwalkerDownloadCovers = new BookwalkerDownloadCovers();if (bookwalkerDownloadCovers.isWebsite()) {GM_registerMenuCommand('[BookWalker] Download Covers v2.4', () =>bookwalkerDownloadCovers.execute());settings.push({id: 'bookwalker-download_covers_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Download Covers',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Download Covers bookmarklet (only the first letter will be used).',defaultValue: ''});}const bookliveDownloadCovers = new BookliveDownloadCovers();if (bookliveDownloadCovers.isWebsite()) {GM_registerMenuCommand('[BookLive] Download Covers v1.7', () =>bookliveDownloadCovers.execute());settings.push({id: 'booklive-download_covers_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Download Covers',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Download Covers bookmarklet (only the first letter will be used).',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')?.trim()?.charAt(0);if (universalSettingsKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === universalSettingsKeyShortcut.toLowerCase())universalSettings.execute();});const mangadexShowCoverDataKeyShortcut = settingsField.getValue('mangadex-show_cover_data_key_shortcut')?.trim()?.charAt(0);if (mangadexShowCoverDataKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexShowCoverDataKeyShortcut.toLowerCase())mangadexShowCoverData.execute();});const mangadexAddCoverDescriptionsKeyShortcut = settingsField.getValue('mangadex-add_cover_descriptions_key_shortcut')?.trim()?.charAt(0);if (mangadexAddCoverDescriptionsKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexAddCoverDescriptionsKeyShortcut.toLowerCase())mangadexAddCoverDescriptions.execute();});const mangadexSearchMissingLinksKeyShortcut = settingsField.getValue('mangadex-search_missing_links_key_shortcut')?.trim()?.charAt(0);if (mangadexSearchMissingLinksKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexSearchMissingLinksKeyShortcut.toLowerCase())mangadexSearchMissingLinks.execute();});const mangadexShortenLinksKeyShortcut = settingsField.getValue('mangadex-shorten_links_key_shortcut')?.trim()?.charAt(0);if (mangadexShortenLinksKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexShortenLinksKeyShortcut.toLowerCase())mangadexShortenLinks.execute();});const mangadexOpenLinksKeyShortcut = settingsField.getValue('mangadex-open_links_key_shortcut')?.trim()?.charAt(0);if (mangadexOpenLinksKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexOpenLinksKeyShortcut.toLowerCase())mangadexOpenLinks.execute();});const mangadexDelCoversByLangKeyShortcut = settingsField.getValue('mangadex-del_covers_by_lang_key_shortcut')?.trim()?.charAt(0);if (mangadexDelCoversByLangKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexDelCoversByLangKeyShortcut.toLowerCase())mangadexDelCoversByLang.execute();});const mangadexSearchAllTitlesKeyShortcut = settingsField.getValue('mangadex-search_all_titles_key_shortcut')?.trim()?.charAt(0);if (mangadexSearchAllTitlesKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexSearchAllTitlesKeyShortcut.toLowerCase())mangadexSearchAllTitles.execute();});const mangadexCloneTitleKeyShortcut = settingsField.getValue('mangadex-clone_title_key_shortcut')?.trim()?.charAt(0);if (mangadexCloneTitleKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexCloneTitleKeyShortcut.toLowerCase())mangadexCloneTitle.execute();});const amazonDownloadCoversKeyShortcut = settingsField.getValue('amazon-download_covers_key_shortcut')?.trim()?.charAt(0);if (amazonDownloadCoversKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === amazonDownloadCoversKeyShortcut.toLowerCase())amazonDownloadCovers.execute();});const bookwalkerDownloadCoversKeyShortcut = settingsField.getValue('bookwalker-download_covers_key_shortcut')?.trim()?.charAt(0);if (bookwalkerDownloadCoversKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === bookwalkerDownloadCoversKeyShortcut.toLowerCase())bookwalkerDownloadCovers.execute();});const bookliveDownloadCoversKeyShortcut = settingsField.getValue('booklive-download_covers_key_shortcut')?.trim()?.charAt(0);if (bookliveDownloadCoversKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === bookliveDownloadCoversKeyShortcut.toLowerCase())bookliveDownloadCovers.execute();});
})();