forked from pdsls.dev/pdsls
this repo has no description
at main 6.5 kB view raw
1import VideoPlayer from "./video-player"; 2import { createEffect, createSignal, For, Show } from "solid-js"; 3import { A } from "@solidjs/router"; 4import { pds } from "./navbar"; 5import Tooltip from "./tooltip"; 6import { hideMedia } from "./settings"; 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" href={`/${part}`}> 39 {part} 40 </A> 41 : DID_RE.test(part) ? 42 <A class="text-blue-400 hover: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 51 class="text-blue-400 hover:underline" 52 href={part} 53 target="_blank" 54 rel="noopener noreferrer" 55 > 56 {part} 57 </a> 58 : part} 59 </> 60 )} 61 </For> 62 " 63 </span> 64 ); 65}; 66 67const JSONNumber = ({ data }: { data: number }) => { 68 return <span>{data}</span>; 69}; 70 71const JSONBoolean = ({ data }: { data: boolean }) => { 72 return <span>{data ? "true" : "false"}</span>; 73}; 74 75const JSONNull = () => { 76 return <span>null</span>; 77}; 78 79const JSONObject = ({ data, repo }: { data: { [x: string]: JSONType }; repo: string }) => { 80 const [hide, setHide] = createSignal(localStorage.hideMedia === "true"); 81 82 createEffect(() => setHide(hideMedia())); 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="max-w-40% sm:max-w-50% break-anywhere group/clip relative flex size-fit shrink-0 items-center text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-300" 96 onclick={() => setShow(!show())} 97 > 98 <span 99 classList={{ 100 "dark:bg-dark-500 absolute w-5 -left-5 bg-zinc-100 text-sm": true, 101 "hidden group-hover/clip:block": show(), 102 }} 103 > 104 {show() ? 105 <div class="i-lucide-chevron-down" /> 106 : <div class="i-lucide-chevron-right" />} 107 </span> 108 {key}: 109 </button> 110 <span 111 classList={{ 112 "self-center": value !== Object(value), 113 "pl-[calc(2ch-1px)] border-l-0.5 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 <span class="flex gap-x-1"> 134 <Show when={blob.mimeType.startsWith("image/") && !hide()}> 135 <a 136 href={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${blob.ref.$link}@jpeg`} 137 target="_blank" 138 > 139 <img 140 class="max-h-[16rem] w-full max-w-[16rem]" 141 src={`https://cdn.bsky.app/img/feed_thumbnail/plain/${repo}/${blob.ref.$link}@jpeg`} 142 /> 143 </a> 144 </Show> 145 <Show when={blob.mimeType === "video/mp4" && !hide()}> 146 <VideoPlayer did={repo} cid={blob.ref.$link} /> 147 </Show> 148 <span 149 classList={{ "flex items-center justify-between gap-2": true, "flex-col": !hide() }} 150 > 151 <Show when={blob.mimeType.startsWith("image/") || blob.mimeType === "video/mp4"}> 152 <Tooltip text={hide() ? "Show" : "Hide"}> 153 <button onclick={() => setHide(!hide())}> 154 <div class={`text-lg ${hide() ? "i-lucide-eye-off" : "i-lucide-eye"}`} /> 155 </button> 156 </Tooltip> 157 </Show> 158 <Show when={pds()}> 159 <a 160 href={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${repo}&cid=${blob.ref.$link}`} 161 target="_blank" 162 class="size-fit" 163 > 164 <Tooltip text="Blob link"> 165 <div class="i-lucide-external-link text-lg" /> 166 </Tooltip> 167 </a> 168 </Show> 169 </span> 170 </span> 171 {rawObj} 172 </> 173 ); 174 } 175 176 return rawObj; 177}; 178 179const JSONArray = ({ data, repo }: { data: JSONType[]; repo: string }) => { 180 return ( 181 <For each={data}> 182 {(value, index) => ( 183 <span 184 classList={{ 185 "flex before:content-['-']": true, 186 "mb-2": value === Object(value) && index() !== data.length - 1, 187 }} 188 > 189 <span class="ml-[1ch] w-full"> 190 <JSONValue data={value} repo={repo} /> 191 </span> 192 </span> 193 )} 194 </For> 195 ); 196}; 197 198export const JSONValue = ({ data, repo }: { data: JSONType; repo: string }) => { 199 if (typeof data === "string") return <JSONString data={data} />; 200 if (typeof data === "number") return <JSONNumber data={data} />; 201 if (typeof data === "boolean") return <JSONBoolean data={data} />; 202 if (data === null) return <JSONNull />; 203 if (Array.isArray(data)) return <JSONArray data={data} repo={repo} />; 204 return <JSONObject data={data} repo={repo} />; 205}; 206 207export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];