|
@@ -101,10 +101,6 @@ export function BrowsePage() {
|
|
|
// Favorites state
|
|
// Favorites state
|
|
|
const [favorites, setFavorites] = useState<Set<string>>(new Set())
|
|
const [favorites, setFavorites] = useState<Set<string>>(new Set())
|
|
|
|
|
|
|
|
- // Upload state
|
|
|
|
|
- const fileInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
|
- const [isUploading, setIsUploading] = useState(false)
|
|
|
|
|
-
|
|
|
|
|
// Close panel when playback starts
|
|
// Close panel when playback starts
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
const handlePlaybackStarted = () => {
|
|
const handlePlaybackStarted = () => {
|
|
@@ -821,52 +817,6 @@ export function BrowsePage() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Handle pattern file upload
|
|
|
|
|
- const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
- const file = e.target.files?.[0]
|
|
|
|
|
- if (!file) return
|
|
|
|
|
-
|
|
|
|
|
- // Validate file extension
|
|
|
|
|
- if (!file.name.endsWith('.thr')) {
|
|
|
|
|
- toast.error('Please select a .thr file')
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- setIsUploading(true)
|
|
|
|
|
- try {
|
|
|
|
|
- const formData = new FormData()
|
|
|
|
|
- formData.append('file', file)
|
|
|
|
|
-
|
|
|
|
|
- const response = await fetch('/upload_theta_rho', {
|
|
|
|
|
- method: 'POST',
|
|
|
|
|
- body: formData,
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- if (!response.ok) {
|
|
|
|
|
- const error = await response.json()
|
|
|
|
|
- throw new Error(error.detail || 'Upload failed')
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- toast.success(`Pattern "${file.name}" uploaded successfully`)
|
|
|
|
|
-
|
|
|
|
|
- // Refresh patterns list
|
|
|
|
|
- const patternsRes = await fetch('/list_theta_rho_files')
|
|
|
|
|
- if (patternsRes.ok) {
|
|
|
|
|
- const data = await patternsRes.json()
|
|
|
|
|
- setPatterns(data.files || [])
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('Upload error:', error)
|
|
|
|
|
- toast.error(error instanceof Error ? error.message : 'Failed to upload pattern')
|
|
|
|
|
- } finally {
|
|
|
|
|
- setIsUploading(false)
|
|
|
|
|
- // Reset file input
|
|
|
|
|
- if (fileInputRef.current) {
|
|
|
|
|
- fileInputRef.current.value = ''
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
if (isLoading) {
|
|
if (isLoading) {
|
|
|
return (
|
|
return (
|
|
|
<div className="flex items-center justify-center min-h-[60vh]">
|
|
<div className="flex items-center justify-center min-h-[60vh]">
|
|
@@ -879,36 +829,12 @@ export function BrowsePage() {
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className={`flex flex-col w-full max-w-5xl mx-auto gap-6 py-6 px-4 transition-all duration-300 ${isPanelOpen ? 'lg:mr-[28rem]' : ''}`}>
|
|
<div className={`flex flex-col w-full max-w-5xl mx-auto gap-6 py-6 px-4 transition-all duration-300 ${isPanelOpen ? 'lg:mr-[28rem]' : ''}`}>
|
|
|
- {/* Hidden file input for pattern upload */}
|
|
|
|
|
- <input
|
|
|
|
|
- ref={fileInputRef}
|
|
|
|
|
- type="file"
|
|
|
|
|
- accept=".thr"
|
|
|
|
|
- onChange={handleFileUpload}
|
|
|
|
|
- className="hidden"
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
{/* Page Header */}
|
|
{/* Page Header */}
|
|
|
- <div className="flex items-start justify-between gap-4">
|
|
|
|
|
- <div className="space-y-1">
|
|
|
|
|
- <h1 className="text-3xl font-bold tracking-tight">Browse Patterns</h1>
|
|
|
|
|
- <p className="text-muted-foreground">
|
|
|
|
|
- Explore and run patterns on your sand table · {patterns.length} patterns available
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Button
|
|
|
|
|
- variant="outline"
|
|
|
|
|
- onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
- disabled={isUploading}
|
|
|
|
|
- className="gap-2 shrink-0"
|
|
|
|
|
- >
|
|
|
|
|
- {isUploading ? (
|
|
|
|
|
- <span className="material-icons-outlined animate-spin text-lg">sync</span>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span className="material-icons-outlined text-lg">add</span>
|
|
|
|
|
- )}
|
|
|
|
|
- <span className="hidden sm:inline">Add Pattern</span>
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ <div className="space-y-1">
|
|
|
|
|
+ <h1 className="text-3xl font-bold tracking-tight">Browse Patterns</h1>
|
|
|
|
|
+ <p className="text-muted-foreground">
|
|
|
|
|
+ Explore and run patterns on your sand table · {patterns.length} patterns available
|
|
|
|
|
+ </p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<Separator />
|
|
<Separator />
|
|
@@ -928,14 +854,12 @@ export function BrowsePage() {
|
|
|
className="pl-10 pr-10"
|
|
className="pl-10 pr-10"
|
|
|
/>
|
|
/>
|
|
|
{searchQuery && (
|
|
{searchQuery && (
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="icon-sm"
|
|
|
|
|
|
|
+ <button
|
|
|
onClick={() => setSearchQuery('')}
|
|
onClick={() => setSearchQuery('')}
|
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
|
>
|
|
>
|
|
|
<span className="material-icons-outlined text-xl">close</span>
|
|
<span className="material-icons-outlined text-xl">close</span>
|
|
|
- </Button>
|
|
|
|
|
|
|
+ </button>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -1063,14 +987,12 @@ export function BrowsePage() {
|
|
|
<h2 className="text-lg font-semibold truncate pr-4">
|
|
<h2 className="text-lg font-semibold truncate pr-4">
|
|
|
{selectedPattern?.name || 'Pattern Details'}
|
|
{selectedPattern?.name || 'Pattern Details'}
|
|
|
</h2>
|
|
</h2>
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="icon"
|
|
|
|
|
|
|
+ <button
|
|
|
onClick={handleClosePanel}
|
|
onClick={handleClosePanel}
|
|
|
- className="rounded-full text-muted-foreground"
|
|
|
|
|
|
|
+ className="rounded-full w-10 h-10 flex items-center justify-center text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
|
|
>
|
|
>
|
|
|
<span className="material-icons-outlined">close</span>
|
|
<span className="material-icons-outlined">close</span>
|
|
|
- </Button>
|
|
|
|
|
|
|
+ </button>
|
|
|
</header>
|
|
</header>
|
|
|
|
|
|
|
|
{selectedPattern && (
|
|
{selectedPattern && (
|
|
@@ -1209,14 +1131,12 @@ export function BrowsePage() {
|
|
|
<h3 className="text-xl font-semibold">
|
|
<h3 className="text-xl font-semibold">
|
|
|
{selectedPattern?.name || 'Animated Preview'}
|
|
{selectedPattern?.name || 'Animated Preview'}
|
|
|
</h3>
|
|
</h3>
|
|
|
- <Button
|
|
|
|
|
- variant="ghost"
|
|
|
|
|
- size="icon"
|
|
|
|
|
|
|
+ <button
|
|
|
onClick={handleCloseAnimatedPreview}
|
|
onClick={handleCloseAnimatedPreview}
|
|
|
- className="rounded-full"
|
|
|
|
|
|
|
+ className="text-muted-foreground hover:text-foreground transition-colors"
|
|
|
>
|
|
>
|
|
|
<span className="material-icons text-2xl">close</span>
|
|
<span className="material-icons text-2xl">close</span>
|
|
|
- </Button>
|
|
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Modal Content */}
|
|
{/* Modal Content */}
|
|
@@ -1347,8 +1267,8 @@ function PatternCard({ pattern, isSelected, isFavorite, onToggleFavorite, onClic
|
|
|
<button
|
|
<button
|
|
|
ref={cardRef}
|
|
ref={cardRef}
|
|
|
onClick={onClick}
|
|
onClick={onClick}
|
|
|
- className={`group flex flex-col items-center gap-2 p-2 rounded-lg transition-all duration-200 ease-out hover:-translate-y-1 hover:scale-[1.02] hover:shadow-lg hover:bg-accent/30 active:scale-95 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${
|
|
|
|
|
- isSelected ? 'ring-2 ring-primary ring-offset-2 ring-offset-background bg-accent/20' : ''
|
|
|
|
|
|
|
+ className={`group flex flex-col items-center gap-2 p-2 rounded-lg transition-all hover:-translate-y-1 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${
|
|
|
|
|
+ isSelected ? 'ring-2 ring-primary ring-offset-2 ring-offset-background' : ''
|
|
|
}`}
|
|
}`}
|
|
|
>
|
|
>
|
|
|
<div className="relative w-full aspect-square">
|
|
<div className="relative w-full aspect-square">
|