From 533b96d1f6f2254027b4a0e7ba9187f2e34e3ced Mon Sep 17 00:00:00 2001 From: Angelos Bouklis Date: Wed, 25 Dec 2024 00:44:29 +0200 Subject: [PATCH] feat(synced-lyrics): multiple lyric sources (#2383) Co-authored-by: JellyBrick --- package.json | 1 + src/i18n/resources/en.json | 4 +- src/index.ts | 18 +- src/plugins/lyrics-genius/types.ts | 12 +- src/plugins/synced-lyrics/index.ts | 2 +- src/plugins/synced-lyrics/menu.ts | 27 ++- src/plugins/synced-lyrics/parsers/lrc.ts | 89 ++++++++ src/plugins/synced-lyrics/providers/LRCLib.ts | 137 ++++++++++++ .../synced-lyrics/providers/LyricsGenius.ts | 132 ++++++++++++ .../synced-lyrics/providers/Megalobiz.ts | 110 ++++++++++ .../synced-lyrics/providers/MusixMatch.ts | 10 + .../synced-lyrics/providers/YTMusic.ts | 201 ++++++++++++++++++ src/plugins/synced-lyrics/providers/index.ts | 189 ++++++++++++++++ .../renderer/components/ErrorDisplay.tsx | 64 ++++++ .../renderer/components/LoadingKaomoji.tsx | 33 +++ .../renderer/components/LyricsContainer.tsx | 147 +++---------- .../renderer/components/LyricsPicker.tsx | 198 +++++++++++++++++ .../renderer/components/PlainLyrics.tsx | 30 +++ .../renderer/components/SyncedLine.tsx | 21 +- src/plugins/synced-lyrics/renderer/index.ts | 22 +- .../synced-lyrics/renderer/lyrics/fetch.ts | 198 ----------------- .../synced-lyrics/renderer/lyrics/index.ts | 44 ---- .../synced-lyrics/renderer/renderer.tsx | 85 +++++++- src/plugins/synced-lyrics/renderer/utils.tsx | 10 +- src/plugins/synced-lyrics/style.css | 96 ++++++++- src/plugins/synced-lyrics/types.ts | 37 ++-- src/types/icons.ts | 48 +++++ src/yt-web-components.d.ts | 9 + 28 files changed, 1527 insertions(+), 447 deletions(-) create mode 100644 src/plugins/synced-lyrics/parsers/lrc.ts create mode 100644 src/plugins/synced-lyrics/providers/LRCLib.ts create mode 100644 src/plugins/synced-lyrics/providers/LyricsGenius.ts create mode 100644 src/plugins/synced-lyrics/providers/Megalobiz.ts create mode 100644 src/plugins/synced-lyrics/providers/MusixMatch.ts create mode 100644 src/plugins/synced-lyrics/providers/YTMusic.ts create mode 100644 src/plugins/synced-lyrics/providers/index.ts create mode 100644 src/plugins/synced-lyrics/renderer/components/ErrorDisplay.tsx create mode 100644 src/plugins/synced-lyrics/renderer/components/LoadingKaomoji.tsx create mode 100644 src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx create mode 100644 src/plugins/synced-lyrics/renderer/components/PlainLyrics.tsx delete mode 100644 src/plugins/synced-lyrics/renderer/lyrics/fetch.ts delete mode 100644 src/plugins/synced-lyrics/renderer/lyrics/index.ts create mode 100644 src/types/icons.ts diff --git a/package.json b/package.json index 645ceb51..882c866e 100644 --- a/package.json +++ b/package.json @@ -195,6 +195,7 @@ "start": "electron-vite preview", "start:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm start", "dev": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev --watch", + "dev:renderer": "cross-env NODE_OPTIONS=--enable-source-maps electron-vite dev", "dev:debug": "cross-env ELECTRON_ENABLE_LOGGING=1 pnpm dev", "clean": "del-cli dist && del-cli pack && del-cli .vite-inspect", "dist": "pnpm clean && pnpm build && pnpm electron-builder --win --mac --linux -p never", diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index c53105db..d548d4cb 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -714,8 +714,8 @@ "synced-lyrics": { "description": "Provides synced lyrics to songs, using providers like LRClib.", "errors": { - "fetch": "⚠️ - An error occurred while fetching the lyrics. Please try again later.", - "not-found": "⚠️ - No lyrics found for this song." + "fetch": "⚠️\tAn error occurred while fetching the lyrics.\n\tPlease try again later.", + "not-found": "⚠️ No lyrics found for this song." }, "menu": { "default-text-string": { diff --git a/src/index.ts b/src/index.ts index 0d10e369..24eef7b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -132,7 +132,7 @@ if (config.get('options.disableHardwareAcceleration')) { if (is.linux()) { // Overrides WM_CLASS for X11 to correspond to icon filename - app.setName("com.github.th_ch.youtube_music"); + app.setName('com.github.th_ch.youtube_music'); // Workaround for issue #2248 if ( @@ -904,9 +904,19 @@ function removeContentSecurityPolicy( betterSession.webRequest.onHeadersReceived((details, callback) => { details.responseHeaders ??= {}; - // Remove the content security policy - delete details.responseHeaders['content-security-policy-report-only']; - delete details.responseHeaders['content-security-policy']; + // prettier-ignore + if (new URL(details.url).protocol === 'https:') { + // Remove the content security policy + delete details.responseHeaders['content-security-policy-report-only']; + delete details.responseHeaders['Content-Security-Policy-Report-Only']; + delete details.responseHeaders['content-security-policy']; + delete details.responseHeaders['Content-Security-Policy']; + + // Only allow cross-origin requests from music.youtube.com + delete details.responseHeaders['access-control-allow-origin']; + delete details.responseHeaders['Access-Control-Allow-Origin']; + details.responseHeaders['access-control-allow-origin'] = ['https://music.youtube.com']; + } callback({ cancel: false, responseHeaders: details.responseHeaders }); }); diff --git a/src/plugins/lyrics-genius/types.ts b/src/plugins/lyrics-genius/types.ts index fac46ada..01cfd47b 100644 --- a/src/plugins/lyrics-genius/types.ts +++ b/src/plugins/lyrics-genius/types.ts @@ -18,8 +18,8 @@ export interface Section { export interface Hit { highlights: Highlight[]; - index: Index; - type: Index; + index: ResultType; + type: ResultType; result: Result; } @@ -35,14 +35,10 @@ export interface Range { end: number; } -export enum Index { - Album = 'album', - Lyric = 'lyric', - Song = 'song', -} +export type ResultType = 'song' | 'album' | 'lyric'; export interface Result { - _type: Index; + _type: ResultType; annotation_count?: number; api_path: string; artist_names?: string; diff --git a/src/plugins/synced-lyrics/index.ts b/src/plugins/synced-lyrics/index.ts index c490631b..3eb7d21d 100644 --- a/src/plugins/synced-lyrics/index.ts +++ b/src/plugins/synced-lyrics/index.ts @@ -20,7 +20,7 @@ export default createPlugin({ showTimeCodes: false, defaultTextString: '♪', lineEffect: 'scale', - } satisfies SyncedLyricsPluginConfig, + } as SyncedLyricsPluginConfig, menu, renderer, diff --git a/src/plugins/synced-lyrics/menu.ts b/src/plugins/synced-lyrics/menu.ts index b6f41f86..419b0e3e 100644 --- a/src/plugins/synced-lyrics/menu.ts +++ b/src/plugins/synced-lyrics/menu.ts @@ -5,13 +5,10 @@ import { t } from '@/i18n'; import type { MenuContext } from '@/types/contexts'; import type { SyncedLyricsPluginConfig } from './types'; -export const menu = async ({ - getConfig, - setConfig, -}: MenuContext): Promise< +export const menu = async (ctx: MenuContext): Promise< MenuItemConstructorOptions[] > => { - const config = await getConfig(); + const config = await ctx.getConfig(); return [ { @@ -20,7 +17,7 @@ export const menu = async ({ type: 'checkbox', checked: config.preciseTiming, click(item) { - setConfig({ + ctx.setConfig({ preciseTiming: item.checked, }); }, @@ -40,7 +37,7 @@ export const menu = async ({ type: 'radio', checked: config.lineEffect === 'scale', click() { - setConfig({ + ctx.setConfig({ lineEffect: 'scale', }); }, @@ -55,7 +52,7 @@ export const menu = async ({ type: 'radio', checked: config.lineEffect === 'offset', click() { - setConfig({ + ctx.setConfig({ lineEffect: 'offset', }); }, @@ -70,7 +67,7 @@ export const menu = async ({ type: 'radio', checked: config.lineEffect === 'focus', click() { - setConfig({ + ctx.setConfig({ lineEffect: 'focus', }); }, @@ -87,7 +84,7 @@ export const menu = async ({ type: 'radio', checked: config.defaultTextString === '♪', click() { - setConfig({ + ctx.setConfig({ defaultTextString: '♪', }); }, @@ -97,7 +94,7 @@ export const menu = async ({ type: 'radio', checked: config.defaultTextString === ' ', click() { - setConfig({ + ctx.setConfig({ defaultTextString: ' ', }); }, @@ -107,7 +104,7 @@ export const menu = async ({ type: 'radio', checked: config.defaultTextString === '...', click() { - setConfig({ + ctx.setConfig({ defaultTextString: '...', }); }, @@ -117,7 +114,7 @@ export const menu = async ({ type: 'radio', checked: config.defaultTextString === '———', click() { - setConfig({ + ctx.setConfig({ defaultTextString: '———', }); }, @@ -130,7 +127,7 @@ export const menu = async ({ type: 'checkbox', checked: config.showTimeCodes, click(item) { - setConfig({ + ctx.setConfig({ showTimeCodes: item.checked, }); }, @@ -143,7 +140,7 @@ export const menu = async ({ type: 'checkbox', checked: config.showLyricsEvenIfInexact, click(item) { - setConfig({ + ctx.setConfig({ showLyricsEvenIfInexact: item.checked, }); }, diff --git a/src/plugins/synced-lyrics/parsers/lrc.ts b/src/plugins/synced-lyrics/parsers/lrc.ts new file mode 100644 index 00000000..355c0a4d --- /dev/null +++ b/src/plugins/synced-lyrics/parsers/lrc.ts @@ -0,0 +1,89 @@ +interface LRCTag { + tag: string; + value: string; +} + +interface LRCLine { + time: string; + timeInMs: number; + duration: number; + text: string; +} + +interface LRC { + tags: LRCTag[]; + lines: LRCLine[]; +} + +const tagRegex = /^\[(?\w+):\s*(?.+?)\s*\]$/; +// prettier-ignore +const lyricRegex = /^\[(?\d+):(?\d+)\.(?\d+)\](?.+)$/; + +export const LRC = { + parse: (text: string): LRC => { + const lrc: LRC = { + tags: [], + lines: [], + }; + + let offset = 0; + let previousLine: LRCLine | null = null; + + for (const line of text.split('\n')) { + if (!line.trim().startsWith('[')) continue; + + const lyric = line.match(lyricRegex)?.groups; + if (!lyric) { + const tag = line.match(tagRegex)?.groups; + if (tag) { + if (tag.tag === 'offset') { + offset = parseInt(tag.value); + continue; + } + + lrc.tags.push({ + tag: tag.tag, + value: tag.value, + }); + } + continue; + } + + const { minutes, seconds, milliseconds, text } = lyric; + const timeInMs = + parseInt(minutes) * 60 * 1000 + + parseInt(seconds) * 1000 + + parseInt(milliseconds); + + const currentLine: LRCLine = { + time: `${minutes}:${seconds}:${milliseconds}`, + timeInMs, + text: text.trim(), + duration: Infinity, + }; + + if (previousLine) { + previousLine.duration = timeInMs - previousLine.timeInMs; + } + + previousLine = currentLine; + lrc.lines.push(currentLine); + } + + for (const line of lrc.lines) { + line.timeInMs += offset; + } + + const first = lrc.lines.at(0); + if (first && first.timeInMs > 300) { + lrc.lines.unshift({ + time: '0:0:0', + timeInMs: 0, + duration: first.timeInMs, + text: '', + }); + } + + return lrc; + }, +}; diff --git a/src/plugins/synced-lyrics/providers/LRCLib.ts b/src/plugins/synced-lyrics/providers/LRCLib.ts new file mode 100644 index 00000000..db44ba8a --- /dev/null +++ b/src/plugins/synced-lyrics/providers/LRCLib.ts @@ -0,0 +1,137 @@ +import { jaroWinkler } from '@skyra/jaro-winkler'; + +import { config } from '../renderer/renderer'; +import { LRC } from '../parsers/lrc'; + +import type { LyricProvider, LyricResult, SearchSongInfo } from '../types'; + +export class LRCLib implements LyricProvider { + name = 'LRCLib'; + baseUrl = 'https://lrclib.net'; + + async search({ + title, + artist, + album, + songDuration, + }: SearchSongInfo): Promise { + let query = new URLSearchParams({ + artist_name: artist, + track_name: title, + }); + + query.set('album_name', album!); + if (query.get('album_name') === 'undefined') { + query.delete('album_name'); + } + + let url = `${this.baseUrl}/api/search?${query.toString()}`; + let response = await fetch(url); + + if (!response.ok) { + throw new Error(`bad HTTPStatus(${response.statusText})`); + } + + let data = (await response.json()) as LRCLIBSearchResponse; + if (!data || !Array.isArray(data)) { + throw new Error(`Expected an array, instead got ${typeof data}`); + } + + if (data.length === 0) { + if (!config()?.showLyricsEvenIfInexact) { + return null; + } + + query = new URLSearchParams({ q: title }); + url = `${this.baseUrl}/api/search?${query.toString()}`; + + response = await fetch(url); + if (!response.ok) { + throw new Error(`bad HTTPStatus(${response.statusText})`); + } + + data = (await response.json()) as LRCLIBSearchResponse; + if (!Array.isArray(data)) { + throw new Error(`Expected an array, instead got ${typeof data}`); + } + } + + const filteredResults = []; + for (const item of data) { + const { artistName } = item; + + const artists = artist.split(/[&,]/g).map((i) => i.trim()); + const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim()); + + const permutations = []; + for (const artistA of artists) { + for (const artistB of itemArtists) { + permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]); + } + } + + for (const artistA of itemArtists) { + for (const artistB of artists) { + permutations.push([artistA.toLowerCase(), artistB.toLowerCase()]); + } + } + + const ratio = Math.max( + ...permutations.map(([x, y]) => jaroWinkler(x, y)), + ); + + if (ratio <= 0.9) continue; + filteredResults.push(item); + } + + filteredResults.sort(({ duration: durationA }, { duration: durationB }) => { + const left = Math.abs(durationA - songDuration); + const right = Math.abs(durationB - songDuration); + + return left - right; + }); + + const closestResult = filteredResults[0]; + if (!closestResult) { + return null; + } + + if (Math.abs(closestResult.duration - songDuration) > 15) { + return null; + } + + if (closestResult.instrumental) { + return null; + } + + const raw = closestResult.syncedLyrics; + const plain = closestResult.plainLyrics; + if (!raw && !plain) { + return null; + } + + return { + title: closestResult.trackName, + artists: closestResult.artistName.split(/[&,]/g), + lines: raw + ? LRC.parse(raw).lines.map((l) => ({ + ...l, + status: 'upcoming' as const, + })) + : undefined, + lyrics: plain, + }; + } +} + +type LRCLIBSearchResponse = { + id: number; + name: string; + trackName: string; + artistName: string; + albumName: string; + duration: number; + instrumental: boolean; + plainLyrics: string; + syncedLyrics: string; +}[]; diff --git a/src/plugins/synced-lyrics/providers/LyricsGenius.ts b/src/plugins/synced-lyrics/providers/LyricsGenius.ts new file mode 100644 index 00000000..519e6f94 --- /dev/null +++ b/src/plugins/synced-lyrics/providers/LyricsGenius.ts @@ -0,0 +1,132 @@ +import type { LyricProvider, LyricResult, SearchSongInfo } from '../types'; + +const preloadedStateRegex = /__PRELOADED_STATE__ = JSON\.parse\('(.*?)'\);/; +const preloadHtmlRegex = /body":{"html":"(.*?)","children"/; + +export class LyricsGenius implements LyricProvider { + public name = 'Genius'; + public baseUrl = 'https://genius.com'; + private domParser = new DOMParser(); + + // prettier-ignore + async search({ title, artist }: SearchSongInfo): Promise { + const query = new URLSearchParams({ + q: `${artist} ${title}`, + page: '1', + per_page: '10', + }); + + const response = await fetch(`${this.baseUrl}/api/search/song?${query}`); + if (!response.ok) { + return null; + } + + const data = (await response.json()) as LyricsGeniusSearch; + const hits = data.response.sections[0].hits; + + hits.sort( + ({ + result: { + title: titleA, + primary_artist: { name: artistA }, + }, + }, + { + result: { + title: titleB, + primary_artist: { name: artistB }, + }, + }) => { + const pointsA = (titleA === title ? 1 : 0) + (artistA.includes(artist) ? 1 : 0); + const pointsB = (titleB === title ? 1 : 0) + (artistB.includes(artist) ? 1 : 0); + + return pointsB - pointsA; + }, + ); + + const closestHit = hits.at(0); + if (!closestHit) { + return null; + } + + const { result: { path } } = closestHit; + + const html = await fetch(`${this.baseUrl}${path}`).then((res) => res.text()); + const doc = this.domParser.parseFromString(html, 'text/html'); + + const preloadedStateScript = Array.prototype.find.call(doc.querySelectorAll('script'), (script: HTMLScriptElement) => { + return script.textContent?.includes('window.__PRELOADED_STATE__'); + }) as HTMLScriptElement; + + const preloadedState = preloadedStateScript.textContent?.match(preloadedStateRegex)?.[1]?.replace(/\\"/g, '"'); + + const lyricsHtml = preloadedState?.match(preloadHtmlRegex)?.[1] + ?.replace(/\\\//g, '/') + ?.replace(/\\\\/g, '\\') + ?.replace(/\\n/g, '\n') + ?.replace(/\\'/g, "'") + ?.replace(/\\"/g, '"'); + + if (!lyricsHtml) throw new Error('Failed to extract lyrics from preloaded state.'); + + const lyricsDoc = this.domParser.parseFromString(lyricsHtml, 'text/html'); + const lyrics = lyricsDoc.body.innerText; + + if (lyrics.trim().toLowerCase().replace(/[[\]]/g, '') === 'instrumental') return null; + + return { + title: closestHit.result.title, + artists: closestHit.result.primary_artists.map(({ name }) => name), + lyrics, + }; + } +} + +interface LyricsGeniusSearch { + response: Response; +} + +interface Response { + sections: Section[]; +} + +interface Section { + hits: { + highlights: unknown[]; + index: string; + type: string; + result: Result; + }[]; +} + +interface Result { + api_path: string; + artist_names: string; + full_title: string; + id: number; + instrumental: boolean; + path: string; + release_date_components: ReleaseDateComponents; + title: string; + title_with_featured: string; + updated_by_human_at: number; + url: string; + featured_artists: Artist[]; + primary_artist: Artist; + primary_artists: Artist[]; +} + +interface Artist { + api_path: string; + id: number; + image_url: string; + name: string; + slug: string; + url: string; +} + +interface ReleaseDateComponents { + year: number; + month: number; + day: number; +} diff --git a/src/plugins/synced-lyrics/providers/Megalobiz.ts b/src/plugins/synced-lyrics/providers/Megalobiz.ts new file mode 100644 index 00000000..11ab8735 --- /dev/null +++ b/src/plugins/synced-lyrics/providers/Megalobiz.ts @@ -0,0 +1,110 @@ +import { jaroWinkler } from '@skyra/jaro-winkler'; + +import { LRC } from '../parsers/lrc'; + +import type { LyricProvider, LyricResult, SearchSongInfo } from '../types'; + +const removeNoise = (text: string) => { + return text + .replace(/\[.*?\]/g, '') + .replace(/\(.*?\)/g, '') + .trim() + .replace(/(^[-•])|([-•]$)/g, '') + .trim() + .replace(/\s+by$/, ''); +}; + +export class Megalobiz implements LyricProvider { + public name = 'Megalobiz'; + public baseUrl = 'https://www.megalobiz.com'; + private domParser = new DOMParser(); + + // prettier-ignore + async search({ title, artist, songDuration }: SearchSongInfo): Promise { + const query = new URLSearchParams({ + qry: `${artist} ${title}`, + }); + + const response = await fetch(`${this.baseUrl}/search/all?${query}`, { + signal: AbortSignal.timeout(5_000), + }); + if (!response.ok) { + throw new Error(`bad HTTPStatus(${response.statusText})`); + } + + const data = await response.text(); + const searchDoc = this.domParser.parseFromString(data, 'text/html'); + + // prettier-ignore + const searchResults: MegalobizSearchResult[] = Array.prototype.map + .call(searchDoc.querySelectorAll('a.entity_name[href^="/lrc/maker/"][name][title]'), + (anchor: HTMLAnchorElement) => { + const { minutes, seconds, millis } = anchor + .getAttribute('title')! + .match(/\[(?\d+):(?\d+)\.(?\d+)\]/)! + .groups!; + + let name = anchor.getAttribute('name')!; + + const artists = [ + removeNoise(name.match(/\(?[Ff]eat\. (.+)\)?/)?.[1] ?? ''), + ...(removeNoise(name).match(/(?.*?) [-•] (?.*)/)?.groups?.artists?.split(/[&,]/)?.map(removeNoise) ?? []), + ...(removeNoise(name).match(/(?<title>.*) by (?<artists>.*)/)?.groups?.artists?.split(/[&,]/)?.map(removeNoise) ?? []), + ].filter(Boolean); + + for (const artist of artists) { + name = name.replace(artist, ''); + name = removeNoise(name); + } + + if (jaroWinkler(title, name) < 0.8) return null; + + return { + title: name, + artists, + href: anchor.getAttribute('href')!, + duration: + parseInt(minutes) * 60 + + parseInt(seconds) + + parseInt(millis) / 1000, + }; + }, + ) + .filter(Boolean); + + const sortedResults = searchResults.sort( + ({ duration: durationA }, { duration: durationB }) => { + const left = Math.abs(durationA - songDuration); + const right = Math.abs(durationB - songDuration); + + return left - right; + }, + ); + + const closestResult = sortedResults[0]; + if (!closestResult) return null; + if (Math.abs(closestResult.duration - songDuration) > 15) { + return null; + } + + const html = await fetch(`${this.baseUrl}${closestResult.href}`).then((r) => r.text()); + const lyricsDoc = this.domParser.parseFromString(html, 'text/html'); + const raw = lyricsDoc.querySelector('span[id^="lrc_"][id$="_lyrics"]')?.textContent; + if (!raw) throw new Error('Failed to extract lyrics from page.'); + + const lyrics = LRC.parse(raw); + + return { + title: closestResult.title, + artists: closestResult.artists, + lines: lyrics.lines.map((l) => ({ ...l, status: 'upcoming' })), + }; + } +} + +interface MegalobizSearchResult { + title: string; + artists: string[]; + href: string; + duration: number; +} diff --git a/src/plugins/synced-lyrics/providers/MusixMatch.ts b/src/plugins/synced-lyrics/providers/MusixMatch.ts new file mode 100644 index 00000000..0262f240 --- /dev/null +++ b/src/plugins/synced-lyrics/providers/MusixMatch.ts @@ -0,0 +1,10 @@ +import type { LyricProvider, LyricResult, SearchSongInfo } from '../types'; + +export class MusixMatch implements LyricProvider { + name = 'MusixMatch'; + baseUrl = 'https://www.musixmatch.com/'; + + search(_: SearchSongInfo): Promise<LyricResult | null> { + throw new Error('Not implemented'); + } +} diff --git a/src/plugins/synced-lyrics/providers/YTMusic.ts b/src/plugins/synced-lyrics/providers/YTMusic.ts new file mode 100644 index 00000000..1a913d10 --- /dev/null +++ b/src/plugins/synced-lyrics/providers/YTMusic.ts @@ -0,0 +1,201 @@ +import type { LyricProvider, LyricResult, SearchSongInfo } from '../types'; + +const headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', +}; + +const client = { + clientName: '26', + clientVersion: '7.01.05', +}; + +export class YTMusic implements LyricProvider { + public name = 'YTMusic'; + public baseUrl = 'https://music.youtube.com/'; + + // prettier-ignore + public async search( + { videoId, title, artist }: SearchSongInfo, + ): Promise<LyricResult | null> { + const data = await this.fetchNext(videoId); + + const { tabs } = + data?.contents?.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer + ?.watchNextTabbedResultsRenderer ?? {}; + if (!Array.isArray(tabs)) return null; + + const lyricsTab = tabs.find((it) => { + const pageType = it?.tabRenderer?.endpoint?.browseEndpoint + ?.browseEndpointContextSupportedConfigs + ?.browseEndpointContextMusicConfig?.pageType; + return pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS'; + }); + + if (!lyricsTab) return null; + + const { browseId } = lyricsTab?.tabRenderer?.endpoint?.browseEndpoint ?? {}; + if (!browseId) return null; + + const { contents } = await this.fetchBrowse(browseId); + if (!contents) return null; + + /* + NOTE: Due to the nature of Youtubei, the json responses are not consistent, + this means we have to check for multiple possible paths to get the lyrics. + */ + + const syncedLines = contents?.elementRenderer?.newElement?.type + ?.componentType?.model?.timedLyricsModel?.lyricsData?.timedLyricsData; + + const synced = syncedLines?.length && syncedLines[0]?.cueRange + ? syncedLines.map((it) => ({ + time: this.millisToTime(parseInt(it.cueRange.startTimeMilliseconds)), + timeInMs: parseInt(it.cueRange.startTimeMilliseconds), + duration: parseInt(it.cueRange.endTimeMilliseconds) - + parseInt(it.cueRange.startTimeMilliseconds), + text: it.lyricLine.trim() === '♪' ? '' : it.lyricLine.trim(), + status: 'upcoming' as const, + })) + : undefined; + + const plain = !synced + ? syncedLines?.length + ? syncedLines.map((it) => it.lyricLine).join('\n') + : contents?.messageRenderer + ? contents?.messageRenderer?.text?.runs?.map((it) => it.text).join('\n') + : contents?.sectionListRenderer?.contents?.[0] + ?.musicDescriptionShelfRenderer?.description?.runs?.map((it) => + it.text + )?.join('\n') + : undefined; + + if (typeof plain === 'string' && plain === 'Lyrics not available') { + return null; + } + + if (synced?.length && synced[0].timeInMs > 300) { + synced.unshift({ + duration: 0, + text: '', + time: '00:00.00', + timeInMs: 0, + status: 'upcoming' as const, + }); + } + + return { + title, + artists: [artist], + + lyrics: plain, + lines: synced, + }; + } + + private millisToTime(millis: number) { + const minutes = Math.floor(millis / 60000); + const seconds = Math.floor((millis - minutes * 60 * 1000) / 1000); + const remaining = (millis - minutes * 60 * 1000 - seconds * 1000) / 10; + return `${minutes.toString().padStart(2, '0')}:${seconds + .toString() + .padStart(2, '0')}.${remaining.toString().padStart(2, '0')}`; + } + + private ENDPOINT = 'https://youtubei.googleapis.com/youtubei/v1/'; + // RATE LIMITED (2 req per sec) + private PROXIED_ENDPOINT = 'https://ytmbrowseproxy.zvz.be/'; + + private fetchNext(videoId: string) { + return fetch(this.ENDPOINT + 'next?prettyPrint=false', { + headers, + method: 'POST', + body: JSON.stringify({ + videoId, + context: { client }, + }), + }).then((res) => res.json()) as Promise<NextData>; + } + + private fetchBrowse(browseId: string) { + return fetch(this.PROXIED_ENDPOINT + 'browse?prettyPrint=false', { + headers, + method: 'POST', + body: JSON.stringify({ + browseId, + context: { client }, + }), + }).then((res) => res.json()) as Promise<BrowseData>; + } +} + +interface NextData { + contents: { + singleColumnMusicWatchNextResultsRenderer: { + tabbedRenderer: { + watchNextTabbedResultsRenderer: { + tabs: { + tabRenderer: { + endpoint: { + browseEndpoint: { + browseId: string; + browseEndpointContextSupportedConfigs: { + browseEndpointContextMusicConfig: { + pageType: string; + }; + }; + }; + }; + }; + }[]; + }; + }; + }; + }; +} + +interface BrowseData { + contents: { + elementRenderer: { + newElement: { + type: { + componentType: { + model: { + timedLyricsModel: { + lyricsData: { + timedLyricsData: SyncedLyricLine[]; + }; + }; + }; + }; + }; + }; + }; + messageRenderer: { + text: PlainLyricsTextRenderer; + }; + sectionListRenderer: { + contents: { + musicDescriptionShelfRenderer: { + description: PlainLyricsTextRenderer; + }; + }[]; + }; + }; +} + +interface SyncedLyricLine { + lyricLine: string; + cueRange: CueRange; +} + +interface CueRange { + startTimeMilliseconds: string; + endTimeMilliseconds: string; +} + +interface PlainLyricsTextRenderer { + runs: { + text: string; + }[]; +} diff --git a/src/plugins/synced-lyrics/providers/index.ts b/src/plugins/synced-lyrics/providers/index.ts new file mode 100644 index 00000000..3f413c96 --- /dev/null +++ b/src/plugins/synced-lyrics/providers/index.ts @@ -0,0 +1,189 @@ +import { createStore } from 'solid-js/store'; + +import { createMemo } from 'solid-js'; + +import { SongInfo } from '@/providers/song-info'; + +import { LRCLib } from './LRCLib'; +import { LyricsGenius } from './LyricsGenius'; +import { YTMusic } from './YTMusic'; + +import { getSongInfo } from '@/providers/song-info-front'; + +import type { LyricProvider, LyricResult } from '../types'; + +export const providers = { + YTMusic: new YTMusic(), + LRCLib: new LRCLib(), + LyricsGenius: new LyricsGenius(), + // MusixMatch: new MusixMatch(), + // Megalobiz: new Megalobiz(), // Disabled because it is too unstable and slow +} as const; + +export type ProviderName = keyof typeof providers; +export const providerNames = Object.keys(providers) as ProviderName[]; + +export type ProviderState = { + state: 'fetching' | 'done' | 'error'; + data: LyricResult | null; + error: Error | null; +}; + +type LyricsStore = { + provider: ProviderName; + current: ProviderState; + lyrics: Record<ProviderName, ProviderState>; +}; + +const initialData = () => + providerNames.reduce( + (acc, name) => { + acc[name] = { state: 'fetching', data: null, error: null }; + return acc; + }, + {} as LyricsStore['lyrics'], + ); + +export const [lyricsStore, setLyricsStore] = createStore<LyricsStore>({ + provider: providerNames[0], + lyrics: initialData(), + get current(): ProviderState { + return this.lyrics[this.provider]; + }, +}); + +export const currentLyrics = createMemo(() => { + const provider = lyricsStore.provider; + return lyricsStore.lyrics[provider]; +}); + +type VideoId = string; + +type SearchCacheData = Record<ProviderName, ProviderState>; +interface SearchCache { + state: 'loading' | 'done'; + data: SearchCacheData; +} + +// TODO: Maybe use localStorage for the cache. +const searchCache = new Map<VideoId, SearchCache>(); +export const fetchLyrics = (info: SongInfo) => { + if (searchCache.has(info.videoId)) { + const cache = searchCache.get(info.videoId)!; + + if (cache.state === 'loading') { + setTimeout(() => { + fetchLyrics(info); + }); + return; + } + + if (getSongInfo().videoId === info.videoId) { + setLyricsStore('lyrics', () => { + // weird bug with solid-js + return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data; + }); + } + + return; + } + + const cache: SearchCache = { + state: 'loading', + data: initialData(), + }; + + searchCache.set(info.videoId, cache); + if (getSongInfo().videoId === info.videoId) { + setLyricsStore('lyrics', () => { + // weird bug with solid-js + return JSON.parse(JSON.stringify(cache.data)) as typeof cache.data; + }); + } + + const tasks: Promise<void>[] = []; + + // prettier-ignore + for ( + const [providerName, provider] of Object.entries(providers) as [ + ProviderName, + LyricProvider, + ][] + ) { + const pCache = cache.data[providerName]; + + tasks.push( + provider + .search(info) + .then((res) => { + pCache.state = 'done'; + pCache.data = res; + + if (getSongInfo().videoId === info.videoId) { + setLyricsStore('lyrics', (old) => { + return { + ...old, + [providerName]: { + state: 'done', + data: res ? { ...res } : null, + error: null, + }, + }; + }); + } + }) + .catch((error: Error) => { + pCache.state = 'error'; + pCache.error = error; + + if (getSongInfo().videoId === info.videoId) { + setLyricsStore('lyrics', (old) => { + return { + ...old, + [providerName]: { state: 'error', error, data: null }, + }; + }); + } + }), + ); + } + + Promise.allSettled(tasks).then(() => { + cache.state = 'done'; + searchCache.set(info.videoId, cache); + }); +}; + +export const retrySearch = (provider: ProviderName, info: SongInfo) => { + setLyricsStore('lyrics', (old) => { + const pCache = { + state: 'fetching', + data: null, + error: null, + }; + + return { + ...old, + [provider]: pCache, + }; + }); + + providers[provider] + .search(info) + .then((res) => { + setLyricsStore('lyrics', (old) => { + return { + ...old, + [provider]: { state: 'done', data: res, error: null }, + }; + }); + }) + .catch((error) => { + setLyricsStore('lyrics', (old) => { + return { + ...old, + [provider]: { state: 'error', data: null, error }, + }; + }); + }); +}; diff --git a/src/plugins/synced-lyrics/renderer/components/ErrorDisplay.tsx b/src/plugins/synced-lyrics/renderer/components/ErrorDisplay.tsx new file mode 100644 index 00000000..f5f14ac7 --- /dev/null +++ b/src/plugins/synced-lyrics/renderer/components/ErrorDisplay.tsx @@ -0,0 +1,64 @@ +import { t } from '@/i18n'; + +import { getSongInfo } from '@/providers/song-info-front'; + +import { lyricsStore, retrySearch } from '../../providers'; + +interface ErrorDisplayProps { + error: Error; +} + +// prettier-ignore +export const ErrorDisplay = (props: ErrorDisplayProps) => { + return ( + <div style={{ 'margin-bottom': '5%' }}> + <pre + style={{ + 'background-color': 'var(--ytmusic-color-black1)', + 'border-radius': '8px', + 'color': '#58f000', + 'max-width': '100%', + 'margin-top': '1em', + 'margin-bottom': '0', + 'padding': '0.5em', + 'font-family': 'serif', + 'font-size': 'large', + }} + > + {t('plugins.synced-lyrics.errors.fetch')} + </pre> + <pre + style={{ + 'background-color': 'var(--ytmusic-color-black1)', + 'border-radius': '8px', + 'color': '#f0a500', + 'white-space': 'pre', + 'overflow-x': 'auto', + 'max-width': '100%', + 'margin-top': '0.5em', + 'padding': '0.5em', + 'font-family': 'monospace', + 'font-size': 'large', + }} + > + {props.error.stack} + </pre> + + <yt-button-renderer + onClick={() => retrySearch(lyricsStore.provider, getSongInfo())} + data={{ + icon: { iconType: 'REFRESH' }, + isDisabled: false, + style: 'STYLE_DEFAULT', + text: { + simpleText: t('plugins.synced-lyrics.refetch-btn.normal') + }, + }} + style={{ + 'margin-top': '1em', + 'width': '100%' + }} + /> + </div> + ); +}; diff --git a/src/plugins/synced-lyrics/renderer/components/LoadingKaomoji.tsx b/src/plugins/synced-lyrics/renderer/components/LoadingKaomoji.tsx new file mode 100644 index 00000000..f16a5dd9 --- /dev/null +++ b/src/plugins/synced-lyrics/renderer/components/LoadingKaomoji.tsx @@ -0,0 +1,33 @@ +import { createSignal, onMount } from 'solid-js'; + +const states = [ + '(>_<)', + '{ (>_<) }', + '{{ (>_<) }}', + '{{{ (>_<) }}}', + '{{ (>_<) }}', + '{ (>_<) }', +]; +export const LoadingKaomoji = () => { + const [counter, setCounter] = createSignal(0); + + onMount(() => { + const interval = setInterval(() => setCounter((old) => old + 1), 500); + return () => clearInterval(interval); + }); + + return ( + <yt-formatted-string + class="text-lyrics description ytmusic-description-shelf-renderer" + style={{ + 'display': 'inline-flex', + 'justify-content': 'center', + 'width': '100%', + 'user-select': 'none', + }} + text={{ + runs: [{ text: states[counter() % states.length] }], + }} + /> + ); +}; diff --git a/src/plugins/synced-lyrics/renderer/components/LyricsContainer.tsx b/src/plugins/synced-lyrics/renderer/components/LyricsContainer.tsx index 991c68f2..6d0e244d 100644 --- a/src/plugins/synced-lyrics/renderer/components/LyricsContainer.tsx +++ b/src/plugins/synced-lyrics/renderer/components/LyricsContainer.tsx @@ -2,145 +2,54 @@ import { createSignal, For, Match, Show, Switch } from 'solid-js'; import { SyncedLine } from './SyncedLine'; -import { t } from '@/i18n'; -import { getSongInfo } from '@/providers/song-info-front'; +import { ErrorDisplay } from './ErrorDisplay'; +import { LoadingKaomoji } from './LoadingKaomoji'; +import { PlainLyrics } from './PlainLyrics'; -import { - differentDuration, - hadSecondAttempt, - isFetching, - isInstrumental, - makeLyricsRequest, -} from '../lyrics/fetch'; - -import type { LineLyrics } from '../../types'; +import { currentLyrics, lyricsStore } from '../../providers'; export const [debugInfo, setDebugInfo] = createSignal<string>(); -export const [lineLyrics, setLineLyrics] = createSignal<LineLyrics[]>([]); export const [currentTime, setCurrentTime] = createSignal<number>(-1); +// prettier-ignore export const LyricsContainer = () => { - const [error, setError] = createSignal(''); - - const onRefetch = async () => { - if (isFetching()) return; - setError(''); - - const info = getSongInfo(); - await makeLyricsRequest(info).catch((err) => { - setError(String(err)); - }); - }; - return ( - <div class={'lyric-container'}> + <div class="lyric-container"> <Switch> - <Match when={isFetching()}> - <div style="margin-bottom: 8px;"> - <tp-yt-paper-spinner-lite - active - class="loading-indicator style-scope" - /> - </div> + <Match when={currentLyrics()?.state === 'fetching'}> + <LoadingKaomoji /> </Match> - <Match when={error()}> + <Match when={!currentLyrics().data?.lines && !currentLyrics().data?.lyrics}> <yt-formatted-string - class="warning-lyrics description ytmusic-description-shelf-renderer" + class="text-lyrics description ytmusic-description-shelf-renderer" + style={{ + 'display': 'inline-flex', + 'justify-content': 'center', + 'width': '100%', + 'user-select': 'none', + }} text={{ - runs: [ - { - text: t('plugins.synced-lyrics.errors.fetch'), - }, - ], + runs: [{ text: '\(〇_o)/' }], }} /> </Match> </Switch> + <Show when={lyricsStore.current.error}> + <ErrorDisplay error={lyricsStore.current.error!} /> + </Show> + <Switch> - <Match when={!lineLyrics().length}> - <Show - when={isInstrumental()} - fallback={ - <> - <yt-formatted-string - class="warning-lyrics description ytmusic-description-shelf-renderer" - text={{ - runs: [ - { - text: t('plugins.synced-lyrics.errors.not-found'), - }, - ], - }} - style={'margin-bottom: 16px;'} - /> - <yt-button-renderer - disabled={isFetching()} - data={{ - icon: { iconType: 'REFRESH' }, - isDisabled: false, - style: 'STYLE_DEFAULT', - text: { - simpleText: isFetching() - ? t('plugins.synced-lyrics.refetch-btn.fetching') - : t('plugins.synced-lyrics.refetch-btn.normal'), - }, - }} - onClick={onRefetch} - /> - </> - } - > - <yt-formatted-string - class="warning-lyrics description ytmusic-description-shelf-renderer" - text={{ - runs: [ - { - text: t('plugins.synced-lyrics.warnings.instrumental'), - }, - ], - }} - /> - </Show> + <Match when={currentLyrics().data?.lines}> + <For each={currentLyrics().data?.lines}> + {(item) => <SyncedLine line={item} />} + </For> </Match> - <Match when={lineLyrics().length && !hadSecondAttempt()}> - <yt-formatted-string - class="warning-lyrics description ytmusic-description-shelf-renderer" - text={{ - runs: [ - { - text: t('plugins.synced-lyrics.warnings.inexact'), - }, - ], - }} - /> - </Match> - <Match when={lineLyrics().length && !differentDuration()}> - <yt-formatted-string - class="warning-lyrics description ytmusic-description-shelf-renderer" - text={{ - runs: [ - { - text: t('plugins.synced-lyrics.warnings.duration-mismatch'), - }, - ], - }} - /> + + <Match when={currentLyrics().data?.lyrics}> + <PlainLyrics lyrics={currentLyrics().data?.lyrics!} /> </Match> </Switch> - - <For each={lineLyrics()}>{(item) => <SyncedLine line={item} />}</For> - - <yt-formatted-string - class="footer style-scope ytmusic-description-shelf-renderer" - text={{ - runs: [ - { - text: 'Source: LRCLIB', - }, - ], - }} - /> </div> ); }; diff --git a/src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx b/src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx new file mode 100644 index 00000000..e29f53a8 --- /dev/null +++ b/src/plugins/synced-lyrics/renderer/components/LyricsPicker.tsx @@ -0,0 +1,198 @@ +import { + createEffect, + createMemo, + createSignal, + For, + Index, + Match, + onMount, + Switch, +} from 'solid-js'; + +import { + currentLyrics, + lyricsStore, + ProviderName, + providerNames, + ProviderState, + setLyricsStore, +} from '../../providers'; + +import { _ytAPI } from '../index'; + +import type { YtIcons } from '@/types/icons'; + +export const providerIdx = createMemo(() => + providerNames.indexOf(lyricsStore.provider), +); + +const shouldSwitchProvider = (providerData: ProviderState) => { + if (providerData.state === 'error') return true; + if (providerData.state === 'fetching') return true; + return ( + providerData.state === 'done' && + !providerData.data?.lines && + !providerData.data?.lyrics + ); +}; + +const providerBias = (p: ProviderName) => + (lyricsStore.lyrics[p].state === 'done' ? 1 : -1) + + (lyricsStore.lyrics[p].data?.lines?.length ? 2 : -1) + + (lyricsStore.lyrics[p].data?.lines?.length && p === 'YTMusic' ? 1 : 0) + + (lyricsStore.lyrics[p].data?.lyrics ? 1 : -1); + +// prettier-ignore +const pickBestProvider = () => { + const providers = Array.from(providerNames); + + providers.sort((a, b) => providerBias(b) - providerBias(a)); + + return providers[0]; +}; + +// prettier-ignore +export const LyricsPicker = () => { + const [hasManuallySwitchedProvider, setHasManuallySwitchedProvider] = createSignal(false); + createEffect(() => { + // fallback to the next source, if the current one has an error + if (!hasManuallySwitchedProvider() + ) { + const bestProvider = pickBestProvider(); + + const allProvidersFailed = providerNames.every((p) => shouldSwitchProvider(lyricsStore.lyrics[p])); + if (allProvidersFailed) return; + + if (providerBias(lyricsStore.provider) < providerBias(bestProvider)) { + setLyricsStore('provider', bestProvider); + } + } + }); + + onMount(() => { + const listener = (name: string) => { + if (name !== 'dataloaded') return; + setHasManuallySwitchedProvider(false); + }; + + _ytAPI?.addEventListener('videodatachange', listener); + return () => _ytAPI?.removeEventListener('videodatachange', listener); + }); + + const next = (automatic: boolean = false) => { + if (!automatic) setHasManuallySwitchedProvider(true); + setLyricsStore('provider', (prevProvider) => { + const idx = providerNames.indexOf(prevProvider); + return providerNames[(idx + 1) % providerNames.length]; + }); + }; + + const previous = (automatic: boolean = false) => { + if (!automatic) setHasManuallySwitchedProvider(true); + setLyricsStore('provider', (prevProvider) => { + const idx = providerNames.indexOf(prevProvider); + return providerNames[(idx + providerNames.length - 1) % providerNames.length]; + }); + }; + + const chevronLeft: YtIcons = 'yt-icons:chevron_left'; + const chevronRight: YtIcons = 'yt-icons:chevron_right'; + + const successIcon: YtIcons = 'yt-icons:check-circle'; + const errorIcon: YtIcons = 'yt-icons:error'; + const notFoundIcon: YtIcons = 'yt-icons:warning'; + + + return ( + <div class="lyrics-picker"> + <div class="lyrics-picker-left"> + <tp-yt-paper-icon-button icon={chevronLeft} onClick={() => previous()} /> + </div> + + <div class="lyrics-picker-content"> + <div class="lyrics-picker-content-label"> + <Index each={providerNames}> + {(provider) => ( + <div + class="lyrics-picker-item" + tabindex="-1" + style={{ + transform: `translateX(${providerIdx() * -100 - 5}%)`, + }} + > + <Switch> + <Match + when={ + // prettier-ignore + currentLyrics().state === 'fetching' + } + > + <tp-yt-paper-spinner-lite + active + tabindex="-1" + class="loading-indicator style-scope" + style={{ padding: '5px', transform: 'scale(0.5)' }} + /> + </Match> + <Match when={currentLyrics().state === 'error'}> + <tp-yt-paper-icon-button + icon={errorIcon} + tabindex="-1" + style={{ padding: '5px', transform: 'scale(0.5)' }} + /> + </Match> + <Match + when={ + currentLyrics().state === 'done' && + (currentLyrics().data?.lines || + currentLyrics().data?.lyrics) + } + > + <tp-yt-paper-icon-button + icon={successIcon} + tabindex="-1" + style={{ padding: '5px', transform: 'scale(0.5)' }} + /> + </Match> + <Match when={ + currentLyrics().state === 'done' + && !currentLyrics().data?.lines + && !currentLyrics().data?.lyrics + }> + <tp-yt-paper-icon-button + icon={notFoundIcon} + tabindex="-1" + style={{ padding: '5px', transform: 'scale(0.5)' }} + /> + </Match> + </Switch> + <yt-formatted-string + class="description ytmusic-description-shelf-renderer" + text={{ runs: [{ text: provider() }] }} + /> + </div> + )} + </Index> + </div> + + <ul class="lyrics-picker-content-dots"> + <For each={providerNames}> + {(_, idx) => ( + <li + class="lyrics-picker-dot" + onClick={() => setLyricsStore('provider', providerNames[idx()])} + style={{ + background: idx() === providerIdx() ? 'white' : 'black', + }} + /> + )} + </For> + </ul> + </div> + + <div class="lyrics-picker-right"> + <tp-yt-paper-icon-button icon={chevronRight} onClick={() => next()} /> + </div> + </div> + ); +}; diff --git a/src/plugins/synced-lyrics/renderer/components/PlainLyrics.tsx b/src/plugins/synced-lyrics/renderer/components/PlainLyrics.tsx new file mode 100644 index 00000000..c93326fd --- /dev/null +++ b/src/plugins/synced-lyrics/renderer/components/PlainLyrics.tsx @@ -0,0 +1,30 @@ +import { createMemo, For } from 'solid-js'; + +interface PlainLyricsProps { + lyrics: string; +} + +export const PlainLyrics = (props: PlainLyricsProps) => { + const lines = createMemo(() => props.lyrics.split('\n')); + + return ( + <div class="plain-lyrics"> + <For each={lines()}> + {(line) => { + if (line.trim() === '') { + return <br />; + } else { + return ( + <yt-formatted-string + class="text-lyrics description ytmusic-description-shelf-renderer" + text={{ + runs: [{ text: line }], + }} + /> + ); + } + }} + </For> + </div> + ); +}; diff --git a/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx b/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx index 2c54bfeb..ed0d7dc5 100644 --- a/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx +++ b/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx @@ -20,16 +20,22 @@ export const SyncedLine = ({ line }: SyncedLineProps) => { return 'current'; }); - let ref: HTMLDivElement; + let ref: HTMLDivElement | undefined; createEffect(() => { if (status() === 'current') { - ref.scrollIntoView({ behavior: 'smooth', block: 'center' }); + ref?.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }); + const text = createMemo(() => { + if (line.text.trim()) return line.text; + return config()?.defaultTextString ?? ''; + }); + + // prettier-ignore return ( <div - ref={ref!} + ref={ref} class={`synced-line ${status()}`} onClick={() => { _ytAPI?.seekTo(line.timeInMs / 1000); @@ -39,13 +45,8 @@ export const SyncedLine = ({ line }: SyncedLineProps) => { class="text-lyrics description ytmusic-description-shelf-renderer" text={{ runs: [ - { - text: '', - }, - { - text: `${config()?.showTimeCodes ? `[${line.time}] ` : ''}${line.text}`, - }, - ], + { text: config()?.showTimeCodes ? `[${line.time}]` : '' }, + { text: text() }], }} /> </div> diff --git a/src/plugins/synced-lyrics/renderer/index.ts b/src/plugins/synced-lyrics/renderer/index.ts index e18a81ec..06762b49 100644 --- a/src/plugins/synced-lyrics/renderer/index.ts +++ b/src/plugins/synced-lyrics/renderer/index.ts @@ -1,15 +1,15 @@ import { createRenderer } from '@/utils'; import { waitForElement } from '@/utils/wait-for-element'; -import { makeLyricsRequest } from './lyrics'; import { selectors, tabStates } from './utils'; import { setConfig } from './renderer'; import { setCurrentTime } from './components/LyricsContainer'; +import { fetchLyrics } from '../providers'; + import type { RendererContext } from '@/types/contexts'; import type { YoutubePlayer } from '@/types/youtube-player'; import type { SongInfo } from '@/providers/song-info'; - import type { SyncedLyricsPluginConfig } from '../types'; export let _ytAPI: YoutubePlayer | null = null; @@ -36,9 +36,7 @@ export const renderer = createRenderer< header.removeAttribute('disabled'); break; case 'aria-selected': - tabStates[header.ariaSelected as 'true' | 'false']?.( - _ytAPI?.getVideoData(), - ); + tabStates[header.ariaSelected ?? 'false'](); break; } } @@ -51,7 +49,6 @@ export const renderer = createRenderer< await this.videoDataChange(); }, - async videoDataChange() { if (!this.updateTimestampInterval) { this.updateTimestampInterval = setInterval( @@ -60,12 +57,17 @@ export const renderer = createRenderer< ); } + // prettier-ignore this.observer ??= new MutationObserver(this.observerCallback); - - // Force the lyrics tab to be enabled at all times. this.observer.disconnect(); + // Force the lyrics tab to be enabled at all times. const header = await waitForElement<HTMLElement>(selectors.head); + { + header.removeAttribute('disabled'); + tabStates[header.ariaSelected ?? 'false'](); + } + this.observer.observe(header, { attributes: true }); header.removeAttribute('disabled'); }, @@ -73,8 +75,8 @@ export const renderer = createRenderer< async start(ctx: RendererContext<SyncedLyricsPluginConfig>) { setConfig(await ctx.getConfig()); - ctx.ipc.on('ytmd:update-song-info', async (info: SongInfo) => { - await makeLyricsRequest(info); + ctx.ipc.on('ytmd:update-song-info', (info: SongInfo) => { + fetchLyrics(info); }); }, }); diff --git a/src/plugins/synced-lyrics/renderer/lyrics/fetch.ts b/src/plugins/synced-lyrics/renderer/lyrics/fetch.ts deleted file mode 100644 index 2a4f6be4..00000000 --- a/src/plugins/synced-lyrics/renderer/lyrics/fetch.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { createSignal } from 'solid-js'; -import { jaroWinkler } from '@skyra/jaro-winkler'; - -import { config } from '../renderer'; - -import { setDebugInfo, setLineLyrics } from '../components/LyricsContainer'; - -import type { SongInfo } from '@/providers/song-info'; -import type { LineLyrics, LRCLIBSearchResponse } from '../../types'; - -export const [isInstrumental, setIsInstrumental] = createSignal(false); -export const [isFetching, setIsFetching] = createSignal(false); -export const [hadSecondAttempt, setHadSecondAttempt] = createSignal(false); -export const [differentDuration, setDifferentDuration] = createSignal(false); - -export const extractTimeAndText = ( - line: string, - index: number, -): LineLyrics | null => { - const groups = /\[(\d+):(\d+)\.(\d+)](.+)/.exec(line); - if (!groups) return null; - - const [, rMinutes, rSeconds, rMillis, text] = groups; - const [minutes, seconds, millis] = [ - parseInt(rMinutes), - parseInt(rSeconds), - parseInt(rMillis), - ]; - - const timeInMs = minutes * 60 * 1000 + seconds * 1000 + millis; - - return { - index, - timeInMs, - time: `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}:${millis}`, - text: text?.trim() || config()!.defaultTextString, - status: 'upcoming', - duration: 0, - }; -}; - -export const makeLyricsRequest = async (extractedSongInfo: SongInfo) => { - setIsFetching(true); - setLineLyrics([]); - - const songData: Parameters<typeof getLyricsList>[0] = { - title: `${extractedSongInfo.title}`, - artist: `${extractedSongInfo.artist}`, - songDuration: extractedSongInfo.songDuration, - }; - - if (extractedSongInfo.album) { - songData.album = extractedSongInfo.album; - } - - let lyrics; - try { - lyrics = await getLyricsList(songData); - } catch {} - - setLineLyrics(lyrics ?? []); - setIsFetching(false); -}; - -export const getLyricsList = async ( - songData: Pick<SongInfo, 'title' | 'artist' | 'album' | 'songDuration'>, -): Promise<LineLyrics[] | null> => { - setIsInstrumental(false); - setHadSecondAttempt(false); - setDifferentDuration(false); - setDebugInfo('Searching for lyrics...'); - - let query = new URLSearchParams({ - artist_name: songData.artist, - track_name: songData.title, - }); - - if (songData.album) { - query.set('album_name', songData.album); - } - - let url = `https://lrclib.net/api/search?${query.toString()}`; - let response = await fetch(url); - - if (!response.ok) { - setDebugInfo('Got non-OK response from server.'); - return null; - } - - let data = (await response.json()) as LRCLIBSearchResponse; - if (!data || !Array.isArray(data)) { - setDebugInfo('Unexpected server response.'); - return null; - } - - // Note: If no lyrics are found, try again with a different search query - if (data.length === 0) { - if (!config()?.showLyricsEvenIfInexact) { - return null; - } - - query = new URLSearchParams({ q: songData.title }); - url = `https://lrclib.net/api/search?${query.toString()}`; - - response = await fetch(url); - if (!response.ok) { - setDebugInfo('Got non-OK response from server. (2)'); - return null; - } - - data = (await response.json()) as LRCLIBSearchResponse; - if (!Array.isArray(data)) { - setDebugInfo('Unexpected server response. (2)'); - return null; - } - - setHadSecondAttempt(true); - } - - const filteredResults: LRCLIBSearchResponse = []; - for (const item of data) { - const { artist } = songData; - const { artistName } = item; - - const artists = artist.split(/[&,]/g).map((i) => i.trim()); - const itemArtists = artistName.split(/[&,]/g).map((i) => i.trim()); - - const permutations = artists.flatMap((artistA) => - itemArtists.map((artistB) => [ - artistA.toLowerCase(), - artistB.toLowerCase(), - ]), - ); - - const ratio = Math.max(...permutations.map(([x, y]) => jaroWinkler(x, y))); - if (ratio > 0.9) filteredResults.push(item); - } - - const duration = songData.songDuration; - filteredResults.sort(({ duration: durationA }, { duration: durationB }) => { - const left = Math.abs(durationA - duration); - const right = Math.abs(durationB - duration); - - return left - right; - }); - - const closestResult = filteredResults[0]; - if (!closestResult) { - setDebugInfo('No search result matched the criteria.'); - return null; - } - - setDebugInfo(JSON.stringify(closestResult, null, 4)); - - if (Math.abs(closestResult.duration - duration) > 15) { - return null; - } - - if (Math.abs(closestResult.duration - duration) > 5) { - // show message that the timings may be wrong - setDifferentDuration(true); - } - - setIsInstrumental(closestResult.instrumental); - if (closestResult.instrumental) { - return null; - } - - // Separate the lyrics into lines - const raw = closestResult.syncedLyrics?.split('\n') ?? []; - if (!raw.length) { - return null; - } - - // Add a blank line at the beginning - raw.unshift('[0:0.0] '); - - const syncedLyricList = raw.reduce<LineLyrics[]>((acc, line) => { - const syncedLine = extractTimeAndText(line, acc.length); - if (syncedLine) { - acc.push(syncedLine); - } - - return acc; - }, []); - - for (const line of syncedLyricList) { - const next = syncedLyricList[line.index + 1]; - if (!next) { - line.duration = Infinity; - break; - } - - line.duration = next.timeInMs - line.timeInMs; - } - - return syncedLyricList; -}; diff --git a/src/plugins/synced-lyrics/renderer/lyrics/index.ts b/src/plugins/synced-lyrics/renderer/lyrics/index.ts deleted file mode 100644 index 13736d54..00000000 --- a/src/plugins/synced-lyrics/renderer/lyrics/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { createEffect } from 'solid-js'; - -import { config } from '../renderer'; - -export { makeLyricsRequest } from './fetch'; - -createEffect(() => { - if (!config()?.enabled) return; - const root = document.documentElement; - - // Set the line effect - switch (config()?.lineEffect) { - case 'scale': - root.style.setProperty( - '--previous-lyrics', - 'var(--ytmusic-text-primary)', - ); - root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)'); - root.style.setProperty('--size-lyrics', '1.2'); - root.style.setProperty('--offset-lyrics', '0'); - root.style.setProperty('--lyric-width', '83%'); - break; - case 'offset': - root.style.setProperty( - '--previous-lyrics', - 'var(--ytmusic-text-primary)', - ); - root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)'); - root.style.setProperty('--size-lyrics', '1'); - root.style.setProperty('--offset-lyrics', '5%'); - root.style.setProperty('--lyric-width', '100%'); - break; - case 'focus': - root.style.setProperty( - '--previous-lyrics', - 'var(--ytmusic-text-secondary)', - ); - root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)'); - root.style.setProperty('--size-lyrics', '1'); - root.style.setProperty('--offset-lyrics', '0'); - root.style.setProperty('--lyric-width', '100%'); - break; - } -}); diff --git a/src/plugins/synced-lyrics/renderer/renderer.tsx b/src/plugins/synced-lyrics/renderer/renderer.tsx index 6ea6bd42..2c90df86 100644 --- a/src/plugins/synced-lyrics/renderer/renderer.tsx +++ b/src/plugins/synced-lyrics/renderer/renderer.tsx @@ -1,22 +1,93 @@ -import { createSignal, Show } from 'solid-js'; +import { createEffect, createSignal, onMount, Show } from 'solid-js'; import { LyricsContainer } from './components/LyricsContainer'; +import { LyricsPicker } from './components/LyricsPicker'; + +import { selectors } from './utils'; -import type { VideoDetails } from '@/types/video-details'; import type { SyncedLyricsPluginConfig } from '../types'; export const [isVisible, setIsVisible] = createSignal<boolean>(false); - export const [config, setConfig] = createSignal<SyncedLyricsPluginConfig | null>(null); -export const [playerState, setPlayerState] = createSignal<VideoDetails | null>( - null, -); + +createEffect(() => { + if (!config()?.enabled) return; + const root = document.documentElement; + + // Set the line effect + switch (config()?.lineEffect) { + case 'scale': + root.style.setProperty( + '--previous-lyrics', + 'var(--ytmusic-text-primary)', + ); + root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)'); + root.style.setProperty('--size-lyrics', '1.2'); + root.style.setProperty('--offset-lyrics', '0'); + root.style.setProperty('--lyric-width', '83%'); + break; + case 'offset': + root.style.setProperty( + '--previous-lyrics', + 'var(--ytmusic-text-primary)', + ); + root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)'); + root.style.setProperty('--size-lyrics', '1'); + root.style.setProperty('--offset-lyrics', '5%'); + root.style.setProperty('--lyric-width', '100%'); + break; + case 'focus': + root.style.setProperty( + '--previous-lyrics', + 'var(--ytmusic-text-secondary)', + ); + root.style.setProperty('--current-lyrics', 'var(--ytmusic-text-primary)'); + root.style.setProperty('--size-lyrics', '1'); + root.style.setProperty('--offset-lyrics', '0'); + root.style.setProperty('--lyric-width', '100%'); + break; + } +}); export const LyricsRenderer = () => { + const [stickyRef, setStickRef] = createSignal<HTMLElement | null>(null); + + // prettier-ignore + onMount(() => { + const tab = document.querySelector<HTMLElement>(selectors.body.tabRenderer)!; + + const mousemoveListener = (e: MouseEvent) => { + const { top } = tab.getBoundingClientRect(); + const { clientHeight: height } = stickyRef()!; + + const showPicker = (e.clientY - top - 5) <= height; + if (showPicker) { + // picker visible + stickyRef()!.style.setProperty('--top', '0'); + } else { + // picker hidden + stickyRef()!.style.setProperty('--top', '-50%'); + } + }; + + tab.addEventListener('mousemove', mousemoveListener); + return () => tab.removeEventListener('mousemove', mousemoveListener); + }); + return ( <Show when={isVisible()}> - <LyricsContainer /> + <div class="lyrics-renderer"> + <div class="lyrics-renderer-sticky" ref={setStickRef}> + <LyricsPicker /> + <div + id="divider" + class="style-scope ytmusic-guide-section-renderer" + style={{ width: '100%', margin: '0' }} + ></div> + </div> + <LyricsContainer /> + </div> </Show> ); }; diff --git a/src/plugins/synced-lyrics/renderer/utils.tsx b/src/plugins/synced-lyrics/renderer/utils.tsx index 912ec49f..e51f976e 100644 --- a/src/plugins/synced-lyrics/renderer/utils.tsx +++ b/src/plugins/synced-lyrics/renderer/utils.tsx @@ -1,10 +1,7 @@ import { render } from 'solid-js/web'; import { waitForElement } from '@/utils/wait-for-element'; - -import { LyricsRenderer, setIsVisible, setPlayerState } from './renderer'; - -import type { VideoDetails } from '@/types/video-details'; +import { LyricsRenderer, setIsVisible } from './renderer'; export const selectors = { head: '#tabsContent > .tab-header:nth-of-type(2)', @@ -14,10 +11,9 @@ export const selectors = { }, }; -export const tabStates = { - true: async (data?: VideoDetails) => { +export const tabStates: Record<string, () => void> = { + true: async () => { setIsVisible(true); - setPlayerState(data ?? null); let container = document.querySelector('#synced-lyrics-container'); if (container) return; diff --git a/src/plugins/synced-lyrics/style.css b/src/plugins/synced-lyrics/style.css index 8795713e..493a84df 100644 --- a/src/plugins/synced-lyrics/style.css +++ b/src/plugins/synced-lyrics/style.css @@ -3,7 +3,8 @@ display: none !important; } -#tab-renderer[page-type='MUSIC_PAGE_TYPE_TRACK_LYRICS'] > #synced-lyrics-container { +#tab-renderer[page-type='MUSIC_PAGE_TYPE_TRACK_LYRICS'] + > #synced-lyrics-container { display: block !important; } @@ -32,10 +33,10 @@ .synced-line { width: var(--lyric-width, 100%); -} -.synced-line > .text-lyrics { - cursor: pointer; + & > .text-lyrics { + cursor: pointer; + } } .synced-lyrics { @@ -56,10 +57,17 @@ display: block; text-align: left; margin: var(--global-margin) 0; - transition: scale 0.3s ease-in-out, translate 0.3s ease-in-out, color 0.1s ease-in-out; + transition: + scale 0.3s ease-in-out, + translate 0.3s ease-in-out, + color 0.1s ease-in-out; transform-origin: 0 50%; } +.text-lyrics > span { + margin-inline: 0.1em; +} + .previous > .text-lyrics { color: var(--previous-lyrics); font-weight: normal; @@ -76,3 +84,81 @@ color: var(--upcoming-lyrics); font-weight: normal; } + +.lyrics-renderer { + display: flex; + flex-direction: column; +} + +.lyrics-picker { + height: 5em; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; + padding-block: 1em; +} + +.lyrics-picker-content { + display: flex; + width: 50%; + + flex-direction: column; + justify-content: space-around; + align-items: center; +} + +.lyrics-picker-content-label { + display: flex; + overflow: hidden; + width: 100%; + /* padding-block: 5%; */ +} + +.lyrics-picker-content-dots { + display: block; + list-style: none; +} + +.lyrics-picker-item { + display: flex; + height: 100%; + min-width: 100%; + + justify-content: center; + align-items: center; + + transition: transform 0.25s ease-in-out; +} + +.lyrics-picker-dot { + display: inline-block; + cursor: pointer; + width: 5px; + height: 5px; + margin: 0 4px 0; + border-radius: 200px; + border: 1px solid #6e7c7c7f; +} + +.lyrics-picker-left, +.lyrics-picker-right { + display: flex; + justify-content: center; + align-items: center; + transition: background-color 0.3s ease; + border-radius: 25%; + + &:hover { + background-color: hsla(0, 0%, 100%, 0.1); + } +} + +.lyrics-renderer-sticky { + position: sticky; + top: var(--top, 0); + z-index: 100; + background-color: var(--ytmusic-background); + + transition: top 325ms ease-in-out; +} diff --git a/src/plugins/synced-lyrics/types.ts b/src/plugins/synced-lyrics/types.ts index 0d826b27..6928d463 100644 --- a/src/plugins/synced-lyrics/types.ts +++ b/src/plugins/synced-lyrics/types.ts @@ -1,3 +1,5 @@ +import { SongInfo } from '@/providers/song-info'; + export type SyncedLyricsPluginConfig = { enabled: boolean; preciseTiming: boolean; @@ -10,29 +12,30 @@ export type SyncedLyricsPluginConfig = { export type LineLyricsStatus = 'previous' | 'current' | 'upcoming'; export type LineLyrics = { - index: number; time: string; timeInMs: number; - text: string; duration: number; - status: LineLyricsStatus; -}; -export type PlayPauseEvent = { - isPaused: boolean; - elapsedSeconds: number; + text: string; + status: LineLyricsStatus; }; export type LineEffect = 'scale' | 'offset' | 'focus'; -export type LRCLIBSearchResponse = { - id: number; +export interface LyricResult { + title: string; + artists: string[]; + + lyrics?: string; + lines?: LineLyrics[]; +} + +// prettier-ignore +export type SearchSongInfo = Pick<SongInfo, 'title' | 'artist' | 'album' | 'songDuration' | 'videoId'>; + +export interface LyricProvider { name: string; - trackName: string; - artistName: string; - albumName: string; - duration: number; - instrumental: boolean; - plainLyrics: string; - syncedLyrics: string; -}[]; + baseUrl: string; + + search(songInfo: SearchSongInfo): Promise<LyricResult | null>; +} diff --git a/src/types/icons.ts b/src/types/icons.ts new file mode 100644 index 00000000..898ebd78 --- /dev/null +++ b/src/types/icons.ts @@ -0,0 +1,48 @@ +export type GeneralIcons = 'icons:3d-rotation' | 'icons:accessibility' | 'icons:accessible' | 'icons:account-balance' | 'icons:account-balance-wallet' | 'icons:account-box' | 'icons:account-circle' | 'icons:add' | 'icons:add-alert' | 'icons:add-box' | 'icons:add-circle' | 'icons:add-circle-outline' | 'icons:add-shopping-cart' | 'icons:alarm' | 'icons:alarm-add' | 'icons:alarm-off' | 'icons:alarm-on' | 'icons:all-out' | 'icons:android' | 'icons:announcement' | 'icons:apps' | 'icons:archive' | 'icons:arrow-back' | 'icons:arrow-downward' | 'icons:arrow-drop-down' | 'icons:arrow-drop-down-circle' | 'icons:arrow-drop-up' | 'icons:arrow-forward' | 'icons:arrow-upward' | 'icons:aspect-ratio' | 'icons:assessment' | 'icons:assignment' | 'icons:assignment-ind' | 'icons:assignment-late' | 'icons:assignment-return' | 'icons:assignment-returned' | 'icons:assignment-turned-in' | 'icons:attachment' | 'icons:autorenew' | 'icons:backspace' | 'icons:backup' | 'icons:block' | 'icons:book' | 'icons:bookmark' | 'icons:bookmark-border' | 'icons:bug-report' | 'icons:build' | 'icons:cached' | 'icons:camera-enhance' | 'icons:cancel' | 'icons:card-giftcard' | 'icons:card-membership' | 'icons:card-travel' | 'icons:change-history' | 'icons:check' | 'icons:check-box' | 'icons:check-box-outline-blank' | 'icons:check-circle' | 'icons:chevron-left' | 'icons:chevron-right' | 'icons:chrome-reader-mode' | 'icons:class' | 'icons:clear' | 'icons:close' | 'icons:cloud' | 'icons:cloud-circle' | 'icons:cloud-done' | 'icons:cloud-download' | 'icons:cloud-off' | 'icons:cloud-queue' | 'icons:cloud-upload' | 'icons:code' | 'icons:compare-arrows' | 'icons:content-copy' | 'icons:content-cut' | 'icons:content-paste' | 'icons:copyright' | 'icons:create' | 'icons:create-new-folder' | 'icons:credit-card' | 'icons:dashboard' | 'icons:date-range' | 'icons:delete' | 'icons:delete-forever' | 'icons:delete-sweep' | 'icons:description' | 'icons:dns' | 'icons:done' | 'icons:done-all' | 'icons:donut-large' | 'icons:donut-small' | 'icons:drafts' | 'icons:eject' | 'icons:error' | 'icons:error-outline' | 'icons:euro-symbol' | 'icons:event' | 'icons:event-seat' | 'icons:exit-to-app' | 'icons:expand-less' | 'icons:expand-more' | 'icons:explore' | 'icons:extension' | 'icons:face' | 'icons:favorite' | 'icons:favorite-border' | 'icons:feedback' | 'icons:file-download' | 'icons:file-upload' | 'icons:filter-list' | 'icons:find-in-page' | 'icons:find-replace' | 'icons:fingerprint' | 'icons:first-page' | 'icons:flag' | 'icons:flight-land' | 'icons:flight-takeoff' | 'icons:flip-to-back' | 'icons:flip-to-front' | 'icons:folder' | 'icons:folder-open' | 'icons:folder-shared' | 'icons:font-download' | 'icons:forward' | 'icons:fullscreen' | 'icons:fullscreen-exit' | 'icons:g-translate' | 'icons:gavel' | 'icons:gesture' | 'icons:get-app' | 'icons:gif' | 'icons:grade' | 'icons:group-work' | 'icons:help' | 'icons:help-outline' | 'icons:highlight-off' | 'icons:history' | 'icons:home' | 'icons:hourglass-empty' | 'icons:hourglass-full' | 'icons:http' | 'icons:https' | 'icons:important-devices' | 'icons:inbox' | 'icons:indeterminate-check-box' | 'icons:info' | 'icons:info-outline' | 'icons:input' | 'icons:invert-colors' | 'icons:label' | 'icons:label-outline' | 'icons:language' | 'icons:last-page' | 'icons:launch' | 'icons:lightbulb-outline' | 'icons:line-style' | 'icons:line-weight' | 'icons:link' | 'icons:list' | 'icons:lock' | 'icons:lock-open' | 'icons:lock-outline' | 'icons:low-priority' | 'icons:loyalty' | 'icons:mail' | 'icons:markunread' | 'icons:markunread-mailbox' | 'icons:menu' | 'icons:more-horiz' | 'icons:more-vert' | 'icons:motorcycle' | 'icons:move-to-inbox' | 'icons:next-week' | 'icons:note-add' | 'icons:offline-pin' | 'icons:opacity' | 'icons:open-in-browser' | 'icons:open-in-new' | 'icons:open-with' | 'icons:pageview' | 'icons:pan-tool' | 'icons:payment' | 'icons:perm-camera-mic' | 'icons:perm-contact-calendar' | 'icons:perm-data-setting' | 'icons:perm-device-information' | 'icons:perm-identity' | 'icons:perm-media' | 'icons:perm-phone-msg' | 'icons:perm-scan-wifi' | 'icons:pets' | 'icons:picture-in-picture' | 'icons:picture-in-picture-alt' | 'icons:play-for-work' | 'icons:polymer' | 'icons:power-settings-new' | 'icons:pregnant-woman' | 'icons:print' | 'icons:query-builder' | 'icons:question-answer' | 'icons:radio-button-checked' | 'icons:radio-button-unchecked' | 'icons:receipt' | 'icons:record-voice-over' | 'icons:redeem' | 'icons:redo' | 'icons:refresh' | 'icons:remove' | 'icons:remove-circle' | 'icons:remove-circle-outline' | 'icons:remove-shopping-cart' | 'icons:reorder' | 'icons:reply' | 'icons:reply-all' | 'icons:report' | 'icons:report-problem' | 'icons:restore' | 'icons:restore-page' | 'icons:room' | 'icons:rounded-corner' | 'icons:rowing' | 'icons:save' | 'icons:schedule' | 'icons:search' | 'icons:select-all' | 'icons:send' | 'icons:settings' | 'icons:settings-applications' | 'icons:settings-backup-restore' | 'icons:settings-bluetooth' | 'icons:settings-brightness' | 'icons:settings-cell' | 'icons:settings-ethernet' | 'icons:settings-input-antenna' | 'icons:settings-input-component' | 'icons:settings-input-composite' | 'icons:settings-input-hdmi' | 'icons:settings-input-svideo' | 'icons:settings-overscan' | 'icons:settings-phone' | 'icons:settings-power' | 'icons:settings-remote' | 'icons:settings-voice' | 'icons:shop' | 'icons:shop-two' | 'icons:shopping-basket' | 'icons:shopping-cart' | 'icons:sort' | 'icons:speaker-notes' | 'icons:speaker-notes-off' | 'icons:spellcheck' | 'icons:star' | 'icons:star-border' | 'icons:star-half' | 'icons:stars' | 'icons:store' | 'icons:subdirectory-arrow-left' | 'icons:subdirectory-arrow-right' | 'icons:subject' | 'icons:supervisor-account' | 'icons:swap-horiz' | 'icons:swap-vert' | 'icons:swap-vertical-circle' | 'icons:system-update-alt' | 'icons:tab' | 'icons:tab-unselected' | 'icons:text-format' | 'icons:theaters' | 'icons:thumb-down' | 'icons:thumb-up' | 'icons:thumbs-up-down' | 'icons:timeline' | 'icons:toc' | 'icons:today' | 'icons:toll' | 'icons:touch-app' | 'icons:track-changes' | 'icons:translate' | 'icons:trending-down' | 'icons:trending-flat' | 'icons:trending-up' | 'icons:turned-in' | 'icons:turned-in-not' | 'icons:unarchive' | 'icons:undo' | 'icons:unfold-less' | 'icons:unfold-more' | 'icons:update' | 'icons:verified-user' | 'icons:view-agenda' | 'icons:view-array' | 'icons:view-carousel' | 'icons:view-column' | 'icons:view-day' | 'icons:view-headline' | 'icons:view-list' | 'icons:view-module' | 'icons:view-quilt' | 'icons:view-stream' | 'icons:view-week' | 'icons:visibility' | 'icons:visibility-off' | 'icons:warning' | 'icons:watch-later' | 'icons:weekend' | 'icons:work' | 'icons:youtube-searched-for' | 'icons:zoom-in' | 'icons:zoom-out'; +export type PaperDropDownMenuIcons = 'paper-dropdown-menu:arrow-drop-down' | 'paper-tabs:chevron-left' | 'paper-tabs:chevron-right'; +export type SocialNetworkIcons = 'socialNetworks:ameba' | 'socialNetworks:bebo' | 'socialNetworks:blogger' | 'socialNetworks:cyworld' | 'socialNetworks:digg' | 'socialNetworks:email' | 'socialNetworks:embed' | 'socialNetworks:facebook' | 'socialNetworks:fotka' | 'socialNetworks:goo' | 'socialNetworks:hi5' | 'socialNetworks:kakao' | 'socialNetworks:linkedin' | 'socialNetworks:livejournal' | 'socialNetworks:mail' | 'socialNetworks:meneame' | 'socialNetworks:mixi' | 'socialNetworks:myspace' | 'socialNetworks:naver' | 'socialNetworks:odnoklassniki' | 'socialNetworks:pinterest' | 'socialNetworks:rakuten' | 'socialNetworks:reddit' | 'socialNetworks:skyblog' | 'socialNetworks:skype' | 'socialNetworks:stumbleupon' | 'socialNetworks:tuenti' | 'socialNetworks:tumblr' | 'socialNetworks:twitter' | 'socialNetworks:vkontakte' | 'socialNetworks:weibo' | 'socialNetworks:wykop' | 'socialNetworks:yahoo' | 'socialNetworksRound:ameba' | 'socialNetworksRound:bebo' | 'socialNetworksRound:blogger' | 'socialNetworksRound:cyworld' | 'socialNetworksRound:digg' | 'socialNetworksRound:email' | 'socialNetworksRound:embed' | 'socialNetworksRound:facebook' | 'socialNetworksRound:fotka' | 'socialNetworksRound:goo' | 'socialNetworksRound:hi5' | 'socialNetworksRound:kakao' | 'socialNetworksRound:linkedin' | 'socialNetworksRound:livejournal' | 'socialNetworksRound:mail' | 'socialNetworksRound:meneame' | 'socialNetworksRound:mixi' | 'socialNetworksRound:myspace' | 'socialNetworksRound:naver' | 'socialNetworksRound:odnoklassniki' | 'socialNetworksRound:pinterest' | 'socialNetworksRound:rakuten' | 'socialNetworksRound:reddit' | 'socialNetworksRound:skyblog' | 'socialNetworksRound:skype' | 'socialNetworksRound:stumbleupon' | 'socialNetworksRound:tuenti' | 'socialNetworksRound:tumblr' | 'socialNetworksRound:twitter' | 'socialNetworksRound:vkontakte' | 'socialNetworksRound:web_system_activity_dialog' | 'socialNetworksRound:weibo' | 'socialNetworksRound:whatsapp' | 'socialNetworksRound:wykop' | 'socialNetworksRound:yahoo' | 'socialNetworksRound:youtube_community_post' | 'socialNetworksRound:youtube_community_repost'; +export type HashtagLandingPageIcons = 'hashtag-landing-page:hashtag_landing_page_empty' | 'hashtag-landing-page:hashtag_landing_page_empty_dark_mode' | 'hashtag-landing-page:hashtag_landing_page_error' | 'hashtag-landing-page:hashtag_landing_page_error_dark_mode'; +export type LiveChatBadgeIcons = 'live-chat-badges:member' | 'live-chat-badges:moderator' | 'live-chat-badges:owner' | 'live-chat-badges:verified'; +export type MiniplayerIcons = 'miniplayer:cast-connected' | 'miniplayer:cast' | 'miniplayer:keyboard-arrow-down' | 'miniplayer:keyboard-arrow-up' | 'miniplayer:miniplayer-expand' | 'miniplayer:pause' | 'miniplayer:play-arrow' | 'miniplayer:skip-next' | 'miniplayer:skip-previous' | 'miniplayer:volume-off' | 'miniplayer:volume-up'; +export type RottenTomatoesIcons = 'rotten-tomatoes:certified' | 'rotten-tomatoes:fresh' | 'rotten-tomatoes:rotten'; +export type SettingsIcons = 'settings:account_advanced' | 'settings:account_notifications' | 'settings:account_playback' | 'settings:account_privacy' | 'settings:account_settings' | 'settings:account_sharing' | 'settings:chrome_icon'; +export type ShortsIcons = 'shorts:shorts-comment' | 'shorts:shorts-dislike' | 'shorts:shorts-like' | 'shorts:shorts-share'; +export type YtIcons = 'yt-icons:accelerator' | 'yt-icons:access_time' | 'yt-icons:account_box' | 'yt-icons:account_circle' | 'yt-icons:account_linked' | 'yt-icons:account_unlinked' | 'yt-icons:add' | 'yt-icons:add_circle' | 'yt-icons:add_friend' | 'yt-icons:add_moderator' | 'yt-icons:add_photo_alternate' | 'yt-icons:add_to_playlist' | 'yt-icons:add_to_queue' | 'yt-icons:add_to_queue_tail' | 'yt-icons:add_video_link' | 'yt-icons:album' | 'yt-icons:align_left' | 'yt-icons:answer_neither_satisfied_nor_dissatisfied' | 'yt-icons:answer_somewhat_dissatisfied' | 'yt-icons:answer_somewhat_satisfied' | 'yt-icons:answer_very_dissatisfied' | 'yt-icons:answer_very_satisfied' | 'yt-icons:applause' | 'yt-icons:apps' | 'yt-icons:arrow-back' | 'yt-icons:arrow-drop-down' | 'yt-icons:arrow-forward' | 'yt-icons:arrow_back' | 'yt-icons:arrow_chart_neutral' | 'yt-icons:arrow_drop_down' | 'yt-icons:arrow_drop_up' | 'yt-icons:arrow_forward' | 'yt-icons:article' | 'yt-icons:artist' | 'yt-icons:audio_badge' | 'yt-icons:audiotrack' | 'yt-icons:auto-awesome' | 'yt-icons:avatar-circle-blue' | 'yt-icons:avatar_anonymous' | 'yt-icons:avatar_logged_out' | 'yt-icons:back' | 'yt-icons:bar_chart' | 'yt-icons:block' | 'yt-icons:bookmark' | 'yt-icons:bookmark_border' | 'yt-icons:breaking_news' | 'yt-icons:breaking_news_alt_1' | 'yt-icons:brightness_three' | 'yt-icons:build' | 'yt-icons:cake' | 'yt-icons:calendar' | 'yt-icons:camera_alt' | 'yt-icons:campaign' | 'yt-icons:channel_notification_preference_off' | 'yt-icons:channel_notification_preference_on' | 'yt-icons:chat_bubble' | 'yt-icons:chat_off' | 'yt-icons:check-circle' | 'yt-icons:check' | 'yt-icons:check_box' | 'yt-icons:check_box_bar' | 'yt-icons:check_box_outline_blank' | 'yt-icons:check_circle_thick' | 'yt-icons:chevron_down' | 'yt-icons:chevron_left' | 'yt-icons:chevron_right' | 'yt-icons:chevron_up' | 'yt-icons:chromecast-filled' | 'yt-icons:chromecast' | 'yt-icons:chromecast_animate_frame-1' | 'yt-icons:chromecast_animate_frame-2' | 'yt-icons:chromecast_animate_frame-3' | 'yt-icons:clarify' | 'yt-icons:close' | 'yt-icons:cloud_upload' | 'yt-icons:collapse' | 'yt-icons:collections' | 'yt-icons:color_lens' | 'yt-icons:colored_gaming_logo' | 'yt-icons:comment' | 'yt-icons:consent_shield' | 'yt-icons:contact_support' | 'yt-icons:content_cut' | 'yt-icons:conversations' | 'yt-icons:copy' | 'yt-icons:course' | 'yt-icons:creation_live' | 'yt-icons:creation_post' | 'yt-icons:creation_upload' | 'yt-icons:creation_upload_red' | 'yt-icons:creator_metadata_monetization' | 'yt-icons:creator_metadata_monetization_off' | 'yt-icons:creator_studio' | 'yt-icons:creator_studio_red_logo' | 'yt-icons:credit_card' | 'yt-icons:dark_theme' | 'yt-icons:delete' | 'yt-icons:delete_sweep' | 'yt-icons:dislike' | 'yt-icons:dislike_outline' | 'yt-icons:dismissal' | 'yt-icons:dogfood' | 'yt-icons:dollar_sign' | 'yt-icons:done_all' | 'yt-icons:down_arrow' | 'yt-icons:drag_handle' | 'yt-icons:edit' | 'yt-icons:email' | 'yt-icons:emoji' | 'yt-icons:emoji_activities' | 'yt-icons:emoji_custom' | 'yt-icons:emoji_flags' | 'yt-icons:emoji_food' | 'yt-icons:emoji_nature' | 'yt-icons:emoji_objects' | 'yt-icons:emoji_people' | 'yt-icons:emoji_recent' | 'yt-icons:emoji_sponsorships' | 'yt-icons:emoji_symbols' | 'yt-icons:emoji_travel' | 'yt-icons:error' | 'yt-icons:error_black' | 'yt-icons:error_outline' | 'yt-icons:event' | 'yt-icons:exit_app' | 'yt-icons:exit_to_app' | 'yt-icons:expand-less' | 'yt-icons:expand-more' | 'yt-icons:expand' | 'yt-icons:explore' | 'yt-icons:external_link' | 'yt-icons:fact_check' | 'yt-icons:fashion_logo' | 'yt-icons:fast_rewind' | 'yt-icons:feedback' | 'yt-icons:fill_dollar_sign_heart_12' | 'yt-icons:filter' | 'yt-icons:find_in_page' | 'yt-icons:first_page' | 'yt-icons:flag' | 'yt-icons:flag_outline' | 'yt-icons:folder' | 'yt-icons:forum' | 'yt-icons:full_heart' | 'yt-icons:fullscreen' | 'yt-icons:fullscreen_exit' | 'yt-icons:g_translate' | 'yt-icons:gaming_logo' | 'yt-icons:gif' | 'yt-icons:google' | 'yt-icons:google_logo' | 'yt-icons:groups' | 'yt-icons:guide_close' | 'yt-icons:happy' | 'yt-icons:health_and_safety' | 'yt-icons:help' | 'yt-icons:help_outline' | 'yt-icons:highlight_off' | 'yt-icons:hourglass' | 'yt-icons:image' | 'yt-icons:info-outline' | 'yt-icons:info' | 'yt-icons:info_circle' | 'yt-icons:info_outline' | 'yt-icons:insert_chart' | 'yt-icons:insert_chart_outlined' | 'yt-icons:insert_photo' | 'yt-icons:insights' | 'yt-icons:invite_only_mode' | 'yt-icons:invite_only_mode_off' | 'yt-icons:keep' | 'yt-icons:keep_off' | 'yt-icons:keyboard' | 'yt-icons:language' | 'yt-icons:last_page' | 'yt-icons:launch' | 'yt-icons:library_add' | 'yt-icons:library_music' | 'yt-icons:library_remove' | 'yt-icons:like' | 'yt-icons:like_outline' | 'yt-icons:likes_playlist' | 'yt-icons:link' | 'yt-icons:live' | 'yt-icons:live_unlisted' | 'yt-icons:local_mall' | 'yt-icons:local_offer' | 'yt-icons:location_pin' | 'yt-icons:lock' | 'yt-icons:lock_clock' | 'yt-icons:lock_open' | 'yt-icons:loop' | 'yt-icons:loop_one' | 'yt-icons:manual-record' | 'yt-icons:maximize' | 'yt-icons:meh' | 'yt-icons:members_only_mode' | 'yt-icons:members_only_mode_off' | 'yt-icons:membership_post_purchase' | 'yt-icons:membership_pre_purchase' | 'yt-icons:memberships' | 'yt-icons:menu' | 'yt-icons:message' | 'yt-icons:message_bubble_question' | 'yt-icons:microphone_off' | 'yt-icons:microphone_on' | 'yt-icons:minimize' | 'yt-icons:mix' | 'yt-icons:mobile_portrait' | 'yt-icons:monetization_on' | 'yt-icons:money_fill' | 'yt-icons:money_fill_jpy' | 'yt-icons:money_fill_more_arrow' | 'yt-icons:money_fill_shopping_bag' | 'yt-icons:money_fill_store' | 'yt-icons:money_heart' | 'yt-icons:more' | 'yt-icons:more_chevron' | 'yt-icons:more_horiz' | 'yt-icons:more_vert' | 'yt-icons:movies' | 'yt-icons:music' | 'yt-icons:music_explicit_badge' | 'yt-icons:music_miniplayer' | 'yt-icons:music_new_release' | 'yt-icons:music_note' | 'yt-icons:music_player_page' | 'yt-icons:music_repeat_all' | 'yt-icons:music_repeat_one' | 'yt-icons:music_shuffle' | 'yt-icons:music_video' | 'yt-icons:my_channel' | 'yt-icons:my_videos' | 'yt-icons:new_release' | 'yt-icons:not_interested' | 'yt-icons:notifications' | 'yt-icons:notifications_active' | 'yt-icons:notifications_done_checkmark' | 'yt-icons:notifications_none' | 'yt-icons:notifications_off' | 'yt-icons:official_artist_badge' | 'yt-icons:offline_cloud' | 'yt-icons:offline_download' | 'yt-icons:offline_downloading_eighty' | 'yt-icons:offline_downloading_forty' | 'yt-icons:offline_downloading_sixty' | 'yt-icons:offline_downloading_spinner' | 'yt-icons:offline_downloading_twenty' | 'yt-icons:offline_downloading_zero' | 'yt-icons:offline_pause' | 'yt-icons:offline_pin' | 'yt-icons:open_in_new' | 'yt-icons:open_with' | 'yt-icons:paid' | 'yt-icons:pause_outlined' | 'yt-icons:pencil' | 'yt-icons:people' | 'yt-icons:people_2' | 'yt-icons:person' | 'yt-icons:person_add' | 'yt-icons:person_setting' | 'yt-icons:phone' | 'yt-icons:phone_download' | 'yt-icons:play_all' | 'yt-icons:play_arrow' | 'yt-icons:play_disabled' | 'yt-icons:play_next' | 'yt-icons:play_outlined' | 'yt-icons:playlist_add' | 'yt-icons:playlist_add_check' | 'yt-icons:playlists' | 'yt-icons:policy' | 'yt-icons:poll' | 'yt-icons:premium' | 'yt-icons:price_tag' | 'yt-icons:privacy_info' | 'yt-icons:privacy_private' | 'yt-icons:privacy_public' | 'yt-icons:privacy_unlisted' | 'yt-icons:prompted_sign_in' | 'yt-icons:purchase_sponsorship' | 'yt-icons:purchase_super_chat' | 'yt-icons:purchase_super_sticker' | 'yt-icons:purchases' | 'yt-icons:question_answer' | 'yt-icons:radio-button-unchecked' | 'yt-icons:refresh' | 'yt-icons:remove-circle-outline' | 'yt-icons:remove' | 'yt-icons:remove_circle' | 'yt-icons:remove_done' | 'yt-icons:remove_moderator' | 'yt-icons:replay' | 'yt-icons:report' | 'yt-icons:report_problem' | 'yt-icons:sad' | 'yt-icons:scatterplot' | 'yt-icons:schedule' | 'yt-icons:search' | 'yt-icons:send' | 'yt-icons:service_toolbox' | 'yt-icons:settings' | 'yt-icons:settings_applications' | 'yt-icons:settings_material' | 'yt-icons:share' | 'yt-icons:share_arrow' | 'yt-icons:shield' | 'yt-icons:shield_with_avatar' | 'yt-icons:shuffle' | 'yt-icons:skip_next' | 'yt-icons:skip_previous' | 'yt-icons:slow_mode' | 'yt-icons:slow_mode_off' | 'yt-icons:sms' | 'yt-icons:sort' | 'yt-icons:sponsorship_star' | 'yt-icons:sponsorships' | 'yt-icons:sponsorships_no_bg' | 'yt-icons:star' | 'yt-icons:star_border' | 'yt-icons:star_half' | 'yt-icons:stars' | 'yt-icons:sticker_emoticon' | 'yt-icons:subdirectory_arrow_right' | 'yt-icons:subscribe' | 'yt-icons:subscription_manager' | 'yt-icons:subscriptions' | 'yt-icons:subtitles' | 'yt-icons:super_chat_for_good' | 'yt-icons:super_store' | 'yt-icons:supervisor_account' | 'yt-icons:switch_accounts' | 'yt-icons:tab_account' | 'yt-icons:tab_explore' | 'yt-icons:tab_home' | 'yt-icons:tab_subscriptions' | 'yt-icons:tab_trending' | 'yt-icons:trailer' | 'yt-icons:translate' | 'yt-icons:trending' | 'yt-icons:trending_up' | 'yt-icons:trophy' | 'yt-icons:tune' | 'yt-icons:tv' | 'yt-icons:unlimited' | 'yt-icons:unplugged_logo' | 'yt-icons:up_arrow' | 'yt-icons:upload' | 'yt-icons:uploads' | 'yt-icons:verified_user' | 'yt-icons:vertical_align_bottom' | 'yt-icons:vertical_align_top' | 'yt-icons:very_happy' | 'yt-icons:very_sad' | 'yt-icons:video_call' | 'yt-icons:video_camera' | 'yt-icons:video_camera_disabled' | 'yt-icons:video_library_white' | 'yt-icons:video_youtube' | 'yt-icons:videogame_asset' | 'yt-icons:view_list' | 'yt-icons:view_module' | 'yt-icons:visibility' | 'yt-icons:visibility_off' | 'yt-icons:volume_off' | 'yt-icons:volume_up' | 'yt-icons:vpn_key' | 'yt-icons:wallpaper' | 'yt-icons:warning' | 'yt-icons:watch_history' | 'yt-icons:watch_later' | 'yt-icons:watch_related_mix' | 'yt-icons:waveform' | 'yt-icons:what_to_watch' | 'yt-icons:wifi_status_bar_four' | 'yt-icons:wifi_status_bar_one' | 'yt-icons:wifi_status_bar_three' | 'yt-icons:wifi_status_bar_zero' | 'yt-icons:work_off' | 'yt-icons:youtube_ad' | 'yt-icons:youtube_kids' | 'yt-icons:youtube_kids_round' | 'yt-icons:youtube_logo' | 'yt-icons:youtube_music' | 'yt-icons:youtube_music_logo_short' | 'yt-icons:youtube_music_monochrome' | 'yt-icons:youtube_premiere_logo_short' | 'yt-icons:youtube_red_logo' | 'yt-icons:youtube_red_logo_short' | 'yt-icons:youtube_round' | 'yt-icons:youtube_shorts_brand_24' | 'yt-icons:zoom_in' | 'yt-icons:zoom_out'; +export type YtLogos = 'yt-logos:lozenge' | 'yt-logos:premium' | 'yt-logos:premium_standalone' | 'yt-logos:premium_standalone_cairo' | 'yt-logos:red'; +export type YtSysIcons = 'yt-sys-icons:1_point_2x' | 'yt-sys-icons:1_point_5x' | 'yt-sys-icons:1_point_8x' | 'yt-sys-icons:1x' | 'yt-sys-icons:2x' | 'yt-sys-icons:account_box-filled' | 'yt-sys-icons:account_box' | 'yt-sys-icons:account_circle-filled' | 'yt-sys-icons:account_circle' | 'yt-sys-icons:account_linked' | 'yt-sys-icons:account_unlinked' | 'yt-sys-icons:add' | 'yt-sys-icons:add_circle' | 'yt-sys-icons:add_moderator' | 'yt-sys-icons:add_to_queue' | 'yt-sys-icons:add_to_queue_tail' | 'yt-sys-icons:add_video_link' | 'yt-sys-icons:admin_panel_settings' | 'yt-sys-icons:album-filled' | 'yt-sys-icons:album' | 'yt-sys-icons:apps-filled' | 'yt-sys-icons:apps' | 'yt-sys-icons:arrow_back' | 'yt-sys-icons:arrow_circle_right' | 'yt-sys-icons:arrow_drop_down' | 'yt-sys-icons:arrow_drop_up' | 'yt-sys-icons:arrow_flip' | 'yt-sys-icons:arrow_forward' | 'yt-sys-icons:arrow_pause' | 'yt-sys-icons:arrow_remix' | 'yt-sys-icons:arrow_solid_down-filled' | 'yt-sys-icons:arrow_solid_down' | 'yt-sys-icons:arrow_solid_up-filled' | 'yt-sys-icons:arrow_undo' | 'yt-sys-icons:auto_awesome-filled' | 'yt-sys-icons:auto_awesome' | 'yt-sys-icons:bag-filled' | 'yt-sys-icons:bag' | 'yt-sys-icons:bar_chart-filled' | 'yt-sys-icons:bar_chart' | 'yt-sys-icons:bar_circle' | 'yt-sys-icons:bar_graph_box_vertical-filled' | 'yt-sys-icons:bar_graph_box_vertical' | 'yt-sys-icons:bar_horizontal' | 'yt-sys-icons:book' | 'yt-sys-icons:bookmark-filled' | 'yt-sys-icons:bookmark' | 'yt-sys-icons:box_check' | 'yt-sys-icons:box_empty' | 'yt-sys-icons:box_open_check' | 'yt-sys-icons:box_pencil' | 'yt-sys-icons:briefcase' | 'yt-sys-icons:broadcast' | 'yt-sys-icons:calendar-filled' | 'yt-sys-icons:calendar' | 'yt-sys-icons:camera_alt' | 'yt-sys-icons:cast_outline' | 'yt-sys-icons:celebration' | 'yt-sys-icons:chat_bubble-filled' | 'yt-sys-icons:chat_bubble' | 'yt-sys-icons:chat_off-filled' | 'yt-sys-icons:chat_off' | 'yt-sys-icons:check-filled' | 'yt-sys-icons:check' | 'yt-sys-icons:check_box' | 'yt-sys-icons:check_box_bar' | 'yt-sys-icons:check_box_outline_blank' | 'yt-sys-icons:check_circle_thick-filled' | 'yt-sys-icons:check_circle_thick' | 'yt-sys-icons:check_double' | 'yt-sys-icons:check_double_off' | 'yt-sys-icons:chevron_left' | 'yt-sys-icons:chevron_right' | 'yt-sys-icons:chromecast-filled' | 'yt-sys-icons:chromecast' | 'yt-sys-icons:chromecast_animate_frame_1' | 'yt-sys-icons:chromecast_animate_frame_2' | 'yt-sys-icons:chromecast_animate_frame_3' | 'yt-sys-icons:circle-filled' | 'yt-sys-icons:circle' | 'yt-sys-icons:circles_6-filled' | 'yt-sys-icons:clapperboard-filled' | 'yt-sys-icons:clapperboard' | 'yt-sys-icons:clarify' | 'yt-sys-icons:clock' | 'yt-sys-icons:clock_half_circle' | 'yt-sys-icons:close' | 'yt-sys-icons:closed_caption' | 'yt-sys-icons:cloud_arrow_up' | 'yt-sys-icons:collapse' | 'yt-sys-icons:comment-filled' | 'yt-sys-icons:comment' | 'yt-sys-icons:copy' | 'yt-sys-icons:countdown_to_close' | 'yt-sys-icons:countdown_to_close_digit' | 'yt-sys-icons:course-filled' | 'yt-sys-icons:course' | 'yt-sys-icons:creation_post-filled' | 'yt-sys-icons:creation_post' | 'yt-sys-icons:creator_academy' | 'yt-sys-icons:creator_metadata_monetization-filled' | 'yt-sys-icons:creator_metadata_monetization' | 'yt-sys-icons:creator_metadata_monetization_off' | 'yt-sys-icons:creator_studio-filled' | 'yt-sys-icons:creator_studio' | 'yt-sys-icons:credit_card-filled' | 'yt-sys-icons:credit_card' | 'yt-sys-icons:dark_theme-filled' | 'yt-sys-icons:dark_theme' | 'yt-sys-icons:delete-filled' | 'yt-sys-icons:delete' | 'yt-sys-icons:dislike-filled' | 'yt-sys-icons:dislike' | 'yt-sys-icons:dollar_sign_circle-filled' | 'yt-sys-icons:dollar_sign_circle' | 'yt-sys-icons:dollar_sign_container' | 'yt-sys-icons:done_all-filled' | 'yt-sys-icons:done_all' | 'yt-sys-icons:down_arrow-filled' | 'yt-sys-icons:down_arrow' | 'yt-sys-icons:drag_handle' | 'yt-sys-icons:edit' | 'yt-sys-icons:email-filled' | 'yt-sys-icons:emoji_activities-filled' | 'yt-sys-icons:emoji_activities' | 'yt-sys-icons:error-filled' | 'yt-sys-icons:error' | 'yt-sys-icons:exit_to_app' | 'yt-sys-icons:expand' | 'yt-sys-icons:explore-filled' | 'yt-sys-icons:explore' | 'yt-sys-icons:eye' | 'yt-sys-icons:fact_check' | 'yt-sys-icons:fashion_logo-filled' | 'yt-sys-icons:fashion_logo' | 'yt-sys-icons:feedback' | 'yt-sys-icons:filter-filled' | 'yt-sys-icons:filter' | 'yt-sys-icons:find_in_page' | 'yt-sys-icons:fire-filled' | 'yt-sys-icons:fire' | 'yt-sys-icons:first_page-filled' | 'yt-sys-icons:first_page' | 'yt-sys-icons:flag-filled' | 'yt-sys-icons:flag' | 'yt-sys-icons:folder-filled' | 'yt-sys-icons:folder' | 'yt-sys-icons:forum-filled' | 'yt-sys-icons:forum' | 'yt-sys-icons:full_heart-filled' | 'yt-sys-icons:full_heart' | 'yt-sys-icons:fullscreen-filled' | 'yt-sys-icons:fullscreen' | 'yt-sys-icons:fullscreen_exit-filled' | 'yt-sys-icons:fullscreen_exit' | 'yt-sys-icons:gaming_logo-filled' | 'yt-sys-icons:gaming_logo' | 'yt-sys-icons:gift-filled' | 'yt-sys-icons:gift' | 'yt-sys-icons:google' | 'yt-sys-icons:happy-filled' | 'yt-sys-icons:happy' | 'yt-sys-icons:headset-filled' | 'yt-sys-icons:headset' | 'yt-sys-icons:health_and_safety' | 'yt-sys-icons:heart_box' | 'yt-sys-icons:heart_circle' | 'yt-sys-icons:help' | 'yt-sys-icons:highlight_off' | 'yt-sys-icons:hourglass' | 'yt-sys-icons:image' | 'yt-sys-icons:info' | 'yt-sys-icons:keep-filled' | 'yt-sys-icons:keep' | 'yt-sys-icons:keep_off-filled' | 'yt-sys-icons:keep_off' | 'yt-sys-icons:keyboard-filled' | 'yt-sys-icons:keyboard' | 'yt-sys-icons:language-filled' | 'yt-sys-icons:language' | 'yt-sys-icons:laptop_mobile' | 'yt-sys-icons:last_page-filled' | 'yt-sys-icons:last_page' | 'yt-sys-icons:library_add' | 'yt-sys-icons:library_music-filled' | 'yt-sys-icons:library_music' | 'yt-sys-icons:library_outline' | 'yt-sys-icons:library_remove' | 'yt-sys-icons:library_saved' | 'yt-sys-icons:like-filled' | 'yt-sys-icons:like' | 'yt-sys-icons:link' | 'yt-sys-icons:list_play_arrow' | 'yt-sys-icons:list_queue' | 'yt-sys-icons:list_queue_last' | 'yt-sys-icons:list_remove' | 'yt-sys-icons:location_pin-filled' | 'yt-sys-icons:location_pin' | 'yt-sys-icons:lock' | 'yt-sys-icons:lock_open' | 'yt-sys-icons:loop-filled' | 'yt-sys-icons:loop' | 'yt-sys-icons:loop_active' | 'yt-sys-icons:loop_one-filled' | 'yt-sys-icons:loop_one' | 'yt-sys-icons:loop_one_active' | 'yt-sys-icons:mail' | 'yt-sys-icons:medal_star' | 'yt-sys-icons:meet' | 'yt-sys-icons:meh-filled' | 'yt-sys-icons:meh' | 'yt-sys-icons:menu' | 'yt-sys-icons:menu_filter' | 'yt-sys-icons:message-filled' | 'yt-sys-icons:message' | 'yt-sys-icons:message_bubble_left_boost' | 'yt-sys-icons:message_bubble_question-filled' | 'yt-sys-icons:message_bubble_question' | 'yt-sys-icons:microphone_on-filled' | 'yt-sys-icons:microphone_on' | 'yt-sys-icons:mix-filled' | 'yt-sys-icons:mix' | 'yt-sys-icons:mobile_portrait-filled' | 'yt-sys-icons:mobile_portrait' | 'yt-sys-icons:monetization_on-filled' | 'yt-sys-icons:monetization_on' | 'yt-sys-icons:money_fill' | 'yt-sys-icons:money_fill_shopping_bag-filled' | 'yt-sys-icons:money_fill_shopping_bag' | 'yt-sys-icons:money_hand' | 'yt-sys-icons:money_heart' | 'yt-sys-icons:moon_z-filled' | 'yt-sys-icons:moon_z' | 'yt-sys-icons:more' | 'yt-sys-icons:more_vert' | 'yt-sys-icons:movies-filled' | 'yt-sys-icons:movies' | 'yt-sys-icons:music-filled' | 'yt-sys-icons:music' | 'yt-sys-icons:music_explicit_badge-filled' | 'yt-sys-icons:music_explicit_badge' | 'yt-sys-icons:music_miniplayer-filled' | 'yt-sys-icons:music_miniplayer' | 'yt-sys-icons:music_new_release' | 'yt-sys-icons:music_player_page-filled' | 'yt-sys-icons:music_player_page' | 'yt-sys-icons:music_video-filled' | 'yt-sys-icons:music_video' | 'yt-sys-icons:my_videos-filled' | 'yt-sys-icons:my_videos' | 'yt-sys-icons:new_release-filled' | 'yt-sys-icons:new_release' | 'yt-sys-icons:news-filled' | 'yt-sys-icons:news' | 'yt-sys-icons:not_interested' | 'yt-sys-icons:notifications-filled' | 'yt-sys-icons:notifications' | 'yt-sys-icons:notifications_active' | 'yt-sys-icons:notifications_off' | 'yt-sys-icons:offline_cloud' | 'yt-sys-icons:offline_download' | 'yt-sys-icons:offline_pause' | 'yt-sys-icons:open_in_new-filled' | 'yt-sys-icons:open_in_new' | 'yt-sys-icons:open_in_panel' | 'yt-sys-icons:outline_arrow_solid_up' | 'yt-sys-icons:panels-filled' | 'yt-sys-icons:panels' | 'yt-sys-icons:paper_corner_folded' | 'yt-sys-icons:pause_outlined' | 'yt-sys-icons:pdf' | 'yt-sys-icons:people-filled' | 'yt-sys-icons:people' | 'yt-sys-icons:people_group' | 'yt-sys-icons:person-filled' | 'yt-sys-icons:person' | 'yt-sys-icons:person_2' | 'yt-sys-icons:person_add-filled' | 'yt-sys-icons:person_add' | 'yt-sys-icons:person_circle_slash' | 'yt-sys-icons:person_minus' | 'yt-sys-icons:person_music-filled' | 'yt-sys-icons:person_music' | 'yt-sys-icons:person_radar' | 'yt-sys-icons:phone-filled' | 'yt-sys-icons:phone' | 'yt-sys-icons:phone_download' | 'yt-sys-icons:play_arrow' | 'yt-sys-icons:play_arrow_half_circle' | 'yt-sys-icons:play_arrow_outline' | 'yt-sys-icons:play_outlined-filled' | 'yt-sys-icons:play_outlined' | 'yt-sys-icons:playlist_add' | 'yt-sys-icons:playlist_add_check' | 'yt-sys-icons:playlists-filled' | 'yt-sys-icons:playlists' | 'yt-sys-icons:podcast-filled' | 'yt-sys-icons:podcast' | 'yt-sys-icons:point_2x' | 'yt-sys-icons:point_5x' | 'yt-sys-icons:point_8x' | 'yt-sys-icons:poll-filled' | 'yt-sys-icons:poll' | 'yt-sys-icons:privacy_info' | 'yt-sys-icons:privacy_public-filled' | 'yt-sys-icons:privacy_public' | 'yt-sys-icons:purchase_super_chat-filled' | 'yt-sys-icons:purchase_super_chat' | 'yt-sys-icons:purchase_super_sticker-filled' | 'yt-sys-icons:purchase_super_sticker' | 'yt-sys-icons:purchases-filled' | 'yt-sys-icons:purchases' | 'yt-sys-icons:quotation_mark' | 'yt-sys-icons:radar_live-filled' | 'yt-sys-icons:radar_live' | 'yt-sys-icons:remove_circle' | 'yt-sys-icons:remove_done-filled' | 'yt-sys-icons:remove_done' | 'yt-sys-icons:remove_moderator-filled' | 'yt-sys-icons:remove_moderator' | 'yt-sys-icons:replay' | 'yt-sys-icons:replay_10' | 'yt-sys-icons:replay_arrow' | 'yt-sys-icons:rss' | 'yt-sys-icons:rss_off' | 'yt-sys-icons:sad-filled' | 'yt-sys-icons:sad' | 'yt-sys-icons:scissors-filled' | 'yt-sys-icons:scissors' | 'yt-sys-icons:screen_default-filled' | 'yt-sys-icons:screen_default' | 'yt-sys-icons:screen_fullscreen-filled' | 'yt-sys-icons:screen_fullscreen' | 'yt-sys-icons:screen_miniplayer-filled' | 'yt-sys-icons:screen_miniplayer' | 'yt-sys-icons:screen_multi_view-fill' | 'yt-sys-icons:screen_multi_view' | 'yt-sys-icons:screen_switch-filled' | 'yt-sys-icons:screen_switch' | 'yt-sys-icons:screen_theatre-filled' | 'yt-sys-icons:screen_theatre' | 'yt-sys-icons:search' | 'yt-sys-icons:send-filled' | 'yt-sys-icons:send' | 'yt-sys-icons:settings-filled' | 'yt-sys-icons:settings' | 'yt-sys-icons:settings_remote' | 'yt-sys-icons:share' | 'yt-sys-icons:share_ios' | 'yt-sys-icons:shield-filled' | 'yt-sys-icons:shield' | 'yt-sys-icons:shield_add' | 'yt-sys-icons:shield_off' | 'yt-sys-icons:shield_overflow' | 'yt-sys-icons:shield_with_avatar-filled' | 'yt-sys-icons:shield_with_avatar' | 'yt-sys-icons:shopping_cart-filled' | 'yt-sys-icons:shopping_cart' | 'yt-sys-icons:shorts_layout_bottom' | 'yt-sys-icons:shorts_layout_centered_square' | 'yt-sys-icons:shorts_layout_foreground' | 'yt-sys-icons:shorts_layout_landscape_bottom' | 'yt-sys-icons:shorts_layout_landscape_top' | 'yt-sys-icons:shorts_layout_left' | 'yt-sys-icons:shorts_layout_pip' | 'yt-sys-icons:shorts_layout_right' | 'yt-sys-icons:shorts_layout_single' | 'yt-sys-icons:shorts_layout_top' | 'yt-sys-icons:shuffle-filled' | 'yt-sys-icons:shuffle' | 'yt-sys-icons:skip_forward_30-filled' | 'yt-sys-icons:skip_forward_30' | 'yt-sys-icons:skip_next' | 'yt-sys-icons:skip_previous' | 'yt-sys-icons:slash_circle_left' | 'yt-sys-icons:sort-filled' | 'yt-sys-icons:sort' | 'yt-sys-icons:spark' | 'yt-sys-icons:sparkle-filled' | 'yt-sys-icons:sparkle_filled' | 'yt-sys-icons:spotlight-filled' | 'yt-sys-icons:spotlight' | 'yt-sys-icons:star-filled' | 'yt-sys-icons:star' | 'yt-sys-icons:star_half' | 'yt-sys-icons:stopwatch' | 'yt-sys-icons:subscribe-filled' | 'yt-sys-icons:subscribe' | 'yt-sys-icons:subscriptions-filled' | 'yt-sys-icons:subscriptions' | 'yt-sys-icons:subtitles-filled' | 'yt-sys-icons:subtitles' | 'yt-sys-icons:super_chat_for_good-filled' | 'yt-sys-icons:super_chat_for_good' | 'yt-sys-icons:super_sticker-filled' | 'yt-sys-icons:super_sticker' | 'yt-sys-icons:superstar' | 'yt-sys-icons:switch_accounts-filled' | 'yt-sys-icons:switch_accounts' | 'yt-sys-icons:tab_home-filled' | 'yt-sys-icons:tab_home' | 'yt-sys-icons:tab_shorts-filled' | 'yt-sys-icons:tab_shorts' | 'yt-sys-icons:tablet' | 'yt-sys-icons:tic_tac_toe-filled' | 'yt-sys-icons:tic_tac_toe' | 'yt-sys-icons:trailer-filled' | 'yt-sys-icons:trailer' | 'yt-sys-icons:transcript' | 'yt-sys-icons:transcript_search' | 'yt-sys-icons:translate-filled' | 'yt-sys-icons:translate' | 'yt-sys-icons:trending_up' | 'yt-sys-icons:trophy-filled' | 'yt-sys-icons:trophy' | 'yt-sys-icons:trophy_star' | 'yt-sys-icons:tune-filled' | 'yt-sys-icons:tune' | 'yt-sys-icons:tv_circle-filled' | 'yt-sys-icons:tv_circle' | 'yt-sys-icons:up_arrow-filled' | 'yt-sys-icons:up_arrow' | 'yt-sys-icons:upload' | 'yt-sys-icons:uploads-filled' | 'yt-sys-icons:uploads' | 'yt-sys-icons:vertical_align_top' | 'yt-sys-icons:very_happy-filled' | 'yt-sys-icons:very_happy' | 'yt-sys-icons:very_sad-filled' | 'yt-sys-icons:very_sad' | 'yt-sys-icons:video_call-filled' | 'yt-sys-icons:video_call' | 'yt-sys-icons:video_camera' | 'yt-sys-icons:video_link' | 'yt-sys-icons:view_list-filled' | 'yt-sys-icons:view_list' | 'yt-sys-icons:view_module-filled' | 'yt-sys-icons:view_module' | 'yt-sys-icons:visibility-filled' | 'yt-sys-icons:visibility' | 'yt-sys-icons:visibility_off' | 'yt-sys-icons:volume_off' | 'yt-sys-icons:volume_up-filled' | 'yt-sys-icons:volume_up' | 'yt-sys-icons:vpn_key' | 'yt-sys-icons:vr-filled' | 'yt-sys-icons:vr' | 'yt-sys-icons:warning-filled' | 'yt-sys-icons:warning' | 'yt-sys-icons:watch_history-filled' | 'yt-sys-icons:watch_history' | 'yt-sys-icons:watch_later-filled' | 'yt-sys-icons:watch_later' | 'yt-sys-icons:x_circle' | 'yt-sys-icons:x_octagon' | 'yt-sys-icons:yen_sign_container' | 'yt-sys-icons:youtube_improve_tv' | 'yt-sys-icons:youtube_linked_tv' | 'yt-sys-icons:youtube_music_monochrome' | 'yt-sys-icons:youtube_shorts_fill_no_triangle_red_16' | 'yt-sys-icons:youtube_shorts_no_triangle-filled' | 'yt-sys-icons:youtube_shorts_no_triangle' | 'yt-sys-icons:youtube_studio_arrow_down-filled' | 'yt-sys-icons:youtube_studio_arrow_up-filled' | 'yt-sys-icons:zoom_in-filled' | 'yt-sys-icons:zoom_in' | 'yt-sys-icons:zoom_out-filled' | 'yt-sys-icons:zoom_out'; +export type YtSysIcons12 = 'yt-sys-icons12:arrow_circle_right' | 'yt-sys-icons12:keep-filled' | 'yt-sys-icons12:keep' | 'yt-sys-icons12:music-filled' | 'yt-sys-icons12:music' | 'yt-sys-icons12:rating_up'; +export type YtSysIcons13 = 'yt-sys-icons13:check-filled' | 'yt-sys-icons13:check' | 'yt-sys-icons13:check_circle_thick-filled' | 'yt-sys-icons13:check_circle_thick' | 'yt-sys-icons13:music-filled' | 'yt-sys-icons13:music'; +export type YtSysIcons15 = 'yt-sys-icons15:open_in_new-filled' | 'yt-sys-icons15:open_in_new'; +export type YtSysIcons16 = 'yt-sys-icons16:add' | 'yt-sys-icons16:arrow_circle_right' | 'yt-sys-icons16:arrow_solid_down-filled' | 'yt-sys-icons16:arrow_solid_down' | 'yt-sys-icons16:arrow_solid_up-filled' | 'yt-sys-icons16:arrow_solid_up' | 'yt-sys-icons16:bar_horizontal' | 'yt-sys-icons16:check_circle_thick-filled' | 'yt-sys-icons16:check_circle_thick' | 'yt-sys-icons16:chevron_left' | 'yt-sys-icons16:chevron_right' | 'yt-sys-icons16:comment-filled' | 'yt-sys-icons16:comment' | 'yt-sys-icons16:dislike-filled' | 'yt-sys-icons16:dislike' | 'yt-sys-icons16:dollar_sign_circle-filled' | 'yt-sys-icons16:dollar_sign_circle' | 'yt-sys-icons16:fire' | 'yt-sys-icons16:heart_circle' | 'yt-sys-icons16:help-filled' | 'yt-sys-icons16:help' | 'yt-sys-icons16:info' | 'yt-sys-icons16:like-filled' | 'yt-sys-icons16:like' | 'yt-sys-icons16:lock' | 'yt-sys-icons16:message_bubble_question-filled' | 'yt-sys-icons16:message_bubble_question' | 'yt-sys-icons16:music-filled' | 'yt-sys-icons16:music' | 'yt-sys-icons16:open_in_new' | 'yt-sys-icons16:podcast' | 'yt-sys-icons16:radar_live' | 'yt-sys-icons16:rating_up' | 'yt-sys-icons16:rss' | 'yt-sys-icons16:rss_off' | 'yt-sys-icons16:super_sticker-filled' | 'yt-sys-icons16:super_sticker' | 'yt-sys-icons16:transcript' | 'yt-sys-icons16:transcript_search' | 'yt-sys-icons16:youtube_shorts_no_triangle-filled' | 'yt-sys-icons16:youtube_shorts_no_triangle'; +export type YtSysIcons18 = 'yt-sys-icons18:arrow_circle_right' | 'yt-sys-icons18:heart_circle' | 'yt-sys-icons18:info' | 'yt-sys-icons18:lock-filled' | 'yt-sys-icons18:lock' | 'yt-sys-icons18:music' | 'yt-sys-icons18:podcast' | 'yt-sys-icons18:privacy_public-filled' | 'yt-sys-icons18:privacy_public'; +export type YtSysIcons20 = 'yt-sys-icons20:heart_circle' | 'yt-sys-icons20:help-filled' | 'yt-sys-icons20:help' | 'yt-sys-icons20:mix-filled' | 'yt-sys-icons20:mix'; +export type YtSysIcons36 = 'yt-sys-icons36:face_happy_v2-filled' | 'yt-sys-icons36:face_happy_v2' | 'yt-sys-icons36:face_meh_v2-filled' | 'yt-sys-icons36:face_meh_v2' | 'yt-sys-icons36:face_sad_v2-filled' | 'yt-sys-icons36:face_sad_v2' | 'yt-sys-icons36:face_unhappy_v2-filled' | 'yt-sys-icons36:face_unhappy_v2' | 'yt-sys-icons36:face_very_happy_v2-filled' | 'yt-sys-icons36:face_very_happy_v2'; +export type YtSysIcons48 = 'yt-sys-icons48:broadcast' | 'yt-sys-icons48:screen_default-filled' | 'yt-sys-icons48:screen_default' | 'yt-sys-icons48:screen_fullscreen-filled' | 'yt-sys-icons48:screen_fullscreen' | 'yt-sys-icons48:screen_miniplayer-filled' | 'yt-sys-icons48:screen_miniplayer' | 'yt-sys-icons48:screen_theatre-filled' | 'yt-sys-icons48:screen_theatre'; +export type YtcpIcons = 'ytcp-icons:arrow-down-alt' | 'ytcp-icons:arrow-right' | 'ytcp-icons:arrow-up-alt' | 'ytcp-icons:check-circle-outline' | 'ytcp-icons:error-on-load-v2' | 'ytcp-icons:error-on-load' | 'ytcp-icons:escape-hatch' | 'ytcp-icons:feature-search' | 'ytcp-icons:first-page' | 'ytcp-icons:last-page' | 'ytcp-icons:no-comments' | 'ytcp-icons:no-held-comments-v2' | 'ytcp-icons:no-held-comments' | 'ytcp-icons:no-search-match-v2' | 'ytcp-icons:no-search-match' | 'ytcp-icons:report-off' | 'ytcp-icons:show-ranking' | 'ytcp-icons:trend-down-circle-outline' | 'ytcp-icons:trend-down-circle' | 'ytcp-icons:trend-down' | 'ytcp-icons:trend-up-circle' | 'ytcp-icons:trend-up' | 'ytcp-icons:web-traffic' | 'ytcp-icons:zoom-in' | 'ytcp-icons:zoom-out'; + +export type Icons = + | GeneralIcons + | PaperDropDownMenuIcons + | SocialNetworkIcons + | HashtagLandingPageIcons + | LiveChatBadgeIcons + | MiniplayerIcons + | RottenTomatoesIcons + | SettingsIcons + | ShortsIcons + | YtIcons + | YtLogos + | YtSysIcons + | YtSysIcons12 + | YtSysIcons13 + | YtSysIcons15 + | YtSysIcons16 + | YtSysIcons18 + | YtSysIcons20 + | YtSysIcons36 + | YtSysIcons48 + | YtcpIcons + | 'backstage:artwork' + | 'offline-no-content:offline_no_content' + | 'promo-full-height:empty_search' + | 'spinners:ring'; diff --git a/src/yt-web-components.d.ts b/src/yt-web-components.d.ts index 0612764b..336ac1f0 100644 --- a/src/yt-web-components.d.ts +++ b/src/yt-web-components.d.ts @@ -1,3 +1,5 @@ +import { Icons } from '@/types/icons'; + import type { ComponentProps } from 'solid-js'; declare module 'solid-js' { @@ -28,11 +30,18 @@ declare module 'solid-js' { active?: boolean; } + interface TpYtPaperIconButtonProps { + icon: Icons; + } + interface IntrinsicElements { + center: ComponentProps<'div'>; 'yt-formatted-string': ComponentProps<'span'> & YtFormattedStringProps; 'yt-button-renderer': ComponentProps<'button'> & YtButtonRendererProps; 'tp-yt-paper-spinner-lite': ComponentProps<'div'> & YpYtPaperSpinnerLiteProps; + 'tp-yt-paper-icon-button': ComponentProps<'div'> & + TpYtPaperIconButtonProps; } } }