// ==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 // @downloadURL none // ==/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' } }] }; /** * * * * **/ 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); }; /** * * * **/ 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); }; /** * * * **/ 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();}); })();