CoroCoroRipper

Download Images From CoroCoro

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        CoroCoroRipper
// @namespace   adrian
// @author      adrian
// @match       https://www.corocoro.jp/*
// @version     1.0
// @description Download Images From CoroCoro
// @require     https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
// @require     https://unpkg.com/@zip.js/[email protected]/dist/zip-full.min.js
// @grant       GM_registerMenuCommand
// @license     MIT
// ==/UserScript==

const SHIFT_LEFT_32 = (1 << 16) * (1 << 16);
const SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32;

// Threshold chosen based on both benchmarking and knowledge about browser string
// data structures (which currently switch structure types at 12 bytes or more)
const TEXT_DECODER_MIN_LENGTH = 12;
const utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8');

const PBF_VARINT = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum
const PBF_FIXED64 = 1; // 64-bit: double, fixed64, sfixed64
const PBF_BYTES = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields
const PBF_FIXED32 = 5; // 32-bit: float, fixed32, sfixed32

class Pbf {
	/**
	 * @param {Uint8Array | ArrayBuffer} [buf]
	 */
	constructor(buf = new Uint8Array(16)) {
		this.buf = ArrayBuffer.isView(buf) ? buf : new Uint8Array(buf);
		this.dataView = new DataView(this.buf.buffer);
		this.pos = 0;
		this.type = 0;
		this.length = this.buf.length;
	}

	// === READING =================================================================

	/**
	 * @template T
	 * @param {(tag: number, result: T, pbf: Pbf) => void} readField
	 * @param {T} result
	 * @param {number} [end]
	 */
	readFields(readField, result, end = this.length) {
		while (this.pos < end) {
			const val = this.readVarint(),
				tag = val >> 3,
				startPos = this.pos;

			this.type = val & 0x7;
			readField(tag, result, this);

			if (this.pos === startPos) this.skip(val);
		}
		return result;
	}

	/**
	 * @template T
	 * @param {(tag: number, result: T, pbf: Pbf) => void} readField
	 * @param {T} result
	 */
	readMessage(readField, result) {
		return this.readFields(readField, result, this.readVarint() + this.pos);
	}

	readFixed32() {
		const val = this.dataView.getUint32(this.pos, true);
		this.pos += 4;
		return val;
	}

	readSFixed32() {
		const val = this.dataView.getInt32(this.pos, true);
		this.pos += 4;
		return val;
	}

	// 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)

	readFixed64() {
		const val = this.dataView.getUint32(this.pos, true) + this.dataView.getUint32(this.pos + 4, true) * SHIFT_LEFT_32;
		this.pos += 8;
		return val;
	}

	readSFixed64() {
		const val = this.dataView.getUint32(this.pos, true) + this.dataView.getInt32(this.pos + 4, true) * SHIFT_LEFT_32;
		this.pos += 8;
		return val;
	}

	readFloat() {
		const val = this.dataView.getFloat32(this.pos, true);
		this.pos += 4;
		return val;
	}

	readDouble() {
		const val = this.dataView.getFloat64(this.pos, true);
		this.pos += 8;
		return val;
	}

	/**
	 * @param {boolean} [isSigned]
	 */
	readVarint(isSigned) {
		const buf = this.buf;
		let val, b;

		b = buf[this.pos++]; val = b & 0x7f; if (b < 0x80) return val;
		b = buf[this.pos++]; val |= (b & 0x7f) << 7; if (b < 0x80) return val;
		b = buf[this.pos++]; val |= (b & 0x7f) << 14; if (b < 0x80) return val;
		b = buf[this.pos++]; val |= (b & 0x7f) << 21; if (b < 0x80) return val;
		b = buf[this.pos]; val |= (b & 0x0f) << 28;

		return readVarintRemainder(val, isSigned, this);
	}

	readVarint64() { // for compatibility with v2.0.1
		return this.readVarint(true);
	}

	readSVarint() {
		const num = this.readVarint();
		return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding
	}

	readBoolean() {
		return Boolean(this.readVarint());
	}

	readString() {
		const end = this.readVarint() + this.pos;
		const pos = this.pos;
		this.pos = end;

		if (end - pos >= TEXT_DECODER_MIN_LENGTH && utf8TextDecoder) {
			// longer strings are fast with the built-in browser TextDecoder API
			return utf8TextDecoder.decode(this.buf.subarray(pos, end));
		}
		// short strings are fast with our custom implementation
		return readUtf8(this.buf, pos, end);
	}

	readBytes() {
		const end = this.readVarint() + this.pos,
			buffer = this.buf.subarray(this.pos, end);
		this.pos = end;
		return buffer;
	}

	// verbose for performance reasons; doesn't affect gzipped size

	/**
	 * @param {number[]} [arr]
	 * @param {boolean} [isSigned]
	 */
	readPackedVarint(arr = [], isSigned) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readVarint(isSigned));
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedSVarint(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readSVarint());
		return arr;
	}
	/** @param {boolean[]} [arr] */
	readPackedBoolean(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readBoolean());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedFloat(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readFloat());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedDouble(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readDouble());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedFixed32(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readFixed32());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedSFixed32(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readSFixed32());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedFixed64(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readFixed64());
		return arr;
	}
	/** @param {number[]} [arr] */
	readPackedSFixed64(arr = []) {
		const end = this.readPackedEnd();
		while (this.pos < end) arr.push(this.readSFixed64());
		return arr;
	}
	readPackedEnd() {
		return this.type === PBF_BYTES ? this.readVarint() + this.pos : this.pos + 1;
	}

	/** @param {number} val */
	skip(val) {
		const type = val & 0x7;
		if (type === PBF_VARINT) while (this.buf[this.pos++] > 0x7f) { }
		else if (type === PBF_BYTES) this.pos = this.readVarint() + this.pos;
		else if (type === PBF_FIXED32) this.pos += 4;
		else if (type === PBF_FIXED64) this.pos += 8;
		else throw new Error(`Unimplemented type: ${type}`);
	}

	// === WRITING =================================================================

	/**
	 * @param {number} tag
	 * @param {number} type
	 */
	writeTag(tag, type) {
		this.writeVarint((tag << 3) | type);
	}

	/** @param {number} min */
	realloc(min) {
		let length = this.length || 16;

		while (length < this.pos + min) length *= 2;

		if (length !== this.length) {
			const buf = new Uint8Array(length);
			buf.set(this.buf);
			this.buf = buf;
			this.dataView = new DataView(buf.buffer);
			this.length = length;
		}
	}

	finish() {
		this.length = this.pos;
		this.pos = 0;
		return this.buf.subarray(0, this.length);
	}

	/** @param {number} val */
	writeFixed32(val) {
		this.realloc(4);
		this.dataView.setInt32(this.pos, val, true);
		this.pos += 4;
	}

	/** @param {number} val */
	writeSFixed32(val) {
		this.realloc(4);
		this.dataView.setInt32(this.pos, val, true);
		this.pos += 4;
	}

	/** @param {number} val */
	writeFixed64(val) {
		this.realloc(8);
		this.dataView.setInt32(this.pos, val & -1, true);
		this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);
		this.pos += 8;
	}

	/** @param {number} val */
	writeSFixed64(val) {
		this.realloc(8);
		this.dataView.setInt32(this.pos, val & -1, true);
		this.dataView.setInt32(this.pos + 4, Math.floor(val * SHIFT_RIGHT_32), true);
		this.pos += 8;
	}

	/** @param {number} val */
	writeVarint(val) {
		val = +val || 0;

		if (val > 0xfffffff || val < 0) {
			writeBigVarint(val, this);
			return;
		}

		this.realloc(4);

		this.buf[this.pos++] = val & 0x7f | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
		this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
		this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
		this.buf[this.pos++] = (val >>> 7) & 0x7f;
	}

	/** @param {number} val */
	writeSVarint(val) {
		this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);
	}

	/** @param {boolean} val */
	writeBoolean(val) {
		this.writeVarint(+val);
	}

	/** @param {string} str */
	writeString(str) {
		str = String(str);
		this.realloc(str.length * 4);

		this.pos++; // reserve 1 byte for short string length

		const startPos = this.pos;
		// write the string directly to the buffer and see how much was written
		this.pos = writeUtf8(this.buf, str, this.pos);
		const len = this.pos - startPos;

		if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);

		// finally, write the message length in the reserved place and restore the position
		this.pos = startPos - 1;
		this.writeVarint(len);
		this.pos += len;
	}

	/** @param {number} val */
	writeFloat(val) {
		this.realloc(4);
		this.dataView.setFloat32(this.pos, val, true);
		this.pos += 4;
	}

	/** @param {number} val */
	writeDouble(val) {
		this.realloc(8);
		this.dataView.setFloat64(this.pos, val, true);
		this.pos += 8;
	}

	/** @param {Uint8Array} buffer */
	writeBytes(buffer) {
		const len = buffer.length;
		this.writeVarint(len);
		this.realloc(len);
		for (let i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];
	}

	/**
	 * @template T
	 * @param {(obj: T, pbf: Pbf) => void} fn
	 * @param {T} obj
	 */
	writeRawMessage(fn, obj) {
		this.pos++; // reserve 1 byte for short message length

		// write the message directly to the buffer and see how much was written
		const startPos = this.pos;
		fn(obj, this);
		const len = this.pos - startPos;

		if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);

		// finally, write the message length in the reserved place and restore the position
		this.pos = startPos - 1;
		this.writeVarint(len);
		this.pos += len;
	}

	/**
	 * @template T
	 * @param {number} tag
	 * @param {(obj: T, pbf: Pbf) => void} fn
	 * @param {T} obj
	 */
	writeMessage(tag, fn, obj) {
		this.writeTag(tag, PBF_BYTES);
		this.writeRawMessage(fn, obj);
	}

	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedVarint(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedVarint, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedSVarint(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedSVarint, arr);
	}
	/**
	 * @param {number} tag
	 * @param {boolean[]} arr
	 */
	writePackedBoolean(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedBoolean, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedFloat(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedFloat, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedDouble(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedDouble, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedFixed32(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedFixed32, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedSFixed32(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedSFixed32, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedFixed64(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedFixed64, arr);
	}
	/**
	 * @param {number} tag
	 * @param {number[]} arr
	 */
	writePackedSFixed64(tag, arr) {
		if (arr.length) this.writeMessage(tag, writePackedSFixed64, arr);
	}

	/**
	 * @param {number} tag
	 * @param {Uint8Array} buffer
	 */
	writeBytesField(tag, buffer) {
		this.writeTag(tag, PBF_BYTES);
		this.writeBytes(buffer);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeFixed32Field(tag, val) {
		this.writeTag(tag, PBF_FIXED32);
		this.writeFixed32(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeSFixed32Field(tag, val) {
		this.writeTag(tag, PBF_FIXED32);
		this.writeSFixed32(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeFixed64Field(tag, val) {
		this.writeTag(tag, PBF_FIXED64);
		this.writeFixed64(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeSFixed64Field(tag, val) {
		this.writeTag(tag, PBF_FIXED64);
		this.writeSFixed64(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeVarintField(tag, val) {
		this.writeTag(tag, PBF_VARINT);
		this.writeVarint(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeSVarintField(tag, val) {
		this.writeTag(tag, PBF_VARINT);
		this.writeSVarint(val);
	}
	/**
	 * @param {number} tag
	 * @param {string} str
	 */
	writeStringField(tag, str) {
		this.writeTag(tag, PBF_BYTES);
		this.writeString(str);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeFloatField(tag, val) {
		this.writeTag(tag, PBF_FIXED32);
		this.writeFloat(val);
	}
	/**
	 * @param {number} tag
	 * @param {number} val
	 */
	writeDoubleField(tag, val) {
		this.writeTag(tag, PBF_FIXED64);
		this.writeDouble(val);
	}
	/**
	 * @param {number} tag
	 * @param {boolean} val
	 */
	writeBooleanField(tag, val) {
		this.writeVarintField(tag, +val);
	}
};

/**
 * @param {number} l
 * @param {boolean | undefined} s
 * @param {Pbf} p
 */
function readVarintRemainder(l, s, p) {
	const buf = p.buf;
	let h, b;

	b = buf[p.pos++]; h = (b & 0x70) >> 4; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x7f) << 3; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x7f) << 10; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x7f) << 17; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x7f) << 24; if (b < 0x80) return toNum(l, h, s);
	b = buf[p.pos++]; h |= (b & 0x01) << 31; if (b < 0x80) return toNum(l, h, s);

	throw new Error('Expected varint not more than 10 bytes');
}

/**
 * @param {number} low
 * @param {number} high
 * @param {boolean} [isSigned]
 */
function toNum(low, high, isSigned) {
	return isSigned ? high * 0x100000000 + (low >>> 0) : ((high >>> 0) * 0x100000000) + (low >>> 0);
}

/**
 * @param {number} val
 * @param {Pbf} pbf
 */
function writeBigVarint(val, pbf) {
	let low, high;

	if (val >= 0) {
		low = (val % 0x100000000) | 0;
		high = (val / 0x100000000) | 0;
	} else {
		low = ~(-val % 0x100000000);
		high = ~(-val / 0x100000000);

		if (low ^ 0xffffffff) {
			low = (low + 1) | 0;
		} else {
			low = 0;
			high = (high + 1) | 0;
		}
	}

	if (val >= 0x10000000000000000 || val < -0x10000000000000000) {
		throw new Error('Given varint doesn\'t fit into 10 bytes');
	}

	pbf.realloc(10);

	writeBigVarintLow(low, high, pbf);
	writeBigVarintHigh(high, pbf);
}

/**
 * @param {number} high
 * @param {number} low
 * @param {Pbf} pbf
 */
function writeBigVarintLow(low, high, pbf) {
	pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
	pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
	pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
	pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
	pbf.buf[pbf.pos] = low & 0x7f;
}

/**
 * @param {number} high
 * @param {Pbf} pbf
 */
function writeBigVarintHigh(high, pbf) {
	const lsb = (high & 0x07) << 4;

	pbf.buf[pbf.pos++] |= lsb | ((high >>>= 3) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
	pbf.buf[pbf.pos++] = high & 0x7f;
}

/**
 * @param {number} startPos
 * @param {number} len
 * @param {Pbf} pbf
 */
function makeRoomForExtraLength(startPos, len, pbf) {
	const extraLen =
		len <= 0x3fff ? 1 :
			len <= 0x1fffff ? 2 :
				len <= 0xfffffff ? 3 : Math.floor(Math.log(len) / (Math.LN2 * 7));

	// if 1 byte isn't enough for encoding message length, shift the data to the right
	pbf.realloc(extraLen);
	for (let i = pbf.pos - 1; i >= startPos; i--) pbf.buf[i + extraLen] = pbf.buf[i];
}

/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedVarint(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeVarint(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedSVarint(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeSVarint(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedFloat(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeFloat(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedDouble(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeDouble(arr[i]);
}
/**
 * @param {boolean[]} arr
 * @param {Pbf} pbf
 */
function writePackedBoolean(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedFixed32(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedSFixed32(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedFixed64(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);
}
/**
 * @param {number[]} arr
 * @param {Pbf} pbf
 */
function writePackedSFixed64(arr, pbf) {
	for (let i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]);
}

// Buffer code below from https://github.com/feross/buffer, MIT-licensed

/**
 * @param {Uint8Array} buf
 * @param {number} pos
 * @param {number} end
 */
function readUtf8(buf, pos, end) {
	let str = '';
	let i = pos;

	while (i < end) {
		const b0 = buf[i];
		let c = null; // codepoint
		let bytesPerSequence =
			b0 > 0xEF ? 4 :
				b0 > 0xDF ? 3 :
					b0 > 0xBF ? 2 : 1;

		if (i + bytesPerSequence > end) break;

		let b1, b2, b3;

		if (bytesPerSequence === 1) {
			if (b0 < 0x80) {
				c = b0;
			}
		} else if (bytesPerSequence === 2) {
			b1 = buf[i + 1];
			if ((b1 & 0xC0) === 0x80) {
				c = (b0 & 0x1F) << 0x6 | (b1 & 0x3F);
				if (c <= 0x7F) {
					c = null;
				}
			}
		} else if (bytesPerSequence === 3) {
			b1 = buf[i + 1];
			b2 = buf[i + 2];
			if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80) {
				c = (b0 & 0xF) << 0xC | (b1 & 0x3F) << 0x6 | (b2 & 0x3F);
				if (c <= 0x7FF || (c >= 0xD800 && c <= 0xDFFF)) {
					c = null;
				}
			}
		} else if (bytesPerSequence === 4) {
			b1 = buf[i + 1];
			b2 = buf[i + 2];
			b3 = buf[i + 3];
			if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) {
				c = (b0 & 0xF) << 0x12 | (b1 & 0x3F) << 0xC | (b2 & 0x3F) << 0x6 | (b3 & 0x3F);
				if (c <= 0xFFFF || c >= 0x110000) {
					c = null;
				}
			}
		}

		if (c === null) {
			c = 0xFFFD;
			bytesPerSequence = 1;

		} else if (c > 0xFFFF) {
			c -= 0x10000;
			str += String.fromCharCode(c >>> 10 & 0x3FF | 0xD800);
			c = 0xDC00 | c & 0x3FF;
		}

		str += String.fromCharCode(c);
		i += bytesPerSequence;
	}

	return str;
}

/**
 * @param {Uint8Array} buf
 * @param {string} str
 * @param {number} pos
 */
function writeUtf8(buf, str, pos) {
	for (let i = 0, c, lead; i < str.length; i++) {
		c = str.charCodeAt(i); // code point

		if (c > 0xD7FF && c < 0xE000) {
			if (lead) {
				if (c < 0xDC00) {
					buf[pos++] = 0xEF;
					buf[pos++] = 0xBF;
					buf[pos++] = 0xBD;
					lead = c;
					continue;
				} else {
					c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;
					lead = null;
				}
			} else {
				if (c > 0xDBFF || (i + 1 === str.length)) {
					buf[pos++] = 0xEF;
					buf[pos++] = 0xBF;
					buf[pos++] = 0xBD;
				} else {
					lead = c;
				}
				continue;
			}
		} else if (lead) {
			buf[pos++] = 0xEF;
			buf[pos++] = 0xBF;
			buf[pos++] = 0xBD;
			lead = null;
		}

		if (c < 0x80) {
			buf[pos++] = c;
		} else {
			if (c < 0x800) {
				buf[pos++] = c >> 0x6 | 0xC0;
			} else {
				if (c < 0x10000) {
					buf[pos++] = c >> 0xC | 0xE0;
				} else {
					buf[pos++] = c >> 0x12 | 0xF0;
					buf[pos++] = c >> 0xC & 0x3F | 0x80;
				}
				buf[pos++] = c >> 0x6 & 0x3F | 0x80;
			}
			buf[pos++] = c & 0x3F | 0x80;
		}
	}
	return pos;
}

const fromHexString = (hexString) =>
	Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));

async function createDecryptFunc(keyHex, ivHex) {
	const key = await window.crypto.subtle.importKey("raw", fromHexString(keyHex), "AES-CBC", true, [
		"decrypt",
	]);
	const iv = fromHexString(ivHex);
	return async (data) => {
		return await window.crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, data);
	};
}

function toPng(webp) {
	return new Promise((resolve, reject) => {
		const canvas = document.createElement("canvas");
		const context = canvas.getContext("2d");
		const image = new Image();
		image.src = URL.createObjectURL(new Blob([webp]));
		image.crossOrigin = "anonymous";
		image.onload = (e) => {
			canvas.width = image.width;
			canvas.height = image.height;
			URL.revokeObjectURL(e.target.src);
			context.drawImage(e.target, 0, 0, canvas.width, canvas.height);
			canvas.toBlob(
				(data) => {
					resolve(data);
				},
				"image/png",
				100,
			);
		};
		image.onerror = (e) => reject(e);
	});
}

function readRoot(tag, data, pbf) {
	if (tag === 2) {
		data.images = pbf.readMessage(readImageMessage, data.images);
		return;
	}
	if (tag === 19) {
		data.key = pbf.readString();
		return;
	}
	if (tag === 20) {
		data.iv = pbf.readString();
		return;
	}
}

function readImageMessage(tag, message, pbf) {
	if (tag === 1) {
		message.push(pbf.readString());
		return;
	}
}

const decodeProtobuf = (data) => new Pbf(data).readFields(readRoot, { images: [] });

const downloadImages = async () => {
	if (
		!/https:\/\/www\.corocoro\.jp\/chapter\/.*\/viewer/.test(
			window.location.href,
		)
	)
		return;
	const progressBar = document.createElement("div");
	progressBar.id = "dl-progress";
	progressBar.textContent = "Starting...";
	progressBar.style.padding = "20px";
	progressBar.style.backgroundColor = "black";
	progressBar.style.borderRadius = "10px";
	progressBar.style.border = "1px solid white";
	progressBar.style.boxShadow = "0 25px 50px -12px rgb(0 0 0 / 0.25)";
	progressBar.style.position = "fixed";
	progressBar.style.left = "50%";
	progressBar.style.top = "50%";
	progressBar.style.transform = "translate(-50%,-50%)";
	progressBar.style.zIndex = "9999";
	progressBar.style.fontSize = "20px";
	progressBar.style.color = "white";
	document.body.appendChild(progressBar);

	const currentPath = window.location.pathname;
	const pathSplit = currentPath.split("/");
	pathSplit.pop();
	const chapterId = pathSplit.pop();
	const apiData = decodeProtobuf(await fetch(
		`https://www.corocoro.jp/api/csr?rq=chapter/viewer&chapter_id=${chapterId}`,
		{
			method: "PUT",
		},
	).then((res) => res.arrayBuffer()));

	const images = apiData.images;
	console.log(images);
	progressBar.textContent = `${images.length} images found.`;
	const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), {
		bufferedWrite: true,
	});
	const decrypt = await createDecryptFunc(apiData.key, apiData.iv);
	for (let i = 0; i < images.length; i++) {
		const image = images[i];
		const response = await fetch(image);
		if (!response.ok) {
			progressBar.textContent = `failed to fetch image ${i + 1}/${images.length}`;
			throw new Error("Failed to fetch image");
		}
		const arrayBuffer = await response.arrayBuffer();
		const decryptedData = await decrypt(new Uint8Array(arrayBuffer));
		zipWriter.add(
			`${i + 1}.png`,
			new zip.BlobReader(await toPng(decryptedData)),
			{},
		);
		progressBar.textContent = `fetched and decrypted image ${i + 1}/${images.length}`;
		console.log("done with ", i + 1);
	}
	console.log("image fetching done. generating zip");
	progressBar.textContent = "image fetching done. generating zip";
	const blobURL = URL.createObjectURL(await zipWriter.close());
	const link = document.createElement("a");
	link.href = blobURL;
	link.download = `${document.title}.zip`;
	link.click();
	progressBar.textContent = "done.";
	setTimeout(() => progressBar.remove(), 1000);
};

const updateButton = () => {
	console.log("loading");
	let dlButton = document.body.querySelector("#dl-button");
	if (!dlButton) {
		dlButton = document.createElement("button");
		dlButton.id = "dl-button";
		dlButton.textContent = "Download";
		dlButton.style.padding = "5px 12px";
		dlButton.style.backgroundColor = "#ef0029";
		dlButton.style.borderRadius = "8px";
		dlButton.style.border = "3px solid #000";
		dlButton.style.boxShadow = "0 4px 0 #000";
		dlButton.style.position = "absolute";
		dlButton.style.right = "5px";
		dlButton.style.bottom = "5px";
		dlButton.style.zIndex = "9999";
		dlButton.style.fontSize = ".75rem";
		dlButton.style.fontWeight = "800";
		dlButton.style.color = "white";
		dlButton.addEventListener("click", () => downloadImages());
		document.body.appendChild(dlButton);
	}
	dlButton.style.display = /https:\/\/www\.corocoro\.jp\/chapter\/.*\/viewer/.test(
		window.location.href,
	) ? "block" : "none";
};

updateButton();

(() => {
	let oldPushState = history.pushState;
	history.pushState = function pushState() {
		let ret = oldPushState.apply(this, arguments);
		window.dispatchEvent(new Event('pushstate'));
		window.dispatchEvent(new Event('locationchange'));
		return ret;
	};

	let oldReplaceState = history.replaceState;
	history.replaceState = function replaceState() {
		let ret = oldReplaceState.apply(this, arguments);
		window.dispatchEvent(new Event('replacestate'));
		window.dispatchEvent(new Event('locationchange'));
		return ret;
	};

	window.addEventListener('popstate', () => {
		window.dispatchEvent(new Event('locationchange'));
	});
})();

window.addEventListener('locationchange', function () {
	updateButton();
});

VM.shortcut.register("cm-s", () => downloadImages());
VM.shortcut.enable();

GM_registerMenuCommand("Download Images (Ctrl/Cmd + S)", () =>
	downloadImages(),
);