feat(lyrics): configurable sources with drag-to-reorder + feat(audio): ReplayGain pre-gain & fallback

Lyrics Sources:
- Replace lyricsServerFirst + enableNeteaselyrics toggles with a new
  Settings section — three sources (Server, LRCLIB, Netease) each
  individually toggled and drag-reorderable via psy-drop DnD
- useLyrics.ts iterates sources in user-defined order, skipping disabled
  ones; embedded SYLT from local files still wins unconditionally
- onRehydrateStorage migration from legacy fields on first load

ReplayGain Pre-Gain:
- New authStore fields: replayGainPreGainDb (0…+6 dB) and
  replayGainFallbackDb (-6…0 dB, untagged / radio)
- audio.rs compute_gain applies pre_gain_db and fallback_db
- Settings sliders under ReplayGain mode selector
- Radio HTML5 volume scaled by fallbackDb factor on playRadio

i18n(ru): incorporate PR #148 translation improvements (kilyabin)
All 7 locales updated for new keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-10 23:57:36 +02:00
parent 003e7a3203
commit 20dabbfd03
12 changed files with 303 additions and 48 deletions
+15 -14
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { invoke } from '@tauri-apps/api/core';
import { fetchLyrics, parseLrc, LrcLine } from '../api/lrclib';
import { fetchNeteaselyrics } from '../api/netease';
@@ -47,8 +48,7 @@ export interface UseLyricsResult {
export function useLyrics(currentTrack: Track | null): UseLyricsResult {
const cached = currentTrack ? lyricsCache.get(currentTrack.id) : undefined;
const lyricsServerFirst = useAuthStore(s => s.lyricsServerFirst);
const enableNeteaselyrics = useAuthStore(s => s.enableNeteaselyrics);
const lyricsSources = useAuthStore(useShallow(s => s.lyricsSources));
const [loading, setLoading] = useState(!cached && !!currentTrack);
const [syncedLines, setSyncedLines] = useState<LrcLine[] | null>(cached?.syncedLines ?? null);
@@ -159,29 +159,30 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
}
};
const fetchFns: Record<string, () => Promise<boolean>> = {
server: fetchServer,
lrclib: fetchLrclibFn,
netease: fetchNetease,
};
(async () => {
// Embedded lyrics from local file always win (most accurate SYLT data).
if (cancelled) return;
if (await fetchEmbedded()) return;
const [first, second] = lyricsServerFirst
? [fetchServer, fetchLrclibFn]
: [fetchLrclibFn, fetchServer];
if (cancelled) return;
if (await first()) return;
if (cancelled) return;
if (await second()) return;
// Netease as last fallback — only when explicitly enabled
if (enableNeteaselyrics) {
// Try enabled sources in user-defined order.
for (const src of lyricsSources) {
if (!src.enabled) continue;
const fn = fetchFns[src.id];
if (!fn) continue;
if (cancelled) return;
if (await fetchNetease()) return;
if (await fn()) return;
}
if (!cancelled) store({ syncedLines: null, plainLyrics: null, source: null, notFound: true });
})();
return () => { cancelled = true; };
}, [currentTrack?.id, enableNeteaselyrics]); // eslint-disable-line react-hooks/exhaustive-deps
}, [currentTrack?.id, lyricsSources]); // eslint-disable-line react-hooks/exhaustive-deps
return { syncedLines, plainLyrics, source, loading, notFound };
}