feat: add SFX volume control and improve audio feedback
- Implemented separate sound effects volume control in settings. - Updated backend to handle new `sfx_volume` parameter. - Enhanced UI to include sound effects volume slider in admin panel. - Integrated sound effects for various user interactions (clicks, navigation, alerts). - Added a new global sound engine to manage audio synthesis without external files. - Updated documentation for Docker deployment and Windows packaging. - Refactored audio context management to ensure sound effects are available post user interaction.
This commit is contained in:
@@ -5,10 +5,13 @@ import MediaHeaderControls from './MediaHeaderControls';
|
||||
import AboutDialog from './AboutDialog';
|
||||
import { PAGE_ORDER, usePageIndex } from '../utils/pageNav';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
|
||||
import { Sfx } from '../utils/sounds';
|
||||
import { getApiBase as API_BASE } from '../config';
|
||||
|
||||
/* =========================================================
|
||||
共享页眉 —— 所有大屏页面统一(由 ScreenLayout 注入)
|
||||
状态文案按当前页面自动识别;左右键循环 + 可点击按钮切换
|
||||
· 音量面板:媒体音量 + 音效音量(写回后端设置并 MQTT 广播同步)
|
||||
========================================================= */
|
||||
|
||||
const STATUS_BY_PATH = {
|
||||
@@ -25,9 +28,66 @@ export default function PageHeader() {
|
||||
const [aboutOpen, setAboutOpen] = useState(false);
|
||||
const [gestureHit, setGestureHit] = useState(false); // 识别到手势 → 惊喜图标 3s
|
||||
const [vision, setVision] = useState({ status: 'off', cameraOn: false, gesture: null }); // 视觉识别状态
|
||||
const [volOpen, setVolOpen] = useState(false);
|
||||
const [mediaVol, setMediaVol] = useState(80); // 媒体音量 0-100
|
||||
const [sfxVol, setSfxVol] = useState(Sfx.getVolume()); // 音效音量 0-100
|
||||
const pageIdx = usePageIndex();
|
||||
const gestureTimer = useRef(null);
|
||||
|
||||
// 启动时拉取后端设置(媒体/音效音量),并监听 MQTT settings_changed 同步
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE()}/api/settings`, { cache: 'no-store' })
|
||||
.then((r) => r.json())
|
||||
.then((s) => {
|
||||
if (typeof s.volume === 'number') setMediaVol(s.volume);
|
||||
if (typeof s.sfx_volume === 'number') {
|
||||
setSfxVol(s.sfx_volume);
|
||||
Sfx.setVolume(s.sfx_volume);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
const onSettings = (e) => {
|
||||
const p = (e.detail && (e.detail.params || e.detail)) || {};
|
||||
if (typeof p.volume === 'number') setMediaVol(p.volume);
|
||||
if (typeof p.sfx_volume === 'number') {
|
||||
setSfxVol(p.sfx_volume);
|
||||
Sfx.setVolume(p.sfx_volume);
|
||||
}
|
||||
};
|
||||
// 后端/MQTT settings_changed → 同步音量(SSE 走 api/events,MQTT 走 dpm:media-control)
|
||||
window.addEventListener('dpm:mqtt-local', onSettings);
|
||||
window.addEventListener('dpm:media-control', onSettings);
|
||||
return () => {
|
||||
window.removeEventListener('dpm:mqtt-local', onSettings);
|
||||
window.removeEventListener('dpm:media-control', onSettings);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 点击面板外部 → 关闭音量面板
|
||||
useEffect(() => {
|
||||
if (!volOpen) return undefined;
|
||||
const close = () => setVolOpen(false);
|
||||
document.addEventListener('pointerdown', close);
|
||||
return () => document.removeEventListener('pointerdown', close);
|
||||
}, [volOpen]);
|
||||
|
||||
// 音量修改 → 写回后端(MQTT settings_changed 广播给所有大屏)
|
||||
const changeVolume = (key, value) => {
|
||||
const v = Math.max(0, Math.min(100, Number(value) || 0));
|
||||
if (key === 'sfx_volume') {
|
||||
setSfxVol(v);
|
||||
Sfx.setVolume(v);
|
||||
Sfx.notify();
|
||||
} else {
|
||||
setMediaVol(v);
|
||||
}
|
||||
fetch(`${API_BASE()}/api/settings`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ [key]: v }),
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const iv = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => clearInterval(iv);
|
||||
@@ -93,16 +153,44 @@ export default function PageHeader() {
|
||||
<div className="bd-header-right">
|
||||
{/* 媒体轮播页:页眉内嵌播放控制 */}
|
||||
{location.pathname === '/screen' && <MediaHeaderControls />}
|
||||
{/* 音量控制(媒体 + 音效) */}
|
||||
<div className="bd-vol-wrap">
|
||||
<button
|
||||
className={`bd-vol-btn${mediaVol === 0 ? ' muted' : ''}`}
|
||||
type="button"
|
||||
title="音量控制"
|
||||
aria-label="音量控制"
|
||||
onClick={() => { setVolOpen((v) => !v); Sfx.click(); }}
|
||||
>
|
||||
<Icon name={mediaVol === 0 ? 'volume-off' : 'volume'} size={15} />
|
||||
</button>
|
||||
{volOpen && (
|
||||
<div className="bd-vol-panel" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="bd-vol-title"><Icon name="volume" size={13} /> 媒体音量</div>
|
||||
<input
|
||||
type="range" min="0" max="100" value={mediaVol}
|
||||
onChange={(e) => changeVolume('volume', e.target.value)}
|
||||
/>
|
||||
<span className="bd-vol-val">{mediaVol}%</span>
|
||||
<div className="bd-vol-title"><Icon name="sparkles" size={13} /> 音效音量</div>
|
||||
<input
|
||||
type="range" min="0" max="100" value={sfxVol}
|
||||
onChange={(e) => changeVolume('sfx_volume', e.target.value)}
|
||||
/>
|
||||
<span className="bd-vol-val">{sfxVol}%</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="bd-nav-keys" title="左右方向键或点击切换页面">
|
||||
{left && (
|
||||
<button className="bd-nav-key" onClick={() => navigate(left.path)}>
|
||||
<button className="bd-nav-key" onClick={() => { Sfx.page(); navigate(left.path); }}>
|
||||
<Icon name="chevron-left" size={13} />
|
||||
{left.label}
|
||||
</button>
|
||||
)}
|
||||
<span className="bd-nav-dot">·</span>
|
||||
{right && (
|
||||
<button className="bd-nav-key" onClick={() => navigate(right.path)}>
|
||||
<button className="bd-nav-key" onClick={() => { Sfx.page(); navigate(right.path); }}>
|
||||
{right.label}
|
||||
<Icon name="chevron-right" size={13} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user