mirror of
https://github.com/Vendicated/Vencord.git
synced 2025-01-26 17:26:22 +00:00
refactor(betterSettings): split index into multiple files
This commit is contained in:
parent
dd5dab5efb
commit
8278641c9b
5 changed files with 332 additions and 263 deletions
69
src/plugins/betterActivities/components/ActivityTooltip.tsx
Normal file
69
src/plugins/betterActivities/components/ActivityTooltip.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { classNameFactory } from "@api/Styles";
|
||||||
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
|
import { findComponentByCodeLazy } from "@webpack";
|
||||||
|
import { moment, React, useMemo } from "@webpack/common";
|
||||||
|
import { User } from "discord-types/general";
|
||||||
|
|
||||||
|
import { Activity, Application } from "../types";
|
||||||
|
import {
|
||||||
|
formatElapsedTime,
|
||||||
|
getActivityImage,
|
||||||
|
getApplicationIcons,
|
||||||
|
getValidStartTimeStamp,
|
||||||
|
getValidTimestamps
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
const TimeBar = findComponentByCodeLazy<{
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
themed: boolean;
|
||||||
|
className: string;
|
||||||
|
}>("isSingleLine");
|
||||||
|
|
||||||
|
const ActivityTooltip = ({ activity, application, user, cl }: Readonly<{ activity: Activity, application?: Application, user: User; cl: ReturnType<typeof classNameFactory> }>) => {
|
||||||
|
const image = useMemo(() => {
|
||||||
|
const activityImage = getActivityImage(activity, application);
|
||||||
|
if (activityImage) {
|
||||||
|
return activityImage;
|
||||||
|
}
|
||||||
|
const icon = getApplicationIcons([activity], true)[0];
|
||||||
|
return icon?.image.src;
|
||||||
|
}, [activity]);
|
||||||
|
const timestamps = useMemo(() => getValidTimestamps(activity), [activity]);
|
||||||
|
const startTime = useMemo(() => getValidStartTimeStamp(activity), [activity]);
|
||||||
|
|
||||||
|
const hasDetails = activity.details ?? activity.state;
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className={cl("activity")}>
|
||||||
|
{image && <img className={cl("activity-image")} src={image} alt="Activity logo" />}
|
||||||
|
<div className={cl("activity-title")}>{activity.name}</div>
|
||||||
|
{hasDetails && <div className={cl("activity-divider")} />}
|
||||||
|
<div className={cl("activity-details")}>
|
||||||
|
<div>{activity.details}</div>
|
||||||
|
<div>{activity.state}</div>
|
||||||
|
{!timestamps && startTime &&
|
||||||
|
<div className={cl("activity-time-bar")}>
|
||||||
|
{formatElapsedTime(moment(startTime), moment())}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{timestamps && (
|
||||||
|
<TimeBar start={timestamps.start}
|
||||||
|
end={timestamps.end}
|
||||||
|
themed={false}
|
||||||
|
className={cl("activity-time-bar")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActivityTooltip;
|
|
@ -18,105 +18,26 @@
|
||||||
|
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
|
|
||||||
import { definePluginSettings } from "@api/Settings";
|
|
||||||
import { classNameFactory } from "@api/Styles";
|
import { classNameFactory } from "@api/Styles";
|
||||||
import ErrorBoundary from "@components/ErrorBoundary";
|
import ErrorBoundary from "@components/ErrorBoundary";
|
||||||
import { Devs } from "@utils/constants";
|
import { Devs } from "@utils/constants";
|
||||||
import definePlugin, { OptionType } from "@utils/types";
|
import definePlugin from "@utils/types";
|
||||||
import { findByPropsLazy, findComponentByCodeLazy, findStoreLazy } from "@webpack";
|
import { findComponentByCodeLazy } from "@webpack";
|
||||||
import { moment, PresenceStore, React, Tooltip, useMemo, useStateFromStores } from "@webpack/common";
|
import { PresenceStore, React, Tooltip, useStateFromStores } from "@webpack/common";
|
||||||
import { Guild, User } from "discord-types/general";
|
import { Guild, User } from "discord-types/general";
|
||||||
|
|
||||||
|
import ActivityTooltip from "./components/ActivityTooltip";
|
||||||
import { Caret } from "./components/Caret";
|
import { Caret } from "./components/Caret";
|
||||||
import { SpotifyIcon } from "./components/SpotifyIcon";
|
import { SpotifyIcon } from "./components/SpotifyIcon";
|
||||||
import { TwitchIcon } from "./components/TwitchIcon";
|
import { TwitchIcon } from "./components/TwitchIcon";
|
||||||
import { Activity, ActivityListIcon, Application, ApplicationIcon, IconCSSProperties, Timestamp } from "./types";
|
import settings from "./settings";
|
||||||
|
import { Activity, ActivityListIcon, ActivityViewProps, ApplicationIcon, IconCSSProperties } from "./types";
|
||||||
const settings = definePluginSettings({
|
import {
|
||||||
memberList: {
|
getApplicationIcons
|
||||||
type: OptionType.BOOLEAN,
|
} from "./utils";
|
||||||
description: "Show activity icons in the member list",
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true,
|
|
||||||
},
|
|
||||||
iconSize: {
|
|
||||||
type: OptionType.SLIDER,
|
|
||||||
description: "Size of the activity icons",
|
|
||||||
markers: [10, 15, 20],
|
|
||||||
default: 15,
|
|
||||||
stickToMarkers: false,
|
|
||||||
},
|
|
||||||
specialFirst: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Show special activities first (Currently Spotify and Twitch)",
|
|
||||||
default: true,
|
|
||||||
restartNeeded: false,
|
|
||||||
},
|
|
||||||
renderGifs: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Allow rendering GIFs",
|
|
||||||
default: true,
|
|
||||||
restartNeeded: false,
|
|
||||||
},
|
|
||||||
divider: {
|
|
||||||
type: OptionType.COMPONENT,
|
|
||||||
description: "",
|
|
||||||
component: () => (
|
|
||||||
<div style={{
|
|
||||||
width: "100%",
|
|
||||||
height: 1,
|
|
||||||
borderTop: "thin solid var(--background-modifier-accent)",
|
|
||||||
paddingTop: 5,
|
|
||||||
paddingBottom: 5
|
|
||||||
}}/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
profileSidebar: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Show all activities in the profile sidebar",
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true,
|
|
||||||
},
|
|
||||||
userPopout: {
|
|
||||||
type: OptionType.BOOLEAN,
|
|
||||||
description: "Show all activities in the user popout",
|
|
||||||
default: true,
|
|
||||||
restartNeeded: true,
|
|
||||||
},
|
|
||||||
allActivitiesStyle: {
|
|
||||||
type: OptionType.SELECT,
|
|
||||||
description: "Style for showing all activities",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
default: true,
|
|
||||||
label: "Carousel",
|
|
||||||
value: "carousel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "List",
|
|
||||||
value: "list",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cl = classNameFactory("vc-bactivities-");
|
const cl = classNameFactory("vc-bactivities-");
|
||||||
|
|
||||||
const ApplicationStore: {
|
|
||||||
getApplication: (id: string) => Application | null;
|
|
||||||
} = findStoreLazy("ApplicationStore");
|
|
||||||
|
|
||||||
const { fetchApplication }: {
|
|
||||||
fetchApplication: (id: string) => Promise<Application | null>;
|
|
||||||
} = findByPropsLazy("fetchApplication");
|
|
||||||
|
|
||||||
const TimeBar = findComponentByCodeLazy<{
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
themed: boolean;
|
|
||||||
className: string;
|
|
||||||
}>("isSingleLine");
|
|
||||||
|
|
||||||
const ActivityView = findComponentByCodeLazy<{
|
const ActivityView = findComponentByCodeLazy<{
|
||||||
activity: Activity | null;
|
activity: Activity | null;
|
||||||
user: User;
|
user: User;
|
||||||
|
@ -128,178 +49,6 @@ const ActivityView = findComponentByCodeLazy<{
|
||||||
// if discord one day decides to change their icon this needs to be updated
|
// if discord one day decides to change their icon this needs to be updated
|
||||||
const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z");
|
const DefaultActivityIcon = findComponentByCodeLazy("M6,7 L2,7 L2,6 L6,6 L6,7 Z M8,5 L2,5 L2,4 L8,4 L8,5 Z M8,3 L2,3 L2,2 L8,2 L8,3 Z M8.88888889,0 L1.11111111,0 C0.494444444,0 0,0.494444444 0,1.11111111 L0,8.88888889 C0,9.50253861 0.497461389,10 1.11111111,10 L8.88888889,10 C9.50253861,10 10,9.50253861 10,8.88888889 L10,1.11111111 C10,0.494444444 9.5,0 8.88888889,0 Z");
|
||||||
|
|
||||||
const fetchedApplications = new Map<string, Application | null>();
|
|
||||||
|
|
||||||
const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png"; // TODO: replace with "renderXboxImage"?
|
|
||||||
|
|
||||||
function getActivityImage(activity: Activity, application?: Application): string | undefined {
|
|
||||||
if (activity.type === 2 && activity.name === "Spotify") {
|
|
||||||
// get either from large or small image
|
|
||||||
const image = activity.assets?.large_image ?? activity.assets?.small_image;
|
|
||||||
// image needs to replace 'spotify:'
|
|
||||||
if (image?.startsWith("spotify:")) {
|
|
||||||
// spotify cover art is always https://i.scdn.co/image/ID
|
|
||||||
return image.replace("spotify:", "https://i.scdn.co/image/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (activity.type === 1 && activity.name === "Twitch") {
|
|
||||||
const image = activity.assets?.large_image;
|
|
||||||
// image needs to replace 'twitch:'
|
|
||||||
if (image?.startsWith("twitch:")) {
|
|
||||||
// twitch images are always https://static-cdn.jtvnw.net/previews-ttv/live_user_USERNAME-RESOLTUON.jpg
|
|
||||||
return `${image.replace("twitch:", "https://static-cdn.jtvnw.net/previews-ttv/live_user_")}-108x60.jpg`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: we could support other assets here
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValidTimestamps(activity: Activity): Required<Timestamp> | null {
|
|
||||||
if (activity.timestamps?.start !== undefined && activity.timestamps?.end !== undefined) {
|
|
||||||
return activity.timestamps as Required<Timestamp>;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValidStartTimeStamp(activity: Activity): number | null {
|
|
||||||
if (activity.timestamps?.start !== undefined) {
|
|
||||||
return activity.timestamps.start;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customFormat = (momentObj: moment.Moment): string => {
|
|
||||||
const hours = momentObj.hours();
|
|
||||||
const formattedTime = momentObj.format("mm:ss");
|
|
||||||
return hours > 0 ? `${momentObj.format("HH:")}${formattedTime}` : formattedTime;
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatElapsedTime(startTime: moment.Moment, endTime: moment.Moment): string {
|
|
||||||
const duration = moment.duration(endTime.diff(startTime));
|
|
||||||
return `${customFormat(moment.utc(duration.asMilliseconds()))} elapsed`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ActivityTooltip = ({ activity, application, user }: Readonly<{ activity: Activity, application?: Application, user: User; }>) => {
|
|
||||||
const image = useMemo(() => {
|
|
||||||
const activityImage = getActivityImage(activity, application);
|
|
||||||
if (activityImage) {
|
|
||||||
return activityImage;
|
|
||||||
}
|
|
||||||
const icon = getApplicationIcons([activity], true)[0];
|
|
||||||
return icon?.image.src;
|
|
||||||
}, [activity]);
|
|
||||||
const timestamps = useMemo(() => getValidTimestamps(activity), [activity]);
|
|
||||||
const startTime = useMemo(() => getValidStartTimeStamp(activity), [activity]);
|
|
||||||
|
|
||||||
const hasDetails = activity.details ?? activity.state;
|
|
||||||
return (
|
|
||||||
<ErrorBoundary>
|
|
||||||
<div className={cl("activity")}>
|
|
||||||
{image && <img className={cl("activity-image")} src={image} alt="Activity logo" />}
|
|
||||||
<div className={cl("activity-title")}>{activity.name}</div>
|
|
||||||
{hasDetails && <div className={cl("activity-divider")} />}
|
|
||||||
<div className={cl("activity-details")}>
|
|
||||||
<div>{activity.details}</div>
|
|
||||||
<div>{activity.state}</div>
|
|
||||||
{!timestamps && startTime &&
|
|
||||||
<div className={cl("activity-time-bar")}>
|
|
||||||
{formatElapsedTime(moment(startTime), moment())}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{timestamps && <TimeBar start={timestamps.start} end={timestamps.end} themed={false} className={cl("activity-time-bar")} />}
|
|
||||||
</div>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function getApplicationIcons(activities: Activity[], preferSmall = false) {
|
|
||||||
const applicationIcons: ApplicationIcon[] = [];
|
|
||||||
const applications = activities.filter(activity => activity.application_id || activity.platform);
|
|
||||||
|
|
||||||
for (const activity of applications) {
|
|
||||||
const { assets, application_id, platform } = activity;
|
|
||||||
if (!application_id && !platform) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (assets) {
|
|
||||||
|
|
||||||
const addImage = (image: string, alt: string) => {
|
|
||||||
if (image.startsWith("mp:")) {
|
|
||||||
const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`;
|
|
||||||
if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) {
|
|
||||||
applicationIcons.push({
|
|
||||||
image: { src: discordMediaLink, alt },
|
|
||||||
activity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
|
||||||
applicationIcons.push({
|
|
||||||
image: { src, alt },
|
|
||||||
activity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const smallImage = assets.small_image;
|
|
||||||
const smallText = assets.small_text ?? "Small Text";
|
|
||||||
const largeImage = assets.large_image;
|
|
||||||
const largeText = assets.large_text ?? "Large Text";
|
|
||||||
if (preferSmall) {
|
|
||||||
if (smallImage) {
|
|
||||||
addImage(smallImage, smallText);
|
|
||||||
} else if (largeImage) {
|
|
||||||
addImage(largeImage, largeText);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (largeImage) {
|
|
||||||
addImage(largeImage, largeText);
|
|
||||||
} else if (smallImage) {
|
|
||||||
addImage(smallImage, smallText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (application_id) {
|
|
||||||
let application = ApplicationStore.getApplication(application_id);
|
|
||||||
if (!application) {
|
|
||||||
if (fetchedApplications.has(application_id)) {
|
|
||||||
application = fetchedApplications.get(application_id) as Application | null;
|
|
||||||
} else {
|
|
||||||
fetchedApplications.set(application_id, null);
|
|
||||||
fetchApplication(application_id).then(app => {
|
|
||||||
fetchedApplications.set(application_id, app);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (application) {
|
|
||||||
if (application.icon) {
|
|
||||||
const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`;
|
|
||||||
applicationIcons.push({
|
|
||||||
image: { src, alt: application.name },
|
|
||||||
activity,
|
|
||||||
application
|
|
||||||
});
|
|
||||||
} else if (platform === "xbox") {
|
|
||||||
applicationIcons.push({
|
|
||||||
image: { src: xboxUrl, alt: "Xbox" },
|
|
||||||
activity,
|
|
||||||
application
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (platform === "xbox") {
|
|
||||||
applicationIcons.push({
|
|
||||||
image: { src: xboxUrl, alt: "Xbox" },
|
|
||||||
activity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return applicationIcons;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default definePlugin({
|
export default definePlugin({
|
||||||
name: "BetterActivities",
|
name: "BetterActivities",
|
||||||
description: "Shows activity icons in the member list and allows showing all activities",
|
description: "Shows activity icons in the member list and allows showing all activities",
|
||||||
|
@ -322,7 +71,12 @@ export default definePlugin({
|
||||||
for (const appIcon of uniqueIcons) {
|
for (const appIcon of uniqueIcons) {
|
||||||
icons.push({
|
icons.push({
|
||||||
iconElement: <img {...appIcon.image} />,
|
iconElement: <img {...appIcon.image} />,
|
||||||
tooltip: <ActivityTooltip activity={appIcon.activity} application={appIcon.application} user={user} />
|
tooltip: <ActivityTooltip
|
||||||
|
activity={appIcon.activity}
|
||||||
|
application={appIcon.application}
|
||||||
|
user={user}
|
||||||
|
cl={cl}
|
||||||
|
/>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -333,7 +87,7 @@ export default definePlugin({
|
||||||
const activity = activities[activityIndex];
|
const activity = activities[activityIndex];
|
||||||
const iconObject: ActivityListIcon = {
|
const iconObject: ActivityListIcon = {
|
||||||
iconElement: <IconComponent />,
|
iconElement: <IconComponent />,
|
||||||
tooltip: <ActivityTooltip activity={activity} user={user} />
|
tooltip: <ActivityTooltip activity={activity} user={user} cl={cl} />
|
||||||
};
|
};
|
||||||
|
|
||||||
if (settings.store.specialFirst) {
|
if (settings.store.specialFirst) {
|
||||||
|
@ -380,7 +134,7 @@ export default definePlugin({
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: { activity: Activity; user: User, guild: Guild, channelId: string, onClose: () => void; }) {
|
showAllActivitiesComponent({ activity, user, guild, channelId, onClose }: ActivityViewProps) {
|
||||||
const [currentActivity, setCurrentActivity] = React.useState<Activity | null>(
|
const [currentActivity, setCurrentActivity] = React.useState<Activity | null>(
|
||||||
activity?.type !== 4 ? activity! : null
|
activity?.type !== 4 ? activity! : null
|
||||||
);
|
);
|
||||||
|
|
79
src/plugins/betterActivities/settings.tsx
Normal file
79
src/plugins/betterActivities/settings.tsx
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { definePluginSettings } from "@api/Settings";
|
||||||
|
import { OptionType } from "@utils/types";
|
||||||
|
import { React } from "@webpack/common";
|
||||||
|
|
||||||
|
const settings = definePluginSettings({
|
||||||
|
memberList: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show activity icons in the member list",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true,
|
||||||
|
},
|
||||||
|
iconSize: {
|
||||||
|
type: OptionType.SLIDER,
|
||||||
|
description: "Size of the activity icons",
|
||||||
|
markers: [10, 15, 20],
|
||||||
|
default: 15,
|
||||||
|
stickToMarkers: false,
|
||||||
|
},
|
||||||
|
specialFirst: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show special activities first (Currently Spotify and Twitch)",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: false,
|
||||||
|
},
|
||||||
|
renderGifs: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Allow rendering GIFs",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: false,
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
type: OptionType.COMPONENT,
|
||||||
|
description: "",
|
||||||
|
component: () => (
|
||||||
|
<div style={{
|
||||||
|
width: "100%",
|
||||||
|
height: 1,
|
||||||
|
borderTop: "thin solid var(--background-modifier-accent)",
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingBottom: 5
|
||||||
|
}}/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
profileSidebar: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show all activities in the profile sidebar",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true,
|
||||||
|
},
|
||||||
|
userPopout: {
|
||||||
|
type: OptionType.BOOLEAN,
|
||||||
|
description: "Show all activities in the user popout",
|
||||||
|
default: true,
|
||||||
|
restartNeeded: true,
|
||||||
|
},
|
||||||
|
allActivitiesStyle: {
|
||||||
|
type: OptionType.SELECT,
|
||||||
|
description: "Style for showing all activities",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
default: true,
|
||||||
|
label: "Carousel",
|
||||||
|
value: "carousel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "List",
|
||||||
|
value: "list",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default settings;
|
|
@ -4,6 +4,7 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Guild, User } from "discord-types/general";
|
||||||
import { CSSProperties, ImgHTMLAttributes } from "react";
|
import { CSSProperties, ImgHTMLAttributes } from "react";
|
||||||
|
|
||||||
export interface Timestamp {
|
export interface Timestamp {
|
||||||
|
@ -80,3 +81,11 @@ export interface ActivityListIcon {
|
||||||
export interface IconCSSProperties extends CSSProperties {
|
export interface IconCSSProperties extends CSSProperties {
|
||||||
"--icon-size": string;
|
"--icon-size": string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActivityViewProps {
|
||||||
|
activity: Activity;
|
||||||
|
user: User;
|
||||||
|
guild: Guild;
|
||||||
|
channelId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
158
src/plugins/betterActivities/utils.ts
Normal file
158
src/plugins/betterActivities/utils.ts
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
/*
|
||||||
|
* Vencord, a Discord client mod
|
||||||
|
* Copyright (c) 2024 Vendicated and contributors
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { findByPropsLazy, findStoreLazy } from "@webpack";
|
||||||
|
import { moment } from "@webpack/common";
|
||||||
|
|
||||||
|
import settings from "./settings";
|
||||||
|
import { Activity, Application, ApplicationIcon, Timestamp } from "./types";
|
||||||
|
|
||||||
|
const ApplicationStore: {
|
||||||
|
getApplication: (id: string) => Application | null;
|
||||||
|
} = findStoreLazy("ApplicationStore");
|
||||||
|
|
||||||
|
const { fetchApplication }: {
|
||||||
|
fetchApplication: (id: string) => Promise<Application | null>;
|
||||||
|
} = findByPropsLazy("fetchApplication");
|
||||||
|
|
||||||
|
export function getActivityImage(activity: Activity, application?: Application): string | undefined {
|
||||||
|
if (activity.type === 2 && activity.name === "Spotify") {
|
||||||
|
// get either from large or small image
|
||||||
|
const image = activity.assets?.large_image ?? activity.assets?.small_image;
|
||||||
|
// image needs to replace 'spotify:'
|
||||||
|
if (image?.startsWith("spotify:")) {
|
||||||
|
// spotify cover art is always https://i.scdn.co/image/ID
|
||||||
|
return image.replace("spotify:", "https://i.scdn.co/image/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activity.type === 1 && activity.name === "Twitch") {
|
||||||
|
const image = activity.assets?.large_image;
|
||||||
|
// image needs to replace 'twitch:'
|
||||||
|
if (image?.startsWith("twitch:")) {
|
||||||
|
// twitch images are always https://static-cdn.jtvnw.net/previews-ttv/live_user_USERNAME-RESOLTUON.jpg
|
||||||
|
return `${image.replace("twitch:", "https://static-cdn.jtvnw.net/previews-ttv/live_user_")}-108x60.jpg`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: we could support other assets here
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedApplications = new Map<string, Application | null>();
|
||||||
|
|
||||||
|
// TODO: replace with "renderXboxImage"?
|
||||||
|
const xboxUrl = "https://discord.com/assets/9a15d086141be29d9fcd.png";
|
||||||
|
|
||||||
|
export function getApplicationIcons(activities: Activity[], preferSmall = false) {
|
||||||
|
const applicationIcons: ApplicationIcon[] = [];
|
||||||
|
const applications = activities.filter(activity => activity.application_id || activity.platform);
|
||||||
|
|
||||||
|
for (const activity of applications) {
|
||||||
|
const { assets, application_id, platform } = activity;
|
||||||
|
if (!application_id && !platform) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (assets) {
|
||||||
|
|
||||||
|
const addImage = (image: string, alt: string) => {
|
||||||
|
if (image.startsWith("mp:")) {
|
||||||
|
const discordMediaLink = `https://media.discordapp.net/${image.replace(/mp:/, "")}`;
|
||||||
|
if (settings.store.renderGifs || !discordMediaLink.endsWith(".gif")) {
|
||||||
|
applicationIcons.push({
|
||||||
|
image: { src: discordMediaLink, alt },
|
||||||
|
activity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const src = `https://cdn.discordapp.com/app-assets/${application_id}/${image}.png`;
|
||||||
|
applicationIcons.push({
|
||||||
|
image: { src, alt },
|
||||||
|
activity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const smallImage = assets.small_image;
|
||||||
|
const smallText = assets.small_text ?? "Small Text";
|
||||||
|
const largeImage = assets.large_image;
|
||||||
|
const largeText = assets.large_text ?? "Large Text";
|
||||||
|
if (preferSmall) {
|
||||||
|
if (smallImage) {
|
||||||
|
addImage(smallImage, smallText);
|
||||||
|
} else if (largeImage) {
|
||||||
|
addImage(largeImage, largeText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (largeImage) {
|
||||||
|
addImage(largeImage, largeText);
|
||||||
|
} else if (smallImage) {
|
||||||
|
addImage(smallImage, smallText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (application_id) {
|
||||||
|
let application = ApplicationStore.getApplication(application_id);
|
||||||
|
if (!application) {
|
||||||
|
if (fetchedApplications.has(application_id)) {
|
||||||
|
application = fetchedApplications.get(application_id) as Application | null;
|
||||||
|
} else {
|
||||||
|
fetchedApplications.set(application_id, null);
|
||||||
|
fetchApplication(application_id).then(app => {
|
||||||
|
fetchedApplications.set(application_id, app);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (application) {
|
||||||
|
if (application.icon) {
|
||||||
|
const src = `https://cdn.discordapp.com/app-icons/${application.id}/${application.icon}.png`;
|
||||||
|
applicationIcons.push({
|
||||||
|
image: { src, alt: application.name },
|
||||||
|
activity,
|
||||||
|
application
|
||||||
|
});
|
||||||
|
} else if (platform === "xbox") {
|
||||||
|
applicationIcons.push({
|
||||||
|
image: { src: xboxUrl, alt: "Xbox" },
|
||||||
|
activity,
|
||||||
|
application
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (platform === "xbox") {
|
||||||
|
applicationIcons.push({
|
||||||
|
image: { src: xboxUrl, alt: "Xbox" },
|
||||||
|
activity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applicationIcons;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValidTimestamps(activity: Activity): Required<Timestamp> | null {
|
||||||
|
if (activity.timestamps?.start !== undefined && activity.timestamps?.end !== undefined) {
|
||||||
|
return activity.timestamps as Required<Timestamp>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getValidStartTimeStamp(activity: Activity): number | null {
|
||||||
|
if (activity.timestamps?.start !== undefined) {
|
||||||
|
return activity.timestamps.start;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customFormat = (momentObj: moment.Moment): string => {
|
||||||
|
const hours = momentObj.hours();
|
||||||
|
const formattedTime = momentObj.format("mm:ss");
|
||||||
|
return hours > 0 ? `${momentObj.format("HH:")}${formattedTime}` : formattedTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatElapsedTime(startTime: moment.Moment, endTime: moment.Moment): string {
|
||||||
|
const duration = moment.duration(endTime.diff(startTime));
|
||||||
|
return `${customFormat(moment.utc(duration.asMilliseconds()))} elapsed`;
|
||||||
|
}
|
Loading…
Reference in a new issue