Compare commits

...

8 Commits

31 changed files with 728 additions and 30 deletions

View File

@ -69,11 +69,13 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"sonner": "2.0.3", "sonner": "2.0.3",
"tailwind-merge": "3.2.0", "tailwind-merge": "3.2.0",
"three": "^0.160.0",
"tw-animate-css": "1.3.8", "tw-animate-css": "1.3.8",
"vaul": "1.1.2" "vaul": "1.1.2"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "4.1.12", "@tailwindcss/vite": "4.1.12",
"@types/three": "^0.160.0",
"@vitejs/plugin-react": "4.7.0", "@vitejs/plugin-react": "4.7.0",
"tailwindcss": "4.1.12", "tailwindcss": "4.1.12",
"vite": "^6.4.2" "vite": "^6.4.2"

View File

@ -10,6 +10,8 @@ import OtherPage from './components/OtherPage';
import ToolsPage from './components/ToolsPage'; 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 RealityOverrideAdminPage from './components/RealityOverrideAdminPage';
import logoSvg from '../imports/logo.svg'; import logoSvg from '../imports/logo.svg';
import { useSettings } from './settings'; import { useSettings } from './settings';
@ -35,7 +37,9 @@ type AppId =
| 'charset-editor' | 'charset-editor'
| 'petscii-editor' | 'petscii-editor'
| 'idle-animation' | 'idle-animation'
| 'loading-animation'; | 'loading-animation'
| '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');
@ -105,6 +109,8 @@ export default function App() {
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
<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="Override Admin" onClick={() => setCurrentPage('reality-override-admin')} />
</div> </div>
</div> </div>
</div> </div>
@ -140,8 +146,10 @@ export default function App() {
'sprite-editor': <AppPage title="Sprite Editor" onBack={() => setCurrentPage('apps')} />, 'sprite-editor': <AppPage title="Sprite Editor" onBack={() => setCurrentPage('apps')} />,
'charset-editor': <AppPage title="Character Set Editor" onBack={() => setCurrentPage('apps')} />, 'charset-editor': <AppPage title="Character Set Editor" onBack={() => setCurrentPage('apps')} />,
'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-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>
);
}

View File

@ -0,0 +1,403 @@
import { useEffect, useRef, useState } from 'react';
import { ChevronLeft, Loader2, Wifi, WifiOff } from 'lucide-react';
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
interface Msg { text: string; id: number; }
interface Star { x: number; y: number; sz: number; ph: number; spd: number; col: [number, number, number]; }
const STAR_PALETTE: [number, number, number][] = [
[255, 255, 255], [255, 240, 210], [210, 220, 255], [255, 210, 210], [200, 255, 230],
];
function mkStar(W: number, H: number): Star {
return {
x: Math.random() * W | 0,
y: Math.random() * H | 0,
sz: Math.random() < 0.8 ? 1 : Math.random() < 0.6 ? 2 : 3,
ph: Math.random() * Math.PI * 2,
spd: Math.random() * 0.04 + 0.005,
col: STAR_PALETTE[Math.random() * STAR_PALETTE.length | 0],
};
}
// ── Shaders (verbatim from public/vortex/script.js) ──────────────────────────
const VERT_SHELL = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const FRAG_SHELL = `
uniform float uTime;
uniform vec2 uGridSize;
uniform vec3 uColor;
uniform float uPulseSpeed;
uniform float uGap;
uniform float uOpacity;
varying vec2 vUv;
float random(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
void main() {
vec2 gridUV = vUv * uGridSize;
vec2 cellID = floor(gridUV);
vec2 cellUV = fract(gridUV);
float padding = uGap;
float square = step(padding, cellUV.x) * step(padding, cellUV.y)
* step(cellUV.x, 1.0 - padding) * step(cellUV.y, 1.0 - padding);
float rnd = random(cellID);
float pulse = 0.5 + 0.5 * sin(uTime * uPulseSpeed + rnd * 6.28);
float alpha = square * (0.05 + 0.95 * pulse) * uOpacity;
gl_FragColor = vec4(uColor, alpha);
}
`;
const VERT_TRAIL = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const FRAG_TRAIL = `
uniform float uTime;
uniform vec3 uColor;
uniform float uSpeed;
uniform float uSpeedMult;
uniform float uLength;
uniform float uLengthMult;
uniform float uOffset;
uniform float uDirection;
varying vec2 vUv;
void main() {
float finalSpeed = uSpeed * uSpeedMult;
float progress = fract(vUv.x * 2.0 - (uTime * finalSpeed * uDirection) + uOffset);
float finalLength = clamp(uLength * uLengthMult, 0.01, 0.99);
float alpha = 0.0;
if (uDirection > 0.0) {
alpha = smoothstep(1.0 - finalLength, 1.0, progress);
} else {
alpha = 1.0 - smoothstep(0.0, finalLength, progress);
}
alpha = pow(alpha, 3.0);
gl_FragColor = vec4(uColor, alpha);
}
`;
// ── Component ─────────────────────────────────────────────────────────────────
export default function RealityOverridePage({ onBack }: { onBack: () => void }) {
const containerRef = useRef<HTMLDivElement>(null);
const starCanvasRef = useRef<HTMLCanvasElement>(null);
const starsRef = useRef<Star[]>([]);
const wsRef = useRef<WebSocket | null>(null);
const msgIdRef = useRef(0);
const fadeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const pausedRef = useRef(localStorage.getItem('ro-bg') === 'off');
const lastTapRef = useRef(0);
const [wsStatus, setWsStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
const [currentCmd, setCurrentCmd] = useState<Msg | null>(null);
const [history, setHistory] = useState<Msg[]>([]);
const [bgVisible, setBgVisible] = useState(() => localStorage.getItem('ro-bg') !== 'off');
const toggleBg = () => {
pausedRef.current = !pausedRef.current;
const next = !pausedRef.current;
setBgVisible(next);
localStorage.setItem('ro-bg', next ? 'on' : 'off');
};
const handleTouchEnd = () => {
const now = Date.now();
if (now - lastTapRef.current < 300) { toggleBg(); lastTapRef.current = 0; }
else { lastTapRef.current = now; }
};
// ── Three.js vortex ─────────────────────────────────────────────────────────
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const W = container.clientWidth || window.innerWidth;
const H = container.clientHeight || window.innerHeight;
// Scene
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.12);
// Camera
const camera = new THREE.PerspectiveCamera(90, W / H, 0.01, 100);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(W, H);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.toneMapping = THREE.NoToneMapping;
container.appendChild(renderer.domElement);
// Post-processing
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(new THREE.Vector2(W, H), 1.5, 0.4, 0.85);
bloomPass.threshold = 0.1;
bloomPass.strength = 0.5;
bloomPass.radius = 0.6;
composer.addPass(bloomPass);
// Tokamak group
const tokamakGroup = new THREE.Group();
scene.add(tokamakGroup);
// Grid shell
const shellMaterial = new THREE.ShaderMaterial({
vertexShader: VERT_SHELL,
fragmentShader: FRAG_SHELL,
uniforms: {
uTime: { value: 0 },
uGridSize: { value: new THREE.Vector2(600, 300) },
uColor: { value: new THREE.Color('#3388ff') },
uPulseSpeed: { value: 4.89 },
uGap: { value: 0.23 },
uOpacity: { value: 1.0 },
},
transparent: true,
side: THREE.DoubleSide,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
tokamakGroup.add(new THREE.Mesh(new THREE.TorusGeometry(10, 3.0, 60, 200), shellMaterial));
// Trail lines
const trailsGroup = new THREE.Group();
tokamakGroup.add(trailsGroup);
const trailMaterials: THREE.ShaderMaterial[] = [];
for (let i = 0; i < 80; i++) {
const laneRadius = 7.5 + Math.random() * 5.0;
const mat = new THREE.ShaderMaterial({
vertexShader: VERT_TRAIL,
fragmentShader: FRAG_TRAIL,
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color('#38cdff') },
uSpeed: { value: 0.1 + Math.random() * 0.3 },
uSpeedMult: { value: 1.0 },
uLength: { value: 0.1 + Math.random() * 0.2 },
uLengthMult: { value: 0.47 },
uOffset: { value: Math.random() * 100.0 },
uDirection: { value: 1.0 },
},
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const mesh = new THREE.Mesh(new THREE.TorusGeometry(laneRadius, 0.005, 6, 120), mat);
mesh.rotation.x = (Math.random() - 0.5) * 0.1;
mesh.position.z = (Math.random() - 0.5) * 0.5;
trailsGroup.add(mesh);
trailMaterials.push(mat);
}
// Animation loop — automated camera flight (no OrbitControls needed)
const clock = new THREE.Clock();
let cameraAngle = 0;
let animId = 0;
const animate = () => {
animId = requestAnimationFrame(animate);
const delta = clock.getDelta();
const elapsed = clock.getElapsedTime();
cameraAngle += 0.3 * delta;
const r = 10;
camera.position.set(Math.cos(cameraAngle) * r, Math.sin(cameraAngle) * r, 0.5);
const la = cameraAngle + 0.1;
camera.lookAt(Math.cos(la) * r, Math.sin(la) * r, 0);
if (pausedRef.current) return;
shellMaterial.uniforms.uTime.value = elapsed;
trailMaterials.forEach(m => { m.uniforms.uTime.value = elapsed; });
composer.render();
};
animate();
// Resize observer keeps canvas filling its container
const ro = new ResizeObserver(() => {
const w = container.clientWidth;
const h = container.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
composer.setSize(w, h);
});
ro.observe(container);
return () => {
cancelAnimationFrame(animId);
ro.disconnect();
renderer.dispose();
if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement);
};
}, []);
// ── Star field (always running, visible under Three.js when vortex is hidden) ─
useEffect(() => {
const canvas = starCanvasRef.current;
if (!canvas) return;
let animId = 0;
let prevW = 0, prevH = 0;
const loop = () => {
const W = canvas.clientWidth, H = canvas.clientHeight;
if (!W || !H) { animId = requestAnimationFrame(loop); return; }
if (W !== prevW || H !== prevH) {
canvas.width = W; canvas.height = H;
prevW = W; prevH = H;
starsRef.current = Array.from({ length: 200 }, () => mkStar(W, H));
}
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = '#000010';
ctx.fillRect(0, 0, W, H);
for (const st of starsRef.current) {
st.ph += st.spd;
const br = Math.sin(st.ph) * 0.4 + 0.6;
const [r, g, b] = st.col;
ctx.fillStyle = `rgba(${r},${g},${b},${br})`;
ctx.fillRect(st.x, st.y, st.sz, st.sz);
if (st.sz >= 3 && br > 0.85) {
ctx.fillStyle = `rgba(${r},${g},${b},${br * 0.35})`;
ctx.fillRect(st.x - 2, st.y + 1, st.sz + 4, 1);
ctx.fillRect(st.x + 1, st.y - 2, 1, st.sz + 4);
}
}
animId = requestAnimationFrame(loop);
};
animId = requestAnimationFrame(loop);
return () => cancelAnimationFrame(animId);
}, []);
// ── 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.onmessage = (e) => {
if (cancelled) return;
const id = msgIdRef.current++;
const msg: Msg = { text: String(e.data), id };
setCurrentCmd(msg);
setHistory(prev => [...prev.slice(-9), msg]);
clearTimeout(fadeTimer.current);
fadeTimer.current = setTimeout(() => setCurrentCmd(null), 5000);
};
ws.onclose = () => { if (!cancelled) { setWsStatus('disconnected'); setTimeout(connect, 3000); } };
ws.onerror = () => ws?.close();
};
connect();
return () => { cancelled = true; ws?.close(); };
}, []);
return (
<div
className="relative w-full h-full bg-black overflow-hidden"
onDoubleClick={toggleBg}
onTouchEnd={handleTouchEnd}
>
{/* Star field — always visible beneath the vortex */}
<canvas ref={starCanvasRef} className="absolute inset-0 w-full h-full" style={{ imageRendering: 'pixelated' }} />
{/* Three.js vortex — fades out on double-click/tap */}
<div ref={containerRef} className={`absolute inset-0 transition-opacity duration-500 ${bgVisible ? 'opacity-100' : 'opacity-0'}`} />
{/* CSS keyframe for command flash */}
<style>{`
@keyframes cmd-flash {
0% { opacity: 0; transform: translate(-50%,-50%) scale(0.92); }
10% { opacity: 1; transform: translate(-50%,-50%) scale(1); }
70% { opacity: 1; transform: translate(-50%,-50%) scale(1); }
100% { opacity: 0; transform: translate(-50%,-50%) scale(1); }
}
.cmd-flash { animation: cmd-flash 5s ease-out forwards; }
`}</style>
{/* Back */}
<button
onClick={onBack}
className="absolute top-3 left-3 z-10 flex items-center gap-1 text-white/50 hover:text-white text-xs px-2 py-1 rounded bg-black/40 border border-white/10"
>
<ChevronLeft className="w-3.5 h-3.5" /> Back
</button>
{/* WS status */}
<div className="absolute top-3 right-3 z-10 flex items-center gap-1.5 text-xs px-2 py-1 rounded bg-black/40 border border-white/10">
{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">Reconnecting</span></>}
</div>
{/* Idle title — shown only before first message */}
{history.length === 0 && (
<div className="absolute inset-0 flex flex-col items-center justify-center z-10 pointer-events-none select-none">
<div
className="text-white/20 text-xl font-mono tracking-[0.3em] uppercase"
style={{ textShadow: '0 0 30px rgba(120,60,255,0.7)' }}
>
Reality Override
</div>
<div className="text-white/10 text-xs font-mono tracking-widest mt-2 uppercase">
Awaiting Commands<span className="animate-pulse">_</span>
</div>
</div>
)}
{/* Centered current command */}
{currentCmd && (
<div
key={currentCmd.id}
className="cmd-flash absolute z-10 pointer-events-none select-none text-center"
style={{ top: '50%', left: '50%', transform: 'translate(-50%,-50%)', maxWidth: '80%' }}
>
<div
className="font-mono text-2xl font-bold tracking-wide text-white break-words"
style={{ textShadow: '0 0 24px rgba(80,200,255,0.9), 0 0 60px rgba(80,200,255,0.4)' }}
>
{currentCmd.text}
</div>
</div>
)}
{/* History log */}
{history.length > 0 && (
<div className="absolute bottom-4 left-4 right-4 z-10">
<div className="bg-black/70 border border-green-900/40 rounded px-3 py-2 space-y-0.5">
{history.map(m => (
<div key={m.id} className="text-green-400 text-xs font-mono truncate">
<span className="text-green-800 mr-2">&gt;</span>{m.text}
</div>
))}
</div>
</div>
)}
</div>
);
}

View File

@ -54,4 +54,3 @@ export type { DownloadResult, NavigationEvent, PermissionCode, Permissions, Prog
export type { WebDAVUIOptions } from './ui/component.js'; export type { WebDAVUIOptions } from './ui/component.js';
export type { WopiApp } from './operations/wopi.js'; export type { WopiApp } from './operations/wopi.js';
export type { TextEditorDeps } from './ui/editor.js'; export type { TextEditorDeps } from './ui/editor.js';
//# sourceMappingURL=index.d.ts.map

View File

@ -50,4 +50,3 @@ export { TextEditor } from './ui/editor.js';
export { buildPageTemplate, buildDialogTemplate, buildParentRow, buildDirRow, buildFileRow, buildPasteWidget, renderEntry, } from './ui/templates.js'; export { buildPageTemplate, buildDialogTemplate, buildParentRow, buildDirRow, buildFileRow, buildPasteWidget, renderEntry, } from './ui/templates.js';
export { normalizeURL, joinURL, dirname, basename, parentCollectionURL, stripHostPrefix, } from './utils/url.js'; export { normalizeURL, joinURL, dirname, basename, parentCollectionURL, stripHostPrefix, } from './utils/url.js';
export { template, htmlEscape, formatBytes, formatDate, makeTranslate } from './utils/format.js'; export { template, htmlEscape, formatBytes, formatDate, makeTranslate } from './utils/format.js';
//# sourceMappingURL=index.js.map

View File

@ -113,4 +113,3 @@ export declare class WebDAVManager extends Emitter {
} }
/** Re-export commonly used types so consumers can `import { WebDAVManager, WebDAVClient } from 'webdav-component'`. */ /** Re-export commonly used types so consumers can `import { WebDAVManager, WebDAVClient } from 'webdav-component'`. */
export type { WebDAVClientOptions, WebDAVEntry, WebDAVListing, WebDAVManagerOptions } from '../types.js'; export type { WebDAVClientOptions, WebDAVEntry, WebDAVListing, WebDAVManagerOptions } from '../types.js';
//# sourceMappingURL=manager.d.ts.map

View File

@ -301,4 +301,3 @@ export class WebDAVManager extends Emitter {
return this.on(event, fn); return this.on(event, fn);
} }
} }
//# sourceMappingURL=manager.js.map

View File

@ -24,4 +24,3 @@ export declare class WopiRegistry {
/** True if the registry knows about any file types. */ /** True if the registry knows about any file types. */
get hasApps(): boolean; get hasApps(): boolean;
} }
//# sourceMappingURL=wopi.d.ts.map

View File

@ -75,4 +75,3 @@ export class WopiRegistry {
return Object.keys(this.byExt).length > 0 || Object.keys(this.byMime).length > 0; return Object.keys(this.byExt).length > 0 || Object.keys(this.byMime).length > 0;
} }
} }
//# sourceMappingURL=wopi.js.map

View File

@ -61,4 +61,3 @@ export declare class WebDAVClient {
/** HEAD request — returns true if the resource exists. */ /** HEAD request — returns true if the resource exists. */
exists(url: string): Promise<boolean>; exists(url: string): Promise<boolean>;
} }
//# sourceMappingURL=client.d.ts.map

View File

@ -135,4 +135,3 @@ export class WebDAVClient {
return r.status === 200; return r.status === 200;
} }
} }
//# sourceMappingURL=client.js.map

View File

@ -16,4 +16,3 @@ export interface ParseContext {
* collection). * collection).
*/ */
export declare function parsePropfindListing(xml: Document, ctx: ParseContext): WebDAVListing; export declare function parsePropfindListing(xml: Document, ctx: ParseContext): WebDAVListing;
//# sourceMappingURL=parse.d.ts.map

View File

@ -73,4 +73,3 @@ export function parsePropfindListing(xml, ctx) {
}); });
return files; return files;
} }
//# sourceMappingURL=parse.js.map

View File

@ -15,4 +15,3 @@ export declare function hasPermission(perms: Permissions | null, code: Permissio
* server doesn't return `oc:permissions` at all. * server doesn't return `oc:permissions` at all.
*/ */
export declare function hasPermissionOrDefault(perms: Permissions | null, code: PermissionCode, fallback: boolean): boolean; export declare function hasPermissionOrDefault(perms: Permissions | null, code: PermissionCode, fallback: boolean): boolean;
//# sourceMappingURL=permissions.d.ts.map

View File

@ -20,4 +20,3 @@ export function hasPermissionOrDefault(perms, code, fallback) {
} }
return perms.indexOf(code) !== -1; return perms.indexOf(code) !== -1;
} }
//# sourceMappingURL=permissions.js.map

View File

@ -100,4 +100,3 @@ export interface DownloadResult {
/** The underlying blob. */ /** The underlying blob. */
blob: Blob; blob: Blob;
} }
//# sourceMappingURL=types.d.ts.map

View File

@ -12,4 +12,3 @@
* Default UI: construct a `WebDAVUI` which composes a manager. * Default UI: construct a `WebDAVUI` which composes a manager.
*/ */
export {}; export {};
//# sourceMappingURL=types.js.map

View File

@ -100,4 +100,3 @@ export declare class WebDAVUI {
private hide; private hide;
private toggle; private toggle;
} }
//# sourceMappingURL=component.d.ts.map

View File

@ -758,4 +758,3 @@ function escapeHtml(s) {
.replace(/"/g, '&quot;') .replace(/"/g, '&quot;')
.replace(/'/g, '&#039;'); .replace(/'/g, '&#039;');
} }
//# sourceMappingURL=component.js.map

View File

@ -39,4 +39,3 @@ export declare class TextEditor {
dispose(): void; dispose(): void;
private handleClose; private handleClose;
} }
//# sourceMappingURL=editor.d.ts.map

View File

@ -114,4 +114,3 @@ export class TextEditor {
textEl.value = this.originalContent; textEl.value = this.originalContent;
} }
} }
//# sourceMappingURL=editor.js.map

View File

@ -20,4 +20,3 @@ export declare function renderEntry(entry: WebDAVEntry, ctx: TemplateContext, is
export declare function buildPasteWidget(t: (k: string) => string, count: number, action: 'copy' | 'move'): string; export declare function buildPasteWidget(t: (k: string) => string, count: number, action: 'copy' | 'move'): string;
/** Re-export the sort-order type so consumers don't need to import from types.ts. */ /** Re-export the sort-order type so consumers don't need to import from types.ts. */
export type { SortOrder }; export type { SortOrder };
//# sourceMappingURL=templates.d.ts.map

View File

@ -128,4 +128,3 @@ export function buildPasteWidget(t, count, action) {
label: action === 'copy' ? t('Copy here') : t('Move here'), label: action === 'copy' ? t('Copy here') : t('Move here'),
}); });
} }
//# sourceMappingURL=templates.js.map

View File

@ -15,4 +15,3 @@ export interface SendOptions {
* Returns the raw `Response` so callers can inspect status / stream body. * Returns the raw `Response` so callers can inspect status / stream body.
*/ */
export declare function sendRequest(fetchImpl: typeof fetch, options: SendOptions, defaultHeaders?: HeaderMap): Promise<Response>; export declare function sendRequest(fetchImpl: typeof fetch, options: SendOptions, defaultHeaders?: HeaderMap): Promise<Response>;
//# sourceMappingURL=fetch.d.ts.map

View File

@ -14,4 +14,3 @@ export function sendRequest(fetchImpl, options, defaultHeaders = {}) {
signal: options.signal, signal: options.signal,
}); });
} }
//# sourceMappingURL=fetch.js.map

View File

@ -11,4 +11,3 @@ export declare function formatBytes(bytes: number, unit?: string): string;
export declare function formatDate(date: Date | null): string; export declare function formatDate(date: Date | null): string;
/** A trivial i18n lookup. Missing keys fall back to the key. */ /** A trivial i18n lookup. Missing keys fall back to the key. */
export declare function makeTranslate(strings: Record<string, string> | undefined): (key: string) => string; export declare function makeTranslate(strings: Record<string, string> | undefined): (key: string) => string;
//# sourceMappingURL=format.d.ts.map

View File

@ -55,4 +55,3 @@ export function makeTranslate(strings) {
const dict = strings || {}; const dict = strings || {};
return (key) => (key in dict ? dict[key] : key); return (key) => (key in dict ? dict[key] : key);
} }
//# sourceMappingURL=format.js.map

View File

@ -32,4 +32,3 @@ export declare function parentCollectionURL(uri: string): string;
* include as a prefix on each entry. * include as a prefix on each entry.
*/ */
export declare function stripHostPrefix(name: string, host: string | null): string; export declare function stripHostPrefix(name: string, host: string | null): string;
//# sourceMappingURL=url.d.ts.map

View File

@ -60,4 +60,3 @@ export function stripHostPrefix(name, host) {
} }
return name; return name;
} }
//# sourceMappingURL=url.js.map

View File

@ -43,7 +43,10 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
import urllib.request, urllib.parse, urllib.error, urllib.parse import urllib.request, urllib.parse, urllib.error, urllib.parse
from time import timezone, strftime, localtime, gmtime from time import timezone, strftime, localtime, gmtime
import os, sys, re, shutil, uuid, hashlib, mimetypes, base64, socket import os, sys, re, shutil, struct, threading, uuid, hashlib, mimetypes, base64, socket
_ws_lock = threading.Lock()
_ws_clients: set = set() # connected WebSocket sockets
# Debug message ( True / False ) # Debug message ( True / False )
sys_debug = False sys_debug = False
@ -687,7 +690,92 @@ class DAVRequestHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
w.flush() w.flush()
def _ws_recv(self, sock, n):
data = b''
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
raise ConnectionError('closed')
data += chunk
return data
def ws_process_message(self, message: str) -> str:
"""Override this to handle incoming WebSocket messages. Return the response to broadcast."""
return message # echo by default
def _ws_broadcast(self, payload: bytes):
with _ws_lock:
clients = set(_ws_clients)
for sock in clients:
try:
self._ws_send(sock, payload)
except Exception:
pass
def _ws_send(self, sock, payload: bytes):
length = len(payload)
if length < 126:
header = bytes([0x81, length])
elif length < 65536:
header = bytes([0x81, 126]) + struct.pack('>H', length)
else:
header = bytes([0x81, 127]) + struct.pack('>Q', length)
sock.send(header + payload)
def _handle_websocket(self):
key = self.headers.get('Sec-WebSocket-Key', '')
accept = base64.b64encode(
hashlib.sha1((key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').encode()).digest()
).decode()
self.send_response(101, 'Switching Protocols')
self.send_header('Upgrade', 'websocket')
self.send_header('Connection', 'Upgrade')
self.send_header('Sec-WebSocket-Accept', accept)
self.end_headers()
client = f'{self.client_address[0]}:{self.client_address[1]}'
sock = self.connection
sock.settimeout(None)
with _ws_lock:
_ws_clients.add(sock)
print(f'[WS] {client} connected (total: {len(_ws_clients)})')
try:
while True:
b0, b1 = self._ws_recv(sock, 2)
opcode = b0 & 0x0F
masked = bool(b1 & 0x80)
length = b1 & 0x7F
if length == 126:
length = struct.unpack('>H', self._ws_recv(sock, 2))[0]
elif length == 127:
length = struct.unpack('>Q', self._ws_recv(sock, 8))[0]
mask = self._ws_recv(sock, 4) if masked else b''
payload = self._ws_recv(sock, length)
if masked:
payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload))
if opcode == 0x8: # close
print(f'[WS] {client} closed')
sock.send(b'\x88\x00')
break
elif opcode == 0x9: # ping → pong
sock.send(bytes([0x8A, len(payload)]) + payload)
elif opcode in (0x1, 0x2): # text or binary
message = payload.decode('utf-8', errors='replace')
print(f'[WS] {client} recv ({length}b): {message}')
response = self.ws_process_message(message)
if response is not None:
self._ws_broadcast(response.encode('utf-8'))
except Exception:
pass
finally:
with _ws_lock:
_ws_clients.discard(sock)
print(f'[WS] {client} disconnected (total: {len(_ws_clients)})')
def do_GET(self, onlyhead=False): def do_GET(self, onlyhead=False):
if (self.path == '/ws'
and self.headers.get('Upgrade', '').lower() == 'websocket'):
self._handle_websocket()
return
if self.WebAuth(): if self.WebAuth():
return return
path, elem = self.path_elem() path, elem = self.path_elem()