Compare commits
8 Commits
e2197c33fd
...
b2c3580e17
| Author | SHA1 | Date | |
|---|---|---|---|
| b2c3580e17 | |||
| 53ed27e250 | |||
| e87aeb6726 | |||
| 50f4167a8b | |||
| 92009c1a63 | |||
| 0df2b9cae5 | |||
| ab5d9bb486 | |||
| ff664fa9d3 |
|
|
@ -69,11 +69,13 @@
|
|||
"remark-gfm": "^4.0.1",
|
||||
"sonner": "2.0.3",
|
||||
"tailwind-merge": "3.2.0",
|
||||
"three": "^0.160.0",
|
||||
"tw-animate-css": "1.3.8",
|
||||
"vaul": "1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.1.12",
|
||||
"@types/three": "^0.160.0",
|
||||
"@vitejs/plugin-react": "4.7.0",
|
||||
"tailwindcss": "4.1.12",
|
||||
"vite": "^6.4.2"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import OtherPage from './components/OtherPage';
|
|||
import ToolsPage from './components/ToolsPage';
|
||||
import SearchOverlay from './components/SearchOverlay';
|
||||
import MediaManager from './components/MediaManager';
|
||||
import RealityOverridePage from './components/RealityOverridePage';
|
||||
import RealityOverrideAdminPage from './components/RealityOverrideAdminPage';
|
||||
import logoSvg from '../imports/logo.svg';
|
||||
import { useSettings } from './settings';
|
||||
|
||||
|
|
@ -35,7 +37,9 @@ type AppId =
|
|||
| 'charset-editor'
|
||||
| 'petscii-editor'
|
||||
| 'idle-animation'
|
||||
| 'loading-animation';
|
||||
| 'loading-animation'
|
||||
| 'reality-override'
|
||||
| 'reality-override-admin';
|
||||
|
||||
export default function App() {
|
||||
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">
|
||||
<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={<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>
|
||||
|
|
@ -141,7 +147,9 @@ export default function App() {
|
|||
'charset-editor': <AppPage title="Character Set Editor" onBack={() => setCurrentPage('apps')} />,
|
||||
'petscii-editor': <AppPage title="Petscii Editor" 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
|
||||
|
|
|
|||
223
src/app/components/RealityOverrideAdminPage.tsx
Normal file
223
src/app/components/RealityOverrideAdminPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
403
src/app/components/RealityOverridePage.tsx
Normal file
403
src/app/components/RealityOverridePage.tsx
Normal 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">></span>{m.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -54,4 +54,3 @@ export type { DownloadResult, NavigationEvent, PermissionCode, Permissions, Prog
|
|||
export type { WebDAVUIOptions } from './ui/component.js';
|
||||
export type { WopiApp } from './operations/wopi.js';
|
||||
export type { TextEditorDeps } from './ui/editor.js';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
1
src/app/vendor/webdav-component/esm/index.js
vendored
1
src/app/vendor/webdav-component/esm/index.js
vendored
|
|
@ -50,4 +50,3 @@ export { TextEditor } from './ui/editor.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 { template, htmlEscape, formatBytes, formatDate, makeTranslate } from './utils/format.js';
|
||||
//# sourceMappingURL=index.js.map
|
||||
|
|
@ -113,4 +113,3 @@ export declare class WebDAVManager extends Emitter {
|
|||
}
|
||||
/** Re-export commonly used types so consumers can `import { WebDAVManager, WebDAVClient } from 'webdav-component'`. */
|
||||
export type { WebDAVClientOptions, WebDAVEntry, WebDAVListing, WebDAVManagerOptions } from '../types.js';
|
||||
//# sourceMappingURL=manager.d.ts.map
|
||||
|
|
@ -301,4 +301,3 @@ export class WebDAVManager extends Emitter {
|
|||
return this.on(event, fn);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=manager.js.map
|
||||
|
|
@ -24,4 +24,3 @@ export declare class WopiRegistry {
|
|||
/** True if the registry knows about any file types. */
|
||||
get hasApps(): boolean;
|
||||
}
|
||||
//# sourceMappingURL=wopi.d.ts.map
|
||||
|
|
@ -75,4 +75,3 @@ export class WopiRegistry {
|
|||
return Object.keys(this.byExt).length > 0 || Object.keys(this.byMime).length > 0;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=wopi.js.map
|
||||
|
|
@ -61,4 +61,3 @@ export declare class WebDAVClient {
|
|||
/** HEAD request — returns true if the resource exists. */
|
||||
exists(url: string): Promise<boolean>;
|
||||
}
|
||||
//# sourceMappingURL=client.d.ts.map
|
||||
|
|
@ -135,4 +135,3 @@ export class WebDAVClient {
|
|||
return r.status === 200;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=client.js.map
|
||||
|
|
@ -16,4 +16,3 @@ export interface ParseContext {
|
|||
* collection).
|
||||
*/
|
||||
export declare function parsePropfindListing(xml: Document, ctx: ParseContext): WebDAVListing;
|
||||
//# sourceMappingURL=parse.d.ts.map
|
||||
|
|
@ -73,4 +73,3 @@ export function parsePropfindListing(xml, ctx) {
|
|||
});
|
||||
return files;
|
||||
}
|
||||
//# sourceMappingURL=parse.js.map
|
||||
|
|
@ -15,4 +15,3 @@ export declare function hasPermission(perms: Permissions | null, code: Permissio
|
|||
* server doesn't return `oc:permissions` at all.
|
||||
*/
|
||||
export declare function hasPermissionOrDefault(perms: Permissions | null, code: PermissionCode, fallback: boolean): boolean;
|
||||
//# sourceMappingURL=permissions.d.ts.map
|
||||
|
|
@ -20,4 +20,3 @@ export function hasPermissionOrDefault(perms, code, fallback) {
|
|||
}
|
||||
return perms.indexOf(code) !== -1;
|
||||
}
|
||||
//# sourceMappingURL=permissions.js.map
|
||||
|
|
@ -100,4 +100,3 @@ export interface DownloadResult {
|
|||
/** The underlying blob. */
|
||||
blob: Blob;
|
||||
}
|
||||
//# sourceMappingURL=types.d.ts.map
|
||||
1
src/app/vendor/webdav-component/esm/types.js
vendored
1
src/app/vendor/webdav-component/esm/types.js
vendored
|
|
@ -12,4 +12,3 @@
|
|||
* Default UI: construct a `WebDAVUI` which composes a manager.
|
||||
*/
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
|
|
@ -100,4 +100,3 @@ export declare class WebDAVUI {
|
|||
private hide;
|
||||
private toggle;
|
||||
}
|
||||
//# sourceMappingURL=component.d.ts.map
|
||||
|
|
@ -758,4 +758,3 @@ function escapeHtml(s) {
|
|||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
//# sourceMappingURL=component.js.map
|
||||
|
|
@ -39,4 +39,3 @@ export declare class TextEditor {
|
|||
dispose(): void;
|
||||
private handleClose;
|
||||
}
|
||||
//# sourceMappingURL=editor.d.ts.map
|
||||
|
|
@ -114,4 +114,3 @@ export class TextEditor {
|
|||
textEl.value = this.originalContent;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=editor.js.map
|
||||
|
|
@ -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;
|
||||
/** Re-export the sort-order type so consumers don't need to import from types.ts. */
|
||||
export type { SortOrder };
|
||||
//# sourceMappingURL=templates.d.ts.map
|
||||
|
|
@ -128,4 +128,3 @@ export function buildPasteWidget(t, count, action) {
|
|||
label: action === 'copy' ? t('Copy here') : t('Move here'),
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=templates.js.map
|
||||
|
|
@ -15,4 +15,3 @@ export interface SendOptions {
|
|||
* Returns the raw `Response` so callers can inspect status / stream body.
|
||||
*/
|
||||
export declare function sendRequest(fetchImpl: typeof fetch, options: SendOptions, defaultHeaders?: HeaderMap): Promise<Response>;
|
||||
//# sourceMappingURL=fetch.d.ts.map
|
||||
|
|
@ -14,4 +14,3 @@ export function sendRequest(fetchImpl, options, defaultHeaders = {}) {
|
|||
signal: options.signal,
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=fetch.js.map
|
||||
|
|
@ -11,4 +11,3 @@ export declare function formatBytes(bytes: number, unit?: string): string;
|
|||
export declare function formatDate(date: Date | null): string;
|
||||
/** A trivial i18n lookup. Missing keys fall back to the key. */
|
||||
export declare function makeTranslate(strings: Record<string, string> | undefined): (key: string) => string;
|
||||
//# sourceMappingURL=format.d.ts.map
|
||||
|
|
@ -55,4 +55,3 @@ export function makeTranslate(strings) {
|
|||
const dict = strings || {};
|
||||
return (key) => (key in dict ? dict[key] : key);
|
||||
}
|
||||
//# sourceMappingURL=format.js.map
|
||||
|
|
@ -32,4 +32,3 @@ export declare function parentCollectionURL(uri: string): string;
|
|||
* include as a prefix on each entry.
|
||||
*/
|
||||
export declare function stripHostPrefix(name: string, host: string | null): string;
|
||||
//# sourceMappingURL=url.d.ts.map
|
||||
|
|
@ -60,4 +60,3 @@ export function stripHostPrefix(name, host) {
|
|||
}
|
||||
return name;
|
||||
}
|
||||
//# sourceMappingURL=url.js.map
|
||||
90
webdav3.py
90
webdav3.py
|
|
@ -43,7 +43,10 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
|||
from socketserver import ThreadingMixIn
|
||||
import urllib.request, urllib.parse, urllib.error, urllib.parse
|
||||
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 )
|
||||
sys_debug = False
|
||||
|
|
@ -687,7 +690,92 @@ class DAVRequestHandler(BaseHTTPRequestHandler):
|
|||
self.end_headers()
|
||||
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):
|
||||
if (self.path == '/ws'
|
||||
and self.headers.get('Upgrade', '').lower() == 'websocket'):
|
||||
self._handle_websocket()
|
||||
return
|
||||
if self.WebAuth():
|
||||
return
|
||||
path, elem = self.path_elem()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user