181 lines
5.5 KiB
TypeScript
181 lines
5.5 KiB
TypeScript
import { createContext, useContext, useState, useEffect, useRef, useCallback, ReactNode } from 'react';
|
|
|
|
type SoundType = 'click' | 'beep' | 'hover' | 'boot' | 'success' | 'error';
|
|
|
|
interface SettingsContextType {
|
|
crtEnabled: boolean;
|
|
setCrtEnabled: (enabled: boolean) => void;
|
|
soundEnabled: boolean;
|
|
setSoundEnabled: (enabled: boolean) => void;
|
|
cryptoConsent: boolean;
|
|
setCryptoConsent: (consent: boolean) => void;
|
|
playSound: (type: SoundType) => void;
|
|
hashrate: number;
|
|
setHashrate: (rate: number) => void;
|
|
totalHashes: number;
|
|
setTotalHashes: (hashes: number) => void;
|
|
acceptedHashes: number;
|
|
setAcceptedHashes: (hashes: number) => void;
|
|
}
|
|
|
|
const SettingsContext = createContext<SettingsContextType | undefined>(undefined);
|
|
|
|
export const SettingsProvider = ({ children }: { children: ReactNode }) => {
|
|
const [crtEnabled, setCrtEnabled] = useState(() => {
|
|
const saved = localStorage.getItem('crtEnabled');
|
|
return saved !== null ? JSON.parse(saved) : true;
|
|
});
|
|
|
|
const [soundEnabled, setSoundEnabled] = useState(() => {
|
|
const saved = localStorage.getItem('soundEnabled');
|
|
return saved !== null ? JSON.parse(saved) : true;
|
|
});
|
|
|
|
const [cryptoConsent, setCryptoConsent] = useState(() => {
|
|
const saved = localStorage.getItem('cryptoConsent');
|
|
return saved !== null ? JSON.parse(saved) : false;
|
|
});
|
|
|
|
const [hashrate, setHashrate] = useState(0);
|
|
const [totalHashes, setTotalHashes] = useState(0);
|
|
const [acceptedHashes, setAcceptedHashes] = useState(0);
|
|
|
|
// Single AudioContext instance, persisted across renders
|
|
const audioContextRef = useRef<AudioContext | null>(null);
|
|
|
|
// Use ref to always have current soundEnabled value in callbacks
|
|
const soundEnabledRef = useRef(soundEnabled);
|
|
useEffect(() => {
|
|
soundEnabledRef.current = soundEnabled;
|
|
}, [soundEnabled]);
|
|
|
|
// Get or create AudioContext
|
|
const getAudioContext = useCallback(() => {
|
|
if (!audioContextRef.current) {
|
|
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
}
|
|
|
|
// Resume if suspended (browser autoplay policy)
|
|
if (audioContextRef.current.state === 'suspended') {
|
|
audioContextRef.current.resume();
|
|
}
|
|
|
|
return audioContextRef.current;
|
|
}, []);
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (audioContextRef.current) {
|
|
audioContextRef.current.close();
|
|
audioContextRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('crtEnabled', JSON.stringify(crtEnabled));
|
|
}, [crtEnabled]);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('soundEnabled', JSON.stringify(soundEnabled));
|
|
}, [soundEnabled]);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('cryptoConsent', JSON.stringify(cryptoConsent));
|
|
}, [cryptoConsent]);
|
|
|
|
const playSound = useCallback((type: SoundType) => {
|
|
// Use ref to ensure we always have the latest value
|
|
if (!soundEnabledRef.current) return;
|
|
|
|
try {
|
|
const audioContext = getAudioContext();
|
|
const oscillator = audioContext.createOscillator();
|
|
const gainNode = audioContext.createGain();
|
|
|
|
oscillator.connect(gainNode);
|
|
gainNode.connect(audioContext.destination);
|
|
|
|
const now = audioContext.currentTime;
|
|
|
|
switch (type) {
|
|
case 'click':
|
|
oscillator.frequency.value = 800;
|
|
gainNode.gain.value = 0.1;
|
|
oscillator.start(now);
|
|
oscillator.stop(now + 0.05);
|
|
break;
|
|
case 'beep':
|
|
oscillator.frequency.value = 1200;
|
|
gainNode.gain.value = 0.08;
|
|
oscillator.start(now);
|
|
oscillator.stop(now + 0.1);
|
|
break;
|
|
case 'hover':
|
|
oscillator.frequency.value = 600;
|
|
gainNode.gain.value = 0.05;
|
|
oscillator.start(now);
|
|
oscillator.stop(now + 0.03);
|
|
break;
|
|
case 'boot':
|
|
oscillator.type = 'sawtooth';
|
|
oscillator.frequency.setValueAtTime(200, now);
|
|
oscillator.frequency.exponentialRampToValueAtTime(800, now + 0.2);
|
|
gainNode.gain.setValueAtTime(0.1, now);
|
|
gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
|
|
oscillator.start(now);
|
|
oscillator.stop(now + 0.3);
|
|
break;
|
|
case 'success':
|
|
oscillator.frequency.setValueAtTime(800, now);
|
|
oscillator.frequency.setValueAtTime(1200, now + 0.1);
|
|
gainNode.gain.value = 0.1;
|
|
oscillator.start(now);
|
|
oscillator.stop(now + 0.2);
|
|
break;
|
|
case 'error':
|
|
oscillator.type = 'square';
|
|
oscillator.frequency.value = 150;
|
|
gainNode.gain.value = 0.08;
|
|
oscillator.start(now);
|
|
oscillator.stop(now + 0.15);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
// Silently fail if audio context has issues
|
|
console.warn('Audio playback failed:', e);
|
|
}
|
|
}, [getAudioContext]);
|
|
|
|
return (
|
|
<SettingsContext.Provider
|
|
value={{
|
|
crtEnabled,
|
|
setCrtEnabled,
|
|
soundEnabled,
|
|
setSoundEnabled,
|
|
cryptoConsent,
|
|
setCryptoConsent,
|
|
playSound,
|
|
hashrate,
|
|
setHashrate,
|
|
totalHashes,
|
|
setTotalHashes,
|
|
acceptedHashes,
|
|
setAcceptedHashes,
|
|
}}
|
|
>
|
|
{children}
|
|
</SettingsContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useSettings = () => {
|
|
const context = useContext(SettingsContext);
|
|
if (context === undefined) {
|
|
throw new Error('useSettings must be used within a SettingsProvider');
|
|
}
|
|
return context;
|
|
};
|