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