From 02278f4cf886ef62267664e64f823550320f65a1 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Tue, 31 Mar 2026 21:03:34 -0700 Subject: [PATCH] feat: shuffle mode, queue-next fix, song switch confirmation, playlist dedup - Add shuffle toggle to mini-player and full-screen player (Fisher-Yates shuffle of upcoming queue items, restores original order on toggle off) - Fix ADD_TO_QUEUE to insert after current track instead of appending to end - Add confirmation dialog ("Switch song, DJ Cutoff?") when clicking a different song while one is playing - Dedup playlist songs by title+artist (keeps first occurrence) - Make artist name clickable in full-screen player (navigates to search) - Music page reads ?q= param to pre-fill search from artist links - Add pre-cache module for upcoming tracks Co-Authored-By: Claude Opus 4.6 --- app/api/music/playlist/[id]/route.ts | 29 +++++++------ app/music/page.tsx | 14 ++++++- components/music/full-screen-player.tsx | 22 +++++++++- components/music/mini-player.tsx | 11 ++++- components/music/music-provider.tsx | 54 +++++++++++++++++++++++-- components/music/search-results.tsx | 35 +++++++++++++++- lib/offline-db.ts | 22 ++++++++++ lib/precache.ts | 44 ++++++++++++++++++++ 8 files changed, 208 insertions(+), 23 deletions(-) create mode 100644 lib/precache.ts diff --git a/app/api/music/playlist/[id]/route.ts b/app/api/music/playlist/[id]/route.ts index 0982ac7..2276fcd 100644 --- a/app/api/music/playlist/[id]/route.ts +++ b/app/api/music/playlist/[id]/route.ts @@ -35,18 +35,23 @@ export async function GET( const pl = data.playlist if (!pl) return NextResponse.json({ error: 'Playlist not found' }, { status: 404 }) - const songs = (pl.entry || []).map((s) => ({ - id: s.id, - title: s.title, - artist: s.artist, - album: s.album, - albumId: s.albumId, - duration: s.duration, - track: s.track, - year: s.year, - coverArt: s.coverArt, - suffix: s.suffix, - })) + // Dedup by title+artist, keeping first occurrence + const seen = new Set() + const songs = (pl.entry || []).reduce>((acc, s) => { + const key = `${s.title.toLowerCase().trim()}|||${s.artist.toLowerCase().trim()}` + if (!seen.has(key)) { + seen.add(key) + acc.push({ + id: s.id, title: s.title, artist: s.artist, album: s.album, + albumId: s.albumId, duration: s.duration, track: s.track, + year: s.year, coverArt: s.coverArt, suffix: s.suffix, + }) + } + return acc + }, []) return NextResponse.json({ id: pl.id, diff --git a/app/music/page.tsx b/app/music/page.tsx index 0dbe702..fec2453 100644 --- a/app/music/page.tsx +++ b/app/music/page.tsx @@ -1,6 +1,7 @@ 'use client' -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef, Suspense } from 'react' +import { useSearchParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { JefflixLogo } from '@/components/jefflix-logo' @@ -33,9 +34,18 @@ interface SlskdFile { } export default function MusicPage() { + return ( + + + + ) +} + +function MusicPageInner() { + const searchParams = useSearchParams() const { state } = useMusicPlayer() const { offlineIds, download: downloadTrack } = useOffline() - const [query, setQuery] = useState('') + const [query, setQuery] = useState(() => searchParams.get('q') || '') const [debouncedQuery, setDebouncedQuery] = useState('') const [songs, setSongs] = useState([]) const [searching, setSearching] = useState(false) diff --git a/components/music/full-screen-player.tsx b/components/music/full-screen-player.tsx index c65c1c7..1f4970f 100644 --- a/components/music/full-screen-player.tsx +++ b/components/music/full-screen-player.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import { useRouter } from 'next/navigation' import { Drawer } from 'vaul' import { useMusicPlayer } from './music-provider' import { DownloadButton } from './download-button' @@ -21,6 +22,7 @@ import { VolumeX, ChevronDown, Speaker, + Shuffle, } from 'lucide-react' function formatTime(secs: number) { @@ -31,7 +33,8 @@ function formatTime(secs: number) { } export function FullScreenPlayer() { - const { state, togglePlay, seek, setVolume, nextTrack, prevTrack, setFullScreen, outputDevices, currentOutputId, setOutputDevice } = useMusicPlayer() + const { state, togglePlay, seek, setVolume, nextTrack, prevTrack, setFullScreen, toggleShuffle, outputDevices, currentOutputId, setOutputDevice } = useMusicPlayer() + const router = useRouter() const [lyrics, setLyrics] = useState(null) const [syncedLyrics, setSyncedLyrics] = useState(null) const [loadingLyrics, setLoadingLyrics] = useState(false) @@ -109,7 +112,15 @@ export function FullScreenPlayer() { {/* Title / Artist */}

{track.title}

-

{track.artist}

+

{track.album}

@@ -130,6 +141,13 @@ export function FullScreenPlayer() { {/* Controls */}
+ diff --git a/components/music/mini-player.tsx b/components/music/mini-player.tsx index d7f0029..11030c3 100644 --- a/components/music/mini-player.tsx +++ b/components/music/mini-player.tsx @@ -5,7 +5,7 @@ import { useMusicPlayer } from './music-provider' import { FullScreenPlayer } from './full-screen-player' import { QueueView } from './queue-view' import { Slider } from '@/components/ui/slider' -import { Play, Pause, SkipBack, SkipForward, ListMusic } from 'lucide-react' +import { Play, Pause, SkipBack, SkipForward, ListMusic, Shuffle } from 'lucide-react' function formatTime(secs: number) { if (!secs || !isFinite(secs)) return '0:00' @@ -15,7 +15,7 @@ function formatTime(secs: number) { } export function MiniPlayer() { - const { state, togglePlay, seek, nextTrack, prevTrack, setFullScreen } = useMusicPlayer() + const { state, togglePlay, seek, nextTrack, prevTrack, setFullScreen, toggleShuffle } = useMusicPlayer() const [queueOpen, setQueueOpen] = useState(false) if (!state.currentTrack) return null @@ -69,6 +69,13 @@ export function MiniPlayer() { {/* Controls */}
+ + +
+ )} + {/* Play button / track number */}