mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(lyrics): make lyrics fully disablable (independent YouLyPlus toggle) (#855)
* feat(lyrics): independent YouLyPlus toggle + all-sources-off state
Replace the binary lyricsMode ('standard' | 'lyricsplus') with an
independent youLyPlusEnabled flag so YouLyPlus and the standard sources
are no longer mutually exclusive — turning one off no longer forces the
other on. YouLyPlus (when on) is tried first with the enabled sources as
fallback; off uses only the enabled sources. When YouLyPlus is off and no
source is enabled, useLyrics fetches nothing (issue #810).
Fresh installs ship with every source off; the rehydrate migration only
restores the old on-by-default set for genuine upgrades, not new installs.
* feat(lyrics): YouLyPlus toggle UI + queue 'no sources' hint
Settings: single YouLyPlus toggle replacing the two mutually exclusive
mode switches; the source list is always visible with a context hint
(fallback vs primary). Queue lyric tab shows a hint when no source is
active. en + de strings; other locales fall back to en.
* docs(changelog): lyrics fully disablable (#855)
This commit is contained in:
committed by
GitHub
parent
cb4d331f99
commit
02b2df1589
@@ -117,6 +117,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Lyrics — sources can be turned off entirely
|
||||||
|
|
||||||
|
**By [@Psychotoxical](https://github.com/Psychotoxical), suggested by sddania, PR [#855](https://github.com/Psychotoxical/psysonic/pull/855)**
|
||||||
|
|
||||||
|
* YouLyPlus is now an independent toggle instead of an either/or with the standard sources, so lyrics can be switched off completely — turn off YouLyPlus and every source under **Settings → Lyrics**. With nothing selected, no lyrics are fetched or shown and the queue lyric tab says so. Fresh installs start with all sources off.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
### In-page browse — virtual scroll and cover-art priority
|
### In-page browse — virtual scroll and cover-art priority
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ export default function LyricsPane({ currentTrack }: Props) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { syncedLines, wordLines, plainLyrics, source, loading, notFound } = useLyrics(currentTrack);
|
const { syncedLines, wordLines, plainLyrics, source, loading, notFound } = useLyrics(currentTrack);
|
||||||
const { staticOnly, sidebarLyricsStyle } = useAuthStore(useShallow(s => ({
|
const { staticOnly, sidebarLyricsStyle, youLyPlusEnabled, lyricsSources } = useAuthStore(useShallow(s => ({
|
||||||
staticOnly: s.lyricsStaticOnly,
|
staticOnly: s.lyricsStaticOnly,
|
||||||
sidebarLyricsStyle: s.sidebarLyricsStyle,
|
sidebarLyricsStyle: s.sidebarLyricsStyle,
|
||||||
|
youLyPlusEnabled: s.youLyPlusEnabled,
|
||||||
|
lyricsSources: s.lyricsSources,
|
||||||
})));
|
})));
|
||||||
|
// Lyrics fully off: YouLyPlus off and no source enabled (issue #810).
|
||||||
|
const lyricsDisabled = !youLyPlusEnabled && !lyricsSources.some(s => s.enabled);
|
||||||
|
|
||||||
const useWords = !staticOnly && wordLines !== null && wordLines.length > 0;
|
const useWords = !staticOnly && wordLines !== null && wordLines.length > 0;
|
||||||
const hasSynced = !staticOnly && !useWords && syncedLines !== null && syncedLines.length > 0;
|
const hasSynced = !staticOnly && !useWords && syncedLines !== null && syncedLines.length > 0;
|
||||||
@@ -146,6 +150,14 @@ export default function LyricsPane({ currentTrack }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lyricsDisabled) {
|
||||||
|
return (
|
||||||
|
<div className="lyrics-pane-empty">
|
||||||
|
<p className="lyrics-status">{t('player.lyricsNoSources')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const sourceLabel = source === 'server'
|
const sourceLabel = source === 'server'
|
||||||
? t('player.lyricsSourceServer')
|
? t('player.lyricsSourceServer')
|
||||||
: source === 'lrclib'
|
: source === 'lrclib'
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ export function LyricsSourcesCustomizer() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const lyricsSources = useAuthStore(useShallow(s => s.lyricsSources));
|
const lyricsSources = useAuthStore(useShallow(s => s.lyricsSources));
|
||||||
const setLyricsSources = useAuthStore(s => s.setLyricsSources);
|
const setLyricsSources = useAuthStore(s => s.setLyricsSources);
|
||||||
const lyricsMode = useAuthStore(s => s.lyricsMode);
|
const youLyPlusEnabled = useAuthStore(s => s.youLyPlusEnabled);
|
||||||
const setLyricsMode = useAuthStore(s => s.setLyricsMode);
|
const setYouLyPlusEnabled = useAuthStore(s => s.setYouLyPlusEnabled);
|
||||||
const lyricsStaticOnly = useAuthStore(s => s.lyricsStaticOnly);
|
const lyricsStaticOnly = useAuthStore(s => s.lyricsStaticOnly);
|
||||||
const setLyricsStaticOnly = useAuthStore(s => s.setLyricsStaticOnly);
|
const setLyricsStaticOnly = useAuthStore(s => s.setLyricsStaticOnly);
|
||||||
const { isDragging: isPsyDragging } = useDragDrop();
|
const { isDragging: isPsyDragging } = useDragDrop();
|
||||||
// useState (not useRef) so the listener-effect re-runs when the container
|
// useState (not useRef) so the listener-effect re-binds if the container
|
||||||
// gets unmounted/remounted by the {lyricsMode === 'standard'} wrapper.
|
// element is ever remounted.
|
||||||
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
|
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
|
||||||
const [dropTarget, setDropTarget] = useState<LyricsDropTarget>(null);
|
const [dropTarget, setDropTarget] = useState<LyricsDropTarget>(null);
|
||||||
const dropTargetRef = useRef<LyricsDropTarget>(null);
|
const dropTargetRef = useRef<LyricsDropTarget>(null);
|
||||||
@@ -107,45 +107,32 @@ export function LyricsSourcesCustomizer() {
|
|||||||
{t('settings.lyricsSourcesDesc')}
|
{t('settings.lyricsSourcesDesc')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Mode switch — standard three-provider pipeline vs. YouLyPlus karaoke.
|
{/* YouLyPlus (karaoke) — independent toggle. When on it is tried first and
|
||||||
YouLyPlus misses silently fall back to the standard pipeline. */}
|
the enabled sources below act as fallback; when off only those sources
|
||||||
|
are used. YouLyPlus off + every source off = lyrics fully disabled. */}
|
||||||
<div className="settings-card" style={{ marginBottom: '0.75rem' }}>
|
<div className="settings-card" style={{ marginBottom: '0.75rem' }}>
|
||||||
<div className="settings-toggle-row">
|
<div className="settings-toggle-row">
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 500 }}>{t('settings.lyricsModeLyricsplus')}</div>
|
<div style={{ fontWeight: 500 }}>{t('settings.lyricsYouLyPlus')}</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.lyricsModeLyricsplusDesc')}</div>
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.lyricsYouLyPlusDesc')}</div>
|
||||||
</div>
|
</div>
|
||||||
<label className="toggle-switch" aria-label={t('settings.lyricsModeLyricsplus')}>
|
<label className="toggle-switch" aria-label={t('settings.lyricsYouLyPlus')}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={lyricsMode === 'lyricsplus'}
|
checked={youLyPlusEnabled}
|
||||||
onChange={e => { if (e.target.checked) setLyricsMode('lyricsplus'); else setLyricsMode('standard'); }}
|
onChange={e => setYouLyPlusEnabled(e.target.checked)}
|
||||||
/>
|
|
||||||
<span className="toggle-track" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="settings-card" style={{ marginBottom: '0.75rem' }}>
|
|
||||||
<div className="settings-toggle-row">
|
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: 500 }}>{t('settings.lyricsModeStandard')}</div>
|
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.lyricsModeStandardDesc')}</div>
|
|
||||||
</div>
|
|
||||||
<label className="toggle-switch" aria-label={t('settings.lyricsModeStandard')}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={lyricsMode === 'standard'}
|
|
||||||
onChange={e => { if (e.target.checked) setLyricsMode('standard'); else setLyricsMode('lyricsplus'); }}
|
|
||||||
/>
|
/>
|
||||||
<span className="toggle-track" />
|
<span className="toggle-track" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{lyricsMode === 'standard' && (
|
<div className="playback-rate-derived" style={{ fontSize: 12, color: 'var(--text-muted)', margin: '0 0 0.4rem' }}>
|
||||||
|
{youLyPlusEnabled ? t('settings.lyricsSourcesFallbackHint') : t('settings.lyricsSourcesPrimaryHint')}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="settings-card"
|
className="settings-card"
|
||||||
style={{ padding: '4px 0', marginBottom: '0.75rem', marginLeft: '1rem' }}
|
style={{ padding: '4px 0', marginBottom: '0.75rem' }}
|
||||||
ref={setContainerEl}
|
ref={setContainerEl}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
>
|
>
|
||||||
@@ -173,7 +160,6 @@ export function LyricsSourcesCustomizer() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Static-only toggle — suppresses line/word tracking in both modes. */}
|
{/* Static-only toggle — suppresses line/word tracking in both modes. */}
|
||||||
<div className="settings-card" style={{ marginBottom: '0.75rem' }}>
|
<div className="settings-card" style={{ marginBottom: '0.75rem' }}>
|
||||||
|
|||||||
+23
-8
@@ -71,11 +71,13 @@ export interface UseLyricsResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
||||||
const cached = currentTrack ? lyricsCache.get(currentTrack.id) : undefined;
|
const { lyricsSources, youLyPlusEnabled } = useAuthStore(useShallow(s => ({
|
||||||
const { lyricsSources, lyricsMode } = useAuthStore(useShallow(s => ({
|
|
||||||
lyricsSources: s.lyricsSources,
|
lyricsSources: s.lyricsSources,
|
||||||
lyricsMode: s.lyricsMode,
|
youLyPlusEnabled: s.youLyPlusEnabled,
|
||||||
})));
|
})));
|
||||||
|
// Lyrics are fully off when YouLyPlus is off and no source is enabled.
|
||||||
|
const lyricsActive = youLyPlusEnabled || lyricsSources.some(s => s.enabled);
|
||||||
|
const cached = (currentTrack && lyricsActive) ? lyricsCache.get(currentTrack.id) : undefined;
|
||||||
|
|
||||||
const [loading, setLoading] = useState(!cached && !!currentTrack);
|
const [loading, setLoading] = useState(!cached && !!currentTrack);
|
||||||
const [syncedLines, setSyncedLines] = useState<LrcLine[] | null>(cached?.syncedLines ?? null);
|
const [syncedLines, setSyncedLines] = useState<LrcLine[] | null>(cached?.syncedLines ?? null);
|
||||||
@@ -87,6 +89,19 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentTrack) return;
|
if (!currentTrack) return;
|
||||||
|
|
||||||
|
// Lyrics fully disabled (YouLyPlus off + every source off): fetch nothing,
|
||||||
|
// show nothing — not even embedded/cache (issue #810). LyricsPane surfaces
|
||||||
|
// the "no sources selected" hint.
|
||||||
|
if (!lyricsActive) {
|
||||||
|
setSyncedLines(null);
|
||||||
|
setWordLines(null);
|
||||||
|
setPlainLyrics(null);
|
||||||
|
setSource(null);
|
||||||
|
setNotFound(false);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hit = lyricsCache.get(currentTrack.id);
|
const hit = lyricsCache.get(currentTrack.id);
|
||||||
if (hit) {
|
if (hit) {
|
||||||
setSyncedLines(hit.syncedLines);
|
setSyncedLines(hit.syncedLines);
|
||||||
@@ -200,7 +215,7 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* lyricsplus (YouLyPlus). Silent miss → caller falls back to the standard
|
* lyricsplus (YouLyPlus). Silent miss → caller falls back to the standard
|
||||||
* pipeline. Only consumed when `lyricsMode === 'lyricsplus'`.
|
* pipeline. Only consumed when `youLyPlusEnabled`.
|
||||||
*/
|
*/
|
||||||
const fetchLyricsPlusFn = async (): Promise<boolean> => {
|
const fetchLyricsPlusFn = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
@@ -258,7 +273,7 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
|||||||
// Skip for 'lyricsplus' mode since the persisted entry might be from
|
// Skip for 'lyricsplus' mode since the persisted entry might be from
|
||||||
// the standard pipeline (no word-level sync) and the user explicitly
|
// the standard pipeline (no word-level sync) and the user explicitly
|
||||||
// wants a fresh lyricsplus attempt.
|
// wants a fresh lyricsplus attempt.
|
||||||
if (lyricsMode !== 'lyricsplus') {
|
if (!youLyPlusEnabled) {
|
||||||
const serverId = useAuthStore.getState().activeServerId ?? '';
|
const serverId = useAuthStore.getState().activeServerId ?? '';
|
||||||
const persisted = await getCachedLyrics(lyricsCacheKey(serverId, currentTrack.id));
|
const persisted = await getCachedLyrics(lyricsCacheKey(serverId, currentTrack.id));
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@@ -270,8 +285,8 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// YouLyPlus mode: try lyricsplus first, silent fallback to standard.
|
// YouLyPlus on: try lyricsplus first, silent fallback to enabled sources.
|
||||||
if (lyricsMode === 'lyricsplus') {
|
if (youLyPlusEnabled) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (await fetchLyricsPlusFn()) return;
|
if (await fetchLyricsPlusFn()) return;
|
||||||
}
|
}
|
||||||
@@ -288,7 +303,7 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [currentTrack?.id, lyricsSources, lyricsMode]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [currentTrack?.id, lyricsSources, youLyPlusEnabled]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return { syncedLines, wordLines, plainLyrics, source, loading, notFound };
|
return { syncedLines, wordLines, plainLyrics, source, loading, notFound };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const player = {
|
|||||||
fsLyricsToggle: 'Lyrics im Vollbild',
|
fsLyricsToggle: 'Lyrics im Vollbild',
|
||||||
lyricsLoading: 'Lyrics werden geladen…',
|
lyricsLoading: 'Lyrics werden geladen…',
|
||||||
lyricsNotFound: 'Keine Lyrics für diesen Titel gefunden',
|
lyricsNotFound: 'Keine Lyrics für diesen Titel gefunden',
|
||||||
|
lyricsNoSources: 'Keine Lyric-Quellen ausgewählt — in Einstellungen → Lyrics aktivieren',
|
||||||
lyricsSourceServer: 'Quelle: Server',
|
lyricsSourceServer: 'Quelle: Server',
|
||||||
lyricsSourceLrclib: 'Quelle: LRCLIB',
|
lyricsSourceLrclib: 'Quelle: LRCLIB',
|
||||||
lyricsSourceNetease: 'Quelle: Netease',
|
lyricsSourceNetease: 'Quelle: Netease',
|
||||||
|
|||||||
@@ -217,10 +217,10 @@ export const settings = {
|
|||||||
lyricsSourceServer: 'Server',
|
lyricsSourceServer: 'Server',
|
||||||
lyricsSourceLrclib: 'LRCLIB',
|
lyricsSourceLrclib: 'LRCLIB',
|
||||||
lyricsSourceNetease: 'Netease Cloud Music',
|
lyricsSourceNetease: 'Netease Cloud Music',
|
||||||
lyricsModeStandard: 'Standard',
|
lyricsYouLyPlus: 'YouLyPlus (Karaoke)',
|
||||||
lyricsModeStandardDesc: 'Drei Quellen mit frei wählbarer Reihenfolge: Server-Tags, LRCLIB, Netease.',
|
lyricsYouLyPlusDesc: 'Wort-für-Wort-Sync über ein Community-Backend (Apple Music, Spotify, Musixmatch, QQ). Wird zuerst versucht; die aktivierten Quellen unten dienen als Fallback.',
|
||||||
lyricsModeLyricsplus: 'YouLyPlus (Karaoke)',
|
lyricsSourcesFallbackHint: 'Dienen als Fallback, wenn YouLyPlus nichts findet. Zum Sortieren ziehen.',
|
||||||
lyricsModeLyricsplusDesc: 'Wort-für-Wort-Sync aus Apple Music, Spotify, Musixmatch & QQ (Community-Backend). Fällt still zurück auf die Standard-Quellen, wenn nichts gefunden wird.',
|
lyricsSourcesPrimaryHint: 'Werden der Reihe nach genutzt. Zum Sortieren ziehen. Alle aus = Lyrics deaktiviert.',
|
||||||
lyricsStaticOnly: 'Nur statische Lyrics anzeigen',
|
lyricsStaticOnly: 'Nur statische Lyrics anzeigen',
|
||||||
lyricsStaticOnlyDesc: 'Synchronisierte Lyrics werden ohne Auto-Scroll und ohne Wort-Hervorhebung als statischer Text dargestellt.',
|
lyricsStaticOnlyDesc: 'Synchronisierte Lyrics werden ohne Auto-Scroll und ohne Wort-Hervorhebung als statischer Text dargestellt.',
|
||||||
downloadsTitle: 'ZIP-Export & Archivierung',
|
downloadsTitle: 'ZIP-Export & Archivierung',
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const player = {
|
|||||||
fsLyricsToggle: 'Lyrics in fullscreen',
|
fsLyricsToggle: 'Lyrics in fullscreen',
|
||||||
lyricsLoading: 'Loading lyrics…',
|
lyricsLoading: 'Loading lyrics…',
|
||||||
lyricsNotFound: 'No lyrics found for this track',
|
lyricsNotFound: 'No lyrics found for this track',
|
||||||
|
lyricsNoSources: 'No lyrics sources selected — enable some in Settings → Lyrics',
|
||||||
lyricsSourceServer: 'Source: Server',
|
lyricsSourceServer: 'Source: Server',
|
||||||
lyricsSourceLrclib: 'Source: LRCLIB',
|
lyricsSourceLrclib: 'Source: LRCLIB',
|
||||||
lyricsSourceNetease: 'Source: Netease',
|
lyricsSourceNetease: 'Source: Netease',
|
||||||
|
|||||||
@@ -220,10 +220,10 @@ export const settings = {
|
|||||||
lyricsSourceServer: 'Server',
|
lyricsSourceServer: 'Server',
|
||||||
lyricsSourceLrclib: 'LRCLIB',
|
lyricsSourceLrclib: 'LRCLIB',
|
||||||
lyricsSourceNetease: 'Netease Cloud Music',
|
lyricsSourceNetease: 'Netease Cloud Music',
|
||||||
lyricsModeStandard: 'Standard',
|
lyricsYouLyPlus: 'YouLyPlus (Karaoke)',
|
||||||
lyricsModeStandardDesc: 'Three sources in a freely orderable list: server tags, LRCLIB, Netease.',
|
lyricsYouLyPlusDesc: 'Word-by-word sync from a community backend (Apple Music, Spotify, Musixmatch, QQ). Tried first; the enabled sources below act as fallback.',
|
||||||
lyricsModeLyricsplus: 'YouLyPlus (Karaoke)',
|
lyricsSourcesFallbackHint: 'Used as fallback when YouLyPlus finds nothing. Drag to reorder.',
|
||||||
lyricsModeLyricsplusDesc: 'Word-by-word sync sourced from Apple Music, Spotify, Musixmatch & QQ (community backend). Silently falls back to the standard sources when nothing is found.',
|
lyricsSourcesPrimaryHint: 'Tried in order. Drag to reorder. All off = lyrics disabled.',
|
||||||
lyricsStaticOnly: 'Show lyrics as static text only',
|
lyricsStaticOnly: 'Show lyrics as static text only',
|
||||||
lyricsStaticOnlyDesc: 'Render synced lyrics without auto-scroll and without word highlighting.',
|
lyricsStaticOnlyDesc: 'Render synced lyrics without auto-scroll and without word highlighting.',
|
||||||
downloadsTitle: 'ZIP Export & Archiving',
|
downloadsTitle: 'ZIP Export & Archiving',
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ export function createLyricsSettingsActions(set: SetState): Pick<
|
|||||||
| 'setLyricsServerFirst'
|
| 'setLyricsServerFirst'
|
||||||
| 'setEnableNeteaselyrics'
|
| 'setEnableNeteaselyrics'
|
||||||
| 'setLyricsSources'
|
| 'setLyricsSources'
|
||||||
| 'setLyricsMode'
|
| 'setYouLyPlusEnabled'
|
||||||
| 'setLyricsStaticOnly'
|
| 'setLyricsStaticOnly'
|
||||||
> {
|
> {
|
||||||
return {
|
return {
|
||||||
setLyricsServerFirst: (v) => set({ lyricsServerFirst: v }),
|
setLyricsServerFirst: (v) => set({ lyricsServerFirst: v }),
|
||||||
setEnableNeteaselyrics: (v) => set({ enableNeteaselyrics: v }),
|
setEnableNeteaselyrics: (v) => set({ enableNeteaselyrics: v }),
|
||||||
setLyricsSources: (sources) => set({ lyricsSources: sources }),
|
setLyricsSources: (sources) => set({ lyricsSources: sources }),
|
||||||
setLyricsMode: (v) => set({ lyricsMode: v }),
|
setYouLyPlusEnabled: (v) => set({ youLyPlusEnabled: v }),
|
||||||
setLyricsStaticOnly: (v) => set({ lyricsStaticOnly: v }),
|
setLyricsStaticOnly: (v) => set({ lyricsStaticOnly: v }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,9 +294,11 @@ describe('lyrics source setters', () => {
|
|||||||
expect(useAuthStore.getState().lyricsSources).toEqual(sources);
|
expect(useAuthStore.getState().lyricsSources).toEqual(sources);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('setLyricsMode + setFsLyricsStyle + setSidebarLyricsStyle write enum values through', () => {
|
it('setYouLyPlusEnabled + setFsLyricsStyle + setSidebarLyricsStyle write values through', () => {
|
||||||
useAuthStore.getState().setLyricsMode('lyricsplus');
|
useAuthStore.getState().setYouLyPlusEnabled(true);
|
||||||
expect(useAuthStore.getState().lyricsMode).toBe('lyricsplus');
|
expect(useAuthStore.getState().youLyPlusEnabled).toBe(true);
|
||||||
|
useAuthStore.getState().setYouLyPlusEnabled(false);
|
||||||
|
expect(useAuthStore.getState().youLyPlusEnabled).toBe(false);
|
||||||
|
|
||||||
useAuthStore.getState().setFsLyricsStyle('apple');
|
useAuthStore.getState().setFsLyricsStyle('apple');
|
||||||
expect(useAuthStore.getState().fsLyricsStyle).toBe('apple');
|
expect(useAuthStore.getState().fsLyricsStyle).toBe('apple');
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
lyricsServerFirst: true,
|
lyricsServerFirst: true,
|
||||||
enableNeteaselyrics: false,
|
enableNeteaselyrics: false,
|
||||||
lyricsSources: DEFAULT_LYRICS_SOURCES,
|
lyricsSources: DEFAULT_LYRICS_SOURCES,
|
||||||
lyricsMode: 'standard',
|
youLyPlusEnabled: false,
|
||||||
lyricsStaticOnly: false,
|
lyricsStaticOnly: false,
|
||||||
showFullscreenLyrics: true,
|
showFullscreenLyrics: true,
|
||||||
fsLyricsStyle: 'rail',
|
fsLyricsStyle: 'rail',
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ export const DEFAULT_TRACK_PREVIEW_LOCATIONS: TrackPreviewLocations = {
|
|||||||
randomMix: true,
|
randomMix: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fresh installs ship with every lyrics source off (issue #810 — users who
|
||||||
|
// don't want lyrics get none until they opt in). Existing users keep their
|
||||||
|
// persisted `lyricsSources`; the rehydrate migration preserves them.
|
||||||
export const DEFAULT_LYRICS_SOURCES: LyricsSourceConfig[] = [
|
export const DEFAULT_LYRICS_SOURCES: LyricsSourceConfig[] = [
|
||||||
{ id: 'server', enabled: true },
|
{ id: 'server', enabled: false },
|
||||||
{ id: 'lrclib', enabled: true },
|
{ id: 'lrclib', enabled: false },
|
||||||
{ id: 'netease', enabled: false },
|
{ id: 'netease', enabled: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -40,3 +40,39 @@ describe('computeAuthStoreRehydration — queueDurationDisplayMode', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('computeAuthStoreRehydration — lyrics', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetAuthStore();
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('migrates legacy lyricsMode "lyricsplus" → youLyPlusEnabled true', () => {
|
||||||
|
const base = useAuthStore.getState();
|
||||||
|
const patch = computeAuthStoreRehydration({ ...base, lyricsMode: 'lyricsplus' } as AuthState);
|
||||||
|
expect(patch.youLyPlusEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('migrates legacy lyricsMode "standard" → youLyPlusEnabled false', () => {
|
||||||
|
const base = useAuthStore.getState();
|
||||||
|
const patch = computeAuthStoreRehydration({ ...base, lyricsMode: 'standard' } as AuthState);
|
||||||
|
expect(patch.youLyPlusEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fresh install (no persisted state) keeps every source off — issue #810', () => {
|
||||||
|
localStorage.removeItem('psysonic-auth');
|
||||||
|
const patch = computeAuthStoreRehydration(useAuthStore.getState());
|
||||||
|
// No migration: the all-off default must survive.
|
||||||
|
expect(patch.lyricsSources).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('upgrade from a build without lyricsSources migrates the old on-by-default set', () => {
|
||||||
|
localStorage.setItem('psysonic-auth', JSON.stringify({ state: { lyricsServerFirst: true } }));
|
||||||
|
const patch = computeAuthStoreRehydration(useAuthStore.getState());
|
||||||
|
expect(patch.lyricsSources).toEqual([
|
||||||
|
{ id: 'server', enabled: true },
|
||||||
|
{ id: 'lrclib', enabled: true },
|
||||||
|
{ id: 'netease', enabled: false },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -40,10 +40,13 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
|||||||
: {};
|
: {};
|
||||||
|
|
||||||
// Migrate lyricsServerFirst + enableNeteaselyrics → lyricsSources (one-time).
|
// Migrate lyricsServerFirst + enableNeteaselyrics → lyricsSources (one-time).
|
||||||
|
// Only for an *existing* persisted state (upgrade from a build without
|
||||||
|
// lyricsSources). Fresh installs have no persisted state → keep the
|
||||||
|
// all-off default (issue #810); don't resurrect the old on-by-default set.
|
||||||
let lyricsSourcesMigrated: { lyricsSources?: LyricsSourceConfig[] } = {};
|
let lyricsSourcesMigrated: { lyricsSources?: LyricsSourceConfig[] } = {};
|
||||||
try {
|
try {
|
||||||
const raw = JSON.parse(localStorage.getItem('psysonic-auth') ?? '{}') as { state?: Record<string, unknown> };
|
const raw = JSON.parse(localStorage.getItem('psysonic-auth') ?? '{}') as { state?: Record<string, unknown> };
|
||||||
if (!raw?.state?.lyricsSources) {
|
if (raw?.state && !raw.state.lyricsSources) {
|
||||||
const serverFirst = (raw?.state?.lyricsServerFirst as boolean | undefined) ?? true;
|
const serverFirst = (raw?.state?.lyricsServerFirst as boolean | undefined) ?? true;
|
||||||
const neteaseOn = (raw?.state?.enableNeteaselyrics as boolean | undefined) ?? false;
|
const neteaseOn = (raw?.state?.enableNeteaselyrics as boolean | undefined) ?? false;
|
||||||
const migrated: LyricsSourceConfig[] = serverFirst
|
const migrated: LyricsSourceConfig[] = serverFirst
|
||||||
@@ -53,6 +56,16 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
|||||||
}
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
// Migrate legacy `lyricsMode` ('standard' | 'lyricsplus') → `youLyPlusEnabled`
|
||||||
|
// (one-time). Existing users keep YouLyPlus on iff they were on lyricsplus
|
||||||
|
// mode; the legacy field is then stripped so it doesn't sit as cruft.
|
||||||
|
let youLyPlusMigrated: { youLyPlusEnabled?: boolean } = {};
|
||||||
|
const legacyLyricsMode = (state as { lyricsMode?: unknown }).lyricsMode;
|
||||||
|
if (legacyLyricsMode === 'lyricsplus' || legacyLyricsMode === 'standard') {
|
||||||
|
youLyPlusMigrated = { youLyPlusEnabled: legacyLyricsMode === 'lyricsplus' };
|
||||||
|
}
|
||||||
|
delete (state as { lyricsMode?: unknown }).lyricsMode;
|
||||||
|
|
||||||
// One-time: older builds could persist smooth=false as the default. Force smooth on once
|
// One-time: older builds could persist smooth=false as the default. Force smooth on once
|
||||||
// so updates do not leave users on discrete scrolling; after this flag exists, only an
|
// so updates do not leave users on discrete scrolling; after this flag exists, only an
|
||||||
// explicit toggle in Settings may turn it off (persisted in psysonic-auth).
|
// explicit toggle in Settings may turn it off (persisted in psysonic-auth).
|
||||||
@@ -145,6 +158,7 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
|||||||
loudnessPreIsRefV1: true,
|
loudnessPreIsRefV1: true,
|
||||||
...conflictingLegacyState,
|
...conflictingLegacyState,
|
||||||
...lyricsSourcesMigrated,
|
...lyricsSourcesMigrated,
|
||||||
|
...youLyPlusMigrated,
|
||||||
...wheelSmoothOneTime,
|
...wheelSmoothOneTime,
|
||||||
...seekbarStyleMigrated,
|
...seekbarStyleMigrated,
|
||||||
...queueDurationDisplayModeMigrated,
|
...queueDurationDisplayModeMigrated,
|
||||||
|
|||||||
@@ -123,11 +123,12 @@ export interface AuthState {
|
|||||||
enableNeteaselyrics: boolean;
|
enableNeteaselyrics: boolean;
|
||||||
lyricsSources: LyricsSourceConfig[];
|
lyricsSources: LyricsSourceConfig[];
|
||||||
/**
|
/**
|
||||||
* `'standard'` → server + lrclib + netease pipeline (configurable order).
|
* YouLyPlus (karaoke) as the primary lyrics source. When on, it is tried
|
||||||
* `'lyricsplus'` → YouLyPlus / lyricsplus first, silent fallback to standard
|
* first and the enabled `lyricsSources` act as fallback; when off, only the
|
||||||
* pipeline when no data is returned.
|
* enabled `lyricsSources` are used. Independent of the source toggles, so all
|
||||||
|
* lyrics can be turned off (YouLyPlus off + every source off).
|
||||||
*/
|
*/
|
||||||
lyricsMode: 'standard' | 'lyricsplus';
|
youLyPlusEnabled: boolean;
|
||||||
/**
|
/**
|
||||||
* Render synced lines as static text (no auto-scroll, no word highlighting).
|
* Render synced lines as static text (no auto-scroll, no word highlighting).
|
||||||
* Honoured in both lyrics modes.
|
* Honoured in both lyrics modes.
|
||||||
@@ -294,7 +295,7 @@ export interface AuthState {
|
|||||||
setLyricsServerFirst: (v: boolean) => void;
|
setLyricsServerFirst: (v: boolean) => void;
|
||||||
setEnableNeteaselyrics: (v: boolean) => void;
|
setEnableNeteaselyrics: (v: boolean) => void;
|
||||||
setLyricsSources: (sources: LyricsSourceConfig[]) => void;
|
setLyricsSources: (sources: LyricsSourceConfig[]) => void;
|
||||||
setLyricsMode: (v: 'standard' | 'lyricsplus') => void;
|
setYouLyPlusEnabled: (v: boolean) => void;
|
||||||
setLyricsStaticOnly: (v: boolean) => void;
|
setLyricsStaticOnly: (v: boolean) => void;
|
||||||
setShowFullscreenLyrics: (v: boolean) => void;
|
setShowFullscreenLyrics: (v: boolean) => void;
|
||||||
setFsLyricsStyle: (v: 'rail' | 'apple') => void;
|
setFsLyricsStyle: (v: 'rail' | 'apple') => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user