diff --git a/src/components/settings/tabs/plugins/index.tsx b/src/components/settings/tabs/plugins/index.tsx index 896d7f3aa..9ab3cdcbd 100644 --- a/src/components/settings/tabs/plugins/index.tsx +++ b/src/components/settings/tabs/plugins/index.tsx @@ -25,13 +25,14 @@ import { classNameFactory } from "@api/Styles"; import { CogWheel, InfoIcon } from "@components/Icons"; import { openPluginModal, SettingsTab } from "@components/settings"; import { AddonCard } from "@components/settings/AddonCard"; +import { debounce } from "@shared/debounce"; import { ChangeList } from "@utils/ChangeList"; import { proxyLazy } from "@utils/lazy"; import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { classes, isObjectEmpty } from "@utils/misc"; import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal"; -import { useAwaiter } from "@utils/react"; +import { useAwaiter, useIntersection } from "@utils/react"; import { Plugin } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { Alerts, Button, Card, Flex, Forms, lodash, Parser, React, Select, Text, TextInput, Toasts, Tooltip, useMemo } from "@webpack/common"; @@ -261,17 +262,13 @@ export default function PluginSettings() { .sort((a, b) => a.name.localeCompare(b.name)), []); const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL }); - const [currentPage, setCurrentPage] = React.useState(1); - const pageSize = 36; const search = searchValue.value.toLowerCase(); const onSearch = (query: string) => { setSearchValue(prev => ({ ...prev, value: query })); - setCurrentPage(1); }; const onStatusChange = (status: SearchStatus) => { setSearchValue(prev => ({ ...prev, status })); - setCurrentPage(1); }; const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => { @@ -476,17 +473,22 @@ export default function PluginSettings() { const totalUserPlugins = totalPlugins.filter(p => PluginMeta[p].userPlugin).length; const enabledStockPlugins = enabledPlugins.filter(p => !PluginMeta[p].userPlugin).length; const enabledUserPlugins = enabledPlugins.filter(p => PluginMeta[p].userPlugin).length; + const pluginsToLoad = Math.min(36, plugins.length); + const [visibleCount, setVisibleCount] = React.useState(pluginsToLoad); + const loadMore = React.useCallback(() => { + setVisibleCount(v => Math.min(v + pluginsToLoad, plugins.length)); + }, [plugins.length]); - const paginatedPlugins = plugins.slice( - (currentPage - 1) * pageSize, - currentPage * pageSize - ); + const dLoadMore = useMemo(() => debounce(loadMore, 100), [loadMore]); - const totalPages = Math.ceil(plugins.length / pageSize); + const [sentinelRef, isSentinelVisible] = useIntersection(); + React.useEffect(() => { + if (isSentinelVisible && visibleCount < plugins.length) { + dLoadMore(); + } + }, [isSentinelVisible, visibleCount, plugins.length, dLoadMore]); - function goToPage(page: number) { - setCurrentPage(Math.max(1, Math.min(page, totalPages))); - } + const visiblePlugins = plugins.slice(0, visibleCount); return ( @@ -539,30 +541,13 @@ export default function PluginSettings() { ? ( <>
- {paginatedPlugins.length - ? paginatedPlugins + {visiblePlugins.length + ? visiblePlugins : No plugins meet the search criteria. }
- - {totalPages > 1 && ( -
- - {`Page ${currentPage} of ${totalPages}`} - -
+ {visibleCount < plugins.length && ( +
)} ) diff --git a/src/equicordplugins/musicControls/settings.tsx b/src/equicordplugins/musicControls/settings.tsx index 4ac213139..5d62d84f3 100644 --- a/src/equicordplugins/musicControls/settings.tsx +++ b/src/equicordplugins/musicControls/settings.tsx @@ -56,46 +56,12 @@ function InstallInstructions() { } export const settings = definePluginSettings({ - showSpotifyControls: { - description: "Show Spotify Controls", - type: OptionType.BOOLEAN, - default: false, - }, - showSpotifyLyrics: { - description: "Show Spotify Lyrics", - type: OptionType.BOOLEAN, - default: false, - }, - installTidalWithWS: { - type: OptionType.COMPONENT, - component: () => - }, - showTidalControls: { - description: "Show Tidal Player", - type: OptionType.BOOLEAN, - default: false, - }, - showTidalLyrics: { - description: "Show Tidal Controls", - type: OptionType.BOOLEAN, - default: false, - }, hoverControls: { description: "Show controls on hover", type: OptionType.BOOLEAN, default: false, onChange: v => toggleHoverControls(v) }, - useSpotifyUris: { - type: OptionType.BOOLEAN, - description: "Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms", - default: false - }, - previousButtonRestartsTrack: { - type: OptionType.BOOLEAN, - description: "Restart currently playing track when pressing the previous button if playtime is >3s", - default: true - }, ShowMusicNoteOnNoLyrics: { description: "Show a music note icon when no lyrics are found", type: OptionType.BOOLEAN, @@ -136,23 +102,6 @@ export const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true, }, - TidalLyricFetch: { - description: "Custom URL for fetching lyrics", - type: OptionType.STRING, - default: "https://api.vmohammad.dev/", - placeholder: "https://api.vmohammad.dev/", - onChange: (value: string) => { - if (!value.endsWith("/")) { - value += "/"; - } - if (URL.canParse(value)) { - settings.store.TidalLyricFetch = value; - } else { - showToast("Invalid URL format for CustomUrl: " + value, Toasts.Type.FAILURE); - settings.store.TidalLyricFetch = "https://api.vmohammad.dev/"; - } - } - }, LyricDelay: { description: "", type: OptionType.SLIDER, @@ -174,4 +123,82 @@ export const settings = definePluginSettings({ ), }, + SpotifySectionTitle: { + type: OptionType.COMPONENT, + component: () => ( + + Spotify + + ) + }, + showSpotifyControls: { + description: "Show Spotify Controls", + type: OptionType.BOOLEAN, + default: false, + }, + showSpotifyLyrics: { + description: "Show Spotify Lyrics", + type: OptionType.BOOLEAN, + default: false, + }, + useSpotifyUris: { + type: OptionType.BOOLEAN, + description: "Open Spotify URIs instead of Spotify URLs. Will only work if you have Spotify installed and might not work on all platforms", + default: false + }, + previousButtonRestartsTrack: { + type: OptionType.BOOLEAN, + description: "Restart currently playing track when pressing the previous button if playtime is >3s", + default: true + }, + + TidalSectionTitle: { + type: OptionType.COMPONENT, + component: () => ( + + Tidal + + ) + }, + installTidalWithWS: { + type: OptionType.COMPONENT, + component: () => + }, + showTidalControls: { + description: "Show Tidal Player", + type: OptionType.BOOLEAN, + default: false, + }, + showTidalLyrics: { + description: "Show Tidal Controls", + type: OptionType.BOOLEAN, + default: false, + }, + TidalLyricFetch: { + description: "Custom URL for fetching lyrics", + type: OptionType.STRING, + default: "https://api.vmohammad.dev/", + placeholder: "https://api.vmohammad.dev/", + onChange: (value: string) => { + if (!value.endsWith("/")) { + value += "/"; + } + if (URL.canParse(value)) { + settings.store.TidalLyricFetch = value; + } else { + showToast("Invalid URL format for CustomUrl: " + value, Toasts.Type.FAILURE); + settings.store.TidalLyricFetch = "https://api.vmohammad.dev/"; + } + } + }, + TidalSyncMode: { + description: "Lyrics sync mode", + type: OptionType.SELECT, + options: [ + { value: "line", label: "Line", default: true }, + { value: "word", label: "Word" }, + { value: "character", label: "Character" }, + ], + default: "line", + }, }); diff --git a/src/equicordplugins/musicControls/styles.css b/src/equicordplugins/musicControls/styles.css index d8e671fbf..3252938e7 100644 --- a/src/equicordplugins/musicControls/styles.css +++ b/src/equicordplugins/musicControls/styles.css @@ -158,4 +158,89 @@ .eq-tidal-lyrics-modal-no-lyrics { text-align: center; padding: 1rem; -} \ No newline at end of file +} + +.eq-tidal-lyrics-char { + color: var(--text-muted); + padding: 0 1px; + display: inline-block; + transition: color .15s ease, opacity .15s ease, transform .15s ease, text-shadow .15s ease; + will-change: transform, color, opacity, text-shadow; +} + +.eq-tidal-lyrics-char-active { + color: var(--brand-experiment); + font-weight: 600; + text-shadow: 0 0 .25em currentcolor; +} + +.eq-tidal-lyrics-word { + color: var(--text-muted); + border-radius: 4px; + padding: 0 1px; + margin-right: 2px; + border: 1px solid transparent; + display: inline-block; + transition: color .15s ease, opacity .15s ease, background-color .15s ease, box-shadow .15s ease, transform .15s ease; + will-change: transform, color, opacity, background-color, box-shadow; +} + +.eq-tidal-lyrics-word:last-child { + margin-right: 0; +} + +.eq-tidal-lyrics-word-active { + color: var(--brand-experiment); + background: none; + box-shadow: none; + text-shadow: 0 0 .25em currentcolor; +} + +.eq-tidal-lyrics-current, +.eq-tidal-lyrics-next, +.eq-tidal-lyrics-prev { + transition: color .2s ease, opacity .2s ease, transform .2s ease, text-shadow .2s ease; +} + +.eq-tidal-lyrics-current { + font-weight: 600; + text-shadow: 0 0 .35em rgb(0 0 0 / 15%); +} + +.eq-tidal-lyrics-next { + opacity: .85; +} + +.eq-tidal-lyrics-prev { + opacity: .65; +} + +.eq-tidal-lyrics-current .eq-tidal-lyrics-char, +.eq-tidal-lyrics-current .eq-tidal-lyrics-word { + color: var(--text-normal); +} + +.eq-tidal-lyrics-next .eq-tidal-lyrics-char, +.eq-tidal-lyrics-prev .eq-tidal-lyrics-char, +.eq-tidal-lyrics-next .eq-tidal-lyrics-word, +.eq-tidal-lyrics-prev .eq-tidal-lyrics-word { + color: var(--text-muted); +} + + +@media (prefers-reduced-motion: reduce) { + + .eq-tidal-lyrics-current, + .eq-tidal-lyrics-next, + .eq-tidal-lyrics-prev, + .eq-tidal-lyrics-char, + .eq-tidal-lyrics-word { + transition: none; + text-shadow: none; + } + + .eq-tidal-lyrics-char-active, + .eq-tidal-lyrics-word-active { + transform: none; + } +} diff --git a/src/equicordplugins/musicControls/tidal/TidalStore.ts b/src/equicordplugins/musicControls/tidal/TidalStore.ts index 37503be81..a5e1b1ec4 100644 --- a/src/equicordplugins/musicControls/tidal/TidalStore.ts +++ b/src/equicordplugins/musicControls/tidal/TidalStore.ts @@ -149,7 +149,7 @@ export const TidalStore = proxyLazyWebpack(() => { public repeat: Repeat = 0; public shuffle = false; public volume = 100; - public playerElement: HTMLElement | null = null; + private playerElement: HTMLElement | null = null; public socket = new TidalSocket((message: Message) => { if (message.type === "update" && message.all && message.fields) { const apiData = message.fields; diff --git a/src/equicordplugins/musicControls/tidal/lyrics/components/lyrics.tsx b/src/equicordplugins/musicControls/tidal/lyrics/components/lyrics.tsx index 414448927..6b6ff969a 100644 --- a/src/equicordplugins/musicControls/tidal/lyrics/components/lyrics.tsx +++ b/src/equicordplugins/musicControls/tidal/lyrics/components/lyrics.tsx @@ -15,11 +15,11 @@ import { LyricsModal } from "./modal"; import { cl, NoteSvg, useLyrics } from "./util"; function LyricsDisplay({ scroll = true }: { scroll?: boolean; }) { - const { ShowMusicNoteOnNoLyrics } = settings.use(["ShowMusicNoteOnNoLyrics"]); - const { lyrics, lyricRefs, currLrcIndex, nextLyric } = useLyrics({ scroll }); + const { ShowMusicNoteOnNoLyrics, TidalSyncMode: SyncMode } = settings.use(["ShowMusicNoteOnNoLyrics", "TidalSyncMode"]); + const { lyrics, lyricRefs, currLrcIndex } = useLyrics({ scroll }); const currentLyrics = lyrics || null; const NoteElement = NoteSvg(cl("music-note")); - + const position = useStateFromStores([TidalStore], () => TidalStore.mPosition); const makeClassName = (index: number): string => { if (currLrcIndex === null) return ""; const diff = index - currLrcIndex; @@ -51,7 +51,53 @@ function LyricsDisplay({ scroll = true }: { scroll?: boolean; }) { variant={currLrcIndex === i ? "text-sm/normal" : "text-xs/normal"} className={makeClassName(i)} > - {line.text || NoteElement} + {SyncMode === "character" && line.characters && line.characters.length > 0 && line.words && line.words.length > 0 ? ( + line.words.map((word, wIdx) => ( + + {word.characters.map((char, j) => { + const charActive = position / 1000 >= char.time && position / 1000 < char.endTime; + return ( + + {char.char} + + ); + })} + {wIdx < line.words.length - 1 && } + + )) + ) : SyncMode === "character" && line.characters && line.characters.length > 0 ? ( + line.characters.map((char, j) => { + const charActive = position / 1000 >= char.time && position / 1000 < char.endTime; + return ( + + {char.char} + + ); + }) + ) : SyncMode === "word" && line.words && line.words.length > 0 ? ( + line.words.map((word, j) => { + const wordActive = position / 1000 >= word.time && position / 1000 < word.endTime; + return ( + + {word.word} + + ); + }) + ) : ( + line.text || NoteElement + )}
))} diff --git a/src/equicordplugins/musicControls/tidal/lyrics/components/modal.tsx b/src/equicordplugins/musicControls/tidal/lyrics/components/modal.tsx index 6016bd5b7..0857dc800 100644 --- a/src/equicordplugins/musicControls/tidal/lyrics/components/modal.tsx +++ b/src/equicordplugins/musicControls/tidal/lyrics/components/modal.tsx @@ -7,6 +7,7 @@ import { ModalContent, ModalHeader, ModalProps, ModalRoot } from "@utils/modal"; import { Text } from "@webpack/common"; +import { settings } from "../../../settings"; import { TidalStore, Track } from "../../TidalStore"; import { cl, NoteSvg, scrollClasses, useLyrics } from "./util"; @@ -40,6 +41,8 @@ function ModalHeaderContent({ track }: { track: Track; }) { export function LyricsModal({ rootProps }: { rootProps: ModalProps; }) { const { track, lyrics, currLrcIndex } = useLyrics({ scroll: false }); const currentLyrics = lyrics || null; + const position = track ? TidalStore.mPosition : 0; + const { TidalSyncMode: SyncMode } = settings.use(["TidalSyncMode"]); return ( @@ -58,7 +61,54 @@ export function LyricsModal({ rootProps }: { rootProps: ModalProps; }) { onClick={() => TidalStore.seek(line.time * 1000)} > {formatTime(line.time)} - {line.text || NoteSvg(cl("modal-note"))} + + {SyncMode === "character" && line.characters && line.characters.length > 0 && line.words && line.words.length > 0 ? ( + line.words.map((word, wIdx) => ( + + {word.characters.map((char, j) => { + const charActive = position / 1000 >= char.time && position / 1000 < char.endTime; + return ( + + {char.char} + + ); + })} + {wIdx < line.words.length - 1 && } + + )) + ) : SyncMode === "character" && line.characters && line.characters.length > 0 ? ( + line.characters.map((char, j) => { + const charActive = position / 1000 >= char.time && position / 1000 < char.endTime; + return ( + + {char.char} + + ); + }) + ) : SyncMode === "word" && line.words && line.words.length > 0 ? ( + line.words.map((word, j) => { + const wordActive = position / 1000 >= word.time && position / 1000 < word.endTime; + return ( + + {word.word} + + ); + }) + ) : ( + line.text || NoteSvg(cl("modal-note")) + )} )) ) : (