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