-
-
Save Chooks22/6f28904bebe7f7cdc0a2a2b4cf6a6df1 to your computer and use it in GitHub Desktop.
// ==UserScript== | |
// @name Kemono Toolkit | |
// @namespace http://kemono.su/ | |
// @version 5.0.2-beta | |
// @author Chooks22 <[email protected]> (https://github.com/Choooks22) | |
// @description Adds features and QoL improvements to kemono.su. | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=kemono.su | |
// @downloadURL https://gist.githubusercontent.com/Chooks22/6f28904bebe7f7cdc0a2a2b4cf6a6df1/raw | |
// @updateURL https://gist.githubusercontent.com/Chooks22/6f28904bebe7f7cdc0a2a2b4cf6a6df1/raw | |
// @match https://kemono.su/* | |
// @connect kemono.su | |
// @grant GM.xmlHttpRequest | |
// @grant GM_notification | |
// @run-at document-idle | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
const e$1 = ({ base: e2 = "", routes: t2 = [], ...r2 } = {}) => ({ __proto__: new Proxy({}, { get: (r3, o2, a2, s2) => (r4, ...c2) => t2.push([o2.toUpperCase(), RegExp(`^${(s2 = (e2 + r4).replace(/\/+(\/|$)/g, "$1")).replace(/(\/?\.?):(\w+)\+/g, "($1(?<$2>*))").replace(/(\/?\.?):(\w+)/g, "($1(?<$2>[^$1/]+?))").replace(/\./g, "\\.").replace(/(\/?)\*/g, "($1.*)?")}/*$`), c2, s2]) && a2 }), routes: t2, ...r2, async fetch(e3, ...r3) { | |
let o2, a2, s2 = new URL(e3.url), c2 = e3.query = { __proto__: null }; | |
for (let [e4, t3] of s2.searchParams) c2[e4] = c2[e4] ? [].concat(c2[e4], t3) : t3; | |
for (let [c3, n2, l2, i2] of t2) if ((c3 == e3.method || "ALL" == c3) && (a2 = s2.pathname.match(n2))) { | |
e3.params = a2.groups || {}, e3.route = i2; | |
for (let t3 of l2) if (null != (o2 = await t3(e3.proxy ?? e3, ...r3))) return o2; | |
} | |
} }), c = (e2) => { | |
e2.proxy = new Proxy(e2.proxy ?? e2, { get: (t2, r2) => { | |
var _a2, _b, _c; | |
return ((_b = (_a2 = t2[r2]) == null ? void 0 : _a2.bind) == null ? void 0 : _b.call(_a2, e2)) ?? t2[r2] ?? ((_c = t2 == null ? void 0 : t2.params) == null ? void 0 : _c[r2]); | |
} }); | |
}; | |
let push_state = history.pushState; | |
history.pushState = function pushState(...args) { | |
let res = push_state.apply(this, args); | |
dispatchEvent(new Event("pushstate")); | |
dispatchEvent(new Event("locationchange")); | |
return res; | |
}; | |
let replace_state = history.replaceState; | |
history.replaceState = function replaceState(...args) { | |
let res = replace_state.apply(this, args); | |
dispatchEvent(new Event("replacestate")); | |
dispatchEvent(new Event("locationchange")); | |
return res; | |
}; | |
addEventListener("popstate", () => { | |
dispatchEvent(new Event("locationchange")); | |
}); | |
function onlocationchange(handler) { | |
handler(); | |
addEventListener("locationchange", handler); | |
} | |
async function get(path) { | |
let res = await fetch(`/api/v1${path}`); | |
return res.json(); | |
} | |
async function get_current_post() { | |
let body = await get(location.pathname); | |
let post = body.post; | |
let attachments = get_post_attachments(post); | |
return { | |
id: post.id, | |
user: post.user, | |
service: post.service, | |
title: post.title, | |
attachments | |
}; | |
} | |
function get_post_attachments(post) { | |
let attachments = []; | |
if (post.file) { | |
attachments.push({ | |
name: `thumbnail-${post.file.name}`, | |
path: post.file.path, | |
type: "thumbnail" | |
}); | |
} | |
for (let i2 = 0; i2 < post.attachments.length; i2++) { | |
let attachment = post.attachments[i2]; | |
attachments.push({ | |
name: `${i2}-${attachment.name}`, | |
path: attachment.path, | |
type: "attachment" | |
}); | |
} | |
return attachments; | |
} | |
"stream" in Blob.prototype || Object.defineProperty(Blob.prototype, "stream", { value() { | |
return new Response(this).body; | |
} }), "setBigUint64" in DataView.prototype || Object.defineProperty(DataView.prototype, "setBigUint64", { value(e2, n2, t2) { | |
const i2 = Number(0xffffffffn & n2), r2 = Number(n2 >> 32n); | |
this.setUint32(e2 + (t2 ? 0 : 4), i2, t2), this.setUint32(e2 + (t2 ? 4 : 0), r2, t2); | |
} }); | |
var e = (e2) => new DataView(new ArrayBuffer(e2)), n = (e2) => new Uint8Array(e2.buffer || e2), t = (e2) => new TextEncoder().encode(String(e2)), i = (e2) => Math.min(4294967295, Number(e2)), r = (e2) => Math.min(65535, Number(e2)); | |
function o(e2, i2, r2) { | |
void 0 === i2 || i2 instanceof Date || (i2 = new Date(i2)); | |
const o2 = void 0 !== e2; | |
if (r2 || (r2 = o2 ? 436 : 509), e2 instanceof File) return { isFile: o2, t: i2 || new Date(e2.lastModified), bytes: e2.stream(), mode: r2 }; | |
if (e2 instanceof Response) return { isFile: o2, t: i2 || new Date(e2.headers.get("Last-Modified") || Date.now()), bytes: e2.body, mode: r2 }; | |
if (void 0 === i2) i2 = /* @__PURE__ */ new Date(); | |
else if (isNaN(i2)) throw new Error("Invalid modification date."); | |
if (!o2) return { isFile: o2, t: i2, mode: r2 }; | |
if ("string" == typeof e2) return { isFile: o2, t: i2, bytes: t(e2), mode: r2 }; | |
if (e2 instanceof Blob) return { isFile: o2, t: i2, bytes: e2.stream(), mode: r2 }; | |
if (e2 instanceof Uint8Array || e2 instanceof ReadableStream) return { isFile: o2, t: i2, bytes: e2, mode: r2 }; | |
if (e2 instanceof ArrayBuffer || ArrayBuffer.isView(e2)) return { isFile: o2, t: i2, bytes: n(e2), mode: r2 }; | |
if (Symbol.asyncIterator in e2) return { isFile: o2, t: i2, bytes: f(e2[Symbol.asyncIterator]()), mode: r2 }; | |
throw new TypeError("Unsupported input format."); | |
} | |
function f(e2, n2 = e2) { | |
return new ReadableStream({ async pull(n3) { | |
let t2 = 0; | |
for (; n3.desiredSize > t2; ) { | |
const i2 = await e2.next(); | |
if (!i2.value) { | |
n3.close(); | |
break; | |
} | |
{ | |
const e3 = a(i2.value); | |
n3.enqueue(e3), t2 += e3.byteLength; | |
} | |
} | |
}, cancel(e3) { | |
var _a2; | |
(_a2 = n2.throw) == null ? void 0 : _a2.call(n2, e3); | |
} }); | |
} | |
function a(e2) { | |
return "string" == typeof e2 ? t(e2) : e2 instanceof Uint8Array ? e2 : n(e2); | |
} | |
function s(e2, i2, r2) { | |
let [o2, f2] = function(e3) { | |
return e3 ? e3 instanceof Uint8Array ? [e3, 1] : ArrayBuffer.isView(e3) || e3 instanceof ArrayBuffer ? [n(e3), 1] : [t(e3), 0] : [void 0, 0]; | |
}(i2); | |
if (e2 instanceof File) return { i: d(o2 || t(e2.name)), o: BigInt(e2.size), u: f2 }; | |
if (e2 instanceof Response) { | |
const n2 = e2.headers.get("content-disposition"), i3 = n2 && n2.match(/;\s*filename\*?\s*=\s*(?:UTF-\d+''|)["']?([^;"'\r\n]*)["']?(?:;|$)/i), a2 = i3 && i3[1] || e2.url && new URL(e2.url).pathname.split("/").findLast(Boolean), s2 = a2 && decodeURIComponent(a2), u2 = r2 || +e2.headers.get("content-length"); | |
return { i: d(o2 || t(s2)), o: BigInt(u2), u: f2 }; | |
} | |
return o2 = d(o2, void 0 !== e2 || void 0 !== r2), "string" == typeof e2 ? { i: o2, o: BigInt(t(e2).length), u: f2 } : e2 instanceof Blob ? { i: o2, o: BigInt(e2.size), u: f2 } : e2 instanceof ArrayBuffer || ArrayBuffer.isView(e2) ? { i: o2, o: BigInt(e2.byteLength), u: f2 } : { i: o2, o: u(e2, r2), u: f2 }; | |
} | |
function u(e2, n2) { | |
return n2 > -1 ? BigInt(n2) : e2 ? void 0 : 0n; | |
} | |
function d(e2, n2 = 1) { | |
if (!e2 || e2.every((c2) => 47 === c2)) throw new Error("The file must have a name."); | |
if (n2) for (; 47 === e2[e2.length - 1]; ) e2 = e2.subarray(0, -1); | |
else 47 !== e2[e2.length - 1] && (e2 = new Uint8Array([...e2, 47])); | |
return e2; | |
} | |
var l = new Uint32Array(256); | |
for (let e2 = 0; e2 < 256; ++e2) { | |
let n2 = e2; | |
for (let e3 = 0; e3 < 8; ++e3) n2 = n2 >>> 1 ^ (1 & n2 && 3988292384); | |
l[e2] = n2; | |
} | |
function y(e2, n2 = 0) { | |
n2 = ~n2; | |
for (var t2 = 0, i2 = e2.length; t2 < i2; t2++) n2 = n2 >>> 8 ^ l[255 & n2 ^ e2[t2]]; | |
return ~n2 >>> 0; | |
} | |
function w(e2, n2, t2 = 0) { | |
const i2 = e2.getSeconds() >> 1 | e2.getMinutes() << 5 | e2.getHours() << 11, r2 = e2.getDate() | e2.getMonth() + 1 << 5 | e2.getFullYear() - 1980 << 9; | |
n2.setUint16(t2, i2, 1), n2.setUint16(t2 + 2, r2, 1); | |
} | |
function B({ i: e2, u: n2 }, t2) { | |
return 8 * (!n2 || (t2 ?? function(e3) { | |
try { | |
b.decode(e3); | |
} catch { | |
return 0; | |
} | |
return 1; | |
}(e2))); | |
} | |
var b = new TextDecoder("utf8", { fatal: 1 }); | |
function p(t2, i2 = 0) { | |
const r2 = e(30); | |
return r2.setUint32(0, 1347093252), r2.setUint32(4, 754976768 | i2), w(t2.t, r2, 10), r2.setUint16(26, t2.i.length, 1), n(r2); | |
} | |
async function* g(e2) { | |
let { bytes: n2 } = e2; | |
if ("then" in n2 && (n2 = await n2), n2 instanceof Uint8Array) yield n2, e2.l = y(n2, 0), e2.o = BigInt(n2.length); | |
else { | |
e2.o = 0n; | |
const t2 = n2.getReader(); | |
for (; ; ) { | |
const { value: n3, done: i2 } = await t2.read(); | |
if (i2) break; | |
e2.l = y(n3, e2.l), e2.o += BigInt(n3.length), yield n3; | |
} | |
} | |
} | |
function I(t2, r2) { | |
const o2 = e(16 + (r2 ? 8 : 0)); | |
return o2.setUint32(0, 1347094280), o2.setUint32(4, t2.isFile ? t2.l : 0, 1), r2 ? (o2.setBigUint64(8, t2.o, 1), o2.setBigUint64(16, t2.o, 1)) : (o2.setUint32(8, i(t2.o), 1), o2.setUint32(12, i(t2.o), 1)), n(o2); | |
} | |
function v(t2, r2, o2 = 0, f2 = 0) { | |
const a2 = e(46); | |
return a2.setUint32(0, 1347092738), a2.setUint32(4, 755182848), a2.setUint16(8, 2048 | o2), w(t2.t, a2, 12), a2.setUint32(16, t2.isFile ? t2.l : 0, 1), a2.setUint32(20, i(t2.o), 1), a2.setUint32(24, i(t2.o), 1), a2.setUint16(28, t2.i.length, 1), a2.setUint16(30, f2, 1), a2.setUint16(40, t2.mode | (t2.isFile ? 32768 : 16384), 1), a2.setUint32(42, i(r2), 1), n(a2); | |
} | |
function h(t2, i2, r2) { | |
const o2 = e(r2); | |
return o2.setUint16(0, 1, 1), o2.setUint16(2, r2 - 4, 1), 16 & r2 && (o2.setBigUint64(4, t2.o, 1), o2.setBigUint64(12, t2.o, 1)), o2.setBigUint64(r2 - 8, i2, 1), n(o2); | |
} | |
function D(e2) { | |
return e2 instanceof File || e2 instanceof Response ? [[e2], [e2]] : [[e2.input, e2.name, e2.size], [e2.input, e2.lastModified, e2.mode]]; | |
} | |
var S = (e2) => function(e3) { | |
let n2 = BigInt(22), t2 = 0n, i2 = 0; | |
for (const r2 of e3) { | |
if (!r2.i) throw new Error("Every file must have a non-empty name."); | |
if (void 0 === r2.o) throw new Error(`Missing size for file "${new TextDecoder().decode(r2.i)}".`); | |
const e4 = r2.o >= 0xffffffffn, o2 = t2 >= 0xffffffffn; | |
t2 += BigInt(46 + r2.i.length + (e4 && 8)) + r2.o, n2 += BigInt(r2.i.length + 46 + (12 * o2 | 28 * e4)), i2 || (i2 = e4); | |
} | |
return (i2 || t2 >= 0xffffffffn) && (n2 += BigInt(76)), n2 + t2; | |
}(function* (e3) { | |
for (const n2 of e3) yield s(...D(n2)[0]); | |
}(e2)); | |
function A(e2, n2 = {}) { | |
const t2 = { "Content-Type": "application/zip", "Content-Disposition": "attachment" }; | |
return ("bigint" == typeof n2.length || Number.isInteger(n2.length)) && n2.length > 0 && (t2["Content-Length"] = String(n2.length)), n2.metadata && (t2["Content-Length"] = String(S(n2.metadata))), new Response(N(e2, n2), { headers: t2 }); | |
} | |
function N(t2, a2 = {}) { | |
const u2 = function(e2) { | |
var _a2; | |
const n2 = e2[Symbol.iterator in e2 ? Symbol.iterator : Symbol.asyncIterator](); | |
return { async next() { | |
const e3 = await n2.next(); | |
if (e3.done) return e3; | |
const [t3, i2] = D(e3.value); | |
return { done: 0, value: Object.assign(o(...i2), s(...t3)) }; | |
}, throw: (_a2 = n2.throw) == null ? void 0 : _a2.bind(n2), [Symbol.asyncIterator]() { | |
return this; | |
} }; | |
}(t2); | |
return f(async function* (t3, o2) { | |
const f2 = []; | |
let a3 = 0n, s2 = 0n, u3 = 0; | |
for await (const e2 of t3) { | |
const n2 = B(e2, o2.buffersAreUTF8); | |
yield p(e2, n2), yield new Uint8Array(e2.i), e2.isFile && (yield* g(e2)); | |
const t4 = e2.o >= 0xffffffffn, i2 = 12 * (a3 >= 0xffffffffn) | 28 * t4; | |
yield I(e2, t4), f2.push(v(e2, a3, n2, i2)), f2.push(e2.i), i2 && f2.push(h(e2, a3, i2)), t4 && (a3 += 8n), s2++, a3 += BigInt(46 + e2.i.length) + e2.o, u3 || (u3 = t4); | |
} | |
let d2 = 0n; | |
for (const e2 of f2) yield e2, d2 += BigInt(e2.length); | |
if (u3 || a3 >= 0xffffffffn) { | |
const t4 = e(76); | |
t4.setUint32(0, 1347094022), t4.setBigUint64(4, BigInt(44), 1), t4.setUint32(12, 755182848), t4.setBigUint64(24, s2, 1), t4.setBigUint64(32, s2, 1), t4.setBigUint64(40, d2, 1), t4.setBigUint64(48, a3, 1), t4.setUint32(56, 1347094023), t4.setBigUint64(64, a3 + d2, 1), t4.setUint32(72, 1, 1), yield n(t4); | |
} | |
const l2 = e(22); | |
l2.setUint32(0, 1347093766), l2.setUint16(8, r(s2), 1), l2.setUint16(10, r(s2), 1), l2.setUint32(12, i(d2), 1), l2.setUint32(16, i(a3), 1), yield n(l2); | |
}(u2, a2), u2); | |
} | |
var _a; | |
if (typeof Promise.withResolvers !== "function") { | |
(_a = Promise.withResolvers) != null ? _a : Promise.withResolvers = function() { | |
let p2 = {}; | |
p2.promise = new Promise((res, rej) => { | |
p2.resolve = res; | |
p2.reject = rej; | |
}); | |
return p2; | |
}; | |
} | |
function create_observable() { | |
let is_closed = false; | |
let cursor = 0; | |
let queue = []; | |
let p2 = null; | |
return { | |
push(...items) { | |
let did_push = !is_closed; | |
if (did_push) { | |
cursor = queue.length; | |
queue.push(...items); | |
p2 == null ? void 0 : p2.resolve(); | |
} | |
return did_push; | |
}, | |
end() { | |
is_closed = true; | |
p2 == null ? void 0 : p2.resolve(); | |
}, | |
async *[Symbol.asyncIterator]() { | |
if (queue.length > 0) { | |
yield* queue; | |
} | |
while (!is_closed) { | |
if (p2 === null) { | |
p2 = Promise.withResolvers(); | |
await p2.promise; | |
p2 = null; | |
} else { | |
await p2.promise; | |
} | |
for (let i2 = cursor; i2 < queue.length; i2++) { | |
yield queue[i2]; | |
} | |
} | |
} | |
}; | |
} | |
const instanceOfAny = (object, constructors) => constructors.some((c2) => object instanceof c2); | |
let idbProxyableTypes; | |
let cursorAdvanceMethods; | |
function getIdbProxyableTypes() { | |
return idbProxyableTypes || (idbProxyableTypes = [ | |
IDBDatabase, | |
IDBObjectStore, | |
IDBIndex, | |
IDBCursor, | |
IDBTransaction | |
]); | |
} | |
function getCursorAdvanceMethods() { | |
return cursorAdvanceMethods || (cursorAdvanceMethods = [ | |
IDBCursor.prototype.advance, | |
IDBCursor.prototype.continue, | |
IDBCursor.prototype.continuePrimaryKey | |
]); | |
} | |
const transactionDoneMap = /* @__PURE__ */ new WeakMap(); | |
const transformCache = /* @__PURE__ */ new WeakMap(); | |
const reverseTransformCache = /* @__PURE__ */ new WeakMap(); | |
function promisifyRequest(request) { | |
const promise = new Promise((resolve, reject) => { | |
const unlisten = () => { | |
request.removeEventListener("success", success); | |
request.removeEventListener("error", error); | |
}; | |
const success = () => { | |
resolve(wrap(request.result)); | |
unlisten(); | |
}; | |
const error = () => { | |
reject(request.error); | |
unlisten(); | |
}; | |
request.addEventListener("success", success); | |
request.addEventListener("error", error); | |
}); | |
reverseTransformCache.set(promise, request); | |
return promise; | |
} | |
function cacheDonePromiseForTransaction(tx) { | |
if (transactionDoneMap.has(tx)) | |
return; | |
const done = new Promise((resolve, reject) => { | |
const unlisten = () => { | |
tx.removeEventListener("complete", complete); | |
tx.removeEventListener("error", error); | |
tx.removeEventListener("abort", error); | |
}; | |
const complete = () => { | |
resolve(); | |
unlisten(); | |
}; | |
const error = () => { | |
reject(tx.error || new DOMException("AbortError", "AbortError")); | |
unlisten(); | |
}; | |
tx.addEventListener("complete", complete); | |
tx.addEventListener("error", error); | |
tx.addEventListener("abort", error); | |
}); | |
transactionDoneMap.set(tx, done); | |
} | |
let idbProxyTraps = { | |
get(target, prop, receiver) { | |
if (target instanceof IDBTransaction) { | |
if (prop === "done") | |
return transactionDoneMap.get(target); | |
if (prop === "store") { | |
return receiver.objectStoreNames[1] ? void 0 : receiver.objectStore(receiver.objectStoreNames[0]); | |
} | |
} | |
return wrap(target[prop]); | |
}, | |
set(target, prop, value) { | |
target[prop] = value; | |
return true; | |
}, | |
has(target, prop) { | |
if (target instanceof IDBTransaction && (prop === "done" || prop === "store")) { | |
return true; | |
} | |
return prop in target; | |
} | |
}; | |
function replaceTraps(callback) { | |
idbProxyTraps = callback(idbProxyTraps); | |
} | |
function wrapFunction(func) { | |
if (getCursorAdvanceMethods().includes(func)) { | |
return function(...args) { | |
func.apply(unwrap(this), args); | |
return wrap(this.request); | |
}; | |
} | |
return function(...args) { | |
return wrap(func.apply(unwrap(this), args)); | |
}; | |
} | |
function transformCachableValue(value) { | |
if (typeof value === "function") | |
return wrapFunction(value); | |
if (value instanceof IDBTransaction) | |
cacheDonePromiseForTransaction(value); | |
if (instanceOfAny(value, getIdbProxyableTypes())) | |
return new Proxy(value, idbProxyTraps); | |
return value; | |
} | |
function wrap(value) { | |
if (value instanceof IDBRequest) | |
return promisifyRequest(value); | |
if (transformCache.has(value)) | |
return transformCache.get(value); | |
const newValue = transformCachableValue(value); | |
if (newValue !== value) { | |
transformCache.set(value, newValue); | |
reverseTransformCache.set(newValue, value); | |
} | |
return newValue; | |
} | |
const unwrap = (value) => reverseTransformCache.get(value); | |
function openDB(name, version, { blocked, upgrade, blocking, terminated } = {}) { | |
const request = indexedDB.open(name, version); | |
const openPromise = wrap(request); | |
if (upgrade) { | |
request.addEventListener("upgradeneeded", (event) => { | |
upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event); | |
}); | |
} | |
if (blocked) { | |
request.addEventListener("blocked", (event) => blocked( | |
// Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 | |
event.oldVersion, | |
event.newVersion, | |
event | |
)); | |
} | |
openPromise.then((db) => { | |
if (terminated) | |
db.addEventListener("close", () => terminated()); | |
if (blocking) { | |
db.addEventListener("versionchange", (event) => blocking(event.oldVersion, event.newVersion, event)); | |
} | |
}).catch(() => { | |
}); | |
return openPromise; | |
} | |
const readMethods = ["get", "getKey", "getAll", "getAllKeys", "count"]; | |
const writeMethods = ["put", "add", "delete", "clear"]; | |
const cachedMethods = /* @__PURE__ */ new Map(); | |
function getMethod(target, prop) { | |
if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === "string")) { | |
return; | |
} | |
if (cachedMethods.get(prop)) | |
return cachedMethods.get(prop); | |
const targetFuncName = prop.replace(/FromIndex$/, ""); | |
const useIndex = prop !== targetFuncName; | |
const isWrite = writeMethods.includes(targetFuncName); | |
if ( | |
// Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. | |
!(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName)) | |
) { | |
return; | |
} | |
const method = async function(storeName, ...args) { | |
const tx = this.transaction(storeName, isWrite ? "readwrite" : "readonly"); | |
let target2 = tx.store; | |
if (useIndex) | |
target2 = target2.index(args.shift()); | |
return (await Promise.all([ | |
target2[targetFuncName](...args), | |
isWrite && tx.done | |
]))[0]; | |
}; | |
cachedMethods.set(prop, method); | |
return method; | |
} | |
replaceTraps((oldTraps) => ({ | |
...oldTraps, | |
get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), | |
has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop) | |
})); | |
const advanceMethodProps = ["continue", "continuePrimaryKey", "advance"]; | |
const methodMap = {}; | |
const advanceResults = /* @__PURE__ */ new WeakMap(); | |
const ittrProxiedCursorToOriginalProxy = /* @__PURE__ */ new WeakMap(); | |
const cursorIteratorTraps = { | |
get(target, prop) { | |
if (!advanceMethodProps.includes(prop)) | |
return target[prop]; | |
let cachedFunc = methodMap[prop]; | |
if (!cachedFunc) { | |
cachedFunc = methodMap[prop] = function(...args) { | |
advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args)); | |
}; | |
} | |
return cachedFunc; | |
} | |
}; | |
async function* iterate(...args) { | |
let cursor = this; | |
if (!(cursor instanceof IDBCursor)) { | |
cursor = await cursor.openCursor(...args); | |
} | |
if (!cursor) | |
return; | |
cursor = cursor; | |
const proxiedCursor = new Proxy(cursor, cursorIteratorTraps); | |
ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor); | |
reverseTransformCache.set(proxiedCursor, unwrap(cursor)); | |
while (cursor) { | |
yield proxiedCursor; | |
cursor = await (advanceResults.get(proxiedCursor) || cursor.continue()); | |
advanceResults.delete(proxiedCursor); | |
} | |
} | |
function isIteratorProp(target, prop) { | |
return prop === Symbol.asyncIterator && instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor]) || prop === "iterate" && instanceOfAny(target, [IDBIndex, IDBObjectStore]); | |
} | |
replaceTraps((oldTraps) => ({ | |
...oldTraps, | |
get(target, prop, receiver) { | |
if (isIteratorProp(target, prop)) | |
return iterate; | |
return oldTraps.get(target, prop, receiver); | |
}, | |
has(target, prop) { | |
return isIteratorProp(target, prop) || oldTraps.has(target, prop); | |
} | |
})); | |
async function select_until(selector) { | |
let el; | |
while (!el) { | |
await new Promise(requestAnimationFrame); | |
el = document.querySelector(selector); | |
} | |
return el; | |
} | |
async function create_button(text) { | |
let button = document.createElement("button"); | |
button.textContent = text; | |
button.className = "post__fav"; | |
let container = await select_until(".post__actions"); | |
container.append(button); | |
return button; | |
} | |
function use_settings(db) { | |
return { | |
async get(key, get_default) { | |
let res = await db.get("settings", key); | |
if (res !== void 0) { | |
return res.value; | |
} | |
let value = await get_default(); | |
await db.add("settings", { key, value }); | |
return value; | |
}, | |
set(key, value) { | |
return db.add("settings", { key, value }); | |
} | |
}; | |
} | |
function open_db() { | |
return openDB("kemono-toolkit", 1, { | |
upgrade(db) { | |
db.createObjectStore("settings", { | |
keyPath: "key" | |
}); | |
} | |
}); | |
} | |
function fs_archiver(root) { | |
return { | |
type: "fs", | |
async file(name, data) { | |
let handle = await root.getFileHandle(name, { create: true }); | |
let writer = await handle.createWritable({ keepExistingData: false }); | |
await writer.write(data); | |
await writer.close(); | |
}, | |
async done() { | |
} | |
}; | |
} | |
function zip_archiver(title) { | |
let files = create_observable(); | |
let zip = A(files); | |
return { | |
type: "zip", | |
async file(name, data) { | |
files.push({ name, input: data }); | |
}, | |
async done() { | |
files.end(); | |
let zip_blob = await zip.blob(); | |
save_as(zip_blob, `${title}.zip`); | |
} | |
}; | |
} | |
function save_as(blob, title) { | |
const link = document.createElement("a"); | |
link.href = URL.createObjectURL(blob); | |
link.download = `${title}.zip`; | |
link.click(); | |
link.remove(); | |
URL.revokeObjectURL(link.href); | |
} | |
async function create_archiver(title) { | |
let uses_new_archiver = typeof window.showDirectoryPicker === "function"; | |
let settings = use_settings(await open_db()); | |
if (uses_new_archiver) { | |
try { | |
let root = await settings.get("root", () => showDirectoryPicker({ startIn: "downloads", mode: "readwrite" })); | |
let dir = await root.getDirectoryHandle(title, { create: true }); | |
return fs_archiver(dir); | |
} catch (e2) { | |
console.error("could not use file system api."); | |
console.error(e2); | |
return zip_archiver(title); | |
} | |
} else { | |
return zip_archiver(title); | |
} | |
} | |
async function download_attachment(path) { | |
console.log("downloading (%s)...", path); | |
let res = await GM.xmlHttpRequest({ | |
url: path, | |
responseType: "arraybuffer" | |
}); | |
console.log("downloaded (%s)"); | |
return res.response; | |
} | |
function create_notification(id) { | |
let title = `Downloading Post #${id}...`; | |
return function(text, done = false) { | |
return new Promise((res) => GM_notification({ | |
tag: id, | |
title, | |
text, | |
timeout: done ? 3e3 : 0, | |
ondone: res | |
})); | |
}; | |
} | |
let router = e$1(); | |
router.get("/:service/user/:user_id/post/:post_id", c, async (c2) => { | |
let btn = await create_button("Download"); | |
btn.onclick = async () => { | |
let notify = create_notification(c2.post_id); | |
void notify("Downloading post data..."); | |
let post = await get_current_post(); | |
let title; | |
{ | |
title = post.title; | |
} | |
let saver = await create_archiver(title); | |
for (let i2 = 0, n2 = post.attachments.length; i2 < n2; i2++) { | |
let attachment = post.attachments[i2]; | |
void notify(`Downloading ${i2 + 1} of ${n2} attachments...`); | |
let contents = await download_attachment(attachment.path); | |
await saver.file(attachment.name, contents); | |
} | |
if (saver.type === "zip") { | |
void notify("Saving files..."); | |
await saver.done(); | |
} | |
await notify("Post downloaded!", true); | |
}; | |
}); | |
onlocationchange(() => { | |
console.log("update():", location.href); | |
router.fetch(new Request(location.pathname)); | |
}); | |
})(); |
// @todo: source code | |
// will be moving it to a dedicated repo, since it now uses multiple files |
The download doesn't work anymore :(
I haven't updated to the new url nor api version, I'll post an update soon
The download doesn't work anymore :(
I haven't updated to the new url nor api version, I'll post an update soon
Thank you so much :D
When do you think it will work again?
hi, I wanna know if there's any function to make the filename to be titlename instead of postid while downloading?
hi, I wanna know if there's any function to make the filename to be titlename instead of postid while downloading?
Posted an update, toggle between id and title from context menu (right click).
I changed it for the zip file's name only though, since the individual filenames come from kemono.
hi, I wanna know if there's any function to make the filename to be titlename instead of postid while downloading?
Posted an update, toggle between id and title from context menu (right click).
I changed it for the zip file's name only though, since the individual filenames come from kemono.
Thank you sooooo much for this updating! So fast!
Hello, I'm so sorry but i'm new to this. How can i get this to work? Do i add the .js file into temper monkey and go to the artist's kemono party page and activate the script?
when i clicked "use id as file name", it does not seem to provide a reaction
How can i get this to work? Do i add the .js file into temper monkey and go to the artist's kemono party page and activate the script?
Click on the Raw
button and tampermonkey should pick it up, if not then manually copy paste it in.
Each post should then have a Download
button right beside the Favorite
button
when i clicked "use id as file name", it does not seem to provide a reaction
Yes, it's just a toggle to change how the zip file is named mentioned above
@Chooks22 Could you, as simple as you can, explain how all of this works so that even a code-illiterate like me can understand?
Kemono updated again and the download no longer works. Do you think you can update?
Kemono updated again and the download no longer works. Do you think you can update?
@fernadogomezmartin cooking up a complete overhaul, will be up soon:tm:
@Chooks22 Could you, as simple as you can, explain how all of this works so that even a code-illiterate like me can understand?
@Paisa2000 it just accesses kemono's api and modifies the page to add some buttons to do so, most of the code is for notifications so users know it's still working
Thank you very much, I hope it works before the holidays :)
Kemono updated again and the download no longer works. Do you think you can update?
@fernadogomezmartin cooking up a complete overhaul, will be up soon:tm:
@Chooks22 Could you, as simple as you can, explain how all of this works so that even a code-illiterate like me can understand?
@Paisa2000 it just accesses kemono's api and modifies the page to add some buttons to do so, most of the code is for notifications so users know it's still working
Excuse me. Is there any update which will be released recently?
Your extension is the best until now, however the website has updated itself.
So sad.
Kemono updated again and the download no longer works. Do you think you can update?
@fernadogomezmartin cooking up a complete overhaul, will be up soon:tm:
@Chooks22 Could you, as simple as you can, explain how all of this works so that even a code-illiterate like me can understand?
@Paisa2000 it just accesses kemono's api and modifies the page to add some buttons to do so, most of the code is for notifications so users know it's still working
Excuse me. Is there any update which will be released recently? Your extension is the best until now, however the website has updated itself. So sad.
@harvey1995 got my hands full on the holidays, and what I discovered was it wasn't just a simple update
To put it in layman's terms they changed how the site functions so now I have to update it to "tap into" its
internals to inject the buttons, otherwise the script goes out of sync and nothing in the site gets updated
Kemono updated again and the download no longer works. Do you think you can update?
@fernadogomezmartin cooking up a complete overhaul, will be up soon:tm:
@Chooks22 Could you, as simple as you can, explain how all of this works so that even a code-illiterate like me can understand?
@Paisa2000 it just accesses kemono's api and modifies the page to add some buttons to do so, most of the code is for notifications so users know it's still working
Excuse me. Is there any update which will be released recently? Your extension is the best until now, however the website has updated itself. So sad.
@harvey1995 got my hands full on the holidays, and what I discovered was it wasn't just a simple update
To put it in layman's terms they changed how the site functions so now I have to update it to "tap into" its internals to inject the buttons, otherwise the script goes out of sync and nothing in the site gets updated
Thanks for your reply. Take it easy.
:P
Idk if the old update link will still detect a new update, since I changed my name and github now returns a 404 for the old link
Just uninstall the old one and reinstall this one, since I also changed the name to reflect my plans for this script, bunch of plans lined up
The download doesn't work anymore :(