forked from pdsls.dev/pdsls
atproto explorer

upload file modal

juli.ee 9156720f 175ea4d2

verified
Changed files
+131 -74
src
+1 -1
src/components/button.tsx
··· 13 13 type="button" 14 14 class={ 15 15 props.class ?? 16 - "dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs font-semibold shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 16 + "dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 17 17 } 18 18 classList={props.classList} 19 19 onClick={props.onClick}
+127 -70
src/components/create.tsx
··· 1 1 import { Client } from "@atcute/client"; 2 2 import { remove } from "@mary/exif-rm"; 3 3 import { useNavigate, useParams } from "@solidjs/router"; 4 - import { createSignal, Show } from "solid-js"; 4 + import { createSignal, onCleanup, Show } from "solid-js"; 5 5 import { Editor, editorView } from "../components/editor.jsx"; 6 6 import { agent } from "../components/login.jsx"; 7 7 import { setNotif } from "../layout.jsx"; ··· 15 15 const params = useParams(); 16 16 const [openDialog, setOpenDialog] = createSignal(false); 17 17 const [notice, setNotice] = createSignal(""); 18 - const [uploading, setUploading] = createSignal(false); 18 + const [openUpload, setOpenUpload] = createSignal(false); 19 + let blobInput!: HTMLInputElement; 19 20 let formRef!: HTMLFormElement; 20 21 21 22 const placeholder = () => { ··· 125 126 } 126 127 }; 127 128 128 - const uploadBlob = async () => { 129 - setNotice(""); 130 - let blob: Blob; 129 + const FileUpload = (props: { file: File }) => { 130 + const [uploading, setUploading] = createSignal(false); 131 + const [error, setError] = createSignal(""); 131 132 132 - const file = (document.getElementById("blob") as HTMLInputElement)?.files?.[0]; 133 - if (!file) return; 133 + onCleanup(() => (blobInput.value = "")); 134 134 135 - const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; 136 - (document.getElementById("mimetype") as HTMLInputElement).value = ""; 137 - if (mimetype) blob = new Blob([file], { type: mimetype }); 138 - else blob = file; 135 + const formatFileSize = (bytes: number) => { 136 + if (bytes === 0) return "0 Bytes"; 137 + const k = 1024; 138 + const sizes = ["Bytes", "KB", "MB", "GB"]; 139 + const i = Math.floor(Math.log(bytes) / Math.log(k)); 140 + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i]; 141 + }; 142 + 143 + const uploadBlob = async () => { 144 + let blob: Blob; 145 + 146 + const mimetype = (document.getElementById("mimetype") as HTMLInputElement)?.value; 147 + (document.getElementById("mimetype") as HTMLInputElement).value = ""; 148 + if (mimetype) blob = new Blob([props.file], { type: mimetype }); 149 + else blob = props.file; 139 150 140 - if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { 141 - const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); 142 - if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); 143 - } 151 + if ((document.getElementById("exif-rm") as HTMLInputElement).checked) { 152 + const exifRemoved = remove(new Uint8Array(await blob.arrayBuffer())); 153 + if (exifRemoved !== null) blob = new Blob([exifRemoved], { type: blob.type }); 154 + } 144 155 145 - const rpc = new Client({ handler: agent()! }); 146 - setUploading(true); 147 - const res = await rpc.post("com.atproto.repo.uploadBlob", { 148 - input: blob, 149 - }); 150 - setUploading(false); 151 - (document.getElementById("blob") as HTMLInputElement).value = ""; 152 - if (!res.ok) { 153 - setNotice(res.data.error); 154 - return; 155 - } 156 - editorView.dispatch({ 157 - changes: { 158 - from: editorView.state.selection.main.head, 159 - insert: JSON.stringify(res.data.blob, null, 2), 160 - }, 161 - }); 156 + const rpc = new Client({ handler: agent()! }); 157 + setUploading(true); 158 + const res = await rpc.post("com.atproto.repo.uploadBlob", { 159 + input: blob, 160 + }); 161 + setUploading(false); 162 + if (!res.ok) { 163 + setError(res.data.error); 164 + return; 165 + } 166 + editorView.dispatch({ 167 + changes: { 168 + from: editorView.state.selection.main.head, 169 + insert: JSON.stringify(res.data.blob, null, 2), 170 + }, 171 + }); 172 + setOpenUpload(false); 173 + }; 174 + 175 + return ( 176 + <div class="dark:bg-dark-300 dark:shadow-dark-800 absolute top-70 left-[50%] max-w-[20rem] min-w-[16rem] -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"> 177 + <h2 class="mb-2 font-semibold">Upload blob</h2> 178 + <div class="flex flex-col gap-2"> 179 + <div class="flex flex-col gap-1"> 180 + <p class="flex gap-1"> 181 + <span class="truncate">{props.file.name}</span> 182 + <span class="shrink-0 text-neutral-600 dark:text-neutral-400"> 183 + ({formatFileSize(props.file.size)}) 184 + </span> 185 + </p> 186 + </div> 187 + <div class="flex items-center gap-x-2"> 188 + <label for="mimetype" class="shrink-0 select-none"> 189 + MIME type 190 + </label> 191 + <TextInput id="mimetype" placeholder={props.file.type} /> 192 + </div> 193 + <div class="flex items-center gap-1"> 194 + <input id="exif-rm" type="checkbox" checked /> 195 + <label for="exif-rm" class="select-none"> 196 + Remove EXIF data 197 + </label> 198 + </div> 199 + <p class="text-xs text-neutral-600 dark:text-neutral-400"> 200 + Metadata will be pasted after the cursor 201 + </p> 202 + <Show when={error()}> 203 + <span class="text-red-500 dark:text-red-400">Error: {error()}</span> 204 + </Show> 205 + <div class="flex justify-between gap-2"> 206 + <Button onClick={() => setOpenUpload(false)}>Cancel</Button> 207 + <Show when={uploading()}> 208 + <div class="flex items-center gap-1"> 209 + <span class="iconify lucide--loader-circle animate-spin"></span> 210 + <span>Uploading</span> 211 + </div> 212 + </Show> 213 + <Show when={!uploading()}> 214 + <Button 215 + onClick={uploadBlob} 216 + class="dark:shadow-dark-800 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" 217 + > 218 + Upload 219 + </Button> 220 + </Show> 221 + </div> 222 + </div> 223 + </div> 224 + ); 162 225 }; 163 226 164 227 return ( ··· 205 268 /> 206 269 </div> 207 270 </Show> 208 - <div class="flex items-center gap-x-2"> 209 - <label for="validate" class="min-w-20 select-none"> 210 - Validate 211 - </label> 212 - <select 213 - name="validate" 214 - id="validate" 215 - class="dark:bg-dark-100 dark:shadow-dark-800 rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 shadow-xs focus:outline-[1px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200" 216 - > 217 - <option value="unset">Unset</option> 218 - <option value="true">True</option> 219 - <option value="false">False</option> 220 - </select> 221 - </div> 222 - <div class="flex items-center gap-2"> 223 - <Show when={!uploading()}> 224 - <div class="dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 flex rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 text-xs font-semibold shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"> 225 - <input type="file" id="blob" class="sr-only" onChange={() => uploadBlob()} /> 226 - <label class="flex items-center gap-1 px-2 py-1.5 select-none" for="blob"> 227 - <span class="iconify lucide--upload text-sm"></span> 228 - Upload 229 - </label> 230 - </div> 231 - <p class="text-xs">Metadata will be pasted after the cursor</p> 232 - </Show> 233 - <Show when={uploading()}> 234 - <span class="iconify lucide--loader-circle animate-spin text-xl"></span> 235 - <p>Uploading...</p> 236 - </Show> 237 - </div> 238 - <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> 271 + <div class="flex justify-between"> 239 272 <div class="flex items-center gap-x-2"> 240 - <label for="mimetype" class="min-w-20 select-none"> 241 - MIME type 273 + <label for="validate" class="min-w-20 select-none"> 274 + Validate 242 275 </label> 243 - <TextInput id="mimetype" placeholder="Optional" class="w-[15rem]" /> 276 + <select 277 + name="validate" 278 + id="validate" 279 + class="dark:bg-dark-100 dark:shadow-dark-800 rounded-lg border-[0.5px] border-neutral-300 bg-white px-1 py-1 shadow-xs focus:outline-[1px] focus:outline-neutral-900 dark:border-neutral-700 dark:focus:outline-neutral-200" 280 + > 281 + <option value="unset">Unset</option> 282 + <option value="true">True</option> 283 + <option value="false">False</option> 284 + </select> 244 285 </div> 245 - <div class="flex items-center gap-1"> 246 - <input id="exif-rm" type="checkbox" checked /> 247 - <label for="exif-rm" class="select-none"> 248 - Remove EXIF data 286 + <div class="dark:hover:bg-dark-200 dark:shadow-dark-800 dark:active:bg-dark-100 flex rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 text-xs shadow-xs hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800"> 287 + <input 288 + type="file" 289 + id="blob" 290 + class="sr-only" 291 + ref={blobInput} 292 + onChange={(e) => { 293 + if (e.target.files !== null) setOpenUpload(true); 294 + }} 295 + /> 296 + <label class="flex items-center gap-1 px-2 py-1.5 select-none" for="blob"> 297 + <span class="iconify lucide--upload text-sm"></span> 298 + Upload 249 299 </label> 250 300 </div> 301 + <Modal 302 + open={openUpload()} 303 + onClose={() => setOpenUpload(false)} 304 + closeOnClick={false} 305 + > 306 + <FileUpload file={blobInput.files![0]} /> 307 + </Modal> 251 308 </div> 252 309 </div> 253 310 <Editor
+2 -2
src/views/collection.tsx
··· 267 267 <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 268 268 <Button 269 269 onClick={deleteRecords} 270 - class={`dark:shadow-dark-800 rounded-lg px-2 py-1.5 text-xs font-semibold text-neutral-200 shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`} 270 + class={`dark:shadow-dark-800 rounded-lg px-2 py-1.5 text-xs text-white shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`} 271 271 > 272 272 {recreate() ? "Recreate" : "Delete"} 273 273 </Button> ··· 301 301 }} 302 302 > 303 303 <span 304 - class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"} text-sm`} 304 + class={`iconify ${reverse() ? "lucide--rotate-ccw" : "lucide--rotate-cw"}`} 305 305 ></span> 306 306 Reverse 307 307 </Button>
+1 -1
src/views/record.tsx
··· 177 177 <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 178 178 <Button 179 179 onClick={deleteRecord} 180 - class="dark:shadow-dark-800 rounded-lg bg-red-500 px-2 py-1.5 text-xs font-semibold text-neutral-200 shadow-xs select-none hover:bg-red-400 active:bg-red-400" 180 + class="dark:shadow-dark-800 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400" 181 181 > 182 182 Delete 183 183 </Button>