atmosphere explorer
at main 104 lines 3.6 kB view raw
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};