atproto explorer pdsls.dev
atproto tool
at main 4.1 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 { 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};