Browse Source

Support double embeds

pull/46/head
coomdev 2 years ago
parent
commit
27eaa92c82
  1. 2
      main.meta.js
  2. 110
      main.user.js
  3. 10
      src/Embedding.svelte
  4. 23
      src/Embeddings.svelte
  5. 11
      src/EyeButton.svelte
  6. 2
      src/global.css
  7. 220
      src/main.ts
  8. 4
      src/stores.ts

2
main.meta.js

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

110
main.user.js

@ -1,7 +1,7 @@
// ==UserScript==
// @name PNGExtraEmbed
// @namespace https://coom.tech/
// @version 0.100
// @version 0.98
// @description uhh
// @author You
// @match https://boards.4channel.org/*
@ -10924,7 +10924,6 @@
}));
var appState = writable({
isCatalog: false,
is4chanX: false,
foundPosts: []
});
appState.subscribe((v) => {
@ -11300,30 +11299,30 @@
xmlhttprequest(gmopt);
});
}
function GM_fetch(...[url, opt, lisn]) {
function blobTo(to, 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);
else if (to == "text")
fileReader.readAsText(blob, "utf-8");
function blobTo(to, 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
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) => {
const gmopt = {
url: url.toString(),
@ -11427,19 +11426,6 @@
preview_url: e.preview_url,
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();
@ -11449,7 +11435,7 @@
sources = new Set(s.sources);
});
var cache = {};
var findFileFrom = async (b, hex, abort) => {
var findFileFrom = async (b, hex) => {
try {
if (b.domain in cache && hex in cache[b.domain])
return cache[b.domain][hex];
@ -11509,7 +11495,7 @@
skip: true,
extract: extract4,
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
@ -15575,9 +15561,14 @@
post.setAttribute("data-processed", "true");
};
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)));
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) => {
for (const rec of reco)
if (rec.type == "childList")
@ -15609,7 +15600,7 @@
document.body.append(scrollHost);
appState.set({
...cappState,
isCatalog: !!document.querySelector(".catalog-small") || !!location.pathname.match(/\/catalog$/)
isCatalog: !!document.querySelector(".catalog-small")
});
await Promise.all(posts.map((e) => processPost(e)));
};
@ -15620,38 +15611,15 @@
});
};
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) => {
const a = document.createElement("a");
const target = e.target;
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...)";
let target;
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);
}
bts?.appendChild(a);
a.onclick = async (e2) => {
const file = await getSelectedFile();
if (!file)
@ -15692,7 +15660,7 @@
};
input.click();
};
}, { once: !cappState.is4chanX });
}, { once: true });
var customStyles = document.createElement("style");
customStyles.appendChild(document.createTextNode(global_default));
document.documentElement.insertBefore(customStyles, null);

10
src/Embedding.svelte

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

23
src/Embeddings.svelte

@ -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>

11
src/EyeButton.svelte

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

2
src/global.css

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

220
src/main.ts

@ -12,7 +12,8 @@ import { GM_fetch, GM_head, headerStringToObject } from "./requests";
import App from "./App.svelte";
import ScrollHighlighter from "./ScrollHighlighter.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';
export interface ImageProcessor {
@ -82,41 +83,40 @@ type EmbeddedFileWithoutPreview = {
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview;
const processImage = async (src: string, fn: string, hex: string): Promise<[EmbeddedFile, boolean] | undefined> => {
const proc = processors.find(e => e.match(fn));
if (!proc)
return;
if (proc.skip) {
// skip file downloading, file is referenced from the filename
// basically does things like filtering out blacklisted tags
const md5 = Buffer.from(hex, 'base64');
if (await proc.has_embed(md5, fn) === true)
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>;
const processImage = async (src: string, fn: string, hex: string): Promise<([EmbeddedFile, boolean] | undefined)[]> => {
return Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
if (proc.skip) {
// skip file downloading, file is referenced from the filename
// basically does things like filtering out blacklisted tags
const md5 = Buffer.from(hex, 'base64');
if (await proc.has_embed(md5, fn) === true)
return [await proc.extract(md5, fn), true] as [EmbeddedFile, boolean];
return;
}
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];
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)
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) =>
@ -127,81 +127,13 @@ const processPost = async (post: HTMLDivElement) => {
const origlink = post.querySelector('.file-info > a[target*="_blank"]') as HTMLAnchorElement;
if (!thumb || !origlink)
return;
const res2 = await processImage(origlink.href,
let res2 = await processImage(origlink.href,
(origlink.querySelector('.fnfull') || origlink).textContent || '',
post.querySelector("[data-md5]")?.getAttribute('data-md5') || '');
if (!res2)
res2 = res2?.filter(e => e);
if (!res2 || res2.length == 0)
return;
const [res, external] = res2;
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");
processAttachments(post, res2?.filter(e => e) as [EmbeddedFile, boolean][]);
};
const startup = async () => {
@ -261,7 +193,7 @@ const getSelectedFile = () => {
};
//if (cappState!.is4chanX)
document.addEventListener('4chanXInitFinished', startup);
document.addEventListener('4chanXInitFinished', startup);
/*else {
document.addEventListener("QRGetFile", (e) => {
const qr = document.getElementById('qrFile') as HTMLInputElement | null;
@ -357,6 +289,78 @@ customStyles.appendChild(document.createTextNode(globalCss));
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']) {
// onload = () => {
// console.log("loaded");

4
src/stores.ts

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

Loading…
Cancel
Save