Equicord/src/equicordplugins/customSounds/SoundOverrideComponent.tsx
2025-12-19 21:32:50 -05:00

286 lines
11 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { AudioPlayerInterface, playAudio } from "@api/AudioPlayer";
import { Card } from "@components/Card";
import { FormSwitch } from "@components/FormSwitch";
import { Heading } from "@components/Heading";
import { classNameFactory } from "@utils/css";
import { Margins } from "@utils/margins";
import { useForceUpdater } from "@utils/react";
import { makeRange } from "@utils/types";
import { findLazy } from "@webpack";
import { Button, React, Select, showToast, Slider } from "@webpack/common";
import { ComponentType, Ref, SyntheticEvent } from "react";
import { deleteAudio, getAllAudio, saveAudio, StoredAudioFile } from "./audioStore";
import { ensureDataURICached } from "./index";
import { SoundOverride, SoundType } from "./types";
type FileInput = ComponentType<{
ref: Ref<HTMLInputElement>;
onChange: (e: SyntheticEvent<HTMLInputElement>) => void;
multiple?: boolean;
filters?: { name?: string; extensions: string[]; }[];
}>;
const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "webm", "wma", "mp4"];
const cl = classNameFactory("vc-custom-sounds-");
const FileInput: FileInput = findLazy(m => m.prototype?.activateUploadDialogue && m.prototype.setRef);
const capitalizeWords = (str: string) =>
str.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
export function SoundOverrideComponent({ type, override, onChange }: {
type: SoundType;
override: SoundOverride;
onChange: () => Promise<void>;
}) {
const fileInputRef = React.useRef<HTMLInputElement>(null);
const update = useForceUpdater();
const sound = React.useRef<AudioPlayerInterface | null>(null);
const [files, setFiles] = React.useState<Record<string, StoredAudioFile>>({});
React.useEffect(() => {
getAllAudio().then(setFiles);
}, []);
const saveAndNotify = async () => {
await onChange();
update();
};
const previewSound = async () => {
sound.current?.stop();
if (!override.enabled) {
sound.current = playAudio(type.id);
return;
}
const { selectedSound } = override;
if (selectedSound === "custom" && override.selectedFileId) {
try {
const dataUri = await ensureDataURICached(override.selectedFileId);
if (!dataUri || !dataUri.startsWith("data:audio/")) {
showToast("No custom sound file available for preview");
return;
}
sound.current = playAudio(dataUri, {
volume: override.volume, onError: e => {
console.error("[CustomSounds] Error playing custom audio:", e);
showToast("Error playing custom sound. File may be corrupted.");
}
});
} catch (error) {
console.error("[CustomSounds] Error in previewSound:", error);
showToast("Error playing sound.");
}
} else if (selectedSound === "default") {
sound.current = playAudio(type.id);
} else {
sound.current = playAudio(selectedSound);
}
};
const uploadFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const fileExtension = file.name.split(".").pop()?.toLowerCase();
if (!fileExtension || !AUDIO_EXTENSIONS.includes(fileExtension)) {
showToast("Invalid file type. Please upload an audio file.");
event.target.value = "";
return;
}
try {
showToast("Uploading file...");
const id = await saveAudio(file);
const savedFiles = await getAllAudio();
setFiles(savedFiles);
override.selectedFileId = id;
override.selectedSound = "custom";
await ensureDataURICached(id);
await saveAndNotify();
showToast(`File uploaded successfully: ${file.name}`);
} catch (error) {
console.error("[CustomSounds] Error uploading file:", error);
showToast(`Error uploading file: ${error}`);
}
event.target.value = "";
};
const deleteFile = async (id: string) => {
try {
await deleteAudio(id);
const updated = await getAllAudio();
setFiles(updated);
if (override.selectedFileId === id) {
override.selectedFileId = undefined;
override.selectedSound = "default";
await saveAndNotify();
} else {
update();
}
showToast("File deleted successfully");
} catch (error) {
console.error("[CustomSounds] Error deleting file:", error);
showToast("Error deleting file.");
}
};
const customFileOptions = Object.entries(files)
.filter(([id, file]) => !!id && !!file?.name)
.map(([id, file]) => ({
value: id,
label: file.name
}));
return (
<Card className={cl("card")}>
<FormSwitch
title={type.name}
value={override.enabled || false}
onChange={async val => {
console.log(`[CustomSounds] Setting ${type.id} enabled to:`, val);
override.enabled = val;
if (val && override.selectedSound === "custom" && override.selectedFileId) {
try {
await ensureDataURICached(override.selectedFileId);
} catch (error) {
console.error(`[CustomSounds] Failed to cache data URI for ${type.id}:`, error);
showToast("Error loading custom sound file");
}
}
await saveAndNotify();
console.log("[CustomSounds] After setting enabled, override.enabled =", override.enabled);
}}
className={Margins.bottom16}
hideBorder
/>
{override.enabled && (
<>
<div style={{ display: "flex", gap: "8px", marginBottom: "16px" }}>
<Button
color={Button.Colors.GREEN}
onClick={previewSound}
>
Preview
</Button>
<Button
color={Button.Colors.RED}
onClick={() => sound.current?.stop()}
>
Stop
</Button>
</div>
<Heading>Volume</Heading>
<Slider
markers={makeRange(0, 100, 10)}
initialValue={override.volume}
onValueChange={val => {
sound.current && (sound.current.volume = val);
override.volume = val;
saveAndNotify();
}}
className={Margins.bottom16}
disabled={!override.enabled}
/>
<Heading>Sound Source</Heading>
<Select
options={[
{ value: "default", label: "Default" },
...(type.seasonal?.map(id => ({ value: id, label: capitalizeWords(id) })) ?? []),
{ value: "custom", label: "Custom" }
]}
isSelected={v => v === override.selectedSound}
select={async v => {
override.selectedSound = v;
if (v === "custom" && override.selectedFileId) {
try {
await ensureDataURICached(override.selectedFileId);
} catch (error) {
console.error(`[CustomSounds] Failed to cache data URI for ${type.id}:`, error);
showToast("Error loading custom sound file");
}
}
await saveAndNotify();
}}
serialize={opt => opt.value}
className={Margins.bottom16}
/>
{override.selectedSound === "custom" && (
<>
<Heading>Custom File</Heading>
<Select
options={[
{ value: "", label: "Select a file..." },
...customFileOptions
]}
isSelected={v => v === (override.selectedFileId || "")}
select={async id => {
if (!id) {
override.selectedFileId = undefined;
} else {
override.selectedFileId = id;
await ensureDataURICached(id);
}
await saveAndNotify();
}}
serialize={opt => opt.value}
className={Margins.bottom8}
/>
<input
ref={fileInputRef}
type="file"
accept=".mp3,.wav,.ogg,.m4a,.flac,.aac,.webm,.wma,.mp4"
style={{ display: "none" }}
onChange={uploadFile}
/>
<div style={{ display: "flex", gap: "8px", marginBottom: "16px" }}>
<Button
onClick={() => fileInputRef.current?.click()}
color={Button.Colors.BRAND}
>
Upload New
</Button>
{override.selectedFileId && files[override.selectedFileId] && (
<Button
color={Button.Colors.RED}
onClick={() => deleteFile(override.selectedFileId!)}
>
Delete Selected File
</Button>
)}
</div>
</>
)}
</>
)}
</Card>
);
}