Jelajahi Sumber

fix(ui): replace all Material Icons with Lucide SVG icons in PlaylistsPage

Replaced ~25 icons including:
- Navigation (add, back, close)
- Actions (play, save, edit, delete, shuffle, repeat)
- Content (playlist, folder, music, image)
- Feedback (check, search, sort arrows)

Material Icons font doesn't load reliably on Raspberry Pi.
Lucide React icons are bundled SVGs that work everywhere.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 Minggu lalu
induk
melakukan
4f1db8baad
1 mengubah file dengan 61 tambahan dan 43 penghapusan
  1. 61 43
      frontend/src/pages/PlaylistsPage.tsx

+ 61 - 43
frontend/src/pages/PlaylistsPage.tsx

@@ -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>
   )