From 3fad1d773ebe4177a1785f36cae4ca5cf69ac7ea Mon Sep 17 00:00:00 2001 From: JSC Date: Sun, 6 Jul 2025 16:57:41 +0200 Subject: [PATCH] feat: add AddUrlDialog component and keyboard shortcut for adding URLs --- src/components/AddUrlDialog.tsx | 146 ++++++++++++++++++++++++++++ src/components/ui/dialog.tsx | 141 +++++++++++++++++++++++++++ src/hooks/use-keyboard-shortcuts.ts | 48 +++++++++ src/pages/SoundboardPage.tsx | 23 ++++- 4 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 src/components/AddUrlDialog.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/hooks/use-keyboard-shortcuts.ts diff --git a/src/components/AddUrlDialog.tsx b/src/components/AddUrlDialog.tsx new file mode 100644 index 0000000..a70a59a --- /dev/null +++ b/src/components/AddUrlDialog.tsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from 'react' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { toast } from 'sonner' +import { apiService } from '@/services/api' + +interface AddUrlDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function AddUrlDialog({ open, onOpenChange }: AddUrlDialogProps) { + const [url, setUrl] = useState('') + const [loading, setLoading] = useState(false) + + // Reset form when dialog opens + useEffect(() => { + if (open) { + setUrl('') + setLoading(false) + } + }, [open]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!url.trim()) { + toast.error('Please enter a URL') + return + } + + if (!isValidUrl(url.trim())) { + toast.error('Please enter a valid URL') + return + } + + setLoading(true) + + try { + const response = await apiService.post('/api/stream/add-url', { url: url.trim() }) + const data = await response.json() + + if (response.ok) { + toast.success('URL added to processing queue!') + onOpenChange(false) + } else { + if (response.status === 409) { + toast.error('This URL is already in the system') + } else { + toast.error(data.error || 'Failed to add URL') + } + } + } catch (error) { + console.error('Error adding URL:', error) + toast.error('Failed to add URL. Please try again.') + } finally { + setLoading(false) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !loading) { + handleSubmit(e as any) + } + if (e.key === 'Escape') { + onOpenChange(false) + } + } + + const isValidUrl = (string: string) => { + try { + const url = new URL(string) + return url.protocol === 'http:' || url.protocol === 'https:' + } catch { + return false + } + } + + const getSupportedServices = () => { + return [ + 'YouTube', + 'SoundCloud', + 'Dailymotion', + 'Spotify', + 'Vimeo', + 'Twitch', + 'and many more...' + ] + } + + return ( + + + + Add Media URL + + +
+
+ + setUrl(e.target.value)} + onKeyDown={handleKeyDown} + disabled={loading} + autoFocus + /> +

+ Supported services: {getSupportedServices().join(', ')} +

+
+ +
+ + +
+
+ +
+

How it works:

+
    +
  • Paste any supported media URL
  • +
  • Audio will be extracted automatically
  • +
  • New sound will be added to your soundboard
  • +
  • Processing may take a few moments
  • +
  • Check the streams page to monitor progress
  • +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..6cb123b --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,141 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/src/hooks/use-keyboard-shortcuts.ts b/src/hooks/use-keyboard-shortcuts.ts new file mode 100644 index 0000000..93ffcbf --- /dev/null +++ b/src/hooks/use-keyboard-shortcuts.ts @@ -0,0 +1,48 @@ +import { useEffect, useCallback } from 'react' + +interface KeyboardShortcut { + key: string + ctrlKey?: boolean + altKey?: boolean + shiftKey?: boolean + metaKey?: boolean + callback: () => void + preventDefault?: boolean +} + +export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { + const handleKeyDown = useCallback((event: KeyboardEvent) => { + for (const shortcut of shortcuts) { + const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase() + const ctrlMatches = !!shortcut.ctrlKey === event.ctrlKey + const altMatches = !!shortcut.altKey === event.altKey + const shiftMatches = !!shortcut.shiftKey === event.shiftKey + const metaMatches = !!shortcut.metaKey === event.metaKey + + if (keyMatches && ctrlMatches && altMatches && shiftMatches && metaMatches) { + if (shortcut.preventDefault !== false) { + event.preventDefault() + } + shortcut.callback() + break + } + } + }, [shortcuts]) + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown) + return () => { + document.removeEventListener('keydown', handleKeyDown) + } + }, [handleKeyDown]) +} + +export function useAddUrlShortcut(onOpen: () => void) { + useKeyboardShortcuts([ + { + key: 'u', + ctrlKey: true, + callback: onOpen, + } + ]) +} \ No newline at end of file diff --git a/src/pages/SoundboardPage.tsx b/src/pages/SoundboardPage.tsx index dbcd4c3..73ca28d 100644 --- a/src/pages/SoundboardPage.tsx +++ b/src/pages/SoundboardPage.tsx @@ -1,9 +1,11 @@ import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { Play, Square, Volume2 } from 'lucide-react'; +import { Play, Square, Volume2, Plus } from 'lucide-react'; import { toast } from 'sonner'; import { apiService } from '@/services/api'; +import { AddUrlDialog } from '@/components/AddUrlDialog'; +import { useAddUrlShortcut } from '@/hooks/use-keyboard-shortcuts'; interface Sound { id: number; @@ -75,6 +77,10 @@ export function SoundboardPage() { const [error, setError] = useState(null); const [playingSound, setPlayingSound] = useState(null); const [searchTerm, setSearchTerm] = useState(''); + const [addUrlDialogOpen, setAddUrlDialogOpen] = useState(false); + + // Setup keyboard shortcut for CTRL+U + useAddUrlShortcut(() => setAddUrlDialogOpen(true)); useEffect(() => { fetchSounds(); @@ -162,6 +168,15 @@ export function SoundboardPage() {

Soundboard

+
)} + + {/* Add URL Dialog */} +
); } \ No newline at end of file