feat(tidalLyrics): add back syncmode
* implement infinite scroll for plugin list instead of pagination (🤢🤢🤮🤮) * feat(tidalLyrics): add back syncmode * refactor(settings): reorganize settings
This commit is contained in:
parent
a3449f747f
commit
c52cc5f911
6 changed files with 285 additions and 92 deletions
|
|
@ -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 (
|
||||
<SettingsTab title="Plugins">
|
||||
|
|
@ -539,30 +541,13 @@ export default function PluginSettings() {
|
|||
? (
|
||||
<>
|
||||
<div className={cl("grid")}>
|
||||
{paginatedPlugins.length
|
||||
? paginatedPlugins
|
||||
{visiblePlugins.length
|
||||
? visiblePlugins
|
||||
: <Text variant="text-md/normal">No plugins meet the search criteria.</Text>
|
||||
}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className={cl("page-buttons")}>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => goToPage(currentPage - 1)}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
<Text>{`Page ${currentPage} of ${totalPages}`}</Text>
|
||||
<Button
|
||||
size={Button.Sizes.SMALL}
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => goToPage(currentPage + 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
{visibleCount < plugins.length && (
|
||||
<div ref={sentinelRef} style={{ height: 32 }} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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: () => <InstallInstructions />
|
||||
},
|
||||
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({
|
|||
</Button>
|
||||
),
|
||||
},
|
||||
SpotifySectionTitle: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Spotify</Forms.FormTitle>
|
||||
</Forms.FormSection>
|
||||
)
|
||||
},
|
||||
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: () => (
|
||||
<Forms.FormSection>
|
||||
<Forms.FormTitle tag="h3">Tidal</Forms.FormTitle>
|
||||
</Forms.FormSection>
|
||||
)
|
||||
},
|
||||
installTidalWithWS: {
|
||||
type: OptionType.COMPONENT,
|
||||
component: () => <InstallInstructions />
|
||||
},
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -158,4 +158,89 @@
|
|||
.eq-tidal-lyrics-modal-no-lyrics {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<span key={wIdx}>
|
||||
{word.characters.map((char, j) => {
|
||||
const charActive = position / 1000 >= char.time && position / 1000 < char.endTime;
|
||||
return (
|
||||
<span
|
||||
key={j}
|
||||
className={charActive ? "eq-tidal-lyrics-char-active" : "eq-tidal-lyrics-char"}
|
||||
style={{ opacity: charActive ? 1 : 0.5 }}
|
||||
>
|
||||
{char.char}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{wIdx < line.words.length - 1 && <span> </span>}
|
||||
</span>
|
||||
))
|
||||
) : SyncMode === "character" && line.characters && line.characters.length > 0 ? (
|
||||
line.characters.map((char, j) => {
|
||||
const charActive = position / 1000 >= char.time && position / 1000 < char.endTime;
|
||||
return (
|
||||
<span
|
||||
key={j}
|
||||
className={charActive ? "eq-tidal-lyrics-char-active" : "eq-tidal-lyrics-char"}
|
||||
style={{ opacity: charActive ? 1 : 0.5 }}
|
||||
>
|
||||
{char.char}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : SyncMode === "word" && line.words && line.words.length > 0 ? (
|
||||
line.words.map((word, j) => {
|
||||
const wordActive = position / 1000 >= word.time && position / 1000 < word.endTime;
|
||||
return (
|
||||
<span
|
||||
key={j}
|
||||
className={wordActive ? "eq-tidal-lyrics-word-active" : "eq-tidal-lyrics-word"}
|
||||
style={{ opacity: wordActive ? 1 : 0.5 }}
|
||||
>
|
||||
{word.word}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
line.text || NoteElement
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ModalRoot {...rootProps}>
|
||||
|
|
@ -58,7 +61,54 @@ export function LyricsModal({ rootProps }: { rootProps: ModalProps; }) {
|
|||
onClick={() => TidalStore.seek(line.time * 1000)}
|
||||
>
|
||||
{formatTime(line.time)}
|
||||
</span>{line.text || NoteSvg(cl("modal-note"))}
|
||||
</span>
|
||||
{SyncMode === "character" && line.characters && line.characters.length > 0 && line.words && line.words.length > 0 ? (
|
||||
line.words.map((word, wIdx) => (
|
||||
<span key={wIdx}>
|
||||
{word.characters.map((char, j) => {
|
||||
const charActive = position / 1000 >= char.time && position / 1000 < char.endTime;
|
||||
return (
|
||||
<span
|
||||
key={j}
|
||||
className={charActive ? cl("char-active") : cl("char")}
|
||||
style={{ opacity: charActive ? 1 : 0.5 }}
|
||||
>
|
||||
{char.char}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{wIdx < line.words.length - 1 && <span> </span>}
|
||||
</span>
|
||||
))
|
||||
) : SyncMode === "character" && line.characters && line.characters.length > 0 ? (
|
||||
line.characters.map((char, j) => {
|
||||
const charActive = position / 1000 >= char.time && position / 1000 < char.endTime;
|
||||
return (
|
||||
<span
|
||||
key={j}
|
||||
className={charActive ? cl("char-active") : cl("char")}
|
||||
style={{ opacity: charActive ? 1 : 0.5 }}
|
||||
>
|
||||
{char.char}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : SyncMode === "word" && line.words && line.words.length > 0 ? (
|
||||
line.words.map((word, j) => {
|
||||
const wordActive = position / 1000 >= word.time && position / 1000 < word.endTime;
|
||||
return (
|
||||
<span
|
||||
key={j}
|
||||
className={wordActive ? cl("word-active") : cl("word")}
|
||||
style={{ opacity: wordActive ? 1 : 0.5, marginRight: 2 }}
|
||||
>
|
||||
{word.word}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
line.text || NoteSvg(cl("modal-note"))
|
||||
)}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue