Support double embeds

This commit is contained in:
coomdev 2022-01-09 07:39:02 +01:00
parent 19d5fb17ed
commit 27eaa92c82
8 changed files with 186 additions and 196 deletions

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name PNGExtraEmbed // @name PNGExtraEmbed
// @namespace https://coom.tech/ // @namespace https://coom.tech/
// @version 0.100 // @version 0.98
// @description uhh // @description uhh
// @author You // @author You
// @match https://boards.4channel.org/* // @match https://boards.4channel.org/*

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name PNGExtraEmbed // @name PNGExtraEmbed
// @namespace https://coom.tech/ // @namespace https://coom.tech/
// @version 0.100 // @version 0.98
// @description uhh // @description uhh
// @author You // @author You
// @match https://boards.4channel.org/* // @match https://boards.4channel.org/*
@ -10924,7 +10924,6 @@
})); }));
var appState = writable({ var appState = writable({
isCatalog: false, isCatalog: false,
is4chanX: false,
foundPosts: [] foundPosts: []
}); });
appState.subscribe((v) => { appState.subscribe((v) => {
@ -11300,30 +11299,30 @@
xmlhttprequest(gmopt); xmlhttprequest(gmopt);
}); });
} }
function GM_fetch(...[url, opt, lisn]) { function blobTo(to, blob) {
function blobTo(to, blob) { if (to == "arrayBuffer" && blob.arrayBuffer)
if (to == "arrayBuffer" && blob.arrayBuffer) return blob.arrayBuffer();
return blob.arrayBuffer(); return new Promise((resolve, reject) => {
return new Promise((resolve, reject) => { const fileReader = new FileReader();
const fileReader = new FileReader(); fileReader.onload = function(event) {
fileReader.onload = function(event) { if (!event)
if (!event) return;
return; if (to == "base64")
if (to == "base64") resolve(event.target.result);
resolve(event.target.result);
else
resolve(event.target.result);
};
if (to == "arrayBuffer")
fileReader.readAsArrayBuffer(blob);
else if (to == "base64")
fileReader.readAsDataURL(blob);
else if (to == "text")
fileReader.readAsText(blob, "utf-8");
else else
reject("unknown to"); resolve(event.target.result);
}); };
} if (to == "arrayBuffer")
fileReader.readAsArrayBuffer(blob);
else if (to == "base64")
fileReader.readAsDataURL(blob);
else if (to == "text")
fileReader.readAsText(blob, "utf-8");
else
reject("unknown to");
});
}
function GM_fetch(...[url, opt, lisn]) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const gmopt = { const gmopt = {
url: url.toString(), url: url.toString(),
@ -11427,19 +11426,6 @@
preview_url: e.preview_url, preview_url: e.preview_url,
tags: e.tags.split(" ") tags: e.tags.split(" ")
})) }))
},
{
name: "ATFbooru",
domain: "booru.allthefallen.moe",
endpoint: "/posts.json?tags=md5:",
quirks: (a) => a.map((e) => ({
source: e.source,
page: `https://booru.allthefallen.moe/posts/${e.id}`,
ext: e.file_url.substr(e.file_url.lastIndexOf(".") + 1),
full_url: e.file_url,
preview_url: e.preview_url,
tags: e.tag_string.split(" ")
}))
} }
]; ];
var black = /* @__PURE__ */ new Set(); var black = /* @__PURE__ */ new Set();
@ -11449,7 +11435,7 @@
sources = new Set(s.sources); sources = new Set(s.sources);
}); });
var cache = {}; var cache = {};
var findFileFrom = async (b, hex, abort) => { var findFileFrom = async (b, hex) => {
try { try {
if (b.domain in cache && hex in cache[b.domain]) if (b.domain in cache && hex in cache[b.domain])
return cache[b.domain][hex]; return cache[b.domain][hex];
@ -11509,7 +11495,7 @@
skip: true, skip: true,
extract: extract4, extract: extract4,
has_embed: has_embed4, has_embed: has_embed4,
match: (fn) => !!fn.match(/^[0-9a-fA-F]{32}\.....?/) match: (fn) => !!fn.match(/^[0-9a-fA-F]{32}\.....?$/)
}; };
// src/App.svelte // src/App.svelte
@ -15575,9 +15561,14 @@
post.setAttribute("data-processed", "true"); post.setAttribute("data-processed", "true");
}; };
var startup = async () => { var startup = async () => {
if (typeof window["FCX"] != "undefined")
appState.set({ ...cappState, is4chanX: true });
await Promise.all([...document.querySelectorAll(".postContainer")].filter((e) => e.textContent?.includes("191 KB")).map((e) => processPost(e))); await Promise.all([...document.querySelectorAll(".postContainer")].filter((e) => e.textContent?.includes("191 KB")).map((e) => processPost(e)));
document.addEventListener("ThreadUpdate", async (e) => {
const newPosts = e.detail.newPosts;
for (const post of newPosts) {
const postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1));
processPost(postContainer);
}
});
const mo = new MutationObserver((reco) => { const mo = new MutationObserver((reco) => {
for (const rec of reco) for (const rec of reco)
if (rec.type == "childList") if (rec.type == "childList")
@ -15609,7 +15600,7 @@
document.body.append(scrollHost); document.body.append(scrollHost);
appState.set({ appState.set({
...cappState, ...cappState,
isCatalog: !!document.querySelector(".catalog-small") || !!location.pathname.match(/\/catalog$/) isCatalog: !!document.querySelector(".catalog-small")
}); });
await Promise.all(posts.map((e) => processPost(e))); await Promise.all(posts.map((e) => processPost(e)));
}; };
@ -15620,38 +15611,15 @@
}); });
}; };
document.addEventListener("4chanXInitFinished", startup); document.addEventListener("4chanXInitFinished", startup);
document.addEventListener("ThreadUpdate", async (e) => {
const newPosts = e.detail.newPosts;
for (const post of newPosts) {
const postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1));
processPost(postContainer);
}
});
if (cappState.is4chanX) {
const qr = window["QR"];
const show = qr.show.bind(qr);
qr.show = (...args) => {
show(...args);
document.dispatchEvent(new CustomEvent("QRDialogCreation", {
detail: document.getElementById("quickReply")
}));
};
}
document.addEventListener("QRDialogCreation", (e) => { document.addEventListener("QRDialogCreation", (e) => {
const a = document.createElement("a"); const target = e.target;
const bts = target.querySelector("#qr-filename-container");
const i = document.createElement("i"); const i = document.createElement("i");
i.className = "fa fa-magnet"; i.className = "fa fa-magnet";
const a = document.createElement("a");
a.appendChild(i); a.appendChild(i);
a.title = "Embed File (Select a file before...)"; a.title = "Embed File (Select a file before...)";
let target; bts?.appendChild(a);
if (cappState.is4chanX) {
i.innerText = "\u{1F9F2}";
target = e.detail;
target.querySelector("input[type=submit]")?.insertAdjacentElement("beforebegin", a);
} else {
target = e.target;
target.querySelector("#qr-filename-container")?.appendChild(a);
}
a.onclick = async (e2) => { a.onclick = async (e2) => {
const file = await getSelectedFile(); const file = await getSelectedFile();
if (!file) if (!file)
@ -15692,7 +15660,7 @@
}; };
input.click(); input.click();
}; };
}, { once: !cappState.is4chanX }); }, { once: true });
var customStyles = document.createElement("style"); var customStyles = document.createElement("style");
customStyles.appendChild(document.createTextNode(global_default)); customStyles.appendChild(document.createTextNode(global_default));
document.documentElement.insertBefore(customStyles, null); document.documentElement.insertBefore(customStyles, null);

View File

@ -247,11 +247,9 @@
<div <div
class:contract={contracted} class:contract={contracted}
class="place" class="place"
on:click={e => e.preventDefault()} on:click={e => e.preventDefault()}
on:auxclick={e => e.preventDefault()} on:auxclick={e => e.preventDefault()}
on:mousedown={bepis} on:mousedown={bepis}
on:mouseover={hoverStart} on:mouseover={hoverStart}
on:mouseout={hoverStop} on:mouseout={hoverStop}
on:mousemove={hoverUpdate} on:mousemove={hoverUpdate}
@ -260,11 +258,7 @@
> >
{#if isImage} {#if isImage}
<!-- svelte-ignore a11y-missing-attribute --> <!-- svelte-ignore a11y-missing-attribute -->
<img <img bind:this={imgElem} alt={file.filename} src={furl || url} />
bind:this={imgElem}
alt={file.filename}
src={furl || url}
/>
{/if} {/if}
{#if isAudio} {#if isAudio}
<audio <audio
@ -279,7 +273,7 @@
{#if isVideo} {#if isVideo}
<!-- svelte-ignore a11y-media-has-caption --> <!-- svelte-ignore a11y-media-has-caption -->
<!-- svelte-ignore a11y-missing-attribute --> <!-- svelte-ignore a11y-missing-attribute -->
<video loop={$settings.loop} bind:this={videoElem} src={furl || url} /> <video loop={$settings.loop} bind:this={videoElem} src={furl || url} />
<!-- assoom videos will never be loaded from thumbnails --> <!-- assoom videos will never be loaded from thumbnails -->
{/if} {/if}
</div> </div>

23
src/Embeddings.svelte Normal file
View File

@ -0,0 +1,23 @@
<script lang="ts">
import type { EmbeddedFile } from './main';
import { createEventDispatcher } from 'svelte';
import Embedding from './Embedding.svelte';
export const dispatch = createEventDispatcher();
export let files: EmbeddedFile[]
export let id: string = '';
let children: {[k in number]: Embedding}= {}
export async function bepis(ev: MouseEvent) {
for (let child of Object.values(children))
child.bepis(ev);
}
</script>
{#each files as file, i}
<Embedding bind:this={children[i]} {id} {file} />
{/each}
<style scoped>
</style>

View File

@ -1,14 +1,15 @@
<script lang="ts"> <script lang="ts">
import { fileTypeFromBuffer } from 'file-type'; import { fileTypeFromBuffer } from 'file-type';
import type Embedding from './Embedding.svelte'; import type Embedding from './Embedding.svelte';
import type Embeddings from './Embeddings.svelte';
import type { EmbeddedFile } from './main'; import type { EmbeddedFile } from './main';
import { settings } from './stores' import { settings } from './stores'
export let id = '' export let id = ''
export let file: EmbeddedFile; export let files: EmbeddedFile[];
export let inst: Embedding; export let inst: Embedding | Embeddings;
let isVideo = false let isVideo = false
@ -23,7 +24,7 @@ import type { EmbeddedFile } from './main';
} }
const isNotChrome = !navigator.userAgent.includes("Chrome/"); const isNotChrome = !navigator.userAgent.includes("Chrome/");
async function downloadFile() { async function downloadFile(file: EmbeddedFile) {
const a = document.createElement("a") as HTMLAnchorElement; const a = document.createElement("a") as HTMLAnchorElement;
document.body.appendChild(a); document.body.appendChild(a);
a.style.display = 'none'; a.style.display = 'none';
@ -45,9 +46,10 @@ import type { EmbeddedFile } from './main';
class="fa clickable" class="fa clickable"
/> />
{/if} {/if}
{#each files as file}
<span <span
title={file.filename} title={file.filename}
on:click={downloadFile} on:click={() => downloadFile(file)}
class="fa fa-download clickable" class="fa fa-download clickable"
/> />
{#if file.source} {#if file.source}
@ -75,6 +77,7 @@ import type { EmbeddedFile } from './main';
>[PEE contract]</a >[PEE contract]</a
> >
{/if} {/if}
{/each}
<style scoped> <style scoped>
.clickable { .clickable {

View File

@ -61,4 +61,6 @@ div.hasext .catalog-host img {
.fileThumb.filehost { .fileThumb.filehost {
margin-left: 0 !important; margin-left: 0 !important;
display: flex;
gap: 20px;
} }

View File

@ -12,7 +12,8 @@ import { GM_fetch, GM_head, headerStringToObject } from "./requests";
import App from "./App.svelte"; import App from "./App.svelte";
import ScrollHighlighter from "./ScrollHighlighter.svelte"; import ScrollHighlighter from "./ScrollHighlighter.svelte";
import SettingsButton from './SettingsButton.svelte'; import SettingsButton from './SettingsButton.svelte';
import Embedding from './Embedding.svelte'; //import Embedding from './Embedding.svelte';
import Embeddings from './Embeddings.svelte';
import EyeButton from './EyeButton.svelte'; import EyeButton from './EyeButton.svelte';
export interface ImageProcessor { export interface ImageProcessor {
@ -82,41 +83,40 @@ type EmbeddedFileWithoutPreview = {
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview; export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview;
const processImage = async (src: string, fn: string, hex: string): Promise<[EmbeddedFile, boolean] | undefined> => { const processImage = async (src: string, fn: string, hex: string): Promise<([EmbeddedFile, boolean] | undefined)[]> => {
const proc = processors.find(e => e.match(fn)); return Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
if (!proc) if (proc.skip) {
return; // skip file downloading, file is referenced from the filename
if (proc.skip) { // basically does things like filtering out blacklisted tags
// skip file downloading, file is referenced from the filename const md5 = Buffer.from(hex, 'base64');
// basically does things like filtering out blacklisted tags if (await proc.has_embed(md5, fn) === true)
const md5 = Buffer.from(hex, 'base64'); return [await proc.extract(md5, fn), true] as [EmbeddedFile, boolean];
if (await proc.has_embed(md5, fn) === true) return;
return [await proc.extract(md5, fn), true];
return;
}
const iter = streamRemote(src);
if (!iter)
return;
let cumul = Buffer.alloc(0);
let found: boolean | undefined;
let chunk: ReadableStreamDefaultReadResult<Buffer> = { done: true };
do {
const { value, done } = await iter.next(found === false);
if (done) {
chunk = { done: true } as ReadableStreamDefaultReadDoneResult;
} else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
} }
if (!done) const iter = streamRemote(src);
cumul = Buffer.concat([cumul, value!]); if (!iter)
found = await proc.has_embed(cumul); return;
} while (found !== false && !chunk.done); let cumul = Buffer.alloc(0);
await iter.next(false); let found: boolean | undefined;
if (found === false) { let chunk: ReadableStreamDefaultReadResult<Buffer> = { done: true };
//console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`); do {
return; const { value, done } = await iter.next(found === false);
} if (done) {
return [await proc.extract(cumul), false]; chunk = { done: true } as ReadableStreamDefaultReadDoneResult;
} else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
}
if (!done)
cumul = Buffer.concat([cumul, value!]);
found = await proc.has_embed(cumul);
} while (found !== false && !chunk.done);
await iter.next(false);
if (found === false) {
//console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`);
return;
}
return [await proc.extract(cumul), false] as [EmbeddedFile, boolean];
}));
}; };
const textToElement = <T = HTMLElement>(s: string) => const textToElement = <T = HTMLElement>(s: string) =>
@ -127,81 +127,13 @@ const processPost = async (post: HTMLDivElement) => {
const origlink = post.querySelector('.file-info > a[target*="_blank"]') as HTMLAnchorElement; const origlink = post.querySelector('.file-info > a[target*="_blank"]') as HTMLAnchorElement;
if (!thumb || !origlink) if (!thumb || !origlink)
return; return;
const res2 = await processImage(origlink.href, let res2 = await processImage(origlink.href,
(origlink.querySelector('.fnfull') || origlink).textContent || '', (origlink.querySelector('.fnfull') || origlink).textContent || '',
post.querySelector("[data-md5]")?.getAttribute('data-md5') || ''); post.querySelector("[data-md5]")?.getAttribute('data-md5') || '');
if (!res2) res2 = res2?.filter(e => e);
if (!res2 || res2.length == 0)
return; return;
const [res, external] = res2; processAttachments(post, res2?.filter(e => e) as [EmbeddedFile, boolean][]);
const replyBox = post.querySelector('.post');
if (external)
replyBox?.classList.add('hasext');
else
replyBox?.classList.add('hasembed');
if (!cappState.foundPosts.includes(replyBox as HTMLElement))
cappState.foundPosts.push(replyBox as HTMLElement);
appState.set(cappState);
const isCatalog = replyBox?.classList.contains('catalog-post');
// add buttons
if (!isCatalog) {
const ft = post.querySelector('div.file') as HTMLDivElement;
const info = post.querySelector("span.file-info") as HTMLSpanElement;
const filehost: HTMLElement | null = ft.querySelector('.filehost');
const eyehost: HTMLElement | null = info.querySelector('.eyehost');
const imgcont = filehost || document.createElement('div');
const eyecont = eyehost || document.createElement('span');
if (!filehost) {
ft.append(imgcont);
imgcont.classList.add("fileThumb");
imgcont.classList.add("filehost");
} else {
imgcont.innerHTML = '';
}
if (!eyehost) {
info.append(eyecont);
eyecont.classList.add("eyehost");
} else {
eyecont.innerHTML = '';
}
const id = ~~(Math.random() * 20000000);
const emb = new Embedding({
target: imgcont,
props: {
file: res,
id: '' + id
}
});
new EyeButton({
target: eyecont,
props: {
file: res,
inst: emb,
id: '' + id
}
});
} else {
const opFile = post.querySelector('.catalog-link');
const ahem = opFile?.querySelector('.catalog-host');
const imgcont = ahem || document.createElement('div');
imgcont.className = "catalog-host";
if (ahem) {
imgcont.innerHTML = '';
}
const emb = new Embedding({
target: imgcont,
props: {
file: res
}
});
if (!ahem)
opFile?.append(imgcont);
}
post.setAttribute('data-processed', "true");
}; };
const startup = async () => { const startup = async () => {
@ -261,7 +193,7 @@ const getSelectedFile = () => {
}; };
//if (cappState!.is4chanX) //if (cappState!.is4chanX)
document.addEventListener('4chanXInitFinished', startup); document.addEventListener('4chanXInitFinished', startup);
/*else { /*else {
document.addEventListener("QRGetFile", (e) => { document.addEventListener("QRGetFile", (e) => {
const qr = document.getElementById('qrFile') as HTMLInputElement | null; const qr = document.getElementById('qrFile') as HTMLInputElement | null;
@ -357,6 +289,78 @@ customStyles.appendChild(document.createTextNode(globalCss));
document.documentElement.insertBefore(customStyles, null); document.documentElement.insertBefore(customStyles, null);
function processAttachments(post: HTMLDivElement, ress: [EmbeddedFile, boolean][]) {
const replyBox = post.querySelector('.post');
const external = ress[0][1];
if (external)
replyBox?.classList.add('hasext');
else
replyBox?.classList.add('hasembed');
if (!cappState.foundPosts.includes(replyBox as HTMLElement))
cappState.foundPosts.push(replyBox as HTMLElement);
appState.set(cappState);
const isCatalog = replyBox?.classList.contains('catalog-post');
// add buttons
if (!isCatalog) {
const ft = post.querySelector('div.file') as HTMLDivElement;
const info = post.querySelector("span.file-info") as HTMLSpanElement;
const filehost: HTMLElement | null = ft.querySelector('.filehost');
const eyehost: HTMLElement | null = info.querySelector('.eyehost');
const imgcont = filehost || document.createElement('div');
const eyecont = eyehost || document.createElement('span');
if (!filehost) {
ft.append(imgcont);
imgcont.classList.add("fileThumb");
imgcont.classList.add("filehost");
} else {
imgcont.innerHTML = '';
}
if (!eyehost) {
info.append(eyecont);
eyecont.classList.add("eyehost");
} else {
eyecont.innerHTML = '';
}
const id = ~~(Math.random() * 20000000);
const emb = new Embeddings({
target: imgcont,
props: {
files: ress.map(e => e[0]),
id: '' + id
}
});
new EyeButton({
target: eyecont,
props: {
files: ress.map(e => e[0]),
inst: emb,
id: '' + id
}
});
} else {
const opFile = post.querySelector('.catalog-link');
const ahem = opFile?.querySelector('.catalog-host');
const imgcont = ahem || document.createElement('div');
imgcont.className = "catalog-host";
if (ahem) {
imgcont.innerHTML = '';
}
const emb = new Embeddings({
target: imgcont,
props: {
files: ress.map(e => e[0])
}
});
if (!ahem)
opFile?.append(imgcont);
}
post.setAttribute('data-processed', "true");
}
//if ((window as any)['pagemode']) { //if ((window as any)['pagemode']) {
// onload = () => { // onload = () => {
// console.log("loaded"); // console.log("loaded");

View File

@ -35,10 +35,6 @@ export const appState = writable({
foundPosts: [] as HTMLElement[] foundPosts: [] as HTMLElement[]
}); });
appState.subscribe(v => {
console.log(v);
});
settings.subscribe(newVal => { settings.subscribe(newVal => {
localSet('settings', newVal); localSet('settings', newVal);
}); });