mirror of
https://github.com/Equicord/Equicord.git
synced 2025-06-26 06:38:24 -04:00
forked!!
This commit is contained in:
parent
538b87062a
commit
ea7451bcdc
326 changed files with 24876 additions and 2280 deletions
|
@ -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>
|
||||
</>;
|
||||
}
|
57
src/equicordplugins/soundBoardLogger/components/Icons.tsx
Normal file
57
src/equicordplugins/soundBoardLogger/components/Icons.tsx
Normal 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>;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
104
src/equicordplugins/soundBoardLogger/components/UserModal.tsx
Normal file
104
src/equicordplugins/soundBoardLogger/components/UserModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
67
src/equicordplugins/soundBoardLogger/index.tsx
Normal file
67
src/equicordplugins/soundBoardLogger/index.tsx
Normal 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;
|
||||
}
|
||||
});
|
93
src/equicordplugins/soundBoardLogger/settings.tsx
Normal file
93
src/equicordplugins/soundBoardLogger/settings.tsx
Normal 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;
|
62
src/equicordplugins/soundBoardLogger/store.tsx
Normal file
62
src/equicordplugins/soundBoardLogger/store.tsx
Normal 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;
|
||||
}
|
||||
}
|
125
src/equicordplugins/soundBoardLogger/styles.css
Normal file
125
src/equicordplugins/soundBoardLogger/styles.css
Normal 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;
|
||||
}
|
81
src/equicordplugins/soundBoardLogger/utils.ts
Normal file
81
src/equicordplugins/soundBoardLogger/utils.ts
Normal 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");
|
Loading…
Add table
Add a link
Reference in a new issue