// ==UserScript==
// @name Roler's Bookmarklets
// @namespace https://github.com/rRoler/bookmarklets
// @version 1.1.3
// @description Various simple bookmarklets
// @author Roler
// @match http*://mangadex.org/*
// @match http*://www.amazon.co.jp/*
// @match http*://www.amazon.com/*
// @match http*://bookwalker.jp/*
// @match http*://r18.bookwalker.jp/*
// @match http*://global.bookwalker.jp/*
// @match http*://viewer-trial.bookwalker.jp/*
// @match http*://booklive.jp/*
// @supportURL https://github.com/rRoler/bookmarklets/issues
// @grant GM_registerMenuCommand
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect c.roler.dev
// @connect www.amazon.co.jp
// @connect www.amazon.com
// @connect viewer-epubs-trial.bookwalker.jp
// @connect res.booklive.jp
// @run-at document-end
// ==/UserScript==
(() => {
/*!
* Licensed under MIT: https://github.com/rRoler/bookmarklets/raw/main/LICENSE
* Third party licenses: https://github.com/rRoler/bookmarklets/raw/main/dist/userscript.dependencies.txt
*/
const userAgentDesktop = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0';
function getMatch(string, regex) {
let index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
const regexMatches = string.match(regex);
if (regexMatches && regexMatches[index]) return regexMatches[index];
}
function splitArray(array) {
let chunkSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100;
const arrayCopy = [...array];
const resArray = [];
while (arrayCopy.length) resArray.push(arrayCopy.splice(0, chunkSize));
return resArray;
}
function waitForElement(reference) {
let noElement = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
const getElement = () => typeof reference === 'string' ? document.body.querySelector(reference) : document.body.contains(reference) ? reference : null;
let element = getElement();
return new Promise(resolve => {
if (noElement ? !element : element) return resolve(element);
const observer = new MutationObserver(() => {
element = getElement();
if (noElement ? !element : element) {
resolve(element);
observer.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
function parseStorage(key) {
const value = localStorage.getItem(key);
if (value) return JSON.parse(value);
}
function saveStorage(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
function createSVG(options) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
if (options.svg.attributes) setAttributes(svg, options.svg.attributes);
if (options.svg.styles) setStyles(svg, options.svg.styles);
for (const pathOptions of options.paths) {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
if (pathOptions.attributes) setAttributes(path, pathOptions.attributes);
if (pathOptions.styles) setStyles(path, pathOptions.styles);
svg.append(path);
}
return svg;
}
function setStyles(element, styles) {
for (const style in styles) {
if (styles[style].endsWith('!important')) element.style.setProperty(style, styles[style].slice(0, -10), 'important');else element.style.setProperty(style, styles[style]);
}
}
function getStyles(element, styles) {
const resStyles = {};
for (const style of styles || element.style) {
const value = element.style.getPropertyValue(style);
const priority = element.style.getPropertyPriority(style);
resStyles[style] = priority ? `${value} !${priority}` : value;
}
return resStyles;
}
function removeStyles(element, styles) {
for (const style of styles || element.style) element.style.removeProperty(style);
}
function setAttributes(element, attributes) {
for (const attribute in attributes) element.setAttribute(attribute, attributes[attribute]);
}
function createUrl(base) {
let path = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '/';
let query = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
const url = new URL(base);
url.pathname = path;
for (const key in query) {
const value = query[key];
if (Array.isArray(value)) {
for (const item of value) url.searchParams.append(key, item);
} else url.searchParams.set(key, value.toString());
}
return url;
}
class Component {
constructor() {
let componentElement = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document.createElement('div');
let {
defaultStyles = true,
defaultEvents = true
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
this.componentElement = componentElement;
if (defaultStyles) this.setDefaultStyles();
if (defaultEvents) this.addDefaultEvents();
}
setDefaultStyles() {
let element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.componentElement;
setStyles(element, {
color: componentColors.text,
'font-family': 'Poppins,Verdana,sans-serif !important',
'font-size': '16px',
'font-weight': 'normal',
'line-height': '20px'
});
}
addDefaultEvents() {
let element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.componentElement;
waitForElement(element).then(() => {
element.dispatchEvent(new CustomEvent('componentadded'));
waitForElement(element, true).then(() => {
element.dispatchEvent(new CustomEvent('componentremoved'));
this.addDefaultEvents();
});
});
}
add() {
let parent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document.body;
return parent.appendChild(this.componentElement);
}
remove() {
this.componentElement.remove();
}
replace(withElement) {
this.componentElement.replaceWith(withElement);
}
hidden = false;
displayStyles = {};
hide() {
if (this.hidden) return;
this.hidden = true;
this.displayStyles = getStyles(this.componentElement, ['display']);
setStyles(this.componentElement, {
display: 'none !important'
});
}
show() {
if (!this.hidden) return;
this.hidden = false;
setStyles(this.componentElement, this.displayStyles);
}
disabled = false;
opacityStyles = {};
disable() {
if (this.disabled) return;
this.disabled = true;
this.opacityStyles = getStyles(this.componentElement, ['opacity', 'pointer-events']);
setStyles(this.componentElement, {
opacity: '0.5 !important',
'pointer-events': 'none !important'
});
}
enable() {
if (!this.disabled) return;
this.disabled = false;
setStyles(this.componentElement, this.opacityStyles);
}
generateId() {
return `bm-component-${Math.random().toString(36).substring(2, 15)}`;
}
}
let componentColors = {
text: '#000',
primary: '#b5e853',
secondary: '#cccccc',
background: '#fff',
accent: '#3c3c3c',
warning: '#ffcf0e',
error: '#FF4040'
};
function setComponentColors(colors) {
componentColors = {
...componentColors,
...colors
};
}
class Button extends Component {
constructor(text, callback) {
super(document.createElement('button'));
setStyles(this.componentElement, {
'font-size': '20px',
'font-weight': 'bold',
'line-height': '24px',
border: 'none',
'border-radius': '8px',
cursor: 'pointer',
padding: '4px 8px'
});
this.componentElement.innerText = text;
this.componentElement.addEventListener('click', callback);
}
}
class PrimaryButton extends Button {
constructor(text, callback) {
super(text, callback);
setStyles(this.componentElement, {
'background-color': componentColors.primary
});
}
}
class SecondaryButton extends Button {
constructor(text, callback) {
super(text, callback);
setStyles(this.componentElement, {
'background-color': componentColors.secondary
});
}
}
class TextInput extends Component {
constructor() {
let defaultValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
let {
labelText
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
super(document.createElement('span'), {
defaultStyles: false
});
const inputId = this.generateId();
const listId = this.generateId();
if (labelText) {
const label = document.createElement('label');
label.innerText = labelText;
this.setDefaultStyles(label);
label.setAttribute('for', inputId);
this.componentElement.append(label);
}
const input = document.createElement('input');
if (typeof defaultValue === 'string') input.value = defaultValue;else input.value = defaultValue[0];
setStyles(input, {
'font-size': '18px',
border: `1px solid ${componentColors.secondary}`,
'border-radius': '4px',
'background-color': componentColors.background,
padding: '2px 8px'
});
input.setAttribute('id', inputId);
this.componentElement.append(input);
this.inputElement = input;
if (Array.isArray(defaultValue)) {
input.setAttribute('list', listId);
const dataList = document.createElement('datalist');
dataList.setAttribute('id', listId);
defaultValue.forEach(value => {
const option = document.createElement('option');
option.value = value;
option.innerText = value;
dataList.append(option);
});
this.componentElement.append(dataList);
}
}
}
class TextArea extends Component {
constructor() {
let defaultValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
let {
rows = 5,
cols = 10,
labelText
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
super(document.createElement('span'), {
defaultStyles: false
});
const textareaId = this.generateId();
if (labelText) {
const label = document.createElement('label');
label.innerText = labelText;
this.setDefaultStyles(label);
label.setAttribute('for', textareaId);
this.componentElement.append(label);
}
const textarea = document.createElement('textarea');
textarea.value = defaultValue;
setStyles(textarea, {
'font-size': '18px',
border: `1px solid ${componentColors.secondary}`,
'border-radius': '4px',
'background-color': componentColors.background,
padding: '2px 8px'
});
textarea.setAttribute('id', textareaId);
textarea.setAttribute('rows', rows.toString());
textarea.setAttribute('cols', cols.toString());
this.componentElement.append(textarea);
this.textareaElement = textarea;
}
}
class Select extends Component {
constructor(values) {
let {
labelText
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
super(document.createElement('span'), {
defaultStyles: false
});
const selectId = this.generateId();
if (labelText) {
const label = document.createElement('label');
label.innerText = labelText;
this.setDefaultStyles(label);
label.setAttribute('for', selectId);
this.componentElement.append(label);
}
const select = document.createElement('select');
this.setDefaultStyles(select);
setStyles(select, {
'font-size': '18px',
border: `1px solid ${componentColors.secondary}`,
'border-radius': '4px',
'background-color': componentColors.background,
padding: '2px 8px'
});
select.setAttribute('id', selectId);
this.componentElement.append(select);
this.selectElement = select;
values.forEach(value => {
const option = document.createElement('option');
option.value = value;
option.innerText = value;
select.append(option);
});
}
}
class Checkbox extends Component {
constructor() {
let callback = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : () => {};
let {
labelText
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
super(document.createElement('span'), {
defaultStyles: false
});
setStyles(this.componentElement, {
gap: '8px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center'
});
const inputId = this.generateId();
const input = document.createElement('input');
this.setDefaultStyles(input);
setStyles(input, {
appearance: 'checkbox',
width: '18px',
height: '18px',
margin: '0',
'accent-color': componentColors.primary,
border: `1px solid ${componentColors.secondary}`,
'border-radius': '2px',
cursor: 'pointer'
});
input.setAttribute('id', inputId);
input.setAttribute('type', 'checkbox');
input.addEventListener('change', () => callback(input.checked));
this.componentElement.append(input);
this.checkboxElement = input;
if (labelText) {
const label = document.createElement('label');
label.innerText = labelText;
this.setDefaultStyles(label);
setStyles(label, {
cursor: 'pointer'
});
label.setAttribute('for', inputId);
this.componentElement.append(label);
}
}
}
var name = "heroicons";
console.debug(name, 'included');
const outlineIconOptions = {
svg: {
attributes: {
fill: 'none',
viewBox: '0 0 24 24',
'stroke-width': '1.5',
stroke: 'currentColor'
},
styles: {
width: '24px',
height: '24px'
}
},
paths: [{
attributes: {
'stroke-linecap': 'round',
'stroke-linejoin': 'round'
}
}]
};
const solidIconOptions = {
svg: {
attributes: {
fill: 'currentColor',
viewBox: '0 0 24 24'
},
styles: {
width: '24px',
height: '24px'
}
},
paths: [{
attributes: {
'fill-rule': 'evenodd',
'clip-rule': 'evenodd'
}
}]
};
const miniIconOptions = {
svg: {
attributes: {
fill: 'currentColor',
viewBox: '0 0 20 20'
},
styles: {
width: '20px',
height: '20px'
}
},
paths: [{
attributes: {
'fill-rule': 'evenodd',
'clip-rule': 'evenodd'
}
}]
};
/**
* <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6 h-6">
* <path fill-rule="evenodd" d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
* </svg>
* **/
const xMarkSolid = () => {
const options = solidIconOptions;
options.paths[0].attributes.d = 'M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z';
return createSVG(options);
};
/**
* <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
* <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
* </svg>
**/
const informationCircleOutline = () => {
const options = outlineIconOptions;
options.paths[0].attributes.d = 'M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z';
return createSVG(options);
};
/**
* <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
* <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
* </svg>
**/
const informationCircleMini = () => {
const options = miniIconOptions;
options.paths[0].attributes.d = 'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z';
return createSVG(options);
};
class Modal extends Component {
constructor(_ref) {
let {
title,
content,
buttons
} = _ref;
super();
setStyles(this.componentElement, {
'z-index': '1000000',
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
display: 'flex',
'align-items': 'center',
'justify-content': 'center'
});
const background = document.createElement('div');
setStyles(background, {
position: 'fixed',
top: '0',
left: '0',
height: '100%',
width: '100%',
'background-color': 'rgba(0, 0, 0, 0.4)',
'backdrop-filter': 'blur(4px)'
});
background.addEventListener('click', () => this.remove());
this.componentElement.append(background);
const box = document.createElement('div');
setStyles(box, {
'z-index': '1',
'min-width': '300px',
'max-width': '80vw',
'max-height': '100vh',
'background-color': componentColors.background,
'box-shadow': '0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.4)',
'border-radius': '8px',
margin: '8px',
padding: '8px'
});
this.componentElement.append(box);
const headerContainer = document.createElement('div');
setStyles(headerContainer, {
'max-height': '32px',
display: 'flex',
'justify-content': 'space-between',
'align-items': 'center',
gap: '8px',
'padding-bottom': '8px'
});
box.append(headerContainer);
const titleContainer = document.createElement('span');
if (title) titleContainer.innerText = title;
this.setDefaultStyles(titleContainer);
setStyles(titleContainer, {
'font-size': '24px',
'line-height': '24px',
'font-weight': 'bold',
overflow: 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap'
});
headerContainer.append(titleContainer);
const close = document.createElement('button');
const closeIcon = xMarkSolid();
setStyles(close, {
width: '32px',
height: '32px',
'flex-shrink': '0',
cursor: 'pointer',
border: 'none',
background: 'none',
padding: '0'
});
setStyles(closeIcon, {
width: '100%',
height: '100%',
cursor: 'pointer'
});
close.addEventListener('click', () => this.remove());
close.append(closeIcon);
headerContainer.append(close);
const contentContainer = document.createElement('div');
if (typeof content === 'string') contentContainer.innerText = content;else contentContainer.append(content);
this.setDefaultStyles(contentContainer);
setStyles(contentContainer, {
'text-align': 'center',
'max-height': '75vh',
'overflow-y': 'auto',
padding: '4px'
});
box.append(contentContainer);
if (buttons) {
const footerContainer = document.createElement('div');
this.setDefaultStyles(footerContainer);
setStyles(footerContainer, {
'max-height': '50px',
display: 'flex',
'align-items': 'center',
gap: '8px',
'padding-top': '8px',
'overflow-x': 'auto'
});
const footerMargin = document.createElement('div');
setStyles(footerMargin, {
'margin-left': 'auto'
});
footerContainer.append(footerMargin);
buttons.forEach(button => {
setStyles(button.componentElement, {
'flex-shrink': '0'
});
button.add(footerContainer);
});
box.append(footerContainer);
}
let isAdded = false;
let bodyOverflows = {};
this.componentElement.addEventListener('componentadded', () => {
if (isAdded) return;
isAdded = true;
bodyOverflows = getStyles(document.body, ['overflow', 'overflow-y', 'overflow-x']);
setStyles(document.body, {
overflow: 'hidden !important'
});
});
this.componentElement.addEventListener('componentremoved', () => {
if (!isAdded) return;
isAdded = false;
setStyles(document.body, bodyOverflows);
});
}
}
async function alertModal(text, level) {
switch (level) {
case 'warning':
console.warn(text);
break;
case 'error':
console.error(text);
break;
default:
console.log(text);
break;
}
try {
const okButton = new PrimaryButton('OK', () => {
modal.remove();
});
const modal = new Modal({
title: level?.toUpperCase().concat('!'),
content: text.toString(),
buttons: [okButton]
});
modal.add();
okButton.componentElement.focus();
return await new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve()));
} catch (error) {
console.error(error);
return alert(text);
}
}
async function promptModal(text) {
let defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
try {
const input = new TextInput(defaultValue, {
labelText: text
});
setStyles(input.inputElement, {
width: '90%'
});
let value;
const okButton = new PrimaryButton('OK', () => {
value = input.inputElement.value;
modal.remove();
});
const cancelButton = new SecondaryButton('Cancel', () => {
value = null;
modal.remove();
});
const modal = new Modal({
content: input.componentElement,
buttons: [okButton, cancelButton]
});
input.inputElement.addEventListener('keydown', event => {
if (event.key === 'Enter') okButton.componentElement.click();
});
modal.add();
input.inputElement.focus();
return await new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(value)));
} catch (error) {
console.error(error);
return prompt(text, Array.isArray(defaultValue) ? defaultValue[0] : defaultValue);
}
}
function promptAreaModal(text) {
let defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
const textarea = new TextArea(defaultValue, {
labelText: text
});
setStyles(textarea.textareaElement, {
width: '90%'
});
let value;
const okButton = new PrimaryButton('OK', () => {
value = textarea.textareaElement.value;
modal.remove();
});
const cancelButton = new SecondaryButton('Cancel', () => {
value = null;
modal.remove();
});
const modal = new Modal({
content: textarea.componentElement,
buttons: [okButton, cancelButton]
});
modal.add();
textarea.textareaElement.focus();
return new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(value)));
}
function selectModal(text, options) {
const select = new Select(options, {
labelText: text
});
setStyles(select.selectElement, {
width: '90%'
});
let value;
const okButton = new PrimaryButton('OK', () => {
value = select.selectElement.value;
modal.remove();
});
const cancelButton = new SecondaryButton('Cancel', () => {
value = null;
modal.remove();
});
const modal = new Modal({
content: select.componentElement,
buttons: [okButton, cancelButton]
});
select.selectElement.addEventListener('keydown', event => {
if (event.key === 'Enter') okButton.componentElement.click();
});
modal.add();
select.selectElement.focus();
return new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(value)));
}
function checkboxModal(text, options) {
let defaultValues = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
const selectedOptions = [];
const checkboxes = options.map(option => {
const checkbox = new Checkbox(checked => {
if (checked) {
selectedOptions.push(option);
} else {
selectedOptions.splice(selectedOptions.indexOf(option), 1);
}
}, {
labelText: option
});
return {
value: option,
element: checkbox.componentElement,
checkboxElement: checkbox.checkboxElement
};
});
let values;
const okButton = new PrimaryButton('OK', () => {
values = selectedOptions;
modal.remove();
});
const cancelButton = new SecondaryButton('Cancel', () => {
values = null;
modal.remove();
});
const contentContainer = document.createElement('div');
setStyles(contentContainer, {
display: 'flex',
'flex-direction': 'column',
'align-items': 'start',
gap: '8px'
});
contentContainer.append(...checkboxes.map(checkbox => checkbox.element));
const modal = new Modal({
title: text,
content: contentContainer,
buttons: [okButton, cancelButton]
});
modal.add();
checkboxes.forEach(checkbox => {
if (defaultValues.includes(checkbox.value)) checkbox.checkboxElement.click();
});
okButton.componentElement.focus();
return new Promise(resolve => modal.componentElement.addEventListener('componentremoved', () => resolve(values)));
}
const storageKey = 'rbm-settings-4abbd04d-2504-4a5a-8cf2-c96bc68bbdea';
function getSavedField(fieldId) {
const savedFields = parseStorage(storageKey) || [];
return savedFields.find(f => f.id === fieldId);
}
function setSavedField(field) {
const savedFields = parseStorage(storageKey) || [];
const fieldIndex = savedFields.findIndex(f => f.id === field.id);
if (fieldIndex === -1) {
savedFields.push(field);
} else {
savedFields[fieldIndex] = field;
}
saveStorage(storageKey, savedFields);
}
class SettingsField {
constructor(props) {
this.id = props.id;
this.name = props.name;
this.description = props.description;
this.settings = props.settings;
this.savedSettings = this.settings.map(setting => ({
...setting
}));
this.newSettings = this.settings.map(setting => ({
...setting
}));
this.load();
}
getValue(id) {
const setting = this.savedSettings.find(s => s.id === id);
if (!setting) return;
switch (setting.type) {
case 'text':
case 'textarea':
{
if (setting.value || setting.value?.trim() === '') return setting.value;
return setting.defaultValue;
}
case 'checkbox':
{
if (setting.value === undefined) return setting.defaultValue;
return setting.value;
}
default:
{
return setting.value || setting.defaultValue;
}
}
}
setValue(id, value) {
const setting = this.newSettings.find(s => s.id === id);
if (setting) setting.value = value;
}
load() {
const loadedSettings = [];
const savedField = getSavedField(this.id);
if (savedField?.settings) {
for (const setting of this.settings) {
const loadedSetting = {
...setting
};
const savedSetting = savedField.settings.find(s => s.id === setting.id && s.type === setting.type && !(setting.type === 'select' && !s.options?.includes(setting.value || setting.defaultValue)));
if (savedSetting) {
loadedSetting.value = savedSetting.value;
}
loadedSettings.push(loadedSetting);
}
} else {
loadedSettings.push(...this.settings.map(setting => ({
...setting
})));
}
this.savedSettings = loadedSettings;
this.newSettings = loadedSettings.map(setting => ({
...setting
}));
}
save() {
const newSettings = this.newSettings.map(setting => ({
...setting
}));
setSavedField({
id: this.id,
name: this.name,
description: this.description,
settings: newSettings
});
this.savedSettings = newSettings;
}
}
class Settings extends Modal {
constructor(fields) {
const cancelButton = new SecondaryButton('Cancel', () => this.remove());
const saveButton = new PrimaryButton('Save', () => this.save());
const content = document.createElement('div');
setStyles(content, {
width: '100%',
display: 'flex',
gap: '12px',
'flex-direction': 'column',
'align-items': 'center',
'justify-content': 'center'
});
super({
title: 'SETTINGS',
content: content,
buttons: [cancelButton, saveButton]
});
this.contentContainer = content;
this.cancelButton = cancelButton;
this.saveButton = saveButton;
this.fields = fields;
}
load() {
this.fields.forEach(field => field.load());
this.updateButtons();
}
save() {
this.fields.forEach(field => field.save());
this.updateButtons();
}
add(parent) {
this.load();
this.render();
return super.add(parent);
}
render() {
while (this.contentContainer.firstChild) {
this.contentContainer.removeChild(this.contentContainer.firstChild);
}
if (!this.fields.length) {
const noSettingsElement = document.createElement('p');
noSettingsElement.innerText = 'No settings available';
setStyles(noSettingsElement, {
width: '100%',
'text-align': 'center',
'font-size': '20px',
'line-height': '24px',
'font-weight': 'semibold'
});
this.contentContainer.append(noSettingsElement);
return;
}
for (const field of this.fields) {
const fieldElement = document.createElement('div');
setStyles(fieldElement, {
width: '100%',
display: 'flex',
'flex-direction': 'column',
'align-items': 'flex-start',
gap: '4px',
padding: '8px',
'background-color': componentColors.secondary,
'border-radius': '8px',
'box-shadow': '0 4px 8px 0 rgba(0, 0, 0, 0.40)'
});
const fieldNameElement = document.createElement('span');
fieldNameElement.innerText = field.name;
setStyles(fieldNameElement, {
width: '100%',
'text-align': 'center',
'font-weight': 'bold',
'font-size': '20px',
'line-height': '24px'
});
const fieldDescriptionElement = document.createElement('span');
if (field.description) {
fieldDescriptionElement.innerText = field.description;
setStyles(fieldDescriptionElement, {
width: '100%',
'text-align': 'center'
});
}
fieldElement.append(fieldNameElement, fieldDescriptionElement);
for (const setting of field.savedSettings) {
const settingElement = document.createElement('div');
setStyles(settingElement, {
width: '100%',
display: 'flex',
'flex-direction': 'column',
'align-items': 'flex-start',
gap: '4px',
padding: '8px',
'box-shadow': '0 2px 4px 0 rgba(0, 0, 0, 0.25)',
'background-color': componentColors.background,
'border-radius': '8px'
});
const settingNameElement = document.createElement('span');
settingNameElement.innerText = setting.name;
setStyles(settingNameElement, {
'font-weight': 'bold',
'font-size': '18px',
'line-height': '22px',
'text-align': 'left'
});
const settingDescriptionElement = document.createElement('span');
if (setting.description) {
settingDescriptionElement.innerText = setting.description;
setStyles(settingDescriptionElement, {
'text-align': 'left'
});
}
const settingInputElements = document.createElement('div');
setStyles(settingInputElements, {
width: '100%',
display: 'flex',
'flex-direction': 'row',
'justify-content': 'space-between',
gap: '4px'
});
settingElement.append(settingNameElement, settingDescriptionElement, settingInputElements);
const inputComponentStyle = {
'flex-grow': '1',
display: 'flex'
};
const inputStyle = {
width: '50%',
'flex-grow': '1'
};
switch (setting.type) {
case 'text':
case 'textarea':
{
const textSettingValue = field.getValue(setting.id);
const textComponent = setting.type === 'textarea' ? new TextArea(textSettingValue) : new TextInput(textSettingValue);
const textInputElement = textComponent.inputElement || textComponent.textareaElement;
setStyles(textComponent.componentElement, inputComponentStyle);
setStyles(textInputElement, inputStyle);
const onTextInput = () => {
field.setValue(setting.id, textInputElement.value);
updateTextResetButton();
this.updateButtons();
};
textInputElement.addEventListener('input', () => onTextInput());
const textResetButton = new SecondaryButton('Reset', () => {
textInputElement.value = setting.defaultValue;
onTextInput();
});
const updateTextResetButton = () => {
if (textInputElement.value === setting.defaultValue) {
textResetButton.disable();
} else {
textResetButton.enable();
}
};
updateTextResetButton();
settingInputElements.append(textComponent.componentElement, textResetButton.componentElement);
break;
}
case 'checkbox':
{
const checkboxSettingValue = field.getValue(setting.id);
const onCheck = () => {
field.setValue(setting.id, checkboxComponent.checkboxElement.checked);
updateCheckboxResetButton();
this.updateButtons();
};
const checkboxComponent = new Checkbox(() => onCheck());
checkboxComponent.checkboxElement.checked = !!checkboxSettingValue;
const checkboxResetButton = new SecondaryButton('Reset', () => {
checkboxComponent.checkboxElement.checked = setting.defaultValue;
onCheck();
});
const updateCheckboxResetButton = () => {
if (checkboxComponent.checkboxElement.checked === setting.defaultValue) {
checkboxResetButton.disable();
} else {
checkboxResetButton.enable();
}
};
updateCheckboxResetButton();
settingInputElements.append(checkboxComponent.componentElement, checkboxResetButton.componentElement);
break;
}
case 'select':
{
const selectSettingValue = field.getValue(setting.id);
const onSelect = () => {
field.setValue(setting.id, selectComponent.selectElement.value);
updateSelectResetButton();
this.updateButtons();
};
const selectComponent = new Select(setting.options);
setStyles(selectComponent.componentElement, inputComponentStyle);
setStyles(selectComponent.selectElement, inputStyle);
selectComponent.selectElement.addEventListener('change', () => onSelect());
selectComponent.selectElement.value = selectSettingValue || setting.defaultValue;
const selectResetButton = new SecondaryButton('Reset', () => {
selectComponent.selectElement.value = setting.defaultValue;
onSelect();
});
const updateSelectResetButton = () => {
if (selectComponent.selectElement.value === setting.defaultValue) {
selectResetButton.disable();
} else {
selectResetButton.enable();
}
};
updateSelectResetButton();
settingInputElements.append(selectComponent.componentElement, selectResetButton.componentElement);
break;
}
}
fieldElement.append(settingElement);
}
this.contentContainer.append(fieldElement);
}
}
updateButtons() {
const hasChanges = this.fields.some(field => field.savedSettings.some(saved => {
const newSetting = field.newSettings.find(n => n.id === saved.id);
return newSetting && newSetting.value !== saved.value;
}));
if (hasChanges) this.saveButton.enable();else this.saveButton.disable();
}
}
class Bookmarklet {
website = 'bookmarklets.roler.dev';
main = () => {
alert('Bookmarklet successfully executed!');
};
isWebsite = () => new RegExp(this.website).test(window.location.hostname);
isRoute = () => {
if (this.routes) {
const routes = this.routes.map(route => {
const toReplace = [['uuid', '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'], ['numid', '[0-9]+']];
toReplace.forEach(strings => route = route.replaceAll(`:${strings[0]}`, strings[1]));
route = `^${route}`;
return route;
});
return routes.some(route => new RegExp(route).test(window.location.pathname + window.location.search));
}
return true;
};
execute() {
let notice;
if (!this.isWebsite()) notice = 'Bookmarklet executed on the wrong website!\n' + `Allowed website: ${this.website}`;
if (!this.isRoute() && !notice) notice = 'Bookmarklet executed on the wrong route!\n' + `Allowed routes: ${this.routes.join(', ')}`;
if (notice) {
console.error(notice);
alert(notice);
return;
}
this.main();
}
}
class UniversalBookmarklet extends Bookmarklet {
website = '.*';
}
class MangadexBookmarklet extends Bookmarklet {
website = '^mangadex.org|canary.mangadex.dev';
}
const titleRoute = '/title/:uuid';
const titleEditRoute = '/title/edit/:uuid';
const titleEditDraftRoute = '/title/edit/:uuid?draft=true';
const titleCreateRoute = '/create/title';
const titleEditRoutes = [titleEditRoute, titleEditDraftRoute];
const titleId = function () {
let path = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.pathname;
return getMatch(path, /\/title\/(?:edit\/)?([-0-9a-f]{20,})/, 1);
};
const titleIsDraft = function () {
let search = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.search;
return /draft=true/.test(search);
};
const titleEditInputs = indexes => Array.from(document.querySelectorAll('div.input-container')).flatMap((div, index) => indexes && indexes.includes(index) ? Array.from(div.querySelectorAll('input.inline-input')) : []);
const titleEditInputValues = indexes => titleEditInputs(indexes).map(input => input.value.trim()).filter(value => value);
const mdComponentColors = {
color: 'rgb(var(--md-color))',
primary: 'rgb(var(--md-primary))',
background: 'rgb(var(--md-background))',
accent: 'rgb(var(--md-accent))',
accent20: 'rgb(var(--md-accent-20))',
buttonAccent: 'rgb(var(--md-button-accent))',
statusYellow: 'rgb(var(--md-status-yellow))',
statusRed: 'rgb(var(--md-status-red))'
};
const useComponents = () => setComponentColors({
text: mdComponentColors.color,
primary: mdComponentColors.primary,
secondary: mdComponentColors.buttonAccent,
background: mdComponentColors.background,
accent: mdComponentColors.accent,
warning: mdComponentColors.statusYellow,
error: mdComponentColors.statusRed
});
const roleColors = {
ROLE_ADMIN: 'rgb(155, 89, 182)',
ROLE_DEVELOPER: 'rgb(255, 110, 233)',
ROLE_GLOBAL_MODERATOR: 'rgb(233, 30, 99)',
ROLE_FORUM_MODERATOR: 'rgb(233, 30, 99)',
ROLE_PUBLIC_RELATIONS: 'rgb(230, 126, 34)',
ROLE_DESIGNER: 'rgb(254, 110, 171)',
ROLE_STAFF: 'rgb(233, 30, 99)',
ROLE_VIP: 'rgb(241, 196, 15)',
ROLE_POWER_UPLOADER: 'rgb(46, 204, 113)',
ROLE_CONTRIBUTOR: 'rgb(32, 102, 148)',
ROLE_GROUP_LEADER: 'rgb(52, 152, 219)',
ROLE_SUPPORTER: 'rgb(93, 93, 180)',
ROLE_MD_AT_HOME: 'rgb(26, 121, 57)',
ROLE_GROUP_MEMBER: 'rgb(250, 250, 250)',
ROLE_MEMBER: 'rgb(250, 250, 250)',
ROLE_USER: 'rgb(250, 250, 250)',
ROLE_UNVERIFIED: 'rgb(250, 250, 250)',
ROLE_GUEST: 'rgb(250, 250, 250)',
ROLE_BANNED: 'rgb(0, 0, 0)'
};
const getUserRoleColor = roles => {
for (const role in roleColors) {
if (roles.includes(role)) return roleColors[role];
}
return roleColors.ROLE_USER;
};
const authToken = () => parseStorage('oidc.user:https://auth.mangadex.org/realms/mangadex:mangadex-frontend-stable') || parseStorage('oidc.user:https://auth.mangadex.org/realms/mangadex:mangadex-frontend-canary');
const storage = () => parseStorage('md');
const locale = () => storage()?.userPreferences?.interfaceLocale || storage()?.userPreferences?.locale || 'en';
const localTime = function () {
let date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Date.now();
return new Date(date).toLocaleString(locale(), {
hour12: false
});
};
const langDisplayName = () => new Intl.DisplayNames([locale()], {
type: 'language'
});
const defaultDescriptionId = 'default_description';
const mangadexAddCoverDescriptionsSettings = new SettingsField({
id: 'e99c3210-1c08-4756-b4f0-565e329569e3',
name: 'Cover Descriptions',
settings: [{
id: defaultDescriptionId,
type: 'textarea',
name: 'Default Description',
defaultValue: 'Volume $volume Cover from BookLive'
}]
});
class MangadexAddCoverDescriptions extends MangadexBookmarklet {
routes = (() => [...titleEditRoutes, titleCreateRoute])();
main = async () => {
useComponents();
const defaultDescription = await promptAreaModal('Enter a description', mangadexAddCoverDescriptionsSettings.getValue(defaultDescriptionId));
if (!defaultDescription) return;
const changedDescriptions = [];
const elements = Array.from(document.querySelectorAll('div.page-sizer'));
for (const element of elements) {
if (/blob:https?:\/\/.*mangadex.*\/+[-0-9a-f]{20,}/.test(element.querySelector('.page').style.getPropertyValue('background-image'))) {
const coverDescription = parseDescription(element, defaultDescription);
const edit = element.parentElement?.querySelector('.volume-edit');
edit?.dispatchEvent(new MouseEvent('click'));
const changed = await setDescription(coverDescription);
if (changed) changedDescriptions.push(element);
}
}
if (changedDescriptions.length <= 0) return alertModal('No newly added covers with empty descriptions found!');
console.log('Added descriptions:', changedDescriptions);
function parseDescription(element, description) {
const volumeElement = element.parentElement?.querySelector('.volume-num input');
const volume = volumeElement?.value;
const languageElement = element.parentElement?.querySelector('.md-select .md-select-inner-wrap .placeholder-text');
const language = languageElement?.innerText;
const masks = {
volume: volume || 'No Volume',
language: language || 'No Language'
};
for (const mask in masks) {
const maskValue = masks[mask];
if (maskValue) {
description = description.replaceAll(`$${mask}`, maskValue);
}
}
return description;
}
function setDescription(description) {
return new Promise(resolve => {
const selectors = '.md-modal__box .md-textarea__input';
waitForElement(selectors).then(element => {
let changed = true;
const save = element?.parentElement?.parentElement?.parentElement?.parentElement?.querySelector('button.primary');
if (!element.value) element.value = description;else changed = false;
element?.dispatchEvent(new InputEvent('input'));
setTimeout(() => {
save?.dispatchEvent(new MouseEvent('click'));
waitForElement(selectors, true).then(() => resolve(changed));
}, 2);
});
});
}
};
}
class UniversalSettings extends UniversalBookmarklet {
additionalFields = [];
main = () => {
const fields = [];
if (new MangadexBookmarklet().isWebsite()) {
fields.push(mangadexAddCoverDescriptionsSettings);
}
new Settings([...fields, ...this.additionalFields]).add();
};
}
class FetchClient {
queue = [];
processing = false;
abortControllers = (() => new Map())();
bucketLastRefill = (() => Date.now())();
activeRequests = 0;
constructor() {
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const {
rateLimitRequests = Infinity,
rateLimitTime = 1000
} = options;
this.rateLimitRequests = rateLimitRequests;
this.rateLimitTime = rateLimitTime;
this.bucketTokens = rateLimitRequests;
}
async processQueue() {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0 && this.activeRequests < this.rateLimitRequests) {
await this.refillBucket();
if (this.bucketTokens > 0) {
const queueItem = this.queue.shift();
if (queueItem) {
this.bucketTokens--;
this.activeRequests++;
queueItem.request().finally(() => {
this.activeRequests--;
this.processQueue();
});
}
} else {
const waitTime = this.calculateWaitTime();
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
this.processing = false;
}
calculateWaitTime() {
const now = Date.now();
const timeSinceLastRefill = now - this.bucketLastRefill;
const timeUntilNextRefill = this.rateLimitTime - timeSinceLastRefill % this.rateLimitTime;
return Math.max(timeUntilNextRefill, 100);
}
async refillBucket() {
const now = Date.now();
const timePassed = now - this.bucketLastRefill;
const tokensToAdd = Math.floor(timePassed / this.rateLimitTime) * this.rateLimitRequests;
if (tokensToAdd > 0) {
this.bucketTokens = Math.min(this.bucketTokens + tokensToAdd, this.rateLimitRequests);
this.bucketLastRefill = now;
}
}
getRetryAfterValue(headers) {
for (const [key, value] of headers.entries()) {
if (key.toLowerCase().endsWith('retry-after')) {
return value;
}
}
return null;
}
async fetch(input, init) {
const requestId = init?.requestId || crypto.randomUUID();
const abortController = new AbortController();
this.abortControllers.set(requestId, abortController);
const _request = async () => {
try {
const response = await fetch(input, {
signal: abortController.signal,
...init
});
if (response.status === 429) {
const retryAfter = this.getRetryAfterValue(response.headers);
throw new Error(`Rate limit exceeded. Retry after: ${retryAfter} seconds`);
}
return response;
} finally {
this.abortControllers.delete(requestId);
}
};
return new Promise((resolve, reject) => {
this.queue.push({
id: requestId,
request: async () => {
try {
const response = await _request();
resolve(response);
} catch (error) {
reject(error);
}
},
abort: () => {
abortController.abort();
this.abortControllers.delete(requestId);
reject(new DOMException('The operation was aborted.', 'AbortError'));
}
});
this.processQueue();
});
}
abort(requestId) {
const index = this.queue.findIndex(item => item.id === requestId);
if (index > -1) {
const [queueItem] = this.queue.splice(index, 1);
queueItem.abort();
}
}
abortAll() {
this.queue.forEach(queueItem => this.abort(queueItem.id));
}
}
const fetchClient = new FetchClient({
rateLimitRequests: 5,
rateLimitTime: 1000
});
const baseUrl = 'https://api.mangadex.org';
async function responsePromise(_ref) {
let {
path,
query,
method = 'GET',
body,
useAuth = false,
contentType
} = _ref;
return await new Promise((resolve, reject) => {
if (query?.offset) if (query?.offset + query?.limit > 10000) reject(new Error('Collection size limit reached'));
const headers = {};
if (useAuth) {
const authToken$1 = authToken();
if (!authToken$1) reject(new Error('Not logged in'));else headers.Authorization = `${authToken$1.token_type} ${authToken$1.access_token}`;
}
if (contentType) headers['Content-Type'] = contentType;
fetchClient.fetch(createUrl(baseUrl, path, query), {
method: method,
body: body,
headers: headers
}).then(response => response.json()).then(responseJson => {
let error;
if (responseJson.result !== 'ok') {
if (Array.isArray(responseJson.errors)) error = JSON.stringify(responseJson.errors) || 'Unknown error';else error = 'Unknown error';
} else if (!responseJson) {
error = 'Response is empty';
}
if (error) reject(new Error(error));else resolve(responseJson);
}).catch(reject);
});
}
async function collectionResponsePromise(_ref2) {
let {
options,
offset = 0,
limit = 10000,
collectionLimit = 100,
callback
} = _ref2;
const responseCollectionLimit = Math.min(collectionLimit, limit);
let allResponses;
let responseOffset = offset;
let responseTotal = Math.min(10000, offset + limit);
while (responseOffset < responseTotal) {
const response = await responsePromise({
...options,
query: {
...options.query,
offset: responseOffset,
limit: responseCollectionLimit
}
});
if (!response.data.length) break;
responseTotal = Math.min(responseTotal, response.total);
responseOffset += responseCollectionLimit;
if (!allResponses) {
allResponses = {
result: response.result,
response: response.response,
data: response.data,
limit: response.limit,
offset: response.offset,
total: response.total
};
} else allResponses.data.push(...response.data);
if (callback) callback(response);
}
if (!allResponses) throw new Error('All responses are empty');
return allResponses;
}
async function getManga() {
let id = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : titleId();
let isDraft = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleIsDraft();
return await responsePromise({
path: `/manga${isDraft ? '/draft/' : '/'}${id}`,
useAuth: isDraft
});
}
async function createManga(data) {
return await responsePromise({
path: '/manga',
method: 'POST',
body: JSON.stringify(data),
useAuth: true,
contentType: 'application/json'
});
}
async function updateManga(data) {
let id = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleId();
return await responsePromise({
path: `/manga/${id}`,
method: 'PUT',
body: JSON.stringify(data),
useAuth: true,
contentType: 'application/json'
});
}
async function getMangaList() {
let {
title,
ids = [titleId()],
includes = [],
contentRating = ['safe', 'suggestive', 'erotica', 'pornographic'],
offset,
limit,
callback
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const query = {
'includes[]': includes,
'contentRating[]': contentRating
};
if (title) query['title'] = title;
if (ids) query['ids[]'] = ids;
return await collectionResponsePromise({
options: {
path: '/manga',
query
},
offset,
limit,
callback: callback
});
}
async function createMangaRelation(data) {
let id = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleId();
return await responsePromise({
path: `/manga/${id}/relation`,
method: 'POST',
body: JSON.stringify(data),
useAuth: true,
contentType: 'application/json'
});
}
async function uploadCover(data) {
let id = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleId();
const formData = new FormData();
formData.append('file', data.file);
if (data.volume) formData.append('volume', data.volume);
if (data.description) formData.append('description', data.description);
if (data.locale) formData.append('locale', data.locale);
return await responsePromise({
path: `/cover/${id}`,
method: 'POST',
body: formData,
useAuth: true
});
}
async function getCoverList() {
let {
mangaIds = [titleId()],
order = {},
includes = [],
offset,
limit,
callback
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const query = {
'manga[]': mangaIds,
'includes[]': includes
};
if (order?.volume) query['order[volume]'] = order.volume;
return await collectionResponsePromise({
options: {
path: '/cover',
query
},
offset,
limit,
callback: callback
});
}
class SimpleProgressBar extends Component {
maxValue = 100;
minValue = 0;
currentValue = this.minValue;
constructor(maxValue, minValue) {
super(document.createElement('div'), {
defaultStyles: false
});
setStyles(this.componentElement, {
'z-index': '1000000',
position: 'fixed',
bottom: '0',
left: '0',
width: '100%',
height: '24px',
'background-color': componentColors.accent,
cursor: 'pointer'
});
const progress = document.createElement('div');
setStyles(progress, {
width: '0%',
height: '100%',
'background-color': componentColors.primary,
transition: 'width 200ms'
});
this.barElement = progress;
this.componentElement.append(progress);
this.componentElement.addEventListener('click', () => this.remove());
this.reset({
maxValue,
minValue
});
}
start() {
let {
maxValue,
minValue,
currentValue
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.reset({
maxValue,
minValue,
currentValue
});
this.add();
}
update() {
let currentValue = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.currentValue + 1;
if (currentValue > this.maxValue) currentValue = this.maxValue;else if (currentValue < this.minValue) currentValue = this.minValue;
const currentPercentageRounded = Math.ceil(this.currentValue / this.maxValue * 100);
const percentageRounded = Math.ceil(currentValue / this.maxValue * 100);
if (percentageRounded >= 100) this.remove();else if (currentPercentageRounded !== percentageRounded && percentageRounded >= 0) setStyles(this.barElement, {
width: `${percentageRounded}%`
});
this.currentValue = currentValue;
}
reset() {
let {
maxValue,
minValue,
currentValue
} = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (maxValue) this.maxValue = maxValue;
if (minValue) this.minValue = minValue;
this.update(currentValue || this.minValue);
}
}
class MangadexShowCoverData extends MangadexBookmarklet {
main = () => {
useComponents();
const maxCoverRetry = 4;
const requestLimit = 100;
const maxRequestOffset = 1000;
const coverElements = [];
const coverFileNames = new Map();
const skippedCoverFileNames = new Map();
const mangaIdsForQuery = {
manga: [],
cover: []
};
const progressBar = new SimpleProgressBar();
document.querySelectorAll('img, div').forEach(element => {
const imageSource = element.src || element.style.getPropertyValue('background-image');
if (!/\/covers\/+[-0-9a-f]{20,}\/+[-0-9a-f]{20,}[^/]+(?:[?#].*)?$/.test(imageSource) || element.classList.contains('banner-image') || element.parentElement?.classList.contains('banner-bg')) return;
const mangaId = getMatch(imageSource, /[-0-9a-f]{20,}/);
const coverFileName = getMatch(imageSource, /([-0-9a-f]{20,}\.[^/.]*)\.[0-9]+\.[^/.?#]*([?#].*)?$/, 1) || getMatch(imageSource, /[-0-9a-f]{20,}\.[^/.]*?$/);
if (!mangaId || !coverFileName) return;
const addCoverFileName = fileNames => {
if (fileNames.has(mangaId)) fileNames.get(mangaId)?.add(coverFileName);else fileNames.set(mangaId, new Set([coverFileName]));
};
if (element.getAttribute('cover-data-bookmarklet') === 'executed') {
addCoverFileName(skippedCoverFileNames);
return;
}
coverElements.push(element);
element.setAttribute('cover-data-bookmarklet', 'executed');
addCoverFileName(coverFileNames);
});
if (coverFileNames.size <= 0) {
if (document.querySelector('[cover-data-bookmarklet="executed"]')) return alertModal('No new covers were found on this page since the last time this bookmarklet was executed!');
return alertModal('No covers were found on this page!');
}
progressBar.start({
maxValue: coverElements.length
});
coverFileNames.forEach((fileNames, mangaId) => {
const skippedCoversSize = skippedCoverFileNames.get(mangaId)?.size || 0;
if (fileNames.size + skippedCoversSize > 1 || titleId() === mangaId) mangaIdsForQuery.cover.push(mangaId);else mangaIdsForQuery.manga.push(mangaId);
});
getAllCoverData().then(covers => {
let addedCoverData = 0;
let failedCoverData = 0;
const coverImagesContainer = document.createElement('div');
setStyles(coverImagesContainer, {
width: 'fit-content',
height: 'fit-content',
opacity: '0',
position: 'absolute',
top: '-10000px',
'z-index': '-10000',
'pointer-events': 'none'
});
document.body.append(coverImagesContainer);
coverElements.forEach(element => {
const imageSource = element.src || element.style.getPropertyValue('background-image');
let coverManga;
const cover = covers.find(cover => {
coverManga = cover.relationships.find(relationship => relationship.type === 'manga');
if (coverManga && new RegExp(`${coverManga.id}/${cover.attributes.fileName}`).test(imageSource)) return cover;
});
if (!cover || !coverManga) {
console.error(`Element changed primary cover image: ${element}`);
++failedCoverData;
reportFailed();
return;
}
let coverRetry = 0;
const coverUrl = `https://mangadex.org/covers/${coverManga.id}/${cover.attributes.fileName}`;
const replacementCoverUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQIW2NgAAIAAAUAAR4f7BQAAAAASUVORK5CYII=';
const fullSizeImage = new Image();
fullSizeImage.setAttribute('cover-data-bookmarklet', 'executed');
coverImagesContainer.append(fullSizeImage);
function reportFailed() {
if (addedCoverData + failedCoverData >= coverElements.length) {
progressBar.remove();
if (failedCoverData > 0) alertModal(`${failedCoverData} cover images failed to load.\n\nReload the page and execute the bookmarklet again!`, 'error').catch(console.error);
}
}
function fallbackMethod() {
fullSizeImage.onerror = () => {
console.error(`Cover image failed to load: ${coverUrl}`);
++failedCoverData;
reportFailed();
};
fullSizeImage.onload = () => {
fullSizeImage.remove();
if (coverImagesContainer.children.length <= 0) coverImagesContainer.remove();
displayCoverData(element, fullSizeImage.naturalWidth, fullSizeImage.naturalHeight, cover);
progressBar.update(++addedCoverData);
reportFailed();
};
}
try {
fullSizeImage.onerror = () => {
console.warn(`Cover image failed to load: ${coverUrl}.\nRetrying...`);
fullSizeImage.removeAttribute('src');
if (++coverRetry >= maxCoverRetry) fallbackMethod();
fullSizeImage.setAttribute('src', coverUrl);
};
new ResizeObserver((_entries, observer) => {
if (coverRetry >= maxCoverRetry) return observer.disconnect();
const fullSizeImageWidth = fullSizeImage.naturalWidth;
const fullSizeImageHeight = fullSizeImage.naturalHeight;
if (fullSizeImageWidth > 0 && fullSizeImageHeight > 0) {
observer.disconnect();
fullSizeImage.remove();
fullSizeImage.src = replacementCoverUrl;
if (coverImagesContainer.children.length <= 0) coverImagesContainer.remove();
displayCoverData(element, fullSizeImageWidth, fullSizeImageHeight, cover);
progressBar.update(++addedCoverData);
reportFailed();
}
}).observe(fullSizeImage);
} catch (error) {
fallbackMethod();
}
fullSizeImage.src = coverUrl;
});
}).catch(e => {
console.error(e);
alertModal('Failed to fetch cover data!\n' + e.message, 'error').catch(console.error);
});
function displayCoverData(element, fullSizeImageWidth, fullSizeImageHeight, cover) {
element.setAttribute('cover-data-cover-id', cover.id);
const showAllInformation = function (event) {
let show = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
const showInformation = element => setStyles(element, {
display: show ? 'flex' : 'none'
});
event.stopPropagation();
event.preventDefault();
if (event.shiftKey) document.querySelectorAll('.cover-data-bookmarklet-information').forEach(element => showInformation(element));else showInformation(informationElement);
};
const user = cover.relationships.find(relationship => relationship.type === 'user' && relationship.id !== 'f8cc4f8a-e596-4618-ab05-ef6572980bbf');
const information = {
Dimensions: `${fullSizeImageWidth}x${fullSizeImageHeight}`,
Version: cover.attributes.version,
Description: cover.attributes.description,
Language: cover.attributes.locale && langDisplayName().of(cover.attributes.locale),
Volume: cover.attributes.volume,
User: user?.attributes?.username,
'Created at': localTime(cover.attributes.createdAt),
'Updated at': localTime(cover.attributes.updatedAt),
ID: cover.id
};
const informationShowElement = document.createElement('span');
setStyles(informationShowElement, {
position: 'absolute',
top: '0',
'z-index': '1'
});
const informationShowElementContent = document.createElement('span');
setStyles(informationShowElementContent, {
width: 'fit-content',
display: 'flex',
gap: '0.1rem',
'align-items': 'center'
});
informationShowElementContent.addEventListener('click', showAllInformation);
informationShowElement.append(informationShowElementContent);
const informationShowElementText = document.createElement('span');
informationShowElementText.innerText = information['Dimensions'];
setStyles(informationShowElementText, {
'padding-top': '0.25px'
});
informationShowElementContent.append(informationShowElementText);
const informationElement = document.createElement('span');
informationElement.classList.add('cover-data-bookmarklet-information');
setStyles(informationElement, {
display: 'none',
position: 'absolute',
width: '100%',
height: '100%',
padding: '0.4rem',
gap: '0.2rem',
overflow: 'auto',
'flex-wrap': 'wrap',
'align-content': 'baseline',
'background-color': mdComponentColors.accent,
'z-index': '2'
});
informationElement.addEventListener('click', e => showAllInformation(e, false));
const informationItemElements = {};
for (const info in information) {
const value = information[info];
if (!value) {
delete information[info];
continue;
}
informationItemElements[info] = document.createElement('small');
informationItemElements[info].innerText = value;
informationItemElements[info].setAttribute('title', `${info}: ${value}`);
setStyles(informationItemElements[info], {
height: 'fit-content',
'max-width': '100%',
'flex-grow': '1',
'text-align': 'center',
'background-color': mdComponentColors.accent20,
padding: '0.2rem 0.4rem',
'border-radius': '0.25rem'
});
informationElement.append(informationItemElements[info]);
}
informationShowElementContent.setAttribute('title', Object.entries(information).map(_ref => {
let [key, value] = _ref;
return `${key}: ${value}`;
}).join('\n'));
if (informationItemElements['Volume']) informationItemElements['Volume'].innerText = `Volume ${information['Volume']}`;
if (informationItemElements['Description']) {
setStyles(informationItemElements['Description'], {
width: '100%',
border: `1px solid ${mdComponentColors.primary}`
});
}
if (informationItemElements['User']) {
const roleColor = getUserRoleColor(user.attributes.roles);
setStyles(informationItemElements['User'], {
width: '100%',
color: roleColor,
border: `1px solid ${roleColor}`,
'background-color': roleColor.replace(')', ',0.1)')
});
const padding = getStyles(informationItemElements['User'], ['padding'])?.padding;
removeStyles(informationItemElements['User'], ['padding']);
const userLinkElement = document.createElement('a');
setStyles(userLinkElement, {
display: 'block',
width: '100%',
height: '100%',
padding: padding,
overflow: 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap'
});
userLinkElement.href = `/user/${user.id}`;
userLinkElement.target = '_blank';
userLinkElement.innerText = informationItemElements['User'].innerText;
informationItemElements['User'].innerText = '';
informationItemElements['User'].append(userLinkElement);
informationItemElements['User'].addEventListener('click', event => {
event.stopPropagation();
event.preventDefault();
window.open(`/user/${user.id}`, '_blank');
});
}
informationItemElements['Version'].innerText = `Version ${information['Version']}`;
informationItemElements['Created at'].innerText = `Created at ${information['Created at']}`;
informationItemElements['Updated at'].innerText = `Updated at ${information['Updated at']}`;
informationItemElements['ID'].innerText = 'Copy Cover ID';
informationItemElements['ID'].addEventListener('click', event => {
const copyId = ids => {
navigator.clipboard.writeText(ids).then(() => console.debug(`Copied cover ids: ${ids}`), () => console.error(`Failed to copy cover ids: ${ids}`)).catch(console.error);
};
event.stopPropagation();
event.preventDefault();
if (event.shiftKey) {
const coverIds = [];
document.querySelectorAll('[cover-data-cover-id]').forEach(element => {
const coverId = element.getAttribute('cover-data-cover-id');
if (coverId && !coverIds.includes(coverId)) coverIds.push(coverId);
});
copyId(coverIds.join(' '));
} else copyId(cover.id);
});
if (element instanceof HTMLImageElement) {
setStyles(informationShowElement, {
padding: '0.2rem 0.4rem 0.5rem',
color: '#fff',
left: '0',
width: '100%',
background: 'linear-gradient(0deg,transparent,rgba(0,0,0,0.8))',
'border-top-right-radius': '0.25rem',
'border-top-left-radius': '0.25rem'
});
if (information['Description']) informationShowElementContent.append(informationCircleOutline());
setStyles(informationElement, {
'border-radius': '0.25rem'
});
element.parentElement?.append(informationShowElement, informationElement);
} else {
setStyles(informationShowElement, {
padding: '0 0.2rem',
'background-color': mdComponentColors.accent,
'border-bottom-left-radius': '4px',
'border-bottom-right-radius': '4px'
});
setStyles(informationShowElementText, {
'max-height': '1.5rem'
});
if (information['Description']) informationShowElementContent.append(informationCircleMini());
element.append(informationShowElement, informationElement);
}
}
function getAllCoverData() {
const covers = [];
async function awaitAllCoverData() {
for (const endpoint in mangaIdsForQuery) {
const isCoverEndpoint = endpoint === 'cover';
const mangaIdsForQuerySplit = splitArray(mangaIdsForQuery[endpoint]);
for (const ids of mangaIdsForQuerySplit) {
const rsp = await getCoverData(ids, isCoverEndpoint);
if (isCoverEndpoint) {
covers.push(...rsp.data);
for (let i = rsp.limit; i < rsp.total; i += rsp.limit) {
const rsp = await getCoverData(ids, isCoverEndpoint, i);
covers.push(...rsp.data);
}
} else {
rsp.data.forEach(manga => {
const cover = manga.relationships.find(relationship => relationship.type === 'cover_art');
if (cover) {
cover.relationships = [{
type: manga.type,
id: manga.id
}];
covers.push(cover);
}
});
}
}
}
return covers;
}
return new Promise((resolve, reject) => awaitAllCoverData().then(resolve).catch(reject));
}
function getCoverData(ids, isCoverEndpoint) {
let offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
return new Promise((resolve, reject) => {
if (offset > maxRequestOffset) return reject(new Error(`Offset is bigger than ${maxRequestOffset}!`));
if (isCoverEndpoint) getCoverList({
mangaIds: ids,
order: {
volume: 'asc'
},
includes: ['user'],
offset: offset,
limit: requestLimit
}).then(resolve).catch(reject);else getMangaList({
ids: ids,
includes: ['cover_art'],
contentRating: ['safe', 'suggestive', 'erotica', 'pornographic'],
offset: offset,
limit: requestLimit
}).then(resolve).catch(reject);
});
}
};
}
class MangadexSearchMissingLinks extends MangadexBookmarklet {
routes = (() => [titleRoute, ...titleEditRoutes, titleCreateRoute])();
main = async () => {
useComponents();
const websites = {
al: 'https://anilist.co/search/manga?search=',
ap: 'https://www.anime-planet.com/manga/all?name=',
kt: 'https://kitsu.io/manga?subtype=manga&text=',
mu: 'https://www.mangaupdates.com/search.html?search=',
mal: 'https://myanimelist.net/manga.php?q=',
nu: 'https://www.novelupdates.com/series-finder/?sf=1&sh=',
bw: 'https://bookwalker.jp/search/?qcat=2&word=',
amz: 'https://www.amazon.co.jp/s?rh=n:466280&k=',
ebj: 'https://ebookjapan.yahoo.co.jp/search/?keyword=',
cdj: 'https://www.cdjapan.co.jp/searchuni?term.media_format=BOOK&q='
};
if (/\/create\/title/.test(window.location.pathname)) {
const inputTitles = titleEditInputValues([0, 1]);
const title = await promptModal('Enter a title to search for', inputTitles.length > 0 ? inputTitles : '');
if (!title) return;
for (const website in websites) window.open(websites[website] + title, '_blank', 'noopener,noreferrer');
return;
}
getManga().then(async titleInfo => {
if (!titleInfo.data.attributes.tags.some(tag => tag.attributes.name.en === 'Adaptation')) delete websites.nu;
const missingWebsites = Object.keys(websites).filter(website => titleInfo.data.attributes.links && !titleInfo.data.attributes.links[website]);
if (missingWebsites.length <= 0) return alertModal('All links are already added!');
const originalLang = titleInfo.data.attributes.originalLanguage;
let originalTitle = undefined;
const altTitles = Array.isArray(titleInfo.data.attributes.altTitles) ? titleInfo.data.attributes.altTitles : undefined;
if (altTitles) originalTitle = altTitles.find(title => title[originalLang]);else console.debug('No alt titles found');
const mainTitleLang = Object.keys(titleInfo.data.attributes.title)[0];
let title = originalTitle ? originalTitle[originalLang] : titleInfo.data.attributes.title[mainTitleLang] || '';
title = await promptModal('Enter a title to search for', [title, ...(altTitles?.map(_title => _title[Object.keys(_title)[0]]).filter(_title => _title !== title) || [])]);
if (!title) return;
missingWebsites.forEach(website => window.open(websites[website] + title, '_blank', 'noopener,noreferrer'));
});
};
}
class MangadexShortenLinks extends MangadexBookmarklet {
routes = (() => [...titleEditRoutes, titleCreateRoute])();
main = async () => {
useComponents();
const inputs = titleEditInputs([3, 4, 5]);
const changedLinks = {};
const progressBar = new SimpleProgressBar(inputs.length);
const numIdRegex = '[0-9]+';
const numAndLetterIdRegex = '[A-Za-z0-9-%]+';
const asinRegex = '[A-Z0-9]{10}';
const regexes = [`(anilist.co/manga/)(${numIdRegex})`, `(www.anime-planet.com/manga/)(${numAndLetterIdRegex})`, `(kitsu.(?:io|app)/manga/)(${numAndLetterIdRegex})`, `(www.mangaupdates.com/series/)(${numAndLetterIdRegex})`, `(myanimelist.net/manga/)(${numIdRegex})`, `(bookwalker.jp/series/)(${numIdRegex}(?:/list)?)`, `(bookwalker.jp/)(${numAndLetterIdRegex})`, `(www.amazon[a-z.]+/).*((?:dp/|gp/product/|kindle-dbs/product/)${asinRegex})`, `(www.amazon[a-z.]+/gp/product).*(/${asinRegex})`, `(ebookjapan.yahoo.co.jp/books/)(${numIdRegex})`, `(www.cdjapan.co.jp/product/)(NEOBK-${numIdRegex})`, '(.*/)(.*)/$'];
progressBar.start();
await Promise.all(inputs.map(async element => {
const link = element.value.trim();
let shortLink = link;
for (const regexPattern of regexes) {
const regex = new RegExp(`(?:https?://${regexPattern}.*)$`);
const websiteUrl = getMatch(link, regex, 1);
let id = getMatch(link, regex, 2);
if (websiteUrl && id) {
if (/^kitsu.(io|app)\/manga\/$/.test(websiteUrl) && !new RegExp(`^${numIdRegex}$`).test(id)) {
try {
const slugResponse = await fetch(`https://${websiteUrl.replace('/manga/', '')}/api/edge/manga?filter[slug]=${id}`);
const {
data
} = await slugResponse.json();
id = data[0].id;
} catch (error) {
console.warn('Failed to find kitsu id:', error);
}
}
shortLink = `https://${websiteUrl}${id}`;
break;
}
}
if (shortLink !== link) {
element.value = shortLink;
element.dispatchEvent(new InputEvent('input'));
changedLinks[link] = shortLink;
}
progressBar.update();
}));
progressBar.remove();
if (Object.keys(changedLinks).length <= 0) return alertModal('No links changed!');
console.log('Changed links:', changedLinks);
};
}
class MangadexOpenLinks extends MangadexBookmarklet {
routes = (() => [titleRoute, ...titleEditRoutes, titleCreateRoute])();
main = async () => {
const titleId$1 = titleId();
const inputLinks = titleEditInputValues([3, 4, 5]);
const links = [];
if (inputLinks.length <= 0 && titleId$1) {
const titleInfo = await getManga(titleId$1);
if (titleInfo.data.attributes.links) {
const websites = {
al: 'https://anilist.co/manga/',
ap: 'https://www.anime-planet.com/manga/',
kt: 'https://kitsu.io/manga/',
mu: /[A-Za-z]/.test(titleInfo.data.attributes.links.mu) ? 'https://www.mangaupdates.com/series/' : 'https://www.mangaupdates.com/series.html?id=',
mal: 'https://myanimelist.net/manga/',
nu: 'https://www.novelupdates.com/series/',
bw: 'https://bookwalker.jp/',
amz: '',
ebj: '',
cdj: ''
};
for (const website in titleInfo.data.attributes.links) {
const websiteUrl = websites[website] || '';
const link = websiteUrl + titleInfo.data.attributes.links[website];
links.push(link);
}
}
} else links.push(...inputLinks);
links.forEach(link => window.open(link, '_blank', 'noopener,noreferrer'));
};
}
class MangadexDelCoversByLang extends MangadexBookmarklet {
routes = (() => [...titleEditRoutes, titleCreateRoute])();
main = async () => {
useComponents();
const languages = Array.from(new Set(Array.from(document.querySelectorAll('div.page-sizer')).map(element => {
const parent = element.parentElement;
if (!parent) return;
const language = parent.querySelector('.placeholder-text.with-label');
if (!language) return;
return language.innerText.trim();
}).filter(language => language)));
if (languages.length <= 0) return alertModal('No covers found!');
const selectedLanguage = await selectModal('Select language', languages);
if (!selectedLanguage) return;
const deletedCovers = [];
document.querySelectorAll('div.page-sizer').forEach(element => {
const parent = element.parentElement;
if (!parent) return;
const close = parent.querySelector('.close');
const language = parent.querySelector('.placeholder-text.with-label');
if (!close || !language) return;
if (selectedLanguage === language.innerText.trim()) {
close.dispatchEvent(new MouseEvent('click'));
deletedCovers.push(element);
}
});
if (deletedCovers.length <= 0) return alertModal('No covers in given language found!');
console.log('Deleted covers:', deletedCovers);
};
}
class MangadexSearchAllTitles extends MangadexBookmarklet {
routes = (() => [titleRoute, ...titleEditRoutes, titleCreateRoute])();
main = async () => {
const titleId$1 = titleId();
const inputTitles = titleEditInputValues([0, 1]);
const titles = [];
const titlesToSearch = [];
const progressBar = new SimpleProgressBar();
const foundTitleIds = titleId$1 ? [titleId$1] : [];
if (inputTitles.length <= 0 && titleId$1) {
const titleInfo = await getManga(titleId$1);
const mainTitleLang = Object.keys(titleInfo.data.attributes.title)[0];
const mainTitle = titleInfo.data.attributes.title[mainTitleLang];
const altTitles = titleInfo.data.attributes.altTitles;
titles.push(mainTitle);
if (Array.isArray(altTitles)) titles.push(...altTitles.map(title => title[Object.keys(title)[0]]));
} else titles.push(...inputTitles);
progressBar.start({
maxValue: titles.length
});
await Promise.all(titles.map(async title => {
if (!title || titlesToSearch.length > 10) return progressBar.update();
const titleList = await getMangaList({
title: title,
offset: 0,
limit: 100,
contentRating: ['safe', 'suggestive', 'erotica', 'pornographic']
});
for (const manga of titleList.data) {
if (foundTitleIds.includes(manga.id)) continue;
foundTitleIds.push(manga.id);
if (!titlesToSearch.includes(title)) titlesToSearch.push(title);
}
if (titleList.total > 100 && !titlesToSearch.includes(title)) titlesToSearch.push(title);
progressBar.update();
}));
progressBar.remove();
titlesToSearch.forEach(title => window.open(createUrl(`https://${window.location.hostname}`, '/titles', {
q: title,
content: 'safe,suggestive,erotica,pornographic'
}), '_blank'));
};
}
class MangadexCloneTitle extends MangadexBookmarklet {
routes = (() => [titleRoute, ...titleEditRoutes])();
main = async () => {
useComponents();
const dataMap = {
title: 'Title',
altTitles: 'Alternative Titles',
description: 'Synopsis',
authors: 'Authors',
artists: 'Artists',
originalLanguage: 'Original Language',
contentRating: 'Content Rating',
publicationDemographic: 'Magazine Demographic',
status: 'Publication Status',
lastVolume: 'Final Chapter',
lastChapter: 'Final Chapter',
year: 'Publication Year',
tags: 'Tags',
links: 'Sites',
relations: 'Relations',
covers: 'Covers',
chapterNumbersResetOnNewVolume: 'Chapter Numbers Reset On New Volume'
};
const dataMapNames = Object.values(dataMap).reduce((acc, current) => acc.includes(current) ? acc : [...acc, current], []);
const dataToClone = await checkboxModal('Data to clone', dataMapNames, dataMapNames.filter(name => name !== dataMap.relations && name !== dataMap.covers));
if (!dataToClone) return;
if (!dataToClone.length) {
await alertModal('You must select some data to clone!', 'error');
return;
}
const progressBar = new SimpleProgressBar(1, 0);
progressBar.start();
const titleInfo = await getManga().catch(error => alertModal('Failed to fetch title data!\n\n' + error, 'error'));
if (!titleInfo) {
progressBar.remove();
return;
}
progressBar.update();
const isSelected = name => dataToClone.includes(name);
const getRelationshipIds = function (type) {
let data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : titleInfo.data.relationships;
return data?.map(rel => rel.type === type && rel.id).filter(id => id);
};
const newTitleData = {
title: isSelected(dataMap.title) ? titleInfo.data.attributes.title : {
en: 'Untitled'
},
altTitles: isSelected(dataMap.altTitles) && Array.isArray(titleInfo.data.attributes.altTitles) ? titleInfo.data.attributes.altTitles : [],
description: isSelected(dataMap.description) ? titleInfo.data.attributes.description : {},
authors: isSelected(dataMap.authors) ? getRelationshipIds('author') : [],
artists: isSelected(dataMap.artists) ? getRelationshipIds('artist') : [],
links: isSelected(dataMap.links) && titleInfo.data.attributes.links ? titleInfo.data.attributes.links : {},
originalLanguage: isSelected(dataMap.originalLanguage) ? titleInfo.data.attributes.originalLanguage : 'ja',
lastVolume: isSelected(dataMap.lastVolume) ? titleInfo.data.attributes.lastVolume : null,
lastChapter: isSelected(dataMap.lastChapter) ? titleInfo.data.attributes.lastChapter : null,
publicationDemographic: isSelected(dataMap.publicationDemographic) ? titleInfo.data.attributes.publicationDemographic : null,
status: isSelected(dataMap.status) ? titleInfo.data.attributes.status : 'ongoing',
year: isSelected(dataMap.year) ? titleInfo.data.attributes.year : null,
contentRating: isSelected(dataMap.contentRating) ? titleInfo.data.attributes.contentRating : 'safe',
chapterNumbersResetOnNewVolume: isSelected(dataMap.chapterNumbersResetOnNewVolume) ? titleInfo.data.attributes.chapterNumbersResetOnNewVolume : false,
tags: isSelected(dataMap.tags) ? getRelationshipIds('tag', titleInfo.data.attributes.tags) : []
};
const createdTitleURLPrompt = (await promptModal('Leave empty to create a new title\nor\nEnter a URL of an existing title to merge', ''))?.trim();
if (createdTitleURLPrompt === null || createdTitleURLPrompt === undefined) return;
progressBar.start();
let createdTitle;
if (createdTitleURLPrompt) {
let createdTitleURL;
try {
createdTitleURL = new URL(createdTitleURLPrompt);
} catch (error) {
progressBar.remove();
await alertModal('Invalid title URL!', 'error');
return;
}
const createdTitleId = titleId(createdTitleURL.pathname);
if (!createdTitleId) {
progressBar.remove();
await alertModal('Invalid title UUID!', 'error');
return;
}
const createdTitleIsDraft = titleIsDraft(createdTitleURL.search);
createdTitle = await getManga(createdTitleId, createdTitleIsDraft).catch(error => alertModal('Failed to fetch created title!\n\n' + error, 'error'));
} else {
createdTitle = await createManga(newTitleData).catch(error => alertModal('Failed to create new title!\n\n' + error, 'error'));
}
if (!createdTitle) {
progressBar.remove();
return;
}
progressBar.update();
if (createdTitleURLPrompt) {
const createdTitleAltTitles = Array.isArray(createdTitle.data.attributes.altTitles) ? createdTitle.data.attributes.altTitles : [];
const dedupedNewTitleAltTitles = newTitleData.altTitles?.filter(altTitle => !createdTitleAltTitles.some(title => altTitle[Object.keys(altTitle)[0]] === title[Object.keys(title)[0]])) || [];
const createdTitleAuthors = getRelationshipIds('author', createdTitle.data.relationships);
const dedupedNewTitleAuthors = newTitleData.authors?.filter(author => !createdTitleAuthors.includes(author)) || [];
const createdTitleArtists = getRelationshipIds('artist', createdTitle.data.relationships);
const dedupedNewTitleArtists = newTitleData.artists?.filter(artist => !createdTitleArtists.includes(artist)) || [];
const createdTitleTags = getRelationshipIds('tag', createdTitle.data.attributes.tags);
const dedupedNewTitleTags = newTitleData.tags?.filter(tag => !createdTitleTags.includes(tag)) || [];
const softMergeType = 'Copy missing only';
const moderateMergeType = 'Overwrite and copy missing';
const hardMergeType = 'Overwrite all';
const mergeType = await selectModal("Choose how to merge the title data\n(doesn't affect relations or covers)", [softMergeType, moderateMergeType, hardMergeType]);
if (mergeType === null || mergeType === undefined) return;
progressBar.start();
const isModerateMerge = mergeType === moderateMergeType;
const isHardMerge = mergeType === hardMergeType;
const mergedTitleData = isHardMerge ? {
...newTitleData,
version: createdTitle.data.attributes.version
} : {
title: isModerateMerge && isSelected(dataMap.title) ? newTitleData.title : createdTitle.data.attributes.title || newTitleData.title,
altTitles: [...createdTitleAltTitles, ...dedupedNewTitleAltTitles],
description: isModerateMerge ? {
...(createdTitle.data.attributes.description || {}),
...(newTitleData.description || {})
} : {
...(newTitleData.description || {}),
...(createdTitle.data.attributes.description || {})
},
authors: [...createdTitleAuthors, ...dedupedNewTitleAuthors],
artists: [...createdTitleArtists, ...dedupedNewTitleArtists],
links: isModerateMerge ? {
...(createdTitle.data.attributes.links || {}),
...(newTitleData.links || {})
} : {
...(newTitleData.links || {}),
...(createdTitle.data.attributes.links || {})
},
originalLanguage: isModerateMerge && isSelected(dataMap.originalLanguage) ? newTitleData.originalLanguage : createdTitle.data.attributes.originalLanguage || newTitleData.originalLanguage,
lastVolume: isModerateMerge && isSelected(dataMap.lastVolume) ? newTitleData.lastVolume : createdTitle.data.attributes.lastVolume || newTitleData.lastVolume,
lastChapter: isModerateMerge && isSelected(dataMap.lastChapter) ? newTitleData.lastChapter : createdTitle.data.attributes.lastChapter || newTitleData.lastChapter,
publicationDemographic: isModerateMerge && isSelected(dataMap.publicationDemographic) ? newTitleData.publicationDemographic : createdTitle.data.attributes.publicationDemographic || newTitleData.publicationDemographic,
status: isModerateMerge && isSelected(dataMap.status) ? newTitleData.status : createdTitle.data.attributes.status || newTitleData.status,
year: isModerateMerge && isSelected(dataMap.year) ? newTitleData.year : createdTitle.data.attributes.year || newTitleData.year,
contentRating: isModerateMerge && isSelected(dataMap.contentRating) ? newTitleData.contentRating : createdTitle.data.attributes.contentRating || newTitleData.contentRating,
chapterNumbersResetOnNewVolume: isModerateMerge && isSelected(dataMap.chapterNumbersResetOnNewVolume) ? newTitleData.chapterNumbersResetOnNewVolume : createdTitle.data.attributes.chapterNumbersResetOnNewVolume || newTitleData.chapterNumbersResetOnNewVolume,
tags: [...createdTitleTags, ...dedupedNewTitleTags],
version: createdTitle.data.attributes.version
};
createdTitle = await updateManga(mergedTitleData, createdTitle.data.id).catch(error => alertModal('Failed to update title data!\n\n' + error, 'error'));
if (!createdTitle) {
progressBar.remove();
return;
}
progressBar.update();
}
const errors = [];
if (isSelected(dataMap.relations)) {
const getMangaRelations = function () {
let relations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : titleInfo.data.relationships;
return relations.filter(rel => rel.type === 'manga' && rel.related).map(rel => ({
targetManga: rel.id,
relation: rel.related
}));
};
const relations = getMangaRelations();
let dedupedRelations = relations;
if (createdTitleURLPrompt) {
const createdTitleRelations = getMangaRelations(createdTitle.data.relationships);
dedupedRelations = relations.filter(relation => !createdTitleRelations.some(createdRelation => createdRelation.targetManga === relation.targetManga && createdRelation.relation === relation.relation));
}
progressBar.start({
maxValue: dedupedRelations.length
});
await Promise.all(dedupedRelations.map(async relation => {
await createMangaRelation(relation, createdTitle.data.id).catch(error => {
fetchClient.abortAll();
if (error.name !== 'AbortError') errors.push('Failed to create relations: ' + error);
});
progressBar.update();
}));
}
if (isSelected(dataMap.covers)) {
progressBar.start();
const getTitleCovers = async function () {
let mangaId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : titleInfo.data.id;
return await getCoverList({
mangaIds: [mangaId],
callback: () => progressBar.update()
}).then(data => data.data).catch(error => {
fetchClient.abortAll();
if (error.name !== 'AbortError') errors.push('Failed to fetch cover data lists: ' + error);
});
};
const allCovers = await getTitleCovers();
let dedupedCovers = allCovers;
if (allCovers && createdTitleURLPrompt) {
progressBar.start();
const createdTitleCovers = await getTitleCovers(createdTitle.data.id);
if (createdTitleCovers) {
dedupedCovers = allCovers.filter(cover => !createdTitleCovers.some(createdCover => createdCover.attributes.volume === cover.attributes.volume && createdCover.attributes.locale === cover.attributes.locale));
}
}
if (dedupedCovers) {
progressBar.start({
maxValue: dedupedCovers.length
});
await Promise.all(dedupedCovers.map(async cover => {
const coverImageResponse = await fetch(`https://mangadex.org/covers/${titleInfo.data.id}/${cover.attributes.fileName}`).catch(error => {
errors.push('Failed to fetch cover image: ' + error);
});
if (!coverImageResponse) return;
const coverBlob = await coverImageResponse.blob();
await uploadCover({
file: new File([coverBlob], cover.attributes.fileName, {
type: coverBlob.type
}),
volume: cover.attributes.volume || null,
description: cover.attributes.description || '',
locale: cover.attributes.locale || titleInfo.data.attributes.originalLanguage
}, createdTitle.data.id).catch(error => {
fetchClient.abortAll();
if (error.name !== 'AbortError') errors.push('Failed to upload covers: ' + error);
});
progressBar.update();
}));
}
}
progressBar.remove();
if (errors.length) {
await alertModal('Failed to clone all title data!\n\n' + errors.join('\n\n'), 'error');
}
window.open(`/title/edit/${createdTitle.data.id}${createdTitle.data.attributes.state === 'draft' ? '?draft=true' : ''}`, '_blank');
};
}
class AmazonBookmarklet extends Bookmarklet {
website = 'www.amazon.*';
}
const anonymousM = () => createSVG({
svg: {
attributes: {
width: '100',
height: '100',
stroke: 'currentColor'
}
},
paths: [{
attributes: {
d: 'M96.64 56.72c-3.18-6.34-6.04-13.9-7.46-19.72-.21-.87-.36-1.8-.53-2.88-.42-2.65-.95-5.95-2.81-10.52-1.15-2.82-4.4-7.12-8.6-7.78l-2.51-7.26c-.11-.26-.33-.46-.6-.53l-3.46-.87c-.01 0-.03 0-.04-.01-.02 0-.04-.01-.06-.01h-.05-.06-.05-.05c-.02 0-.04.01-.05.01-.02 0-.04.01-.05.01-.02 0-.03.01-.05.02s-.03.01-.05.02-.03.02-.05.02c-.02.01-.03.02-.05.03-.03.01-.05.02-.06.03-.02.01-.03.02-.05.03-.01.01-.03.02-.04.04-.01.01-.02.02-.03.02l-4.97 4.85s-.01.01-.01.02c-.01.01-.02.02-.02.03-.02.02-.04.04-.05.07-.01.01-.01.02-.02.03-.02.03-.04.06-.06.1-.02.03-.03.07-.04.1 0 .01-.01.02-.01.03-.01.03-.01.05-.02.08 0 .01 0 .02-.01.03-.01.04-.01.08-.01.11v.09c0 .03 0 .04.01.05.01.03.01.06.02.09l.79 2.55c-1.62-1.26-3.47-2.31-5.58-3.02-2.87-.96-5.94-1.28-9.14-.95-3.59-.5-10.16-1.39-15.84 2.33-.22.14-.43.29-.65.44l.23-1.38c0-.04.01-.06.01-.09v-.06-.08-.03c0-.03-.01-.07-.02-.1-.01-.05-.02-.08-.04-.11 0-.01-.01-.02-.01-.03-.01-.02-.02-.05-.04-.07-.01-.01-.01-.02-.02-.03-.01-.02-.03-.04-.05-.06-.01-.01-.01-.02-.02-.02-.02-.03-.05-.05-.07-.07l-5.18-4.63c-.01-.01-.02-.01-.03-.02-.02-.03-.03-.04-.05-.05s-.03-.02-.05-.03-.03-.02-.05-.03-.03-.02-.05-.02c-.02-.01-.03-.01-.05-.02s-.04-.01-.05-.02c-.02 0-.03-.01-.05-.01s-.04-.01-.06-.01-.03-.01-.05-.01h-.06-.05c-.02 0-.04 0-.06.01-.02 0-.03 0-.05.01-.02 0-.04.01-.06.01-.01 0-.03.01-.04.01l-3.42 1.02a.83.83 0 0 0-.56.56l-2.06 6.92c-4.49 1.01-7.67 6.51-8.58 8.73-1.86 4.57-2.39 7.86-2.81 10.52-.17 1.08-.32 2.01-.53 2.88-1.65 6.72-4.17 13.72-6.92 19.22-.1.2-.12.43-.04.65 2.31 6.72 4.89 10.62 8.28 15.38a.83.83 0 0 0 .77.35.83.83 0 0 0 .68-.5c.49-1.15.6-1.56.85-2.48l.26-.97-.04 1.27c-.04 1.69-.08 3.03-.44 4.57-.06.27.01.56.21.77l2.07 2.19a.83.83 0 0 0 .61.26c.06 0 .12-.01.18-.02a.82.82 0 0 0 .61-.54c2.43-7.01 2.58-11.98 2.73-16.8.13-4.11.25-8 1.7-13.09a135.84 135.84 0 0 1 2-6.42c-.02.71-.02 1.42 0 2.13l.29 7.92a8.31 8.31 0 0 1-.29 2.51c-.23.83-.55 1.63-.94 2.4-.58 1.14-1.32 2.19-2.19 3.13-.23.25-.29.61-.15.92s.46.5.8.48c.74-.04 2.18-.26 3.46-1.35.15-.13.3-.27.44-.41.46.91 1.14 1.9 2 3.16l.1.15c.75 1.1 1.6 1.9 2.45 2.47-.05 1.49.61 2.95 1.8 3.87.83.64 1.83.96 2.84.96a4.48 4.48 0 0 0 .89-.09l.01.15c.02.22.13.42.3.56a.86.86 0 0 0 .53.19h.08l3.03-.3a.84.84 0 0 0 .75-.91l-.17-1.7c-.02-.22-.13-.42-.3-.56s-.39-.21-.61-.19l-3.03.3a.84.84 0 0 0-.75.91l.02.16c-.95.22-1.96.01-2.74-.59-.64-.5-1.07-1.21-1.21-1.99a8.66 8.66 0 0 0 4.17.6c.28-.03.52-.2.65-.45a.85.85 0 0 0-.01-.79c-.31-.55-.58-1.07-.84-1.57.92.96 1.95 1.81 3.09 2.53l3.34 2.12c-.02.64-.05 1.69-.15 2.89l-.79-.59a.97.97 0 0 0-.87-.15c-.3.09-.53.32-.63.62l-1.18 3.54-2.34 2.34a.97.97 0 0 0-.14 1.2c-.07.08-.13.16-.17.25-.66.43-3.15 1.21-4.83 1.74-4.76 1.5-6.32 2.08-6.6 3.25-.11.46.03.95.37 1.3 1.36 1.36 13.7 7.84 25.29 7.84.19 0 .37 0 .56-.01 10.57-.19 22.17-5.06 25.7-7.73.27-.21.44-.53.44-.87.01-.34-.15-.67-.41-.88-1.41-1.14-3.63-1.9-5.98-2.69-1.98-.67-4.02-1.37-5.15-2.21a.97.97 0 0 0-.15-1.18l-2.34-2.34-1.18-3.54c-.1-.3-.33-.53-.63-.62s-.62-.04-.87.15l-.83.63c-.07-1.19-.1-2.22-.1-2.74l3.64-2.32c1.29-.82 2.39-1.82 3.33-3.05-.27.79-.6 1.61-1.01 2.54a.82.82 0 0 0 .06.79c.15.24.42.38.7.38 1.08 0 4.82-.28 7.36-3.9a15.69 15.69 0 0 0 1.83-3.43c.1.14.21.27.32.4 1.25 1.44 2.77 2.07 3.82 2.34.35.09.72-.06.92-.36.19-.31.17-.7-.07-.98-.89-1.05-1.63-2.22-2.21-3.47-.66-1.42-1.1-2.93-1.31-4.49V38.2c1.41 5.08 2.49 13.09 3.61 21.44.86 6.4 1.75 13.01 2.86 18.82a.81.81 0 0 0 .58.64c.08.02.16.03.24.03.22 0 .44-.09.6-.26l2.43-2.54c.2-.2.27-.49.21-.77-.52-2.18-.89-4.4-1.11-6.63a37.61 37.61 0 0 0 1.65 4.6.83.83 0 0 0 .68.5.84.84 0 0 0 .77-.35c3.53-4.96 5.98-9.84 8.2-16.31.02-.21.01-.45-.09-.65zM69.37 19.14l-1.25-5.03 2.56-2.83 1.76 5.33c-1.18.59-2.24 1.45-3.07 2.53zm3.6-.92l2.64 8-1.45 1.74c-.9-2.26-2.16-4.82-3.87-7.24a7.36 7.36 0 0 1 2.68-2.5zM29.08 19c-.63-.81-1.41-1.47-2.2-1.98l1.54-5.44 2.68 2.71-.67 3.32c-.46.45-.91.91-1.35 1.39zm-2.13 10.12l-1.07-.95c.83-.97 1.65-1.83 2.28-2.35.08-.07.16-.13.25-.2l-1.46 3.5zm-.54-10.41c.56.41 1.11.93 1.54 1.56-1.15 1.33-2.21 2.73-3.14 4.1l1.6-5.66zm-6.23 29.16c-1.51 5.29-1.64 9.46-1.76 13.5-.14 4.38-.27 8.9-2.21 14.99l-.81-.86c.31-1.52.35-2.84.39-4.47.02-.83.04-1.78.11-2.86a52.74 52.74 0 0 0-.29-9.65.83.83 0 0 0-.88-.73.84.84 0 0 0-.78.84c.04 3.6-.43 7.17-1.39 10.59l-.28 1.05-.22.81c-2.89-4.12-5.03-7.61-7.04-13.35 2.75-5.55 5.25-12.55 6.9-19.25.23-.94.39-1.95.56-3.01.41-2.57.92-5.76 2.71-10.15.69-1.69 3.15-6.06 6.46-7.43l-2.71 9.08c-.02.04-.04.09-.05.13a.82.82 0 0 0 .27.88l2.45 2.01c-.68 1.53-1.1 2.87-1.17 3.87a.83.83 0 0 0 .76.89.83.83 0 0 0 .9-.75c.05-.5.56-1.46 1.29-2.55l1.93 1.58c-1.99 4.84-3.71 9.82-5.14 14.84zm19.8-8.5c.64 2 1.49 3.75 2.43 5.27h-6.77c.85-1.01 1.64-2.07 2.35-3.18.63-.99 1.2-2.02 1.71-3.07.08.32.18.65.28.98zm13.37-2.15l-.1-1.68a37.79 37.79 0 0 0 2.97 4.04c1.58 1.86 3.34 3.55 5.25 5.06h-7.29c-.38-2.46-.66-4.94-.83-7.42zM39.5 76.16a1.02 1.02 0 0 0 .24-.38l.82-2.47 8.24 6.18a65.07 65.07 0 0 1-1.75 2.78c-1.47 2.2-2.31 3.08-2.72 3.42-1.45-2.28-5.12-6.13-6.66-7.71l1.83-1.82zm-12.72 7.93c1.12-.47 3-1.06 4.37-1.49 3.51-1.1 5.25-1.69 5.92-2.45 2.09 2.19 5.12 5.5 5.88 7.02a1.14 1.14 0 0 0 .18.25 1.36 1.36 0 0 0 .98.41c.07 0 .14 0 .2-.01.4-.05.91-.26 1.71-1.06l.8 1.6c.1.2.28.35.49.42-.02.13-.06.27-.12.46-.15.46-.38.91-.58 1.25-4.68-.43-9.19-1.8-12.41-3-3.44-1.27-6.07-2.6-7.42-3.4zm46.26.18c-3.81 2.21-11.74 5.27-19.54 6.14a6.59 6.59 0 0 1-.4-.95 5.91 5.91 0 0 1-.19-.69c.18-.08.33-.21.43-.39l.8-1.6c.79.8 1.31 1 1.71 1.06a1.41 1.41 0 0 0 .2.01c.37 0 .72-.14.98-.41.07-.07.14-.16.18-.25.77-1.53 3.84-4.9 5.94-7.09 1.41 1.14 3.63 1.9 5.98 2.69 1.39.47 2.8.95 3.91 1.48zM59.61 73.31l.82 2.47a1.02 1.02 0 0 0 .24.38l1.82 1.82c-1.54 1.57-5.21 5.42-6.66 7.71-.41-.34-1.24-1.22-2.72-3.42a65.07 65.07 0 0 1-1.75-2.78l8.25-6.18zm-2.78-.36l-6.75 5.06-6.81-5.11c.12-1.14.18-2.2.22-3.05l2.94 1.87c1.15.73 2.47 1.1 3.79 1.1s2.64-.37 3.79-1.1l2.64-1.68.18 2.91zm4.2-8.32l-8.2 5.22c-1.58 1-3.62 1-5.2 0l-8.18-5.21c-2.67-1.7-4.69-4.21-5.78-7.15h32.46c-1.14 3.41-2.73 5.63-5.1 7.14zm15.09-5.43l-.04-.04a5.52 5.52 0 0 1-1.02-1.72c-.13-.34-.47-.56-.83-.54a.83.83 0 0 0-.76.63c-.43 1.73-1.16 3.34-2.17 4.78-1.45 2.06-3.35 2.8-4.68 3.05 1.06-2.64 1.44-4.64 1.64-7.25.07-.2.13-.41.2-.62h1.6c.38 0 .7-.31.7-.7V45.34c0-.38-.31-.7-.7-.7h-2.05c-.58-5.04-1.7-10.04-3.36-14.96a.83.83 0 0 0-1.58.54c1.6 4.74 2.69 9.56 3.26 14.42h-2.05c-2.53-1.75-4.8-3.8-6.79-6.14-1.68-1.97-3.13-4.14-4.34-6.45a84.54 84.54 0 0 1 .73-11.58.97.97 0 0 0-.84-1.09c-.54-.07-1.02.3-1.09.84a86.3 86.3 0 0 0-.55 17.15 85.51 85.51 0 0 0 .81 7.29h-2.87a15.22 15.22 0 0 1-1.58-3.87c-.12-.46-.56-.77-1.03-.73-.48.04-.85.43-.88.91a22.86 22.86 0 0 0 .05 3.7h-1.16c-1.13-1.62-2.17-3.57-2.9-5.87-2.56-8.02.31-14.97 1.66-17.58.25-.48.06-1.07-.42-1.31-.48-.25-1.07-.06-1.31.42-1.24 2.4-3.67 8.2-2.66 15.23-.67 1.76-1.52 3.45-2.52 5.03-.93 1.46-1.99 2.82-3.17 4.08h-.93a76.44 76.44 0 0 1 2.35-15.47c.12-.45-.15-.9-.6-1.02s-.9.15-1.02.6c-1.35 5.21-2.15 10.53-2.41 15.89h-2.69c-.38 0-.7.31-.7.7v11.46c0 .38.31.7.7.7h3.14c.43 2.69 1.21 4.94 2.63 7.67-.76-.07-1.73-.28-2.71-.82-.09-.17-.25-.31-.45-.35-.05-.01-.1-.02-.14-.02-.66-.46-1.31-1.09-1.89-1.95l-.1-.15c-1.26-1.84-2.09-3.06-2.35-4.17a.84.84 0 0 0-1.57-.18 3.97 3.97 0 0 1-.98 1.27c.23-.38.45-.76.65-1.16a14.52 14.52 0 0 0 1.05-2.7 9.8 9.8 0 0 0 .35-3.03l-.29-7.92c-.15-4.22.59-8.34 2.21-12.24l3.44-8.27a.85.85 0 0 0-.24-.97.84.84 0 0 0-1-.04c-.95.65-1.88 1.35-2.77 2.07-.29.24-.64.56-1.02.94l.27-.4c1.02-1.47 2.1-2.86 3.22-4.12.03-.03.05-.06.08-.09 2.01-2.27 4.12-4.16 6.19-5.51 5.22-3.41 11.45-2.53 14.8-2.06a.74.74 0 0 0 .21 0c2.99-.33 5.85-.04 8.53.86 2.87.96 5.22 2.63 7.15 4.6.04.05.09.1.14.14.48.5.93 1.02 1.36 1.55.03.06.08.11.12.15.15.19.29.38.43.57.04.06.08.12.13.18 3.44 4.71 5.04 10.16 5.59 12.46v19.53a.41.41 0 0 0 .01.11 17.21 17.21 0 0 0 1.46 5.03c.1.21.22.45.35.69zm11.87 12.25c-.42-1.09-.78-2.19-1.09-3.31-.96-3.42-1.42-6.98-1.39-10.59a.84.84 0 0 0-.78-.84.83.83 0 0 0-.88.73 52.74 52.74 0 0 0-.29 9.65 52.97 52.97 0 0 0 1.21 8.4l-.99 1.04c-.96-5.37-1.77-11.34-2.55-17.13-1.51-11.24-2.95-21.86-5.32-26.01-.1-.42-.23-.93-.4-1.52l5.16-4.61a.83.83 0 0 0 .23-.89c-.02-.05-.04-.09-.06-.13l-2.96-8.55c3.2 1.03 5.62 4.63 6.4 6.54 1.79 4.39 2.3 7.58 2.71 10.15.17 1.07.33 2.08.56 3.01 1.43 5.84 4.27 13.38 7.44 19.75-1.95 5.62-4.09 9.99-7 14.31z'
}
}]
});
class LoadingCircle extends Component {
constructor() {
super(anonymousM(), {
defaultStyles: false
});
setStyles(this.componentElement, {
width: '100px',
height: '100px'
});
this.componentElement.animate({
transform: ['rotate(0deg)', 'rotate(360deg)']
}, {
duration: 1000,
iterations: Infinity,
easing: 'linear'
});
}
}
class Skeleton extends Component {
constructor() {
super(document.createElement('div'), {
defaultStyles: false
});
setStyles(this.componentElement, {
width: '100%',
height: '100%',
'background-color': componentColors.secondary,
opacity: '0.4',
'border-radius': '4px'
});
this.componentElement.animate({
opacity: [0.2, 0.4, 0.2]
}, {
duration: 2000,
iterations: Infinity,
easing: 'ease-in-out'
});
}
}
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var FileSaver_min$1 = {exports: {}};
var FileSaver_min = FileSaver_min$1.exports;
var hasRequiredFileSaver_min;
function requireFileSaver_min () {
if (hasRequiredFileSaver_min) return FileSaver_min$1.exports;
hasRequiredFileSaver_min = 1;
(function (module, exports) {
(function(a,b){b();})(FileSaver_min,function(){function b(a,b){return "undefined"==typeof b?b={autoBom:false}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c);},d.onerror=function(){console.error("could not download file");},d.send();}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,false);try{b.send();}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"));}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",true,true,window,0,0,0,80,20,false,false,false,false,0,null),a.dispatchEvent(b);}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof commonjsGlobal&&commonjsGlobal.global===commonjsGlobal?commonjsGlobal:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href);},4E4),setTimeout(function(){e(j);},0));}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else {var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i);});}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null;},k.readAsDataURL(b);}else {var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m);},4E4);}});f.saveAs=g.saveAs=g,(module.exports=g);});
} (FileSaver_min$1));
return FileSaver_min$1.exports;
}
var FileSaver_minExports = requireFileSaver_min();
var fileSaver = /*@__PURE__*/getDefaultExportFromCjs(FileSaver_minExports);
// DEFLATE is a complex format; to read this code, you should probably check the RFC first:
// https://tools.ietf.org/html/rfc1951
// You may also wish to take a look at the guide I made about this program:
// https://gist.github.com/101arrowz/253f31eb5abc3d9275ab943003ffecad
// Some of the following code is similar to that of UZIP.js:
// https://github.com/photopea/UZIP.js
// However, the vast majority of the codebase has diverged from UZIP.js to increase performance and reduce bundle size.
// Sometimes 0 will appear where -1 would be more appropriate. This is because using a uint
// is better for memory in most engines (I *think*).
// aliases for shorter compressed code (most minifers don't do this)
var u8 = Uint8Array, u16 = Uint16Array, i32 = Int32Array;
// fixed length extra bits
var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]);
// fixed distance extra bits
var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]);
// get base, reverse index map from extra bits
var freb = function (eb, start) {
var b = new u16(31);
for (var i = 0; i < 31; ++i) {
b[i] = start += 1 << eb[i - 1];
}
// numbers here are at max 18 bits
var r = new i32(b[30]);
for (var i = 1; i < 30; ++i) {
for (var j = b[i]; j < b[i + 1]; ++j) {
r[j] = ((j - b[i]) << 5) | i;
}
}
return { b: b, r: r };
};
var _a = freb(fleb, 2), fl = _a.b, revfl = _a.r;
// we can ignore the fact that the other numbers are wrong; they never happen anyway
fl[28] = 258, revfl[258] = 28;
freb(fdeb, 0);
// map of value to reverse (assuming 16 bits)
var rev = new u16(32768);
for (var i = 0; i < 32768; ++i) {
// reverse table algorithm from SO
var x = ((i & 0xAAAA) >> 1) | ((i & 0x5555) << 1);
x = ((x & 0xCCCC) >> 2) | ((x & 0x3333) << 2);
x = ((x & 0xF0F0) >> 4) | ((x & 0x0F0F) << 4);
rev[i] = (((x & 0xFF00) >> 8) | ((x & 0x00FF) << 8)) >> 1;
}
// fixed length tree
var flt = new u8(288);
for (var i = 0; i < 144; ++i)
flt[i] = 8;
for (var i = 144; i < 256; ++i)
flt[i] = 9;
for (var i = 256; i < 280; ++i)
flt[i] = 7;
for (var i = 280; i < 288; ++i)
flt[i] = 8;
// fixed distance tree
var fdt = new u8(32);
for (var i = 0; i < 32; ++i)
fdt[i] = 5;
// typed array slice - allows garbage collector to free original reference,
// while being more compatible than .slice
var slc = function (v, s, e) {
if (e == null || e > v.length)
e = v.length;
// can't use .constructor in case user-supplied
return new u8(v.subarray(s, e));
};
// error codes
var ec = [
'unexpected EOF',
'invalid block type',
'invalid length/literal',
'invalid distance',
'stream finished',
'no stream handler',
,
'no callback',
'invalid UTF-8 data',
'extra field too long',
'date not in range 1980-2099',
'filename too long',
'stream finishing',
'invalid zip data'
// determined by unknown compression method
];
var err = function (ind, msg, nt) {
var e = new Error(msg || ec[ind]);
e.code = ind;
if (Error.captureStackTrace)
Error.captureStackTrace(e, err);
if (!nt)
throw e;
return e;
};
// empty
var et = /*#__PURE__*/ new u8(0);
// CRC32 table
var crct = /*#__PURE__*/ (function () {
var t = new Int32Array(256);
for (var i = 0; i < 256; ++i) {
var c = i, k = 9;
while (--k)
c = ((c & 1) && -306674912) ^ (c >>> 1);
t[i] = c;
}
return t;
})();
// CRC32
var crc = function () {
var c = -1;
return {
p: function (d) {
// closures have awful performance
var cr = c;
for (var i = 0; i < d.length; ++i)
cr = crct[(cr & 255) ^ d[i]] ^ (cr >>> 8);
c = cr;
},
d: function () { return ~c; }
};
};
// Walmart object spread
var mrg = function (a, b) {
var o = {};
for (var k in a)
o[k] = a[k];
for (var k in b)
o[k] = b[k];
return o;
};
// write bytes
var wbytes = function (d, b, v) {
for (; v; ++b)
d[b] = v, v >>>= 8;
};
// text encoder
var te = typeof TextEncoder != 'undefined' && /*#__PURE__*/ new TextEncoder();
// text decoder
var td = typeof TextDecoder != 'undefined' && /*#__PURE__*/ new TextDecoder();
// text decoder stream
var tds = 0;
try {
td.decode(et, { stream: true });
tds = 1;
}
catch (e) { }
/**
* Converts a string into a Uint8Array for use with compression/decompression methods
* @param str The string to encode
* @param latin1 Whether or not to interpret the data as Latin-1. This should
* not need to be true unless decoding a binary string.
* @returns The string encoded in UTF-8/Latin-1 binary
*/
function strToU8(str, latin1) {
var i;
if (te)
return te.encode(str);
var l = str.length;
var ar = new u8(str.length + (str.length >> 1));
var ai = 0;
var w = function (v) { ar[ai++] = v; };
for (var i = 0; i < l; ++i) {
if (ai + 5 > ar.length) {
var n = new u8(ai + 8 + ((l - i) << 1));
n.set(ar);
ar = n;
}
var c = str.charCodeAt(i);
if (c < 128 || latin1)
w(c);
else if (c < 2048)
w(192 | (c >> 6)), w(128 | (c & 63));
else if (c > 55295 && c < 57344)
c = 65536 + (c & 1023 << 10) | (str.charCodeAt(++i) & 1023),
w(240 | (c >> 18)), w(128 | ((c >> 12) & 63)), w(128 | ((c >> 6) & 63)), w(128 | (c & 63));
else
w(224 | (c >> 12)), w(128 | ((c >> 6) & 63)), w(128 | (c & 63));
}
return slc(ar, 0, ai);
}
// extra field length
var exfl = function (ex) {
var le = 0;
if (ex) {
for (var k in ex) {
var l = ex[k].length;
if (l > 65535)
err(9);
le += l + 4;
}
}
return le;
};
// write zip header
var wzh = function (d, b, f, fn, u, c, ce, co) {
var fl = fn.length, ex = f.extra, col = co && co.length;
var exl = exfl(ex);
wbytes(d, b, ce != null ? 0x2014B50 : 0x4034B50), b += 4;
if (ce != null)
d[b++] = 20, d[b++] = f.os;
d[b] = 20, b += 2; // spec compliance? what's that?
d[b++] = (f.flag << 1) | (c < 0 && 8), d[b++] = u && 8;
d[b++] = f.compression & 255, d[b++] = f.compression >> 8;
var dt = new Date(f.mtime == null ? Date.now() : f.mtime), y = dt.getFullYear() - 1980;
if (y < 0 || y > 119)
err(10);
wbytes(d, b, (y << 25) | ((dt.getMonth() + 1) << 21) | (dt.getDate() << 16) | (dt.getHours() << 11) | (dt.getMinutes() << 5) | (dt.getSeconds() >> 1)), b += 4;
if (c != -1) {
wbytes(d, b, f.crc);
wbytes(d, b + 4, c < 0 ? -c - 2 : c);
wbytes(d, b + 8, f.size);
}
wbytes(d, b + 12, fl);
wbytes(d, b + 14, exl), b += 16;
if (ce != null) {
wbytes(d, b, col);
wbytes(d, b + 6, f.attrs);
wbytes(d, b + 10, ce), b += 14;
}
d.set(fn, b);
b += fl;
if (exl) {
for (var k in ex) {
var exf = ex[k], l = exf.length;
wbytes(d, b, +k);
wbytes(d, b + 2, l);
d.set(exf, b + 4), b += 4 + l;
}
}
if (col)
d.set(co, b), b += col;
return b;
};
// write zip footer (end of central directory)
var wzf = function (o, b, c, d, e) {
wbytes(o, b, 0x6054B50); // skip disk
wbytes(o, b + 8, c);
wbytes(o, b + 10, c);
wbytes(o, b + 12, d);
wbytes(o, b + 16, e);
};
/**
* A pass-through stream to keep data uncompressed in a ZIP archive.
*/
var ZipPassThrough = /*#__PURE__*/ (function () {
/**
* Creates a pass-through stream that can be added to ZIP archives
* @param filename The filename to associate with this data stream
*/
function ZipPassThrough(filename) {
this.filename = filename;
this.c = crc();
this.size = 0;
this.compression = 0;
}
/**
* Processes a chunk and pushes to the output stream. You can override this
* method in a subclass for custom behavior, but by default this passes
* the data through. You must call this.ondata(err, chunk, final) at some
* point in this method.
* @param chunk The chunk to process
* @param final Whether this is the last chunk
*/
ZipPassThrough.prototype.process = function (chunk, final) {
this.ondata(null, chunk, final);
};
/**
* Pushes a chunk to be added. If you are subclassing this with a custom
* compression algorithm, note that you must push data from the source
* file only, pre-compression.
* @param chunk The chunk to push
* @param final Whether this is the last chunk
*/
ZipPassThrough.prototype.push = function (chunk, final) {
if (!this.ondata)
err(5);
this.c.p(chunk);
this.size += chunk.length;
if (final)
this.crc = this.c.d();
this.process(chunk, final || false);
};
return ZipPassThrough;
}());
// TODO: Better tree shaking
/**
* A zippable archive to which files can incrementally be added
*/
var Zip = /*#__PURE__*/ (function () {
/**
* Creates an empty ZIP archive to which files can be added
* @param cb The callback to call whenever data for the generated ZIP archive
* is available
*/
function Zip(cb) {
this.ondata = cb;
this.u = [];
this.d = 1;
}
/**
* Adds a file to the ZIP archive
* @param file The file stream to add
*/
Zip.prototype.add = function (file) {
var _this = this;
if (!this.ondata)
err(5);
// finishing or finished
if (this.d & 2)
this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, false);
else {
var f = strToU8(file.filename), fl_1 = f.length;
var com = file.comment, o = com && strToU8(com);
var u = fl_1 != file.filename.length || (o && (com.length != o.length));
var hl_1 = fl_1 + exfl(file.extra) + 30;
if (fl_1 > 65535)
this.ondata(err(11, 0, 1), null, false);
var header = new u8(hl_1);
wzh(header, 0, file, f, u, -1);
var chks_1 = [header];
var pAll_1 = function () {
for (var _i = 0, chks_2 = chks_1; _i < chks_2.length; _i++) {
var chk = chks_2[_i];
_this.ondata(null, chk, false);
}
chks_1 = [];
};
var tr_1 = this.d;
this.d = 0;
var ind_1 = this.u.length;
var uf_1 = mrg(file, {
f: f,
u: u,
o: o,
t: function () {
if (file.terminate)
file.terminate();
},
r: function () {
pAll_1();
if (tr_1) {
var nxt = _this.u[ind_1 + 1];
if (nxt)
nxt.r();
else
_this.d = 1;
}
tr_1 = 1;
}
});
var cl_1 = 0;
file.ondata = function (err, dat, final) {
if (err) {
_this.ondata(err, dat, final);
_this.terminate();
}
else {
cl_1 += dat.length;
chks_1.push(dat);
if (final) {
var dd = new u8(16);
wbytes(dd, 0, 0x8074B50);
wbytes(dd, 4, file.crc);
wbytes(dd, 8, cl_1);
wbytes(dd, 12, file.size);
chks_1.push(dd);
uf_1.c = cl_1, uf_1.b = hl_1 + cl_1 + 16, uf_1.crc = file.crc, uf_1.size = file.size;
if (tr_1)
uf_1.r();
tr_1 = 1;
}
else if (tr_1)
pAll_1();
}
};
this.u.push(uf_1);
}
};
/**
* Ends the process of adding files and prepares to emit the final chunks.
* This *must* be called after adding all desired files for the resulting
* ZIP file to work properly.
*/
Zip.prototype.end = function () {
var _this = this;
if (this.d & 2) {
this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, true);
return;
}
if (this.d)
this.e();
else
this.u.push({
r: function () {
if (!(_this.d & 1))
return;
_this.u.splice(-1, 1);
_this.e();
},
t: function () { }
});
this.d = 3;
};
Zip.prototype.e = function () {
var bt = 0, l = 0, tl = 0;
for (var _i = 0, _a = this.u; _i < _a.length; _i++) {
var f = _a[_i];
tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0);
}
var out = new u8(tl + 22);
for (var _b = 0, _c = this.u; _b < _c.length; _b++) {
var f = _c[_b];
wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o);
bt += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b;
}
wzf(out, bt, this.u.length, tl, l);
this.ondata(null, out, true);
this.d = 2;
};
/**
* A method to terminate any internal workers used by the stream. Subsequent
* calls to add() will fail.
*/
Zip.prototype.terminate = function () {
for (var _i = 0, _a = this.u; _i < _a.length; _i++) {
var f = _a[_i];
f.t();
}
this.d = 2;
};
return Zip;
}());
class CoverDownloader extends Modal {
knownFileNames = {};
aborted = false;
loadMax = 1;
currentLoad = 0;
covers = [];
busy = false;
constructor(getCovers) {
let {
loadMax = 1,
title,
fileNamePrefix = 'Volume'
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
const resultsContainer = document.createElement('div');
setStyles(resultsContainer, {
width: '100%',
'min-width': '200px',
height: '100%',
'min-height': '200px',
display: 'flex',
'flex-wrap': 'wrap',
gap: '8px',
'justify-content': 'center',
'align-items': 'center'
});
const loadContainer = document.createElement('div');
setStyles(loadContainer, {
width: '90%',
'flex-shrink': '0',
'margin-top': '2px'
});
const loadButton = new SecondaryButton('LOAD MORE', () => this.loadCovers());
setStyles(loadButton.componentElement, {
width: '100%'
});
loadButton.add(loadContainer);
const buttons = {
selectAll: new PrimaryButton('Select All', () => this.selectAll()),
crop: new PrimaryButton('Crop', () => this.crop()),
open: new PrimaryButton('Open', () => this.open()),
copy: new PrimaryButton('Copy', () => this.copy()),
zip: new PrimaryButton('Zip', () => this.zip()),
save: new PrimaryButton('Save', () => this.save())
};
setStyles(buttons.selectAll.componentElement, {
'min-width': '150px'
});
setStyles(buttons.crop.componentElement, {
'min-width': '100px'
});
super({
title: 'Cover Downloader',
content: resultsContainer,
buttons: Object.values(buttons)
});
this.resultsContainer = resultsContainer;
this.loadContainer = loadContainer;
this.loadButton = loadButton;
this.loadCircle = new LoadingCircle();
this.buttons = buttons;
this.loadingCircle = new LoadingCircle();
this.loadMax = loadMax;
this.getCovers = getCovers;
this.title = title?.trim();
this.fileNamePrefix = fileNamePrefix;
this.componentElement.addEventListener('componentadded', () => {
this.aborted = false;
Object.values(buttons).forEach(button => button.hide());
this.loadingCircle.add(this.resultsContainer);
this.loadCovers();
});
this.componentElement.addEventListener('componentremoved', () => {
this.aborted = true;
this.clearCovers();
});
}
loadCovers() {
if (this.currentLoad >= this.loadMax) this.currentLoad = 0;
++this.currentLoad;
this.loadButton.replace(this.loadCircle.componentElement);
const progressBar = new SimpleProgressBar();
this.getCovers(this.currentLoad).then(covers => {
if (this.aborted) return;
const coverUrls = this.covers.map(cover => cover.url);
covers = covers.filter(cover => !coverUrls.includes(cover.url));
if (covers.length <= 0) throw new Error('No covers found');
covers.forEach(cover => cover.title = cover.title || `${this.covers.length + 1}`);
covers.forEach(cover => this.parseTitle(cover));
covers.sort((a, b) => {
return a.parsedTitle.localeCompare(b.parsedTitle, undefined, {
numeric: true,
sensitivity: 'base'
});
});
covers.forEach(cover => this.setCoverFilename(cover));
this.covers.push(...covers);
progressBar.start({
maxValue: covers.length
});
const afterLoad = () => {
progressBar.update();
if (progressBar.currentValue >= progressBar.maxValue) {
progressBar.remove();
if (covers.some(cover => cover.cropAmount && !cover.cropped)) this.crop(covers, true).catch(console.error);
}
};
covers.forEach(cover => this.loadCover(cover).then(afterLoad).catch(afterLoad));
}).catch(error => {
console.error(error);
progressBar.remove();
this.remove();
alertModal('Failed to load covers!\n' + error, 'error').catch(console.error);
});
}
async loadCover(cover) {
var _this = this;
const result = document.createElement('div');
setStyles(result, {
'min-width': '134px',
'max-width': '140px',
'min-height': '234px',
'max-height': '240px',
'flex-grow': '1',
'background-color': componentColors.background,
border: `1px solid ${componentColors.secondary}`,
'border-radius': '4px',
'box-shadow': '0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 3px 5px 0 rgba(0, 0, 0, 0.2)',
overflow: 'hidden',
display: 'flex',
'flex-direction': 'column',
cursor: 'pointer',
'user-select': 'none'
});
if (cover.element) cover.element.replaceWith(result);
cover.element = result;
const headerContainer = document.createElement('div');
this.setDefaultStyles(headerContainer);
setStyles(headerContainer, {
'font-size': '14px',
'line-height': '14px',
display: 'flex',
'justify-content': 'space-between',
'align-items': 'center',
gap: '2px',
padding: '4px'
});
result.append(headerContainer);
const dimensionsElementPlaceholder = new Skeleton();
setStyles(dimensionsElementPlaceholder.componentElement, {
height: '14px'
});
dimensionsElementPlaceholder.add(headerContainer);
const checkboxElementPlaceholder = new Skeleton();
setStyles(checkboxElementPlaceholder.componentElement, {
height: '14px',
width: '14px',
'flex-shrink': '0'
});
checkboxElementPlaceholder.add(headerContainer);
const imageContainer = document.createElement('div');
setStyles(imageContainer, {
position: 'relative',
'flex-grow': '1'
});
result.append(imageContainer);
const imageElementPlaceholder = new Skeleton();
setStyles(imageElementPlaceholder.componentElement, {
position: 'absolute',
top: '0',
left: '0'
});
imageContainer.append(imageElementPlaceholder.componentElement);
const footerContainer = document.createElement('div');
this.setDefaultStyles(footerContainer);
setStyles(footerContainer, {
'font-size': '14px',
'line-height': '14px',
'text-align': 'center',
padding: '4px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap'
});
result.append(footerContainer);
const titleElementPlaceholder = new Skeleton();
setStyles(titleElementPlaceholder.componentElement, {
height: '14px'
});
titleElementPlaceholder.add(footerContainer);
if (this.covers.every(c => c.element)) {
this.loadingCircle.remove();
this.covers.forEach(cover => {
if (!this.resultsContainer.contains(cover.element)) this.resultsContainer.append(cover.element);
});
this.loadCircle.replace(this.loadButton.componentElement);
if (this.currentLoad < this.loadMax) this.resultsContainer.append(this.loadContainer);else this.loadContainer.remove();
}
const titleElement = document.createElement('span');
titleElement.innerText = cover.parsedTitle;
titleElement.setAttribute('title', cover.parsedTitle);
titleElementPlaceholder.replace(titleElement);
let imageUrl = cover.url;
try {
await this.download(cover);
if (cover.blobUrl) imageUrl = cover.blobUrl;
} catch (error) {
console.warn('Failed to download cover', cover.url, error);
}
if (this.aborted) return;
const imageElement = document.createElement('img');
imageElement.alt = cover.filename;
setStyles(imageElement, {
height: '100%',
width: '100%',
position: 'absolute',
top: '0',
left: '0',
'object-fit': 'cover',
'object-position': 'center'
});
imageElementPlaceholder.replace(imageElement);
const checkbox = new Checkbox();
setStyles(checkbox.checkboxElement, {
width: '14px',
height: '14px',
position: 'unset',
'vertical-align': 'unset'
});
checkboxElementPlaceholder.replace(checkbox.componentElement);
cover.select = function () {
let select = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
cover.selected = select;
checkbox.checkboxElement.checked = cover.selected;
_this.lastSelected = cover;
let borderColor = componentColors.secondary;
if (cover.selected) {
if (cover.errored) borderColor = componentColors.error;else if (!cover.blobUrl) borderColor = componentColors.warning;else borderColor = componentColors.primary;
}
let backgroundColor = componentColors.background;
if (cover.selected) {
if (cover.errored) backgroundColor = componentColors.error;else if (!cover.blobUrl) backgroundColor = componentColors.warning;else backgroundColor = componentColors.primary;
}
setStyles(result, {
'border-color': borderColor,
'background-color': backgroundColor
});
if (cover.errored) setStyles(checkbox.checkboxElement, {
'accent-color': componentColors.error
});else if (!cover.blobUrl) setStyles(checkbox.checkboxElement, {
'accent-color': componentColors.warning
});
_this.updateButtons();
};
result.addEventListener('click', event => {
if (!cover.select) return;
if (event.shiftKey && this.lastSelected) {
this.selectRange(this.lastSelected, cover, !cover.selected);
} else cover.select(!cover.selected);
});
const imageElementLoaded = await new Promise(resolve => {
imageElement.onerror = () => resolve(false);
imageElement.onload = () => resolve(true);
imageElement.src = imageUrl;
});
if (imageElementLoaded) {
cover.imageElement = imageElement;
cover.width = imageElement.naturalWidth;
cover.height = imageElement.naturalHeight;
cover.cropAmount = this.getCropMethod(cover);
const dimensionsElement = document.createElement('span');
dimensionsElement.innerText = `${cover.width}x${cover.height}${cover.cropped ? 'c' : ''}`;
dimensionsElementPlaceholder.replace(dimensionsElement);
} else {
console.error('Failed to load cover:', imageUrl);
cover.errored = true;
const errorElement = document.createElement('span');
this.setDefaultStyles(errorElement);
setStyles(errorElement, {
'font-size': '32px',
'font-weight': 'bold',
width: '100%',
height: '100%',
position: 'absolute',
top: '0',
left: '0',
'background-color': componentColors.error,
display: 'flex',
'justify-content': 'center',
'align-items': 'center'
});
errorElement.innerText = 'ERROR';
imageElement.replaceWith(errorElement);
if (cover.selected && cover.select) cover.select();
}
if (cover.selected || this.covers.length === 1) cover.select();
cover.loaded = true;
this.updateButtons();
}
clearCovers() {
this.removeBlobs();
this.covers.forEach(cover => cover.element?.remove());
this.loadContainer.remove();
this.covers = [];
this.currentLoad = 0;
this.knownFileNames = {};
}
createBlobUrl(cover) {
if (!cover.blob) return;
if (!cover.blobUrl) cover.blobUrl = URL.createObjectURL(cover.blob);
}
removeBlob(cover) {
if (cover.blobUrl) {
URL.revokeObjectURL(cover.blobUrl);
delete cover.blobUrl;
}
if (cover.blob) delete cover.blob;
}
removeBlobs() {
this.covers.forEach(cover => this.removeBlob(cover));
}
setBlob(cover, blob) {
if (this.aborted) {
this.removeBlob(cover);
throw new Error('aborted');
} else {
cover.blob = cover.blob || blob;
this.createBlobUrl(cover);
return cover.blob;
}
}
parseTitle(cover) {
let volumeString = cover.title;
const japaneseCharacters = '0123456789'.split('');
japaneseCharacters.forEach((character, i) => volumeString = volumeString.replaceAll(character, i.toString()));
const spaceMatch = volumeString.match(/\((\d+)(\.\d+)?\)| (\d+)(\.\d+)? /);
if (spaceMatch && spaceMatch[0]) volumeString = spaceMatch[0];
const volumeNumbers = volumeString.match(/\d+(?:\.\d+)?/g);
if (volumeNumbers) cover.parsedTitle = `${this.fileNamePrefix} ${volumeNumbers.pop()}`.trim();else cover.parsedTitle = cover.title.trim();
}
setCoverFilename(cover) {
const name = cover.parsedTitle || 'cover';
const extension = cover.blob?.type.split('/')[1]?.replace('jpeg', 'jpg') || getMatch(cover.url, /\.(\w+)$/, 1) || 'jpg';
if (this.knownFileNames[name] === undefined) this.knownFileNames[name] = 0;else ++this.knownFileNames[name];
if (this.knownFileNames[name] === 0) cover.filename = name;else cover.filename = `${name} (${this.knownFileNames[name]})`;
cover.extension = extension;
}
async download(cover) {
if (cover.blob) return this.setBlob(cover, cover.blob);
return await new Promise((resolve, reject) => {
try {
GM_xmlhttpRequest({
url: cover.url,
method: 'GET',
responseType: 'blob',
anonymous: true,
headers: {
Origin: window.location.origin,
Referer: window.location.href
},
onload: response => {
if (response.status < 200 || response.status > 299) return reject(response.statusText);
try {
resolve(this.setBlob(cover, response.response));
} catch (error) {
reject(error);
}
},
onerror: reject,
onabort: reject,
ontimeout: reject
});
} catch (error) {
fetch(cover.url).then(response => {
if (!response.ok) throw new Error(response.statusText);
return response.blob();
}).then(blob => resolve(this.setBlob(cover, blob))).catch(reject);
}
});
}
selectRange(rangeStartCover, rangeEndCover) {
let select = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
if (!this.covers) return;
let rangeStart = this.covers.indexOf(rangeStartCover);
let rangeEnd = this.covers.indexOf(rangeEndCover);
if (rangeStart > rangeEnd) [rangeStart, rangeEnd] = [rangeEnd, rangeStart];
for (let i = rangeStart; i <= rangeEnd; i++) {
const cover = this.covers[i];
if (select && cover.errored) continue;
if (cover.select) cover.select(select);
}
}
isSelectAll = () => !this.covers.some(cover => cover.selected);
isCropped = () => this.covers.some(cover => cover.selected && cover.cropped);
updateButtons() {
const select = this.isSelectAll();
const cropped = this.isCropped();
if (select) this.buttons.selectAll.componentElement.innerText = 'Select All';else this.buttons.selectAll.componentElement.innerText = 'Deselect All';
this.buttons.selectAll.show();
if (select && this.covers.every(cover => cover.errored) || select && this.covers.some(cover => !cover.loaded && !cover.errored)) this.buttons.selectAll.disable();else this.buttons.selectAll.enable();
if (!cropped) this.buttons.crop.componentElement.innerText = 'Crop';else this.buttons.crop.componentElement.innerText = 'Uncrop';
if (this.covers.every(cover => !cover.cropAmount || !cover.blob || !cover.blobUrl)) this.buttons.crop.hide();else this.buttons.crop.show();
if (this.busy || select || this.covers.filter(cover => cover.selected && cover.cropAmount && cover.blob && cover.blobUrl).length <= 0) this.buttons.crop.disable();else this.buttons.crop.enable();
if (!this.covers.some(cover => cover.selected)) {
this.buttons.open.disable();
this.buttons.copy.disable();
} else {
this.buttons.open.enable();
this.buttons.copy.enable();
}
this.buttons.open.show();
this.buttons.copy.show();
if (this.covers.every(cover => !cover.blob)) this.buttons.zip.hide();else this.buttons.zip.show();
if (this.busy || select || this.covers.some(cover => cover.selected && !cover.blob)) this.buttons.zip.disable();else this.buttons.zip.enable();
if (this.covers.every(cover => !cover.blobUrl)) this.buttons.save.hide();else this.buttons.save.show();
if (this.busy || select || this.covers.some(cover => cover.selected && !cover.blobUrl)) this.buttons.save.disable();else this.buttons.save.enable();
}
selectAll() {
if (!this.covers) return;
this.selectRange(this.covers[0], this.covers[this.covers.length - 1], this.isSelectAll());
delete this.lastSelected;
}
getCropMethod(cover) {
if (cover.cropAmount) return cover.cropAmount;
if (cover.cropped || !cover.width || !cover.height) return;
const aspect = Math.floor(cover.width / cover.height * 100) / 100;
if (cover.width >= 880 && cover.width <= 964 && cover.height === 1200) return 120;
if (cover.width >= 220 && cover.width <= 241 && cover.height === 300) return 30;
if (cover.height > 4000 && aspect >= 0.73 && aspect < 0.8) return -355;
if (cover.width > 2000 && cover.height > 2000 && aspect >= 0.73 && aspect < 0.8) return -211;
if (cover.width < 2000 && cover.height > 2000 && aspect >= 0.73 && aspect < 0.8) return -224;
}
async crop() {
let covers = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.covers;
let force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (this.busy || !covers) return;
this.busy = true;
this.updateButtons();
const cropped = this.isCropped();
const coversToCrop = covers.filter(cover => force && cover.imageElement && cover.cropAmount && cover.blob && cover.blobUrl || cover.selected && cover.imageElement && cover.cropAmount && cover.blob && cover.blobUrl);
const progressBar = new SimpleProgressBar(coversToCrop.length);
progressBar.start();
await Promise.all(coversToCrop.map(async cover => {
if (force && cover.cropped) {
progressBar.update();
return;
} else if (cropped && !force) {
if (cover.cropped) {
cover.loaded = false;
this.removeBlob(cover);
cover.extension = cover.croppedExtension;
cover.cropped = false;
await this.loadCover(cover).catch(console.error);
}
progressBar.update();
return;
}
try {
const img = cover.imageElement;
const width = cover.width;
const height = cover.height;
const cropAmount = cover.cropAmount;
const absoluteCropAmount = Math.abs(cropAmount);
const croppedWidth = width - absoluteCropAmount;
const canvas = document.createElement('canvas');
canvas.width = croppedWidth;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (cropAmount > 0) ctx?.drawImage(img, 0, 0, croppedWidth, height, 0, 0, croppedWidth, height);else if (cropAmount < 0) ctx?.drawImage(img, absoluteCropAmount, 0, croppedWidth, height, 0, 0, croppedWidth, height);
const blob = await new Promise(resolve => canvas?.toBlob(blob => resolve(blob), 'image/png'));
if (blob) {
cover.loaded = false;
this.removeBlob(cover);
this.setBlob(cover, blob);
cover.croppedExtension = cover.extension;
cover.extension = 'png';
cover.cropped = true;
await this.loadCover(cover).catch(console.error);
}
} catch (error) {
console.error('Failed to crop cover:', cover.url, error);
cover.loaded = false;
this.removeBlob(cover);
if (cover.croppedExtension) cover.extension = cover.croppedExtension;
cover.cropped = false;
await this.loadCover(cover).catch(console.error);
}
progressBar.update();
}));
progressBar.remove();
this.busy = false;
this.updateButtons();
}
open() {
this.covers.forEach(cover => {
if (!cover.selected) return;
window.open(cover.blobUrl && cover.cropped ? cover.blobUrl : cover.url, '_blank');
});
}
copy() {
let clipboardText = '';
this.covers.forEach(cover => {
if (!cover.selected) return;
clipboardText += cover.url + '\n';
});
navigator.clipboard.writeText(clipboardText).then(() => console.debug('Copied to clipboard:', clipboardText), () => {
console.error('Failed to copy to clipboard:', clipboardText);
alertModal('Failed to copy to clipboard!\n' + clipboardText, 'error').catch(console.error);
});
}
filterFileName = name => name.replace(/[\\/:"*?<>|]/g, '_');
save() {
this.covers.forEach(cover => {
if (this.busy || !cover.selected) return;
const saveName = `${window.location.hostname}/${this.title ? this.filterFileName(this.title) : 'unknown title'}/${this.filterFileName(cover.filename)}.${cover.extension}`;
const saveFile = () => fileSaver.saveAs(cover.blobUrl || cover.url, saveName.replaceAll('/', ' - '));
try {
GM_download({
url: cover.url,
name: `covers/${saveName}`,
// @ts-ignore
saveAs: false,
headers: {
Origin: window.location.origin,
Referer: window.location.href
},
onerror: saveFile
});
} catch (error) {
saveFile();
}
});
}
async zip() {
if (this.busy) return;
this.busy = true;
this.updateButtons();
const progressBar = new SimpleProgressBar();
const onError = error => {
console.error(error);
progressBar.remove();
this.busy = false;
this.updateButtons();
alertModal('Failed to zip covers!\n' + error, 'error').catch(console.error);
};
const chunks = [];
const zip = new Zip((error, chunk, final) => {
if (error) onError(error);else chunks.push(chunk);
if (final) {
progressBar.remove();
this.busy = false;
this.updateButtons();
if (this.aborted) return;
fileSaver.saveAs(new Blob(chunks, {
type: 'application/zip'
}), `${window.location.hostname} - ${this.title ? this.filterFileName(this.title) : 'unknown title'} - covers.zip`);
}
});
const covers = this.covers.filter(cover => cover.selected && cover.blob);
progressBar.start({
maxValue: covers.length
});
for (const cover of covers) {
if (this.aborted) {
zip.end();
break;
}
try {
await this.zipCover(zip, cover);
progressBar.update();
} catch (error) {
zip.end();
onError(error);
break;
}
if (progressBar.currentValue >= progressBar.maxValue) zip.end();
}
}
async zipCover(zip, cover) {
return await new Promise((resolve, reject) => {
if (!cover.blob) throw new Error('No blob');
const reader = new FileReader();
reader.addEventListener('load', event => {
if (!event.target) return reject('No target');
const data = new Uint8Array(event.target.result);
const file = new ZipPassThrough(`${cover.filename}.${cover.extension}`);
zip.add(file);
file.push(data, true);
resolve();
});
reader.addEventListener('error', reject);
reader.readAsArrayBuffer(cover.blob);
});
}
}
const asinRegex = '(?:[/dp]|$)([A-Z0-9]{10})';
class AmazonDownloadCovers extends AmazonBookmarklet {
routes = (() => [`.*${asinRegex}`])();
main = () => {
const getAsin = url => getMatch(url, new RegExp(asinRegex), 1);
const getCoverUrl = asin => `https://${window.location.host}/images/P/${asin}.01.MAIN._SCRM_.jpg`;
const books = function () {
let element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
return element.querySelectorAll('a.itemImageLink');
};
let downloader;
const covers = [];
const locationAsin = getAsin(window.location.pathname);
if (!locationAsin) {
const error = new Error('Asin not found!');
console.error(error);
alertModal(error, 'error').catch(console.error);
return;
}
if (books().length > 0) {
const pageSize = 100;
const itemsElement = document.querySelector('#seriesAsinListPagination, #seriesAsinListPagination_volume');
const maxItems = parseInt(itemsElement?.getAttribute('data-number_of_items') || books().length.toString());
const maxPage = Math.ceil(maxItems / pageSize);
downloader = new CoverDownloader(async loadIndex => {
let seriesPage = await fetch(`https://${window.location.host}/kindle-dbs/productPage/ajax/seriesAsinList?asin=${locationAsin}&pageNumber=${loadIndex}&pageSize=${pageSize}`, {
headers: {
'User-Agent': userAgentDesktop
}
}).then(response => response.text()).then(html => new DOMParser().parseFromString(html, 'text/html')).catch(console.error);
if (!seriesPage || books(seriesPage).length < 1) {
if (loadIndex !== 1) throw new Error('Failed to fetch series page!');
seriesPage = document;
}
books(seriesPage).forEach(element => {
const asin = getAsin(element.href);
if (!asin) return;
covers.push({
url: getCoverUrl(asin),
title: element.getAttribute('title')
});
});
return covers;
}, {
loadMax: maxPage,
title: document.querySelector('#collection-masthead__title, #title-sdp-aw')?.textContent
});
} else {
const bookTitle = document.querySelector('#productTitle, #ebooksTitle, #title')?.textContent?.split(' ')[0];
downloader = new CoverDownloader(async () => {
covers.push({
url: getCoverUrl(locationAsin),
title: bookTitle
});
return covers;
}, {
title: (document.querySelector('#seriesBulletWidget_feature_div > .a-link-normal') || document.querySelector('#mobile_productTitleGroup_inner_feature_div > .a-row > .a-row > .a-link-normal'))?.textContent?.replace(/.*: /, '')
});
}
downloader.add();
};
}
class BookwalkerBookmarklet extends Bookmarklet {
website = '^((r18|global|viewer-trial).)?bookwalker.jp';
}
class BookwalkerDownloadCovers extends BookwalkerBookmarklet {
routes = ['/de:uuid', '/series/:numid', '/:numid/:numid/viewer.html'];
main = () => {
const getSeriesId = link => getMatch(link, /series\/(\d+)/, 1);
const getBookId = link => getMatch(link, /(?:de|cid=)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/, 1);
const getLastPage = elements => {
let lastPage = 1;
elements.forEach(element => {
const url = element.getAttribute('href') || element.getAttribute('value');
if (!url) return;
const page = getMatch(url, /page=(\d+)/, 1);
if (!page) return;
const pageNum = parseInt(page);
if (lastPage < pageNum) lastPage = pageNum;
});
return lastPage;
};
let downloader;
const covers = [];
if (window.location.hostname === 'viewer-trial.bookwalker.jp') {
const bookId = getBookId(window.location.search);
downloader = new CoverDownloader(async () => {
const readerInfoUrl = `https://${window.location.host}/trial-page/c?cid=${bookId}&BID=0`;
const readerInfoResponse = await fetch(readerInfoUrl);
const readerInfo = await readerInfoResponse.json();
const readerUrl = readerInfo.cty === 0 ? readerInfo.url + 'normal_default/' : readerInfo.url;
const authString = `?pfCd=${readerInfo.auth_info.pfCd}&Policy=${readerInfo.auth_info.Policy}&Signature=${readerInfo.auth_info.Signature}&Key-Pair-Id=${readerInfo.auth_info['Key-Pair-Id']}`;
const configurationUrl = readerUrl + 'configuration_pack.json' + authString;
const configurationResponse = await fetch(configurationUrl);
const readerConfiguration = await configurationResponse.json();
const pages = getPages(readerConfiguration);
pages.forEach((page, i) => {
const imageUrl = readerUrl + page.chapter.file + '/' + page.page.No + '.' + page.chapter.type + authString;
covers.push({
url: imageUrl,
title: i.toString()
});
});
return covers;
function getPages(readerConfiguration) {
const pages = [];
for (const chapterInfo of readerConfiguration.configuration.contents) {
for (const pageInfo of readerConfiguration[chapterInfo.file].FileLinkInfo.PageLinkInfoList) {
const newPage = {
page: pageInfo.Page,
chapter: chapterInfo
};
pages.push(newPage);
}
}
return pages;
}
}, {
fileNamePrefix: 'Page',
title: document.querySelector('title')?.textContent
});
} else if (getBookId(window.location.pathname)) {
const bookId = getBookId(window.location.pathname);
const bookTitle = document.querySelector('.detail-book-title')?.textContent || document.querySelector('meta[property="og:title"]')?.getAttribute('content');
downloader = new CoverDownloader(async () => {
covers.push({
url: `https://c.roler.dev/bw/${bookId}`,
title: bookTitle
});
return covers;
}, {
title: document.querySelector(`a[href^="https://${window.location.host}/series/"]`)?.firstChild?.textContent
});
} else if (/series\/\d+/.test(window.location.pathname)) {
const seriesId = getSeriesId(window.location.pathname);
const lastPage = function () {
let element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
return getLastPage(element.querySelectorAll('a[href*="page="], option[value*="page="]'));
};
const seriesTitle = document.querySelector('.o-contents-section__title, .o-headline-ttl')?.textContent;
const wayomiSeriesTitle = document.querySelector('.o-ttsk-card__title')?.textContent;
const globalSeriesTitle = document.querySelector('.title-main-inner')?.textContent?.split('\n').find(title => title) || document.querySelector('.title-main')?.textContent;
downloader = new CoverDownloader(async loadIndex => {
let seriesPage = document;
if (wayomiSeriesTitle) {
seriesPage.querySelectorAll('.o-ttsk-list-item > a').forEach(element => {
const bookId = element.getAttribute('data-book-uuid');
if (!bookId) return;
covers.push({
url: `https://c.roler.dev/bw/${bookId}`,
title: element.getAttribute('data-book-title')
});
});
return covers;
}
if (downloader.loadMax > 1 || !/\/list/.test(window.location.pathname)) {
seriesPage = await fetch(`https://${window.location.host}/series/${seriesId}/list/?order=title&page=${loadIndex}`, {
headers: {
'User-Agent': userAgentDesktop
}
}).then(response => response.text()).then(html => new DOMParser().parseFromString(html, 'text/html'));
}
if (!/\/list/.test(window.location.pathname) && loadIndex === 1) downloader.loadMax = lastPage(seriesPage);
seriesPage.querySelectorAll('a.m-thumb__image > img, a.a-thumb-img > img, a.a-tile-thumb-img > img').forEach(element => {
const bookId = getBookId(element.parentElement.href);
if (!bookId) return;
covers.push({
url: `https://c.roler.dev/bw/${bookId}`,
title: element.alt
});
});
return covers;
}, {
loadMax: wayomiSeriesTitle ? 1 : lastPage(),
title: wayomiSeriesTitle || seriesTitle || globalSeriesTitle,
fileNamePrefix: wayomiSeriesTitle ? 'Chapter' : 'Volume'
});
}
try {
downloader.add();
} catch (error) {
console.error(error);
alertModal('Failed to initialize cover downloader!\n' + error, 'error').catch(console.error);
}
};
}
class BookliveBookmarklet extends Bookmarklet {
website = 'booklive.jp';
}
class BookliveDownloadCovers extends BookliveBookmarklet {
routes = ['/product/index/title_id/:numid/vol_no/:numid'];
main = () => {
const getTitleId = link => getMatch(link, /title_id\/(\d+)/, 1);
const getVolumeId = link => getMatch(link, /vol_no\/(\d+)/, 1);
const downloader = new CoverDownloader(async () => {
const covers = [];
const titleId = getTitleId(window.location.pathname);
document.querySelectorAll(`a[href^="/product/index/title_id/${titleId}/vol_no/"] > img`).forEach(element => {
const volumeId = getVolumeId(element.parentElement.href);
if (!volumeId) return;
const cover = {
title: element.alt,
url: `https://res.booklive.jp/${titleId}/${volumeId}/thumbnail/X.jpg`
};
if (covers.some(c => c.url === cover.url)) return;
covers.push(cover);
});
if (!covers.length) {
const volumeId = getVolumeId(window.location.pathname);
covers.push({
title: document.querySelector('#product_display_1')?.textContent,
url: `https://res.booklive.jp/${titleId}/${volumeId}/thumbnail/X.jpg`
});
}
return covers;
}, {
title: document.querySelector('.heading_title')?.textContent
});
downloader.add();
};
}
const settings = [];const universalSettings = new UniversalSettings();if (universalSettings.isWebsite()) {GM_registerMenuCommand('[Any Website] Settings Manager v1.0', () =>universalSettings.execute());settings.push({id: 'universal-settings_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Settings Manager',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Settings Manager bookmarklet (only the first letter will be used).',defaultValue: 's'});}const mangadexShowCoverData = new MangadexShowCoverData();if (mangadexShowCoverData.isWebsite()) {GM_registerMenuCommand('[MangaDex] Show Cover Data v4.2', () =>mangadexShowCoverData.execute());settings.push({id: 'mangadex-show_cover_data_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Show Cover Data',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Show Cover Data bookmarklet (only the first letter will be used).',defaultValue: ''});}const mangadexAddCoverDescriptions = new MangadexAddCoverDescriptions();if (mangadexAddCoverDescriptions.isWebsite()) {GM_registerMenuCommand('[MangaDex] Add Cover Descriptions v2.9', () =>mangadexAddCoverDescriptions.execute());settings.push({id: 'mangadex-add_cover_descriptions_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Add Cover Descriptions',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Add Cover Descriptions bookmarklet (only the first letter will be used).',defaultValue: ''});}const mangadexSearchMissingLinks = new MangadexSearchMissingLinks();if (mangadexSearchMissingLinks.isWebsite()) {GM_registerMenuCommand('[MangaDex] Search Missing Links v2.8', () =>mangadexSearchMissingLinks.execute());settings.push({id: 'mangadex-search_missing_links_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Search Missing Links',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Search Missing Links bookmarklet (only the first letter will be used).',defaultValue: ''});}const mangadexShortenLinks = new MangadexShortenLinks();if (mangadexShortenLinks.isWebsite()) {GM_registerMenuCommand('[MangaDex] Shorten Links v3.0', () =>mangadexShortenLinks.execute());settings.push({id: 'mangadex-shorten_links_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Shorten Links',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Shorten Links bookmarklet (only the first letter will be used).',defaultValue: ''});}const mangadexOpenLinks = new MangadexOpenLinks();if (mangadexOpenLinks.isWebsite()) {GM_registerMenuCommand('[MangaDex] Open Links v2.2', () =>mangadexOpenLinks.execute());settings.push({id: 'mangadex-open_links_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Open Links',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Open Links bookmarklet (only the first letter will be used).',defaultValue: ''});}const mangadexDelCoversByLang = new MangadexDelCoversByLang();if (mangadexDelCoversByLang.isWebsite()) {GM_registerMenuCommand('[MangaDex] Delete Covers by Language v2.4', () =>mangadexDelCoversByLang.execute());settings.push({id: 'mangadex-del_covers_by_lang_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Delete Covers by Language',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Delete Covers by Language bookmarklet (only the first letter will be used).',defaultValue: ''});}const mangadexSearchAllTitles = new MangadexSearchAllTitles();if (mangadexSearchAllTitles.isWebsite()) {GM_registerMenuCommand('[MangaDex] Search All Titles v1.3', () =>mangadexSearchAllTitles.execute());settings.push({id: 'mangadex-search_all_titles_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Search All Titles',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Search All Titles bookmarklet (only the first letter will be used).',defaultValue: ''});}const mangadexCloneTitle = new MangadexCloneTitle();if (mangadexCloneTitle.isWebsite()) {GM_registerMenuCommand('[MangaDex] Clone/Merge Title v1.7', () =>mangadexCloneTitle.execute());settings.push({id: 'mangadex-clone_title_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Clone/Merge Title',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Clone/Merge Title bookmarklet (only the first letter will be used).',defaultValue: ''});}const amazonDownloadCovers = new AmazonDownloadCovers();if (amazonDownloadCovers.isWebsite()) {GM_registerMenuCommand('[Amazon] Download Covers v3.2', () =>amazonDownloadCovers.execute());settings.push({id: 'amazon-download_covers_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Download Covers',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Download Covers bookmarklet (only the first letter will be used).',defaultValue: ''});}const bookwalkerDownloadCovers = new BookwalkerDownloadCovers();if (bookwalkerDownloadCovers.isWebsite()) {GM_registerMenuCommand('[BookWalker] Download Covers v2.4', () =>bookwalkerDownloadCovers.execute());settings.push({id: 'bookwalker-download_covers_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Download Covers',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Download Covers bookmarklet (only the first letter will be used).',defaultValue: ''});}const bookliveDownloadCovers = new BookliveDownloadCovers();if (bookliveDownloadCovers.isWebsite()) {GM_registerMenuCommand('[BookLive] Download Covers v1.7', () =>bookliveDownloadCovers.execute());settings.push({id: 'booklive-download_covers_key_shortcut',type: 'text',name: 'Keyboard Shortcut for Download Covers',description: 'A key to press while holding Ctrl + Shift + Alt to execute the Download Covers bookmarklet (only the first letter will be used).',defaultValue: ''});}const settingsField = new SettingsField({id: '1ed69755-08c1-4d22-8a7d-6c4377102cc7',name: 'UserScript',description: 'Settings only available when using the UserScript (reload the page to apply changes).',settings});universalSettings.additionalFields.push(settingsField);settingsField.load();const universalSettingsKeyShortcut = settingsField.getValue('universal-settings_key_shortcut')?.trim()?.charAt(0);if (universalSettingsKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === universalSettingsKeyShortcut.toLowerCase())universalSettings.execute();});const mangadexShowCoverDataKeyShortcut = settingsField.getValue('mangadex-show_cover_data_key_shortcut')?.trim()?.charAt(0);if (mangadexShowCoverDataKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexShowCoverDataKeyShortcut.toLowerCase())mangadexShowCoverData.execute();});const mangadexAddCoverDescriptionsKeyShortcut = settingsField.getValue('mangadex-add_cover_descriptions_key_shortcut')?.trim()?.charAt(0);if (mangadexAddCoverDescriptionsKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexAddCoverDescriptionsKeyShortcut.toLowerCase())mangadexAddCoverDescriptions.execute();});const mangadexSearchMissingLinksKeyShortcut = settingsField.getValue('mangadex-search_missing_links_key_shortcut')?.trim()?.charAt(0);if (mangadexSearchMissingLinksKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexSearchMissingLinksKeyShortcut.toLowerCase())mangadexSearchMissingLinks.execute();});const mangadexShortenLinksKeyShortcut = settingsField.getValue('mangadex-shorten_links_key_shortcut')?.trim()?.charAt(0);if (mangadexShortenLinksKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexShortenLinksKeyShortcut.toLowerCase())mangadexShortenLinks.execute();});const mangadexOpenLinksKeyShortcut = settingsField.getValue('mangadex-open_links_key_shortcut')?.trim()?.charAt(0);if (mangadexOpenLinksKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexOpenLinksKeyShortcut.toLowerCase())mangadexOpenLinks.execute();});const mangadexDelCoversByLangKeyShortcut = settingsField.getValue('mangadex-del_covers_by_lang_key_shortcut')?.trim()?.charAt(0);if (mangadexDelCoversByLangKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexDelCoversByLangKeyShortcut.toLowerCase())mangadexDelCoversByLang.execute();});const mangadexSearchAllTitlesKeyShortcut = settingsField.getValue('mangadex-search_all_titles_key_shortcut')?.trim()?.charAt(0);if (mangadexSearchAllTitlesKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexSearchAllTitlesKeyShortcut.toLowerCase())mangadexSearchAllTitles.execute();});const mangadexCloneTitleKeyShortcut = settingsField.getValue('mangadex-clone_title_key_shortcut')?.trim()?.charAt(0);if (mangadexCloneTitleKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === mangadexCloneTitleKeyShortcut.toLowerCase())mangadexCloneTitle.execute();});const amazonDownloadCoversKeyShortcut = settingsField.getValue('amazon-download_covers_key_shortcut')?.trim()?.charAt(0);if (amazonDownloadCoversKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === amazonDownloadCoversKeyShortcut.toLowerCase())amazonDownloadCovers.execute();});const bookwalkerDownloadCoversKeyShortcut = settingsField.getValue('bookwalker-download_covers_key_shortcut')?.trim()?.charAt(0);if (bookwalkerDownloadCoversKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === bookwalkerDownloadCoversKeyShortcut.toLowerCase())bookwalkerDownloadCovers.execute();});const bookliveDownloadCoversKeyShortcut = settingsField.getValue('booklive-download_covers_key_shortcut')?.trim()?.charAt(0);if (bookliveDownloadCoversKeyShortcut) document.body.addEventListener('keydown', (e) => {if (e.ctrlKey && e.shiftKey && e.altKey && e.key.toLowerCase() === bookliveDownloadCoversKeyShortcut.toLowerCase())bookliveDownloadCovers.execute();});
})();