diff --git a/.claude/launch.json b/.claude/launch.json
index 20859bb..e6f1b40 100644
--- a/.claude/launch.json
+++ b/.claude/launch.json
@@ -5,7 +5,7 @@
"name": "static-buffet-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
- "autoPort": true
+ "port": 5001
}
]
}
diff --git a/PRD.md b/PRD.md
index cb75f2b..e2150fa 100644
--- a/PRD.md
+++ b/PRD.md
@@ -75,9 +75,10 @@ Ten themes, theme-specific title puns, TTS soundboards, triple-click easter eggs
### Non-goals (v2)
- **Not Resolume.** No layer stacks beyond A/B + overlay, no projection mapping, no DMX.
- **No user accounts, no cloud saves.** Sets are local JSON files/localStorage. (The dead passport/session stack gets deleted, not finished.)
-- **No mobile performance mode.** Desktop Chromium is the performance target; Firefox/Safari get graceful fallback for browsing/preview.
+- **No mobile performance mode.** Desktop Chromium is the performance target; Firefox/Safari get graceful fallback for browsing/preview. (iPad arrives later via its own shell — see §5.5.)
- **No generative-AI video in the core loop.** AI is a roadmap-tier assistant (curation, set-building), never a dependency for performing.
-- **No native NDI/Syphon.** Document the OBS-browser-source bridge instead.
+- **No native NDI/Syphon in the browser.** The OBS-browser-source bridge covers the tab; real Syphon/Spout/NDI output arrives with the Electron shell (§5.5, Phase 1.5).
+- **No full native rewrite.** Web tech end to end; platforms are shells around one portable engine (§5.5).
---
@@ -123,7 +124,21 @@ One WebGL2 compositor canvas; WebGPU later as a fast path, never a requirement.
- **Record my set**: `MediaRecorder` on the output stream → downloadable video. Every session becomes shareable content — this is how tools like this spread.
- **OBS bridge documented**: add the app as a browser source for streaming/NDI-world interop.
-### 5.5 The backend (slim to purpose)
+### 5.5 Platform strategy (browser → Electron → iPad)
+
+**The engine is the portable core; platforms are shells around it.** The WebGL compositor, ISF effects, and beat clock are identical work in a browser tab, an Electron window, or an iPad WebView — so "go native" and "build the engine" are sequential, not competing. The one architectural requirement this imposes *now*: the engine lives in a clean module with no DOM assumptions beyond "give me a canvas."
+
+- **Phase 1 builds web-first.** Fastest iteration loop, and the zero-install URL stays alive — research shows URL-shareable demos are how tools like this spread (Hydra, LumaDeck). The browser version remains the free front door permanently.
+- **Phase 1.5 wraps in Electron — specifically Electron, not Tauri.** Tauri uses the system WebView (WKWebView on macOS = Safari engine), which kills Web MIDI and weakens WebCodecs/`captureStream` — exactly the Chromium-first bets this PRD makes. Electron ships Chromium, so everything Phase 1 builds runs unchanged. The shell earns its existence by delivering only what a browser physically cannot:
+ - **Syphon/Spout/NDI output** via native Node modules — the table-stakes pro I/O feature.
+ - **The offline set cache, resurrected**: the Express server runs in-process with a real filesystem, so "download my whole set before the gig, play from local disk" becomes a gig-reliability feature no tab can offer. (The disk-cache/transcode services deleted in Phase 0 return here, deliberately, on a platform that supports them.)
+ - Multi-display fullscreen without permission prompts, MIDI without permission gates, app icon/dock identity.
+- **A full native rewrite (Swift/Metal) is rejected.** It discards the entire codebase for 6–12 months of solo re-implementation before reaching parity — the Resolume-envy trap named in §4.
+- **iPad comes through a third shell, not Electron** (Electron does not run on iPadOS): a Capacitor wrapper over the same engine core, App Store distributable, with a native CoreMIDI bridge plugin giving iPad better controller support than Safari alone. Sequenced after desktop, once the touch UI is worth designing on its own terms.
+
+One engine, three shells: browser (free front door), Electron (the pro instrument), Capacitor (the touch surface).
+
+### 5.6 The backend (slim to purpose)
- **Hot path only**: Archive.org search proxy + streaming proxy with rate limiting. That's the product.
- **Delete the Postgres/Drizzle layer.** Its only job is caching Archive.org responses — an in-memory LRU with TTL (or Vercel edge caching) does this without a database, a connection string, or migrations.
@@ -176,12 +191,18 @@ The cut list, one theme, fixed layout, slim server. App does *less*, feels drama
**Phase 1 — The Engine (3–5 weeks equiv.)**
WebGL2 compositor; deck A as texture; ISF rack with intensity contract; feedback pass; beat clock + default routings; crossfader with deck B; quantized triggering. `captureStream` output window + set recording. *This is the moment it stops feeling like 2003.*
+**Phase 1.5 — The Shell (1–2 weeks equiv.)**
+Electron wrapper around the finished engine: Syphon/Spout (NDI as a follow-on) output, in-process Express with the offline set cache, one-click projector fullscreen, auto-update, signed/notarized builds. The browser version keeps shipping from the same repo — the shell adds, never replaces. Wrapping waits for the engine deliberately: a native-feeling app that still plays CSS-filtered video is lipstick on the 2003.
+
**Phase 2 — The Instrument (2–4 weeks equiv.)**
MIDI learn + controller presets; cue points + chop; hover-scrub; set bundles; scene morphing (lerp between saved effect states over N beats — the one-knob contract makes every state lerp-able); Emergency Mix on the new engine.
**Phase 3 — The Amplifiers (ongoing)**
Text-to-set AI curation; vibe tagging; WebCodecs frame-accurate loops for short clips; WebGPU fast path; shot-detection chop; optional cloud restyling — each only if the core still holds 60 fps.
+**Phase 4 — The Touch Surface (when desktop is stable)**
+Capacitor shell for iPad over the same engine core: touch-first trigger UI, CoreMIDI bridge plugin, App Store distribution.
+
Each phase ends shippable. No long-lived rewrite branch — the compositor lands behind the existing player, then replaces it.
---
diff --git a/client/src/App.tsx b/client/src/App.tsx
index f61a7db..96416ef 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -3,11 +3,8 @@ import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
-import { EasterEgg } from "@/components/EasterEgg";
-import { useEasterEgg } from "@/hooks/use-easter-egg";
import { DragDropProvider } from "@/components/DragDropProvider";
import { KeyboardShortcuts } from "@/components/KeyboardShortcuts";
-import { AsciiModeIndicator } from "@/components/AsciiModeIndicator";
import { CommandPalette } from "@/components/CommandPalette";
import { useCommandPalette } from "@/hooks/use-command-palette";
import Home from "@/pages/home";
@@ -23,7 +20,6 @@ function Router() {
}
function App() {
- const { isEasterEggActive, closeEasterEgg } = useEasterEgg();
const { isOpen: isCommandPaletteOpen, close: closeCommandPalette } = useCommandPalette();
return (
@@ -32,9 +28,7 @@ function App() {
-
-
diff --git a/client/src/components/ActionButtons.tsx b/client/src/components/ActionButtons.tsx
index 96eafe7..0018e27 100644
--- a/client/src/components/ActionButtons.tsx
+++ b/client/src/components/ActionButtons.tsx
@@ -2,56 +2,28 @@ import React from 'react';
import { EmergencyMix } from '@/components/EmergencyMix';
import { LuckyDip } from '@/components/LuckyDip';
import { useStore } from '@/lib/store';
-
interface ActionButtonsProps {
- onLuckyDipResults?: (results: any) => void;
+ onLuckyDipResults?: (results: any) => void;
}
-
export function ActionButtons({ onLuckyDipResults }: ActionButtonsProps) {
- const { brandSkin } = useStore();
-
- const handleLuckyDipResults = (results: any) => {
- onLuckyDipResults?.(results);
- };
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'bg-slate-800/40 border-slate-400/30';
- case 'waffle':
- return 'bg-amber-900/40 border-yellow-400/30';
- case 'ebn':
- return 'bg-gray-900/60 border-lime-500/30';
- case 'ozzy':
- return 'bg-red-900/40 border-red-500/30';
- case 'hogan':
- return 'bg-yellow-900/40 border-yellow-400/30';
- case 'dx':
- return 'bg-pink-900/40 border-pink-500/30';
- case 'maxheadroom':
- return 'bg-gray-900/60 border-green-500/30';
- case 'mario':
- return 'bg-red-900/40 border-yellow-400/30';
- case 'dakota':
- return 'bg-gray-800/40 border-gray-400/30';
- case 'blondie':
- return 'bg-amber-900/40 border-amber-400/30';
- default:
- return 'bg-slate-800/40 border-slate-400/30';
- }
- };
-
- return (
-
+ const { brandSkin } = useStore();
+ const handleLuckyDipResults = (results: any) => {
+ onLuckyDipResults?.(results);
+ };
+ const getThemeClasses = () => {
+ {
+ return 'bg-gray-900/60 border-lime-500/30';
+ }
+ };
+ return (
- );
-}
\ No newline at end of file
+
);
+}
diff --git a/client/src/components/AnimatedTransitions.tsx b/client/src/components/AnimatedTransitions.tsx
index 3b2e2cb..09def14 100644
--- a/client/src/components/AnimatedTransitions.tsx
+++ b/client/src/components/AnimatedTransitions.tsx
@@ -1,254 +1,155 @@
-import React, { ReactNode } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
+import { ReactNode } from 'react';
-// Fade in/out transition
-export const FadeTransition = ({ children, className = "", delay = 0 }: {
- children: ReactNode;
- className?: string;
- delay?: number;
+// CSS-only transition shims. Same API as the old framer-motion versions,
+// but no animation-library dependency and no per-frame JS cost during playback.
+
+export const FadeTransition = ({ children, className = "", delay = 0 }: {
+ children: ReactNode;
+ className?: string;
+ delay?: number;
}) => (
-
{children}
-
+
);
-// Slide in from direction
-export const SlideTransition = ({
- children,
- direction = 'up',
- className = "",
- delay = 0
-}: {
- children: ReactNode;
- direction?: 'up' | 'down' | 'left' | 'right';
- className?: string;
- delay?: number;
+export const SlideTransition = ({
+ children,
+ direction = 'up',
+ className = "",
+ delay = 0
+}: {
+ children: ReactNode;
+ direction?: 'up' | 'down' | 'left' | 'right';
+ className?: string;
+ delay?: number;
}) => {
- const getInitialPosition = () => {
- switch (direction) {
- case 'up': return { y: 20 };
- case 'down': return { y: -20 };
- case 'left': return { x: 20 };
- case 'right': return { x: -20 };
- default: return { y: 20 };
- }
- };
+ const slideClass = {
+ up: 'slide-in-from-bottom-4',
+ down: 'slide-in-from-top-4',
+ left: 'slide-in-from-right-4',
+ right: 'slide-in-from-left-4',
+ }[direction];
return (
-
{children}
-
+
);
};
-// Scale animation for buttons and interactive elements
-export const ScaleTransition = ({
- children,
+export const ScaleTransition = ({
+ children,
className = "",
- hoverScale = 1.05,
- tapScale = 0.95
-}: {
- children: ReactNode;
+}: {
+ children: ReactNode;
className?: string;
hoverScale?: number;
tapScale?: number;
}) => (
-
+
{children}
-
+
);
-// Panel collapse/expand animation
-export const PanelTransition = ({
- children,
- isCollapsed,
- className = ""
-}: {
- children: ReactNode;
- isCollapsed: boolean;
- className?: string;
+export const PanelTransition = ({
+ children,
+ isCollapsed,
+ className = ""
+}: {
+ children: ReactNode;
+ isCollapsed: boolean;
+ className?: string;
}) => (
-
-
- {!isCollapsed && children}
-
-
+
+ {!isCollapsed && children}
+
);
-// Stagger animation for lists/grids
-export const StaggerContainer = ({
- children,
+export const StaggerContainer = ({
+ children,
className = "",
- staggerDelay = 0.05
-}: {
- children: ReactNode;
+}: {
+ children: ReactNode;
className?: string;
staggerDelay?: number;
}) => (
-
+
{children}
-
+
);
-export const StaggerItem = ({
- children,
- className = ""
-}: {
- children: ReactNode;
- className?: string;
+export const StaggerItem = ({
+ children,
+ className = ""
+}: {
+ children: ReactNode;
+ className?: string;
}) => (
-
+
{children}
-
+
);
-// Floating panel animation
-export const FloatingPanelTransition = ({
- children,
+export const FloatingPanelTransition = ({
+ children,
className = "",
- isDragging = false
-}: {
- children: ReactNode;
+ isDragging = false
+}: {
+ children: ReactNode;
className?: string;
isDragging?: boolean;
}) => (
-
+
{children}
-
+
);
-// Theme transition wrapper
-export const ThemeTransition = ({
- children,
+export const ThemeTransition = ({
+ children,
className = "",
- brandSkin
-}: {
- children: ReactNode;
+ brandSkin
+}: {
+ children: ReactNode;
className?: string;
brandSkin: string;
}) => (
-
+
{children}
-
+
);
-// Pulse animation for active states
-export const PulseTransition = ({
- children,
+export const PulseTransition = ({
+ children,
className = "",
- isActive = false
-}: {
- children: ReactNode;
+ isActive = false
+}: {
+ children: ReactNode;
className?: string;
isActive?: boolean;
}) => (
-
+
{children}
-
+
);
-// Loading skeleton animation
-export const SkeletonTransition = ({
- width = "100%",
- height = "1rem",
- className = ""
-}: {
- width?: string;
- height?: string;
- className?: string;
+export const SkeletonTransition = ({
+ width = "100%",
+ height = "1rem",
+ className = ""
+}: {
+ width?: string;
+ height?: string;
+ className?: string;
}) => (
-
-);
\ No newline at end of file
+);
diff --git a/client/src/components/AsciiModeIndicator.tsx b/client/src/components/AsciiModeIndicator.tsx
deleted file mode 100644
index 32eb0ac..0000000
--- a/client/src/components/AsciiModeIndicator.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useStore } from '@/lib/store';
-import { Terminal, X } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-
-export function AsciiModeIndicator() {
- const { isAsciiMode, setAsciiMode } = useStore();
-
- if (!isAsciiMode) return null;
-
- return (
-
-
- ASCII TERMINAL MODE
- setAsciiMode(false)}
- className="h-6 w-6 p-0 hover:bg-green-400 hover:text-black"
- title="Exit ASCII Mode (or press Esc)"
- >
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/BottomHUD.tsx b/client/src/components/BottomHUD.tsx
index d2e23c4..bb6aebb 100644
--- a/client/src/components/BottomHUD.tsx
+++ b/client/src/components/BottomHUD.tsx
@@ -2,195 +2,143 @@ import { useStore } from '@/lib/store';
import { Monitor, Clock, Maximize2, Wifi, WifiOff, Smartphone, Router } from 'lucide-react';
import { useState, useEffect } from 'react';
import { getThemeClasses } from '@/lib/theme-utils';
-
export function BottomHUD() {
- const { brandSkin } = useStore();
- const [fps, setFps] = useState(30);
- const [resolution, setResolution] = useState('1920x1080');
- const [currentTime, setCurrentTime] = useState(new Date());
- const [connectionType, setConnectionType] = useState('unknown');
- const [isOnline, setIsOnline] = useState(navigator.onLine);
-
- const themeClasses = getThemeClasses(brandSkin);
-
- // Real FPS monitoring
- useEffect(() => {
- let frameCount = 0;
- let lastTime = performance.now();
-
- const measureFPS = () => {
- frameCount++;
- const currentTime = performance.now();
-
- if (currentTime >= lastTime + 1000) {
- setFps(Math.round((frameCount * 1000) / (currentTime - lastTime)));
- frameCount = 0;
- lastTime = currentTime;
- }
-
- requestAnimationFrame(measureFPS);
+ const { brandSkin } = useStore();
+ const [fps, setFps] = useState(30);
+ const [resolution, setResolution] = useState('1920x1080');
+ const [currentTime, setCurrentTime] = useState(new Date());
+ const [connectionType, setConnectionType] = useState('unknown');
+ const [isOnline, setIsOnline] = useState(navigator.onLine);
+ const themeClasses = getThemeClasses(brandSkin);
+ // Real FPS monitoring
+ useEffect(() => {
+ let frameCount = 0;
+ let lastTime = performance.now();
+ const measureFPS = () => {
+ frameCount++;
+ const currentTime = performance.now();
+ if (currentTime >= lastTime + 1000) {
+ setFps(Math.round((frameCount * 1000) / (currentTime - lastTime)));
+ frameCount = 0;
+ lastTime = currentTime;
+ }
+ requestAnimationFrame(measureFPS);
+ };
+ requestAnimationFrame(measureFPS);
+ }, []);
+ // Real resolution monitoring
+ useEffect(() => {
+ const updateResolution = () => {
+ const width = window.innerWidth;
+ const height = window.innerHeight;
+ setResolution(`${width}x${height}`);
+ };
+ // Initial resolution
+ updateResolution();
+ // Update on window resize
+ window.addEventListener('resize', updateResolution);
+ return () => window.removeEventListener('resize', updateResolution);
+ }, []);
+ // Real connection monitoring
+ useEffect(() => {
+ const updateConnection = () => {
+ setIsOnline(navigator.onLine);
+ // Get connection info if available
+ const connection = (navigator as any).connection ||
+ (navigator as any).mozConnection ||
+ (navigator as any).webkitConnection;
+ if (connection) {
+ setConnectionType(connection.effectiveType || connection.type || 'unknown');
+ }
+ };
+ // Initial connection check
+ updateConnection();
+ // Listen for online/offline events
+ window.addEventListener('online', updateConnection);
+ window.addEventListener('offline', updateConnection);
+ // Listen for connection changes if supported
+ const connection = (navigator as any).connection;
+ if (connection) {
+ connection.addEventListener('change', updateConnection);
+ }
+ return () => {
+ window.removeEventListener('online', updateConnection);
+ window.removeEventListener('offline', updateConnection);
+ if (connection) {
+ connection.removeEventListener('change', updateConnection);
+ }
+ };
+ }, []);
+ // Real-time clock
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+ // Format time for display
+ const formatTime = (date: Date) => {
+ return date.toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ });
};
-
- requestAnimationFrame(measureFPS);
- }, []);
-
- // Real resolution monitoring
- useEffect(() => {
- const updateResolution = () => {
- const width = window.innerWidth;
- const height = window.innerHeight;
- setResolution(`${width}x${height}`);
+ // Get connection icon based on type and status
+ const getConnectionIcon = () => {
+ if (!isOnline)
+ return WifiOff;
+ switch (connectionType) {
+ case 'slow-2g':
+ case '2g':
+ case '3g':
+ return Smartphone;
+ case '4g':
+ case '5g':
+ return Smartphone;
+ case 'wifi':
+ return Wifi;
+ default:
+ return isOnline ? Wifi : WifiOff;
+ }
};
-
- // Initial resolution
- updateResolution();
-
- // Update on window resize
- window.addEventListener('resize', updateResolution);
- return () => window.removeEventListener('resize', updateResolution);
- }, []);
-
- // Real connection monitoring
- useEffect(() => {
- const updateConnection = () => {
- setIsOnline(navigator.onLine);
-
- // Get connection info if available
- const connection = (navigator as any).connection ||
- (navigator as any).mozConnection ||
- (navigator as any).webkitConnection;
-
- if (connection) {
- setConnectionType(connection.effectiveType || connection.type || 'unknown');
- }
+ const ConnectionIcon = getConnectionIcon();
+ // Get theme-specific darker background for contrast
+ const getPerformanceBarBg = () => {
+ {
+ return 'bg-gray-900';
+ }
};
-
- // Initial connection check
- updateConnection();
-
- // Listen for online/offline events
- window.addEventListener('online', updateConnection);
- window.addEventListener('offline', updateConnection);
-
- // Listen for connection changes if supported
- const connection = (navigator as any).connection;
- if (connection) {
- connection.addEventListener('change', updateConnection);
- }
-
- return () => {
- window.removeEventListener('online', updateConnection);
- window.removeEventListener('offline', updateConnection);
- if (connection) {
- connection.removeEventListener('change', updateConnection);
- }
- };
- }, []);
-
- // Real-time clock
- useEffect(() => {
- const interval = setInterval(() => {
- setCurrentTime(new Date());
- }, 1000);
-
- return () => clearInterval(interval);
- }, []);
-
- // Format time for display
- const formatTime = (date: Date) => {
- return date.toLocaleTimeString('en-US', {
- hour12: false,
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- });
- };
-
- // Get connection icon based on type and status
- const getConnectionIcon = () => {
- if (!isOnline) return WifiOff;
-
- switch (connectionType) {
- case 'slow-2g':
- case '2g':
- case '3g':
- return Smartphone;
- case '4g':
- case '5g':
- return Smartphone;
- case 'wifi':
- return Wifi;
- default:
- return isOnline ? Wifi : WifiOff;
- }
- };
-
- const ConnectionIcon = getConnectionIcon();
-
- // Get theme-specific darker background for contrast
- const getPerformanceBarBg = () => {
- switch (brandSkin) {
- case 'ebn':
- return 'bg-gray-900';
- case 'waffle':
- return 'bg-yellow-900';
- case 'ozzy':
- return 'bg-red-900';
- case 'hogan':
- return 'bg-gray-900';
- case 'dx':
- return 'bg-gray-900';
- case 'mario':
- return 'bg-red-900';
- case 'dakota':
- return 'bg-gray-900';
- case 'blondie':
- return 'bg-amber-900';
- case 'testcard':
- return 'bg-slate-900';
- default:
- return 'bg-gray-900';
- }
- };
-
- return (
-
+ return (
{/* Scanlines overlay for EBN theme only */}
- {brandSkin === 'ebn' && (
-
)}
{/* Performance indicators */}
{/* FPS */}
-
+
{fps} FPS
{/* Divider */}
-
+
{/* Resolution */}
-
+
{resolution}
@@ -199,21 +147,20 @@ export function BottomHUD() {
{/* System Clock */}
-
+
{formatTime(currentTime)}
{/* Divider */}
-
+
{/* Connection */}
-
+
-
- );
+
);
}
diff --git a/client/src/components/BrandSkinToggle.tsx b/client/src/components/BrandSkinToggle.tsx
deleted file mode 100644
index 349a667..0000000
--- a/client/src/components/BrandSkinToggle.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useStore } from '@/lib/store';
-import { Button } from '@/components/ui/button';
-
-export function BrandSkinToggle() {
- const { brandSkin, setBrandSkin } = useStore();
-
- const toggleSkin = () => {
- setBrandSkin(brandSkin === 'diner' ? 'ebn' : 'diner');
- };
-
- return (
-
- {brandSkin === 'diner' ? 'EBN Mode' : 'Diner Mode'}
-
- );
-}
diff --git a/client/src/components/CommandPalette.tsx b/client/src/components/CommandPalette.tsx
index acf5f0b..e8518cf 100644
--- a/client/src/components/CommandPalette.tsx
+++ b/client/src/components/CommandPalette.tsx
@@ -8,7 +8,6 @@ import { useToast } from '@/hooks/use-toast';
import { getVideoMetadata } from '@/lib/archive-api';
import { type BrandSkin } from '@/lib/types';
-const BRAND_SKINS: BrandSkin[] = ['testcard', 'waffle', 'ebn', 'ozzy', 'hogan', 'dx', 'maxheadroom', 'mario', 'dakota', 'blondie', 'diner'];
export interface CommandPaletteProps {
isOpen: boolean;
@@ -189,26 +188,6 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
group: 'Quick Search',
},
- // Theme & UI
- {
- id: 'toggle-theme',
- title: 'Cycle Brand Skin',
- description: 'Switch to the next theme',
- icon: ,
- keywords: ['theme', 'skin', 'brand', 'toggle', 'switch', 'cycle'],
- action: () => {
- const nextSkin = BRAND_SKINS[(BRAND_SKINS.indexOf(brandSkin) + 1) % BRAND_SKINS.length];
- setBrandSkin(nextSkin);
- onClose();
- toast({
- title: 'Theme changed',
- description: `Switched to ${nextSkin} theme`
- });
- },
- group: 'Appearance',
- badge: brandSkin,
- },
-
// Export & Tools
{
id: 'export-queue',
@@ -248,28 +227,6 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
group: 'Export',
badge: queueItems.length > 0 ? `${queueItems.length} items` : 'Empty',
},
- {
- id: 'cache-stats',
- title: 'View Cache Statistics',
- description: 'Check performance and cache hit rates',
- icon: ,
- keywords: ['cache', 'stats', 'performance', 'monitor'],
- action: async () => {
- try {
- const response = await fetch('/api/cache-stats');
- const stats = await response.json();
- console.log('Cache Statistics:', stats);
- toast({
- title: 'Cache Statistics',
- description: `Search: ${stats.searchCache?.memoryEntries || 0} in memory, Transcode: ${stats.transcode?.totalFiles || 0} files`
- });
- } catch (error) {
- toast({ title: 'Failed to fetch cache stats', variant: 'destructive' });
- }
- onClose();
- },
- group: 'Tools',
- },
{
id: 'emergency-mix',
title: 'Generate Emergency Mix',
diff --git a/client/src/components/CoreSoundboards.tsx b/client/src/components/CoreSoundboards.tsx
deleted file mode 100644
index df2c75e..0000000
--- a/client/src/components/CoreSoundboards.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
-import { Volume2 } from 'lucide-react';
-import { useStore } from '@/lib/store';
-// Simple TTS implementation
-const speakText = (text: string, voiceConfig: any) => {
- return new Promise((resolve) => {
- if ('speechSynthesis' in window) {
- const utterance = new SpeechSynthesisUtterance(text);
- utterance.rate = voiceConfig.rate || 1.0;
- utterance.pitch = voiceConfig.pitch || 1.0;
- utterance.onend = () => resolve();
- speechSynthesis.speak(utterance);
- } else {
- resolve();
- }
- });
-};
-import { useToast } from '@/hooks/use-toast';
-
-// Consolidated soundboard data
-const soundboards = {
- waffle: {
- name: 'Waffle House',
- sounds: [
- { id: 'eggs', text: "I'll take my eggs scattered, smothered, and covered!", voice: { pitch: 1.2, rate: 0.9 } },
- { id: 'hash', text: "Hash browns all the way!", voice: { pitch: 1.1, rate: 1.0 } },
- { id: 'coffee', text: "Keep that coffee coming!", voice: { pitch: 0.9, rate: 1.1 } },
- { id: 'shift', text: "Third shift crew checking in", voice: { pitch: 1.0, rate: 0.8 } }
- ]
- },
- ozzy: {
- name: 'Heavy Metal',
- sounds: [
- { id: 'train', text: "All aboard! Crazy train!", voice: { pitch: 0.7, rate: 1.2 } },
- { id: 'darkness', text: "Hello darkness my old friend", voice: { pitch: 0.6, rate: 0.8 } },
- { id: 'metal', text: "Metal up your life!", voice: { pitch: 0.8, rate: 1.1 } },
- { id: 'rock', text: "Let's rock and roll!", voice: { pitch: 0.7, rate: 1.0 } }
- ]
- },
- mario: {
- name: 'Mario World',
- sounds: [
- { id: 'mario', text: "It's-a me, Mario!", voice: { pitch: 1.4, rate: 1.1 } },
- { id: 'jump', text: "Wahoo! Here we go!", voice: { pitch: 1.5, rate: 1.2 } },
- { id: 'power', text: "Power up! Let's-a-go!", voice: { pitch: 1.3, rate: 1.0 } },
- { id: 'coin', text: "Mamma mia! Thank you so much!", voice: { pitch: 1.4, rate: 0.9 } }
- ]
- },
- max: {
- name: 'Max Headroom',
- sounds: [
- { id: 'stutter', text: "Th-th-that's all folks!", voice: { pitch: 1.2, rate: 1.3 } },
- { id: 'digital', text: "Welcome to the digital frontier!", voice: { pitch: 1.1, rate: 1.2 } },
- { id: 'glitch', text: "System glitch detected!", voice: { pitch: 1.0, rate: 1.4 } },
- { id: 'future', text: "The future is now!", voice: { pitch: 1.3, rate: 1.1 } }
- ]
- }
-};
-
-export function CoreSoundboards() {
- const { brandSkin } = useStore();
- const { toast } = useToast();
- const [activeSounds, setActiveSounds] = useState>(new Set());
-
- const playSound = async (soundboard: string, sound: any) => {
- const soundId = `${soundboard}-${sound.id}`;
-
- if (activeSounds.has(soundId)) return;
-
- setActiveSounds(prev => new Set([...Array.from(prev), soundId]));
-
- try {
- await speakText(sound.text, sound.voice);
-
- toast({
- title: `${soundboards[soundboard as keyof typeof soundboards].name}`,
- description: sound.text,
- duration: 2000,
- });
- } catch (error) {
- console.error('TTS Error:', error);
- } finally {
- setTimeout(() => {
- setActiveSounds(prev => {
- const newSet = new Set(Array.from(prev));
- newSet.delete(soundId);
- return newSet;
- });
- }, 3000);
- }
- };
-
- // Only show soundboard for current theme
- const getCurrentSoundboard = () => {
- switch (brandSkin) {
- case 'waffle': return soundboards.waffle;
- case 'ozzy': return soundboards.ozzy;
- case 'mario': return soundboards.mario;
- case 'maxheadroom': return soundboards.max;
- default: return null;
- }
- };
-
- const currentSoundboard = getCurrentSoundboard();
-
- if (!currentSoundboard) return null;
-
- return (
-
-
-
-
-
- {currentSoundboard.name}
-
- Triple-click theme
-
-
-
- {currentSoundboard.sounds.map((sound) => {
- const soundId = `${brandSkin}-${sound.id}`;
- const isActive = activeSounds.has(soundId);
-
- return (
- playSound(brandSkin, sound)}
- disabled={isActive}
- className="h-8 text-xs p-2 bg-white/10 hover:bg-white/20 text-white border border-white/20"
- data-testid={`button-sound-${sound.id}`}
- >
- {isActive ? '🔊' : sound.text.slice(0, 12)}
-
- );
- })}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/DevicePrompt.tsx b/client/src/components/DevicePrompt.tsx
deleted file mode 100644
index f6a067b..0000000
--- a/client/src/components/DevicePrompt.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Monitor, Tablet, Smartphone, X } from 'lucide-react';
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
-import { useStore } from '@/lib/store';
-
-export function DevicePrompt() {
- const { brandSkin } = useStore();
- const [isOpen, setIsOpen] = useState(false);
- const [isMobile, setIsMobile] = useState(false);
-
- useEffect(() => {
- // Check if user is on mobile/small screen
- const checkDevice = () => {
- const isMobileDevice = window.innerWidth < 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
- setIsMobile(isMobileDevice);
-
- // Check if user has seen this prompt before
- const hasSeenPrompt = localStorage.getItem('staticBuffet-devicePromptSeen');
-
- if (isMobileDevice && !hasSeenPrompt) {
- setIsOpen(true);
- }
- };
-
- checkDevice();
- window.addEventListener('resize', checkDevice);
-
- return () => window.removeEventListener('resize', checkDevice);
- }, []);
-
- const handleClose = () => {
- setIsOpen(false);
- localStorage.setItem('staticBuffet-devicePromptSeen', 'true');
- };
-
- const handleContinueAnyway = () => {
- setIsOpen(false);
- localStorage.setItem('staticBuffet-devicePromptSeen', 'true');
- };
-
- if (!isOpen) return null;
-
- return (
-
-
-
-
-
- Optimal Viewing Experience
-
-
-
-
-
- Static Buffet is designed for professional VJ mixing and works best on:
-
-
-
-
-
- Laptop or Desktop (Recommended)
-
-
-
-
Tablet in Landscape Mode
-
-
-
- Mobile Version Coming Soon
-
-
-
-
- For the best VJ experience with full panel visibility and precise controls,
- we recommend using a larger screen device.
-
-
-
-
-
- Got It
-
-
- Continue Anyway
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/DockingGuides.tsx b/client/src/components/DockingGuides.tsx
deleted file mode 100644
index 421e4bb..0000000
--- a/client/src/components/DockingGuides.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import { useState, useEffect } from 'react';
-import { useStore } from '@/lib/store';
-
-interface DockingGuidesProps {
- isDragging: boolean;
- dragPosition: { x: number; y: number } | null;
-}
-
-export function DockingGuides({ isDragging, dragPosition }: DockingGuidesProps) {
- const { brandSkin } = useStore();
- const [snapZones, setSnapZones] = useState>([]);
-
- useEffect(() => {
- if (isDragging) {
- // Calculate snap zones based on current viewport
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
- const headerHeight = 120; // Approximate header height
-
- const zones = [
- // Left third
- { x: 0, y: headerHeight, width: viewportWidth * 0.33, height: viewportHeight - headerHeight, type: 'left' },
- // Center third
- { x: viewportWidth * 0.33, y: headerHeight, width: viewportWidth * 0.34, height: viewportHeight - headerHeight, type: 'center' },
- // Right third
- { x: viewportWidth * 0.67, y: headerHeight, width: viewportWidth * 0.33, height: viewportHeight - headerHeight, type: 'right' },
- // Top half of center
- { x: viewportWidth * 0.25, y: headerHeight, width: viewportWidth * 0.5, height: (viewportHeight - headerHeight) * 0.5, type: 'top-center' },
- // Bottom half of center
- { x: viewportWidth * 0.25, y: headerHeight + (viewportHeight - headerHeight) * 0.5, width: viewportWidth * 0.5, height: (viewportHeight - headerHeight) * 0.5, type: 'bottom-center' },
- ];
-
- setSnapZones(zones);
- } else {
- setSnapZones([]);
- }
- }, [isDragging]);
-
- const getActiveZone = () => {
- if (!dragPosition || !isDragging) return null;
-
- return snapZones.find(zone =>
- dragPosition.x >= zone.x &&
- dragPosition.x <= zone.x + zone.width &&
- dragPosition.y >= zone.y &&
- dragPosition.y <= zone.y + zone.height
- );
- };
-
- const activeZone = getActiveZone();
-
- const guideColor = brandSkin === 'testcard' ? 'border-blue-400' :
- brandSkin === 'waffle' ? 'border-amber-400' :
- brandSkin === 'ebn' ? 'border-lime-400' :
- brandSkin === 'ozzy' ? 'border-red-400' :
- brandSkin === 'mario' ? 'border-red-400' :
- 'border-yellow-400';
-
- const activeFillColor = brandSkin === 'testcard' ? 'bg-blue-400/20' :
- brandSkin === 'waffle' ? 'bg-amber-400/20' :
- brandSkin === 'ebn' ? 'bg-lime-400/20' :
- brandSkin === 'ozzy' ? 'bg-red-400/20' :
- brandSkin === 'mario' ? 'bg-red-400/20' :
- 'bg-yellow-400/20';
-
- if (!isDragging) return null;
-
- return (
-
- {snapZones.map((zone, index) => (
-
- {/* Corner indicators */}
-
-
-
-
-
- {/* Center indicator */}
-
-
- {/* Zone type label */}
-
- {zone.type.toUpperCase()}
-
-
- ))}
-
- {/* Crosshair cursor indicator */}
- {dragPosition && (
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/DonationCTA.tsx b/client/src/components/DonationCTA.tsx
index aef3a41..7d55405 100644
--- a/client/src/components/DonationCTA.tsx
+++ b/client/src/components/DonationCTA.tsx
@@ -1,71 +1,47 @@
import React, { useState } from "react";
import { useStore } from "@/lib/store";
-
const KO_FI_URL = "https://ko-fi.com/staticbuffet";
-
const tips = {
- waffle: [
- "Your tip = more loops on the table.",
- "Help us keep the hash browns hot.",
- "Every dollar buys another weird clip.",
- "We work for tips and test patterns.",
- "Hot coffee refills included."
- ],
- ebn: [
- "Boost our broadcast range.",
- "Static travels farther with your help.",
- "Your contribution just hijacked another channel.",
- "Because dead air is boring.",
- "More chaos, less silence."
- ]
+ waffle: [
+ "Your tip = more loops on the table.",
+ "Help us keep the hash browns hot.",
+ "Every dollar buys another weird clip.",
+ "We work for tips and test patterns.",
+ "Hot coffee refills included."
+ ],
+ ebn: [
+ "Boost our broadcast range.",
+ "Static travels farther with your help.",
+ "Your contribution just hijacked another channel.",
+ "Because dead air is boring.",
+ "More chaos, less silence."
+ ]
};
-
-function rand(arr: string[]): string {
- return arr[Math.floor(Math.random() * arr.length)];
+function rand(arr: string[]): string {
+ return arr[Math.floor(Math.random() * arr.length)];
}
-
interface DonationCTAProps {
- href?: string;
+ href?: string;
}
-
export default function DonationCTA({ href = KO_FI_URL }: DonationCTAProps) {
- const { brandSkin } = useStore();
- const mode = brandSkin === 'waffle' ? 'waffle' : 'ebn';
- const [tip, setTip] = useState(rand(tips[mode] || tips.ebn));
- const label = mode === "waffle" ? "Feed the Buffet" : "Send a Signal";
-
- const btnBase = "px-4 py-2 rounded-2xl font-semibold transition transform active:scale-[0.98] focus:outline-none focus:ring";
- const btnWaffle = "bg-yellow-300 text-black hover:bg-yellow-200 ring-yellow-400";
- const btnEbn = "bg-black text-[#FFD300] border border-[#FFD300]/60 hover:bg-zinc-900 ring-yellow-400";
-
- const openKoFi = () =>
- window.open(href, "_blank", "noopener,noreferrer");
-
- return (
-
-
setTip(rand(tips[mode] || tips.ebn))}
- onClick={openKoFi}
- aria-label={`${label} — opens Ko-fi in a new tab`}
- data-testid="button-donate"
- >
+ const { brandSkin } = useStore();
+ const mode = 'ebn';
+ const [tip, setTip] = useState(rand(tips[mode] || tips.ebn));
+ const label = "Send a Signal";
+ const btnBase = "px-4 py-2 rounded-2xl font-semibold transition transform active:scale-[0.98] focus:outline-none focus:ring";
+ const btnEbn = "bg-black text-[#FFD300] border border-[#FFD300]/60 hover:bg-zinc-900 ring-yellow-400";
+ const openKoFi = () => window.open(href, "_blank", "noopener,noreferrer");
+ return (
+
setTip(rand(tips[mode] || tips.ebn))} onClick={openKoFi} aria-label={`${label} — opens Ko-fi in a new tab`} data-testid="button-donate">
{label}
{/* Tooltip */}
-
+ bg-zinc-900 text-[#FFD300] border border-[#FFD300]/60`} role="tooltip">
{tip}
-
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/EDLRecorder.tsx b/client/src/components/EDLRecorder.tsx
index 53af74b..55b9717 100644
--- a/client/src/components/EDLRecorder.tsx
+++ b/client/src/components/EDLRecorder.tsx
@@ -8,393 +8,290 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
-import {
- CircleDot,
- Square,
- Download,
- Clock,
- Scissors,
- PlayCircle,
- PauseCircle,
- RotateCcw,
- Settings,
- FileText,
- Timer
-} from 'lucide-react';
+import { CircleDot, Square, Download, Clock, Scissors, PlayCircle, PauseCircle, RotateCcw, Settings, FileText, Timer } from 'lucide-react';
import { useStore } from '@/lib/store';
import { useToast } from '@/hooks/use-toast';
import { type EDLEvent, type EDLSession } from '@/lib/types';
-
-interface EDLRecorderProps {}
-
+interface EDLRecorderProps {
+}
export function EDLRecorder({}: EDLRecorderProps) {
- const {
- brandSkin,
- isPlaying,
- queueItems,
- currentQueueIndex,
- videoEffects,
- audioEffects
- } = useStore();
- const { toast } = useToast();
-
- const [isRecording, setIsRecording] = useState(false);
- const [currentSession, setCurrentSession] = useState(null);
- const [sessionName, setSessionName] = useState('');
- const [venue, setVenue] = useState('');
- const [description, setDescription] = useState('');
- const [events, setEvents] = useState([]);
- const [eventCount, setEventCount] = useState(0);
- const [recordingStartTime, setRecordingStartTime] = useState(null);
- const [recordingDuration, setRecordingDuration] = useState(0);
-
- const lastStateRef = useRef({
- isPlaying: false,
- currentQueueIndex: 0,
- videoEffects: { ...videoEffects },
- audioEffects: { ...audioEffects }
- });
-
- const timerRef = useRef();
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return {
- accent: 'text-blue-400 border-blue-400',
- button: 'bg-blue-600 hover:bg-blue-700 text-white',
- recording: 'bg-red-600 hover:bg-red-700 text-white animate-pulse',
- badge: 'bg-blue-100 text-blue-800'
- };
- case 'waffle':
- return {
- accent: 'text-amber-800 border-amber-800',
- button: 'bg-amber-600 hover:bg-amber-700 text-white',
- recording: 'bg-red-600 hover:bg-red-700 text-white animate-pulse',
- badge: 'bg-amber-100 text-amber-800'
- };
- case 'ebn':
- return {
- accent: 'text-lime-400 border-lime-400',
- button: 'bg-lime-600 hover:bg-lime-700 text-white',
- recording: 'bg-red-600 hover:bg-red-700 text-white animate-pulse',
- badge: 'bg-lime-100 text-lime-800'
- };
- case 'ozzy':
- return {
- accent: 'text-red-300 border-red-300',
- button: 'bg-red-600 hover:bg-red-700 text-white',
- recording: 'bg-red-800 hover:bg-red-900 text-white animate-pulse',
- badge: 'bg-red-100 text-red-800'
- };
- case 'hogan':
- return {
- accent: 'text-yellow-300 border-yellow-300',
- button: 'bg-yellow-600 hover:bg-yellow-700 text-white',
- recording: 'bg-red-600 hover:bg-red-700 text-white animate-pulse',
- badge: 'bg-yellow-100 text-yellow-800'
- };
- default:
- return {
- accent: 'text-blue-400 border-blue-400',
- button: 'bg-blue-600 hover:bg-blue-700 text-white',
- recording: 'bg-red-600 hover:bg-red-700 text-white animate-pulse',
- badge: 'bg-blue-100 text-blue-800'
+ const { brandSkin, isPlaying, queueItems, currentQueueIndex, videoEffects, audioEffects } = useStore();
+ const { toast } = useToast();
+ const [isRecording, setIsRecording] = useState(false);
+ const [currentSession, setCurrentSession] = useState(null);
+ const [sessionName, setSessionName] = useState('');
+ const [venue, setVenue] = useState('');
+ const [description, setDescription] = useState('');
+ const [events, setEvents] = useState([]);
+ const [eventCount, setEventCount] = useState(0);
+ const [recordingStartTime, setRecordingStartTime] = useState(null);
+ const [recordingDuration, setRecordingDuration] = useState(0);
+ const lastStateRef = useRef({
+ isPlaying: false,
+ currentQueueIndex: 0,
+ videoEffects: { ...videoEffects },
+ audioEffects: { ...audioEffects }
+ });
+ const timerRef = useRef();
+ const getThemeClasses = () => {
+ {
+ return {
+ accent: 'text-lime-400 border-lime-400',
+ button: 'bg-lime-600 hover:bg-lime-700 text-white',
+ recording: 'bg-red-600 hover:bg-red-700 text-white animate-pulse',
+ badge: 'bg-lime-100 text-lime-800'
+ };
+ }
+ };
+ const theme = getThemeClasses();
+ // Generate unique session ID
+ const generateSessionId = () => {
+ return `edl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ };
+ // Generate unique event ID
+ const generateEventId = () => {
+ return `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ };
+ // Format duration for display
+ const formatDuration = (seconds: number) => {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+ if (hours > 0) {
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
+ };
+ // Start recording session
+ const startRecording = () => {
+ if (!sessionName.trim()) {
+ toast({
+ title: "Session Name Required",
+ description: "Please enter a name for your recording session.",
+ variant: "destructive",
+ });
+ return;
+ }
+ const sessionId = generateSessionId();
+ const startTime = new Date();
+ const newSession: EDLSession = {
+ id: sessionId,
+ name: sessionName,
+ startTime: startTime.toISOString(),
+ venue: venue || undefined,
+ description: description || undefined,
+ events: [],
+ metadata: {
+ theme: brandSkin,
+ audioReactive: false,
+ totalClips: 0,
+ totalCuts: 0,
+ }
};
- }
- };
-
- const theme = getThemeClasses();
-
- // Generate unique session ID
- const generateSessionId = () => {
- return `edl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- };
-
- // Generate unique event ID
- const generateEventId = () => {
- return `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- };
-
- // Format duration for display
- const formatDuration = (seconds: number) => {
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const secs = Math.floor(seconds % 60);
-
- if (hours > 0) {
- return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
- }
- return `${minutes}:${secs.toString().padStart(2, '0')}`;
- };
-
- // Start recording session
- const startRecording = () => {
- if (!sessionName.trim()) {
- toast({
- title: "Session Name Required",
- description: "Please enter a name for your recording session.",
- variant: "destructive",
- });
- return;
- }
-
- const sessionId = generateSessionId();
- const startTime = new Date();
-
- const newSession: EDLSession = {
- id: sessionId,
- name: sessionName,
- startTime: startTime.toISOString(),
- venue: venue || undefined,
- description: description || undefined,
- events: [],
- metadata: {
- theme: brandSkin,
- audioReactive: false,
- totalClips: 0,
- totalCuts: 0,
- }
+ setCurrentSession(newSession);
+ setIsRecording(true);
+ setRecordingStartTime(startTime);
+ setEvents([]);
+ setEventCount(0);
+ // Start the timer
+ timerRef.current = setInterval(() => {
+ setRecordingDuration(prev => prev + 1);
+ }, 1000);
+ // Log initial state
+ const currentVideo = queueItems[currentQueueIndex];
+ if (currentVideo) {
+ logEvent('play', {
+ clipId: currentVideo.id,
+ clipTitle: currentVideo.title,
+ timecode: '00:00:00',
+ trimIn: currentVideo.trimIn,
+ trimOut: currentVideo.trimOut,
+ notes: 'Session started'
+ });
+ }
+ toast({
+ title: "EDL Recording Started",
+ description: `Now recording "${sessionName}" - all cuts and effects will be logged.`,
+ });
};
-
- setCurrentSession(newSession);
- setIsRecording(true);
- setRecordingStartTime(startTime);
- setEvents([]);
- setEventCount(0);
-
- // Start the timer
- timerRef.current = setInterval(() => {
- setRecordingDuration(prev => prev + 1);
- }, 1000);
-
- // Log initial state
- const currentVideo = queueItems[currentQueueIndex];
- if (currentVideo) {
- logEvent('play', {
- clipId: currentVideo.id,
- clipTitle: currentVideo.title,
- timecode: '00:00:00',
- trimIn: currentVideo.trimIn,
- trimOut: currentVideo.trimOut,
- notes: 'Session started'
- });
- }
-
- toast({
- title: "EDL Recording Started",
- description: `Now recording "${sessionName}" - all cuts and effects will be logged.`,
- });
- };
-
- // Stop recording session
- const stopRecording = () => {
- if (!currentSession || !recordingStartTime) return;
-
- const endTime = new Date();
- const totalDuration = Math.floor((endTime.getTime() - recordingStartTime.getTime()) / 1000);
-
- const updatedSession: EDLSession = {
- ...currentSession,
- endTime: endTime.toISOString(),
- totalDuration: formatDuration(totalDuration),
- events: events,
- metadata: {
- ...currentSession.metadata,
- totalClips: new Set(events.map(e => e.clipId)).size,
- totalCuts: events.filter(e => e.eventType === 'cut').length,
- }
+ // Stop recording session
+ const stopRecording = () => {
+ if (!currentSession || !recordingStartTime)
+ return;
+ const endTime = new Date();
+ const totalDuration = Math.floor((endTime.getTime() - recordingStartTime.getTime()) / 1000);
+ const updatedSession: EDLSession = {
+ ...currentSession,
+ endTime: endTime.toISOString(),
+ totalDuration: formatDuration(totalDuration),
+ events: events,
+ metadata: {
+ ...currentSession.metadata,
+ totalClips: new Set(events.map(e => e.clipId)).size,
+ totalCuts: events.filter(e => e.eventType === 'cut').length,
+ }
+ };
+ setCurrentSession(updatedSession);
+ setIsRecording(false);
+ setRecordingDuration(0);
+ if (timerRef.current) {
+ clearInterval(timerRef.current);
+ }
+ // Save to localStorage
+ const savedSessions = JSON.parse(localStorage.getItem('staticBuffetEDLSessions') || '[]');
+ savedSessions.push(updatedSession);
+ localStorage.setItem('staticBuffetEDLSessions', JSON.stringify(savedSessions));
+ toast({
+ title: "EDL Recording Stopped",
+ description: `Session "${updatedSession.name}" saved with ${events.length} events.`,
+ });
};
-
- setCurrentSession(updatedSession);
- setIsRecording(false);
- setRecordingDuration(0);
-
- if (timerRef.current) {
- clearInterval(timerRef.current);
- }
-
- // Save to localStorage
- const savedSessions = JSON.parse(localStorage.getItem('staticBuffetEDLSessions') || '[]');
- savedSessions.push(updatedSession);
- localStorage.setItem('staticBuffetEDLSessions', JSON.stringify(savedSessions));
-
- toast({
- title: "EDL Recording Stopped",
- description: `Session "${updatedSession.name}" saved with ${events.length} events.`,
- });
- };
-
- // Log an EDL event
- const logEvent = (eventType: EDLEvent['eventType'], params: Partial) => {
- if (!isRecording || !currentSession) return;
-
- const currentVideo = queueItems[currentQueueIndex];
- if (!currentVideo && eventType !== 'pause') return;
-
- const event: EDLEvent = {
- id: generateEventId(),
- sessionId: currentSession.id,
- timestamp: new Date().toISOString(),
- eventType,
- clipId: params.clipId || currentVideo?.id || 'unknown',
- clipTitle: params.clipTitle || currentVideo?.title || 'Unknown',
- timecode: params.timecode || '00:00:00',
- trimIn: params.trimIn || currentVideo?.trimIn || '00:00:00',
- trimOut: params.trimOut || currentVideo?.trimOut || '00:00:00',
- parameters: params.parameters,
- notes: params.notes,
+ // Log an EDL event
+ const logEvent = (eventType: EDLEvent['eventType'], params: Partial) => {
+ if (!isRecording || !currentSession)
+ return;
+ const currentVideo = queueItems[currentQueueIndex];
+ if (!currentVideo && eventType !== 'pause')
+ return;
+ const event: EDLEvent = {
+ id: generateEventId(),
+ sessionId: currentSession.id,
+ timestamp: new Date().toISOString(),
+ eventType,
+ clipId: params.clipId || currentVideo?.id || 'unknown',
+ clipTitle: params.clipTitle || currentVideo?.title || 'Unknown',
+ timecode: params.timecode || '00:00:00',
+ trimIn: params.trimIn || currentVideo?.trimIn || '00:00:00',
+ trimOut: params.trimOut || currentVideo?.trimOut || '00:00:00',
+ parameters: params.parameters,
+ notes: params.notes,
+ };
+ setEvents(prev => [...prev, event]);
+ setEventCount(prev => prev + 1);
};
-
- setEvents(prev => [...prev, event]);
- setEventCount(prev => prev + 1);
- };
-
- // Watch for state changes to log events
- useEffect(() => {
- const lastState = lastStateRef.current;
-
- // Play/Pause changes
- if (isPlaying !== lastState.isPlaying) {
- logEvent(isPlaying ? 'play' : 'pause', {
- notes: isPlaying ? 'Playback started' : 'Playback paused'
- });
- }
-
- // Queue index changes (cuts)
- if (currentQueueIndex !== lastState.currentQueueIndex) {
- const currentVideo = queueItems[currentQueueIndex];
- if (currentVideo) {
- logEvent('cut', {
- clipId: currentVideo.id,
- clipTitle: currentVideo.title,
- trimIn: currentVideo.trimIn,
- trimOut: currentVideo.trimOut,
- notes: `Cut to track ${currentQueueIndex + 1}`
+ // Watch for state changes to log events
+ useEffect(() => {
+ const lastState = lastStateRef.current;
+ // Play/Pause changes
+ if (isPlaying !== lastState.isPlaying) {
+ logEvent(isPlaying ? 'play' : 'pause', {
+ notes: isPlaying ? 'Playback started' : 'Playback paused'
+ });
+ }
+ // Queue index changes (cuts)
+ if (currentQueueIndex !== lastState.currentQueueIndex) {
+ const currentVideo = queueItems[currentQueueIndex];
+ if (currentVideo) {
+ logEvent('cut', {
+ clipId: currentVideo.id,
+ clipTitle: currentVideo.title,
+ trimIn: currentVideo.trimIn,
+ trimOut: currentVideo.trimOut,
+ notes: `Cut to track ${currentQueueIndex + 1}`
+ });
+ }
+ }
+ // Update last state
+ lastStateRef.current = {
+ isPlaying,
+ currentQueueIndex,
+ videoEffects: { ...videoEffects },
+ audioEffects: { ...audioEffects }
+ };
+ }, [isPlaying, currentQueueIndex, queueItems, videoEffects, audioEffects]);
+ // Export EDL as file
+ const exportEDL = () => {
+ if (!currentSession)
+ return;
+ // Create detailed EDL text format
+ const edlText = [
+ `STATIC BUFFET EDL - ${currentSession.name}`,
+ `Session ID: ${currentSession.id}`,
+ `Date: ${new Date(currentSession.startTime).toLocaleString()}`,
+ `Duration: ${currentSession.totalDuration || 'In Progress'}`,
+ `Venue: ${currentSession.venue || 'N/A'}`,
+ `Theme: ${currentSession.metadata?.theme || 'Unknown'}`,
+ `Total Events: ${events.length}`,
+ ``,
+ `EVENT LOG:`,
+ `========================================`,
+ ...events.map((event, index) => {
+ const time = new Date(event.timestamp).toLocaleTimeString();
+ return [
+ `${(index + 1).toString().padStart(3, '0')}. [${time}] ${event.eventType.toUpperCase()}`,
+ ` Clip: ${event.clipTitle}`,
+ ` Timecode: ${event.timecode} (${event.trimIn} - ${event.trimOut})`,
+ event.notes ? ` Notes: ${event.notes}` : '',
+ event.parameters ? ` Parameters: ${JSON.stringify(event.parameters)}` : '',
+ ``,
+ ].filter(Boolean).join('\n');
+ }),
+ ``,
+ `END OF EDL`,
+ ].join('\n');
+ // Download as text file
+ const blob = new Blob([edlText], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${currentSession.name.replace(/[^a-z0-9]/gi, '_')}_EDL.txt`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ toast({
+ title: "EDL Exported",
+ description: `Edit Decision List saved as ${a.download}`,
});
- }
- }
-
- // Update last state
- lastStateRef.current = {
- isPlaying,
- currentQueueIndex,
- videoEffects: { ...videoEffects },
- audioEffects: { ...audioEffects }
};
- }, [isPlaying, currentQueueIndex, queueItems, videoEffects, audioEffects]);
-
- // Export EDL as file
- const exportEDL = () => {
- if (!currentSession) return;
-
- // Create detailed EDL text format
- const edlText = [
- `STATIC BUFFET EDL - ${currentSession.name}`,
- `Session ID: ${currentSession.id}`,
- `Date: ${new Date(currentSession.startTime).toLocaleString()}`,
- `Duration: ${currentSession.totalDuration || 'In Progress'}`,
- `Venue: ${currentSession.venue || 'N/A'}`,
- `Theme: ${currentSession.metadata?.theme || 'Unknown'}`,
- `Total Events: ${events.length}`,
- ``,
- `EVENT LOG:`,
- `========================================`,
- ...events.map((event, index) => {
- const time = new Date(event.timestamp).toLocaleTimeString();
- return [
- `${(index + 1).toString().padStart(3, '0')}. [${time}] ${event.eventType.toUpperCase()}`,
- ` Clip: ${event.clipTitle}`,
- ` Timecode: ${event.timecode} (${event.trimIn} - ${event.trimOut})`,
- event.notes ? ` Notes: ${event.notes}` : '',
- event.parameters ? ` Parameters: ${JSON.stringify(event.parameters)}` : '',
- ``,
- ].filter(Boolean).join('\n');
- }),
- ``,
- `END OF EDL`,
- ].join('\n');
-
- // Download as text file
- const blob = new Blob([edlText], { type: 'text/plain' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `${currentSession.name.replace(/[^a-z0-9]/gi, '_')}_EDL.txt`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-
- toast({
- title: "EDL Exported",
- description: `Edit Decision List saved as ${a.download}`,
- });
- };
-
- // Export EDL as JSON
- const exportEDLJSON = () => {
- if (!currentSession) return;
-
- const jsonData = {
- ...currentSession,
- events: events,
- exportedAt: new Date().toISOString(),
- format: 'static-buffet-edl-v1'
+ // Export EDL as JSON
+ const exportEDLJSON = () => {
+ if (!currentSession)
+ return;
+ const jsonData = {
+ ...currentSession,
+ events: events,
+ exportedAt: new Date().toISOString(),
+ format: 'static-buffet-edl-v1'
+ };
+ const blob = new Blob([JSON.stringify(jsonData, null, 2)], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${currentSession.name.replace(/[^a-z0-9]/gi, '_')}_EDL.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ toast({
+ title: "EDL JSON Exported",
+ description: `Machine-readable EDL saved as ${a.download}`,
+ });
};
-
- const blob = new Blob([JSON.stringify(jsonData, null, 2)], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `${currentSession.name.replace(/[^a-z0-9]/gi, '_')}_EDL.json`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-
- toast({
- title: "EDL JSON Exported",
- description: `Machine-readable EDL saved as ${a.download}`,
- });
- };
-
- return (
-
+ return (
{/* Recording Status */}
- {isRecording ? (
-
-
+ {isRecording ? (
+
REC
-
+
{formatDuration(recordingDuration)}
-
- ) : (
-
-
+ ) : (
+
Standby
-
- )}
+ )}
- {eventCount > 0 && (
-
+ {eventCount > 0 && (
{eventCount} events
-
- )}
+ )}
- {!isRecording ? (
-
+ {!isRecording ? (
-
-
+
+
Record Set
@@ -405,95 +302,50 @@ export function EDLRecorder({}: EDLRecorderProps) {
Session Name *
- setSessionName(e.target.value)}
- placeholder="e.g., Club Night, Wedding Set, Practice Session"
- data-testid="input-session-name"
- />
+ setSessionName(e.target.value)} placeholder="e.g., Club Night, Wedding Set, Practice Session" data-testid="input-session-name"/>
Venue
- setVenue(e.target.value)}
- placeholder="e.g., The Underground, Studio A"
- data-testid="input-venue"
- />
+ setVenue(e.target.value)} placeholder="e.g., The Underground, Studio A" data-testid="input-venue"/>
Description
-
-
-
+
+
Start Recording
-
- ) : (
-
-
+ ) : (
+
Stop
-
- )}
+ )}
- {currentSession && (
-
-
-
+ {currentSession && (
+
+
-
-
+
+
-
- )}
+ )}
{/* Live Event Feed (only during recording) */}
- {isRecording && events.length > 0 && (
-
+ {isRecording && events.length > 0 && (
-
+
Live Event Log
- {events.slice(-5).map((event, index) => (
-
+ {events.slice(-5).map((event, index) => (
{new Date(event.timestamp).toLocaleTimeString()}
@@ -501,17 +353,14 @@ export function EDLRecorder({}: EDLRecorderProps) {
{event.eventType}
{event.clipTitle}
-
- ))}
+
))}
-
- )}
+ )}
{/* Session Summary */}
- {currentSession && !isRecording && (
-
+ {currentSession && !isRecording && (
Last Session: {currentSession.name}
@@ -535,14 +384,10 @@ export function EDLRecorder({}: EDLRecorderProps) {
Theme: {currentSession.metadata?.theme}
- {currentSession.venue && (
-
+ {currentSession.venue && (
Venue: {currentSession.venue}
-
- )}
+
)}
-
- )}
-
- );
-}
\ No newline at end of file
+ )}
+ );
+}
diff --git a/client/src/components/EasterEgg.tsx b/client/src/components/EasterEgg.tsx
deleted file mode 100644
index d61a960..0000000
--- a/client/src/components/EasterEgg.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import React from 'react';
-import { Button } from '@/components/ui/button';
-import { useStore } from '@/lib/store';
-
-interface EasterEggProps {
- isActive: boolean;
- onClose: () => void;
-}
-
-export function EasterEgg({ isActive, onClose }: EasterEggProps) {
- const { setVideoEffects, videoEffects, addToQueue, queueItems } = useStore();
-
- const activateAcidTrip = async () => {
- // Add the Marilyn Manson video to the front of the queue
- const easterEggVideo = {
- identifier: 'TheVistaGroup-MarilynMansonGetYourGunnonBeavisandButthead',
- title: 'Marilyn Manson - Get Your Gunn (Easter Egg)',
- creator: 'Marilyn Manson',
- duration: '4:20',
- licenseurl: 'https://creativecommons.org/publicdomain/mark/1.0/',
- };
-
- const videoUrl = '/api/video/TheVistaGroup-MarilynMansonGetYourGunnonBeavisandButthead/Marilyn Manson - Get Your Gunn on Beavis and Butthead VHS • 60 FPS 1996.ia.mp4';
-
- // Add to front of queue (will be at index 0)
- addToQueue(easterEggVideo, videoUrl, true);
-
- // Enable intense video effects for acid trip mode
- setVideoEffects({
- ...videoEffects,
- intensity: 100,
- brightness: 150,
- contrast: 200,
- saturation: 300,
- hue: 180,
- chromaticAberration: 100,
- glitchIntensity: 80,
- });
-
- // Auto-close the easter egg after activating
- setTimeout(onClose, 1000);
- };
-
- if (!isActive) return null;
-
- return (
-
-
-
🌀
-
- EASTER EGG UNLOCKED
-
-
- You've discovered the secret Acid Trip effect!
- This will add the Marilyn Manson clip to your queue
- and activate maximum visual chaos mode.
-
-
-
-
- 🚀 ACTIVATE ACID TRIP MODE 🚀
-
-
-
- Maybe Later
-
-
-
-
- Press 'bb' again anytime to return here
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/EmergencyMix.tsx b/client/src/components/EmergencyMix.tsx
index c14999d..c419078 100644
--- a/client/src/components/EmergencyMix.tsx
+++ b/client/src/components/EmergencyMix.tsx
@@ -7,106 +7,64 @@ import { Label } from '@/components/ui/label';
import { useStore } from '@/lib/store';
import { generateEmergencyMix, type EmergencyMixOptions } from '@/lib/emergency-mix';
import { useToast } from '@/hooks/use-toast';
-
export function EmergencyMix() {
- const { searchResults, queueItems, setQueueItems, brandSkin } = useStore();
- const { toast } = useToast();
- const [isOpen, setIsOpen] = useState(false);
- const [options, setOptions] = useState({
- duration: 150, // 2.5 minutes
- segmentLength: [2, 5],
- crossfadeDuration: 0.5,
- maxClips: 10,
- });
-
- const handleGenerateMix = () => {
- console.log('EmergencyMix: handleGenerateMix called');
- console.log('EmergencyMix: searchResults.length =', searchResults.length);
- try {
- if (searchResults.length === 0) {
- toast({
- title: "No results available",
- description: "Search for videos first to generate an emergency mix",
- variant: "destructive",
- });
- return;
- }
-
- const mixItems = generateEmergencyMix(searchResults, options);
-
- // Replace current queue with emergency mix
- setQueueItems(mixItems);
-
- toast({
- title: "Emergency Mix Generated!",
- description: `Created ${mixItems.length} clips totaling ${Math.floor(options.duration / 60)}:${(options.duration % 60).toString().padStart(2, '0')}`,
- });
-
- setIsOpen(false);
- } catch (error) {
- toast({
- title: "Mix Generation Failed",
- description: error instanceof Error ? error.message : "Failed to generate emergency mix",
- variant: "destructive",
- });
- }
- };
-
- const handleQuickMix = () => {
- handleGenerateMix();
- };
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-400/50';
- case 'waffle':
- return 'bg-red-400/20 text-red-600 hover:bg-red-400/30 border-red-400/50';
- case 'ebn':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-500/50';
- case 'ozzy':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-500/50';
- case 'hogan':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-400/50';
- case 'dx':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-500/50';
- case 'maxheadroom':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-500/50';
- case 'mario':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-400/50';
- case 'dakota':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-400/50';
- case 'blondie':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-400/50';
- default:
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-400/50';
- }
- };
-
- return (
-
+ const { searchResults, queueItems, setQueueItems, brandSkin } = useStore();
+ const { toast } = useToast();
+ const [isOpen, setIsOpen] = useState(false);
+ const [options, setOptions] = useState
({
+ duration: 150, // 2.5 minutes
+ segmentLength: [2, 5],
+ crossfadeDuration: 0.5,
+ maxClips: 10,
+ });
+ const handleGenerateMix = () => {
+ console.log('EmergencyMix: handleGenerateMix called');
+ console.log('EmergencyMix: searchResults.length =', searchResults.length);
+ try {
+ if (searchResults.length === 0) {
+ toast({
+ title: "No results available",
+ description: "Search for videos first to generate an emergency mix",
+ variant: "destructive",
+ });
+ return;
+ }
+ const mixItems = generateEmergencyMix(searchResults, options);
+ // Replace current queue with emergency mix
+ setQueueItems(mixItems);
+ toast({
+ title: "Emergency Mix Generated!",
+ description: `Created ${mixItems.length} clips totaling ${Math.floor(options.duration / 60)}:${(options.duration % 60).toString().padStart(2, '0')}`,
+ });
+ setIsOpen(false);
+ }
+ catch (error) {
+ toast({
+ title: "Mix Generation Failed",
+ description: error instanceof Error ? error.message : "Failed to generate emergency mix",
+ variant: "destructive",
+ });
+ }
+ };
+ const handleQuickMix = () => {
+ handleGenerateMix();
+ };
+ const getThemeClasses = () => {
+ {
+ return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-500/50';
+ }
+ };
+ return (
{/* Quick Emergency Mix */}
-
-
+
+
{/* Advanced Options Dialog */}
-
-
+
+
@@ -118,16 +76,7 @@ export function EmergencyMix() {
Total Duration (seconds)
- setOptions(prev => ({ ...prev, duration: parseInt(e.target.value) || 150 }))}
- className="mt-1"
- min="30"
- max="600"
- data-testid="input-mix-duration"
- />
+ setOptions(prev => ({ ...prev, duration: parseInt(e.target.value) || 150 }))} className="mt-1" min="30" max="600" data-testid="input-mix-duration"/>
{Math.floor(options.duration / 60)}:{(options.duration % 60).toString().padStart(2, '0')} total
@@ -138,37 +87,19 @@ export function EmergencyMix() {
Min Segment (sec)
- setOptions(prev => ({
- ...prev,
- segmentLength: [parseInt(e.target.value) || 2, prev.segmentLength[1]]
- }))}
- className="mt-1"
- min="1"
- max="30"
- data-testid="input-segment-min"
- />
+ setOptions(prev => ({
+ ...prev,
+ segmentLength: [parseInt(e.target.value) || 2, prev.segmentLength[1]]
+ }))} className="mt-1" min="1" max="30" data-testid="input-segment-min"/>
Max Segment (sec)
- setOptions(prev => ({
- ...prev,
- segmentLength: [prev.segmentLength[0], parseInt(e.target.value) || 5]
- }))}
- className="mt-1"
- min="2"
- max="60"
- data-testid="input-segment-max"
- />
+ setOptions(prev => ({
+ ...prev,
+ segmentLength: [prev.segmentLength[0], parseInt(e.target.value) || 5]
+ }))} className="mt-1" min="2" max="60" data-testid="input-segment-max"/>
@@ -176,55 +107,26 @@ export function EmergencyMix() {
Max Clips to Use
- setOptions(prev => ({ ...prev, maxClips: parseInt(e.target.value) || 10 }))}
- className="mt-1"
- min="3"
- max="50"
- data-testid="input-max-clips"
- />
+ setOptions(prev => ({ ...prev, maxClips: parseInt(e.target.value) || 10 }))} className="mt-1" min="3" max="50" data-testid="input-max-clips"/>
Crossfade Duration (seconds)
- setOptions(prev => ({ ...prev, crossfadeDuration: parseFloat(e.target.value) || 0.5 }))}
- className="mt-1"
- min="0"
- max="2"
- data-testid="input-crossfade"
- />
+ setOptions(prev => ({ ...prev, crossfadeDuration: parseFloat(e.target.value) || 0.5 }))} className="mt-1" min="0" max="2" data-testid="input-crossfade"/>
- setIsOpen(false)}
- data-testid="button-cancel-mix"
- >
+ setIsOpen(false)} data-testid="button-cancel-mix">
Cancel
-
+
Generate Mix
-
- );
+ );
}
diff --git a/client/src/components/FirstRunTour.tsx b/client/src/components/FirstRunTour.tsx
deleted file mode 100644
index db04344..0000000
--- a/client/src/components/FirstRunTour.tsx
+++ /dev/null
@@ -1,434 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
-import { Slider } from '@/components/ui/slider';
-import {
- Search,
- Play,
- Scissors,
- Plus,
- Zap,
- ArrowRight,
- ArrowLeft,
- CheckCircle,
- X,
- Target,
- Timer,
-} from 'lucide-react';
-import { useStore } from '@/lib/store';
-import { useToast } from '@/hooks/use-toast';
-
-interface TourStep {
- id: number;
- title: string;
- description: string;
- icon: React.ReactNode;
- target: string;
- action?: string;
- tip?: string;
-}
-
-const tourSteps: TourStep[] = [
- {
- id: 1,
- title: "Search for Content",
- description: "Start by searching for public domain video footage. Try searching for 'vintage cartoon' or click a Smart Query chip.",
- icon: ,
- target: "search-section",
- action: "search",
- tip: "Use Smart Query chips for instant content discovery!"
- },
- {
- id: 2,
- title: "Preview & Trim",
- description: "Click any video to preview it. Use the trim controls to set your in/out points for the perfect clip length.",
- icon: ,
- target: "player-section",
- action: "preview",
- tip: "Double-click the player to go fullscreen for better preview."
- },
- {
- id: 3,
- title: "Add to Queue",
- description: "Found the perfect clip? Click 'Add to Queue' to build your video timeline for mixing and playback.",
- icon: ,
- target: "queue-section",
- action: "queue",
- tip: "Drag clips in the queue to reorder them for your set."
- },
- {
- id: 4,
- title: "Emergency Mix",
- description: "Need content fast? Hit Emergency Mix to instantly fill your queue with diverse, legally-safe clips ready for live mixing.",
- icon: ,
- target: "emergency-mix",
- action: "emergency",
- tip: "Perfect for when you need backup content during live sets!"
- }
-];
-
-interface FirstRunTourProps {}
-
-export function FirstRunTour({}: FirstRunTourProps) {
- const { brandSkin } = useStore();
- const { toast } = useToast();
-
- const [isOpen, setIsOpen] = useState(false);
- const [currentStep, setCurrentStep] = useState(1);
- const [completedSteps, setCompletedSteps] = useState([]);
- const [hasShownTour, setHasShownTour] = useState(false);
-
- // Auto-start tour disabled - now only accessible through menu
- // useEffect(() => {
- // const hasSeenTour = localStorage.getItem('staticBuffetHasSeenTour');
- // if (!hasSeenTour && !hasShownTour) {
- // // Small delay to let the interface load
- // setTimeout(() => {
- // setIsOpen(true);
- // setHasShownTour(true);
- // }, 1500);
- // }
- // }, [hasShownTour]);
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return {
- accent: 'text-blue-400 border-blue-400 bg-blue-50',
- button: 'bg-blue-600 hover:bg-blue-700',
- progress: 'bg-blue-600'
- };
- case 'waffle':
- return {
- accent: 'text-amber-800 border-amber-600 bg-yellow-50',
- button: 'bg-amber-600 hover:bg-amber-700',
- progress: 'bg-amber-600'
- };
- case 'ebn':
- return {
- accent: 'text-lime-400 border-lime-400 bg-lime-50',
- button: 'bg-lime-600 hover:bg-lime-700',
- progress: 'bg-lime-600'
- };
- case 'ozzy':
- return {
- accent: 'text-red-300 border-red-400 bg-red-50',
- button: 'bg-red-600 hover:bg-red-700',
- progress: 'bg-red-600'
- };
- case 'hogan':
- return {
- accent: 'text-yellow-300 border-yellow-400 bg-yellow-50',
- button: 'bg-yellow-600 hover:bg-yellow-700',
- progress: 'bg-yellow-600'
- };
- default:
- return {
- accent: 'text-blue-400 border-blue-400 bg-blue-50',
- button: 'bg-blue-600 hover:bg-blue-700',
- progress: 'bg-blue-600'
- };
- }
- };
-
- const theme = getThemeClasses();
- const currentTourStep = tourSteps[currentStep - 1];
- const progress = (currentStep / tourSteps.length) * 100;
-
- const nextStep = () => {
- if (currentStep < tourSteps.length) {
- setCurrentStep(currentStep + 1);
- }
- };
-
- const prevStep = () => {
- if (currentStep > 1) {
- setCurrentStep(currentStep - 1);
- }
- };
-
- const markStepCompleted = (stepId: number) => {
- if (!completedSteps.includes(stepId)) {
- setCompletedSteps([...completedSteps, stepId]);
-
- toast({
- title: "Step Completed!",
- description: `Step ${stepId}: ${tourSteps[stepId - 1].title}`,
- });
-
- // Auto-advance to next step
- if (stepId === currentStep && stepId < tourSteps.length) {
- setTimeout(nextStep, 1000);
- }
- }
- };
-
- const skipTour = () => {
- localStorage.setItem('staticBuffetHasSeenTour', 'true');
- setIsOpen(false);
-
- toast({
- title: "Tour Skipped",
- description: "You can restart the tour anytime from the help menu.",
- });
- };
-
- const completeTour = () => {
- localStorage.setItem('staticBuffetHasSeenTour', 'true');
- setIsOpen(false);
-
- toast({
- title: "Welcome to Static Buffet!",
- description: "You're ready to start mixing public domain video content like a pro.",
- });
- };
-
- const restartTour = () => {
- setCurrentStep(1);
- setCompletedSteps([]);
- setIsOpen(true);
- };
-
- // Add spotlight effect for current target
- const addSpotlight = (targetId: string) => {
- // Remove existing spotlights
- document.querySelectorAll('.tour-spotlight').forEach(el => {
- el.classList.remove('tour-spotlight');
- });
-
- // Add spotlight to current target
- const target = document.querySelector(`[data-tour-target="${targetId}"]`);
- if (target) {
- target.classList.add('tour-spotlight');
- }
- };
-
- useEffect(() => {
- if (isOpen && currentTourStep) {
- addSpotlight(currentTourStep.target);
- }
-
- return () => {
- // Cleanup spotlights when component unmounts
- document.querySelectorAll('.tour-spotlight').forEach(el => {
- el.classList.remove('tour-spotlight');
- });
- };
- }, [isOpen, currentStep, currentTourStep]);
-
- // Public method to start tour (can be called from other components)
- const startTour = () => {
- restartTour();
- };
-
- // Attach restart function to window for external access
- useEffect(() => {
- (window as any).startStaticBuffetTour = startTour;
- return () => {
- delete (window as any).startStaticBuffetTour;
- };
- }, []);
-
- if (!isOpen) {
- return null;
- }
-
- return (
- <>
- {/* Overlay with spotlight effect */}
-
-
- {/* Tour Dialog */}
- {}}>
- e.preventDefault()}
- onEscapeKeyDown={(e) => e.preventDefault()}
- >
-
-
-
-
- Static Buffet Tour
-
-
- {currentStep} of {tourSteps.length}
-
-
-
-
-
- {/* Progress Bar */}
-
-
- Progress
- {Math.round(progress)}%
-
-
-
-
- {/* Current Step */}
-
-
-
-
- {currentTourStep.icon}
-
-
-
{currentTourStep.title}
-
- Step {currentStep}
-
-
-
-
-
-
- {currentTourStep.description}
-
-
- {currentTourStep.tip && (
-
-
-
-
- Pro Tip: {currentTourStep.tip}
-
-
-
- )}
-
- {/* Step completion indicator */}
- {completedSteps.includes(currentStep) && (
-
-
- Step completed!
-
- )}
-
-
-
- {/* Navigation */}
-
-
-
-
- Back
-
-
- {currentStep < tourSteps.length ? (
-
- Next
-
-
- ) : (
-
-
- Get Started
-
- )}
-
-
-
-
- Skip Tour
-
-
-
- {/* Step indicators */}
-
- {tourSteps.map((step) => (
- setCurrentStep(step.id)}
- className={`w-3 h-3 rounded-full transition-colors ${
- step.id === currentStep
- ? theme.button.split(' ')[0]
- : completedSteps.includes(step.id)
- ? 'bg-green-500'
- : 'bg-gray-300'
- }`}
- title={step.title}
- data-testid={`button-tour-step-${step.id}`}
- />
- ))}
-
-
-
-
-
- {/* CSS for spotlight effect */}
-
- >
- );
-}
-
-// Export the start tour function for external use
-export const startTour = () => {
- if ((window as any).startStaticBuffetTour) {
- (window as any).startStaticBuffetTour();
- }
-};
\ No newline at end of file
diff --git a/client/src/components/FloatingPanel.tsx b/client/src/components/FloatingPanel.tsx
deleted file mode 100644
index 10ba478..0000000
--- a/client/src/components/FloatingPanel.tsx
+++ /dev/null
@@ -1,230 +0,0 @@
-import React, { useRef, useState, useEffect, ReactNode } from 'react';
-import { Button } from '@/components/ui/button';
-import { Lock, Unlock, X } from 'lucide-react';
-import { useStore } from '@/lib/store';
-import { PanelPosition, FloatingPanelStates, BrandSkin } from '@/lib/types';
-import { getThemeClasses } from '@/lib/theme-utils';
-import { ScaleTransition } from './AnimatedTransitions';
-
-interface FloatingPanelProps {
- id: keyof FloatingPanelStates;
- title: string;
- children: ReactNode;
- brandSkin: BrandSkin;
-}
-
-export function FloatingPanel({ id, title, children, brandSkin }: FloatingPanelProps) {
- const {
- floatingPanelStates,
- updatePanelPosition,
- togglePanelLock,
- setFloatingPanelVisible,
- bringPanelToFront
- } = useStore();
-
- const panelRef = useRef(null);
- const [isDragging, setIsDragging] = useState(false);
- const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
- const [isResizing, setIsResizing] = useState(false);
- const [resizeStart, setResizeStart] = useState({ x: 0, y: 0, width: 0, height: 0 });
-
- const position = floatingPanelStates[id];
- const themeClasses = getThemeClasses(brandSkin);
-
- // Handle mouse down on header for dragging
- const handleMouseDown = (e: React.MouseEvent) => {
- if (position.isLocked) return;
-
- e.preventDefault();
- bringPanelToFront(id);
- setIsDragging(true);
- setDragStart({
- x: e.clientX - position.x,
- y: e.clientY - position.y,
- });
- };
-
- // Handle mouse down on resize handle
- const handleResizeMouseDown = (e: React.MouseEvent) => {
- if (position.isLocked) return;
-
- e.preventDefault();
- e.stopPropagation();
- setIsResizing(true);
- setResizeStart({
- x: e.clientX,
- y: e.clientY,
- width: position.width,
- height: position.height,
- });
- };
-
- // Handle mouse move for dragging and resizing
- useEffect(() => {
- const handleMouseMove = (e: MouseEvent) => {
- if (isDragging) {
- // Account for header height only (no more MainToolbar)
- // Header: ~70px
- const headerHeight = window.innerWidth >= 1024 ? 80 : 90;
- const newX = Math.max(0, Math.min(window.innerWidth - position.width, e.clientX - dragStart.x));
- const newY = Math.max(headerHeight, Math.min(window.innerHeight - position.height - 10, e.clientY - dragStart.y)); // Added 10px compensation
-
- updatePanelPosition(id, { x: newX, y: newY });
- } else if (isResizing) {
- const deltaX = e.clientX - resizeStart.x;
- const deltaY = e.clientY - resizeStart.y;
-
- const newWidth = Math.max(300, Math.min(window.innerWidth - position.x, resizeStart.width + deltaX));
- const newHeight = Math.max(200, Math.min(window.innerHeight - position.y - 10, resizeStart.height + deltaY)); // Added 10px compensation
-
- updatePanelPosition(id, { width: newWidth, height: newHeight });
- }
- };
-
- const handleMouseUp = () => {
- setIsDragging(false);
- setIsResizing(false);
- };
-
- if (isDragging || isResizing) {
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
-
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- };
- }
- }, [isDragging, isResizing, dragStart, resizeStart, id, position, updatePanelPosition]);
-
- // Handle window resize to keep panels in bounds
- useEffect(() => {
- const handleResize = () => {
- const headerHeight = window.innerWidth >= 1024 ? 80 : 90;
- const maxX = Math.max(0, window.innerWidth - position.width);
- const maxY = Math.max(headerHeight, window.innerHeight - position.height);
-
- let needsUpdate = false;
- const updates: Partial = {};
-
- if (position.x > maxX) {
- updates.x = maxX;
- needsUpdate = true;
- }
-
- if (position.y < headerHeight) {
- updates.y = headerHeight;
- needsUpdate = true;
- } else if (position.y > maxY) {
- updates.y = maxY;
- needsUpdate = true;
- }
-
- if (position.width > window.innerWidth) {
- updates.width = Math.max(300, window.innerWidth - 40);
- needsUpdate = true;
- }
-
- if (position.height > window.innerHeight - headerHeight) {
- updates.height = Math.max(200, window.innerHeight - headerHeight - 50); // Added extra 10px compensation
- needsUpdate = true;
- }
-
- if (needsUpdate) {
- updatePanelPosition(id, updates);
- }
- };
-
- window.addEventListener('resize', handleResize);
- return () => window.removeEventListener('resize', handleResize);
- }, [id, position.x, position.y, position.width, position.height, updatePanelPosition]);
-
- // Don't render if panel is not visible
- if (position.visible === false) {
- return null;
- }
-
- return (
- bringPanelToFront(id)}
- data-testid={`floating-panel-${id}`}
- >
- {/* Panel Header */}
-
-
-
{title}
-
-
-
-
- {
- e.stopPropagation();
- setFloatingPanelVisible(id, false);
- }}
- className={`h-6 w-6 p-0 ${themeClasses.accent} hover:text-red-500`}
- data-testid={`button-close-${id}`}
- title="Close"
- >
-
-
-
-
-
- {
- e.stopPropagation();
- togglePanelLock(id);
- }}
- className={`h-6 w-6 p-0 ${themeClasses.accent}`}
- data-testid={`button-lock-${id}`}
- title={position.isLocked ? 'Unlock' : 'Lock'}
- >
- {position.isLocked ? : }
-
-
-
-
-
- {/* Panel Content */}
-
- {children}
-
-
- {/* Resize Handle */}
- {!position.isLocked && (
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/FloatingPanelsManager.tsx b/client/src/components/FloatingPanelsManager.tsx
deleted file mode 100644
index 1a0039e..0000000
--- a/client/src/components/FloatingPanelsManager.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-import React from 'react';
-import { FloatingPanel } from './FloatingPanel';
-import { useStore } from '@/lib/store';
-
-// Import the existing panel components
-import { Filters } from './Filters';
-import { Player } from './Player';
-import { QueuePanel } from './QueuePanel';
-import { ResultsGrid } from './ResultsGrid';
-import { LiveVideoMode } from './LiveVideoMode';
-import { RecordSetPanel } from './RecordSetPanel';
-import { LoopControlsPanel } from './LoopControlsPanel';
-import { PresetEffectsPanel } from './PresetEffectsPanel';
-import { MediaControls } from './MediaControls';
-import { EmergencyMix } from './EmergencyMix';
-import { LuckyDip } from './LuckyDip';
-import { KeyboardShortcuts } from './KeyboardShortcuts';
-import { PreviewPanel } from './PreviewPanel';
-import { VideoEffectsPanel } from './VideoEffectsPanel';
-import { AudioEffectsPanel } from './AudioEffectsPanel';
-import { GeometryPanel } from './GeometryPanel';
-import { TrimControlsPanel } from './TrimControlsPanel';
-
-
-export function FloatingPanelsManager() {
- const { brandSkin, isFloatingMode, floatingPanelStates } = useStore();
-
- if (!isFloatingMode) {
- return null;
- }
-
- return (
-
- {/* Search Results Panel - no duplicate search UI */}
-
-
- {}} />
-
-
-
- {/* Player Panel */}
-
-
- {/* Timeline Panel */}
-
-
-
-
-
-
-
- {/* Live Video Input Panel */}
-
-
-
-
-
-
- {/* Record Set Micro Panel */}
-
-
-
-
-
-
- {/* Loop Controls Micro Panel */}
-
-
-
-
-
-
-
- {/* Preset Effects Panel */}
-
-
- {/* Results Grid Panel */}
-
-
- {}} />
-
-
-
- {/* Media Controls Panel */}
-
-
-
-
-
-
-
- {/* Emergency Mix Panel */}
-
-
-
-
-
-
- {/* Lucky Dip Panel */}
-
-
- {}} />
-
-
-
- {/* Keyboard Shortcuts Panel */}
-
-
-
-
-
-
- {/* Preview Panel */}
- {floatingPanelStates.preview?.visible && (
-
- )}
-
- {/* Video Effects Panel */}
-
-
-
-
-
-
- {/* Audio Effects Panel */}
-
-
- {/* Effects Panel (Trim Controls) */}
-
-
-
-
-
-
- {/* Geometry Panel (Hidden Easter Egg) */}
- {floatingPanelStates.geometry?.visible && (
-
-
-
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/Footer.tsx b/client/src/components/Footer.tsx
index aea911e..7e130f1 100644
--- a/client/src/components/Footer.tsx
+++ b/client/src/components/Footer.tsx
@@ -1,81 +1,68 @@
import { useEffect, useState, useRef } from 'react';
import { Tv } from 'lucide-react';
import { useStore } from '@/lib/store';
-
export function Footer() {
- const { brandSkin, queueItems, isPlaying } = useStore();
- const [fps, setFps] = useState(60);
- const [memoryInfo, setMemoryInfo] = useState(null);
- const fpsCounterRef = useRef<{ frameCount: number; lastTime: number }>({
- frameCount: 0,
- lastTime: performance.now()
- });
-
- // FPS counter with more stable implementation
- useEffect(() => {
- let animationFrame: number;
-
- const updateFPS = () => {
- const now = performance.now();
- const delta = now - fpsCounterRef.current.lastTime;
-
- if (delta >= 1000) {
- setFps(Math.round((fpsCounterRef.current.frameCount * 1000) / delta));
- fpsCounterRef.current.frameCount = 0;
- fpsCounterRef.current.lastTime = now;
- } else {
- fpsCounterRef.current.frameCount++;
- }
-
- animationFrame = requestAnimationFrame(updateFPS);
+ const { brandSkin, queueItems, isPlaying } = useStore();
+ const [fps, setFps] = useState(60);
+ const [memoryInfo, setMemoryInfo] = useState(null);
+ const fpsCounterRef = useRef<{
+ frameCount: number;
+ lastTime: number;
+ }>({
+ frameCount: 0,
+ lastTime: performance.now()
+ });
+ // FPS counter with more stable implementation
+ useEffect(() => {
+ let animationFrame: number;
+ const updateFPS = () => {
+ const now = performance.now();
+ const delta = now - fpsCounterRef.current.lastTime;
+ if (delta >= 1000) {
+ setFps(Math.round((fpsCounterRef.current.frameCount * 1000) / delta));
+ fpsCounterRef.current.frameCount = 0;
+ fpsCounterRef.current.lastTime = now;
+ }
+ else {
+ fpsCounterRef.current.frameCount++;
+ }
+ animationFrame = requestAnimationFrame(updateFPS);
+ };
+ animationFrame = requestAnimationFrame(updateFPS);
+ return () => {
+ if (animationFrame) {
+ cancelAnimationFrame(animationFrame);
+ }
+ };
+ }, []);
+ // Memory info (if available)
+ useEffect(() => {
+ if ('memory' in performance) {
+ setMemoryInfo((performance as any).memory);
+ }
+ const interval = setInterval(() => {
+ if ('memory' in performance) {
+ setMemoryInfo((performance as any).memory);
+ }
+ }, 5000);
+ return () => clearInterval(interval);
+ }, []);
+ const formatBytes = (bytes: number) => {
+ if (bytes === 0)
+ return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
};
-
- animationFrame = requestAnimationFrame(updateFPS);
-
- return () => {
- if (animationFrame) {
- cancelAnimationFrame(animationFrame);
- }
+ const buildInfo = {
+ version: '0.1.0-alpha',
+ build: '2025.01.11',
+ engine: 'FS9000',
+ audio: 'web.A.ap1',
+ video: 'htmlcsskewl.run'
};
- }, []);
-
- // Memory info (if available)
- useEffect(() => {
- if ('memory' in performance) {
- setMemoryInfo((performance as any).memory);
- }
-
- const interval = setInterval(() => {
- if ('memory' in performance) {
- setMemoryInfo((performance as any).memory);
- }
- }, 5000);
-
- return () => clearInterval(interval);
- }, []);
-
- const formatBytes = (bytes: number) => {
- if (bytes === 0) return '0 B';
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
- };
-
- const buildInfo = {
- version: '0.1.0-alpha',
- build: '2025.01.11',
- engine: 'FS9000',
- audio: 'web.A.ap1',
- video: 'htmlcsskewl.run'
- };
-
- return (
-
+ return (
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/KeyboardShortcuts.tsx b/client/src/components/KeyboardShortcuts.tsx
index 17fb1b1..d122ba2 100644
--- a/client/src/components/KeyboardShortcuts.tsx
+++ b/client/src/components/KeyboardShortcuts.tsx
@@ -4,226 +4,147 @@ import { Button } from '@/components/ui/button';
import { useStore } from '@/lib/store';
import { Kbd } from '@/components/ui/kbd';
import { toast } from '@/hooks/use-toast';
-
declare global {
- interface Window {
- showShortcuts: () => void;
- }
+ interface Window {
+ showShortcuts: () => void;
+ }
}
-
interface ShortcutGroup {
- title: string;
- shortcuts: {
- keys: string[];
- description: string;
- }[];
+ title: string;
+ shortcuts: {
+ keys: string[];
+ description: string;
+ }[];
}
-
const shortcutGroups: ShortcutGroup[] = [
- {
- title: 'Video Playback',
- shortcuts: [
- { keys: ['Space'], description: 'Play/Pause current video' },
- { keys: ['F'], description: 'Toggle fullscreen' },
- { keys: ['Esc'], description: 'Exit fullscreen' },
- { keys: ['←'], description: 'Skip back 10 seconds' },
- { keys: ['→'], description: 'Skip forward 10 seconds' },
- { keys: ['↑'], description: 'Increase volume' },
- { keys: ['↓'], description: 'Decrease volume' },
- ],
- },
- {
- title: 'Trim Controls',
- shortcuts: [
- { keys: ['Ctrl', 'T'], description: 'Toggle trim controls visibility (⌘+T on Mac)' },
- { keys: ['I'], description: 'Set trim in point' },
- { keys: ['O'], description: 'Set trim out point' },
- { keys: ['Ctrl', 'R'], description: 'Reset trim points' },
- ],
- },
- {
- title: 'Quick Effect Presets',
- shortcuts: [
- { keys: ['1'], description: 'Apply Cyberpunk preset' },
- { keys: ['2'], description: 'Apply Vintage preset' },
- { keys: ['3'], description: 'Apply Glitch preset' },
- { keys: ['4'], description: 'Apply Film Noir preset' },
- { keys: ['5'], description: 'Apply Video Vortex preset' },
- { keys: ['6'], description: 'Apply Demonic Portal preset' },
- { keys: ['7'], description: 'Apply Fractal Storm preset' },
- { keys: ['8'], description: 'Apply Time Warp preset' },
- ],
- },
- {
- title: 'Navigation & Help',
- shortcuts: [
- { keys: ['?'], description: 'Show this keyboard shortcuts panel' },
- { keys: ['/'], description: 'Focus search input' },
- { keys: ['Ctrl', 'K'], description: 'Open command palette' },
- { keys: ['Ctrl', 'P'], description: 'Pop-out player window' },
- { keys: ['Esc'], description: 'Close dialogs and panels' },
- ],
- },
- {
- title: 'Hidden Features 🥚',
- shortcuts: [
- { keys: ['bb'], description: 'Acid Trip Mode (type quickly)' },
- { keys: ['etc'], description: 'Unlock Geometry Panel (type anywhere)' },
- { keys: ['Ctrl', 'Shift', 'A'], description: 'Toggle ASCII Terminal Mode (⌘+Shift+A on Mac)' },
- ],
- },
+ {
+ title: 'Video Playback',
+ shortcuts: [
+ { keys: ['Space'], description: 'Play/Pause current video' },
+ { keys: ['F'], description: 'Toggle fullscreen' },
+ { keys: ['Esc'], description: 'Exit fullscreen' },
+ { keys: ['←'], description: 'Skip back 10 seconds' },
+ { keys: ['→'], description: 'Skip forward 10 seconds' },
+ { keys: ['↑'], description: 'Increase volume' },
+ { keys: ['↓'], description: 'Decrease volume' },
+ ],
+ },
+ {
+ title: 'Trim Controls',
+ shortcuts: [
+ { keys: ['Ctrl', 'T'], description: 'Toggle trim controls visibility (⌘+T on Mac)' },
+ { keys: ['I'], description: 'Set trim in point' },
+ { keys: ['O'], description: 'Set trim out point' },
+ { keys: ['Ctrl', 'R'], description: 'Reset trim points' },
+ ],
+ },
+ {
+ title: 'Quick Effect Presets',
+ shortcuts: [
+ { keys: ['1'], description: 'Apply Cyberpunk preset' },
+ { keys: ['2'], description: 'Apply Vintage preset' },
+ { keys: ['3'], description: 'Apply Glitch preset' },
+ { keys: ['4'], description: 'Apply Film Noir preset' },
+ { keys: ['5'], description: 'Apply Video Vortex preset' },
+ { keys: ['6'], description: 'Apply Demonic Portal preset' },
+ { keys: ['7'], description: 'Apply Fractal Storm preset' },
+ { keys: ['8'], description: 'Apply Time Warp preset' },
+ ],
+ },
+ {
+ title: 'Navigation & Help',
+ shortcuts: [
+ { keys: ['?'], description: 'Show this keyboard shortcuts panel' },
+ { keys: ['/'], description: 'Focus search input' },
+ { keys: ['Ctrl', 'K'], description: 'Open command palette' },
+ { keys: ['Ctrl', 'P'], description: 'Pop-out player window' },
+ { keys: ['Esc'], description: 'Close dialogs and panels' },
+ ],
+ },
];
-
export function KeyboardShortcuts() {
- const [isOpen, setIsOpen] = useState(false);
- const [typingSequence, setTypingSequence] = useState('');
- const [lastTypingTime, setLastTypingTime] = useState(0);
- const { brandSkin, isAsciiMode, setAsciiMode, setFloatingPanelVisible } = useStore();
-
- useEffect(() => {
- const handleKeyDown = (event: KeyboardEvent) => {
- // Don't trigger shortcuts when typing in inputs
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
- return;
- }
-
- // Handle typing sequence for "etc" easter egg
- const currentTime = Date.now();
- if (currentTime - lastTypingTime > 2000) {
- setTypingSequence(''); // Reset if too much time has passed
- }
- setLastTypingTime(currentTime);
-
- // Only track single letter keys (no modifiers)
- if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
- const newSequence = typingSequence + event.key.toLowerCase();
- setTypingSequence(newSequence);
-
- // Check for "etc" sequence
- if (newSequence.endsWith('etc')) {
- event.preventDefault();
- setFloatingPanelVisible('geometry', true);
- setTypingSequence(''); // Reset sequence
- toast({
- title: "🔓 Easter Egg Unlocked!",
- description: "Geometry Panel is now available",
- });
- }
-
- // Keep sequence manageable (last 10 chars)
- if (newSequence.length > 10) {
- setTypingSequence(newSequence.slice(-10));
- }
- }
-
- if (event.key === '?' && !event.shiftKey) {
- event.preventDefault();
- setIsOpen(true);
- }
-
- if (event.key === 'Escape') {
- setIsOpen(false);
- // Also exit ASCII mode on Escape
- if (isAsciiMode) {
- setAsciiMode(false);
+ const [isOpen, setIsOpen] = useState(false);
+ const { brandSkin } = useStore();
+ useEffect(() => {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ // Don't trigger shortcuts when typing in inputs
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
+ return;
+ }
+ if (event.key === '?' && !event.shiftKey) {
+ event.preventDefault();
+ setIsOpen(true);
+ }
+ if (event.key === 'Escape') {
+ setIsOpen(false);
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, []);
+ // Global keyboard shortcut handler
+ useEffect(() => {
+ const handleGlobalShortcuts = (event: KeyboardEvent) => {
+ // Don't trigger shortcuts when typing in inputs or when dialog is open
+ if (event.target instanceof HTMLInputElement ||
+ event.target instanceof HTMLTextAreaElement ||
+ isOpen) {
+ return;
+ }
+ // Command palette - handled by useCommandPalette hook in App component
+ // No need to handle here as it's managed globally
+ // Search focus
+ if (event.key === '/') {
+ event.preventDefault();
+ const searchInput = document.querySelector('[data-testid="input-search"]') as HTMLInputElement;
+ if (searchInput) {
+ searchInput.focus();
+ }
+ }
+ };
+ window.addEventListener('keydown', handleGlobalShortcuts);
+ return () => window.removeEventListener('keydown', handleGlobalShortcuts);
+ }, [isOpen]);
+ const getThemeClasses = () => {
+ {
+ return 'bg-gray-900/95 border-lime-500/50';
}
- }
-
- // ASCII Terminal Mode: Ctrl+Shift+A (or Cmd+Shift+A on Mac)
- if (event.key === 'a' && (event.ctrlKey || event.metaKey) && event.shiftKey) {
- event.preventDefault();
- setAsciiMode(!isAsciiMode);
- }
- };
-
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [typingSequence, lastTypingTime, isAsciiMode]);
-
- // Global keyboard shortcut handler
- useEffect(() => {
- const handleGlobalShortcuts = (event: KeyboardEvent) => {
- // Don't trigger shortcuts when typing in inputs or when dialog is open
- if (
- event.target instanceof HTMLInputElement ||
- event.target instanceof HTMLTextAreaElement ||
- isOpen
- ) {
- return;
- }
-
- // Command palette - handled by useCommandPalette hook in App component
- // No need to handle here as it's managed globally
-
- // Search focus
- if (event.key === '/') {
- event.preventDefault();
- const searchInput = document.querySelector('[data-testid="input-search"]') as HTMLInputElement;
- if (searchInput) {
- searchInput.focus();
- }
- }
};
-
- window.addEventListener('keydown', handleGlobalShortcuts);
- return () => window.removeEventListener('keydown', handleGlobalShortcuts);
- }, [isOpen]);
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'waffle':
- return 'bg-yellow-50/95 border-yellow-400/50';
- case 'ebn':
- return 'bg-gray-900/95 border-lime-500/50';
- case 'ozzy':
- return 'bg-red-950/95 border-red-500/50';
- default:
- return 'bg-white/95 dark:bg-gray-900/95 border-gray-200 dark:border-gray-700';
- }
- };
-
- return (
- <>
+ return (<>
⌨️ Keyboard Shortcuts
-
+
- {shortcutGroups.map((group) => (
-
+ {shortcutGroups.map((group) => (
{group.title}
- {group.shortcuts.map((shortcut, index) => (
-
+ {group.shortcuts.map((shortcut, index) => (
{shortcut.description}
-
-
- ))}
+
+ ))}
-
- ))}
+
))}
Pro Tip: Effect presets (1-8) work globally and show confirmation toasts.
Video controls work when a video is loaded. Trim controls (Ctrl+T, I, O) help set precise in/out points.
- Press to close dialogs. Shortcuts are disabled while typing in form inputs.
+ Press to close dialogs. Shortcuts are disabled while typing in form inputs.
- >
- );
+ >);
}
-
// Make shortcuts available globally
window.showShortcuts = () => {
- const event = new KeyboardEvent('keydown', { key: '?' });
- window.dispatchEvent(event);
-};
\ No newline at end of file
+ const event = new KeyboardEvent('keydown', { key: '?' });
+ window.dispatchEvent(event);
+};
diff --git a/client/src/components/LayoutAnimationWrapper.tsx b/client/src/components/LayoutAnimationWrapper.tsx
deleted file mode 100644
index 2be008c..0000000
--- a/client/src/components/LayoutAnimationWrapper.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { useEffect, useState } from 'react';
-import { useResponsiveLayout } from '@/hooks/use-responsive-layout';
-import { useStore } from '@/lib/store';
-
-interface LayoutAnimationWrapperProps {
- children: React.ReactNode;
- panelType?: 'search' | 'preview' | 'queue' | 'effects';
-}
-
-export function LayoutAnimationWrapper({ children, panelType }: LayoutAnimationWrapperProps) {
- const { currentLayout, isTransitioning } = useResponsiveLayout();
- const { brandSkin } = useStore();
- const [animationKey, setAnimationKey] = useState(0);
-
- // Trigger re-animation when layout changes
- useEffect(() => {
- if (isTransitioning) {
- setAnimationKey(prev => prev + 1);
- }
- }, [isTransitioning, currentLayout]);
-
- const getAnimationClass = () => {
- if (!isTransitioning) return '';
-
- switch (currentLayout) {
- case 'mobile':
- return 'animate-fade-in';
- case 'tablet':
- return 'animate-slide-in-left';
- case 'desktop':
- default:
- return 'animate-scale-in';
- }
- };
-
- const getLayoutSpecificClass = () => {
- const base = 'transition-all duration-300';
-
- switch (currentLayout) {
- case 'mobile':
- return `${base} w-full mb-2`;
- case 'tablet':
- return `${base} ${panelType === 'search' ? 'w-1/3' : panelType === 'preview' ? 'flex-1' : 'w-1/4'}`;
- case 'desktop':
- default:
- return base;
- }
- };
-
- return (
-
- {children}
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/LayoutControls.tsx b/client/src/components/LayoutControls.tsx
deleted file mode 100644
index 61ed864..0000000
--- a/client/src/components/LayoutControls.tsx
+++ /dev/null
@@ -1,123 +0,0 @@
-import { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
-import { Card } from '@/components/ui/card';
-import { Smartphone, Tablet, Monitor, Layout, Info, Settings } from 'lucide-react';
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
-import { useResponsiveLayout } from '@/hooks/use-responsive-layout';
-import { useToast } from '@/hooks/use-toast';
-
-export function LayoutControls() {
- const [showInfo, setShowInfo] = useState(false);
- const { currentLayout, setManualLayout } = useResponsiveLayout();
- const { toast } = useToast();
-
- const layouts = [
- { id: 'mobile', icon: Smartphone, label: 'Mobile', width: '< 768px' },
- { id: 'tablet', icon: Tablet, label: 'Tablet', width: '768-1024px' },
- { id: 'desktop', icon: Monitor, label: 'Desktop', width: '> 1024px' }
- ] as const;
-
- const switchLayout = (layoutId: typeof layouts[number]['id']) => {
- setManualLayout(layoutId);
- toast({
- title: `Switched to ${layoutId} layout`,
- description: `Interface adapted for ${layoutId} view`,
- duration: 2000,
- });
- };
-
- const resetToAuto = () => {
- const width = window.innerWidth;
- const autoLayout = width < 768 ? 'mobile' : width < 1024 ? 'tablet' : 'desktop';
- setManualLayout(autoLayout);
-
- toast({
- title: "Layout reset to automatic",
- description: "Now responding to actual screen size",
- duration: 2000,
- });
- };
-
- return (
- <>
-
-
-
-
-
- {currentLayout}
-
- setShowInfo(true)}
- className="h-6 px-2 text-white/60 hover:text-white"
- data-testid="button-layout-info"
- >
-
-
-
-
-
- {layouts.map(({ id, icon: Icon }) => (
- switchLayout(id)}
- className="h-7 px-2 text-white/80 hover:text-white hover:bg-white/20"
- data-testid={`button-layout-${id}`}
- >
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
- Responsive Layout
-
-
-
-
- The interface automatically adapts to your screen size. Use these controls to test different layouts.
-
-
-
- {layouts.map(({ id, icon: Icon, label, width }) => (
-
-
-
- {currentLayout === id && (
-
Current
- )}
-
- ))}
-
-
-
-
• Layout changes auto-revert after 8 seconds
-
• Use Settings button to return to auto-detection
-
-
-
-
- >
- );
-}
\ No newline at end of file
diff --git a/client/src/components/LayoutDemonstrator.tsx b/client/src/components/LayoutDemonstrator.tsx
deleted file mode 100644
index dfe992d..0000000
--- a/client/src/components/LayoutDemonstrator.tsx
+++ /dev/null
@@ -1,286 +0,0 @@
-import { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
-import { Badge } from '@/components/ui/badge';
-import { Smartphone, Tablet, Monitor, Play, X, RotateCcw } from 'lucide-react';
-import { useResponsiveLayout } from '@/hooks/use-responsive-layout';
-import { useStore } from '@/lib/store';
-import { useToast } from '@/hooks/use-toast';
-
-export function LayoutDemonstrator() {
- const [isOpen, setIsOpen] = useState(false);
- const [demoLayout, setDemoLayout] = useState<'mobile' | 'tablet' | 'desktop'>('desktop');
- const [isAnimating, setIsAnimating] = useState(false);
- const { setManualLayout, currentLayout } = useResponsiveLayout();
- const { brandSkin } = useStore();
- const { toast } = useToast();
-
- const layouts = {
- mobile: {
- icon: Smartphone,
- title: 'Mobile Layout',
- description: 'Single column, stacked interface',
- width: '375px',
- features: ['Touch optimized', 'Vertical stack', 'Full width panels'],
- demoClass: 'w-[300px] h-[500px] bg-gradient-to-b from-blue-100 to-blue-200 rounded-lg p-4'
- },
- tablet: {
- icon: Tablet,
- title: 'Tablet Layout',
- description: 'Dual column adaptive interface',
- width: '768px',
- features: ['Side navigation', '2-column grid', 'Medium density'],
- demoClass: 'w-[400px] h-[300px] bg-gradient-to-b from-orange-100 to-orange-200 rounded-lg p-4'
- },
- desktop: {
- icon: Monitor,
- title: 'Desktop Layout',
- description: 'Full multi-panel workspace',
- width: '1024px+',
- features: ['Resizable panels', '3+ columns', 'High density'],
- demoClass: 'w-[500px] h-[350px] bg-gradient-to-b from-green-100 to-green-200 rounded-lg p-4'
- }
- };
-
- const runLayoutDemo = async () => {
- setIsAnimating(true);
-
- const sequence: ('mobile' | 'tablet' | 'desktop')[] = ['mobile', 'tablet', 'desktop'];
-
- for (const layout of sequence) {
- setDemoLayout(layout);
- setManualLayout(layout);
-
- toast({
- title: `Demonstrating ${layout} layout`,
- description: `Interface adapting to ${layout} breakpoint`,
- duration: 1500,
- });
-
- // Wait 2 seconds before next layout
- await new Promise(resolve => setTimeout(resolve, 2000));
- }
-
- // Reset to actual layout
- const actualWidth = window.innerWidth;
- const autoLayout = actualWidth < 768 ? 'mobile' : actualWidth < 1024 ? 'tablet' : 'desktop';
- setManualLayout(autoLayout);
- setDemoLayout(autoLayout);
- setIsAnimating(false);
-
- toast({
- title: "Layout demo complete",
- description: "Returned to automatic detection",
- duration: 2000,
- });
- };
-
- const resetDemo = () => {
- const actualWidth = window.innerWidth;
- const autoLayout = actualWidth < 768 ? 'mobile' : actualWidth < 1024 ? 'tablet' : 'desktop';
- setManualLayout(autoLayout);
- setDemoLayout(autoLayout);
- setIsAnimating(false);
- };
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'bg-slate-900/95 border-blue-400/60 text-blue-100';
- case 'waffle':
- return 'bg-amber-50/95 border-amber-500/60 text-amber-900';
- case 'ebn':
- return 'bg-gray-900/95 border-lime-400/60 text-lime-100';
- case 'ozzy':
- return 'bg-red-950/95 border-red-400/60 text-red-100';
- case 'hogan':
- return 'bg-yellow-950/95 border-yellow-400/60 text-yellow-100';
- case 'dx':
- return 'bg-pink-950/95 border-pink-400/60 text-pink-100';
- case 'maxheadroom':
- return 'bg-green-950/95 border-green-400/60 text-green-100';
- case 'mario':
- return 'bg-red-950/95 border-yellow-400/60 text-yellow-100';
- case 'dakota':
- return 'bg-gray-900/95 border-gray-400/60 text-gray-100';
- case 'blondie':
- return 'bg-amber-950/95 border-amber-400/60 text-amber-100';
- default:
- return 'bg-slate-900/95 border-blue-400/60 text-blue-100';
- }
- };
-
- return (
- <>
- setIsOpen(true)}
- variant="outline"
- size="sm"
- className="fixed bottom-16 left-4 z-40"
- data-testid="button-layout-demo"
- >
-
- Layout Demo
-
-
-
-
-
-
- Interactive Layout Demonstration
- setIsOpen(false)}>
-
-
-
-
-
-
- {/* Demo Controls */}
-
-
- Layout Demo Controls
-
-
-
-
- {isAnimating ? (
- <>
-
- Running Demo...
- >
- ) : (
- <>
-
- Run Full Demo
- >
- )}
-
-
-
-
- Reset
-
-
-
-
- Current Layout: {demoLayout}
- {isAnimating && • Demo Running }
-
-
- {/* Individual Layout Buttons */}
-
- {(Object.keys(layouts) as Array
).map((layout) => {
- const config = layouts[layout];
- const Icon = config.icon;
- const isActive = demoLayout === layout;
-
- return (
- {
- setDemoLayout(layout);
- setManualLayout(layout);
- }}
- disabled={isAnimating}
- className="flex flex-col items-center p-3 h-auto"
- data-testid={`button-demo-${layout}`}
- >
-
-
-
{layout}
-
{config.width}
-
-
- );
- })}
-
-
-
-
- {/* Current Layout Preview */}
-
-
-
- Current Preview: {layouts[demoLayout].title}
-
-
-
-
- {/* Layout Mockup */}
-
-
-
- {(() => {
- const IconComponent = layouts[demoLayout].icon;
- return
;
- })()}
-
{layouts[demoLayout].title}
-
{layouts[demoLayout].width}
-
-
-
-
- {/* Layout Details */}
-
-
-
Description
-
{layouts[demoLayout].description}
-
-
-
-
Key Features
-
- {layouts[demoLayout].features.map((feature, index) => (
-
- {feature}
-
- ))}
-
-
-
-
-
Breakpoint Range
-
- {demoLayout === 'mobile' && '< 768px'}
- {demoLayout === 'tablet' && '768px - 1024px'}
- {demoLayout === 'desktop' && '> 1024px'}
-
-
-
-
-
-
-
- {/* Instructions */}
-
-
- How to Use
-
-
-
-
• Run Full Demo: Automatically cycles through all layouts
-
• Individual Buttons: Switch to specific layout immediately
-
• Reset: Return to automatic screen-size detection
-
• Live Preview: See how the interface adapts in real-time
-
-
-
-
-
-
- >
- );
-}
\ No newline at end of file
diff --git a/client/src/components/LicenseBadge.tsx b/client/src/components/LicenseBadge.tsx
index ded5980..a64d544 100644
--- a/client/src/components/LicenseBadge.tsx
+++ b/client/src/components/LicenseBadge.tsx
@@ -1,110 +1,71 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { ExternalLink } from 'lucide-react';
import { useStore } from '@/lib/store';
-
interface LicenseBadgeProps {
- license?: string;
- className?: string;
- showTooltip?: boolean;
+ license?: string;
+ className?: string;
+ showTooltip?: boolean;
}
-
export function LicenseBadge({ license, className = '', showTooltip = true }: LicenseBadgeProps) {
- const { brandSkin } = useStore();
-
- if (!license) return null;
-
- const getThemeColors = (baseType: string) => {
- switch (brandSkin) {
- case 'waffle':
- return baseType === 'pd'
- ? 'bg-green-200 text-green-900 border-green-400 dark:bg-green-900/30 dark:text-green-200 dark:border-green-600'
- : baseType === 'cc0'
- ? 'bg-blue-200 text-blue-900 border-blue-400 dark:bg-blue-900/30 dark:text-blue-200 dark:border-blue-600'
- : baseType === 'ccby'
- ? 'bg-orange-200 text-orange-900 border-orange-400 dark:bg-orange-900/30 dark:text-orange-200 dark:border-orange-600'
- : 'bg-amber-200 text-amber-900 border-amber-400 dark:bg-amber-900/30 dark:text-amber-200 dark:border-amber-600';
- case 'ebn':
- return baseType === 'pd'
- ? 'bg-lime-900/40 text-lime-300 border-lime-500 hover:bg-lime-900/60'
- : baseType === 'cc0'
- ? 'bg-cyan-900/40 text-cyan-300 border-cyan-500 hover:bg-cyan-900/60'
- : baseType === 'ccby'
- ? 'bg-purple-900/40 text-purple-300 border-purple-500 hover:bg-purple-900/60'
- : 'bg-yellow-900/40 text-yellow-300 border-yellow-500 hover:bg-yellow-900/60';
- case 'ozzy':
- return baseType === 'pd'
- ? 'bg-green-900/50 text-green-300 border-green-500 hover:bg-green-900/70'
- : baseType === 'cc0'
- ? 'bg-blue-900/50 text-blue-300 border-blue-500 hover:bg-blue-900/70'
- : baseType === 'ccby'
- ? 'bg-red-900/50 text-red-300 border-red-500 hover:bg-red-900/70'
- : 'bg-gray-900/50 text-gray-300 border-gray-500 hover:bg-gray-900/70';
- default:
- return baseType === 'pd'
- ? 'bg-green-100 text-green-800 border-green-300 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700'
- : baseType === 'cc0'
- ? 'bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700'
- : baseType === 'ccby'
- ? 'bg-orange-100 text-orange-800 border-orange-300 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-700'
- : 'bg-gray-100 text-gray-800 border-gray-300 dark:bg-gray-900/30 dark:text-gray-300 dark:border-gray-700';
- }
- };
-
- const getLicenseInfo = (licenseUrl: string) => {
- const url = licenseUrl.toLowerCase();
- if (url.includes('publicdomain')) {
- return {
- label: 'PD',
- fullName: 'Public Domain',
- color: getThemeColors('pd'),
- description: 'This work is in the public domain and free of known copyright restrictions.',
- url: licenseUrl
- };
- }
- if (url.includes('cc0')) {
- return {
- label: 'CC0',
- fullName: 'Creative Commons Zero',
- color: getThemeColors('cc0'),
- description: 'The creator has waived all copyright and related rights.',
- url: licenseUrl
- };
- }
- if (url.includes('by')) {
- return {
- label: 'CC-BY',
- fullName: 'Creative Commons Attribution',
- color: getThemeColors('ccby'),
- description: 'You can use this work if you provide proper attribution.',
- url: licenseUrl
- };
- }
- return {
- label: 'Other',
- fullName: 'Other License',
- color: getThemeColors('other'),
- description: 'Please review the license terms before use.',
- url: licenseUrl
+ const { brandSkin } = useStore();
+ if (!license)
+ return null;
+ const getThemeColors = (baseType: string) => {
+ {
+ return baseType === 'pd'
+ ? 'bg-lime-900/40 text-lime-300 border-lime-500 hover:bg-lime-900/60'
+ : baseType === 'cc0'
+ ? 'bg-cyan-900/40 text-cyan-300 border-cyan-500 hover:bg-cyan-900/60'
+ : baseType === 'ccby'
+ ? 'bg-purple-900/40 text-purple-300 border-purple-500 hover:bg-purple-900/60'
+ : 'bg-yellow-900/40 text-yellow-300 border-yellow-500 hover:bg-yellow-900/60';
+ }
+ };
+ const getLicenseInfo = (licenseUrl: string) => {
+ const url = licenseUrl.toLowerCase();
+ if (url.includes('publicdomain')) {
+ return {
+ label: 'PD',
+ fullName: 'Public Domain',
+ color: getThemeColors('pd'),
+ description: 'This work is in the public domain and free of known copyright restrictions.',
+ url: licenseUrl
+ };
+ }
+ if (url.includes('cc0')) {
+ return {
+ label: 'CC0',
+ fullName: 'Creative Commons Zero',
+ color: getThemeColors('cc0'),
+ description: 'The creator has waived all copyright and related rights.',
+ url: licenseUrl
+ };
+ }
+ if (url.includes('by')) {
+ return {
+ label: 'CC-BY',
+ fullName: 'Creative Commons Attribution',
+ color: getThemeColors('ccby'),
+ description: 'You can use this work if you provide proper attribution.',
+ url: licenseUrl
+ };
+ }
+ return {
+ label: 'Other',
+ fullName: 'Other License',
+ color: getThemeColors('other'),
+ description: 'Please review the license terms before use.',
+ url: licenseUrl
+ };
};
- };
-
- const licenseInfo = getLicenseInfo(license);
-
- const badge = (
-
+ const licenseInfo = getLicenseInfo(license);
+ const badge = (
{licenseInfo.label}
-
- );
-
- if (!showTooltip) {
- return badge;
- }
-
- return (
-
+ );
+ if (!showTooltip) {
+ return badge;
+ }
+ return (
{badge}
@@ -116,20 +77,13 @@ export function LicenseBadge({ license, className = '', showTooltip = true }: Li
{licenseInfo.description}
-
- );
+ );
}
diff --git a/client/src/components/LicenseGuardrail.tsx b/client/src/components/LicenseGuardrail.tsx
index 9ee1818..e34121c 100644
--- a/client/src/components/LicenseGuardrail.tsx
+++ b/client/src/components/LicenseGuardrail.tsx
@@ -6,151 +6,62 @@ import { AlertTriangle, Info, Shield, ShieldOff } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from '@/components/ui/dialog';
import { useStore } from '@/lib/store';
import { useToast } from '@/hooks/use-toast';
-
interface LicenseGuardrailProps {
- onLicenseChange: () => void;
+ onLicenseChange: () => void;
}
-
export function LicenseGuardrail({ onLicenseChange }: LicenseGuardrailProps) {
- const { searchState, setSearchState, brandSkin } = useStore();
- const { toast } = useToast();
- const [showDisclaimer, setShowDisclaimer] = useState(false);
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return {
- text: 'text-blue-400',
- warning: 'text-blue-300',
- bg: 'bg-blue-400/10',
- border: 'border-blue-400/30'
- };
- case 'waffle':
- return {
- text: 'text-amber-800',
- warning: 'text-amber-700',
- bg: 'bg-yellow-100/50',
- border: 'border-yellow-400/30'
- };
- case 'ebn':
- return {
- text: 'text-lime-400',
- warning: 'text-lime-300',
- bg: 'bg-lime-900/50',
- border: 'border-lime-500/30'
- };
- case 'ozzy':
- return {
- text: 'text-red-300',
- warning: 'text-red-200',
- bg: 'bg-red-900/30',
- border: 'border-red-500/30'
- };
- case 'hogan':
- return {
- text: 'text-yellow-300',
- warning: 'text-yellow-200',
- bg: 'bg-yellow-900/50',
- border: 'border-yellow-400/30'
- };
- case 'dx':
- return {
- text: 'text-pink-300',
- warning: 'text-pink-200',
- bg: 'bg-pink-900/50',
- border: 'border-pink-500/30'
- };
- case 'maxheadroom':
- return {
- text: 'text-green-300',
- warning: 'text-green-200',
- bg: 'bg-green-900/50',
- border: 'border-green-500/30'
- };
- case 'mario':
- return {
- text: 'text-yellow-300',
- warning: 'text-yellow-200',
- bg: 'bg-red-900/50',
- border: 'border-yellow-400/30'
- };
- case 'dakota':
- return {
- text: 'text-gray-300',
- warning: 'text-gray-200',
- bg: 'bg-gray-800/50',
- border: 'border-gray-400/30'
- };
- case 'blondie':
- return {
- text: 'text-amber-300',
- warning: 'text-amber-200',
- bg: 'bg-amber-900/50',
- border: 'border-amber-400/30'
- };
- default:
- return {
- text: 'text-blue-400',
- warning: 'text-blue-300',
- bg: 'bg-blue-400/10',
- border: 'border-blue-400/30'
- };
- }
- };
-
- const theme = getThemeClasses();
-
- const handleToggleRestricted = (enabled: boolean) => {
- if (enabled) {
- setShowDisclaimer(true);
- } else {
- setSearchState({
- allowRestrictedLicenses: false,
- license: searchState.license === 'restricted' ? 'all' : searchState.license,
- page: 1
- });
- onLicenseChange();
-
- toast({
- title: "License Guardrail Enabled",
- description: "Only Public Domain, CC0, and CC-BY content will be shown.",
- });
- }
- };
-
- const confirmRestrictedAccess = () => {
- setSearchState({ allowRestrictedLicenses: true, page: 1 });
- setShowDisclaimer(false);
- onLicenseChange();
-
- toast({
- title: "Restricted Licenses Enabled",
- description: "CC-NC and CC-ND content is now included. Check licenses before use.",
- variant: "destructive",
- });
- };
-
- return (
-
+ const { searchState, setSearchState, brandSkin } = useStore();
+ const { toast } = useToast();
+ const [showDisclaimer, setShowDisclaimer] = useState(false);
+ const getThemeClasses = () => {
+ {
+ return {
+ text: 'text-lime-400',
+ warning: 'text-lime-300',
+ bg: 'bg-lime-900/50',
+ border: 'border-lime-500/30'
+ };
+ }
+ };
+ const theme = getThemeClasses();
+ const handleToggleRestricted = (enabled: boolean) => {
+ if (enabled) {
+ setShowDisclaimer(true);
+ }
+ else {
+ setSearchState({
+ allowRestrictedLicenses: false,
+ license: searchState.license === 'restricted' ? 'all' : searchState.license,
+ page: 1
+ });
+ onLicenseChange();
+ toast({
+ title: "License Guardrail Enabled",
+ description: "Only Public Domain, CC0, and CC-BY content will be shown.",
+ });
+ }
+ };
+ const confirmRestrictedAccess = () => {
+ setSearchState({ allowRestrictedLicenses: true, page: 1 });
+ setShowDisclaimer(false);
+ onLicenseChange();
+ toast({
+ title: "Restricted Licenses Enabled",
+ description: "CC-NC and CC-ND content is now included. Check licenses before use.",
+ variant: "destructive",
+ });
+ };
+ return (
- {searchState.allowRestrictedLicenses ? (
-
- ) : (
-
- )}
- handleToggleRestricted(!checked)}
- data-testid="switch-license-guardrail"
- />
+ {searchState.allowRestrictedLicenses ? ( ) : ( )}
+ handleToggleRestricted(!checked)} data-testid="switch-license-guardrail"/>
-
+
License Warning
@@ -176,17 +87,10 @@ export function LicenseGuardrail({ onLicenseChange }: LicenseGuardrailProps) {
- setShowDisclaimer(false)}
- className={theme.border}
- >
+ setShowDisclaimer(false)} className={theme.border}>
Cancel
-
+
I Understand - Enable
@@ -196,13 +100,8 @@ export function LicenseGuardrail({ onLicenseChange }: LicenseGuardrailProps) {
-
-
+
+
@@ -240,6 +139,5 @@ export function LicenseGuardrail({ onLicenseChange }: LicenseGuardrailProps) {
-
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/LiveVideoMode.tsx b/client/src/components/LiveVideoMode.tsx
index edb9651..9454f36 100644
--- a/client/src/components/LiveVideoMode.tsx
+++ b/client/src/components/LiveVideoMode.tsx
@@ -4,225 +4,175 @@ import { useStore } from '@/lib/store';
import { useToast } from '@/hooks/use-toast';
import { Camera, CameraOff, Settings, Monitor, Square } from 'lucide-react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-
interface CameraDevice {
- deviceId: string;
- label: string;
+ deviceId: string;
+ label: string;
}
-
export function LiveVideoMode() {
- const videoRef = useRef(null);
- const streamRef = useRef(null);
- const [isLiveMode, setIsLiveMode] = useState(false);
- const [cameras, setCameras] = useState([]);
- const [selectedCamera, setSelectedCamera] = useState('');
- const [isLoading, setIsLoading] = useState(false);
- const { toast } = useToast();
- const { brandSkin, videoEffects, addToQueue, setLiveStream, liveStream } = useStore();
-
- // Get available cameras
- useEffect(() => {
- const getCameras = async () => {
- try {
- const devices = await navigator.mediaDevices.enumerateDevices();
- const videoDevices = devices
- .filter(device => device.kind === 'videoinput')
- .map(device => ({
- deviceId: device.deviceId,
- label: device.label || `Camera ${device.deviceId.slice(0, 8)}`
- }));
-
- setCameras(videoDevices);
- if (videoDevices.length > 0 && !selectedCamera) {
- setSelectedCamera(videoDevices[0].deviceId);
+ const videoRef = useRef(null);
+ const streamRef = useRef(null);
+ const [isLiveMode, setIsLiveMode] = useState(false);
+ const [cameras, setCameras] = useState([]);
+ const [selectedCamera, setSelectedCamera] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const { toast } = useToast();
+ const { brandSkin, videoEffects, addToQueue, setLiveStream, liveStream } = useStore();
+ // Get available cameras
+ useEffect(() => {
+ const getCameras = async () => {
+ try {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const videoDevices = devices
+ .filter(device => device.kind === 'videoinput')
+ .map(device => ({
+ deviceId: device.deviceId,
+ label: device.label || `Camera ${device.deviceId.slice(0, 8)}`
+ }));
+ setCameras(videoDevices);
+ if (videoDevices.length > 0 && !selectedCamera) {
+ setSelectedCamera(videoDevices[0].deviceId);
+ }
+ }
+ catch (error) {
+ console.error('Error enumerating devices:', error);
+ }
+ };
+ getCameras();
+ }, [selectedCamera]);
+ const startLiveVideo = async () => {
+ if (!videoRef.current)
+ return;
+ setIsLoading(true);
+ try {
+ // Stop existing stream
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ }
+ // Request camera access
+ const constraints = {
+ video: {
+ deviceId: selectedCamera ? { exact: selectedCamera } : undefined,
+ width: { ideal: 1920 },
+ height: { ideal: 1080 },
+ frameRate: { ideal: 30 }
+ },
+ audio: true
+ };
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
+ streamRef.current = stream;
+ videoRef.current.srcObject = stream;
+ // Store the stream in global state so Player can access it
+ setLiveStream(stream, true, selectedCamera);
+ await videoRef.current.play();
+ setIsLiveMode(true);
+ toast({
+ title: "📹 Live Video Active",
+ description: "Webcam feed is now streaming with real-time effects",
+ duration: 3000,
+ });
+ }
+ catch (error) {
+ console.error('Error starting live video:', error);
+ let errorMessage = "Could not access camera";
+ if (error instanceof Error) {
+ if (error.name === 'NotAllowedError') {
+ errorMessage = "Camera permission denied. Please allow camera access.";
+ }
+ else if (error.name === 'NotFoundError') {
+ errorMessage = "No camera found. Please connect a camera.";
+ }
+ else if (error.name === 'NotReadableError') {
+ errorMessage = "Camera is being used by another application.";
+ }
+ }
+ toast({
+ title: "❌ Camera Error",
+ description: errorMessage,
+ duration: 5000,
+ });
+ }
+ finally {
+ setIsLoading(false);
}
- } catch (error) {
- console.error('Error enumerating devices:', error);
- }
};
-
- getCameras();
- }, [selectedCamera]);
-
- const startLiveVideo = async () => {
- if (!videoRef.current) return;
-
- setIsLoading(true);
-
- try {
- // Stop existing stream
- if (streamRef.current) {
- streamRef.current.getTracks().forEach(track => track.stop());
- }
-
- // Request camera access
- const constraints = {
- video: {
- deviceId: selectedCamera ? { exact: selectedCamera } : undefined,
- width: { ideal: 1920 },
- height: { ideal: 1080 },
- frameRate: { ideal: 30 }
- },
- audio: true
- };
-
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
- streamRef.current = stream;
- videoRef.current.srcObject = stream;
-
- // Store the stream in global state so Player can access it
- setLiveStream(stream, true, selectedCamera);
-
- await videoRef.current.play();
- setIsLiveMode(true);
-
- toast({
- title: "📹 Live Video Active",
- description: "Webcam feed is now streaming with real-time effects",
- duration: 3000,
- });
-
- } catch (error) {
- console.error('Error starting live video:', error);
-
- let errorMessage = "Could not access camera";
- if (error instanceof Error) {
- if (error.name === 'NotAllowedError') {
- errorMessage = "Camera permission denied. Please allow camera access.";
- } else if (error.name === 'NotFoundError') {
- errorMessage = "No camera found. Please connect a camera.";
- } else if (error.name === 'NotReadableError') {
- errorMessage = "Camera is being used by another application.";
+ const stopLiveVideo = () => {
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ streamRef.current = null;
}
- }
-
- toast({
- title: "❌ Camera Error",
- description: errorMessage,
- duration: 5000,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- const stopLiveVideo = () => {
- if (streamRef.current) {
- streamRef.current.getTracks().forEach(track => track.stop());
- streamRef.current = null;
- }
-
- if (videoRef.current) {
- videoRef.current.srcObject = null;
- }
-
- // Clear the stream from global state
- setLiveStream(null, false, '');
-
- setIsLiveMode(false);
-
- toast({
- title: "📹 Live Video Stopped",
- description: "Webcam feed disconnected",
- duration: 2000,
- });
- };
-
- const addLiveVideoToQueue = () => {
- if (!isLiveMode || !streamRef.current) return;
-
- // Create a mock video result for the live stream
- const liveVideoItem = {
- identifier: 'live_webcam_' + Date.now(),
- title: 'Live Webcam Feed',
- creator: 'Real-time Camera',
- description: 'Live webcam video for real-time mixing',
- year: new Date().getFullYear().toString(),
- licenseurl: 'live_stream',
- downloads: 0
+ if (videoRef.current) {
+ videoRef.current.srcObject = null;
+ }
+ // Clear the stream from global state
+ setLiveStream(null, false, '');
+ setIsLiveMode(false);
+ toast({
+ title: "📹 Live Video Stopped",
+ description: "Webcam feed disconnected",
+ duration: 2000,
+ });
};
-
- // Add to queue with special live video URL
- addToQueue(liveVideoItem, 'live_webcam_stream', false);
-
- toast({
- title: "📹 Live Feed Added",
- description: "Webcam stream added to queue for mixing",
- duration: 3000,
- });
- };
-
- // Cleanup on unmount
- useEffect(() => {
- return () => {
- if (streamRef.current) {
- streamRef.current.getTracks().forEach(track => track.stop());
- }
- // Clear the stream from global state on unmount
- setLiveStream(null, false, '');
+ const addLiveVideoToQueue = () => {
+ if (!isLiveMode || !streamRef.current)
+ return;
+ // Create a mock video result for the live stream
+ const liveVideoItem = {
+ identifier: 'live_webcam_' + Date.now(),
+ title: 'Live Webcam Feed',
+ creator: 'Real-time Camera',
+ description: 'Live webcam video for real-time mixing',
+ year: new Date().getFullYear().toString(),
+ licenseurl: 'live_stream',
+ downloads: 0
+ };
+ // Add to queue with special live video URL
+ addToQueue(liveVideoItem, 'live_webcam_stream', false);
+ toast({
+ title: "📹 Live Feed Added",
+ description: "Webcam stream added to queue for mixing",
+ duration: 3000,
+ });
};
- }, [setLiveStream]);
-
- return (
-
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ if (streamRef.current) {
+ streamRef.current.getTracks().forEach(track => track.stop());
+ }
+ // Clear the stream from global state on unmount
+ setLiveStream(null, false, '');
+ };
+ }, [setLiveStream]);
+ return (
{/* Compact Status */}
- {isLiveMode && (
-
+ {isLiveMode && (
- )}
+
)}
{/* Compact Camera Selection */}
- {cameras.length > 0 && !isLiveMode && (
-
+ {cameras.length > 0 && !isLiveMode && (
0 ? (cameras[0].deviceId || 'camera-0') : 'none')} onValueChange={setSelectedCamera} disabled={isLiveMode}>
-
+
- {cameras.length === 0 && (
- No cameras available
- )}
- {cameras.map((camera, index) => (
-
+ {cameras.length === 0 && (No cameras available )}
+ {cameras.map((camera, index) => (
{camera.label || `Camera ${index + 1}`}
-
- ))}
+ ))}
-
- )}
+
)}
{/* Compact Preview Video */}
-
+ }} muted={false} playsInline/>
{/* Compact Controls */}
- {!isLiveMode ? (
-
-
+ {!isLiveMode ? (
+
{isLoading ? 'Starting...' : 'Start'}
-
- ) : (
- <>
-
-
+ ) : (<>
+
+
Stop
-
-
+
+
Queue
- >
- )}
+ >)}
- {cameras.length === 0 && (
-
+ {cameras.length === 0 && (
No cameras detected. Please connect a webcam or check camera permissions.
-
- )}
-
- );
-}
\ No newline at end of file
+
)}
+
);
+}
diff --git a/client/src/components/LiveVideoWindow.tsx b/client/src/components/LiveVideoWindow.tsx
deleted file mode 100644
index 4c568e0..0000000
--- a/client/src/components/LiveVideoWindow.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import { FloatingPanel } from '@/components/FloatingPanel';
-import { LiveVideoMode } from '@/components/LiveVideoMode';
-import { useStore } from '@/lib/store';
-
-export function LiveVideoWindow() {
- const { brandSkin } = useStore();
-
- return (
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/LuckyDip.tsx b/client/src/components/LuckyDip.tsx
index 64dfa93..ea39fb7 100644
--- a/client/src/components/LuckyDip.tsx
+++ b/client/src/components/LuckyDip.tsx
@@ -4,161 +4,114 @@ import { Dices, Loader2 } from 'lucide-react';
import { useStore } from '@/lib/store';
import { useToast } from '@/hooks/use-toast';
import { searchVideos } from '@/lib/archive-api';
-
interface LuckyDipProps {
- onDipResults: (results: any) => void;
+ onDipResults: (results: any) => void;
}
-
export function LuckyDip({ onDipResults }: LuckyDipProps) {
- const { brandSkin, setSearchState, setSearchResults, setTotalResults, setLoading } = useStore();
- const { toast } = useToast();
- const [isDipping, setIsDipping] = useState(false);
-
- const LUCKY_DIP_QUERIES = [
- 'vintage training film',
- 'public service announcement',
- 'educational short',
- 'safety demonstration',
- 'promotional film',
- 'documentary short',
- 'instructional video',
- 'industrial film',
- 'newsreel footage',
- 'animation short',
- 'advertising film',
- 'government film'
- ];
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'bg-blue-400/20 text-blue-300 hover:bg-blue-400/30 border-blue-400/50';
- case 'waffle':
- return 'bg-yellow-400/20 text-amber-800 hover:bg-yellow-400/30 border-yellow-400/50';
- case 'ebn':
- return 'bg-lime-400/20 text-lime-300 hover:bg-lime-400/30 border-lime-500/50';
- case 'ozzy':
- return 'bg-red-400/20 text-red-300 hover:bg-red-400/30 border-red-500/50';
- case 'hogan':
- return 'bg-yellow-400/20 text-yellow-300 hover:bg-yellow-400/30 border-yellow-400/50';
- case 'dx':
- return 'bg-pink-400/20 text-pink-300 hover:bg-pink-400/30 border-pink-500/50';
- case 'maxheadroom':
- return 'bg-green-400/20 text-green-300 hover:bg-green-400/30 border-green-500/50';
- case 'mario':
- return 'bg-yellow-400/20 text-yellow-300 hover:bg-yellow-400/30 border-yellow-400/50';
- case 'dakota':
- return 'bg-gray-400/20 text-gray-300 hover:bg-gray-400/30 border-gray-400/50';
- case 'blondie':
- return 'bg-amber-400/20 text-amber-300 hover:bg-amber-400/30 border-amber-400/50';
- default:
- return 'bg-blue-400/20 text-blue-300 hover:bg-blue-400/30 border-blue-400/50';
- }
- };
-
- const handleLuckyDip = async () => {
- setIsDipping(true);
- setLoading(true);
-
- try {
- // Pick random queries and combine search terms
- const randomQueries = [...LUCKY_DIP_QUERIES]
- .sort(() => 0.5 - Math.random())
- .slice(0, 2); // Use fewer terms to get better results
-
- // Create search parameters for legally safe, short content with improved query
- const combinedQuery = `${randomQueries.join(' OR ')} AND mediatype:movies AND (licenseurl:*publicdomain* OR collection:*publicdomain*) AND (collection:prelinger OR collection:fedflix) AND year:[1940 TO 1990]`;
-
- const searchParams = {
- query: combinedQuery,
- license: 'publicdomain' as const,
- duration: 'short' as const,
- yearFrom: '1940',
- yearTo: '1990',
- sort: 'relevance' as const,
- page: Math.floor(Math.random() * 3) + 1, // Random page 1-3 for better results
- sources: ['prelinger', 'fedflix'],
- allowRestrictedLicenses: false
- };
-
- console.log('Lucky Dip search params:', searchParams);
- setSearchState(searchParams);
-
- const results = await searchVideos(searchParams);
- console.log('Lucky Dip results:', results);
-
- if (results?.docs && results.docs.length > 0) {
- // Take up to 15 random results
- const shuffledResults = [...results.docs]
- .sort(() => 0.5 - Math.random())
- .slice(0, 15);
-
- setSearchResults(shuffledResults);
- setTotalResults(shuffledResults.length);
- onDipResults(shuffledResults);
-
- toast({
- title: "Lucky Dip Success!",
- description: `Found ${shuffledResults.length} legally safe vintage clips ready for mixing.`,
- });
- } else {
- // Try a fallback search with broader terms
- const fallbackQuery = `vintage AND mediatype:movies AND (licenseurl:*publicdomain* OR collection:*publicdomain*)`;
- const fallbackParams = {
- ...searchParams,
- query: fallbackQuery,
- page: 1
- };
-
- const fallbackResults = await searchVideos(fallbackParams);
-
- if (fallbackResults?.docs && fallbackResults.docs.length > 0) {
- const shuffledResults = [...fallbackResults.docs]
- .sort(() => 0.5 - Math.random())
- .slice(0, 10);
-
- setSearchResults(shuffledResults);
- setTotalResults(shuffledResults.length);
- onDipResults(shuffledResults);
-
- toast({
- title: "Lucky Dip Found Content!",
- description: `Found ${shuffledResults.length} vintage clips using broader search.`,
- });
- } else {
- toast({
- title: "No luck this time",
- description: "Archive.org might be slow. Try again in a moment.",
- variant: "destructive",
- });
+ const { brandSkin, setSearchState, setSearchResults, setTotalResults, setLoading } = useStore();
+ const { toast } = useToast();
+ const [isDipping, setIsDipping] = useState(false);
+ const LUCKY_DIP_QUERIES = [
+ 'vintage training film',
+ 'public service announcement',
+ 'educational short',
+ 'safety demonstration',
+ 'promotional film',
+ 'documentary short',
+ 'instructional video',
+ 'industrial film',
+ 'newsreel footage',
+ 'animation short',
+ 'advertising film',
+ 'government film'
+ ];
+ const getThemeClasses = () => {
+ {
+ return 'bg-lime-400/20 text-lime-300 hover:bg-lime-400/30 border-lime-500/50';
}
- }
- } catch (error) {
- console.error('Lucky Dip error:', error);
- toast({
- title: "Lucky Dip Failed",
- description: "Network issue or Archive.org is busy. Please try again.",
- variant: "destructive",
- });
- } finally {
- setIsDipping(false);
- setLoading(false);
- }
- };
-
- return (
-
- {isDipping ? (
-
- ) : (
-
- )}
-
- );
-}
\ No newline at end of file
+ };
+ const handleLuckyDip = async () => {
+ setIsDipping(true);
+ setLoading(true);
+ try {
+ // Pick random queries and combine search terms
+ const randomQueries = [...LUCKY_DIP_QUERIES]
+ .sort(() => 0.5 - Math.random())
+ .slice(0, 2); // Use fewer terms to get better results
+ // Create search parameters for legally safe, short content with improved query
+ const combinedQuery = `${randomQueries.join(' OR ')} AND mediatype:movies AND (licenseurl:*publicdomain* OR collection:*publicdomain*) AND (collection:prelinger OR collection:fedflix) AND year:[1940 TO 1990]`;
+ const searchParams = {
+ query: combinedQuery,
+ license: 'publicdomain' as const,
+ duration: 'short' as const,
+ yearFrom: '1940',
+ yearTo: '1990',
+ sort: 'relevance' as const,
+ page: Math.floor(Math.random() * 3) + 1, // Random page 1-3 for better results
+ sources: ['prelinger', 'fedflix'],
+ allowRestrictedLicenses: false
+ };
+ console.log('Lucky Dip search params:', searchParams);
+ setSearchState(searchParams);
+ const results = await searchVideos(searchParams);
+ console.log('Lucky Dip results:', results);
+ if (results?.docs && results.docs.length > 0) {
+ // Take up to 15 random results
+ const shuffledResults = [...results.docs]
+ .sort(() => 0.5 - Math.random())
+ .slice(0, 15);
+ setSearchResults(shuffledResults);
+ setTotalResults(shuffledResults.length);
+ onDipResults(shuffledResults);
+ toast({
+ title: "Lucky Dip Success!",
+ description: `Found ${shuffledResults.length} legally safe vintage clips ready for mixing.`,
+ });
+ }
+ else {
+ // Try a fallback search with broader terms
+ const fallbackQuery = `vintage AND mediatype:movies AND (licenseurl:*publicdomain* OR collection:*publicdomain*)`;
+ const fallbackParams = {
+ ...searchParams,
+ query: fallbackQuery,
+ page: 1
+ };
+ const fallbackResults = await searchVideos(fallbackParams);
+ if (fallbackResults?.docs && fallbackResults.docs.length > 0) {
+ const shuffledResults = [...fallbackResults.docs]
+ .sort(() => 0.5 - Math.random())
+ .slice(0, 10);
+ setSearchResults(shuffledResults);
+ setTotalResults(shuffledResults.length);
+ onDipResults(shuffledResults);
+ toast({
+ title: "Lucky Dip Found Content!",
+ description: `Found ${shuffledResults.length} vintage clips using broader search.`,
+ });
+ }
+ else {
+ toast({
+ title: "No luck this time",
+ description: "Archive.org might be slow. Try again in a moment.",
+ variant: "destructive",
+ });
+ }
+ }
+ }
+ catch (error) {
+ console.error('Lucky Dip error:', error);
+ toast({
+ title: "Lucky Dip Failed",
+ description: "Network issue or Archive.org is busy. Please try again.",
+ variant: "destructive",
+ });
+ }
+ finally {
+ setIsDipping(false);
+ setLoading(false);
+ }
+ };
+ return (
+ {isDipping ? ( ) : ()}
+ );
+}
diff --git a/client/src/components/MainToolbar.tsx b/client/src/components/MainToolbar.tsx
deleted file mode 100644
index b35cbb6..0000000
--- a/client/src/components/MainToolbar.tsx
+++ /dev/null
@@ -1,287 +0,0 @@
-import React from 'react';
-import { Button } from '@/components/ui/button';
-import { useStore } from '@/lib/store';
-import {
- Eye,
- Headphones,
- Sparkles,
- Search,
- List,
- Grid3x3,
- Music,
- Settings,
- Video,
- Image,
- Tv,
- Monitor,
- Palette,
- Filter,
- FileText,
- BarChart3,
- Layers,
- PanelLeft,
- PanelRight,
- Square,
- Play,
- Wand2,
- Compass,
- MonitorPlay,
- Wrench
-} from 'lucide-react';
-
-export function MainToolbar() {
- const {
- floatingPanelStates,
- setFloatingPanelVisible,
- setFloatingPanelMinimized
- } = useStore();
-
- const togglePanel = (panelId: keyof typeof floatingPanelStates) => {
- const panel = floatingPanelStates[panelId];
- if (panel?.visible) {
- // If visible, hide it
- setFloatingPanelVisible(panelId, false);
- } else {
- // If hidden, show it and un-minimize
- setFloatingPanelVisible(panelId, true);
- setFloatingPanelMinimized(panelId, false);
- }
- };
-
- const toolbarSections = [
- // === VISIBLE BY DEFAULT ===
- {
- title: "Core Panels",
- icon: Compass,
- description: "Main Interface (Always Visible)",
- items: [
- {
- id: 'search',
- label: 'Search',
- icon: Search,
- color: 'text-orange-500',
- bgColor: 'bg-orange-50 hover:bg-orange-100',
- borderColor: 'border-orange-200'
- },
- {
- id: 'player',
- label: 'Player',
- icon: MonitorPlay,
- color: 'text-blue-600',
- bgColor: 'bg-blue-50 hover:bg-blue-100',
- borderColor: 'border-blue-200'
- },
- {
- id: 'queue',
- label: 'Timeline',
- icon: List,
- color: 'text-teal-500',
- bgColor: 'bg-teal-50 hover:bg-teal-100',
- borderColor: 'border-teal-200'
- },
- {
- id: 'preview',
- label: 'Preview',
- icon: Eye,
- color: 'text-purple-500',
- bgColor: 'bg-purple-50 hover:bg-purple-100',
- borderColor: 'border-purple-200'
- }
- ]
- },
- // === HIDDEN PANELS ===
- {
- title: "Video Input",
- icon: Video,
- description: "Recording & Live Input",
- items: [
- {
- id: 'liveVideo',
- label: 'Live Video',
- icon: Video,
- color: 'text-red-500',
- bgColor: 'bg-red-50 hover:bg-red-100',
- borderColor: 'border-red-200'
- },
- {
- id: 'recordSet',
- label: 'Record Set',
- icon: FileText,
- color: 'text-orange-600',
- bgColor: 'bg-orange-50 hover:bg-orange-100',
- borderColor: 'border-orange-200'
- }
- ]
- },
- {
- title: "Playback Controls",
- icon: Play,
- description: "Media Control & Loops",
- items: [
- {
- id: 'mediaControls',
- label: 'Controls',
- icon: Play,
- color: 'text-gray-600',
- bgColor: 'bg-gray-50 hover:bg-gray-100',
- borderColor: 'border-gray-200'
- },
- {
- id: 'loopControls',
- label: 'Loops',
- icon: BarChart3,
- color: 'text-indigo-500',
- bgColor: 'bg-indigo-50 hover:bg-indigo-100',
- borderColor: 'border-indigo-200'
- },
- {
- id: 'popOutPlayer',
- label: 'Pop Out',
- icon: Monitor,
- color: 'text-cyan-500',
- bgColor: 'bg-cyan-50 hover:bg-cyan-100',
- borderColor: 'border-cyan-200'
- }
- ]
- },
- {
- title: "Effects & Processing",
- icon: Wand2,
- description: "Video & Audio Effects",
- items: [
- {
- id: 'presetEffects',
- label: 'Instant FX',
- icon: Sparkles,
- color: 'text-purple-500',
- bgColor: 'bg-purple-50 hover:bg-purple-100',
- borderColor: 'border-purple-200'
- },
- {
- id: 'videoEffects',
- label: 'Video FX',
- icon: Palette,
- color: 'text-blue-500',
- bgColor: 'bg-blue-50 hover:bg-blue-100',
- borderColor: 'border-blue-200'
- },
- {
- id: 'audioEffects',
- label: 'Audio FX',
- icon: Headphones,
- color: 'text-green-500',
- bgColor: 'bg-green-50 hover:bg-green-100',
- borderColor: 'border-green-200'
- },
- {
- id: 'geometry',
- label: 'Geometry',
- icon: Square,
- color: 'text-pink-500',
- bgColor: 'bg-pink-50 hover:bg-pink-100',
- borderColor: 'border-pink-200'
- }
- ]
- },
- {
- title: "Browse & Search",
- icon: Grid3x3,
- description: "Content Discovery",
- items: [
- {
- id: 'resultsGrid',
- label: 'Results Grid',
- icon: Grid3x3,
- color: 'text-indigo-500',
- bgColor: 'bg-indigo-50 hover:bg-indigo-100',
- borderColor: 'border-indigo-200'
- }
- ]
- },
- {
- title: "Creative Tools",
- icon: Layers,
- description: "Experimental Features",
- items: [
- {
- id: 'emergencyMix',
- label: 'Emergency Mix',
- icon: Layers,
- color: 'text-red-600',
- bgColor: 'bg-red-50 hover:bg-red-100',
- borderColor: 'border-red-200'
- },
- {
- id: 'luckyDip',
- label: 'Lucky Dip',
- icon: Compass,
- color: 'text-yellow-600',
- bgColor: 'bg-yellow-50 hover:bg-yellow-100',
- borderColor: 'border-yellow-200'
- }
- ]
- },
- {
- title: "Help & Settings",
- icon: Settings,
- description: "Support & Configuration",
- items: [
- {
- id: 'keyboardShortcuts',
- label: 'Shortcuts',
- icon: Settings,
- color: 'text-gray-500',
- bgColor: 'bg-gray-50 hover:bg-gray-100',
- borderColor: 'border-gray-200'
- }
- ]
- }
- ];
-
- return (
-
-
- {toolbarSections.map((section, sectionIndex) => {
- const SectionIcon = section.icon;
- return (
-
- {sectionIndex > 0 && (
-
- )}
-
-
-
-
- {section.title}
-
-
- {section.items.map((item) => {
- const Icon = item.icon;
- const isVisible = floatingPanelStates[item.id as keyof typeof floatingPanelStates]?.visible ?? false;
-
- return (
-
togglePanel(item.id as keyof typeof floatingPanelStates)}
- className={`h-8 px-3 text-xs transition-all duration-200 ${
- isVisible
- ? `${item.bgColor} ${item.borderColor} border text-gray-800 font-medium shadow-sm`
- : 'text-white/70 hover:text-white hover:bg-white/10'
- }`}
- title={`Toggle ${item.label} panel`}
- >
-
- {item.label}
-
- );
- })}
-
-
- );
- })}
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/MediaControls.tsx b/client/src/components/MediaControls.tsx
index 82272af..3e8e5c1 100644
--- a/client/src/components/MediaControls.tsx
+++ b/client/src/components/MediaControls.tsx
@@ -6,208 +6,153 @@ import { useToast } from '@/hooks/use-toast';
import { ScaleTransition, PulseTransition } from './AnimatedTransitions';
import { LuckyDip } from '@/components/LuckyDip';
import { EmergencyMix } from '@/components/EmergencyMix';
-
export function MediaControls() {
- const { brandSkin } = useStore();
- const { toast } = useToast();
- const [isPlaying, setIsPlaying] = useState(false);
- const [isRecording, setIsRecording] = useState(false);
- const mediaRecorderRef = useRef(null);
- const recordedChunksRef = useRef([]);
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'text-blue-400 hover:bg-blue-400/10';
- case 'waffle':
- return 'text-amber-800 hover:bg-yellow-100/50';
- case 'ebn':
- return 'text-lime-400 hover:bg-lime-900/50';
- case 'ozzy':
- return 'text-red-300 hover:bg-red-900/30';
- case 'hogan':
- return 'text-yellow-300 hover:bg-yellow-900/50';
- case 'dx':
- return 'text-pink-300 hover:bg-pink-900/50';
- case 'maxheadroom':
- return 'text-green-300 hover:bg-green-900/50';
- case 'mario':
- return 'text-yellow-300 hover:bg-red-900/50';
- case 'dakota':
- return 'text-gray-300 hover:bg-gray-800/50';
- case 'blondie':
- return 'text-amber-300 hover:bg-amber-900/50';
- default:
- return 'text-blue-400 hover:bg-blue-400/10';
- }
- };
-
- const handlePlay = () => {
- const videos = document.querySelectorAll('video');
- if (videos.length > 0) {
- const video = videos[0]; // Get the first video element
- if (isPlaying) {
- video.pause();
- setIsPlaying(false);
- toast({
- title: "Playback Paused",
- description: "Video playback has been paused.",
- });
- } else {
- video.play();
- setIsPlaying(true);
- toast({
- title: "Playback Started",
- description: "Video playback has been started.",
- });
- }
- } else {
- toast({
- title: "No Video",
- description: "No video available to play.",
- variant: "destructive",
- });
- }
- };
-
- const handleStop = () => {
- const videos = document.querySelectorAll('video');
- if (videos.length > 0) {
- const video = videos[0];
- video.pause();
- video.currentTime = 0;
- setIsPlaying(false);
- toast({
- title: "Playback Stopped",
- description: "Video playback has been stopped and reset.",
- });
- }
- };
-
- const handleRecord = async () => {
- if (isRecording) {
- // Stop recording
- if (mediaRecorderRef.current) {
- mediaRecorderRef.current.stop();
- setIsRecording(false);
- toast({
- title: "Recording Stopped",
- description: "Screen recording has been stopped.",
- });
- }
- } else {
- // Start recording
- try {
- const stream = await navigator.mediaDevices.getDisplayMedia({
- video: true,
- audio: true
- });
-
- const mediaRecorder = new MediaRecorder(stream, {
- mimeType: 'video/webm;codecs=vp9'
- });
-
- recordedChunksRef.current = [];
-
- mediaRecorder.ondataavailable = (event) => {
- if (event.data.size > 0) {
- recordedChunksRef.current.push(event.data);
- }
- };
-
- mediaRecorder.onstop = () => {
- const blob = new Blob(recordedChunksRef.current, {
- type: 'video/webm'
- });
-
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `static-buffet-recording-${Date.now()}.webm`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-
- // Clean up stream
- stream.getTracks().forEach(track => track.stop());
- };
-
- mediaRecorderRef.current = mediaRecorder;
- mediaRecorder.start();
- setIsRecording(true);
-
- toast({
- title: "Recording Started",
- description: "Screen recording has begun. Click record again to stop.",
- });
- } catch (error) {
- console.error('Error starting screen recording:', error);
- toast({
- title: "Recording Failed",
- description: "Could not start screen recording. Permission may be denied.",
- variant: "destructive",
- });
- }
- }
- };
-
- const handleLuckyDipResults = (results: any) => {
- // Results are already set by LuckyDip component
- console.log('Lucky Dip found', results.length, 'clips');
- };
-
- return (
-
+ const { brandSkin } = useStore();
+ const { toast } = useToast();
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [isRecording, setIsRecording] = useState(false);
+ const mediaRecorderRef = useRef
(null);
+ const recordedChunksRef = useRef([]);
+ const getThemeClasses = () => {
+ {
+ return 'text-lime-400 hover:bg-lime-900/50';
+ }
+ };
+ const handlePlay = () => {
+ const videos = document.querySelectorAll('video');
+ if (videos.length > 0) {
+ const video = videos[0]; // Get the first video element
+ if (isPlaying) {
+ video.pause();
+ setIsPlaying(false);
+ toast({
+ title: "Playback Paused",
+ description: "Video playback has been paused.",
+ });
+ }
+ else {
+ video.play();
+ setIsPlaying(true);
+ toast({
+ title: "Playback Started",
+ description: "Video playback has been started.",
+ });
+ }
+ }
+ else {
+ toast({
+ title: "No Video",
+ description: "No video available to play.",
+ variant: "destructive",
+ });
+ }
+ };
+ const handleStop = () => {
+ const videos = document.querySelectorAll('video');
+ if (videos.length > 0) {
+ const video = videos[0];
+ video.pause();
+ video.currentTime = 0;
+ setIsPlaying(false);
+ toast({
+ title: "Playback Stopped",
+ description: "Video playback has been stopped and reset.",
+ });
+ }
+ };
+ const handleRecord = async () => {
+ if (isRecording) {
+ // Stop recording
+ if (mediaRecorderRef.current) {
+ mediaRecorderRef.current.stop();
+ setIsRecording(false);
+ toast({
+ title: "Recording Stopped",
+ description: "Screen recording has been stopped.",
+ });
+ }
+ }
+ else {
+ // Start recording
+ try {
+ const stream = await navigator.mediaDevices.getDisplayMedia({
+ video: true,
+ audio: true
+ });
+ const mediaRecorder = new MediaRecorder(stream, {
+ mimeType: 'video/webm;codecs=vp9'
+ });
+ recordedChunksRef.current = [];
+ mediaRecorder.ondataavailable = (event) => {
+ if (event.data.size > 0) {
+ recordedChunksRef.current.push(event.data);
+ }
+ };
+ mediaRecorder.onstop = () => {
+ const blob = new Blob(recordedChunksRef.current, {
+ type: 'video/webm'
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `static-buffet-recording-${Date.now()}.webm`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ // Clean up stream
+ stream.getTracks().forEach(track => track.stop());
+ };
+ mediaRecorderRef.current = mediaRecorder;
+ mediaRecorder.start();
+ setIsRecording(true);
+ toast({
+ title: "Recording Started",
+ description: "Screen recording has begun. Click record again to stop.",
+ });
+ }
+ catch (error) {
+ console.error('Error starting screen recording:', error);
+ toast({
+ title: "Recording Failed",
+ description: "Could not start screen recording. Permission may be denied.",
+ variant: "destructive",
+ });
+ }
+ }
+ };
+ const handleLuckyDipResults = (results: any) => {
+ // Results are already set by LuckyDip component
+ console.log('Lucky Dip found', results.length, 'clips');
+ };
+ return (
{/* Play/Pause Button */}
-
- {isPlaying ? : }
+
+ {isPlaying ? : }
{/* Stop Button */}
-
-
+
+
{/* Record Button with Pulse Animation */}
-
-
+
+
{/* Lucky Dip & Emergency Mix Buttons */}
-
+
-
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/PanelHeader.tsx b/client/src/components/PanelHeader.tsx
index ddfb97e..a578201 100644
--- a/client/src/components/PanelHeader.tsx
+++ b/client/src/components/PanelHeader.tsx
@@ -1,56 +1,29 @@
import { ChevronDown, ChevronUp, Minimize2, Maximize2 } from 'lucide-react';
import { useStore } from '@/lib/store';
-
interface PanelHeaderProps {
- title: string;
- status?: string;
- isCollapsed: boolean;
- onToggleCollapse: () => void;
- children?: React.ReactNode;
+ title: string;
+ status?: string;
+ isCollapsed: boolean;
+ onToggleCollapse: () => void;
+ children?: React.ReactNode;
}
-
export function PanelHeader({ title, status, isCollapsed, onToggleCollapse, children }: PanelHeaderProps) {
- const { brandSkin } = useStore();
-
- return (
-
+ const { brandSkin } = useStore();
+ return (
-
+
{title}
- {status && (
-
+ {status && (
{status}
-
- )}
+ )}
{children}
-
- {isCollapsed ? (
-
- ) : (
-
- )}
+
+ {isCollapsed ? () : ()}
-
- );
-}
\ No newline at end of file
+
);
+}
diff --git a/client/src/components/Player.tsx b/client/src/components/Player.tsx
index 389d1f8..1f4bcdb 100644
--- a/client/src/components/Player.tsx
+++ b/client/src/components/Player.tsx
@@ -582,19 +582,6 @@ export function Player() {
e.preventDefault();
applyPreset('timewarp');
break;
- case 'KeyT':
- if (e.ctrlKey || e.metaKey) {
- e.preventDefault();
- const { floatingPanelStates, setFloatingPanelVisible, bringPanelToFront } = useStore.getState();
- const isVisible = floatingPanelStates.effects?.visible;
- setFloatingPanelVisible('effects', !isVisible);
- if (!isVisible) bringPanelToFront('effects');
- toast({
- title: isVisible ? "Trim Controls Hidden" : "Trim Controls Shown",
- description: isVisible ? "Trim controls panel closed" : "Trim controls panel opened",
- });
- }
- break;
case 'KeyP':
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
@@ -1161,20 +1148,6 @@ export function Player() {
- {
- const { floatingPanelStates, setFloatingPanelVisible, bringPanelToFront } = useStore.getState();
- const isVisible = floatingPanelStates.effects?.visible;
- setFloatingPanelVisible('effects', !isVisible);
- if (!isVisible) bringPanelToFront('effects');
- }}
- title="Toggle trim controls panel"
- >
-
-
-
diff --git a/client/src/components/PopOutPlayer.tsx b/client/src/components/PopOutPlayer.tsx
index 467996f..578f987 100644
--- a/client/src/components/PopOutPlayer.tsx
+++ b/client/src/components/PopOutPlayer.tsx
@@ -4,259 +4,227 @@ import { ExternalLink, X } from 'lucide-react';
import { useStore } from '@/lib/store';
import { useToast } from '@/hooks/use-toast';
import { type QueueItem } from '@/lib/types';
-
// Global singleton for pop-out window management
let globalPopOutWindow: Window | null = null;
let globalPopOutWindowOpen = false;
-
interface PopOutPlayerProps {
- currentVideo?: QueueItem | null;
+ currentVideo?: QueueItem | null;
}
-
export function PopOutPlayer({ currentVideo }: PopOutPlayerProps) {
- const {
- brandSkin,
- queueItems,
- currentQueueIndex,
- isPlaying,
- nextTrack,
- previousTrack,
- setVideoEffects,
- videoEffects
- } = useStore();
- const { toast } = useToast();
- const windowCheckInterval = useRef();
-
- // Use global singleton instead of local state
- const [isPopOutOpen, setIsPopOutOpen] = useState(globalPopOutWindowOpen);
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'text-blue-400 hover:bg-blue-400/10';
- case 'waffle':
- return 'text-amber-800 hover:bg-yellow-100/50';
- case 'ebn':
- return 'text-lime-400 hover:bg-lime-900/50';
- case 'ozzy':
- return 'text-red-300 hover:bg-red-900/30';
- case 'hogan':
- return 'text-yellow-300 hover:bg-yellow-900/50';
- case 'dx':
- return 'text-pink-300 hover:bg-pink-900/50';
- case 'maxheadroom':
- return 'text-green-300 hover:bg-green-900/50';
- case 'mario':
- return 'text-yellow-300 hover:bg-red-900/50';
- case 'dakota':
- return 'text-gray-300 hover:bg-gray-800/50';
- case 'blondie':
- return 'text-amber-300 hover:bg-amber-900/50';
- default:
- return 'text-blue-400 hover:bg-blue-400/10';
- }
- };
-
- // Effect presets for keyboard shortcuts
- const applyPreset = (preset: string) => {
- switch (preset) {
- case 'cyberpunk':
- setVideoEffects({
- ...videoEffects,
- brightness: 120,
- contrast: 140,
- saturation: 150,
- hue: 180,
- chromaticAberration: 30,
- scanlines: true,
- });
- break;
- case 'vintage':
- setVideoEffects({
- ...videoEffects,
- brightness: 90,
- contrast: 110,
- saturation: 80,
- sepia: 40,
- blur: 1,
- });
- break;
- case 'glitch':
- setVideoEffects({
- ...videoEffects,
- glitchIntensity: 50,
- chromaticAberration: 60,
- datamosh: true,
- pixelate: 30,
- });
- break;
- case 'noir':
- setVideoEffects({
- ...videoEffects,
- grayscale: 100,
- contrast: 150,
- brightness: 80,
- });
- break;
- case 'vortex':
- setVideoEffects({
- ...videoEffects,
- rotate: 180,
- scaleX: 120,
- scaleY: 80,
- blur: 2,
- brightness: 130,
- contrast: 160,
- saturation: 200,
- hue: 120,
- chromaticAberration: 40,
- pixelate: 15,
- });
- break;
- case 'portal':
- setVideoEffects({
- ...videoEffects,
- brightness: 80,
- contrast: 200,
- saturation: 50,
- hue: -60,
- invert: 30,
- sepia: 20,
- chromaticAberration: 80,
- glitchIntensity: 60,
- scaleX: 150,
- scaleY: 150,
- });
- break;
- case 'fractal':
- setVideoEffects({
- ...videoEffects,
- brightness: 140,
- contrast: 180,
- saturation: 250,
- hue: 240,
- blur: 1,
- scaleX: 110,
- scaleY: 110,
- rotate: 45,
- chromaticAberration: 25,
- pixelate: 8,
- });
- break;
- case 'timewarp':
- setVideoEffects({
- ...videoEffects,
- brightness: 110,
- contrast: 130,
- saturation: 120,
- hue: 30,
- blur: 3,
- sepia: 40,
- scaleX: 95,
- scaleY: 105,
- rotate: -15,
- glitchIntensity: 35,
- });
- break;
- }
- };
-
- // Handle keyboard commands from pop-out window
- const handleKeyboardCommand = (keyCode: string, modifiers: { ctrlKey: boolean; metaKey: boolean; altKey: boolean; shiftKey: boolean }) => {
- if (!currentVideo) return;
-
- switch (keyCode) {
- case 'Space':
- // Toggle play/pause via video element in pop-out
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
- if (video) {
- if (video.paused) {
- video.play();
- } else {
- video.pause();
- }
- }
+ const { brandSkin, queueItems, currentQueueIndex, isPlaying, nextTrack, previousTrack, setVideoEffects, videoEffects } = useStore();
+ const { toast } = useToast();
+ const windowCheckInterval = useRef();
+ // Use global singleton instead of local state
+ const [isPopOutOpen, setIsPopOutOpen] = useState(globalPopOutWindowOpen);
+ const getThemeClasses = () => {
+ {
+ return 'text-lime-400 hover:bg-lime-900/50';
}
- toast({
- title: isPlaying ? "Video Paused" : "Video Playing",
- description: `${currentVideo.title}`,
- });
- break;
- case 'ArrowLeft':
- // Seek backward
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
- if (video) {
- video.currentTime = Math.max(0, video.currentTime - 10);
- }
- }
- break;
- case 'ArrowRight':
- // Seek forward
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
- if (video) {
- video.currentTime = Math.min(video.duration || 0, video.currentTime + 10);
- }
- }
- break;
- case 'ArrowUp':
- // Volume up
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
- if (video) {
- video.volume = Math.min(1, video.volume + 0.1);
- }
+ };
+ // Effect presets for keyboard shortcuts
+ const applyPreset = (preset: string) => {
+ switch (preset) {
+ case 'cyberpunk':
+ setVideoEffects({
+ ...videoEffects,
+ brightness: 120,
+ contrast: 140,
+ saturation: 150,
+ hue: 180,
+ chromaticAberration: 30,
+ scanlines: true,
+ });
+ break;
+ case 'vintage':
+ setVideoEffects({
+ ...videoEffects,
+ brightness: 90,
+ contrast: 110,
+ saturation: 80,
+ sepia: 40,
+ blur: 1,
+ });
+ break;
+ case 'glitch':
+ setVideoEffects({
+ ...videoEffects,
+ glitchIntensity: 50,
+ chromaticAberration: 60,
+ datamosh: true,
+ pixelate: 30,
+ });
+ break;
+ case 'noir':
+ setVideoEffects({
+ ...videoEffects,
+ grayscale: 100,
+ contrast: 150,
+ brightness: 80,
+ });
+ break;
+ case 'vortex':
+ setVideoEffects({
+ ...videoEffects,
+ rotate: 180,
+ scaleX: 120,
+ scaleY: 80,
+ blur: 2,
+ brightness: 130,
+ contrast: 160,
+ saturation: 200,
+ hue: 120,
+ chromaticAberration: 40,
+ pixelate: 15,
+ });
+ break;
+ case 'portal':
+ setVideoEffects({
+ ...videoEffects,
+ brightness: 80,
+ contrast: 200,
+ saturation: 50,
+ hue: -60,
+ invert: 30,
+ sepia: 20,
+ chromaticAberration: 80,
+ glitchIntensity: 60,
+ scaleX: 150,
+ scaleY: 150,
+ });
+ break;
+ case 'fractal':
+ setVideoEffects({
+ ...videoEffects,
+ brightness: 140,
+ contrast: 180,
+ saturation: 250,
+ hue: 240,
+ blur: 1,
+ scaleX: 110,
+ scaleY: 110,
+ rotate: 45,
+ chromaticAberration: 25,
+ pixelate: 8,
+ });
+ break;
+ case 'timewarp':
+ setVideoEffects({
+ ...videoEffects,
+ brightness: 110,
+ contrast: 130,
+ saturation: 120,
+ hue: 30,
+ blur: 3,
+ sepia: 40,
+ scaleX: 95,
+ scaleY: 105,
+ rotate: -15,
+ glitchIntensity: 35,
+ });
+ break;
}
- break;
- case 'ArrowDown':
- // Volume down
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
- if (video) {
- video.volume = Math.max(0, video.volume - 0.1);
- }
+ };
+ // Handle keyboard commands from pop-out window
+ const handleKeyboardCommand = (keyCode: string, modifiers: {
+ ctrlKey: boolean;
+ metaKey: boolean;
+ altKey: boolean;
+ shiftKey: boolean;
+ }) => {
+ if (!currentVideo)
+ return;
+ switch (keyCode) {
+ case 'Space':
+ // Toggle play/pause via video element in pop-out
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
+ if (video) {
+ if (video.paused) {
+ video.play();
+ }
+ else {
+ video.pause();
+ }
+ }
+ }
+ toast({
+ title: isPlaying ? "Video Paused" : "Video Playing",
+ description: `${currentVideo.title}`,
+ });
+ break;
+ case 'ArrowLeft':
+ // Seek backward
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
+ if (video) {
+ video.currentTime = Math.max(0, video.currentTime - 10);
+ }
+ }
+ break;
+ case 'ArrowRight':
+ // Seek forward
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
+ if (video) {
+ video.currentTime = Math.min(video.duration || 0, video.currentTime + 10);
+ }
+ }
+ break;
+ case 'ArrowUp':
+ // Volume up
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
+ if (video) {
+ video.volume = Math.min(1, video.volume + 0.1);
+ }
+ }
+ break;
+ case 'ArrowDown':
+ // Volume down
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
+ if (video) {
+ video.volume = Math.max(0, video.volume - 0.1);
+ }
+ }
+ break;
+ case 'Digit1':
+ applyPreset('cyberpunk');
+ toast({ title: "Cyberpunk Effect Applied", description: "High contrast, neon colors" });
+ break;
+ case 'Digit2':
+ applyPreset('vintage');
+ toast({ title: "Vintage Effect Applied", description: "Warm, sepia tones" });
+ break;
+ case 'Digit3':
+ applyPreset('glitch');
+ toast({ title: "Glitch Effect Applied", description: "Digital distortion" });
+ break;
+ case 'Digit4':
+ applyPreset('noir');
+ toast({ title: "Noir Effect Applied", description: "Black and white, high contrast" });
+ break;
+ case 'Digit5':
+ applyPreset('vortex');
+ toast({ title: "Vortex Effect Applied", description: "Twisted reality" });
+ break;
+ case 'Digit6':
+ applyPreset('portal');
+ toast({ title: "Portal Effect Applied", description: "Otherworldly distortion" });
+ break;
+ case 'Digit7':
+ applyPreset('fractal');
+ toast({ title: "Fractal Effect Applied", description: "Geometric patterns" });
+ break;
+ case 'Digit8':
+ applyPreset('timewarp');
+ toast({ title: "Timewarp Effect Applied", description: "Temporal distortion" });
+ break;
}
- break;
- case 'Digit1':
- applyPreset('cyberpunk');
- toast({ title: "Cyberpunk Effect Applied", description: "High contrast, neon colors" });
- break;
- case 'Digit2':
- applyPreset('vintage');
- toast({ title: "Vintage Effect Applied", description: "Warm, sepia tones" });
- break;
- case 'Digit3':
- applyPreset('glitch');
- toast({ title: "Glitch Effect Applied", description: "Digital distortion" });
- break;
- case 'Digit4':
- applyPreset('noir');
- toast({ title: "Noir Effect Applied", description: "Black and white, high contrast" });
- break;
- case 'Digit5':
- applyPreset('vortex');
- toast({ title: "Vortex Effect Applied", description: "Twisted reality" });
- break;
- case 'Digit6':
- applyPreset('portal');
- toast({ title: "Portal Effect Applied", description: "Otherworldly distortion" });
- break;
- case 'Digit7':
- applyPreset('fractal');
- toast({ title: "Fractal Effect Applied", description: "Geometric patterns" });
- break;
- case 'Digit8':
- applyPreset('timewarp');
- toast({ title: "Timewarp Effect Applied", description: "Temporal distortion" });
- break;
- }
- };
-
- // Apply video effects to pop-out video element
- const applyEffectsToPopOutVideo = (video: HTMLVideoElement) => {
- const filterString = `
+ };
+ // Apply video effects to pop-out video element
+ const applyEffectsToPopOutVideo = (video: HTMLVideoElement) => {
+ const filterString = `
brightness(${videoEffects.brightness}%)
contrast(${videoEffects.contrast}%)
saturate(${videoEffects.saturation}%)
@@ -267,97 +235,79 @@ export function PopOutPlayer({ currentVideo }: PopOutPlayerProps) {
invert(${videoEffects.invert}%)
sepia(${videoEffects.sepia}%)
`;
-
- const transformString = `rotate(${videoEffects.rotate}deg) scaleX(${videoEffects.scaleX / 100}) scaleY(${videoEffects.scaleY / 100})`;
-
- video.style.filter = filterString;
- video.style.transform = transformString;
- };
-
- const openPopOutPlayer = () => {
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- globalPopOutWindow.focus();
- return;
- }
-
- const screenWidth = window.screen.width;
- const screenHeight = window.screen.height;
- const windowWidth = Math.min(1280, screenWidth * 0.8);
- const windowHeight = Math.min(720, screenHeight * 0.8);
- const left = (screenWidth - windowWidth) / 2;
- const top = (screenHeight - windowHeight) / 2;
-
- const newWindow = window.open(
- '',
- 'StaticBuffetPlayer',
- `width=${windowWidth},height=${windowHeight},left=${left},top=${top},resizable=yes,scrollbars=no,toolbar=no,menubar=no,location=no,status=no`
- );
-
- if (!newWindow) {
- toast({
- title: "Pop-out Blocked",
- description: "Please allow pop-ups for this site to use the pop-out player.",
- variant: "destructive",
- });
- return;
- }
-
- globalPopOutWindow = newWindow;
- globalPopOutWindowOpen = true;
- setIsPopOutOpen(true);
-
- // Check if window is still open periodically
- windowCheckInterval.current = setInterval(() => {
- if (newWindow.closed) {
- globalPopOutWindowOpen = false;
- globalPopOutWindow = null;
- setIsPopOutOpen(false);
- if (windowCheckInterval.current) {
- clearInterval(windowCheckInterval.current);
+ const transformString = `rotate(${videoEffects.rotate}deg) scaleX(${videoEffects.scaleX / 100}) scaleY(${videoEffects.scaleY / 100})`;
+ video.style.filter = filterString;
+ video.style.transform = transformString;
+ };
+ const openPopOutPlayer = () => {
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ globalPopOutWindow.focus();
+ return;
}
- }
- }, 1000);
-
- // Create the pop-out player HTML
- createPopOutPlayerContent(newWindow);
-
- // Send current video immediately if one exists
- if (currentVideo) {
- setTimeout(() => {
- newWindow.postMessage({
- type: 'VIDEO_UPDATE',
- videoUrl: currentVideo.videoUrl,
- title: currentVideo.title,
- queueIndex: currentQueueIndex,
- queueLength: queueItems.length
- }, '*');
- }, 500); // Small delay to ensure DOM is ready
- }
-
- toast({
- title: "Pop-out Player Opened",
- description: "Video player is now in a separate window.",
- });
- };
-
- const createPopOutPlayerContent = (window: Window) => {
- const themeColors = {
- testcard: { bg: '#0f172a', accent: '#3b82f6', text: '#e2e8f0' },
- waffle: { bg: '#fef3c7', accent: '#d97706', text: '#92400e' },
- ebn: { bg: '#1a2e05', accent: '#84cc16', text: '#bef264' },
- ozzy: { bg: '#0f0f0f', accent: '#ef4444', text: '#fca5a5' },
- hogan: { bg: '#1f2937', accent: '#eab308', text: '#fde047' },
- dx: { bg: '#1f2937', accent: '#ec4899', text: '#f9a8d4' },
- maxheadroom: { bg: '#064e3b', accent: '#22c55e', text: '#86efac' },
- mario: { bg: '#7f1d1d', accent: '#eab308', text: '#fde047' },
- dakota: { bg: '#374151', accent: '#9ca3af', text: '#d1d5db' },
- blondie: { bg: '#451a03', accent: '#f59e0b', text: '#fbbf24' },
- diner: { bg: '#451a03', accent: '#f59e0b', text: '#fbbf24' }
+ const screenWidth = window.screen.width;
+ const screenHeight = window.screen.height;
+ const windowWidth = Math.min(1280, screenWidth * 0.8);
+ const windowHeight = Math.min(720, screenHeight * 0.8);
+ const left = (screenWidth - windowWidth) / 2;
+ const top = (screenHeight - windowHeight) / 2;
+ const newWindow = window.open('', 'StaticBuffetPlayer', `width=${windowWidth},height=${windowHeight},left=${left},top=${top},resizable=yes,scrollbars=no,toolbar=no,menubar=no,location=no,status=no`);
+ if (!newWindow) {
+ toast({
+ title: "Pop-out Blocked",
+ description: "Please allow pop-ups for this site to use the pop-out player.",
+ variant: "destructive",
+ });
+ return;
+ }
+ globalPopOutWindow = newWindow;
+ globalPopOutWindowOpen = true;
+ setIsPopOutOpen(true);
+ // Check if window is still open periodically
+ windowCheckInterval.current = setInterval(() => {
+ if (newWindow.closed) {
+ globalPopOutWindowOpen = false;
+ globalPopOutWindow = null;
+ setIsPopOutOpen(false);
+ if (windowCheckInterval.current) {
+ clearInterval(windowCheckInterval.current);
+ }
+ }
+ }, 1000);
+ // Create the pop-out player HTML
+ createPopOutPlayerContent(newWindow);
+ // Send current video immediately if one exists
+ if (currentVideo) {
+ setTimeout(() => {
+ newWindow.postMessage({
+ type: 'VIDEO_UPDATE',
+ videoUrl: currentVideo.videoUrl,
+ title: currentVideo.title,
+ queueIndex: currentQueueIndex,
+ queueLength: queueItems.length
+ }, '*');
+ }, 500); // Small delay to ensure DOM is ready
+ }
+ toast({
+ title: "Pop-out Player Opened",
+ description: "Video player is now in a separate window.",
+ });
};
-
- const theme = themeColors[brandSkin] || themeColors.testcard;
-
- window.document.write(`
+ const createPopOutPlayerContent = (window: Window) => {
+ const themeColors = {
+ testcard: { bg: '#0f172a', accent: '#3b82f6', text: '#e2e8f0' },
+ waffle: { bg: '#fef3c7', accent: '#d97706', text: '#92400e' },
+ ebn: { bg: '#1a2e05', accent: '#84cc16', text: '#bef264' },
+ ozzy: { bg: '#0f0f0f', accent: '#ef4444', text: '#fca5a5' },
+ hogan: { bg: '#1f2937', accent: '#eab308', text: '#fde047' },
+ dx: { bg: '#1f2937', accent: '#ec4899', text: '#f9a8d4' },
+ maxheadroom: { bg: '#064e3b', accent: '#22c55e', text: '#86efac' },
+ mario: { bg: '#7f1d1d', accent: '#eab308', text: '#fde047' },
+ dakota: { bg: '#374151', accent: '#9ca3af', text: '#d1d5db' },
+ blondie: { bg: '#451a03', accent: '#f59e0b', text: '#fbbf24' },
+ diner: { bg: '#451a03', accent: '#f59e0b', text: '#fbbf24' }
+ };
+ const theme = themeColors[brandSkin] || themeColors.testcard;
+ window.document.write(`
@@ -458,278 +408,227 @@ export function PopOutPlayer({ currentVideo }: PopOutPlayerProps) {
`);
-
- window.document.close();
-
- // Set up message listener for updates from main window
- const handleMessage = (event: MessageEvent) => {
- if (event.data.type === 'VIDEO_UPDATE') {
- updatePopOutVideo(window, event.data);
- } else if (event.data.type === 'PLAYBACK_UPDATE') {
- updatePlaybackState(window, event.data);
- }
- };
-
- window.addEventListener('message', handleMessage);
- window.addEventListener('beforeunload', () => {
- globalPopOutWindowOpen = false;
- globalPopOutWindow = null;
- setIsPopOutOpen(false);
- });
-
- // Add keyboard shortcuts to the pop-out window
- const setupKeyboardShortcuts = () => {
- const handleKeyDown = (e: KeyboardEvent) => {
- // Don't trigger shortcuts when user is typing in inputs, textareas, or contenteditable elements
- const target = e.target as HTMLElement;
- if (target && (
- target.tagName === 'INPUT' ||
- target.tagName === 'TEXTAREA' ||
- target.tagName === 'SELECT' ||
- target.contentEditable === 'true' ||
- target.closest('[contenteditable="true"]')
- )) {
- return;
+ window.document.close();
+ // Set up message listener for updates from main window
+ const handleMessage = (event: MessageEvent) => {
+ if (event.data.type === 'VIDEO_UPDATE') {
+ updatePopOutVideo(window, event.data);
+ }
+ else if (event.data.type === 'PLAYBACK_UPDATE') {
+ updatePlaybackState(window, event.data);
+ }
+ };
+ window.addEventListener('message', handleMessage);
+ window.addEventListener('beforeunload', () => {
+ globalPopOutWindowOpen = false;
+ globalPopOutWindow = null;
+ setIsPopOutOpen(false);
+ });
+ // Add keyboard shortcuts to the pop-out window
+ const setupKeyboardShortcuts = () => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // Don't trigger shortcuts when user is typing in inputs, textareas, or contenteditable elements
+ const target = e.target as HTMLElement;
+ if (target && (target.tagName === 'INPUT' ||
+ target.tagName === 'TEXTAREA' ||
+ target.tagName === 'SELECT' ||
+ target.contentEditable === 'true' ||
+ target.closest('[contenteditable="true"]'))) {
+ return;
+ }
+ // Send keyboard command back to main window
+ if (window.opener && !window.opener.closed) {
+ window.opener.postMessage({
+ type: 'KEYBOARD_COMMAND',
+ keyCode: e.code,
+ ctrlKey: e.ctrlKey,
+ metaKey: e.metaKey,
+ altKey: e.altKey,
+ shiftKey: e.shiftKey
+ }, '*');
+ e.preventDefault();
+ }
+ };
+ window.document.addEventListener('keydown', handleKeyDown);
+ // Cleanup function
+ return () => {
+ window.document.removeEventListener('keydown', handleKeyDown);
+ };
+ };
+ // Set up keyboard shortcuts after DOM is ready
+ if (window.document.readyState === 'complete') {
+ setupKeyboardShortcuts();
}
-
- // Send keyboard command back to main window
- if (window.opener && !window.opener.closed) {
- window.opener.postMessage({
- type: 'KEYBOARD_COMMAND',
- keyCode: e.code,
- ctrlKey: e.ctrlKey,
- metaKey: e.metaKey,
- altKey: e.altKey,
- shiftKey: e.shiftKey
- }, '*');
- e.preventDefault();
+ else {
+ window.addEventListener('load', setupKeyboardShortcuts);
}
- };
-
- window.document.addEventListener('keydown', handleKeyDown);
-
- // Cleanup function
- return () => {
- window.document.removeEventListener('keydown', handleKeyDown);
- };
- };
-
- // Set up keyboard shortcuts after DOM is ready
- if (window.document.readyState === 'complete') {
- setupKeyboardShortcuts();
- } else {
- window.addEventListener('load', setupKeyboardShortcuts);
- }
- };
-
- const updatePopOutVideo = (popWindow: Window, data: any) => {
- const { videoUrl, title, queueIndex, queueLength } = data;
-
- const videoContainer = popWindow.document.getElementById('videoContainer');
- const placeholder = popWindow.document.getElementById('placeholder');
- const queueInfo = popWindow.document.getElementById('queueInfo');
- const currentTrack = popWindow.document.getElementById('currentTrack');
- const trackTitle = popWindow.document.getElementById('trackTitle');
-
- if (!videoContainer) return;
-
- if (videoUrl) {
- placeholder?.style.setProperty('display', 'none');
- queueInfo?.style.setProperty('display', 'block');
-
- if (currentTrack) currentTrack.textContent = `Track ${queueIndex + 1} of ${queueLength}`;
- if (trackTitle) trackTitle.textContent = title || 'Untitled Video';
-
- // Remove existing video
- const existingVideo = videoContainer.querySelector('video');
- if (existingVideo) {
- existingVideo.remove();
- }
-
- // Create new video element
- const video = popWindow.document.createElement('video');
- video.className = 'video';
- video.src = videoUrl;
- video.controls = true;
- video.autoplay = isPlaying;
- video.crossOrigin = 'anonymous'; // Add CORS support like main player
- video.preload = 'auto'; // Match main player settings
-
- // Apply current video effects
- applyEffectsToPopOutVideo(video);
-
- console.log('🎬 Creating pop-out video element:', {
- src: videoUrl,
- controls: true,
- autoplay: isPlaying
- });
-
- // Add error handling to pop-out video
- video.addEventListener('error', (e) => {
- console.error('❌ Pop-out video error:', {
- error: e,
- errorCode: video.error?.code,
- errorMessage: video.error?.message,
- src: video.src
- });
- });
-
- video.addEventListener('loadstart', () => {
- console.log('🔄 Pop-out video load started');
- });
-
- video.addEventListener('canplay', () => {
- console.log('✅ Pop-out video can play');
- });
-
- videoContainer.appendChild(video);
- } else {
- placeholder?.style.setProperty('display', 'flex');
- queueInfo?.style.setProperty('display', 'none');
-
- const existingVideo = videoContainer.querySelector('video');
- if (existingVideo) {
- existingVideo.remove();
- }
- }
- };
-
- const updatePlaybackState = (popWindow: Window, data: any) => {
- const video = popWindow.document.querySelector('video') as HTMLVideoElement;
- if (video) {
- if (data.isPlaying) {
- video.play();
- } else {
- video.pause();
- }
- }
- };
-
- // Send updates to pop-out window when video changes
- useEffect(() => {
- if (globalPopOutWindow && !globalPopOutWindow.closed && currentVideo) {
- console.log('📺 Sending video update to pop-out:', {
- videoUrl: currentVideo.videoUrl,
- title: currentVideo.title,
- queueIndex: currentQueueIndex,
- queueLength: queueItems.length
- });
-
- globalPopOutWindow.postMessage({
- type: 'VIDEO_UPDATE',
- videoUrl: currentVideo.videoUrl,
- title: currentVideo.title,
- queueIndex: currentQueueIndex,
- queueLength: queueItems.length
- }, '*');
- }
- }, [currentVideo, currentQueueIndex, queueItems.length]);
-
- // Send playback state updates
- useEffect(() => {
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- globalPopOutWindow.postMessage({
- type: 'PLAYBACK_UPDATE',
- isPlaying
- }, '*');
- }
- }, [isPlaying]);
-
- // Listen for keyboard commands from pop-out window
- useEffect(() => {
- const handlePopOutMessage = (event: MessageEvent) => {
- if (event.data.type === 'KEYBOARD_COMMAND') {
- handleKeyboardCommand(event.data.keyCode, {
- ctrlKey: event.data.ctrlKey,
- metaKey: event.data.metaKey,
- altKey: event.data.altKey,
- shiftKey: event.data.shiftKey
- });
- }
- };
-
- window.addEventListener('message', handlePopOutMessage);
-
- return () => {
- window.removeEventListener('message', handlePopOutMessage);
};
- }, [currentVideo, isPlaying, videoEffects]);
-
- // Update video effects in pop-out window when they change
- useEffect(() => {
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
- if (video) {
- applyEffectsToPopOutVideo(video);
- }
- }
- }, [videoEffects]);
-
- // Cleanup on unmount
- useEffect(() => {
- return () => {
- if (windowCheckInterval.current) {
- clearInterval(windowCheckInterval.current);
- }
- // Don't close global window on unmount - let user close it manually
- };
- }, []);
-
- // Expose function globally for testing
- useEffect(() => {
- (window as any).testPopOutPlayer = () => {
- console.log('testPopOutPlayer: Manual test triggered');
- openPopOutPlayer();
+ const updatePopOutVideo = (popWindow: Window, data: any) => {
+ const { videoUrl, title, queueIndex, queueLength } = data;
+ const videoContainer = popWindow.document.getElementById('videoContainer');
+ const placeholder = popWindow.document.getElementById('placeholder');
+ const queueInfo = popWindow.document.getElementById('queueInfo');
+ const currentTrack = popWindow.document.getElementById('currentTrack');
+ const trackTitle = popWindow.document.getElementById('trackTitle');
+ if (!videoContainer)
+ return;
+ if (videoUrl) {
+ placeholder?.style.setProperty('display', 'none');
+ queueInfo?.style.setProperty('display', 'block');
+ if (currentTrack)
+ currentTrack.textContent = `Track ${queueIndex + 1} of ${queueLength}`;
+ if (trackTitle)
+ trackTitle.textContent = title || 'Untitled Video';
+ // Remove existing video
+ const existingVideo = videoContainer.querySelector('video');
+ if (existingVideo) {
+ existingVideo.remove();
+ }
+ // Create new video element
+ const video = popWindow.document.createElement('video');
+ video.className = 'video';
+ video.src = videoUrl;
+ video.controls = true;
+ video.autoplay = isPlaying;
+ video.crossOrigin = 'anonymous'; // Add CORS support like main player
+ video.preload = 'auto'; // Match main player settings
+ // Apply current video effects
+ applyEffectsToPopOutVideo(video);
+ console.log('🎬 Creating pop-out video element:', {
+ src: videoUrl,
+ controls: true,
+ autoplay: isPlaying
+ });
+ // Add error handling to pop-out video
+ video.addEventListener('error', (e) => {
+ console.error('❌ Pop-out video error:', {
+ error: e,
+ errorCode: video.error?.code,
+ errorMessage: video.error?.message,
+ src: video.src
+ });
+ });
+ video.addEventListener('loadstart', () => {
+ console.log('🔄 Pop-out video load started');
+ });
+ video.addEventListener('canplay', () => {
+ console.log('✅ Pop-out video can play');
+ });
+ videoContainer.appendChild(video);
+ }
+ else {
+ placeholder?.style.setProperty('display', 'flex');
+ queueInfo?.style.setProperty('display', 'none');
+ const existingVideo = videoContainer.querySelector('video');
+ if (existingVideo) {
+ existingVideo.remove();
+ }
+ }
};
-
- return () => {
- delete (window as any).testPopOutPlayer;
+ const updatePlaybackState = (popWindow: Window, data: any) => {
+ const video = popWindow.document.querySelector('video') as HTMLVideoElement;
+ if (video) {
+ if (data.isPlaying) {
+ video.play();
+ }
+ else {
+ video.pause();
+ }
+ }
};
- }, [openPopOutPlayer]);
-
- const closePopOut = () => {
- if (globalPopOutWindow && !globalPopOutWindow.closed) {
- globalPopOutWindow.close();
- }
- globalPopOutWindowOpen = false;
- globalPopOutWindow = null;
- setIsPopOutOpen(false);
-
- toast({
- title: "Pop-out Closed",
- description: "Video player returned to main interface.",
- });
- };
-
- return (
-
- {!isPopOutOpen ? (
- {
- e.preventDefault();
- e.stopPropagation();
+ // Send updates to pop-out window when video changes
+ useEffect(() => {
+ if (globalPopOutWindow && !globalPopOutWindow.closed && currentVideo) {
+ console.log('📺 Sending video update to pop-out:', {
+ videoUrl: currentVideo.videoUrl,
+ title: currentVideo.title,
+ queueIndex: currentQueueIndex,
+ queueLength: queueItems.length
+ });
+ globalPopOutWindow.postMessage({
+ type: 'VIDEO_UPDATE',
+ videoUrl: currentVideo.videoUrl,
+ title: currentVideo.title,
+ queueIndex: currentQueueIndex,
+ queueLength: queueItems.length
+ }, '*');
+ }
+ }, [currentVideo, currentQueueIndex, queueItems.length]);
+ // Send playback state updates
+ useEffect(() => {
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ globalPopOutWindow.postMessage({
+ type: 'PLAYBACK_UPDATE',
+ isPlaying
+ }, '*');
+ }
+ }, [isPlaying]);
+ // Listen for keyboard commands from pop-out window
+ useEffect(() => {
+ const handlePopOutMessage = (event: MessageEvent) => {
+ if (event.data.type === 'KEYBOARD_COMMAND') {
+ handleKeyboardCommand(event.data.keyCode, {
+ ctrlKey: event.data.ctrlKey,
+ metaKey: event.data.metaKey,
+ altKey: event.data.altKey,
+ shiftKey: event.data.shiftKey
+ });
+ }
+ };
+ window.addEventListener('message', handlePopOutMessage);
+ return () => {
+ window.removeEventListener('message', handlePopOutMessage);
+ };
+ }, [currentVideo, isPlaying, videoEffects]);
+ // Update video effects in pop-out window when they change
+ useEffect(() => {
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ const video = globalPopOutWindow.document.querySelector('video') as HTMLVideoElement;
+ if (video) {
+ applyEffectsToPopOutVideo(video);
+ }
+ }
+ }, [videoEffects]);
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ if (windowCheckInterval.current) {
+ clearInterval(windowCheckInterval.current);
+ }
+ // Don't close global window on unmount - let user close it manually
+ };
+ }, []);
+ // Expose function globally for testing
+ useEffect(() => {
+ (window as any).testPopOutPlayer = () => {
+ console.log('testPopOutPlayer: Manual test triggered');
openPopOutPlayer();
- }}
- variant="ghost"
- size="sm"
- className={`w-8 h-8 p-0 ${getThemeClasses()}`}
- title="Open video player in separate window"
- data-testid="button-pop-out-player"
- >
-
-
- ) : (
-
-
-
- )}
-
- );
-}
\ No newline at end of file
+ };
+ return () => {
+ delete (window as any).testPopOutPlayer;
+ };
+ }, [openPopOutPlayer]);
+ const closePopOut = () => {
+ if (globalPopOutWindow && !globalPopOutWindow.closed) {
+ globalPopOutWindow.close();
+ }
+ globalPopOutWindowOpen = false;
+ globalPopOutWindow = null;
+ setIsPopOutOpen(false);
+ toast({
+ title: "Pop-out Closed",
+ description: "Video player returned to main interface.",
+ });
+ };
+ return (
+ {!isPopOutOpen ? ( {
+ e.preventDefault();
+ e.stopPropagation();
+ openPopOutPlayer();
+ }} variant="ghost" size="sm" className={`w-8 h-8 p-0 ${getThemeClasses()}`} title="Open video player in separate window" data-testid="button-pop-out-player">
+
+ ) : (
+
+ )}
+
);
+}
diff --git a/client/src/components/PreviewWindow.tsx b/client/src/components/PreviewWindow.tsx
index 542ba00..7bb0cc5 100644
--- a/client/src/components/PreviewWindow.tsx
+++ b/client/src/components/PreviewWindow.tsx
@@ -1,172 +1,125 @@
import React, { useRef, useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { useStore } from '@/lib/store';
-import {
- Play,
- Pause,
- ArrowRight
-} from 'lucide-react';
-
+import { Play, Pause, ArrowRight } from 'lucide-react';
interface PreviewWindowProps {
- videoUrl?: string;
- video?: any;
- className?: string;
+ videoUrl?: string;
+ video?: any;
+ className?: string;
}
-
export function PreviewWindow({ videoUrl, video, className = '' }: PreviewWindowProps) {
- const videoRef = useRef(null);
- const [isPlaying, setIsPlaying] = useState(false);
- const [currentTime, setCurrentTime] = useState(0);
- const [duration, setDuration] = useState(0);
- const [isVideoLoading, setIsVideoLoading] = useState(false);
- const [videoLoadError, setVideoLoadError] = useState(false);
-
- const {
- brandSkin,
- videoEffects,
- addToQueue
- } = useStore();
-
- // Check if this is static content (images, not videos)
- const isStaticImage = videoUrl?.endsWith('.svg') || videoUrl?.endsWith('.png') || videoUrl?.endsWith('.jpg') || videoUrl?.endsWith('.jpeg') || videoUrl?.endsWith('.gif');
-
- // Load video when URL changes (only for actual video content)
- useEffect(() => {
- if (videoRef.current && videoUrl && !isStaticImage) {
- console.log('🎬 PreviewWindow: Loading preview video:', {
- url: videoUrl.substring(0, 100) + '...',
- hasVideo: !!video,
- videoIdentifier: video?.identifier
- });
- setIsVideoLoading(true);
- setVideoLoadError(false);
-
- const videoEl = videoRef.current;
-
- const handleLoadStart = () => {
- setIsVideoLoading(true);
- setVideoLoadError(false);
- };
-
- const handleLoadedData = () => {
- setIsVideoLoading(false);
- setDuration(videoEl.duration || 0);
- };
-
- const handleError = (e: any) => {
- console.error('❌ PreviewWindow: Video load error:', {
- error: e,
- videoUrl: videoUrl?.substring(0, 100) + '...',
- errorCode: videoEl.error?.code,
- errorMessage: videoEl.error?.message,
- networkState: videoEl.networkState,
- readyState: videoEl.readyState
- });
- setIsVideoLoading(false);
- setVideoLoadError(true);
- };
-
- const handleTimeUpdate = () => {
- setCurrentTime(videoEl.currentTime);
- };
-
- videoEl.addEventListener('loadstart', handleLoadStart);
- videoEl.addEventListener('loadeddata', handleLoadedData);
- videoEl.addEventListener('error', handleError);
- videoEl.addEventListener('timeupdate', handleTimeUpdate);
-
- videoEl.src = videoUrl;
- videoEl.load();
-
- return () => {
- videoEl.removeEventListener('loadstart', handleLoadStart);
- videoEl.removeEventListener('loadeddata', handleLoadedData);
- videoEl.removeEventListener('error', handleError);
- videoEl.removeEventListener('timeupdate', handleTimeUpdate);
- };
- } else if (isStaticImage) {
- // For static content, immediately set as loaded
- setIsVideoLoading(false);
- setVideoLoadError(false);
- setDuration(0); // Static images have no duration
- }
- }, [videoUrl, isStaticImage]);
-
- // Apply volume changes (only for video content)
- useEffect(() => {
- if (videoRef.current && !isStaticImage) {
- videoRef.current.volume = 0.5; // Set default volume for preview
- }
- }, [isStaticImage]);
-
- const handlePlayPause = () => {
- // Static images can't be played/paused
- if (isStaticImage) return;
-
- if (videoRef.current) {
- if (isPlaying) {
- videoRef.current.pause();
- setIsPlaying(false);
- } else {
- videoRef.current.play();
- setIsPlaying(true);
- }
- }
- };
-
-
- const handleCueToProgram = () => {
- if (video && videoUrl) {
- console.log('🎯 Cueing video to program output:', video.identifier);
- addToQueue(video, videoUrl, true); // Add to front of queue
- }
- };
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'bg-slate-900/95 border-slate-600 text-slate-100';
- case 'waffle':
- return 'bg-yellow-900/95 border-yellow-600 text-yellow-100';
- case 'ebn':
- return 'bg-lime-900/95 border-lime-600 text-lime-100';
- default:
- return 'bg-gray-900/95 border-gray-600 text-gray-100';
- }
- };
-
- return (
-
+ const videoRef = useRef
(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [duration, setDuration] = useState(0);
+ const [isVideoLoading, setIsVideoLoading] = useState(false);
+ const [videoLoadError, setVideoLoadError] = useState(false);
+ const { brandSkin, videoEffects, addToQueue } = useStore();
+ // Check if this is static content (images, not videos)
+ const isStaticImage = videoUrl?.endsWith('.svg') || videoUrl?.endsWith('.png') || videoUrl?.endsWith('.jpg') || videoUrl?.endsWith('.jpeg') || videoUrl?.endsWith('.gif');
+ // Load video when URL changes (only for actual video content)
+ useEffect(() => {
+ if (videoRef.current && videoUrl && !isStaticImage) {
+ console.log('🎬 PreviewWindow: Loading preview video:', {
+ url: videoUrl.substring(0, 100) + '...',
+ hasVideo: !!video,
+ videoIdentifier: video?.identifier
+ });
+ setIsVideoLoading(true);
+ setVideoLoadError(false);
+ const videoEl = videoRef.current;
+ const handleLoadStart = () => {
+ setIsVideoLoading(true);
+ setVideoLoadError(false);
+ };
+ const handleLoadedData = () => {
+ setIsVideoLoading(false);
+ setDuration(videoEl.duration || 0);
+ };
+ const handleError = (e: any) => {
+ console.error('❌ PreviewWindow: Video load error:', {
+ error: e,
+ videoUrl: videoUrl?.substring(0, 100) + '...',
+ errorCode: videoEl.error?.code,
+ errorMessage: videoEl.error?.message,
+ networkState: videoEl.networkState,
+ readyState: videoEl.readyState
+ });
+ setIsVideoLoading(false);
+ setVideoLoadError(true);
+ };
+ const handleTimeUpdate = () => {
+ setCurrentTime(videoEl.currentTime);
+ };
+ videoEl.addEventListener('loadstart', handleLoadStart);
+ videoEl.addEventListener('loadeddata', handleLoadedData);
+ videoEl.addEventListener('error', handleError);
+ videoEl.addEventListener('timeupdate', handleTimeUpdate);
+ videoEl.src = videoUrl;
+ videoEl.load();
+ return () => {
+ videoEl.removeEventListener('loadstart', handleLoadStart);
+ videoEl.removeEventListener('loadeddata', handleLoadedData);
+ videoEl.removeEventListener('error', handleError);
+ videoEl.removeEventListener('timeupdate', handleTimeUpdate);
+ };
+ }
+ else if (isStaticImage) {
+ // For static content, immediately set as loaded
+ setIsVideoLoading(false);
+ setVideoLoadError(false);
+ setDuration(0); // Static images have no duration
+ }
+ }, [videoUrl, isStaticImage]);
+ // Apply volume changes (only for video content)
+ useEffect(() => {
+ if (videoRef.current && !isStaticImage) {
+ videoRef.current.volume = 0.5; // Set default volume for preview
+ }
+ }, [isStaticImage]);
+ const handlePlayPause = () => {
+ // Static images can't be played/paused
+ if (isStaticImage)
+ return;
+ if (videoRef.current) {
+ if (isPlaying) {
+ videoRef.current.pause();
+ setIsPlaying(false);
+ }
+ else {
+ videoRef.current.play();
+ setIsPlaying(true);
+ }
+ }
+ };
+ const handleCueToProgram = () => {
+ if (video && videoUrl) {
+ console.log('🎯 Cueing video to program output:', video.identifier);
+ addToQueue(video, videoUrl, true); // Add to front of queue
+ }
+ };
+ const getThemeClasses = () => {
+ {
+ return 'bg-lime-900/95 border-lime-600 text-lime-100';
+ }
+ };
+ return (
- {isVideoLoading ? (
-
+ {isVideoLoading ? (
Loading preview...
{video &&
{video.title?.substring(0, 30)}...
}
-
- ) : videoLoadError || !videoUrl ? (
-
+
) : videoLoadError || !videoUrl ? (
{videoLoadError ? 'Load failed' : 'No preview'}
- {videoLoadError && videoUrl && (
-
Check console for details
- )}
- {!videoUrl && video && (
-
{video.title?.substring(0, 30)}...
- )}
+ {videoLoadError && videoUrl && (
Check console for details
)}
+ {!videoUrl && video && (
{video.title?.substring(0, 30)}...
)}
-
- ) : (
- <>
+
) : (<>
{/* Handle static images */}
- {isStaticImage ? (
-
-
+
-
- ) : (
-
+
) : (
- )}
+ transform: `rotate(${videoEffects.rotate}deg) scaleX(${videoEffects.scaleX / 100}) scaleY(${videoEffects.scaleY / 100})`
+ }} muted playsInline preload="metadata" crossOrigin="anonymous"/>)}
{/* Compact Controls Overlay */}
{/* Only show play/pause for video content */}
- {!isStaticImage && (
-
- {isPlaying ? : }
-
- )}
+ {!isStaticImage && (
+ {isPlaying ? : }
+ )}
- {video && (
-
-
+ {video && (
+
Cue
-
- )}
+ )}
{/* Progress Bar - only for video content with duration */}
- {duration > 0 && !isStaticImage && (
-
- )}
- >
- )}
+ {duration > 0 && !isStaticImage && (
)}
+ >)}
-
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/QueueDropZone.tsx b/client/src/components/QueueDropZone.tsx
index f06bff6..f51fc79 100644
--- a/client/src/components/QueueDropZone.tsx
+++ b/client/src/components/QueueDropZone.tsx
@@ -3,122 +3,86 @@ import { Plus } from 'lucide-react';
import { useStore } from '@/lib/store';
import { getVideoMetadata } from '@/lib/archive-api';
import { type VideoResult } from '@/lib/types';
-
interface QueueDropZoneProps {
- className?: string;
+ className?: string;
}
-
export function QueueDropZone({ className = '' }: QueueDropZoneProps) {
- const [isDragOver, setIsDragOver] = useState(false);
- const [isDropping, setIsDropping] = useState(false);
- const { addToQueue, brandSkin } = useStore();
-
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault();
- e.dataTransfer.dropEffect = 'copy';
- setIsDragOver(true);
- };
-
- const handleDragLeave = (e: React.DragEvent) => {
- e.preventDefault();
- // Only hide if we're leaving the drop zone itself, not a child element
- if (!e.currentTarget.contains(e.relatedTarget as Node)) {
- setIsDragOver(false);
- }
- };
-
- const handleDrop = async (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragOver(false);
- setIsDropping(true);
-
- try {
- const dragData = e.dataTransfer.getData('application/json');
- if (!dragData) return;
-
- const { video, type, sourceType } = JSON.parse(dragData);
-
- if (type === 'video' && sourceType === 'search') {
- console.log('🎬 Adding dropped video to queue:', video.identifier);
-
- // Get metadata and add to queue
- const metadata = await getVideoMetadata(video.identifier);
- let videoUrl = metadata.streamUrl;
- if (!videoUrl && metadata.videoFile) {
- videoUrl = `/api/video/${video.identifier}/${encodeURIComponent(metadata.videoFile.name)}`;
- } else if (!videoUrl) {
- console.error(`❌ No streamUrl available for ${video.identifier}`);
- throw new Error(`Video ${video.identifier} has no playable stream URL. Please try a different video.`);
+ const [isDragOver, setIsDragOver] = useState(false);
+ const [isDropping, setIsDropping] = useState(false);
+ const { addToQueue, brandSkin } = useStore();
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'copy';
+ setIsDragOver(true);
+ };
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault();
+ // Only hide if we're leaving the drop zone itself, not a child element
+ if (!e.currentTarget.contains(e.relatedTarget as Node)) {
+ setIsDragOver(false);
}
-
- addToQueue(video, videoUrl);
- console.log('✅ Video added to queue via drag-and-drop');
- }
- } catch (error) {
- console.error('❌ Failed to add dropped video to queue:', error);
- } finally {
- setIsDropping(false);
- }
- };
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'waffle':
- return {
- base: 'border-yellow-400/50 bg-yellow-600/10',
- active: 'border-yellow-500 bg-yellow-600/20',
- text: 'text-yellow-200'
- };
- case 'ebn':
- return {
- base: 'border-lime-500/50 bg-lime-500/10',
- active: 'border-lime-400 bg-lime-500/20',
- text: 'text-lime-300'
- };
- default:
- return {
- base: 'border-gray-400/50 bg-gray-600/10',
- active: 'border-blue-400/60 bg-blue-500/15',
- text: 'text-gray-300'
- };
- }
- };
-
- const theme = getThemeClasses();
-
- return (
- {
+ e.preventDefault();
+ setIsDragOver(false);
+ setIsDropping(true);
+ try {
+ const dragData = e.dataTransfer.getData('application/json');
+ if (!dragData)
+ return;
+ const { video, type, sourceType } = JSON.parse(dragData);
+ if (type === 'video' && sourceType === 'search') {
+ console.log('🎬 Adding dropped video to queue:', video.identifier);
+ // Get metadata and add to queue
+ const metadata = await getVideoMetadata(video.identifier);
+ let videoUrl = metadata.streamUrl;
+ if (!videoUrl && metadata.videoFile) {
+ videoUrl = `/api/video/${video.identifier}/${encodeURIComponent(metadata.videoFile.name)}`;
+ }
+ else if (!videoUrl) {
+ console.error(`❌ No streamUrl available for ${video.identifier}`);
+ throw new Error(`Video ${video.identifier} has no playable stream URL. Please try a different video.`);
+ }
+ addToQueue(video, videoUrl);
+ console.log('✅ Video added to queue via drag-and-drop');
+ }
+ }
+ catch (error) {
+ console.error('❌ Failed to add dropped video to queue:', error);
+ }
+ finally {
+ setIsDropping(false);
+ }
+ };
+ const getThemeClasses = () => {
+ {
+ return {
+ base: 'border-lime-500/50 bg-lime-500/10',
+ active: 'border-lime-400 bg-lime-500/20',
+ text: 'text-lime-300'
+ };
+ }
+ };
+ const theme = getThemeClasses();
+ return (
+ `} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} data-testid="queue-drop-zone">
- {isDropping ? (
- <>
-
+ {isDropping ? (<>
+
Adding to queue...
- >
- ) : isDragOver ? (
- <>
-
+ >) : isDragOver ? (<>
+
Drop video here to add to queue
- >
- ) : (
- <>
-
+ >) : (<>
+
Drag videos from search results
or use the + button
- >
- )}
+ >)}
-
- );
-}
\ No newline at end of file
+
);
+}
diff --git a/client/src/components/QueuePanel.tsx b/client/src/components/QueuePanel.tsx
index 53f4287..40684bf 100644
--- a/client/src/components/QueuePanel.tsx
+++ b/client/src/components/QueuePanel.tsx
@@ -3,80 +3,45 @@ import { Trash2, GripVertical, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useStore } from '@/lib/store';
import { QueueDropZone } from './QueueDropZone';
-
export function QueuePanel() {
- const {
- queueItems,
- removeFromQueue,
- updateQueueItem,
- reorderQueue,
- clearQueue,
- brandSkin
- } = useStore();
-
- const handleDragEnd = (result: any) => {
- if (!result.destination) {
- return;
- }
-
- reorderQueue(result.source.index, result.destination.index);
- };
-
- const calculateTotalDuration = () => {
- let totalSeconds = 0;
- queueItems.forEach(item => {
- const startSeconds = parseTimeToSeconds(item.trimIn);
- const endSeconds = parseTimeToSeconds(item.trimOut);
- totalSeconds += Math.max(0, endSeconds - startSeconds);
- });
-
- const minutes = Math.floor(totalSeconds / 60);
- const seconds = Math.floor(totalSeconds % 60);
- return `${minutes}:${seconds.toString().padStart(2, '0')}`;
- };
-
- const parseTimeToSeconds = (time: string): number => {
- const parts = time.split(':').map(Number);
- if (parts.length === 2) {
- return parts[0] * 60 + parts[1];
- } else if (parts.length === 3) {
- return parts[0] * 3600 + parts[1] * 60 + parts[2];
- }
- return 0;
- };
-
- return (
-
- {queueItems.length === 0 ? (
-
- ) : (
-
+ const { queueItems, removeFromQueue, updateQueueItem, reorderQueue, clearQueue, brandSkin } = useStore();
+ const handleDragEnd = (result: any) => {
+ if (!result.destination) {
+ return;
+ }
+ reorderQueue(result.source.index, result.destination.index);
+ };
+ const calculateTotalDuration = () => {
+ let totalSeconds = 0;
+ queueItems.forEach(item => {
+ const startSeconds = parseTimeToSeconds(item.trimIn);
+ const endSeconds = parseTimeToSeconds(item.trimOut);
+ totalSeconds += Math.max(0, endSeconds - startSeconds);
+ });
+ const minutes = Math.floor(totalSeconds / 60);
+ const seconds = Math.floor(totalSeconds % 60);
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`;
+ };
+ const parseTimeToSeconds = (time: string): number => {
+ const parts = time.split(':').map(Number);
+ if (parts.length === 2) {
+ return parts[0] * 60 + parts[1];
+ }
+ else if (parts.length === 3) {
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
+ }
+ return 0;
+ };
+ return (
+ {queueItems.length === 0 ? (
) : (
{/* Timeline Segments */}
- {(provided) => (
-
- {queueItems.map((item, index) => (
-
- {(provided, snapshot) => (
-
+ {(provided) => (
+ {queueItems.map((item, index) => (
+ {(provided, snapshot) => (
{item.title.slice(0, 10)}
@@ -84,63 +49,37 @@ export function QueuePanel() {
{item.duration}
- {(item.trimIn !== '00:00' || item.trimOut !== item.duration) && (
-
+ {(item.trimIn !== '00:00' || item.trimOut !== item.duration) && (
✂
-
- )}
- {item.loop && (
-
+
)}
+ {item.loop && (
🔄
-
- )}
+
)}
- updateQueueItem(item.id, { loop: !item.loop })}
- className="absolute -top-1 -left-1 w-4 h-4 bg-blue-500 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity"
- title={item.loop ? "Disable loop" : "Enable loop"}
- >
+ updateQueueItem(item.id, { loop: !item.loop })} className="absolute -top-1 -left-1 w-4 h-4 bg-blue-500 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity" title={item.loop ? "Disable loop" : "Enable loop"}>
🔄
- removeFromQueue(item.id)}
- className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity"
- >
+ removeFromQueue(item.id)} className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity">
×
-
- )}
-
- ))}
+
)}
+ ))}
{provided.placeholder}
-
- )}
+
)}
- {queueItems.length > 0 && (
-
+ {queueItems.length > 0 && (
{queueItems.length} clips • {calculateTotalDuration()}
-
+
Clear
-
- )}
-
- )}
-
- );
-}
\ No newline at end of file
+
)}
+
)}
+
);
+}
diff --git a/client/src/components/ResizablePanels.tsx b/client/src/components/ResizablePanels.tsx
deleted file mode 100644
index 06045e8..0000000
--- a/client/src/components/ResizablePanels.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-import { useState, useCallback } from 'react';
-import { Panel, PanelGroup, PanelResizeHandle, ImperativePanelHandle } from 'react-resizable-panels';
-import { GripVertical, GripHorizontal } from 'lucide-react';
-
-import { Filters } from '@/components/Filters';
-import { ResultsGrid } from '@/components/ResultsGrid';
-import { Player } from '@/components/Player';
-import { QueuePanel } from '@/components/QueuePanel';
-import { EffectsPanel } from '@/components/EffectsPanel';
-import { PanelHeader } from '@/components/PanelHeader';
-import { useStore } from '@/lib/store';
-import { type VideoResult } from '@/lib/types';
-
-interface ResizablePanelsProps {
- searchResults: VideoResult[];
- totalResults: number;
- isLoading: boolean;
- onSearch: (query: string) => void;
- onVideoSelect: (video: VideoResult) => void;
-}
-
-export function ResizablePanels({
- searchResults,
- totalResults,
- isLoading,
- onSearch,
- onVideoSelect
-}: ResizablePanelsProps) {
- const { brandSkin, queueItems, panelSizes, setPanelSizes } = useStore();
-
- const handleLayoutChange = useCallback((sizes: number[]) => {
- setPanelSizes(sizes);
- }, [setPanelSizes]);
-
- const panelHeaderClass = `sticky top-0 z-10 ${
- brandSkin === 'testcard'
- ? 'glass-testcard border-slate-400/20'
- : brandSkin === 'waffle'
- ? 'glass border-yellow-400/30'
- : brandSkin === 'ebn'
- ? 'glass-dark border-lime-500/20'
- : brandSkin === 'ozzy'
- ? 'glass-ozzy border-red-500/30'
- : brandSkin === 'mario'
- ? 'glass-mario border-yellow-400/30'
- : 'glass-hogan border-yellow-400/30'
- }`;
-
- const resizeHandleClass = `${
- brandSkin === 'testcard'
- ? 'bg-slate-600/20 hover:bg-blue-400/30 border-slate-400/30'
- : brandSkin === 'waffle'
- ? 'bg-amber-600/20 hover:bg-amber-400/30 border-yellow-400/30'
- : brandSkin === 'ebn'
- ? 'bg-lime-600/20 hover:bg-lime-400/30 border-lime-500/30'
- : brandSkin === 'ozzy'
- ? 'bg-red-600/20 hover:bg-red-400/30 border-red-500/30'
- : brandSkin === 'mario'
- ? 'bg-red-600/20 hover:bg-red-400/30 border-red-500/30'
- : 'bg-yellow-600/20 hover:bg-yellow-400/30 border-yellow-400/30'
- }`;
-
- return (
-
- {/* Top Row - Search, Preview, Effects */}
-
-
- {/* Search & Results Panel */}
-
-
-
-
-
-
- {/* Vertical Resize Handle */}
-
-
-
-
-
- {/* Snap Guide Indicators */}
-
-
-
- {/* Preview Panel */}
-
-
-
-
-
-
- {/* Vertical Resize Handle */}
-
-
-
-
-
-
- {/* Effects Panel */}
-
-
-
-
-
-
-
-
-
-
- {/* Bottom Timeline Panel - Full Width */}
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ResponsiveBreakpointIndicator.tsx b/client/src/components/ResponsiveBreakpointIndicator.tsx
deleted file mode 100644
index 64cec3c..0000000
--- a/client/src/components/ResponsiveBreakpointIndicator.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import { useEffect, useState } from 'react';
-import { useIsMobile } from '@/hooks/use-mobile';
-import { useResponsiveLayout } from '@/hooks/use-responsive-layout';
-import { useStore } from '@/lib/store';
-import { Smartphone, Tablet, Monitor, Settings, Eye } from 'lucide-react';
-import { Badge } from '@/components/ui/badge';
-import { Button } from '@/components/ui/button';
-import { useToast } from '@/hooks/use-toast';
-
-export function ResponsiveBreakpointIndicator() {
- const { brandSkin } = useStore();
- const { toast } = useToast();
- const isMobile = useIsMobile();
- const {
- currentLayout,
- setManualLayout,
- triggerLayoutAnimation,
- canResize,
- breakpoints
- } = useResponsiveLayout();
- const [currentBreakpoint, setCurrentBreakpoint] = useState<'mobile' | 'tablet' | 'desktop'>('desktop');
- const [showIndicator, setShowIndicator] = useState(false);
- const [isManualMode, setIsManualMode] = useState(false);
-
- // Sync with responsive layout hook
- useEffect(() => {
- if (currentLayout !== currentBreakpoint) {
- setCurrentBreakpoint(currentLayout);
- setShowIndicator(true);
-
- // Hide indicator after 3 seconds
- setTimeout(() => setShowIndicator(false), 3000);
- }
- }, [currentLayout, currentBreakpoint]);
-
- // Manual layout switching functions
- const switchToLayout = (layout: 'mobile' | 'tablet' | 'desktop') => {
- setManualLayout(layout);
- setIsManualMode(true);
-
- toast({
- title: `Switched to ${layout} layout`,
- description: `Interface adapted for ${layout} view`,
- duration: 2000,
- });
-
- // Auto-revert after 8 seconds
- setTimeout(() => {
- setIsManualMode(false);
- const actualWidth = window.innerWidth;
- const autoLayout = actualWidth < 768 ? 'mobile' : actualWidth < 1024 ? 'tablet' : 'desktop';
- setManualLayout(autoLayout);
- }, 8000);
- };
-
- const resetToAuto = () => {
- const width = window.innerWidth;
- const autoLayout = width < 768 ? 'mobile' : width < 1024 ? 'tablet' : 'desktop';
- setManualLayout(autoLayout);
- setIsManualMode(false);
-
- toast({
- title: "Layout reset to automatic",
- description: "Now responding to actual screen size",
- duration: 2000,
- });
- };
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'bg-blue-900/90 border-blue-400/50 text-blue-300';
- case 'waffle':
- return 'bg-amber-100/90 border-amber-400/50 text-amber-800';
- case 'ebn':
- return 'bg-lime-900/90 border-lime-400/50 text-lime-300';
- case 'ozzy':
- return 'bg-red-900/90 border-red-400/50 text-red-300';
- case 'hogan':
- return 'bg-yellow-900/90 border-yellow-400/50 text-yellow-300';
- case 'dx':
- return 'bg-pink-900/90 border-pink-400/50 text-pink-300';
- case 'maxheadroom':
- return 'bg-green-900/90 border-green-400/50 text-green-300';
- case 'mario':
- return 'bg-red-900/90 border-yellow-400/50 text-yellow-300';
- case 'dakota':
- return 'bg-gray-800/90 border-gray-400/50 text-gray-300';
- case 'blondie':
- return 'bg-amber-900/90 border-amber-400/50 text-amber-300';
- default:
- return 'bg-blue-900/90 border-blue-400/50 text-blue-300';
- }
- };
-
- const breakpointConfig = {
- mobile: {
- icon: Smartphone,
- label: 'Mobile',
- width: '< 768px'
- },
- tablet: {
- icon: Tablet,
- label: 'Tablet',
- width: '768px - 1024px'
- },
- desktop: {
- icon: Monitor,
- label: 'Desktop',
- width: '> 1024px'
- }
- };
-
- const config = breakpointConfig[currentBreakpoint];
- const IconComponent = config.icon;
-
- // Enhanced indicator display logic
- if (!showIndicator && !isManualMode) return null;
-
- return (
-
-
-
-
- {config.label} Layout
- {isManualMode && (
-
- Manual
-
- )}
-
-
-
- {window.innerWidth}px
-
- {isManualMode && (
-
-
- Auto
-
- )}
-
-
-
- {/* Quick Layout Switcher */}
-
- {(Object.keys(breakpointConfig) as Array).map((layout) => {
- const layoutConfig = breakpointConfig[layout];
- const LayoutIcon = layoutConfig.icon;
- const isActive = currentBreakpoint === layout;
- return (
- switchToLayout(layout)}
- disabled={isActive}
- className="h-7 px-2 text-xs flex items-center hover:bg-white/20"
- data-testid={`button-quick-${layout}`}
- >
-
- {layout}
-
- );
- })}
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ResponsiveLayoutHints.tsx b/client/src/components/ResponsiveLayoutHints.tsx
deleted file mode 100644
index d83f433..0000000
--- a/client/src/components/ResponsiveLayoutHints.tsx
+++ /dev/null
@@ -1,396 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Monitor, Tablet, Smartphone, RotateCcw, Play, Pause, Eye, EyeOff } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { useStore } from '@/lib/store';
-import { useIsMobile } from '@/hooks/use-mobile';
-
-interface LayoutBreakpoint {
- name: string;
- width: number;
- icon: React.ComponentType<{ className?: string; style?: React.CSSProperties }>;
- description: string;
-}
-
-const breakpoints: LayoutBreakpoint[] = [
- {
- name: 'Desktop',
- width: 1024,
- icon: Monitor,
- description: 'Full VJ workspace - Search, Preview, Queue, Effects all visible'
- },
- {
- name: 'Tablet',
- width: 768,
- icon: Tablet,
- description: 'Streamlined layout - Search + Preview with collapsible queue'
- },
- {
- name: 'Mobile',
- width: 375,
- icon: Smartphone,
- description: 'Touch-first stacked panels with swipe navigation'
- }
-];
-
-export function ResponsiveLayoutHints() {
- const { brandSkin } = useStore();
- const isMobile = useIsMobile();
- const [isAnimating, setIsAnimating] = useState(false);
- const [currentBreakpoint, setCurrentBreakpoint] = useState(0);
- const [isVisible, setIsVisible] = useState(false);
- const [showRealTime, setShowRealTime] = useState(false);
-
- // Auto-cycle through breakpoints when animating
- useEffect(() => {
- if (!isAnimating) return;
-
- const interval = setInterval(() => {
- setCurrentBreakpoint((prev) => (prev + 1) % breakpoints.length);
- }, 2500); // Slightly slower for better visibility
-
- return () => clearInterval(interval);
- }, [isAnimating]);
-
- // Add a pulsing effect during animation
- const [isPulsing, setIsPulsing] = useState(false);
-
- useEffect(() => {
- if (isAnimating) {
- setIsPulsing(true);
- const pulse = setInterval(() => {
- setIsPulsing(prev => !prev);
- }, 1000);
- return () => clearInterval(pulse);
- } else {
- setIsPulsing(false);
- }
- }, [isAnimating]);
-
- const toggleAnimation = () => {
- setIsAnimating(!isAnimating);
- };
-
- const resetToDesktop = () => {
- setCurrentBreakpoint(0);
- setIsAnimating(false);
- };
-
- const toggleVisibility = () => {
- setIsVisible(!isVisible);
- };
-
- // Real-time breakpoint detection for demonstration
- const getCurrentBreakpointIndex = () => {
- if (typeof window === 'undefined') return 0;
- const width = window.innerWidth;
- if (width < 768) return 2; // Mobile
- if (width < 1024) return 1; // Tablet
- return 0; // Desktop
- };
-
- if (!isVisible) {
- return (
-
-
- Layout Hints
-
- );
- }
-
- const currentBreakpointData = breakpoints[currentBreakpoint];
- const IconComponent = currentBreakpointData.icon;
-
- return (
-
- {/* Header */}
-
-
-
-
Layout Hints
-
-
- ×
-
-
-
- {/* Breakpoint Visualization */}
-
-
- {currentBreakpointData.name}
- {currentBreakpointData.width}px
-
-
- {/* Animated viewport representation */}
-
- {/* Simulated VJ interface elements */}
-
- {/* Header with search controls */}
-
-
- {/* Main workspace panels */}
-
- {/* Search & Results panel */}
-
- {/* Video thumbnails simulation */}
-
-
-
- {currentBreakpoint !== 2 &&
}
-
-
-
- {/* Video Player (hidden on mobile) */}
- {currentBreakpoint !== 2 && (
-
- {/* Play button simulation */}
-
- {/* Timeline */}
-
-
- )}
-
- {/* Queue/Timeline panel (desktop only) */}
- {currentBreakpoint === 0 && (
-
- {/* Queue items simulation */}
-
-
- )}
-
-
- {/* Effects panel (bottom row - mobile gets stacked) */}
- {currentBreakpoint === 2 ? (
-
- ) : (
-
-
- {currentBreakpoint === 0 && (
-
- )}
-
- )}
-
-
-
-
- {currentBreakpointData.description}
-
-
-
- {/* Breakpoint indicators */}
-
- {breakpoints.map((bp, index) => {
- const BpIcon = bp.icon;
- return (
- {
- setCurrentBreakpoint(index);
- setIsAnimating(false);
- }}
- className={`p-2 rounded transition-colors ${
- index === currentBreakpoint
- ? 'bg-primary text-primary-foreground'
- : 'hover:bg-muted'
- }`}
- data-testid={`button-breakpoint-${bp.name.toLowerCase()}`}
- >
-
-
- );
- })}
-
-
- {/* Enhanced Controls */}
-
-
- {isAnimating ? : }
- {isAnimating ? 'Pause' : 'Animate'}
-
-
-
setShowRealTime(!showRealTime)}
- variant="outline"
- size="sm"
- title={showRealTime ? 'Hide real-time detection' : 'Show real-time detection'}
- data-testid="button-toggle-realtime"
- >
- {showRealTime ? : }
-
-
-
-
-
-
-
- {/* Additional info with enhanced status */}
-
-
-
- {isAnimating
- ? `Auto-cycling every 2.5s (${currentBreakpoint + 1}/${breakpoints.length})`
- : 'Manual control - Click icons or animate'
- }
-
- {isAnimating && (
-
- {breakpoints.map((_, index) => (
-
- ))}
-
- )}
-
-
- {/* Real-time indicator or Pro tip */}
- {showRealTime ? (
-
-
-
Current Screen:
-
-
- {typeof window !== 'undefined' ? window.innerWidth : 1024}px
-
-
- {(() => {
- const realBreakpoint = getCurrentBreakpointIndex();
- const RealIcon = breakpoints[realBreakpoint].icon;
- return (
- <>
-
-
- {breakpoints[realBreakpoint].name}
-
- >
- );
- })()}
-
-
-
-
- ) : (
-
- 💡 VJ interface automatically adapts to your screen size
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ResponsiveLayoutHintsSimple.tsx b/client/src/components/ResponsiveLayoutHintsSimple.tsx
deleted file mode 100644
index d44d675..0000000
--- a/client/src/components/ResponsiveLayoutHintsSimple.tsx
+++ /dev/null
@@ -1,260 +0,0 @@
-import { useState, useEffect } from 'react';
-import { useIsMobile } from '@/hooks/use-mobile';
-import { useStore } from '@/lib/store';
-import { useResponsiveLayout } from '@/hooks/use-responsive-layout';
-import { Smartphone, Tablet, Monitor, X, RotateCcw, Eye, Settings } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
-import { useToast } from '@/hooks/use-toast';
-
-export function ResponsiveLayoutHintsSimple() {
- const { brandSkin } = useStore();
- const { toast } = useToast();
- const isMobile = useIsMobile();
- const {
- currentLayout: actualLayout,
- setManualLayout,
- triggerLayoutAnimation,
- canResize,
- breakpoints
- } = useResponsiveLayout();
- const [currentLayout, setCurrentLayout] = useState<'mobile' | 'tablet' | 'desktop'>('desktop');
- const [showHints, setShowHints] = useState(false);
- const [isVisible, setIsVisible] = useState(false);
- const [previewMode, setPreviewMode] = useState(false);
-
- // Sync with actual layout changes
- useEffect(() => {
- if (actualLayout !== currentLayout) {
- setCurrentLayout(actualLayout);
- setIsVisible(true);
- // Auto-hide after 4 seconds
- setTimeout(() => setIsVisible(false), 4000);
- }
- }, [actualLayout, currentLayout]);
-
- // Manual layout switching functionality
- const switchToLayout = (layout: 'mobile' | 'tablet' | 'desktop') => {
- setManualLayout(layout);
- setCurrentLayout(layout);
- setPreviewMode(true);
-
- toast({
- title: `Switched to ${layout} layout`,
- description: `Interface adapted for ${layout} view`,
- duration: 2000,
- });
-
- // Auto-revert after 10 seconds
- setTimeout(() => {
- setPreviewMode(false);
- const actualWidth = window.innerWidth;
- const autoLayout = actualWidth < 768 ? 'mobile' : actualWidth < 1024 ? 'tablet' : 'desktop';
- setManualLayout(autoLayout);
- }, 10000);
- };
-
- // Reset to automatic detection
- const resetToAuto = () => {
- const width = window.innerWidth;
- const autoLayout = width < 768 ? 'mobile' : width < 1024 ? 'tablet' : 'desktop';
- setManualLayout(autoLayout);
- setCurrentLayout(autoLayout);
- setPreviewMode(false);
-
- toast({
- title: "Layout reset to automatic",
- description: "Responding to actual screen size",
- duration: 2000,
- });
- };
-
- const layoutConfig = {
- mobile: {
- icon: Smartphone,
- title: 'Mobile View',
- description: 'Single column, touch-optimized',
- features: ['Touch controls', 'Stacked panels', 'Full-width layout'],
- breakpoint: '< 768px'
- },
- tablet: {
- icon: Tablet,
- title: 'Tablet View',
- description: 'Adaptive dual-column layout',
- features: ['Side navigation', 'Responsive panels', 'Medium density'],
- breakpoint: '768px - 1024px'
- },
- desktop: {
- icon: Monitor,
- title: 'Desktop View',
- description: 'Full workspace with resizable panels',
- features: ['Drag to resize', 'Multi-column', 'High density'],
- breakpoint: '> 1024px'
- }
- };
-
- const currentConfig = layoutConfig[currentLayout];
- const IconComponent = currentConfig.icon;
-
- // Show button when not visible
- if (!showHints && !isVisible) {
- return (
- setShowHints(true)}
- className="fixed bottom-4 right-4 z-50 bg-white/90 hover:bg-white text-gray-800 border-2 shadow-lg backdrop-blur-sm flex items-center"
- data-testid="button-show-layout-hints"
- >
-
- Layout: {currentLayout}
-
- );
- }
-
- // Show layout info when visible or manually opened
- if (showHints || isVisible) {
- return (
-
- {/* Header */}
-
-
-
-
-
- {currentConfig.title}
-
- {previewMode && (
-
- Preview Mode
-
- )}
-
-
- {showHints && (
-
setShowHints(false)}
- className="text-gray-600 hover:text-gray-900 hover:bg-gray-100"
- data-testid="button-hide-layout-hints"
- >
-
-
- )}
-
-
- {/* Description */}
-
- {currentConfig.description}
-
-
- {/* Features */}
-
-
FEATURES:
-
- {currentConfig.features.map((feature, index) => (
-
- {feature}
-
- ))}
-
-
-
- {/* Interactive Layout Switcher */}
-
-
TRY OTHER LAYOUTS:
-
- {(['mobile', 'tablet', 'desktop'] as const).map((layout) => {
- const config = layoutConfig[layout];
- const Icon = config.icon;
- const isActive = currentLayout === layout;
-
- return (
- switchToLayout(layout)}
- className="flex flex-col items-center p-2 h-auto"
- disabled={isActive}
- data-testid={`button-switch-${layout}`}
- >
-
- {layout}
-
- );
- })}
-
-
-
- {/* Preview Mode Controls */}
- {previewMode && (
-
-
-
-
- Previewing {currentLayout} layout
-
-
-
- Reset
-
-
-
- Auto-resets in 10 seconds
-
-
- )}
-
- {/* Screen size info */}
-
-
- Screen: {window.innerWidth}px
- Breakpoint: {currentConfig.breakpoint}
-
-
-
- {/* Layout specific hints */}
- {currentLayout === 'desktop' && (
-
-
- Drag panel edges to resize workspace
-
- )}
-
- {currentLayout === 'tablet' && (
-
-
- Panels adapt to available space
-
- )}
-
- {currentLayout === 'mobile' && (
-
-
- Touch-optimized controls active
-
- )}
-
- {/* Auto-hide timer for automatic display */}
- {isVisible && !showHints && (
-
- Auto-hiding in a few seconds...
-
- )}
-
- );
- }
-
- return null;
-}
\ No newline at end of file
diff --git a/client/src/components/ResponsiveLayoutManager.tsx b/client/src/components/ResponsiveLayoutManager.tsx
deleted file mode 100644
index d031043..0000000
--- a/client/src/components/ResponsiveLayoutManager.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useEffect } from 'react';
-import { useResponsiveLayout, type LayoutMode } from '@/hooks/use-responsive-layout';
-import { useStore } from '@/lib/store';
-
-interface ResponsiveLayoutManagerProps {
- children: React.ReactNode;
-}
-
-export function ResponsiveLayoutManager({ children }: ResponsiveLayoutManagerProps) {
- const { currentLayout, panelConfig, isTransitioning } = useResponsiveLayout();
- const { setResizableMode } = useStore();
-
- // Automatically adjust resizable mode based on layout
- useEffect(() => {
- if (currentLayout === 'mobile') {
- setResizableMode(false);
- } else if (currentLayout === 'tablet') {
- setResizableMode(false);
- } else {
- // Desktop - enable full resizable mode
- setResizableMode(true);
- }
- }, [currentLayout, setResizableMode]);
-
- return (
-
- {children}
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ResultCard.tsx b/client/src/components/ResultCard.tsx
index 0cd3aa7..b0479d6 100644
--- a/client/src/components/ResultCard.tsx
+++ b/client/src/components/ResultCard.tsx
@@ -7,117 +7,81 @@ import { generateThumbnailUrl, preloadThumbnail } from '@/lib/archive-api';
import { useStore } from '@/lib/store';
import { ThumbnailSkeleton } from './SkeletonLoader';
import { ScaleTransition, FadeTransition } from './AnimatedTransitions';
-
interface ResultCardProps {
- video: VideoResult;
- onSelect: (video: VideoResult) => void;
+ video: VideoResult;
+ onSelect: (video: VideoResult) => void;
}
-
export function ResultCard({ video, onSelect }: ResultCardProps) {
- const { brandSkin } = useStore();
- const [thumbnailLoaded, setThumbnailLoaded] = useState(false);
- const [thumbnailError, setThumbnailError] = useState(false);
- const [thumbnailUrl, setThumbnailUrl] = useState(null);
- const [retryCount, setRetryCount] = useState(0);
- const [isDragging, setIsDragging] = useState(false);
- const [showHoverInfo, setShowHoverInfo] = useState(false);
- const maxRetries = 3;
-
-
- const handleDragStart = (e: React.DragEvent) => {
- setIsDragging(true);
- e.dataTransfer.setData('application/json', JSON.stringify({
- type: 'video',
- video: video,
- sourceType: 'search'
- }));
- e.dataTransfer.effectAllowed = 'copy';
- };
-
- const handleDragEnd = () => {
- setIsDragging(false);
- };
-
- // Preload thumbnail with retry logic
- const loadThumbnail = useCallback(async () => {
- if (retryCount >= maxRetries) return;
-
- try {
- setThumbnailError(false);
- const url = await preloadThumbnail(video.identifier);
- setThumbnailUrl(url);
- setThumbnailLoaded(true);
- } catch (error) {
- console.warn(`Thumbnail load failed for ${video.identifier}:`, error);
- setThumbnailError(true);
-
- // Retry with exponential backoff
- if (retryCount < maxRetries) {
- setTimeout(() => {
- setRetryCount(prev => prev + 1);
- }, Math.pow(2, retryCount) * 1000);
- }
- }
- }, [video.identifier, retryCount, maxRetries]);
-
- useEffect(() => {
- loadThumbnail();
- }, [loadThumbnail]);
-
- // Fallback URL generation
- const fallbackUrl = generateThumbnailUrl(video.identifier);
-
- return (
- onSelect(video)}
- onMouseEnter={() => setShowHoverInfo(true)}
- onMouseLeave={() => setShowHoverInfo(false)}
- draggable
- onDragStart={handleDragStart}
- onDragEnd={handleDragEnd}
- data-testid={`card-video-${video.identifier}`}
- >
+ const { brandSkin } = useStore();
+ const [thumbnailLoaded, setThumbnailLoaded] = useState(false);
+ const [thumbnailError, setThumbnailError] = useState(false);
+ const [thumbnailUrl, setThumbnailUrl] = useState
(null);
+ const [retryCount, setRetryCount] = useState(0);
+ const [isDragging, setIsDragging] = useState(false);
+ const [showHoverInfo, setShowHoverInfo] = useState(false);
+ const maxRetries = 3;
+ const handleDragStart = (e: React.DragEvent) => {
+ setIsDragging(true);
+ e.dataTransfer.setData('application/json', JSON.stringify({
+ type: 'video',
+ video: video,
+ sourceType: 'search'
+ }));
+ e.dataTransfer.effectAllowed = 'copy';
+ };
+ const handleDragEnd = () => {
+ setIsDragging(false);
+ };
+ // Preload thumbnail with retry logic
+ const loadThumbnail = useCallback(async () => {
+ if (retryCount >= maxRetries)
+ return;
+ try {
+ setThumbnailError(false);
+ const url = await preloadThumbnail(video.identifier);
+ setThumbnailUrl(url);
+ setThumbnailLoaded(true);
+ }
+ catch (error) {
+ console.warn(`Thumbnail load failed for ${video.identifier}:`, error);
+ setThumbnailError(true);
+ // Retry with exponential backoff
+ if (retryCount < maxRetries) {
+ setTimeout(() => {
+ setRetryCount(prev => prev + 1);
+ }, Math.pow(2, retryCount) * 1000);
+ }
+ }
+ }, [video.identifier, retryCount, maxRetries]);
+ useEffect(() => {
+ loadThumbnail();
+ }, [loadThumbnail]);
+ // Fallback URL generation
+ const fallbackUrl = generateThumbnailUrl(video.identifier);
+ return ( onSelect(video)} onMouseEnter={() => setShowHoverInfo(true)} onMouseLeave={() => setShowHoverInfo(false)} draggable onDragStart={handleDragStart} onDragEnd={handleDragEnd} data-testid={`card-video-${video.identifier}`}>
{/* Hover info overlay */}
- {showHoverInfo && (
-
+ {showHoverInfo && (
Drag to Queue
-
+
{video.duration || 'Unknown duration'}
-
- )}
+ )}
- {!thumbnailLoaded && !thumbnailError ? (
-
- ) : (
-
setThumbnailLoaded(true)}
- onError={(e) => {
- if (retryCount < maxRetries) {
- setRetryCount(prev => prev + 1);
- } else {
- (e.target as HTMLImageElement).src = `https://via.placeholder.com/400x225/e5e7eb/6b7280?text=${encodeURIComponent(video.title)}`;
- }
- }}
- />
- )}
+ {!thumbnailLoaded && !thumbnailError ? (
) : (
setThumbnailLoaded(true)} onError={(e) => {
+ if (retryCount < maxRetries) {
+ setRetryCount(prev => prev + 1);
+ }
+ else {
+ (e.target as HTMLImageElement).src = `https://via.placeholder.com/400x225/e5e7eb/6b7280?text=${encodeURIComponent(video.title)}`;
+ }
+ }}/>)}
-
+
@@ -125,7 +89,7 @@ export function ResultCard({ video, onSelect }: ResultCardProps) {
@@ -141,6 +105,5 @@ export function ResultCard({ video, onSelect }: ResultCardProps) {
-
- );
+ );
}
diff --git a/client/src/components/SavedSearches.tsx b/client/src/components/SavedSearches.tsx
index e336f73..bc4b050 100644
--- a/client/src/components/SavedSearches.tsx
+++ b/client/src/components/SavedSearches.tsx
@@ -7,124 +7,92 @@ import { Badge } from '@/components/ui/badge';
import { Bookmark, BookmarkPlus, Trash2, Search } from 'lucide-react';
import { useStore } from '@/lib/store';
import { type SearchState } from '@/lib/types';
-
interface SavedSearch {
- id: string;
- name: string;
- searchState: SearchState;
- createdAt: number;
- usageCount: number;
+ id: string;
+ name: string;
+ searchState: SearchState;
+ createdAt: number;
+ usageCount: number;
}
-
const STORAGE_KEY = 'staticBuffet_savedSearches';
-
export function SavedSearches() {
- const [savedSearches, setSavedSearches] = useState([]);
- const [isOpen, setIsOpen] = useState(false);
- const [isCreating, setIsCreating] = useState(false);
- const [newSearchName, setNewSearchName] = useState('');
- const { searchState, setSearchState, brandSkin } = useStore();
-
- // Load saved searches from localStorage
- useEffect(() => {
- try {
- const saved = localStorage.getItem(STORAGE_KEY);
- if (saved) {
- setSavedSearches(JSON.parse(saved));
- }
- } catch (error) {
- console.warn('Failed to load saved searches:', error);
- }
- }, []);
-
- // Save to localStorage whenever savedSearches changes
- useEffect(() => {
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(savedSearches));
- } catch (error) {
- console.warn('Failed to save searches:', error);
- }
- }, [savedSearches]);
-
- const saveCurrentSearch = () => {
- if (!newSearchName.trim() || !searchState.query) return;
-
- const newSearch: SavedSearch = {
- id: Date.now().toString(),
- name: newSearchName.trim(),
- searchState: { ...searchState },
- createdAt: Date.now(),
- usageCount: 0,
- };
-
- setSavedSearches(prev => [newSearch, ...prev].slice(0, 20)); // Keep only 20 most recent
- setNewSearchName('');
- setIsCreating(false);
- };
-
- const loadSearch = (search: SavedSearch) => {
- // Update usage count
- setSavedSearches(prev =>
- prev.map(s =>
- s.id === search.id ? { ...s, usageCount: s.usageCount + 1 } : s
- )
- );
-
- // Apply the search
- setSearchState(search.searchState);
- setIsOpen(false);
- };
-
- const deleteSearch = (id: string) => {
- setSavedSearches(prev => prev.filter(s => s.id !== id));
- };
-
- const formatSearchDescription = (searchState: SearchState) => {
- const parts = [];
- if (searchState.query) parts.push(`"${searchState.query}"`);
- if (searchState.license) parts.push(`License: ${searchState.license}`);
- if (searchState.duration) parts.push(`Duration: ${searchState.duration}`);
- if (searchState.yearFrom || searchState.yearTo) {
- parts.push(`Years: ${searchState.yearFrom || '?'}-${searchState.yearTo || '?'}`);
- }
- return parts.join(' • ');
- };
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'waffle':
- return {
- button: 'bg-yellow-100/50 text-amber-800 hover:bg-yellow-200/50 border-yellow-400/30',
- dialog: 'bg-yellow-50/95 border-yellow-400/50',
- badge: 'bg-yellow-200/50 text-amber-900'
- };
- case 'ebn':
- return {
- button: 'bg-lime-900/30 text-lime-400 hover:bg-lime-900/50 border-lime-500/30',
- dialog: 'bg-gray-900/95 border-lime-500/50',
- badge: 'bg-lime-900/50 text-lime-300'
+ const [savedSearches, setSavedSearches] = useState([]);
+ const [isOpen, setIsOpen] = useState(false);
+ const [isCreating, setIsCreating] = useState(false);
+ const [newSearchName, setNewSearchName] = useState('');
+ const { searchState, setSearchState, brandSkin } = useStore();
+ // Load saved searches from localStorage
+ useEffect(() => {
+ try {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ if (saved) {
+ setSavedSearches(JSON.parse(saved));
+ }
+ }
+ catch (error) {
+ console.warn('Failed to load saved searches:', error);
+ }
+ }, []);
+ // Save to localStorage whenever savedSearches changes
+ useEffect(() => {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(savedSearches));
+ }
+ catch (error) {
+ console.warn('Failed to save searches:', error);
+ }
+ }, [savedSearches]);
+ const saveCurrentSearch = () => {
+ if (!newSearchName.trim() || !searchState.query)
+ return;
+ const newSearch: SavedSearch = {
+ id: Date.now().toString(),
+ name: newSearchName.trim(),
+ searchState: { ...searchState },
+ createdAt: Date.now(),
+ usageCount: 0,
};
- default:
- return {
- button: 'bg-gray-100 text-gray-700 hover:bg-gray-200 border-gray-300',
- dialog: 'bg-white dark:bg-gray-900',
- badge: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'
- };
- }
- };
-
- const theme = getThemeClasses();
-
- return (
-
+ setSavedSearches(prev => [newSearch, ...prev].slice(0, 20)); // Keep only 20 most recent
+ setNewSearchName('');
+ setIsCreating(false);
+ };
+ const loadSearch = (search: SavedSearch) => {
+ // Update usage count
+ setSavedSearches(prev => prev.map(s => s.id === search.id ? { ...s, usageCount: s.usageCount + 1 } : s));
+ // Apply the search
+ setSearchState(search.searchState);
+ setIsOpen(false);
+ };
+ const deleteSearch = (id: string) => {
+ setSavedSearches(prev => prev.filter(s => s.id !== id));
+ };
+ const formatSearchDescription = (searchState: SearchState) => {
+ const parts = [];
+ if (searchState.query)
+ parts.push(`"${searchState.query}"`);
+ if (searchState.license)
+ parts.push(`License: ${searchState.license}`);
+ if (searchState.duration)
+ parts.push(`Duration: ${searchState.duration}`);
+ if (searchState.yearFrom || searchState.yearTo) {
+ parts.push(`Years: ${searchState.yearFrom || '?'}-${searchState.yearTo || '?'}`);
+ }
+ return parts.join(' • ');
+ };
+ const getThemeClasses = () => {
+ {
+ return {
+ button: 'bg-lime-900/30 text-lime-400 hover:bg-lime-900/50 border-lime-500/30',
+ dialog: 'bg-gray-900/95 border-lime-500/50',
+ badge: 'bg-lime-900/50 text-lime-300'
+ };
+ }
+ };
+ const theme = getThemeClasses();
+ return (
-
-
+
+
@@ -135,19 +103,11 @@ export function SavedSearches() {
{/* Save current search */}
- {searchState.query && (
-
- {isCreating ? (
-
+ {searchState.query && (
+ {isCreating ? (
Save current search:
-
setNewSearchName(e.target.value)}
- placeholder="e.g., PSA <60s, Newsreel 40s-70s"
- onKeyPress={(e) => e.key === 'Enter' && saveCurrentSearch()}
- />
+
setNewSearchName(e.target.value)} placeholder="e.g., PSA <60s, Newsreel 40s-70s" onKeyPress={(e) => e.key === 'Enter' && saveCurrentSearch()}/>
Save
@@ -158,9 +118,7 @@ export function SavedSearches() {
Current: {formatSearchDescription(searchState)}
-
- ) : (
-
+
) : (
Save current search
@@ -168,36 +126,25 @@ export function SavedSearches() {
setIsCreating(true)} size="sm">
-
+
Save
-
- )}
-
- )}
+
)}
+
)}
{/* Saved searches list */}
- {savedSearches.length === 0 ? (
-
-
+ {savedSearches.length === 0 ? (
+
No saved searches yet
Save your favorite search combinations for quick access
-
- ) : (
- savedSearches.map((search) => (
-
+
) : (savedSearches.map((search) => (
{search.name}
- {search.usageCount > 0 && (
-
+ {search.usageCount > 0 && (
{search.usageCount}x
-
- )}
+ )}
{formatSearchDescription(search.searchState)}
@@ -207,29 +154,17 @@ export function SavedSearches() {
- loadSearch(search)}
- className="bg-blue-600 hover:bg-blue-700 text-white"
- >
-
+ loadSearch(search)} className="bg-blue-600 hover:bg-blue-700 text-white">
+
Load
- deleteSearch(search.id)}
- className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20"
- >
-
+ deleteSearch(search.id)} className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20">
+
-
- ))
- )}
+
)))}
-
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/SkeletonLoader.tsx b/client/src/components/SkeletonLoader.tsx
index 4ef0691..a85d6ed 100644
--- a/client/src/components/SkeletonLoader.tsx
+++ b/client/src/components/SkeletonLoader.tsx
@@ -1,160 +1,113 @@
import { useStore } from '@/lib/store';
-
interface SkeletonLoaderProps {
- variant?: 'card' | 'thumbnail' | 'text' | 'video';
- className?: string;
- count?: number;
+ variant?: 'card' | 'thumbnail' | 'text' | 'video';
+ className?: string;
+ count?: number;
}
-
export function SkeletonLoader({ variant = 'card', className = '', count = 1 }: SkeletonLoaderProps) {
- const { brandSkin } = useStore();
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return 'bg-blue-200/20 animate-pulse';
- case 'waffle':
- return 'bg-yellow-200/30 animate-pulse';
- case 'ebn':
- return 'bg-lime-200/20 animate-pulse';
- case 'ozzy':
- return 'bg-red-200/20 animate-pulse';
- case 'hogan':
- return 'bg-yellow-200/20 animate-pulse';
- case 'dx':
- return 'bg-pink-200/20 animate-pulse';
- case 'maxheadroom':
- return 'bg-green-200/20 animate-pulse';
- case 'mario':
- return 'bg-yellow-200/20 animate-pulse';
- case 'dakota':
- return 'bg-gray-200/20 animate-pulse';
- case 'blondie':
- return 'bg-amber-200/20 animate-pulse';
- default:
- return 'bg-gray-200/30 animate-pulse';
- }
- };
-
- const themeClass = getThemeClasses();
-
- const renderSkeleton = () => {
- switch (variant) {
- case 'card':
- return (
-
+ const { brandSkin } = useStore();
+ const getThemeClasses = () => {
+ {
+ return 'bg-lime-200/20 animate-pulse';
+ }
+ };
+ const themeClass = getThemeClasses();
+ const renderSkeleton = () => {
+ switch (variant) {
+ case 'card':
+ return (
{/* Thumbnail skeleton */}
-
+
{/* Content skeleton */}
{/* Title lines */}
-
-
+
+
{/* Metadata line */}
-
- );
-
- case 'thumbnail':
- return (
-
- );
-
- case 'text':
- return (
-
- );
-
- case 'video':
- return (
-
+
);
+ case 'thumbnail':
+ return (
);
+ case 'text':
+ return (
);
+ case 'video':
+ return (
- );
-
- default:
- return
;
+
);
+ default:
+ return
;
+ }
+ };
+ if (count === 1) {
+ return renderSkeleton();
}
- };
-
- if (count === 1) {
- return renderSkeleton();
- }
-
- return (
- <>
- {Array.from({ length: count }, (_, index) => (
-
+ return (<>
+ {Array.from({ length: count }, (_, index) => (
{renderSkeleton()}
-
- ))}
- >
- );
+
))}
+ >);
}
-
// Specific skeleton components for common use cases
-export function VideoCardSkeleton({ className = '' }: { className?: string }) {
- return
;
+export function VideoCardSkeleton({ className = '' }: {
+ className?: string;
+}) {
+ return
;
}
-
-export function ThumbnailSkeleton({ className = '' }: { className?: string }) {
- return ;
+export function ThumbnailSkeleton({ className = '' }: {
+ className?: string;
+}) {
+ return ;
}
-
-export function TextSkeleton({ className = '' }: { className?: string }) {
- return ;
+export function TextSkeleton({ className = '' }: {
+ className?: string;
+}) {
+ return ;
}
-
-export function VideoPlayerSkeleton({ className = '' }: { className?: string }) {
- return ;
+export function VideoPlayerSkeleton({ className = '' }: {
+ className?: string;
+}) {
+ return ;
}
-
// Grid skeleton for search results
-export function SearchResultsSkeleton({ count = 9 }: { count?: number }) {
- return (
-
-
-
- );
+export function SearchResultsSkeleton({ count = 9 }: {
+ count?: number;
+}) {
+ return (
+
+
);
}
-
// Queue item skeleton
-export function QueueItemSkeleton({ className = '' }: { className?: string }) {
- const { brandSkin } = useStore();
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'waffle':
- return 'bg-yellow-200/30';
- default:
- return 'bg-gray-200/30';
- }
- };
-
- return (
-
+export function QueueItemSkeleton({ className = '' }: {
+ className?: string;
+}) {
+ const { brandSkin } = useStore();
+ const getThemeClasses = () => {
+ {
+ return 'bg-gray-200/30';
+ }
+ };
+ return (
{/* Thumbnail */}
-
+
{/* Content */}
-
-
+
+
{/* Controls */}
-
- );
-}
\ No newline at end of file
+
);
+}
diff --git a/client/src/components/SmartQueryChips.tsx b/client/src/components/SmartQueryChips.tsx
index 20cc1a3..fb94327 100644
--- a/client/src/components/SmartQueryChips.tsx
+++ b/client/src/components/SmartQueryChips.tsx
@@ -2,150 +2,75 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useStore } from '@/lib/store';
import { type SmartQuery } from '@/lib/types';
-
const SMART_QUERIES: SmartQuery[] = [
- {
- id: 'psa',
- label: 'PSA',
- query: 'public service announcement safety',
- description: 'Public service announcements and safety films'
- },
- {
- id: 'training',
- label: 'Training',
- query: 'training educational instructional',
- description: 'Educational and training videos'
- },
- {
- id: 'infomercial',
- label: 'Infomercial',
- query: 'infomercial commercial advertising',
- description: 'Vintage commercials and infomercials'
- },
- {
- id: 'newsreel',
- label: 'Newsreel',
- query: 'newsreel news bulletin',
- description: 'Historical newsreels and bulletins'
- },
- {
- id: 'cartoon',
- label: 'Cartoon',
- query: 'cartoon animation animated',
- description: 'Vintage cartoons and animations'
- },
- {
- id: 'safety',
- label: 'Safety',
- query: 'safety industrial workplace',
- description: 'Workplace and industrial safety films'
- },
- {
- id: 'cspan',
- label: 'C-SPAN',
- query: 'c-span government political',
- description: 'Government proceedings and political content'
- }
+ {
+ id: 'psa',
+ label: 'PSA',
+ query: 'public service announcement safety',
+ description: 'Public service announcements and safety films'
+ },
+ {
+ id: 'training',
+ label: 'Training',
+ query: 'training educational instructional',
+ description: 'Educational and training videos'
+ },
+ {
+ id: 'infomercial',
+ label: 'Infomercial',
+ query: 'infomercial commercial advertising',
+ description: 'Vintage commercials and infomercials'
+ },
+ {
+ id: 'newsreel',
+ label: 'Newsreel',
+ query: 'newsreel news bulletin',
+ description: 'Historical newsreels and bulletins'
+ },
+ {
+ id: 'cartoon',
+ label: 'Cartoon',
+ query: 'cartoon animation animated',
+ description: 'Vintage cartoons and animations'
+ },
+ {
+ id: 'safety',
+ label: 'Safety',
+ query: 'safety industrial workplace',
+ description: 'Workplace and industrial safety films'
+ },
+ {
+ id: 'cspan',
+ label: 'C-SPAN',
+ query: 'c-span government political',
+ description: 'Government proceedings and political content'
+ }
];
-
interface SmartQueryChipsProps {
- onQuerySelect: (query: string) => void;
+ onQuerySelect: (query: string) => void;
}
-
export function SmartQueryChips({ onQuerySelect }: SmartQueryChipsProps) {
- const { brandSkin, searchState } = useStore();
-
- const getThemeClasses = () => {
- switch (brandSkin) {
- case 'testcard':
- return {
- chip: 'bg-blue-400/10 text-blue-400 border-blue-400/30 hover:bg-blue-400/20',
- active: 'bg-blue-400/30 text-blue-100'
- };
- case 'waffle':
- return {
- chip: 'bg-yellow-100/50 text-amber-800 border-yellow-400/30 hover:bg-yellow-100/70',
- active: 'bg-yellow-400/50 text-amber-900'
- };
- case 'ebn':
- return {
- chip: 'bg-lime-900/50 text-lime-400 border-lime-500/30 hover:bg-lime-900/70',
- active: 'bg-lime-500/30 text-lime-100'
- };
- case 'ozzy':
- return {
- chip: 'bg-red-900/30 text-red-300 border-red-500/30 hover:bg-red-900/50',
- active: 'bg-red-500/30 text-red-100'
- };
- case 'hogan':
- return {
- chip: 'bg-yellow-900/50 text-yellow-300 border-yellow-400/30 hover:bg-yellow-900/70',
- active: 'bg-yellow-400/30 text-yellow-100'
- };
- case 'dx':
- return {
- chip: 'bg-pink-900/50 text-pink-300 border-pink-500/30 hover:bg-pink-900/70',
- active: 'bg-pink-500/30 text-pink-100'
- };
- case 'maxheadroom':
- return {
- chip: 'bg-green-900/50 text-green-300 border-green-500/30 hover:bg-green-900/70',
- active: 'bg-green-500/30 text-green-100'
- };
- case 'mario':
- return {
- chip: 'bg-red-900/50 text-yellow-300 border-yellow-400/30 hover:bg-red-900/70',
- active: 'bg-yellow-400/30 text-red-900'
- };
- case 'dakota':
- return {
- chip: 'bg-gray-800/50 text-gray-300 border-gray-400/30 hover:bg-gray-800/70',
- active: 'bg-gray-400/30 text-gray-100'
- };
- case 'blondie':
- return {
- chip: 'bg-amber-900/50 text-amber-300 border-amber-400/30 hover:bg-amber-900/70',
- active: 'bg-amber-400/30 text-amber-100'
- };
- default:
- return {
- chip: 'bg-blue-400/10 text-blue-400 border-blue-400/30 hover:bg-blue-400/20',
- active: 'bg-blue-400/30 text-blue-100'
- };
- }
- };
-
- const theme = getThemeClasses();
-
- const handleChipClick = (smartQuery: SmartQuery) => {
- onQuerySelect(smartQuery.query);
- };
-
- return (
-
+ const { brandSkin, searchState } = useStore();
+ const getThemeClasses = () => {
+ {
+ return {
+ chip: 'bg-lime-900/50 text-lime-400 border-lime-500/30 hover:bg-lime-900/70',
+ active: 'bg-lime-500/30 text-lime-100'
+ };
+ }
+ };
+ const theme = getThemeClasses();
+ const handleChipClick = (smartQuery: SmartQuery) => {
+ onQuerySelect(smartQuery.query);
+ };
+ return (
Quick Search:
{SMART_QUERIES.map((smartQuery) => {
- const isActive = searchState.query.includes(smartQuery.query) ||
- smartQuery.query.split(' ').some(term =>
- searchState.query.toLowerCase().includes(term.toLowerCase())
- );
-
- return (
-
handleChipClick(smartQuery)}
- className={`px-1.5 py-1 sm:px-2 sm:py-1 text-xs rounded-full border transition-colors ${
- isActive ? theme.active : theme.chip
- }`}
- title={smartQuery.description}
- data-testid={`chip-${smartQuery.id}`}
- >
+ const isActive = searchState.query.includes(smartQuery.query) ||
+ smartQuery.query.split(' ').some(term => searchState.query.toLowerCase().includes(term.toLowerCase()));
+ return ( handleChipClick(smartQuery)} className={`px-1.5 py-1 sm:px-2 sm:py-1 text-xs rounded-full border transition-colors ${isActive ? theme.active : theme.chip}`} title={smartQuery.description} data-testid={`chip-${smartQuery.id}`}>
{smartQuery.label}
-
- );
- })}
-
- );
-}
\ No newline at end of file
+ );
+ })}
+
);
+}
diff --git a/client/src/components/SourceToggles.tsx b/client/src/components/SourceToggles.tsx
index d4415e9..aa953b2 100644
--- a/client/src/components/SourceToggles.tsx
+++ b/client/src/components/SourceToggles.tsx
@@ -5,176 +5,115 @@ import { Archive, Rocket, Building, Camera, Sparkles, Film, Tv } from 'lucide-re
import { useStore } from '@/lib/store';
import { getThemeClasses } from '@/lib/theme-utils';
import { type SearchSource } from '@/lib/types';
-
const DEFAULT_SOURCES: SearchSource[] = [
- {
- id: 'prelinger',
- name: 'Prelinger Archives',
- collection: 'prelinger',
- enabled: true,
- description: 'Educational films, advertising, industrial videos'
- },
- {
- id: 'fedflix',
- name: 'FedFlix',
- collection: 'fedflix',
- enabled: true,
- description: 'US Government training films and documentaries'
- },
- {
- id: 'nasa',
- name: 'NASA',
- collection: 'nasa',
- enabled: false,
- description: 'Space missions, launches, educational content'
- },
- {
- id: 'loc',
- name: 'Library of Congress',
- collection: 'library_of_congress',
- enabled: false,
- description: 'Historical footage, newsreels, cultural content'
- },
- {
- id: 'wikimedia',
- name: 'Wikimedia Video',
- collection: 'wikimedia',
- enabled: false,
- description: 'Open educational and documentary videos'
- }
+ {
+ id: 'prelinger',
+ name: 'Prelinger Archives',
+ collection: 'prelinger',
+ enabled: true,
+ description: 'Educational films, advertising, industrial videos'
+ },
+ {
+ id: 'fedflix',
+ name: 'FedFlix',
+ collection: 'fedflix',
+ enabled: true,
+ description: 'US Government training films and documentaries'
+ },
+ {
+ id: 'nasa',
+ name: 'NASA',
+ collection: 'nasa',
+ enabled: false,
+ description: 'Space missions, launches, educational content'
+ },
+ {
+ id: 'loc',
+ name: 'Library of Congress',
+ collection: 'library_of_congress',
+ enabled: false,
+ description: 'Historical footage, newsreels, cultural content'
+ },
+ {
+ id: 'wikimedia',
+ name: 'Wikimedia Video',
+ collection: 'wikimedia',
+ enabled: false,
+ description: 'Open educational and documentary videos'
+ }
];
-
interface SourceTogglesProps {
- onSourcesChange: () => void;
+ onSourcesChange: () => void;
}
-
export function SourceToggles({ onSourcesChange }: SourceTogglesProps) {
- const { searchState, setSearchState, brandSkin } = useStore();
- const [sources, setSources] = useState(() => {
- // Initialize sources with state from store or defaults
- const currentSources = searchState.sources || ['prelinger', 'fedflix'];
- return DEFAULT_SOURCES.map(source => ({
- ...source,
- enabled: currentSources.includes(source.id)
- }));
- });
-
- const theme = getThemeClasses(brandSkin);
-
- const getSourceIcon = (sourceId: string) => {
- switch (sourceId) {
- case 'nasa':
- return ;
- case 'loc':
- return ;
- case 'wikimedia':
- return ;
- case 'prelinger':
- return ;
- case 'fedflix':
- return ;
- default:
- return ;
- }
- };
-
- const handleSourceToggle = (sourceId: string, checked: boolean) => {
- const updatedSources = sources.map(source =>
- source.id === sourceId ? { ...source, enabled: checked } : source
- );
- setSources(updatedSources);
-
- const enabledSources = updatedSources
- .filter(source => source.enabled)
- .map(source => source.id);
-
- setSearchState({ sources: enabledSources, page: 1 });
- onSourcesChange();
- };
-
- const enabledCount = sources.filter(s => s.enabled).length;
-
- return (
-
+ const { searchState, setSearchState, brandSkin } = useStore();
+ const [sources, setSources] = useState
(() => {
+ // Initialize sources with state from store or defaults
+ const currentSources = searchState.sources || ['prelinger', 'fedflix'];
+ return DEFAULT_SOURCES.map(source => ({
+ ...source,
+ enabled: currentSources.includes(source.id)
+ }));
+ });
+ const theme = getThemeClasses(brandSkin);
+ const getSourceIcon = (sourceId: string) => {
+ switch (sourceId) {
+ case 'nasa':
+ return ;
+ case 'loc':
+ return ;
+ case 'wikimedia':
+ return ;
+ case 'prelinger':
+ return ;
+ case 'fedflix':
+ return ;
+ default:
+ return ;
+ }
+ };
+ const handleSourceToggle = (sourceId: string, checked: boolean) => {
+ const updatedSources = sources.map(source => source.id === sourceId ? { ...source, enabled: checked } : source);
+ setSources(updatedSources);
+ const enabledSources = updatedSources
+ .filter(source => source.enabled)
+ .map(source => source.id);
+ setSearchState({ sources: enabledSources, page: 1 });
+ onSourcesChange();
+ };
+ const enabledCount = sources.filter(s => s.enabled).length;
+ return (
{/* Sparkle icon for fun */}
-
+
{/* Source checkboxes */}
- {sources.map((source) => (
-
- handleSourceToggle(source.id, !!checked)}
- className={`h-3 w-3 border-2 ${
- source.enabled
- ? brandSkin === 'mario' ? 'border-yellow-400 bg-red-500/50' :
- brandSkin === 'hogan' ? 'border-yellow-400 bg-red-600/50' :
- brandSkin === 'dx' ? 'border-green-500 bg-green-600/50' :
- brandSkin === 'maxheadroom' ? 'border-cyan-400 bg-pink-500/50' :
+ {sources.map((source) => (
+ handleSourceToggle(source.id, !!checked)} className={`h-3 w-3 border-2 ${source.enabled
+ ?
'border-white/60 bg-white/20'
- : 'border-white/30 bg-transparent'
- }`}
- data-testid={`checkbox-source-${source.id}`}
- />
-
+ : 'border-white/30 bg-transparent'}`} data-testid={`checkbox-source-${source.id}`}/>
+
{source.id === 'prelinger' ? 'PRE' :
- source.id === 'fedflix' ? 'FED' :
- source.id === 'nasa' ? 'NASA' :
- source.id === 'loc' ? 'LOC' :
- source.id === 'wikimedia' ? 'WIKI' :
- source.name.slice(0, 3).toUpperCase()}
+ source.id === 'fedflix' ? 'FED' :
+ source.id === 'nasa' ? 'NASA' :
+ source.id === 'loc' ? 'LOC' :
+ source.id === 'wikimedia' ? 'WIKI' :
+ source.name.slice(0, 3).toUpperCase()}
{getSourceIcon(source.id)}
-
- ))}
+ ))}
{/* Count badge */}
-
- );
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/StatusBar.tsx b/client/src/components/StatusBar.tsx
index 1f007ff..fb54674 100644
--- a/client/src/components/StatusBar.tsx
+++ b/client/src/components/StatusBar.tsx
@@ -2,103 +2,65 @@ import { useStore } from '@/lib/store';
import { Activity, HardDrive, Film, Settings, Tv, Layers, Monitor, Volume2 } from 'lucide-react';
import { getThemeClasses } from '@/lib/theme-utils';
import { useState, useEffect } from 'react';
-
export function StatusBar() {
- const {
- brandSkin,
- queueItems,
- searchResults,
- videoEffects,
- isPlaying,
- floatingPanelStates
- } = useStore();
-
- const [fps, setFps] = useState(30);
- const [resolution, setResolution] = useState('1080p');
- const [audioLevel, setAudioLevel] = useState(0.6);
-
- // Simulate live data updates
- useEffect(() => {
- const interval = setInterval(() => {
- // Simulate FPS fluctuation (25-60 fps)
- setFps(Math.floor(25 + Math.random() * 35));
-
- // Simulate audio levels (VU meter bounce)
- setAudioLevel(0.2 + Math.random() * 0.6);
- }, 100);
-
- return () => clearInterval(interval);
- }, []);
-
- const themeClasses = getThemeClasses(brandSkin);
-
- // Count visible panels
- const visiblePanels = Object.values(floatingPanelStates).filter(panel => panel.visible).length;
-
- // Get current FX status
- const fxActive = videoEffects.glitchIntensity > 0 ||
- videoEffects.chromaticAberration > 0 ||
- videoEffects.blur > 0 ||
- videoEffects.sepia > 0;
-
- return (
-
+ const { brandSkin, queueItems, searchResults, videoEffects, isPlaying } = useStore();
+ const [fps, setFps] = useState(30);
+ const [resolution, setResolution] = useState('1080p');
+ const [audioLevel, setAudioLevel] = useState(0.6);
+ // Simulate live data updates
+ useEffect(() => {
+ const interval = setInterval(() => {
+ // Simulate FPS fluctuation (25-60 fps)
+ setFps(Math.floor(25 + Math.random() * 35));
+ // Simulate audio levels (VU meter bounce)
+ setAudioLevel(0.2 + Math.random() * 0.6);
+ }, 100);
+ return () => clearInterval(interval);
+ }, []);
+ const themeClasses = getThemeClasses(brandSkin);
+ // Get current FX status
+ const fxActive = videoEffects.glitchIntensity > 0 ||
+ videoEffects.chromaticAberration > 0 ||
+ videoEffects.blur > 0 ||
+ videoEffects.sepia > 0;
+ return (
{/* Left Section - Content Stats */}
-
+
{searchResults.length} results
-
+
{queueItems.length} queued
- {fxActive && (
-
-
+ {fxActive && (
+
FX ON
-
- )}
+
)}
- {isPlaying && (
-
-
+ {isPlaying && (
- )}
+
)}
{/* Center Section - Theme Quotes */}
-
+
- {brandSkin === 'testcard' ? 'PLEASE STAND BY' :
- brandSkin === 'waffle' ? 'Scattered, Smothered and Covered' :
- brandSkin === 'ebn' ? 'Commercial Entertainment Product' :
- brandSkin === 'ozzy' ? "You can't kill Rock 'n' Roll [RIP Ozzy]" :
- brandSkin === 'hogan' ? 'Eat Your Vitamins | nWo 4 Life [RIP Hulkster]' :
- brandSkin === 'dx' ? 'Are You Ready?' :
- brandSkin === 'maxheadroom' ? 'T-t-t-tune into Network 23!' :
- brandSkin === 'mario' ? 'Slide Down My Flagpole!' :
- brandSkin === 'dakota' ? 'Nice Pickup! (96 Dodge Dakota)' :
- brandSkin === 'blondie' ? 'Suck it on the Side!' :
- brandSkin.toUpperCase()}
+ {'Commercial Entertainment Product'}
-
+
{/* Right Section - Workspace Info */}
-
- {visiblePanels} panels
+
-
- );
-}
\ No newline at end of file
+
);
+}
diff --git a/client/src/components/StreamlinedWelcome.tsx b/client/src/components/StreamlinedWelcome.tsx
deleted file mode 100644
index 60bc594..0000000
--- a/client/src/components/StreamlinedWelcome.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
-import { Search, Play, List, Zap, Tv } from 'lucide-react';
-import { useStore } from '@/lib/store';
-import { getThemeClasses } from '@/lib/theme-utils';
-
-export function StreamlinedWelcome() {
- const [isOpen, setIsOpen] = useState(false);
- const { brandSkin } = useStore();
- const themeClasses = getThemeClasses(brandSkin);
-
- // Public method to show welcome (can be called from other components)
- const showWelcome = () => setIsOpen(true);
-
- // Attach function to window for external access
- useEffect(() => {
- (window as any).showStaticBuffetWelcome = showWelcome;
- return () => {
- delete (window as any).showStaticBuffetWelcome;
- };
- }, []);
-
- // Auto-start welcome disabled - now only accessible through menu
- // useEffect(() => {
- // const hasSeenWelcome = localStorage.getItem('staticBuffetWelcomeSeen');
- // if (!hasSeenWelcome) {
- // setIsOpen(true);
- // }
- // }, []);
-
- const handleClose = () => {
- localStorage.setItem('staticBuffetWelcomeSeen', 'true');
- setIsOpen(false);
- };
-
- const features = [
- { icon: Search, title: 'Content Discovery', desc: 'Search thousands of public domain videos' },
- { icon: Play, title: 'Preview & Queue', desc: 'Test and trim clips before performance' },
- { icon: List, title: 'Live Mixing', desc: 'Real-time video playback and transitions' },
- { icon: Zap, title: 'Professional Effects', desc: 'Video and audio processing pipeline' }
- ];
-
- return (
-
-
-
-
-
-
- STATIC BUFFET
-
-
- Professional VJ Tool
-
-
-
-
-
-
- Professional VJ tool for discovering, previewing, and mixing public domain video content for live performances.
-
-
-
- {features.map(({ icon: Icon, title, desc }) => (
-
- ))}
-
-
-
-
- Close
-
- {
- handleClose();
- // Start the tour
- setTimeout(() => {
- (window as any).startStaticBuffetTour?.();
- }, 300);
- }}
- className={`${themeClasses.accentBg} text-white hover:opacity-90`}
- >
- Take Interactive Tour
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ThemeExplanations.tsx b/client/src/components/ThemeExplanations.tsx
deleted file mode 100644
index 6dc823e..0000000
--- a/client/src/components/ThemeExplanations.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import { useState } from 'react';
-import { ChevronDown, ChevronUp } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from '@/components/ui/collapsible';
-import { useStore } from '@/lib/store';
-
-const THEME_EXPLANATIONS = [
- {
- id: 'waffle',
- icon: '🧇',
- name: 'Waffle House',
- description: 'Inspired by the Waffle House menu, the unofficial after-gig hangout for every VJ. Cozy diner colors, laminated-menu vibes, late-night comfort food aesthetic.',
- features: ['Warm yellows and ambers', 'Southern comfort theme', 'Classic diner aesthetic']
- },
- {
- id: 'ebn',
- icon: '📺',
- name: 'EBN Hijack',
- description: 'A nod to the pioneers at Emergency Broadcast Network. Tactical overlays, neon alerts, hacked-broadcast energy with animated scanlines.',
- features: ['Lime green and purple cyberpunk colors', 'Animated scanline effects', 'Video art glitch aesthetic']
- },
- {
- id: 'ozzy',
- icon: '🦇',
- name: 'Heavy Metal',
- description: 'A tribute to Ozzy Osbourne and the heavy metal aesthetic. Dark metallic textures, blood-red accents, and industrial brutality for the headbanging VJ.',
- features: ['Dark metallics with red accents', 'Diagonal metal texture patterns', 'Heavy metal aesthetic']
- },
- {
- id: 'hogan',
- icon: '💪',
- name: 'NWO Hollywood',
- description: 'Channel the Hollywood Hogan era with NWO red, black, and gold. Wrestling stripes and attitude-era energy.',
- features: ['Red wrestling stripes with golden pulsing effects', 'NWO black and white styling', 'Wrestling attitude era aesthetic']
- },
- {
- id: 'dx',
- icon: '🤘',
- name: 'D-Generation X',
- description: 'WWE attitude-era styling with blue and pink gradients. Rebellious energy from the legendary DX faction.',
- features: ['Green and black wrestling colors', 'Pulsing border effects in DX Mode', 'Attitude era rebellion styling']
- },
- {
- id: 'maxheadroom',
- icon: '📺',
- name: 'Max Headroom',
- description: 'Retro-futuristic green matrix aesthetic with cyberpunk elements. Digital prophet from the edge of tomorrow.',
- features: ['Orange and yellow digital colors', 'Screen flicker effects and terminal aesthetics', '80s TV digital character styling']
- },
- {
- id: 'mario',
- icon: '🍄',
- name: 'Sexy Mario Plumber',
- description: 'Meme-inspired theme with Italian-American charm and Nintendo colors. Inappropriate-but-funny plumber humor.',
- features: ['Rainbow Mario colors with coin sparkle effects', 'Nintendo-inspired visual styling', 'Classic video game aesthetic']
- }
-];
-
-export function ThemeExplanations() {
- const { brandSkin } = useStore();
- const [isOpen, setIsOpen] = useState(false);
-
- return (
-
-
-
-
- Visual Themes ({THEME_EXPLANATIONS.length} Total)
-
-
-
- {isOpen ? : }
-
-
-
-
-
-
- {THEME_EXPLANATIONS.map((theme) => (
-
-
- {theme.icon}
-
- {theme.name}
- {brandSkin === theme.id && (Active) }
-
-
-
-
- {theme.description}
-
-
-
- {theme.features.map((feature, index) => (
-
- ))}
-
-
- ))}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ThemeSelector.tsx b/client/src/components/ThemeSelector.tsx
deleted file mode 100644
index 2057c2f..0000000
--- a/client/src/components/ThemeSelector.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-import { useState } from 'react';
-import { Button } from '@/components/ui/button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuTrigger,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuItem
-} from '@/components/ui/dropdown-menu';
-import { Badge } from '@/components/ui/badge';
-import { Palette } from 'lucide-react';
-import { Switch } from '@/components/ui/switch';
-import { useStore } from '@/lib/store';
-
-const themes = [
- { id: 'testcard', name: 'Test Card', color: 'bg-blue-500' },
- { id: 'waffle', name: 'Waffle House', color: 'bg-yellow-500' },
- { id: 'ebn', name: 'EBN Hijack', color: 'bg-lime-500' },
- { id: 'ozzy', name: 'Heavy Metal', color: 'bg-red-600' },
- { id: 'hogan', name: 'NWO Hollywood', color: 'bg-yellow-600' },
- { id: 'dx', name: 'D-Generation X', color: 'bg-pink-500' },
- { id: 'maxheadroom', name: 'Max Headroom', color: 'bg-green-500' },
- { id: 'mario', name: 'Mario Plumber', color: 'bg-red-500' },
- { id: 'dakota', name: 'Dodge Dakota', color: 'bg-gray-600' },
- { id: 'blondie', name: 'Blondie ETC.', color: 'bg-amber-600' }
-] as const;
-
-export function ThemeSelector() {
- const { brandSkin, setBrandSkin, adaptiveColorsEnabled, setAdaptiveColorsEnabled } = useStore();
- const [isOpen, setIsOpen] = useState(false);
-
- const currentTheme = themes.find(t => t.id === brandSkin) || themes[0];
-
- return (
-
-
-
-
-
-
-
-
-
- {/* Adaptive Colors Toggle */}
-
- Adaptive Colors
-
-
-
-
- Visual Themes
-
-
- {themes.map((theme) => {
- const isActive = brandSkin === theme.id;
-
- return (
- setBrandSkin(theme.id)}
- className="cursor-pointer hover:bg-white/30 focus:bg-white/30 text-white"
- data-testid={`menu-theme-${theme.id}`}
- >
-
-
-
- {isActive && (
-
- Active
-
- )}
-
-
-
- );
- })}
-
-
-
- Choose a visual theme to customize the interface
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ThemeSwitcher.tsx b/client/src/components/ThemeSwitcher.tsx
deleted file mode 100644
index e2eb5d5..0000000
--- a/client/src/components/ThemeSwitcher.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
-import { useStore } from '@/lib/store';
-import { type BrandSkin } from '@/lib/types';
-
-export function ThemeSwitcher() {
- const { brandSkin, setBrandSkin } = useStore();
-
- const handleThemeChange = (value: BrandSkin) => {
- setBrandSkin(value);
- };
-
- return (
-
-
-
-
-
- 📺 Test Card
- 🧇 Waffle House
- 📺 EBN Hijack
- 🦇 Heavy Metal
- 💪 NWO Hollywood
- 🤘 D-Generation X
- 📺 Max Headroom
- 🍄 Mario Plumber
- 🚚 Dodge Dakota
- 🎵 Blondie
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ToolsPanel.tsx b/client/src/components/ToolsPanel.tsx
deleted file mode 100644
index 12fb8fd..0000000
--- a/client/src/components/ToolsPanel.tsx
+++ /dev/null
@@ -1,633 +0,0 @@
-import React, { useState, useRef, useEffect } from 'react';
-import { Button } from '@/components/ui/button';
-import {
- Search,
- Play,
- Users,
- Camera,
- Sparkles,
- Layers,
- Palette,
- Music,
- Video,
- Zap,
- Dice6,
- Settings,
- Grid3X3,
- RotateCcw,
- Save,
- Monitor,
- Pause,
- Square,
- Circle,
- RefreshCw,
- Eye,
- Type,
- Lock,
- Unlock,
- Move,
- Repeat
-} from 'lucide-react';
-import { useStore } from '@/lib/store';
-import { getThemeClasses } from '@/lib/theme-utils';
-import { MediaControls } from '@/components/MediaControls';
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
-import { useToast } from '@/hooks/use-toast';
-import { searchVideos } from '@/lib/archive-api';
-import { generateEmergencyMix } from '@/lib/emergency-mix';
-import { TextGenerator } from '@/components/TextGenerator';
-
-export function ToolsPanel() {
- const {
- brandSkin,
- floatingPanelStates,
- setFloatingPanelVisible,
- togglePanelLock,
- bringPanelToFront,
- searchResults,
- queueItems,
- setQueueItems,
- setSearchState,
- setSearchResults,
- addToQueue,
- setTotalResults,
- setLoading,
- resetToDefaultLayout,
- generateAutoLayout
- } = useStore();
-
- const { toast } = useToast();
-
- const themeClasses = getThemeClasses(brandSkin);
- const [testCardOpen, setTestCardOpen] = useState(false);
- const [textToolOpen, setTextToolOpen] = useState(false);
-
- // Toolbar drag state
- const [isLocked, setIsLocked] = useState(true);
- const [position, setPosition] = useState({ x: 16, y: 128 }); // left-4 top-32 in pixels
- const [isDragging, setIsDragging] = useState(false);
- const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
- const toolbarRef = useRef(null);
-
- // Drag handlers
- const handleMouseDown = (e: React.MouseEvent) => {
- if (isLocked) return;
-
- // Don't start drag if clicking on a button
- if ((e.target as HTMLElement).closest('button')) return;
-
- const rect = toolbarRef.current?.getBoundingClientRect();
- if (rect) {
- setIsDragging(true);
- setDragOffset({
- x: e.clientX - rect.left,
- y: e.clientY - rect.top
- });
- e.preventDefault();
- }
- };
-
- const handleMouseMove = (e: MouseEvent) => {
- if (!isDragging || isLocked) return;
-
- const newX = e.clientX - dragOffset.x;
- const newY = e.clientY - dragOffset.y;
-
- // Keep toolbar within viewport bounds
- const maxX = window.innerWidth - 128; // toolbar width
- const maxY = window.innerHeight - 400; // approximate toolbar height
-
- setPosition({
- x: Math.max(0, Math.min(newX, maxX)),
- y: Math.max(0, Math.min(newY, maxY))
- });
- };
-
- const handleMouseUp = () => {
- setIsDragging(false);
- };
-
- useEffect(() => {
- if (isDragging) {
- document.addEventListener('mousemove', handleMouseMove);
- document.addEventListener('mouseup', handleMouseUp);
-
- return () => {
- document.removeEventListener('mousemove', handleMouseMove);
- document.removeEventListener('mouseup', handleMouseUp);
- };
- }
- }, [isDragging, dragOffset, isLocked]);
-
- const toggleLock = () => {
- setIsLocked(!isLocked);
- toast({
- title: isLocked ? "Toolbar Unlocked" : "Toolbar Locked",
- description: isLocked ? "You can now drag the toolbar to move it" : "Toolbar position is now locked",
- });
- };
-
- const togglePanel = (panelId: keyof typeof floatingPanelStates) => {
- const panel = floatingPanelStates[panelId];
- if (panel?.visible) {
- setFloatingPanelVisible(panelId, false);
- } else {
- setFloatingPanelVisible(panelId, true);
- bringPanelToFront(panelId);
- }
- };
-
- // Lucky Dip function
- const handleLuckyDip = async () => {
- setLoading(true);
-
- try {
- const LUCKY_DIP_QUERIES = [
- 'vintage training film',
- 'public service announcement',
- 'educational short',
- 'safety demonstration',
- 'promotional film',
- 'documentary short',
- 'instructional video',
- 'industrial film',
- 'newsreel footage',
- 'animation short',
- 'advertising film',
- 'government film'
- ];
-
- // Pick random queries and combine search terms
- const randomQueries = [...LUCKY_DIP_QUERIES]
- .sort(() => 0.5 - Math.random())
- .slice(0, 2);
-
- const combinedQuery = `${randomQueries.join(' OR ')} AND mediatype:movies AND (licenseurl:*publicdomain* OR collection:*publicdomain*) AND (collection:prelinger OR collection:fedflix) AND year:[1940 TO 1990]`;
-
- const searchParams = {
- query: combinedQuery,
- license: 'publicdomain' as const,
- duration: 'short' as const,
- yearFrom: '1940',
- yearTo: '1990',
- sort: 'relevance' as const,
- page: Math.floor(Math.random() * 3) + 1,
- sources: ['prelinger', 'fedflix'],
- allowRestrictedLicenses: false
- };
-
- setSearchState(searchParams);
- const results = await searchVideos(searchParams);
-
- if (results?.docs && results.docs.length > 0) {
- const shuffledResults = [...results.docs]
- .sort(() => 0.5 - Math.random())
- .slice(0, 15);
-
- setSearchResults(shuffledResults);
- setTotalResults(shuffledResults.length);
-
- toast({
- title: "Lucky Dip Success!",
- description: `Found ${shuffledResults.length} legally safe vintage clips ready for mixing.`,
- });
- } else {
- toast({
- title: "Lucky Dip Failed",
- description: "No results found. Try again for a different random selection.",
- variant: "destructive",
- });
- }
- } catch (error) {
- toast({
- title: "Lucky Dip Error",
- description: "Something went wrong with the search.",
- variant: "destructive",
- });
- }
-
- setLoading(false);
- };
-
- // Emergency Mix function
- const handleEmergencyMix = () => {
- try {
- if (searchResults.length === 0) {
- toast({
- title: "No results available",
- description: "Search for videos first to generate an emergency mix",
- variant: "destructive",
- });
- return;
- }
-
- const emergencyMixSettings = {
- duration: 150,
- segmentLength: [2, 5] as [number, number],
- crossfadeDuration: 0.5,
- maxClips: 10,
- };
-
- const mixItems = generateEmergencyMix(searchResults, emergencyMixSettings);
- setQueueItems(mixItems);
-
- toast({
- title: "Emergency Mix Generated!",
- description: `Created ${mixItems.length} clips totaling ${Math.floor(emergencyMixSettings.duration / 60)}:${(emergencyMixSettings.duration % 60).toString().padStart(2, '0')}`,
- });
-
- } catch (error) {
- toast({
- title: "Mix Generation Failed",
- description: error instanceof Error ? error.message : "Failed to generate emergency mix",
- variant: "destructive",
- });
- }
- };
-
- // Column 1 Tools (Left)
- const leftColumnTools = [
- { id: "search", icon: Search, label: "Search Results", tooltip: "Show/Hide Search Results Panel" },
- { id: "player", icon: Play, label: "Program Output", tooltip: "Show/Hide Program Output Panel" },
- { id: "queue", icon: Users, label: "Queue", tooltip: "Show/Hide Queue Panel" },
- { id: "liveVideo", icon: Camera, label: "Live Input", tooltip: "Enable Live Video Input", isSpecial: true },
- { id: "videoEffects", icon: Video, label: "Video FX", tooltip: "Show/Hide Video Effects Panel" }
- ];
-
- // Column 2 Tools (Right)
- const rightColumnTools = [
- { id: "audioEffects", icon: Music, label: "Audio FX", tooltip: "Show/Hide Audio Effects Panel" },
- { id: "preview", icon: Eye, label: "Preview", tooltip: "Show/Hide Preview Window" },
- { id: "presetEffects", icon: Palette, label: "Quick Effects", tooltip: "Show/Hide Quick Effects Panel" },
- { id: "textTool", icon: Type, label: "Text Tool", tooltip: "Open Text Generator", isSpecial: true },
- { id: "testCard", icon: Monitor, label: "Test Card", tooltip: "Show Test Card for VJ Output", isSpecial: true }
- ];
-
- return (
- <>
-
-
- {/* Header with Lock/Unlock */}
-
-
TOOLBAR
-
- {isLocked ? : }
-
-
-
- {/* Media Controls Section - Two Rows */}
-
-
MEDIA
-
- {/* Media Controls - embedded directly */}
-
- {/* Row 1: Play/Pause, Stop, Record */}
-
-
-
-
-
-
- {/* Row 2: Emergency Mix, Lucky Dip, Reset Layout */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Separator */}
-
-
- {/* Tools Section - Two Columns */}
-
-
TOOLS
-
-
- {/* Left Column */}
-
- {leftColumnTools.map((tool) => {
- const Icon = tool.icon;
-
- // Handle special text tool button
- if (tool.id === 'textTool') {
- return (
- setTextToolOpen(true)}
- variant="ghost"
- size="sm"
- className={`w-12 h-10 p-0 transition-all duration-200 ${themeClasses.textSecondary} hover:${themeClasses.accent} hover:${themeClasses.bgSecondary}`}
- title={tool.tooltip}
- data-testid={`tool-${tool.id}`}
- >
-
-
- );
- }
-
- // Handle regular panel toggle buttons
- const panel = floatingPanelStates[tool.id as keyof typeof floatingPanelStates];
- const isVisible = panel?.visible ?? false;
-
- return (
- togglePanel(tool.id as keyof typeof floatingPanelStates)}
- variant="ghost"
- size="sm"
- className={`w-12 h-10 p-0 transition-all duration-200 ${
- isVisible
- ? `${themeClasses.accent} ${themeClasses.bgSecondary} shadow-md`
- : `${themeClasses.textSecondary} hover:${themeClasses.accent} hover:${themeClasses.bgSecondary}`
- }`}
- title={tool.tooltip}
- data-testid={`tool-${tool.id}`}
- >
-
-
- );
- })}
-
-
- {/* Right Column */}
-
- {rightColumnTools.map((tool) => {
- const Icon = tool.icon;
-
- // Handle special buttons
- if (tool.id === 'testCard') {
- return (
- setTestCardOpen(true)}
- variant="ghost"
- size="sm"
- className={`w-12 h-10 p-0 transition-all duration-200 ${themeClasses.textSecondary} hover:${themeClasses.accent} hover:${themeClasses.bgSecondary}`}
- title={tool.tooltip}
- data-testid={`tool-${tool.id}`}
- >
-
-
- );
- }
-
- if (tool.id === 'textTool') {
- return (
- setTextToolOpen(true)}
- variant="ghost"
- size="sm"
- className={`w-12 h-10 p-0 transition-all duration-200 ${themeClasses.textSecondary} hover:${themeClasses.accent} hover:${themeClasses.bgSecondary}`}
- title={tool.tooltip}
- data-testid={`tool-${tool.id}`}
- >
-
-
- );
- }
-
- // Handle regular panel toggle buttons
- const panel = floatingPanelStates[tool.id as keyof typeof floatingPanelStates];
- const isVisible = panel?.visible ?? false;
-
- return (
- togglePanel(tool.id as keyof typeof floatingPanelStates)}
- variant="ghost"
- size="sm"
- className={`w-12 h-10 p-0 transition-all duration-200 ${
- isVisible
- ? `${themeClasses.accent} ${themeClasses.bgSecondary} shadow-md`
- : `${themeClasses.textSecondary} hover:${themeClasses.accent} hover:${themeClasses.bgSecondary}`
- }`}
- title={tool.tooltip}
- data-testid={`tool-${tool.id}`}
- >
-
-
- );
- })}
-
-
-
-
- {/* Separator */}
-
-
- {/* Layout Preferences Section */}
-
-
LAYOUT
-
-
- {
- generateAutoLayout();
- toast({
- title: "Auto Layout Applied",
- description: "Panels arranged for your screen size",
- });
- }}
- variant="ghost"
- size="sm"
- className={`w-8 h-8 p-0 transition-all duration-200 ${themeClasses.textSecondary} hover:${themeClasses.accent}`}
- title="Auto-arrange panels for screen size"
- >
-
-
-
- {
- resetToDefaultLayout();
- toast({
- title: "Layout Reset",
- description: "Panels restored to default positions",
- });
- }}
- variant="ghost"
- size="sm"
- className={`w-8 h-8 p-0 transition-all duration-200 ${themeClasses.textSecondary} hover:${themeClasses.accent}`}
- title="Reset to default layout"
- >
-
-
-
-
-
-
-
- {/* Test Card Dialog */}
-
-
-
- Test Card - VJ Output
-
-
-
-
- {
- const testCardVideo = {
- identifier: 'static-buffet-test-card',
- title: 'SMPTE Color Bars Test Pattern',
- creator: 'Static Buffet VJ Tool',
- year: '2025',
- description: 'Professional SMPTE color bars test pattern video',
- duration: '00:30',
- licenseurl: 'https://creativecommons.org/licenses/publicdomain/',
- downloads: 1,
- date: new Date().toISOString().split('T')[0]
- };
-
- addToQueue(testCardVideo, '/test-card.mp4');
-
- toast({
- title: "Test Card Added",
- description: "SMPTE test pattern added to queue",
- });
-
- setTestCardOpen(false);
- }}
- className={`${themeClasses.accentBg} text-white hover:opacity-90`}
- >
- Add to Output
-
- setTestCardOpen(false)}
- className={`border ${themeClasses.border} ${themeClasses.textSecondary} ${themeClasses.hover}`}
- >
- Close
-
-
-
-
-
-
- {/* Text Tool Dialog */}
- {
- setTextToolOpen(open);
- // Ensure focus is properly managed when dialog closes
- if (!open) {
- setTimeout(() => {
- // Return focus to document body to prevent focus trapping
- if (document.activeElement && document.activeElement !== document.body) {
- (document.activeElement as HTMLElement).blur();
- }
- }, 100);
- }
- }}
- modal={true}
- >
- {
- // Allow interaction outside to prevent focus trapping issues
- e.preventDefault();
- }}
- >
-
-
-
- Text Generator
-
-
-
-
-
-
-
- >
- );
-}
-
-// Helper component for media control buttons
-function MediaControlButton({ icon: Icon, tooltip }: { icon: any, tooltip: string }) {
- return (
-
-
-
- );
-}
-
diff --git a/client/src/components/VideoWarmingState.tsx b/client/src/components/VideoWarmingState.tsx
deleted file mode 100644
index ddda591..0000000
--- a/client/src/components/VideoWarmingState.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Progress } from '@/components/ui/progress';
-import { Badge } from '@/components/ui/badge';
-import { Loader2, Download, Zap, CheckCircle } from 'lucide-react';
-import { pollJobStatus } from '@/lib/archive-api';
-
-interface VideoWarmingStateProps {
- identifier: string;
- jobId: string;
- initialProgress?: number;
- initialStatus?: string;
- onComplete?: () => void;
- className?: string;
-}
-
-export function VideoWarmingState({
- identifier,
- jobId,
- initialProgress = 0,
- initialStatus = 'pending',
- onComplete,
- className = ''
-}: VideoWarmingStateProps) {
- const [progress, setProgress] = useState(initialProgress);
- const [status, setStatus] = useState(initialStatus);
- const [timeRemaining, setTimeRemaining] = useState('');
- const [startTime] = useState(Date.now());
-
- useEffect(() => {
- const handleProgress = (newProgress: number, newStatus: string) => {
- setProgress(newProgress);
- setStatus(newStatus);
-
- // Estimate time remaining
- if (newProgress > 0 && newProgress < 100) {
- const elapsed = Date.now() - startTime;
- const totalEstimated = (elapsed / newProgress) * 100;
- const remaining = totalEstimated - elapsed;
-
- if (remaining > 0) {
- const minutes = Math.floor(remaining / 60000);
- const seconds = Math.floor((remaining % 60000) / 1000);
- setTimeRemaining(minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`);
- }
- }
- };
-
- // Start polling
- pollJobStatus(jobId, handleProgress, 1500)
- .then(() => {
- setProgress(100);
- setStatus('completed');
- if (onComplete) {
- setTimeout(onComplete, 500); // Small delay to show completion
- }
- })
- .catch((error) => {
- console.error('Transcoding failed:', error);
- setStatus('failed');
- });
- }, [jobId, onComplete, startTime]);
-
- const getStatusInfo = () => {
- switch (status) {
- case 'pending':
- return {
- icon: ,
- label: 'Queued',
- description: 'Waiting to start processing...',
- color: 'bg-yellow-500'
- };
- case 'downloading':
- return {
- icon: ,
- label: 'Downloading',
- description: 'Fetching from Internet Archive...',
- color: 'bg-blue-500'
- };
- case 'transcoding':
- return {
- icon: ,
- label: 'Transcoding',
- description: 'Converting to optimized format...',
- color: 'bg-purple-500'
- };
- case 'completed':
- return {
- icon: ,
- label: 'Complete',
- description: 'Video ready for streaming!',
- color: 'bg-green-500'
- };
- case 'failed':
- return {
- icon:
,
- label: 'Failed',
- description: 'Processing failed. Using fallback.',
- color: 'bg-red-500'
- };
- default:
- return {
- icon: ,
- label: 'Processing',
- description: 'Working on it...',
- color: 'bg-gray-500'
- };
- }
- };
-
- const statusInfo = getStatusInfo();
-
- return (
-
-
-
- {statusInfo.icon}
-
- {statusInfo.label}
-
-
-
- {timeRemaining && status !== 'completed' && status !== 'failed' && (
-
- ~{timeRemaining} remaining
-
- )}
-
-
-
-
- {statusInfo.description}
- {Math.round(progress)}%
-
-
-
-
-
-
- {identifier}
- {status === 'completed' && (
- • Cached for instant playback
- )}
- {status === 'failed' && (
- • Streaming directly from archive
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/WelcomeModal.tsx b/client/src/components/WelcomeModal.tsx
deleted file mode 100644
index 163f354..0000000
--- a/client/src/components/WelcomeModal.tsx
+++ /dev/null
@@ -1,265 +0,0 @@
-import { useState, useEffect } from 'react';
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
-import { Badge } from '@/components/ui/badge';
-import { Separator } from '@/components/ui/separator';
-import { Volume2, Play, Search, Zap, Tv, MousePointer, Headphones, Sparkles } from 'lucide-react';
-import { useStore } from '@/lib/store';
-
-export function WelcomeModal() {
- const [isOpen, setIsOpen] = useState(false);
- const { brandSkin } = useStore();
-
- useEffect(() => {
- // Check if user has seen the welcome modal before
- const hasSeenWelcome = localStorage.getItem('staticBuffetWelcomeSeen');
- if (!hasSeenWelcome) {
- setIsOpen(true);
- }
- }, []);
-
- const handleClose = () => {
- localStorage.setItem('staticBuffetWelcomeSeen', 'true');
- setIsOpen(false);
- };
-
- const handleGetStarted = () => {
- localStorage.setItem('staticBuffetWelcomeSeen', 'true');
- setIsOpen(false);
- // Optionally trigger a demo search or feature tour
- };
-
- return (
-
-
-
-
-
-
- STATIC BUFFET
-
-
-
- Trash Team × Nulltone.TV
-
-
-
-
-
- {/* Main Description */}
-
-
- All-You-Can-Eat Video Chaos
-
-
- The ultimate VJ buffet for mixing free public domain and Creative Commons video content from Archive.org.
- Turn discarded culture into something new with real-time audio-reactive mixing.
-
-
-
- {/* Volume Alert */}
-
-
-
- TURN UP THE VOLUME!
-
-
-
- Static Buffet includes audio-reactive features and live mixing capabilities.
-
-
-
-
-
- {/* Quick Start Guide */}
-
-
- Quick Start Guide
-
-
-
- {/* Search & Queue */}
-
-
-
- 1. Search & Queue
-
-
- • Search Archive.org for videos
- • Filter by license (Public Domain, CC)
- • Click videos to preview
- • Add to timeline queue
-
-
-
- {/* Mix & Effects */}
-
-
-
- 2. Mix & Effects
-
-
- • Apply video/audio effects
- • Enable audio-reactive mode
- • Use Emergency Mix generator
- • Export playlists with licensing
-
-
-
- {/* Themes & Features */}
-
-
-
- 3. Special Features
-
-
- • 8 visual themes with easter eggs
- • Triple-click theme buttons for surprises
- • Resizable panel workspace
- • Save custom layouts
-
-
-
- {/* Pro Tips */}
-
-
-
- • Use keyboard shortcuts for live VJ
- • Try "Waffle House" theme 🧇
- • Enable webcam for live mixing
- • Check out the long timeline!
-
-
-
-
-
-
-
- {/* Themes Preview */}
-
-
- Choose Your Vibe
-
-
- Test Card
- Waffle House 🧇
- EBN Hijack
- Heavy Metal 🤘
- NWO Hollywood
- D-Generation X
- Max Headroom
- Sexy Mario 🍄
- Dodge Dakota 🚚
- Blondie 🎵
-
-
- Each theme has unique easter eggs and visual effects!
-
-
-
- {/* Action Buttons */}
-
-
-
- Let's Go! Start Mixing
-
-
- Skip Intro
-
-
-
- {/* Footer Note */}
-
-
- All content sourced from Archive.org • Respecting Creative Commons & Public Domain licensing
-
-
- Built for VJs, by VJs • Open source alternative programming
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx
deleted file mode 100644
index 8722561..0000000
--- a/client/src/components/ui/alert-dialog.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import * as React from "react"
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
-
-import { cn } from "@/lib/utils"
-import { buttonVariants } from "@/components/ui/button"
-
-const AlertDialog = AlertDialogPrimitive.Root
-
-const AlertDialogTrigger = AlertDialogPrimitive.Trigger
-
-const AlertDialogPortal = AlertDialogPrimitive.Portal
-
-const AlertDialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
-
-const AlertDialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-
-))
-AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
-
-const AlertDialogHeader = ({
- className,
- ...props
-}: React.HTMLAttributes) => (
-
-)
-AlertDialogHeader.displayName = "AlertDialogHeader"
-
-const AlertDialogFooter = ({
- className,
- ...props
-}: React.HTMLAttributes) => (
-
-)
-AlertDialogFooter.displayName = "AlertDialogFooter"
-
-const AlertDialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
-
-const AlertDialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogDescription.displayName =
- AlertDialogPrimitive.Description.displayName
-
-const AlertDialogAction = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
-
-const AlertDialogCancel = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
-
-export {
- AlertDialog,
- AlertDialogPortal,
- AlertDialogOverlay,
- AlertDialogTrigger,
- AlertDialogContent,
- AlertDialogHeader,
- AlertDialogFooter,
- AlertDialogTitle,
- AlertDialogDescription,
- AlertDialogAction,
- AlertDialogCancel,
-}
diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx
deleted file mode 100644
index c4abbf3..0000000
--- a/client/src/components/ui/aspect-ratio.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
-
-const AspectRatio = AspectRatioPrimitive.Root
-
-export { AspectRatio }
diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx
deleted file mode 100644
index 51e507b..0000000
--- a/client/src/components/ui/avatar.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
-
-import { cn } from "@/lib/utils"
-
-const Avatar = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-Avatar.displayName = AvatarPrimitive.Root.displayName
-
-const AvatarImage = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AvatarImage.displayName = AvatarPrimitive.Image.displayName
-
-const AvatarFallback = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
-
-export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx
deleted file mode 100644
index 60e6c96..0000000
--- a/client/src/components/ui/breadcrumb.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { ChevronRight, MoreHorizontal } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-
-const Breadcrumb = React.forwardRef<
- HTMLElement,
- React.ComponentPropsWithoutRef<"nav"> & {
- separator?: React.ReactNode
- }
->(({ ...props }, ref) => )
-Breadcrumb.displayName = "Breadcrumb"
-
-const BreadcrumbList = React.forwardRef<
- HTMLOListElement,
- React.ComponentPropsWithoutRef<"ol">
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbList.displayName = "BreadcrumbList"
-
-const BreadcrumbItem = React.forwardRef<
- HTMLLIElement,
- React.ComponentPropsWithoutRef<"li">
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbItem.displayName = "BreadcrumbItem"
-
-const BreadcrumbLink = React.forwardRef<
- HTMLAnchorElement,
- React.ComponentPropsWithoutRef<"a"> & {
- asChild?: boolean
- }
->(({ asChild, className, ...props }, ref) => {
- const Comp = asChild ? Slot : "a"
-
- return (
-
- )
-})
-BreadcrumbLink.displayName = "BreadcrumbLink"
-
-const BreadcrumbPage = React.forwardRef<
- HTMLSpanElement,
- React.ComponentPropsWithoutRef<"span">
->(({ className, ...props }, ref) => (
-
-))
-BreadcrumbPage.displayName = "BreadcrumbPage"
-
-const BreadcrumbSeparator = ({
- children,
- className,
- ...props
-}: React.ComponentProps<"li">) => (
- svg]:w-3.5 [&>svg]:h-3.5", className)}
- {...props}
- >
- {children ?? }
-
-)
-BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
-
-const BreadcrumbEllipsis = ({
- className,
- ...props
-}: React.ComponentProps<"span">) => (
-
-
- More
-
-)
-BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
-
-export {
- Breadcrumb,
- BreadcrumbList,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbPage,
- BreadcrumbSeparator,
- BreadcrumbEllipsis,
-}
diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx
deleted file mode 100644
index 2174f71..0000000
--- a/client/src/components/ui/calendar.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import * as React from "react"
-import { ChevronLeft, ChevronRight } from "lucide-react"
-import { DayPicker } from "react-day-picker"
-
-import { cn } from "@/lib/utils"
-import { buttonVariants } from "@/components/ui/button"
-
-export type CalendarProps = React.ComponentProps
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- ...props
-}: CalendarProps) {
- return (
- (
-
- ),
- IconRight: ({ className, ...props }) => (
-
- ),
- }}
- {...props}
- />
- )
-}
-Calendar.displayName = "Calendar"
-
-export { Calendar }
diff --git a/client/src/components/ui/carousel.tsx b/client/src/components/ui/carousel.tsx
deleted file mode 100644
index 9c2b9bf..0000000
--- a/client/src/components/ui/carousel.tsx
+++ /dev/null
@@ -1,260 +0,0 @@
-import * as React from "react"
-import useEmblaCarousel, {
- type UseEmblaCarouselType,
-} from "embla-carousel-react"
-import { ArrowLeft, ArrowRight } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-
-type CarouselApi = UseEmblaCarouselType[1]
-type UseCarouselParameters = Parameters
-type CarouselOptions = UseCarouselParameters[0]
-type CarouselPlugin = UseCarouselParameters[1]
-
-type CarouselProps = {
- opts?: CarouselOptions
- plugins?: CarouselPlugin
- orientation?: "horizontal" | "vertical"
- setApi?: (api: CarouselApi) => void
-}
-
-type CarouselContextProps = {
- carouselRef: ReturnType[0]
- api: ReturnType[1]
- scrollPrev: () => void
- scrollNext: () => void
- canScrollPrev: boolean
- canScrollNext: boolean
-} & CarouselProps
-
-const CarouselContext = React.createContext(null)
-
-function useCarousel() {
- const context = React.useContext(CarouselContext)
-
- if (!context) {
- throw new Error("useCarousel must be used within a ")
- }
-
- return context
-}
-
-const Carousel = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes & CarouselProps
->(
- (
- {
- orientation = "horizontal",
- opts,
- setApi,
- plugins,
- className,
- children,
- ...props
- },
- ref
- ) => {
- const [carouselRef, api] = useEmblaCarousel(
- {
- ...opts,
- axis: orientation === "horizontal" ? "x" : "y",
- },
- plugins
- )
- const [canScrollPrev, setCanScrollPrev] = React.useState(false)
- const [canScrollNext, setCanScrollNext] = React.useState(false)
-
- const onSelect = React.useCallback((api: CarouselApi) => {
- if (!api) {
- return
- }
-
- setCanScrollPrev(api.canScrollPrev())
- setCanScrollNext(api.canScrollNext())
- }, [])
-
- const scrollPrev = React.useCallback(() => {
- api?.scrollPrev()
- }, [api])
-
- const scrollNext = React.useCallback(() => {
- api?.scrollNext()
- }, [api])
-
- const handleKeyDown = React.useCallback(
- (event: React.KeyboardEvent) => {
- if (event.key === "ArrowLeft") {
- event.preventDefault()
- scrollPrev()
- } else if (event.key === "ArrowRight") {
- event.preventDefault()
- scrollNext()
- }
- },
- [scrollPrev, scrollNext]
- )
-
- React.useEffect(() => {
- if (!api || !setApi) {
- return
- }
-
- setApi(api)
- }, [api, setApi])
-
- React.useEffect(() => {
- if (!api) {
- return
- }
-
- onSelect(api)
- api.on("reInit", onSelect)
- api.on("select", onSelect)
-
- return () => {
- api?.off("select", onSelect)
- }
- }, [api, onSelect])
-
- return (
-
-
- {children}
-
-
- )
- }
-)
-Carousel.displayName = "Carousel"
-
-const CarouselContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => {
- const { carouselRef, orientation } = useCarousel()
-
- return (
-
- )
-})
-CarouselContent.displayName = "CarouselContent"
-
-const CarouselItem = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => {
- const { orientation } = useCarousel()
-
- return (
-
- )
-})
-CarouselItem.displayName = "CarouselItem"
-
-const CarouselPrevious = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps
->(({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel()
-
- return (
-
-
- Previous slide
-
- )
-})
-CarouselPrevious.displayName = "CarouselPrevious"
-
-const CarouselNext = React.forwardRef<
- HTMLButtonElement,
- React.ComponentProps
->(({ className, variant = "outline", size = "icon", ...props }, ref) => {
- const { orientation, scrollNext, canScrollNext } = useCarousel()
-
- return (
-
-
- Next slide
-
- )
-})
-CarouselNext.displayName = "CarouselNext"
-
-export {
- type CarouselApi,
- Carousel,
- CarouselContent,
- CarouselItem,
- CarouselPrevious,
- CarouselNext,
-}
diff --git a/client/src/components/ui/chart.tsx b/client/src/components/ui/chart.tsx
deleted file mode 100644
index 39fba6d..0000000
--- a/client/src/components/ui/chart.tsx
+++ /dev/null
@@ -1,365 +0,0 @@
-"use client"
-
-import * as React from "react"
-import * as RechartsPrimitive from "recharts"
-
-import { cn } from "@/lib/utils"
-
-// Format: { THEME_NAME: CSS_SELECTOR }
-const THEMES = { light: "", dark: ".dark" } as const
-
-export type ChartConfig = {
- [k in string]: {
- label?: React.ReactNode
- icon?: React.ComponentType
- } & (
- | { color?: string; theme?: never }
- | { color?: never; theme: Record }
- )
-}
-
-type ChartContextProps = {
- config: ChartConfig
-}
-
-const ChartContext = React.createContext(null)
-
-function useChart() {
- const context = React.useContext(ChartContext)
-
- if (!context) {
- throw new Error("useChart must be used within a ")
- }
-
- return context
-}
-
-const ChartContainer = React.forwardRef<
- HTMLDivElement,
- React.ComponentProps<"div"> & {
- config: ChartConfig
- children: React.ComponentProps<
- typeof RechartsPrimitive.ResponsiveContainer
- >["children"]
- }
->(({ id, className, children, config, ...props }, ref) => {
- const uniqueId = React.useId()
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
-
- return (
-
-
-
-
- {children}
-
-
-
- )
-})
-ChartContainer.displayName = "Chart"
-
-const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
- const colorConfig = Object.entries(config).filter(
- ([, config]) => config.theme || config.color
- )
-
- if (!colorConfig.length) {
- return null
- }
-
- return (
-