experiments of a tiny cytube-like player with yt-dlp
0

Configure Feed

Select the types of activity you want to include in your feed.

playlist: merge playlist & queue, better caching, DST tests

karitham (Jun 1, 2026, 12:55 AM +0200) c8a997d0 98b90ee1

+1285 -667
+121
Cargo.lock
··· 182 182 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 183 183 184 184 [[package]] 185 + name = "bit-set" 186 + version = "0.8.0" 187 + source = "registry+https://github.com/rust-lang/crates.io-index" 188 + checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" 189 + dependencies = [ 190 + "bit-vec", 191 + ] 192 + 193 + [[package]] 194 + name = "bit-vec" 195 + version = "0.8.0" 196 + source = "registry+https://github.com/rust-lang/crates.io-index" 197 + checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 198 + 199 + [[package]] 185 200 name = "bitflags" 186 201 version = "2.11.1" 187 202 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 457 472 ] 458 473 459 474 [[package]] 475 + name = "fastrand" 476 + version = "2.4.1" 477 + source = "registry+https://github.com/rust-lang/crates.io-index" 478 + checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" 479 + 480 + [[package]] 460 481 name = "find-msvc-tools" 461 482 version = "0.1.9" 462 483 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 472 493 "futures-sink", 473 494 "spin", 474 495 ] 496 + 497 + [[package]] 498 + name = "fnv" 499 + version = "1.0.7" 500 + source = "registry+https://github.com/rust-lang/crates.io-index" 501 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 475 502 476 503 [[package]] 477 504 name = "foldhash" ··· 988 1015 ] 989 1016 990 1017 [[package]] 1018 + name = "linux-raw-sys" 1019 + version = "0.12.1" 1020 + source = "registry+https://github.com/rust-lang/crates.io-index" 1021 + checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" 1022 + 1023 + [[package]] 991 1024 name = "litemap" 992 1025 version = "0.8.2" 993 1026 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1077 1110 "clap", 1078 1111 "futures-util", 1079 1112 "mime_guess", 1113 + "proptest", 1080 1114 "serde", 1081 1115 "serde_json", 1082 1116 "sqlx", ··· 1277 1311 ] 1278 1312 1279 1313 [[package]] 1314 + name = "proptest" 1315 + version = "1.11.0" 1316 + source = "registry+https://github.com/rust-lang/crates.io-index" 1317 + checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" 1318 + dependencies = [ 1319 + "bit-set", 1320 + "bit-vec", 1321 + "bitflags", 1322 + "num-traits", 1323 + "rand 0.9.4", 1324 + "rand_chacha 0.9.0", 1325 + "rand_xorshift", 1326 + "regex-syntax", 1327 + "rusty-fork", 1328 + "tempfile", 1329 + "unarray", 1330 + ] 1331 + 1332 + [[package]] 1333 + name = "quick-error" 1334 + version = "1.2.3" 1335 + source = "registry+https://github.com/rust-lang/crates.io-index" 1336 + checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 1337 + 1338 + [[package]] 1280 1339 name = "quote" 1281 1340 version = "1.0.45" 1282 1341 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1357 1416 ] 1358 1417 1359 1418 [[package]] 1419 + name = "rand_xorshift" 1420 + version = "0.4.0" 1421 + source = "registry+https://github.com/rust-lang/crates.io-index" 1422 + checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" 1423 + dependencies = [ 1424 + "rand_core 0.9.5", 1425 + ] 1426 + 1427 + [[package]] 1360 1428 name = "redox_syscall" 1361 1429 version = "0.5.18" 1362 1430 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1412 1480 ] 1413 1481 1414 1482 [[package]] 1483 + name = "rustix" 1484 + version = "1.1.4" 1485 + source = "registry+https://github.com/rust-lang/crates.io-index" 1486 + checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" 1487 + dependencies = [ 1488 + "bitflags", 1489 + "errno", 1490 + "libc", 1491 + "linux-raw-sys", 1492 + "windows-sys 0.61.2", 1493 + ] 1494 + 1495 + [[package]] 1415 1496 name = "rustversion" 1416 1497 version = "1.0.22" 1417 1498 source = "registry+https://github.com/rust-lang/crates.io-index" 1418 1499 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 1419 1500 1420 1501 [[package]] 1502 + name = "rusty-fork" 1503 + version = "0.3.1" 1504 + source = "registry+https://github.com/rust-lang/crates.io-index" 1505 + checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" 1506 + dependencies = [ 1507 + "fnv", 1508 + "quick-error", 1509 + "tempfile", 1510 + "wait-timeout", 1511 + ] 1512 + 1513 + [[package]] 1421 1514 name = "ryu" 1422 1515 version = "1.0.23" 1423 1516 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1852 1945 ] 1853 1946 1854 1947 [[package]] 1948 + name = "tempfile" 1949 + version = "3.27.0" 1950 + source = "registry+https://github.com/rust-lang/crates.io-index" 1951 + checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" 1952 + dependencies = [ 1953 + "fastrand", 1954 + "getrandom 0.4.2", 1955 + "once_cell", 1956 + "rustix", 1957 + "windows-sys 0.61.2", 1958 + ] 1959 + 1960 + [[package]] 1855 1961 name = "thiserror" 1856 1962 version = "2.0.18" 1857 1963 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2107 2213 checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" 2108 2214 2109 2215 [[package]] 2216 + name = "unarray" 2217 + version = "0.1.4" 2218 + source = "registry+https://github.com/rust-lang/crates.io-index" 2219 + checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" 2220 + 2221 + [[package]] 2110 2222 name = "unicase" 2111 2223 version = "2.9.0" 2112 2224 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2198 2310 version = "0.9.5" 2199 2311 source = "registry+https://github.com/rust-lang/crates.io-index" 2200 2312 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2313 + 2314 + [[package]] 2315 + name = "wait-timeout" 2316 + version = "0.2.1" 2317 + source = "registry+https://github.com/rust-lang/crates.io-index" 2318 + checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 2319 + dependencies = [ 2320 + "libc", 2321 + ] 2201 2322 2202 2323 [[package]] 2203 2324 name = "wasi"
+1
Cargo.toml
··· 25 25 26 26 [dev-dependencies] 27 27 tower = { version = "0.5", features = ["util"] } 28 + proptest = "1" 28 29 29 30 [profile.release] 30 31 lto = "fat"
+23 -137
frontend/src/App.tsx
··· 1 - import { createSignal, createEffect, onSettled, untrack, type Component } from "solid-js"; 1 + import { createSignal, untrack, type Component } from "solid-js"; 2 2 import { Router, Route, useParams, useNavigate } from "@solidjs/router"; 3 3 import { Player } from "./Player"; 4 4 import { Chat } from "./Chat"; 5 5 import { Queue } from "./Queue"; 6 - import { connectWs } from "./ws"; 7 - import type { RoomState, ChatMessage, ClientMessage } from "./types"; 6 + import { useAutoDismiss, useRoomConnection } from "./hooks"; 7 + import { applyTheme, cycleTheme, storedTheme } from "./theme"; 8 8 9 9 /// Landing page: create or join a room. 10 10 const Home: Component = () => { ··· 13 13 const [creating, setCreating] = createSignal(false); 14 14 const [roomInput, setRoomInput] = createSignal(""); 15 15 const [homeError, setHomeError] = createSignal<string | null>(null); 16 + 17 + useAutoDismiss([homeError, setHomeError]); 16 18 17 19 const createRoom = async () => { 18 20 if (!name().trim()) return; ··· 28 30 setCreating(false); 29 31 } 30 32 }; 31 - 32 - createEffect( 33 - () => homeError(), 34 - (err) => { 35 - if (err) { 36 - const timer = setTimeout(() => setHomeError(null), 5000); 37 - return () => clearTimeout(timer); 38 - } 39 - }, 40 - ); 41 33 42 34 const joinRoom = () => { 43 35 const id = roomInput().trim(); ··· 91 83 const userName = searchParams.get("name") || "anonymous"; 92 84 const navigate = useNavigate(); 93 85 94 - // Theme: persisted in localStorage, defaults to system preference. 95 - const [theme, setTheme] = createSignal(localStorage.getItem("moqbox-theme") || "system"); 96 - const applyTheme = (t: string) => { 97 - const root = document.documentElement; 98 - if (t === "latte") { 99 - root.setAttribute("data-theme", "latte"); 100 - root.style.colorScheme = "light"; 101 - } else if (t === "macchiato") { 102 - root.setAttribute("data-theme", "macchiato"); 103 - root.style.colorScheme = "dark"; 104 - } else { 105 - root.removeAttribute("data-theme"); 106 - root.style.colorScheme = "light dark"; 107 - } 108 - localStorage.setItem("moqbox-theme", t); 109 - setTheme(t); 110 - }; 111 - const cycleTheme = () => { 112 - const next = theme() === "macchiato" ? "latte" : theme() === "latte" ? "system" : "macchiato"; 86 + const [theme, setTheme] = createSignal(storedTheme()); 87 + const doCycle = () => { 88 + const next = cycleTheme(theme()); 113 89 applyTheme(next); 114 - }; 115 - 116 - const [state, setState] = createSignal<RoomState | null>(null); 117 - const [chats, setChats] = createSignal<ChatMessage[]>([]); 118 - const [connected, setConnected] = createSignal(false); 119 - const [error, setError] = createSignal<string | null>(null); 120 - const initialLoad = { current: true }; 121 - 122 - // Track the send function as a signal so the reconnect effect sees changes. 123 - const [sendFn, setSendFn] = createSignal<((msg: ClientMessage) => void) | null>(null); 124 - 125 - const startConnecting = () => { 126 - // Don't reconnect if we already have a live sendFn. 127 - if (sendFn()) return; 128 - 129 - const send = connectWs(roomId, userName, { 130 - onState: (s) => { 131 - initialLoad.current = false; 132 - setState(s); 133 - }, 134 - onChat: (msg) => 135 - setChats((prev) => { 136 - const next = [...prev, msg]; 137 - return next.length > 200 ? next.slice(-200) : next; 138 - }), 139 - onDisconnect: () => { 140 - setConnected(false); 141 - setSendFn(null); 142 - }, 143 - onError: (reason) => { 144 - setError(reason); 145 - }, 146 - }); 147 - setSendFn(() => send); 148 - setConnected(true); 149 - setError(null); 150 - }; 151 - 152 - // Initial connection once the component settles (replaces onMount in Solid 2.x). 153 - // Must not call startConnecting in the component body — signal writes inside an 154 - // owned reactive scope trigger SIGNAL_WRITE_IN_OWNED_SCOPE. 155 - onSettled(() => { 156 - startConnecting(); 157 - setTimeout(() => { 158 - if (initialLoad.current) { 159 - setError("Room not found or unavailable"); 160 - } 161 - }, 8000); 162 - }); 163 - 164 - // Reconnect on disconnect with backoff. 165 - createEffect( 166 - () => [connected(), sendFn()], 167 - ([isConnected, send]) => { 168 - if (!isConnected && !send) { 169 - const timer = setTimeout(startConnecting, 3000); 170 - return () => clearTimeout(timer); 171 - } 172 - }, 173 - ); 174 - 175 - // Auto-dismiss error after 5s (except "not found" which is permanent). 176 - createEffect( 177 - () => error(), 178 - (err) => { 179 - if (err && !err.includes("not found")) { 180 - const timer = setTimeout(() => setError(null), 5000); 181 - return () => clearTimeout(timer); 182 - } 183 - }, 184 - ); 185 - 186 - const sendChat = (content: string) => { 187 - sendFn()?.({ type: "chat", content }); 188 - }; 189 - 190 - const skip = () => { 191 - sendFn()?.({ type: "skip" }); 90 + setTheme(next); 192 91 }; 193 92 194 - const trackEnded = () => { 195 - const track = state()?.current_track; 196 - if (track) { 197 - sendFn()?.({ type: "track_ended", item_id: track.id }); 198 - } 199 - }; 93 + const { state, chats, connected, error, setError, send } = useRoomConnection(roomId, userName); 200 94 201 - // Unified add URL state. 202 95 const [showAdd, setShowAdd] = createSignal(false); 203 96 const [addUrl, setAddUrl] = createSignal(""); 204 97 const [addTarget, setAddTarget] = createSignal<"queue" | "playlist">("queue"); ··· 252 145 {error() && ( 253 146 <div class="toast toast-error" role="alert"> 254 147 <span>{error()}</span> 255 - <button 256 - type="button" 257 - onClick={() => navigate("/")} 258 - style="margin-left:0.5rem;background:rgba(0,0,0,0.15);border:1px solid rgba(255,255,255,0.2);color:inherit;font-size:0.75rem;padding:0.15rem 0.5rem;border-radius:3px;cursor:pointer;" 259 - > 148 + <button type="button" class="toast-action" onClick={() => navigate("/")}> 260 149 Go back 261 150 </button> 262 151 </div> ··· 264 153 265 154 <div class="room-layout"> 266 155 <div class="room-main"> 267 - <Player track={state()?.current_track ?? null} roomId={roomId} onEnded={trackEnded} /> 156 + <Player 157 + track={state()?.current_track ?? null} 158 + roomId={roomId} 159 + onEnded={() => { 160 + const track = state()?.current_track; 161 + if (track) send({ type: "track_ended", item_id: track.id }); 162 + }} 163 + /> 268 164 269 165 <div class="room-actions"> 270 166 <button ··· 278 174 <button 279 175 type="button" 280 176 class="secondary" 281 - onClick={skip} 177 + onClick={() => send({ type: "skip" })} 282 178 disabled={!state()?.current_track} 283 179 > 284 180 Skip ··· 324 220 history={state()?.history ?? []} 325 221 playlist={state()?.playlist ?? []} 326 222 /> 327 - <Chat messages={chats()} onSend={sendChat} /> 223 + <Chat messages={chats()} onSend={(content) => send({ type: "chat", content })} /> 328 224 <div class="room-footer"> 329 225 <button 330 226 type="button" 331 227 class="theme-toggle-sm" 332 - onClick={cycleTheme} 228 + onClick={doCycle} 333 229 title={`Theme: ${theme()}`} 334 230 > 335 231 {theme() === "macchiato" ? "Dark" : theme() === "latte" ? "Light" : "System"} ··· 343 239 344 240 /// Root app component. 345 241 const App: Component = () => { 346 - // Initialize theme from localStorage on mount. 347 - const saved = localStorage.getItem("moqbox-theme") || "system"; 348 - if (saved === "latte") { 349 - document.documentElement.setAttribute("data-theme", "latte"); 350 - document.documentElement.style.colorScheme = "light"; 351 - } else if (saved === "macchiato") { 352 - document.documentElement.setAttribute("data-theme", "macchiato"); 353 - document.documentElement.style.colorScheme = "dark"; 354 - } else { 355 - document.documentElement.style.colorScheme = "light dark"; 356 - } 242 + applyTheme(storedTheme()); 357 243 358 244 return ( 359 245 <Router>
+2 -2
frontend/src/Player.tsx
··· 35 35 // Captures the track object in a ref inside the tracking (compute) phase 36 36 // so the apply phase can read it without triggering Solid 2.x's strict 37 37 // read-props-in-untracked-context warning/error. 38 - let trackRef: TrackState | undefined | null; 38 + let trackRef: TrackState | null = null; 39 39 40 40 createEffect( 41 41 () => { ··· 61 61 } 62 62 63 63 const started_at = track.started_at; 64 - const mediaUrl = `/api/v1/rooms/${roomId}/media/${id}`; 64 + const mediaUrl = `/api/v1/rooms/${roomId}/media/${track.hash}`; 65 65 el.src = mediaUrl; 66 66 67 67 let onTime: ((this: HTMLMediaElement, ev: Event) => void) | undefined;
+52 -54
frontend/src/Queue.tsx
··· 1 1 import { For } from "solid-js"; 2 2 import type { TrackState, QueueSummary, HistoryEntry, PlaylistEntry } from "./types"; 3 3 4 + interface QueueItemProps { 5 + title: string; 6 + duration: string; 7 + thumbnail: string | null; 8 + pending?: boolean; 9 + extraClass?: string; 10 + } 11 + 12 + function QueueItem(props: QueueItemProps) { 13 + return ( 14 + <div class={`queue-item${props.extraClass ? " " + props.extraClass : ""}`}> 15 + {props.thumbnail && ( 16 + <img 17 + class="queue-thumb" 18 + src={props.thumbnail} 19 + alt="" 20 + onError={(e) => (e.currentTarget.hidden = true)} 21 + /> 22 + )} 23 + <span class="queue-title"> 24 + {props.pending ? "\u23F3 " : ""} 25 + {props.title} 26 + </span> 27 + <span class="queue-duration">{props.duration}</span> 28 + </div> 29 + ); 30 + } 31 + 4 32 interface QueueProps { 5 33 currentTrack: TrackState | null; 6 34 queue: QueueSummary[]; ··· 14 42 <div class="queue-section"> 15 43 <h3>Now Playing</h3> 16 44 {props.currentTrack ? ( 17 - <div class="queue-item current"> 18 - {props.currentTrack.thumbnail && ( 19 - <img 20 - class="queue-thumb" 21 - src={props.currentTrack.thumbnail} 22 - alt="" 23 - onError={(e) => (e.currentTarget.hidden = true)} 24 - /> 25 - )} 26 - <span class="queue-title">{props.currentTrack.title}</span> 27 - <span class="queue-duration">{props.currentTrack.duration}</span> 28 - </div> 45 + <QueueItem 46 + title={props.currentTrack.title} 47 + duration={props.currentTrack.duration} 48 + thumbnail={props.currentTrack.thumbnail} 49 + extraClass="current" 50 + /> 29 51 ) : ( 30 52 <div class="queue-empty">Paste a URL to start watching together</div> 31 53 )} ··· 35 57 <h3>Up Next ({props.queue.length})</h3> 36 58 <For each={props.queue}> 37 59 {(item) => ( 38 - <div class="queue-item"> 39 - {item.thumbnail && ( 40 - <img 41 - class="queue-thumb" 42 - src={item.thumbnail} 43 - alt="" 44 - onError={(e) => (e.currentTarget.hidden = true)} 45 - /> 46 - )} 47 - <span class="queue-title"> 48 - {item.pending ? "\u23F3 " : ""} 49 - {item.title} 50 - </span> 51 - <span class="queue-duration">{item.duration}</span> 52 - </div> 60 + <QueueItem 61 + title={item.title} 62 + duration={item.duration} 63 + thumbnail={item.thumbnail} 64 + pending={item.pending} 65 + /> 53 66 )} 54 67 </For> 55 68 {props.queue.length === 0 && <div class="queue-empty">Tracks you add will appear here</div>} ··· 59 72 <h3>Recently Played</h3> 60 73 <For each={props.history}> 61 74 {(item) => ( 62 - <div class="queue-item history"> 63 - {item.thumbnail && ( 64 - <img 65 - class="queue-thumb" 66 - src={item.thumbnail} 67 - alt="" 68 - onError={(e) => (e.currentTarget.hidden = true)} 69 - /> 70 - )} 71 - <span class="queue-title">{item.title}</span> 72 - <span class="queue-duration">{item.duration}</span> 73 - </div> 75 + <QueueItem 76 + title={item.title} 77 + duration={item.duration} 78 + thumbnail={item.thumbnail} 79 + extraClass="history" 80 + /> 74 81 )} 75 82 </For> 76 83 {props.history.length === 0 && ( ··· 82 89 <h3>Playlist ({props.playlist.length})</h3> 83 90 <For each={props.playlist}> 84 91 {(item) => ( 85 - <div class="queue-item"> 86 - {item.thumbnail && ( 87 - <img 88 - class="queue-thumb" 89 - src={item.thumbnail} 90 - alt="" 91 - onError={(e) => (e.currentTarget.hidden = true)} 92 - /> 93 - )} 94 - <span class="queue-title"> 95 - {item.pending ? "\u23F3 " : ""} 96 - {item.title} 97 - </span> 98 - <span class="queue-duration">{item.duration}</span> 99 - </div> 92 + <QueueItem 93 + title={item.title} 94 + duration={item.duration} 95 + thumbnail={item.thumbnail} 96 + pending={item.pending} 97 + /> 100 98 )} 101 99 </For> 102 100 {props.playlist.length === 0 && (
+82
frontend/src/hooks.ts
··· 1 + import { createSignal, createEffect, onSettled } from "solid-js"; 2 + import { connectWs } from "./ws"; 3 + import type { RoomState, ChatMessage, ClientMessage } from "./types"; 4 + 5 + /** Auto-clear an error signal after `ms`, unless the `unless` predicate matches the error. */ 6 + export function useAutoDismiss( 7 + [error, setError]: [() => string | null, (v: string | null) => void], 8 + opts: { ms?: number; unless?: (err: string) => boolean } = {}, 9 + ): void { 10 + createEffect( 11 + () => error(), 12 + (err) => { 13 + if (!err) return; 14 + if (opts.unless?.(err)) return; 15 + const timer = setTimeout(() => setError(null), opts.ms ?? 5000); 16 + return () => clearTimeout(timer); 17 + }, 18 + ); 19 + } 20 + 21 + /** Manage WebSocket connection lifecycle for a room. */ 22 + export function useRoomConnection(roomId: string, userName: string) { 23 + const [state, setState] = createSignal<RoomState | null>(null); 24 + const [chats, setChats] = createSignal<ChatMessage[]>([]); 25 + const [connected, setConnected] = createSignal(false); 26 + const [error, setError] = createSignal<string | null>(null); 27 + const [sendFn, setSendFn] = createSignal<((msg: ClientMessage) => void) | null>(null); 28 + const initialLoad = { current: true }; 29 + 30 + const startConnecting = () => { 31 + if (sendFn()) return; 32 + 33 + const send = connectWs(roomId, userName, { 34 + onState: (s) => { 35 + initialLoad.current = false; 36 + setState(s); 37 + }, 38 + onChat: (msg) => 39 + setChats((prev) => { 40 + const next = [...prev, msg]; 41 + return next.length > 200 ? next.slice(-200) : next; 42 + }), 43 + onDisconnect: () => { 44 + setConnected(false); 45 + setSendFn(null); 46 + }, 47 + onError: (reason) => { 48 + setError(reason); 49 + }, 50 + }); 51 + setSendFn(() => send); 52 + setConnected(true); 53 + setError(null); 54 + }; 55 + 56 + onSettled(() => { 57 + startConnecting(); 58 + setTimeout(() => { 59 + if (initialLoad.current) { 60 + setError("Room not found or unavailable"); 61 + } 62 + }, 8000); 63 + }); 64 + 65 + // Reconnect on disconnect with backoff. 66 + createEffect( 67 + () => [connected(), sendFn()], 68 + ([isConnected, send]) => { 69 + if (!isConnected && !send) { 70 + const timer = setTimeout(startConnecting, 3000); 71 + return () => clearTimeout(timer); 72 + } 73 + }, 74 + ); 75 + 76 + // Auto-dismiss transient errors, but not "not found". 77 + useAutoDismiss([error, setError], { unless: (err) => err.includes("not found") }); 78 + 79 + const send = (msg: ClientMessage) => sendFn()?.(msg); 80 + 81 + return { state, chats, connected, error, setError, send }; 82 + }
+16 -6
frontend/src/style.css
··· 42 42 --border: var(--surface0); 43 43 --text-primary: var(--text); 44 44 --text-muted: var(--overlay1); 45 - --accent: var(--peach); 46 - --accent-hover: var(--rosewater); 45 + --accent: var(--mauve); 46 + --accent-hover: var(--lavender); 47 47 --danger: var(--red); 48 48 --success: var(--green); 49 49 --warning: var(--yellow); ··· 160 160 background: var(--danger); 161 161 color: var(--crust); 162 162 font-weight: 600; 163 + } 164 + .toast-action { 165 + background: rgba(0, 0, 0, 0.15); 166 + border: 1px solid rgba(255, 255, 255, 0.2); 167 + color: inherit; 168 + font-size: 0.75rem; 169 + padding: 0.15rem 0.5rem; 170 + border-radius: 3px; 171 + cursor: pointer; 172 + margin-left: 0.5rem; 163 173 } 164 174 165 175 /* ── Room ── */ ··· 328 338 background: rgba(0, 0, 0, 0.7); 329 339 } 330 340 .live-btn--on { 331 - background: var(--green); 332 - color: var(--crust); 333 - border-color: var(--green); 341 + background: var(--accent); 342 + color: var(--base); 343 + border-color: var(--accent); 334 344 } 335 345 .live-btn--on:hover { 336 - background: color-mix(in srgb, var(--green) 85%, #000); 346 + background: color-mix(in srgb, var(--accent) 85%, var(--base)); 337 347 } 338 348 .player-idle { 339 349 color: var(--text-muted);
+27
frontend/src/theme.ts
··· 1 + const STORAGE_KEY = "moqbox-theme"; 2 + 3 + /** Apply a theme to the document root and persist to localStorage. */ 4 + export function applyTheme(t: string): void { 5 + const root = document.documentElement; 6 + if (t === "latte") { 7 + root.setAttribute("data-theme", "latte"); 8 + root.style.colorScheme = "light"; 9 + } else if (t === "macchiato") { 10 + root.setAttribute("data-theme", "macchiato"); 11 + root.style.colorScheme = "dark"; 12 + } else { 13 + root.removeAttribute("data-theme"); 14 + root.style.colorScheme = "light dark"; 15 + } 16 + localStorage.setItem(STORAGE_KEY, t); 17 + } 18 + 19 + /** Cycle to the next theme: macchiato → latte → system → macchiato. */ 20 + export function cycleTheme(current: string): string { 21 + return current === "macchiato" ? "latte" : current === "latte" ? "system" : "macchiato"; 22 + } 23 + 24 + /** Get the stored theme, defaulting to "system". */ 25 + export function storedTheme(): string { 26 + return localStorage.getItem(STORAGE_KEY) || "system"; 27 + }
+4 -2
frontend/src/types.ts
··· 2 2 3 3 export interface TrackState { 4 4 id: string; 5 + hash: string; 5 6 title: string; 6 7 url: string; 7 8 duration: string; 8 9 thumbnail: string | null; 9 - started_at: number; // epoch ms — client computes elapsed from wall clock 10 + started_at: number; 10 11 } 11 12 12 13 export interface QueueSummary { 13 14 id: string; 15 + hash: string; 14 16 title: string; 15 17 url: string; 16 18 duration: string; ··· 20 22 21 23 export interface HistoryEntry { 22 24 id: string; 25 + hash: string; 23 26 title: string; 24 27 url: string; 25 28 duration: string; ··· 37 40 added_by: string | null; 38 41 added_at: string; 39 42 pending: boolean; 40 - stream_url: string | null; 41 43 } 42 44 43 45 export interface RoomState {
+7
proptest-regressions/state.txt
··· 1 + # Seeds for failure cases proptest has generated in the past. It is 2 + # automatically read and these particular cases re-run before any 3 + # novel cases are generated. 4 + # 5 + # It is recommended to check this file in to source control so that 6 + # everyone who runs the test benefits from these saved cases. 7 + cc e68e654f36d5af1cc26441258aebd42f4c2b2182b17658f78de107b2cbf070b9 # shrinks to ops = [Queue(0), Queue(1)]
+202 -262
src/room.rs
··· 11 11 //! 3. Commit: execute returned [`Effect`]s 12 12 13 13 use std::collections::HashMap; 14 + use std::hash::{Hash, Hasher}; 14 15 use std::path::PathBuf; 15 16 use std::sync::Arc; 16 17 use std::time::Duration; ··· 26 27 TrackId, TrackState, 27 28 }; 28 29 29 - #[cfg(test)] 30 - use crate::types::ChatMessageId; 31 - #[cfg(test)] 32 - use tokio::sync::oneshot; 33 - 34 30 /// All room operations, processed sequentially by the per-room actor. 35 31 pub(crate) enum RoomCommand { 36 32 QueueUrl(String), 37 33 DownloadReady { 38 - item_id: TrackId, 34 + url: String, 39 35 media: crate::ingest::IngestedTrack, 40 36 }, 41 37 DownloadFailed { 42 - item_id: TrackId, 38 + url: String, 43 39 error: String, 44 40 }, 45 41 Skip, ··· 63 59 RemovePlaylistEntry { 64 60 id: PlaylistEntryId, 65 61 }, 66 - PlaylistDownloadReady { 67 - id: PlaylistEntryId, 68 - media: crate::ingest::IngestedTrack, 69 - }, 70 - PlaylistDownloadFailed { 71 - id: PlaylistEntryId, 72 - error: String, 73 - }, 74 62 PublishState, 75 63 Shutdown, 76 64 } ··· 98 86 /// Index into the playlist for round-robin auto-fill. 99 87 playlist_index: usize, 100 88 101 - /// Track handles of running downloads, keyed by track ID. 102 - download_tasks: HashMap<TrackId, tokio::task::JoinHandle<()>>, 103 - /// Completed downloads whose tracks haven't advanced to active yet. 104 - download_results: HashMap<TrackId, crate::ingest::IngestedTrack>, 89 + /// Running downloads keyed by URL (one task per URL — deduplicated). 90 + download_tasks: HashMap<String, tokio::task::JoinHandle<()>>, 91 + /// Completed downloads keyed by URL. Shared across all tracks/entries 92 + /// referencing the same media URL. 93 + download_results: HashMap<String, crate::ingest::IngestedTrack>, 105 94 } 106 95 107 96 /// Thread-safe registry of all active rooms with persistent store. ··· 246 235 let _ = handle.cmd_tx.send(RoomCommand::QueueUrl(url)).await; 247 236 } 248 237 249 - /// Remove a room (from store and in-memory state). 250 - #[cfg(test)] 251 - pub(crate) async fn remove(&self, id: &RoomId) { 252 - if let Some(handle) = self.handle(id).await { 253 - let _ = tokio::time::timeout( 254 - Duration::from_secs(1), // generous 1s window for graceful shutdown 255 - handle.cmd_tx.send(RoomCommand::Shutdown), 256 - ) 257 - .await; 258 - } 259 - self.inner.write().await.rooms.remove(id); 260 - let _ = self.store.delete_room(&id.as_str_buf()).await; 261 - } 262 - 263 - /// Send a chat message: validate, send to actor, return best-effort ChatMessage. 264 - #[cfg(test)] 265 - pub(crate) async fn send_chat( 266 - &self, 267 - room_id: &RoomId, 268 - user_name: &str, 269 - content: &str, 270 - msg_type: &str, 271 - ) -> Result<ChatMessage, anyhow::Error> { 272 - if content.is_empty() { 273 - anyhow::bail!("content is empty"); 274 - } 275 - if content.len() > 2000 { 276 - anyhow::bail!("content exceeds 2000 characters"); 277 - } 278 - if user_name.is_empty() { 279 - anyhow::bail!("username is empty"); 280 - } 281 - if user_name.len() > 32 { 282 - anyhow::bail!("username exceeds 32 characters"); 283 - } 284 - if let Some(handle) = self.handle(room_id).await { 285 - let _ = handle 286 - .cmd_tx 287 - .send(RoomCommand::Chat { 288 - user_name: user_name.to_string(), 289 - content: content.to_string(), 290 - msg_type: msg_type.to_string(), 291 - created_at: chrono::Utc::now() 292 - .format("%Y-%m-%dT%H:%M:%S%.3fZ") 293 - .to_string(), 294 - }) 295 - .await; 296 - } 297 - Ok(ChatMessage { 298 - id: ChatMessageId::new(), 299 - user_name: user_name.to_string(), 300 - content: content.to_string(), 301 - msg_type: msg_type.to_string(), 302 - created_at: chrono::Utc::now() 303 - .format("%Y-%m-%dT%H:%M:%S%.3fZ") 304 - .to_string(), 305 - }) 306 - } 307 - 308 - /// Get the most recent chat messages from the store. 309 - /// Register a client in the room. 310 - #[cfg(test)] 311 - pub(crate) async fn register_client(&self, room_id: &RoomId) -> Option<u64> { 312 - let handle = self.handle(room_id).await?; 313 - let (resp_tx, resp_rx) = oneshot::channel(); 314 - handle 315 - .cmd_tx 316 - .send(RoomCommand::RegisterClient { resp: resp_tx }) 317 - .await 318 - .ok()?; 319 - resp_rx.await.ok() 320 - } 321 - 322 238 /// Skip the current track. 323 239 pub(crate) async fn skip(&self, room_id: &RoomId) { 324 240 if let Some(handle) = self.handle(room_id).await { ··· 411 327 Some((entry, index.wrapping_add(1))) 412 328 } 413 329 330 + /// Compute a filesystem-safe hash of a URL for use as a filename. 331 + fn file_hash(url: &str) -> String { 332 + let mut h = std::collections::hash_map::DefaultHasher::new(); 333 + url.hash(&mut h); 334 + format!("{:x}", h.finish()) 335 + } 336 + 414 337 /// Construct a [`RoomActor`] and spawn it on the tokio runtime. 415 338 /// 416 339 /// Shared helper used by both [`Registry::create`] and [`Registry::handle`] ··· 475 398 async fn process_cmd(&mut self, cmd: RoomCommand) -> bool { 476 399 match cmd { 477 400 RoomCommand::QueueUrl(url) => self.handle_queue_url(url).await, 478 - RoomCommand::DownloadReady { item_id, media } => { 479 - self.handle_download_ready(item_id, media).await 480 - } 481 - RoomCommand::DownloadFailed { item_id, error } => { 482 - self.handle_download_failed(item_id, error).await 483 - } 484 - RoomCommand::Skip => { 485 - let result = self.handle_skip().await; 486 - if !result { 487 - self.maybe_auto_fill().await; 488 - } 489 - result 401 + RoomCommand::DownloadReady { url, media } => { 402 + self.handle_download_ready(url, media).await 490 403 } 491 - RoomCommand::TrackEnded { item_id } => { 492 - let result = self.handle_track_ended(item_id).await; 493 - if !result { 494 - self.maybe_auto_fill().await; 495 - } 496 - result 404 + RoomCommand::DownloadFailed { url, error } => { 405 + self.handle_download_failed(url, error).await 497 406 } 407 + RoomCommand::Skip => self.handle_skip().await, 408 + RoomCommand::TrackEnded { item_id } => self.handle_track_ended(item_id).await, 498 409 RoomCommand::Chat { 499 410 user_name, 500 411 content, ··· 510 421 self.handle_add_playlist_entry(url, added_by).await 511 422 } 512 423 RoomCommand::RemovePlaylistEntry { id } => self.handle_remove_playlist_entry(id).await, 513 - RoomCommand::PlaylistDownloadReady { id, media } => { 514 - self.handle_playlist_download_ready(id, media).await 515 - } 516 - RoomCommand::PlaylistDownloadFailed { id, error } => { 517 - self.handle_playlist_download_failed(id, error).await 518 - } 519 424 RoomCommand::PublishState => self.handle_publish_state().await, 520 425 RoomCommand::Shutdown => self.handle_shutdown().await, 521 426 } ··· 531 436 } 532 437 } 533 438 439 + /// Queue a URL. If the room is idle the track advances to active 440 + /// immediately. When the download is already cached we resolve 441 + /// `started_at` before publishing — single snapshot, no flicker. 534 442 async fn handle_queue_url(&mut self, url: String) -> bool { 535 443 tracing::debug!(room = %self.room_id, %url, "track url queued"); 536 444 537 445 let item_id = TrackId::new(); 538 446 let now = Utc::now().timestamp_millis(); 539 447 448 + // Gather: check whether we already have the file. 449 + let cached = self.download_results.get(&url); 450 + let (title, duration, thumbnail, pending) = if let Some(c) = cached { 451 + ( 452 + c.title.clone(), 453 + format_duration(c.duration), 454 + c.thumbnail.clone(), 455 + false, 456 + ) 457 + } else { 458 + ("Loading...".into(), "--:--".into(), None, true) 459 + }; 460 + 461 + // Pure transition. 540 462 let track = QueuedTrack { 541 463 id: item_id, 542 - title: "Loading...".into(), 464 + title, 543 465 url: url.clone(), 544 - duration: "--:--".into(), 545 - thumbnail: None, 546 - pending: true, 466 + duration, 467 + thumbnail, 468 + pending, 547 469 }; 548 - 549 470 let effects = self.state.transition(&Event::TrackQueued(track), now); 550 471 self.persist_effects(&effects).await; 472 + 473 + // If the track became active and the file is cached, resolve BEFORE 474 + // publishing — the snapshot goes out with a real started_at. 475 + let became_active = effects 476 + .iter() 477 + .any(|e| matches!(e, Effect::PersistStarted(_))); 478 + if became_active && !pending { 479 + self.state.resolve_started_at(now); 480 + } 481 + 551 482 self.execute_effects(effects).await; 552 - self.start_download(item_id, &url).await; 483 + self.ensure_downloaded(&url).await; 484 + self.try_start_active().await; 553 485 false 554 486 } 555 487 488 + /// Download completed. Cache the result, update all in-memory state 489 + /// (tracks + playlist entries referencing this URL), persist metadata 490 + /// to media_cache, and broadcast. 556 491 async fn handle_download_ready( 557 492 &mut self, 558 - item_id: TrackId, 493 + url: String, 559 494 media: crate::ingest::IngestedTrack, 560 495 ) -> bool { 561 - tracing::debug!(room = %self.room_id, %item_id, title = %media.title, "download ready"); 496 + tracing::debug!(room = %self.room_id, %url, title = %media.title, "download ready"); 562 497 563 - self.download_results.insert(item_id, media.clone()); 498 + self.download_results.insert(url.clone(), media.clone()); 499 + let title = media.title; 500 + let duration = format_duration(media.duration); 501 + let thumbnail = media.thumbnail; 502 + let source = media.source; 503 + let now = Utc::now().timestamp_millis(); 564 504 565 - let now = Utc::now().timestamp_millis(); 505 + // Update all tracks via the pure state machine (URL-keyed event). 566 506 let effects = self.state.transition( 567 - &Event::MetadataUpdated { 568 - item_id, 569 - title: media.title, 570 - duration: format_duration(media.duration), 571 - thumbnail: media.thumbnail, 572 - source: media.source, 507 + &Event::MetadataResolved { 508 + url: url.clone(), 509 + title: title.clone(), 510 + duration: duration.clone(), 511 + thumbnail: thumbnail.clone(), 512 + source: source.clone(), 573 513 }, 574 514 now, 575 515 ); 516 + self.persist_effects(&effects).await; 576 517 577 - self.persist_effects(&effects).await; 578 - self.execute_effects(effects).await; 518 + // Update playlist entries in memory (they aren't owned by the state 519 + // machine, so we update them directly). 520 + for entry in &mut self.playlist { 521 + if entry.url == url { 522 + entry.title = title.clone(); 523 + entry.duration = duration.clone(); 524 + entry.thumbnail = thumbnail.clone(); 525 + entry.source = source.clone(); 526 + entry.pending = false; 527 + } 528 + } 579 529 580 - let is_active_and_pending = self 530 + // Check whether the active track was waiting for this file. 531 + let needs_start = self 581 532 .state 582 533 .active 583 534 .as_ref() 584 - .map(|t| t.id == item_id && t.started_at_wall == 0) 585 - .unwrap_or(false); 535 + .is_some_and(|a| a.url == url && a.started_at_wall == 0); 586 536 587 - if is_active_and_pending { 588 - let now = Utc::now().timestamp_millis(); 537 + self.execute_effects(effects).await; 538 + 539 + if needs_start { 589 540 self.state.resolve_started_at(now); 590 - self.publish_state_snapshot().await; 541 + self.publish_state_snapshot().await; // second publish, now with started_at 591 542 self.ensure_next_downloaded().await; 592 543 } 593 544 545 + self.maybe_auto_fill().await; 594 546 false 595 547 } 596 548 597 - async fn handle_download_failed(&mut self, item_id: TrackId, error: String) -> bool { 598 - tracing::debug!(room = %self.room_id, %item_id, %error, "download failed"); 549 + /// Common post-transition cleanup: resolve the active track's started_at 550 + /// if the file is cached, start any needed downloads, and auto-fill from 551 + /// the playlist if the room became idle. 552 + /// 553 + /// All operations are idempotent — safe to call after any state change. 554 + async fn post_advance(&mut self) -> bool { 555 + self.try_start_active().await; 556 + self.ensure_active_download().await; 557 + self.ensure_next_downloaded().await; 558 + let _ = self.maybe_auto_fill().await; 559 + false 560 + } 561 + 562 + async fn handle_download_failed(&mut self, url: String, error: String) -> bool { 563 + tracing::debug!(room = %self.room_id, %url, %error, "download failed"); 564 + 565 + // Remove the failed task so a retry can start a fresh download. 566 + self.download_tasks.remove(&url); 599 567 600 568 let now = Utc::now().timestamp_millis(); 601 - let effects = self.state.transition( 602 - &Event::MetadataUpdated { 603 - item_id, 604 - title: format!("Failed to load: {error}"), 605 - duration: "--:--".into(), 606 - thumbnail: None, 607 - source: "ytdlp".into(), 608 - }, 609 - now, 610 - ); 569 + 570 + // Only affect tracks whose URL matches the failed download. 571 + let affected: Vec<TrackId> = self 572 + .state 573 + .queue 574 + .iter() 575 + .filter(|t| t.url == url) 576 + .map(|t| t.id) 577 + .chain( 578 + self.state 579 + .active 580 + .iter() 581 + .filter(|a| a.url == url) 582 + .map(|a| a.id), 583 + ) 584 + .collect(); 585 + 586 + // If no tracks reference this URL, nothing to do. 587 + if affected.is_empty() { 588 + return false; 589 + } 590 + 591 + let mut effects = Vec::new(); 592 + let failed_title = format!("Failed to load: {error}"); 593 + for id in &affected { 594 + effects.extend(self.state.transition( 595 + &Event::MetadataUpdated { 596 + item_id: *id, 597 + url: url.clone(), 598 + title: failed_title.clone(), 599 + duration: "--:--".into(), 600 + thumbnail: None, 601 + source: "ytdlp".into(), 602 + }, 603 + now, 604 + )); 605 + effects.extend( 606 + self.state 607 + .transition(&Event::TrackFailed { item_id: *id }, now), 608 + ); 609 + } 611 610 612 611 self.persist_effects(&effects).await; 613 612 self.execute_effects(effects).await; 614 - false 613 + self.post_advance().await 615 614 } 616 615 617 616 async fn handle_skip(&mut self) -> bool { ··· 619 618 620 619 let now = Utc::now().timestamp_millis(); 621 620 let effects = self.state.transition(&Event::Skip, now); 622 - 623 621 self.persist_effects(&effects).await; 624 622 self.execute_effects(effects).await; 625 - self.try_start_active().await; 626 - self.ensure_active_download().await; 627 - false 623 + self.post_advance().await 628 624 } 629 625 630 626 async fn handle_track_ended(&mut self, item_id: TrackId) -> bool { ··· 632 628 633 629 let now = Utc::now().timestamp_millis(); 634 630 let effects = self.state.transition(&Event::TrackEnded { item_id }, now); 635 - 636 631 self.persist_effects(&effects).await; 637 632 self.execute_effects(effects).await; 638 - self.try_start_active().await; 639 - self.ensure_active_download().await; 640 - false 633 + self.post_advance().await 641 634 } 642 635 643 636 async fn handle_add_playlist_entry(&mut self, url: String, added_by: Option<String>) -> bool { ··· 652 645 added_by, 653 646 added_at: crate::util::now_iso(), 654 647 pending: true, 655 - stream_url: None, 656 648 }; 657 649 let effects = vec![Effect::AddPlaylistEntry { 658 650 id, ··· 672 664 } 673 665 self.playlist.push(entry); 674 666 self.publish_state_snapshot().await; 675 - self.start_playlist_download(id, &url).await; 667 + self.ensure_downloaded(&url).await; 676 668 false 677 669 } 678 670 ··· 691 683 false 692 684 } 693 685 694 - async fn handle_playlist_download_ready( 695 - &mut self, 696 - id: PlaylistEntryId, 697 - media: crate::ingest::IngestedTrack, 698 - ) -> bool { 699 - tracing::debug!(room = %self.room_id, %id, title = %media.title, "playlist download ready"); 700 - 701 - if let Some(entry) = self.playlist.iter_mut().find(|e| e.id == id) { 702 - entry.title = media.title.clone(); 703 - entry.duration = format_duration(media.duration); 704 - entry.thumbnail = media.thumbnail.clone(); 705 - entry.source = media.source.clone(); 706 - entry.pending = false; 707 - } 708 - let effects = vec![Effect::UpdatePlaylistEntry { 709 - id, 710 - title: media.title, 711 - duration: format_duration(media.duration), 712 - thumbnail: media.thumbnail, 713 - source: media.source, 714 - }]; 715 - self.persist_effects(&effects).await; 716 - self.publish_state_snapshot().await; 717 - self.maybe_auto_fill().await; 718 - false 719 - } 720 - 721 - async fn handle_playlist_download_failed( 722 - &mut self, 723 - id: PlaylistEntryId, 724 - error: String, 725 - ) -> bool { 726 - tracing::warn!(room = %self.room_id, %id, %error, "playlist download failed"); 727 - let effects = vec![Effect::RemovePlaylistEntry(id)]; 728 - self.persist_effects(&effects).await; 729 - self.playlist.retain(|e| e.id != id); 730 - self.publish_state_snapshot().await; 731 - false 732 - } 733 - 734 686 async fn handle_chat( 735 687 &mut self, 736 688 user_name: String, ··· 815 767 } 816 768 817 769 /// If the room is idle and has playlist entries, auto-fill the next track. 818 - async fn maybe_auto_fill(&mut self) { 770 + /// Returns `true` if a track was added (so callers can chain post-advance 771 + /// work). 772 + async fn maybe_auto_fill(&mut self) -> bool { 819 773 let entry = match next_playlist_track(&self.state, &self.playlist, self.playlist_index) { 820 774 Some((e, idx)) => { 821 775 self.playlist_index = idx; 822 776 e.clone() 823 777 } 824 - None => return, 778 + None => return false, 825 779 }; 826 780 827 781 let id = TrackId::new(); 828 782 let now = Utc::now().timestamp_millis(); 829 - let track_url = entry.url.clone(); 783 + let url = entry.url.clone(); 830 784 let track = QueuedTrack { 831 785 id, 832 786 title: entry.title, 833 - url: entry.url, 787 + url, 834 788 duration: entry.duration, 835 789 thumbnail: entry.thumbnail, 836 790 pending: false, ··· 845 799 } 846 800 self.execute_effects(effects).await; 847 801 848 - // Start the download for this new track. 849 - self.start_download(id, &track_url).await; 802 + // The newly advanced track starts with started_at: 0. If the file is 803 + // already cached, resolve it immediately so the frontend loads. 804 + self.try_start_active().await; 805 + self.ensure_active_download().await; 806 + true 850 807 } 851 808 809 + /// Common post-transition cleanup: resolve the active track's started_at 852 810 /// If the active track has a placeholder `started_at_wall` (0), resolve it 853 811 /// to now. Used after rehydration where the file was already downloaded. 854 812 async fn resolve_active_if_ready(&mut self) { 855 813 if let Some(active) = &self.state.active { 856 814 if active.started_at_wall == 0 857 - && (self.download_results.contains_key(&active.id) 815 + && (self.download_results.contains_key(&active.url) 858 816 || self 859 817 .cache_dir 860 - .join(active.id.as_str_buf()) 818 + .join(file_hash(&active.url)) 861 819 .with_extension("mp4") 862 820 .exists()) 863 821 { ··· 872 830 async fn publish_state_snapshot(&self) { 873 831 let current = self.state.active.as_ref().map(|t| TrackState { 874 832 id: t.id, 833 + hash: file_hash(&t.url), 875 834 title: t.title.clone(), 876 835 url: t.url.clone(), 877 836 duration: t.duration.clone(), ··· 885 844 .iter() 886 845 .map(|q| QueueSummary { 887 846 id: q.id, 847 + hash: file_hash(&q.url), 888 848 title: q.title.clone(), 889 849 url: q.url.clone(), 890 850 duration: q.duration.clone(), ··· 899 859 .iter() 900 860 .map(|h| HistoryEntry { 901 861 id: h.id, 862 + hash: file_hash(&h.url), 902 863 title: h.title.clone(), 903 864 url: h.url.clone(), 904 865 duration: h.duration.clone(), ··· 925 886 self.publishers.state_tx.send_replace(snapshot); 926 887 } 927 888 928 - /// Spawn a download task for a queued track. 929 - async fn start_download(&mut self, item_id: TrackId, url: &str) { 889 + /// Ensure a URL has been downloaded (or is in progress). Deduplicated by 890 + /// URL: if the URL is already downloaded or downloading, this is a no-op. 891 + /// Safe to call from both queue and playlist paths. 892 + async fn ensure_downloaded(&mut self, url: &str) { 893 + let url = url.to_string(); 894 + if self.download_results.contains_key(&url) || self.download_tasks.contains_key(&url) { 895 + return; 896 + } 897 + // Hash the URL for a stable file path, avoiding filename conflicts. 898 + let hash = file_hash(&url); 899 + let dest = self.cache_dir.join(&hash); 930 900 let cmd_tx = self.cmd_tx.clone(); 931 - let dest = self.cache_dir.join(item_id.as_str_buf()); 932 - let url = url.to_string(); 901 + let url_clone = url.clone(); 933 902 let handle = tokio::spawn(async move { 934 - match crate::ingest::download(&url, &dest).await { 903 + match crate::ingest::download(&url_clone, &dest).await { 935 904 Ok(media) => { 936 905 let _ = cmd_tx 937 - .send(RoomCommand::DownloadReady { item_id, media }) 938 - .await; 939 - } 940 - Err(e) => { 941 - let _ = cmd_tx 942 - .send(RoomCommand::DownloadFailed { 943 - item_id, 944 - error: e.to_string(), 906 + .send(RoomCommand::DownloadReady { 907 + url: url_clone, 908 + media, 945 909 }) 946 910 .await; 947 911 } 948 - } 949 - }); 950 - self.download_tasks.insert(item_id, handle); 951 - } 952 - 953 - /// Spawn a download task for a playlist entry. 954 - async fn start_playlist_download(&mut self, id: PlaylistEntryId, url: &str) { 955 - let cmd_tx = self.cmd_tx.clone(); 956 - let dest = self.cache_dir.join(format!("playlist_{}", id.as_str_buf())); 957 - let url = url.to_string(); 958 - tokio::spawn(async move { 959 - match crate::ingest::download(&url, &dest).await { 960 - Ok(media) => { 961 - let _ = cmd_tx 962 - .send(RoomCommand::PlaylistDownloadReady { id, media }) 963 - .await; 964 - } 965 912 Err(e) => { 966 913 let _ = cmd_tx 967 - .send(RoomCommand::PlaylistDownloadFailed { 968 - id, 914 + .send(RoomCommand::DownloadFailed { 915 + url: url_clone, 969 916 error: e.to_string(), 970 917 }) 971 918 .await; 972 919 } 973 920 } 974 921 }); 922 + self.download_tasks.insert(url, handle); 975 923 } 976 924 977 925 /// If the active track has a completed download cached, resolve its ··· 982 930 .state 983 931 .active 984 932 .as_ref() 985 - .map(|t| t.started_at_wall == 0 && self.download_results.contains_key(&t.id)) 933 + .map(|t| t.started_at_wall == 0 && self.download_results.contains_key(&t.url)) 986 934 .unwrap_or(false); 987 935 if should_start { 988 936 let now = chrono::Utc::now().timestamp_millis(); ··· 1001 949 .queue 1002 950 .first() 1003 951 .map(|next| { 1004 - !self.download_results.contains_key(&next.id) 1005 - && !self.download_tasks.contains_key(&next.id) 952 + !self.download_results.contains_key(&next.url) 953 + && !self.download_tasks.contains_key(&next.url) 1006 954 }) 1007 955 .unwrap_or(false); 1008 956 if needs_download { 1009 - let next = self 957 + let url = self 1010 958 .state 1011 959 .queue 1012 960 .first() 961 + .map(|t| t.url.clone()) 1013 962 .expect("queue must be non-empty after needs_download check"); 1014 - let id = next.id; 1015 - let url = next.url.clone(); 1016 - self.start_download(id, &url).await; 963 + self.ensure_downloaded(&url).await; 1017 964 } 1018 965 } 1019 966 ··· 1026 973 .as_ref() 1027 974 .map(|t| { 1028 975 t.started_at_wall == 0 1029 - && !self.download_results.contains_key(&t.id) 1030 - && !self.download_tasks.contains_key(&t.id) 976 + && !self.download_results.contains_key(&t.url) 977 + && !self.download_tasks.contains_key(&t.url) 1031 978 }) 1032 979 .unwrap_or(false); 1033 980 if needs_download { 1034 - // Clone before borrow to satisfy the borrow checker. 1035 - let active = self 981 + let url = self 1036 982 .state 1037 983 .active 1038 984 .as_ref() 985 + .map(|t| t.url.clone()) 1039 986 .expect("active must be Some after needs_download check"); 1040 - let id = active.id; 1041 - let url = active.url.clone(); 1042 - self.start_download(id, &url).await; 987 + self.ensure_downloaded(&url).await; 1043 988 } 1044 989 } 1045 990 } ··· 1091 1036 added_by: None, 1092 1037 added_at: "now".into(), 1093 1038 pending: false, 1094 - stream_url: None, 1095 1039 }]; 1096 1040 assert!(next_playlist_track(&state, &playlist, 0).is_none()); 1097 1041 } ··· 1117 1061 added_by: None, 1118 1062 added_at: "now".into(), 1119 1063 pending: false, 1120 - stream_url: None, 1121 1064 }]; 1122 1065 assert!(next_playlist_track(&state, &playlist, 0).is_none()); 1123 1066 } ··· 1142 1085 added_by: None, 1143 1086 added_at: "now".into(), 1144 1087 pending: false, 1145 - stream_url: None, 1146 1088 }]; 1147 1089 let (entry, index) = next_playlist_track(&state, &playlist, 0).unwrap(); 1148 1090 assert_eq!(entry.id, pid(1)); ··· 1163 1105 added_by: None, 1164 1106 added_at: "now".into(), 1165 1107 pending: false, 1166 - stream_url: None, 1167 1108 }, 1168 1109 PlaylistEntry { 1169 1110 id: pid(2), ··· 1175 1116 added_by: None, 1176 1117 added_at: "now".into(), 1177 1118 pending: false, 1178 - stream_url: None, 1179 1119 }, 1180 1120 ]; 1181 1121 let (entry, index) = next_playlist_track(&state, &playlist, 1).unwrap();
+621 -64
src/state.rs
··· 37 37 Skip, 38 38 /// Frontend reported the track finished playing. 39 39 TrackEnded { item_id: TrackId }, 40 - /// Asynchronous download + metadata extraction completed. 40 + /// A download failed. Remove the track from wherever it sits and advance. 41 + TrackFailed { item_id: TrackId }, 42 + /// Download completed: update metadata for ALL queued/active tracks 43 + /// and playlist entries that share this URL. No item_id needed — 44 + /// the URL is the deduplication key. 45 + MetadataResolved { 46 + url: String, 47 + title: String, 48 + duration: String, 49 + thumbnail: Option<String>, 50 + source: String, 51 + }, 52 + /// Asynchronous metadata extraction completed for a specific track. 41 53 MetadataUpdated { 42 54 item_id: TrackId, 55 + url: String, 43 56 title: String, 44 57 duration: String, 45 58 thumbnail: Option<String>, ··· 63 76 /// Persist a newly queued track to the store. 64 77 PersistQueuedTrack(QueuedTrack), 65 78 /// Persist metadata update after async extraction completes. 79 + /// `url` targets media_cache (canonical metadata store). 66 80 PersistMetadata { 67 81 item_id: TrackId, 82 + url: String, 68 83 title: String, 69 84 duration: String, 70 85 thumbnail: Option<String>, ··· 86 101 thumbnail: Option<String>, 87 102 source: String, 88 103 }, 89 - /// Update a playlist entry's metadata after extraction. 90 - UpdatePlaylistEntry { 91 - id: PlaylistEntryId, 92 - title: String, 93 - duration: String, 94 - thumbnail: Option<String>, 95 - source: String, 96 - }, 97 104 /// Remove a playlist entry. 98 105 RemovePlaylistEntry(PlaylistEntryId), 99 106 } ··· 119 126 Event::TrackQueued(track) => self.handle_queued(track), 120 127 Event::Skip => self.handle_skip(now), 121 128 Event::TrackEnded { item_id } => self.handle_track_ended(*item_id, now), 129 + Event::TrackFailed { item_id } => self.handle_track_failed(*item_id, now), 130 + Event::MetadataResolved { 131 + url, 132 + title, 133 + duration, 134 + thumbnail, 135 + source, 136 + } => self.handle_metadata_resolved(url, title, duration, thumbnail, source), 122 137 Event::MetadataUpdated { 123 138 item_id, 139 + url, 124 140 title, 125 141 duration, 126 142 thumbnail, 127 143 source, 128 - } => self.handle_metadata_updated(*item_id, title, duration, thumbnail, source), 144 + } => self.handle_metadata_updated(*item_id, url, title, duration, thumbnail, source), 129 145 } 130 146 } 131 147 ··· 148 164 149 165 /// User requested skip. 150 166 fn handle_skip(&mut self, _now: i64) -> Vec<Effect> { 151 - let mut effects = Vec::new(); 152 - 153 - if let Some(active) = self.active.take() { 154 - effects.push(Effect::AbortDownload); 155 - effects.push(Effect::PersistFinished(active.id)); 156 - self.history.insert( 157 - 0, 158 - FinishedTrack { 159 - id: active.id, 160 - title: active.title, 161 - url: active.url, 162 - duration: active.duration, 163 - thumbnail: active.thumbnail, 164 - // RoomActor overrides via PersistFinished handler which sets 165 - // the real played_at timestamp in the DB. 166 - played_at: String::new(), 167 - }, 168 - ); 169 - } 170 - 171 - if effects.is_empty() { 172 - return effects; 173 - } 174 - 175 - if let Some(next) = self.queue.first().cloned() { 176 - effects.extend(self.advance(next)); 177 - } else if !effects.is_empty() { 178 - effects.push(Effect::PublishSnapshot); 179 - } 180 - 181 - effects 167 + self.finish_active_and_advance(true) 182 168 } 183 169 184 170 /// Frontend reported the current track finished playing. 185 171 fn handle_track_ended(&mut self, item_id: TrackId, _now: i64) -> Vec<Effect> { 186 172 // Only react if this matches the currently active track. 187 - let is_current = self 188 - .active 189 - .as_ref() 190 - .map(|t| t.id == item_id) 191 - .unwrap_or(false); 192 - 173 + let is_current = self.active.as_ref().is_some_and(|t| t.id == item_id); 193 174 if !is_current { 194 175 return Vec::new(); // stale event, ignore 195 176 } 177 + self.finish_active_and_advance(false) 178 + } 196 179 180 + /// Shared logic: move the active track to history, then advance the next 181 + /// queued track (if any). `abort` controls whether `AbortDownload` is emitted. 182 + fn finish_active_and_advance(&mut self, abort: bool) -> Vec<Effect> { 197 183 let mut effects = Vec::new(); 198 184 199 - if let Some(active) = self.active.take() { 200 - effects.push(Effect::PersistFinished(active.id)); 201 - self.history.insert( 202 - 0, 203 - FinishedTrack { 204 - id: active.id, 205 - title: active.title, 206 - url: active.url, 207 - duration: active.duration, 208 - thumbnail: active.thumbnail, 209 - // RoomActor overrides via PersistFinished handler which sets 210 - // the real played_at timestamp in the DB. 211 - played_at: String::new(), 212 - }, 213 - ); 185 + let Some(active) = self.active.take() else { 186 + return effects; 187 + }; 188 + 189 + if abort { 190 + effects.push(Effect::AbortDownload); 214 191 } 192 + effects.push(Effect::PersistFinished(active.id)); 193 + self.history.insert( 194 + 0, 195 + FinishedTrack { 196 + id: active.id, 197 + title: active.title, 198 + url: active.url, 199 + duration: active.duration, 200 + thumbnail: active.thumbnail, 201 + // RoomActor overrides via PersistFinished handler which sets 202 + // the real played_at timestamp in the DB. 203 + played_at: String::new(), 204 + }, 205 + ); 215 206 216 207 if let Some(next) = self.queue.first().cloned() { 217 208 effects.extend(self.advance(next)); ··· 222 213 effects 223 214 } 224 215 216 + /// A download failed. Remove the track from wherever it sits (active or 217 + /// queue) and advance if it was the currently playing track. 218 + fn handle_track_failed(&mut self, item_id: TrackId, _now: i64) -> Vec<Effect> { 219 + if self.active.as_ref().is_some_and(|t| t.id == item_id) { 220 + return self.finish_active_and_advance(true); 221 + } 222 + 223 + if self.queue.iter().any(|t| t.id == item_id) { 224 + self.queue.retain(|t| t.id != item_id); 225 + return vec![Effect::PublishSnapshot]; 226 + } 227 + 228 + Vec::new() 229 + } 230 + 225 231 /// Asynchronous metadata resolution completed. Updates the matching item 226 232 /// in the queue and/or the active track in-place. Does not change playback 227 233 /// state, only updates display fields. 228 234 fn handle_metadata_updated( 229 235 &mut self, 230 236 item_id: TrackId, 237 + url: &str, 231 238 title: &str, 232 239 duration: &str, 233 240 thumbnail: &Option<String>, ··· 249 256 vec![ 250 257 Effect::PersistMetadata { 251 258 item_id, 259 + url: url.to_string(), 260 + title: title.to_string(), 261 + duration: duration.to_string(), 262 + thumbnail: thumbnail.clone(), 263 + source: source.to_string(), 264 + }, 265 + Effect::PublishSnapshot, 266 + ] 267 + } 268 + 269 + /// Download completed: update ALL queued and active tracks matching `url`. 270 + /// Produces a single PersistMetadata effect (media_cache is URL-keyed) 271 + /// and a PublishSnapshot to broadcast the updated metadata. 272 + fn handle_metadata_resolved( 273 + &mut self, 274 + url: &str, 275 + title: &str, 276 + duration: &str, 277 + thumbnail: &Option<String>, 278 + source: &str, 279 + ) -> Vec<Effect> { 280 + for item in &mut self.queue { 281 + if item.url == url { 282 + item.title = title.to_string(); 283 + item.duration = duration.to_string(); 284 + item.thumbnail.clone_from(thumbnail); 285 + item.pending = false; 286 + } 287 + } 288 + if let Some(ref mut active) = self.active { 289 + if active.url == url { 290 + active.title = title.to_string(); 291 + active.duration = duration.to_string(); 292 + active.thumbnail.clone_from(thumbnail); 293 + } 294 + } 295 + vec![ 296 + Effect::PersistMetadata { 297 + // item_id unused — media_cache is keyed by url 298 + item_id: TrackId::nil(), 299 + url: url.to_string(), 252 300 title: title.to_string(), 253 301 duration: duration.to_string(), 254 302 thumbnail: thumbnail.clone(), ··· 587 635 let effects = s.transition( 588 636 &Event::MetadataUpdated { 589 637 item_id: track_id(1), 638 + url: "https://example.com/A".into(), 590 639 title: "Track A (Real)".into(), 591 640 duration: "4:20".into(), 592 641 thumbnail: Some("https://img.example/a.jpg".into()), ··· 594 643 }, 595 644 0, 596 645 ); 597 - 598 646 assert_eq!( 599 647 effects, 600 648 vec![ 601 649 Effect::PersistMetadata { 602 650 item_id: track_id(1), 651 + url: "https://example.com/A".into(), 603 652 title: "Track A (Real)".into(), 604 653 duration: "4:20".into(), 605 654 thumbnail: Some("https://img.example/a.jpg".into()), ··· 609 658 ] 610 659 ); 611 660 612 - // Queue should have empty (track was advanced to active), but active 661 + // Queue should be empty (track was advanced to active), but active 613 662 // should have updated metadata. 614 663 assert_eq!(s.active.as_ref().unwrap().title, "Track A (Real)"); 615 664 assert_eq!(s.active.as_ref().unwrap().duration, "4:20"); ··· 629 678 let effects = s.transition( 630 679 &Event::MetadataUpdated { 631 680 item_id: track_id(2), 681 + url: "https://example.com/B".into(), 632 682 title: "Track B (Real)".into(), 633 683 duration: "5:55".into(), 634 684 thumbnail: None, ··· 641 691 vec![ 642 692 Effect::PersistMetadata { 643 693 item_id: track_id(2), 694 + url: "https://example.com/B".into(), 644 695 title: "Track B (Real)".into(), 645 696 duration: "5:55".into(), 646 697 thumbnail: None, ··· 659 710 let effects = s.transition( 660 711 &Event::MetadataUpdated { 661 712 item_id: track_id(999), 713 + url: "https://example.com/Ghost".into(), 662 714 title: "Ghost".into(), 663 715 duration: "0:00".into(), 664 716 thumbnail: None, ··· 673 725 vec![ 674 726 Effect::PersistMetadata { 675 727 item_id: track_id(999), 728 + url: "https://example.com/Ghost".into(), 676 729 title: "Ghost".into(), 677 730 duration: "0:00".into(), 678 731 thumbnail: None, ··· 683 736 ); 684 737 assert!(s.active.is_none()); 685 738 assert!(s.queue.is_empty()); 739 + } 740 + 741 + // --- TrackFailed tests --- 742 + 743 + #[test] 744 + fn test_track_failed_removes_from_queue() { 745 + let mut s = PlaybackState::default(); 746 + s.transition(&Event::TrackQueued(queued("A", 1)), 0); 747 + s.transition(&Event::TrackQueued(queued("B", 2)), 0); 748 + // B is in queue, behind active A. 749 + let effects = s.transition( 750 + &Event::TrackFailed { 751 + item_id: track_id(2), 752 + }, 753 + 0, 754 + ); 755 + 756 + assert!(!s.queue.iter().any(|t| t.id == track_id(2))); 757 + assert_eq!(effects, vec![Effect::PublishSnapshot]); 758 + // A still active; B removed. 759 + assert_eq!(s.active.as_ref().unwrap().title, "A"); 760 + } 761 + 762 + #[test] 763 + fn test_track_failed_while_active_acts_like_skip() { 764 + let mut s = PlaybackState::default(); 765 + s.transition(&Event::TrackQueued(queued("A", 1)), 0); 766 + // A is now active. 767 + let effects = s.transition( 768 + &Event::TrackFailed { 769 + item_id: track_id(1), 770 + }, 771 + 0, 772 + ); 773 + 774 + assert_eq!( 775 + effects, 776 + vec![ 777 + Effect::AbortDownload, 778 + Effect::PersistFinished(track_id(1)), 779 + Effect::PublishSnapshot, 780 + ] 781 + ); 782 + assert!(s.active.is_none()); 783 + assert_eq!(s.history.len(), 1); 784 + assert_eq!(s.history[0].title, "A"); 785 + } 786 + 787 + #[test] 788 + fn test_track_failed_active_with_next_advances() { 789 + let mut s = PlaybackState::default(); 790 + s.transition(&Event::TrackQueued(queued("A", 1)), 0); 791 + s.transition(&Event::TrackQueued(queued("B", 2)), 0); 792 + // A active, B queued. 793 + let effects = s.transition( 794 + &Event::TrackFailed { 795 + item_id: track_id(1), 796 + }, 797 + 0, 798 + ); 799 + 800 + assert_eq!(s.active.as_ref().unwrap().title, "B"); 801 + assert!(s.queue.is_empty()); 802 + assert_eq!(s.history.len(), 1); 803 + assert_eq!(s.history[0].title, "A"); 804 + // PersistFinished for A, PersistStarted for B, PublishSnapshot. 805 + assert!(effects.contains(&Effect::AbortDownload)); 806 + assert!(effects.contains(&Effect::PersistFinished(track_id(1)))); 807 + assert!(effects.contains(&Effect::PersistStarted(track_id(2)))); 808 + assert!(effects.contains(&Effect::PublishSnapshot)); 809 + } 810 + 811 + #[test] 812 + fn test_track_failed_stale_id_noop() { 813 + let mut s = PlaybackState::default(); 814 + s.transition(&Event::TrackQueued(queued("A", 1)), 0); 815 + let effects = s.transition( 816 + &Event::TrackFailed { 817 + item_id: track_id(999), 818 + }, 819 + 0, 820 + ); 821 + 822 + assert!(effects.is_empty()); 823 + assert_eq!(s.active.as_ref().unwrap().title, "A"); 824 + } 825 + 826 + #[test] 827 + fn test_track_failed_idle_noop() { 828 + let mut s = PlaybackState::default(); 829 + let effects = s.transition( 830 + &Event::TrackFailed { 831 + item_id: track_id(1), 832 + }, 833 + 0, 834 + ); 835 + 836 + assert!(effects.is_empty()); 837 + assert!(s.active.is_none()); 838 + assert!(s.queue.is_empty()); 839 + } 840 + 841 + #[test] 842 + fn test_metadata_resolved_updates_all_matching_by_url() { 843 + // Four tracks: first advances to active (different URL), three in queue 844 + // where two share the same URL. 845 + let mut s = PlaybackState::default(); 846 + s.transition(&Event::TrackQueued(queued("Active", 1)), 0); 847 + let mut q1 = queued("B", 2); 848 + q1.url = "https://example.com/shared".into(); 849 + q1.pending = true; 850 + s.transition(&Event::TrackQueued(q1), 0); 851 + let mut qc = queued("C-other", 3); 852 + qc.pending = true; 853 + s.transition(&Event::TrackQueued(qc), 0); 854 + let mut q2 = queued("D", 4); 855 + q2.url = "https://example.com/shared".into(); 856 + q2.pending = true; 857 + s.transition(&Event::TrackQueued(q2), 0); 858 + 859 + // Queue: [B (shared), C (other), D (shared)] — indices 0, 1, 2. 860 + 861 + let effects = s.transition( 862 + &Event::MetadataResolved { 863 + url: "https://example.com/shared".into(), 864 + title: "Real Title".into(), 865 + duration: "3:30".into(), 866 + thumbnail: Some("https://img.example/thumb.jpg".into()), 867 + source: "ytdlp".into(), 868 + }, 869 + 0, 870 + ); 871 + 872 + // Matching queue tracks (B at 0, D at 2) updated. 873 + assert_eq!(s.queue[0].title, "Real Title"); 874 + assert_eq!(s.queue[0].duration, "3:30"); 875 + assert!(!s.queue[0].pending); 876 + 877 + assert_eq!(s.queue[2].title, "Real Title"); 878 + assert_eq!(s.queue[2].duration, "3:30"); 879 + assert!(!s.queue[2].pending); 880 + 881 + // Non-matching queue track (C at 1) unchanged. 882 + assert_eq!(s.queue[1].title, "C-other"); 883 + assert!(s.queue[1].pending); 884 + 885 + // Active track has different URL, should be unchanged. 886 + assert_eq!(s.active.as_ref().unwrap().title, "Active"); 887 + 888 + assert_eq!( 889 + effects, 890 + vec![ 891 + Effect::PersistMetadata { 892 + item_id: TrackId::nil(), 893 + url: "https://example.com/shared".into(), 894 + title: "Real Title".into(), 895 + duration: "3:30".into(), 896 + thumbnail: Some("https://img.example/thumb.jpg".into()), 897 + source: "ytdlp".into(), 898 + }, 899 + Effect::PublishSnapshot, 900 + ] 901 + ); 902 + } 903 + 904 + #[test] 905 + fn test_metadata_resolved_updates_active_track() { 906 + let mut s = PlaybackState::default(); 907 + // Queue one track — room is idle, so it advances to active. 908 + s.transition(&Event::TrackQueued(queued("Active", 1)), 0); 909 + s.active.as_mut().unwrap().url = "https://example.com/active".into(); 910 + 911 + let effects = s.transition( 912 + &Event::MetadataResolved { 913 + url: "https://example.com/active".into(), 914 + title: "Resolved Active".into(), 915 + duration: "2:00".into(), 916 + thumbnail: None, 917 + source: "ytdlp".into(), 918 + }, 919 + 0, 920 + ); 921 + 922 + assert_eq!(s.active.as_ref().unwrap().title, "Resolved Active"); 923 + assert_eq!(s.active.as_ref().unwrap().duration, "2:00"); 924 + 925 + assert_eq!( 926 + effects, 927 + vec![ 928 + Effect::PersistMetadata { 929 + item_id: TrackId::nil(), 930 + url: "https://example.com/active".into(), 931 + title: "Resolved Active".into(), 932 + duration: "2:00".into(), 933 + thumbnail: None, 934 + source: "ytdlp".into(), 935 + }, 936 + Effect::PublishSnapshot, 937 + ] 938 + ); 939 + } 940 + } 941 + 942 + /// Deterministic simulation testing: random event sequences with invariant 943 + /// checks after every step and at rest. 944 + #[cfg(test)] 945 + mod dst { 946 + use proptest::prelude::*; 947 + 948 + use super::*; 949 + 950 + fn tid(n: u64) -> TrackId { 951 + TrackId(uuid::Uuid::from_u64_pair(0, n)) 952 + } 953 + 954 + fn queued(n: u64) -> QueuedTrack { 955 + QueuedTrack { 956 + id: tid(n), 957 + title: format!("Track-{n}"), 958 + url: format!("https://example.com/track-{n}"), 959 + duration: "3:00".into(), 960 + thumbnail: None, 961 + pending: n % 3 == 0, 962 + } 963 + } 964 + 965 + #[derive(Debug, Clone)] 966 + enum Op { 967 + Queue(u64), 968 + Skip, 969 + TrackEnded(u64), 970 + TrackFailed(u64), 971 + MetadataResolved(u64), 972 + MetadataUpdated(u64), 973 + } 974 + 975 + fn op_strategy() -> impl Strategy<Value = Vec<Op>> { 976 + let op = prop_oneof![ 977 + (0u64..12).prop_map(Op::Queue), 978 + Just(Op::Skip), 979 + (0u64..12).prop_map(Op::TrackEnded), 980 + (0u64..12).prop_map(Op::TrackFailed), 981 + (0u64..12).prop_map(Op::MetadataResolved), 982 + (0u64..12).prop_map(Op::MetadataUpdated), 983 + ]; 984 + proptest::collection::vec(op, 0..50) 985 + } 986 + 987 + fn apply_ops(ops: &[Op]) { 988 + let mut state = PlaybackState::default(); 989 + let mut queued_ids = std::collections::HashSet::new(); 990 + let mut history_snapshot = Vec::new(); 991 + 992 + for op in ops { 993 + let prev_active_id = state.active.as_ref().map(|a| a.id); 994 + 995 + let effects = match op { 996 + Op::Queue(n) => { 997 + if !queued_ids.insert(*n) { 998 + continue; 999 + } 1000 + 1001 + let t = queued(*n); 1002 + state.transition(&Event::TrackQueued(t), 0) 1003 + } 1004 + Op::Skip => state.transition(&Event::Skip, 0), 1005 + Op::TrackEnded(n) => state.transition(&Event::TrackEnded { item_id: tid(*n) }, 0), 1006 + Op::TrackFailed(n) => { 1007 + let id = tid(*n); 1008 + let effects = state.transition(&Event::TrackFailed { item_id: id }, 0); 1009 + 1010 + // TrackFailed must only affect the named track. 1011 + if prev_active_id.is_some_and(|aid| aid != id) { 1012 + assert!( 1013 + state.active.is_some(), 1014 + "TrackFailed({id}) removed active track {aid}", 1015 + aid = prev_active_id.unwrap() 1016 + ); 1017 + assert_eq!( 1018 + state.active.as_ref().map(|a| a.id), 1019 + prev_active_id, 1020 + "TrackFailed({id}) changed active track from {aid}", 1021 + aid = prev_active_id.unwrap() 1022 + ); 1023 + } 1024 + effects 1025 + } 1026 + Op::MetadataResolved(n) => { 1027 + let q = queued(*n); 1028 + let effects = state.transition( 1029 + &Event::MetadataResolved { 1030 + url: q.url.clone(), 1031 + title: format!("Resolved-{n}"), 1032 + duration: "4:20".into(), 1033 + thumbnail: None, 1034 + source: "ytdlp".into(), 1035 + }, 1036 + 0, 1037 + ); 1038 + // Check per-event: all queue tracks matching this URL 1039 + // must now be !pending. 1040 + for t in &state.queue { 1041 + if t.url == q.url { 1042 + assert!( 1043 + !t.pending, 1044 + "queue track {id} still pending after MetadataResolved({url})", 1045 + id = t.id, 1046 + url = q.url, 1047 + ); 1048 + } 1049 + } 1050 + effects 1051 + } 1052 + Op::MetadataUpdated(n) => { 1053 + let q = queued(*n); 1054 + state.transition( 1055 + &Event::MetadataUpdated { 1056 + item_id: q.id, 1057 + url: q.url, 1058 + title: format!("Updated-{n}"), 1059 + duration: "5:00".into(), 1060 + thumbnail: None, 1061 + source: "ytdlp".into(), 1062 + }, 1063 + 0, 1064 + ) 1065 + } 1066 + }; 1067 + 1068 + check_invariants(&state, &effects, &queued_ids, &mut history_snapshot); 1069 + } 1070 + 1071 + // Final-state crash test: applying any event to the final state 1072 + // should never panic. 1073 + let _ = state.transition(&Event::Skip, 0); 1074 + let _ = state.transition( 1075 + &Event::MetadataResolved { 1076 + url: "https://example.com/ghost".into(), 1077 + title: "Ghost".into(), 1078 + duration: "0:00".into(), 1079 + thumbnail: None, 1080 + source: "ytdlp".into(), 1081 + }, 1082 + 0, 1083 + ); 1084 + // Determinism: same event on identical state should produce same effects. 1085 + let mut clone = state.clone(); 1086 + let e1 = state.transition(&Event::Skip, 0); 1087 + let e2 = clone.transition(&Event::Skip, 0); 1088 + assert_eq!(e1, e2, "non-deterministic Skip on final state"); 1089 + } 1090 + 1091 + fn check_invariants( 1092 + state: &PlaybackState, 1093 + effects: &[Effect], 1094 + queued_ids: &std::collections::HashSet<u64>, 1095 + history_snapshot: &mut Vec<FinishedTrack>, 1096 + ) { 1097 + // === State structural invariants === 1098 + 1099 + // 1. All track IDs across active + queue + history are unique. 1100 + let mut ids = std::collections::HashSet::new(); 1101 + if let Some(ref a) = state.active { 1102 + assert!(ids.insert(a.id), "duplicate id in active: {a}", a = a.id); 1103 + } 1104 + for t in &state.queue { 1105 + assert!(ids.insert(t.id), "duplicate id in queue: {t}", t = t.id); 1106 + } 1107 + for h in &state.history { 1108 + assert!(ids.insert(h.id), "duplicate id in history: {h}", h = h.id); 1109 + } 1110 + 1111 + // 2. Active track is never also in queue or history. 1112 + if let Some(ref a) = state.active { 1113 + assert!( 1114 + !state.queue.iter().any(|t| t.id == a.id), 1115 + "active track {a} also in queue", 1116 + a = a.id 1117 + ); 1118 + assert!( 1119 + !state.history.iter().any(|h| h.id == a.id), 1120 + "active track {a} also in history", 1121 + a = a.id 1122 + ); 1123 + } 1124 + 1125 + // 3. Queue and history are disjoint. 1126 + for h in &state.history { 1127 + assert!( 1128 + !state.queue.iter().any(|t| t.id == h.id), 1129 + "track {h} in both queue and history", 1130 + h = h.id 1131 + ); 1132 + } 1133 + 1134 + // 4. Conservation: every track in active/queue/history was once queued. 1135 + for id in &ids { 1136 + let n = id.0.as_u64_pair().1; 1137 + assert!( 1138 + queued_ids.contains(&n), 1139 + "orphan track id {id} was never queued" 1140 + ); 1141 + } 1142 + 1143 + // 5. History is monotonic: never shrinks, and held tracks are stable. 1144 + assert!( 1145 + state.history.len() >= history_snapshot.len(), 1146 + "history shrank from {} to {}", 1147 + history_snapshot.len(), 1148 + state.history.len() 1149 + ); 1150 + for prev in history_snapshot.iter() { 1151 + assert!( 1152 + state.history.iter().any(|h| h.id == prev.id), 1153 + "history lost track {id}", 1154 + id = prev.id 1155 + ); 1156 + } 1157 + *history_snapshot = state.history.clone(); 1158 + 1159 + // === Effect composition invariants === 1160 + 1161 + // 6. Within a single transition, all Persist* effects precede 1162 + // PublishSnapshot. There may be zero or one PublishSnapshot. 1163 + let publish_pos = effects 1164 + .iter() 1165 + .position(|e| matches!(e, Effect::PublishSnapshot)); 1166 + if let Some(pos) = publish_pos { 1167 + for e in &effects[pos + 1..] { 1168 + assert!( 1169 + !is_persist_effect(e), 1170 + "persist effect {e:?} appears after PublishSnapshot" 1171 + ); 1172 + } 1173 + } 1174 + 1175 + // 7. State-changing effects must be followed by PublishSnapshot. 1176 + let needs_publish = effects.iter().any(|e| { 1177 + matches!( 1178 + e, 1179 + Effect::PersistStarted(_) 1180 + | Effect::PersistFinished(_) 1181 + | Effect::PersistQueuedTrack(_) 1182 + | Effect::PersistMetadata { .. } 1183 + | Effect::AbortDownload 1184 + ) 1185 + }); 1186 + if needs_publish { 1187 + assert!( 1188 + effects.iter().any(|e| matches!(e, Effect::PublishSnapshot)), 1189 + "state-changing effects without PublishSnapshot: {effects:?}" 1190 + ); 1191 + } 1192 + 1193 + // 8. PersistStarted and PersistFinished for the same track can't 1194 + // appear in the same transition. 1195 + let started: std::collections::HashSet<TrackId> = effects 1196 + .iter() 1197 + .filter_map(|e| { 1198 + if let Effect::PersistStarted(id) = e { 1199 + Some(*id) 1200 + } else { 1201 + None 1202 + } 1203 + }) 1204 + .collect(); 1205 + for e in effects { 1206 + if let Effect::PersistFinished(id) = e { 1207 + assert!( 1208 + !started.contains(id), 1209 + "PersistStarted and PersistFinished for same track: {id}" 1210 + ); 1211 + } 1212 + } 1213 + 1214 + // 9. AbortDownload always accompanies PersistFinished (only fired 1215 + // when the active track is aborted during skip/fail). 1216 + if effects.iter().any(|e| matches!(e, Effect::AbortDownload)) { 1217 + assert!( 1218 + effects 1219 + .iter() 1220 + .any(|e| matches!(e, Effect::PersistFinished(_))), 1221 + "AbortDownload without PersistFinished" 1222 + ); 1223 + } 1224 + } 1225 + 1226 + fn is_persist_effect(e: &Effect) -> bool { 1227 + matches!( 1228 + e, 1229 + Effect::PersistStarted(_) 1230 + | Effect::PersistFinished(_) 1231 + | Effect::PersistQueuedTrack(_) 1232 + | Effect::PersistMetadata { .. } 1233 + | Effect::AddPlaylistEntry { .. } 1234 + | Effect::RemovePlaylistEntry(_) 1235 + ) 1236 + } 1237 + 1238 + proptest! { 1239 + #[test] 1240 + fn dst_random_sequences_invariants_hold(ops in op_strategy()) { 1241 + apply_ops(&ops); 1242 + } 686 1243 } 687 1244 }
+99 -113
src/store.rs
··· 18 18 use crate::types::{ChatMessage, ChatMessageId, PlaylistEntryId, TrackId}; 19 19 20 20 /// An entry in a room's perpetual playlist — used for auto-fill when the 21 - /// queue runs dry. Metadata is pre-resolved at add-time so auto-played 22 - /// tracks never show a "Loading..." placeholder. 21 + /// queue runs dry. Metadata is pre-resolved via the [`MEDIA_CACHE`] table so 22 + /// auto-played tracks never show a "Loading..." placeholder. 23 + /// 24 + /// The `title`, `duration`, `thumbnail`, `source`, and `pending` fields are 25 + /// populated at load time via a JOIN with `media_cache`; they are NOT stored 26 + /// independently in the `playlist_entries` table. 23 27 #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] 24 28 pub(crate) struct PlaylistEntry { 25 29 pub id: PlaylistEntryId, ··· 30 34 pub source: String, 31 35 pub added_by: Option<String>, 32 36 pub added_at: String, 37 + /// Derived from `media_cache.state` at load time. 33 38 pub pending: bool, 34 - /// Pre-resolved direct stream URL, cached at extraction time. 35 - pub stream_url: Option<String>, 36 39 } 37 40 38 41 /// Full snapshot of a room's state loaded from the store. ··· 110 113 added_by: Option<String>, 111 114 added_at: String, 112 115 pending: bool, 113 - stream_url: Option<String>, 114 116 } 115 117 116 118 impl From<InMemoryPlaylistEntry> for PlaylistEntry { ··· 125 127 added_by: e.added_by, 126 128 added_at: e.added_at, 127 129 pending: e.pending, 128 - stream_url: e.stream_url, 129 130 } 130 131 } 131 132 } ··· 231 232 } 232 233 Effect::PersistMetadata { 233 234 item_id, 235 + url: _, 234 236 title, 235 237 duration, 236 238 thumbnail, ··· 285 287 added_by: None, 286 288 added_at: crate::util::now_iso(), 287 289 pending: true, 288 - stream_url: None, 289 290 }); 290 291 } 291 - Effect::UpdatePlaylistEntry { 292 - id, 293 - title, 294 - duration, 295 - thumbnail, 296 - source, 297 - } => { 298 - if let Some(entry) = room.playlist.iter_mut().find(|e| e.id == *id) { 299 - entry.title.clone_from(title); 300 - entry.duration.clone_from(duration); 301 - entry.thumbnail.clone_from(thumbnail); 302 - entry.source.clone_from(source); 303 - entry.pending = false; 304 - } 305 - } 306 292 Effect::RemovePlaylistEntry(id) => { 307 293 room.playlist.retain(|e| e.id != *id); 308 294 } ··· 407 393 .execute(&pool) 408 394 .await?; 409 395 396 + // Canonical resolved media keyed by URL. Tracks and playlist entries 397 + // reference this table to avoid duplicating metadata and downloads. 398 + sqlx::query( 399 + "CREATE TABLE IF NOT EXISTS media_cache ( 400 + url TEXT PRIMARY KEY, 401 + title TEXT NOT NULL DEFAULT '', 402 + duration TEXT NOT NULL DEFAULT '--:--', 403 + thumbnail TEXT, 404 + source TEXT NOT NULL DEFAULT '', 405 + file_path TEXT NOT NULL DEFAULT '', 406 + state TEXT NOT NULL DEFAULT 'pending' 407 + )", 408 + ) 409 + .execute(&pool) 410 + .await?; 411 + 410 412 sqlx::query( 411 413 "CREATE TABLE IF NOT EXISTS tracks ( 412 414 id TEXT PRIMARY KEY, 413 415 room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, 414 416 url TEXT NOT NULL, 415 - title TEXT NOT NULL, 416 - duration TEXT NOT NULL DEFAULT '--:--', 417 - thumbnail TEXT, 418 - source TEXT NOT NULL DEFAULT '', 419 417 position INTEGER NOT NULL, 420 418 added_at TEXT NOT NULL, 421 419 played INTEGER NOT NULL DEFAULT 0, 422 420 played_at TEXT, 423 - started_at_ms INTEGER, 424 - pending INTEGER NOT NULL DEFAULT 1 421 + started_at_ms INTEGER 425 422 )", 426 423 ) 427 424 .execute(&pool) ··· 453 450 id TEXT PRIMARY KEY, 454 451 room_id TEXT NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, 455 452 url TEXT NOT NULL, 456 - title TEXT NOT NULL DEFAULT '', 457 - duration TEXT NOT NULL DEFAULT '--:--', 458 - thumbnail TEXT, 459 - source TEXT NOT NULL DEFAULT '', 460 453 added_by TEXT, 461 - added_at TEXT NOT NULL, 462 - pending INTEGER NOT NULL DEFAULT 1, 463 - stream_url TEXT 454 + added_at TEXT NOT NULL 464 455 )", 465 456 ) 466 457 .execute(&pool) ··· 495 486 .bind(room_id) 496 487 .fetch_one(&mut *tx) 497 488 .await?; 489 + 490 + // Ensure a media_cache row exists for deduplication. 491 + sqlx::query("INSERT OR IGNORE INTO media_cache (url) VALUES (?1)") 492 + .bind(&track.url) 493 + .execute(&mut *tx) 494 + .await?; 498 495 499 496 sqlx::query( 500 - "INSERT INTO tracks 501 - (id, room_id, url, title, duration, thumbnail, source, 502 - position, added_at, pending) 503 - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", 497 + "INSERT INTO tracks (id, room_id, url, position, added_at) 498 + VALUES (?1, ?2, ?3, ?4, ?5)", 504 499 ) 505 500 .bind(track.id.as_str_buf()) 506 501 .bind(room_id) 507 502 .bind(&track.url) 508 - .bind(&track.title) 509 - .bind(&track.duration) 510 - .bind(track.thumbnail.as_deref()) 511 - .bind("") 512 503 .bind(position) 513 504 .bind(crate::util::now_iso()) 514 - .bind(track.pending as i64) 515 505 .execute(&mut *tx) 516 506 .await?; 517 507 } ··· 534 524 .await?; 535 525 } 536 526 Effect::PersistMetadata { 537 - item_id, 527 + item_id: _, 528 + url, 538 529 title, 539 530 duration, 540 531 thumbnail, 541 532 source, 542 533 } => { 543 - let src_str = source.to_string(); 544 534 sqlx::query( 545 - "UPDATE tracks 535 + "UPDATE media_cache 546 536 SET title = ?1, duration = ?2, thumbnail = ?3, 547 - source = ?4, pending = 0 548 - WHERE id = ?5", 537 + source = ?4, state = 'ready' 538 + WHERE url = ?5", 549 539 ) 550 540 .bind(title.as_str()) 551 541 .bind(duration.as_str()) 552 542 .bind(thumbnail.as_deref()) 553 - .bind(&src_str) 554 - .bind(item_id.as_str_buf()) 543 + .bind(source.as_str()) 544 + .bind(url.as_str()) 555 545 .execute(&mut *tx) 556 546 .await?; 557 547 } ··· 577 567 .await?; 578 568 output.chat_message_id = Some(id); 579 569 } 580 - Effect::AddPlaylistEntry { 581 - id, 582 - url, 583 - title, 584 - duration, 585 - thumbnail, 586 - source, 587 - } => { 570 + Effect::AddPlaylistEntry { id, url, .. } => { 571 + // Ensure a media_cache row exists for deduplication. 572 + sqlx::query("INSERT OR IGNORE INTO media_cache (url) VALUES (?1)") 573 + .bind(url.as_str()) 574 + .execute(&mut *tx) 575 + .await?; 576 + 588 577 sqlx::query( 589 - "INSERT INTO playlist_entries (id, room_id, url, title, duration, thumbnail, source, added_at, pending) 590 - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 1)", 578 + "INSERT INTO playlist_entries (id, room_id, url, added_at) 579 + VALUES (?1, ?2, ?3, ?4)", 591 580 ) 592 581 .bind(id.as_str_buf()) 593 582 .bind(room_id) 594 583 .bind(url.as_str()) 595 - .bind(title.as_str()) 596 - .bind(duration.as_str()) 597 - .bind(thumbnail.as_deref()) 598 - .bind(source.as_str()) 599 584 .bind(crate::util::now_iso()) 600 585 .execute(&mut *tx) 601 586 .await?; 602 587 } 603 - Effect::UpdatePlaylistEntry { 604 - id, 605 - title, 606 - duration, 607 - thumbnail, 608 - source, 609 - } => { 610 - sqlx::query( 611 - "UPDATE playlist_entries SET title = ?1, duration = ?2, thumbnail = ?3, source = ?4, pending = 0 WHERE id = ?5", 612 - ) 613 - .bind(title.as_str()) 614 - .bind(duration.as_str()) 615 - .bind(thumbnail.as_deref()) 616 - .bind(source.as_str()) 617 - .bind(id.as_str_buf()) 618 - .execute(&mut *tx) 619 - .await?; 620 - } 621 588 Effect::RemovePlaylistEntry(id) => { 622 589 sqlx::query("DELETE FROM playlist_entries WHERE id = ?") 623 590 .bind(id.as_str_buf()) ··· 633 600 } 634 601 } 635 602 636 - /// Helper row type for deserialising `tracks` rows. 603 + /// Helper row type for deserialising tracks JOINed with media_cache. 637 604 #[derive(sqlx::FromRow)] 638 605 struct TrackRow { 639 606 id: String, 640 - title: String, 641 607 url: String, 608 + played_at: Option<String>, 609 + title: String, 642 610 duration: String, 643 611 thumbnail: Option<String>, 644 - played_at: Option<String>, 645 - pending: i64, 612 + state: String, 646 613 } 647 614 648 - /// Helper row type for deserialising `playlist_entries` rows. 615 + /// Helper row type for deserialising playlist_entries JOINed with media_cache. 649 616 #[derive(sqlx::FromRow)] 650 617 struct PlaylistRow { 651 618 id: String, 652 619 url: String, 620 + added_by: Option<String>, 621 + added_at: String, 653 622 title: String, 654 623 duration: String, 655 624 thumbnail: Option<String>, 656 625 source: String, 657 - added_by: Option<String>, 658 - added_at: String, 659 - pending: i64, 660 - stream_url: Option<String>, 626 + state: String, 661 627 } 662 628 663 629 /// Helper row type for deserialising `chat_messages` rows. ··· 742 708 }; 743 709 744 710 // 2. Queue: not played, not started, ordered by position. 711 + // JOIN media_cache for resolved metadata. 745 712 let queue_rows: Vec<TrackRow> = sqlx::query_as( 746 - "SELECT id, url, title, duration, thumbnail, played_at, pending 747 - FROM tracks 748 - WHERE room_id = ? AND played = 0 AND started_at_ms IS NULL 749 - ORDER BY position", 713 + "SELECT t.id, t.url, t.played_at, 714 + COALESCE(m.title, 'Loading...') AS title, 715 + COALESCE(m.duration, '--:--') AS duration, 716 + m.thumbnail, m.state 717 + FROM tracks t 718 + LEFT JOIN media_cache m ON m.url = t.url 719 + WHERE t.room_id = ? AND t.played = 0 AND t.started_at_ms IS NULL 720 + ORDER BY t.position", 750 721 ) 751 722 .bind(room_id) 752 723 .fetch_all(&self.pool) ··· 754 725 755 726 // 3. Active: not played, started (at most one). 756 727 let active_row: Option<TrackRow> = sqlx::query_as( 757 - "SELECT id, url, title, duration, thumbnail, played_at, pending 758 - FROM tracks 759 - WHERE room_id = ? AND played = 0 AND started_at_ms IS NOT NULL 760 - ORDER BY position 728 + "SELECT t.id, t.url, t.played_at, 729 + COALESCE(m.title, 'Loading...') AS title, 730 + COALESCE(m.duration, '--:--') AS duration, 731 + m.thumbnail, m.state 732 + FROM tracks t 733 + LEFT JOIN media_cache m ON m.url = t.url 734 + WHERE t.room_id = ? AND t.played = 0 AND t.started_at_ms IS NOT NULL 735 + ORDER BY t.position 761 736 LIMIT 1", 762 737 ) 763 738 .bind(room_id) ··· 766 741 767 742 // 4. History: played, newest first. 768 743 let history_rows: Vec<TrackRow> = sqlx::query_as( 769 - "SELECT id, url, title, duration, thumbnail, played_at, pending 770 - FROM tracks 771 - WHERE room_id = ? AND played = 1 772 - ORDER BY played_at DESC", 744 + "SELECT t.id, t.url, t.played_at, 745 + COALESCE(m.title, 'Unknown') AS title, 746 + COALESCE(m.duration, '--:--') AS duration, 747 + m.thumbnail, m.state 748 + FROM tracks t 749 + LEFT JOIN media_cache m ON m.url = t.url 750 + WHERE t.room_id = ? AND t.played = 1 751 + ORDER BY t.played_at DESC", 773 752 ) 774 753 .bind(room_id) 775 754 .fetch_all(&self.pool) ··· 787 766 .fetch_all(&self.pool) 788 767 .await?; 789 768 790 - // 6. Playlist entries, ordered by id. 769 + // 6. Playlist entries with media_cache metadata. 791 770 let playlist_rows: Vec<PlaylistRow> = sqlx::query_as( 792 - "SELECT id, url, title, duration, thumbnail, source, added_by, added_at, pending, stream_url 793 - FROM playlist_entries 794 - WHERE room_id = ? 795 - ORDER BY id", 771 + "SELECT p.id, p.url, p.added_by, p.added_at, 772 + COALESCE(m.title, 'Loading...') AS title, 773 + COALESCE(m.duration, '--:--') AS duration, 774 + m.thumbnail, m.source, m.state 775 + FROM playlist_entries p 776 + LEFT JOIN media_cache m ON m.url = p.url 777 + WHERE p.room_id = ? 778 + ORDER BY p.id", 796 779 ) 797 780 .bind(room_id) 798 781 .fetch_all(&self.pool) 799 782 .await?; 783 + 784 + let pending_from_state = |state: &str| state == "pending"; 800 785 801 786 let queue: Vec<QueuedTrack> = queue_rows 802 787 .into_iter() ··· 806 791 url: r.url, 807 792 duration: r.duration, 808 793 thumbnail: r.thumbnail, 809 - pending: r.pending != 0, 794 + pending: pending_from_state(&r.state), 810 795 }) 811 796 .collect(); 812 797 ··· 816 801 url: r.url, 817 802 duration: r.duration, 818 803 thumbnail: r.thumbnail, 819 - pending: r.pending != 0, 804 + pending: pending_from_state(&r.state), 820 805 }); 821 806 822 807 let history: Vec<FinishedTrack> = history_rows ··· 853 838 source: r.source, 854 839 added_by: r.added_by, 855 840 added_at: r.added_at, 856 - pending: r.pending != 0, 857 - stream_url: r.stream_url, 841 + pending: pending_from_state(&r.state), 858 842 }) 859 843 .collect(); 860 844 ··· 1118 1102 "r", 1119 1103 &[Effect::PersistMetadata { 1120 1104 item_id: tid(1), 1105 + url: "https://e.com/t".into(), 1121 1106 title: "Real Title".into(), 1122 1107 duration: "4:20".into(), 1123 1108 thumbnail: Some("https://img.example/t.jpg".into()), ··· 1164 1149 "r", 1165 1150 &[Effect::PersistMetadata { 1166 1151 item_id: tid(1), 1152 + url: "https://e.com/t".into(), 1167 1153 title: "Real Title".into(), 1168 1154 duration: "4:20".into(), 1169 1155 thumbnail: None,
+12
src/types.rs
··· 16 16 Self(uuid::Uuid::now_v7()) 17 17 } 18 18 19 + /// Sentinel value: no specific track. Used in PersistMetadata effects 20 + /// generated by URL-keyed operations where the effect targets media_cache, 21 + /// not a particular track row. 22 + pub(crate) fn nil() -> Self { 23 + Self(uuid::Uuid::nil()) 24 + } 25 + 19 26 pub(crate) fn as_str_buf(&self) -> String { 20 27 self.0.to_string() 21 28 } ··· 103 110 #[derive(Debug, Clone, Serialize)] 104 111 pub(crate) struct TrackState { 105 112 pub(crate) id: TrackId, 113 + /// Stable hash of the media URL, used for file serving. 114 + pub(crate) hash: String, 106 115 pub(crate) title: String, 107 116 pub(crate) url: String, 108 117 pub(crate) duration: String, ··· 114 123 #[derive(Debug, Clone, Serialize)] 115 124 pub(crate) struct QueueSummary { 116 125 pub(crate) id: TrackId, 126 + /// Stable hash of the media URL, used for file serving. 127 + pub(crate) hash: String, 117 128 pub(crate) title: String, 118 129 pub(crate) url: String, 119 130 pub(crate) duration: String, ··· 130 141 #[derive(Debug, Clone, Serialize)] 131 142 pub(crate) struct HistoryEntry { 132 143 pub(crate) id: TrackId, 144 + pub(crate) hash: String, 133 145 pub(crate) title: String, 134 146 pub(crate) url: String, 135 147 pub(crate) duration: String,
+12 -23
src/web.rs
··· 21 21 use crate::room; 22 22 use crate::types::{PlaylistEntryId, RoomId}; 23 23 24 - const MEDIA_EXTENSIONS: &[&str] = &[ 25 - "mp4", "m4a", "mp3", "webm", "mkv", "ogg", "wav", "flac", "aac", "opus", "mov", 26 - ]; 27 - 28 24 /// Per-IP rate limiter for POST /api/ingest (max 1 req/5s per IP). 29 25 #[derive(Clone)] 30 26 pub(crate) struct RateLimiter { ··· 89 85 .route("/api/v1/skip", post(skip_handler)) 90 86 .route("/api/v1/playlist/add", post(add_playlist_entry)) 91 87 .route("/api/v1/playlist/remove", post(remove_playlist_entry)) 92 - .route( 93 - "/api/v1/rooms/{room_id}/media/{track_id}", 94 - get(media_handler), 95 - ) 88 + .route("/api/v1/rooms/{room_id}/media/{hash}", get(media_handler)) 96 89 .route("/api/v1/ws/{room_id}", get(ws_handler)) 97 90 // SPA: serve built frontend from static/dist/ 98 91 .nest_service( ··· 132 125 Ok(Json(serde_json::json!({ "room_id": room.to_string() }))) 133 126 } 134 127 135 - /// Find a media file in a room's cache directory by trying extensions. 136 - /// yt-dlp downloads to `{track_id}.{ext}` but the media handler receives 137 - /// the bare track ID (UUID) without extension. 138 - async fn find_media_file(dir: &std::path::Path, track_id: &str) -> Option<std::path::PathBuf> { 139 - // Try bare path first (was renamed or is an exact match). 140 - let bare = dir.join(track_id); 128 + /// Find a media file in the cache directory by hash (derived from URL). 129 + async fn find_media_file(dir: &std::path::Path, hash: &str) -> Option<std::path::PathBuf> { 130 + // Try exact hash path, with mp4 extension. 131 + let path = dir.join(hash).with_extension("mp4"); 132 + if path.exists() { 133 + return Some(path); 134 + } 135 + // Fall back to bare hash (no extension), for any legacy files. 136 + let bare = dir.join(hash); 141 137 if bare.exists() { 142 138 return Some(bare); 143 - } 144 - // Scan for `{track_id}.{ext}` with common media extensions. 145 - for ext in MEDIA_EXTENSIONS { 146 - let path = dir.join(format!("{track_id}.{ext}")); 147 - if path.exists() { 148 - return Some(path); 149 - } 150 139 } 151 140 None 152 141 } ··· 271 260 272 261 async fn media_handler( 273 262 State(state): State<AppState>, 274 - Path((room_id, track_id)): Path<(String, String)>, 263 + Path((room_id, hash)): Path<(String, String)>, 275 264 headers: axum::http::HeaderMap, 276 265 ) -> Response { 277 266 let room_id = match RoomId::parse(&room_id) { ··· 283 272 return (StatusCode::NOT_FOUND, "room not found").into_response(); 284 273 } 285 274 286 - let file_path = find_media_file(&state.cache_dir, &track_id).await; 275 + let file_path = find_media_file(&state.cache_dir, &hash).await; 287 276 288 277 let Some(file_path) = file_path else { 289 278 return (StatusCode::NOT_FOUND, "media not found").into_response();
+1
static/dist/assets/index-BZS9IGWz.css
··· 1 + :root{color-scheme:light dark;--rosewater: light-dark(#dc8a78, #f4dbd6);--flamingo: light-dark(#dd7878, #f0c6c6);--pink: light-dark(#ea76cb, #f5bde6);--mauve: light-dark(#8839ef, #c6a0f6);--red: light-dark(#d20f39, #ed8796);--maroon: light-dark(#e64553, #ee99a0);--peach: light-dark(#fe640b, #f5a97f);--yellow: light-dark(#df8e1d, #eed49f);--green: light-dark(#40a02b, #a6da95);--teal: light-dark(#179299, #8bd5ca);--sky: light-dark(#04a5e5, #91d7e3);--sapphire: light-dark(#209fb5, #7dc4e4);--blue: light-dark(#1e66f5, #8aadf4);--lavender: light-dark(#7287fd, #b7bdf8);--text: light-dark(#4c4f69, #cad3f5);--subtext1: light-dark(#5c5f77, #b8c0e0);--subtext0: light-dark(#6c6f85, #a5adcb);--overlay2: light-dark(#7c7f93, #939ab7);--overlay1: light-dark(#8c8fa1, #8087a2);--overlay0: light-dark(#9ca0b0, #6e738d);--surface2: light-dark(#acb0be, #5b6078);--surface1: light-dark(#bcc0cc, #494d64);--surface0: light-dark(#ccd0da, #363a4f);--crust: light-dark(#dce0e8, #181926);--mantle: light-dark(#e6e9ef, #1e2030);--base: light-dark(#eff1f5, #24273a);--bg: var(--base);--surface: var(--mantle);--surface-raised: var(--crust);--border: var(--surface0);--text-primary: var(--text);--text-muted: var(--overlay1);--accent: var(--mauve);--accent-hover: var(--lavender);--danger: var(--red);--success: var(--green);--warning: var(--yellow);font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text-primary);line-height:1.5}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}body{min-height:100vh}input,button,select{font:inherit;color:inherit}input{background:var(--surface-raised);border:1px solid var(--border);border-radius:6px;padding:.5rem .75rem;outline:none;transition:border-color .15s,box-shadow .15s}input:focus{border-color:var(--accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 25%,transparent)}button{background:var(--accent);color:var(--crust);border:none;border-radius:6px;padding:.5rem 1rem;cursor:pointer;transition:background .15s,transform .1s;font-weight:600;font-size:.875rem}button:hover:not(:disabled){background:var(--accent-hover)}button:active:not(:disabled){transform:scale(.97)}button:disabled{opacity:.35;cursor:not-allowed}button.secondary{background:var(--surface0);color:var(--text-primary)}button.secondary:hover:not(:disabled){background:var(--surface1)}.home{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:1rem;padding:2rem}.home h1{font-size:3rem;font-weight:700;letter-spacing:-.02em;color:var(--accent)}.home p{color:var(--text-muted);margin-bottom:.5rem}.home-form,.home-join{display:flex;gap:.5rem;flex-wrap:wrap;justify-content:center}.toast{padding:.5rem 1rem;font-size:.85rem;cursor:pointer;user-select:none}.toast-error{background:var(--danger);color:var(--crust);font-weight:600}.toast-action{background:#00000026;border:1px solid rgba(255,255,255,.2);color:inherit;font-size:.75rem;padding:.15rem .5rem;border-radius:3px;cursor:pointer;margin-left:.5rem}.room{display:flex;flex-direction:column;height:100vh}.room-header{display:flex;align-items:center;gap:1rem;padding:.6rem 1rem;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}.room-header h2{font-size:1rem;font-weight:600}.client-count{color:var(--text-muted);font-size:.8rem;margin-left:auto}.disconnected{color:var(--danger);font-size:.8rem;margin-left:.5rem}.theme-toggle-sm{background:none;border:none;color:var(--text-muted);font-size:.65rem;cursor:pointer;padding:.15rem .35rem;border-radius:3px;font-weight:500}.theme-toggle-sm:hover{color:var(--text-primary);background:var(--surface0)}.room-layout{display:flex;flex:1;overflow:hidden}.room-main{flex:1;display:flex;flex-direction:column;padding:.75rem;gap:.75rem;min-width:0;overflow-y:auto}.room-sidebar{width:340px;display:flex;flex-direction:column;border-left:1px solid var(--border);overflow:hidden;flex-shrink:0}.room-actions{display:flex;gap:.5rem;flex-wrap:wrap}.room-actions button{font-size:.8rem;padding:.4rem .75rem}.inline-form{display:flex;gap:.4rem}.inline-form input{flex:1;font-size:.85rem}.player{background:var(--surface);border-radius:8px;padding:.75rem;display:flex;flex-direction:column;gap:.5rem}.player-info{display:flex;align-items:center;gap:.75rem}.player-thumb{width:48px;height:48px;border-radius:6px;object-fit:cover;flex-shrink:0;background:var(--surface0)}.player-meta{display:flex;flex-direction:column;gap:.1rem;min-width:0;flex:1}.player-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600;font-size:.95rem}.player-duration-row{display:flex;gap:.75rem;font-size:.8rem;color:var(--text-muted)}.player-media video,.player-media audio{width:100%;max-height:60vh;border-radius:6px;background:light-dark(#111,#000)}.player-media-wrap{position:relative}.live-btn{position:absolute;top:.75rem;right:.75rem;font-size:.75rem;font-weight:700;padding:.25rem .65rem;border-radius:4px;letter-spacing:.04em;text-transform:uppercase;background:#00000080;color:#fff;border:1px solid rgba(255,255,255,.15);z-index:10;cursor:pointer;transition:background .15s,color .15s,border-color .15s}.live-btn:hover{background:#000000b3}.live-btn--on{background:var(--accent);color:var(--base);border-color:var(--accent)}.live-btn--on:hover{background:color-mix(in srgb,var(--accent) 85%,var(--base))}.player-idle{color:var(--text-muted);padding:2rem;text-align:center;font-style:italic}.chat{flex:1;display:flex;flex-direction:column;min-height:0}.chat-messages{flex:1;overflow-y:auto;padding:.5rem;display:flex;flex-direction:column;gap:.2rem}.chat-message{padding:.2rem .5rem;border-radius:4px;transition:background .1s}.chat-message:hover{background:var(--surface-raised)}.chat-user{font-weight:600;font-size:.8rem;color:var(--accent)}.chat-time{font-size:.65rem;color:var(--text-muted);margin-left:.4rem;opacity:.7}.chat-content{font-size:.88rem;margin-top:.05rem;word-break:break-word}.chat-input{display:flex;gap:.25rem;padding:.5rem;border-top:1px solid var(--border)}.chat-input input{flex:1;font-size:.85rem}.queue-panel{overflow-y:auto;flex-shrink:0;max-height:50%}.queue-section{padding:.6rem .75rem;border-bottom:1px solid var(--border)}.queue-section h3{font-size:.72rem;text-transform:uppercase;color:var(--text-muted);margin-bottom:.4rem;letter-spacing:.06em;font-weight:700}.queue-item{display:flex;align-items:center;gap:.4rem;padding:.2rem 0;font-size:.82rem;transition:background .1s;border-radius:4px}.queue-item:hover{background:var(--surface-raised)}.queue-item.current{color:var(--accent);font-weight:600}.queue-item.history{opacity:.65}.queue-thumb{width:28px;height:20px;border-radius:3px;object-fit:cover;flex-shrink:0;background:var(--surface0)}.queue-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.queue-duration{color:var(--text-muted);font-size:.75rem;margin-left:.3rem;flex-shrink:0}.queue-empty{color:var(--text-muted);font-size:.78rem;font-style:italic}.room-footer{padding:.3rem .75rem;border-top:1px solid var(--border);display:flex;justify-content:flex-end;flex-shrink:0}.add-target-toggle{display:flex;gap:0}.add-target-toggle button{font-size:.72rem;padding:.25rem .5rem;background:var(--surface0);color:var(--text-muted);border:1px solid var(--border);font-weight:500;border-radius:0}.add-target-toggle button:first-child{border-radius:4px 0 0 4px}.add-target-toggle button:last-child{border-radius:0 4px 4px 0}.add-target-toggle button.active{background:var(--accent);color:var(--crust);border-color:var(--accent)}.chat-message.system .chat-user,.chat-message.system .chat-time{display:none}.chat-message.system .chat-content{font-size:.78rem;color:var(--text-muted);font-style:italic}@media(max-width:768px){.room-header{padding:.4rem .75rem}.room-main{padding:.5rem}.room-sidebar{width:100%;border-left:none;border-top:1px solid var(--border);max-height:40vh}.room-layout{flex-direction:column-reverse}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{transition-duration:0s!important;animation-duration:0s!important}}
-1
static/dist/assets/index-B_qULDRc.css
··· 1 - :root{color-scheme:light dark;--rosewater: light-dark(#dc8a78, #f4dbd6);--flamingo: light-dark(#dd7878, #f0c6c6);--pink: light-dark(#ea76cb, #f5bde6);--mauve: light-dark(#8839ef, #c6a0f6);--red: light-dark(#d20f39, #ed8796);--maroon: light-dark(#e64553, #ee99a0);--peach: light-dark(#fe640b, #f5a97f);--yellow: light-dark(#df8e1d, #eed49f);--green: light-dark(#40a02b, #a6da95);--teal: light-dark(#179299, #8bd5ca);--sky: light-dark(#04a5e5, #91d7e3);--sapphire: light-dark(#209fb5, #7dc4e4);--blue: light-dark(#1e66f5, #8aadf4);--lavender: light-dark(#7287fd, #b7bdf8);--text: light-dark(#4c4f69, #cad3f5);--subtext1: light-dark(#5c5f77, #b8c0e0);--subtext0: light-dark(#6c6f85, #a5adcb);--overlay2: light-dark(#7c7f93, #939ab7);--overlay1: light-dark(#8c8fa1, #8087a2);--overlay0: light-dark(#9ca0b0, #6e738d);--surface2: light-dark(#acb0be, #5b6078);--surface1: light-dark(#bcc0cc, #494d64);--surface0: light-dark(#ccd0da, #363a4f);--crust: light-dark(#dce0e8, #181926);--mantle: light-dark(#e6e9ef, #1e2030);--base: light-dark(#eff1f5, #24273a);--bg: var(--base);--surface: var(--mantle);--surface-raised: var(--crust);--border: var(--surface0);--text-primary: var(--text);--text-muted: var(--overlay1);--accent: var(--peach);--accent-hover: var(--rosewater);--danger: var(--red);--success: var(--green);--warning: var(--yellow);font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text-primary);line-height:1.5}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}body{min-height:100vh}input,button,select{font:inherit;color:inherit}input{background:var(--surface-raised);border:1px solid var(--border);border-radius:6px;padding:.5rem .75rem;outline:none;transition:border-color .15s,box-shadow .15s}input:focus{border-color:var(--accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 25%,transparent)}button{background:var(--accent);color:var(--crust);border:none;border-radius:6px;padding:.5rem 1rem;cursor:pointer;transition:background .15s,transform .1s;font-weight:600;font-size:.875rem}button:hover:not(:disabled){background:var(--accent-hover)}button:active:not(:disabled){transform:scale(.97)}button:disabled{opacity:.35;cursor:not-allowed}button.secondary{background:var(--surface0);color:var(--text-primary)}button.secondary:hover:not(:disabled){background:var(--surface1)}.home{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;gap:1rem;padding:2rem}.home h1{font-size:3rem;font-weight:700;letter-spacing:-.02em;color:var(--accent)}.home p{color:var(--text-muted);margin-bottom:.5rem}.home-form,.home-join{display:flex;gap:.5rem;flex-wrap:wrap;justify-content:center}.toast{padding:.5rem 1rem;font-size:.85rem;cursor:pointer;user-select:none}.toast-error{background:var(--danger);color:var(--crust);font-weight:600}.room{display:flex;flex-direction:column;height:100vh}.room-header{display:flex;align-items:center;gap:1rem;padding:.6rem 1rem;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}.room-header h2{font-size:1rem;font-weight:600}.client-count{color:var(--text-muted);font-size:.8rem;margin-left:auto}.disconnected{color:var(--danger);font-size:.8rem;margin-left:.5rem}.theme-toggle-sm{background:none;border:none;color:var(--text-muted);font-size:.65rem;cursor:pointer;padding:.15rem .35rem;border-radius:3px;font-weight:500}.theme-toggle-sm:hover{color:var(--text-primary);background:var(--surface0)}.room-layout{display:flex;flex:1;overflow:hidden}.room-main{flex:1;display:flex;flex-direction:column;padding:.75rem;gap:.75rem;min-width:0;overflow-y:auto}.room-sidebar{width:340px;display:flex;flex-direction:column;border-left:1px solid var(--border);overflow:hidden;flex-shrink:0}.room-actions{display:flex;gap:.5rem;flex-wrap:wrap}.room-actions button{font-size:.8rem;padding:.4rem .75rem}.inline-form{display:flex;gap:.4rem}.inline-form input{flex:1;font-size:.85rem}.player{background:var(--surface);border-radius:8px;padding:.75rem;display:flex;flex-direction:column;gap:.5rem}.player-info{display:flex;align-items:center;gap:.75rem}.player-thumb{width:48px;height:48px;border-radius:6px;object-fit:cover;flex-shrink:0;background:var(--surface0)}.player-meta{display:flex;flex-direction:column;gap:.1rem;min-width:0;flex:1}.player-title{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600;font-size:.95rem}.player-duration-row{display:flex;gap:.75rem;font-size:.8rem;color:var(--text-muted)}.player-media video,.player-media audio{width:100%;max-height:60vh;border-radius:6px;background:light-dark(#111,#000)}.player-media-wrap{position:relative}.live-btn{position:absolute;top:.75rem;right:.75rem;font-size:.75rem;font-weight:700;padding:.25rem .65rem;border-radius:4px;letter-spacing:.04em;text-transform:uppercase;background:#00000080;color:#fff;border:1px solid rgba(255,255,255,.15);z-index:10;cursor:pointer;transition:background .15s,color .15s,border-color .15s}.live-btn:hover{background:#000000b3}.live-btn--on{background:var(--green);color:var(--crust);border-color:var(--green)}.live-btn--on:hover{background:color-mix(in srgb,var(--green) 85%,#000)}.player-idle{color:var(--text-muted);padding:2rem;text-align:center;font-style:italic}.chat{flex:1;display:flex;flex-direction:column;min-height:0}.chat-messages{flex:1;overflow-y:auto;padding:.5rem;display:flex;flex-direction:column;gap:.2rem}.chat-message{padding:.2rem .5rem;border-radius:4px;transition:background .1s}.chat-message:hover{background:var(--surface-raised)}.chat-user{font-weight:600;font-size:.8rem;color:var(--accent)}.chat-time{font-size:.65rem;color:var(--text-muted);margin-left:.4rem;opacity:.7}.chat-content{font-size:.88rem;margin-top:.05rem;word-break:break-word}.chat-input{display:flex;gap:.25rem;padding:.5rem;border-top:1px solid var(--border)}.chat-input input{flex:1;font-size:.85rem}.queue-panel{overflow-y:auto;flex-shrink:0;max-height:50%}.queue-section{padding:.6rem .75rem;border-bottom:1px solid var(--border)}.queue-section h3{font-size:.72rem;text-transform:uppercase;color:var(--text-muted);margin-bottom:.4rem;letter-spacing:.06em;font-weight:700}.queue-item{display:flex;align-items:center;gap:.4rem;padding:.2rem 0;font-size:.82rem;transition:background .1s;border-radius:4px}.queue-item:hover{background:var(--surface-raised)}.queue-item.current{color:var(--accent);font-weight:600}.queue-item.history{opacity:.65}.queue-thumb{width:28px;height:20px;border-radius:3px;object-fit:cover;flex-shrink:0;background:var(--surface0)}.queue-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.queue-duration{color:var(--text-muted);font-size:.75rem;margin-left:.3rem;flex-shrink:0}.queue-empty{color:var(--text-muted);font-size:.78rem;font-style:italic}.room-footer{padding:.3rem .75rem;border-top:1px solid var(--border);display:flex;justify-content:flex-end;flex-shrink:0}.add-target-toggle{display:flex;gap:0}.add-target-toggle button{font-size:.72rem;padding:.25rem .5rem;background:var(--surface0);color:var(--text-muted);border:1px solid var(--border);font-weight:500;border-radius:0}.add-target-toggle button:first-child{border-radius:4px 0 0 4px}.add-target-toggle button:last-child{border-radius:0 4px 4px 0}.add-target-toggle button.active{background:var(--accent);color:var(--crust);border-color:var(--accent)}.chat-message.system .chat-user,.chat-message.system .chat-time{display:none}.chat-message.system .chat-content{font-size:.78rem;color:var(--text-muted);font-style:italic}@media(max-width:768px){.room-header{padding:.4rem .75rem}.room-main{padding:.5rem}.room-sidebar{width:100%;border-left:none;border-top:1px solid var(--border);max-height:40vh}.room-layout{flex-direction:column-reverse}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{transition-duration:0s!important;animation-duration:0s!important}}
-1
static/dist/assets/index-Sw8RlqPJ.js
··· 1 - (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const s of o.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();class U extends Error{source;constructor(t){super(),this.source=t}}class Kt extends Error{source;constructor(t,n){super(n instanceof Error?n.message:String(n),{cause:n}),this.source=t}}class An extends Error{constructor(){super("")}}class dr extends Error{constructor(){super("")}}const Rn=0,Ve=1,ke=2,$t=4,ce=8,He=16,H=32,he=64,be=128,Yt=256,dt=512,Ze=1024,vt=1,hr=2,_t=4,gr=8,Pn=16,Ne=32,Qt=64,A=1,K=2,B=4,Re=1,J=2,Se=3,E={},mr=typeof Proxy=="function",Xt={},pr=Symbol("refresh");function kn(e,t){const n=(e.i?.t?e.i.u?.o:e.i?.o)??-1;n>=e.o&&(e.o=n+1);const r=e.o,i=t.l[r];if(i===void 0)t.l[r]=e;else{const o=i.S;o.T=e,e.S=o,i.S=e}r>t._&&(t._=r)}function Xe(e,t){let n=e.O;n&(ce|$t|Ze)||(n&Ve?e.O=n&-4|ke|ce:e.O=n|ce,n&He||kn(e,t))}function In(e,t){let n=e.O;n&(ce|$t|He|Ze)||(e.O=n|He,kn(e,t))}function qe(e,t){const n=e.O;if(!(n&(ce|He)))return;e.O=n&-25;const r=e.o;if(e.S===e)t.l[r]=void 0;else{const i=e.T,o=t.l[r],s=i??o;e===o?t.l[r]=i:e.S.T=i,s.S=e.S}e.S=e,e.T=void 0}function yr(e){if(!e.R){e.R=!0;for(let t=0;t<=e._;t++)for(let n=e.l[t];n!==void 0;n=n.T)n.O&ce&&ht(n)}}function ht(e,t=ke){const n=e.O;if(!((n&(Ve|ke))>=t)){e.O=n&-4|t;for(let r=e.I;r!==null;r=r.p)ht(r.h,Ve);if(e.N!==null)for(let r=e.N;r!==null;r=r.A)for(let i=r.I;i!==null;i=i.p)ht(i.h,Ve)}}function Fe(e,t){for(e.R=!1,e.C=0;e.C<=e._;e.C++){let n=e.l[e.C];for(;n!==void 0;)n.O&ce?t(n):wr(n,e),n=e.l[e.C]}e._=0}function wr(e,t){qe(e,t);let n=e.o;for(let r=e.P;r;r=r.D){const i=r.m,o=i.V||i;o.L&&o.o>=n&&(n=o.o+1)}if(e.o!==n){e.o=n;for(let r=e.I;r!==null;r=r.p)In(r.h,t)}}const gt=new WeakMap,re=new Set;function br(e){let t=gt.get(e);if(t)return W(t);const n=e.U,r=n?.G?W(n.G):null;return t={k:e,F:new Set,W:[[],[]],H:null,M:b,j:r},gt.set(e,t),re.add(t),e.$=!1,t}function W(e){for(;e.H;)e=e.H;return e}function xn(e,t){if(e=W(e),t=W(t),e===t)return e;t.H=e;for(const n of t.F)e.F.add(n);return e.W[0].push(...t.W[0]),e.W[1].push(...t.W[1]),e}function Pe(e){const t=e.G;if(!t)return;const n=W(t);if(re.has(n))return n;e.G=void 0}function je(e){return Pe(e)?.M??e.M}function St(e){return e.K!==void 0&&e.K!==E}function en(e,t){const n=W(t),r=e.G;if(r){if(r.H){e.G=t;return}const i=W(r);if(re.has(i)){i!==n&&!St(e)&&(n.j&&W(n.j)===i?e.G=t:i.j&&W(i.j)===n||xn(n,i));return}}e.G=t}const Ue=new Set,I={l:new Array(2e3).fill(void 0),R:!1,C:0,_:0},Z={l:new Array(2e3).fill(void 0),R:!1,C:0,_:0};let M=0,b=null,Ye=!1,Wt=0,ct=null;const Ke=new Set;function $r(e){return Ue.size===0&&re.size===0&&e.Y.length===0&&e.Z.length===0&&e.q.size===0&&Ke.size===0}function vr(){if(Ke.size!==0)for(const e of Ke){if(e.I!==null){Ke.delete(e);continue}e.B===E&&(e.K!==void 0&&e.K!==E||(Ke.delete(e),e.X?.()))}}function _r(e){return!!ct?.has(e)}function it(e){for(const t of re){if(t.H||t.F.size>0)continue;const n=t.W[e-1];n.length&&(t.W[e-1]=[],pt(n,e))}}function Sr(e){for(let t=e.I;t!==null;t=t.p){const n=t.h;if(!n.J)continue;if(n.J===Se){n.ee||(n.ee=!0,n.te.enqueue(J,n.ne));continue}const r=n.O&H?Z:I;r.C>n.o&&(r.C=n.o),Xe(n,r)}}function Er(e,t){t.ie=e,e.re.push(...t.re);for(const n of re)n.M===t&&(n.M=e);e.Z.push(...t.Z);for(const n of t.q)e.q.add(n);for(const[n,r]of t.oe){let i=e.oe.get(n);i||e.oe.set(n,i=new Set);for(const o of r)i.add(o)}for(const n of t.se)e.se.add(n)}function Cr(e){for(let t=0;t<e.length;t++){const n=e[t];n.G=void 0,n.B!==E&&(n.ue=n.B,n.B=E);const r=n.K;n.K=E,r!==E&&n.ue!==r&&Ie(n,!0),n.M=null}e.length=0}function Or(e){for(const t of re)(e?t.M===e:!t.M)&&(t.H||(t.W[0].length&&pt(t.W[0],Re),t.W[1].length&&pt(t.W[1],J)),t.k.G===t&&(t.k.G=void 0),t.F.clear(),t.W[0].length=0,t.W[1].length=0,re.delete(t),gt.delete(t.k))}function ve(){Ye||(Ye=!0,!Wt&&!O.ce&&queueMicrotask(xe))}let Tr=class{i=null;le=[[],[]];Y=[];created=M;addChild(t){this.Y.push(t),t.i=this}removeChild(t){const n=this.Y.indexOf(t);n>=0&&(this.Y.splice(n,1),t.i=null)}notify(t,n,r,i){return this.i?this.i.notify(t,n,r,i):!1}run(t){if(this.le[t-1].length){const n=this.le[t-1];this.le[t-1]=[],pt(n,t)}for(let n=0;n<this.Y.length;n++)this.Y[n].run?.(t)}enqueue(t,n){t&&(V?W(V).W[t-1].push(n):this.le[t-1].push(n)),ve()}stashQueues(t){t.le[0].push(...this.le[0]),t.le[1].push(...this.le[1]),this.le=[[],[]];for(let n=0;n<this.Y.length;n++){let r=this.Y[n],i=t.Y[n];i||(i={le:[[],[]],Y:[]},t.Y[n]=i),r.stashQueues(i)}}restoreQueues(t){this.le[0].push(...t.le[0]),this.le[1].push(...t.le[1]);for(let n=0;n<t.Y.length;n++){const r=t.Y[n];let i=this.Y[n];i&&i.restoreQueues(r)}}};class ee extends Tr{ce=!1;ae=null;fe=[];Z=[];q=new Set;static Ee;static Se;static Te;static de=null;flush(){if(!this.ce){this.ce=!0;try{if(Fe(I,ee.Ee),b){if(!Rr(b)){const r=b;if(Fe(Z,ee.Ee),this.ae=null,this.fe=[],this.Z=[],this.q=new Set,it(Re),it(J),this.stashQueues(r._e),M++,Ye=I._>=I.C,gn(r.fe),b=null,!r.re.length&&!r.oe.size&&r.Z.length){ct=new Set;for(let i=0;i<r.Z.length;i++){const o=r.Z[i];o.L||o.Oe&vt||(ct.add(o),Sr(o))}}try{Nt(null,!0)}finally{ct=null}return}this.fe!==b.fe&&this.fe.push(...b.fe),this.restoreQueues(b._e),Ue.delete(b);const n=b;b=null,gn(this.fe),Nt(n)}else $r(this)?(mt(),I._>=I.C&&(Fe(I,ee.Ee),mt())):(Ue.size&&Fe(Z,ee.Ee),Nt());M++,Ye=I._>=I.C,re.size&&it(Re),this.run(Re),re.size&&it(J),this.run(J)}finally{this.ce=!1}}}notify(t,n,r,i){if(n&A){if(r&A){const o=i!==void 0?i:t.Re;if(b&&o){const s=o.source;let u=b.oe.get(s);u||b.oe.set(s,u=new Set);const l=u.size;u.add(t),u.size!==l&&ve()}}return!0}return!1}initTransition(t){if(t&&(t=Nn(t)),!(t&&t===b)&&!(!t&&b&&b.Ie===M)){if(!b)b=t??{Ie:M,fe:[],oe:new Map,Z:[],q:new Set,re:[],_e:{le:[[],[]],Y:[]},ie:!1,se:new Set};else if(t){const n=b;Er(t,n),Ue.delete(n),b=t}if(Ue.add(b),b.Ie=M,this.ae!==null&&(this.ae.M=b,b.fe.push(this.ae),this.ae=null),this.fe!==b.fe){for(let n=0;n<this.fe.length;n++){const r=this.fe[n];r.M=b,b.fe.push(r)}this.fe=b.fe}if(this.Z!==b.Z){for(let n=0;n<this.Z.length;n++){const r=this.Z[n];r.M=b,b.Z.push(r)}this.Z=b.Z}for(const n of re)n.M||(n.M=b);if(this.q!==b.q){for(const n of this.q)b.q.add(n);this.q=b.q}}}}function Et(e){if(b){O.fe.push(e);return}if(O.ae===null&&O.fe.length===0){O.ae=e;return}O.ae!==null&&(O.fe.push(O.ae),O.ae=null),O.fe.push(e)}function Ie(e,t=!1){const n=e.G||V,r=e.pe!==void 0;for(let i=e.I;i!==null;i=i.p){if(r&&i.h.Oe&gr){i.h.O|=Yt;continue}t&&n?(i.h.O|=be,en(i.h,n)):t&&(i.h.O|=be,i.h.G=void 0);const o=i.h;if(o.J===Se){o.ee||(o.ee=!0,o.te.enqueue(J,o.ne));continue}const s=i.h.O&H?Z:I;s.C>i.h.o&&(s.C=i.h.o),Xe(i.h,s)}}function hn(e){const t=e;if(!t.L){e.B!==E&&(e.ue=e.B,e.B=E);return}e.B!==E&&(e.ue=e.B,e.B=E,e.J&&e.J!==Se&&(e.ee=!0)),t.O&=~Ze,t.he&A||(t.he&=~B),(t.Ne!==null||t.Ae!==null)&&ee.Se(t,!1,!0)}function mt(){O.ae!==null&&(hn(O.ae),O.ae=null);const e=O.fe;for(let t=0;t<e.length;t++)hn(e[t]);e.length=0}function Nt(e=null,t=!1){const n=!t;n&&mt(),!t&&O.Y.length&&Ln(O);const r=I._>=I.C;if(r&&Fe(I,ee.Ee),n){if(r&&mt(),Cr(e?e.Z:O.Z),e&&e.se.size){for(const i of e.se){if(i.O&he)continue;if(i.J===Se){i.ee||(i.ee=!0,i.te.enqueue(J,i.ne));continue}const o=i.O&H?Z:I;o.C>i.o&&(o.C=i.o),Xe(i,o)}e.se.clear()}e?e.q:O.q,vr(),Or(e)}}function Ln(e){for(const t of e.Y)t.checkSources?.(),Ln(t)}function gn(e){for(let t=0;t<e.length;t++)e[t].M=b}const O=new ee;function xe(e){if(e){Wt++;try{return e()}finally{xe(),Wt--}}if(!O.ce)for(;Ye||b;)O.flush()}function pt(e,t){for(let n=0;n<e.length;n++)e[n](t)}function Ar(e,t){if(e.O&(H|he))return!1;if(e.Ce===t||e.Pe?.has(t))return!0;for(let n=e.P;n;n=n.D){let r=n.m;for(;r;){if(r===t||r.V===t)return!0;r=r.U}}return!!(e.he&A&&e.Re instanceof U&&e.Re.source===t)}function Rr(e){if(e.ie)return!0;if(e.re.length)return!1;let t=!0;for(const[n,r]of e.oe){let i=!1;for(const o of r){if(Ar(o,n)){i=!0;break}r.delete(o)}if(!i)e.oe.delete(n);else if(n.he&A&&n.Re?.source===n){t=!1;break}}if(t)for(let n=0;n<e.Z.length;n++){const r=e.Z[n];if(St(r)&&"he"in r&&r.he&A&&r.Re instanceof U&&r.Re.source!==r){t=!1;break}}return t&&(e.ie=!0),t}function Nn(e){for(;e.ie&&typeof e.ie=="object";)e=e.ie;return e}function Pr(e,t){const n=b;try{return b=Nn(e),t()}finally{b=n}}function qn(e){let t=e.ge;for(;t;)t.O|=H,t.O&ce&&(qe(t,I),Xe(t,Z)),qn(t),t=t.De}function et(e,t=!1,n){const r=e.O;if(r&he)return;t&&(e.O=r|he),t&&e.L&&(e.ve=null);let i=n?e.Ne:e.ge;for(;i;){const o=i.De;if(i.P){const s=i;qe(s,s.O&H?Z:I);let u=s.P;do u=rn(u);while(u!==null);s.P=null,s.ye=null}et(i,!0),i=o}if(n?e.Ne=null:(e.ge=null,e.me=0),t&&!n&&!(r&H)&&e.i!==null&&!(e.i.O&he)){const o=e.we,s=e.De;o!==null?o.De=s:e.i.ge=s,s!==null&&(s.we=o),e.we=null}kr(e,n)}function kr(e,t){let n=t?e.Ae:e.be;if(n){if(Array.isArray(n))for(let r=0;r<n.length;r++){const i=n[r];i.call(i)}else n.call(n);t?e.Ae=null:e.be=null}}function Ir(e,t){let n=e;for(;n.Oe&_t&&n.i;)n=n.i;if(n.id!=null)return xr(n.id,n.me++);throw new Error("Cannot get child id from owner without an id")}function tn(e){return Ir(e)}function xr(e,t){const n=t.toString(36),r=n.length-1;return e+(r?String.fromCharCode(64+r):"")+n}function tt(){return _}function Ct(e){return _&&(_.be?Array.isArray(_.be)?_.be.push(e):_.be=[_.be,e]:_.be=e),e}function Lr(e=!0){et(this,e)}function Ge(e){const t=_,n=e?.transparent??!1,r={id:e?.id??(n?t?.id:t?.id!=null?tn(t):void 0),Oe:n?_t:0,t:!0,u:t?.t?t.u:t,ge:null,De:null,we:null,be:null,te:t?.te??O,Ve:t?.Ve||Xt,me:0,Ae:null,Ne:null,i:t,dispose:Lr};if(t){const i=t.ge;i===null||(r.De=i,i.we=r),t.ge=r}return r}function nn(e,t){const n=Ge(t);return ue(n,()=>e(()=>n.dispose()))}function rn(e){const t=e.m,n=e.D,r=e.p,i=e.Le;if(r!==null?r.Le=i:t.Ue=i,i!==null)i.p=r;else if(t.I=r,r===null){t.X?.();const o=t;o.L&&o.Oe&Ne&&!(o.O&H)&&Mn(o)}return n}function Dn(e){const t=e.ye;let n=t!==null?t.D:e.P;if(n!==null){do n=rn(n);while(n!==null);t!==null?t.D=null:e.P=null}}function Mn(e){qe(e,e.O&H?Z:I);let t=e.P;for(;t!==null;)t=rn(t);e.P=null,e.ye=null,et(e,!0)}function Be(e,t){const n=t.ye;if(n!==null&&n.m===e)return;let r=null;const i=t.O&$t;if(i&&(r=n!==null?n.D:t.P,r!==null&&r.m===e)){t.ye=r;return}const o=e.Ue;if(o!==null&&o.h===t&&(!i||Nr(o,t)))return;const s=t.ye=e.Ue={m:e,h:t,D:r,Le:o,p:null};n!==null?n.D=s:t.P=s,o!==null?o.p=s:e.I=s}function Nr(e,t){const n=t.ye;if(n!==null){let r=t.P;do{if(r===e)return!0;if(r===n)break;r=r.D}while(r!==null)}return!1}function qr(e,t){return e.Ce===t||e.Pe?.has(t)?!1:e.Ce?(e.Pe?e.Pe.add(t):e.Pe=new Set([e.Ce,t]),e.Ce=void 0,!0):(e.Ce=t,!0)}function Dr(e,t){return e.Ce?e.Ce!==t?!1:(e.Ce=void 0,!0):e.Pe?.delete(t)?(e.Pe.size===1?(e.Ce=e.Pe.values().next().value,e.Pe=void 0):e.Pe.size===0&&(e.Pe=void 0),!0):!1}function Bn(e){e.Ce=void 0,e.Pe?.clear(),e.Pe=void 0}function yt(e,t,n){if(!t){e.Re=null;return}if(n instanceof U&&n.source===t){e.Re=n;return}const r=e.Re;(!(r instanceof U)||r.source!==t)&&(e.Re=new U(t))}function zt(e,t){for(let n=e.I;n!==null;n=n.p)t(n.h);for(let n=e.N;n!==null;n=n.A)for(let r=n.I;r!==null;r=r.p)t(r.h)}function Mr(e){let t=!1;const n=new Set,r=i=>{if(n.has(i)||!Dr(i,e))return;n.add(i),i.Ie=M;const o=i.Ce??i.Pe?.values().next().value;if(o)yt(i,o),_e(i);else{if(i.he&=~A,yt(i),_e(i),i.Ge){if(i.J===Se){const s=i;s.ee||(s.ee=!0,s.te.enqueue(J,s.ne))}else{const s=i.O&H?Z:I;s.C>i.o&&(s.C=i.o),Xe(i,s)}t=!0}i.Ge=!1}zt(i,r)};zt(e,r),t&&ve()}function Br(e,t,n){let r=!1,i=!1;if(typeof t=="object"&&t!==null&&te(()=>{r=t[Symbol.asyncIterator],i=!r&&typeof t.then=="function"}),!i&&!r)return e.ve=null,t;e.ve=t;let o;const s=l=>{e.ve===t&&(O.initTransition(je(e)),on(e,l instanceof U?A:K,l),e.Ie=M)},u=(l,f)=>{if(e.ve!==t||e.O&(ke|be))return;O.initTransition(je(e));const h=!!(e.he&B);Dn(e),Fn(e);const d=Pe(e);if(d&&d.F.delete(e),e.K!==void 0)e.K!==void 0&&e.K!==E?e.B=l:(e.ue=l,Ie(e)),e.Ie=M;else if(d){const g=e.J,m=e.ue,w=e.ke;(!g&&h||!w||!w(l,m))&&(e.ue=l,e.Ie=M,e.Fe&&fe(e.Fe,l),Ie(e,!0))}else fe(e,()=>l);Mr(e),ve(),xe(),f?.()};if(i){let l=!1,f=!0;if(t.then(h=>{f?(o=h,l=!0):u(h)},h=>{f||s(h)}),f=!1,!l)throw O.initTransition(je(e)),new U(_)}if(r){const l=t[Symbol.asyncIterator]();let f=!1,h=!1;Ct(()=>{if(!h){h=!0;try{const m=l.return?.();m&&typeof m.then=="function"&&m.then(void 0,()=>{})}catch{}}});const d=()=>{let m,w=!1,a=!0;return l.next().then(c=>{if(a)m=c,w=!0,c.done&&(h=!0);else{if(e.ve!==t)return;c.done?(h=!0,ve(),xe()):u(c.value,d)}},c=>{!a&&e.ve===t&&(h=!0,s(c))}),a=!1,w&&!m.done?(o=m.value,f=!0,d()):w&&m.done},g=d();if(!f&&!g)throw O.initTransition(je(e)),new U(_)}return o}function Fn(e,t=!1){(e.Ce||e.Pe)&&Bn(e),e.Ge&&(e.Ge=!1),e.he=t?0:e.he&B,e.Re&&yt(e),e.We&&_e(e),e.xe&&e.xe()}function on(e,t,n,r,i){t===K&&!(n instanceof Kt)&&!(n instanceof U)&&(n=new Kt(e,n));const o=t===A&&n instanceof U?n.source:void 0,s=o===e,u=t===A&&e.K!==void 0&&!s,l=u&&St(e);r||(t===A&&o?(qr(e,o),e.he=A|e.he&B,yt(e,o,n)):(Bn(e),e.he=t|(t!==K?e.he&B:0),e.Re=n),_e(e)),i&&!r&&en(e,i);const f=r||l,h=r||u?void 0:i;if(e.xe){if(r&&t===A)return;f?e.xe(t,n):e.xe();return}zt(e,d=>{d.Ie=M,(t===A&&o&&d.Ce!==o&&!d.Pe?.has(o)||t!==A&&(d.Re!==n||d.Ce||d.Pe))&&(!f&&!d.M&&Et(d),on(d,t,n,f,h))})}let Fr=null;ee.Ee=ae;ee.Se=et;let j=!1,oe=!1,_=null,V=null;function ae(e,t=!1){const n=e.J;t||(e.M&&(!n||b)&&b!==e.M&&O.initTransition(e.M),qe(e,e.O&H?Z:I),e.ve=null,e.M||n===Se?et(e):(e.ge!==null||e.be!==null)&&(qn(e),e.Ae=e.be,e.Ne=e.ge,e.be=null,e.ge=null,e.me=0));let r=!!(e.O&be);const i=e.K!==void 0&&e.K!==E,o=!!(e.he&A),s=!!(e.he&B),u=_;_=e,e.ye=null,e.O=$t,e.Ie=M;let l=e.B===E?e.ue:e.B,f=e.o,h=j,d=V;if(j=!0,r){const a=Pe(e);a&&(V=a)}else if(b&&!t&&b.Z.length)for(let a=e.P;a;a=a.D){const c=a.m;if(c.O&be){const p=Pe(c);if(p){r=!0,V=p,e.O|=be,en(e,p);break}}}const g=n&&n!==J,m=oe;g&&(oe=!0);try{if(e.Oe&Qt)l=e.L(l),e.ve=null;else{const a=e.ve,c=e.L(l),p=typeof c=="object"&&c!==null,y=e.ve!==a;l=y||!p?c:Br(e,c),!y&&!p&&(e.ve=null)}if(Fn(e,t),e.G){const a=Pe(e);a&&(a.F.delete(e),_e(a.k))}}catch(a){if(a instanceof U&&V){const c=W(V);c.k!==e&&(c.F.add(e),e.G=c,_e(c.k))}a instanceof U&&(e.Ge=!0),on(e,a instanceof U?A:K,a,void 0,a instanceof U?e.G:void 0)}finally{j=h,g&&(oe=m),e.O=Rn|(t?e.O&Yt:0),_=u}if(!e.Re){Dn(e);const a=i?e.K:e.B===E?e.ue:e.B,c=!n&&s||!e.ke||!e.ke(a,l);if(n&&c&&(e.ee=!e.Re,t||e.te.enqueue(n,ee.Te.bind(null,e))),c){const p=i?e.K:void 0;t||n&&b!==e.M||r?(e.ue=l,i&&r&&(e.K=l,e.B=l)):e.B=l,i&&!r&&o&&!e.$&&(e.K=l),(!i||r||e.K!==p)&&Ie(e,r||i)}else if(i)e.B=l;else if(e.o!=f)for(let p=e.I;p!==null;p=p.p)In(p.h,p.h.O&H?Z:I)}V=d,(e.B!==E||e.Ne!==null||e.Ae!==null||!!(e.he&(A|B)))&&(!t||e.he&A)&&!e.M&&!(b&&i)&&Et(e),e.M&&n&&b!==e.M&&Pr(e.M,()=>ae(e))}function jn(e){if(e.O&Ve)for(let t=e.P;t;t=t.D){const n=t.m,r=n.V||n;if(r.L&&jn(r),e.O&ke)break}(e.O&(ke|be)||e.Re&&e.Ie<M&&!e.ve)&&ae(e),e.O=e.O&(Yt|ce|He)}function Ot(e,t){const n=t?.transparent??!1,r={id:t?.id??(n?_?.id:_?.id!=null?tn(_):void 0),Oe:(n?_t:0)|(t?.ownedWrite?vt:0)|(!_||t?.lazy?Ne:0)|(t?.sync?Qt:0)|0,ke:t?.equals!=null?t.equals:Kn,X:t?.unobserved,be:null,te:_?.te??O,Ve:_?.Ve??Xt,me:0,L:e,ue:void 0,o:0,N:null,T:void 0,S:null,P:null,ye:null,I:null,Ue:null,i:_,De:null,we:null,ge:null,O:t?.lazy?dt:Rn,he:B,Ie:M,B:E,Ae:null,Ne:null,ve:null,M:null};return Un(r,t),r}function jr(e,t,n,r,i,o){const s=o?.transparent??!1,u={id:o?.id??(s?_?.id:_?.id!=null?tn(_):void 0),Oe:(s?_t:0)|(o?.ownedWrite?vt:0)|(o?.sync?Qt:0)|0,ke:!1,X:o?.unobserved,be:null,te:_?.te??O,Ve:_?.Ve??Xt,me:0,L:e,ue:void 0,o:0,N:null,T:void 0,S:null,P:null,ye:null,I:null,Ue:null,i:_,De:null,we:null,ge:null,O:dt,he:B,Ie:M,B:E,Ae:null,Ne:null,ve:null,M:null,ee:!1,Me:void 0,Qe:t,je:n,$e:void 0,Ke:!1,J:r,xe:i};return Un(u,Ur),u}const Ur={lazy:!0};function Un(e,t){e.S=e;const n=_?.t?_.u:_;if(_){const r=_.ge;r===null||(e.De=r,r.we=e),_.ge=e}n&&(e.o=n.o+1),!t?.lazy&&ae(e,!0)}function We(e,t,n=null){const r={ke:t?.equals!=null?t.equals:Kn,Oe:(t?.ownedWrite?vt:0)|(t?.Ye?hr:0),X:t?.unobserved,ue:e,I:null,Ue:null,Ie:M,V:n,A:n?.N||null,B:E};return n&&(n.N=r),r}function Kn(e,t){return e===t}function te(e,t){if(!j)return e();const n=j;j=!1;try{return e()}finally{j=n}}function Wn(e){let t=_;t?.t&&(t=t.u);const n=e;if(typeof n.L=="function"){const o=e;o.O&dt?(o.O&=~dt,ae(o,!0)):o.O&he&&ae(o,!0)}const r=e.V||e;if(!n.L&&r===e&&e.K===void 0&&e.pe===void 0&&b===null&&V===null)return t&&j&&Be(e,t),!t||e.B===E?e.ue:e.B;if(t&&j&&(Be(e,t),r.L)){const o=e.O&H;r.o>=(o?Z.C:I.C)&&(ht(t),yr(o?Z:I),jn(r));const s=r.o;s>=t.o&&e.i!==t&&(t.o=s+1)}if(r.he&A)if(t&&!(oe&&r.M&&b!==r.M))if(V){const o=r.G,s=W(V);if(o&&W(o)===s&&!St(r))throw!j&&e!==t&&Be(e,t),r.Re}else throw!j&&e!==t&&Be(e,t),r.Re;else{if(t&&r!==e&&r.he&B)throw!j&&e!==t&&Be(e,t),r.Re;if(!t&&r.he&B)throw r.Re}if(e.L&&e.he&K){if(e.Ie<M)return ae(e),Wn(e);throw e.Re}if(e.K!==void 0&&e.K!==E)return t&&oe&&_r(e)?e.ue:e.K;if(b!==null&&V!==null&&e.B!==E&&r===e&&!e.L&&t)return b.se.add(t),e.ue;const i=!t||V!==null&&(e.K!==void 0||e.G||r===e&&oe||r.he&A)||e.B===E||oe&&e.M&&b!==e.M?e.ue:e.B;return!t&&r===e&&typeof n.L=="function"&&e.Oe&Ne&&!(r.he&A)&&!e.I&&Mn(e),i}function fe(e,t){e.M&&b!==e.M&&O.initTransition(e.M);const n=e.K!==void 0&&!0,r=e.K!==void 0&&e.K!==E,i=n?r?e.K:e.ue:e.B===E?e.ue:e.B;if(typeof t=="function"&&(t=t(i)),!(!e.ke||!e.ke(i,t)||!!(e.he&B)))return n&&r&&e.L&&(Ie(e,!0),ve()),t;if(n){const s=e.K===E;s||O.initTransition(je(e)),s&&(e.B=e.ue,O.Z.push(e)),e.$=!0;const u=br(e);e.G=u,e.K=t}else e.B===E&&Et(e),e.B=t;return e.We&&_e(e),e.Fe&&fe(e.Fe,t),e.Ie=M,Ie(e,n),ve(),t}function Kr(e){qe(e,e.O&H?Z:I),!(e.O&Ze)&&e.B===E&&Et(e),e.O=e.O&-4|Ze}function Wr(e,t){const n=fe(e,t);return Kr(e),n}function ue(e,t){const n=_,r=j;_=e,j=!1;try{return t()}finally{_=n,j=r}}function zr(e){const t=e,n=e.V;if(e.U){const r=e.U;return r.he&A&&!(r.he&B)?!0:e.B!==E&&!(t.he&B)}if(n&&e.B!==E)return!n.ve&&!(n.he&A);if(e.K!==void 0&&e.K!==E){if(t.he&A&&!(t.he&B))return!0;if(e.U){const r=e.G?W(e.G):null;return!!(r&&r.F.size>0)}return!0}return e.K!==void 0&&e.K===E&&!e.U?!1:e.B!==E&&!(t.he&B)?!0:!!(t.he&A&&!(t.he&B))}function _e(e){if(e.We){const t=zr(e),n=e.We;if(fe(n,t),!t&&n.G){const r=Pe(e);if(r&&r.F.size>0){const i=W(n.G);i!==r&&xn(r,i)}gt.delete(n),n.G=void 0}}}function Vr(e,t=!0){const n=oe;oe=t;try{return e()}finally{oe=n}}function Gr(e,t=tt()){if(!t)throw new An;const n=Hr(e,t)?t.Ve[e.id]:e.defaultValue;if(sn(n))throw new dr;return n}function Jr(e,t,n=tt()){if(!n)throw new An;n.Ve={...n.Ve,[e.id]:sn(t)?e.defaultValue:t}}function Hr(e,t){return!sn(t?.Ve[e.id])}function sn(e){return typeof e>"u"}function zn(e,t,n,r){const i=!!r?.user,o=jr(e,t,n,i?J:Re,Zr,r);ae(o,!0),!r?.defer&&(o.J===J||r?.schedule?o.te.enqueue(o.J,Vt.bind(null,o)):Vt(o))}function Zr(e,t){const n=e!==void 0?e:this.he,r=t!==void 0?t:this.Re;if(n&K){let i=r;if(this.te.notify(this,A,0),this.J===J)try{return this.je?this.je(i,()=>{this.$e?.(),this.$e=void 0}):console.error(i)}catch(o){i=o}if(!this.te.notify(this,K,K))throw i}else this.J===Re&&this.te.notify(this,A|K,n,r)}function Vt(e){if(!(!e.ee||e.O&he)){e.$e?.(),e.$e=void 0;try{const t=e.Qe(e.ue,e.Me);e.$e=t,e.$e&&!e.Ke&&(e.Ke=!0,ue(e.i,()=>Ct(()=>e.$e?.())))}catch(t){if(e.Re=new Kt(e,t),e.he|=K,!e.te.notify(e,K,K))throw t}finally{e.Me=e.ue,e.ee=!1}}}ee.Te=Vt;function Yr(e,t){const n=()=>{if(!(!r.ee||r.O&he))try{r.ee=!1,ae(r)}finally{}},r=Ot(()=>{r.$e?.(),r.$e=void 0;const i=Vr(e);r.$e=i},{...t,lazy:!0});r.$e=void 0,r.Oe=r.Oe&~Ne|Pn,r.ee=!0,r.J=Se,r.xe=(i,o)=>{if((i!==void 0?i:r.he)&K){r.te.notify(r,A,0);const u=o!==void 0?o:r.Re;if(!r.te.notify(r,K,K))throw u}},r.ne=n,r.te.enqueue(J,n),Ct(()=>r.$e?.())}function Vn(e){return Ct(e)}function de(e){const t=Wn.bind(null,e);return t[pr]=e,t}function Qr(e,t){if(typeof e=="function"){const r=Ot(e,t);return r.Oe&=~Ne,[de(r),Wr.bind(null,r)]}const n=We(e,t);return[de(n),fe.bind(null,n)]}function $e(e,t){return de(Ot(e,t))}function Xr(e,t,n){zn(e,t.effect||t,t.error,{user:!0,...n})}function ei(e,t,n){zn(e,t,void 0,n)}function ti(e,t){Yr(e,t)}function ni(e){const t=tt();t&&!(t.Oe&Pn)?ti(()=>te(e),void 0):O.enqueue(J,()=>{e()?.()})}const ri=Symbol(0),Gt=Symbol(0);function mn(e){return e==null||typeof e!="object"||Object.isFrozen(e)?!1:typeof Node>"u"||!(e instanceof Node)}const Gn=Symbol(0);function pn(e){return e==="__proto__"||e==="constructor"||e==="prototype"}function ze(e,t,n=0){let r,i=e;if(n<t.length-1){r=t[n];const s=typeof r,u=Array.isArray(e);if(s==="string"&&pn(r))return;if(Array.isArray(r)){for(let l=0;l<r.length;l++)t[n]=r[l],ze(e,t,n);t[n]=r;return}else if(u&&s==="function"){for(let l=0;l<e.length;l++)r(e[l],l)&&(t[n]=l,ze(e,t,n));t[n]=r;return}else if(u&&s==="object"){const{from:l=0,to:f=e.length-1,by:h=1}=r;for(let d=l;d<=f;d+=h)t[n]=d,ze(e,t,n);t[n]=r;return}else if(n<t.length-2){ze(e[r],t,n+1);return}i=e[r]}let o=t[t.length-1];if(!(typeof o=="function"&&(o=o(i),o===i))&&!(r===void 0&&o==null))if(o===Gn)delete e[r];else if(r===void 0||mn(i)&&mn(o)&&!Array.isArray(o)){const s=r!==void 0?e[r]:e,u=Object.keys(o);for(let l=0;l<u.length;l++){const f=u[l];if(pn(f))continue;const h=Object.getOwnPropertyDescriptor(o,f);h.get||h.set?Object.defineProperty(s,f,h):s[f]=h.value}}else e[r]=o}Object.assign(function(...t){return n=>{ze(n,t)}},{DELETE:Gn});function ot(){return!0}const ii={get(e,t,n){return t===Gt?n:e.get(t)},has(e,t){return t===Gt?!0:e.has(t)},set:ot,deleteProperty:ot,getOwnPropertyDescriptor(e,t){return{configurable:!0,enumerable:!0,get(){return e.get(t)},set:ot,deleteProperty:ot}},ownKeys(e){return e.keys()}};function qt(e){return(e=typeof e=="function"?e():e)?e:{}}const Dt=Symbol(0);function oi(...e){if(e.length===1&&typeof e[0]!="function")return e[0];let t=!1;const n=[];for(let l=0;l<e.length;l++){const f=e[l];t=t||!!f&&Gt in f;const h=!!f&&f[Dt];if(h)for(let d=0;d<h.length;d++)n.push(h[d]);else n.push(typeof f=="function"?(t=!0,$e(f)):f)}if(mr&&t)return new Proxy({get(l){if(l===Dt)return n;for(let f=n.length-1;f>=0;f--){const h=qt(n[f]);if(l in h)return h[l]}},has(l){for(let f=n.length-1;f>=0;f--)if(l in qt(n[f]))return!0;return!1},keys(){const l=new Set;for(let f=0;f<n.length;f++){const h=Object.keys(qt(n[f]));for(let d=0;d<h.length;d++)l.add(h[d])}return[...l]}},ii);const r=Object.create(null);let i=!1,o=n.length-1;for(let l=o;l>=0;l--){const f=n[l];if(!f){l===o&&o--;continue}const h=Object.getOwnPropertyNames(f);for(let d=h.length-1;d>=0;d--){const g=h[d];if(!(g==="__proto__"||g==="constructor")&&!r[g]){i=i||l!==o;const m=Object.getOwnPropertyDescriptor(f,g);r[g]=m.get?{enumerable:!0,configurable:!0,get:m.get.bind(f)}:m}}}if(!i)return n[o];const s={},u=Object.keys(r);for(let l=u.length-1;l>=0;l--){const f=u[l],h=r[f];h.get?Object.defineProperty(s,f,h):s[f]=h.value}return s[Dt]=n,s}function si(e,t,n){const r=typeof n?.keyed=="function"?n.keyed:void 0,i=t.length>1,o=t,s={Ze:Ge(),qe:0,Be:e,ze:[],Xe:o,Je:[],et:[],tt:r,nt:r||n?.keyed===!1?[]:void 0,it:i&&n?.keyed!==!1?[]:void 0,rt:n?.keyed===!1,ot:n?.fallback},u=Ot(li.bind(s));return s.Ze.u=u,u.Oe&=~Ne,de(u)}const st={ownedWrite:!0};function li(){const e=this.Be()||[],t=e.length;return e[ri],ue(this.Ze,()=>{let n,r,i=this.nt?this.rt?()=>(this.nt[r]=We(e[r],st),this.Xe(de(this.nt[r]),r)):()=>(this.nt[r]=We(e[r],st),this.it&&(this.it[r]=We(r,st)),this.Xe(de(this.nt[r]),this.it?de(this.it[r]):void 0)):this.it?()=>{const o=e[r];return this.it[r]=We(r,st),this.Xe(o,de(this.it[r]))}:()=>{const o=e[r];return this.Xe(o)};if(t===0)this.qe!==0&&(this.Ze.dispose(!1),this.et=[],this.ze=[],this.Je=[],this.qe=0,this.nt&&(this.nt=[]),this.it&&(this.it=[])),this.ot&&!this.Je[0]&&(this.Je[0]=ue(this.et[0]=Ge(),this.ot));else if(this.qe===0){for(this.et[0]&&this.et[0].dispose(),this.Je=new Array(t),r=0;r<t;r++)this.ze[r]=e[r],this.Je[r]=ue(this.et[r]=Ge(),i);this.qe=t}else{let o,s,u,l,f,h,d,g=new Array(t),m=new Array(t),w=this.nt?new Array(t):void 0,a=this.it?new Array(t):void 0;for(o=0,s=Math.min(this.qe,t);o<s&&(this.ze[o]===e[o]||this.nt&&yn(this.tt,this.ze[o],e[o]));o++)this.nt&&fe(this.nt[o],e[o]);for(s=this.qe-1,u=t-1;s>=o&&u>=o&&(this.ze[s]===e[u]||this.nt&&yn(this.tt,this.ze[s],e[u]));s--,u--)g[u]=this.Je[s],m[u]=this.et[s],w&&(w[u]=this.nt[s]),a&&(a[u]=this.it[s]);for(h=new Map,d=new Array(u+1),r=u;r>=o;r--)l=e[r],f=this.tt?this.tt(l):l,n=h.get(f),d[r]=n===void 0?-1:n,h.set(f,r);for(n=o;n<=s;n++)l=this.ze[n],f=this.tt?this.tt(l):l,r=h.get(f),r!==void 0&&r!==-1?(g[r]=this.Je[n],m[r]=this.et[n],w&&(w[r]=this.nt[n]),a&&(a[r]=this.it[n]),r=d[r],h.set(f,r)):this.et[n].dispose();for(r=o;r<t;r++)r in g?(this.Je[r]=g[r],this.et[r]=m[r],w&&(this.nt[r]=w[r],fe(this.nt[r],e[r])),a&&(this.it[r]=a[r],fe(this.it[r],r))):this.Je[r]=ue(this.et[r]=Ge(),i);this.Je=this.Je.slice(0,this.qe=t),this.ze=e.slice(0)}}),this.Je}function yn(e,t,n){return e?e(t)===e(n):!0}function ln(e,t){if(typeof e=="function"&&!e.length){if(t?.doNotUnwrap)return e;do e=e();while(typeof e=="function"&&!e.length)}if(!(t?.skipNonRendered&&(e==null||e===!0||e===!1||e===""))){if(Array.isArray(e)){let n=[];return Jt(e,n,t)?()=>{let r=[];return Jt(n,r,{...t,doNotUnwrap:!1}),r}:n}return e}}function Jt(e,t=[],n){let r=null,i=!1;for(let o=0;o<e.length;o++)try{let s=e[o];if(typeof s=="function"&&!s.length){if(n?.doNotUnwrap){t.push(s),i=!0;continue}do s=s();while(typeof s=="function"&&!s.length)}Array.isArray(s)?i=Jt(s,t,n):n?.skipNonRendered&&(s==null||s===!0||s===!1||s==="")||t.push(s)}catch(s){if(!(s instanceof U))throw s;r=s}if(r)throw r;return i}function Jn(e,t){const n=Symbol("");function r(i){return nn(()=>(Jr(r,i.value),un(()=>i.children)))}return r.id=n,r.defaultValue=e,r}function Hn(e){return Gr(e)}function un(e){const t=$e(e,{lazy:!0}),n=$e(()=>ln(t()),{lazy:!0,sync:!0});return n.toArray=()=>{const r=n();return Array.isArray(r)?r:r!=null?[r]:[]},n}class Ae{static{for(const t of["all","allSettled","any","race","reject","resolve"])Ae[t]=()=>new Ae}catch(){return new Ae}then(){return new Ae}finally(){return new Ae}}const Q=(...e)=>$e(...e),N=(...e)=>Qr(...e),ui=(...e)=>ei(...e),Qe=(...e)=>Xr(...e);function q(e,t){return te(()=>e(t||{}))}const ci=e=>`Stale read from <${e}>.`;function at(e){const t="fallback"in e?{keyed:e.keyed,fallback:()=>e.fallback}:{keyed:e.keyed};return si(()=>e.each,e.children,t)}function Zn(e){const t=e.keyed,n=$e(()=>e.when,void 0),r=t?n:$e(n,{equals:(i,o)=>!i==!o,sync:!0});return $e(()=>{const i=r();if(i){const o=e.children;return typeof o=="function"&&o.length>0?te(t?()=>o(i):()=>o(()=>{if(!te(r))throw ci("Show");return n()})):o}return e.fallback},{sync:!0})}const F=Symbol("slot"),ai={transparent:!0,sync:!0},fi={sync:!0},G=(e,t,n)=>ui(e,t,n?{transparent:!0,sync:!0,...n}:ai),D=e=>Q(()=>e(),fi);function di(e,t,n,r){let i=n.length,o=t.length,s=i,u=0,l=0,f=t[o-1],h=f[F],d=f.parentNode===e&&(!h||h===r)?f.nextSibling:r||null,g=null,m,w;for(;u<o||l<s;){if(t[u]===n[l]){u++,l++;continue}for(;t[o-1]===n[s-1];)o--,s--;if(o===u){let a;if(s<i)if(l){const c=n[l-1],p=c[F];a=c.parentNode===e&&(!p||p===r)?c.nextSibling:d}else a=n[s-l];else a=d;for(;l<s;){const c=n[l++];e.insertBefore(c,a),r&&(c[F]=r)}}else if(s===l)for(;u<o;){const a=t[u++];if(!g||!g.has(a)){const c=a[F];a.parentNode===e&&(!c||c===r)&&a.remove()}}else if((m=t[u])===n[s-1]&&n[l]===t[o-1]&&m.parentNode===e&&(!(w=m[F])||w===r))if(r)do{const a=t[--o];if(e.insertBefore(a,m),a[F]=r,l++,u>=o-1||l>=s)break}while(t[u]===n[s-1]&&n[l]===t[o-1]);else do if(e.insertBefore(t[--o],m),l++,u>=o-1||l>=s)break;while(t[u]===n[s-1]&&n[l]===t[o-1]);else{if(!g){g=new Map;let c=l;for(;c<s;)g.set(n[c],c++)}const a=g.get(t[u]);if(a!=null)if(l<a&&a<s){let c=u,p=1,y;for(;++c<o&&c<s&&!((y=g.get(t[c]))==null||y!==a+p);)p++;if(p>a-l){const C=t[u],v=C[F],R=C.parentNode===e&&(!v||v===r)?C:d;for(;l<a;){const X=n[l++];e.insertBefore(X,R),r&&(X[F]=r)}}else{const C=t[u++],v=n[l++],R=C[F];C.parentNode===e&&(!R||R===r)?e.replaceChild(v,C):e.insertBefore(v,d),r&&(v[F]=r)}}else u++;else{const c=t[u++],p=c[F];c.parentNode===e&&(!p||p===r)&&c.remove()}}}}const wn="_$DX_EVENT_OWNER",bn={},Ht=new Set,Le=new Map;function hi(e,t,n,r={}){let i;mi(t);try{nn(o=>{if(i=o,t===document){const s=e();G(()=>ln(s),()=>{})}else{const s=e();$(t,()=>s,t.firstChild?null:void 0,n,r.insertOptions)}},{id:r.renderId})}catch(o){throw i&&i(),$n(t),o}return()=>{i(),$n(t),t.textContent=""}}function gi(e,t,n){const r=document.createElement("template");return r.innerHTML=e,n===2?r.content.firstChild.firstChild:r.content.firstChild}function P(e,t){let n;return i=>(n||(n=gi(e,i,t))).cloneNode(!0)}function Tt(e){for(let t=0,n=e.length;t<n;t++){const r=e[t];Ht.has(r)||(Ht.add(r),Le.forEach((i,o)=>Yn(r,o,i)))}}function mi(e){const t=pi(e,e);t&&(t.roots=(t.roots||0)+1)}function $n(e){const t=Le.get(e);t&&(t.roots>1?t.roots--:delete t.roots),yi(e,e)}function pi(e,t=e){if(!e||!t)return;let n=Le.get(e);return n||Le.set(e,n={owners:new Map,handlers:new Map}),n.owners.set(t,(n.owners.get(t)||0)+1),Ht.forEach(r=>Yn(r,e,n)),n}function yi(e,t=e){const n=Le.get(e);if(!n)return;const r=n.owners.get(t);r>1?n.owners.set(t,r-1):n.owners.delete(t),!n.owners.size&&(n.handlers.forEach((i,o)=>e.removeEventListener(o,i)),Le.delete(e))}function Yn(e,t,n){if(n.handlers.has(e))return;const r=i=>$i(i,t,n);n.handlers.set(e,r),t.addEventListener(e,r)}function wi(e,t){let n=e,r=0;for(;n;){if(t.owners.has(n))return{owner:n,distance:r};r++,n=n._$host||n.parentNode||n.host}}function ne(e,t,n){n==null||n===!1?e.removeAttribute(t):e.setAttribute(t,n===!0?"":n)}function wt(e,t,n){if(t==null||t===!1){n&&e.removeAttribute("class");return}if(typeof t=="string"){t!==n&&e.setAttribute("class",t);return}typeof n=="string"?(n={},e.removeAttribute("class")):n=_n(n||{}),t=_n(t);const r=Object.keys(t||{}),i=Object.keys(n);let o,s;for(o=0,s=i.length;o<s;o++){const u=i[o];!u||u==="undefined"||t[u]||e.classList.remove(u)}for(o=0,s=r.length;o<s;o++){const u=r[o],l=!!t[u];!u||u==="undefined"||n[u]===l||!l||e.classList.add(u)}}function vn(e,t,n,r){if(Array.isArray(n)){const i=n[0];e.addEventListener(t,n[0]=o=>i.call(e,n[1],o))}else e.addEventListener(t,n,typeof n!="function"&&n)}function bi(e,t){Array.isArray(e)?e.flat(1/0).forEach(n=>n&&n(t)):e(t)}function bt(e,t){const n=te(e);ue(null,()=>bi(n,t))}function $(e,t,n,r,i){const o=n!==void 0;if(o&&!r&&(r=[]),typeof t!="function"&&(t=Bt(t,r,o,!0),typeof t!="function"))return Mt(e,t,r,n);if(o&&r.length===0){const u=document.createTextNode("");e.insertBefore(u,n),r=[u]}let s=r;G(u=>{const l=Bt(t(),s,o,!0);return typeof l!="function"?l:(G(()=>Bt(l,s,o),f=>{Mt(e,f,s,n),s=f},u!==void 0&&!(i&&i.schedule)?{...i,schedule:!0}:i),bn)},u=>{u!==bn&&(Mt(e,u,s,n),s=u)},i)}function _n(e){if(Array.isArray(e)){const t={};Qn(e,t),e=t}if(e&&typeof e=="object"){const t={},n=Object.keys(e);for(let r=0,i=n.length;r<i;r++){const o=n[r];if(!e[o])continue;const s=o.trim().split(/\s+/);for(let u=0,l=s.length;u<l;u++)s[u]&&(t[s[u]]=!0)}return t}return e}function Qn(e,t){for(let n=0,r=e.length;n<r;n++){const i=e[n];Array.isArray(i)?Qn(i,t):typeof i=="object"&&i!=null?Object.assign(t,i):(i||i===0)&&(t[i]=!0)}}function $i(e,t,n){if(e[wn])return;const r=n&&(n.owners.size===1&&n.owners.has(t)?t:wi(e.target,n)?.owner);if(n&&!r)return;e[wn]=r||!0;let i=e.target;const o=`$$${e.type}`,s=e.target,u=r||t||e.currentTarget,l=d=>Object.defineProperty(e,"target",{configurable:!0,value:d}),f=()=>{const d=i[o];if(d&&!i.disabled){const g=i[`${o}Data`];if(g!==void 0?d.call(i,g,e):d.call(i,e),e.cancelBubble)return}return i.host&&typeof i.host!="string"&&!i.host._$host&&i.contains(e.target)&&l(i.host),!0},h=()=>{for(;f()&&!(i===u||i.parentNode===u);)i=i._$host||i.parentNode||i.host};if(Object.defineProperty(e,"currentTarget",{configurable:!0,get(){return i||u||document}}),e.composedPath){const d=e.composedPath();if(d.length){l(d[0]);for(let g=0;g<d.length&&(i=d[g],!!f());g++){if(i._$host){i=i._$host,h();break}if(i===u||i.parentNode===u)break}}else h()}else h();l(s)}function Mt(e,t,n,r){if(t===n)return;const i=typeof t,o=r!==void 0;if(i==="string"||i==="number"){const s=typeof n;s==="string"||s==="number"?e.firstChild.data=t:e.textContent=t}else if(t===void 0)lt(e,n,r);else if(t.nodeType)Array.isArray(n)?lt(e,n,o?r:null,t):n&&n.nodeType?n.parentNode===e?e.replaceChild(t,n):e.appendChild(t):n&&e.firstChild?e.replaceChild(t,e.firstChild):e.appendChild(t),r&&(t[F]=r);else if(Array.isArray(t)){const s=n&&Array.isArray(n);t.length===0?lt(e,n,r):s?n.length===0?Sn(e,t,r):di(e,n,t,r):(n&&lt(e),Sn(e,t))}}function Bt(e,t,n,r){if(e=ln(e,{skipNonRendered:!0,doNotUnwrap:r}),r&&typeof e=="function")return e;if(n&&!Array.isArray(e)&&(e=[e??""]),Array.isArray(e))for(let i=0,o=e.length;i<o;i++){const s=e[i],u=t&&t[i],l=typeof s;(l==="string"||l==="number")&&(e[i]=u&&u.nodeType===3&&u.data===""+s?u:document.createTextNode(s))}return e}function Sn(e,t,n=null){for(let r=0,i=t.length;r<i;r++){const o=t[r];e.insertBefore(o,n),n&&(o[F]=n)}}function lt(e,t,n,r){if(n===void 0)return e.textContent="";if(t.length){let i=!1;for(let o=t.length-1;o>=0;o--){const s=t[o];if(r!==s){const u=s[F],l=s.parentNode===e&&(!u||u===n);r&&!i&&!o?l?e.replaceChild(r,s):e.insertBefore(r,n):l&&s.remove()}else i=!0}}else r&&e.insertBefore(r,n);r&&n&&(r[F]=n)}const vi=!1;function _i(e,t,n,r={}){try{const i=hi(e,t,n,{...r,insertOptions:{schedule:!0}});return xe(),i}finally{}}function Xn(){let e=new Set;function t(i){return e.add(i),()=>e.delete(i)}let n=!1;function r(i,o){if(n)return!(n=!1);const s={to:i,options:o,defaultPrevented:!1,preventDefault:()=>s.defaultPrevented=!0};for(const u of e)u.listener({...s,from:u.location,retry:l=>{l&&(n=!0),u.navigate(i,{...o,resolve:!1})}});return!s.defaultPrevented}return{subscribe:t,confirm:r}}let Zt;function cn(){(!window.history.state||window.history.state._depth==null)&&window.history.replaceState({...window.history.state,_depth:window.history.length-1},""),Zt=window.history.state._depth}cn();function Si(e){return{...e,_depth:window.history.state&&window.history.state._depth}}function Ei(e,t){let n=!1;return()=>{const r=Zt;cn();const i=r==null?null:Zt-r;if(n){n=!1;return}i&&t(i)?(n=!0,window.history.go(-i)):e()}}const Ci=/^(?:[a-z0-9]+:)?\/\//i,Oi=/^\/+|(\/)\/+$/g,er="http://sr";function Je(e,t=!1){const n=e.replace(Oi,"$1");return n?t||/^[?#]/.test(n)?n:"/"+n:""}function ft(e,t,n){if(Ci.test(t))return;const r=Je(e),i=n&&Je(n);let o="";return!i||t.startsWith("/")?o=r:i.toLowerCase().indexOf(r.toLowerCase())!==0?o=r+i:o=i,(o||"/")+Je(t,!o)}function Ti(e,t){if(e==null)throw new Error(t);return e}function Ai(e,t){return Je(e).replace(/\/*(\*.*)?$/g,"")+Je(t)}function tr(e){const t={};return e.searchParams.forEach((n,r)=>{r in t?Array.isArray(t[r])?t[r].push(n):t[r]=[t[r],n]:t[r]=n}),t}function Ri(e,t,n){const[r,i]=e.split("/*",2),o=r.split("/").filter(Boolean),s=o.length;return u=>{const l=u.split("/").filter(Boolean),f=l.length-s;if(f<0||f>0&&i===void 0&&!t)return null;const h={path:s?"":"/",params:{}},d=g=>n===void 0?void 0:n[g];for(let g=0;g<s;g++){const m=o[g],w=m[0]===":",a=w?l[g]:l[g].toLowerCase(),c=w?m.slice(1):m.toLowerCase();if(w&&Ft(a,d(c)))h.params[c]=a;else if(w||!Ft(a,c))return null;h.path+=`/${a}`}if(i){const g=f?l.slice(-f).join("/"):"";if(Ft(g,d(i)))h.params[i]=g;else return null}return h}}function Ft(e,t){const n=r=>r===e;return t===void 0?!0:typeof t=="string"?n(t):typeof t=="function"?t(e):Array.isArray(t)?t.some(n):t instanceof RegExp?t.test(e):!1}function Pi(e){const[t,n]=e.pattern.split("/*",2),r=t.split("/").filter(Boolean);return r.reduce((i,o)=>i+(o.startsWith(":")?2:3),r.length-(n===void 0?0:1))}function nr(e){const t=new Map,n=tt();return new Proxy({},{get(r,i){return t.has(i)||ue(n,()=>t.set(i,Q(()=>e()[i]))),t.get(i)()},getOwnPropertyDescriptor(){return{enumerable:!0,configurable:!0}},ownKeys(){return Reflect.ownKeys(e())},has(r,i){return i in e()}})}function rr(e){let t=/(\/?\:[^\/]+)\?/.exec(e);if(!t)return[e];let n=e.slice(0,t.index),r=e.slice(t.index+t[0].length);const i=[n,n+=t[1]];for(;t=/^(\/\:[^\/]+)\?/.exec(r);)i.push(n+=t[1]),r=r.slice(t[0].length);return rr(r).reduce((o,s)=>[...o,...i.map(u=>u+s)],[])}const ki=100,ir=Jn(),or=Jn();function Ii(e){try{return Hn(e)}catch{return}}const sr=()=>Ti(Hn(ir),"<A> and 'use' router primitives can be only used inside a Route."),lr=()=>sr().navigatorFactory(),xi=()=>sr().params;function Li(e,t=""){const{component:n,preload:r,children:i,info:o}=e,s=!i||Array.isArray(i)&&!i.length,u={key:e,component:n,preload:r,info:o};return ur(e.path).reduce((l,f)=>{for(const h of rr(f)){const d=Ai(t,h);let g=s?d:d.split("/*",1)[0];g=g.split("/").map(m=>m.startsWith(":")||m.startsWith("*")?m:encodeURIComponent(m)).join("/"),l.push({...u,originalPath:f,pattern:g,matcher:Ri(g,!s,e.matchFilters)})}return l},[])}function Ni(e,t=0){return{routes:e,score:Pi(e[e.length-1])*1e4-t,matcher(n){const r=[];for(let i=e.length-1;i>=0;i--){const o=e[i],s=o.matcher(n);if(!s)return null;r.unshift({...s,route:o})}return r}}}function ur(e){return Array.isArray(e)?e:[e]}function cr(e,t="",n=[],r=[]){const i=ur(e);for(let o=0,s=i.length;o<s;o++){const u=i[o];if(u&&typeof u=="object"){u.hasOwnProperty("path")||(u.path="");const l=Li(u,t);for(const f of l){n.push(f);const h=Array.isArray(u.children)&&u.children.length===0;if(u.children&&!h)cr(u.children,f.pattern,n,r);else{const d=Ni([...n],r.length);r.push(d)}n.pop()}}}return n.length?r:r.sort((o,s)=>s.score-o.score)}function jt(e,t){for(let n=0,r=e.length;n<r;n++){const i=e[n].matcher(t);if(i)return i}return[]}function qi(e,t,n){const r=new URL(er),i=Q((h=r)=>{const d=e();try{return new URL(d,r)}catch{return console.error(`Invalid path ${d}`),h}},{equals:(h,d)=>h.href===d.href}),o=Q(()=>i().pathname),s=Q(()=>i().search),u=Q(()=>i().hash),l=()=>"",f=Q(()=>tr(i()));return{get pathname(){return o()},get search(){return s()},get hash(){return u()},get state(){return t()},get key(){return l()},query:n?n(f):nr(f)}}let we;function Di(){return we}function Mi(e,t,n,r={}){const{signal:[i,o],utils:s={}}=e,u=s.parsePath||(k=>k),l=s.renderPath||(k=>k),f=s.beforeLeave||Xn(),h=ft("",r.base||""),d=te(i);if(h===void 0)throw new Error(`${h} is not a valid base path`);h&&!d.value&&o({value:h,replace:!0,scroll:!1});const[g,m]=N(!1,{ownedWrite:!0}),[w,a]=N(void 0,{ownedWrite:!0});let c;const p=Q(()=>w()??i()),y=qi(()=>p().value,()=>p().state,s.queryWrapper),C=[],v=N([],{ownedWrite:!0}),R=Q(()=>typeof r.transformUrl=="function"?jt(t(),r.transformUrl(y.pathname)):jt(t(),y.pathname)),X=()=>{const k=R(),x={};for(let z=0;z<k.length;z++)Object.assign(x,k[z].params);return x},Ee=s.paramsWrapper?s.paramsWrapper(X,t):nr(X),se={pattern:h,path:()=>h,outlet:()=>null,resolvePath(k){return ft(h,k)}};return{base:se,location:y,params:Ee,isRouting:g,renderPath:l,parsePath:u,navigatorFactory:De,matches:R,beforeLeave:f,preloadRoute:Ce,singleFlight:r.singleFlight===void 0?!0:r.singleFlight,submissions:v};function nt(k,x,z){te(()=>{if(typeof x=="number"){x&&(s.go?s.go(x):console.warn("Router integration does not support relative routing"));return}const ge=!x||x[0]==="?",{replace:me,resolve:ie,scroll:Oe,state:pe}={replace:!1,resolve:!ge,scroll:!0,...z},ye=ie?k.resolvePath(x):ft(ge&&y.pathname||"",x);if(ye===void 0)throw new Error(`Path '${x}' is not a routable path`);if(C.length>=ki)throw new Error("Too many redirects");const le=p();if((ye!==le.value||pe!==le.state)&&!vi){if(f.confirm(ye,z)){C.push({value:le.value,replace:me,scroll:Oe,state:le.state});const Te={value:ye,state:pe};c===void 0&&(m(!0),xe()),we="navigate",c=Te,c===Te&&(a({...c}),queueMicrotask(()=>{c===Te&&(we=void 0,rt(c),a(void 0),m(!1),c=void 0)}))}}})}function De(k){return k=k||Ii(or)||se,(x,z)=>nt(k,x,z)}function rt(k){const x=C[0];x&&(o({...k,replace:x.replace,scroll:x.scroll}),C.length=0)}function Ce(k,x){const z=jt(t(),k.pathname),ge=we;we="preload";for(let me in z){const{route:ie,params:Oe}=z[me];ie.component&&ie.component.preload&&ie.component.preload();const{preload:pe}=ie;x&&pe&&ue(n(),()=>pe({params:Oe,location:{pathname:k.pathname,search:k.search,hash:k.hash,query:tr(k),state:null,key:""},intent:"preload"}))}we=ge}}function Bi(e,t,n,r){const{base:i,location:o,params:s}=e,{pattern:u,component:l,preload:f}=r().route,h=Q(()=>r().path);l&&l.preload&&l.preload();const d=f?f({params:s,location:o,intent:we||"initial"}):void 0;return{parent:t,pattern:u,path:h,outlet:()=>l?q(l,{params:s,location:o,data:d,get children(){return n()}}):n(),resolvePath(m){return ft(i.path(),m,h())}}}const Fi=e=>function(n){const{base:r,singleFlight:i,transformUrl:o,root:s,rootPreload:u,routeChildren:l}=te(()=>({base:n.base,singleFlight:n.singleFlight,transformUrl:n.transformUrl,root:n.root,rootPreload:n.rootPreload,routeChildren:n.children})),f=un(()=>l),h=Q(()=>cr(f(),r||""));let d;const g=Mi(e,h,()=>d,{base:r,singleFlight:i,transformUrl:o});return e.create&&e.create(g),q(ir,{value:g,get children(){return q(ji,{routerState:g,root:s,preload:u,get children(){return[D(()=>(d=tt())&&null),q(Ui,{routerState:g,get branches(){return h()}})]}})}})};function ji(e){const t=e.routerState.location,n=e.routerState.params,r=Q(()=>e.preload&&te(()=>{e.preload({params:n,location:t,intent:Di()||"initial"})})),i=e.root;return i?q(i,{params:n,location:t,get data(){return r()},get children(){return e.children}}):e.children}function Ui(e){const t=[];let n,r;const i=Q(s=>{const u=e.routerState.matches(),l=r;let f=l&&u.length===l.length;const h=[];for(let d=0,g=u.length;d<g;d++){const m=l&&l[d],w=u[d];s&&m&&w.route.key===m.route.key?h[d]=s[d]:(f=!1,t[d]&&t[d](),nn(a=>{t[d]=a,h[d]=Bi(e.routerState,h[d-1]||e.routerState.base,En(()=>i()?.[d+1]),()=>{const c=e.routerState.matches();return c[d]??c[0]})}))}return t.splice(u.length).forEach(d=>d()),s&&f?(r=u,s):(n=h[0],r=u,h)}),o=En(()=>i()&&n);return D(o)}const En=e=>()=>{const t=e();if(t)return q(or,{value:t,get children(){return t.outlet()}})},Cn=e=>{const t=un(()=>e.children);return oi(e,{get children(){return t()}})};function Ki(e){let t=!1;const n=s=>typeof s=="string"?{value:s}:s,[r,i]=N(n(e.get()),{equals:(s,u)=>s.value===u.value&&s.state===u.state,ownedWrite:!0}),o=[r,s=>{!t&&e.set(s),i(s)}];return e.init&&Vn(e.init((s=e.get())=>{t=!0,o[1](n(s)),t=!1})),Fi({signal:o,create:e.create,utils:e.utils})}function Wi(e,t,n){return e.addEventListener(t,n),()=>e.removeEventListener(t,n)}function zi(e,t){const n=e&&document.getElementById(e);n?n.scrollIntoView():t&&window.scrollTo(0,0)}const Vi=new Map;function Gi({preload:e=!0,explicitLinks:t=!1,actionBase:n="/_server",transformUrl:r}={}){return i=>{const o=i.base.path(),s=i.navigatorFactory(i.base);let u,l;function f(a){return a.namespaceURI==="http://www.w3.org/2000/svg"}function h(a){if(a.defaultPrevented||a.button!==0||a.metaKey||a.altKey||a.ctrlKey||a.shiftKey)return;const c=a.composedPath().find(X=>X instanceof Node&&X.nodeName.toUpperCase()==="A");if(!c||t&&!c.hasAttribute("link"))return;const p=f(c),y=p?c.href.baseVal:c.href;if((p?c.target.baseVal:c.target)||!y&&!c.hasAttribute("state"))return;const v=(c.getAttribute("rel")||"").split(/\s+/);if(c.hasAttribute("download")||v&&v.includes("external"))return;const R=p?new URL(y,document.baseURI):new URL(y);if(!(R.origin!==window.location.origin||o&&R.pathname&&!R.pathname.toLowerCase().startsWith(o.toLowerCase())))return[c,R]}function d(a){const c=h(a);if(!c)return;const[p,y]=c,C=i.parsePath(y.pathname+y.search+y.hash),v=p.getAttribute("state");a.preventDefault(),s(C,{resolve:!1,replace:p.hasAttribute("replace"),scroll:!p.hasAttribute("noscroll"),state:v?JSON.parse(v):void 0})}function g(a){const c=h(a);if(!c)return;const[p,y]=c;r&&(y.pathname=r(y.pathname)),i.preloadRoute(y,p.getAttribute("preload")!=="false")}function m(a){clearTimeout(u);const c=h(a);if(!c)return l=null;const[p,y]=c;l!==p&&(r&&(y.pathname=r(y.pathname)),u=setTimeout(()=>{i.preloadRoute(y,p.getAttribute("preload")!=="false"),l=p},20))}function w(a){if(a.defaultPrevented)return;let c=a.submitter&&a.submitter.hasAttribute("formaction")?a.submitter.getAttribute("formaction"):a.target.getAttribute("action");if(!c)return;if(!c.startsWith("https://action/")){const y=new URL(c,er);if(c=i.parsePath(y.pathname+y.search),!c.startsWith(n))return}if(a.target.method.toUpperCase()!=="POST")throw new Error("Only POST forms are supported for Actions");const p=Vi.get(c);if(p){a.preventDefault();const y=new FormData(a.target,a.submitter);p.call({r:i,f:a.target},a.target.enctype==="multipart/form-data"?y:new URLSearchParams(y))}}Tt(["click","submit"]),document.addEventListener("click",d),e&&(document.addEventListener("mousemove",m,{passive:!0}),document.addEventListener("focusin",g,{passive:!0}),document.addEventListener("touchstart",g,{passive:!0})),document.addEventListener("submit",w),Vn(()=>{document.removeEventListener("click",d),e&&(document.removeEventListener("mousemove",m),document.removeEventListener("focusin",g),document.removeEventListener("touchstart",g)),document.removeEventListener("submit",w)})}}function Ji(e){const t=()=>{const r=window.location.pathname.replace(/^\/+/,"/")+window.location.search,i=window.history.state&&window.history.state._depth&&Object.keys(window.history.state).length===1?void 0:window.history.state;return{value:r+window.location.hash,state:i}},n=Xn();return Ki({get:t,set({value:r,replace:i,scroll:o,state:s}){i?window.history.replaceState(Si(s),"",r):window.history.pushState(s,"",r),zi(decodeURIComponent(window.location.hash.slice(1)),o),cn()},init:r=>Wi(window,"popstate",Ei(r,i=>{if(i)return!n.confirm(i);{const o=t();return!n.confirm(o.value,{state:o.state})}})),create:Gi({preload:e.preload,explicitLinks:e.explicitLinks,actionBase:e.actionBase,transformUrl:e.transformUrl}),utils:{go:r=>window.history.go(r),beforeLeave:n}})(e)}var Hi=P("<div class=player>"),Zi=P("<div class=player-idle>Paste a URL to start watching together"),Yi=P("<div class=player-info><div class=player-meta><strong class=player-title></strong><span class=player-duration-row><span class=duration-label></span><span class=elapsed-label>"),Qi=P("<div class=player-media-wrap><div class=player-media></div><button type=button>"),Xi=P("<img class=player-thumb alt>"),eo=P("<video controls><track kind=captions>"),to=P("<audio controls><track kind=captions>");function no(e){const t=e.url.split(".").pop()?.toLowerCase();return t?["mp3","m4a","ogg","wav","flac","aac"].includes(t)?!1:(["mp4","webm","mov","mkv","avi","m4v"].includes(t),!0):!0}function Ut(e,t){const n=(Date.now()-t)/1e3;e.currentTime=n}function ro(e){let t;const[n,r]=N(!0),i=e.roomId,o=h=>{t=h??void 0};let s;Qe(()=>{const h=e.track;return s=h,h?.started_at?h.id:null},(h,d)=>{if(h===d)return;const g=s,m=t;if(!m||!g?.started_at){m&&(m.pause(),m.src=""),r(!0);return}const w=g.started_at,a=`/api/v1/rooms/${i}/media/${h}`;m.src=a;let c;const p=()=>{Ut(m,w);const C=()=>{r(!0),m.play().catch(()=>{}),c=()=>{const v=s?.started_at;if(!v)return;const R=(Date.now()-v)/1e3;r(Math.abs(m.currentTime-R)<.5)},m.addEventListener("timeupdate",c),m.addEventListener("pause",y)};m.addEventListener("seeked",C,{once:!0})};m.addEventListener("loadedmetadata",p,{once:!0}),m.addEventListener("canplay",p,{once:!0});const y=()=>{m.ended||Ut(m,w)};return()=>{m.removeEventListener("loadedmetadata",p),m.removeEventListener("canplay",p),c&&m.removeEventListener("timeupdate",c),m.removeEventListener("pause",y)}});const u=h=>{if(h<1)return"--:--";const d=Math.floor(h/1e3),g=Math.floor(d/60),m=d%60;return`${g}:${m.toString().padStart(2,"0")}`},l=()=>{const h=t,d=e.track?.started_at;h&&d&&(Ut(h,d),r(!0))};var f=Hi();return $(f,q(Zn,{get when(){return e.track},get fallback(){return Zi()},children:h=>{const d=h();return[(()=>{var g=Yi(),m=g.firstChild,w=m.firstChild,a=w.nextSibling,c=a.firstChild,p=c.nextSibling;return $(g,(()=>{var y=D(()=>!!d.thumbnail);return()=>y()&&(()=>{var C=Xi();return C.addEventListener("error",v=>v.currentTarget.hidden=!0),G(()=>d.thumbnail,v=>{ne(C,"src",v)}),C})()})(),m),$(w,()=>d.title),$(c,()=>d.duration),$(p,()=>u(Date.now()-d.started_at)),g})(),(()=>{var g=Qi(),m=g.firstChild,w=m.nextSibling;return $(m,(()=>{var a=D(()=>!!no(d));return()=>a()?(()=>{var c=eo();return vn(c,"ended",e.onEnded),bt(()=>o,c),c})():(()=>{var c=to();return vn(c,"ended",e.onEnded),bt(()=>o,c),c})()})()),w.$$click=l,$(w,()=>n()?"Live":"Go live"),G(()=>["live-btn",n()&&"live-btn--on"],(a,c)=>{wt(w,a,c)}),g})()]}})),f}Tt(["click"]);var io=P('<div class=chat><div class=chat-messages></div><div class=chat-input><input type=text placeholder="Type a message..."><button type=button>Send'),oo=P("<span class=chat-user>"),so=P("<span class=chat-time>"),lo=P("<div><div class=chat-content>");function uo(e){let t,n;Qe(()=>e.messages.length,()=>{n&&n.lastElementChild&&n.lastElementChild.scrollIntoView({behavior:"instant"})});const[r,i]=N(""),o=()=>{const a=r().trim();a&&(e.onSend(a),i(""),t?.focus())},s=a=>{a.key==="Enter"&&!a.shiftKey&&(a.preventDefault(),o())},u=a=>{try{const c=new Date(a),p=Math.floor((Date.now()-c.getTime())/1e3);if(p<60)return"just now";const y=Math.floor(p/60);if(y<60)return`${y}m ago`;const C=Math.floor(y/60);return C<24?`${C}h ago`:c.toLocaleDateString([],{month:"short",day:"numeric"})}catch{return a}};var l=io(),f=l.firstChild,h=f.nextSibling,d=h.firstChild,g=d.nextSibling,m=n;typeof m=="function"||Array.isArray(m)?bt(()=>m,f):n=f,$(f,q(at,{get each(){return e.messages},children:a=>(()=>{var c=lo(),p=c.firstChild;return $(c,q(Zn,{get when(){return a.msg_type!=="system"},get children(){return[(()=>{var y=oo();return $(y,()=>a.user_name),y})(),(()=>{var y=so();return $(y,()=>u(a.created_at)),y})()]}}),p),$(p,()=>a.content),G(()=>`chat-message${a.msg_type==="system"?" system":""}`,(y,C)=>{wt(c,y,C)}),c})()})),d.$$keydown=s,d.$$input=a=>i(a.currentTarget.value);var w=t;return typeof w=="function"||Array.isArray(w)?bt(()=>w,d):t=d,g.$$click=o,G(()=>({e:r(),t:!r().trim()}),({e:a,t:c},p)=>{d.value=a??"",c!==p?.t&&ne(g,"disabled",c)}),l}Tt(["input","keydown","click"]);var co=P("<div class=queue-panel><div class=queue-section><h3>Now Playing</h3></div><div class=queue-section><h3>Up Next (<!>)</h3></div><div class=queue-section><h3>Recently Played</h3></div><div class=queue-section><h3>Playlist (<!>)"),ao=P('<div class="queue-item current"><span class=queue-title></span><span class=queue-duration>'),ut=P("<img class=queue-thumb alt>"),fo=P("<div class=queue-empty>Paste a URL to start watching together"),On=P("<div class=queue-item><span class=queue-title></span><span class=queue-duration>"),ho=P("<div class=queue-empty>Tracks you add will appear here"),go=P('<div class="queue-item history"><span class=queue-title></span><span class=queue-duration>'),mo=P("<div class=queue-empty>Recently played tracks show up here"),po=P("<div class=queue-empty>Add links to build a perpetual playlist");function yo(e){var t=co(),n=t.firstChild;n.firstChild;var r=n.nextSibling,i=r.firstChild,o=i.firstChild,s=o.nextSibling;s.nextSibling;var u=r.nextSibling;u.firstChild;var l=u.nextSibling,f=l.firstChild,h=f.firstChild,d=h.nextSibling;return d.nextSibling,$(n,(()=>{var g=D(()=>!!e.currentTrack);return()=>g()?(()=>{var m=ao(),w=m.firstChild,a=w.nextSibling;return $(m,(()=>{var c=D(()=>!!e.currentTrack.thumbnail);return()=>c()&&(()=>{var p=ut();return p.addEventListener("error",y=>y.currentTarget.hidden=!0),G(()=>e.currentTrack.thumbnail,y=>{ne(p,"src",y)}),p})()})(),w),$(w,()=>e.currentTrack.title),$(a,()=>e.currentTrack.duration),m})():fo()})(),null),$(i,()=>e.queue.length,s),$(r,q(at,{get each(){return e.queue},children:g=>(()=>{var m=On(),w=m.firstChild,a=w.nextSibling;return $(m,(()=>{var c=D(()=>!!g.thumbnail);return()=>c()&&(()=>{var p=ut();return p.addEventListener("error",y=>y.currentTarget.hidden=!0),G(()=>g.thumbnail,y=>{ne(p,"src",y)}),p})()})(),w),$(w,()=>g.pending?"⏳ ":"",null),$(w,()=>g.title,null),$(a,()=>g.duration),m})()}),null),$(r,(()=>{var g=D(()=>e.queue.length===0);return()=>g()&&ho()})(),null),$(u,q(at,{get each(){return e.history},children:g=>(()=>{var m=go(),w=m.firstChild,a=w.nextSibling;return $(m,(()=>{var c=D(()=>!!g.thumbnail);return()=>c()&&(()=>{var p=ut();return p.addEventListener("error",y=>y.currentTarget.hidden=!0),G(()=>g.thumbnail,y=>{ne(p,"src",y)}),p})()})(),w),$(w,()=>g.title),$(a,()=>g.duration),m})()}),null),$(u,(()=>{var g=D(()=>e.history.length===0);return()=>g()&&mo()})(),null),$(f,()=>e.playlist.length,d),$(l,q(at,{get each(){return e.playlist},children:g=>(()=>{var m=On(),w=m.firstChild,a=w.nextSibling;return $(m,(()=>{var c=D(()=>!!g.thumbnail);return()=>c()&&(()=>{var p=ut();return p.addEventListener("error",y=>y.currentTarget.hidden=!0),G(()=>g.thumbnail,y=>{ne(p,"src",y)}),p})()})(),w),$(w,()=>g.pending?"⏳ ":"",null),$(w,()=>g.title,null),$(a,()=>g.duration),m})()}),null),$(l,(()=>{var g=D(()=>e.playlist.length===0);return()=>g()&&po()})(),null),t}function wo(e){return!e||typeof e!="object"?null:"room_name"in e&&typeof e.room_name=="string"?{kind:"state",data:e}:"user_name"in e&&"content"in e?{kind:"chat",data:e}:null}function bo(e,t,n){const i=`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/api/v1/ws/${e}?name=${encodeURIComponent(t)}`;let o=new WebSocket(i),s=[],u=!1;function l(f){o?.readyState===WebSocket.OPEN?o.send(f):s.push(f)}return o.onopen=()=>{u=!0;for(const f of s)o?.send(f);s=[]},o.onmessage=f=>{if(typeof f.data=="string")try{const h=JSON.parse(f.data),d=wo(h);d?.kind==="state"?n.onState(d.data):d?.kind==="chat"&&n.onChat(d.data)}catch{}},o.onclose=f=>{o=null,u?f.code!==1e3&&f.code!==1001&&n.onError(`Connection lost (code: ${f.code})`):n.onError("Failed to connect to room"),n.onDisconnect()},o.onerror=()=>{o?.close()},f=>{l(JSON.stringify(f))}}var $o=P('<div class=home><h1>moqbox</h1><p>Watch videos together with friends</p><div class=home-form><input type=text placeholder="Your name"><button type=button></button></div><div class=home-join><input type=text placeholder="Room ID to join"><button type=button>Join Room'),vo=P('<div class="toast toast-error"role=alert>'),_o=P("<div class=room><header class=room-header><h2></h2><span class=client-count></span></header><div class=room-layout><div class=room-main><div class=room-actions><button type=button></button><button type=button class=secondary>Skip</button></div></div><div class=room-sidebar><div class=room-footer><button type=button class=theme-toggle-sm>"),So=P("<span class=disconnected>Disconnected"),Eo=P('<div class="toast toast-error"role=alert><span></span><button type=button style="margin-left:0.5rem;background:rgba(0,0,0,0.15);border:1px solid rgba(255,255,255,0.2);color:inherit;font-size:0.75rem;padding:0.15rem 0.5rem;border-radius:3px;cursor:pointer">Go back'),Co=P('<div class=inline-form><input type=text placeholder="Paste a YouTube / media URL..."><div class=add-target-toggle><button type=button>Queue</button><button type=button>Playlist</button></div><button type=button>');const Oo=()=>{const e=lr(),[t,n]=N(""),[r,i]=N(!1),[o,s]=N(""),[u,l]=N(null),f=async()=>{if(t().trim()){i(!0);try{const v=await fetch("/api/v1/rooms",{method:"POST"});if(!v.ok)throw new Error(`create room: ${v.status}`);const R=await v.json();e(`/room/${R.room_id}?name=${encodeURIComponent(t().trim())}`)}catch(v){console.error("Failed to create room:",v),l("Failed to create room"),i(!1)}}};Qe(()=>u(),v=>{if(v){const R=setTimeout(()=>l(null),5e3);return()=>clearTimeout(R)}});const h=()=>{const v=o().trim();v&&t().trim()&&e(`/room/${v}?name=${encodeURIComponent(t().trim())}`)};var d=$o(),g=d.firstChild,m=g.nextSibling,w=m.nextSibling,a=w.firstChild,c=a.nextSibling,p=w.nextSibling,y=p.firstChild,C=y.nextSibling;return a.$$keydown=v=>v.key==="Enter"&&f(),a.$$input=v=>n(v.currentTarget.value),c.$$click=f,$(c,()=>r()?"Creating...":"Create Room"),y.$$keydown=v=>v.key==="Enter"&&h(),y.$$input=v=>s(v.currentTarget.value),C.$$click=h,$(d,(()=>{var v=D(()=>!!u());return()=>v()&&(()=>{var R=vo();return $(R,u),R})()})(),null),G(()=>({e:t(),t:r()||!t().trim(),a:o(),o:!t().trim()||!o().trim()}),({e:v,t:R,a:X,o:Ee},se)=>{a.value=v??"",R!==se?.t&&ne(c,"disabled",R),y.value=X??"",Ee!==se?.o&&ne(C,"disabled",Ee)}),d},To=()=>{const e=xi(),t=te(()=>e.id),r=new URLSearchParams(location.search).get("name")||"anonymous",i=lr(),[o,s]=N(localStorage.getItem("moqbox-theme")||"system"),u=S=>{const T=document.documentElement;S==="latte"?(T.setAttribute("data-theme","latte"),T.style.colorScheme="light"):S==="macchiato"?(T.setAttribute("data-theme","macchiato"),T.style.colorScheme="dark"):(T.removeAttribute("data-theme"),T.style.colorScheme="light dark"),localStorage.setItem("moqbox-theme",S),s(S)},l=()=>{const S=o()==="macchiato"?"latte":o()==="latte"?"system":"macchiato";u(S)},[f,h]=N(null),[d,g]=N([]),[m,w]=N(!1),[a,c]=N(null),p={current:!0},[y,C]=N(null),v=()=>{if(y())return;const S=bo(t,r,{onState:T=>{p.current=!1,h(T)},onChat:T=>g(L=>{const Y=[...L,T];return Y.length>200?Y.slice(-200):Y}),onDisconnect:()=>{w(!1),C(null)},onError:T=>{c(T)}});C(()=>S),w(!0),c(null)};ni(()=>{v(),setTimeout(()=>{p.current&&c("Room not found or unavailable")},8e3)}),Qe(()=>[m(),y()],([S,T])=>{if(!S&&!T){const L=setTimeout(v,3e3);return()=>clearTimeout(L)}}),Qe(()=>a(),S=>{if(S&&!S.includes("not found")){const T=setTimeout(()=>c(null),5e3);return()=>clearTimeout(T)}});const R=S=>{y()?.({type:"chat",content:S})},X=()=>{y()?.({type:"skip"})},Ee=()=>{const S=f()?.current_track;S&&y()?.({type:"track_ended",item_id:S.id})},[se,nt]=N(!1),[De,rt]=N(""),[Ce,k]=N("queue"),[x,z]=N(!1),ge=async()=>{const S=De().trim();if(S){z(!0);try{const T=Ce()==="queue"?"/api/v1/ingest":"/api/v1/playlist/add",L=await fetch(T,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:S,room_id:t})});if(!L.ok){if(L.status===429){const Y=L.headers.get("Retry-After");c(Y?`Too fast — try again in ${Y}s`:"Too fast — try again in a moment")}else{const Y=await L.json().catch(()=>({error:L.statusText}));c(Y.error||`${Ce()} failed`)}return}rt(""),nt(!1),c(null)}catch(T){c(String(T))}finally{z(!1)}}};var me=_o(),ie=me.firstChild,Oe=ie.firstChild,pe=Oe.nextSibling,ye=ie.nextSibling,le=ye.firstChild,Te=le.firstChild,At=Te.firstChild,an=At.nextSibling,Rt=le.nextSibling,Pt=Rt.firstChild,kt=Pt.firstChild;return $(Oe,()=>f()?.room_name??"Loading..."),$(pe,(()=>{var S=D(()=>f()?.clients!=null);return()=>S()?`${f()?.clients} connected`:""})()),$(ie,(()=>{var S=D(()=>!m());return()=>S()&&So()})(),null),$(me,(()=>{var S=D(()=>!!a());return()=>S()&&(()=>{var T=Eo(),L=T.firstChild,Y=L.nextSibling;return $(L,a),Y.$$click=()=>i("/"),T})()})(),ye),$(le,q(ro,{get track(){return f()?.current_track??null},roomId:t,onEnded:Ee}),Te),At.$$click=()=>{nt(!se())},$(At,()=>se()?"Cancel":"Add"),an.$$click=X,$(le,(()=>{var S=D(()=>!!se());return()=>S()&&(()=>{var T=Co(),L=T.firstChild,Y=L.nextSibling,It=Y.firstChild,fn=It.nextSibling,xt=Y.nextSibling;return L.$$keydown=Me=>Me.key==="Enter"&&ge(),L.$$input=Me=>rt(Me.currentTarget.value),It.$$click=()=>k("queue"),fn.$$click=()=>k("playlist"),xt.$$click=ge,$(xt,()=>x()?"Adding...":"Add"),G(()=>({e:De(),t:Ce()==="queue"?"active":"",a:Ce()==="playlist"?"active":"",o:x()||!De().trim()}),({e:Me,t:ar,a:fr,o:dn},Lt)=>{L.value=Me??"",wt(It,ar,Lt?.t),wt(fn,fr,Lt?.a),dn!==Lt?.o&&ne(xt,"disabled",dn)}),T})()})(),null),$(Rt,q(yo,{get currentTrack(){return f()?.current_track??null},get queue(){return f()?.queue??[]},get history(){return f()?.history??[]},get playlist(){return f()?.playlist??[]}}),Pt),$(Rt,q(uo,{get messages(){return d()},onSend:R}),Pt),kt.$$click=l,$(kt,(()=>{var S=D(()=>o()==="macchiato");return()=>S()?"Dark":o()==="latte"?"Light":"System"})()),G(()=>({e:!f()?.current_track,t:`Theme: ${o()}`}),({e:S,t:T},L)=>{S!==L?.e&&ne(an,"disabled",S),T!==L?.t&&ne(kt,"title",T)}),me},Ao=()=>{const e=localStorage.getItem("moqbox-theme")||"system";return e==="latte"?(document.documentElement.setAttribute("data-theme","latte"),document.documentElement.style.colorScheme="light"):e==="macchiato"?(document.documentElement.setAttribute("data-theme","macchiato"),document.documentElement.style.colorScheme="dark"):document.documentElement.style.colorScheme="light dark",q(Ji,{get children(){return[q(Cn,{path:"/",component:Oo}),q(Cn,{path:"/room/:id",component:To})]}})};Tt(["input","keydown","click"]);const Tn=document.getElementById("root");Tn&&_i(()=>q(Ao,{}),Tn);
+1
static/dist/assets/index-uQDzhXyk.js
··· 1 + (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const s of o.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();class U extends Error{source;constructor(t){super(),this.source=t}}class Lt extends Error{source;constructor(t,n){super(n instanceof Error?n.message:String(n),{cause:n}),this.source=t}}class yn extends Error{constructor(){super("")}}class or extends Error{constructor(){super("")}}const wn=0,je=1,Ae=2,gt=4,le=8,We=16,H=32,he=64,we=128,Ut=256,lt=512,Ve=1024,pt=1,sr=2,mt=4,lr=8,bn=16,Ie=32,Kt=64,O=1,K=2,M=4,Ce=1,J=2,_e=3,S={},ur=typeof Proxy=="function",Wt={},cr=Symbol("refresh");function $n(e,t){const n=(e.i?.t?e.i.u?.o:e.i?.o)??-1;n>=e.o&&(e.o=n+1);const r=e.o,i=t.l[r];if(i===void 0)t.l[r]=e;else{const o=i.S;o.T=e,e.S=o,i.S=e}r>t._&&(t._=r)}function Je(e,t){let n=e.O;n&(le|gt|Ve)||(n&je?e.O=n&-4|Ae|le:e.O=n|le,n&We||$n(e,t))}function vn(e,t){let n=e.O;n&(le|gt|We|Ve)||(e.O=n|We,$n(e,t))}function ke(e,t){const n=e.O;if(!(n&(le|We)))return;e.O=n&-25;const r=e.o;if(e.S===e)t.l[r]=void 0;else{const i=e.T,o=t.l[r],s=i??o;e===o?t.l[r]=i:e.S.T=i,s.S=e.S}e.S=e,e.T=void 0}function ar(e){if(!e.R){e.R=!0;for(let t=0;t<=e._;t++)for(let n=e.l[t];n!==void 0;n=n.T)n.O&le&&ut(n)}}function ut(e,t=Ae){const n=e.O;if(!((n&(je|Ae))>=t)){e.O=n&-4|t;for(let r=e.I;r!==null;r=r.p)ut(r.h,je);if(e.N!==null)for(let r=e.N;r!==null;r=r.A)for(let i=r.I;i!==null;i=i.p)ut(i.h,je)}}function Ne(e,t){for(e.R=!1,e.C=0;e.C<=e._;e.C++){let n=e.l[e.C];for(;n!==void 0;)n.O&le?t(n):fr(n,e),n=e.l[e.C]}e._=0}function fr(e,t){ke(e,t);let n=e.o;for(let r=e.P;r;r=r.D){const i=r.m,o=i.V||i;o.L&&o.o>=n&&(n=o.o+1)}if(e.o!==n){e.o=n;for(let r=e.I;r!==null;r=r.p)vn(r.h,t)}}const ct=new WeakMap,ne=new Set;function dr(e){let t=ct.get(e);if(t)return W(t);const n=e.U,r=n?.G?W(n.G):null;return t={k:e,F:new Set,W:[[],[]],H:null,M:w,j:r},ct.set(e,t),ne.add(t),e.$=!1,t}function W(e){for(;e.H;)e=e.H;return e}function _n(e,t){if(e=W(e),t=W(t),e===t)return e;t.H=e;for(const n of t.F)e.F.add(n);return e.W[0].push(...t.W[0]),e.W[1].push(...t.W[1]),e}function Oe(e){const t=e.G;if(!t)return;const n=W(t);if(ne.has(n))return n;e.G=void 0}function De(e){return Oe(e)?.M??e.M}function yt(e){return e.K!==void 0&&e.K!==S}function Vt(e,t){const n=W(t),r=e.G;if(r){if(r.H){e.G=t;return}const i=W(r);if(ne.has(i)){i!==n&&!yt(e)&&(n.j&&W(n.j)===i?e.G=t:i.j&&W(i.j)===n||_n(n,i));return}}e.G=t}const qe=new Set,k={l:new Array(2e3).fill(void 0),R:!1,C:0,_:0},Z={l:new Array(2e3).fill(void 0),R:!1,C:0,_:0};let q=0,w=null,ze=!1,Nt=0,it=null;const Me=new Set;function hr(e){return qe.size===0&&ne.size===0&&e.Y.length===0&&e.Z.length===0&&e.q.size===0&&Me.size===0}function gr(){if(Me.size!==0)for(const e of Me){if(e.I!==null){Me.delete(e);continue}e.B===S&&(e.K!==void 0&&e.K!==S||(Me.delete(e),e.X?.()))}}function pr(e){return!!it?.has(e)}function Xe(e){for(const t of ne){if(t.H||t.F.size>0)continue;const n=t.W[e-1];n.length&&(t.W[e-1]=[],ft(n,e))}}function mr(e){for(let t=e.I;t!==null;t=t.p){const n=t.h;if(!n.J)continue;if(n.J===_e){n.ee||(n.ee=!0,n.te.enqueue(J,n.ne));continue}const r=n.O&H?Z:k;r.C>n.o&&(r.C=n.o),Je(n,r)}}function yr(e,t){t.ie=e,e.re.push(...t.re);for(const n of ne)n.M===t&&(n.M=e);e.Z.push(...t.Z);for(const n of t.q)e.q.add(n);for(const[n,r]of t.oe){let i=e.oe.get(n);i||e.oe.set(n,i=new Set);for(const o of r)i.add(o)}for(const n of t.se)e.se.add(n)}function wr(e){for(let t=0;t<e.length;t++){const n=e[t];n.G=void 0,n.B!==S&&(n.ue=n.B,n.B=S);const r=n.K;n.K=S,r!==S&&n.ue!==r&&Re(n,!0),n.M=null}e.length=0}function br(e){for(const t of ne)(e?t.M===e:!t.M)&&(t.H||(t.W[0].length&&ft(t.W[0],Ce),t.W[1].length&&ft(t.W[1],J)),t.k.G===t&&(t.k.G=void 0),t.F.clear(),t.W[0].length=0,t.W[1].length=0,ne.delete(t),ct.delete(t.k))}function $e(){ze||(ze=!0,!Nt&&!C.ce&&queueMicrotask(Te))}let $r=class{i=null;le=[[],[]];Y=[];created=q;addChild(t){this.Y.push(t),t.i=this}removeChild(t){const n=this.Y.indexOf(t);n>=0&&(this.Y.splice(n,1),t.i=null)}notify(t,n,r,i){return this.i?this.i.notify(t,n,r,i):!1}run(t){if(this.le[t-1].length){const n=this.le[t-1];this.le[t-1]=[],ft(n,t)}for(let n=0;n<this.Y.length;n++)this.Y[n].run?.(t)}enqueue(t,n){t&&(G?W(G).W[t-1].push(n):this.le[t-1].push(n)),$e()}stashQueues(t){t.le[0].push(...this.le[0]),t.le[1].push(...this.le[1]),this.le=[[],[]];for(let n=0;n<this.Y.length;n++){let r=this.Y[n],i=t.Y[n];i||(i={le:[[],[]],Y:[]},t.Y[n]=i),r.stashQueues(i)}}restoreQueues(t){this.le[0].push(...t.le[0]),this.le[1].push(...t.le[1]);for(let n=0;n<t.Y.length;n++){const r=t.Y[n];let i=this.Y[n];i&&i.restoreQueues(r)}}};class X extends $r{ce=!1;ae=null;fe=[];Z=[];q=new Set;static Ee;static Se;static Te;static de=null;flush(){if(!this.ce){this.ce=!0;try{if(Ne(k,X.Ee),w){if(!_r(w)){const r=w;if(Ne(Z,X.Ee),this.ae=null,this.fe=[],this.Z=[],this.q=new Set,Xe(Ce),Xe(J),this.stashQueues(r._e),q++,ze=k._>=k.C,rn(r.fe),w=null,!r.re.length&&!r.oe.size&&r.Z.length){it=new Set;for(let i=0;i<r.Z.length;i++){const o=r.Z[i];o.L||o.Oe&pt||(it.add(o),mr(o))}}try{Ot(null,!0)}finally{it=null}return}this.fe!==w.fe&&this.fe.push(...w.fe),this.restoreQueues(w._e),qe.delete(w);const n=w;w=null,rn(this.fe),Ot(n)}else hr(this)?(at(),k._>=k.C&&(Ne(k,X.Ee),at())):(qe.size&&Ne(Z,X.Ee),Ot());q++,ze=k._>=k.C,ne.size&&Xe(Ce),this.run(Ce),ne.size&&Xe(J),this.run(J)}finally{this.ce=!1}}}notify(t,n,r,i){if(n&O){if(r&O){const o=i!==void 0?i:t.Re;if(w&&o){const s=o.source;let u=w.oe.get(s);u||w.oe.set(s,u=new Set);const l=u.size;u.add(t),u.size!==l&&$e()}}return!0}return!1}initTransition(t){if(t&&(t=En(t)),!(t&&t===w)&&!(!t&&w&&w.Ie===q)){if(!w)w=t??{Ie:q,fe:[],oe:new Map,Z:[],q:new Set,re:[],_e:{le:[[],[]],Y:[]},ie:!1,se:new Set};else if(t){const n=w;yr(t,n),qe.delete(n),w=t}if(qe.add(w),w.Ie=q,this.ae!==null&&(this.ae.M=w,w.fe.push(this.ae),this.ae=null),this.fe!==w.fe){for(let n=0;n<this.fe.length;n++){const r=this.fe[n];r.M=w,w.fe.push(r)}this.fe=w.fe}if(this.Z!==w.Z){for(let n=0;n<this.Z.length;n++){const r=this.Z[n];r.M=w,w.Z.push(r)}this.Z=w.Z}for(const n of ne)n.M||(n.M=w);if(this.q!==w.q){for(const n of this.q)w.q.add(n);this.q=w.q}}}}function wt(e){if(w){C.fe.push(e);return}if(C.ae===null&&C.fe.length===0){C.ae=e;return}C.ae!==null&&(C.fe.push(C.ae),C.ae=null),C.fe.push(e)}function Re(e,t=!1){const n=e.G||G,r=e.pe!==void 0;for(let i=e.I;i!==null;i=i.p){if(r&&i.h.Oe&lr){i.h.O|=Ut;continue}t&&n?(i.h.O|=we,Vt(i.h,n)):t&&(i.h.O|=we,i.h.G=void 0);const o=i.h;if(o.J===_e){o.ee||(o.ee=!0,o.te.enqueue(J,o.ne));continue}const s=i.h.O&H?Z:k;s.C>i.h.o&&(s.C=i.h.o),Je(i.h,s)}}function nn(e){const t=e;if(!t.L){e.B!==S&&(e.ue=e.B,e.B=S);return}e.B!==S&&(e.ue=e.B,e.B=S,e.J&&e.J!==_e&&(e.ee=!0)),t.O&=~Ve,t.he&O||(t.he&=~M),(t.Ne!==null||t.Ae!==null)&&X.Se(t,!1,!0)}function at(){C.ae!==null&&(nn(C.ae),C.ae=null);const e=C.fe;for(let t=0;t<e.length;t++)nn(e[t]);e.length=0}function Ot(e=null,t=!1){const n=!t;n&&at(),!t&&C.Y.length&&Sn(C);const r=k._>=k.C;if(r&&Ne(k,X.Ee),n){if(r&&at(),wr(e?e.Z:C.Z),e&&e.se.size){for(const i of e.se){if(i.O&he)continue;if(i.J===_e){i.ee||(i.ee=!0,i.te.enqueue(J,i.ne));continue}const o=i.O&H?Z:k;o.C>i.o&&(o.C=i.o),Je(i,o)}e.se.clear()}e?e.q:C.q,gr(),br(e)}}function Sn(e){for(const t of e.Y)t.checkSources?.(),Sn(t)}function rn(e){for(let t=0;t<e.length;t++)e[t].M=w}const C=new X;function Te(e){if(e){Nt++;try{return e()}finally{Te(),Nt--}}if(!C.ce)for(;ze||w;)C.flush()}function ft(e,t){for(let n=0;n<e.length;n++)e[n](t)}function vr(e,t){if(e.O&(H|he))return!1;if(e.Ce===t||e.Pe?.has(t))return!0;for(let n=e.P;n;n=n.D){let r=n.m;for(;r;){if(r===t||r.V===t)return!0;r=r.U}}return!!(e.he&O&&e.Re instanceof U&&e.Re.source===t)}function _r(e){if(e.ie)return!0;if(e.re.length)return!1;let t=!0;for(const[n,r]of e.oe){let i=!1;for(const o of r){if(vr(o,n)){i=!0;break}r.delete(o)}if(!i)e.oe.delete(n);else if(n.he&O&&n.Re?.source===n){t=!1;break}}if(t)for(let n=0;n<e.Z.length;n++){const r=e.Z[n];if(yt(r)&&"he"in r&&r.he&O&&r.Re instanceof U&&r.Re.source!==r){t=!1;break}}return t&&(e.ie=!0),t}function En(e){for(;e.ie&&typeof e.ie=="object";)e=e.ie;return e}function Sr(e,t){const n=w;try{return w=En(e),t()}finally{w=n}}function Cn(e){let t=e.ge;for(;t;)t.O|=H,t.O&le&&(ke(t,k),Je(t,Z)),Cn(t),t=t.De}function He(e,t=!1,n){const r=e.O;if(r&he)return;t&&(e.O=r|he),t&&e.L&&(e.ve=null);let i=n?e.Ne:e.ge;for(;i;){const o=i.De;if(i.P){const s=i;ke(s,s.O&H?Z:k);let u=s.P;do u=Jt(u);while(u!==null);s.P=null,s.ye=null}He(i,!0),i=o}if(n?e.Ne=null:(e.ge=null,e.me=0),t&&!n&&!(r&H)&&e.i!==null&&!(e.i.O&he)){const o=e.we,s=e.De;o!==null?o.De=s:e.i.ge=s,s!==null&&(s.we=o),e.we=null}Er(e,n)}function Er(e,t){let n=t?e.Ae:e.be;if(n){if(Array.isArray(n))for(let r=0;r<n.length;r++){const i=n[r];i.call(i)}else n.call(n);t?e.Ae=null:e.be=null}}function Cr(e,t){let n=e;for(;n.Oe&mt&&n.i;)n=n.i;if(n.id!=null)return Or(n.id,n.me++);throw new Error("Cannot get child id from owner without an id")}function zt(e){return Cr(e)}function Or(e,t){const n=t.toString(36),r=n.length-1;return e+(r?String.fromCharCode(64+r):"")+n}function Ze(){return _}function bt(e){return _&&(_.be?Array.isArray(_.be)?_.be.push(e):_.be=[_.be,e]:_.be=e),e}function Ar(e=!0){He(this,e)}function Ue(e){const t=_,n=e?.transparent??!1,r={id:e?.id??(n?t?.id:t?.id!=null?zt(t):void 0),Oe:n?mt:0,t:!0,u:t?.t?t.u:t,ge:null,De:null,we:null,be:null,te:t?.te??C,Ve:t?.Ve||Wt,me:0,Ae:null,Ne:null,i:t,dispose:Ar};if(t){const i=t.ge;i===null||(r.De=i,i.we=r),t.ge=r}return r}function Gt(e,t){const n=Ue(t);return se(n,()=>e(()=>n.dispose()))}function Jt(e){const t=e.m,n=e.D,r=e.p,i=e.Le;if(r!==null?r.Le=i:t.Ue=i,i!==null)i.p=r;else if(t.I=r,r===null){t.X?.();const o=t;o.L&&o.Oe&Ie&&!(o.O&H)&&An(o)}return n}function On(e){const t=e.ye;let n=t!==null?t.D:e.P;if(n!==null){do n=Jt(n);while(n!==null);t!==null?t.D=null:e.P=null}}function An(e){ke(e,e.O&H?Z:k);let t=e.P;for(;t!==null;)t=Jt(t);e.P=null,e.ye=null,He(e,!0)}function Le(e,t){const n=t.ye;if(n!==null&&n.m===e)return;let r=null;const i=t.O&gt;if(i&&(r=n!==null?n.D:t.P,r!==null&&r.m===e)){t.ye=r;return}const o=e.Ue;if(o!==null&&o.h===t&&(!i||Rr(o,t)))return;const s=t.ye=e.Ue={m:e,h:t,D:r,Le:o,p:null};n!==null?n.D=s:t.P=s,o!==null?o.p=s:e.I=s}function Rr(e,t){const n=t.ye;if(n!==null){let r=t.P;do{if(r===e)return!0;if(r===n)break;r=r.D}while(r!==null)}return!1}function Tr(e,t){return e.Ce===t||e.Pe?.has(t)?!1:e.Ce?(e.Pe?e.Pe.add(t):e.Pe=new Set([e.Ce,t]),e.Ce=void 0,!0):(e.Ce=t,!0)}function Pr(e,t){return e.Ce?e.Ce!==t?!1:(e.Ce=void 0,!0):e.Pe?.delete(t)?(e.Pe.size===1?(e.Ce=e.Pe.values().next().value,e.Pe=void 0):e.Pe.size===0&&(e.Pe=void 0),!0):!1}function Rn(e){e.Ce=void 0,e.Pe?.clear(),e.Pe=void 0}function dt(e,t,n){if(!t){e.Re=null;return}if(n instanceof U&&n.source===t){e.Re=n;return}const r=e.Re;(!(r instanceof U)||r.source!==t)&&(e.Re=new U(t))}function Dt(e,t){for(let n=e.I;n!==null;n=n.p)t(n.h);for(let n=e.N;n!==null;n=n.A)for(let r=n.I;r!==null;r=r.p)t(r.h)}function Ir(e){let t=!1;const n=new Set,r=i=>{if(n.has(i)||!Pr(i,e))return;n.add(i),i.Ie=q;const o=i.Ce??i.Pe?.values().next().value;if(o)dt(i,o),ve(i);else{if(i.he&=~O,dt(i),ve(i),i.Ge){if(i.J===_e){const s=i;s.ee||(s.ee=!0,s.te.enqueue(J,s.ne))}else{const s=i.O&H?Z:k;s.C>i.o&&(s.C=i.o),Je(i,s)}t=!0}i.Ge=!1}Dt(i,r)};Dt(e,r),t&&$e()}function kr(e,t,n){let r=!1,i=!1;if(typeof t=="object"&&t!==null&&ee(()=>{r=t[Symbol.asyncIterator],i=!r&&typeof t.then=="function"}),!i&&!r)return e.ve=null,t;e.ve=t;let o;const s=l=>{e.ve===t&&(C.initTransition(De(e)),Ht(e,l instanceof U?O:K,l),e.Ie=q)},u=(l,d)=>{if(e.ve!==t||e.O&(Ae|we))return;C.initTransition(De(e));const h=!!(e.he&M);On(e),Tn(e);const a=Oe(e);if(a&&a.F.delete(e),e.K!==void 0)e.K!==void 0&&e.K!==S?e.B=l:(e.ue=l,Re(e)),e.Ie=q;else if(a){const g=e.J,p=e.ue,b=e.ke;(!g&&h||!b||!b(l,p))&&(e.ue=l,e.Ie=q,e.Fe&&ce(e.Fe,l),Re(e,!0))}else ce(e,()=>l);Ir(e),$e(),Te(),d?.()};if(i){let l=!1,d=!0;if(t.then(h=>{d?(o=h,l=!0):u(h)},h=>{d||s(h)}),d=!1,!l)throw C.initTransition(De(e)),new U(_)}if(r){const l=t[Symbol.asyncIterator]();let d=!1,h=!1;bt(()=>{if(!h){h=!0;try{const p=l.return?.();p&&typeof p.then=="function"&&p.then(void 0,()=>{})}catch{}}});const a=()=>{let p,b=!1,c=!0;return l.next().then(f=>{if(c)p=f,b=!0,f.done&&(h=!0);else{if(e.ve!==t)return;f.done?(h=!0,$e(),Te()):u(f.value,a)}},f=>{!c&&e.ve===t&&(h=!0,s(f))}),c=!1,b&&!p.done?(o=p.value,d=!0,a()):b&&p.done},g=a();if(!d&&!g)throw C.initTransition(De(e)),new U(_)}return o}function Tn(e,t=!1){(e.Ce||e.Pe)&&Rn(e),e.Ge&&(e.Ge=!1),e.he=t?0:e.he&M,e.Re&&dt(e),e.We&&ve(e),e.xe&&e.xe()}function Ht(e,t,n,r,i){t===K&&!(n instanceof Lt)&&!(n instanceof U)&&(n=new Lt(e,n));const o=t===O&&n instanceof U?n.source:void 0,s=o===e,u=t===O&&e.K!==void 0&&!s,l=u&&yt(e);r||(t===O&&o?(Tr(e,o),e.he=O|e.he&M,dt(e,o,n)):(Rn(e),e.he=t|(t!==K?e.he&M:0),e.Re=n),ve(e)),i&&!r&&Vt(e,i);const d=r||l,h=r||u?void 0:i;if(e.xe){if(r&&t===O)return;d?e.xe(t,n):e.xe();return}Dt(e,a=>{a.Ie=q,(t===O&&o&&a.Ce!==o&&!a.Pe?.has(o)||t!==O&&(a.Re!==n||a.Ce||a.Pe))&&(!d&&!a.M&&wt(a),Ht(a,t,n,d,h))})}let xr=null;X.Ee=ue;X.Se=He;let F=!1,ie=!1,_=null,G=null;function ue(e,t=!1){const n=e.J;t||(e.M&&(!n||w)&&w!==e.M&&C.initTransition(e.M),ke(e,e.O&H?Z:k),e.ve=null,e.M||n===_e?He(e):(e.ge!==null||e.be!==null)&&(Cn(e),e.Ae=e.be,e.Ne=e.ge,e.be=null,e.ge=null,e.me=0));let r=!!(e.O&we);const i=e.K!==void 0&&e.K!==S,o=!!(e.he&O),s=!!(e.he&M),u=_;_=e,e.ye=null,e.O=gt,e.Ie=q;let l=e.B===S?e.ue:e.B,d=e.o,h=F,a=G;if(F=!0,r){const c=Oe(e);c&&(G=c)}else if(w&&!t&&w.Z.length)for(let c=e.P;c;c=c.D){const f=c.m;if(f.O&we){const m=Oe(f);if(m){r=!0,G=m,e.O|=we,Vt(e,m);break}}}const g=n&&n!==J,p=ie;g&&(ie=!0);try{if(e.Oe&Kt)l=e.L(l),e.ve=null;else{const c=e.ve,f=e.L(l),m=typeof f=="object"&&f!==null,y=e.ve!==c;l=y||!m?f:kr(e,f),!y&&!m&&(e.ve=null)}if(Tn(e,t),e.G){const c=Oe(e);c&&(c.F.delete(e),ve(c.k))}}catch(c){if(c instanceof U&&G){const f=W(G);f.k!==e&&(f.F.add(e),e.G=f,ve(f.k))}c instanceof U&&(e.Ge=!0),Ht(e,c instanceof U?O:K,c,void 0,c instanceof U?e.G:void 0)}finally{F=h,g&&(ie=p),e.O=wn|(t?e.O&Ut:0),_=u}if(!e.Re){On(e);const c=i?e.K:e.B===S?e.ue:e.B,f=!n&&s||!e.ke||!e.ke(c,l);if(n&&f&&(e.ee=!e.Re,t||e.te.enqueue(n,X.Te.bind(null,e))),f){const m=i?e.K:void 0;t||n&&w!==e.M||r?(e.ue=l,i&&r&&(e.K=l,e.B=l)):e.B=l,i&&!r&&o&&!e.$&&(e.K=l),(!i||r||e.K!==m)&&Re(e,r||i)}else if(i)e.B=l;else if(e.o!=d)for(let m=e.I;m!==null;m=m.p)vn(m.h,m.h.O&H?Z:k)}G=a,(e.B!==S||e.Ne!==null||e.Ae!==null||!!(e.he&(O|M)))&&(!t||e.he&O)&&!e.M&&!(w&&i)&&wt(e),e.M&&n&&w!==e.M&&Sr(e.M,()=>ue(e))}function Pn(e){if(e.O&je)for(let t=e.P;t;t=t.D){const n=t.m,r=n.V||n;if(r.L&&Pn(r),e.O&Ae)break}(e.O&(Ae|we)||e.Re&&e.Ie<q&&!e.ve)&&ue(e),e.O=e.O&(Ut|le|We)}function $t(e,t){const n=t?.transparent??!1,r={id:t?.id??(n?_?.id:_?.id!=null?zt(_):void 0),Oe:(n?mt:0)|(t?.ownedWrite?pt:0)|(!_||t?.lazy?Ie:0)|(t?.sync?Kt:0)|0,ke:t?.equals!=null?t.equals:kn,X:t?.unobserved,be:null,te:_?.te??C,Ve:_?.Ve??Wt,me:0,L:e,ue:void 0,o:0,N:null,T:void 0,S:null,P:null,ye:null,I:null,Ue:null,i:_,De:null,we:null,ge:null,O:t?.lazy?lt:wn,he:M,Ie:q,B:S,Ae:null,Ne:null,ve:null,M:null};return In(r,t),r}function Lr(e,t,n,r,i,o){const s=o?.transparent??!1,u={id:o?.id??(s?_?.id:_?.id!=null?zt(_):void 0),Oe:(s?mt:0)|(o?.ownedWrite?pt:0)|(o?.sync?Kt:0)|0,ke:!1,X:o?.unobserved,be:null,te:_?.te??C,Ve:_?.Ve??Wt,me:0,L:e,ue:void 0,o:0,N:null,T:void 0,S:null,P:null,ye:null,I:null,Ue:null,i:_,De:null,we:null,ge:null,O:lt,he:M,Ie:q,B:S,Ae:null,Ne:null,ve:null,M:null,ee:!1,Me:void 0,Qe:t,je:n,$e:void 0,Ke:!1,J:r,xe:i};return In(u,Nr),u}const Nr={lazy:!0};function In(e,t){e.S=e;const n=_?.t?_.u:_;if(_){const r=_.ge;r===null||(e.De=r,r.we=e),_.ge=e}n&&(e.o=n.o+1),!t?.lazy&&ue(e,!0)}function Be(e,t,n=null){const r={ke:t?.equals!=null?t.equals:kn,Oe:(t?.ownedWrite?pt:0)|(t?.Ye?sr:0),X:t?.unobserved,ue:e,I:null,Ue:null,Ie:q,V:n,A:n?.N||null,B:S};return n&&(n.N=r),r}function kn(e,t){return e===t}function ee(e,t){if(!F)return e();const n=F;F=!1;try{return e()}finally{F=n}}function xn(e){let t=_;t?.t&&(t=t.u);const n=e;if(typeof n.L=="function"){const o=e;o.O&lt?(o.O&=~lt,ue(o,!0)):o.O&he&&ue(o,!0)}const r=e.V||e;if(!n.L&&r===e&&e.K===void 0&&e.pe===void 0&&w===null&&G===null)return t&&F&&Le(e,t),!t||e.B===S?e.ue:e.B;if(t&&F&&(Le(e,t),r.L)){const o=e.O&H;r.o>=(o?Z.C:k.C)&&(ut(t),ar(o?Z:k),Pn(r));const s=r.o;s>=t.o&&e.i!==t&&(t.o=s+1)}if(r.he&O)if(t&&!(ie&&r.M&&w!==r.M))if(G){const o=r.G,s=W(G);if(o&&W(o)===s&&!yt(r))throw!F&&e!==t&&Le(e,t),r.Re}else throw!F&&e!==t&&Le(e,t),r.Re;else{if(t&&r!==e&&r.he&M)throw!F&&e!==t&&Le(e,t),r.Re;if(!t&&r.he&M)throw r.Re}if(e.L&&e.he&K){if(e.Ie<q)return ue(e),xn(e);throw e.Re}if(e.K!==void 0&&e.K!==S)return t&&ie&&pr(e)?e.ue:e.K;if(w!==null&&G!==null&&e.B!==S&&r===e&&!e.L&&t)return w.se.add(t),e.ue;const i=!t||G!==null&&(e.K!==void 0||e.G||r===e&&ie||r.he&O)||e.B===S||ie&&e.M&&w!==e.M?e.ue:e.B;return!t&&r===e&&typeof n.L=="function"&&e.Oe&Ie&&!(r.he&O)&&!e.I&&An(e),i}function ce(e,t){e.M&&w!==e.M&&C.initTransition(e.M);const n=e.K!==void 0&&!0,r=e.K!==void 0&&e.K!==S,i=n?r?e.K:e.ue:e.B===S?e.ue:e.B;if(typeof t=="function"&&(t=t(i)),!(!e.ke||!e.ke(i,t)||!!(e.he&M)))return n&&r&&e.L&&(Re(e,!0),$e()),t;if(n){const s=e.K===S;s||C.initTransition(De(e)),s&&(e.B=e.ue,C.Z.push(e)),e.$=!0;const u=dr(e);e.G=u,e.K=t}else e.B===S&&wt(e),e.B=t;return e.We&&ve(e),e.Fe&&ce(e.Fe,t),e.Ie=q,Re(e,n),$e(),t}function Dr(e){ke(e,e.O&H?Z:k),!(e.O&Ve)&&e.B===S&&wt(e),e.O=e.O&-4|Ve}function qr(e,t){const n=ce(e,t);return Dr(e),n}function se(e,t){const n=_,r=F;_=e,F=!1;try{return t()}finally{_=n,F=r}}function Mr(e){const t=e,n=e.V;if(e.U){const r=e.U;return r.he&O&&!(r.he&M)?!0:e.B!==S&&!(t.he&M)}if(n&&e.B!==S)return!n.ve&&!(n.he&O);if(e.K!==void 0&&e.K!==S){if(t.he&O&&!(t.he&M))return!0;if(e.U){const r=e.G?W(e.G):null;return!!(r&&r.F.size>0)}return!0}return e.K!==void 0&&e.K===S&&!e.U?!1:e.B!==S&&!(t.he&M)?!0:!!(t.he&O&&!(t.he&M))}function ve(e){if(e.We){const t=Mr(e),n=e.We;if(ce(n,t),!t&&n.G){const r=Oe(e);if(r&&r.F.size>0){const i=W(n.G);i!==r&&_n(r,i)}ct.delete(n),n.G=void 0}}}function Br(e,t=!0){const n=ie;ie=t;try{return e()}finally{ie=n}}function Fr(e,t=Ze()){if(!t)throw new yn;const n=Ur(e,t)?t.Ve[e.id]:e.defaultValue;if(Zt(n))throw new or;return n}function jr(e,t,n=Ze()){if(!n)throw new yn;n.Ve={...n.Ve,[e.id]:Zt(t)?e.defaultValue:t}}function Ur(e,t){return!Zt(t?.Ve[e.id])}function Zt(e){return typeof e>"u"}function Ln(e,t,n,r){const i=!!r?.user,o=Lr(e,t,n,i?J:Ce,Kr,r);ue(o,!0),!r?.defer&&(o.J===J||r?.schedule?o.te.enqueue(o.J,qt.bind(null,o)):qt(o))}function Kr(e,t){const n=e!==void 0?e:this.he,r=t!==void 0?t:this.Re;if(n&K){let i=r;if(this.te.notify(this,O,0),this.J===J)try{return this.je?this.je(i,()=>{this.$e?.(),this.$e=void 0}):console.error(i)}catch(o){i=o}if(!this.te.notify(this,K,K))throw i}else this.J===Ce&&this.te.notify(this,O|K,n,r)}function qt(e){if(!(!e.ee||e.O&he)){e.$e?.(),e.$e=void 0;try{const t=e.Qe(e.ue,e.Me);e.$e=t,e.$e&&!e.Ke&&(e.Ke=!0,se(e.i,()=>bt(()=>e.$e?.())))}catch(t){if(e.Re=new Lt(e,t),e.he|=K,!e.te.notify(e,K,K))throw t}finally{e.Me=e.ue,e.ee=!1}}}X.Te=qt;function Wr(e,t){const n=()=>{if(!(!r.ee||r.O&he))try{r.ee=!1,ue(r)}finally{}},r=$t(()=>{r.$e?.(),r.$e=void 0;const i=Br(e);r.$e=i},{...t,lazy:!0});r.$e=void 0,r.Oe=r.Oe&~Ie|bn,r.ee=!0,r.J=_e,r.xe=(i,o)=>{if((i!==void 0?i:r.he)&K){r.te.notify(r,O,0);const u=o!==void 0?o:r.Re;if(!r.te.notify(r,K,K))throw u}},r.ne=n,r.te.enqueue(J,n),bt(()=>r.$e?.())}function Nn(e){return bt(e)}function de(e){const t=xn.bind(null,e);return t[cr]=e,t}function Vr(e,t){if(typeof e=="function"){const r=$t(e,t);return r.Oe&=~Ie,[de(r),qr.bind(null,r)]}const n=Be(e,t);return[de(n),ce.bind(null,n)]}function be(e,t){return de($t(e,t))}function zr(e,t,n){Ln(e,t.effect||t,t.error,{user:!0,...n})}function Gr(e,t,n){Ln(e,t,void 0,n)}function Jr(e,t){Wr(e,t)}function Hr(e){const t=Ze();t&&!(t.Oe&bn)?Jr(()=>ee(e),void 0):C.enqueue(J,()=>{e()?.()})}const Zr=Symbol(0),Mt=Symbol(0);function on(e){return e==null||typeof e!="object"||Object.isFrozen(e)?!1:typeof Node>"u"||!(e instanceof Node)}const Dn=Symbol(0);function sn(e){return e==="__proto__"||e==="constructor"||e==="prototype"}function Fe(e,t,n=0){let r,i=e;if(n<t.length-1){r=t[n];const s=typeof r,u=Array.isArray(e);if(s==="string"&&sn(r))return;if(Array.isArray(r)){for(let l=0;l<r.length;l++)t[n]=r[l],Fe(e,t,n);t[n]=r;return}else if(u&&s==="function"){for(let l=0;l<e.length;l++)r(e[l],l)&&(t[n]=l,Fe(e,t,n));t[n]=r;return}else if(u&&s==="object"){const{from:l=0,to:d=e.length-1,by:h=1}=r;for(let a=l;a<=d;a+=h)t[n]=a,Fe(e,t,n);t[n]=r;return}else if(n<t.length-2){Fe(e[r],t,n+1);return}i=e[r]}let o=t[t.length-1];if(!(typeof o=="function"&&(o=o(i),o===i))&&!(r===void 0&&o==null))if(o===Dn)delete e[r];else if(r===void 0||on(i)&&on(o)&&!Array.isArray(o)){const s=r!==void 0?e[r]:e,u=Object.keys(o);for(let l=0;l<u.length;l++){const d=u[l];if(sn(d))continue;const h=Object.getOwnPropertyDescriptor(o,d);h.get||h.set?Object.defineProperty(s,d,h):s[d]=h.value}}else e[r]=o}Object.assign(function(...t){return n=>{Fe(n,t)}},{DELETE:Dn});function et(){return!0}const Yr={get(e,t,n){return t===Mt?n:e.get(t)},has(e,t){return t===Mt?!0:e.has(t)},set:et,deleteProperty:et,getOwnPropertyDescriptor(e,t){return{configurable:!0,enumerable:!0,get(){return e.get(t)},set:et,deleteProperty:et}},ownKeys(e){return e.keys()}};function At(e){return(e=typeof e=="function"?e():e)?e:{}}const Rt=Symbol(0);function Qr(...e){if(e.length===1&&typeof e[0]!="function")return e[0];let t=!1;const n=[];for(let l=0;l<e.length;l++){const d=e[l];t=t||!!d&&Mt in d;const h=!!d&&d[Rt];if(h)for(let a=0;a<h.length;a++)n.push(h[a]);else n.push(typeof d=="function"?(t=!0,be(d)):d)}if(ur&&t)return new Proxy({get(l){if(l===Rt)return n;for(let d=n.length-1;d>=0;d--){const h=At(n[d]);if(l in h)return h[l]}},has(l){for(let d=n.length-1;d>=0;d--)if(l in At(n[d]))return!0;return!1},keys(){const l=new Set;for(let d=0;d<n.length;d++){const h=Object.keys(At(n[d]));for(let a=0;a<h.length;a++)l.add(h[a])}return[...l]}},Yr);const r=Object.create(null);let i=!1,o=n.length-1;for(let l=o;l>=0;l--){const d=n[l];if(!d){l===o&&o--;continue}const h=Object.getOwnPropertyNames(d);for(let a=h.length-1;a>=0;a--){const g=h[a];if(!(g==="__proto__"||g==="constructor")&&!r[g]){i=i||l!==o;const p=Object.getOwnPropertyDescriptor(d,g);r[g]=p.get?{enumerable:!0,configurable:!0,get:p.get.bind(d)}:p}}}if(!i)return n[o];const s={},u=Object.keys(r);for(let l=u.length-1;l>=0;l--){const d=u[l],h=r[d];h.get?Object.defineProperty(s,d,h):s[d]=h.value}return s[Rt]=n,s}function Xr(e,t,n){const r=typeof n?.keyed=="function"?n.keyed:void 0,i=t.length>1,o=t,s={Ze:Ue(),qe:0,Be:e,ze:[],Xe:o,Je:[],et:[],tt:r,nt:r||n?.keyed===!1?[]:void 0,it:i&&n?.keyed!==!1?[]:void 0,rt:n?.keyed===!1,ot:n?.fallback},u=$t(ei.bind(s));return s.Ze.u=u,u.Oe&=~Ie,de(u)}const tt={ownedWrite:!0};function ei(){const e=this.Be()||[],t=e.length;return e[Zr],se(this.Ze,()=>{let n,r,i=this.nt?this.rt?()=>(this.nt[r]=Be(e[r],tt),this.Xe(de(this.nt[r]),r)):()=>(this.nt[r]=Be(e[r],tt),this.it&&(this.it[r]=Be(r,tt)),this.Xe(de(this.nt[r]),this.it?de(this.it[r]):void 0)):this.it?()=>{const o=e[r];return this.it[r]=Be(r,tt),this.Xe(o,de(this.it[r]))}:()=>{const o=e[r];return this.Xe(o)};if(t===0)this.qe!==0&&(this.Ze.dispose(!1),this.et=[],this.ze=[],this.Je=[],this.qe=0,this.nt&&(this.nt=[]),this.it&&(this.it=[])),this.ot&&!this.Je[0]&&(this.Je[0]=se(this.et[0]=Ue(),this.ot));else if(this.qe===0){for(this.et[0]&&this.et[0].dispose(),this.Je=new Array(t),r=0;r<t;r++)this.ze[r]=e[r],this.Je[r]=se(this.et[r]=Ue(),i);this.qe=t}else{let o,s,u,l,d,h,a,g=new Array(t),p=new Array(t),b=this.nt?new Array(t):void 0,c=this.it?new Array(t):void 0;for(o=0,s=Math.min(this.qe,t);o<s&&(this.ze[o]===e[o]||this.nt&&ln(this.tt,this.ze[o],e[o]));o++)this.nt&&ce(this.nt[o],e[o]);for(s=this.qe-1,u=t-1;s>=o&&u>=o&&(this.ze[s]===e[u]||this.nt&&ln(this.tt,this.ze[s],e[u]));s--,u--)g[u]=this.Je[s],p[u]=this.et[s],b&&(b[u]=this.nt[s]),c&&(c[u]=this.it[s]);for(h=new Map,a=new Array(u+1),r=u;r>=o;r--)l=e[r],d=this.tt?this.tt(l):l,n=h.get(d),a[r]=n===void 0?-1:n,h.set(d,r);for(n=o;n<=s;n++)l=this.ze[n],d=this.tt?this.tt(l):l,r=h.get(d),r!==void 0&&r!==-1?(g[r]=this.Je[n],p[r]=this.et[n],b&&(b[r]=this.nt[n]),c&&(c[r]=this.it[n]),r=a[r],h.set(d,r)):this.et[n].dispose();for(r=o;r<t;r++)r in g?(this.Je[r]=g[r],this.et[r]=p[r],b&&(this.nt[r]=b[r],ce(this.nt[r],e[r])),c&&(this.it[r]=c[r],ce(this.it[r],r))):this.Je[r]=se(this.et[r]=Ue(),i);this.Je=this.Je.slice(0,this.qe=t),this.ze=e.slice(0)}}),this.Je}function ln(e,t,n){return e?e(t)===e(n):!0}function Yt(e,t){if(typeof e=="function"&&!e.length){if(t?.doNotUnwrap)return e;do e=e();while(typeof e=="function"&&!e.length)}if(!(t?.skipNonRendered&&(e==null||e===!0||e===!1||e===""))){if(Array.isArray(e)){let n=[];return Bt(e,n,t)?()=>{let r=[];return Bt(n,r,{...t,doNotUnwrap:!1}),r}:n}return e}}function Bt(e,t=[],n){let r=null,i=!1;for(let o=0;o<e.length;o++)try{let s=e[o];if(typeof s=="function"&&!s.length){if(n?.doNotUnwrap){t.push(s),i=!0;continue}do s=s();while(typeof s=="function"&&!s.length)}Array.isArray(s)?i=Bt(s,t,n):n?.skipNonRendered&&(s==null||s===!0||s===!1||s==="")||t.push(s)}catch(s){if(!(s instanceof U))throw s;r=s}if(r)throw r;return i}function qn(e,t){const n=Symbol("");function r(i){return Gt(()=>(jr(r,i.value),Qt(()=>i.children)))}return r.id=n,r.defaultValue=e,r}function Mn(e){return Fr(e)}function Qt(e){const t=be(e,{lazy:!0}),n=be(()=>Yt(t()),{lazy:!0,sync:!0});return n.toArray=()=>{const r=n();return Array.isArray(r)?r:r!=null?[r]:[]},n}class Ee{static{for(const t of["all","allSettled","any","race","reject","resolve"])Ee[t]=()=>new Ee}catch(){return new Ee}then(){return new Ee}finally(){return new Ee}}const Q=(...e)=>be(...e),N=(...e)=>Vr(...e),ti=(...e)=>Gr(...e),vt=(...e)=>zr(...e);function x(e,t){return ee(()=>e(t||{}))}const ni=e=>`Stale read from <${e}>.`;function ot(e){const t="fallback"in e?{keyed:e.keyed,fallback:()=>e.fallback}:{keyed:e.keyed};return Xr(()=>e.each,e.children,t)}function Bn(e){const t=e.keyed,n=be(()=>e.when,void 0),r=t?n:be(n,{equals:(i,o)=>!i==!o,sync:!0});return be(()=>{const i=r();if(i){const o=e.children;return typeof o=="function"&&o.length>0?ee(t?()=>o(i):()=>o(()=>{if(!ee(r))throw ni("Show");return n()})):o}return e.fallback},{sync:!0})}const B=Symbol("slot"),ri={transparent:!0,sync:!0},ii={sync:!0},te=(e,t,n)=>ti(e,t,n?{transparent:!0,sync:!0,...n}:ri),j=e=>Q(()=>e(),ii);function oi(e,t,n,r){let i=n.length,o=t.length,s=i,u=0,l=0,d=t[o-1],h=d[B],a=d.parentNode===e&&(!h||h===r)?d.nextSibling:r||null,g=null,p,b;for(;u<o||l<s;){if(t[u]===n[l]){u++,l++;continue}for(;t[o-1]===n[s-1];)o--,s--;if(o===u){let c;if(s<i)if(l){const f=n[l-1],m=f[B];c=f.parentNode===e&&(!m||m===r)?f.nextSibling:a}else c=n[s-l];else c=a;for(;l<s;){const f=n[l++];e.insertBefore(f,c),r&&(f[B]=r)}}else if(s===l)for(;u<o;){const c=t[u++];if(!g||!g.has(c)){const f=c[B];c.parentNode===e&&(!f||f===r)&&c.remove()}}else if((p=t[u])===n[s-1]&&n[l]===t[o-1]&&p.parentNode===e&&(!(b=p[B])||b===r))if(r)do{const c=t[--o];if(e.insertBefore(c,p),c[B]=r,l++,u>=o-1||l>=s)break}while(t[u]===n[s-1]&&n[l]===t[o-1]);else do if(e.insertBefore(t[--o],p),l++,u>=o-1||l>=s)break;while(t[u]===n[s-1]&&n[l]===t[o-1]);else{if(!g){g=new Map;let f=l;for(;f<s;)g.set(n[f],f++)}const c=g.get(t[u]);if(c!=null)if(l<c&&c<s){let f=u,m=1,y;for(;++f<o&&f<s&&!((y=g.get(t[f]))==null||y!==c+m);)m++;if(m>c-l){const E=t[u],$=E[B],R=E.parentNode===e&&(!$||$===r)?E:a;for(;l<c;){const Y=n[l++];e.insertBefore(Y,R),r&&(Y[B]=r)}}else{const E=t[u++],$=n[l++],R=E[B];E.parentNode===e&&(!R||R===r)?e.replaceChild($,E):e.insertBefore($,a),r&&($[B]=r)}}else u++;else{const f=t[u++],m=f[B];f.parentNode===e&&(!m||m===r)&&f.remove()}}}}const un="_$DX_EVENT_OWNER",cn={},Ft=new Set,Pe=new Map;function si(e,t,n,r={}){let i;ui(t);try{Gt(o=>{if(i=o,t===document){const s=e();te(()=>Yt(s),()=>{})}else{const s=e();v(t,()=>s,t.firstChild?null:void 0,n,r.insertOptions)}},{id:r.renderId})}catch(o){throw i&&i(),an(t),o}return()=>{i(),an(t),t.textContent=""}}function li(e,t,n){const r=document.createElement("template");return r.innerHTML=e,n===2?r.content.firstChild.firstChild:r.content.firstChild}function P(e,t){let n;return i=>(n||(n=li(e,i,t))).cloneNode(!0)}function _t(e){for(let t=0,n=e.length;t<n;t++){const r=e[t];Ft.has(r)||(Ft.add(r),Pe.forEach((i,o)=>Fn(r,o,i)))}}function ui(e){const t=ci(e,e);t&&(t.roots=(t.roots||0)+1)}function an(e){const t=Pe.get(e);t&&(t.roots>1?t.roots--:delete t.roots),ai(e,e)}function ci(e,t=e){if(!e||!t)return;let n=Pe.get(e);return n||Pe.set(e,n={owners:new Map,handlers:new Map}),n.owners.set(t,(n.owners.get(t)||0)+1),Ft.forEach(r=>Fn(r,e,n)),n}function ai(e,t=e){const n=Pe.get(e);if(!n)return;const r=n.owners.get(t);r>1?n.owners.set(t,r-1):n.owners.delete(t),!n.owners.size&&(n.handlers.forEach((i,o)=>e.removeEventListener(o,i)),Pe.delete(e))}function Fn(e,t,n){if(n.handlers.has(e))return;const r=i=>hi(i,t,n);n.handlers.set(e,r),t.addEventListener(e,r)}function fi(e,t){let n=e,r=0;for(;n;){if(t.owners.has(n))return{owner:n,distance:r};r++,n=n._$host||n.parentNode||n.host}}function ge(e,t,n){n==null||n===!1?e.removeAttribute(t):e.setAttribute(t,n===!0?"":n)}function Ge(e,t,n){if(t==null||t===!1){n&&e.removeAttribute("class");return}if(typeof t=="string"){t!==n&&e.setAttribute("class",t);return}typeof n=="string"?(n={},e.removeAttribute("class")):n=dn(n||{}),t=dn(t);const r=Object.keys(t||{}),i=Object.keys(n);let o,s;for(o=0,s=i.length;o<s;o++){const u=i[o];!u||u==="undefined"||t[u]||e.classList.remove(u)}for(o=0,s=r.length;o<s;o++){const u=r[o],l=!!t[u];!u||u==="undefined"||n[u]===l||!l||e.classList.add(u)}}function fn(e,t,n,r){if(Array.isArray(n)){const i=n[0];e.addEventListener(t,n[0]=o=>i.call(e,n[1],o))}else e.addEventListener(t,n,typeof n!="function"&&n)}function di(e,t){Array.isArray(e)?e.flat(1/0).forEach(n=>n&&n(t)):e(t)}function ht(e,t){const n=ee(e);se(null,()=>di(n,t))}function v(e,t,n,r,i){const o=n!==void 0;if(o&&!r&&(r=[]),typeof t!="function"&&(t=Pt(t,r,o,!0),typeof t!="function"))return Tt(e,t,r,n);if(o&&r.length===0){const u=document.createTextNode("");e.insertBefore(u,n),r=[u]}let s=r;te(u=>{const l=Pt(t(),s,o,!0);return typeof l!="function"?l:(te(()=>Pt(l,s,o),d=>{Tt(e,d,s,n),s=d},u!==void 0&&!(i&&i.schedule)?{...i,schedule:!0}:i),cn)},u=>{u!==cn&&(Tt(e,u,s,n),s=u)},i)}function dn(e){if(Array.isArray(e)){const t={};jn(e,t),e=t}if(e&&typeof e=="object"){const t={},n=Object.keys(e);for(let r=0,i=n.length;r<i;r++){const o=n[r];if(!e[o])continue;const s=o.trim().split(/\s+/);for(let u=0,l=s.length;u<l;u++)s[u]&&(t[s[u]]=!0)}return t}return e}function jn(e,t){for(let n=0,r=e.length;n<r;n++){const i=e[n];Array.isArray(i)?jn(i,t):typeof i=="object"&&i!=null?Object.assign(t,i):(i||i===0)&&(t[i]=!0)}}function hi(e,t,n){if(e[un])return;const r=n&&(n.owners.size===1&&n.owners.has(t)?t:fi(e.target,n)?.owner);if(n&&!r)return;e[un]=r||!0;let i=e.target;const o=`$$${e.type}`,s=e.target,u=r||t||e.currentTarget,l=a=>Object.defineProperty(e,"target",{configurable:!0,value:a}),d=()=>{const a=i[o];if(a&&!i.disabled){const g=i[`${o}Data`];if(g!==void 0?a.call(i,g,e):a.call(i,e),e.cancelBubble)return}return i.host&&typeof i.host!="string"&&!i.host._$host&&i.contains(e.target)&&l(i.host),!0},h=()=>{for(;d()&&!(i===u||i.parentNode===u);)i=i._$host||i.parentNode||i.host};if(Object.defineProperty(e,"currentTarget",{configurable:!0,get(){return i||u||document}}),e.composedPath){const a=e.composedPath();if(a.length){l(a[0]);for(let g=0;g<a.length&&(i=a[g],!!d());g++){if(i._$host){i=i._$host,h();break}if(i===u||i.parentNode===u)break}}else h()}else h();l(s)}function Tt(e,t,n,r){if(t===n)return;const i=typeof t,o=r!==void 0;if(i==="string"||i==="number"){const s=typeof n;s==="string"||s==="number"?e.firstChild.data=t:e.textContent=t}else if(t===void 0)nt(e,n,r);else if(t.nodeType)Array.isArray(n)?nt(e,n,o?r:null,t):n&&n.nodeType?n.parentNode===e?e.replaceChild(t,n):e.appendChild(t):n&&e.firstChild?e.replaceChild(t,e.firstChild):e.appendChild(t),r&&(t[B]=r);else if(Array.isArray(t)){const s=n&&Array.isArray(n);t.length===0?nt(e,n,r):s?n.length===0?hn(e,t,r):oi(e,n,t,r):(n&&nt(e),hn(e,t))}}function Pt(e,t,n,r){if(e=Yt(e,{skipNonRendered:!0,doNotUnwrap:r}),r&&typeof e=="function")return e;if(n&&!Array.isArray(e)&&(e=[e??""]),Array.isArray(e))for(let i=0,o=e.length;i<o;i++){const s=e[i],u=t&&t[i],l=typeof s;(l==="string"||l==="number")&&(e[i]=u&&u.nodeType===3&&u.data===""+s?u:document.createTextNode(s))}return e}function hn(e,t,n=null){for(let r=0,i=t.length;r<i;r++){const o=t[r];e.insertBefore(o,n),n&&(o[B]=n)}}function nt(e,t,n,r){if(n===void 0)return e.textContent="";if(t.length){let i=!1;for(let o=t.length-1;o>=0;o--){const s=t[o];if(r!==s){const u=s[B],l=s.parentNode===e&&(!u||u===n);r&&!i&&!o?l?e.replaceChild(r,s):e.insertBefore(r,n):l&&s.remove()}else i=!0}}else r&&e.insertBefore(r,n);r&&n&&(r[B]=n)}const gi=!1;function pi(e,t,n,r={}){try{const i=si(e,t,n,{...r,insertOptions:{schedule:!0}});return Te(),i}finally{}}function Un(){let e=new Set;function t(i){return e.add(i),()=>e.delete(i)}let n=!1;function r(i,o){if(n)return!(n=!1);const s={to:i,options:o,defaultPrevented:!1,preventDefault:()=>s.defaultPrevented=!0};for(const u of e)u.listener({...s,from:u.location,retry:l=>{l&&(n=!0),u.navigate(i,{...o,resolve:!1})}});return!s.defaultPrevented}return{subscribe:t,confirm:r}}let jt;function Xt(){(!window.history.state||window.history.state._depth==null)&&window.history.replaceState({...window.history.state,_depth:window.history.length-1},""),jt=window.history.state._depth}Xt();function mi(e){return{...e,_depth:window.history.state&&window.history.state._depth}}function yi(e,t){let n=!1;return()=>{const r=jt;Xt();const i=r==null?null:jt-r;if(n){n=!1;return}i&&t(i)?(n=!0,window.history.go(-i)):e()}}const wi=/^(?:[a-z0-9]+:)?\/\//i,bi=/^\/+|(\/)\/+$/g,Kn="http://sr";function Ke(e,t=!1){const n=e.replace(bi,"$1");return n?t||/^[?#]/.test(n)?n:"/"+n:""}function st(e,t,n){if(wi.test(t))return;const r=Ke(e),i=n&&Ke(n);let o="";return!i||t.startsWith("/")?o=r:i.toLowerCase().indexOf(r.toLowerCase())!==0?o=r+i:o=i,(o||"/")+Ke(t,!o)}function $i(e,t){if(e==null)throw new Error(t);return e}function vi(e,t){return Ke(e).replace(/\/*(\*.*)?$/g,"")+Ke(t)}function Wn(e){const t={};return e.searchParams.forEach((n,r)=>{r in t?Array.isArray(t[r])?t[r].push(n):t[r]=[t[r],n]:t[r]=n}),t}function _i(e,t,n){const[r,i]=e.split("/*",2),o=r.split("/").filter(Boolean),s=o.length;return u=>{const l=u.split("/").filter(Boolean),d=l.length-s;if(d<0||d>0&&i===void 0&&!t)return null;const h={path:s?"":"/",params:{}},a=g=>n===void 0?void 0:n[g];for(let g=0;g<s;g++){const p=o[g],b=p[0]===":",c=b?l[g]:l[g].toLowerCase(),f=b?p.slice(1):p.toLowerCase();if(b&&It(c,a(f)))h.params[f]=c;else if(b||!It(c,f))return null;h.path+=`/${c}`}if(i){const g=d?l.slice(-d).join("/"):"";if(It(g,a(i)))h.params[i]=g;else return null}return h}}function It(e,t){const n=r=>r===e;return t===void 0?!0:typeof t=="string"?n(t):typeof t=="function"?t(e):Array.isArray(t)?t.some(n):t instanceof RegExp?t.test(e):!1}function Si(e){const[t,n]=e.pattern.split("/*",2),r=t.split("/").filter(Boolean);return r.reduce((i,o)=>i+(o.startsWith(":")?2:3),r.length-(n===void 0?0:1))}function Vn(e){const t=new Map,n=Ze();return new Proxy({},{get(r,i){return t.has(i)||se(n,()=>t.set(i,Q(()=>e()[i]))),t.get(i)()},getOwnPropertyDescriptor(){return{enumerable:!0,configurable:!0}},ownKeys(){return Reflect.ownKeys(e())},has(r,i){return i in e()}})}function zn(e){let t=/(\/?\:[^\/]+)\?/.exec(e);if(!t)return[e];let n=e.slice(0,t.index),r=e.slice(t.index+t[0].length);const i=[n,n+=t[1]];for(;t=/^(\/\:[^\/]+)\?/.exec(r);)i.push(n+=t[1]),r=r.slice(t[0].length);return zn(r).reduce((o,s)=>[...o,...i.map(u=>u+s)],[])}const Ei=100,Gn=qn(),Jn=qn();function Ci(e){try{return Mn(e)}catch{return}}const Hn=()=>$i(Mn(Gn),"<A> and 'use' router primitives can be only used inside a Route."),Zn=()=>Hn().navigatorFactory(),Oi=()=>Hn().params;function Ai(e,t=""){const{component:n,preload:r,children:i,info:o}=e,s=!i||Array.isArray(i)&&!i.length,u={key:e,component:n,preload:r,info:o};return Yn(e.path).reduce((l,d)=>{for(const h of zn(d)){const a=vi(t,h);let g=s?a:a.split("/*",1)[0];g=g.split("/").map(p=>p.startsWith(":")||p.startsWith("*")?p:encodeURIComponent(p)).join("/"),l.push({...u,originalPath:d,pattern:g,matcher:_i(g,!s,e.matchFilters)})}return l},[])}function Ri(e,t=0){return{routes:e,score:Si(e[e.length-1])*1e4-t,matcher(n){const r=[];for(let i=e.length-1;i>=0;i--){const o=e[i],s=o.matcher(n);if(!s)return null;r.unshift({...s,route:o})}return r}}}function Yn(e){return Array.isArray(e)?e:[e]}function Qn(e,t="",n=[],r=[]){const i=Yn(e);for(let o=0,s=i.length;o<s;o++){const u=i[o];if(u&&typeof u=="object"){u.hasOwnProperty("path")||(u.path="");const l=Ai(u,t);for(const d of l){n.push(d);const h=Array.isArray(u.children)&&u.children.length===0;if(u.children&&!h)Qn(u.children,d.pattern,n,r);else{const a=Ri([...n],r.length);r.push(a)}n.pop()}}}return n.length?r:r.sort((o,s)=>s.score-o.score)}function kt(e,t){for(let n=0,r=e.length;n<r;n++){const i=e[n].matcher(t);if(i)return i}return[]}function Ti(e,t,n){const r=new URL(Kn),i=Q((h=r)=>{const a=e();try{return new URL(a,r)}catch{return console.error(`Invalid path ${a}`),h}},{equals:(h,a)=>h.href===a.href}),o=Q(()=>i().pathname),s=Q(()=>i().search),u=Q(()=>i().hash),l=()=>"",d=Q(()=>Wn(i()));return{get pathname(){return o()},get search(){return s()},get hash(){return u()},get state(){return t()},get key(){return l()},query:n?n(d):Vn(d)}}let ye;function Pi(){return ye}function Ii(e,t,n,r={}){const{signal:[i,o],utils:s={}}=e,u=s.parsePath||(T=>T),l=s.renderPath||(T=>T),d=s.beforeLeave||Un(),h=st("",r.base||""),a=ee(i);if(h===void 0)throw new Error(`${h} is not a valid base path`);h&&!a.value&&o({value:h,replace:!0,scroll:!1});const[g,p]=N(!1,{ownedWrite:!0}),[b,c]=N(void 0,{ownedWrite:!0});let f;const m=Q(()=>b()??i()),y=Ti(()=>m().value,()=>m().state,s.queryWrapper),E=[],$=N([],{ownedWrite:!0}),R=Q(()=>typeof r.transformUrl=="function"?kt(t(),r.transformUrl(y.pathname)):kt(t(),y.pathname)),Y=()=>{const T=R(),I={};for(let V=0;V<T.length;V++)Object.assign(I,T[V].params);return I},ae=s.paramsWrapper?s.paramsWrapper(Y,t):Vn(Y),oe={pattern:h,path:()=>h,outlet:()=>null,resolvePath(T){return st(h,T)}};return{base:oe,location:y,params:ae,isRouting:g,renderPath:l,parsePath:u,navigatorFactory:St,matches:R,beforeLeave:d,preloadRoute:Se,singleFlight:r.singleFlight===void 0?!0:r.singleFlight,submissions:$};function Ye(T,I,V){ee(()=>{if(typeof I=="number"){I&&(s.go?s.go(I):console.warn("Router integration does not support relative routing"));return}const fe=!I||I[0]==="?",{replace:pe,resolve:re,scroll:A,state:D}={replace:!1,resolve:!fe,scroll:!0,...V},L=re?T.resolvePath(I):st(fe&&y.pathname||"",I);if(L===void 0)throw new Error(`Path '${I}' is not a routable path`);if(E.length>=Ei)throw new Error("Too many redirects");const z=m();if((L!==z.value||D!==z.state)&&!gi){if(d.confirm(L,V)){E.push({value:z.value,replace:pe,scroll:A,state:z.state});const me={value:L,state:D};f===void 0&&(p(!0),Te()),ye="navigate",f=me,f===me&&(c({...f}),queueMicrotask(()=>{f===me&&(ye=void 0,Qe(f),c(void 0),p(!1),f=void 0)}))}}})}function St(T){return T=T||Ci(Jn)||oe,(I,V)=>Ye(T,I,V)}function Qe(T){const I=E[0];I&&(o({...T,replace:I.replace,scroll:I.scroll}),E.length=0)}function Se(T,I){const V=kt(t(),T.pathname),fe=ye;ye="preload";for(let pe in V){const{route:re,params:A}=V[pe];re.component&&re.component.preload&&re.component.preload();const{preload:D}=re;I&&D&&se(n(),()=>D({params:A,location:{pathname:T.pathname,search:T.search,hash:T.hash,query:Wn(T),state:null,key:""},intent:"preload"}))}ye=fe}}function ki(e,t,n,r){const{base:i,location:o,params:s}=e,{pattern:u,component:l,preload:d}=r().route,h=Q(()=>r().path);l&&l.preload&&l.preload();const a=d?d({params:s,location:o,intent:ye||"initial"}):void 0;return{parent:t,pattern:u,path:h,outlet:()=>l?x(l,{params:s,location:o,data:a,get children(){return n()}}):n(),resolvePath(p){return st(i.path(),p,h())}}}const xi=e=>function(n){const{base:r,singleFlight:i,transformUrl:o,root:s,rootPreload:u,routeChildren:l}=ee(()=>({base:n.base,singleFlight:n.singleFlight,transformUrl:n.transformUrl,root:n.root,rootPreload:n.rootPreload,routeChildren:n.children})),d=Qt(()=>l),h=Q(()=>Qn(d(),r||""));let a;const g=Ii(e,h,()=>a,{base:r,singleFlight:i,transformUrl:o});return e.create&&e.create(g),x(Gn,{value:g,get children(){return x(Li,{routerState:g,root:s,preload:u,get children(){return[j(()=>(a=Ze())&&null),x(Ni,{routerState:g,get branches(){return h()}})]}})}})};function Li(e){const t=e.routerState.location,n=e.routerState.params,r=Q(()=>e.preload&&ee(()=>{e.preload({params:n,location:t,intent:Pi()||"initial"})})),i=e.root;return i?x(i,{params:n,location:t,get data(){return r()},get children(){return e.children}}):e.children}function Ni(e){const t=[];let n,r;const i=Q(s=>{const u=e.routerState.matches(),l=r;let d=l&&u.length===l.length;const h=[];for(let a=0,g=u.length;a<g;a++){const p=l&&l[a],b=u[a];s&&p&&b.route.key===p.route.key?h[a]=s[a]:(d=!1,t[a]&&t[a](),Gt(c=>{t[a]=c,h[a]=ki(e.routerState,h[a-1]||e.routerState.base,gn(()=>i()?.[a+1]),()=>{const f=e.routerState.matches();return f[a]??f[0]})}))}return t.splice(u.length).forEach(a=>a()),s&&d?(r=u,s):(n=h[0],r=u,h)}),o=gn(()=>i()&&n);return j(o)}const gn=e=>()=>{const t=e();if(t)return x(Jn,{value:t,get children(){return t.outlet()}})},pn=e=>{const t=Qt(()=>e.children);return Qr(e,{get children(){return t()}})};function Di(e){let t=!1;const n=s=>typeof s=="string"?{value:s}:s,[r,i]=N(n(e.get()),{equals:(s,u)=>s.value===u.value&&s.state===u.state,ownedWrite:!0}),o=[r,s=>{!t&&e.set(s),i(s)}];return e.init&&Nn(e.init((s=e.get())=>{t=!0,o[1](n(s)),t=!1})),xi({signal:o,create:e.create,utils:e.utils})}function qi(e,t,n){return e.addEventListener(t,n),()=>e.removeEventListener(t,n)}function Mi(e,t){const n=e&&document.getElementById(e);n?n.scrollIntoView():t&&window.scrollTo(0,0)}const Bi=new Map;function Fi({preload:e=!0,explicitLinks:t=!1,actionBase:n="/_server",transformUrl:r}={}){return i=>{const o=i.base.path(),s=i.navigatorFactory(i.base);let u,l;function d(c){return c.namespaceURI==="http://www.w3.org/2000/svg"}function h(c){if(c.defaultPrevented||c.button!==0||c.metaKey||c.altKey||c.ctrlKey||c.shiftKey)return;const f=c.composedPath().find(Y=>Y instanceof Node&&Y.nodeName.toUpperCase()==="A");if(!f||t&&!f.hasAttribute("link"))return;const m=d(f),y=m?f.href.baseVal:f.href;if((m?f.target.baseVal:f.target)||!y&&!f.hasAttribute("state"))return;const $=(f.getAttribute("rel")||"").split(/\s+/);if(f.hasAttribute("download")||$&&$.includes("external"))return;const R=m?new URL(y,document.baseURI):new URL(y);if(!(R.origin!==window.location.origin||o&&R.pathname&&!R.pathname.toLowerCase().startsWith(o.toLowerCase())))return[f,R]}function a(c){const f=h(c);if(!f)return;const[m,y]=f,E=i.parsePath(y.pathname+y.search+y.hash),$=m.getAttribute("state");c.preventDefault(),s(E,{resolve:!1,replace:m.hasAttribute("replace"),scroll:!m.hasAttribute("noscroll"),state:$?JSON.parse($):void 0})}function g(c){const f=h(c);if(!f)return;const[m,y]=f;r&&(y.pathname=r(y.pathname)),i.preloadRoute(y,m.getAttribute("preload")!=="false")}function p(c){clearTimeout(u);const f=h(c);if(!f)return l=null;const[m,y]=f;l!==m&&(r&&(y.pathname=r(y.pathname)),u=setTimeout(()=>{i.preloadRoute(y,m.getAttribute("preload")!=="false"),l=m},20))}function b(c){if(c.defaultPrevented)return;let f=c.submitter&&c.submitter.hasAttribute("formaction")?c.submitter.getAttribute("formaction"):c.target.getAttribute("action");if(!f)return;if(!f.startsWith("https://action/")){const y=new URL(f,Kn);if(f=i.parsePath(y.pathname+y.search),!f.startsWith(n))return}if(c.target.method.toUpperCase()!=="POST")throw new Error("Only POST forms are supported for Actions");const m=Bi.get(f);if(m){c.preventDefault();const y=new FormData(c.target,c.submitter);m.call({r:i,f:c.target},c.target.enctype==="multipart/form-data"?y:new URLSearchParams(y))}}_t(["click","submit"]),document.addEventListener("click",a),e&&(document.addEventListener("mousemove",p,{passive:!0}),document.addEventListener("focusin",g,{passive:!0}),document.addEventListener("touchstart",g,{passive:!0})),document.addEventListener("submit",b),Nn(()=>{document.removeEventListener("click",a),e&&(document.removeEventListener("mousemove",p),document.removeEventListener("focusin",g),document.removeEventListener("touchstart",g)),document.removeEventListener("submit",b)})}}function ji(e){const t=()=>{const r=window.location.pathname.replace(/^\/+/,"/")+window.location.search,i=window.history.state&&window.history.state._depth&&Object.keys(window.history.state).length===1?void 0:window.history.state;return{value:r+window.location.hash,state:i}},n=Un();return Di({get:t,set({value:r,replace:i,scroll:o,state:s}){i?window.history.replaceState(mi(s),"",r):window.history.pushState(s,"",r),Mi(decodeURIComponent(window.location.hash.slice(1)),o),Xt()},init:r=>qi(window,"popstate",yi(r,i=>{if(i)return!n.confirm(i);{const o=t();return!n.confirm(o.value,{state:o.state})}})),create:Fi({preload:e.preload,explicitLinks:e.explicitLinks,actionBase:e.actionBase,transformUrl:e.transformUrl}),utils:{go:r=>window.history.go(r),beforeLeave:n}})(e)}var Ui=P("<div class=player>"),Ki=P("<div class=player-idle>Paste a URL to start watching together"),Wi=P("<div class=player-info><div class=player-meta><strong class=player-title></strong><span class=player-duration-row><span class=duration-label></span><span class=elapsed-label>"),Vi=P("<div class=player-media-wrap><div class=player-media></div><button type=button>"),zi=P("<img class=player-thumb alt>"),Gi=P("<video controls><track kind=captions>"),Ji=P("<audio controls><track kind=captions>");function Hi(e){const t=e.url.split(".").pop()?.toLowerCase();return t?["mp3","m4a","ogg","wav","flac","aac"].includes(t)?!1:(["mp4","webm","mov","mkv","avi","m4v"].includes(t),!0):!0}function xt(e,t){const n=(Date.now()-t)/1e3;e.currentTime=n}function Zi(e){let t;const[n,r]=N(!0),i=e.roomId,o=h=>{t=h??void 0};let s=null;vt(()=>{const h=e.track;return s=h,h?.started_at?h.id:null},(h,a)=>{if(h===a)return;const g=s,p=t;if(!p||!g?.started_at){p&&(p.pause(),p.src=""),r(!0);return}const b=g.started_at,c=`/api/v1/rooms/${i}/media/${g.hash}`;p.src=c;let f;const m=()=>{xt(p,b);const E=()=>{r(!0),p.play().catch(()=>{}),f=()=>{const $=s?.started_at;if(!$)return;const R=(Date.now()-$)/1e3;r(Math.abs(p.currentTime-R)<.5)},p.addEventListener("timeupdate",f),p.addEventListener("pause",y)};p.addEventListener("seeked",E,{once:!0})};p.addEventListener("loadedmetadata",m,{once:!0}),p.addEventListener("canplay",m,{once:!0});const y=()=>{p.ended||xt(p,b)};return()=>{p.removeEventListener("loadedmetadata",m),p.removeEventListener("canplay",m),f&&p.removeEventListener("timeupdate",f),p.removeEventListener("pause",y)}});const u=h=>{if(h<1)return"--:--";const a=Math.floor(h/1e3),g=Math.floor(a/60),p=a%60;return`${g}:${p.toString().padStart(2,"0")}`},l=()=>{const h=t,a=e.track?.started_at;h&&a&&(xt(h,a),r(!0))};var d=Ui();return v(d,x(Bn,{get when(){return e.track},get fallback(){return Ki()},children:h=>{const a=h();return[(()=>{var g=Wi(),p=g.firstChild,b=p.firstChild,c=b.nextSibling,f=c.firstChild,m=f.nextSibling;return v(g,(()=>{var y=j(()=>!!a.thumbnail);return()=>y()&&(()=>{var E=zi();return E.addEventListener("error",$=>$.currentTarget.hidden=!0),te(()=>a.thumbnail,$=>{ge(E,"src",$)}),E})()})(),p),v(b,()=>a.title),v(f,()=>a.duration),v(m,()=>u(Date.now()-a.started_at)),g})(),(()=>{var g=Vi(),p=g.firstChild,b=p.nextSibling;return v(p,(()=>{var c=j(()=>!!Hi(a));return()=>c()?(()=>{var f=Gi();return fn(f,"ended",e.onEnded),ht(()=>o,f),f})():(()=>{var f=Ji();return fn(f,"ended",e.onEnded),ht(()=>o,f),f})()})()),b.$$click=l,v(b,()=>n()?"Live":"Go live"),te(()=>["live-btn",n()&&"live-btn--on"],(c,f)=>{Ge(b,c,f)}),g})()]}})),d}_t(["click"]);var Yi=P('<div class=chat><div class=chat-messages></div><div class=chat-input><input type=text placeholder="Type a message..."><button type=button>Send'),Qi=P("<span class=chat-user>"),Xi=P("<span class=chat-time>"),eo=P("<div><div class=chat-content>");function to(e){let t,n;vt(()=>e.messages.length,()=>{n&&n.lastElementChild&&n.lastElementChild.scrollIntoView({behavior:"instant"})});const[r,i]=N(""),o=()=>{const c=r().trim();c&&(e.onSend(c),i(""),t?.focus())},s=c=>{c.key==="Enter"&&!c.shiftKey&&(c.preventDefault(),o())},u=c=>{try{const f=new Date(c),m=Math.floor((Date.now()-f.getTime())/1e3);if(m<60)return"just now";const y=Math.floor(m/60);if(y<60)return`${y}m ago`;const E=Math.floor(y/60);return E<24?`${E}h ago`:f.toLocaleDateString([],{month:"short",day:"numeric"})}catch{return c}};var l=Yi(),d=l.firstChild,h=d.nextSibling,a=h.firstChild,g=a.nextSibling,p=n;typeof p=="function"||Array.isArray(p)?ht(()=>p,d):n=d,v(d,x(ot,{get each(){return e.messages},children:c=>(()=>{var f=eo(),m=f.firstChild;return v(f,x(Bn,{get when(){return c.msg_type!=="system"},get children(){return[(()=>{var y=Qi();return v(y,()=>c.user_name),y})(),(()=>{var y=Xi();return v(y,()=>u(c.created_at)),y})()]}}),m),v(m,()=>c.content),te(()=>`chat-message${c.msg_type==="system"?" system":""}`,(y,E)=>{Ge(f,y,E)}),f})()})),a.$$keydown=s,a.$$input=c=>i(c.currentTarget.value);var b=t;return typeof b=="function"||Array.isArray(b)?ht(()=>b,a):t=a,g.$$click=o,te(()=>({e:r(),t:!r().trim()}),({e:c,t:f},m)=>{a.value=c??"",f!==m?.t&&ge(g,"disabled",f)}),l}_t(["input","keydown","click"]);var no=P("<div><span class=queue-title></span><span class=queue-duration>"),ro=P("<img class=queue-thumb alt>"),io=P("<div class=queue-panel><div class=queue-section><h3>Now Playing</h3></div><div class=queue-section><h3>Up Next (<!>)</h3></div><div class=queue-section><h3>Recently Played</h3></div><div class=queue-section><h3>Playlist (<!>)"),oo=P("<div class=queue-empty>Paste a URL to start watching together"),so=P("<div class=queue-empty>Tracks you add will appear here"),lo=P("<div class=queue-empty>Recently played tracks show up here"),uo=P("<div class=queue-empty>Add links to build a perpetual playlist");function rt(e){var t=no(),n=t.firstChild,r=n.nextSibling;return v(t,(()=>{var i=j(()=>!!e.thumbnail);return()=>i()&&(()=>{var o=ro();return o.addEventListener("error",s=>s.currentTarget.hidden=!0),te(()=>e.thumbnail,s=>{ge(o,"src",s)}),o})()})(),n),v(n,()=>e.pending?"⏳ ":"",null),v(n,()=>e.title,null),v(r,()=>e.duration),te(()=>`queue-item${e.extraClass?" "+e.extraClass:""}`,(i,o)=>{Ge(t,i,o)}),t}function co(e){var t=io(),n=t.firstChild;n.firstChild;var r=n.nextSibling,i=r.firstChild,o=i.firstChild,s=o.nextSibling;s.nextSibling;var u=r.nextSibling;u.firstChild;var l=u.nextSibling,d=l.firstChild,h=d.firstChild,a=h.nextSibling;return a.nextSibling,v(n,(()=>{var g=j(()=>!!e.currentTrack);return()=>g()?x(rt,{get title(){return e.currentTrack.title},get duration(){return e.currentTrack.duration},get thumbnail(){return e.currentTrack.thumbnail},extraClass:"current"}):oo()})(),null),v(i,()=>e.queue.length,s),v(r,x(ot,{get each(){return e.queue},children:g=>x(rt,{get title(){return g.title},get duration(){return g.duration},get thumbnail(){return g.thumbnail},get pending(){return g.pending}})}),null),v(r,(()=>{var g=j(()=>e.queue.length===0);return()=>g()&&so()})(),null),v(u,x(ot,{get each(){return e.history},children:g=>x(rt,{get title(){return g.title},get duration(){return g.duration},get thumbnail(){return g.thumbnail},extraClass:"history"})}),null),v(u,(()=>{var g=j(()=>e.history.length===0);return()=>g()&&lo()})(),null),v(d,()=>e.playlist.length,a),v(l,x(ot,{get each(){return e.playlist},children:g=>x(rt,{get title(){return g.title},get duration(){return g.duration},get thumbnail(){return g.thumbnail},get pending(){return g.pending}})}),null),v(l,(()=>{var g=j(()=>e.playlist.length===0);return()=>g()&&uo()})(),null),t}function ao(e){return!e||typeof e!="object"?null:"room_name"in e&&typeof e.room_name=="string"?{kind:"state",data:e}:"user_name"in e&&"content"in e?{kind:"chat",data:e}:null}function fo(e,t,n){const i=`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/api/v1/ws/${e}?name=${encodeURIComponent(t)}`;let o=new WebSocket(i),s=[],u=!1;function l(d){o?.readyState===WebSocket.OPEN?o.send(d):s.push(d)}return o.onopen=()=>{u=!0;for(const d of s)o?.send(d);s=[]},o.onmessage=d=>{if(typeof d.data=="string")try{const h=JSON.parse(d.data),a=ao(h);a?.kind==="state"?n.onState(a.data):a?.kind==="chat"&&n.onChat(a.data)}catch{}},o.onclose=d=>{o=null,u?d.code!==1e3&&d.code!==1001&&n.onError(`Connection lost (code: ${d.code})`):n.onError("Failed to connect to room"),n.onDisconnect()},o.onerror=()=>{o?.close()},d=>{l(JSON.stringify(d))}}function Xn([e,t],n={}){vt(()=>e(),r=>{if(!r||n.unless?.(r))return;const i=setTimeout(()=>t(null),n.ms??5e3);return()=>clearTimeout(i)})}function ho(e,t){const[n,r]=N(null),[i,o]=N([]),[s,u]=N(!1),[l,d]=N(null),[h,a]=N(null),g={current:!0},p=()=>{if(h())return;const c=fo(e,t,{onState:f=>{g.current=!1,r(f)},onChat:f=>o(m=>{const y=[...m,f];return y.length>200?y.slice(-200):y}),onDisconnect:()=>{u(!1),a(null)},onError:f=>{d(f)}});a(()=>c),u(!0),d(null)};return Hr(()=>{p(),setTimeout(()=>{g.current&&d("Room not found or unavailable")},8e3)}),vt(()=>[s(),h()],([c,f])=>{if(!c&&!f){const m=setTimeout(p,3e3);return()=>clearTimeout(m)}}),Xn([l,d],{unless:c=>c.includes("not found")}),{state:n,chats:i,connected:s,error:l,setError:d,send:c=>h()?.(c)}}const er="moqbox-theme";function tr(e){const t=document.documentElement;e==="latte"?(t.setAttribute("data-theme","latte"),t.style.colorScheme="light"):e==="macchiato"?(t.setAttribute("data-theme","macchiato"),t.style.colorScheme="dark"):(t.removeAttribute("data-theme"),t.style.colorScheme="light dark"),localStorage.setItem(er,e)}function go(e){return e==="macchiato"?"latte":e==="latte"?"system":"macchiato"}function nr(){return localStorage.getItem(er)||"system"}var po=P('<div class=home><h1>moqbox</h1><p>Watch videos together with friends</p><div class=home-form><input type=text placeholder="Your name"><button type=button></button></div><div class=home-join><input type=text placeholder="Room ID to join"><button type=button>Join Room'),mo=P('<div class="toast toast-error"role=alert>'),yo=P("<div class=room><header class=room-header><h2></h2><span class=client-count></span></header><div class=room-layout><div class=room-main><div class=room-actions><button type=button></button><button type=button class=secondary>Skip</button></div></div><div class=room-sidebar><div class=room-footer><button type=button class=theme-toggle-sm>"),wo=P("<span class=disconnected>Disconnected"),bo=P('<div class="toast toast-error"role=alert><span></span><button type=button class=toast-action>Go back'),$o=P('<div class=inline-form><input type=text placeholder="Paste a YouTube / media URL..."><div class=add-target-toggle><button type=button>Queue</button><button type=button>Playlist</button></div><button type=button>');const vo=()=>{const e=Zn(),[t,n]=N(""),[r,i]=N(!1),[o,s]=N(""),[u,l]=N(null);Xn([u,l]);const d=async()=>{if(t().trim()){i(!0);try{const $=await fetch("/api/v1/rooms",{method:"POST"});if(!$.ok)throw new Error(`create room: ${$.status}`);const R=await $.json();e(`/room/${R.room_id}?name=${encodeURIComponent(t().trim())}`)}catch($){console.error("Failed to create room:",$),l("Failed to create room"),i(!1)}}},h=()=>{const $=o().trim();$&&t().trim()&&e(`/room/${$}?name=${encodeURIComponent(t().trim())}`)};var a=po(),g=a.firstChild,p=g.nextSibling,b=p.nextSibling,c=b.firstChild,f=c.nextSibling,m=b.nextSibling,y=m.firstChild,E=y.nextSibling;return c.$$keydown=$=>$.key==="Enter"&&d(),c.$$input=$=>n($.currentTarget.value),f.$$click=d,v(f,()=>r()?"Creating...":"Create Room"),y.$$keydown=$=>$.key==="Enter"&&h(),y.$$input=$=>s($.currentTarget.value),E.$$click=h,v(a,(()=>{var $=j(()=>!!u());return()=>$()&&(()=>{var R=mo();return v(R,u),R})()})(),null),te(()=>({e:t(),t:r()||!t().trim(),a:o(),o:!t().trim()||!o().trim()}),({e:$,t:R,a:Y,o:ae},oe)=>{c.value=$??"",R!==oe?.t&&ge(f,"disabled",R),y.value=Y??"",ae!==oe?.o&&ge(E,"disabled",ae)}),a},_o=()=>{const e=Oi(),t=ee(()=>e.id),r=new URLSearchParams(location.search).get("name")||"anonymous",i=Zn(),[o,s]=N(nr()),u=()=>{const A=go(o());tr(A),s(A)},{state:l,chats:d,connected:h,error:a,setError:g,send:p}=ho(t,r),[b,c]=N(!1),[f,m]=N(""),[y,E]=N("queue"),[$,R]=N(!1),Y=async()=>{const A=f().trim();if(A){R(!0);try{const D=y()==="queue"?"/api/v1/ingest":"/api/v1/playlist/add",L=await fetch(D,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:A,room_id:t})});if(!L.ok){if(L.status===429){const z=L.headers.get("Retry-After");g(z?`Too fast — try again in ${z}s`:"Too fast — try again in a moment")}else{const z=await L.json().catch(()=>({error:L.statusText}));g(z.error||`${y()} failed`)}return}m(""),c(!1),g(null)}catch(D){g(String(D))}finally{R(!1)}}};var ae=yo(),oe=ae.firstChild,Ye=oe.firstChild,St=Ye.nextSibling,Qe=oe.nextSibling,Se=Qe.firstChild,T=Se.firstChild,I=T.firstChild,V=I.nextSibling,fe=Se.nextSibling,pe=fe.firstChild,re=pe.firstChild;return v(Ye,()=>l()?.room_name??"Loading..."),v(St,(()=>{var A=j(()=>l()?.clients!=null);return()=>A()?`${l()?.clients} connected`:""})()),v(oe,(()=>{var A=j(()=>!h());return()=>A()&&wo()})(),null),v(ae,(()=>{var A=j(()=>!!a());return()=>A()&&(()=>{var D=bo(),L=D.firstChild,z=L.nextSibling;return v(L,a),z.$$click=()=>i("/"),D})()})(),Qe),v(Se,x(Zi,{get track(){return l()?.current_track??null},roomId:t,onEnded:()=>{const A=l()?.current_track;A&&p({type:"track_ended",item_id:A.id})}}),T),I.$$click=()=>{c(!b())},v(I,()=>b()?"Cancel":"Add"),V.$$click=()=>p({type:"skip"}),v(Se,(()=>{var A=j(()=>!!b());return()=>A()&&(()=>{var D=$o(),L=D.firstChild,z=L.nextSibling,me=z.firstChild,en=me.nextSibling,Et=z.nextSibling;return L.$$keydown=xe=>xe.key==="Enter"&&Y(),L.$$input=xe=>m(xe.currentTarget.value),me.$$click=()=>E("queue"),en.$$click=()=>E("playlist"),Et.$$click=Y,v(Et,()=>$()?"Adding...":"Add"),te(()=>({e:f(),t:y()==="queue"?"active":"",a:y()==="playlist"?"active":"",o:$()||!f().trim()}),({e:xe,t:rr,a:ir,o:tn},Ct)=>{L.value=xe??"",Ge(me,rr,Ct?.t),Ge(en,ir,Ct?.a),tn!==Ct?.o&&ge(Et,"disabled",tn)}),D})()})(),null),v(fe,x(co,{get currentTrack(){return l()?.current_track??null},get queue(){return l()?.queue??[]},get history(){return l()?.history??[]},get playlist(){return l()?.playlist??[]}}),pe),v(fe,x(to,{get messages(){return d()},onSend:A=>p({type:"chat",content:A})}),pe),re.$$click=u,v(re,(()=>{var A=j(()=>o()==="macchiato");return()=>A()?"Dark":o()==="latte"?"Light":"System"})()),te(()=>({e:!l()?.current_track,t:`Theme: ${o()}`}),({e:A,t:D},L)=>{A!==L?.e&&ge(V,"disabled",A),D!==L?.t&&ge(re,"title",D)}),ae},So=()=>(tr(nr()),x(ji,{get children(){return[x(pn,{path:"/",component:vo}),x(pn,{path:"/room/:id",component:_o})]}}));_t(["input","keydown","click"]);const mn=document.getElementById("root");mn&&pi(()=>x(So,{}),mn);
+2 -2
static/dist/index.html
··· 10 10 if (t === "latte") { document.documentElement.style.colorScheme = "light"; } 11 11 else if (t === "macchiato") { document.documentElement.setAttribute("data-theme", "macchiato"); document.documentElement.style.colorScheme = "dark"; } 12 12 </script> 13 - <script type="module" crossorigin src="/assets/index-Sw8RlqPJ.js"></script> 14 - <link rel="stylesheet" crossorigin href="/assets/index-B_qULDRc.css"> 13 + <script type="module" crossorigin src="/assets/index-uQDzhXyk.js"></script> 14 + <link rel="stylesheet" crossorigin href="/assets/index-BZS9IGWz.css"> 15 15 </head> 16 16 <body> 17 17 <div id="root"></div>