|
|
@@ -1,6 +1,32 @@
|
|
|
import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
|
|
import { toast } from 'sonner'
|
|
|
-import { Trash2 } from 'lucide-react'
|
|
|
+import {
|
|
|
+ Trash2,
|
|
|
+ Plus,
|
|
|
+ ListPlus,
|
|
|
+ ListMusic,
|
|
|
+ Pencil,
|
|
|
+ ArrowLeft,
|
|
|
+ Hand,
|
|
|
+ Music,
|
|
|
+ X,
|
|
|
+ Shuffle,
|
|
|
+ Repeat,
|
|
|
+ Minus,
|
|
|
+ ArrowUpDown,
|
|
|
+ ArrowUp,
|
|
|
+ ArrowDown,
|
|
|
+ Loader2,
|
|
|
+ Play,
|
|
|
+ Save,
|
|
|
+ Search,
|
|
|
+ SearchX,
|
|
|
+ Folder,
|
|
|
+ CheckCircle,
|
|
|
+ Check,
|
|
|
+ Image,
|
|
|
+ Sparkles,
|
|
|
+} from 'lucide-react'
|
|
|
import { apiClient } from '@/lib/apiClient'
|
|
|
import {
|
|
|
initPreviewCacheDB,
|
|
|
@@ -551,7 +577,7 @@ export function PlaylistsPage() {
|
|
|
setIsCreateModalOpen(true)
|
|
|
}}
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-xl">add</span>
|
|
|
+ <Plus className="h-5 w-5" />
|
|
|
</Button>
|
|
|
</div>
|
|
|
|
|
|
@@ -562,7 +588,7 @@ export function PlaylistsPage() {
|
|
|
</div>
|
|
|
) : playlists.length === 0 ? (
|
|
|
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground gap-2">
|
|
|
- <span className="material-icons-outlined text-3xl">playlist_add</span>
|
|
|
+ <ListPlus className="h-8 w-8" />
|
|
|
<span className="text-sm">No playlists yet</span>
|
|
|
</div>
|
|
|
) : (
|
|
|
@@ -577,7 +603,7 @@ export function PlaylistsPage() {
|
|
|
onClick={() => handleSelectPlaylist(name)}
|
|
|
>
|
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
|
- <span className="material-icons-outlined text-lg">playlist_play</span>
|
|
|
+ <ListMusic className="h-5 w-5" />
|
|
|
<span className="truncate text-sm font-medium">{name}</span>
|
|
|
</div>
|
|
|
<div className="flex items-center gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
|
|
|
@@ -592,7 +618,7 @@ export function PlaylistsPage() {
|
|
|
setIsRenameModalOpen(true)
|
|
|
}}
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-base">edit</span>
|
|
|
+ <Pencil className="h-4 w-4" />
|
|
|
</Button>
|
|
|
<Button
|
|
|
variant="ghost"
|
|
|
@@ -630,7 +656,7 @@ export function PlaylistsPage() {
|
|
|
className="h-8 w-8 lg:hidden shrink-0"
|
|
|
onClick={handleMobileBack}
|
|
|
>
|
|
|
- <span className="material-icons-outlined">arrow_back</span>
|
|
|
+ <ArrowLeft className="h-5 w-5" />
|
|
|
</Button>
|
|
|
<div className="min-w-0">
|
|
|
<h2 className="text-lg font-semibold truncate">
|
|
|
@@ -649,7 +675,7 @@ export function PlaylistsPage() {
|
|
|
size="sm"
|
|
|
className="gap-2"
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-base">add</span>
|
|
|
+ <Plus className="h-4 w-4" />
|
|
|
<span className="hidden sm:inline">Add Patterns</span>
|
|
|
</Button>
|
|
|
</header>
|
|
|
@@ -659,7 +685,7 @@ export function PlaylistsPage() {
|
|
|
{!selectedPlaylist ? (
|
|
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
|
|
|
<div className="p-4 rounded-full bg-muted">
|
|
|
- <span className="material-icons-outlined text-5xl">touch_app</span>
|
|
|
+ <Hand className="h-12 w-12" />
|
|
|
</div>
|
|
|
<div className="text-center">
|
|
|
<p className="font-medium">No playlist selected</p>
|
|
|
@@ -669,14 +695,14 @@ export function PlaylistsPage() {
|
|
|
) : playlistPatterns.length === 0 ? (
|
|
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
|
|
|
<div className="p-4 rounded-full bg-muted">
|
|
|
- <span className="material-icons-outlined text-5xl">library_music</span>
|
|
|
+ <Music className="h-12 w-12" />
|
|
|
</div>
|
|
|
<div className="text-center">
|
|
|
<p className="font-medium">Empty playlist</p>
|
|
|
<p className="text-sm">Add patterns to get started</p>
|
|
|
</div>
|
|
|
<Button variant="secondary" className="mt-2 gap-2" onClick={openPatternPicker}>
|
|
|
- <span className="material-icons-outlined text-base">add</span>
|
|
|
+ <Plus className="h-4 w-4" />
|
|
|
Add Patterns
|
|
|
</Button>
|
|
|
</div>
|
|
|
@@ -701,7 +727,7 @@ export function PlaylistsPage() {
|
|
|
onClick={() => handleRemovePattern(path)}
|
|
|
title="Remove from playlist"
|
|
|
>
|
|
|
- <span className="material-icons" style={{ fontSize: '12px' }}>close</span>
|
|
|
+ <X className="h-3 w-3" />
|
|
|
</button>
|
|
|
</div>
|
|
|
<p className="text-[10px] sm:text-xs truncate font-medium w-full text-center">{getPatternName(path)}</p>
|
|
|
@@ -732,7 +758,7 @@ export function PlaylistsPage() {
|
|
|
}`}
|
|
|
title="Shuffle"
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-lg sm:text-xl">shuffle</span>
|
|
|
+ <Shuffle className="h-5 w-5 sm:h-6 sm:w-6" />
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={() => setRunMode(runMode === 'indefinite' ? 'single' : 'indefinite')}
|
|
|
@@ -743,7 +769,7 @@ export function PlaylistsPage() {
|
|
|
}`}
|
|
|
title={runMode === 'indefinite' ? 'Loop mode' : 'Play once mode'}
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-lg sm:text-xl">repeat</span>
|
|
|
+ <Repeat className="h-5 w-5 sm:h-6 sm:w-6" />
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
@@ -760,7 +786,7 @@ export function PlaylistsPage() {
|
|
|
setPauseTime(Math.max(0, pauseTime - step))
|
|
|
}}
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-sm">remove</span>
|
|
|
+ <Minus className="h-3 w-3" />
|
|
|
</Button>
|
|
|
<button
|
|
|
onClick={() => {
|
|
|
@@ -772,7 +798,7 @@ export function PlaylistsPage() {
|
|
|
title="Click to change unit"
|
|
|
>
|
|
|
{pauseTime}{pauseUnit === 'sec' ? 's' : pauseUnit === 'min' ? 'm' : 'h'}
|
|
|
- <span className="material-icons-outlined text-xs opacity-50 scale-75 ml-0.5">swap_vert</span>
|
|
|
+ <ArrowUpDown className="h-2.5 w-2.5 opacity-50 ml-0.5" />
|
|
|
</button>
|
|
|
<Button
|
|
|
variant="secondary"
|
|
|
@@ -783,7 +809,7 @@ export function PlaylistsPage() {
|
|
|
setPauseTime(pauseTime + step)
|
|
|
}}
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-sm">add</span>
|
|
|
+ <Plus className="h-3 w-3" />
|
|
|
</Button>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -794,9 +820,9 @@ export function PlaylistsPage() {
|
|
|
<SelectTrigger className={`w-9 h-9 sm:w-10 sm:h-10 rounded-full border-0 p-0 shadow-none focus:ring-0 justify-center [&>svg]:hidden transition ${
|
|
|
clearPattern !== 'none' ? '!bg-primary/10' : '!bg-transparent hover:!bg-muted'
|
|
|
}`}>
|
|
|
- <span className={`material-icons-outlined text-lg sm:text-xl ${
|
|
|
+ <Sparkles className={`h-5 w-5 sm:h-6 sm:w-6 ${
|
|
|
clearPattern !== 'none' ? 'text-primary' : 'text-muted-foreground'
|
|
|
- }`}>cleaning_services</span>
|
|
|
+ }`} />
|
|
|
</SelectTrigger>
|
|
|
<SelectContent>
|
|
|
{preExecutionOptions.map(opt => (
|
|
|
@@ -817,9 +843,9 @@ export function PlaylistsPage() {
|
|
|
title="Run Playlist"
|
|
|
>
|
|
|
{isRunning ? (
|
|
|
- <span className="material-icons-outlined text-xl sm:text-2xl animate-spin">sync</span>
|
|
|
+ <Loader2 className="h-5 w-5 sm:h-6 sm:w-6 animate-spin" />
|
|
|
) : (
|
|
|
- <span className="material-icons text-xl sm:text-2xl ml-0.5">play_arrow</span>
|
|
|
+ <Play className="h-5 w-5 sm:h-6 sm:w-6 ml-0.5" />
|
|
|
)}
|
|
|
</button>
|
|
|
</div>
|
|
|
@@ -833,7 +859,7 @@ export function PlaylistsPage() {
|
|
|
<DialogContent className="sm:max-w-md">
|
|
|
<DialogHeader>
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
- <span className="material-icons-outlined text-primary">playlist_add</span>
|
|
|
+ <ListPlus className="h-5 w-5 text-primary" />
|
|
|
Create New Playlist
|
|
|
</DialogTitle>
|
|
|
</DialogHeader>
|
|
|
@@ -855,7 +881,7 @@ export function PlaylistsPage() {
|
|
|
Cancel
|
|
|
</Button>
|
|
|
<Button onClick={handleCreatePlaylist} className="gap-2">
|
|
|
- <span className="material-icons-outlined text-base">add</span>
|
|
|
+ <Plus className="h-4 w-4" />
|
|
|
Create Playlist
|
|
|
</Button>
|
|
|
</DialogFooter>
|
|
|
@@ -867,7 +893,7 @@ export function PlaylistsPage() {
|
|
|
<DialogContent className="sm:max-w-md">
|
|
|
<DialogHeader>
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
- <span className="material-icons-outlined text-primary">edit</span>
|
|
|
+ <Pencil className="h-5 w-5 text-primary" />
|
|
|
Rename Playlist
|
|
|
</DialogTitle>
|
|
|
</DialogHeader>
|
|
|
@@ -889,7 +915,7 @@ export function PlaylistsPage() {
|
|
|
Cancel
|
|
|
</Button>
|
|
|
<Button onClick={handleRenamePlaylist} className="gap-2">
|
|
|
- <span className="material-icons-outlined text-base">save</span>
|
|
|
+ <Save className="h-4 w-4" />
|
|
|
Save Name
|
|
|
</Button>
|
|
|
</DialogFooter>
|
|
|
@@ -901,7 +927,7 @@ export function PlaylistsPage() {
|
|
|
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
|
|
<DialogHeader>
|
|
|
<DialogTitle className="flex items-center gap-2">
|
|
|
- <span className="material-icons-outlined text-primary">playlist_add</span>
|
|
|
+ <ListPlus className="h-5 w-5 text-primary" />
|
|
|
Add Patterns to {selectedPlaylist}
|
|
|
</DialogTitle>
|
|
|
</DialogHeader>
|
|
|
@@ -909,9 +935,7 @@ export function PlaylistsPage() {
|
|
|
{/* Search and Filters */}
|
|
|
<div className="space-y-3 py-2">
|
|
|
<div className="relative">
|
|
|
- <span className="material-icons-outlined absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground text-lg">
|
|
|
- search
|
|
|
- </span>
|
|
|
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground h-5 w-5" />
|
|
|
<Input
|
|
|
value={searchQuery}
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
@@ -923,7 +947,7 @@ export function PlaylistsPage() {
|
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
|
onClick={() => setSearchQuery('')}
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-lg">close</span>
|
|
|
+ <X className="h-5 w-5" />
|
|
|
</button>
|
|
|
)}
|
|
|
</div>
|
|
|
@@ -932,7 +956,7 @@ export function PlaylistsPage() {
|
|
|
{/* Folder dropdown - icon only on mobile, with text on sm+ */}
|
|
|
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
|
|
<SelectTrigger className="h-9 w-9 sm:w-auto rounded-full bg-card border-border shadow-sm text-sm px-0 sm:px-3 justify-center sm:justify-between [&>svg]:hidden sm:[&>svg]:block [&>span:last-of-type]:hidden sm:[&>span:last-of-type]:inline gap-2">
|
|
|
- <span className="material-icons-outlined text-lg shrink-0">folder</span>
|
|
|
+ <Folder className="h-5 w-5 shrink-0" />
|
|
|
<SelectValue />
|
|
|
</SelectTrigger>
|
|
|
<SelectContent>
|
|
|
@@ -947,7 +971,7 @@ export function PlaylistsPage() {
|
|
|
{/* Sort dropdown - icon only on mobile, with text on sm+ */}
|
|
|
<Select value={sortBy} onValueChange={(v) => setSortBy(v as SortOption)}>
|
|
|
<SelectTrigger className="h-9 w-9 sm:w-auto rounded-full bg-card border-border shadow-sm text-sm px-0 sm:px-3 justify-center sm:justify-between [&>svg]:hidden sm:[&>svg]:block [&>span:last-of-type]:hidden sm:[&>span:last-of-type]:inline gap-2">
|
|
|
- <span className="material-icons-outlined text-lg shrink-0">sort</span>
|
|
|
+ <ArrowUpDown className="h-5 w-5 shrink-0" />
|
|
|
<SelectValue />
|
|
|
</SelectTrigger>
|
|
|
<SelectContent>
|
|
|
@@ -966,16 +990,14 @@ export function PlaylistsPage() {
|
|
|
onClick={() => setSortAsc(!sortAsc)}
|
|
|
title={sortAsc ? 'Ascending' : 'Descending'}
|
|
|
>
|
|
|
- <span className="material-icons-outlined text-lg">
|
|
|
- {sortAsc ? 'arrow_upward' : 'arrow_downward'}
|
|
|
- </span>
|
|
|
+ {sortAsc ? <ArrowUp className="h-5 w-5" /> : <ArrowDown className="h-5 w-5" />}
|
|
|
</Button>
|
|
|
|
|
|
<div className="flex-1" />
|
|
|
|
|
|
{/* Selection count - compact on mobile */}
|
|
|
<div className="flex items-center gap-1 sm:gap-2 text-sm bg-card rounded-full px-2 sm:px-3 py-2 shadow-sm border">
|
|
|
- <span className="material-icons-outlined text-base text-primary">check_circle</span>
|
|
|
+ <CheckCircle className="h-4 w-4 text-primary" />
|
|
|
<span className="font-medium">{selectedPatternPaths.size}</span>
|
|
|
<span className="hidden sm:inline text-muted-foreground">selected</span>
|
|
|
</div>
|
|
|
@@ -987,7 +1009,7 @@ export function PlaylistsPage() {
|
|
|
{filteredPatterns.length === 0 ? (
|
|
|
<div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
|
|
|
<div className="p-4 rounded-full bg-muted">
|
|
|
- <span className="material-icons-outlined text-5xl">search_off</span>
|
|
|
+ <SearchX className="h-12 w-12" />
|
|
|
</div>
|
|
|
<span className="text-sm">No patterns found</span>
|
|
|
</div>
|
|
|
@@ -1016,9 +1038,7 @@ export function PlaylistsPage() {
|
|
|
/>
|
|
|
{isSelected && (
|
|
|
<div className="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-primary flex items-center justify-center shadow-md">
|
|
|
- <span className="material-icons text-primary-foreground" style={{ fontSize: '14px' }}>
|
|
|
- check
|
|
|
- </span>
|
|
|
+ <Check className="h-3.5 w-3.5 text-primary-foreground" />
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
@@ -1037,7 +1057,7 @@ export function PlaylistsPage() {
|
|
|
Cancel
|
|
|
</Button>
|
|
|
<Button onClick={handleSavePatterns} className="gap-2">
|
|
|
- <span className="material-icons-outlined text-base">save</span>
|
|
|
+ <Save className="h-4 w-4" />
|
|
|
Save Selection
|
|
|
</Button>
|
|
|
</DialogFooter>
|
|
|
@@ -1091,9 +1111,7 @@ function LazyPatternPreview({ path, previewUrl, requestPreview, alt, className =
|
|
|
className="w-full h-full object-cover pattern-preview"
|
|
|
/>
|
|
|
) : (
|
|
|
- <span className="material-icons-outlined text-muted-foreground text-sm sm:text-base">
|
|
|
- image
|
|
|
- </span>
|
|
|
+ <Image className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" />
|
|
|
)}
|
|
|
</div>
|
|
|
)
|