/* * 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(); 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 { 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 { 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(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) => { const file = event.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = async (e: ProgressEvent) => { 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 (
Search Sounds setSearchQuery(e)} placeholder="Search by name or ID" />
{filteredSoundTypes.map(type => { const currentOverride = getOverride(type.id); return ( { 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); }} /> ); })}
); } } }); 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"); } });