feat(App): add Reality Override Admin page and integrate into app navigation

This commit is contained in:
Jaime Idolpx 2026-06-08 18:24:46 -04:00
parent 0df2b9cae5
commit 92009c1a63
2 changed files with 229 additions and 2 deletions

View File

@ -11,6 +11,7 @@ import ToolsPage from './components/ToolsPage';
import SearchOverlay from './components/SearchOverlay'; import SearchOverlay from './components/SearchOverlay';
import MediaManager from './components/MediaManager'; import MediaManager from './components/MediaManager';
import RealityOverridePage from './components/RealityOverridePage'; import RealityOverridePage from './components/RealityOverridePage';
import RealityOverrideAdminPage from './components/RealityOverrideAdminPage';
import logoSvg from '../imports/logo.svg'; import logoSvg from '../imports/logo.svg';
import { useSettings } from './settings'; import { useSettings } from './settings';
@ -37,7 +38,8 @@ type AppId =
| 'petscii-editor' | 'petscii-editor'
| 'idle-animation' | 'idle-animation'
| 'loading-animation' | 'loading-animation'
| 'reality-override'; | 'reality-override'
| 'reality-override-admin';
export default function App() { export default function App() {
const [currentPage, setCurrentPage] = useState<Page>('status'); const [currentPage, setCurrentPage] = useState<Page>('status');
@ -108,6 +110,7 @@ export default function App() {
<AppCard icon={<Activity className="w-7 h-7" />} label="Idle Animation" onClick={() => setCurrentPage('idle-animation')} /> <AppCard icon={<Activity className="w-7 h-7" />} label="Idle Animation" onClick={() => setCurrentPage('idle-animation')} />
<AppCard icon={<Loader2 className="w-7 h-7" />} label="Loading Animation" onClick={() => setCurrentPage('loading-animation')} /> <AppCard icon={<Loader2 className="w-7 h-7" />} label="Loading Animation" onClick={() => setCurrentPage('loading-animation')} />
<AppCard icon={<Wifi className="w-7 h-7" />} label="Reality Override" onClick={() => setCurrentPage('reality-override')} /> <AppCard icon={<Wifi className="w-7 h-7" />} label="Reality Override" onClick={() => setCurrentPage('reality-override')} />
<AppCard icon={<Wifi className="w-7 h-7" />} label="Override Admin" onClick={() => setCurrentPage('reality-override-admin')} />
</div> </div>
</div> </div>
</div> </div>
@ -145,7 +148,8 @@ export default function App() {
'petscii-editor': <AppPage title="Petscii Editor" onBack={() => setCurrentPage('apps')} />, 'petscii-editor': <AppPage title="Petscii Editor" onBack={() => setCurrentPage('apps')} />,
'idle-animation': <AppPage title="Idle Animation" onBack={() => setCurrentPage('apps')} />, 'idle-animation': <AppPage title="Idle Animation" onBack={() => setCurrentPage('apps')} />,
'loading-animation': <AppPage title="Loading Animation" onBack={() => setCurrentPage('apps')} />, 'loading-animation': <AppPage title="Loading Animation" onBack={() => setCurrentPage('apps')} />,
'reality-override': <RealityOverridePage onBack={() => setCurrentPage('apps')} /> 'reality-override': <RealityOverridePage onBack={() => setCurrentPage('apps')} />,
'reality-override-admin': <RealityOverrideAdminPage onBack={() => setCurrentPage('apps')} />
}; };
// AppCard component for app grid // AppCard component for app grid

View File

@ -0,0 +1,223 @@
import { useEffect, useRef, useState } from 'react';
import {
ChevronLeft, Loader2, Wifi, WifiOff, Send,
Image as ImageIcon, Film, Music,
Crop, RotateCcw, Minimize2,
Scissors, FileOutput, LayoutGrid,
Mic, Clapperboard, Wand2, FlipHorizontal,
} from 'lucide-react';
// ── Command definitions ───────────────────────────────────────────────────────
interface Cmd { id: string; label: string; icon: React.ReactNode; payload: string; }
interface Group { id: string; label: string; color: string; border: string; icon: React.ReactNode; commands: Cmd[]; }
const img = (cls = 'w-4 h-4') => <ImageIcon className={cls} />;
const aud = (cls = 'w-4 h-4') => <Music className={cls} />;
const vid = (cls = 'w-4 h-4') => <Film className={cls} />;
const GROUPS: Group[] = [
{
id: 'image', label: 'Image Conversion',
color: 'text-violet-400', border: 'border-violet-800/50',
icon: <ImageIcon className="w-5 h-5" />,
commands: [
{ id: 'img-jpg', label: '→ JPEG', icon: img(), payload: 'CONVERT IMAGE → JPEG' },
{ id: 'img-png', label: '→ PNG', icon: img(), payload: 'CONVERT IMAGE → PNG' },
{ id: 'img-webp', label: '→ WebP', icon: img(), payload: 'CONVERT IMAGE → WEBP' },
{ id: 'img-gif', label: '→ GIF', icon: img(), payload: 'CONVERT IMAGE → GIF' },
{ id: 'img-bmp', label: '→ BMP', icon: img(), payload: 'CONVERT IMAGE → BMP' },
{ id: 'img-avif', label: '→ AVIF', icon: img(), payload: 'CONVERT IMAGE → AVIF' },
{ id: 'img-resize', label: 'Resize', icon: <Minimize2 className="w-4 h-4" />, payload: 'IMAGE: RESIZE' },
{ id: 'img-crop', label: 'Crop', icon: <Crop className="w-4 h-4" />, payload: 'IMAGE: CROP' },
{ id: 'img-rotate', label: 'Rotate 90°', icon: <RotateCcw className="w-4 h-4" />, payload: 'IMAGE: ROTATE 90°' },
{ id: 'img-fliph', label: 'Flip H', icon: <FlipHorizontal className="w-4 h-4" />, payload: 'IMAGE: FLIP HORIZONTAL' },
{ id: 'img-magic', label: 'Auto Enhance', icon: <Wand2 className="w-4 h-4" />, payload: 'IMAGE: AUTO ENHANCE' },
],
},
{
id: 'audio', label: 'Audio Conversion',
color: 'text-teal-400', border: 'border-teal-800/50',
icon: <Music className="w-5 h-5" />,
commands: [
{ id: 'aud-mp3', label: '→ MP3', icon: aud(), payload: 'CONVERT AUDIO → MP3' },
{ id: 'aud-wav', label: '→ WAV', icon: aud(), payload: 'CONVERT AUDIO → WAV' },
{ id: 'aud-ogg', label: '→ OGG', icon: aud(), payload: 'CONVERT AUDIO → OGG' },
{ id: 'aud-flac', label: '→ FLAC', icon: aud(), payload: 'CONVERT AUDIO → FLAC' },
{ id: 'aud-aac', label: '→ AAC', icon: aud(), payload: 'CONVERT AUDIO → AAC' },
{ id: 'aud-opus', label: '→ Opus', icon: aud(), payload: 'CONVERT AUDIO → OPUS' },
{ id: 'aud-norm', label: 'Normalize', icon: <Mic className="w-4 h-4" />, payload: 'AUDIO: NORMALIZE' },
{ id: 'aud-extract', label: 'Extract from Video', icon: <Scissors className="w-4 h-4" />, payload: 'AUDIO: EXTRACT FROM VIDEO' },
],
},
{
id: 'video', label: 'Video Conversion',
color: 'text-rose-400', border: 'border-rose-800/50',
icon: <Film className="w-5 h-5" />,
commands: [
{ id: 'vid-mp4', label: '→ MP4', icon: vid(), payload: 'CONVERT VIDEO → MP4' },
{ id: 'vid-webm', label: '→ WebM', icon: vid(), payload: 'CONVERT VIDEO → WEBM' },
{ id: 'vid-avi', label: '→ AVI', icon: vid(), payload: 'CONVERT VIDEO → AVI' },
{ id: 'vid-mkv', label: '→ MKV', icon: vid(), payload: 'CONVERT VIDEO → MKV' },
{ id: 'vid-mov', label: '→ MOV', icon: vid(), payload: 'CONVERT VIDEO → MOV' },
{ id: 'vid-gif', label: '→ GIF', icon: vid(), payload: 'CONVERT VIDEO → GIF' },
{ id: 'vid-audio', label: 'Extract Audio', icon: <Music className="w-4 h-4" />, payload: 'VIDEO: EXTRACT AUDIO' },
{ id: 'vid-frames', label: 'Extract Frames', icon: <LayoutGrid className="w-4 h-4" />, payload: 'VIDEO: EXTRACT FRAMES' },
{ id: 'vid-clip', label: 'Trim Clip', icon: <Scissors className="w-4 h-4" />, payload: 'VIDEO: TRIM CLIP' },
{ id: 'vid-thumb', label: 'Thumbnail', icon: <Clapperboard className="w-4 h-4" />, payload: 'VIDEO: GENERATE THUMBNAIL' },
{ id: 'vid-export', label: 'Export Subtitles',icon: <FileOutput className="w-4 h-4" />, payload: 'VIDEO: EXPORT SUBTITLES' },
],
},
];
// ── Color maps keyed by group id ─────────────────────────────────────────────
const GROUP_BTN: Record<string, string> = {
image: 'border-violet-800/40 hover:border-violet-600/70 hover:bg-violet-900/20 active:bg-violet-800/30',
audio: 'border-teal-800/40 hover:border-teal-600/70 hover:bg-teal-900/20 active:bg-teal-800/30',
video: 'border-rose-800/40 hover:border-rose-600/70 hover:bg-rose-900/20 active:bg-rose-800/30',
};
const GROUP_ICON: Record<string, string> = {
image: 'text-violet-400', audio: 'text-teal-400', video: 'text-rose-400',
};
const GROUP_HEAD: Record<string, string> = {
image: 'text-violet-300 border-violet-800/50',
audio: 'text-teal-300 border-teal-800/50',
video: 'text-rose-300 border-rose-800/50',
};
// ── Component ─────────────────────────────────────────────────────────────────
export default function RealityOverrideAdminPage({ onBack }: { onBack: () => void }) {
const wsRef = useRef<WebSocket | null>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const [wsStatus, setWsStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
const [lastSent, setLastSent] = useState<string | null>(null);
const [flashId, setFlashId] = useState(0);
const [freeform, setFreeform] = useState('');
// ── WebSocket ───────────────────────────────────────────────────────────────
useEffect(() => {
let cancelled = false;
let ws: WebSocket | null = null;
const connect = () => {
if (cancelled) return;
ws = new WebSocket(`ws://${window.location.hostname}/ws`);
wsRef.current = ws;
setWsStatus('connecting');
ws.onopen = () => { if (!cancelled) setWsStatus('connected'); };
ws.onclose = () => { if (!cancelled) { setWsStatus('disconnected'); setTimeout(connect, 3000); } };
ws.onerror = () => ws?.close();
};
connect();
return () => { cancelled = true; ws?.close(); };
}, []);
// ── Send command ────────────────────────────────────────────────────────────
const send = (payload: string) => {
if (wsRef.current?.readyState !== WebSocket.OPEN) return;
wsRef.current.send(payload);
setLastSent(payload);
setFlashId(n => n + 1);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setLastSent(null), 3000);
};
return (
<div className="flex flex-col h-full bg-neutral-950 text-white">
{/* CSS for sent flash */}
<style>{`
@keyframes sent-flash {
0% { opacity: 0; transform: translateY(-6px); }
12% { opacity: 1; transform: translateY(0); }
70% { opacity: 1; }
100% { opacity: 0; }
}
.sent-flash { animation: sent-flash 3s ease-out forwards; }
`}</style>
{/* ── Header ── */}
<div className="flex-shrink-0 flex items-center gap-3 px-4 py-3 border-b border-neutral-800 bg-neutral-900">
<button
onClick={onBack}
className="flex items-center gap-1 text-neutral-400 hover:text-white text-sm"
>
<ChevronLeft className="w-4 h-4" /> Back
</button>
<span className="flex-1 font-mono text-sm tracking-widest text-neutral-300 uppercase">
Reality Override Admin
</span>
<div className="flex items-center gap-1.5 text-xs">
{wsStatus === 'connecting' && <><Loader2 className="w-3.5 h-3.5 text-yellow-400 animate-spin" /><span className="text-yellow-400">Connecting</span></>}
{wsStatus === 'connected' && <><Wifi className="w-3.5 h-3.5 text-green-400" /><span className="text-green-400">Connected</span></>}
{wsStatus === 'disconnected' && <><WifiOff className="w-3.5 h-3.5 text-red-400" /><span className="text-red-400">Offline</span></>}
</div>
</div>
{/* ── Freeform input ── */}
<div className="flex-shrink-0 flex gap-2 px-4 py-2 border-b border-neutral-800 bg-neutral-900">
<input
type="text"
value={freeform}
onChange={e => setFreeform(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && freeform.trim()) { send(freeform.trim()); setFreeform(''); } }}
placeholder="Send custom command…"
disabled={wsStatus !== 'connected'}
className="flex-1 bg-neutral-800 border border-neutral-700 rounded px-3 py-1.5 text-sm font-mono text-neutral-200 placeholder-neutral-600 focus:outline-none focus:border-neutral-500 disabled:opacity-40"
/>
<button
onClick={() => { if (freeform.trim()) { send(freeform.trim()); setFreeform(''); } }}
disabled={wsStatus !== 'connected' || !freeform.trim()}
className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-neutral-700 hover:bg-neutral-600 text-neutral-200 text-sm disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
>
<Send className="w-4 h-4" />
</button>
</div>
{/* ── Sent indicator ── */}
<div className="relative h-8 flex-shrink-0">
{lastSent && (
<div key={flashId} className="sent-flash absolute inset-x-0 top-1 flex justify-center pointer-events-none">
<span className="text-xs font-mono text-green-400 bg-green-950/60 border border-green-800/40 px-3 py-1 rounded">
Sent: {lastSent}
</span>
</div>
)}
</div>
{/* ── Command palette ── */}
<div className="flex-1 overflow-y-auto px-4 pb-6 space-y-6">
{GROUPS.map(group => (
<section key={group.id}>
<div className={`flex items-center gap-2 pb-2 mb-3 border-b ${GROUP_HEAD[group.id]}`}>
<span className={GROUP_ICON[group.id]}>{group.icon}</span>
<h2 className="font-mono text-sm tracking-wider uppercase">{group.label}</h2>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
{group.commands.map(cmd => (
<button
key={cmd.id}
onClick={() => send(cmd.payload)}
disabled={wsStatus !== 'connected'}
className={`
flex items-center gap-2 px-3 py-2.5 rounded border text-left text-sm
bg-neutral-900 text-neutral-300
transition-colors disabled:opacity-30 disabled:cursor-not-allowed
${GROUP_BTN[group.id]}
`}
>
<span className={`flex-shrink-0 ${GROUP_ICON[group.id]}`}>{cmd.icon}</span>
<span className="truncate font-mono text-xs">{cmd.label}</span>
</button>
))}
</div>
</section>
))}
</div>
</div>
);
}