feat: v1.28.0 — Infinite Queue, Start Radio, Single-click Play, Performance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-02 17:40:39 +02:00
parent 53d5888ebf
commit 95283d792b
18 changed files with 272 additions and 113 deletions
+4
View File
@@ -33,6 +33,7 @@ interface AuthState {
crossfadeEnabled: boolean;
crossfadeSecs: number;
gaplessEnabled: boolean;
infiniteQueueEnabled: boolean;
minimizeToTray: boolean;
nowPlayingEnabled: boolean;
showChangelogOnUpdate: boolean;
@@ -66,6 +67,7 @@ interface AuthState {
setCrossfadeEnabled: (v: boolean) => void;
setCrossfadeSecs: (v: number) => void;
setGaplessEnabled: (v: boolean) => void;
setInfiniteQueueEnabled: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
setNowPlayingEnabled: (v: boolean) => void;
setShowChangelogOnUpdate: (v: boolean) => void;
@@ -100,6 +102,7 @@ export const useAuthStore = create<AuthState>()(
crossfadeEnabled: false,
crossfadeSecs: 3,
gaplessEnabled: false,
infiniteQueueEnabled: false,
minimizeToTray: false,
nowPlayingEnabled: false,
showChangelogOnUpdate: true,
@@ -166,6 +169,7 @@ export const useAuthStore = create<AuthState>()(
setCrossfadeEnabled: (v) => set({ crossfadeEnabled: v }),
setCrossfadeSecs: (v) => set({ crossfadeSecs: v }),
setGaplessEnabled: (v) => set({ gaplessEnabled: v }),
setInfiniteQueueEnabled: (v) => set({ infiniteQueueEnabled: v }),
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
setShowChangelogOnUpdate: (v) => set({ showChangelogOnUpdate: v }),
+26 -5
View File
@@ -2,7 +2,7 @@ import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { buildStreamUrl, buildCoverArtUrl, getPlayQueue, savePlayQueue, reportNowPlaying, scrobbleSong, SubsonicSong, getSong } from '../api/subsonic';
import { buildStreamUrl, buildCoverArtUrl, getPlayQueue, savePlayQueue, reportNowPlaying, scrobbleSong, SubsonicSong, getSong, getRandomSongs } from '../api/subsonic';
import { lastfmScrobble, lastfmUpdateNowPlaying, lastfmLoveTrack, lastfmUnloveTrack, lastfmGetTrackLoved, lastfmGetAllLovedTracks } from '../api/lastfm';
import { useAuthStore } from './authStore';
import { useOfflineStore } from './offlineStore';
@@ -28,6 +28,7 @@ export interface Track {
genre?: string;
samplingRate?: number;
bitDepth?: number;
autoAdded?: boolean;
}
export function songToTrack(song: SubsonicSong): Track {
@@ -671,16 +672,36 @@ export const usePlayerStore = create<PlayerState>()(
// ── next / previous ──────────────────────────────────────────────────────
next: () => {
const { queue, queueIndex, repeatMode } = get();
const { queue, queueIndex, repeatMode, currentTrack } = get();
const nextIdx = queueIndex + 1;
if (nextIdx < queue.length) {
get().playTrack(queue[nextIdx], queue);
} else if (repeatMode === 'all' && queue.length > 0) {
get().playTrack(queue[0], queue);
} else {
invoke('audio_stop').catch(console.error);
isAudioPaused = false;
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
const { infiniteQueueEnabled } = useAuthStore.getState();
if (infiniteQueueEnabled && repeatMode === 'off') {
getRandomSongs(25, currentTrack?.genre).then(songs => {
if (songs.length === 0) {
invoke('audio_stop').catch(console.error);
isAudioPaused = false;
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
return;
}
const newTracks: Track[] = songs.map(s => ({ ...songToTrack(s), autoAdded: true }));
const currentQueue = get().queue;
const newQueue = [...currentQueue, ...newTracks];
get().playTrack(newTracks[0], newQueue);
}).catch(() => {
invoke('audio_stop').catch(console.error);
isAudioPaused = false;
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
});
} else {
invoke('audio_stop').catch(console.error);
isAudioPaused = false;
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
}
}
},