diff --git a/src/components/ava/script-tab.tsx b/src/components/ava/script-tab.tsx index 7c7c35e..4d076ea 100755 --- a/src/components/ava/script-tab.tsx +++ b/src/components/ava/script-tab.tsx @@ -2,14 +2,28 @@ // // Toolbar: Apply, Save (to the preset picked in the Presets tab, when a save // target is set), Save As…, Save as Default, Save to File, Load from File, -// New, Help. Editor: line-number gutter + monospace textarea (synced scroll, -// Tab inserts two spaces), error lines flagged in the gutter. Status line -// under the editor shows compile status or runtime errors. +// Script⇢Hash / Hash⇢Script (v7.7 — share the script as one short Unicode +// string and paste one back), New, Help. Editor: line-number gutter + +// monospace textarea (synced scroll, Tab inserts two spaces), error lines +// flagged in the gutter. Status line under the editor shows compile status +// or runtime errors. 'use client' import { useEffect, useMemo, useRef, useState } from 'react' -import { Check, CircleHelp, Copy, Download, FilePlus2, FolderOpen, Play, Save, X } from 'lucide-react' +import { + Check, + CircleHelp, + ClipboardPaste, + Copy, + Download, + FilePlus2, + FolderOpen, + Play, + Save, + Share2, + X, +} from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, @@ -29,6 +43,7 @@ import { } from '@/lib/store' import { applyScriptText } from '@/components/ava/audio-source' import { HelpPanel } from '@/components/ava/help-panel' +import { HashToScriptDialog, ScriptToHashDialog } from '@/components/ava/share-dialogs' import { sanitizeFileName } from '@/components/ava/shared' import { useToast } from '@/hooks/use-toast' import { cn } from '@/lib/utils' @@ -62,6 +77,9 @@ export function ScriptTab() { const [saveAsName, setSaveAsName] = useState('') const [saveAsBusy, setSaveAsBusy] = useState(false) const [saveAsError, setSaveAsError] = useState(null) + // v7.7 — share-string dialogs + const [shareOutOpen, setShareOutOpen] = useState(false) + const [shareInOpen, setShareInOpen] = useState(false) // debounced live validation (compile is cheap, scripts are small) useEffect(() => { @@ -332,6 +350,24 @@ export function ScriptTab() { > Load from File + + + ) + })} + + + {sessions?.unavailable && ( +

+ This browser context is not secure (https), so login cookies cannot be stored here. +

+ )} + +
+ - ) - })} -
- - {sessions?.unavailable && ( -

- This browser context is not secure (https), so login cookies cannot be stored here. -

- )} - -
- - {anyLoggedIn && ( - - )} -
- + + Log in + + {anyLoggedIn && ( + + )} + + + + )} @@ -355,14 +375,16 @@ export function SettingsTab() { - + {MUSIC_SERVICES_UI && ( + + )} ) } diff --git a/src/components/ava/share-dialogs.tsx b/src/components/ava/share-dialogs.tsx new file mode 100644 index 0000000..7025089 --- /dev/null +++ b/src/components/ava/share-dialogs.tsx @@ -0,0 +1,317 @@ +// AVA — Script ⇢ Hash share dialogs (v7.7). +// +// Two dialogs used by the Script tab toolbar: +// • ScriptToHashDialog — encodes the current editor script into ONE +// shareable Unicode string (see lib/ava/share-codec.ts for the format), +// shows it with a Copy button, live stats and the symbol legend. +// Compiling is NOT required — sharing a broken script to get it +// debugged is a feature. +// • HashToScriptDialog — pastes a share string back; on success it shows +// what came out (name, size, compile status) and loads the script into +// the editor exactly as it was, ready to Apply / Save As…. + +'use client' + +import { useMemo, useState } from 'react' +import { ArrowRightLeft, Check, ClipboardPaste, CircleHelp, FileCode2, Share2, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Textarea } from '@/components/ui/textarea' +import { decodeScriptHash, encodeScriptHash, SHARE_LEGEND } from '@/lib/ava/share-codec' +import { validateScript } from '@/lib/ava/script' +import { useToast } from '@/hooks/use-toast' +import { cn } from '@/lib/utils' + +/** Copy text to the clipboard with a legacy fallback. */ +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 + } + } +} + +// ── Script ⇢ Hash ───────────────────────────────────────────────────────── + +export function ScriptToHashDialog({ + open, + onOpenChange, + scriptText, +}: { + open: boolean + onOpenChange: (open: boolean) => void + scriptText: string +}) { + const { toast } = useToast() + const [copied, setCopied] = useState(false) + + const encoded = useMemo(() => encodeScriptHash(scriptText), [scriptText]) + const savedPct = Math.round((1 - encoded.hashLength / Math.max(1, encoded.scriptLength)) * 100) + const empty = scriptText.trim() === '' + + async function copy() { + const ok = await copyText(encoded.hash) + if (ok) { + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } else { + toast({ + title: 'Copy failed', + description: 'Select the string in the box and copy it manually (⌘/Ctrl+C).', + variant: 'destructive', + }) + } + } + + return ( + + + + + + Script ⇢ Hash + + + Your script as ONE shareable string — post it anywhere (chat, social, forums). Anyone + pastes it back with Hash⇢Script and gets your script exactly as it is now. + + + +
+