atmosphere explorer

fetch image blob and create object url

handle.invalid db027778 1a7d02e7

verified
+67 -37
+59 -34
src/components/json.tsx
··· 7 7 ErrorBoundary, 8 8 For, 9 9 on, 10 + onCleanup, 11 + onMount, 10 12 Show, 11 13 useContext, 12 14 } from "solid-js"; ··· 260 262 !ctx.hideBlobs && 261 263 (blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"); 262 264 263 - const MediaDisplay = () => ( 264 - <div> 265 - <span class="group/media relative flex w-fit"> 266 - <Show when={!hide()}> 267 - <Show when={blob.mimeType.startsWith("image/")}> 268 - <img 269 - class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64" 270 - src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${ctx.repo}&cid=${blob.ref.$link}`} 271 - onLoad={() => setMediaLoaded(true)} 272 - /> 273 - </Show> 274 - <Show when={blob.mimeType === "video/mp4"}> 275 - <ErrorBoundary fallback={() => <span>Failed to load video</span>}> 276 - <VideoPlayer 277 - did={ctx.repo} 278 - cid={blob.ref.$link} 265 + const MediaDisplay = () => { 266 + const [imageObjectUrl, setImageObjectUrl] = createSignal<string>(); 267 + 268 + onMount(() => { 269 + if (blob.mimeType.startsWith("image/")) { 270 + const fetchImage = async () => { 271 + const res = await fetch( 272 + `https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${ctx.repo}&cid=${blob.ref.$link}`, 273 + ); 274 + if (!res.ok) throw new Error(res.statusText); 275 + const blobData = await res.blob(); 276 + const url = URL.createObjectURL(blobData); 277 + setImageObjectUrl(url); 278 + }; 279 + fetchImage().catch((err) => console.error("Failed to load image:", err)); 280 + } 281 + }); 282 + 283 + onCleanup(() => { 284 + if (imageObjectUrl()) URL.revokeObjectURL(imageObjectUrl()!); 285 + }); 286 + 287 + return ( 288 + <div> 289 + <span class="group/media relative flex w-fit"> 290 + <Show when={!hide()}> 291 + <Show when={blob.mimeType.startsWith("image/") && imageObjectUrl()}> 292 + <img 293 + class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64" 294 + src={imageObjectUrl()} 279 295 onLoad={() => setMediaLoaded(true)} 280 296 /> 281 - </ErrorBoundary> 297 + </Show> 298 + <Show when={blob.mimeType === "video/mp4"}> 299 + <ErrorBoundary fallback={() => <span>Failed to load video</span>}> 300 + <VideoPlayer 301 + did={ctx.repo} 302 + cid={blob.ref.$link} 303 + onLoad={() => setMediaLoaded(true)} 304 + /> 305 + </ErrorBoundary> 306 + </Show> 307 + <Show when={mediaLoaded()}> 308 + <button 309 + onclick={() => setHide(true)} 310 + class="absolute top-1 right-1 flex items-center rounded-lg bg-neutral-700/70 p-1.5 text-white opacity-0 backdrop-blur-sm transition-opacity group-hover/media:opacity-100 hover:bg-neutral-700 active:bg-neutral-800 dark:bg-neutral-100/70 dark:text-neutral-900 dark:hover:bg-neutral-100 dark:active:bg-neutral-200" 311 + > 312 + <span class="iconify lucide--eye-off text-base"></span> 313 + </button> 314 + </Show> 282 315 </Show> 283 - <Show when={mediaLoaded()}> 316 + <Show when={hide()}> 284 317 <button 285 - onclick={() => setHide(true)} 286 - class="absolute top-1 right-1 flex items-center rounded-lg bg-neutral-700/70 p-1.5 text-white opacity-0 backdrop-blur-sm transition-opacity group-hover/media:opacity-100 hover:bg-neutral-700 active:bg-neutral-800 dark:bg-neutral-100/70 dark:text-neutral-900 dark:hover:bg-neutral-100 dark:active:bg-neutral-200" 318 + onclick={() => setHide(false)} 319 + class="flex items-center gap-1 rounded-md bg-neutral-200 px-2 py-1.5 text-sm transition-colors hover:bg-neutral-300 active:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 287 320 > 288 - <span class="iconify lucide--eye-off text-base"></span> 321 + <span class="iconify lucide--image"></span> 322 + <span class="font-sans">Show media</span> 289 323 </button> 290 324 </Show> 291 - </Show> 292 - <Show when={hide()}> 293 - <button 294 - onclick={() => setHide(false)} 295 - class="flex items-center gap-1 rounded-md bg-neutral-200 px-2 py-1.5 text-sm transition-colors hover:bg-neutral-300 active:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:active:bg-neutral-500" 296 - > 297 - <span class="iconify lucide--image"></span> 298 - <span class="font-sans">Show media</span> 299 - </button> 300 - </Show> 301 - </span> 302 - </div> 303 - ); 325 + </span> 326 + </div> 327 + ); 328 + }; 304 329 305 330 if (blob.$type === "blob") { 306 331 return (
+8 -3
src/components/video-player.tsx
··· 1 - import { onMount } from "solid-js"; 1 + import { onCleanup, onMount } from "solid-js"; 2 2 import { pds } from "./navbar"; 3 3 4 4 export interface VideoPlayerProps { ··· 9 9 10 10 const VideoPlayer = (props: VideoPlayerProps) => { 11 11 let video!: HTMLVideoElement; 12 + let objectUrl: string | undefined; 12 13 13 14 onMount(async () => { 14 15 // thanks bf <3 ··· 17 18 ); 18 19 if (!res.ok) throw new Error(res.statusText); 19 20 const blob = await res.blob(); 20 - const url = URL.createObjectURL(blob); 21 - if (video) video.src = url; 21 + objectUrl = URL.createObjectURL(blob); 22 + if (video) video.src = objectUrl; 23 + }); 24 + 25 + onCleanup(() => { 26 + if (objectUrl) URL.revokeObjectURL(objectUrl); 22 27 }); 23 28 24 29 return (