/* * Vencord, a Discord client mod * Copyright (c) 2026 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import { type AudioPlayerInterface, createAudioPlayer, defaultAudioNames } from "@api/AudioPlayer"; import { useEffect, useMemo, useRef, useState } from "@webpack/common"; import type { JSX, MouseEvent } from "react"; import { startAutoFetchingQuests } from "../settings/fetching"; import { settings } from "../settings/store"; import { q } from "../utils/ui"; import { ManaSelectFormattedOption, ManaSelectOption, SettingsCard, SettingsDescription, SettingsHeader, SettingsRow, SettingsRowItem, SettingsSelect, SettingsSlider, SettingsSubheader, SettingsSubtleSwitch } from "./shared"; const questFetchIntervalOptions = [ { id: "30-minutes", label: "30 Minutes", value: String(30 * 60) }, { id: "45-minutes", label: "45 Minutes", value: String(45 * 60) }, { id: "1-hour", label: "1 Hour", value: String(60 * 60) }, { id: "3-hours", label: "3 Hours", value: String(3 * 60 * 60) }, { id: "6-hours", label: "6 Hours", value: String(6 * 60 * 60) }, { id: "12-hours", label: "12 Hours", value: String(12 * 60 * 60) }, ] satisfies ManaSelectOption[]; function SoundIcon({ className }: { className?: string; }): JSX.Element { return ( ); } function getSoundOptions(): ManaSelectOption[] { return defaultAudioNames() .map(sound => ({ id: sound, label: formatSoundName(sound), value: sound, })) .sort((a, b) => a.label.localeCompare(b.label)); } function formatSoundName(sound: string): string { return sound .toUpperCase() .replace(/_/g, " ") .replace(/(\d+)/g, " $1"); } interface QuestNotificationSoundSelectProps { disabled?: boolean; label: string; onChange: (value: string | null) => void; onPreview: (sound: string) => void; options: ManaSelectOption[]; playingSound: string | null; tooltip?: { position: "top" | "bottom", text: string; }; value: string | null; } function QuestNotificationSoundSelect({ disabled, label, onChange, onPreview, options, playingSound, tooltip, value, }: QuestNotificationSoundSelectProps): JSX.Element { function formatOption(option: ManaSelectOption): ManaSelectFormattedOption { const sound = option.value || null; const isPlaying = sound != null && playingSound === sound; function handlePreviewMouseDown(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); } function handlePreviewClick(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); if (sound && !disabled) { onPreview(sound); } } return { ...option, trailing: sound ? ( ) : undefined, }; } return ( { if (nextValue != null && typeof nextValue !== "string") return; onChange(nextValue ?? null); }} /> ); } export function QuestNotificationsSetting(): JSX.Element { const questNotifications = settings.use([ "newExcludedQuestAlertSound", "newExcludedQuestAlertVolume", "newQuestAlertSound", "newQuestAlertVolume", "questFetchInterval", "disableQuestsEverything", "notifyOnNewExcludedQuests", "notifyOnNewQuests", "notifyOnQuestComplete", "questCompletedAlertSound", "questCompletedAlertVolume", ]); const soundOptions = useMemo(getSoundOptions, []); const activePlayer = useRef(null); const [playingSound, setPlayingSound] = useState(null); const disabled = questNotifications.disableQuestsEverything; function clearActivePlayer(): void { const player = activePlayer.current; activePlayer.current = null; player?.stop(); setPlayingSound(null); } function previewSound(sound: string, volume: number): void { if (playingSound === sound) { clearActivePlayer(); return; } clearActivePlayer(); const player = createAudioPlayer(sound, { volume: Math.max(0, Math.min(100, volume)), onEnded: clearActivePlayer }); activePlayer.current = player; setPlayingSound(sound); player?.play(); } useEffect(() => clearActivePlayer, []); useEffect(() => { if (disabled) { clearActivePlayer(); } }, [disabled]); function updateFetchInterval(value: string | string[] | null): void { if (value != null && typeof value !== "string") return; const interval = value == null ? 0 : Number(value); settings.store.questFetchInterval = interval; startAutoFetchingQuests(true); } return ( Quest Notifications Configure Quest completed and new Quest detected notifications and alerts. Quest Completed { settings.store.notifyOnQuestComplete = checked; }} bottomSpacing="5" /> previewSound(sound, questNotifications.questCompletedAlertVolume)} onChange={value => { settings.store.questCompletedAlertSound = value; }} /> { settings.store.questCompletedAlertVolume = value; }} /> New Quests Detected { settings.store.notifyOnNewQuests = checked; startAutoFetchingQuests(true); }} /> previewSound(sound, questNotifications.newQuestAlertVolume)} onChange={value => { settings.store.newQuestAlertSound = value; startAutoFetchingQuests(true); }} /> { settings.store.newQuestAlertVolume = value; }} /> { settings.store.notifyOnNewExcludedQuests = checked; startAutoFetchingQuests(true); }} tooltip={{ position: "top", text: "Some Quests are excluded from your available Quests list due to region or platform restrictions." + "\n\nWhen enabled, Questify will fetch their Quest configs, apply your included Quest and reward type filters, print the resolved excluded Quest data to console, and show a separate notification for matching excluded Quests." }} /> previewSound(sound, questNotifications.newExcludedQuestAlertVolume)} onChange={value => { settings.store.newExcludedQuestAlertSound = value; startAutoFetchingQuests(true); }} tooltip={{ position: "top", text: "Some Quests are excluded from your available Quests list due to region or platform restrictions." + "\n\nWhen a sound is selected, Questify will fetch their Quest configs, apply your included Quest and reward type filters, print the resolved excluded Quest data to console, and play this sound for matching excluded Quests." }} /> { settings.store.newExcludedQuestAlertVolume = value; }} /> 0 ? String(questNotifications.questFetchInterval) : null} selectionMode="single" disabled={disabled} clearable={true} fullWidth={true} placeholder="DISABLED" maxOptionsVisible={questFetchIntervalOptions.length} onSelectionChange={updateFetchInterval} tooltip={{ position: "top", text: "Discord only fetches Quests on load and when you visit the Quests page." + "\n\nThis interval periodically fetches Quests for you while the client stays open, so Quest Button indicators and new Quest alerts can stay up to date throughout the day." + "\n\nThis only runs if enabled and if the Quest Button or Quest Notifications settings are configured in a way which makes fetching periodically meaningful." }} /> ); }