feat(playlists): integrate Navidrome smart playlist flow into playlists page

Move smart playlist creation and management into Playlists, including Navidrome-only gating, smart-name/icon presentation, and smarter refresh handling while server-side smart rules are being applied.
This commit is contained in:
Maxim Isaev
2026-04-24 16:41:17 +03:00
parent 1c761682f4
commit 27a59f6b23
8 changed files with 785 additions and 15 deletions
+17 -2
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useParams, useNavigate } from 'react-router-dom';
import { ChevronDown, ChevronLeft, Play, ListPlus, Trash2, Search, X, Loader2, Plus, GripVertical, Star, RefreshCw, Shuffle, Heart, HardDriveDownload, Check, Pencil, Globe, Lock, Camera, Download, FileUp, RotateCcw } from 'lucide-react';
import { ChevronDown, ChevronLeft, Play, ListPlus, Trash2, Search, X, Loader2, Plus, GripVertical, Star, RefreshCw, Shuffle, Heart, HardDriveDownload, Check, Pencil, Globe, Lock, Camera, Download, FileUp, RotateCcw, Sparkles } from 'lucide-react';
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
import { AddToPlaylistSubmenu } from '../components/ContextMenu';
import {
@@ -55,6 +55,18 @@ function totalDurationLabel(songs: SubsonicSong[]): string {
return formatHumanHoursMinutes(total);
}
const SMART_PREFIX = 'psy-smart-';
function isSmartPlaylistName(name: string): boolean {
return (name ?? '').toLowerCase().startsWith(SMART_PREFIX);
}
function displayPlaylistName(name: string): string {
const n = name ?? '';
if (isSmartPlaylistName(n)) return n.slice(SMART_PREFIX.length);
return n;
}
function codecLabel(song: SubsonicSong, showBitrate: boolean): string {
const parts: string[] = [];
if (song.suffix) parts.push(song.suffix.toUpperCase());
@@ -1146,7 +1158,10 @@ export default function PlaylistDetail() {
<div className="album-detail-meta">
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<h1 className="album-detail-title" style={{ marginBottom: 0, marginTop: 6 }}>{playlist.name}</h1>
<h1 className="album-detail-title" style={{ marginBottom: 0, marginTop: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
{isSmartPlaylistName(playlist.name) && <Sparkles size={16} style={{ color: 'var(--text-muted)' }} />}
<span>{displayPlaylistName(playlist.name)}</span>
</h1>
<button
className="btn btn-ghost"
onClick={() => setEditingMeta(true)}