1import { Client } from "@atcute/client";
2import { remove } from "@mary/exif-rm";
3import { createSignal, onCleanup, Show } from "solid-js";
4import { agent } from "../../auth/state";
5import { Button } from "../button.jsx";
6import { TextInput } from "../text-input.jsx";
7import { editorInstance } from "./state";
8
9export const FileUpload = (props: {
10 file: File;
11 blobInput: HTMLInputElement;
12 onClose: () => void;
13}) => {
14 const [uploading, setUploading] = createSignal(false);
15 const [error, setError] = createSignal("");
16
17 onCleanup(() => (props.blobInput.value = ""));
18
19 const formatFileSize = (bytes: number) => {
20 if (bytes === 0) return "0 Bytes";
21 const k = 1024;
22 const sizes = ["Bytes", "KB", "MB", "GB"];
23 const i = Math.floor(Math.log(bytes) / Math.log(k));
24 return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
25 };
26
27 const uploadBlob = async () => {
28 let blob: Blob;
29
30 const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value;
31 (document.getElementById("mimetype") as HTMLInputElement).value = "";
32 if (mimetype) blob = new Blob([props.file], { type: mimetype });
33 else blob = props.file;
34
35 if ((document.getElementById("exif-rm") as HTMLInputElement).checked) {
36 const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer()));
37 if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type });
38 }
39
40 const rpc = new Client({ handler: agent()! });
41 setUploading(true);
42 const res = await rpc.post("com.atproto.repo.uploadBlob", {
43 input: blob,
44 });
45 setUploading(false);
46 if (!res.ok) {
47 setError(res.data.error);
48 return;
49 }
50 editorInstance.view.dispatch({
51 changes: {
52 from: editorInstance.view.state.selection.main.head,
53 insert: JSON.stringify(res.data.blob, null, 2),
54 },
55 });
56 props.onClose();
57 };
58
59 return (
60 <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] w-[20rem] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0">
61 <h2 class="mb-2 font-semibold">Upload blob</h2>
62 <div class="flex flex-col gap-2 text-sm">
63 <div class="flex flex-col gap-1">
64 <p class="flex gap-1">
65 <span class="truncate">{props.file.name}</span>
66 <span class="shrink-0 text-neutral-600 dark:text-neutral-400">
67 ({formatFileSize(props.file.size)})
68 </span>
69 </p>
70 </div>
71 <div class="flex items-center gap-x-2">
72 <label for="mimetype" class="shrink-0 select-none">
73 MIME type
74 </label>
75 <TextInput id="mimetype" placeholder={props.file.type} />
76 </div>
77 <div class="flex items-center gap-1">
78 <input id="exif-rm" type="checkbox" checked />
79 <label for="exif-rm" class="select-none">
80 Remove EXIF data
81 </label>
82 </div>
83 <p class="text-xs text-neutral-600 dark:text-neutral-400">
84 Metadata will be pasted after the cursor
85 </p>
86 <Show when={error()}>
87 <span class="text-red-500 dark:text-red-400">Error: {error()}</span>
88 </Show>
89 <div class="flex justify-between gap-2">
90 <Button onClick={props.onClose}>Cancel</Button>
91 <Show when={uploading()}>
92 <div class="flex items-center gap-1">
93 <span class="iconify lucide--loader-circle animate-spin"></span>
94 <span>Uploading</span>
95 </div>
96 </Show>
97 <Show when={!uploading()}>
98 <Button
99 onClick={uploadBlob}
100 class="dark:shadow-dark-700 flex items-center gap-1 rounded-lg bg-blue-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 dark:active:bg-blue-400"
101 >
102 Upload
103 </Button>
104 </Show>
105 </div>
106 </div>
107 </div>
108 );
109};