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
|
||||
|
||||
### In-page browse — virtual scroll and cover-art priority
|
||||
|
||||
@@ -23,10 +23,14 @@ export default function LyricsPane({ currentTrack }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
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,
|
||||
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 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'
|
||||
? t('player.lyricsSourceServer')
|
||||
: source === 'lrclib'
|
||||
|
||||
@@ -36,13 +36,13 @@ export function LyricsSourcesCustomizer() {
|
||||
const { t } = useTranslation();
|
||||
const lyricsSources = useAuthStore(useShallow(s => s.lyricsSources));
|
||||
const setLyricsSources = useAuthStore(s => s.setLyricsSources);
|
||||
const lyricsMode = useAuthStore(s => s.lyricsMode);
|
||||
const setLyricsMode = useAuthStore(s => s.setLyricsMode);
|
||||
const youLyPlusEnabled = useAuthStore(s => s.youLyPlusEnabled);
|
||||
const setYouLyPlusEnabled = useAuthStore(s => s.setYouLyPlusEnabled);
|
||||
const lyricsStaticOnly = useAuthStore(s => s.lyricsStaticOnly);
|
||||
const setLyricsStaticOnly = useAuthStore(s => s.setLyricsStaticOnly);
|
||||
const { isDragging: isPsyDragging } = useDragDrop();
|
||||
// useState (not useRef) so the listener-effect re-runs when the container
|
||||
// gets unmounted/remounted by the {lyricsMode === 'standard'} wrapper.
|
||||
// useState (not useRef) so the listener-effect re-binds if the container
|
||||
// element is ever remounted.
|
||||
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
|
||||
const [dropTarget, setDropTarget] = useState<LyricsDropTarget>(null);
|
||||
const dropTargetRef = useRef<LyricsDropTarget>(null);
|
||||
@@ -107,45 +107,32 @@ export function LyricsSourcesCustomizer() {
|
||||
{t('settings.lyricsSourcesDesc')}
|
||||
</p>
|
||||
|
||||
{/* Mode switch — standard three-provider pipeline vs. YouLyPlus karaoke.
|
||||
YouLyPlus misses silently fall back to the standard pipeline. */}
|
||||
{/* YouLyPlus (karaoke) — independent toggle. When on it is tried first and
|
||||
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-toggle-row">
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{t('settings.lyricsModeLyricsplus')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.lyricsModeLyricsplusDesc')}</div>
|
||||
<div style={{ fontWeight: 500 }}>{t('settings.lyricsYouLyPlus')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.lyricsYouLyPlusDesc')}</div>
|
||||
</div>
|
||||
<label className="toggle-switch" aria-label={t('settings.lyricsModeLyricsplus')}>
|
||||
<label className="toggle-switch" aria-label={t('settings.lyricsYouLyPlus')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={lyricsMode === 'lyricsplus'}
|
||||
onChange={e => { if (e.target.checked) setLyricsMode('lyricsplus'); else setLyricsMode('standard'); }}
|
||||
/>
|
||||
<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'); }}
|
||||
checked={youLyPlusEnabled}
|
||||
onChange={e => setYouLyPlusEnabled(e.target.checked)}
|
||||
/>
|
||||
<span className="toggle-track" />
|
||||
</label>
|
||||
</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
|
||||
className="settings-card"
|
||||
style={{ padding: '4px 0', marginBottom: '0.75rem', marginLeft: '1rem' }}
|
||||
style={{ padding: '4px 0', marginBottom: '0.75rem' }}
|
||||
ref={setContainerEl}
|
||||
onMouseMove={handleMouseMove}
|
||||
>
|
||||
@@ -173,7 +160,6 @@ export function LyricsSourcesCustomizer() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Static-only toggle — suppresses line/word tracking in both modes. */}
|
||||
<div className="settings-card" style={{ marginBottom: '0.75rem' }}>
|
||||
|
||||
+23
-8
@@ -71,11 +71,13 @@ export interface UseLyricsResult {
|
||||
}
|
||||
|
||||
export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
||||
const cached = currentTrack ? lyricsCache.get(currentTrack.id) : undefined;
|
||||
const { lyricsSources, lyricsMode } = useAuthStore(useShallow(s => ({
|
||||
const { lyricsSources, youLyPlusEnabled } = useAuthStore(useShallow(s => ({
|
||||
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 [syncedLines, setSyncedLines] = useState<LrcLine[] | null>(cached?.syncedLines ?? null);
|
||||
@@ -87,6 +89,19 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
||||
useEffect(() => {
|
||||
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);
|
||||
if (hit) {
|
||||
setSyncedLines(hit.syncedLines);
|
||||
@@ -200,7 +215,7 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
||||
|
||||
/**
|
||||
* 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> => {
|
||||
try {
|
||||
@@ -258,7 +273,7 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
||||
// Skip for 'lyricsplus' mode since the persisted entry might be from
|
||||
// the standard pipeline (no word-level sync) and the user explicitly
|
||||
// wants a fresh lyricsplus attempt.
|
||||
if (lyricsMode !== 'lyricsplus') {
|
||||
if (!youLyPlusEnabled) {
|
||||
const serverId = useAuthStore.getState().activeServerId ?? '';
|
||||
const persisted = await getCachedLyrics(lyricsCacheKey(serverId, currentTrack.id));
|
||||
if (cancelled) return;
|
||||
@@ -270,8 +285,8 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
||||
}
|
||||
}
|
||||
|
||||
// YouLyPlus mode: try lyricsplus first, silent fallback to standard.
|
||||
if (lyricsMode === 'lyricsplus') {
|
||||
// YouLyPlus on: try lyricsplus first, silent fallback to enabled sources.
|
||||
if (youLyPlusEnabled) {
|
||||
if (cancelled) return;
|
||||
if (await fetchLyricsPlusFn()) return;
|
||||
}
|
||||
@@ -288,7 +303,7 @@ export function useLyrics(currentTrack: Track | null): UseLyricsResult {
|
||||
})();
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export const player = {
|
||||
fsLyricsToggle: 'Lyrics im Vollbild',
|
||||
lyricsLoading: 'Lyrics werden geladen…',
|
||||
lyricsNotFound: 'Keine Lyrics für diesen Titel gefunden',
|
||||
lyricsNoSources: 'Keine Lyric-Quellen ausgewählt — in Einstellungen → Lyrics aktivieren',
|
||||
lyricsSourceServer: 'Quelle: Server',
|
||||
lyricsSourceLrclib: 'Quelle: LRCLIB',
|
||||
lyricsSourceNetease: 'Quelle: Netease',
|
||||
|
||||
@@ -217,10 +217,10 @@ export const settings = {
|
||||
lyricsSourceServer: 'Server',
|
||||
lyricsSourceLrclib: 'LRCLIB',
|
||||
lyricsSourceNetease: 'Netease Cloud Music',
|
||||
lyricsModeStandard: 'Standard',
|
||||
lyricsModeStandardDesc: 'Drei Quellen mit frei wählbarer Reihenfolge: Server-Tags, LRCLIB, Netease.',
|
||||
lyricsModeLyricsplus: 'YouLyPlus (Karaoke)',
|
||||
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.',
|
||||
lyricsYouLyPlus: 'YouLyPlus (Karaoke)',
|
||||
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.',
|
||||
lyricsSourcesFallbackHint: 'Dienen als Fallback, wenn YouLyPlus nichts findet. Zum Sortieren ziehen.',
|
||||
lyricsSourcesPrimaryHint: 'Werden der Reihe nach genutzt. Zum Sortieren ziehen. Alle aus = Lyrics deaktiviert.',
|
||||
lyricsStaticOnly: 'Nur statische Lyrics anzeigen',
|
||||
lyricsStaticOnlyDesc: 'Synchronisierte Lyrics werden ohne Auto-Scroll und ohne Wort-Hervorhebung als statischer Text dargestellt.',
|
||||
downloadsTitle: 'ZIP-Export & Archivierung',
|
||||
|
||||
@@ -51,6 +51,7 @@ export const player = {
|
||||
fsLyricsToggle: 'Lyrics in fullscreen',
|
||||
lyricsLoading: 'Loading lyrics…',
|
||||
lyricsNotFound: 'No lyrics found for this track',
|
||||
lyricsNoSources: 'No lyrics sources selected — enable some in Settings → Lyrics',
|
||||
lyricsSourceServer: 'Source: Server',
|
||||
lyricsSourceLrclib: 'Source: LRCLIB',
|
||||
lyricsSourceNetease: 'Source: Netease',
|
||||
|
||||
@@ -220,10 +220,10 @@ export const settings = {
|
||||
lyricsSourceServer: 'Server',
|
||||
lyricsSourceLrclib: 'LRCLIB',
|
||||
lyricsSourceNetease: 'Netease Cloud Music',
|
||||
lyricsModeStandard: 'Standard',
|
||||
lyricsModeStandardDesc: 'Three sources in a freely orderable list: server tags, LRCLIB, Netease.',
|
||||
lyricsModeLyricsplus: 'YouLyPlus (Karaoke)',
|
||||
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.',
|
||||
lyricsYouLyPlus: 'YouLyPlus (Karaoke)',
|
||||
lyricsYouLyPlusDesc: 'Word-by-word sync from a community backend (Apple Music, Spotify, Musixmatch, QQ). Tried first; the enabled sources below act as fallback.',
|
||||
lyricsSourcesFallbackHint: 'Used as fallback when YouLyPlus finds nothing. Drag to reorder.',
|
||||
lyricsSourcesPrimaryHint: 'Tried in order. Drag to reorder. All off = lyrics disabled.',
|
||||
lyricsStaticOnly: 'Show lyrics as static text only',
|
||||
lyricsStaticOnlyDesc: 'Render synced lyrics without auto-scroll and without word highlighting.',
|
||||
downloadsTitle: 'ZIP Export & Archiving',
|
||||
|
||||
@@ -15,14 +15,14 @@ export function createLyricsSettingsActions(set: SetState): Pick<
|
||||
| 'setLyricsServerFirst'
|
||||
| 'setEnableNeteaselyrics'
|
||||
| 'setLyricsSources'
|
||||
| 'setLyricsMode'
|
||||
| 'setYouLyPlusEnabled'
|
||||
| 'setLyricsStaticOnly'
|
||||
> {
|
||||
return {
|
||||
setLyricsServerFirst: (v) => set({ lyricsServerFirst: v }),
|
||||
setEnableNeteaselyrics: (v) => set({ enableNeteaselyrics: v }),
|
||||
setLyricsSources: (sources) => set({ lyricsSources: sources }),
|
||||
setLyricsMode: (v) => set({ lyricsMode: v }),
|
||||
setYouLyPlusEnabled: (v) => set({ youLyPlusEnabled: v }),
|
||||
setLyricsStaticOnly: (v) => set({ lyricsStaticOnly: v }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -294,9 +294,11 @@ describe('lyrics source setters', () => {
|
||||
expect(useAuthStore.getState().lyricsSources).toEqual(sources);
|
||||
});
|
||||
|
||||
it('setLyricsMode + setFsLyricsStyle + setSidebarLyricsStyle write enum values through', () => {
|
||||
useAuthStore.getState().setLyricsMode('lyricsplus');
|
||||
expect(useAuthStore.getState().lyricsMode).toBe('lyricsplus');
|
||||
it('setYouLyPlusEnabled + setFsLyricsStyle + setSidebarLyricsStyle write values through', () => {
|
||||
useAuthStore.getState().setYouLyPlusEnabled(true);
|
||||
expect(useAuthStore.getState().youLyPlusEnabled).toBe(true);
|
||||
useAuthStore.getState().setYouLyPlusEnabled(false);
|
||||
expect(useAuthStore.getState().youLyPlusEnabled).toBe(false);
|
||||
|
||||
useAuthStore.getState().setFsLyricsStyle('apple');
|
||||
expect(useAuthStore.getState().fsLyricsStyle).toBe('apple');
|
||||
|
||||
@@ -79,7 +79,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
lyricsServerFirst: true,
|
||||
enableNeteaselyrics: false,
|
||||
lyricsSources: DEFAULT_LYRICS_SOURCES,
|
||||
lyricsMode: 'standard',
|
||||
youLyPlusEnabled: false,
|
||||
lyricsStaticOnly: false,
|
||||
showFullscreenLyrics: true,
|
||||
fsLyricsStyle: 'rail',
|
||||
|
||||
@@ -28,9 +28,12 @@ export const DEFAULT_TRACK_PREVIEW_LOCATIONS: TrackPreviewLocations = {
|
||||
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[] = [
|
||||
{ id: 'server', enabled: true },
|
||||
{ id: 'lrclib', enabled: true },
|
||||
{ id: 'server', enabled: false },
|
||||
{ id: 'lrclib', 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).
|
||||
// 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[] } = {};
|
||||
try {
|
||||
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 neteaseOn = (raw?.state?.enableNeteaselyrics as boolean | undefined) ?? false;
|
||||
const migrated: LyricsSourceConfig[] = serverFirst
|
||||
@@ -53,6 +56,16 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
||||
}
|
||||
} 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
|
||||
// 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).
|
||||
@@ -145,6 +158,7 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
||||
loudnessPreIsRefV1: true,
|
||||
...conflictingLegacyState,
|
||||
...lyricsSourcesMigrated,
|
||||
...youLyPlusMigrated,
|
||||
...wheelSmoothOneTime,
|
||||
...seekbarStyleMigrated,
|
||||
...queueDurationDisplayModeMigrated,
|
||||
|
||||
@@ -123,11 +123,12 @@ export interface AuthState {
|
||||
enableNeteaselyrics: boolean;
|
||||
lyricsSources: LyricsSourceConfig[];
|
||||
/**
|
||||
* `'standard'` → server + lrclib + netease pipeline (configurable order).
|
||||
* `'lyricsplus'` → YouLyPlus / lyricsplus first, silent fallback to standard
|
||||
* pipeline when no data is returned.
|
||||
* YouLyPlus (karaoke) as the primary lyrics source. When on, it is tried
|
||||
* first and the enabled `lyricsSources` act as fallback; when off, only the
|
||||
* 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).
|
||||
* Honoured in both lyrics modes.
|
||||
@@ -294,7 +295,7 @@ export interface AuthState {
|
||||
setLyricsServerFirst: (v: boolean) => void;
|
||||
setEnableNeteaselyrics: (v: boolean) => void;
|
||||
setLyricsSources: (sources: LyricsSourceConfig[]) => void;
|
||||
setLyricsMode: (v: 'standard' | 'lyricsplus') => void;
|
||||
setYouLyPlusEnabled: (v: boolean) => void;
|
||||
setLyricsStaticOnly: (v: boolean) => void;
|
||||
setShowFullscreenLyrics: (v: boolean) => void;
|
||||
setFsLyricsStyle: (v: 'rail' | 'apple') => void;
|
||||
|
||||
Reference in New Issue
Block a user