Repeat Modes (Canonical Behavior)

Supersedes: REPEAT_MODE_FIX.md and REPEAT_ONE_SKIP_FIX.md (kept for historical context).

Modes: none, all, one.

  • Next/Previous with one: restart current track at 0:00
  • End-of-track with one: restart current track at 0:00
  • With all: advance and loop at end
  • With none: stop at end of queue

Implementation references:

src/lib/stores/playerStore.js#34-61
    nextTrack: () =>
      update((s) => {
        if (!s.currentTrack) return s;
 
        const currentIndex = s.queue.findIndex(
          (t) => t.id === s.currentTrack.id,
        );
 
        if (s.repeatMode === "one") {
          // In repeat-one, "next" means restart current track
          return { ...s, currentTime: 0, isPlaying: true };
        }
 
        if (s.isShuffled) {
          const randomIndex = Math.floor(Math.random() * s.queue.length);
          return { ...s, currentTrack: s.queue[randomIndex], isPlaying: true };
        }
 
        const nextIndex = currentIndex + 1;
        if (nextIndex >= s.queue.length) {
          if (s.repeatMode === "all") {
            return { ...s, currentTrack: s.queue[0], isPlaying: true };
          } else {
            return { ...s, isPlaying: false }; // End of queue
          }
        } else {
          return { ...s, currentTrack: s.queue[nextIndex], isPlaying: true };
        }
      }),
src/lib/stores/playerStore.js#63-77
    prevTrack: () =>
      update((s) => {
        if (!s.currentTrack || s.queue.length === 0) return s;
 
        if (s.repeatMode === "one") {
          // In repeat-one, "previous" means restart current track
          return { ...s, currentTime: 0, isPlaying: true };
        }
 
        const currentIndex = s.queue.findIndex(
          (t) => t.id === s.currentTrack.id,
        );
        const prevIndex = (currentIndex - 1 + s.queue.length) % s.queue.length;
        return { ...s, currentTrack: s.queue[prevIndex], isPlaying: true };
      }),

User-facing logic aligns with standard player UX (Spotify, Apple Music, etc.).