feat: integrate Pi backend and Pi Web

This commit is contained in:
luckyyzh
2026-07-30 19:37:53 +08:00
commit 7392ab9dd7
1390 changed files with 337197 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
"use client";
import { useState, useRef, useCallback, useEffect } from "react";
function playTone(ctx: AudioContext) {
const now = ctx.currentTime;
const freqs = [523.25, 659.25];
freqs.forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = "sine";
osc.frequency.value = freq;
const t = now + i * 0.18;
gain.gain.setValueAtTime(0, t);
gain.gain.linearRampToValueAtTime(0.18, t + 0.02);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.45);
osc.start(t);
osc.stop(t + 0.45);
});
}
export function useAudio() {
const [enabled, setEnabled] = useState<boolean>(() => {
if (typeof window === "undefined") return true;
const stored = localStorage.getItem("pi-sound-enabled");
return stored === null ? true : stored === "true";
});
const enabledRef = useRef(enabled);
useEffect(() => { enabledRef.current = enabled; }, [enabled]);
// Reuse a single AudioContext so it can be resumed if the browser
// autoplay policy suspends it (contexts created outside user gestures
// start in "suspended" state and produce no sound).
const ctxRef = useRef<AudioContext | null>(null);
const getCtx = useCallback((): AudioContext | null => {
if (ctxRef.current && ctxRef.current.state !== "closed") return ctxRef.current;
try {
ctxRef.current = new AudioContext();
} catch {
return null;
}
return ctxRef.current;
}, []);
const unlockAudio = useCallback((force = false) => {
if (!force && !enabledRef.current) return;
const ctx = getCtx();
if (!ctx || ctx.state !== "suspended") return;
ctx.resume().catch(() => {});
}, [getCtx]);
const toggle = useCallback(() => {
const next = !enabledRef.current;
if (next) unlockAudio(true);
enabledRef.current = next;
localStorage.setItem("pi-sound-enabled", String(next));
setEnabled(next);
}, [unlockAudio]);
const playDone = useCallback(() => {
if (!enabledRef.current) return;
const ctx = getCtx();
if (!ctx) return;
const play = () => {
try {
playTone(ctx);
} catch {
// AudioContext not available
}
};
if (ctx.state === "suspended") {
ctx.resume().then(play).catch(() => {});
return;
}
play();
}, [getCtx]);
return { soundEnabled: enabled, onSoundToggle: toggle, playDoneSound: playDone, unlockAudio, soundEnabledRef: enabledRef };
}