mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat: v1.31.0 — AutoEQ, resizable tracklist columns, Discord Listening type, layout fixes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+25
-7
@@ -37,19 +37,22 @@ export const BUILTIN_PRESETS: EqPreset[] = [
|
||||
interface EqState {
|
||||
gains: number[]; // 10 values, -12 to +12 dB
|
||||
enabled: boolean;
|
||||
preGain: number; // pre-amplification in dB (-30 to +6), applied before bands
|
||||
activePreset: string | null;
|
||||
customPresets: EqPreset[];
|
||||
|
||||
setBandGain: (index: number, gain: number) => void;
|
||||
setEnabled: (v: boolean) => void;
|
||||
setPreGain: (v: number) => void;
|
||||
applyPreset: (name: string) => void;
|
||||
applyAutoEq: (name: string, gains: number[], preGain: number) => void;
|
||||
saveCustomPreset: (name: string) => void;
|
||||
deleteCustomPreset: (name: string) => void;
|
||||
syncToRust: () => void;
|
||||
}
|
||||
|
||||
function syncEq(gains: number[], enabled: boolean) {
|
||||
invoke('audio_set_eq', { gains: gains.map(g => g), enabled }).catch(() => {});
|
||||
function syncEq(gains: number[], enabled: boolean, preGain: number) {
|
||||
invoke('audio_set_eq', { gains: gains.map(g => g), enabled, preGain }).catch(() => {});
|
||||
}
|
||||
|
||||
export const useEqStore = create<EqState>()(
|
||||
@@ -57,6 +60,7 @@ export const useEqStore = create<EqState>()(
|
||||
(set, get) => ({
|
||||
gains: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
enabled: false,
|
||||
preGain: 0,
|
||||
activePreset: 'Flat',
|
||||
customPresets: [],
|
||||
|
||||
@@ -65,12 +69,25 @@ export const useEqStore = create<EqState>()(
|
||||
const gains = [...get().gains];
|
||||
gains[index] = clamped;
|
||||
set({ gains, activePreset: null });
|
||||
syncEq(gains, get().enabled);
|
||||
syncEq(gains, get().enabled, get().preGain);
|
||||
},
|
||||
|
||||
setEnabled: (v) => {
|
||||
set({ enabled: v });
|
||||
syncEq(get().gains, v);
|
||||
syncEq(get().gains, v, get().preGain);
|
||||
},
|
||||
|
||||
setPreGain: (v) => {
|
||||
const clamped = Math.max(-30, Math.min(6, v));
|
||||
set({ preGain: clamped, activePreset: null });
|
||||
syncEq(get().gains, get().enabled, clamped);
|
||||
},
|
||||
|
||||
applyAutoEq: (name, gains, preGain) => {
|
||||
const clampedPreGain = Math.max(-30, Math.min(6, preGain));
|
||||
const clampedGains = gains.map(g => Math.max(-12, Math.min(12, g)));
|
||||
set({ gains: clampedGains, preGain: clampedPreGain, activePreset: name });
|
||||
syncEq(clampedGains, get().enabled, clampedPreGain);
|
||||
},
|
||||
|
||||
applyPreset: (name) => {
|
||||
@@ -78,7 +95,7 @@ export const useEqStore = create<EqState>()(
|
||||
const preset = all.find(p => p.name === name);
|
||||
if (!preset) return;
|
||||
set({ gains: [...preset.gains], activePreset: name });
|
||||
syncEq(preset.gains, get().enabled);
|
||||
syncEq(preset.gains, get().enabled, get().preGain);
|
||||
},
|
||||
|
||||
saveCustomPreset: (name) => {
|
||||
@@ -95,8 +112,8 @@ export const useEqStore = create<EqState>()(
|
||||
},
|
||||
|
||||
syncToRust: () => {
|
||||
const { gains, enabled } = get();
|
||||
syncEq(gains, enabled);
|
||||
const { gains, enabled, preGain } = get();
|
||||
syncEq(gains, enabled, preGain);
|
||||
},
|
||||
}),
|
||||
{
|
||||
@@ -105,6 +122,7 @@ export const useEqStore = create<EqState>()(
|
||||
partialize: (s) => ({
|
||||
gains: s.gains,
|
||||
enabled: s.enabled,
|
||||
preGain: s.preGain,
|
||||
activePreset: s.activePreset,
|
||||
customPresets: s.customPresets,
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { showToast } from '../utils/toast';
|
||||
import { buildStreamUrl, buildCoverArtUrl, getPlayQueue, savePlayQueue, reportNowPlaying, scrobbleSong, SubsonicSong, getSong, getRandomSongs, getSimilarSongs2, getTopSongs } from '../api/subsonic';
|
||||
import { buildStreamUrl, buildCoverArtUrl, getPlayQueue, savePlayQueue, reportNowPlaying, scrobbleSong, SubsonicSong, getSong, getRandomSongs, getSimilarSongs2, getTopSongs, InternetRadioStation } from '../api/subsonic';
|
||||
import { lastfmScrobble, lastfmUpdateNowPlaying, lastfmLoveTrack, lastfmUnloveTrack, lastfmGetTrackLoved, lastfmGetAllLovedTracks } from '../api/lastfm';
|
||||
import { useAuthStore } from './authStore';
|
||||
import { useOfflineStore } from './offlineStore';
|
||||
@@ -60,6 +60,7 @@ export function songToTrack(song: SubsonicSong): Track {
|
||||
|
||||
interface PlayerState {
|
||||
currentTrack: Track | null;
|
||||
currentRadio: InternetRadioStation | null;
|
||||
queue: Track[];
|
||||
queueIndex: number;
|
||||
isPlaying: boolean;
|
||||
@@ -73,6 +74,7 @@ interface PlayerState {
|
||||
starredOverrides: Record<string, boolean>;
|
||||
setStarredOverride: (id: string, starred: boolean) => void;
|
||||
|
||||
playRadio: (station: InternetRadioStation) => void;
|
||||
playTrack: (track: Track, queue?: Track[], manual?: boolean) => void;
|
||||
pause: () => void;
|
||||
resume: () => void;
|
||||
@@ -262,6 +264,13 @@ function handleAudioEnded() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Radio stream disconnected — just stop; don't advance queue.
|
||||
if (usePlayerStore.getState().currentRadio) {
|
||||
isAudioPaused = false;
|
||||
usePlayerStore.setState({ isPlaying: false, currentRadio: null, progress: 0, currentTime: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
const { repeatMode, currentTrack, queue } = usePlayerStore.getState();
|
||||
isAudioPaused = false;
|
||||
usePlayerStore.setState({ isPlaying: false, progress: 0, currentTime: 0, buffered: 0 });
|
||||
@@ -481,6 +490,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
currentTrack: null,
|
||||
currentRadio: null,
|
||||
queue: [],
|
||||
queueIndex: 0,
|
||||
isPlaying: false,
|
||||
@@ -572,7 +582,31 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
invoke('audio_stop').catch(console.error);
|
||||
isAudioPaused = false;
|
||||
if (seekDebounce) { clearTimeout(seekDebounce); seekDebounce = null; } seekTarget = null;
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0 });
|
||||
set({ isPlaying: false, progress: 0, buffered: 0, currentTime: 0, currentRadio: null });
|
||||
},
|
||||
|
||||
// ── playRadio ────────────────────────────────────────────────────────────
|
||||
playRadio: (station) => {
|
||||
const { volume } = get();
|
||||
++playGeneration;
|
||||
isAudioPaused = false;
|
||||
gaplessPreloadingId = null;
|
||||
if (seekDebounce) { clearTimeout(seekDebounce); seekDebounce = null; } seekTarget = null;
|
||||
invoke('audio_play_radio', { url: station.streamUrl, volume }).catch((err: unknown) => {
|
||||
console.error('[psysonic] audio_play_radio failed:', err);
|
||||
set({ isPlaying: false, currentRadio: null });
|
||||
});
|
||||
set({
|
||||
currentRadio: station,
|
||||
currentTrack: null,
|
||||
queue: [],
|
||||
queueIndex: 0,
|
||||
isPlaying: true,
|
||||
progress: 0,
|
||||
currentTime: 0,
|
||||
buffered: 0,
|
||||
scrobbled: true, // no scrobbling for radio
|
||||
});
|
||||
},
|
||||
|
||||
// ── playTrack ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -18,6 +18,7 @@ export const DEFAULT_SIDEBAR_ITEMS: SidebarItemConfig[] = [
|
||||
{ id: 'randomMix', visible: true },
|
||||
{ id: 'favorites', visible: true },
|
||||
{ id: 'playlists', visible: true },
|
||||
{ id: 'radio', visible: true },
|
||||
{ id: 'statistics', visible: true },
|
||||
{ id: 'help', visible: true },
|
||||
];
|
||||
@@ -42,6 +43,16 @@ export const useSidebarStore = create<SidebarStore>()(
|
||||
|
||||
reset: () => set({ items: DEFAULT_SIDEBAR_ITEMS }),
|
||||
}),
|
||||
{ name: 'psysonic_sidebar' }
|
||||
{
|
||||
name: 'psysonic_sidebar',
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (!state) return;
|
||||
const known = new Set(state.items.map(i => i.id));
|
||||
const missing = DEFAULT_SIDEBAR_ITEMS.filter(i => !known.has(i.id));
|
||||
if (missing.length > 0) {
|
||||
state.items = [...state.items, ...missing];
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user