This commit is contained in:
thororen 2024-04-17 14:29:47 -04:00
parent 538b87062a
commit ea7451bcdc
326 changed files with 24876 additions and 2280 deletions

View file

@ -0,0 +1,193 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { classes } from "@utils/misc";
import { closeModal, ModalCloseButton,ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import { LazyComponent } from "@utils/react";
import { find, findByPropsLazy } from "@webpack";
import { Button, Clickable, Forms, GuildStore, PermissionsBits, PermissionStore, Popout, SearchableSelect, showToast, Text, TextInput, Toasts, useMemo, UserStore, useState } from "@webpack/common";
import { Guild } from "discord-types/general";
import { HtmlHTMLAttributes } from "react";
import { cl, getEmojiUrl,SoundEvent } from "../utils";
export function openCloneSoundModal(item) {
const key = openModal(props =>
<ModalRoot {...props}>
<CloneSoundModal item={item} closeModal={() => closeModal(key)} />
</ModalRoot>
);
}
// Thanks https://github.com/Vendicated/Vencord/blob/ea11f2244fde469ce308f8a4e7224430be62f8f1/src/plugins/emoteCloner/index.tsx#L173-L177
const getFontSize = (s: string, small: boolean = false) => {
const sizes = [20, 20, 18, 18, 16, 14, 12];
return sizes[s.length + (small ? 1 : 0)] ?? 8;
};
function GuildAcronym({ acronym, small, style = {} }) {
return (
<Flex style={{ alignItems: "center", justifyContent: "center", overflow: "hidden", fontSize: getFontSize(acronym, small), ...style }}>
<Text>{acronym}</Text>
</Flex>
);
}
function CustomInput({ children, className = "", ...props }: HtmlHTMLAttributes<HTMLDivElement>) {
return <div className={classes(cl("clone-input"), className)} {...props}>
{children}
</div>;
}
const EmojiPicker = LazyComponent(() => find(e => e.default?.type?.render?.toString?.().includes?.(".updateNewlyAddedLastSeen)")).default);
const sounds = findByPropsLazy("uploadSound", "updateSound");
export function CloneSoundModal({ item, closeModal }: { item: SoundEvent, closeModal: () => void; }) {
const ownedGuilds = useMemo(() => {
return Object.values(GuildStore.getGuilds()).filter(guild =>
guild.ownerId === UserStore.getCurrentUser().id ||
(PermissionStore.getGuildPermissions({ id: guild.id }) & PermissionsBits.CREATE_GUILD_EXPRESSIONS) === PermissionsBits.CREATE_GUILD_EXPRESSIONS);
}, []);
const [selectedGuild, setSelectedGuild] = useState<Guild | null>(null);
const [soundName, setSoundName] = useState<string | undefined>(undefined);
const [soundEmoji, setSoundEmoji] = useState<any | undefined>(undefined);
const [loadingButton, setLoadingButton] = useState<boolean>(false);
const [show, setShow] = useState(false);
const isEmojiValid = soundEmoji?.guildId ? soundEmoji.guildId === selectedGuild?.id : true;
const styles = {
selected: { height: "24px", width: "24px" },
nonselected: { width: "36px", height: "36px", marginTop: "4px" }
};
const getStyle = key => key === "selected" ? styles.selected : styles.nonselected;
function onSelectEmoji(emoji) {
setShow(false);
setSoundEmoji(emoji);
}
return <>
<ModalHeader>
<Flex style={{ width: "100%", justifyContent: "center" }}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>Clone Sound</Text>
<ModalCloseButton onClick={closeModal} />
</Flex>
</ModalHeader>
<ModalContent>
<Forms.FormTitle className={Margins.top16}>Cloning Sound</Forms.FormTitle>
<CustomInput style={{ display: "flex", flexDirection: "row", gap: "10px", alignItems: "center" }} className={Margins.bottom16}>
<img src={getEmojiUrl(item.emoji)} width="24" height="24" />
<Text>{item.soundId}</Text>
</CustomInput>
<Forms.FormTitle required={true} aria-required="true">Add to server:</Forms.FormTitle>
<SearchableSelect
options={
ownedGuilds.map(guild => ({
label: guild.name,
value: guild
}))
}
placeholder="Select a server"
value={selectedGuild ? ({
label: selectedGuild.name,
value: selectedGuild,
key: "selected"
}) : undefined}
onChange={v => setSelectedGuild(v)}
closeOnSelect={true}
renderOptionPrefix={v => v ? (
v.value.icon ?
<img width={36} height={36} src={v.value.getIconURL(96, true)} style={{ borderRadius: "50%", ...getStyle(v.key) }} /> :
<GuildAcronym acronym={v.value.acronym} style={getStyle(v.key)} small={v.key === "selected"} />
) : null}
/>
<Flex flexDirection="row" style={{ gap: "10px", justifyContent: "space-between" }} className={classes(Margins.top16, Margins.bottom16)}>
<div style={{ flex: 1 }}>
<Forms.FormTitle required={true} aria-required="true">Sound Name</Forms.FormTitle>
<TextInput value={soundName} onChange={v => setSoundName(v)} placeholder="Sound Name" />
</div>
<div style={{ flex: 1 }}>
<Forms.FormTitle>Related Emoji</Forms.FormTitle>
<Popout
position="bottom"
align="right"
animation={Popout.Animation.NONE}
shouldShow={show}
onRequestClose={() => setShow(false)}
renderPopout={() => <EmojiPicker pickerIntention={2} channel={{ getGuildId: () => selectedGuild?.id }} onSelectEmoji={onSelectEmoji} />}
>
{() => (
<Clickable onClick={() => setShow(v => !v)}>
<CustomInput style={{ display: "flex", flexDirection: "row", gap: "10px", alignItems: "center", cursor: "pointer" }}>
{soundEmoji ?
<>
<img src={getEmojiUrl({ name: soundEmoji.surrogates, id: soundEmoji.id })} width="24" height="24" style={{ cursor: "pointer" }} />
<Text style={{ color: "var(--text-muted)", cursor: "pointer" }}>:{soundEmoji.name ? soundEmoji.name.split("~")[0] : soundEmoji.uniqueName}:</Text>
</> :
<>
<img src={getEmojiUrl({ name: "😊" })} width="24" height="24" style={{ filter: "grayscale(100%)", cursor: "pointer" }} />
<Text style={{ color: "var(--text-muted)", cursor: "pointer" }}>Click to Select</Text>
</>
}
</CustomInput>
</Clickable>
)}
</Popout>
</div>
</Flex>
{!isEmojiValid && <Forms.FormText style={{ color: "var(--text-danger)" }} className={Margins.bottom16}>You can't use that emoji in that server</Forms.FormText>}
<Button onClick={() => {
setLoadingButton(true);
fetch(`https://cdn.discordapp.com/soundboard-sounds/${item.soundId}`).then(function (response) {
if (!response.body) {
setLoadingButton(false);
showToast("Error fetching the sound", Toasts.Type.FAILURE);
return;
}
response.body.getReader().read().then(function (result) {
if (!result.value) {
setLoadingButton(false);
showToast("Error reading the sound content", Toasts.Type.FAILURE);
return;
}
return btoa(String.fromCharCode(...result.value));
}).then(function (b64) {
sounds.uploadSound({
guildId: selectedGuild?.id,
name: soundName,
sound: `data:audio/ogg;base64,${b64}`,
...(soundEmoji.id ? { emojiId: soundEmoji.id } : { emojiName: soundEmoji.surrogates }),
volume: 1
}).then(() => {
showToast(`Sound added to ${selectedGuild?.name}`, Toasts.Type.SUCCESS);
closeModal();
}).catch(() => {
setLoadingButton(false);
showToast("Error while adding sound", Toasts.Type.FAILURE);
});
});
}).catch(e => {
setLoadingButton(false);
showToast("Error fetching the sound", Toasts.Type.FAILURE);
return;
});
}}
disabled={(!(selectedGuild && soundName && isEmojiValid)) || loadingButton}
size={Button.Sizes.MEDIUM}
style={{ width: "100%" }}
className={Margins.bottom16}>Add to Server</Button>
</ModalContent>
</>;
}

View file

@ -0,0 +1,57 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classes } from "@utils/misc";
import { Button, ButtonLooks, ButtonWrapperClasses, Tooltip } from "@webpack/common";
import { cl } from "../utils";
// Thanks svgrepo.com for the play and download icons.
// Licensed under CC Attribution License https://www.svgrepo.com/page/licensing/#CC%20Attribution
// https://www.svgrepo.com/svg/438144/multimedia-play-icon-circle-button
export function PlayIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 20 20"><g fill="none" fill-rule="evenodd" transform="translate(-2 -2)"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" /><path fill="currentColor" d="m9.998 8.428 5.492 3.138a.5.5 0 0 1 0 .868l-5.492 3.139a.5.5 0 0 1-.748-.435V8.862a.5.5 0 0 1 .748-.435z" /></g></svg>
);
}
// https://www.svgrepo.com/svg/528952/download
export function DownloadIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M8 22h8c2.828 0 4.243 0 5.121-.879C22 20.243 22 18.828 22 16v-1c0-2.828 0-4.243-.879-5.121-.768-.769-1.946-.865-4.121-.877m-10 0c-2.175.012-3.353.108-4.121.877C2 10.757 2 12.172 2 15v1c0 2.828 0 4.243.879 5.121.3.3.662.498 1.121.628" /><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 2v13m0 0-3-3.5m3 3.5 3-3.5" /></svg>
);
}
export function LogIcon({ height = 24, width = 24, className }: { height?: number; width?: number; className?: string; }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} className={classes(cl("icon"), className)} viewBox="1.134 10.59 87.732 68.821"><path fill="currentColor" d="M84.075 10.597 5.932 10.59a4.799 4.799 0 0 0-4.798 4.79v59.226a4.801 4.801 0 0 0 4.798 4.798l78.144.007a4.79 4.79 0 0 0 4.79-4.794V15.391a4.796 4.796 0 0 0-4.791-4.794zm-11.704 8.766c0-.283.229-.509.51-.509h7.105c.279 0 .501.226.501.509v7.102c0 .28-.222.509-.501.509h-7.105a.511.511 0 0 1-.51-.509v-7.102zm-63.968 0c0-.283.229-.509.509-.509h29.399c.28 0 .502.226.502.509v7.102c0 .28-.222.509-.502.509H8.912a.51.51 0 0 1-.509-.509v-7.102zm7.863 45.454a.727.727 0 0 1-.727.727H14.28v6.466c0 1-.822 1.821-1.829 1.821-.516 0-.97-.199-1.301-.53a1.829 1.829 0 0 1-.531-1.291v-6.466H9.51a.726.726 0 0 1-.727-.727v-6.854c0-.4.327-.727.727-.727h1.11V37.583c0-1.017.822-1.839 1.832-1.839.505 0 .96.211 1.291.542a1.8 1.8 0 0 1 .538 1.297v19.653h1.259c.4 0 .727.326.727.727v6.854zm11.02-13.204a.73.73 0 0 1-.727.728h-1.11v19.651a1.835 1.835 0 0 1-1.832 1.839c-.506 0-.96-.21-1.291-.541a1.8 1.8 0 0 1-.538-1.298V52.341H20.53a.73.73 0 0 1-.727-.728v-6.855c0-.403.327-.727.727-.727h1.259v-6.466a1.83 1.83 0 0 1 1.829-1.821c.516 0 .97.2 1.301.531.331.331.531.792.531 1.29v6.466h1.11c.4 0 .727.324.727.727v6.855zm11.268 13.204a.727.727 0 0 1-.727.727h-1.259v6.466c0 1-.821 1.821-1.829 1.821-.516 0-.97-.199-1.301-.53a1.828 1.828 0 0 1-.53-1.291v-6.466h-1.11a.726.726 0 0 1-.727-.727v-6.854c0-.4.327-.727.727-.727h1.11V37.583c0-1.017.821-1.839 1.832-1.839.505 0 .959.211 1.291.542a1.8 1.8 0 0 1 .538 1.297v19.653h1.259c.4 0 .727.326.727.727v6.854zm10.94-13.204a.73.73 0 0 1-.728.728h-1.11v19.651a1.835 1.835 0 0 1-1.832 1.839 1.82 1.82 0 0 1-1.29-.541 1.802 1.802 0 0 1-.539-1.298V52.341h-1.259a.73.73 0 0 1-.727-.728v-6.855c0-.403.327-.727.727-.727h1.259v-6.466c0-.999.822-1.821 1.829-1.821.516 0 .97.2 1.301.531.331.331.53.792.53 1.29v6.466h1.11c.4 0 .728.324.728.727v6.855zm8.703-32.25c0-.283.229-.509.508-.509h7.106c.279 0 .501.226.501.509v7.102c0 .28-.222.509-.501.509h-7.106a.51.51 0 0 1-.508-.509v-7.102zm4.176 53.927a4.083 4.083 0 0 1-4.082-4.082c0-2.25 1.828-4.078 4.082-4.078s4.078 1.828 4.078 4.078a4.08 4.08 0 0 1-4.078 4.082zm0-14.709a4.082 4.082 0 1 1 0-8.163 4.078 4.078 0 0 1 4.078 4.081 4.08 4.08 0 0 1-4.078 4.082zm0-14.713a4.082 4.082 0 0 1-4.082-4.081 4.08 4.08 0 0 1 8.16 0 4.079 4.079 0 0 1-4.078 4.081zM76.438 73.29a4.082 4.082 0 0 1-4.081-4.082 4.081 4.081 0 0 1 4.081-4.078 4.078 4.078 0 0 1 4.078 4.078 4.078 4.078 0 0 1-4.078 4.082zm0-14.709a4.081 4.081 0 0 1 0-8.163 4.078 4.078 0 0 1 4.078 4.081 4.08 4.08 0 0 1-4.078 4.082zm0-14.713a4.082 4.082 0 0 1-4.081-4.081 4.08 4.08 0 0 1 8.159 0 4.078 4.078 0 0 1-4.078 4.081z" /></svg>
);
}
export function IconWithTooltip({ text, icon, onClick }) {
return <Tooltip text={text}>
{({ onMouseEnter, onMouseLeave }) => (
<div style={{ display: "flex" }}>
<Button
aria-haspopup="dialog"
aria-label={text}
size=""
look={ButtonLooks.BLANK}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
innerClassName={ButtonWrapperClasses.button}
onClick={onClick}
style={{ padding: "0 4px" }}
>
<div className={ButtonWrapperClasses.buttonWrapper}>
{icon}
</div>
</Button>
</div>
)}
</Tooltip>;
}

View file

@ -0,0 +1,62 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { closeModal, ModalContent, ModalRoot, openModal } from "@utils/modal";
import { Clickable, Forms } from "@webpack/common";
import { cl, getEmojiUrl,SoundLogEntry, User } from "../utils";
export function openMoreUsersModal(item: SoundLogEntry, users: User[], onClickUser: Function) {
const key = openModal(props => (
<ErrorBoundary>
<ModalRoot {...props}>
<MoreUsersModal item={item} users={users} onClickUser={onClickUser} closeModal={() => closeModal(key)} />
</ModalRoot>
</ErrorBoundary>
));
}
export default function MoreUsersModal({ item, users, onClickUser, closeModal }: { item: SoundLogEntry, users: User[], onClickUser: Function, closeModal: Function; }) {
return (
<ModalContent className={cl("more")}>
<div className={cl("more-header")}>
<img
className={cl("more-emoji")}
src={getEmojiUrl(item.emoji)}
alt=""
/>
<Forms.FormTitle tag="h2" className={cl("more-soundId")}>{item.soundId}</Forms.FormTitle>
</div>
<div className={cl("more-users")}>
{users.map(user => {
const currentUser = item.users.find(({ id }) => id === user.id) ?? { id: "", plays: [0] };
return (
<Clickable onClick={() => {
closeModal();
onClickUser(item, user);
}}>
<div className={cl("more-user")} style={{ cursor: "pointer" }}>
<Flex flexDirection="row" className={cl("more-user-profile")}>
<img
className={cl("user-avatar")}
src={user.getAvatarURL(void 0, 512, true)}
alt=""
style={{ cursor: "pointer" }}
/>
<Forms.FormText variant="text-xs/medium" style={{ cursor: "pointer" }}>{user.username}</Forms.FormText>
</Flex>
<Forms.FormText variant="text-xs/medium" style={{ cursor: "pointer" }}>Played {currentUser.plays.length} {currentUser.plays.length === 1 ? "time" : "times"}</Forms.FormText>
</div>
</Clickable>
);
})}
</div>
</ModalContent>
);
}

View file

@ -0,0 +1,192 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { Margins } from "@utils/margins";
import { classes, copyWithToast } from "@utils/misc";
import { closeModal, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalRoot, ModalSize, openModal } from "@utils/modal";
import { Button, Clickable, ContextMenuApi, FluxDispatcher, Forms, Menu, Text, Tooltip, useEffect, UserUtils, useState } from "@webpack/common";
import { User } from "discord-types/general";
import { clearLoggedSounds, getLoggedSounds } from "../store";
import { addListener, AvatarStyles, cl, downloadAudio, getEmojiUrl, getSoundboardVolume, playSound, removeListener, SoundLogEntry, UserSummaryItem } from "../utils";
import { openCloneSoundModal } from "./CloneSoundModal";
import { openMoreUsersModal } from "./MoreUsersModal";
import { openUserModal } from "./UserModal";
export async function openSoundBoardLog(): Promise<void> {
const data = await getLoggedSounds();
const key = openModal(props => <ErrorBoundary>
<ModalRoot {...props} size={ModalSize.LARGE}>
<SoundBoardLog data={data} closeModal={() => closeModal(key)} />
</ModalRoot>
</ErrorBoundary>);
}
export default function SoundBoardLog({ data, closeModal }) {
const [sounds, setSounds] = useState(data);
const [users, setUsers] = useState<User[]>([]);
const update = async () => setSounds(await getLoggedSounds());
// Update the sounds state when a new sound is played
useEffect(() => {
const onSound = () => update();
addListener(onSound);
return () => removeListener(onSound);
}, []);
const avatarsMax = 2;
// Update the users state when a new sound is played
useEffect(() => {
(async () => {
/** Array of user IDs without a resolved user object */
const missing = sounds
.flatMap(sound => sound.users) // Get all users who have used any sound
.map(user => user.id) // Get their ID ( user is {id: string, plays: number[]} )
.filter((id, index, self) => index === self.indexOf(id)) // Filter the array to remove non unique values
.filter(id => !users.map(user => user.id).includes(id)); // Filter the IDs to only get the ones not already in the users state
if (!missing.length) return; // return if every user ID is already in users
for (const id of missing) {
const user = await UserUtils.getUser(id).catch(() => void 0);
if (user) setUsers(u => [...u, user]);
}
})();
}, [sounds]);
function renderMoreUsers(item, itemUsers) {
return (
<Clickable
className={AvatarStyles.clickableAvatar}
onClick={() => {
onClickShowMoreUsers(item, itemUsers);
}}
>
<Tooltip text={`${itemUsers.length - avatarsMax} other people used this sound...`}>
{({ onMouseEnter, onMouseLeave }) => (
<div
className={AvatarStyles.moreUsers}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
+{itemUsers.length - avatarsMax}
</div>
)}
</Tooltip>
</Clickable>
);
}
/** This function is called when you click the "Show more users" button. */
function onClickShowMoreUsers(item: SoundLogEntry, users: User[]): void {
openMoreUsersModal(item, users, onClickUser);
}
function onClickUser(item: SoundLogEntry, user: User) {
openUserModal(item, user, sounds);
}
function SoundContextMenu({ item }) {
const label = id => `soundboardlogger-${id}`;
return (
<Menu.Menu
navId="soundboardlogger-sound-menu"
onClose={() => FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" })}
>
<Menu.MenuGroup label="Extra buttons">
<Menu.MenuItem
id={label("clone")}
label="Clone sound"
action={() => openCloneSoundModal(item)}
/>
</Menu.MenuGroup>
</Menu.Menu>
);
}
return (
<>
<ModalHeader className={cl("modal-header")}>
<Text variant="heading-lg/semibold" style={{ flexGrow: 1 }}>SoundBoard log</Text>
<ModalCloseButton onClick={closeModal} />
</ModalHeader>
<ModalContent className={classes(cl("modal-content"), Margins.top8)}>
{sounds.length ? sounds.map(item => {
const itemUsers = users.filter(user => item.users.map(u => u.id).includes(user.id));
return (
<div
className={cl("sound")}
onContextMenu={e =>
ContextMenuApi.openContextMenu(e, () => <SoundContextMenu item={item} />)
}
>
<Flex flexDirection="row" className={cl("sound-info")}>
<img
src={getEmojiUrl(item.emoji)}
className={cl("sound-emoji")}
/>
<Forms.FormText variant="text-xs/medium" className={cl("sound-id")}>{item.soundId}</Forms.FormText>
</Flex>
<UserSummaryItem
users={itemUsers.slice(0, avatarsMax)} // Trimmed array to the size of max
count={item.users.length - 1} // True size (counting users that aren't rendered) - 1
guildId={undefined}
renderIcon={false}
max={avatarsMax}
showDefaultAvatarsForNullUsers
showUserPopout
renderMoreUsers={() => renderMoreUsers(item, itemUsers)}
className={cl("sound-users")}
renderUser={(user: User) => (
<Clickable
className={AvatarStyles.clickableAvatar}
onClick={() => {
onClickUser(item, user);
}}
>
<img
className={AvatarStyles.avatar}
src={user.getAvatarURL(void 0, 80, true)}
alt={user.username}
title={user.username}
/>
</Clickable>
)}
/>
<Flex flexDirection="row" className={cl("sound-buttons")}>
<Button color={Button.Colors.PRIMARY} size={Button.Sizes.SMALL} onClick={() => downloadAudio(item.soundId)}>Download</Button>
<Button color={Button.Colors.GREEN} size={Button.Sizes.SMALL} onClick={() => copyWithToast(item.soundId, "ID copied to clipboard!")}>Copy ID</Button>
<Tooltip text={`Soundboard volume: ${Math.floor(getSoundboardVolume())}%`}>
{({ onMouseEnter, onMouseLeave }) =>
<Button color={Button.Colors.BRAND} size={Button.Sizes.SMALL} onClick={() => playSound(item.soundId)} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>Play Sound</Button>
}
</Tooltip>
</Flex>
</div>
);
}) :
<div style={{ textAlign: "center" }} className={Margins.top16}>
<img
src="https://raw.githubusercontent.com/fres621/assets/main/shiggy.png"
height="200px"
/>
<Forms.FormText variant="text-sm/medium" style={{ color: "var(--text-muted)" }} className={Margins.bottom16}>No sounds logged yet. Join a voice chat to start logging!</Forms.FormText>
</div>
}
</ModalContent >
<ModalFooter className={cl("modal-footer")}>
<Button color={Button.Colors.RED} onClick={async () => { await clearLoggedSounds(); update(); }}>
Clear logs
</Button>
</ModalFooter>
</>
);
}

View file

@ -0,0 +1,104 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Flex } from "@components/Flex";
import { CopyIcon } from "@components/Icons";
import { openUserProfile } from "@utils/discord";
import { Margins } from "@utils/margins";
import { classes, copyWithToast } from "@utils/misc";
import { closeModal, ModalContent, ModalRoot, openModal } from "@utils/modal";
import { Clickable, Forms, Text, Timestamp } from "@webpack/common";
import moment from "moment";
import { AvatarStyles, cl, downloadAudio, getEmojiUrl, playSound, SoundLogEntry, User,UserSummaryItem } from "../utils";
import { DownloadIcon, IconWithTooltip, PlayIcon } from "./Icons";
export function openUserModal(item, user, sounds) {
const key = openModal(props =>
<ModalRoot {...props}>
<UserModal item={item} user={user} sounds={sounds} closeModal={() => closeModal(key)} />
</ModalRoot>
);
}
export default function UserModal({ item, user, sounds, closeModal }: { item: SoundLogEntry, user: User, sounds: SoundLogEntry[], closeModal: Function; }) {
const currentUser = item.users.find(({ id }) => id === user.id) ?? { id: "", plays: [0] };
const soundsDoneByCurrentUser = sounds.filter(sound => sound.users.some(itemUser => itemUser.id === user.id) && sound.soundId !== item.soundId);
return (
<ModalContent className={cl("user")}>
<Clickable onClick={() => {
closeModal();
openUserProfile(user.id);
}}>
<div className={cl("user-header")}>
<img
className={cl("user-avatar")}
src={user.getAvatarURL(void 0, 512, true)}
alt=""
style={{ cursor: "pointer" }}
/>
<Forms.FormTitle tag="h2" className={cl("user-name")} style={{ textTransform: "none", cursor: "pointer" }}>{user.username}</Forms.FormTitle>
</div>
</Clickable>
<Flex flexDirection="row" style={{ gap: "10px" }}>
<img
className={cl("user-sound-emoji")}
src={getEmojiUrl(item.emoji)}
alt=""
/>
<Flex flexDirection="column" style={{ gap: "7px", height: "68px", justifyContent: "space-between" }}>
<Text variant="text-md/bold" style={{ height: "20px" }}>{item.soundId}</Text>
<Text variant="text-md/normal">Played {currentUser.plays.length} {currentUser.plays.length === 1 ? "time" : "times"}.</Text>
<Text variant="text-md/normal">Last played: <Timestamp timestamp={moment(currentUser.plays.at(-1))} /></Text>
</Flex>
</Flex>
<Text variant="heading-lg/semibold" tag="h2" className={classes(Margins.top16, Margins.bottom8)}>
{soundsDoneByCurrentUser.length ? "Also played:" : " "}
</Text>
<Flex style={{ justifyContent: "space-between" }}>
<UserSummaryItem
users={soundsDoneByCurrentUser}
count={soundsDoneByCurrentUser.length}
guildId={undefined}
renderIcon={false}
max={10}
showDefaultAvatarsForNullUsers
showUserPopout
renderMoreUsers={() =>
<div className={AvatarStyles.emptyUser}>
<div className={AvatarStyles.moreUsers}>
...
</div>
</div>
}
className={cl("user-sounds")}
renderUser={({ soundId, emoji }) => (
<Clickable
className={AvatarStyles.clickableAvatar}
onClick={() => {
closeModal();
openUserModal(sounds.find(sound => sound.soundId === soundId), user, sounds);
}}
>
<img
className={AvatarStyles.avatar}
src={getEmojiUrl(emoji)}
alt={soundId}
title={soundId}
/>
</Clickable>
)}
/>
<div className={cl("user-buttons")}>
<IconWithTooltip text="Download" icon={<DownloadIcon />} onClick={() => downloadAudio(item.soundId)} />
<IconWithTooltip text="Copy ID" icon={<CopyIcon />} onClick={() => copyWithToast(item.soundId, "ID copied to clipboard!")} />
<IconWithTooltip text="Play Sound" icon={<PlayIcon />} onClick={() => playSound(item.soundId)} />
</div>
</Flex>
</ModalContent>
);
}

View file

@ -0,0 +1,67 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { addChatBarButton, ChatBarButton, removeChatBarButton } from "@api/ChatButtons";
import { disableStyle, enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { IconWithTooltip, LogIcon } from "./components/Icons";
import { openSoundBoardLog } from "./components/SoundBoardLog";
import settings from "./settings";
import { updateLoggedSounds } from "./store";
import styles from "./styles.css?managed";
import { getListeners } from "./utils";
const chatBarIcon: ChatBarButton = () => {
return (
<ChatBarButton tooltip="Open SoundBoard Log"
onClick={openSoundBoardLog}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" className={"soundboardlogger-icon"} viewBox="1.134 10.59 87.732 68.821"><path fill="currentColor" d="M84.075 10.597 5.932 10.59a4.799 4.799 0 0 0-4.798 4.79v59.226a4.801 4.801 0 0 0 4.798 4.798l78.144.007a4.79 4.79 0 0 0 4.79-4.794V15.391a4.796 4.796 0 0 0-4.791-4.794zm-11.704 8.766c0-.283.229-.509.51-.509h7.105c.279 0 .501.226.501.509v7.102c0 .28-.222.509-.501.509h-7.105a.511.511 0 0 1-.51-.509v-7.102zm-63.968 0c0-.283.229-.509.509-.509h29.399c.28 0 .502.226.502.509v7.102c0 .28-.222.509-.502.509H8.912a.51.51 0 0 1-.509-.509v-7.102zm7.863 45.454a.727.727 0 0 1-.727.727H14.28v6.466c0 1-.822 1.821-1.829 1.821-.516 0-.97-.199-1.301-.53a1.829 1.829 0 0 1-.531-1.291v-6.466H9.51a.726.726 0 0 1-.727-.727v-6.854c0-.4.327-.727.727-.727h1.11V37.583c0-1.017.822-1.839 1.832-1.839.505 0 .96.211 1.291.542a1.8 1.8 0 0 1 .538 1.297v19.653h1.259c.4 0 .727.326.727.727v6.854zm11.02-13.204a.73.73 0 0 1-.727.728h-1.11v19.651a1.835 1.835 0 0 1-1.832 1.839c-.506 0-.96-.21-1.291-.541a1.8 1.8 0 0 1-.538-1.298V52.341H20.53a.73.73 0 0 1-.727-.728v-6.855c0-.403.327-.727.727-.727h1.259v-6.466a1.83 1.83 0 0 1 1.829-1.821c.516 0 .97.2 1.301.531.331.331.531.792.531 1.29v6.466h1.11c.4 0 .727.324.727.727v6.855zm11.268 13.204a.727.727 0 0 1-.727.727h-1.259v6.466c0 1-.821 1.821-1.829 1.821-.516 0-.97-.199-1.301-.53a1.828 1.828 0 0 1-.53-1.291v-6.466h-1.11a.726.726 0 0 1-.727-.727v-6.854c0-.4.327-.727.727-.727h1.11V37.583c0-1.017.821-1.839 1.832-1.839.505 0 .959.211 1.291.542a1.8 1.8 0 0 1 .538 1.297v19.653h1.259c.4 0 .727.326.727.727v6.854zm10.94-13.204a.73.73 0 0 1-.728.728h-1.11v19.651a1.835 1.835 0 0 1-1.832 1.839 1.82 1.82 0 0 1-1.29-.541 1.802 1.802 0 0 1-.539-1.298V52.341h-1.259a.73.73 0 0 1-.727-.728v-6.855c0-.403.327-.727.727-.727h1.259v-6.466c0-.999.822-1.821 1.829-1.821.516 0 .97.2 1.301.531.331.331.53.792.53 1.29v6.466h1.11c.4 0 .728.324.728.727v6.855zm8.703-32.25c0-.283.229-.509.508-.509h7.106c.279 0 .501.226.501.509v7.102c0 .28-.222.509-.501.509h-7.106a.51.51 0 0 1-.508-.509v-7.102zm4.176 53.927a4.083 4.083 0 0 1-4.082-4.082c0-2.25 1.828-4.078 4.082-4.078s4.078 1.828 4.078 4.078a4.08 4.08 0 0 1-4.078 4.082zm0-14.709a4.082 4.082 0 1 1 0-8.163 4.078 4.078 0 0 1 4.078 4.081 4.08 4.08 0 0 1-4.078 4.082zm0-14.713a4.082 4.082 0 0 1-4.082-4.081 4.08 4.08 0 0 1 8.16 0 4.079 4.079 0 0 1-4.078 4.081zM76.438 73.29a4.082 4.082 0 0 1-4.081-4.082 4.081 4.081 0 0 1 4.081-4.078 4.078 4.078 0 0 1 4.078 4.078 4.078 4.078 0 0 1-4.078 4.082zm0-14.709a4.081 4.081 0 0 1 0-8.163 4.078 4.078 0 0 1 4.078 4.081 4.08 4.08 0 0 1-4.078 4.082zm0-14.713a4.082 4.082 0 0 1-4.081-4.081 4.08 4.08 0 0 1 8.159 0 4.078 4.078 0 0 1-4.078 4.081z" /></svg>
</ChatBarButton>
);
};
export default definePlugin({
name: "SoundBoardLogger",
authors: [
Devs.ImpishMoxxie,
Devs.fres,
Devs.echo,
Devs.thororen
],
settings,
patches: [
{
predicate: () => settings.store.IconLocation === "toolbar",
find: ".iconBadge}):null",
replacement: {
match: /className:(\i).toolbar,children:(\i)/,
replace: "className:$1.toolbar,children:$self.toolbarPatch($2)"
}
}
],
description: "Logs all soundboards that are played in a voice chat and allows you to download them",
start() {
enableStyle(styles);
FluxDispatcher.subscribe("VOICE_CHANNEL_EFFECT_SEND", async sound => {
if (!sound?.soundId) return;
await updateLoggedSounds(sound);
getListeners().forEach(cb => cb());
});
if (settings.store.IconLocation === "chat") addChatBarButton("soundboardlogger-button", chatBarIcon);
},
stop() {
disableStyle(styles);
if (settings.store.IconLocation === "chat") removeChatBarButton("soundboardlogger-button");
},
toolbarPatch: obj => {
if (!obj?.props?.children) return obj;
obj.props.children = [<IconWithTooltip text="Open SoundBoard Log" icon={<LogIcon className="chatBarLogIcon" />} onClick={openSoundBoardLog} />, ...obj.props.children];
return obj;
}
});

View file

@ -0,0 +1,93 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings } from "@api/Settings";
import { OptionType } from "@utils/types";
import { Button, Forms, TextInput, useState } from "@webpack/common";
import { openSoundBoardLog } from "./components/SoundBoardLog";
const settings = definePluginSettings({
SavedIds: {
description: "The amount of soundboard ids you want to save at a time (0 lets you save infinite)",
type: OptionType.COMPONENT,
component: ({ setValue, setError }) => {
const value = settings.store.SavedIds ?? 50;
const [state, setState] = useState(`${value}`);
const [shouldShowWarning, setShouldShowWarning] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
function handleChange(newValue) {
const changed = Number(newValue);
if (Number.isNaN(changed) || changed % 1 !== 0 || changed < 0) {
setError(true);
let errorMsg = "";
errorMsg += Number.isNaN(changed) ? "The value is not a number.\n" : "";
errorMsg += (changed % 1 !== 0) ? "The value can't be a decimal number.\n" : "";
errorMsg += (changed < 0) ? "The value can't be a negative number.\n" : "";
setErrorMessage(errorMsg);
return;
} else {
setError(false);
setErrorMessage(null);
}
if (changed < value) {
setShouldShowWarning(true);
} else {
setShouldShowWarning(false);
}
setState(newValue);
setValue(changed);
}
return (
<Forms.FormSection>
<Forms.FormTitle>The amount of soundboard ids you want to save at a time (0 lets you save infinite)</Forms.FormTitle>
<TextInput
type="number"
pattern="-?[0-9]+"
value={state}
onChange={handleChange}
placeholder={"Enter a number"}
/>
{shouldShowWarning && <Forms.FormText style={{ color: "var(--text-danger)" }}>Warning! Setting the number to a lower value will reset the log!</Forms.FormText>}
{errorMessage && <Forms.FormText style={{ color: "var(--text-danger)" }}>{errorMessage}</Forms.FormText>}
</Forms.FormSection>
);
}
},
FileType: {
description: "the format that you want to save your file",
type: OptionType.SELECT,
options: [
{ label: ".ogg", value: ".ogg", default: true },
{ label: ".mp3", value: ".mp3" },
{ label: ".wav", value: ".wav" },
],
},
IconLocation: {
description: "choose where to show the SoundBoard Log icon (requires restart)",
type: OptionType.SELECT,
options: [
{ label: "Toolbar", value: "toolbar", default: true },
{ label: "Chat input", value: "chat" }
],
restartNeeded: true
},
OpenLogs: {
type: OptionType.COMPONENT,
description: "show the logs",
component: () =>
<Button color={Button.Colors.LINK} size={Button.Sizes.SMALL} onClick={openSoundBoardLog}>Open Logs</Button>
}
});
export default settings;

View file

@ -0,0 +1,62 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { DataStore } from "@api/index";
import settings from "./settings";
import { SoundEvent, SoundLogEntry } from "./utils";
/** Attempts to add a sound event to the log */
export async function updateLoggedSounds(sound: SoundEvent): Promise<void> {
const data = await getLoggedSounds();
if (!data) {
await DataStore.set("SoundBoardLogList", [{ ...sound, users: [{ id: sound.userId, plays: [+Date.now()] }] }]);
} else {
if (data.some(item => item.soundId === sound.soundId)) {
const newSounds = data.map(item => {
if (item.soundId !== sound.soundId) return item;
return {
...item,
users: item.users.some(user => user.id === sound.userId) ?
item.users.map(user => {
if (user.id !== sound.userId) return user;
return { id: sound.userId, plays: [...user.plays, +Date.now()] };
}) :
[
...item.users,
{ id: sound.userId, plays: [+Date.now()] }
]
};
});
await DataStore.set("SoundBoardLogList", newSounds);
return;
}
let limit = settings.store.SavedIds ?? 50;
if (limit === 0) limit = Infinity;
const modified = [{ ...sound, users: [{ id: sound.userId, plays: [+Date.now()] }] }, ...data].slice(0, limit);
await DataStore.set("SoundBoardLogList", modified);
}
}
/** Clears the logged sounds array */
export async function clearLoggedSounds() {
await DataStore.set("SoundBoardLogList", []);
}
/** Returns an array with the logged sounds */
export async function getLoggedSounds(): Promise<SoundLogEntry[]> {
const data = await DataStore.get("SoundBoardLogList");
if (!data) {
DataStore.set("SoundBoardLogList", []);
return [];
}
else {
return data;
}
}

View file

@ -0,0 +1,125 @@
.vc-soundlog-more-user,
.vc-soundlog-sound {
box-sizing: border-box;
position: relative;
background: var(--background-secondary);
border-radius: var(--radius-sm);
height: 40px;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-weight: 600;
border: 1px solid transparent;
overflow: hidden;
text-align: center;
margin-top: 10px;
padding-left: 10px;
padding-right: 10px
}
.vc-soundlog-more-user-profile,
.vc-soundlog-sound-info {
gap: 10px;
align-content: center;
align-items: center;
flex: 2;
}
.vc-soundlog-sound-buttons {
gap: 10px;
align-content: center;
flex: 1;
}
.vc-soundlog-sound-users {
flex: 2;
}
.vc-soundlog-sound-emoji {
width: 24px;
height: 24px;
}
.vc-soundlog-more,
.vc-soundlog-user {
padding: 1em;
}
.vc-soundlog-more-header,
.vc-soundlog-user-header {
display: flex;
align-items: center;
margin-bottom: 1em;
width: fit-content;
}
.vc-soundlog-more-soundid,
.vc-soundlog-user-name {
text-transform: none;
flex-grow: 0;
background: var(--background-tertiary);
border-radius: 0 9999px 9999px 0;
padding: 6px 0.8em 6px 0.5em;
font-size: 20px;
height: 20px;
position: relative;
text-wrap: nowrap;
}
.vc-soundlog-more-soundid {
background: unset;
}
.vc-soundlog-user-name::before {
content: "";
display: block;
position: absolute;
height: 100%;
width: 16px;
background: var(--background-tertiary);
z-index: -1;
left: -16px;
top: 0;
}
.vc-soundlog-more-emoji,
.vc-soundlog-user-avatar {
height: 32px;
width: 32px;
border-radius: 50%;
}
.vc-soundlog-more-emoji {
border-radius: 0%;
}
.vc-soundlog-user-buttons {
margin-left: auto;
display: flex;
gap: 0.2em;
}
.vc-soundlog-user-sound-emoji {
height: 48px;
margin-top: 20px;
}
/* input style from https://github.com/Vendicated/Vencord/blob/ea11f2244fde469ce308f8a4e7224430be62f8f1/src/plugins/sendTimestamps/styles.css#L1-L13 */
.vc-soundlog-clone-input {
background-color: var(--input-background);
color: var(--text-normal);
width: 100%;
padding: 8px 8px 8px 12px;
outline: none;
border: 1px solid var(--input-background);
border-radius: 4px;
font-weight: 500;
font-style: inherit;
font-size: 100%;
box-sizing: border-box;
height: 40px;
}

View file

@ -0,0 +1,81 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2023 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { classNameFactory } from "@api/Styles";
import { proxyLazy } from "@utils/lazy";
import { LazyComponent } from "@utils/react";
import { saveFile } from "@utils/web";
import { findByCode, findByProps, findByPropsLazy } from "@webpack";
import type { User } from "discord-types/general";
import settings from "./settings";
export { User };
export interface SoundEvent {
type: "VOICE_CHANNEL_EFFECT_SEND",
emoji: { name: string, id?: string, animated: boolean; },
channelId: string,
userId: string,
animationType: number,
animationId: number,
soundId: string,
soundVolume: number;
}
export interface SoundLogEntry extends SoundEvent {
users: { id: string, plays: number[]; }[];
}
export const cl = classNameFactory("vc-soundlog-");
export function getEmojiUrl(emoji) {
const { getURL } = proxyLazy(() => findByProps("getEmojiColors", "getURL"));
if (!emoji) return getURL("❓"); // If the sound doesn't have a related emoji
return emoji.id ? `https://cdn.discordapp.com/emojis/${emoji.id}.png?size=32` : getURL(emoji.name);
}
const v1 = findByPropsLazy("amplitudeToPerceptual");
const v2 = findByPropsLazy("getAmplitudinalSoundboardVolume");
export const getSoundboardVolume = () => v1.amplitudeToPerceptual(v2.getAmplitudinalSoundboardVolume());
export const playSound = id => {
const audio = new Audio(`https://cdn.discordapp.com/soundboard-sounds/${id}`);
audio.volume = getSoundboardVolume() / 100;
audio.play();
};
export async function downloadAudio(id: string): Promise<void> {
const filename = id + settings.store.FileType;
const data = await fetch(`https://cdn.discordapp.com/soundboard-sounds/${id}`).then(e => e.arrayBuffer());
if (IS_DISCORD_DESKTOP) {
DiscordNative.fileManager.saveWithDialog(data, filename);
} else {
saveFile(new File([data], filename, { type: "audio/ogg" }));
}
}
let listeners: Function[] = [];
export function getListeners(): Function[] {
return listeners;
}
export function addListener(fn): void {
listeners.push(fn);
}
export function removeListener(fn): void {
listeners = listeners.filter(f => f !== fn);
}
// Taken from https://github.com/Vendicated/Vencord/blob/86e94343cca10b950f2dc8d18d496d6db9f3b728/src/components/PluginSettings/PluginModal.tsx#L45
export const UserSummaryItem = LazyComponent(() => findByCode("defaultRenderUser", "showDefaultAvatarsForNullUsers"));
export const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar");