SongSpotlight: UI updates (#868)

* feat(SongSpotlight): mostly UI updates!
- new compact settings
- css fixes
- & more!

* fix(SongSpotlight): dont really need the filter

* feat(SongSpotlight): update the wording
This commit is contained in:
nexpid 2026-03-03 21:18:20 +01:00 • committed by GitHub
parent 5fe5bd62a3
commit 7a546afa1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 223 additions and 33 deletions

View file

@ -6,15 +6,14 @@
import "./style.css";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { EquicordDevs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types";
import definePlugin from "@utils/types";
import { useAuthorizationStore } from "./lib/stores/AuthorizationStore";
import { useSongStore } from "./lib/stores/SongStore";
import { Native } from "./service";
import Settings from "./ui/settings";
import settings from "./settings";
import ProfileSongs from "./ui/songs/ProfileSongs";
import WidgetSongs from "./ui/songs/WidgetSongs";
@ -22,12 +21,7 @@ export default definePlugin({
name: "SongSpotlight",
description: "Show off songs on your profile",
authors: [EquicordDevs.nexpid],
settings: definePluginSettings({
manager: {
type: OptionType.COMPONENT,
component: () => <Settings />,
},
}),
settings,
patches: [
// Personal profile popout
{
@ -50,7 +44,7 @@ export default definePlugin({
find: ".SIDEBAR}),nicknameIcons:",
replacement: {
match: /{userId:(\i)\.id}\)}\).{0,100}]}\)(?=\]\}\))/,
replace: "$&,$self.renderProfileSongs({userId:$1.id})",
replace: "$&,$self.renderProfileSongs({userId:$1.id,isSidebar:true})",
},
},
// Full profile modal sections (lazy loaded)
@ -58,14 +52,14 @@ export default definePlugin({
find: ".MUTUAL_GUILDS})),",
replacement: {
match: /(\i).push\({text.{0,50}}\);/,
replace: "$&$1.push({text:\"Song Spotlight\",section:\"SONG_SPOTLIGHT\"});",
replace: '$&$1.push({text:"Song Spotlight",section:"SONG_SPOTLIGHT"});',
},
},
{
find: ".hasUnsavedChanges()&&",
replacement: {
match: /({user:(\i),.{0,80}return (\i===))/,
replace: "$1\"SONG_SPOTLIGHT\"?$self.renderWidgetSongs({user:$2}):$3",
replace: '$1"SONG_SPOTLIGHT"?$self.renderWidgetSongs({user:$2}):$3',
},
},
],

View file

@ -0,0 +1,34 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { makeRange, OptionType } from "@utils/types";
import Settings from "./ui/settings";
export default definePluginSettings({
collapseSongList: {
type: OptionType.BOOLEAN,
description: "Collapses the song list on user profiles to a button which opens a separate menu",
default: false,
},
profileSongsLimit: {
type: OptionType.SLIDER,
description: "How many songs are shown when initially clicking on a user",
default: 6,
markers: makeRange(1, 6),
},
manager: {
type: OptionType.COMPONENT,
component: () => <Settings />,
},
}, {
profileSongsLimit: {
disabled() {
return this.store.collapseSongList;
},
},
});

View file

@ -23,7 +23,10 @@
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
/* components */
@ -137,13 +140,13 @@
--list-height: calc(var(--base-height) + 80px);
--number-size: 32px;
padding: 8px;
gap: 12px;
overflow: visible;
}
display: flex;
flex-direction: column;
gap: 6px;
.user-profile-sidebar .vc-songspotlight-songs-container {
padding: 12px;
&.vc-songspotlight-songs-container-sidebar {
padding: 12px;
}
}
.vc-songspotlight-songs-more {
@ -194,6 +197,10 @@
}
}
.user-profile-sidebar .vc-songspotlight-song-container {
padding: 8px;
}
.user-profile-popout:not(.custom-user-profile-theme) .vc-songspotlight-song-container {
background: var(--opacity-16);
}
@ -211,7 +218,6 @@
grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto;
gap: 8px;
overflow: visible;
border-radius: inherit;
}
@ -220,7 +226,7 @@
grid-column: 1;
grid-row: 1 / 3;
border-radius: inherit;
overflow: visible;
min-width: 0;
}
.vc-songspotlight-song-cover {
@ -232,7 +238,6 @@
flex-shrink: 0;
box-shadow: 0 3px 12px 0 rgb(0 0 0 / 16%);
transition: 0.18s transform;
overflow: visible;
&:hover {
transform: scale(1.03);

View file

@ -12,6 +12,14 @@ import { Tooltip } from "@webpack/common";
import { ComponentType, HTMLAttributes, JSX } from "react";
export const OverlayClasses = findCssClassesLazy("overlay", "inner", "outer");
export const ContainerClasses = findCssClassesLazy(
"breadcrumb",
"innerContainer",
"icons",
"icon",
"displayCount",
"displayCountText",
);
export const CardClasses = findCssClassesLazy("card", "headerText", "headerContextMenu");
export const WidgetClasses = findCssClassesLazy("tabPanelScroller", "tabListScroller");

View file

@ -0,0 +1,103 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BaseText } from "@components/index";
import { Native } from "@equicordplugins/songSpotlight.desktop/service";
import { CardClasses, ContainerClasses, OverlayClasses, Spinner } from "@equicordplugins/songSpotlight.desktop/ui/common";
import { RenderSongInfo } from "@song-spotlight/api/handlers";
import { UserData } from "@song-spotlight/api/structs";
import { sid } from "@song-spotlight/api/util";
import { classes } from "@utils/index";
import {
SelectedChannelStore,
SelectedGuildStore,
useEffect,
useMemo,
UserProfileActions,
UserStore,
useState,
} from "@webpack/common";
import { ProfileSongsProps } from "./ProfileSongs";
const shownSongs = 4;
interface CollapsedProfileSongsProps extends ProfileSongsProps {
data?: UserData;
}
export default function CollapsedProfileSongs({ data, userId, isSidebar }: CollapsedProfileSongsProps) {
const [renders, setRenders] = useState(new Map<string, RenderSongInfo>());
const previews = useMemo(() => data?.slice(0, shownSongs), [data]);
useEffect(() => {
setRenders(new Map());
if (!previews) return;
for (const song of previews) {
Native.renderSong(song)
.catch(() => null)
.then(info => {
if (!info) return;
setRenders(renders => new Map(renders).set(sid(song), info));
});
}
}, [previews]);
return (
<div
className={ContainerClasses.breadcrumb}
aria-label="Song Spotlight"
role="button"
tabIndex={0}
onClick={() => {
const user = UserStore.getUser(userId);
if (!user) return;
const guildId = SelectedGuildStore.getGuildId();
UserProfileActions.openUserProfileModal({
userId,
guildId,
channelId: SelectedChannelStore.getChannelId(),
analyticsLocation: {
page: guildId ? "Guild Channel" : "DM Channel",
section: "Profile Popout",
},
tabSection: "SONG_SPOTLIGHT",
});
}}
>
<div className={classes(OverlayClasses.overlay, ContainerClasses.innerContainer, CardClasses.card)}>
<BaseText size={isSidebar ? "sm" : "xs"} weight="medium">Song Spotlight</BaseText>
<div className={ContainerClasses.icons}>
{previews && data
? previews.map((song, i) => {
const render = renders.get(sid(song));
const extra = i === shownSongs - 1 && data.length > shownSongs;
return (
<div className={ContainerClasses.icon} key={i}>
{render?.thumbnailUrl && (
<img
src={render.thumbnailUrl}
alt={render.label}
className={classes(extra && ContainerClasses.displayCount)}
/>
)}
{extra && (
<div className={ContainerClasses.displayCountText}>
<BaseText size="xs" weight="medium">+{data.length - shownSongs}</BaseText>
</div>
)}
</div>
);
})
: <Spinner type={Spinner.Type.SPINNING_CIRCLE} />}
</div>
</div>
</div>
);
}

View file

@ -7,10 +7,12 @@
import { BaseText } from "@components/BaseText";
import { Flex } from "@components/Flex";
import { LinkIcon, PencilIcon } from "@components/Icons";
import { Link } from "@components/Link";
import { listData } from "@equicordplugins/songSpotlight.desktop/lib/api";
import { useAuthorizationStore } from "@equicordplugins/songSpotlight.desktop/lib/stores/AuthorizationStore";
import { useSongStore } from "@equicordplugins/songSpotlight.desktop/lib/stores/SongStore";
import { cl } from "@equicordplugins/songSpotlight.desktop/lib/utils";
import settings from "@equicordplugins/songSpotlight.desktop/settings";
import {
CardClasses,
MoreHorizontalIcon,
@ -20,40 +22,75 @@ import {
import { openSettingsModal } from "@equicordplugins/songSpotlight.desktop/ui/settings";
import { sid } from "@song-spotlight/api/util";
import { copyWithToast } from "@utils/discord";
import { classes } from "@utils/misc";
import { ContextMenuApi, FluxDispatcher, Menu, React, Tooltip, useEffect, UserStore, useState } from "@webpack/common";
import { classes } from "@utils/index";
import {
ContextMenuApi,
FluxDispatcher,
Menu,
React,
Tooltip,
useEffect,
useMemo,
UserStore,
useState,
} from "@webpack/common";
import Song from ".";
import CollapsedProfileSongs from "./CollapsedProfileSongs";
interface ProfileSongsProps {
export interface ProfileSongsProps {
userId: string;
isSidebar: boolean;
}
export default function ProfileSongs({ userId }: ProfileSongsProps) {
export default function ProfileSongs({ userId, isSidebar }: ProfileSongsProps) {
const [failed, setFailed] = useState(false);
const { isAuthorized } = useAuthorizationStore();
const { users } = useSongStore();
const { profileSongsLimit, collapseSongList } = settings.use();
const data = users[userId]?.data, at = users[userId]?.at;
const data = users[userId]?.data;
useEffect(() => {
if (isAuthorized() && !data) listData(userId).catch(() => setFailed(true));
}, [isAuthorized()]);
const [clamped, setClamped] = useState(true);
const clampedData = useMemo(() => data && (clamped ? data.slice(0, profileSongsLimit) : data), [
data,
clamped,
profileSongsLimit,
]);
const owned = UserStore.getCurrentUser().id === userId;
if (isAuthorized() && !data && !failed) {
const pending = isAuthorized() && !clampedData && !data && !failed;
if (!pending && !clampedData?.[0]) return null;
if (collapseSongList) {
return (
<CollapsedProfileSongs
data={data}
userId={userId}
isSidebar={isSidebar}
/>
);
} else if (pending) {
return <Spinner type={Spinner.Type.WANDERING_CUBES} />;
} else if (!data?.[0]) {
return null;
}
return (
<div
className={classes(OverlayClasses.overlay, CardClasses.card, cl("songs-container"))}
className={classes(
isSidebar && OverlayClasses.overlay,
isSidebar && CardClasses.card,
cl("songs-container", isSidebar && "songs-container-sidebar"),
)}
key="song-spotlight-profile-songs"
>
<Flex justifyContent="space-between">
<BaseText size="xs" weight="semibold" className={cl("header")}>Song Spotlight</BaseText>
<BaseText size="xs" weight={isSidebar ? "semibold" : "medium"} className={cl("header")}>
Song Spotlight
</BaseText>
<Tooltip text="More">
{props => (
<MoreHorizontalIcon
@ -89,8 +126,17 @@ export default function ProfileSongs({ userId }: ProfileSongsProps) {
</Tooltip>
</Flex>
<Flex flexDirection="column" gap="6px">
{data.map((song, i) => <Song owned={owned} song={song} index={i} key={sid(song)} />)}
{clampedData.map((song, i) => <Song owned={owned} song={song} index={i} key={sid(song)} />)}
</Flex>
{clamped && data.length > profileSongsLimit && (
<Link
onClick={() => setClamped(false)}
>
<BaseText size="sm" weight="medium" className={cl("sub")} style={{ textAlign: "center" }}>
Show {data.length - profileSongsLimit} more
</BaseText>
</Link>
)}
</div>
);
}

View file

@ -187,7 +187,7 @@ function SongInfo({ owned, song, render, big }: SongInfoProps) {
icon={LinkIcon}
action={() => copyWithToast(render.link)}
/>
{owned
{!owned
? (
<Menu.MenuItem
id="steal-song"