diff --git a/modules/lyrics-plus/config.test.mts b/modules/lyrics-plus/config.test.mts index fdf7b3a..296adc1 100644 --- a/modules/lyrics-plus/config.test.mts +++ b/modules/lyrics-plus/config.test.mts @@ -64,6 +64,8 @@ describe("CONFIG", () => { assert.equal(CONFIG.visual.alignment, "center"); assert.equal(CONFIG.visual["background-color"], "var(--spice-main)"); assert.equal(CONFIG.visual["playbar-button"], false); + assert.equal(CONFIG.visual["animated-background"], false); + assert.equal(CONFIG.visual["inactive-blur"], false); // Numeric coercion runs after the literal. assert.equal(CONFIG.visual["font-size"], 32); assert.equal(CONFIG.visual["lines-before"], 0); @@ -89,6 +91,7 @@ describe("CONFIG", () => { it("rejects duplicate and non-string provider keys from stored JSON", async () => { for (const order of [ + ["local", "lrclib", "lyricsovh", "musixmatch", "netease"], ["lrclib", "lrclib", "musixmatch", "spotify", "local"], ["lrclib", "netease", "musixmatch", "spotify", {}], ]) { @@ -147,6 +150,22 @@ describe("CONFIG", () => { it("exposes only supported providers in configuration", async () => { const { CONFIG } = await import(`./config.ts?providers=${Date.now()}`); - assert.deepEqual(Object.keys(CONFIG.providers).sort(), ["local", "lrclib", "musixmatch", "netease", "spotify"]); + assert.deepEqual(Object.keys(CONFIG.providers).sort(), [ + "local", + "lrclib", + "lyricsovh", + "musixmatch", + "netease", + "spotify", + ]); + }); + + it("appends Lyrics.ovh without resetting the user's existing provider priority", async () => { + const order = ["local", "spotify", "musixmatch", "netease", "lrclib"]; + localStorage.setItem("lyrics-plus:services-order", JSON.stringify(order)); + const { CONFIG, UNSYNCED } = await import(`./config.ts?new-provider=${Date.now()}`); + assert.deepEqual(CONFIG.providersOrder, [...order, "lyricsovh"]); + assert.deepEqual(CONFIG.providers.lyricsovh.modes, [UNSYNCED]); + assert.deepEqual(JSON.parse(localStorage.getItem("lyrics-plus:services-order")!), CONFIG.providersOrder); }); }); diff --git a/modules/lyrics-plus/config.ts b/modules/lyrics-plus/config.ts index 05adf71..501c06b 100644 --- a/modules/lyrics-plus/config.ts +++ b/modules/lyrics-plus/config.ts @@ -75,6 +75,11 @@ const providers = { desc: "Provide lyrics from cache/local files loaded from previous Spotify sessions.", modes: [KARAOKE, SYNCED, UNSYNCED], }, + lyricsovh: { + on: getConfig("lyrics-plus:provider:lyricsovh:on"), + desc: "Unsynced lyrics from lyrics.ovh. Used as a fallback when earlier providers have no lyrics.", + modes: [UNSYNCED], + }, }; export type ProviderKey = keyof typeof providers; @@ -85,6 +90,8 @@ export const CONFIG = { visual: { "playbar-button": getConfig("lyrics-plus:visual:playbar-button", false), colorful: getConfig("lyrics-plus:visual:colorful"), + "animated-background": getConfig("lyrics-plus:visual:animated-background", false), + "inactive-blur": getConfig("lyrics-plus:visual:inactive-blur", false), noise: getConfig("lyrics-plus:visual:noise"), "background-color": localStorage.getItem("lyrics-plus:visual:background-color") || "var(--spice-main)", "active-color": localStorage.getItem("lyrics-plus:visual:active-color") || "var(--spice-text)", @@ -134,13 +141,20 @@ try { const storedOrder: unknown = JSON.parse(localStorage.getItem("lyrics-plus:services-order") ?? "null"); if ( !Array.isArray(storedOrder) || - storedOrder.length !== providerKeys.length || - new Set(storedOrder).size !== providerKeys.length || + !( + storedOrder.length === providerKeys.length || + (storedOrder.length === providerKeys.length - 1 && !storedOrder.includes("lyricsovh")) + ) || + new Set(storedOrder).size !== storedOrder.length || !storedOrder.every(isProviderKey) ) { throw new Error("Invalid stored provider order"); } - CONFIG.providersOrder = storedOrder; + const missingProviders = providerKeys.filter((key) => !storedOrder.includes(key)); + CONFIG.providersOrder = [...storedOrder, ...missingProviders]; + if (missingProviders.length) { + localStorage.setItem("lyrics-plus:services-order", JSON.stringify(CONFIG.providersOrder)); + } } catch { CONFIG.providersOrder = providerKeys; localStorage.setItem("lyrics-plus:services-order", JSON.stringify(CONFIG.providersOrder)); diff --git a/modules/lyrics-plus/container-requests.test.mts b/modules/lyrics-plus/container-requests.test.mts index 36329f5..daf2587 100644 --- a/modules/lyrics-plus/container-requests.test.mts +++ b/modules/lyrics-plus/container-requests.test.mts @@ -25,7 +25,14 @@ const components: Record = { "./options-menu.tsx": ["AdjustmentsMenu", "TranslationMenu"], "./tab-bar.tsx": ["TopBarContent"], "./settings.tsx": ["LyricsPlusSettings", "openLyricsPlusAppearanceSettings"], - "./pages.tsx": ["GeniusPage", "LoadingIcon", "SyncedExpandedLyricsPage", "SyncedLyricsPage", "UnsyncedLyricsPage"], + "./pages.tsx": [ + "GeniusPage", + "LoadingIcon", + "LyricsBackground", + "SyncedExpandedLyricsPage", + "SyncedLyricsPage", + "UnsyncedLyricsPage", + ], "/modules/stdlib/lib/primitives.js": ["SettingsSection", "Tooltip"], }; const hooks = registerHooks({ diff --git a/modules/lyrics-plus/index.scss b/modules/lyrics-plus/index.scss index 538223d..22f8f33 100644 --- a/modules/lyrics-plus/index.scss +++ b/modules/lyrics-plus/index.scss @@ -64,6 +64,7 @@ .lyrics-lyricsContainer-LyricsContainer { display: grid; + isolation: isolate; grid-template-rows: 1fr; position: absolute; height: 100%; @@ -116,12 +117,82 @@ } .lyrics-lyricsContainer-LyricsBackground { + position: relative; + z-index: -1; + overflow: hidden; + pointer-events: none; background-color: var(--lyrics-color-background); - background-image: var(--lyrics-background-noise); grid-area: 1 / 1 / -1 / -1; transition: background-color 0.25s ease-out; } +.lyrics-lyricsContainer-LyricsBackground::after { + content: ""; + position: absolute; + inset: 0; + background-image: var(--lyrics-background-noise); +} + +.lyrics-album-art { + position: absolute; + inset: 0; + overflow: hidden; + filter: brightness(0.5) saturate(1.3); +} + +.lyrics-album-art img { + position: absolute; + width: 150%; + height: 150%; + top: -25%; + left: -25%; + object-fit: cover; + filter: blur(48px); + animation: lyrics-album-drift 40s ease-in-out infinite alternate; +} + +.lyrics-album-art img:nth-child(2) { + opacity: 0.5; + animation-direction: alternate-reverse; + animation-duration: 55s; +} + +@keyframes lyrics-album-drift { + from { + transform: rotate(-15deg) scale(1); + } + to { + transform: rotate(15deg) scale(1.2); + } +} + +.lyrics-lyricsContainer-LyricsContainer.inactive-blur-enabled + .lyrics-expanded-synced + .lyrics-lyricsContainer-LyricsLine { + filter: blur(calc(var(--blur-index, 0) * 1px)); + transition: + filter 0.2s ease-out, + color 0.25s ease-out; +} + +.lyrics-lyricsContainer-LyricsContainer.inactive-blur-enabled + .lyrics-expanded-synced + .lyrics-lyricsContainer-LyricsLine:hover { + filter: none; +} + +@media (prefers-reduced-motion: reduce) { + .lyrics-album-art img { + animation: none; + } + + .lyrics-lyricsContainer-LyricsContainer.inactive-blur-enabled + .lyrics-expanded-synced + .lyrics-lyricsContainer-LyricsLine { + transition: none; + } +} + .lyrics-lyricsContainer-Provider { align-self: end; color: var(--lyrics-color-inactive); @@ -198,7 +269,7 @@ .lyrics-lyricsContainer-UnsyncedLyricsPage .lyrics-lyricsContainer-LyricsLine.lyrics-lyricsContainer-LyricsLine-past { color: var(--lyrics-color-active); opacity: 1; - filter: none !important; + filter: none; } .lyrics-lyricsContainer-LyricsLine, diff --git a/modules/lyrics-plus/metadata.json b/modules/lyrics-plus/metadata.json index 49dea12..ec47d7e 100644 --- a/modules/lyrics-plus/metadata.json +++ b/modules/lyrics-plus/metadata.json @@ -1,7 +1,7 @@ { "name": "lyrics-plus", "kind": "app", - "version": "0.2.6", + "version": "0.3.0", "authors": ["spicetify"], "description": "Full-featured lyrics: synced, karaoke, unsynced and translated, from Musixmatch, Spotify, LRCLIB and Netease.", "entries": { diff --git a/modules/lyrics-plus/mod.tsx b/modules/lyrics-plus/mod.tsx index 92e3abb..e3f3dcf 100644 --- a/modules/lyrics-plus/mod.tsx +++ b/modules/lyrics-plus/mod.tsx @@ -56,7 +56,14 @@ import { TopBarContent } from "./tab-bar.tsx"; import { LyricsPlusSettings, openLyricsPlusAppearanceSettings } from "./settings.tsx"; import { lyricsReplacementReady, mountLyricsPlaybarStyleWhenReady, watchLyricsHistory } from "./playbar-lifecycle.ts"; import type { LyricsHistory } from "./playbar-lifecycle.ts"; -import { GeniusPage, LoadingIcon, SyncedExpandedLyricsPage, SyncedLyricsPage, UnsyncedLyricsPage } from "./pages.tsx"; +import { + GeniusPage, + LoadingIcon, + LyricsBackground, + SyncedExpandedLyricsPage, + SyncedLyricsPage, + UnsyncedLyricsPage, +} from "./pages.tsx"; import { ProviderMusixmatch } from "./providers/musixmatch.ts"; import { configureLyricsClient, getLyricsResponse, requestLyrics } from "./runtime-client.ts"; @@ -76,6 +83,7 @@ const ICON = interface LyricsState extends CachedLyrics { currentLyrics: DisplayLyricLine[] | null; colors: { background: string; inactive: string }; + image: string; tempo: string; explicitMode: number; lockMode: number; @@ -348,6 +356,7 @@ export class LyricsContainer extends react.Component { background: "", inactive: "", }, + image: "", tempo: "0.25s", explicitMode: -1, lockMode: CONFIG.locked, @@ -646,9 +655,11 @@ export class LyricsContainer extends react.Component { const generation = ++this.requestGeneration; const info = this.infoFromTrack(track); if (!info) { - this.setState({ error: "No track info" }); + this.setState({ error: "No track info", image: "" }); return; } + const image = info.image ?? ""; + this.setState((state) => (state.image === image ? null : { image })); if (mode === -1) mode = this.props.queries.preferredMode(info.uri) ?? -1; this.state.explicitMode = mode; @@ -1285,6 +1296,15 @@ export class LyricsContainer extends react.Component { "--lyrics-background-noise": CONFIG.visual.noise ? "var(--background-noise)" : "unset", }; } + if (CONFIG.visual["animated-background"] && !this.state.isFADMode) { + this.styleVariables = { + ...this.styleVariables, + "--lyrics-color-active": "white", + "--lyrics-color-inactive": "rgba(255,255,255,0.65)", + "--lyrics-color-background": "#161616", + "--lyrics-highlight-background": "rgba(255,255,255,0.2)", + }; + } this.styleVariables = { ...this.styleVariables, @@ -1400,7 +1420,7 @@ export class LyricsContainer extends react.Component { const out = react.createElement( "div", { - className: `lyrics-lyricsContainer-LyricsContainer${CONFIG.visual["fade-blur"] ? " blur-enabled" : ""}${ + className: `lyrics-lyricsContainer-LyricsContainer${CONFIG.visual["fade-blur"] ? " blur-enabled" : ""}${CONFIG.visual["inactive-blur"] ? " inactive-blur-enabled" : ""}${ fadLyricsContainer ? " fad-enabled" : "" }`, style: this.styleVariables, @@ -1409,8 +1429,8 @@ export class LyricsContainer extends react.Component { el.onwheel = this.onFontSizeChange; }, }, - react.createElement("div", { - className: "lyrics-lyricsContainer-LyricsBackground", + react.createElement(LyricsBackground, { + image: CONFIG.visual["animated-background"] && !fadLyricsContainer ? this.state.image : "", }), react.createElement( "div", diff --git a/modules/lyrics-plus/pages.test.mts b/modules/lyrics-plus/pages.test.mts index fc47d5f..e61d830 100644 --- a/modules/lyrics-plus/pages.test.mts +++ b/modules/lyrics-plus/pages.test.mts @@ -48,7 +48,8 @@ const hooks = registerHooks({ return nextLoad(url, context); }, }); -const { SearchBar, SyncedLyricsPage, SyncedExpandedLyricsPage, VersionSelector } = await import("./pages.tsx"); +const { LyricsBackground, SearchBar, SyncedLyricsPage, SyncedExpandedLyricsPage, VersionSelector } = + await import("./pages.tsx"); hooks.deregister(); const roots: Root[] = []; @@ -75,6 +76,39 @@ it("renders both synchronized views while lyrics are empty", async () => { assert.equal(document.querySelectorAll(".lyrics-idling-indicator").length, 2); }); +it("uses Spotify artwork for the backdrop and falls back if it fails to load", async () => { + const container = await render(React.createElement(LyricsBackground, { image: "spotify:image:abc123" })); + const image = container.querySelector("img"); + assert.ok(image); + assert.equal(image.src, "https://i.scdn.co/image/abc123"); + assert.equal(image.alt, ""); + assert.equal(container.firstElementChild?.getAttribute("aria-hidden"), "true"); + await React.act(async () => image.dispatchEvent(new Event("error"))); + assert.equal(container.querySelectorAll("img").length, 0); +}); + +it("keeps a solid backdrop when there is no artwork", async () => { + const container = await render(React.createElement(LyricsBackground, { image: "" })); + assert.equal(container.querySelectorAll("img").length, 0); +}); + +it("keeps active scrolling lyrics sharp and caps blur on distant lines", async () => { + playback.position = 1000; + const container = await render( + React.createElement(SyncedExpandedLyricsPage, { + lyrics: Array.from({ length: 8 }, (_, i) => ({ text: `Line ${i}`, startTime: i * 1000 })), + }), + ); + const active = container.querySelector( + ".lyrics-lyricsContainer-LyricsLine-active:not(.lyrics-idling-indicator)", + ); + assert.equal(active?.style.getPropertyValue("--blur-index"), "0"); + const distant = [...container.querySelectorAll(".lyrics-lyricsContainer-LyricsLine")].find( + (line) => line.textContent === "Line 7", + ); + assert.equal(distant?.style.getPropertyValue("--blur-index"), "4"); +}); + it("selects Genius versions with a numeric index", async () => { const selections: number[] = []; const versions = [ diff --git a/modules/lyrics-plus/pages.tsx b/modules/lyrics-plus/pages.tsx index becea06..54e5990 100644 --- a/modules/lyrics-plus/pages.tsx +++ b/modules/lyrics-plus/pages.tsx @@ -15,6 +15,32 @@ import { convertParsedToLRC, convertParsedToUnsynced, lyricText, isKaraokeWords const { useState, useEffect, useMemo, useRef } = react; +export function LyricsBackground({ image }: { image: string }) { + const [failedImage, setFailedImage] = useState(null); + const source = image.startsWith("spotify:image:") + ? `https://i.scdn.co/image/${image.slice("spotify:image:".length)}` + : image; + return react.createElement( + "div", + { className: "lyrics-lyricsContainer-LyricsBackground", "aria-hidden": true }, + source && + failedImage !== image && + react.createElement( + "div", + { className: "lyrics-album-art" }, + [0, 1].map((layer) => + react.createElement("img", { + key: layer, + src: source, + alt: "", + draggable: false, + onError: () => setFailedImage(image), + }), + ), + ), + ); +} + interface CreditProps { reRenderLyricsPage?: boolean; provider?: string | null; @@ -676,7 +702,7 @@ export const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyrigh return react.createElement( "div", { - className: "lyrics-lyricsContainer-UnsyncedLyricsPage", + className: "lyrics-lyricsContainer-UnsyncedLyricsPage lyrics-expanded-synced", key: lyricsId, ref: pageRef, }, @@ -730,6 +756,7 @@ export const SyncedExpandedLyricsPage = react.memo(({ lyrics, provider, copyrigh key: i, style: { cursor: "pointer", + "--blur-index": isActive ? 0 : Math.min(Math.abs(i - activeLineIndex), 4), }, dir: "auto", ref: isFocused ? activeLineRef : null, diff --git a/modules/lyrics-plus/providers/index.ts b/modules/lyrics-plus/providers/index.ts index 1523746..2e1c91c 100644 --- a/modules/lyrics-plus/providers/index.ts +++ b/modules/lyrics-plus/providers/index.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -// The six-entry provider registry: four delegate to the provider files, +// The provider registry delegates to the provider files; // spotify and local are implemented inline. Client-bound policy arrives as // injected deps — the caller supplies the PLAYING track's duration (KTD5a) // and the Han-simplification helper (KTD6) — so this file imports clean @@ -16,6 +16,7 @@ import { getLyricsResponse } from "../runtime-client.ts"; import { processLyrics } from "../utils.ts"; import { ProviderGenius } from "./genius.ts"; import { ProviderLRCLIB } from "./lrclib.ts"; +import { lyricsOvh } from "./lyricsovh.ts"; import { ProviderMusixmatch } from "./musixmatch.ts"; import { ProviderNetease } from "./netease.ts"; @@ -162,6 +163,7 @@ export interface ProviderDeps { export function createProviders(deps: ProviderDeps) { return { + lyricsovh: lyricsOvh, spotify: async (info: Pick, signal?: AbortSignal): Promise => { const result: ProviderResult = { uri: info.uri, diff --git a/modules/lyrics-plus/providers/lyricsovh.test.mts b/modules/lyrics-plus/providers/lyricsovh.test.mts new file mode 100644 index 0000000..cb68762 --- /dev/null +++ b/modules/lyrics-plus/providers/lyricsovh.test.mts @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2026 spicetify + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "../../stdlib/lib/test-setup.mts"; +import assert from "node:assert/strict"; +import { it } from "node:test"; +import { createProviders } from "./index.ts"; + +const providers = createProviders({ + trackDurationMs: () => 180000, + simplifyChinese: async (value) => value, + spicetifyVersion: () => undefined, +}); +const track = { + uri: "spotify:track:test", + title: "Song / One?", + artist: "Artist & Two", + album: "Album", + duration: 180000, +}; + +it("loads plain lyrics with encoded artist/title and credits Lyrics.ovh", async (t) => { + t.mock.method(globalThis, "fetch", async (input: string, init: RequestInit) => { + assert.equal(input, "https://api.lyrics.ovh/v1/Artist%20%26%20Two/Song%20%2F%20One%3F"); + assert.ok(init.signal instanceof AbortSignal); + return Response.json({ lyrics: "First line\r\n\r\nSecond line" }); + }); + const result = await providers.lyricsovh(track); + assert.equal(result.uri, track.uri); + assert.equal(result.provider, "Lyrics.ovh"); + assert.equal(result.synced, null); + assert.equal(result.karaoke, null); + assert.deepEqual(result.unsynced, [{ text: "First line" }, { text: "" }, { text: "Second line" }]); +}); + +it("returns no lyrics for HTTP errors, invalid JSON, malformed and empty responses", async (t) => { + for (const response of [ + Response.json({ error: "No lyrics found" }, { status: 404 }), + new Response("unavailable", { status: 503 }), + new Response("not json"), + Response.json(null), + Response.json({ lyrics: 123 }), + Response.json({ lyrics: " \n " }), + ]) { + const mock = t.mock.method(globalThis, "fetch", async () => response); + const result = await providers.lyricsovh(track); + assert.equal(result.unsynced, null); + assert.equal(result.error, "No lyrics"); + mock.mock.restore(); + } +}); + +it("does not start a request after cancellation", async (t) => { + const fetch = t.mock.method(globalThis, "fetch", async () => Response.json({ lyrics: "Late lyrics" })); + const result = await providers.lyricsovh(track, AbortSignal.abort()); + assert.equal(fetch.mock.callCount(), 0); + assert.equal(result.unsynced, null); +}); + +it("returns no lyrics when a provider request fails", async (t) => { + t.mock.method(globalThis, "fetch", async () => { + throw new TypeError("Network error"); + }); + assert.equal((await providers.lyricsovh(track)).error, "No lyrics"); +}); + +it("retries a missing edition title with the existing title-cleanup helper", async (t) => { + const calls: string[] = []; + t.mock.method(globalThis, "fetch", async (url: string) => { + calls.push(url); + return calls.length === 1 + ? Response.json({ error: "No lyrics found" }, { status: 404 }) + : Response.json({ lyrics: "Found the original song" }); + }); + const result = await providers.lyricsovh({ ...track, title: "Let It Be - Remastered 2009" }); + assert.deepEqual(calls, [ + "https://api.lyrics.ovh/v1/Artist%20%26%20Two/Let%20It%20Be%20-%20Remastered%202009", + "https://api.lyrics.ovh/v1/Artist%20%26%20Two/Let%20It%20Be", + ]); + assert.deepEqual(result.unsynced, [{ text: "Found the original song" }]); +}); + +it("does not retry edition titles on server errors or after cancellation", async (t) => { + for (const cancel of [false, true]) { + const controller = new AbortController(); + const fetch = t.mock.method(globalThis, "fetch", async () => { + if (cancel) controller.abort(); + return Response.json({}, { status: cancel ? 404 : 503 }); + }); + await providers.lyricsovh({ ...track, title: "Let It Be - Remastered 2009" }, controller.signal); + assert.equal(fetch.mock.callCount(), 1); + fetch.mock.restore(); + } +}); diff --git a/modules/lyrics-plus/providers/lyricsovh.ts b/modules/lyrics-plus/providers/lyricsovh.ts new file mode 100644 index 0000000..7763c83 --- /dev/null +++ b/modules/lyrics-plus/providers/lyricsovh.ts @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2026 spicetify + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import type { ProviderResult, TrackInfo } from "../types.ts"; +import { requestLyrics } from "../runtime-client.ts"; +import { removeExtraInfo } from "../utils.ts"; + +export async function lyricsOvh(info: TrackInfo, signal?: AbortSignal): Promise { + const result: ProviderResult = { + uri: info.uri, + provider: "Lyrics.ovh", + karaoke: null, + synced: null, + unsynced: null, + copyright: null, + }; + try { + const body: unknown = await requestLyrics(async (requestSignal) => { + const base = `https://api.lyrics.ovh/v1/${encodeURIComponent(info.artist)}/`; + let response = await fetch(`${base}${encodeURIComponent(info.title)}`, { signal: requestSignal }); + const title = removeExtraInfo(info.title).trim(); + if (response.status === 404 && title && title !== info.title) { + requestSignal.throwIfAborted(); + response = await fetch(`${base}${encodeURIComponent(title)}`, { signal: requestSignal }); + } + return response.ok ? response.json() : null; + }, signal); + if ( + body && + typeof body === "object" && + "lyrics" in body && + typeof body.lyrics === "string" && + body.lyrics.trim() + ) { + result.unsynced = body.lyrics + .trim() + .split(/\r?\n/) + .map((text) => ({ text })); + return result; + } + } catch { + // A missing or unavailable provider must leave the other sources usable. + } + return { ...result, error: "No lyrics" }; +} diff --git a/modules/lyrics-plus/providers/providers.test.mts b/modules/lyrics-plus/providers/providers.test.mts index 3d888fb..a320fa4 100644 --- a/modules/lyrics-plus/providers/providers.test.mts +++ b/modules/lyrics-plus/providers/providers.test.mts @@ -499,11 +499,12 @@ describe("createProviders registry", () => { spicetifyVersion: () => "3.2.0", }); - it("exposes exactly the six entries, all callable", () => { + it("exposes all supported entries, all callable", () => { assert.deepEqual(Object.keys(providers).sort(), [ "genius", "local", "lrclib", + "lyricsovh", "musixmatch", "netease", "spotify", diff --git a/modules/lyrics-plus/settings.tsx b/modules/lyrics-plus/settings.tsx index 3c8d917..7054b97 100644 --- a/modules/lyrics-plus/settings.tsx +++ b/modules/lyrics-plus/settings.tsx @@ -458,7 +458,8 @@ export const ServiceOption = ({ const toggleDisabled = musixmatchInvalid; return react.createElement(SettingsProviderRow, { - label: item.name.replace(/^./, (character) => character.toUpperCase()), + label: + item.name === "lyricsovh" ? "Lyrics.ovh" : item.name.replace(/^./, (character) => character.toUpperCase()), description: item.desc, value: active, disabled: toggleDisabled, @@ -763,6 +764,12 @@ export function LyricsPlusAppearanceSettings() { right: "Right", }, }, + { + desc: "Blur inactive lines", + info: "In the scrolling view, soften lines farther from the current lyric. Hover a line to read it.", + key: "inactive-blur", + kind: "toggle", + }, { desc: "Fullscreen hotkey", info: "Focus the field, then press the shortcut you want to use.", @@ -801,6 +808,12 @@ export function LyricsPlusAppearanceSettings() { !CONFIG.visual["animated-background"], }, { desc: "Background color", key: "background-color", kind: "text", - when: () => !CONFIG.visual.colorful, + when: () => !CONFIG.visual.colorful && !CONFIG.visual["animated-background"], }, { desc: "Active text color", key: "active-color", kind: "text", - when: () => !CONFIG.visual.colorful, + when: () => !CONFIG.visual.colorful && !CONFIG.visual["animated-background"], }, { desc: "Inactive text color", key: "inactive-color", kind: "text", - when: () => !CONFIG.visual.colorful, + when: () => !CONFIG.visual.colorful && !CONFIG.visual["animated-background"], }, { desc: "Highlight text background", key: "highlight-color", kind: "text", - when: () => !CONFIG.visual.colorful, + when: () => !CONFIG.visual.colorful && !CONFIG.visual["animated-background"], }, ]} onChange={onChange}