// OVE — Directory tab (v7.11): the community script browser. // // Lists every script published to the OVE-Directory (its own VPS, reached // through this server's backend proxy) with the name its author chose, its // type, its date, and its star rating. Visitors rate entries 1–5 (one vote // per script per visitor, enforced upstream), see the live average to the // right of the stars, sort the whole list by Name / Type / Date / Rating, // and "Get" any entry: the hash string is fetched, decoded back into the // exact script, and filed in this browser's preset tree under // "root/user created/OVE-Directory" -- from where it opens in the editor. 'use client' import { useCallback, useEffect, useState } from 'react' import { Check, CloudUpload, Copy, Download, FileCode2, Loader2, RefreshCw, Star, TriangleAlert, } from 'lucide-react' import { Button } from '@/components/ui/button' import { decodeScriptHash } from '@/lib/ava/share-codec' import { validateScript } from '@/lib/ava/script' import { useAvaStore } from '@/lib/store' import { useToast } from '@/hooks/use-toast' import { DEFAULT_ORDER, DEFAULT_SORT, DIR_SORTS, DOWNLOAD_FOLDER, fetchDirList, fetchDirScript, fetchDirStatus, formatDirDate, loadMyVotes, rateDirScript, rememberMyVote, saveDownloadedPreset, type DirMeta, type DirOrder, type DirSortKey, type DirStatus, } from '@/lib/ove-directory' import { cn } from '@/lib/utils' /** Copy text to the clipboard with a legacy fallback (same trick as the share dialogs). */ async function copyText(text: string): Promise { try { await navigator.clipboard.writeText(text) return true } catch { try { const ta = document.createElement('textarea') ta.value = text ta.style.position = 'fixed' ta.style.opacity = '0' document.body.appendChild(ta) ta.select() const ok = document.execCommand('copy') ta.remove() return ok } catch { return false } } } interface GotPanel { hash: string script: string compiles: boolean savedAs: string alreadyHad: boolean } // ── one directory row ───────────────────────────────────────────────────── function StarRow({ entry, myVote, busy, onVote, }: { entry: DirMeta myVote: number busy: boolean onVote: (stars: number) => void }) { const [hover, setHover] = useState(0) const active = hover || myVote const locked = myVote > 0 return ( setHover(0)} role="group" aria-label={`Rating for ${entry.name}`} > {[1, 2, 3, 4, 5].map((n) => { const filled = n <= active return ( ) })} ) } function DirRow({ entry, myVote, voting, getting, got, onVote, onGet, }: { entry: DirMeta myVote: number voting: boolean getting: boolean got: GotPanel | null onVote: (stars: number) => void onGet: () => void }) { const [copied, setCopied] = useState(false) async function copyHash() { if (!got) return const ok = await copyText(got.hash) if (ok) { setCopied(true) setTimeout(() => setCopied(false), 2000) } } function openInEditor() { if (!got) return useAvaStore.getState().setScriptText(got.script) useAvaStore.getState().setTab('script') } return (

{entry.name}

{entry.type}
Average: {entry.average.toFixed(1)} · {entry.votes} {entry.votes === 1 ? 'vote' : 'votes'} {getting ? ( ) : ( )}
{got && (

{got.alreadyHad ? ( <>You already had this exact script -- no duplicate was made. ) : ( <> Saved to your presets as {got.savedAs} in{' '} {DOWNLOAD_FOLDER}. )}

Share string (the stored hash):

{got.hash}

{got.compiles ? 'Decodes and compiles OK' : "Decodes, but doesn't compile (yet)"}

)}
) } // ── the tab ─────────────────────────────────────────────────────────────── export function DirectoryTab() { const { toast } = useToast() const [status, setStatus] = useState(null) const [scripts, setScripts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [sort, setSort] = useState(DEFAULT_SORT) const [order, setOrder] = useState(DEFAULT_ORDER) const [myVotes, setMyVotes] = useState>({}) const [voting, setVoting] = useState(null) const [getting, setGetting] = useState(null) const [got, setGot] = useState>({}) useEffect(() => { setMyVotes(loadMyVotes()) void (async () => { const s = await fetchDirStatus() setStatus(s) })() }, []) const reload = useCallback(async () => { setLoading(true) setError(null) try { const list = await fetchDirList(sort, order) setScripts(list) } catch (e) { setError(e instanceof Error ? e.message : 'The directory could not be listed.') } finally { setLoading(false) } }, [sort, order]) useEffect(() => { void reload() }, [reload]) const vote = useCallback( async (entry: DirMeta, stars: number) => { if (voting !== null) return setVoting(entry.id) try { const r = await rateDirScript(entry.id, stars) rememberMyVote(entry.id, stars) setMyVotes(loadMyVotes()) setScripts((prev) => prev.map((s) => (s.id === entry.id ? { ...s, average: r.average, votes: r.votes } : s)), ) if (r.duplicate) { toast({ title: 'Already rated', description: 'One vote per script per visitor -- the directory kept the first one.', }) } } catch (e) { const msg = e instanceof Error ? e.message : 'Rating failed.' if (msg.includes('no longer in the directory')) { setScripts((prev) => prev.filter((s) => s.id !== entry.id)) } toast({ title: 'Rating failed', description: msg, variant: 'destructive' }) } finally { setVoting(null) } }, [voting, toast], ) const get = useCallback( async (entry: DirMeta) => { if (getting !== null) return setGetting(entry.id) try { const full = await fetchDirScript(entry.id) const decoded = decodeScriptHash(full.hash) if (!decoded.ok) throw new Error('The stored share string did not decode -- it may be corrupted.') const v = validateScript(decoded.script) const save = saveDownloadedPreset(decoded.script, entry.name) if (!save.ok) throw new Error(save.error ?? 'Could not save to this browser.') setGot((prev) => ({ ...prev, [entry.id]: { hash: full.hash, script: decoded.script, compiles: v.ok, savedAs: save.preset?.name ?? entry.name, alreadyHad: Boolean(save.alreadyHad), }, })) toast({ title: save.alreadyHad ? 'Already in your presets' : 'Saved to your presets', description: `"${save.preset?.name ?? entry.name}" -- open it in the editor from the panel below, or hit Apply after switching to the Script tab.`, }) } catch (e) { toast({ title: 'Get failed', description: e instanceof Error ? e.message : 'Get failed.', variant: 'destructive', }) } finally { setGetting(null) } }, [getting, toast], ) const online = status?.status === 'ok' return (
{/* header: service status + refresh */}

OVE-Directory

{status === null ? 'connecting…' : online ? `${status.scripts_total} ${status.scripts_total === 1 ? 'script' : 'scripts'} · v${status.version}` : 'unreachable'}
{/* sort controls: Name / Type / Date / Rating + direction */}
{DIR_SORTS.map((s) => ( ))} {sort}/{order}
{/* list / states */} {error ? (

{error}

) : loading ? (
{[0, 1, 2, 3].map((i) => (
))}
) : scripts.length === 0 ? (

Nothing published yet.

Build a script in the Script tab, make sure it shows{' '} ✔ Compiled OK, then hit{' '} Upload to OVE-Directory -- published scripts appear here for everyone to browse, rate, and Get.

) : (
{scripts.map((entry) => ( void vote(entry, stars)} onGet={() => void get(entry)} /> ))}

{scripts.length} {scripts.length === 1 ? 'entry' : 'entries'} -- publish yours from the Script tab

)}
) }