Can embed any file in a PNG/WebM/GIF/JPEG and upload it to a third-party host through 4chan
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

256 lines
6.9 KiB

<script lang="ts">
import { appState, settings } from "../stores";
import type { ImageProcessor } from "../main";
import {
addToEmbeds,
embeddedToBlob,
externalDispatch,
fireNotification,
getFileFromHydrus,
uploadFiles,
} from "../utils";
import { fileTypeFromBuffer } from "file-type";
import { onMount } from "svelte";
export let processors: ImageProcessor[] = [];
export let textinput: HTMLTextAreaElement;
export let links: string[] = [];
let floating: HTMLDivElement;
const isParentOrSame = (p: HTMLElement, ofe: HTMLElement | null) => {
while (ofe && ofe != document.body) {
if (p == ofe) return true;
ofe = ofe.parentElement;
}
return false;
};
const handleGlobalPaste = (e: ClipboardEvent) => {
if (!e.clipboardData) {
return;
}
if (isParentOrSame(floating, e.target as HTMLElement)) {
if (e.clipboardData.files.length == 0) {
if (e.clipboardData.types.includes("text/plain")) {
const data = e.clipboardData.getData("text/plain");
addContent(
new File(
[new Blob([data], { type: "text/plain" })],
`message${links.length}.txt`
)
);
}
} else {
addContent(...[...e.clipboardData.files]);
}
e.stopImmediatePropagation();
e.stopPropagation();
return true;
}
};
onMount(() => {
document.addEventListener("paste", handleGlobalPaste, true);
});
const addContent = async (...newfiles: File[]) => {
links = [...links, ...(await uploadFiles(newfiles))];
return embedContent({} as any);
};
let original: File | undefined;
let currentEmbed: { file: File } | undefined;
function restore() {
externalDispatch("QRSetFile", { file: original });
}
// This is an event to signal a change in the container file
let inhibit = false;
const isSame = (a: File | null, b: File | null) => {
if (a == null || b == null) return false;
return (["size", "name", "lastModified"] as const).every(
(e) => a[e] == b[e]
);
};
document.addEventListener("PEEFile", async (e) => {
let file = (e as any).detail as File;
if (!currentEmbed || (!isSame(currentEmbed.file, file) && !inhibit)) {
original = file;
if ($settings.auto_embed && $appState.client) {
const tags = $settings.auto_tags
.split(" ")
.map((e) => e.replaceAll("_", " "));
const efs = await getFileFromHydrus(
$appState.client,
tags.concat(["system:limit=" + $settings.auto_embed]),
{ file_sort_type: 4 }
);
const files = await embeddedToBlob(...efs.map((e) => e[1]));
const nlinks = await uploadFiles(files);
links = [...links, ...nlinks];
}
inhibit = true;
await embedContent(e);
setTimeout(() => (inhibit = false), 500); // hack around 4chan(X)(?) inconsistent getFile
}
});
document.addEventListener("QRPostSuccessful", () => {
if (currentEmbed) {
links = []; // cleanup
currentEmbed = undefined;
original = undefined;
}
});
document.addEventListener("AddPEE", (e) => {
let link = (e as any).detail as string | string[];
links = links.concat(link);
embedContent(e);
});
const embedText = async (e: Event) => {
if (textinput.value == "") return;
if (textinput.value.length > 2000) {
fireNotification(
"error",
"Message attachments are limited to 2000 characters"
);
return;
}
await addContent(
new File(
[new Blob([textinput.value], { type: "text/plain" })],
`message${links.length}.txt`
)
);
textinput.value = "";
};
const downloadFile = (f: File | { file: File }) => {
let file: File;
if ("file" in f) file = f.file;
else file = f;
var element = document.createElement("a");
element.setAttribute("href", URL.createObjectURL(file));
element.setAttribute("download", file.name);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
const embedContent = async (e: Event) => {
let tfile: File | Blob | undefined = original;
if (!tfile) return;
if (links.length == 0) return;
const type = tfile.type;
let file: File;
if (!(tfile instanceof File)) {
const et = await fileTypeFromBuffer(await tfile.arrayBuffer());
if (!et) throw new Error("Unsupported container type");
file = new File([tfile], `file.${et.ext}`);
} else {
file = tfile;
}
try {
const proc = processors
.filter((e) => e.inject)
.find((e) => e.match(file.name));
if (!proc) throw new Error("Container filetype not supported");
const buff = await proc.inject!(file, links.slice(0, $settings.maxe));
currentEmbed = {
file: new File([buff], file.name, { type }),
} as { file: File };
//downloadFile(currentEmbed);
externalDispatch("QRSetFile", currentEmbed);
fireNotification(
"success",
`File${links.length > 1 ? "s" : ""} successfully embedded!`
);
} catch (err) {
const e = err as Error;
fireNotification("error", "Couldn't embed file: " + e.message);
}
};
const embedFile = async (e: Event) => {
const input = document.createElement("input") as HTMLInputElement;
input.setAttribute("type", "file");
input.multiple = true;
input.onchange = async (ev) => {
if (input.files) {
addContent(...input.files);
}
};
input.click();
};
</script>
<div class="root">
<!-- svelte-ignore a11y-missing-attribute -->
<a on:click={embedFile} title="Add a file">
<i class="fa fa-magnet"> {$appState.is4chanX ? "" : "🧲"} </i>
</a>
<div bind:this={floating} class="additionnal">
<!-- svelte-ignore a11y-missing-attribute -->
<a
on:click={embedText}
title="Add a message (this uses the content of the comment text box)"
>
<i class="fa fa-pencil"> {$appState.is4chanX ? "" : "🖉"} </i>
</a>
<!-- svelte-ignore a11y-missing-attribute -->
<a title="Add from clipboard (click this then CTRL+v)">
<i class="fa fa-clipboard"> {$appState.is4chanX ? "" : "📋"} </i>
</a>
{#if links.length}
<!-- svelte-ignore a11y-missing-attribute -->
<a
on:click={() => ((links = []), restore())}
title="Discard ALL {links.length} files"
>
<i class="fa fa-times"> {$appState.is4chanX ? "" : "❌"} </i>
</a>
{/if}
</div>
</div>
<style scoped>
a i {
font-style: normal;
}
a {
cursor: pointer;
}
.root {
position: relative;
}
.additionnal {
display: none;
position: absolute;
flex-direction: column;
gap: 5px;
outline: 1px solid #ce3d08;
padding: 5px;
background-color: #fffdee;
border-radius: 5px;
left: 50%;
transform: translateX(-50%);
}
.root:hover > .additionnal {
display: flex;
}
</style>