From 3b2e2b1b36132b6acf541a4d02c91a6030cfe9ad Mon Sep 17 00:00:00 2001 From: ali entezari Date: Tue, 13 Feb 2024 23:20:13 -0500 Subject: [PATCH 01/11] update --- .idea/.gitignore | 5 + .idea/codeStyles/Project.xml | 59 ++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/lofi.iml | 12 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 + package.json | 2 +- src/constants.ts | 3 + src/main/main.ts | 41 ++++++ src/main/main.utils.ts | 42 ++++++ src/renderer/api/lyrics-api.ts | 134 +++++++++++++++++++ src/renderer/app/bars/index.tsx | 3 + src/renderer/app/cover/index.tsx | 17 +++ src/renderer/windows/lyric/index.tsx | 108 +++++++++++++++ 15 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/lofi.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 src/renderer/api/lyrics-api.ts create mode 100644 src/renderer/windows/lyric/index.tsx diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..b58b603f --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..4e86a5e2 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..03d9549e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/lofi.iml b/.idea/lofi.iml new file mode 100644 index 00000000..24643cc3 --- /dev/null +++ b/.idea/lofi.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..911071a0 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/package.json b/package.json index 476e0b20..73f13e47 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "install": "yarn run build", "config-xcode": "node-gyp configure -- -f xcode", - "build": "node-gyp rebuild --target=4.0.1 --dist-url=https://electronjs.org/headers --openssl_fips=X && genversion --es6 --semi version.generated.ts", + "build": "set npm_config_python=C:/Users/AliEntezari/.conda/envs/py2/python.exe & node-gyp rebuild --target=4.0.1 --dist-url=https://electronjs.org/headers --openssl_fips=X && genversion --es6 --semi version.generated.ts", "start": "electron ./pack/main.bundle.js", "dev": "yarn run development", "development": "rimraf pack && webpack --watch --config ./webpack.dev.js --progress --color", diff --git a/src/constants.ts b/src/constants.ts index 1ccd12b6..b9bdcb83 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,7 @@ export const MAX_BAR_THICKNESS = 20; export const MIN_FONT_SIZE = 6; export const MAX_FONT_SIZE = 32; export const TRACK_INFO_GAP = { X: 10, Y: 10 }; +export const LYRIC_GAP = { X: 0, Y: 10 }; export const MAX_CORNER_RADIUS = 20; export const MIN_SKIP_SONG_DELAY = 5; @@ -22,6 +23,7 @@ export enum WindowTitle { FullscreenViz = 'fullscreen-visualization', Settings = 'Lofi Settings', TrackInfo = 'track-info', + Lyric = 'lyric', } export enum WindowName { @@ -30,6 +32,7 @@ export enum WindowName { FullscreenViz = 'fullscreen-visualization', Settings = 'settings', TrackInfo = 'track-info', + Lyric = 'lyric', } export enum IpcMessage { diff --git a/src/main/main.ts b/src/main/main.ts index b6ff89e8..cfcb3817 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -13,6 +13,7 @@ import { nativeImage, Rectangle, screen, + session, shell, Tray, } from 'electron'; @@ -37,8 +38,10 @@ import { getAboutWindowOptions, getFullscreenVisualizationWindowOptions, getFullscreenVizBounds, + getLyricWindowOptions, getSettingsWindowOptions, getTrackInfoWindowOptions, + moveLyric, moveTrackInfo, setAlwaysOnTop, settingsSchema, @@ -142,6 +145,7 @@ const createMainWindow = (): void => { // See: https://github.com/electron/electron/issues/9477#issuecomment-406833003 mainWindow.setBounds(bounds); moveTrackInfo(mainWindow, screen); + moveLyric(mainWindow, screen); mainWindow.webContents.send(IpcMessage.WindowMoved, bounds); }); @@ -162,6 +166,7 @@ const createMainWindow = (): void => { mainWindow.on('resize', () => { moveTrackInfo(mainWindow, screen); + moveLyric(mainWindow, screen); }); mainWindow.on('resized', () => { @@ -184,6 +189,7 @@ const createMainWindow = (): void => { mainWindow.center(); } moveTrackInfo(mainWindow, screen); + moveLyric(mainWindow, screen); const fullscreenVizWindow = findWindow(WindowTitle.FullscreenViz); if (fullscreenVizWindow) { @@ -225,6 +231,10 @@ const createMainWindow = (): void => { } }); + // ipcMain.on('set-cookie', (event, url, name, value) => { + // session.defaultSession.cookies.set({ url, name, value }); + // }); + const windowOpenHandler = ( details: HandlerDetails ): { action: 'allow' | 'deny'; overrideBrowserWindowOptions?: BrowserWindowConstructorOptions } => { @@ -262,6 +272,13 @@ const createMainWindow = (): void => { }; } + case WindowName.Lyric: { + return { + action: 'allow', + overrideBrowserWindowOptions: getLyricWindowOptions(mainWindow, settings.isAlwaysOnTop), + }; + } + case WindowName.Auth: { shell.openExternal(details.url); break; @@ -324,6 +341,16 @@ const createMainWindow = (): void => { break; } + case WindowName.Lyric: { + moveLyric(mainWindow, screen); + childWindow.setIgnoreMouseEvents(true); + setAlwaysOnTop({ window: childWindow, isAlwaysOnTop: settings.isAlwaysOnTop }); + if (MACOS) { + childWindow.setWindowButtonVisibility(false); + } + break; + } + default: { break; } @@ -341,6 +368,19 @@ app.on('ready', () => { settings = store.get('settings') as Settings; } + session.defaultSession.cookies.set({ + url: 'https://spotify.com', + name: 'sp_dc', + value: + 'AQDkOH45Eb7QlEgTLWDjlP9YTbK8p0kJD02mr2vq8gtISw7v-YMDcRgBBW0nCpaJeg0rSeic_-Q0hCvwf_RtbBLV7E3yN64wQueDcyvXrvdSlk3TZF-Vl1UuZG1LBYp0rG4wbT1xUNL-sSZmbsCl6KhzoucvvLGX', + sameSite: 'no_restriction', + domain: '.spotify.com', + secure: true, + httpOnly: true, + path: '/', + expirationDate: 1739375434, + }); + createMainWindow(); tray = new Tray(icon); @@ -397,6 +437,7 @@ app.on('ready', () => { const isOnLeft = checkIfAppIsOnLeftSide(currentDisplay, bounds.x, bounds.width); mainWindow.webContents.send(IpcMessage.WindowReady, { isOnLeft, displays }); moveTrackInfo(mainWindow, screen); + moveLyric(mainWindow, screen); }); }); diff --git a/src/main/main.utils.ts b/src/main/main.utils.ts index ec15e43f..c98a7245 100644 --- a/src/main/main.utils.ts +++ b/src/main/main.utils.ts @@ -9,6 +9,7 @@ import { MIN_SIDE_LENGTH, MIN_SKIP_SONG_DELAY, TRACK_INFO_GAP, + LYRIC_GAP, WindowTitle, } from '../constants'; import { VisualizationType } from '../models/settings'; @@ -74,6 +75,26 @@ export const getTrackInfoWindowOptions = ( }; }; +export const getLyricWindowOptions = ( + mainWindow: BrowserWindow, + isAlwaysOnTop: boolean +): BrowserWindowConstructorOptions => { + const { x, y, width, height } = mainWindow.getBounds(); + return { + ...getCommonWindowOptions(), + parent: mainWindow, + skipTaskbar: true, + alwaysOnTop: isAlwaysOnTop, + height: 200, + width: 400, + transparent: true, + center: false, + x: x + LYRIC_GAP.X - width, + y: y + height + LYRIC_GAP.Y, + title: WindowTitle.Lyric, + }; +}; + export const showDevTool = (window: BrowserWindow, isShow: boolean): void => { if (isShow) { window.webContents.openDevTools({ mode: 'detach' }); @@ -128,6 +149,27 @@ export const moveTrackInfo = (mainWindow: BrowserWindow, screen: Screen): void = mainWindow.webContents.send(IpcMessage.SideChanged, { isOnLeft }); }; +export const moveLyric = (mainWindow: BrowserWindow, screen: Screen): void => { + const { x, y, width, height } = mainWindow.getBounds(); + const LyricWindow = findWindow(WindowTitle.Lyric); + if (!LyricWindow) { + return; + } + + const currentDisplay = screen.getDisplayNearestPoint({ x, y }); + const isOnLeft = checkIfAppIsOnLeftSide(currentDisplay, x, width); + + const originalBounds = LyricWindow.getBounds(); + const newBounds = { + ...originalBounds, + x: isOnLeft ? x + LYRIC_GAP.X - width : x - originalBounds.width - LYRIC_GAP.X + width, + y: y + height + LYRIC_GAP.Y, + }; + + LyricWindow.setBounds(newBounds); + mainWindow.webContents.send(IpcMessage.SideChanged, { isOnLeft }); +}; + export const settingsSchema = z.object({ x: z.number(), y: z.number(), diff --git a/src/renderer/api/lyrics-api.ts b/src/renderer/api/lyrics-api.ts new file mode 100644 index 00000000..3b3bb5fb --- /dev/null +++ b/src/renderer/api/lyrics-api.ts @@ -0,0 +1,134 @@ +import { ipcRenderer, ipcMain } from 'electron'; + +// eslint-disable-next-line camelcase +const sp_dc = + 'AQDkOH45Eb7QlEgTLWDjlP9YTbK8p0kJD02mr2vq8gtISw7v-YMDcRgBBW0nCpaJeg0rSeic_-Q0hCvwf_RtbBLV7E3yN64wQueDcyvXrvdSlk3TZF-Vl1UuZG1LBYp0rG4wbT1xUNL-sSZmbsCl6KhzoucvvLGX'; + +const token = + 'BQBISYKmgH9KBYhLFEOop-QvkS0eB141iHePtS91dqCDxvIt9r_oOTk3pGWR5vIlyS9oMOK6h1EmX1ANS0XftaTGRYResMuZnb3EC2oGNjCCxAtg92jSAAPggBBjCLUnirmX6E6yZC8HtwxpE2PHBPp9oFx63pbBBU7t9Ij7BYbDd26jwSzD0gTPBlwTrImKQBCM2r2R8g0bnRwMTUhKew2HU4OqC38aIybThYyn8ZquDikuXtqLlgbhn2ht6AHRqB_Wo657J61EuDM6umkcF2ceEowFOvTG0MK8YlL9U_6iWONn8WU-XUTr7OEfC68gOI9Hv0fNtJQBKsNavMuERNMN'; + +const TOKEN_URL = 'https://open.spotify.com/get_access_token?reason=transport&productType=web_player'; +const USER_AGENT = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36'; + +export interface Line { + startTimeMs: string; + words: string; + syllables: []; + endTimeMs: string; +} +export interface LyricsData { + lyrics: { + syncType: string; + lines: { + startTimeMs: string; + words: string; + syllables: []; + endTimeMs: string; + }[]; + provider: string; + providerLyricsId: string; + providerDisplayName: string; + syncLyricsUri: string; + isDenseTypeface: boolean; + alternatives: []; + language: string; + isRtlLanguage: boolean; + fullscreenAction: string; + showUpsell: boolean; + capStatus: string; + impressionsRemaining: number; + }; + colors: { + background: number; + text: number; + highlightText: number; + }; + hasVocalRemoval: boolean; +} + +class SpotifyLyricsAPI { + private token: string; + + loggedIn: boolean; + + constructor(private dcToken: string) { + this.loggedIn = false; + this.login().then((r) => { + this.loggedIn = r; + }); + this.token = token; + // this.loggedIn = true; + } + + async login(): Promise { + try { + // Set the cookie + ipcRenderer.send('set-cookie', 'https://spotify.com/', 'sp_dc', this.dcToken); + + // Make the request with fetch + const response = await fetch(TOKEN_URL, { + method: 'GET', + credentials: 'same-origin', + headers: { + 'User-Agent': USER_AGENT, + 'app-platform': 'WebPlayer', + }, + redirect: 'manual', + }); + + // Check if the request was successful + if (!response.ok) { + throw new Error(`Request failed: ${response.status}`); + } + + // Parse the response body as JSON + const data = await response.json(); + + // Store the access token + this.token = data.accessToken; + + return true; + } catch (error) { + console.error(error); + throw new Error('sp_dc provided is invalid, please check it again!'); + } + } + + async getLyrics(trackId: string): Promise { + console.log(this.loggedIn, trackId); + if (!this.loggedIn || trackId === '') { + return null; + } + try { + const response = await fetch( + `https://spclient.wg.spotify.com/color-lyrics/v2/track/${trackId}?format=json&market=from_token`, + { + method: 'GET', + credentials: 'same-origin', + headers: { + 'User-Agent': USER_AGENT, + 'app-platform': 'WebPlayer', + authorization: `Bearer ${this.token}`, + }, + redirect: 'manual', + } + ); + + // Check if the request was successful + if (!response.ok) { + throw new Error(`Request failed: ${response.status}`); + } + + // Parse the response body as JSON + const data = await response.json(); + + return data; + } catch (error) { + console.error(error); + return null; + } + } +} + +export const SpotifyLyricsApiInstance = new SpotifyLyricsAPI(sp_dc); diff --git a/src/renderer/app/bars/index.tsx b/src/renderer/app/bars/index.tsx index 7dd0a05c..174471d1 100644 --- a/src/renderer/app/bars/index.tsx +++ b/src/renderer/app/bars/index.tsx @@ -50,6 +50,9 @@ export const Bars: FunctionComponent = ({ barColor, barThickness, alwaysS className="vertical bar draggable" style={{ width: `${barThickness}px` }} /> +
+

Lyrics

+
); }; diff --git a/src/renderer/app/cover/index.tsx b/src/renderer/app/cover/index.tsx index 6e6fce32..4d2cf9c3 100644 --- a/src/renderer/app/cover/index.tsx +++ b/src/renderer/app/cover/index.tsx @@ -7,10 +7,12 @@ import styled, { css } from 'styled-components'; import { IpcMessage, WindowName } from '../../../constants'; import { Settings, VisualizationType } from '../../../models/settings'; import { AccountType, SpotifyApiInstance } from '../../api/spotify-api'; +import { LyricsData, SpotifyLyricsApiInstance } from '../../api/lyrics-api'; import { WindowPortal } from '../../components'; import { useCurrentlyPlaying } from '../../contexts/currently-playing.context'; import { CurrentlyPlayingActions, CurrentlyPlayingType } from '../../reducers/currently-playing.reducer'; import { TrackInfo } from '../../windows/track-info'; +import { Lyric } from '../../windows/lyric'; import { Bars } from '../bars'; import { Controls } from './controls'; import Menu from './menu'; @@ -64,6 +66,7 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza const [currentSongId, setCurrentSongId] = useState(''); const [shouldShowTrackInfo, setShouldShowTrackInfo] = useState(isAlwaysShowTrackInfo); const [errorToDisplay, setErrorToDisplay] = useState(''); + const [currentLyrics, setCurrentLyrics] = useState(); useEffect(() => setErrorToDisplay(message), [message]); @@ -115,6 +118,15 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza })(); }, [artist, currentSongId, refreshTrackLiked, songTitle, state.id, state.isPlaying]); + useEffect(() => { + (async () => { + if (SpotifyLyricsApiInstance.loggedIn) { + const lyrics = await SpotifyLyricsApiInstance.getLyrics(currentSongId); + setCurrentLyrics(lyrics); + } + })(); + }, [currentSongId]); + const keepAlive = useCallback(async (): Promise => { if (state.isPlaying || state.userProfile?.accountType !== AccountType.Premium) { return; @@ -224,6 +236,11 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza )} + {shouldShowTrackInfo && ( + + + + )} = ({ lyrics }) => { + const { state } = useCurrentlyPlaying(); + if (lyrics === null || lyrics === undefined || lyrics.lyrics === undefined || lyrics.lyrics.lines === undefined) { + return ( +
+ No Lyrics found +
+ ); + } + + let closestLineIndex = 0; + let closestLineDiff = Infinity; + + lyrics.lyrics.lines.forEach((line: Line, index: number) => { + const diff = Math.abs(Number(line.startTimeMs) - state.progress); + if (diff < closestLineDiff) { + closestLineDiff = diff; + closestLineIndex = index; + } + }); + + const prevLine = closestLineIndex > 0 ? lyrics.lyrics.lines[closestLineIndex - 1] : null; + const line = lyrics.lyrics.lines[closestLineIndex]; + const nextLine = closestLineIndex < lyrics.lyrics.lines.length - 1 ? lyrics.lyrics.lines[closestLineIndex + 1] : null; + + return ( +
+ {prevLine && {prevLine.words}} + {line.words} + {nextLine && {nextLine.words}} +
+ ); +}; + +interface TrackInfoProps { + track?: string; + artist?: string; + lyrics?: LyricsData; + isOnLeft?: boolean; +} + +export const Lyric: FunctionComponent = ({ track, artist, lyrics, isOnLeft }) => { + const { state } = useSettings(); + const backgroundColor = useMemo(() => { + const normalizedOpacity = Math.floor((state.trackInfoBackgroundOpacity / 100) * 255); + const color = state.trackInfoBackgroundColor || DEFAULT_SETTINGS.trackInfoBackgroundColor; + + return `${color}${normalizedOpacity.toString(16)}`; + }, [state]); + + return ( + + + + ); +}; From 4dbb3bf1381b1cf31252e0a154acb835d352d5eb Mon Sep 17 00:00:00 2001 From: ali entezari Date: Tue, 13 Feb 2024 23:35:10 -0500 Subject: [PATCH 02/11] update --- src/main/main.utils.ts | 2 +- src/renderer/api/lyrics-api.ts | 3 ++- src/renderer/app/cover/index.tsx | 6 +++--- src/renderer/windows/lyric/index.tsx | 12 +++++------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/main.utils.ts b/src/main/main.utils.ts index c98a7245..a7db734d 100644 --- a/src/main/main.utils.ts +++ b/src/main/main.utils.ts @@ -3,13 +3,13 @@ import { z } from 'zod'; import { IpcMessage, + LYRIC_GAP, MAX_BAR_THICKNESS, MAX_SIDE_LENGTH, MAX_SKIP_SONG_DELAY, MIN_SIDE_LENGTH, MIN_SKIP_SONG_DELAY, TRACK_INFO_GAP, - LYRIC_GAP, WindowTitle, } from '../constants'; import { VisualizationType } from '../models/settings'; diff --git a/src/renderer/api/lyrics-api.ts b/src/renderer/api/lyrics-api.ts index 3b3bb5fb..ebab95f1 100644 --- a/src/renderer/api/lyrics-api.ts +++ b/src/renderer/api/lyrics-api.ts @@ -1,4 +1,5 @@ -import { ipcRenderer, ipcMain } from 'electron'; +/* eslint-disable no-console */ +import { ipcRenderer } from 'electron'; // eslint-disable-next-line camelcase const sp_dc = diff --git a/src/renderer/app/cover/index.tsx b/src/renderer/app/cover/index.tsx index 4d2cf9c3..f6c1d2b2 100644 --- a/src/renderer/app/cover/index.tsx +++ b/src/renderer/app/cover/index.tsx @@ -6,13 +6,13 @@ import styled, { css } from 'styled-components'; import { IpcMessage, WindowName } from '../../../constants'; import { Settings, VisualizationType } from '../../../models/settings'; -import { AccountType, SpotifyApiInstance } from '../../api/spotify-api'; import { LyricsData, SpotifyLyricsApiInstance } from '../../api/lyrics-api'; +import { AccountType, SpotifyApiInstance } from '../../api/spotify-api'; import { WindowPortal } from '../../components'; import { useCurrentlyPlaying } from '../../contexts/currently-playing.context'; import { CurrentlyPlayingActions, CurrentlyPlayingType } from '../../reducers/currently-playing.reducer'; -import { TrackInfo } from '../../windows/track-info'; import { Lyric } from '../../windows/lyric'; +import { TrackInfo } from '../../windows/track-info'; import { Bars } from '../bars'; import { Controls } from './controls'; import Menu from './menu'; @@ -238,7 +238,7 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza )} {shouldShowTrackInfo && ( - + )} = ({ lyrics }) => { let closestLineDiff = Infinity; lyrics.lyrics.lines.forEach((line: Line, index: number) => { - const diff = Math.abs(Number(line.startTimeMs) - state.progress); + const diff = Math.abs(Number(line.startTimeMs) - state.progress + 150); if (diff < closestLineDiff) { closestLineDiff = diff; closestLineIndex = index; @@ -77,13 +77,11 @@ const LyricText: FunctionComponent = ({ lyrics }) => { }; interface TrackInfoProps { - track?: string; - artist?: string; lyrics?: LyricsData; isOnLeft?: boolean; } -export const Lyric: FunctionComponent = ({ track, artist, lyrics, isOnLeft }) => { +export const Lyric: FunctionComponent = ({ lyrics, isOnLeft }) => { const { state } = useSettings(); const backgroundColor = useMemo(() => { const normalizedOpacity = Math.floor((state.trackInfoBackgroundOpacity / 100) * 255); From 0319ca78ff8855afa57dee823bf062014cf9d2e3 Mon Sep 17 00:00:00 2001 From: ali entezari Date: Wed, 14 Feb 2024 09:38:24 -0500 Subject: [PATCH 03/11] fixed lyrics sync problem --- src/renderer/windows/lyric/index.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/renderer/windows/lyric/index.tsx b/src/renderer/windows/lyric/index.tsx index e20c6149..506000a7 100644 --- a/src/renderer/windows/lyric/index.tsx +++ b/src/renderer/windows/lyric/index.tsx @@ -51,21 +51,20 @@ const LyricText: FunctionComponent = ({ lyrics }) => { ); } - - let closestLineIndex = 0; - let closestLineDiff = Infinity; - + let closestLineIndex = -1; lyrics.lyrics.lines.forEach((line: Line, index: number) => { - const diff = Math.abs(Number(line.startTimeMs) - state.progress + 150); - if (diff < closestLineDiff) { - closestLineDiff = diff; + if (Number(line.startTimeMs) < state.progress) { closestLineIndex = index; } }); const prevLine = closestLineIndex > 0 ? lyrics.lyrics.lines[closestLineIndex - 1] : null; - const line = lyrics.lyrics.lines[closestLineIndex]; + let line = lyrics.lyrics.lines[closestLineIndex]; const nextLine = closestLineIndex < lyrics.lyrics.lines.length - 1 ? lyrics.lyrics.lines[closestLineIndex + 1] : null; + if (line === undefined) { + line = nextLine; + line.words = lyrics.lyrics.lines[0].words; + } return (
From 92bbe0b72d16e613ccf1000ba97d1cd75f3cf2ea Mon Sep 17 00:00:00 2001 From: ali entezari Date: Sat, 17 Feb 2024 23:44:25 +0330 Subject: [PATCH 04/11] added Settings --- src/constants.ts | 7 +- src/main/main.ts | 53 +++--- src/main/main.utils.ts | 30 ++-- src/models/settings.ts | 18 ++ src/renderer/api/lyrics-api.ts | 37 ++--- src/renderer/app/cover/index.tsx | 42 +++-- src/renderer/components/form.styled.ts | 8 + src/renderer/components/index.tsx | 2 + src/renderer/components/mantine.styled.ts | 2 +- src/renderer/windows/lyric/index.tsx | 105 ------------ src/renderer/windows/lyrics/index.tsx | 157 ++++++++++++++++++ src/renderer/windows/settings/help-link.tsx | 20 +++ src/renderer/windows/settings/index.tsx | 8 + .../windows/settings/lyrics-settings.tsx | 116 +++++++++++++ 14 files changed, 424 insertions(+), 181 deletions(-) delete mode 100644 src/renderer/windows/lyric/index.tsx create mode 100644 src/renderer/windows/lyrics/index.tsx create mode 100644 src/renderer/windows/settings/help-link.tsx create mode 100644 src/renderer/windows/settings/lyrics-settings.tsx diff --git a/src/constants.ts b/src/constants.ts index b9bdcb83..dbd91bbd 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,7 +12,7 @@ export const MAX_BAR_THICKNESS = 20; export const MIN_FONT_SIZE = 6; export const MAX_FONT_SIZE = 32; export const TRACK_INFO_GAP = { X: 10, Y: 10 }; -export const LYRIC_GAP = { X: 0, Y: 10 }; +export const LYRICS_GAP = { X: 0, Y: 10 }; export const MAX_CORNER_RADIUS = 20; export const MIN_SKIP_SONG_DELAY = 5; @@ -23,7 +23,7 @@ export enum WindowTitle { FullscreenViz = 'fullscreen-visualization', Settings = 'Lofi Settings', TrackInfo = 'track-info', - Lyric = 'lyric', + Lyrics = 'lyrics', } export enum WindowName { @@ -32,7 +32,7 @@ export enum WindowName { FullscreenViz = 'fullscreen-visualization', Settings = 'settings', TrackInfo = 'track-info', - Lyric = 'lyric', + Lyrics = 'lyrics', } export enum IpcMessage { @@ -56,4 +56,5 @@ export enum ApplicationUrl { Help = 'https://www.lofi.rocks/help', Discord = 'https://discord.gg/YuH9UJk', GitHub = 'https://github.com/dvx/lofi', + FindLyricsToken = 'https://github.com/akashrchandran/syrics/wiki/Finding-sp_dc', } diff --git a/src/main/main.ts b/src/main/main.ts index cfcb3817..f906425a 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -38,7 +38,7 @@ import { getAboutWindowOptions, getFullscreenVisualizationWindowOptions, getFullscreenVizBounds, - getLyricWindowOptions, + getLyricsWindowOptions, getSettingsWindowOptions, getTrackInfoWindowOptions, moveLyric, @@ -179,7 +179,7 @@ const createMainWindow = (): void => { ipcMain.on( IpcMessage.SettingsChanged, - (_: Event, { x, y, size, isAlwaysOnTop, isDebug, isVisibleInTaskbar, visualizationScreenId }: Settings) => { + (_: Event, { x, y, size, isAlwaysOnTop, isDebug, isVisibleInTaskbar, visualizationScreenId, SPDCToken }: Settings) => { setAlwaysOnTop({ window: mainWindow, isAlwaysOnTop }); mainWindow.setSkipTaskbar(!isVisibleInTaskbar); showDevTool(mainWindow, isDebug); @@ -196,6 +196,19 @@ const createMainWindow = (): void => { const fullscreenVizBounds = getFullscreenVizBounds(mainWindow.getBounds(), screen, visualizationScreenId); fullscreenVizWindow.setBounds(fullscreenVizBounds); } + if (SPDCToken !== '') { + session.defaultSession.cookies.set({ + url: 'https://spotify.com', + name: 'sp_dc', + value: SPDCToken, + sameSite: 'no_restriction', + domain: '.spotify.com', + secure: true, + httpOnly: true, + path: '/', + expirationDate: new Date().getTime() + 24 * 60 * 60 * 1000, + }); + } } ); @@ -231,10 +244,6 @@ const createMainWindow = (): void => { } }); - // ipcMain.on('set-cookie', (event, url, name, value) => { - // session.defaultSession.cookies.set({ url, name, value }); - // }); - const windowOpenHandler = ( details: HandlerDetails ): { action: 'allow' | 'deny'; overrideBrowserWindowOptions?: BrowserWindowConstructorOptions } => { @@ -272,10 +281,10 @@ const createMainWindow = (): void => { }; } - case WindowName.Lyric: { + case WindowName.Lyrics: { return { action: 'allow', - overrideBrowserWindowOptions: getLyricWindowOptions(mainWindow, settings.isAlwaysOnTop), + overrideBrowserWindowOptions: getLyricsWindowOptions(mainWindow, settings.isAlwaysOnTop), }; } @@ -341,7 +350,7 @@ const createMainWindow = (): void => { break; } - case WindowName.Lyric: { + case WindowName.Lyrics: { moveLyric(mainWindow, screen); childWindow.setIgnoreMouseEvents(true); setAlwaysOnTop({ window: childWindow, isAlwaysOnTop: settings.isAlwaysOnTop }); @@ -368,20 +377,20 @@ app.on('ready', () => { settings = store.get('settings') as Settings; } - session.defaultSession.cookies.set({ - url: 'https://spotify.com', - name: 'sp_dc', - value: - 'AQDkOH45Eb7QlEgTLWDjlP9YTbK8p0kJD02mr2vq8gtISw7v-YMDcRgBBW0nCpaJeg0rSeic_-Q0hCvwf_RtbBLV7E3yN64wQueDcyvXrvdSlk3TZF-Vl1UuZG1LBYp0rG4wbT1xUNL-sSZmbsCl6KhzoucvvLGX', - sameSite: 'no_restriction', - domain: '.spotify.com', - secure: true, - httpOnly: true, - path: '/', - expirationDate: 1739375434, - }); - createMainWindow(); + if (settings.SPDCToken !== '') { + session.defaultSession.cookies.set({ + url: 'https://spotify.com', + name: 'sp_dc', + value: settings.SPDCToken, + sameSite: 'no_restriction', + domain: '.spotify.com', + secure: true, + httpOnly: true, + path: '/', + expirationDate: new Date().getTime() + 24 * 60 * 60 * 1000, + }); + } tray = new Tray(icon); diff --git a/src/main/main.utils.ts b/src/main/main.utils.ts index a7db734d..f4f014b4 100644 --- a/src/main/main.utils.ts +++ b/src/main/main.utils.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { IpcMessage, - LYRIC_GAP, + LYRICS_GAP, MAX_BAR_THICKNESS, MAX_SIDE_LENGTH, MAX_SKIP_SONG_DELAY, @@ -30,8 +30,8 @@ export const getCommonWindowOptions = (): BrowserWindowConstructorOptions => ({ export const getSettingsWindowOptions = (): BrowserWindowConstructorOptions => ({ ...getCommonWindowOptions(), - height: 420, - minHeight: 420, + height: 540, + minHeight: 540, width: 420, minWidth: 420, title: WindowTitle.Settings, @@ -75,7 +75,7 @@ export const getTrackInfoWindowOptions = ( }; }; -export const getLyricWindowOptions = ( +export const getLyricsWindowOptions = ( mainWindow: BrowserWindow, isAlwaysOnTop: boolean ): BrowserWindowConstructorOptions => { @@ -85,13 +85,13 @@ export const getLyricWindowOptions = ( parent: mainWindow, skipTaskbar: true, alwaysOnTop: isAlwaysOnTop, - height: 200, - width: 400, + height: 1000, + width: 1000, transparent: true, center: false, - x: x + LYRIC_GAP.X - width, - y: y + height + LYRIC_GAP.Y, - title: WindowTitle.Lyric, + x: x + LYRICS_GAP.X - width, + y: y + height + LYRICS_GAP.Y, + title: WindowTitle.Lyrics, }; }; @@ -151,22 +151,22 @@ export const moveTrackInfo = (mainWindow: BrowserWindow, screen: Screen): void = export const moveLyric = (mainWindow: BrowserWindow, screen: Screen): void => { const { x, y, width, height } = mainWindow.getBounds(); - const LyricWindow = findWindow(WindowTitle.Lyric); - if (!LyricWindow) { + const LyricsWindow = findWindow(WindowTitle.Lyrics); + if (!LyricsWindow) { return; } const currentDisplay = screen.getDisplayNearestPoint({ x, y }); const isOnLeft = checkIfAppIsOnLeftSide(currentDisplay, x, width); - const originalBounds = LyricWindow.getBounds(); + const originalBounds = LyricsWindow.getBounds(); const newBounds = { ...originalBounds, - x: isOnLeft ? x + LYRIC_GAP.X - width : x - originalBounds.width - LYRIC_GAP.X + width, - y: y + height + LYRIC_GAP.Y, + x: isOnLeft ? x + LYRICS_GAP.X : x - originalBounds.width - LYRICS_GAP.X + width, + y: y + height + LYRICS_GAP.Y, }; - LyricWindow.setBounds(newBounds); + LyricsWindow.setBounds(newBounds); mainWindow.webContents.send(IpcMessage.SideChanged, { isOnLeft }); }; diff --git a/src/models/settings.ts b/src/models/settings.ts index 2e3ee41d..33b41c44 100644 --- a/src/models/settings.ts +++ b/src/models/settings.ts @@ -35,6 +35,15 @@ export interface Settings { trackInfoBackgroundOpacity: number; showFreemiumWarning: boolean; cornerRadius: number; + SPDCToken: string; + isShowLyrics: boolean; + isAlwaysShowLyrics: boolean; + lyricMaxLength: number; + lyricsFontSize: number; + lyricsColor: string; + lyricsBackgroundColor: string; + lyricsBackgroundOpacity: number; + lyricsFont: string; } export const DEFAULT_SETTINGS: Settings = { @@ -66,4 +75,13 @@ export const DEFAULT_SETTINGS: Settings = { trackInfoBackgroundOpacity: 50, showFreemiumWarning: true, cornerRadius: 0, + SPDCToken: '', + isShowLyrics: false, + isAlwaysShowLyrics: false, + lyricMaxLength: 30, + lyricsFontSize: 14, + lyricsColor: '#FFFFFF', + lyricsBackgroundColor: '#000000', + lyricsBackgroundOpacity: 50, + lyricsFont: 'Inter UI', }; diff --git a/src/renderer/api/lyrics-api.ts b/src/renderer/api/lyrics-api.ts index ebab95f1..0cee2328 100644 --- a/src/renderer/api/lyrics-api.ts +++ b/src/renderer/api/lyrics-api.ts @@ -1,12 +1,4 @@ /* eslint-disable no-console */ -import { ipcRenderer } from 'electron'; - -// eslint-disable-next-line camelcase -const sp_dc = - 'AQDkOH45Eb7QlEgTLWDjlP9YTbK8p0kJD02mr2vq8gtISw7v-YMDcRgBBW0nCpaJeg0rSeic_-Q0hCvwf_RtbBLV7E3yN64wQueDcyvXrvdSlk3TZF-Vl1UuZG1LBYp0rG4wbT1xUNL-sSZmbsCl6KhzoucvvLGX'; - -const token = - 'BQBISYKmgH9KBYhLFEOop-QvkS0eB141iHePtS91dqCDxvIt9r_oOTk3pGWR5vIlyS9oMOK6h1EmX1ANS0XftaTGRYResMuZnb3EC2oGNjCCxAtg92jSAAPggBBjCLUnirmX6E6yZC8HtwxpE2PHBPp9oFx63pbBBU7t9Ij7BYbDd26jwSzD0gTPBlwTrImKQBCM2r2R8g0bnRwMTUhKew2HU4OqC38aIybThYyn8ZquDikuXtqLlgbhn2ht6AHRqB_Wo657J61EuDM6umkcF2ceEowFOvTG0MK8YlL9U_6iWONn8WU-XUTr7OEfC68gOI9Hv0fNtJQBKsNavMuERNMN'; const TOKEN_URL = 'https://open.spotify.com/get_access_token?reason=transport&productType=web_player'; const USER_AGENT = @@ -51,22 +43,17 @@ export interface LyricsData { class SpotifyLyricsAPI { private token: string; - loggedIn: boolean; - - constructor(private dcToken: string) { - this.loggedIn = false; - this.login().then((r) => { - this.loggedIn = r; - }); - this.token = token; - // this.loggedIn = true; + constructor() { + this.login() + .then() + .catch(() => { + this.token = ''; + }); } async login(): Promise { + this.token = ''; try { - // Set the cookie - ipcRenderer.send('set-cookie', 'https://spotify.com/', 'sp_dc', this.dcToken); - // Make the request with fetch const response = await fetch(TOKEN_URL, { method: 'GET', @@ -80,7 +67,8 @@ class SpotifyLyricsAPI { // Check if the request was successful if (!response.ok) { - throw new Error(`Request failed: ${response.status}`); + console.error(`Request failed: ${response.status}`); + return false; } // Parse the response body as JSON @@ -92,13 +80,12 @@ class SpotifyLyricsAPI { return true; } catch (error) { console.error(error); - throw new Error('sp_dc provided is invalid, please check it again!'); + return false; } } async getLyrics(trackId: string): Promise { - console.log(this.loggedIn, trackId); - if (!this.loggedIn || trackId === '') { + if (this.token === '' || trackId === '') { return null; } try { @@ -132,4 +119,4 @@ class SpotifyLyricsAPI { } } -export const SpotifyLyricsApiInstance = new SpotifyLyricsAPI(sp_dc); +export const SpotifyLyricsApiInstance = new SpotifyLyricsAPI(); diff --git a/src/renderer/app/cover/index.tsx b/src/renderer/app/cover/index.tsx index f6c1d2b2..5a0ce9fc 100644 --- a/src/renderer/app/cover/index.tsx +++ b/src/renderer/app/cover/index.tsx @@ -11,7 +11,7 @@ import { AccountType, SpotifyApiInstance } from '../../api/spotify-api'; import { WindowPortal } from '../../components'; import { useCurrentlyPlaying } from '../../contexts/currently-playing.context'; import { CurrentlyPlayingActions, CurrentlyPlayingType } from '../../reducers/currently-playing.reducer'; -import { Lyric } from '../../windows/lyric'; +import { Lyrics } from '../../windows/lyrics'; import { TrackInfo } from '../../windows/track-info'; import { Bars } from '../bars'; import { Controls } from './controls'; @@ -54,9 +54,12 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza barColor, isAlwaysShowSongProgress, isAlwaysShowTrackInfo, + isAlwaysShowLyrics, + isShowLyrics, isOnLeft, size, skipSongDelay, + SPDCToken, volumeIncrement, visualizationId, visualizationType, @@ -65,8 +68,10 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza const [currentSongId, setCurrentSongId] = useState(''); const [shouldShowTrackInfo, setShouldShowTrackInfo] = useState(isAlwaysShowTrackInfo); + const [shouldAlwaysShowLyrics, setShouldAlwaysShowLyrics] = useState(isAlwaysShowLyrics); const [errorToDisplay, setErrorToDisplay] = useState(''); const [currentLyrics, setCurrentLyrics] = useState(); + const [lyricsLoggedIn, setLyricsLoggedIn] = useState(false); useEffect(() => setErrorToDisplay(message), [message]); @@ -74,6 +79,10 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza setShouldShowTrackInfo(isAlwaysShowTrackInfo); }, [isAlwaysShowTrackInfo]); + useEffect(() => { + setShouldAlwaysShowLyrics(isAlwaysShowLyrics); + }, [isAlwaysShowLyrics]); + const artist = useMemo(() => truncateText(state.artist), [state.artist]); const songTitle = useMemo(() => truncateText(state.track), [state.track]); @@ -120,13 +129,20 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza useEffect(() => { (async () => { - if (SpotifyLyricsApiInstance.loggedIn) { - const lyrics = await SpotifyLyricsApiInstance.getLyrics(currentSongId); - setCurrentLyrics(lyrics); - } + const lyrics = await SpotifyLyricsApiInstance.getLyrics(currentSongId); + setCurrentLyrics(lyrics); })(); }, [currentSongId]); + useEffect(() => { + (async () => { + const loggedIn = await SpotifyLyricsApiInstance.login(); + setLyricsLoggedIn(loggedIn); + if (loggedIn) console.log(`Lyrics API logged in with sp_dc: ${SPDCToken}`); + else console.log(`The provided sp_dc is invalid, please check it again!\n sp_dc: ${SPDCToken}`); + })(); + }, [SPDCToken]); + const keepAlive = useCallback(async (): Promise => { if (state.isPlaying || state.userProfile?.accountType !== AccountType.Premium) { return; @@ -222,8 +238,14 @@ export const Cover: FunctionComponent = ({ settings, message, onVisualiza return (
!isAlwaysShowTrackInfo && setShouldShowTrackInfo(true)} - onMouseLeave={() => !isAlwaysShowTrackInfo && setShouldShowTrackInfo(false)}> + onMouseEnter={() => { + if (!isAlwaysShowTrackInfo) setShouldShowTrackInfo(true); + if (isShowLyrics && !isAlwaysShowLyrics) setShouldAlwaysShowLyrics(true); + }} + onMouseLeave={() => { + if (!isAlwaysShowTrackInfo) setShouldShowTrackInfo(false); + if (isShowLyrics && !isAlwaysShowLyrics) setShouldAlwaysShowLyrics(false); + }}> = ({ settings, message, onVisualiza )} - {shouldShowTrackInfo && ( - - + {shouldAlwaysShowLyrics && ( + + )} = ({ lyrics }) => { - const { state } = useCurrentlyPlaying(); - if (lyrics === null || lyrics === undefined || lyrics.lyrics === undefined || lyrics.lyrics.lines === undefined) { - return ( -
- No Lyrics found -
- ); - } - let closestLineIndex = -1; - lyrics.lyrics.lines.forEach((line: Line, index: number) => { - if (Number(line.startTimeMs) < state.progress) { - closestLineIndex = index; - } - }); - - const prevLine = closestLineIndex > 0 ? lyrics.lyrics.lines[closestLineIndex - 1] : null; - let line = lyrics.lyrics.lines[closestLineIndex]; - const nextLine = closestLineIndex < lyrics.lyrics.lines.length - 1 ? lyrics.lyrics.lines[closestLineIndex + 1] : null; - if (line === undefined) { - line = nextLine; - line.words = lyrics.lyrics.lines[0].words; - } - - return ( -
- {prevLine && {prevLine.words}} - {line.words} - {nextLine && {nextLine.words}} -
- ); -}; - -interface TrackInfoProps { - lyrics?: LyricsData; - isOnLeft?: boolean; -} - -export const Lyric: FunctionComponent = ({ lyrics, isOnLeft }) => { - const { state } = useSettings(); - const backgroundColor = useMemo(() => { - const normalizedOpacity = Math.floor((state.trackInfoBackgroundOpacity / 100) * 255); - const color = state.trackInfoBackgroundColor || DEFAULT_SETTINGS.trackInfoBackgroundColor; - - return `${color}${normalizedOpacity.toString(16)}`; - }, [state]); - - return ( - - - - ); -}; diff --git a/src/renderer/windows/lyrics/index.tsx b/src/renderer/windows/lyrics/index.tsx new file mode 100644 index 00000000..d3ae8052 --- /dev/null +++ b/src/renderer/windows/lyrics/index.tsx @@ -0,0 +1,157 @@ +/* eslint-disable no-console */ +import React, { FunctionComponent, useMemo } from 'react'; +import styled from 'styled-components'; + +import { DEFAULT_SETTINGS } from '../../../models/settings'; +import { Line, LyricsData } from '../../api/lyrics-api'; +import { useCurrentlyPlaying } from '../../contexts/currently-playing.context'; +import { useSettings } from '../../contexts/settings.context'; + +const LyricsWrapper = styled.div` + position: fixed; + transition: 0.1s; + padding: 0.5em; + max-width: 30ch; + border-radius: 5px; + + div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; + + .left { + text-align: 'start'; + } + + .right { + text-align: 'end'; + } + } +`; + +const FocusedText = styled.div` + //font-weight: bold; + margin-top: 5px; + margin-bottom: 5px; +`; + +const FocusedTextWrapper = styled.div` + margin-top: 10px; + margin-bottom: 10px; +`; + +function breakTextIntoLines(text: string, maxLength: number): string[] { + const isCJK = /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(text); + const segments = isCJK ? Array.from(text) : text.split(' '); + const lines: string[] = ['']; + let currentLine = 0; + + for (let i = 0; i < segments.length; i += 1) { + if ((lines[currentLine] + segments[i]).length > (isCJK ? 0.5 * maxLength : maxLength)) { + currentLine += 1; + lines[currentLine] = ''; + } + + lines[currentLine] += isCJK ? segments[i] : `${segments[i]} `; + } + + return lines; +} + +interface LyricTextProps { + lyrics?: LyricsData; + loggedIn?: boolean; + isTokenEmpty?: boolean; + maxLength?: number; +} + +const LyricsText: FunctionComponent = ({ lyrics, loggedIn, isTokenEmpty, maxLength }) => { + const { state } = useCurrentlyPlaying(); + if (!loggedIn) { + return ( +
+ {isTokenEmpty && Add sp_dc token} + {!isTokenEmpty && Not a valid sp_dc token} +
+ ); + } + if (lyrics === null || lyrics === undefined || lyrics.lyrics === undefined || lyrics.lyrics.lines === undefined) { + return ( +
+ No Lyrics found +
+ ); + } + let closestLineIndex = -1; + lyrics.lyrics.lines.forEach((line: Line, index: number) => { + if (Number(line.startTimeMs) < state.progress) { + closestLineIndex = index; + } + }); + + const prevLyric = closestLineIndex > 0 ? lyrics.lyrics.lines[closestLineIndex - 1] : null; + let lyric = lyrics.lyrics.lines[closestLineIndex]; + const nextLyric = + closestLineIndex < lyrics.lyrics.lines.length - 1 ? lyrics.lyrics.lines[closestLineIndex + 1] : null; + + if (lyric === undefined) { + nextLyric.words = lyrics.lyrics.lines[0].words; + lyric = { ...nextLyric }; + lyric.words = ''; + } + + return ( +
+ {prevLyric && + breakTextIntoLines(prevLyric.words, maxLength).map((line) => ( + + {line} + + ))} + + {breakTextIntoLines(lyric.words, maxLength).map((line) => ( + {line} + ))} + + {nextLyric && + breakTextIntoLines(nextLyric.words, maxLength).map((line) => ( + + {line} + + ))} +
+ ); +}; + +interface LyricsProps { + lyrics?: LyricsData; + loggedIn?: boolean; + isOnLeft?: boolean; +} + +export const Lyrics: FunctionComponent = ({ lyrics, loggedIn, isOnLeft }) => { + const { state } = useSettings(); + const backgroundColor = useMemo(() => { + const normalizedOpacity = Math.floor((state.lyricsBackgroundOpacity / 100) * 255); + const color = state.lyricsBackgroundColor || DEFAULT_SETTINGS.lyricsBackgroundColor; + + return `${color}${normalizedOpacity.toString(16)}`; + }, [state]); + const maxLength = state.lyricMaxLength || DEFAULT_SETTINGS.lyricMaxLength; + + return ( + + + + ); +}; diff --git a/src/renderer/windows/settings/help-link.tsx b/src/renderer/windows/settings/help-link.tsx new file mode 100644 index 00000000..344c93a9 --- /dev/null +++ b/src/renderer/windows/settings/help-link.tsx @@ -0,0 +1,20 @@ +import { ipcRenderer } from 'electron'; +import React, { FunctionComponent, useCallback } from 'react'; + +import { ApplicationUrl, IpcMessage } from '../../../constants'; + +interface Props { + url: ApplicationUrl; + icon: string; +} + +export const HelpLink: FunctionComponent = ({ url, icon }) => { + const openLink = useCallback((link: ApplicationUrl) => { + ipcRenderer.send(IpcMessage.OpenLink, link); + }, []); + return ( +