forked from Tadrisino/MathApp_frontend
- muted text #9e9ec0 -> #727299 (contrast 2.6 -> 4.5 on white); tab labels 11px - map: chapter titles wrap to two lines instead of being cut off - reduced-motion respected (MotionConfig + CSS), focus-visible ring, no pull-to-refresh, short vibration on correct/wrong answers - src/lib/bazaar.ts: bridge to the Android WebView wrapper; checkout uses Bazaar IAP only inside that app, Zarinpal everywhere else (unchanged) - android/: WebView wrapper with Poolakey; written but never built Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
import { KEYS, store } from './storage';
|
||
|
||
/**
|
||
* صداهای کوتاهِ رابط. یک AudioContext برای کلِ اپ؛ ساختنِ یکی برای هر صدا
|
||
* در Safari بعد از چند بار شکست میخورد.
|
||
*/
|
||
let ctx: AudioContext | null = null;
|
||
|
||
function context(): AudioContext | null {
|
||
if (ctx) return ctx;
|
||
const Ctor = window.AudioContext ?? window.webkitAudioContext;
|
||
if (!Ctor) return null;
|
||
try {
|
||
ctx = new Ctor();
|
||
} catch {
|
||
return null;
|
||
}
|
||
return ctx;
|
||
}
|
||
|
||
export function soundEnabled(): boolean {
|
||
return store.get(KEYS.sound) !== 'off';
|
||
}
|
||
|
||
export function setSoundEnabled(on: boolean) {
|
||
store.set(KEYS.sound, on ? 'on' : 'off');
|
||
}
|
||
|
||
function tone(frequency: number, start: number, duration: number, volume = 0.05) {
|
||
const audio = context();
|
||
if (!audio || !soundEnabled()) return;
|
||
if (audio.state === 'suspended') void audio.resume();
|
||
const t = audio.currentTime + start;
|
||
const osc = audio.createOscillator();
|
||
const gain = audio.createGain();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(frequency, t);
|
||
gain.gain.setValueAtTime(0.0001, t);
|
||
gain.gain.exponentialRampToValueAtTime(volume, t + 0.01);
|
||
gain.gain.exponentialRampToValueAtTime(0.0001, t + duration);
|
||
osc.connect(gain).connect(audio.destination);
|
||
osc.start(t);
|
||
osc.stop(t + duration + 0.02);
|
||
}
|
||
|
||
/**
|
||
* لرزشِ کوتاه کنارِ صدا، با همان کلیدِ «صدا» خاموش میشود. فقط اندروید/کروم دارد؛
|
||
* Safariِ آیفون `vibrate` ندارد و اینجا هیچ کاری نمیکند.
|
||
*/
|
||
function buzz(pattern: number | number[]) {
|
||
if (!soundEnabled() || typeof navigator.vibrate !== 'function') return;
|
||
try {
|
||
navigator.vibrate(pattern);
|
||
} catch {
|
||
/* بعضی WebViewها بیاجازه throw میکنند؛ لرزش فقط تزئینی است */
|
||
}
|
||
}
|
||
|
||
export const sounds = {
|
||
tick: () => tone(720, 0, 0.045, 0.04),
|
||
correct: () => {
|
||
tone(660, 0, 0.12);
|
||
tone(990, 0.1, 0.18);
|
||
buzz(30);
|
||
},
|
||
wrong: () => {
|
||
tone(220, 0, 0.25, 0.06);
|
||
buzz([40, 60, 40]);
|
||
},
|
||
finish: () => [523, 659, 784, 1047].forEach((f, i) => tone(f, i * 0.11, 0.2)),
|
||
};
|