0?1:0}var compare;if((compare=sgn(date1.getFullYear()-date2.getFullYear()))===0){if((compare=sgn(date1.getMonth()-date2.getMonth()))===0){compare=sgn(date1.getDate()-date2.getDate())}}return compare}function getFirstWeekStartDate(janFourth){switch(janFourth.getDay()){case 0:return new Date(janFourth.getFullYear()-1,11,29);case 1:return janFourth;case 2:return new Date(janFourth.getFullYear(),0,3);case 3:return new Date(janFourth.getFullYear(),0,2);case 4:return new Date(janFourth.getFullYear(),0,1);case 5:return new Date(janFourth.getFullYear()-1,11,31);case 6:return new Date(janFourth.getFullYear()-1,11,30)}}function getWeekBasedYear(date){var thisDate=addDays(new Date(date.tm_year+1900,0,1),date.tm_yday);var janFourthThisYear=new Date(thisDate.getFullYear(),0,4);var janFourthNextYear=new Date(thisDate.getFullYear()+1,0,4);var firstWeekStartThisYear=getFirstWeekStartDate(janFourthThisYear);var firstWeekStartNextYear=getFirstWeekStartDate(janFourthNextYear);if(compareByDay(firstWeekStartThisYear,thisDate)<=0){if(compareByDay(firstWeekStartNextYear,thisDate)<=0){return thisDate.getFullYear()+1}return thisDate.getFullYear()}return thisDate.getFullYear()-1}var EXPANSION_RULES_2={\"%a\":function(date){return WEEKDAYS[date.tm_wday].substring(0,3)},\"%A\":function(date){return WEEKDAYS[date.tm_wday]},\"%b\":function(date){return MONTHS[date.tm_mon].substring(0,3)},\"%B\":function(date){return MONTHS[date.tm_mon]},\"%C\":function(date){var year=date.tm_year+1900;return leadingNulls(year/100|0,2)},\"%d\":function(date){return leadingNulls(date.tm_mday,2)},\"%e\":function(date){return leadingSomething(date.tm_mday,2,\" \")},\"%g\":function(date){return getWeekBasedYear(date).toString().substring(2)},\"%G\":function(date){return getWeekBasedYear(date)},\"%H\":function(date){return leadingNulls(date.tm_hour,2)},\"%I\":function(date){var twelveHour=date.tm_hour;if(twelveHour==0)twelveHour=12;else if(twelveHour>12)twelveHour-=12;return leadingNulls(twelveHour,2)},\"%j\":function(date){return leadingNulls(date.tm_mday+arraySum(isLeapYear(date.tm_year+1900)?MONTH_DAYS_LEAP:MONTH_DAYS_REGULAR,date.tm_mon-1),3)},\"%m\":function(date){return leadingNulls(date.tm_mon+1,2)},\"%M\":function(date){return leadingNulls(date.tm_min,2)},\"%n\":function(){return\"\\n\"},\"%p\":function(date){if(date.tm_hour>=0&&date.tm_hour<12){return\"AM\"}return\"PM\"},\"%S\":function(date){return leadingNulls(date.tm_sec,2)},\"%t\":function(){return\"\\t\"},\"%u\":function(date){return date.tm_wday||7},\"%U\":function(date){var days=date.tm_yday+7-date.tm_wday;return leadingNulls(Math.floor(days/7),2)},\"%V\":function(date){var val=Math.floor((date.tm_yday+7-(date.tm_wday+6)%7)/7);if((date.tm_wday+371-date.tm_yday-2)%7<=2){val++}if(!val){val=52;var dec31=(date.tm_wday+7-date.tm_yday-1)%7;if(dec31==4||dec31==5&&isLeapYear(date.tm_year%400-1)){val++}}else if(val==53){var jan1=(date.tm_wday+371-date.tm_yday)%7;if(jan1!=4&&(jan1!=3||!isLeapYear(date.tm_year)))val=1}return leadingNulls(val,2)},\"%w\":function(date){return date.tm_wday},\"%W\":function(date){var days=date.tm_yday+7-(date.tm_wday+6)%7;return leadingNulls(Math.floor(days/7),2)},\"%y\":function(date){return(date.tm_year+1900).toString().substring(2)},\"%Y\":function(date){return date.tm_year+1900},\"%z\":function(date){var off=date.tm_gmtoff;var ahead=off>=0;off=Math.abs(off)/60;off=off/60*100+off%60;return(ahead?\"+\":\"-\")+String(\"0000\"+off).slice(-4)},\"%Z\":function(date){return date.tm_zone},\"%%\":function(){return\"%\"}};pattern=pattern.replace(/%%/g,\"\\0\\0\");for(var rule in EXPANSION_RULES_2){if(pattern.includes(rule)){pattern=pattern.replace(new RegExp(rule,\"g\"),EXPANSION_RULES_2[rule](date))}}pattern=pattern.replace(/\\0\\0/g,\"%\");var bytes=intArrayFromString(pattern,false);if(bytes.length>maxsize){return 0}writeArrayToMemory(bytes,s);return bytes.length-1}var FSNode=function(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev};var readMode=292|73;var writeMode=146;Object.defineProperties(FSNode.prototype,{read:{get:function(){return(this.mode&readMode)===readMode},set:function(val){val?this.mode|=readMode:this.mode&=~readMode}},write:{get:function(){return(this.mode&writeMode)===writeMode},set:function(val){val?this.mode|=writeMode:this.mode&=~writeMode}},isFolder:{get:function(){return FS.isDir(this.mode)}},isDevice:{get:function(){return FS.isChrdev(this.mode)}}});FS.FSNode=FSNode;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();var wasmImports={\"b\":___assert_fail,\"f\":___cxa_throw,\"ka\":___dlsym,\"R\":___syscall__newselect,\"L\":___syscall_accept4,\"K\":___syscall_bind,\"J\":___syscall_connect,\"la\":___syscall_faccessat,\"g\":___syscall_fcntl64,\"ha\":___syscall_fstat64,\"U\":___syscall_getdents64,\"I\":___syscall_getpeername,\"H\":___syscall_getsockname,\"G\":___syscall_getsockopt,\"y\":___syscall_ioctl,\"F\":___syscall_listen,\"ea\":___syscall_lstat64,\"$\":___syscall_mkdirat,\"fa\":___syscall_newfstatat,\"w\":___syscall_openat,\"V\":___syscall_poll,\"E\":___syscall_recvfrom,\"T\":___syscall_renameat,\"S\":___syscall_rmdir,\"D\":___syscall_sendto,\"v\":___syscall_socket,\"ga\":___syscall_stat64,\"O\":___syscall_unlinkat,\"ia\":__emscripten_get_now_is_monotonic,\"M\":__emscripten_throw_longjmp,\"Y\":__gmtime_js,\"Z\":__localtime_js,\"_\":__mktime_js,\"W\":__mmap_js,\"X\":__munmap_js,\"P\":__tzset_js,\"a\":_abort,\"t\":_dlopen,\"oa\":_emscripten_asm_const_int,\"l\":_emscripten_date_now,\"Q\":_emscripten_get_heap_max,\"p\":_emscripten_get_now,\"ja\":_emscripten_memcpy_big,\"N\":_emscripten_resize_heap,\"ca\":_environ_get,\"da\":_environ_sizes_get,\"o\":_exit,\"m\":_fd_close,\"ba\":_fd_fdstat_get,\"x\":_fd_read,\"aa\":_fd_seek,\"q\":_fd_write,\"k\":_getaddrinfo,\"i\":_getnameinfo,\"pa\":invoke_i,\"na\":invoke_ii,\"c\":invoke_iii,\"n\":invoke_iiii,\"s\":invoke_iiiii,\"z\":invoke_iiiiii,\"r\":invoke_iiiiiiiii,\"B\":invoke_iiiijj,\"qa\":invoke_iij,\"h\":invoke_vi,\"j\":invoke_vii,\"d\":invoke_viiii,\"ma\":invoke_viiiiii,\"A\":invoke_viiiiiiii,\"C\":is_timeout,\"u\":send_progress,\"e\":_strftime};var asm=createWasm();var ___wasm_call_ctors=function(){return(___wasm_call_ctors=Module[\"asm\"][\"sa\"]).apply(null,arguments)};var _malloc=Module[\"_malloc\"]=function(){return(_malloc=Module[\"_malloc\"]=Module[\"asm\"][\"ta\"]).apply(null,arguments)};var ___errno_location=function(){return(___errno_location=Module[\"asm\"][\"va\"]).apply(null,arguments)};var _ntohs=function(){return(_ntohs=Module[\"asm\"][\"wa\"]).apply(null,arguments)};var _htons=function(){return(_htons=Module[\"asm\"][\"xa\"]).apply(null,arguments)};var _ffmpeg=Module[\"_ffmpeg\"]=function(){return(_ffmpeg=Module[\"_ffmpeg\"]=Module[\"asm\"][\"ya\"]).apply(null,arguments)};var _htonl=function(){return(_htonl=Module[\"asm\"][\"za\"]).apply(null,arguments)};var _emscripten_builtin_memalign=function(){return(_emscripten_builtin_memalign=Module[\"asm\"][\"Aa\"]).apply(null,arguments)};var _setThrew=function(){return(_setThrew=Module[\"asm\"][\"Ba\"]).apply(null,arguments)};var stackSave=function(){return(stackSave=Module[\"asm\"][\"Ca\"]).apply(null,arguments)};var stackRestore=function(){return(stackRestore=Module[\"asm\"][\"Da\"]).apply(null,arguments)};var ___cxa_is_pointer_type=function(){return(___cxa_is_pointer_type=Module[\"asm\"][\"Ea\"]).apply(null,arguments)};var _ff_h264_cabac_tables=Module[\"_ff_h264_cabac_tables\"]=1537004;var ___start_em_js=Module[\"___start_em_js\"]=6059629;var ___stop_em_js=Module[\"___stop_em_js\"]=6059806;function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}Module[\"setValue\"]=setValue;Module[\"getValue\"]=getValue;Module[\"UTF8ToString\"]=UTF8ToString;Module[\"stringToUTF8\"]=stringToUTF8;Module[\"lengthBytesUTF8\"]=lengthBytesUTF8;Module[\"FS\"]=FS;var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module[\"calledRun\"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);if(Module[\"onRuntimeInitialized\"])Module[\"onRuntimeInitialized\"]();postRun()}if(Module[\"setStatus\"]){Module[\"setStatus\"](\"Running...\");setTimeout(function(){setTimeout(function(){Module[\"setStatus\"](\"\")},1);doRun()},1)}else{doRun()}}if(Module[\"preInit\"]){if(typeof Module[\"preInit\"]==\"function\")Module[\"preInit\"]=[Module[\"preInit\"]];while(Module[\"preInit\"].length>0){Module[\"preInit\"].pop()()}}run();\n\n\n return createFFmpegCore.ready\n}\n\n);\n})();\nexport default createFFmpegCore;";
var FFMessageType;
(function (FFMessageType) {
FFMessageType["LOAD"] = "LOAD";
FFMessageType["EXEC"] = "EXEC";
FFMessageType["WRITE_FILE"] = "WRITE_FILE";
FFMessageType["READ_FILE"] = "READ_FILE";
FFMessageType["DELETE_FILE"] = "DELETE_FILE";
FFMessageType["RENAME"] = "RENAME";
FFMessageType["CREATE_DIR"] = "CREATE_DIR";
FFMessageType["LIST_DIR"] = "LIST_DIR";
FFMessageType["DELETE_DIR"] = "DELETE_DIR";
FFMessageType["ERROR"] = "ERROR";
FFMessageType["DOWNLOAD"] = "DOWNLOAD";
FFMessageType["PROGRESS"] = "PROGRESS";
FFMessageType["LOG"] = "LOG";
FFMessageType["MOUNT"] = "MOUNT";
FFMessageType["UNMOUNT"] = "UNMOUNT";
})(FFMessageType || (FFMessageType = {}));
/**
* Generate an unique message ID.
*/
const getMessageID = (() => {
let messageID = 0;
return () => messageID++;
})();
const ERROR_NOT_LOADED = new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first");
const ERROR_TERMINATED = new Error("called FFmpeg.terminate()");
/**
* Provides APIs to interact with ffmpeg web worker.
*
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* ```
*/
class FFmpeg {
#worker = null;
/**
* #resolves and #rejects tracks Promise resolves and rejects to
* be called when we receive message from web worker.
*/
#resolves = {};
#rejects = {};
#logEventCallbacks = [];
#progressEventCallbacks = [];
loaded = false;
/**
* register worker message event handlers.
*/
#registerHandlers = () => {
if (this.#worker) {
this.#worker.onmessage = ({ data: { id, type, data }, }) => {
switch (type) {
case FFMessageType.LOAD:
this.loaded = true;
this.#resolves[id](data);
break;
case FFMessageType.MOUNT:
case FFMessageType.UNMOUNT:
case FFMessageType.EXEC:
case FFMessageType.WRITE_FILE:
case FFMessageType.READ_FILE:
case FFMessageType.DELETE_FILE:
case FFMessageType.RENAME:
case FFMessageType.CREATE_DIR:
case FFMessageType.LIST_DIR:
case FFMessageType.DELETE_DIR:
this.#resolves[id](data);
break;
case FFMessageType.LOG:
this.#logEventCallbacks.forEach((f) => f(data));
break;
case FFMessageType.PROGRESS:
this.#progressEventCallbacks.forEach((f) => f(data));
break;
case FFMessageType.ERROR:
this.#rejects[id](data);
break;
}
delete this.#resolves[id];
delete this.#rejects[id];
};
}
};
/**
* Generic function to send messages to web worker.
*/
#send = ({ type, data }, trans = [], signal) => {
if (!this.#worker) {
return Promise.reject(ERROR_NOT_LOADED);
}
return new Promise((resolve, reject) => {
const id = getMessageID();
this.#worker && this.#worker.postMessage({ id, type, data }, trans);
this.#resolves[id] = resolve;
this.#rejects[id] = reject;
signal?.addEventListener("abort", () => {
reject(new DOMException(`Message # ${id} was aborted`, "AbortError"));
}, { once: true });
});
};
on(event, callback) {
if (event === "log") {
this.#logEventCallbacks.push(callback);
}
else if (event === "progress") {
this.#progressEventCallbacks.push(callback);
}
}
off(event, callback) {
if (event === "log") {
this.#logEventCallbacks = this.#logEventCallbacks.filter((f) => f !== callback);
}
else if (event === "progress") {
this.#progressEventCallbacks = this.#progressEventCallbacks.filter((f) => f !== callback);
}
}
/**
* Loads ffmpeg-core inside web worker. It is required to call this method first
* as it initializes WebAssembly and other essential variables.
*
* @category FFmpeg
* @returns `true` if ffmpeg core is loaded for the first time.
*/
load = ({ classWorkerURL, ...config } = {}, { signal } = {}) => {
if (!this.#worker) {
this.#worker = classWorkerURL ?
new Worker(new URL(classWorkerURL, (_documentCurrentScript && _documentCurrentScript.src || new URL('__entry.js', document.baseURI).href)), {
type: "module",
}) :
// We need to duplicated the code here to enable webpack
// to bundle worekr.js here.
new Worker(new URL('' + "/worker-CRu_gK8D.js", (_documentCurrentScript && _documentCurrentScript.src || new URL('__entry.js', document.baseURI).href)), {
type: "module",
});
this.#registerHandlers();
}
return this.#send({
type: FFMessageType.LOAD,
data: config,
}, undefined, signal);
};
/**
* Execute ffmpeg command.
*
* @remarks
* To avoid common I/O issues, ["-nostdin", "-y"] are prepended to the args
* by default.
*
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* await ffmpeg.load();
* await ffmpeg.writeFile("video.avi", ...);
* // ffmpeg -i video.avi video.mp4
* await ffmpeg.exec(["-i", "video.avi", "video.mp4"]);
* const data = ffmpeg.readFile("video.mp4");
* ```
*
* @returns `0` if no error, `!= 0` if timeout (1) or error.
* @category FFmpeg
*/
exec = (
/** ffmpeg command line args */
args,
/**
* milliseconds to wait before stopping the command execution.
*
* @defaultValue -1
*/
timeout = -1, { signal } = {}) => this.#send({
type: FFMessageType.EXEC,
data: { args, timeout },
}, undefined, signal);
/**
* Terminate all ongoing API calls and terminate web worker.
* `FFmpeg.load()` must be called again before calling any other APIs.
*
* @category FFmpeg
*/
terminate = () => {
const ids = Object.keys(this.#rejects);
// rejects all incomplete Promises.
for (const id of ids) {
this.#rejects[id](ERROR_TERMINATED);
delete this.#rejects[id];
delete this.#resolves[id];
}
if (this.#worker) {
this.#worker.terminate();
this.#worker = null;
this.loaded = false;
}
};
/**
* Write data to ffmpeg.wasm.
*
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* await ffmpeg.load();
* await ffmpeg.writeFile("video.avi", await fetchFile("../video.avi"));
* await ffmpeg.writeFile("text.txt", "hello world");
* ```
*
* @category File System
*/
writeFile = (path, data, { signal } = {}) => {
const trans = [];
if (data instanceof Uint8Array) {
trans.push(data.buffer);
}
return this.#send({
type: FFMessageType.WRITE_FILE,
data: { path, data },
}, trans, signal);
};
mount = (fsType, options, mountPoint) => {
const trans = [];
return this.#send({
type: FFMessageType.MOUNT,
data: { fsType, options, mountPoint },
}, trans);
};
unmount = (mountPoint) => {
const trans = [];
return this.#send({
type: FFMessageType.UNMOUNT,
data: { mountPoint },
}, trans);
};
/**
* Read data from ffmpeg.wasm.
*
* @example
* ```ts
* const ffmpeg = new FFmpeg();
* await ffmpeg.load();
* const data = await ffmpeg.readFile("video.mp4");
* ```
*
* @category File System
*/
readFile = (path,
/**
* File content encoding, supports two encodings:
* - utf8: read file as text file, return data in string type.
* - binary: read file as binary file, return data in Uint8Array type.
*
* @defaultValue binary
*/
encoding = "binary", { signal } = {}) => this.#send({
type: FFMessageType.READ_FILE,
data: { path, encoding },
}, undefined, signal);
/**
* Delete a file.
*
* @category File System
*/
deleteFile = (path, { signal } = {}) => this.#send({
type: FFMessageType.DELETE_FILE,
data: { path },
}, undefined, signal);
/**
* Rename a file or directory.
*
* @category File System
*/
rename = (oldPath, newPath, { signal } = {}) => this.#send({
type: FFMessageType.RENAME,
data: { oldPath, newPath },
}, undefined, signal);
/**
* Create a directory.
*
* @category File System
*/
createDir = (path, { signal } = {}) => this.#send({
type: FFMessageType.CREATE_DIR,
data: { path },
}, undefined, signal);
/**
* List directory contents.
*
* @category File System
*/
listDir = (path, { signal } = {}) => this.#send({
type: FFMessageType.LIST_DIR,
data: { path },
}, undefined, signal);
/**
* Delete an empty directory.
*
* @category File System
*/
deleteDir = (path, { signal } = {}) => this.#send({
type: FFMessageType.DELETE_DIR,
data: { path },
}, undefined, signal);
}
const ERROR_RESPONSE_BODY_READER = new Error("failed to get response body reader");
const ERROR_INCOMPLETED_DOWNLOAD = new Error("failed to complete download");
const HeaderContentLength = "Content-Length";
/**
* Download content of a URL with progress.
*
* Progress only works when Content-Length is provided by the server.
*
*/
const downloadWithProgress = async (url, cb) => {
const resp = await fetch(url);
let buf;
try {
// Set total to -1 to indicate that there is not Content-Type Header.
const total = parseInt(resp.headers.get(HeaderContentLength) || "-1");
const reader = resp.body?.getReader();
if (!reader)
throw ERROR_RESPONSE_BODY_READER;
const chunks = [];
let received = 0;
for (;;) {
const { done, value } = await reader.read();
const delta = value ? value.length : 0;
if (done) {
if (total != -1 && total !== received)
throw ERROR_INCOMPLETED_DOWNLOAD;
cb && cb({ url, total, received, delta, done });
break;
}
chunks.push(value);
received += delta;
cb && cb({ url, total, received, delta, done });
}
const data = new Uint8Array(received);
let position = 0;
for (const chunk of chunks) {
data.set(chunk, position);
position += chunk.length;
}
buf = data.buffer;
}
catch (e) {
console.log(`failed to send download progress event: `, e);
// Fetch arrayBuffer directly when it is not possible to get progress.
buf = await resp.arrayBuffer();
cb &&
cb({
url,
total: buf.byteLength,
received: buf.byteLength,
delta: 0,
done: true,
});
}
return buf;
};
/**
* toBlobURL fetches data from an URL and return a blob URL.
*
* Example:
*
* ```ts
* await toBlobURL("http://localhost:3000/ffmpeg.js", "text/javascript");
* ```
*/
const toBlobURL = async (url, mimeType, progress = false, cb) => {
const buf = progress
? await downloadWithProgress(url, cb)
: await (await fetch(url)).arrayBuffer();
const blob = new Blob([buf], { type: mimeType });
return URL.createObjectURL(blob);
};
class FFmpegConvertor {
coreURL;
wasmURL;
classWorkerURL;
ffmpeg;
size = 0;
/// 140MB, don't know why, but it's the limit, if execced, ffmpeg throw index out of bounds error
maxSize = 14e7;
taskCount = 0;
reloadLock = false;
async init() {
const en = new TextEncoder();
this.coreURL = URL.createObjectURL(new Blob([en.encode(core_raw)], { type: "text/javascript" }));
this.classWorkerURL = URL.createObjectURL(new Blob([en.encode(class_worker_raw)], { type: "text/javascript" }));
this.wasmURL = await toBlobURL("https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.wasm", "application/wasm");
this.ffmpeg = new FFmpeg();
await this.load();
return this;
}
async load() {
await this.ffmpeg.load(
{
coreURL: this.coreURL,
wasmURL: this.wasmURL,
classWorkerURL: this.classWorkerURL
}
);
}
async check() {
if (!this.coreURL || !this.wasmURL || !this.classWorkerURL || !this.ffmpeg) {
throw new Error("FFmpegConvertor not init");
}
if (this.size > this.maxSize) {
const verLock = this.reloadLock;
await this.waitForTaskZero();
if (!this.reloadLock) {
this.reloadLock = true;
try {
evLog("info", "FFmpegConvertor: size limit exceeded, terminate ffmpeg, verLock: ", verLock);
this.ffmpeg.terminate();
await this.load();
this.size = 0;
this.taskCount = 0;
} finally {
this.reloadLock = false;
}
} else {
await this.waitForReloadLock();
}
}
}
async writeFiles(files, randomPrefix) {
const ffmpeg = this.ffmpeg;
await Promise.all(
files.map(async (f) => {
this.size += f.data.byteLength;
await ffmpeg.writeFile(randomPrefix + f.name, f.data);
})
);
}
async readOutputFile(file) {
const result = await this.ffmpeg.readFile(file);
this.size += result.length;
return result;
}
// TODO: find a way to reduce time cost; to mp4 30MB takes 50s; to gif 30MB takes 26s
async convertTo(files, format, meta) {
await this.check();
this.taskCount++;
try {
const ffmpeg = this.ffmpeg;
const randomPrefix = Math.random().toString(36).substring(7);
await this.writeFiles(files, randomPrefix);
let metaStr;
if (meta) {
metaStr = meta.map((m) => `file '${randomPrefix}${m.file}'
duration ${m.delay / 1e3}`).join("\n");
} else {
metaStr = files.map((f) => `file '${randomPrefix}${f.name}'
duration 0.04`).join("\n");
}
await ffmpeg.writeFile(randomPrefix + "meta.txt", metaStr);
let resultFile;
let mimeType;
switch (format) {
case "GIF":
resultFile = randomPrefix + "output.gif";
mimeType = "image/gif";
await ffmpeg.exec(["-f", "concat", "-safe", "0", "-i", randomPrefix + "meta.txt", "-vf", "split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=2", resultFile]);
break;
case "MP4":
resultFile = randomPrefix + "output.mp4";
mimeType = "video/mp4";
await ffmpeg.exec(["-f", "concat", "-safe", "0", "-i", randomPrefix + "meta.txt", "-c:v", "h264", "-pix_fmt", "yuv420p", resultFile]);
break;
}
const result = await this.readOutputFile(resultFile);
const deletePromise = files.map((f) => ffmpeg.deleteFile(randomPrefix + f.name));
if (meta) {
deletePromise.push(ffmpeg.deleteFile(randomPrefix + "meta.txt"));
}
deletePromise.push(ffmpeg.deleteFile(resultFile));
await Promise.all(deletePromise);
return new Blob([result], { type: mimeType });
} finally {
this.taskCount--;
}
}
async waitForTaskZero() {
while (this.taskCount > 0) {
await new Promise((r) => setTimeout(r, 100));
}
await new Promise((r) => setTimeout(r, Math.random() * 100 + 10));
}
async waitForReloadLock() {
while (this.reloadLock) {
await new Promise((r) => setTimeout(r, 100));
}
await new Promise((r) => setTimeout(r, Math.random() * 100 + 10));
}
}
const PID_EXTRACT = /\/(\d+)_([a-z]+)\d*\.\w*$/;
class PixivMatcher extends BaseMatcher {
authorID;
meta;
pidList = [];
pageCount = 0;
works = {};
ugoiraMetas = {};
pageSize = {};
convertor;
first;
constructor() {
super();
this.meta = new GalleryMeta(window.location.href, "UNTITLE");
}
async processData(data, contentType, url) {
const meta = this.ugoiraMetas[url];
if (!meta)
return [data, contentType];
const zipReader = new zip_js__namespace.ZipReader(new zip_js__namespace.Uint8ArrayReader(data));
const start = performance.now();
if (!this.convertor)
this.convertor = await new FFmpegConvertor().init();
const initConvertorEnd = performance.now();
const promises = await zipReader.getEntries().then(
(entries) => entries.map(
(e) => e.getData?.(new zip_js__namespace.Uint8ArrayWriter()).then((data2) => ({ name: e.filename, data: data2 }))
)
);
const files = await Promise.all(promises).then((entries) => entries.filter((f) => f && f.data.length > 0).map((f) => f));
const unpackUgoira = performance.now();
if (files.length !== meta.body.frames.length) {
throw new Error("unpack ugoira file error: file count not equal to meta");
}
const blob = await this.convertor.convertTo(files, conf.convertTo, meta.body.frames);
const convertEnd = performance.now();
evLog("debug", `convert ugoira to ${conf.convertTo}
init convertor cost: ${initConvertorEnd - start}ms
unpack ugoira cost: ${unpackUgoira - initConvertorEnd}ms
ffmpeg convert cost: ${convertEnd - unpackUgoira}ms
total cost: ${(convertEnd - start) / 1e3}s
size: ${blob.size / 1e3} KB, original size: ${data.byteLength / 1e3} KB
before contentType: ${contentType}, after contentType: ${blob.type}
`);
return blob.arrayBuffer().then((buffer) => [new Uint8Array(buffer), blob.type]);
}
workURL() {
return /pixiv.net\/(\w*\/)?(artworks|users)\/.*/;
}
galleryMeta() {
this.meta.title = `PIXIV_${this.authorID}_w${this.pidList.length}_p${this.pageCount}` || "UNTITLE";
let tags = Object.values(this.works).map((w) => w.tags).flat();
this.meta.tags = { "author": [this.authorID || "UNTITLE"], "all": [...new Set(tags)], "pids": this.pidList, "works": Object.values(this.works) };
return this.meta;
}
async fetchOriginMeta(url) {
const matches = url.match(PID_EXTRACT);
if (!matches || matches.length < 2) {
return { url };
}
const pid = matches[1];
const p = matches[2];
if (this.works[pid]?.illustType !== 2 || p !== "ugoira") {
return { url };
}
const meta = await window.fetch(`https://www.pixiv.net/ajax/illust/${pid}/ugoira_meta?lang=en`).then((resp) => resp.json());
this.ugoiraMetas[meta.body.src] = meta;
return { url: meta.body.src };
}
async fetchTagsByPids(pids) {
try {
const raw = await window.fetch(`https://www.pixiv.net/ajax/user/${this.authorID}/profile/illusts?ids[]=${pids.join("&ids[]=")}&work_category=illustManga&is_first_page=0&lang=en`).then((resp) => resp.json());
const data = raw;
if (!data.error) {
const works = {};
Object.entries(data.body.works).forEach(([k, w]) => {
works[k] = {
id: w.id,
title: w.title,
alt: w.alt,
illustType: w.illustType,
description: w.description,
tags: w.tags,
pageCount: w.pageCount
};
});
this.works = { ...this.works, ...works };
} else {
evLog("error", "WARN: fetch tags by pids error: ", data.message);
}
} catch (error) {
evLog("error", "ERROR: fetch tags by pids error: ", error);
}
}
async parseImgNodes(source) {
const list = [];
const pidList = JSON.parse(source);
this.fetchTagsByPids(pidList);
const pageListData = await fetchUrls(pidList.map((p) => `https://www.pixiv.net/ajax/illust/${p}/pages?lang=en`), 5);
for (let i = 0; i < pidList.length; i++) {
const pid = pidList[i];
const data = JSON.parse(pageListData[i]);
if (data.error) {
throw new Error(`Fetch page list error: ${data.message}`);
}
this.pageCount += data.body.length;
let digits = data.body.length.toString().length;
let j = -1;
for (const p of data.body) {
this.pageSize[p.urls.original] = [p.width, p.height];
let title = p.urls.original.split("/").pop() || `${pid}_p${j.toString().padStart(digits)}.jpg`;
const matches = p.urls.original.match(PID_EXTRACT);
if (matches && matches.length > 2 && matches[2] && matches[2] === "ugoira") {
title = title.replace(/\.\w+$/, ".gif");
}
j++;
const node = new ImageNode(
p.urls.small,
p.urls.original,
title
);
list.push(node);
}
}
return list;
}
async *fetchPagesSource() {
let u = document.querySelector("a[data-gtm-value][href*='/users/']")?.href || document.querySelector("a.user-details-icon[href*='/users/']")?.href || window.location.href;
const author = /users\/(\d+)/.exec(u)?.[1];
if (!author) {
throw new Error("Cannot find author id!");
}
this.authorID = author;
const res = await window.fetch(`https://www.pixiv.net/ajax/user/${author}/profile/all`).then((resp) => resp.json());
if (res.error) {
throw new Error(`Fetch illust list error: ${res.message}`);
}
let pidList = [...Object.keys(res.body.illusts), ...Object.keys(res.body.manga)];
this.pidList = [...pidList];
pidList = pidList.sort((a, b) => parseInt(b) - parseInt(a));
this.first = window.location.href.match(/artworks\/(\d+)$/)?.[1];
if (this.first) {
const index = pidList.indexOf(this.first);
if (index > -1) {
pidList.splice(index, 1);
}
pidList.unshift(this.first);
}
while (pidList.length > 0) {
const pids = pidList.splice(0, 20);
yield JSON.stringify(pids);
}
}
}
async function fetchUrls(urls, concurrency) {
const results = new Array(urls.length);
let i = 0;
while (i < urls.length) {
const batch = urls.slice(i, i + concurrency);
const batchPromises = batch.map(
(url, index) => window.fetch(url).then((resp) => {
if (resp.ok) {
return resp.text();
}
throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}`);
}).then((raw) => results[index + i] = raw)
);
await Promise.all(batchPromises);
i += concurrency;
}
return results;
}
class RokuHentaiMatcher extends BaseMatcher {
sprites = [];
fetchedThumbnail = [];
galleryId = "";
imgCount = 0;
workURL() {
return /rokuhentai.com\/\w+$/;
}
galleryMeta(doc) {
const title = doc.querySelector(".site-manga-info__title-text")?.textContent || "UNTITLE";
const meta = new GalleryMeta(window.location.href, title);
meta.originTitle = title;
const tagTrList = doc.querySelectorAll("div.mdc-chip .site-tag-count");
const tags = {};
tagTrList.forEach((tr) => {
const splits = tr.getAttribute("data-tag")?.trim().split(":");
if (splits === void 0 || splits.length === 0)
return;
const cat = splits[0];
if (tags[cat] === void 0)
tags[cat] = [];
tags[cat].push(splits[1].replaceAll('"', ""));
});
meta.tags = tags;
return meta;
}
async fetchOriginMeta(url, _) {
return { url };
}
async parseImgNodes(source) {
const range = source.split("-").map(Number);
const list = [];
const digits = this.imgCount.toString().length;
for (let i = range[0]; i < range[1]; i++) {
let thumbnail = `https://rokuhentai.com/_images/page-thumbnails/${this.galleryId}/${i}.jpg`;
if (this.sprites[i]) {
thumbnail = await this.fetchThumbnail(i);
}
const newNode = new ImageNode(
thumbnail,
`https://rokuhentai.com/_images/pages/${this.galleryId}/${i}.jpg`,
i.toString().padStart(digits, "0") + ".jpg"
);
list.push(newNode);
}
return list;
}
async *fetchPagesSource() {
const doc = document;
const imgCount = parseInt(doc.querySelector(".mdc-typography--caption")?.textContent || "");
if (isNaN(imgCount)) {
throw new Error("error: failed query image count!");
}
this.imgCount = imgCount;
this.galleryId = window.location.href.split("/").pop();
const images = Array.from(doc.querySelectorAll(".mdc-layout-grid__cell .site-page-card__media"));
for (const img of images) {
this.fetchedThumbnail.push(void 0);
const x = parseInt(img.getAttribute("data-offset-x") || "");
const y = parseInt(img.getAttribute("data-offset-y") || "");
const width = parseInt(img.getAttribute("data-width") || "");
const height = parseInt(img.getAttribute("data-height") || "");
if (isNaN(x) || isNaN(y) || isNaN(width) || isNaN(height)) {
this.sprites.push(void 0);
continue;
}
const src = img.getAttribute("data-src");
this.sprites.push({ src, pos: { x, y, width, height } });
}
for (let i = 0; i < this.imgCount; i += 20) {
yield `${i}-${Math.min(i + 20, this.imgCount)}`;
}
}
async fetchThumbnail(index) {
if (this.fetchedThumbnail[index]) {
return this.fetchedThumbnail[index];
} else {
const src = this.sprites[index].src;
const positions = [];
for (let i = index; i < this.imgCount; i++) {
if (src === this.sprites[i]?.src) {
positions.push(this.sprites[i].pos);
} else {
break;
}
}
const urls = await splitImagesFromUrl(src, positions);
for (let i = index; i < index + urls.length; i++) {
this.fetchedThumbnail[i] = urls[i - index];
}
return this.fetchedThumbnail[index];
}
}
}
const STEAM_THUMB_IMG_URL_REGEX = /background-image:\surl\(.*?(h.*\/).*?\)/;
class SteamMatcher extends BaseMatcher {
workURL() {
return /steamcommunity.com\/id\/[^/]+\/screenshots.*/;
}
async fetchOriginMeta(href) {
let raw = "";
try {
raw = await window.fetch(href).then((resp) => resp.text());
if (!raw)
throw new Error("[text] is empty");
} catch (error) {
throw new Error(`Fetch source page error, expected [text]! ${error}`);
}
const domParser = new DOMParser();
const doc = domParser.parseFromString(raw, "text/html");
let imgURL = doc.querySelector(".actualmediactn > a")?.getAttribute("href");
if (!imgURL) {
throw new Error("Cannot Query Steam original Image URL");
}
return { url: imgURL };
}
async parseImgNodes(source) {
const list = [];
const doc = await window.fetch(source).then((resp) => resp.text()).then((raw) => new DOMParser().parseFromString(raw, "text/html"));
if (!doc) {
throw new Error("warn: steam matcher failed to get document from source page!");
}
const nodes = doc.querySelectorAll(".profile_media_item");
if (!nodes || nodes.length == 0) {
throw new Error("warn: failed query image nodes!");
}
for (const node of Array.from(nodes)) {
const src = STEAM_THUMB_IMG_URL_REGEX.exec(node.innerHTML)?.[1];
if (!src) {
throw new Error(`Cannot Match Steam Image URL, Content: ${node.innerHTML}`);
}
const newNode = new ImageNode(
src,
node.getAttribute("href"),
node.getAttribute("data-publishedfileid") + ".jpg"
);
list.push(newNode);
}
return list;
}
async *fetchPagesSource() {
let totalPages = -1;
document.querySelectorAll(".pagingPageLink").forEach((ele) => {
totalPages = Number(ele.textContent);
});
let url = new URL(window.location.href);
url.searchParams.set("view", "grid");
if (totalPages === -1) {
const doc = await window.fetch(url.href).then((response) => response.text()).then((text) => new DOMParser().parseFromString(text, "text/html")).catch(() => null);
if (!doc) {
throw new Error("warn: steam matcher failed to get document from source page!");
}
doc.querySelectorAll(".pagingPageLink").forEach((ele) => totalPages = Number(ele.textContent));
}
if (totalPages > 0) {
for (let p = 1; p <= totalPages; p++) {
url.searchParams.set("p", p.toString());
yield url.href;
}
} else {
yield url.href;
}
}
parseGalleryMeta() {
const url = new URL(window.location.href);
let appid = url.searchParams.get("appid");
return new GalleryMeta(window.location.href, "steam-" + appid || "all");
}
}
class TwitterMatcher extends BaseMatcher {
mediaPages = /* @__PURE__ */ new Map();
largeSrcMap = /* @__PURE__ */ new Map();
uuid = uuid();
postCount = 0;
mediaCount = 0;
userID;
async fetchUserMedia(cursor) {
if (!this.userID) {
this.userID = getUserID();
}
if (!this.userID)
throw new Error("Cannot obatained User ID");
const variables = `{"userId":"${this.userID}","count":20,${cursor ? '"cursor":"' + cursor + '",' : ""}"includePromotedContent":false,"withClientEventToken":false,"withBirdwatchNotes":false,"withVoice":true,"withV2Timeline":true}`;
const features = "&features=%7B%22rweb_tipjar_consumption_enabled%22%3Atrue%2C%22responsive_web_graphql_exclude_directive_enabled%22%3Atrue%2C%22verified_phone_label_enabled%22%3Afalse%2C%22creator_subscriptions_tweet_preview_api_enabled%22%3Atrue%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22responsive_web_graphql_skip_user_profile_image_extensions_enabled%22%3Afalse%2C%22communities_web_enable_tweet_community_results_fetch%22%3Atrue%2C%22c9s_tweet_anatomy_moderator_badge_enabled%22%3Atrue%2C%22articles_preview_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22view_counts_everywhere_api_enabled%22%3Atrue%2C%22longform_notetweets_consumption_enabled%22%3Atrue%2C%22responsive_web_twitter_article_tweet_consumption_enabled%22%3Atrue%2C%22tweet_awards_web_tipping_enabled%22%3Afalse%2C%22creator_subscriptions_quote_tweet_preview_enabled%22%3Afalse%2C%22freedom_of_speech_not_reach_fetch_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_media_interstitial_enabled%22%3Atrue%2C%22rweb_video_timestamps_enabled%22%3Atrue%2C%22longform_notetweets_rich_text_read_enabled%22%3Atrue%2C%22longform_notetweets_inline_media_enabled%22%3Atrue%2C%22responsive_web_enhance_cards_enabled%22%3Afalse%7D&fieldToggles=%7B%22withArticlePlainText%22%3Afalse%7D";
const url = `${window.location.origin}/i/api/graphql/aQQLnkexAl5z9ec_UgbEIA/UserMedia?variables=${encodeURIComponent(variables)}${features}`;
const headers = new Headers();
headers.set("authorization", "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA");
headers.set("Pragma", "no-cache");
headers.set("Cache-Control", "no-cache");
headers.set("content-type", "application/json");
headers.set("x-client-uuid", this.uuid);
headers.set("x-twitter-auth-type", "OAuth2Session");
headers.set("x-twitter-client-language", "en");
headers.set("x-twitter-active-user", "yes");
headers.set("x-client-transaction-id", transactionId());
headers.set("Sec-Fetch-Dest", "empty");
headers.set("Sec-Fetch-Mode", "cors");
headers.set("Sec-Fetch-Site", "same-origin");
const csrfToken = document.cookie.match(/ct0=(\w+)/)?.[1];
if (!csrfToken)
throw new Error("Not found csrfToken");
headers.set("x-csrf-token", csrfToken);
const res = await window.fetch(url, { headers });
try {
const json = await res.json();
const instructions = json.data.user.result.timeline_v2.timeline.instructions;
let items;
const addToModule = instructions.find((ins) => ins.type === "TimelineAddToModule");
const addEntries = instructions.find((ins) => ins.type === "TimelineAddEntries");
if (!addEntries) {
throw new Error("Not found TimelineAddEntries");
}
if (addToModule) {
items = addToModule.moduleItems;
}
if (!items) {
const timelineModule = addEntries.entries.find((entry) => entry.content.entryType === "TimelineTimelineModule")?.content;
items = timelineModule?.items;
}
if (!items) {
return [[], void 0];
}
const timelineCursor = addEntries.entries.find((entry) => entry.content.entryType === "TimelineTimelineCursor" && entry.entryId.startsWith("cursor-bottom"))?.content;
return [items, timelineCursor?.value];
} catch (error) {
throw new Error(`fetchUserMedia error: ${error}`);
}
}
async *fetchPagesSource() {
let cursor;
while (true) {
const [mediaPage, nextCursor] = await this.fetchUserMedia(cursor);
cursor = nextCursor || "last";
if (!mediaPage || mediaPage.length === 0)
break;
this.mediaPages.set(cursor, mediaPage);
yield cursor;
if (!nextCursor)
break;
}
}
async parseImgNodes(cursor) {
const items = this.mediaPages.get(cursor);
if (!items)
throw new Error("warn: cannot find items");
const list = [];
for (const item of items) {
let mediaList = item?.item?.itemContent?.tweet_results?.result?.legacy?.entities?.media || item?.item?.itemContent?.tweet_results?.result?.tweet?.legacy?.entities?.media;
if (mediaList === void 0) {
evLog("error", "Not found mediaList: ", item);
continue;
}
this.postCount++;
if (conf.reverseMultipleImagesPost) {
mediaList.reverse();
}
for (let i = 0; i < mediaList.length; i++) {
const media = mediaList[i];
if (media.type !== "video" && media.type !== "photo" && media.type !== "animated_gif") {
evLog("error", `Not supported media type: ${media.type}`);
continue;
}
const ext = media.media_url_https.split(".").pop();
const baseSrc = media.media_url_https.replace(`.${ext}`, "");
const src = `${baseSrc}?format=${ext}&name=${media.sizes.small ? "small" : "thumb"}`;
let href = media.expanded_url.replace(/\/(photo|video)\/\d+/, "");
href = `${href}/${media.type === "video" ? "video" : "photo"}/${i + 1}`;
let largeSrc = `${baseSrc}?format=${ext}&name=${media.sizes.large ? "large" : media.sizes.medium ? "medium" : "small"}`;
const title = `${media.id_str}-${baseSrc.split("/").pop()}.${ext}`;
const node = new ImageNode(src, href, title);
if (media.video_info) {
let bitrate = 0;
for (const variant of media.video_info.variants) {
if (variant.bitrate !== void 0 && variant.bitrate >= bitrate) {
bitrate = variant.bitrate;
largeSrc = variant.url;
node.mimeType = variant.content_type;
node.title = node.title.replace(/\.\w+$/, `.${variant.content_type.split("/")[1]}`);
}
}
}
this.largeSrcMap.set(href, largeSrc);
list.push(node);
this.mediaCount++;
}
}
return list;
}
async fetchOriginMeta(href) {
return {
url: this.largeSrcMap.get(href) || href
};
}
workURL() {
return /(x|twitter).com\/(?!(home|explore|notifications|messages)$|i\/|search\?)\w+/;
}
galleryMeta(doc) {
const userName = window.location.href.match(/(twitter|x).com\/(\w+)\/?/)?.[2];
return new GalleryMeta(window.location.href, `twitter-${userName || doc.title}-${this.postCount}-${this.mediaCount}`);
}
}
function getUserID() {
const userName = window.location.href.match(/(twitter|x).com\/(\w+)\/?/)?.[2] || "lililjiliijili";
const followBTNs = Array.from(document.querySelectorAll("button[data-testid][aria-label]"));
if (followBTNs.length === 0)
return void 0;
const theBTN = followBTNs.find((btn) => btn.getAttribute("aria-label")?.includes(`@${userName}`)) || followBTNs[0];
return theBTN.getAttribute("data-testid").match(/(\d+)/)?.[1];
}
class WnacgMatcher extends BaseMatcher {
meta;
baseURL;
async *fetchPagesSource() {
const id = this.extractIDFromHref(window.location.href);
if (!id) {
throw new Error("Cannot find gallery ID");
}
this.baseURL = `${window.location.origin}/photos-index-page-1-aid-${id}.html`;
let doc = await window.fetch(this.baseURL).then((res) => res.text()).then((text) => new DOMParser().parseFromString(text, "text/html"));
this.meta = this.pasrseGalleryMeta(doc);
yield doc;
while (true) {
const next = doc.querySelector(".paginator > .next > a");
if (!next)
break;
const url = next.href;
doc = await window.fetch(url).then((res) => res.text()).then((text) => new DOMParser().parseFromString(text, "text/html"));
yield doc;
}
}
async parseImgNodes(page) {
const doc = page;
const result = [];
const list = Array.from(doc.querySelectorAll(".grid > .gallary_wrap > .cc > li"));
for (const li of list) {
const anchor = li.querySelector(".pic_box > a");
if (!anchor)
continue;
const img = anchor.querySelector("img");
if (!img)
continue;
const title = li.querySelector(".title > .name")?.textContent || "unknown";
result.push(new ImageNode(img.src, anchor.href, title));
}
return result;
}
async fetchOriginMeta(href) {
const doc = await window.fetch(href).then((res) => res.text()).then((text) => new DOMParser().parseFromString(text, "text/html"));
const img = doc.querySelector("#picarea");
if (!img)
throw new Error(`Cannot find #picarea from ${href}`);
const url = img.src;
const title = url.split("/").pop();
return { url, title };
}
workURL() {
return /(wnacg.com|hm\d{2}.lol)\/photos-index/;
}
galleryMeta(doc) {
return this.meta || super.galleryMeta(doc);
}
// https://www.hm19.lol/photos-index-page-1-aid-253297.html
extractIDFromHref(href) {
const match = href.match(/-(\d+).html$/);
if (!match)
return void 0;
return match[1];
}
pasrseGalleryMeta(doc) {
const title = doc.querySelector("#bodywrap > h2")?.textContent || "unknown";
const meta = new GalleryMeta(this.baseURL || window.location.href, title);
const tags = Array.from(doc.querySelectorAll(".asTB .tagshow")).map((ele) => ele.textContent).filter(Boolean);
const description = Array.from(doc.querySelector(".asTB > .asTBcell.uwconn > p")?.childNodes || []).map((e) => e.textContent).filter(Boolean);
meta.tags = { "tags": tags, "description": description };
return meta;
}
}
function getMatchers() {
return [
new EHMatcher(),
new NHMatcher(),
new HitomiMather(),
new PixivMatcher(),
new SteamMatcher(),
new RokuHentaiMatcher(),
new Comic18Matcher(),
new DanbooruDonmaiMatcher(),
new Rule34Matcher(),
new YandereMatcher(),
new KonachanMatcher(),
new GelBooruMatcher(),
new IMHentaiMatcher(),
new TwitterMatcher(),
new WnacgMatcher(),
new HentaiNexusMatcher(),
new KoharuMatcher()
];
}
function adaptMatcher(url) {
const matchers = getMatchers();
const workURLs = matchers.flatMap((m) => m.workURLs()).map((r) => r.source);
const checkValid = (urls) => {
const newURLs2 = urls.filter((u) => workURLs.includes(u));
return newURLs2.length === urls.length ? null : newURLs2;
};
let newURLs = checkValid(conf.excludeURLs);
if (newURLs) {
conf.excludeURLs = newURLs;
saveConf(conf);
}
newURLs = checkValid(conf.autoOpenExcludeURLs);
if (newURLs) {
conf.autoOpenExcludeURLs = newURLs;
saveConf(conf);
}
if (conf.excludeURLs.length < matchers.length) {
for (const regex of conf.excludeURLs) {
if (new RegExp(regex).test(url)) {
return null;
}
}
}
return matchers.find((m) => m.workURLs().find((r) => r.test(url))) || null;
}
function enableAutoOpen(url) {
return conf.autoOpenExcludeURLs.find((excludeReg) => RegExp(excludeReg).test(url)) == void 0;
}
function parseKey(event) {
const keys = [];
if (event.ctrlKey)
keys.push("Ctrl");
if (event.shiftKey)
keys.push("Shift");
if (event.altKey)
keys.push("Alt");
let key = event.key;
if (key === " ")
key = "Space";
keys.push(key);
return keys.join("+");
}
function queryRule(root, selector) {
return Array.from(root.cssRules).find((rule) => rule.selectorText === selector);
}
function relocateElement(element, anchor, vw, vh) {
const rect = anchor.getBoundingClientRect();
let left = rect.left + rect.width / 2 - element.offsetWidth / 2;
left = Math.min(left, vw - element.offsetWidth);
left = Math.max(left, 0);
element.style.left = left + "px";
if (rect.top > vh / 2) {
element.style.bottom = vh - rect.top + "px";
element.style.top = "unset";
} else {
element.style.top = rect.bottom + "px";
element.style.bottom = "unset";
}
}
function scrollSmoothly(element, y) {
let scroller = TASKS.get(element);
if (!scroller) {
scroller = new Scroller(element);
TASKS.set(element, scroller);
}
scroller.step = conf.scrollingSpeed;
scroller.scroll(y > 0 ? "down" : "up").then(() => element.dispatchEvent(new CustomEvent("smoothlyscrollend")));
}
function scrollTerminate(element) {
const scroller = TASKS.get(element);
if (scroller) {
scroller.timer = void 0;
scroller.scrolling = false;
}
}
const TASKS = /* @__PURE__ */ new WeakMap();
class Scroller {
element;
timer;
scrolling = false;
step;
// [1, 100]
additional = 0;
lastDirection;
directionChanged = false;
constructor(element, step) {
this.element = element;
this.step = step || 1;
}
scroll(direction, duration = 100) {
this.directionChanged = this.lastDirection !== void 0 && this.lastDirection !== direction;
this.lastDirection = direction;
let resolve;
const promise = new Promise((r) => resolve = r);
if (!this.timer) {
this.timer = new Timer(duration);
}
if (this.scrolling) {
this.additional = 0;
this.timer.extend(duration);
return promise;
}
this.additional = 0;
this.scrolling = true;
const scrolled = () => {
this.scrolling = false;
this.timer = void 0;
this.directionChanged = false;
this.lastDirection = void 0;
resolve?.();
};
const doFrame = () => {
const [ok, finished] = this.timer?.tick() ?? [false, true];
if (ok) {
let scrollTop = this.element.scrollTop + (this.step + this.additional) * (direction === "up" ? -1 : 1);
scrollTop = Math.max(scrollTop, 0);
scrollTop = Math.min(scrollTop, this.element.scrollHeight - this.element.clientHeight);
this.element.scrollTop = scrollTop;
if (scrollTop === 0 || scrollTop === this.element.scrollHeight - this.element.clientHeight) {
return scrolled();
}
}
if (finished || this.directionChanged)
return scrolled();
window.requestAnimationFrame(doFrame);
};
window.requestAnimationFrame(doFrame);
return promise;
}
}
class Timer {
interval = 1;
last;
endAt;
constructor(duration, interval) {
const now = Date.now();
if (interval) {
this.interval = interval;
}
this.last = now;
this.endAt = now + duration;
}
tick() {
const now = Date.now();
let ok = now >= this.last + this.interval;
if (ok) {
this.last = now;
}
let finished = now >= this.endAt;
return [ok, finished];
}
extend(duration) {
this.endAt = this.last + duration;
}
}
const scroller = {
scrollSmoothly,
scrollTerminate,
Scroller
};
function createExcludeURLPanel(root, urls, autoOpen = false) {
const workURLs = getMatchers().flatMap((m) => m.workURLs()).map((r) => r.source);
const HTML_STR = `
${autoOpen ? "Auto Open " : ""}Exclude URL|Site
✖
${workURLs.map((r, index) => `
${r}
`).join("")}
`;
const fullPanel = document.createElement("div");
fullPanel.classList.add("ehvp-full-panel");
fullPanel.innerHTML = HTML_STR;
fullPanel.addEventListener("click", (event) => {
if (event.target.classList.contains("ehvp-full-panel")) {
fullPanel.remove();
}
});
root.appendChild(fullPanel);
fullPanel.querySelector(".ehvp-custom-panel-close").addEventListener("click", () => fullPanel.remove());
const list = Array.from(fullPanel.querySelectorAll(".ehvp-custom-panel-list-item"));
list.forEach((li) => {
const index = parseInt(li.getAttribute("data-index"));
li.addEventListener("click", () => {
const i = urls.indexOf(workURLs[index]);
if (i === -1) {
li.classList.add("ehvp-custom-panel-list-item-disable");
urls.push(workURLs[index]);
} else {
li.classList.remove("ehvp-custom-panel-list-item-disable");
urls.splice(i, 1);
}
saveConf(conf);
});
});
}
const lang = navigator.language;
const i18nIndex = lang.startsWith("zh") ? 1 : 0;
class I18nValue extends Array {
constructor(...value) {
super(...value);
}
get() {
return this[i18nIndex];
}
}
const keyboardCustom = {
inMain: {
"open-full-view-grid": new I18nValue("Enter Read Mode", "进入阅读模式")
},
inBigImageMode: {
"step-image-prev": new I18nValue("Go Prev Image", "切换到上一张图片"),
"step-image-next": new I18nValue("Go Next Image", "切换到下一张图片"),
"exit-big-image-mode": new I18nValue("Exit Big Image Mode", "退出大图模式"),
"step-to-first-image": new I18nValue("Go First Image", "跳转到第一张图片"),
"step-to-last-image": new I18nValue("Go Last Image", "跳转到最后一张图片"),
"scale-image-increase": new I18nValue("Increase Image Scale", "放大图片"),
"scale-image-decrease": new I18nValue("Decrease Image Scale", "缩小图片"),
"scroll-image-up": new I18nValue("Scroll Image Up (Please Keep Default Keys)", "向上滚动图片 (请保留默认按键)"),
"scroll-image-down": new I18nValue("Scroll Image Down (Please Keep Default Keys)", "向下滚动图片 (请保留默认按键)")
},
inFullViewGrid: {
"open-big-image-mode": new I18nValue("Enter Big Image Mode", "进入大图阅读模式"),
"pause-auto-load-temporarily": new I18nValue("Pause Auto Load Temporarily", "临时停止自动加载"),
"exit-full-view-grid": new I18nValue("Exit Read Mode", "退出阅读模式"),
"columns-increase": new I18nValue("Increase Columns ", "增加每行数量"),
"columns-decrease": new I18nValue("Decrease Columns ", "减少每行数量"),
"back-chapters-selection": new I18nValue("Back to Chapters Selection", "返回章节选择")
}
};
const i18n = {
// page-helper
imageScale: new I18nValue("SCALE", "缩放"),
config: new I18nValue("CONF", "配置"),
backChapters: new I18nValue("Chapters", "章节"),
autoPagePlay: new I18nValue("PLAY", "播放"),
autoPagePause: new I18nValue("PAUSE", "暂停"),
collapse: new I18nValue("FOLD", "收起"),
// config panel number option
colCount: new I18nValue("Columns", "每行数量"),
threads: new I18nValue("Preload Threads", "最大同时加载"),
threadsTooltip: new I18nValue("Max Preload Threads", "大图浏览时,每次滚动到下一张时,预加载的图片数量,大于1时体现为越看加载的图片越多,将提升浏览体验。"),
downloadThreads: new I18nValue("Download Threads", "最大同时下载"),
downloadThreadsTooltip: new I18nValue("Max Download Threads, suggest: <5", "下载模式下,同时加载的图片数量,建议小于等于5"),
paginationIMGCount: new I18nValue("Images Per Page", "每页图片数量"),
paginationIMGCountTooltip: new I18nValue("In Pagination Read mode, the number of images displayed on each page", "当阅读模式为翻页模式时,每页展示的图片数量"),
timeout: new I18nValue("Timeout(second)", "超时时间(秒)"),
preventScrollPageTime: new I18nValue("Min Paging Time", "最小翻页时间"),
preventScrollPageTimeTooltip: new I18nValue("In Pagination read mode, prevent immediate page flipping when scrolling to the bottom/top to improve the reading experience. Set to 0 to disable this feature, If set to less than 0, page-flipping via scrolling is always disabled, except for the spacebar. measured in milliseconds.", "当阅读模式为翻页模式时,滚动浏览时,阻止滚动到底部时立即翻页,提升阅读体验。 设置为0时则禁用此功能,单位为毫秒。 设置小于0时则永远禁止通过滚动的方式翻页。空格键除外。"),
autoPageSpeed: new I18nValue("Auto Paging Speed", "自动翻页速度"),
autoPageSpeedTooltip: new I18nValue("In Pagination read mode, Auto Page Speed means how many seconds it takes to flip the page automatically. In Continuous read mode, Auto Page Speed means the scrolling speed.", "当阅读模式为翻页模式时,自动翻页速度表示为多少秒后翻页。 当阅读模式为连续模式时,自动翻页速度表示为滚动速度。"),
scrollingSpeed: new I18nValue("Scrolling Speed", "按键滚动速度"),
scrollingSpeedTooltip: new I18nValue("The scrolling Speed for Custom KeyBoard Keys for scrolling, not Auto Paging|Scrolling Speed", "自定义按键的滚动速度,并不是连续阅读模式下的自动翻页的滚动速度。"),
// config panel boolean option
fetchOriginal: new I18nValue("Raw Image", "最佳质量"),
fetchOriginalTooltip: new I18nValue("enable will download the original source, cost more traffic and quotas", "启用后,将加载未经过压缩的原档文件,下载打包后的体积也与画廊所标体积一致。 注意:这将消耗更多的流量与配额,请酌情启用。"),
autoLoad: new I18nValue("Auto Load", "自动加载"),
autoLoadTooltip: new I18nValue("", "进入本脚本的浏览模式后,即使不浏览也会一张接一张的加载图片。直至所有图片加载完毕。"),
reversePages: new I18nValue("Reverse Pages", "反向翻页"),
reversePagesTooltip: new I18nValue("Clicking on the side navigation, if enable then reverse paging, which is a reading style similar to Japanese manga where pages are read from right to left.", "点击侧边导航时,是否反向翻页,反向翻页类似日本漫画那样的从右到左的阅读方式。"),
autoPlay: new I18nValue("Auto Page", "自动翻页"),
autoPlayTooltip: new I18nValue("Auto Page when entering the big image readmode.", "当阅读大图时,开启自动播放模式。"),
autoLoadInBackground: new I18nValue("Keep Loading", "后台加载"),
autoLoadInBackgroundTooltip: new I18nValue("Keep Auto-Loading after the tab loses focus", "当标签页失去焦点后保持自动加载。"),
autoOpen: new I18nValue("Auto Open", "自动展开"),
autoOpenTooltip: new I18nValue("Automatically open after the gallery page is loaded", "进入画廊页面后,自动展开阅读视图。"),
autoCollapsePanel: new I18nValue("Auto Fold Control Panel", "自动收起控制面板"),
autoCollapsePanelTooltip: new I18nValue("When the mouse is moved out of the control panel, the control panel will automatically fold. If disabled, the display of the control panel can only be toggled through the button on the control bar.", "当鼠标移出控制面板时,自动收起控制面板。禁用此选项后,只能通过控制栏上的按钮切换控制面板的显示。"),
// config panel select option
readMode: new I18nValue("Read Mode", "阅读模式"),
readModeTooltip: new I18nValue("Switch to the next picture when scrolling, otherwise read continuously", "滚动时切换到下一张图片,否则连续阅读"),
stickyMouse: new I18nValue("Sticky Mouse", "黏糊糊鼠标"),
stickyMouseTooltip: new I18nValue("In non-continuous reading mode, scroll a single image automatically by moving the mouse.", "非连续阅读模式下,通过鼠标移动来自动滚动单张图片。"),
minifyPageHelper: new I18nValue("Minify Control Bar", "最小化控制栏"),
minifyPageHelperTooltip: new I18nValue("Minify Control Bar", "最小化控制栏"),
hitomiFormat: new I18nValue("Hitomi Image Format", "Hitomi 图片格式"),
hitomiFormatTooltip: new I18nValue("In Hitomi, Fetch images by the format. if Auto then try Avif > Jxl > Webp, Requires Refresh", "在Hitomi中的源图格式。 如果是Auto,则优先获取Avif > Jxl > Webp,修改后需要刷新生效。"),
ehentaiTitlePrefer: new I18nValue("EHentai Prefer Title", "EHentai标题语言"),
ehentaiTitlePreferTooltip: new I18nValue("Many galleries have both an English/Romanized title and a title in Japanese script. Which one do you want to use as the archive filename?", "许多图库都同时拥有英文/罗马音标题和日文标题, 您希望下载时哪个作为文件名?"),
reverseMultipleImagesPost: new I18nValue("Descending Images In Post", "反转推文图片顺序"),
reverseMultipleImagesPostTooltip: new I18nValue("Reverse order for post with multiple images attatched", "反转推文图片顺序"),
dragToMove: new I18nValue("Drag to Move", "拖动移动"),
originalCheck: new I18nValue("Enable RawImage Transient ", "临时开启最佳质量 "),
showHelp: new I18nValue("Help", "帮助"),
showKeyboard: new I18nValue("Keyboard", "快捷键"),
showExcludes: new I18nValue("Excludes", "站点排除"),
showAutoOpenExcludes: new I18nValue("AutoOpenExcludes", "自动打开排除"),
letUsStar: new I18nValue("Let's Star", "点星"),
// download panel
download: new I18nValue("DL", "下载"),
forceDownload: new I18nValue("Take Loaded", "获取已下载的"),
downloadStart: new I18nValue("Start Download", "开始下载"),
downloading: new I18nValue("Downloading...", "下载中..."),
downloadFailed: new I18nValue("Failed(Retry)", "下载失败(重试)"),
downloaded: new I18nValue("Downloaded", "下载完成"),
packaging: new I18nValue("Packaging...", "打包中..."),
status: new I18nValue("Status", "状态"),
selectChapters: new I18nValue("Select Chapters", "章节选择"),
cherryPick: new I18nValue("Cherry Pick", "范围选择"),
help: new I18nValue(`
[How to Use? Where is the Entry?]
The script typically activates on gallery homepages or artist homepages. For example, on E-Hentai, it activates on the gallery detail page, or on Twitter, it activates on the user's homepage or tweets.
When active, a <🎑> icon will appear at the bottom left of the page. Click it to enter the script's reading interface.
[Can the Script's Entry Point or Control Bar be Relocated?]
Yes! At the bottom of the configuration panel, there's a Drag to Move option. Drag the icon to reposition the control bar anywhere on the page.
[Can the Script Auto-Open When Navigating to the Corresponding Page?]
Yes! There is an Auto Open option in the configuration panel. Enable it to activate this feature.
[How to Zoom Images?]
There are several ways to zoom images in big image reading mode:
Right-click + mouse wheel
Keyboard shortcuts
Zoom controls on the control bar: click the -/+ buttons, scroll the mouse wheel over the numbers, or drag the numbers left or right.
[How to Open Images from a Specific Page?]
In the thumbnail list interface, simply type the desired page number on your keyboard (without any prompt) and press Enter or your custom shortcuts.
[About the Thumbnail List]
The thumbnail list interface is the script's most important feature, allowing you to quickly get an overview of the entire gallery.
Thumbnails are also lazy-loaded, typically loading about 20 images, which is comparable to or even fewer requests than normal browsing.
Pagination is also lazy-loaded, meaning not all gallery pages load at once. Only when you scroll near the bottom does the next page load.
Don't worry about generating a lot of requests by quickly scrolling through the thumbnail list; the script is designed to handle this efficiently.
[About Auto-Loading and Pre-Loading]
By default, the script automatically and slowly loads large images one by one.
You can still click any thumbnail to start loading and reading from that point, at which time auto-loading will stop and pre-load 3 images from the reading position.
Just like the thumbnail list, you don't need to worry about generating a lot of loading requests by fast scrolling.
[About Downloading]
Downloading is integrated with large image loading. When you finish browsing a gallery and want to save and download the images, you can click Start Download in the download panel. don't worry about re-downloading already loaded images.
You can also directly click Start Download in the download panel without reading.
Alternatively, click the Take Loaded button in the download panel if some images consistently fail to load. This will save the images that have already been loaded.
The download panel's status indicators provide a clear view of image loading progress.
Note: When the download file size exceeds 1.2GB, split compression will be automatically enabled. If you encounter errors while extracting the files, please update your extraction software or use 7-Zip.
[Can I Select the Download Range?]
Yes, the download panel has an option to select the download range(Cherry Pick), which applies to downloading, auto-loading, and pre-loading.
Even if an image is excluded from the download range, you can still click its thumbnail to view it, which will load the corresponding large image.
[How to Select Images on Some Illustration Sites?]
In the thumbnail list, you can use some hotkeys to select images:
Ctrl + Left Click: Selects the image. The first selection will exclude all other images.
Ctrl + Shift + Left Click: Selects the range of images between this image and the last selected image.
Alt + Left Click: Excludes the image. The first exclusion will select all other images.
Alt + Shift + Left Click: Excludes the range of images between this image and the last excluded image.
In addition, there are several other methods:
Middle-click on a thumbnail to open the original image url, then right-click to save the image.
Set the download range to 1 in the download panel. This excludes all images except the first one. Then, click on thumbnails of interest in the list, which will load the corresponding large images. After selecting, clear the download range and click Take Loaded to package and download your selected images.
Turn off auto-loading and set pre-loading to 1 in the configuration panel, then proceed as described above.
[Can I Operate the Script via Keyboard?]
Yes! There's a Keyboard button at the bottom of the configuration panel. Click it to view or configure keyboard operations.
You can even configure it for one-handed full keyboard operation, freeing up your other hand!
[How to Disable Auto-Open on Certain Sites?]
There's an Auto Open Excludes button at the bottom of the configuration panel. Click it to exclude certain sites from auto-opening. For example, Twitter or Booru-type sites.
[How to Disable This Script on Certain Sites?]
There's a Excludes button at the bottom of the configuration panel to exclude specific sites. Once excluded, the script will no longer activate on those sites.
To re-enable a site, you need to do so from a site that hasn't been excluded.
[How to Feed the Author]
Give me a star on Github or a good review on Greasyfork .
Please do not review on Greasyfork, as its notification system cannot track subsequent feedback. Many people leave an issue and never back.
Report issues here: issue
[How to Reopen the Guide?]
Click the Help button at the bottom of the configuration panel.
[Some Unresolved Issues]
When using Firefox to open Twitter's homepage in a new tab, then navigating to the user's homepage, the script doesn't activate and requires page refresh.
Still Firefox, Download function not working on twitter.com, firefox will not redirect twitter.com to x.com when open in new tab, you should use x.com instead twitter.com.
`, `
[如何使用?入口在哪里?]
脚本一般生效于画廊详情页或画家的主页或作品页。比如在E-Hentai上,生效于画廊详情页,或者在Twitter上,生效于推主的主页或推文。
生效时,在页面的左下方会有一个<🎑> 图标,点击后即可进入脚本的阅读界面。
[脚本的入口或控制栏可以更改位置吗?]
可以!在配置面板的下方,有一个拖拽移动 的选项,对着图标进行拖动,你可以将控制栏移动到页面上的任意位置。
[进入对应的页面的,可以自动打开脚本吗?]
可以!在配置面板中,有一个自动打开 的选项,启用即可。
[如何缩放图片?]
有几种方式可以在大图阅读模式中缩放图片:
鼠标右键+滚轮
键盘快捷键
控制栏上的缩放控制,点击-/+按钮,或者在数字上滚动滚轮,或者左右拖动数字。
[如何打开指定页数的图片?]
在缩略图列表界面中,直接在键盘上输入数字(没有提示),然后按下回车或自定义的快捷键。
[关于缩略图列表。]
缩略图列表是脚本最重要的特性,可以让你快速地了解整个画廊的情况。
并且缩略图也是延迟加载的,通常会加载20张左右,与正常浏览所发出的请求相当,甚至更低。
并且分页也是延迟加载的,并不会一次性加载画廊的所有分页,只有滚动到接近底部时,才会加载下一页。
不用担心因为在缩略图列表中快速滚动而导致发出大量的请求,脚本充分考虑到了这一点。
[关于自动加载和预加载。]
默认配置下,脚本会自动且缓慢地一张接一张地加载大图。
你仍然可以点击任意位置的缩略图,并从该处开始加载并阅读,此时会自动加载会停止并从阅读的位置预加载3张图片。
同缩略图列表一样,无需担心因为快速滚动而导致发出大量的加载请求。
[关于下载。]
下载与大图加载是一体的,当你浏览完画廊时,突然想起来要保存下载,此时你可以在下载面板中点击开始下载 ,不必担心会重复下载已经加载过的图片。
当然你也可以不浏览,直接在下载面板中点击开始下载 。
或者点击下载面板中的获取已下载的 按钮,当一些图片总是加载失败的时候,你可以使用此功能来保存已经加载过的图片。
通过下载面板中的状态可以直观地看到图片加载的情况。
注意: 当下载文件大小超过1.2G后,会自动启用分卷压缩。当使用解压软件解压出错时,请更新解压软件或使用7-Zip。
[可以选择下载范围吗?]
可以,在下载面板中有选择下载范围的功能,该功能对下载、自动加载、预加载都生效。
另外,如果一张图片被排除在下载范围之外,你仍然可以点击该图片的缩略图进行浏览,这会加载对应的大图。
[如何在一些插画网站上挑选图片?]
在缩略图列表中使用一些快捷键可以进行图片的挑选。
Ctrl+鼠标左键: 选中该图片,当第一次选中时,其他的图片都会被排除。
Ctrl+Shift+鼠标左键: 选中该图片与上一张选中的图片之间的范围。
Alt+鼠标左键: 排除该图片,当第一次排除时,其他的图片都会被选中。
Alt+Shift+鼠标左键: 排除该图片与上一张排除的图片之间的范围。
除此之外还有几种方式:
在缩略图上按下鼠标中键,即可打开图片的原始地址,之后你可以右键保存图片。
在下载面板中设置下载范围为1,这样会排除第一张图片以外的所有图片,之后在缩略图列表上点击你感兴趣的图片,对应的大图会被加载,最终挑选完毕后,删除掉下载范围并点击获取已下载的 ,这样你挑选的图片会被打包下载。
在配置面板中关闭自动加载,并设置预加载数量为1,之后与上面的方法类似。
[可以通过键盘来操作吗?]
可以!在配置面板的下方,有一个快捷键 按钮,点击后可以查看键盘操作,或进行配置。
甚至可以配置为单手全键盘操作,解放另一只手!
[不想在某些网站启用自动打开功能?]
在配置面板的下方,有一个自动打开排除 按钮,点击后可以对一些不适合自动打开的网站进行排除。比如Twitter或Booru类的网站。
[不想在某些网站使用这个脚本?]
在配置面板的下方,有一个站点排除 的按钮,可对一些站点进行排除,排除后脚本不会再生效。
如果想重新启用该站点,需要在其他未排除的站点中启用被禁用的站点。
[如何Feed作者。]
给我Github 星星,或者Greasyfork 上好评。
请勿在Greasyfork上反馈问题,因为该站点的通知系统无法跟踪后续的反馈。很多人只是留下一个问题,再也没有回来过。
请在此反馈问题: issue
[如何再次打开指南?]
在配置面板的下方,点击帮助 按钮。
[一些未能解决的问题。]
使用Firefox通过新标签页打开Twitter的首页后,然后跳转到推主的主页,脚本无法生效,需要刷新页面。
使用Firefox打开twitter.com这个域名,下载功能会失效,这可能和Firefox不能自动跳转到x.com有关,你需要停止使用twitter.com这个域名。
`),
keyboardCustom
};
function createHelpPanel(root) {
const HTML_STR = `
`;
const fullPanel = document.createElement("div");
fullPanel.classList.add("ehvp-full-panel");
fullPanel.innerHTML = HTML_STR;
fullPanel.addEventListener("click", (event) => {
if (event.target.classList.contains("ehvp-full-panel")) {
fullPanel.remove();
}
});
root.appendChild(fullPanel);
fullPanel.querySelector(".ehvp-custom-panel-close").addEventListener("click", () => fullPanel.remove());
}
function createKeyboardCustomPanel(keyboardEvents, root) {
function addKeyboardDescElement(btn, category, id, key) {
const str = `${key} x `;
const tamplate = document.createElement("div");
tamplate.innerHTML = str;
const element = tamplate.firstElementChild;
btn.before(element);
element.querySelector("button").addEventListener("click", (event) => {
const keys = conf.keyboards[category][id];
if (keys && keys.length > 0) {
const index = keys.indexOf(key);
if (index !== -1)
keys.splice(index, 1);
if (keys.length === 0) {
delete conf.keyboards[category][id];
}
saveConf(conf);
}
event.target.parentElement.remove();
const values = Array.from(btn.parentElement.querySelectorAll(".ehvp-custom-panel-item-value"));
if (values.length === 0) {
const desc = keyboardEvents[category][id];
desc.defaultKeys.forEach((key2) => addKeyboardDescElement(btn, category, id, key2));
}
});
tamplate.remove();
}
const HTML_STR = `
Custom Keyboard
✖
${Object.entries(keyboardEvents.inMain).map(([id]) => `
${i18n.keyboardCustom.inMain[id].get()}
+
`).join("")}
${Object.entries(keyboardEvents.inFullViewGrid).map(([id]) => `
${i18n.keyboardCustom.inFullViewGrid[id].get()}
+
`).join("")}
${Object.entries(keyboardEvents.inBigImageMode).map(([id]) => `
${i18n.keyboardCustom.inBigImageMode[id].get()}
+
`).join("")}
`;
const fullPanel = document.createElement("div");
fullPanel.classList.add("ehvp-full-panel");
fullPanel.innerHTML = HTML_STR;
fullPanel.addEventListener("click", (event) => {
if (event.target.classList.contains("ehvp-full-panel")) {
fullPanel.remove();
}
});
root.appendChild(fullPanel);
fullPanel.querySelector(".ehvp-custom-panel-close").addEventListener("click", () => fullPanel.remove());
const buttons = Array.from(fullPanel.querySelectorAll(".ehvp-custom-panel-item-add-btn"));
buttons.forEach((btn) => {
const category = btn.getAttribute("data-cate");
const id = btn.getAttribute("data-id");
let keys = conf.keyboards[category][id];
if (keys === void 0 || keys.length === 0) {
keys = keyboardEvents[category][id].defaultKeys;
}
keys.forEach((key) => addKeyboardDescElement(btn, category, id, key));
const addKeyBoardDesc = (event) => {
event.preventDefault();
if (event.key === "Alt" || event.key === "Shift" || event.key === "Control")
return;
const key = parseKey(event);
if (conf.keyboards[category][id] !== void 0) {
conf.keyboards[category][id].push(key);
} else {
conf.keyboards[category][id] = keys.concat(key);
}
saveConf(conf);
addKeyboardDescElement(btn, category, id, key);
btn.textContent = "+";
};
btn.addEventListener("click", () => {
btn.textContent = "Press Key";
btn.addEventListener("keydown", addKeyBoardDesc);
});
btn.addEventListener("mouseleave", () => {
btn.textContent = "+";
btn.removeEventListener("keydown", addKeyBoardDesc);
});
});
}
class KeyboardDesc {
defaultKeys;
cb;
noPreventDefault = false;
constructor(defaultKeys, cb, noPreventDefault) {
this.defaultKeys = defaultKeys;
this.cb = cb;
this.noPreventDefault = noPreventDefault || false;
}
}
function initEvents(HTML, BIFM, IFQ, IL, PH) {
function modNumberConfigEvent(key, data) {
const range = {
colCount: [1, 12],
threads: [1, 10],
downloadThreads: [1, 10],
timeout: [8, 40],
autoPageSpeed: [1, 100],
preventScrollPageTime: [-1, 9e4],
paginationIMGCount: [1, 5],
scrollingSpeed: [1, 100]
};
let mod = key === "preventScrollPageTime" ? 10 : 1;
if (data === "add") {
if (conf[key] < range[key][1]) {
conf[key] += mod;
}
} else if (data === "minus") {
if (conf[key] > range[key][0]) {
conf[key] -= mod;
}
}
const inputElement = q(`#${key}Input`, HTML.config.panel);
if (inputElement) {
inputElement.value = conf[key].toString();
}
if (key === "colCount") {
const rule = queryRule(HTML.styleSheet, ".full-view-grid");
if (rule)
rule.style.gridTemplateColumns = `repeat(${conf[key]}, 1fr)`;
}
if (key === "paginationIMGCount") {
const rule = queryRule(HTML.styleSheet, ".bifm-img");
if (rule)
rule.style.minWidth = conf[key] > 1 ? "" : "100vw";
q("#paginationInput", HTML.paginationAdjustBar).textContent = conf.paginationIMGCount.toString();
BIFM.setNow(IFQ[IFQ.currIndex], "next");
}
saveConf(conf);
}
function modBooleanConfigEvent(key) {
const inputElement = q(`#${key}Checkbox`, HTML.config.panel);
conf[key] = inputElement?.checked || false;
saveConf(conf);
if (key === "autoLoad") {
IL.autoLoad = conf.autoLoad;
IL.abort(0, conf.restartIdleLoader / 3);
}
if (key === "reversePages") {
const rule = queryRule(HTML.styleSheet, ".bifm-flex");
if (rule) {
rule.style.flexDirection = conf.reversePages ? "row-reverse" : "row";
}
}
}
function changeReadModeEvent(value) {
if (value) {
conf.readMode = value;
saveConf(conf);
}
conf.autoPageSpeed = conf.readMode === "pagination" ? 5 : 1;
q("#autoPageSpeedInput", HTML.config.panel).value = conf.autoPageSpeed.toString();
BIFM.resetScaleBigImages(true);
if (conf.readMode === "pagination") {
BIFM.frame.classList.add("bifm-flex");
if (BIFM.visible) {
const queue = BIFM.getChapter(BIFM.chapterIndex).queue;
const index = parseInt(BIFM.elements.curr[0]?.getAttribute("d-index") || "0");
BIFM.initElements(queue[index]);
}
} else {
BIFM.frame.classList.remove("bifm-flex");
}
Array.from(HTML.readModeSelect.querySelectorAll(".b-main-option")).forEach((element) => {
if (element.getAttribute("data-value") === conf.readMode) {
element.classList.add("b-main-option-selected");
} else {
element.classList.remove("b-main-option-selected");
}
});
}
function modSelectConfigEvent(key) {
const inputElement = q(`#${key}Select`, HTML.config.panel);
const value = inputElement?.value;
if (value) {
conf[key] = value;
saveConf(conf);
}
if (key === "readMode") {
changeReadModeEvent();
}
if (key === "minifyPageHelper") {
switch (conf.minifyPageHelper) {
case "always":
PH.minify("bigImageFrame");
break;
case "inBigMode":
case "never":
PH.minify(BIFM.visible ? "bigImageFrame" : "fullViewGrid");
break;
}
}
}
const cancelIDContext = {};
function collapsePanelEvent(target, id) {
if (id) {
abortMouseleavePanelEvent(id);
}
const timeoutId = window.setTimeout(() => target.classList.add("p-collapse"), 100);
if (id) {
cancelIDContext[id] = timeoutId;
}
}
function abortMouseleavePanelEvent(id) {
(id ? [id] : [...Object.keys(cancelIDContext)]).forEach((k) => {
window.clearTimeout(cancelIDContext[k]);
delete cancelIDContext[k];
});
}
function togglePanelEvent(id, collapse, target) {
let element = q(`#${id}-panel`, HTML.pageHelper);
if (!element)
return;
if (collapse === void 0) {
togglePanelEvent(id, !element.classList.contains("p-collapse"), target);
return;
}
if (collapse) {
collapsePanelEvent(element, id);
} else {
["config", "downloader"].filter((k) => k !== id).forEach((id2) => togglePanelEvent(id2, true));
element.classList.remove("p-collapse");
if (target) {
relocateElement(element, target, HTML.root.clientWidth, HTML.root.clientHeight);
}
}
}
let bodyOverflow = document.body.style.overflow;
function showFullViewGrid() {
PH.minify("fullViewGrid");
HTML.root.classList.remove("ehvp-root-collapse");
HTML.fullViewGrid.focus();
document.body.style.overflow = "hidden";
}
function hiddenFullViewGrid() {
BIFM.hidden();
PH.minify("exit");
HTML.entryBTN.setAttribute("data-stage", "exit");
HTML.root.classList.add("ehvp-root-collapse");
HTML.fullViewGrid.blur();
document.body.style.overflow = bodyOverflow;
}
function shouldStep(oriented, shouldPrevent) {
if (BIFM.isReachedBoundary(oriented)) {
if (shouldPrevent && BIFM.tryPreventStep())
return false;
return true;
}
return false;
}
const scrollEventDebouncer = new Debouncer();
function initKeyboardEvent() {
const inBigImageMode = {
"exit-big-image-mode": new KeyboardDesc(
["Escape", "Enter"],
() => BIFM.hidden()
),
"step-image-prev": new KeyboardDesc(
["ArrowLeft"],
() => BIFM.stepNext(conf.reversePages ? "next" : "prev")
),
"step-image-next": new KeyboardDesc(
["ArrowRight"],
() => BIFM.stepNext(conf.reversePages ? "prev" : "next")
),
"step-to-first-image": new KeyboardDesc(
["Home"],
() => BIFM.stepNext("next", 0, -1)
),
"step-to-last-image": new KeyboardDesc(
["End"],
() => BIFM.stepNext("prev", 0, -1)
),
"scale-image-increase": new KeyboardDesc(
["="],
() => BIFM.scaleBigImages(1, 5)
),
"scale-image-decrease": new KeyboardDesc(
["-"],
() => BIFM.scaleBigImages(-1, 5)
),
"scroll-image-up": new KeyboardDesc(
["PageUp", "ArrowUp", "Shift+Space"],
(event) => {
const key = parseKey(event);
const customKey = !["PageUp", "ArrowUp", "Shift+Space"].includes(key);
if (customKey) {
scroller.scrollSmoothly(BIFM.frame, -1);
}
const shouldPrevent = !["PageUp", "Shift+Space"].includes(key);
if (shouldPrevent) {
if (!customKey) {
scrollEventDebouncer.addEvent("SCROLL-IMAGE-UP", () => BIFM.frame.dispatchEvent(new CustomEvent("smoothlyscrollend")), 100);
}
BIFM.frame.addEventListener("smoothlyscrollend", () => shouldStep("prev", true), { once: true });
}
if (shouldStep("prev", shouldPrevent)) {
event.preventDefault();
scroller.scrollTerminate(BIFM.frame);
BIFM.onWheel(new WheelEvent("wheel", { deltaY: -1 }), false);
}
},
true
),
"scroll-image-down": new KeyboardDesc(
["PageDown", "ArrowDown", "Space"],
(event) => {
const key = parseKey(event);
const customKey = !["PageDown", "ArrowDown", "Space"].includes(key);
if (customKey) {
scroller.scrollSmoothly(BIFM.frame, 1);
}
const shouldPrevent = !["PageDown", "Space"].includes(key);
if (shouldPrevent) {
if (!customKey) {
scrollEventDebouncer.addEvent("SCROLL-IMAGE-DOWN", () => BIFM.frame.dispatchEvent(new CustomEvent("smoothlyscrollend")), 100);
}
BIFM.frame.addEventListener("smoothlyscrollend", () => shouldStep("next", true), { once: true });
}
if (shouldStep("next", shouldPrevent)) {
event.preventDefault();
scroller.scrollTerminate(BIFM.frame);
BIFM.onWheel(new WheelEvent("wheel", { deltaY: 1 }), false);
}
},
true
)
};
const inFullViewGrid = {
"open-big-image-mode": new KeyboardDesc(
["Enter"],
() => {
let start = IFQ.currIndex;
if (numberRecord && numberRecord.length > 0) {
start = Number(numberRecord.join("")) - 1;
numberRecord = null;
if (isNaN(start))
return;
start = Math.max(0, Math.min(start, IFQ.length - 1));
}
IFQ[start].node.root?.querySelector("a")?.dispatchEvent(new MouseEvent("click", { bubbles: false, cancelable: true }));
}
),
"pause-auto-load-temporarily": new KeyboardDesc(
["p"],
() => {
IL.autoLoad = !IL.autoLoad;
if (IL.autoLoad) {
IL.abort(IFQ.currIndex, conf.restartIdleLoader / 3);
}
}
),
"exit-full-view-grid": new KeyboardDesc(
["Escape"],
() => EBUS.emit("toggle-main-view", false)
),
"columns-increase": new KeyboardDesc(
["="],
() => modNumberConfigEvent("colCount", "add")
),
"columns-decrease": new KeyboardDesc(
["-"],
() => modNumberConfigEvent("colCount", "minus")
),
"back-chapters-selection": new KeyboardDesc(
["b"],
() => EBUS.emit("back-chapters-selection")
)
};
const inMain = {
"open-full-view-grid": new KeyboardDesc(["Enter"], (_) => {
const activeElement = document.activeElement;
if (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLSelectElement)
return;
EBUS.emit("toggle-main-view", true);
}, true)
};
return { inBigImageMode, inFullViewGrid, inMain };
}
const keyboardEvents = initKeyboardEvent();
let numberRecord = null;
function bigImageFrameKeyBoardEvent(event) {
if (HTML.bigImageFrame.classList.contains("big-img-frame-collapse"))
return;
const key = parseKey(event);
const triggered = Object.entries(keyboardEvents.inBigImageMode).some(([id, desc]) => {
const override = conf.keyboards.inBigImageMode[id];
if (override !== void 0 && override.length > 0 ? override.includes(key) : desc.defaultKeys.includes(key)) {
desc.cb(event);
return !desc.noPreventDefault;
}
return false;
});
if (triggered) {
event.preventDefault();
}
}
function fullViewGridKeyBoardEvent(event) {
if (HTML.root.classList.contains("ehvp-root-collapse"))
return;
const key = parseKey(event);
const triggered = Object.entries(keyboardEvents.inFullViewGrid).some(([id, desc]) => {
const override = conf.keyboards.inFullViewGrid[id];
if (override !== void 0 && override.length > 0 ? override.includes(key) : desc.defaultKeys.includes(key)) {
desc.cb(event);
return !desc.noPreventDefault;
}
return false;
});
if (triggered) {
event.preventDefault();
} else if (event.key.length === 1 && event.key >= "0" && event.key <= "9") {
numberRecord = numberRecord ? [...numberRecord, Number(event.key)] : [Number(event.key)];
event.preventDefault();
}
}
function keyboardEvent(event) {
if (!HTML.root.classList.contains("ehvp-root-collapse"))
return;
if (!HTML.bigImageFrame.classList.contains("big-img-frame-collapse"))
return;
const key = parseKey(event);
const triggered = Object.entries(keyboardEvents.inMain).some(([id, desc]) => {
const override = conf.keyboards.inMain[id];
if (override !== void 0 ? override.includes(key) : desc.defaultKeys.includes(key)) {
desc.cb(event);
return !desc.noPreventDefault;
}
return false;
});
if (triggered) {
event.preventDefault();
}
}
function showGuideEvent() {
createHelpPanel(HTML.root);
}
function showKeyboardCustomEvent() {
createKeyboardCustomPanel(keyboardEvents, HTML.root);
}
function showExcludeURLEvent() {
createExcludeURLPanel(HTML.root, conf.excludeURLs);
}
function showAutoOpenExcludeURLEvent() {
createExcludeURLPanel(HTML.root, conf.autoOpenExcludeURLs, true);
}
return {
modNumberConfigEvent,
modBooleanConfigEvent,
modSelectConfigEvent,
togglePanelEvent,
showFullViewGrid,
hiddenFullViewGrid,
fullViewGridKeyBoardEvent,
bigImageFrameKeyBoardEvent,
keyboardEvent,
showGuideEvent,
collapsePanelEvent,
abortMouseleavePanelEvent,
showKeyboardCustomEvent,
showExcludeURLEvent,
showAutoOpenExcludeURLEvent,
changeReadModeEvent
};
}
class FullViewGridManager {
root;
// renderRangeRecord: [number, number] = [0, 0];
queue = [];
done = false;
chapterIndex = 0;
constructor(HTML, BIFM) {
this.root = HTML.fullViewGrid;
EBUS.subscribe("pf-on-appended", (_total, nodes, chapterIndex, done) => {
if (this.chapterIndex > -1 && chapterIndex !== this.chapterIndex)
return;
this.append(nodes);
this.done = done || false;
setTimeout(() => this.renderCurrView(), 200);
});
EBUS.subscribe("pf-change-chapter", (index) => {
this.chapterIndex = index;
this.root.innerHTML = "";
this.queue = [];
this.done = false;
});
EBUS.subscribe("ifq-do", (_, imf) => {
if (!BIFM.visible)
return;
if (imf.chapterIndex !== this.chapterIndex)
return;
if (!imf.node.root)
return;
let scrollTo = imf.node.root.offsetTop - window.screen.availHeight / 3;
scrollTo = scrollTo <= 0 ? 0 : scrollTo >= this.root.scrollHeight ? this.root.scrollHeight : scrollTo;
if (this.root.scrollTo.toString().includes("[native code]")) {
this.root.scrollTo({ top: scrollTo, behavior: "smooth" });
} else {
this.root.scrollTop = scrollTo;
}
});
EBUS.subscribe("cherry-pick-changed", (chapterIndex) => this.chapterIndex === chapterIndex && this.updateRender());
const debouncer = new Debouncer();
this.root.addEventListener("scroll", () => debouncer.addEvent("FULL-VIEW-SCROLL-EVENT", () => {
if (HTML.root.classList.contains("ehvp-root-collapse"))
return;
this.renderCurrView();
this.tryExtend();
}, 400));
this.root.addEventListener("click", (event) => {
if (event.target === HTML.fullViewGrid || event.target.classList.contains("img-node")) {
EBUS.emit("toggle-main-view", false);
}
});
}
append(nodes) {
if (nodes.length > 0) {
const list = nodes.map((n) => {
return {
node: n,
element: n.create()
};
});
this.queue.push(...list);
this.root.append(...list.map((l) => l.element));
}
}
tryExtend() {
if (this.done)
return;
const nodes = Array.from(this.root.childNodes);
if (nodes.length === 0)
return;
const lastImgNode = nodes[nodes.length - 1];
const viewButtom = this.root.scrollTop + this.root.clientHeight;
if (viewButtom + this.root.clientHeight * 2.5 < lastImgNode.offsetTop + lastImgNode.offsetHeight) {
return;
}
EBUS.emit("pf-try-extend");
}
updateRender() {
this.queue.forEach(({ node }) => node.isRender() && node.render());
}
/**
* 当滚动停止时,检查当前显示的页面上的是什么元素,然后渲染图片
*/
renderCurrView() {
const [scrollTop, clientHeight] = [this.root.scrollTop, this.root.clientHeight];
const [start, end] = this.findOutsideRoundView(scrollTop, clientHeight);
this.queue.slice(start, end + 1 + conf.colCount).forEach((e) => e.node.render());
}
findOutsideRoundView(currTop, clientHeight) {
const viewButtom = currTop + clientHeight;
let outsideTop = 0;
let outsideBottom = 0;
for (let i = 0; i < this.queue.length; i += conf.colCount) {
const element = this.queue[i].element;
if (outsideBottom === 0) {
if (element.offsetTop + 2 >= currTop) {
outsideBottom = i + 1;
} else {
outsideTop = i;
}
} else {
outsideBottom = i;
if (element.offsetTop + element.offsetHeight > viewButtom) {
break;
}
}
}
return [outsideTop, Math.min(outsideBottom + conf.colCount, this.queue.length - 1)];
}
}
function toPositions(vw, vh, mouseX, mouseY) {
let pos = { vw, vh };
if (mouseX <= vw / 2) {
pos.left = Math.max(mouseX, 5);
} else {
pos.right = Math.max(vw - mouseX, 5);
}
if (mouseY <= vh / 2) {
pos.top = Math.max(mouseY, 5);
} else {
pos.bottom = Math.max(vh - mouseY, 5);
}
return pos;
}
function dragElement(element, callbacks, dragHub) {
(dragHub ?? element).addEventListener("mousedown", (event) => {
event.preventDefault();
const wh = window.innerHeight;
const ww = window.innerWidth;
const abort = new AbortController();
callbacks.onStart?.(event.clientX, event.clientY);
document.addEventListener("mousemove", (event2) => {
callbacks.onMoving?.(toPositions(ww, wh, event2.clientX, event2.clientY));
}, { signal: abort.signal });
document.addEventListener("mouseup", () => {
abort.abort();
callbacks.onFinish?.(toPositions(ww, wh, event.clientX, event.clientY));
}, { once: true });
});
}
function dragElementWithLine(event, element, lock, callback) {
if (event.buttons !== 1)
return;
document.querySelector("#drag-element-with-line")?.remove();
const canvas = document.createElement("canvas");
canvas.id = "drag-element-with-line";
canvas.style.position = "fixed";
canvas.style.zIndex = "100000";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.width = "100vw";
canvas.style.height = "100vh";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
const rect = element.getBoundingClientRect();
const height = Math.floor(rect.height / 2.2);
const [startX, startY] = [rect.left + rect.width / 2, rect.top + rect.height / 2];
const ctx = canvas.getContext("2d", { alpha: true });
const abort = new AbortController();
canvas.addEventListener("mouseup", () => {
document.body.removeChild(canvas);
abort.abort();
}, { once: true });
canvas.addEventListener("mousemove", (evt) => {
let [endX, endY] = [
lock.x ? startX : evt.clientX,
lock.y ? startY : evt.clientY
];
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.strokeStyle = "#ffffffa0";
ctx.lineWidth = 4;
ctx.stroke();
ctx.beginPath();
ctx.arc(endX, endY, height, 0, 2 * Math.PI);
ctx.fillStyle = "#ffffffa0";
ctx.fill();
callback(toMouseMoveData(startX, startY, endX, endY));
}, { signal: abort.signal });
}
function toMouseMoveData(startX, startY, endX, endY) {
const distance = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));
const direction = 1 << (startY > endY ? 3 : 2) | 1 << (startX > endX ? 1 : 0);
return { start: { x: startX, y: startY }, end: { x: endX, y: endY }, distance, direction };
}
function styleCSS() {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent);
const css = `
.ehvp-root {
--ehvp-background-color: #333343bb;
--ehvp-border: 1px solid #2f7b10;
--ehvp-font-color: #fff;
font-size: 16px;
}
.ehvp-root {
width: 100vw;
height: 100vh;
background-color: #000;
position: fixed;
top: 0px;
left: 0px;
z-index: 2000;
box-sizing: border-box;
overflow: clip;
}
.ehvp-root input[type="checkbox"] {
width: 1em;
height: unset !important;
}
.ehvp-root select {
width: 8em;
height: 2em;
}
.ehvp-root input {
width: 3em;
height: 1.5em;
}
.ehvp-root-collapse {
height: 0;
}
.full-view-grid {
width: 100vw;
height: 100vh;
display: grid;
align-content: start;
grid-gap: 0.7em;
grid-template-columns: repeat(${conf.colCount}, 1fr);
overflow: hidden scroll;
padding: 0.3em;
box-sizing: border-box;
}
.ehvp-root input, .ehvp-root select {
color: #f1f1f1;
background-color: #34353b;
color-scheme: dark;
border: 1px solid #000000;
border-radius: 4px;
margin: 0px;
padding: 0px;
text-align: center;
vertical-align: middle;
}
.ehvp-root input:enabled:hover, .ehvp-root select:enabled:hover, .ehvp-root input:enabled:focus, .ehvp-root select:enabled:focus {
background-color: #34355b !important;
}
.ehvp-root select option {
background-color: #34355b !important;
color: #f1f1f1;
font-size: 1em;
}
.p-label {
cursor: pointer;
}
.full-view-grid .img-node {
position: relative;
}
.img-node canvas, .img-node img {
position: relative;
width: 100%;
height: auto;
border: 3px solid #fff;
box-sizing: border-box;
}
.img-node:hover .ehvp-chapter-description {
color: #ffe7f5;
}
.img-node > a {
display: block;
line-height: 0;
position: relative;
}
.ehvp-chapter-description, .img-node-error-hint {
display: block;
position: absolute;
bottom: 3px;
left: 3px;
background-color: #708090e3;
color: #ffe785;
width: calc(100% - 6px);
font-weight: 600;
min-height: 3em;
font-size: 1.2em;
padding: 0.5em;
box-sizing: border-box;
line-height: 1.3em;
}
.img-node-error-hint {
color: #8a0000;
}
.img-fetched img, .img-fetched canvas {
border: 3px solid #90ffae !important;
}
.img-fetch-failed img, .img-fetch-failed canvas {
border: 3px solid red !important;
}
.img-fetching img, .img-fetching canvas {
border: 3px solid #00000000 !important;
}
.img-excluded img, .img-excluded canvas {
border: 3px solid #777 !important;
}
.img-excluded a::after {
content: '';
position: absolute;
z-index: 1;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
/**aspect-ratio: 1;*/
background-color: #333333b0;
}
.img-fetching a::after {
content: '';
position: absolute;
z-index: -1;
top: 0%;
left: 0%;
width: 30%;
height: 30%;
background-color: #ff0000;
animation: img-loading 1s linear infinite;
}
@keyframes img-loading {
25% {
background-color: #ff00ff;
top: 0%;
left: 70%;
}
50% {
background-color: #00ffff;
top: 70%;
left: 70%;
}
75% {
background-color: #ffff00;
top: 70%;
left: 0%;
}
}
.big-img-frame::-webkit-scrollbar {
display: none;
}
.big-img-frame {
position: fixed;
width: 100%;
height: 100%;
top: 0;
right: 0;
overflow: auto;
scrollbar-width: none;
z-index: 2001;
background-color: #000000d6;
}
.big-img-frame > img, .big-img-frame > video {
object-fit: contain;
display: block;
}
.bifm-flex {
display: flex;
justify-content: flex-start;
flex-direction: ${conf.reversePages ? "row-reverse" : "row"};
}
.bifm-img { }
.p-helper {
position: fixed;
z-index: 2011 !important;
box-sizing: border-box;
top: ${conf.pageHelperAbTop};
left: ${conf.pageHelperAbLeft};
bottom: ${conf.pageHelperAbBottom};
right: ${conf.pageHelperAbRight};
}
.p-panel {
z-index: 2012 !important;
background-color: #333343aa;
box-sizing: border-box;
position: fixed;
color: white;
overflow: auto scroll;
padding: 3px;
scrollbar-width: none;
border-radius: 4px;
font-weight: 800;
width: 24em;
height: 32em;
}
.p-panel::-webkit-scrollbar {
display: none;
}
.clickable {
text-decoration-line: underline;
user-select: none;
text-align: center;
white-space: nowrap;
}
.clickable:hover {
color: #90ea90 !important;
}
.p-collapse {
height: 0px !important;
padding: 0px !important;
}
.b-main {
display: flex;
user-select: none;
flex-direction: ${conf.pageHelperAbLeft === "unset" ? "row-reverse" : "row"};
flex-wrap: wrap-reverse;
}
.b-main-item {
box-sizing: border-box;
border: var(--ehvp-border);
border-radius: 4px;
background-color: var(--ehvp-background-color);
color: var(--ehvp-font-color);
font-weight: 800;
padding: 0em 0.3em;
margin: 0em 0.2em;
position: relative;
white-space: nowrap;
font-size: 1em;
line-height: 1.2em;
}
.b-main-option {
padding: 0em 0.2em;
}
.b-main-option-selected {
color: black;
background-color: #ffffffa0;
border-radius: 6px;
}
.b-main-btn {
display: inline-block;
width: 1em;
}
.b-main-input {
color: black;
background-color: #ffffffa0;
border-radius: 6px;
display: inline-block;
text-align: center;
width: 1.5em;
cursor: ns-resize;
}
.p-config {
display: grid;
grid-template-columns: repeat(10, 1fr);
align-content: start;
line-height: 2em;
}
.p-config label {
display: flex;
justify-content: space-between;
padding-right: 10px;
margin-bottom: unset;
}
.p-config input {
cursor: ns-resize;
}
.p-downloader {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.p-downloader canvas {
/* border: 1px solid greenyellow; */
}
.p-downloader .download-notice {
text-align: center;
width: 100%;
}
.p-downloader .downloader-btn-group {
align-items: center;
text-align: right;
width: 100%;
}
.p-btn {
color: var(--ehvp-font-color);
cursor: pointer;
font-weight: 800;
background-color: rgb(81, 81, 81);
vertical-align: middle;
width: 1.5em;
height: 1.5em;
border: 1px solid #000000;
border-radius: 4px;
}
@keyframes main-progress {
from {
width: 0%;
}
to {
width: 100%;
}
}
.big-img-frame-collapse {
width: 0px !important;
}
.big-img-frame-collapse .img-land-left,
.big-img-frame-collapse .img-land-right,
.big-img-frame-collapse .img-land-top,
.big-img-frame-collapse .img-land-bottom {
display: none !important;
}
.download-bar {
background-color: #333333c0;
height: 0.3em;
width: 100%;
bottom: -0.3em;
position: absolute;
border-left: 3px solid #00000000;
border-right: 3px solid #00000000;
box-sizing: border-box;
}
.download-bar > div {
background-color: #f0fff0;
height: 100%;
border: none;
}
.img-land-left, .img-land-right {
width: 15%;
height: 50%;
position: fixed;
z-index: 2004;
top: 25%;
}
.img-land-left {
left: 0;
cursor: url("https://exhentai.org/img/p.png"), auto;
}
.img-land-right {
right: 0;
cursor: url("https://exhentai.org/img/n.png"), auto;
}
.p-tooltip { }
.p-tooltip .p-tooltiptext {
visibility: hidden;
max-width: 24em;
background-color: #000000df;
color: var(--ehvp-font-color);
border-radius: 6px;
position: fixed;
z-index: 1;
font-size: medium;
white-space: normal;
text-align: left;
padding: 0.3em 1em;
box-sizing: border-box;
display: block;
}
.page-loading {
width: 100vw;
height: 100vh;
justify-content: center;
align-items: center;
background-color: #333333a6;
}
.page-loading-text {
color: var(--ehvp-font-color);
font-size: 6em;
}
@keyframes rotate {
100% {
transform: rotate(1turn);
}
}
.border-ani {
position: relative;
z-index: 0;
overflow: hidden;
}
.border-ani::before {
content: '';
position: absolute;
z-index: -2;
left: -50%;
top: -50%;
width: 200%;
height: 200%;
background-color: #fff;
animation: rotate 4s linear infinite;
}
.border-ani::after {
content: '';
position: absolute;
z-index: -1;
left: 6px;
top: 6px;
width: calc(100% - 16px);
height: calc(100% - 16px);
background-color: #333;
}
.overlay-tip {
position: absolute;
top: 3px;
right: 3px;
z-index: 10;
height: 1em;
border-radius: 10%;
border: 1px solid #333;
color: var(--ehvp-font-color);
background-color: #959595d1;
text-align: center;
font-weight: 800;
}
.lightgreen { color: #90ea90; }
.ehvp-full-panel {
position: fixed;
width: 100vw;
height: 100vh;
background-color: #000000e8;
z-index: 3000;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
top: 0;
}
.ehvp-custom-panel {
min-width: 50vw;
min-height: 50vh;
max-width: 80vw;
max-height: 80vh;
background-color: #333343aa;
border: 1px solid #000000;
display: flex;
flex-direction: column;
text-align: start;
color: var(--ehvp-font-color);
}
.ehvp-custom-panel-title {
font-size: 2em;
font-weight: 800;
display: flex;
justify-content: space-between;
padding-left: 1em;
}
.ehvp-custom-panel-close {
width: 2em;
text-align: center;
}
.ehvp-custom-panel-close:hover {
background-color: #c3c0e0;
}
.ehvp-custom-panel-container {
overflow: auto;
}
.ehvp-custom-panel-content {
border: 1px solid #000000;
border-radius: 4px;
margin: 0.5em;
padding: 0.5em;
}
.ehvp-custom-panel-item {
margin: 0.2em 0em;
}
.ehvp-custom-panel-item-title {
font-size: 1.4em;
}
.ehvp-custom-panel-item-values {
margin-top: 0.3em;
text-align: end;
line-height: 1.3em;
}
.ehvp-custom-panel-item-value {
font-size: 1.1em;
font-weight: 800;
color: black;
background-color: #c5c5c5;
border: 1px solid #000000;
box-sizing: border-box;
margin-left: 0.3em;
display: inline-flex;
}
.ehvp-custom-panel-item-value span {
padding: 0em 0.5em;
}
.ehvp-custom-panel-item-value button {
background-color: #fff;
color: black;
border: none;
}
.ehvp-custom-panel-item-value button:hover {
background-color: #ffff00;
}
.ehvp-custom-panel-item-add-btn, .ehvp-custom-panel-item-input, .ehvp-custom-panel-item-span {
font-size: 1.1em;
font-weight: 800;
background-color: #7fef7b;
color: black;
border: none;
}
.ehvp-custom-panel-item-span {
background-color: #34355b;
color: white;
}
.ehvp-custom-panel-item-add-btn:hover {
background-color: #ffff00 !important;
}
.ehvp-custom-panel-list > li {
line-height: 3em;
margin-left: 0.5em;
font-size: 1.4em;
}
.ehvp-custom-panel-list-item-disable {
text-decoration: line-through;
color: red;
}
.ehvp-help-panel > div > h2 {
color: #c1ffc9;
}
.ehvp-help-panel > div > p {
font-size: 1.1em;
margin-left: 1em;
font-weight: 600;
}
.ehvp-help-panel > div > ul {
font-size: 1em;
}
.ehvp-help-panel > div a {
color: #ff5959;
}
.ehvp-help-panel > div strong {
color: #d76d00;
}
.bifm-vid-ctl {
position: fixed;
z-index: 2010;
padding: 3px 10px;
bottom: 0.2em;
${conf.pageHelperAbLeft === "unset" ? "left: 0.2em;" : "right: 0.2em;"}
}
.bifm-vid-ctl > div {
display: flex;
align-items: center;
line-height: 1.2em;
}
.bifm-vid-ctl > div > * {
margin: 0 0.1em;
}
.bifm-vid-ctl:not(:hover) .bifm-vid-ctl-btn,
.bifm-vid-ctl:not(:hover) .bifm-vid-ctl-span,
.bifm-vid-ctl:not(:hover) #bifm-vid-ctl-volume
{
opacity: 0;
}
.bifm-vid-ctl-btn {
height: 1.5em;
width: 1.5em;
font-size: 1.2em;
padding: 0;
margin: 0;
border: none;
background-color: #00000000;
cursor: pointer;
}
#bifm-vid-ctl-volume {
width: 5em;
height: 0.5em;
}
.bifm-vid-ctl-pg {
border: 1px solid #00000000;
background-color: #3333337e;
-webkit-appearance: none;
}
#bifm-vid-ctl-pg {
width: 100%;
height: 0.2em;
background-color: #333333ee;
}
.bifm-vid-ctl:hover {
background-color: var(--ehvp-background-color);
}
.bifm-vid-ctl:hover #bifm-vid-ctl-pg {
height: 0.8em;
}
.bifm-vid-ctl-pg-inner {
background-color: #ffffffa0;
height: 100%;
}
.bifm-vid-ctl:hover #bifm-vid-ctl-pg .bifm-vid-ctl-pg-inner {
background-color: #fff;
}
.bifm-vid-ctl-span {
color: white;
font-weight: 800;
}
.download-middle {
width: 100%;
height: auto;
flex-grow: 1;
overflow: hidden;
}
.download-middle .ehvp-tabs + div {
width: 100%;
height: calc(100% - 2em);
}
.ehvp-tabs {
height: 2em;
width: 100%;
line-height: 2em;
}
.ehvp-p-tab {
border: 1px dotted #ff0;
font-size: 1em;
padding: 0 0.4em;
}
.download-chapters, .download-status, .download-cherry-pick {
width: 100%;
height: 100%;
}
.download-chapters {
overflow: hidden auto;
}
.download-chapters label {
white-space: nowrap;
}
.download-chapters label span {
margin-left: 0.5em;
}
.ehvp-p-tab-selected {
color: rgb(120, 240, 80) !important;
}
.ehvp-root-collapse .ehvp-message-box {
display: none;
}
.ehvp-message-box {
position: fixed;
z-index: 4001;
top: 0;
left: 0;
}
.ehvp-message {
margin-top: 1em;
margin-left: 1em;
line-height: 2em;
background-color: #ffffffd6;
border-radius: 6px;
padding-left: 0.3em;
position: relative;
box-shadow: inset 0 0 5px 2px #8273ff;
color: black;
}
.ehvp-message > button {
border: 1px solid #00000000;
margin-left: 1em;
color: black;
background-color: #00000000;
height: 2em;
width: 2em;
text-align: center;
font-weight: 800;
}
.ehvp-message > button:hover {
background-color: #444;
}
.ehvp-message-duration-bar {
position: absolute;
bottom: 0;
width: 0%;
left: 0;
height: 0.1em;
background: red;
}
@media (max-width: ${isMobile ? "1440px" : "720px"}) {
.ehvp-root {
font-size: 4cqw;
}
.ehvp-root-collapse #entry-btn {
font-size: 2.2em;
}
.p-helper {
bottom: 0px;
left: 0px;
top: unset;
right: unset;
}
.b-main {
flex-direction: row;
}
.b-main-item {
font-size: 1.3em;
margin-top: 0.2em;
}
#pagination-adjust-bar {
display: none;
}
.bifm-img {
min-weight: 100vw !important;
}
.p-panel {
width: 100vw;
font-size: 5cqw;
}
.ehvp-custom-panel {
max-width: 100vw;
}
.ehvp-root input, .ehvp-root select {
width: 2em;
height: 1.2em;
font-size: 1em;
}
.ehvp-root select {
width: 7em !important;
}
.p-btn {
font-size: 1em;
}
.bifm-vid-ctl {
display: none;
}
}
`;
return css;
}
const bookIcon = `📖`;
const moonViewCeremony = `<🎑>`;
const sixPointedStar = `🔯`;
const entryIcon = `⍇⍈`;
const zoomIcon = `⇱⇲`;
const icons = {
bookIcon,
moonViewCeremony,
sixPointedStar,
entryIcon,
zoomIcon
};
class DownloaderPanel {
panel;
canvas;
tabStatus;
tabChapters;
tabCherryPick;
statusElement;
chaptersElement;
cherryPickElement;
noticeElement;
forceBTN;
startBTN;
btn;
constructor(root) {
this.btn = q("#downloader-panel-btn", root);
this.panel = q("#downloader-panel", root);
this.canvas = q("#downloader-canvas", root);
this.tabStatus = q("#download-tab-status", root);
this.tabChapters = q("#download-tab-chapters", root);
this.tabCherryPick = q("#download-tab-cherry-pick", root);
this.statusElement = q("#download-status", root);
this.chaptersElement = q("#download-chapters", root);
this.cherryPickElement = q("#download-cherry-pick", root);
this.noticeElement = q("#download-notice", root);
this.forceBTN = q("#download-force", root);
this.startBTN = q("#download-start", root);
this.panel.addEventListener("transitionend", () => EBUS.emit("downloader-canvas-resize"));
}
initTabs() {
const elements = [this.statusElement, this.chaptersElement, this.cherryPickElement];
const tabs = [
{
ele: this.tabStatus,
cb: () => {
elements.forEach((e, i) => e.hidden = i != 0);
EBUS.emit("downloader-canvas-resize");
}
},
{
ele: this.tabChapters,
cb: () => {
elements.forEach((e, i) => e.hidden = i != 1);
}
},
{
ele: this.tabCherryPick,
cb: () => {
elements.forEach((e, i) => e.hidden = i != 2);
q("#download-cherry-pick-input", this.cherryPickElement).focus();
}
}
];
tabs.forEach(({ ele, cb }, i) => {
ele.addEventListener("click", () => {
ele.classList.add("ehvp-p-tab-selected");
tabs.filter((_, j) => j != i).forEach((t) => t.ele.classList.remove("ehvp-p-tab-selected"));
cb();
});
});
}
switchTab(tabID) {
switch (tabID) {
case "status":
this.tabStatus.click();
break;
case "chapters":
this.tabChapters.click();
break;
case "cherry-pick":
this.tabCherryPick.click();
break;
}
}
noticeOriginal(cb) {
this.noticeElement.innerHTML = `${i18n.originalCheck.get()} `;
this.noticeElement.querySelector("a")?.addEventListener("click", cb);
}
abort(stage) {
this.flushUI(stage);
this.normalizeBTN();
}
flushUI(stage) {
this.noticeElement.innerHTML = `${i18n[stage].get()} `;
this.startBTN.style.color = stage === "downloadFailed" ? "red" : "";
this.startBTN.textContent = i18n[stage].get();
this.btn.style.color = stage === "downloadFailed" ? "red" : "";
}
noticeableBTN() {
if (!this.btn.classList.contains("lightgreen")) {
this.btn.classList.add("lightgreen");
if (!/✓/.test(this.btn.textContent)) {
this.btn.textContent += "✓";
}
}
}
normalizeBTN() {
this.btn.textContent = i18n.download.get();
this.btn.classList.remove("lightgreen");
}
createChapterSelectList(chapters, selectedChapters) {
const selectAll = chapters.length === 1;
this.chaptersElement.innerHTML = `
Select All
Unselect All
${chapters.map((c, i) => `
sel.index === i) ? "checked" : ""} />
${c.title}
`).join("")}
`;
[["#download-chapters-select-all", true], ["#download-chapters-unselect-all", false]].forEach(
([id, checked]) => this.chaptersElement.querySelector(id)?.addEventListener(
"click",
() => chapters.forEach((c) => {
const checkbox = this.chaptersElement.querySelector("#ch-" + c.id);
if (checkbox)
checkbox.checked = checked;
})
)
);
}
selectedChapters() {
const idSet = /* @__PURE__ */ new Set();
this.chaptersElement.querySelectorAll("input[type=checkbox][id^=ch-]:checked").forEach((checkbox) => idSet.add(Number(checkbox.value)));
return idSet;
}
initCherryPick(onAdd, onRemove, onClear) {
function addRangeElements(container, rangeList, onRemove2) {
container.querySelectorAll(".ehvp-custom-panel-item-value").forEach((e) => e.remove());
const tamplate = document.createElement("div");
rangeList.forEach((range) => {
const str = `${range.toString()} x `;
tamplate.innerHTML = str;
const element = tamplate.firstElementChild;
element.style.backgroundColor = range.positive ? "#7fef7b" : "#ffa975";
container.appendChild(element);
element.querySelector("button").addEventListener("click", (event) => {
const parent = event.target.parentElement;
onRemove2(parent.getAttribute("data-id"));
parent.remove();
});
tamplate.remove();
});
}
const pickBTN = q("#download-cherry-pick-btn-add", this.cherryPickElement);
const excludeBTN = q("#download-cherry-pick-btn-exclude", this.cherryPickElement);
const clearBTN = q("#download-cherry-pick-btn-clear", this.cherryPickElement);
const rangeBeforeSpan = q("#download-cherry-pick-btn-range-before", this.cherryPickElement);
const rangeAfterSpan = q("#download-cherry-pick-btn-range-after", this.cherryPickElement);
const input = q("#download-cherry-pick-input", this.cherryPickElement);
const addCherryPick = (exclude, range) => {
const rangeList = range ? [CherryPickRnage.from((exclude ? "!" : "") + range)].filter((r) => r !== null) : (input.value || "").split(",").map((s) => (exclude ? "!" : "") + s).map(CherryPickRnage.from).filter((r) => r !== null);
if (rangeList.length > 0) {
rangeList.forEach((range2) => {
const newList = onAdd(0, range2);
if (newList === null)
return;
addRangeElements(this.cherryPickElement.firstElementChild, newList, (id) => onRemove(0, id));
});
}
input.value = "";
input.focus();
};
const clearPick = () => {
onClear(0);
addRangeElements(this.cherryPickElement.firstElementChild, [], (id) => onRemove(0, id));
input.value = "";
input.focus();
};
pickBTN.addEventListener("click", () => addCherryPick(false));
excludeBTN.addEventListener("click", () => addCherryPick(true));
clearBTN.addEventListener("click", clearPick);
this.cherryPickElement.querySelectorAll(".download-cherry-pick-follow-btn").forEach((btn) => {
const followBTNClick = () => {
const step = parseInt(btn.getAttribute("data-sibling-step") || "1");
let sibling = btn;
for (let i = 0; i < step; i++) {
sibling = sibling.previousElementSibling;
}
if (step <= 1) {
clearPick();
}
addCherryPick(step > 1, sibling.getAttribute("data-range") || void 0);
};
btn.addEventListener("click", followBTNClick);
});
input.addEventListener("keypress", (event) => event.key === "Enter" && addCherryPick(false));
let lastIndex = 0;
EBUS.subscribe("add-cherry-pick-range", (chapterIndex, index, positive, shiftKey) => {
const range = new CherryPickRnage([index + 1, shiftKey ? (lastIndex ?? index) + 1 : index + 1], positive);
lastIndex = index;
const newList = onAdd(chapterIndex, range);
if (newList === null)
return;
addRangeElements(this.cherryPickElement.firstElementChild, newList, (id) => onRemove(chapterIndex, id));
});
let pad = 0;
EBUS.subscribe("pf-on-appended", (total) => {
pad = total.toString().length;
const rAfter = rangeAfterSpan.getAttribute("data-range").split("-").map((v) => v.padStart(pad, "0")).join("-");
rangeAfterSpan.textContent = rAfter;
rangeAfterSpan.setAttribute("data-range", rAfter);
const rBefore = rangeBeforeSpan.getAttribute("data-range").split("-").map((v, i) => i === 1 ? total.toString() : v.padStart(pad, "0")).join("-");
rangeBeforeSpan.textContent = rBefore;
rangeBeforeSpan.setAttribute("data-range", rBefore);
});
EBUS.subscribe("ifq-do", (index) => {
const rAfter = [1, index + 1].map((v) => v.toString().padStart(pad, "0")).join("-");
rangeAfterSpan.textContent = rAfter;
rangeAfterSpan.setAttribute("data-range", rAfter);
const rBefore = rangeBeforeSpan.getAttribute("data-range").split("-").map((v, i) => i === 0 ? (index + 1).toString().padStart(pad, "0") : v).join("-");
rangeBeforeSpan.textContent = rBefore;
rangeBeforeSpan.setAttribute("data-range", rBefore);
});
}
static html() {
return `
Pick
Exclude
Clear
1-1 pick exclude
1-1 pick exclude
`;
}
}
class ConfigPanel {
panel;
btn;
constructor(root) {
this.panel = q("#config-panel", root);
this.btn = q("#config-panel-btn", root);
this.panel.querySelectorAll(".p-tooltip").forEach((element) => {
const child = element.querySelector(".p-tooltiptext");
if (!child)
return;
element.addEventListener("mouseenter", () => {
relocateElement(child, element, root.offsetWidth, root.offsetHeight);
child.style.visibility = "visible";
});
element.addEventListener("mouseleave", () => child.style.visibility = "hidden");
});
}
initEvents(events) {
ConfigItems.forEach((item) => {
switch (item.typ) {
case "number":
q(`#${item.key}MinusBTN`, this.panel).addEventListener("click", () => events.modNumberConfigEvent(item.key, "minus"));
q(`#${item.key}AddBTN`, this.panel).addEventListener("click", () => events.modNumberConfigEvent(item.key, "add"));
q(`#${item.key}Input`, this.panel).addEventListener("wheel", (event) => {
event.preventDefault();
if (event.deltaY < 0) {
events.modNumberConfigEvent(item.key, "add");
} else if (event.deltaY > 0) {
events.modNumberConfigEvent(item.key, "minus");
}
});
break;
case "boolean":
q(`#${item.key}Checkbox`, this.panel).addEventListener("click", () => events.modBooleanConfigEvent(item.key));
break;
case "select":
q(`#${item.key}Select`, this.panel).addEventListener("change", () => events.modSelectConfigEvent(item.key));
break;
}
});
}
static html() {
const configItemStr = ConfigItems.map(createOption).join("");
return `
${configItemStr}
${i18n.dragToMove.get()}:
✠
`;
}
}
function createOption(item) {
const i18nKey = item.i18nKey || item.key;
const i18nValue = i18n[i18nKey];
const i18nValueTooltip = i18n[`${i18nKey}Tooltip`];
if (!i18nValue) {
throw new Error(`i18n key ${i18nKey} not found`);
}
let display = true;
if (item.displayInSite) {
display = item.displayInSite.test(location.href);
}
let input = "";
switch (item.typ) {
case "boolean":
input = ` `;
break;
case "number":
input = `
-
+ `;
break;
case "select":
if (!item.options) {
throw new Error(`options for ${item.key} not found`);
}
const optionsStr = item.options.map((o) => `${o.display} `).join("");
input = `${optionsStr} `;
break;
}
const [start, end] = item.gridColumnRange ? item.gridColumnRange : [1, 11];
return `${i18nValue.get()} ${i18nValueTooltip ? " ?:" : " :"}${i18nValueTooltip?.get() || ""} ${input}
`;
}
function createHTML() {
const base = document.createElement("div");
base.id = "ehvp-base";
base.setAttribute("tabindex", "0");
base.setAttribute("style", "all: initial");
document.body.after(base);
const HTML_STRINGS = `
${ConfigPanel.html()}
${DownloaderPanel.html()}
${icons.moonViewCeremony}
1 / 0
FIN: 0
${i18n.autoPagePlay.get()}
${i18n.config.get()}
${i18n.download.get()}
${i18n.backChapters.get()}
<
-
${conf.paginationIMGCount}
+
>
${icons.zoomIcon}
-
${conf.imgScale}
+
`;
const shadowRoot = base.attachShadow({ mode: "open" });
const root = document.createElement("div");
root.classList.add("ehvp-root");
root.classList.add("ehvp-root-collapse");
root.innerHTML = HTML_STRINGS;
const style = document.createElement("style");
style.innerHTML = styleCSS();
shadowRoot.append(style);
shadowRoot.append(root);
return {
root,
fullViewGrid: q("#ehvp-nodes-container", root),
bigImageFrame: q("#big-img-frame", root),
pageHelper: q("#p-helper", root),
configPanelBTN: q("#config-panel-btn", root),
downloaderPanelBTN: q("#downloader-panel-btn", root),
entryBTN: q("#entry-btn", root),
currPageElement: q("#p-curr-page", root),
totalPageElement: q("#p-total", root),
finishedElement: q("#p-finished", root),
showGuideElement: q("#show-guide-element", root),
showKeyboardCustomElement: q("#show-keyboard-custom-element", root),
showExcludeURLElement: q("#show-exclude-url-element", root),
showAutoOpenExcludeURLElement: q("#show-autoopen-exclude-url-element", root),
imgLandLeft: q("#img-land-left", root),
imgLandRight: q("#img-land-right", root),
autoPageBTN: q("#auto-page-btn", root),
pageLoading: q("#page-loading", root),
messageBox: q("#message-box", root),
config: new ConfigPanel(root),
downloader: new DownloaderPanel(root),
readModeSelect: q("#read-mode-select", root),
paginationAdjustBar: q("#pagination-adjust-bar", root),
styleSheet: style.sheet
};
}
function addEventListeners(events, HTML, BIFM, DL, PH) {
HTML.config.initEvents(events);
HTML.configPanelBTN.addEventListener("click", () => events.togglePanelEvent("config", void 0, HTML.configPanelBTN));
HTML.downloaderPanelBTN.addEventListener("click", () => {
events.togglePanelEvent("downloader", void 0, HTML.downloaderPanelBTN);
DL.check();
});
function collapsePanel(key) {
const elements = { "config": HTML.config.panel, "downloader": HTML.downloader.panel };
conf.autoCollapsePanel && events.collapsePanelEvent(elements[key], key);
if (BIFM.visible) {
HTML.bigImageFrame.focus();
} else {
HTML.root.focus();
}
}
HTML.config.panel.addEventListener("mouseleave", () => collapsePanel("config"));
HTML.config.panel.addEventListener("blur", () => collapsePanel("config"));
HTML.downloader.panel.addEventListener("mouseleave", () => collapsePanel("downloader"));
HTML.downloader.panel.addEventListener("blur", () => collapsePanel("downloader"));
let hovering = false;
HTML.pageHelper.addEventListener("mouseover", () => {
hovering = true;
events.abortMouseleavePanelEvent();
PH.minify(PH.lastStage, true);
});
HTML.pageHelper.addEventListener("mouseleave", () => {
hovering = false;
["config", "downloader"].forEach((k) => collapsePanel(k));
setTimeout(() => !hovering && PH.minify(PH.lastStage, false), 700);
});
HTML.entryBTN.addEventListener("click", () => {
let stage = HTML.entryBTN.getAttribute("data-stage") || "exit";
stage = stage === "open" ? "exit" : "open";
HTML.entryBTN.setAttribute("data-stage", stage);
EBUS.emit("toggle-main-view", stage === "open");
});
HTML.currPageElement.addEventListener("wheel", (event) => BIFM.stepNext(event.deltaY > 0 ? "next" : "prev", event.deltaY > 0 ? -1 : 1, parseInt(HTML.currPageElement.textContent) - 1));
document.addEventListener("keydown", (event) => events.keyboardEvent(event));
HTML.fullViewGrid.addEventListener("keydown", (event) => {
events.fullViewGridKeyBoardEvent(event);
event.stopPropagation();
});
HTML.bigImageFrame.addEventListener("keydown", (event) => {
events.bigImageFrameKeyBoardEvent(event);
event.stopPropagation();
});
HTML.imgLandLeft.addEventListener("click", (event) => {
BIFM.stepNext(conf.reversePages ? "next" : "prev");
event.stopPropagation();
});
HTML.imgLandRight.addEventListener("click", (event) => {
BIFM.stepNext(conf.reversePages ? "prev" : "next");
event.stopPropagation();
});
HTML.showGuideElement.addEventListener("click", events.showGuideEvent);
HTML.showKeyboardCustomElement.addEventListener("click", events.showKeyboardCustomEvent);
HTML.showExcludeURLElement.addEventListener("click", events.showExcludeURLEvent);
HTML.showAutoOpenExcludeURLElement.addEventListener("click", events.showAutoOpenExcludeURLEvent);
dragElement(HTML.pageHelper, {
onFinish: () => {
conf.pageHelperAbTop = HTML.pageHelper.style.top;
conf.pageHelperAbLeft = HTML.pageHelper.style.left;
conf.pageHelperAbBottom = HTML.pageHelper.style.bottom;
conf.pageHelperAbRight = HTML.pageHelper.style.right;
saveConf(conf);
},
onMoving: (pos) => {
HTML.pageHelper.style.top = pos.top === void 0 ? "unset" : `${pos.top}px`;
HTML.pageHelper.style.bottom = pos.bottom === void 0 ? "unset" : `${pos.bottom}px`;
HTML.pageHelper.style.left = pos.left === void 0 ? "unset" : `${pos.left}px`;
HTML.pageHelper.style.right = pos.right === void 0 ? "unset" : `${pos.right}px`;
const rule = queryRule(HTML.styleSheet, ".b-main");
if (rule)
rule.style.flexDirection = pos.left === void 0 ? "row-reverse" : "row";
}
}, q("#dragHub", HTML.pageHelper));
HTML.readModeSelect.addEventListener("click", (event) => {
const value = event.target.getAttribute("data-value");
if (value) {
events.changeReadModeEvent(value);
PH.minify(PH.lastStage);
}
});
q("#paginationStepPrev", HTML.pageHelper).addEventListener("click", () => BIFM.stepNext(conf.reversePages ? "next" : "prev", conf.reversePages ? -1 : 1));
q("#paginationStepNext", HTML.pageHelper).addEventListener("click", () => BIFM.stepNext(conf.reversePages ? "prev" : "next", conf.reversePages ? 1 : -1));
q("#paginationMinusBTN", HTML.pageHelper).addEventListener("click", () => events.modNumberConfigEvent("paginationIMGCount", "minus"));
q("#paginationAddBTN", HTML.pageHelper).addEventListener("click", () => events.modNumberConfigEvent("paginationIMGCount", "add"));
q("#paginationInput", HTML.pageHelper).addEventListener("wheel", (event) => events.modNumberConfigEvent("paginationIMGCount", event.deltaY < 0 ? "add" : "minus"));
q("#scaleInput", HTML.pageHelper).addEventListener("mousedown", (event) => {
const element = event.target;
const scale = conf.imgScale || (conf.readMode === "pagination" ? 100 : 80);
dragElementWithLine(event, element, { y: true }, (data) => {
const fix = (data.direction & 3) === 1 ? 1 : -1;
BIFM.scaleBigImages(1, 0, Math.floor(scale + data.distance * 0.6 * fix));
element.textContent = conf.imgScale.toString();
});
});
q("#scaleMinusBTN", HTML.pageHelper).addEventListener("click", () => BIFM.scaleBigImages(-1, 10));
q("#scaleAddBTN", HTML.pageHelper).addEventListener("click", () => BIFM.scaleBigImages(1, 10));
q("#scaleInput", HTML.pageHelper).addEventListener("wheel", (event) => BIFM.scaleBigImages(event.deltaY > 0 ? -1 : 1, 5));
}
function showMessage(box, level, message, duration) {
const element = document.createElement("div");
element.classList.add("ehvp-message");
element.innerHTML = `${message} X
`;
box.appendChild(element);
element.querySelector("button")?.addEventListener("click", () => element.remove());
const durationBar = element.querySelector("div.ehvp-message-duration-bar");
if (duration) {
durationBar.style.animation = `${duration}ms linear main-progress`;
durationBar.addEventListener("animationend", () => element.remove());
}
}
class PageHelper {
html;
chapterIndex = -1;
lastChapterIndex = 0;
pageNumInChapter = {};
lastStage = "exit";
chapters;
constructor(html, chapters) {
this.html = html;
this.chapters = chapters;
EBUS.subscribe("pf-change-chapter", (index) => {
let current = 0;
if (index === -1) {
current = this.lastChapterIndex;
} else {
this.lastChapterIndex = index;
current = this.pageNumInChapter[index] || 0;
}
this.chapterIndex = index;
const [total, finished] = (() => {
const queue = this.chapters()[index]?.queue;
if (!queue)
return [0, 0];
const finished2 = queue.filter((imf) => imf.stage === FetchState.DONE).length;
return [queue.length, finished2];
})();
this.setPageState({ finished: finished.toString(), total: total.toString(), current: (current + 1).toString() });
this.minify(this.lastStage);
});
EBUS.subscribe("bifm-on-show", () => this.minify("bigImageFrame"));
EBUS.subscribe("bifm-on-hidden", () => this.minify("fullViewGrid"));
EBUS.subscribe("ifq-do", (index, imf) => {
if (imf.chapterIndex !== this.chapterIndex)
return;
const queue = this.chapters()[this.chapterIndex]?.queue;
if (!queue)
return;
this.pageNumInChapter[this.chapterIndex] = index;
this.setPageState({ current: (index + 1).toString() });
});
EBUS.subscribe("ifq-on-finished-report", (index, queue) => {
if (queue.chapterIndex !== this.chapterIndex)
return;
this.setPageState({ finished: queue.finishedIndex.size.toString() });
evLog("info", `No.${index + 1} Finished,Current index at No.${queue.currIndex + 1}`);
});
EBUS.subscribe("pf-on-appended", (total, _ifs, chapterIndex, done) => {
if (this.chapterIndex > -1 && chapterIndex !== this.chapterIndex)
return;
this.setPageState({ total: `${total}${done ? "" : ".."}` });
});
html.currPageElement.addEventListener("click", (event) => {
const ele = event.target;
const index = parseInt(ele.textContent || "1") - 1;
if (this.chapterIndex === -1) {
this.chapters()[this.lastChapterIndex]?.onclick?.(this.lastChapterIndex);
} else {
const queue = this.chapters()[this.chapterIndex]?.queue;
if (!queue || !queue[index])
return;
EBUS.emit("imf-on-click", queue[index]);
}
});
const chaptersSelectionElement = q("#chapters-btn", this.html.pageHelper);
chaptersSelectionElement.addEventListener("click", () => EBUS.emit("back-chapters-selection"));
}
setPageState({ total, current, finished }) {
if (total !== void 0) {
this.html.totalPageElement.textContent = total;
}
if (current !== void 0) {
this.html.currPageElement.textContent = current;
}
if (finished !== void 0) {
this.html.finishedElement.textContent = finished;
}
}
// const arr = ["entry-btn", "auto-page-btn", "page-status", "fin-status", "chapters-btn", "config-panel-btn", "downloader-panel-btn", "scale-bar", "read-mode-bar", "pagination-adjust-bar"];
minify(stage, hover = false) {
this.lastStage = stage;
let level = [0, 0];
if (stage === "exit") {
level = [0, 0];
} else {
switch (stage) {
case "fullViewGrid":
if (conf.minifyPageHelper === "never" || conf.minifyPageHelper === "inBigMode") {
level = [1, 1];
} else {
level = hover ? [1, 1] : [3, 1];
}
break;
case "bigImageFrame":
if (conf.minifyPageHelper === "never") {
level = [2, 2];
} else {
level = hover ? [2, 2] : [3, 2];
}
break;
}
}
function getPick(lvl) {
switch (lvl) {
case 0:
return ["entry-btn"];
case 1:
return ["page-status", "fin-status", "auto-page-btn", "config-panel-btn", "downloader-panel-btn", "chapters-btn", "entry-btn"];
case 2:
return ["page-status", "fin-status", "auto-page-btn", "config-panel-btn", "downloader-panel-btn", "entry-btn", "read-mode-bar", "pagination-adjust-bar", "scale-bar"];
case 3:
return ["page-status", "auto-page-btn"];
}
return [];
}
const filter = (id) => {
if (id === "chapters-btn")
return this.chapterIndex > -1 && this.chapters().length > 1;
if (id === "auto-page-btn" && level[0] === 3)
return this.html.pageHelper.querySelector("#auto-page-btn")?.getAttribute("data-status") === "playing";
if (id === "pagination-adjust-bar")
return conf.readMode === "pagination";
return true;
};
const pick = getPick(level[0]).filter(filter);
const notHidden = getPick(level[1]).filter(filter);
const items = Array.from(this.html.pageHelper.querySelectorAll(".b-main > .b-main-item"));
for (const item of items) {
const index = pick.indexOf(item.id);
item.style.order = index === -1 ? "99" : index.toString();
item.style.opacity = index === -1 ? "0" : "1";
item.hidden = !notHidden.includes(item.id);
}
this.html.pageHelper.querySelector("#entry-btn").textContent = stage === "exit" ? icons.moonViewCeremony : i18n.collapse.get();
}
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function onMouse(ele, callback, signal) {
ele.addEventListener("mousedown", (event) => {
const { left } = ele.getBoundingClientRect();
const mouseMove = (event2) => {
const xInProgress = event2.clientX - left;
const percent = Math.round(xInProgress / ele.clientWidth * 100);
callback(percent);
};
mouseMove(event);
ele.addEventListener("mousemove", mouseMove);
ele.addEventListener("mouseup", () => {
ele.removeEventListener("mousemove", mouseMove);
}, { once: true });
ele.addEventListener("mouseleave", () => {
ele.removeEventListener("mousemove", mouseMove);
}, { once: true });
}, { signal });
}
const PLAY_ICON = ` `;
const PAUSE_ICON = ` `;
const VOLUME_ICON = ` `;
const MUTED_ICON = ` `;
class VideoControl {
ui;
paused = false;
abortController;
root;
constructor(root) {
this.root = root;
this.ui = this.create(this.root);
this.flushUI();
}
show() {
this.ui.root.hidden = false;
}
hidden() {
this.ui.root.hidden = true;
}
create(root) {
const ui = document.createElement("div");
ui.classList.add("bifm-vid-ctl");
ui.innerHTML = `
${PLAY_ICON}
${MUTED_ICON}
00:00
/
10:00
`;
root.appendChild(ui);
return {
root: ui,
playBTN: q("#bifm-vid-ctl-play", ui),
volumeBTN: q("#bifm-vid-ctl-mute", ui),
volumeProgress: q("#bifm-vid-ctl-volume", ui),
progress: q("#bifm-vid-ctl-pg", ui),
time: q("#bifm-vid-ctl-time", ui),
duration: q("#bifm-vid-ctl-duration", ui)
};
}
flushUI(state, onlyState) {
let { value, max } = state ? { value: state.time, max: state.duration } : { value: 0, max: 10 };
const percent = value / max * 100;
this.ui.progress.firstElementChild.style.width = `${percent}%`;
this.ui.time.textContent = secondsToTime(value);
this.ui.duration.textContent = secondsToTime(max);
if (onlyState)
return;
this.ui.playBTN.innerHTML = this.paused ? PLAY_ICON : PAUSE_ICON;
this.ui.volumeBTN.innerHTML = conf.muted ? MUTED_ICON : VOLUME_ICON;
this.ui.volumeProgress.firstElementChild.style.width = `${conf.volume || 30}%`;
}
attach(element) {
this.detach();
this.show();
this.abortController = new AbortController();
const state = { time: element.currentTime, duration: element.duration };
this.flushUI(state);
element.addEventListener("timeupdate", (event) => {
const ele = event.target;
if (!state)
return;
state.time = ele.currentTime;
this.flushUI(state, true);
}, { signal: this.abortController.signal });
element.onwaiting = () => evLog("debug", "onwaiting");
element.loop = true;
element.muted = conf.muted || false;
element.volume = (conf.volume || 30) / 100;
if (!this.paused) {
element.play();
}
let elementID = element.id;
if (!elementID) {
elementID = "vid-" + Math.random().toString(36).slice(2);
element.id = elementID;
}
this.ui.playBTN.addEventListener("click", () => {
const vid = this.root.querySelector(`#${elementID}`);
if (vid) {
this.paused = !this.paused;
if (this.paused) {
vid.pause();
} else {
vid.play();
}
this.flushUI(state);
}
}, { signal: this.abortController.signal });
this.ui.volumeBTN.addEventListener("click", () => {
const vid = this.root.querySelector(`#${elementID}`);
if (vid) {
conf.muted = !conf.muted;
vid.muted = conf.muted;
saveConf(conf);
this.flushUI(state);
}
}, { signal: this.abortController.signal });
onMouse(this.ui.progress, (percent) => {
const vid = this.root.querySelector(`#${elementID}`);
if (vid) {
vid.currentTime = vid.duration * (percent / 100);
state.time = vid.currentTime;
this.flushUI(state);
}
}, this.abortController.signal);
onMouse(this.ui.volumeProgress, (percent) => {
const vid = this.root.querySelector(`#${elementID}`);
if (vid) {
conf.volume = percent;
saveConf(conf);
vid.volume = conf.volume / 100;
this.flushUI(state);
}
}, this.abortController.signal);
}
detach() {
this.abortController?.abort();
this.abortController = void 0;
this.flushUI();
}
}
function secondsToTime(seconds) {
const min = Math.floor(seconds / 60).toString().padStart(2, "0");
const sec = Math.floor(seconds % 60).toString().padStart(2, "0");
return `${min}:${sec}`;
}
class BigImageFrameManager {
frame;
lockInit;
lastMouse;
fragment;
// image decode will take a while, so cache it to fragment
elements = { next: [], curr: [], prev: [] };
debouncer;
throttler;
callbackOnWheel;
hammer;
preventStep = { currentPreventFinished: false };
visible = false;
html;
frameScrollAbort;
vidController;
chapterIndex = 0;
getChapter;
loadingHelper;
currLoadingState = /* @__PURE__ */ new Map();
constructor(HTML, getChapter) {
this.html = HTML;
this.frame = HTML.bigImageFrame;
this.fragment = new DocumentFragment();
this.debouncer = new Debouncer();
this.throttler = new Debouncer("throttle");
this.lockInit = false;
this.getChapter = getChapter;
this.resetStickyMouse();
this.initFrame();
this.initImgScaleStyle();
this.initHammer();
EBUS.subscribe("pf-change-chapter", (index) => this.chapterIndex = Math.max(0, index));
EBUS.subscribe("imf-on-click", (imf) => this.show(imf));
EBUS.subscribe("imf-on-finished", (index, success, imf) => {
if (imf.chapterIndex !== this.chapterIndex)
return;
this.currLoadingState.delete(index);
if (!this.visible || !success)
return;
const elements = [
...this.elements.curr.map((e, i) => ({ img: e, eleIndex: i, key: "curr" })),
...this.elements.prev.map((e, i) => ({ img: e, eleIndex: i, key: "prev" })),
...this.elements.next.map((e, i) => ({ img: e, eleIndex: i, key: "next" })),
...this.getMediaNodes().map((e, i) => ({ img: e, eleIndex: i, key: "" }))
];
const ret = elements.find((o) => index === parseIndex(o.img));
if (!ret)
return;
let { img, eleIndex, key } = ret;
if (imf.contentType?.startsWith("video")) {
const vid = this.newMediaNode(index, imf);
if (["curr", "prev", "next"].includes(key)) {
this.elements[key][eleIndex] = vid;
}
img.replaceWith(vid);
img.remove();
return;
}
img.setAttribute("src", imf.blobUrl);
this.debouncer.addEvent("FLUSH-LOADING-HELPER", () => this.flushLoadingHelper(), 20);
});
this.loadingHelper = document.createElement("span");
this.loadingHelper.id = "bifm-loading-helper";
this.loadingHelper.style.position = "absolute";
this.loadingHelper.style.zIndex = "3000";
this.loadingHelper.style.display = "none";
this.loadingHelper.style.padding = "0px 3px";
this.loadingHelper.style.backgroundColor = "#ffffff90";
this.loadingHelper.style.fontWeight = "bold";
this.loadingHelper.style.left = "0px";
this.frame.append(this.loadingHelper);
EBUS.subscribe("imf-download-state-change", (imf) => {
if (imf.chapterIndex !== this.chapterIndex)
return;
const element = this.elements.curr.find((e) => e.getAttribute("d-random-id") === imf.randomID);
if (!element)
return;
const index = parseIndex(element);
this.currLoadingState.set(index, Math.floor(imf.downloadState.loaded / imf.downloadState.total * 100));
this.debouncer.addEvent("FLUSH-LOADING-HELPER", () => this.flushLoadingHelper(), 20);
});
new AutoPage(this, HTML.autoPageBTN);
}
initHammer() {
this.hammer = new Hammer(this.frame, {
// touchAction: "auto",
recognizers: [
[Hammer.Swipe, { direction: Hammer.DIRECTION_ALL, enable: false }]
]
});
this.hammer.on("swipe", (ev) => {
ev.preventDefault();
if (conf.readMode === "pagination") {
switch (ev.direction) {
case Hammer.DIRECTION_LEFT:
this.stepNext(conf.reversePages ? "prev" : "next");
break;
case Hammer.DIRECTION_UP:
this.stepNext("next");
break;
case Hammer.DIRECTION_RIGHT:
this.stepNext(conf.reversePages ? "next" : "prev");
break;
case Hammer.DIRECTION_DOWN:
this.stepNext("prev");
break;
}
}
});
}
resetStickyMouse() {
this.lastMouse = void 0;
}
initFrame() {
this.frame.addEventListener("wheel", (event) => this.onWheel(event, true));
this.frame.addEventListener("click", (event) => this.hidden(event));
this.frame.addEventListener("contextmenu", (event) => event.preventDefault());
const debouncer = new Debouncer("throttle");
this.frame.addEventListener("mousemove", (event) => {
debouncer.addEvent("BIG-IMG-MOUSE-MOVE", () => {
if (this.lastMouse)
this.stickyMouse(event, this.lastMouse);
this.lastMouse = { x: event.clientX, y: event.clientY };
}, 5);
});
}
hidden(event) {
if (event && event.target && event.target.tagName === "SPAN")
return;
this.visible = false;
EBUS.emit("bifm-on-hidden");
this.html.fullViewGrid.focus();
this.frameScrollAbort?.abort();
this.frame.classList.add("big-img-frame-collapse");
this.debouncer.addEvent("TOGGLE-CHILDREN", () => this.resetElements(), 200);
}
show(imf) {
this.visible = true;
this.frame.classList.remove("big-img-frame-collapse");
this.frame.focus();
this.frameScrollAbort = new AbortController();
this.frame.addEventListener("scroll", () => this.onScroll(), { signal: this.frameScrollAbort.signal });
this.debouncer.addEvent("TOGGLE-CHILDREN-D", () => imf.chapterIndex === this.chapterIndex && this.setNow(imf), 100);
EBUS.emit("bifm-on-show");
}
setNow(imf, oriented) {
if (this.visible) {
this.resetStickyMouse();
this.initElements(imf, oriented);
} else {
const queue = this.getChapter(this.chapterIndex).queue;
const index = queue.indexOf(imf);
if (index === -1)
return;
EBUS.emit("ifq-do", index, imf, oriented || "next");
}
this.currLoadingState.clear();
this.flushLoadingHelper();
}
initElements(imf, oriented = "next") {
this.resetPreventStep();
const queue = this.getChapter(this.chapterIndex).queue;
const index = queue.indexOf(imf);
if (index === -1)
return;
if (conf.readMode === "continuous") {
this.resetElements();
this.elements.curr[0] = this.newMediaNode(index, imf);
this.frame.appendChild(this.elements.curr[0]);
this.tryExtend();
this.hammer?.get("swipe").set({ enable: false });
} else {
this.balanceElements(index, queue, oriented);
this.placeElements();
this.checkFrameOverflow();
this.hammer?.get("swipe").set({ enable: true });
}
EBUS.emit("ifq-do", index, imf, oriented);
this.elements.curr[0]?.scrollIntoView();
}
placeElements() {
this.removeMediaNode();
this.elements.curr.forEach((element) => this.frame.appendChild(element));
this.elements.prev.forEach((element) => this.fragment.appendChild(element));
this.elements.next.forEach((element) => this.fragment.appendChild(element));
const vid = this.elements.curr[0];
if (vid && vid instanceof HTMLVideoElement) {
if (vid.paused)
this.tryPlayVideo(vid);
}
}
balanceElements(index, queue, oriented) {
const indices = { prev: [], curr: [], next: [] };
for (let i = 0; i < conf.paginationIMGCount; i++) {
const prevIndex = i + index - conf.paginationIMGCount;
const currIndex = i + index;
const nextIndex = i + index + conf.paginationIMGCount;
if (prevIndex > -1)
indices.prev.push(prevIndex);
if (currIndex > -1 && currIndex < queue.length)
indices.curr.push(currIndex);
if (nextIndex < queue.length)
indices.next.push(nextIndex);
}
if (oriented === "next") {
this.elements.prev = this.elements.curr;
this.elements.curr = this.elements.next;
this.elements.next = [];
} else {
this.elements.next = this.elements.curr;
this.elements.curr = this.elements.prev;
this.elements.prev = [];
}
Object.entries(indices).forEach(([k, indexRange]) => {
const elements = this.elements[k];
if (elements.length > indexRange.length) {
elements.splice(indexRange.length, elements.length - indexRange.length).forEach((ele) => ele.remove());
}
for (let j = 0; j < indexRange.length; j++) {
if (indexRange[j] === parseIndex(elements[j]))
continue;
if (elements[j])
elements[j].remove();
elements[j] = this.newMediaNode(indexRange[j], queue[indexRange[j]]);
}
});
}
resetElements() {
this.elements = { prev: [], curr: [], next: [] };
this.fragment.childNodes.forEach((child) => child.remove());
this.removeMediaNode();
}
removeMediaNode() {
this.vidController?.detach();
this.vidController?.hidden();
this.getMediaNodes().forEach((ele) => {
if (ele instanceof HTMLVideoElement) {
ele.pause();
}
ele.remove();
});
}
getMediaNodes() {
const list = Array.from(this.frame.querySelectorAll("img, video"));
let last = 0;
for (const ele of list) {
const index = parseIndex(ele);
if (index < last) {
throw new Error("BIFM: getMediaNodes: list is not ordered by d-index");
}
last = index;
}
return list;
}
stepNext(oriented, fixStep = 0, current) {
let index = current !== void 0 ? current : this.elements.curr[0] ? parseInt(this.elements.curr[0].getAttribute("d-index")) : void 0;
if (index === void 0 || isNaN(index))
return;
const queue = this.getChapter(this.chapterIndex)?.queue;
if (!queue || queue.length === 0)
return;
index = oriented === "next" ? index + conf.paginationIMGCount : index - conf.paginationIMGCount;
if (conf.paginationIMGCount > 1) {
index += fixStep;
}
if (index < -conf.paginationIMGCount) {
index = queue.length - 1;
} else {
index = Math.max(0, index);
}
if (!queue[index])
return;
this.setNow(queue[index], oriented);
}
// isMouse: onWheel triggered by mousewheel, if not, means by keyboard control
onWheel(event, isMouse, preventCallback) {
if (!preventCallback)
this.callbackOnWheel?.(event);
if (event.buttons === 2) {
event.preventDefault();
this.scaleBigImages(event.deltaY > 0 ? -1 : 1, 5);
return;
}
if (conf.readMode === "continuous")
return;
const oriented = event.deltaY > 0 ? "next" : "prev";
if (conf.stickyMouse === "disable") {
if (!this.isReachedBoundary(oriented))
return;
if (isMouse && this.tryPreventStep())
return;
}
event.preventDefault();
this.stepNext(oriented);
}
onScroll() {
if (conf.readMode === "continuous") {
this.consecutive();
}
}
resetPreventStep(fin) {
this.preventStep.ani?.cancel();
this.preventStep.ele?.remove();
this.preventStep = { currentPreventFinished: fin ?? false };
}
// prevent scroll to next page while mouse scrolling;
tryPreventStep() {
if (!conf.imgScale || conf.imgScale === 100 || conf.preventScrollPageTime === 0) {
return false;
}
if (this.preventStep.currentPreventFinished) {
this.resetPreventStep();
return false;
} else {
if (!this.preventStep.ele) {
const lockEle = document.createElement("div");
lockEle.style.width = "100vw";
lockEle.style.position = "fixed";
lockEle.style.display = "flex";
lockEle.style.justifyContent = "center";
lockEle.style.bottom = "0px";
lockEle.innerHTML = `
`;
this.frame.appendChild(lockEle);
this.preventStep.ele = lockEle;
if (conf.preventScrollPageTime > 0) {
const ani = lockEle.children[0].animate([{ width: "30vw" }, { width: "0vw" }], { duration: conf.preventScrollPageTime });
ani.onfinish = () => this.preventStep.ele && this.resetPreventStep(true);
this.preventStep.ani = ani;
}
this.preventStep.currentPreventFinished = false;
}
return true;
}
}
isReachedBoundary(oriented) {
if (oriented === "prev") {
return this.frame.scrollTop <= 0;
}
if (oriented === "next") {
return this.frame.scrollTop >= this.frame.scrollHeight - this.frame.offsetHeight;
}
return false;
}
consecutive() {
this.throttler.addEvent("SCROLL", () => {
this.debouncer.addEvent("REDUCE", () => {
if (!this.elements.curr[0])
return;
const distance2 = this.getRealOffsetTop(this.elements.curr[0]) - this.frame.scrollTop;
if (this.tryReduce()) {
this.restoreScrollTop(this.elements.curr[0], distance2);
}
}, 500);
let mediaNodes = this.getMediaNodes();
let index = this.findMediaNodeIndexOnCenter(mediaNodes);
const centerNode = mediaNodes[index];
if (this.elements.curr[0] !== centerNode) {
const oldIndex = parseIndex(this.elements.curr[0]);
const newIndex = parseIndex(centerNode);
const oriented = oldIndex < newIndex ? "next" : "prev";
const queue = this.getChapter(this.chapterIndex).queue;
if (queue.length === 0 || newIndex < 0 || newIndex > queue.length - 1)
return;
const imf = queue[newIndex];
EBUS.emit("ifq-do", newIndex, imf, oriented);
if (this.elements.curr[0] instanceof HTMLVideoElement) {
this.elements.curr[0].pause();
}
this.tryPlayVideo(centerNode);
}
this.elements.curr[0] = centerNode;
const distance = this.getRealOffsetTop(this.elements.curr[0]) - this.frame.scrollTop;
if (this.tryExtend() > 0) {
this.restoreScrollTop(this.elements.curr[0], distance);
}
}, 60);
}
restoreScrollTop(imgNode, distance) {
this.frame.scrollTop = this.getRealOffsetTop(imgNode) - distance;
}
getRealOffsetTop(imgNode) {
return imgNode.offsetTop;
}
tryExtend() {
let indexOffset = 0;
let mediaNodes = [];
let scrollTopFix = 0;
while (true) {
mediaNodes = this.getMediaNodes();
const frist = mediaNodes[0];
if (frist.offsetTop + frist.offsetHeight > this.frame.scrollTop + scrollTopFix) {
const extended = this.extendImgNode(frist, "prev");
if (extended === null) {
break;
} else {
scrollTopFix += extended.offsetHeight;
}
indexOffset++;
} else {
break;
}
}
while (true) {
mediaNodes = this.getMediaNodes();
const last = mediaNodes[mediaNodes.length - 1];
if (last.offsetTop < this.frame.scrollTop + this.frame.offsetHeight) {
if (this.extendImgNode(last, "next") === null)
break;
} else {
break;
}
}
return indexOffset;
}
tryReduce() {
const imgNodes = this.getMediaNodes();
const shouldRemoveNodes = [];
let oriented = "prev";
for (const imgNode of imgNodes) {
if (oriented === "prev") {
if (imgNode.offsetTop + imgNode.offsetHeight < this.frame.scrollTop) {
shouldRemoveNodes.push(imgNode);
} else {
oriented = "next";
shouldRemoveNodes.pop();
}
} else if (oriented === "next") {
if (imgNode.offsetTop > this.frame.scrollTop + this.frame.offsetHeight) {
oriented = "remove";
}
} else {
shouldRemoveNodes.push(imgNode);
}
}
if (shouldRemoveNodes.length === 0)
return false;
for (const imgNode of shouldRemoveNodes) {
imgNode.remove();
}
return true;
}
extendImgNode(mediaNode, oriented) {
let extendedNode;
const index = parseIndex(mediaNode);
if (index === -1) {
throw new Error("BIFM: extendImgNode: media node index is NaN");
}
const queue = this.getChapter(this.chapterIndex).queue;
if (queue.length === 0)
return null;
if (oriented === "prev") {
if (index === 0)
return null;
extendedNode = this.newMediaNode(index - 1, queue[index - 1]);
mediaNode.before(extendedNode);
} else {
if (index === queue.length - 1)
return null;
extendedNode = this.newMediaNode(index + 1, queue[index + 1]);
mediaNode.after(extendedNode);
}
return extendedNode;
}
newMediaNode(index, imf) {
if (!imf)
throw new Error("BIFM: newMediaNode: img fetcher is null");
if (imf.contentType?.startsWith("video")) {
const vid = document.createElement("video");
vid.classList.add("bifm-img");
vid.classList.add("bifm-vid");
vid.setAttribute("d-index", index.toString());
vid.setAttribute("d-random-id", imf.randomID);
vid.onloadeddata = () => {
if (this.visible && vid === this.elements.curr[0]) {
this.tryPlayVideo(vid);
}
};
vid.src = imf.blobUrl;
return vid;
} else {
const img = document.createElement("img");
img.decoding = "sync";
img.classList.add("bifm-img");
img.setAttribute("d-index", index.toString());
img.setAttribute("d-random-id", imf.randomID);
if (imf.stage === FetchState.DONE) {
img.src = imf.blobUrl;
} else {
img.src = imf.node.src;
}
return img;
}
}
tryPlayVideo(vid) {
if (vid instanceof HTMLVideoElement) {
if (!this.vidController) {
this.vidController = new VideoControl(this.html.root);
}
this.vidController.attach(vid);
} else {
this.vidController?.hidden();
}
}
/**
* @param fix: 1 or -1, means scale up or down
* @param rate: step of scale, eg: current scale is 80, rate is 10, then new scale is 90
* @param _percent: directly set width percent
*/
scaleBigImages(fix, rate, _percent) {
const rule = queryRule(this.html.styleSheet, ".bifm-img");
if (!rule)
return 0;
let percent = _percent || parseInt(conf.readMode === "pagination" ? rule.style.height : rule.style.width);
if (isNaN(percent))
percent = 100;
percent = percent + rate * fix;
switch (conf.readMode) {
case "pagination":
percent = Math.max(percent, 100);
percent = Math.min(percent, 300);
rule.style.height = `${percent}vh`;
break;
case "continuous":
percent = Math.max(percent, 20);
percent = Math.min(percent, 100);
rule.style.width = `${percent}vw`;
break;
}
if (conf.readMode === "pagination") {
this.checkFrameOverflow();
rule.style.minWidth = percent > 100 ? "" : "100vw";
if (percent === 100)
this.resetScaleBigImages(false);
}
conf.imgScale = percent;
saveConf(conf);
q("#scaleInput", this.html.pageHelper).textContent = `${conf.imgScale}`;
return percent;
}
checkFrameOverflow() {
const flexRule = queryRule(this.html.styleSheet, ".bifm-flex");
if (flexRule) {
if (this.frame.offsetWidth < this.frame.scrollWidth) {
flexRule.style.justifyContent = "flex-start";
} else {
flexRule.style.justifyContent = "center";
}
}
}
resetScaleBigImages(syncConf) {
const rule = queryRule(this.html.styleSheet, ".bifm-img");
if (!rule)
return;
rule.style.minWidth = "";
rule.style.minHeight = "";
rule.style.maxWidth = "";
rule.style.maxHeight = "";
rule.style.height = "";
rule.style.width = "";
rule.style.margin = "";
if (conf.readMode === "pagination") {
rule.style.height = "100vh";
rule.style.margin = "0";
if (conf.paginationIMGCount === 1)
rule.style.minWidth = "100vw";
} else {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(navigator.userAgent);
rule.style.maxWidth = "100vw";
rule.style.width = isMobile ? "100vw" : "80vw";
rule.style.margin = "0 auto";
}
if (syncConf) {
conf.imgScale = conf.readMode === "pagination" ? 100 : 80;
saveConf(conf);
q("#scaleInput", this.html.pageHelper).textContent = `${conf.imgScale}`;
}
}
initImgScaleStyle() {
this.resetScaleBigImages(false);
if (conf.imgScale && conf.imgScale > 0) {
this.scaleBigImages(1, 0, conf.imgScale);
}
}
stickyMouse(event, lastMouse) {
if (conf.readMode !== "pagination" || conf.stickyMouse === "disable")
return;
let [distanceY, distanceX] = [event.clientY - lastMouse.y, event.clientX - lastMouse.x];
if (conf.stickyMouse === "enable")
[distanceY, distanceX] = [-distanceY, -distanceX];
const overflowY = this.frame.scrollHeight - this.frame.offsetHeight;
if (overflowY > 0) {
const rateY = overflowY / (this.frame.offsetHeight / 4) * 3;
let scrollTop = this.frame.scrollTop + distanceY * rateY;
scrollTop = Math.max(scrollTop, 0);
scrollTop = Math.min(scrollTop, overflowY);
this.frame.scrollTop = scrollTop;
}
const overflowX = this.frame.scrollWidth - this.frame.offsetWidth;
if (overflowX > 0) {
const rateX = overflowX / (this.frame.offsetWidth / 4) * 3;
let scrollLeft = this.frame.scrollLeft + distanceX * rateX;
if (conf.reversePages) {
scrollLeft = Math.min(scrollLeft, 0);
scrollLeft = Math.max(scrollLeft, -overflowX);
} else {
scrollLeft = Math.max(scrollLeft, 0);
scrollLeft = Math.min(scrollLeft, overflowX);
}
this.frame.scrollLeft = scrollLeft;
}
}
findMediaNodeIndexOnCenter(imgNodes) {
const centerLine = this.frame.offsetHeight / 2;
for (let i = 0; i < imgNodes.length; i++) {
const imgNode = imgNodes[i];
const realOffsetTop = imgNode.offsetTop - this.frame.scrollTop;
if (realOffsetTop < centerLine && realOffsetTop + imgNode.offsetHeight >= centerLine) {
return i;
}
}
return 0;
}
flushLoadingHelper() {
if (this.currLoadingState.size === 0) {
this.loadingHelper.style.display = "none";
} else {
if (this.loadingHelper.style.display === "none") {
this.loadingHelper.style.display = "inline-block";
}
const ret = Array.from(this.currLoadingState).map(([k, v]) => `[P-${k + 1}: ${v}%]`);
if (conf.reversePages)
ret.reverse();
this.loadingHelper.textContent = `Loading ${ret.join(",")}`;
}
}
}
class AutoPage {
bifm;
status;
button;
lockVer;
scroller;
constructor(BIFM, root) {
this.bifm = BIFM;
this.scroller = new Scroller(this.bifm.frame);
this.status = "stop";
this.button = root;
this.lockVer = 0;
this.bifm.callbackOnWheel = () => {
if (this.status === "running") {
this.stop();
this.start(this.lockVer);
}
};
EBUS.subscribe("bifm-on-hidden", () => this.stop());
EBUS.subscribe("bifm-on-show", () => conf.autoPlay && this.start(this.lockVer));
this.initPlayButton();
}
initPlayButton() {
this.button.addEventListener("click", () => {
if (this.status === "stop") {
this.start(this.lockVer);
} else {
this.stop();
}
});
}
async start(lockVer) {
this.status = "running";
this.button.setAttribute("data-status", "playing");
this.button.firstElementChild.innerText = i18n.autoPagePause.get();
const frame = this.bifm.frame;
if (!this.bifm.visible) {
const queue = this.bifm.getChapter(this.bifm.chapterIndex).queue;
if (queue.length === 0)
return;
const index = Math.max(parseIndex(this.bifm.elements.curr[0]), 0);
this.bifm.show(queue[index]);
}
const progress = q("#auto-page-progress", this.button);
const interval = () => conf.readMode === "pagination" ? conf.autoPageSpeed : 1;
while (true) {
await sleep(10);
progress.style.animation = `${interval() * 1e3}ms linear main-progress`;
await sleep(interval() * 1e3);
if (this.lockVer !== lockVer) {
return;
}
progress.style.animation = ``;
if (this.status !== "running") {
break;
}
if (this.bifm.elements.curr.length === 0)
break;
const index = parseInt(this.bifm.elements.curr[0]?.getAttribute("d-index"));
const queue = this.bifm.getChapter(this.bifm.chapterIndex).queue;
if (index < 0 || index >= queue.length)
break;
if (conf.readMode === "pagination") {
if (this.bifm.isReachedBoundary("next")) {
const curr = this.bifm.elements.curr[0];
if (curr instanceof HTMLVideoElement) {
let resolve;
const promise = new Promise((r) => resolve = r);
curr.addEventListener("timeupdate", () => {
if (curr.currentTime >= curr.duration - 1) {
sleep(1e3).then(resolve);
}
});
await promise;
}
this.bifm.onWheel(new WheelEvent("wheel", { deltaY: 1 }), false, true);
} else {
const deltaY = this.bifm.frame.offsetHeight / 2;
frame.scrollBy({ top: deltaY, behavior: "smooth" });
}
} else {
this.scroller.step = conf.autoPageSpeed;
this.scroller.scroll("down", interval() * 1e3 + 10);
}
}
this.stop();
}
stop() {
this.status = "stop";
this.button.setAttribute("data-status", "paused");
const progress = q("#auto-page-progress", this.button);
progress.style.animation = ``;
this.lockVer += 1;
this.button.firstElementChild.innerText = i18n.autoPagePlay.get();
this.scroller.scroll("up", 0);
}
}
function parseIndex(ele) {
if (!ele)
return -1;
const d = ele.getAttribute("d-index") || "";
const i = parseInt(d);
return isNaN(i) ? -1 : i;
}
function revertMonkeyPatch(element) {
const originalScrollTo = Element.prototype.scrollTo;
Object.defineProperty(element, "scrollTo", {
value: originalScrollTo,
writable: true,
configurable: true
});
}
function main(MATCHER) {
const HTML = createHTML();
[HTML.fullViewGrid, HTML.bigImageFrame].forEach((e) => revertMonkeyPatch(e));
const IFQ = IMGFetcherQueue.newQueue();
const IL = new IdleLoader(IFQ);
const PF = new PageFetcher(IFQ, MATCHER);
const DL = new Downloader(HTML, IFQ, IL, PF, MATCHER);
const PH = new PageHelper(HTML, () => PF.chapters);
const BIFM = new BigImageFrameManager(HTML, (index) => PF.chapters[index]);
new FullViewGridManager(HTML, BIFM);
const events = initEvents(HTML, BIFM, IFQ, IL, PH);
addEventListeners(events, HTML, BIFM, DL, PH);
EBUS.subscribe("downloader-canvas-on-click", (index) => {
IFQ.currIndex = index;
if (IFQ.chapterIndex !== BIFM.chapterIndex)
return;
BIFM.show(IFQ[index]);
});
EBUS.subscribe("notify-message", (level, msg) => showMessage(HTML.messageBox, level, msg));
PF.beforeInit = () => HTML.pageLoading.style.display = "flex";
PF.afterInit = () => {
HTML.pageLoading.style.display = "none";
IL.processingIndexList = [0];
IL.start();
};
if (conf.first) {
events.showGuideEvent();
conf.first = false;
saveConf(conf);
}
const href = window.location.href;
const signal = { first: true };
function entry(expand) {
if (HTML.pageHelper) {
if (expand) {
events.showFullViewGrid();
if (signal.first) {
signal.first = false;
EBUS.emit("pf-init", () => {
});
}
} else {
["config", "downloader"].forEach((id) => events.togglePanelEvent(id, true));
events.hiddenFullViewGrid();
}
}
}
EBUS.subscribe("toggle-main-view", entry);
if (conf.autoOpen && enableAutoOpen(href)) {
HTML.entryBTN.setAttribute("data-stage", "open");
entry(true);
}
return () => {
console.log("destory eh-view-enhance");
entry(false);
PF.abort();
IL.abort();
IFQ.length = 0;
EBUS.reset();
document.querySelector("#ehvp-base")?.remove();
return sleep(500);
};
}
let destoryFunc;
const debouncer = new Debouncer();
function reMain() {
debouncer.addEvent("LOCATION-CHANGE", () => {
const newStart = () => {
if (document.querySelector(".ehvp-base"))
return;
const matcher = adaptMatcher(window.location.href);
matcher && (destoryFunc = main(matcher));
};
if (destoryFunc) {
destoryFunc().then(newStart);
} else {
newStart();
}
}, 20);
}
setTimeout(() => {
const oldPushState = history.pushState;
history.pushState = function pushState(...args) {
reMain();
return oldPushState.apply(this, args);
};
const oldReplaceState = history.replaceState;
history.replaceState = function replaceState(...args) {
return oldReplaceState.apply(this, args);
};
window.addEventListener("popstate", reMain);
reMain();
}, 300);
})(saveAs, pica, zip, Hammer);