atproto explorer
at main 212 lines 7.1 kB view raw
1import { A, useParams } from "@solidjs/router"; 2import { createEffect, createSignal, ErrorBoundary, For, Show } from "solid-js"; 3import { hideMedia } from "../views/settings"; 4import { pds } from "./navbar"; 5import Tooltip from "./tooltip"; 6import VideoPlayer from "./video-player"; 7 8interface AtBlob { 9 $type: string; 10 ref: { $link: string }; 11 mimeType: string; 12} 13 14const ATURI_RE = 15 /^at:\/\/([a-zA-Z0-9._:%-]+)(?:\/([a-zA-Z0-9-.]+)(?:\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 16 17const DID_RE = /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/; 18 19const JSONString = ({ data }: { data: string }) => { 20 const isURL = 21 URL.canParse ?? 22 ((url, base) => { 23 try { 24 new URL(url, base); 25 return true; 26 } catch { 27 return false; 28 } 29 }); 30 31 return ( 32 <span> 33 " 34 <For each={data.split(/(\s)/)}> 35 {(part) => ( 36 <> 37 {ATURI_RE.test(part) ? 38 <A class="text-blue-400 hover:underline active:underline" href={`/${part}`}> 39 {part} 40 </A> 41 : DID_RE.test(part) ? 42 <A class="text-blue-400 hover:underline active:underline" href={`/at://${part}`}> 43 {part} 44 </A> 45 : ( 46 isURL(part) && 47 ["http:", "https:", "web+at:"].includes(new URL(part).protocol) && 48 part.split("\n").length === 1 49 ) ? 50 <a class="underline" href={part} target="_blank" rel="noopener noreferrer"> 51 {part} 52 </a> 53 : part} 54 </> 55 )} 56 </For> 57 " 58 </span> 59 ); 60}; 61 62const JSONNumber = ({ data }: { data: number }) => { 63 return <span>{data}</span>; 64}; 65 66const JSONBoolean = ({ data }: { data: boolean }) => { 67 return <span>{data ? "true" : "false"}</span>; 68}; 69 70const JSONNull = () => { 71 return <span>null</span>; 72}; 73 74const JSONObject = ({ data, repo }: { data: { [x: string]: JSONType }; repo: string }) => { 75 const params = useParams(); 76 const [hide, setHide] = createSignal( 77 localStorage.hideMedia === "true" || params.rkey === undefined, 78 ); 79 80 createEffect(() => { 81 if (hideMedia()) setHide(hideMedia()); 82 }); 83 84 const Obj = ({ key, value }: { key: string; value: JSONType }) => { 85 const [show, setShow] = createSignal(true); 86 87 return ( 88 <span 89 classList={{ 90 "group/indent flex gap-x-1 w-full": true, 91 "flex-col": value === Object(value), 92 }} 93 > 94 <button 95 class="group/clip relative flex size-fit max-w-[40%] shrink-0 items-center wrap-anywhere text-neutral-500 hover:text-neutral-700 active:text-neutral-700 sm:max-w-[50%] dark:text-neutral-400 dark:hover:text-neutral-300 dark:active:text-neutral-300" 96 onclick={() => setShow(!show())} 97 > 98 <span 99 classList={{ 100 "dark:bg-dark-500 absolute w-5 flex items-center -left-5 bg-neutral-100 text-sm": true, 101 "hidden group-hover/clip:flex": show(), 102 }} 103 > 104 {show() ? 105 <span class="iconify lucide--chevron-down"></span> 106 : <span class="iconify lucide--chevron-right"></span>} 107 </span> 108 {key}: 109 </button> 110 <span 111 classList={{ 112 "self-center": value !== Object(value), 113 "pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 dark:has-hover:group-hover/indent:border-neutral-300": 114 value === Object(value), 115 "invisible h-0": !show(), 116 }} 117 > 118 <JSONValue data={value} repo={repo} /> 119 </span> 120 </span> 121 ); 122 }; 123 124 const rawObj = ( 125 <For each={Object.entries(data)}>{([key, value]) => <Obj key={key} value={value} />}</For> 126 ); 127 128 const blob: AtBlob = data as any; 129 130 if (blob.$type === "blob") { 131 return ( 132 <> 133 <Show when={pds() && params.rkey}> 134 <span class="flex gap-x-1"> 135 <Show when={blob.mimeType.startsWith("image/") && !hide()}> 136 <img 137 class="max-h-[16rem] w-fit max-w-[16rem]" 138 src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`} 139 /> 140 </Show> 141 <Show when={blob.mimeType === "video/mp4" && !hide()}> 142 <ErrorBoundary fallback={() => <span>Failed to load video</span>}> 143 <VideoPlayer did={repo} cid={blob.ref.$link} /> 144 </ErrorBoundary> 145 </Show> 146 <span 147 classList={{ 148 "flex items-center justify-between gap-1": true, 149 "flex-col": !hide(), 150 }} 151 > 152 <Show when={blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"}> 153 <Tooltip text={hide() ? "Show" : "Hide"}> 154 <button 155 onclick={() => setHide(!hide())} 156 class={`${!hide() ? "-mt-1 -ml-0.5" : ""} flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 157 > 158 <span 159 class={`iconify text-base ${hide() ? "lucide--eye-off" : "lucide--eye"}`} 160 ></span> 161 </button> 162 </Tooltip> 163 </Show> 164 <Tooltip text="Blob on PDS"> 165 <a 166 href={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`} 167 target="_blank" 168 class={`${!hide() ? "-mb-1 -ml-0.5" : ""} flex items-center rounded-lg p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600`} 169 > 170 <span class="iconify lucide--external-link text-base"></span> 171 </a> 172 </Tooltip> 173 </span> 174 </span> 175 </Show> 176 {rawObj} 177 </> 178 ); 179 } 180 181 return rawObj; 182}; 183 184const JSONArray = ({ data, repo }: { data: JSONType[]; repo: string }) => { 185 return ( 186 <For each={data}> 187 {(value, index) => ( 188 <span 189 classList={{ 190 "flex before:content-['-']": true, 191 "mb-2": value === Object(value) && index() !== data.length - 1, 192 }} 193 > 194 <span class="ml-[1ch] w-full"> 195 <JSONValue data={value} repo={repo} /> 196 </span> 197 </span> 198 )} 199 </For> 200 ); 201}; 202 203export const JSONValue = ({ data, repo }: { data: JSONType; repo: string }) => { 204 if (typeof data === "string") return <JSONString data={data} />; 205 if (typeof data === "number") return <JSONNumber data={data} />; 206 if (typeof data === "boolean") return <JSONBoolean data={data} />; 207 if (data === null) return <JSONNull />; 208 if (Array.isArray(data)) return <JSONArray data={data} repo={repo} />; 209 return <JSONObject data={data} repo={repo} />; 210}; 211 212export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];