Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "static-buffet-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"autoPort": true
}
]
}
41 changes: 0 additions & 41 deletions .env

This file was deleted.

21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Typecheck
run: npm run check
- name: Build
run: npm run build
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ dist
.DS_Store
server/public
vite.config.ts.*
*.tar.gz
*.tar.gz
.env
video-cache/
local.db
50 changes: 31 additions & 19 deletions client/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
import { Badge } from '@/components/ui/badge';
import { useStore } from '@/lib/store';
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;
Expand All @@ -24,9 +28,9 @@ interface CommandAction {
}

export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
const {
setSearchQuery,
searchState,
const {
setSearchState,
searchState,
queueItems,
clearQueue,
setDetailDrawerOpen,
Expand Down Expand Up @@ -68,7 +72,7 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
icon: <Search className="w-4 h-4" />,
keywords: ['clear', 'reset', 'empty', 'search'],
action: () => {
setSearchQuery('');
setSearchState({ query: '', page: 1 });
onClose();
toast({ title: 'Search cleared' });
},
Expand Down Expand Up @@ -120,10 +124,18 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
description: selectedVideo ? `Add "${selectedVideo.title}" to queue` : 'No video selected',
icon: <Plus className="w-4 h-4" />,
keywords: ['add', 'queue', 'current', 'video', 'plus'],
action: () => {
action: async () => {
if (selectedVideo) {
addToQueue(selectedVideo);
toast({ title: 'Added to queue', description: selectedVideo.title });
try {
const metadata = await getVideoMetadata(selectedVideo.identifier);
if (!metadata.streamUrl) {
throw new Error('No playable stream URL');
}
addToQueue(selectedVideo, metadata.streamUrl);
toast({ title: 'Added to queue', description: selectedVideo.title });
} catch {
toast({ title: 'Could not add video', description: 'No playable stream found', variant: 'destructive' });
}
} else {
toast({ title: 'No video selected', variant: 'destructive' });
}
Expand Down Expand Up @@ -157,7 +169,7 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
icon: <Video className="w-4 h-4" />,
keywords: ['public', 'domain', 'free', 'creative', 'commons'],
action: () => {
setSearchQuery('collection:(prelinger OR communitytexts OR opensource_movies)');
setSearchState({ query: 'collection:(prelinger OR communitytexts OR opensource_movies)', page: 1 });
onClose();
toast({ title: 'Searching public domain videos' });
},
Expand All @@ -170,7 +182,7 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
icon: <Music className="w-4 h-4" />,
keywords: ['music', 'audio', 'sound', 'songs'],
action: () => {
setSearchQuery('mediatype:audio');
setSearchState({ query: 'mediatype:audio', page: 1 });
onClose();
toast({ title: 'Searching audio content' });
},
Expand All @@ -180,21 +192,21 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
// Theme & UI
{
id: 'toggle-theme',
title: 'Toggle Brand Skin',
description: `Switch to ${brandSkin === 'trashteam' ? 'NULLTONE.TV' : 'Trash Team'} theme`,
title: 'Cycle Brand Skin',
description: 'Switch to the next theme',
icon: <Palette className="w-4 h-4" />,
keywords: ['theme', 'skin', 'brand', 'toggle', 'switch'],
keywords: ['theme', 'skin', 'brand', 'toggle', 'switch', 'cycle'],
action: () => {
const newSkin = brandSkin === 'trashteam' ? 'nulltone' : 'trashteam';
setBrandSkin(newSkin);
const nextSkin = BRAND_SKINS[(BRAND_SKINS.indexOf(brandSkin) + 1) % BRAND_SKINS.length];
setBrandSkin(nextSkin);
onClose();
toast({
title: 'Theme changed',
description: `Switched to ${newSkin === 'trashteam' ? 'Trash Team' : 'NULLTONE.TV'} theme`
toast({
title: 'Theme changed',
description: `Switched to ${nextSkin} theme`
});
},
group: 'Appearance',
badge: brandSkin === 'trashteam' ? 'Trash Team' : 'NULLTONE.TV',
badge: brandSkin,
},

// Export & Tools
Expand Down Expand Up @@ -276,7 +288,7 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
group: 'Tools',
badge: searchResults.length > 0 ? `${searchResults.length} results` : 'No results',
},
], [queueItems, selectedVideo, searchResults, brandSkin, onClose, setSearchQuery, clearQueue, addToQueue, setDetailDrawerOpen, setBrandSkin, toast]);
], [queueItems, selectedVideo, searchResults, brandSkin, onClose, setSearchState, clearQueue, addToQueue, setDetailDrawerOpen, setBrandSkin, toast]);

// Filter commands based on search
const filteredCommands = useMemo(() => {
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function Filters({ onFiltersChange }: FiltersProps) {
return (
<div className="flex items-center space-x-3 text-xs">
<div className="flex items-center space-x-1">
<Calendar className="h-3 w-3 text-gray-300" title="Year Range" />
<span title="Year Range"><Calendar className="h-3 w-3 text-gray-300" /></span>
<Select
value={searchState.yearFrom || '1950'}
onValueChange={(value) => handleFilterChange('yearFrom', value)}
Expand Down Expand Up @@ -88,7 +88,7 @@ export function Filters({ onFiltersChange }: FiltersProps) {


<div className="flex items-center space-x-1">
<Shield className="h-3 w-3 text-gray-300" title="License" />
<span title="License"><Shield className="h-3 w-3 text-gray-300" /></span>
<Select
value={searchState.license || 'all'}
onValueChange={(value) => handleFilterChange('license', value)}
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/LicenseGuardrail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ export function LicenseGuardrail({ onLicenseChange }: LicenseGuardrailProps) {
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1">
{searchState.allowRestrictedLicenses ? (
<ShieldOff size={16} className="text-orange-400" title="License Guardrail Off - Restricted licenses allowed" />
<span title="License Guardrail Off - Restricted licenses allowed"><ShieldOff size={16} className="text-orange-400" /></span>
) : (
<Shield size={16} className={theme.text} title="License Guardrail On - Safe licenses only" />
<span title="License Guardrail On - Safe licenses only"><Shield size={16} className={theme.text} /></span>
)}
<Switch
id="license-guardrail"
Expand Down
28 changes: 0 additions & 28 deletions client/src/components/MarioPipeEffect.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion client/src/components/PreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function PreviewPanel() {

{isExpanded && (
<PreviewWindow
videoUrl={previewVideo?.videoUrl}
videoUrl={previewVideo?.videoUrl ?? undefined}
video={previewVideo}
className="w-full"
/>
Expand Down
34 changes: 17 additions & 17 deletions client/src/components/PreviewWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function PreviewWindow({ videoUrl, video, className = '' }: PreviewWindow
setIsVideoLoading(true);
setVideoLoadError(false);

const video = videoRef.current;
const videoEl = videoRef.current;

const handleLoadStart = () => {
setIsVideoLoading(true);
Expand All @@ -50,39 +50,39 @@ export function PreviewWindow({ videoUrl, video, className = '' }: PreviewWindow

const handleLoadedData = () => {
setIsVideoLoading(false);
setDuration(video.duration || 0);
setDuration(videoEl.duration || 0);
};

const handleError = (e: any) => {
console.error('❌ PreviewWindow: Video load error:', {
error: e,
videoUrl: videoUrl?.substring(0, 100) + '...',
errorCode: video.error?.code,
errorMessage: video.error?.message,
networkState: video.networkState,
readyState: video.readyState
errorCode: videoEl.error?.code,
errorMessage: videoEl.error?.message,
networkState: videoEl.networkState,
readyState: videoEl.readyState
});
setIsVideoLoading(false);
setVideoLoadError(true);
};

const handleTimeUpdate = () => {
setCurrentTime(video.currentTime);
setCurrentTime(videoEl.currentTime);
};

video.addEventListener('loadstart', handleLoadStart);
video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('error', handleError);
video.addEventListener('timeupdate', handleTimeUpdate);
videoEl.addEventListener('loadstart', handleLoadStart);
videoEl.addEventListener('loadeddata', handleLoadedData);
videoEl.addEventListener('error', handleError);
videoEl.addEventListener('timeupdate', handleTimeUpdate);

video.src = videoUrl;
video.load();
videoEl.src = videoUrl;
videoEl.load();

return () => {
video.removeEventListener('loadstart', handleLoadStart);
video.removeEventListener('loadeddata', handleLoadedData);
video.removeEventListener('error', handleError);
video.removeEventListener('timeupdate', handleTimeUpdate);
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
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ResponsiveLayoutManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface ResponsiveLayoutManagerProps {

export function ResponsiveLayoutManager({ children }: ResponsiveLayoutManagerProps) {
const { currentLayout, panelConfig, isTransitioning } = useResponsiveLayout();
const { setResizableMode, panelStates } = useStore();
const { setResizableMode } = useStore();

// Automatically adjust resizable mode based on layout
useEffect(() => {
Expand Down
Loading
Loading