Equicord/src/equicordplugins/customSounds/index.tsx
2025-10-10 21:11:31 -04:00

419 lines
16 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import "./styles.css";
import { addAudioProcessor, AudioProcessor, PreprocessAudioData, removeAudioProcessor } from "@api/AudioPlayer";
import { get as getFromDataStore } from "@api/DataStore";
import { definePluginSettings } from "@api/Settings";
import { classNameFactory } from "@api/Styles";
import { Heading } from "@components/Heading";
import { Devs } from "@utils/constants";
import definePlugin, { OptionType, StartAt } from "@utils/types";
import { Button, React, showToast, TextInput } from "@webpack/common";
import { getAllAudio, getAudioDataURI } from "./audioStore";
import { SoundOverrideComponent } from "./SoundOverrideComponent";
import { makeEmptyOverride, seasonalSounds, SoundOverride, soundTypes } from "./types";
const cl = classNameFactory("vc-custom-sounds-");
const allSoundTypes = soundTypes || [];
const AUDIO_STORE_KEY = "ScattrdCustomSounds";
const dataUriCache = new Map<string, string>();
function getOverride(id: string): SoundOverride {
const stored = settings.store[id];
if (!stored) return makeEmptyOverride();
if (typeof stored === "object") return stored;
try {
return JSON.parse(stored);
} catch {
return makeEmptyOverride();
}
}
function setOverride(id: string, override: SoundOverride) {
settings.store[id] = JSON.stringify(override);
}
export const getCustomSoundURL: AudioProcessor = (data: PreprocessAudioData) => {
let audioOverride = data.audio;
if (data.audio in seasonalSounds) {
audioOverride = soundTypes.find(sound => sound.seasonal?.includes(data.audio))?.id || data.audio;
}
const override = getOverride(audioOverride);
if (!override?.enabled) {
return;
}
if (override.selectedSound === "custom" && override.selectedFileId) {
const dataUri = dataUriCache.get(override.selectedFileId);
if (dataUri) {
data.audio = dataUri;
data.volume = override.volume;
return;
} else {
return;
}
}
if (override.selectedSound !== "default" && override.selectedSound !== "custom") {
if (override.selectedSound in seasonalSounds) {
data.audio = seasonalSounds[override.selectedSound];
data.volume = override.volume;
return;
}
const soundType = allSoundTypes.find(t => t.id === data.audio);
if (soundType?.seasonal) {
const seasonalId = soundType.seasonal.find(seasonalId =>
seasonalId.startsWith(`${override.selectedSound}_`)
);
if (seasonalId && seasonalId in seasonalSounds) {
data.audio = seasonalSounds[seasonalId];
data.volume = override.volume;
return;
}
}
}
data.volume = override.volume;
return;
};
export async function ensureDataURICached(fileId: string): Promise<string | null> {
if (dataUriCache.has(fileId)) {
return dataUriCache.get(fileId)!;
}
try {
const dataUri = await getAudioDataURI(fileId);
if (dataUri) {
dataUriCache.set(fileId, dataUri);
console.log(`[CustomSounds] Cached data URI for file ${fileId}`);
return dataUri;
}
} catch (error) {
console.error(`[CustomSounds] Error generating data URI for ${fileId}:`, error);
}
return null;
}
export async function refreshDataURI(id: string): Promise<void> {
const override = getOverride(id);
if (!override?.selectedFileId) {
console.log(`[CustomSounds] refreshDataURI called for ${id} but no selectedFileId`);
return;
}
console.log(`[CustomSounds] Refreshing data URI for ${id} with file ID ${override.selectedFileId}`);
const dataUri = await ensureDataURICached(override.selectedFileId);
if (dataUri) {
console.log(`[CustomSounds] Successfully cached data URI for ${id} (length: ${dataUri.length})`);
} else {
console.error(`[CustomSounds] Failed to cache data URI for ${id}`);
}
}
async function preloadDataURIs() {
console.log("[CustomSounds] Preloading data URIs into memory cache...");
for (const soundType of allSoundTypes) {
const override = getOverride(soundType.id);
if (override?.enabled && override.selectedSound === "custom" && override.selectedFileId) {
try {
await ensureDataURICached(override.selectedFileId);
console.log(`[CustomSounds] Preloaded data URI for ${soundType.id}`);
} catch (error) {
console.error(`[CustomSounds] Failed to preload data URI for ${soundType.id}:`, error);
}
}
}
console.log(`[CustomSounds] Memory cache contains ${dataUriCache.size} data URIs`);
}
export async function debugCustomSounds() {
console.log("[CustomSounds] === DEBUG INFO ===");
const rawDataStore = await getFromDataStore(AUDIO_STORE_KEY);
console.log("[CustomSounds] Raw DataStore content:", rawDataStore);
const allFiles = await getAllAudio();
console.log(`[CustomSounds] Stored files: ${Object.keys(allFiles).length}`);
let totalBufferSize = 0;
let totalDataUriSize = 0;
for (const [id, file] of Object.entries(allFiles)) {
const bufferSize = file.buffer?.byteLength || 0;
const dataUriSize = file.dataUri?.length || 0;
totalBufferSize += bufferSize;
totalDataUriSize += dataUriSize;
console.log(`[CustomSounds] File ${id}:`, {
name: file.name,
type: file.type,
bufferSize: `${(bufferSize / 1024).toFixed(1)}KB`,
hasValidBuffer: file.buffer instanceof ArrayBuffer,
hasDataUri: !!file.dataUri,
dataUriSize: `${(dataUriSize / 1024).toFixed(1)}KB`
});
}
console.log(`[CustomSounds] Total storage - Buffers: ${(totalBufferSize / 1024).toFixed(1)}KB, DataURIs: ${(totalDataUriSize / 1024).toFixed(1)}KB`);
console.log(`[CustomSounds] Memory cache contains ${dataUriCache.size} data URIs`);
console.log("[CustomSounds] Settings store structure:", Object.keys(settings.store));
console.log("[CustomSounds] Sound override status:");
let enabledCount = 0;
let totalSettingsSize = 0;
for (const [soundId, storedValue] of Object.entries(settings.store)) {
if (soundId === "overrides") continue;
const override = getOverride(soundId);
const settingsSize = JSON.stringify(override).length;
totalSettingsSize += settingsSize;
console.log(`[CustomSounds] ${soundId}:`, {
enabled: override.enabled,
selectedSound: override.selectedSound,
selectedFileId: override.selectedFileId,
volume: override.volume,
settingsSize: `${settingsSize}B`
});
if (override.enabled) enabledCount++;
}
console.log(`[CustomSounds] Total enabled overrides: ${enabledCount}`);
console.log(`[CustomSounds] Estimated settings size: ${(totalSettingsSize / 1024).toFixed(1)}KB`);
console.log("[CustomSounds] === END DEBUG ===");
}
const soundSettings = Object.fromEntries(
allSoundTypes.map(type => [
type.id,
{
type: OptionType.STRING,
description: `Override for ${type.name}`,
default: JSON.stringify(makeEmptyOverride()),
hidden: true
}
])
);
const settings = definePluginSettings({
...soundSettings,
overrides: {
type: OptionType.COMPONENT,
description: "",
component: () => {
const [resetTrigger, setResetTrigger] = React.useState(0);
const [searchQuery, setSearchQuery] = React.useState("");
const fileInputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => {
allSoundTypes.forEach(type => {
if (!settings.store[type.id]) {
setOverride(type.id, makeEmptyOverride());
}
});
}, []);
const resetOverrides = () => {
allSoundTypes.forEach(type => {
setOverride(type.id, makeEmptyOverride());
});
dataUriCache.clear();
setResetTrigger(prev => prev + 1);
showToast("All overrides reset successfully!");
};
const triggerFileUpload = () => {
fileInputRef.current?.click();
};
const handleSettingsUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e: ProgressEvent<FileReader>) => {
try {
resetOverrides();
const imported = JSON.parse(e.target?.result as string);
if (imported.overrides && Array.isArray(imported.overrides)) {
imported.overrides.forEach((setting: any) => {
if (setting.id) {
const override: SoundOverride = {
enabled: setting.enabled ?? false,
selectedSound: setting.selectedSound ?? "default",
selectedFileId: setting.selectedFileId ?? undefined,
volume: setting.volume ?? 100,
useFile: false
};
setOverride(setting.id, override);
}
});
}
setResetTrigger(prev => prev + 1);
showToast("Settings imported successfully!");
} catch (error) {
console.error("Error importing settings:", error);
showToast("Error importing settings. Check console for details.");
}
};
reader.readAsText(file);
event.target.value = "";
}
};
const downloadSettings = async () => {
const overrides = allSoundTypes.map(type => {
const override = getOverride(type.id);
return {
id: type.id,
enabled: override.enabled,
selectedSound: override.selectedSound,
selectedFileId: override.selectedFileId ?? undefined,
volume: override.volume
};
}).filter(o => o.enabled || o.selectedSound !== "default");
const exportPayload = {
overrides,
__note: "Audio files are not included in exports and will need to be re-uploaded after import"
};
const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "customSounds-settings.json";
a.click();
URL.revokeObjectURL(url);
showToast(`Exported ${overrides.length} settings (audio files not included)`);
};
const filteredSoundTypes = allSoundTypes.filter(type =>
type.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
type.id.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div>
<div className="vc-custom-sounds-buttons">
<Button color={Button.Colors.BRAND} onClick={triggerFileUpload}>Import</Button>
<Button color={Button.Colors.PRIMARY} onClick={downloadSettings}>Export</Button>
<Button color={Button.Colors.RED} onClick={resetOverrides}>Reset All</Button>
<Button color={Button.Colors.WHITE} onClick={debugCustomSounds}>Debug</Button>
<input
ref={fileInputRef}
type="file"
accept=".json"
style={{ display: "none" }}
onChange={handleSettingsUpload}
/>
</div>
<div className={cl("search")}>
<Heading>Search Sounds</Heading>
<TextInput
value={searchQuery}
onChange={e => setSearchQuery(e)}
placeholder="Search by name or ID"
/>
</div>
<div className={cl("sounds-list")}>
{filteredSoundTypes.map(type => {
const currentOverride = getOverride(type.id);
return (
<SoundOverrideComponent
key={`${type.id}-${resetTrigger}`}
type={type}
override={currentOverride}
onChange={async () => {
setOverride(type.id, currentOverride);
if (currentOverride.enabled && currentOverride.selectedSound === "custom" && currentOverride.selectedFileId) {
try {
await ensureDataURICached(currentOverride.selectedFileId);
} catch (error) {
console.error(`[CustomSounds] Failed to cache data URI for ${type.id}:`, error);
showToast("Error loading custom sound file");
}
}
console.log(`[CustomSounds] Settings saved for ${type.id}:`, currentOverride);
}}
/>
);
})}
</div>
</div>
);
}
}
});
export function isOverriden(id: string): boolean {
return !!getOverride(id)?.enabled;
}
export function findOverride(id: string): SoundOverride | null {
const override = getOverride(id);
return override?.enabled ? override : null;
}
export default definePlugin({
name: "CustomSounds",
description: "Customize Discord's sounds.",
authors: [Devs.ScattrdBlade, Devs.TheKodeToad],
dependencies: ["AudioPlayerAPI"],
settings,
getCustomSoundURL,
startAt: StartAt.Init,
async start() {
console.log("[CustomSounds] Plugin starting...");
addAudioProcessor("CustomSounds", this.getCustomSoundURL);
try {
await preloadDataURIs();
console.log("[CustomSounds] Startup complete");
} catch (error) {
console.error("[CustomSounds] Startup failed:", error);
}
},
stop() {
removeAudioProcessor("CustomSounds");
console.log("[CustomSounds] Plugin stopped");
}
});