import { Buffer } from "buffer"; import { fileTypeFromBuffer } from 'file-type'; import * as png from "./png"; import * as webm from "./webm"; type Awaited = T extends PromiseLike ? U : T const xmlhttprequest = typeof GM_xmlhttpRequest != 'undefined' ? GM_xmlhttpRequest : (GM ? GM.xmlHttpRequest : GM_xmlhttpRequest); function GM_fetch(...[url, opt]: Parameters) { function blobTo(to: string, blob: Blob) { if (to == "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer(); return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = function (event) { if (!event) return; if (to == "base64") resolve(event.target!.result); else resolve(event.target!.result); }; if (to == "arrayBuffer") fileReader.readAsArrayBuffer(blob); else if (to == "base64") fileReader.readAsDataURL(blob); // "data:*/*;base64,......" else if (to == "text") fileReader.readAsText(blob, "utf-8"); else reject("unknown to"); }); } return new Promise>((resolve, reject) => { // https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_xmlhttpRequest const gmopt: Tampermonkey.Request = { url: url.toString(), data: opt?.body?.toString(), responseType: "blob", method: "GET", onload: (resp) => { const blob = resp.response as Blob; const ref = resp as any as Awaited>; ref.blob = () => Promise.resolve(blob); ref.arrayBuffer = () => blobTo("arrayBuffer", blob) as Promise; ref.text = () => blobTo("text", blob) as Promise; ref.json = async () => JSON.parse(await (blobTo("text", blob) as Promise)); resolve(resp as any); }, ontimeout: () => reject("fetch timeout"), onerror: () => reject("fetch error"), onabort: () => reject("fetch abort") }; xmlhttprequest(gmopt); }); } const processors: [RegExp, (reader: ReadableStreamDefaultReader) => Promise<{ filename: string; data: Buffer } | undefined>, (container: File, inj: File) => Promise][] = [ [/\.png$/, png.extract, png.inject], [/\.webm$/, webm.extract, webm.inject] ]; const processImage = async (src: string) => { const proc = processors.find(e => src.match(e[0])); if (!proc) return; const resp = await GM_fetch(src); const reader = (await resp.blob()).stream(); if (!reader) return; return await proc[1](reader.getReader()); }; const processPost = async (post: HTMLDivElement) => { if (post.hasAttribute('data-processed')) return; post.setAttribute('data-processed', "true"); const thumb = post.querySelector(".fileThumb") as HTMLAnchorElement; if (!thumb) return; const res = await processImage(thumb.href); if (!res) return; const replyBox = post.querySelector('.post'); replyBox?.classList.toggle('hasembed'); // add buttons const fi = post.querySelector(".file-info")!; const cf = ` `; const a = document.createRange().createContextualFragment(cf).children[0] as HTMLAnchorElement; const type = await fileTypeFromBuffer(res.data); let cont: HTMLImageElement | HTMLVideoElement; let w: number, h: number; if (type?.mime.startsWith("image")) { cont = document.createElement("img"); } else if (type?.mime.startsWith("video")) { cont = document.createElement("video"); } else return; // TODO: handle new file types??? Or direct "download"? cont.src = URL.createObjectURL(new Blob([res.data])); await new Promise(res => { cont.onload = res; }); if (cont instanceof HTMLImageElement) { w = cont.naturalWidth; h = cont.naturalHeight; } if (cont instanceof HTMLVideoElement) { w = cont.width; h = cont.height; } const contract = () => { // ugh }; const expand = () => { cont.style.width = `${w}px`; cont.style.height = `${h}px`; cont.style.maxWidth = "unset"; cont.style.maxHeight = "unset"; }; const imgcont = document.createElement('div'); const p = thumb.parentElement!; p.removeChild(thumb); imgcont.appendChild(thumb); p.appendChild(imgcont); thumb.style.display = "flex"; thumb.style.gap = "5px"; thumb.style.flexDirection = "column"; a.classList.toggle("disabled"); let contracted = true; contract(); cont.onclick = (e) => { contracted = !contracted; (contracted) ? contract() : expand(); e.stopPropagation(); }; let visible = false; a.onclick = () => { visible = !visible; if (visible) { imgcont.appendChild(cont); } else { imgcont.removeChild(cont); } a.classList.toggle("disabled"); }; fi.children[1].insertAdjacentElement('afterend', a); }; const startup = async () => { await Promise.all([...document.querySelectorAll('.postContainer')].map(e => processPost(e as any))); //await Promise.all([...document.querySelectorAll('.postContainer')].filter(e => e.textContent?.includes("191 KB")).map(e => processPost(e as any))); document.addEventListener('PostsInserted', (async (e: CustomEvent) => { const threadelement = e.target as HTMLDivElement; const posts = [...threadelement.querySelectorAll("postContainer")].filter(e => e.hasAttribute('data-processed')); posts.map(e => processPost(e as any)); })); const getSelectedFile = () => { return new Promise(res => { document.addEventListener('QRFile', e => res((e as any).detail), { once: true }); document.dispatchEvent(new CustomEvent('QRGetFile')); }); }; let injected = false; document.addEventListener('QRDialogCreation', ((e: CustomEvent) => { if (injected) return; injected = true; const target = e.target as HTMLDivElement; const bts = target.querySelector('#qr-filename-container'); const i = document.createElement('i'); i.className = "fa fa-magnet"; const a = document.createElement('a'); a.appendChild(i); a.title = "Embed File (Select a file before...)"; bts?.appendChild(a); a.onclick = async (e) => { const file = await getSelectedFile(); if (!file) return; const input = document.createElement('input') as HTMLInputElement; input.setAttribute("type", "file"); input.onchange = (async ev => { if (input.files) { const proc = processors.find(e => file.name.match(e[0])); if (!proc) return; document.dispatchEvent(new CustomEvent('QRSetFile', { detail: await proc[2](file, input.files[0]) })); } }); input.click(); }; })); }; document.addEventListener('4chanXInitFinished', startup); const customStyles = document.createElement('style'); customStyles.appendChild(document.createTextNode( ` .extractedImg { width:auto; height:auto; max-width:125px; max-height:125px; cursor: pointer; } .hasembed { border-right: 3px dashed deeppink !important; } ` )); document.documentElement.insertBefore(customStyles, null); // onload = () => { // let container = document.getElementById("container") as HTMLInputElement; // let injection = document.getElementById("injection") as HTMLInputElement; // container.onchange = injection.onchange = async () => { // if (container.files?.length && injection.files?.length) { // let res = await buildInjection(container.files[0], injection.files[0]); // let result = document.getElementById("result") as HTMLImageElement; // let extracted = document.getElementById("extracted") as HTMLImageElement; // result.src = URL.createObjectURL(res.file); // let embedded = await extractEmbedded(res.file.stream().getReader()); // extracted.src = URL.createObjectURL(new Blob([embedded?.data!])); // } // } // }