ba000111d7
H5 原生能力只信任真实 host.getRuntime 回包 移除桌面壳未声明网络事件白名单 补齐原生壳能力门控测试和共享决策记录
123 lines
3.6 KiB
TypeScript
123 lines
3.6 KiB
TypeScript
import { requestHostHapticsImpact } from './host-bridge/hostBridge';
|
|
|
|
export const DEFAULT_RUNTIME_CLICK_SOUND_SRC = '/audio/ui-click-soft.wav';
|
|
export const DEFAULT_RUNTIME_LEVEL_CLEAR_SOUND_SRC =
|
|
'/audio/ui-level-clear.wav';
|
|
export const DEFAULT_RUNTIME_MERGE_SOUND_SRC =
|
|
DEFAULT_RUNTIME_LEVEL_CLEAR_SOUND_SRC;
|
|
export const DEFAULT_RUNTIME_COUNTDOWN_SOUND_SRC =
|
|
'/audio/ui-countdown-warning.wav';
|
|
export const DEFAULT_RUNTIME_COUNTDOWN_WARNING_THRESHOLD_MS = 5_000;
|
|
|
|
export const DEFAULT_RUNTIME_LEVEL_AUDIO_CONFIG = {
|
|
clickSoundSrc: DEFAULT_RUNTIME_CLICK_SOUND_SRC,
|
|
levelClearSoundSrc: DEFAULT_RUNTIME_LEVEL_CLEAR_SOUND_SRC,
|
|
countdownSoundSrc: DEFAULT_RUNTIME_COUNTDOWN_SOUND_SRC,
|
|
countdownWarningThresholdMs: DEFAULT_RUNTIME_COUNTDOWN_WARNING_THRESHOLD_MS,
|
|
} as const;
|
|
|
|
const runtimeAudioCache = new Map<string, HTMLAudioElement>();
|
|
|
|
function triggerBrowserRuntimeVibration(patternMs: number) {
|
|
if (typeof navigator === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
const vibrate = navigator.vibrate;
|
|
if (typeof vibrate !== 'function') {
|
|
return;
|
|
}
|
|
|
|
vibrate.call(navigator, [patternMs]);
|
|
}
|
|
|
|
function clampRuntimeAudioVolume(value: number) {
|
|
if (!Number.isFinite(value)) {
|
|
return 0.6;
|
|
}
|
|
return Math.max(0, Math.min(1, value));
|
|
}
|
|
|
|
export function playRuntimeClickSound(
|
|
source = DEFAULT_RUNTIME_CLICK_SOUND_SRC,
|
|
volume = 0.6,
|
|
) {
|
|
if (import.meta.env.MODE === 'test' || typeof Audio === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
const normalizedSource = source.trim();
|
|
if (!normalizedSource) {
|
|
return;
|
|
}
|
|
|
|
const audio =
|
|
runtimeAudioCache.get(normalizedSource) ?? new Audio(normalizedSource);
|
|
runtimeAudioCache.set(normalizedSource, audio);
|
|
audio.currentTime = 0;
|
|
audio.volume = clampRuntimeAudioVolume(volume);
|
|
try {
|
|
const playResult = audio.play();
|
|
void playResult?.catch?.(() => {
|
|
// 中文注释:浏览器可能在用户手势外拒绝播放,点击反馈不应中断主交互。
|
|
});
|
|
} catch {
|
|
// 中文注释:测试环境或极端浏览器可能未实现 play,同样不能影响主交互。
|
|
}
|
|
}
|
|
|
|
export function playRuntimeLevelClearSound(volume = 0.6) {
|
|
playRuntimeClickSound(DEFAULT_RUNTIME_LEVEL_CLEAR_SOUND_SRC, volume);
|
|
}
|
|
|
|
export function playRuntimeMergeSound(volume = 0.6) {
|
|
playRuntimeClickSound(DEFAULT_RUNTIME_MERGE_SOUND_SRC, volume);
|
|
}
|
|
|
|
export function playRuntimeCountdownSound(volume = 0.6) {
|
|
playRuntimeClickSound(DEFAULT_RUNTIME_COUNTDOWN_SOUND_SRC, volume);
|
|
}
|
|
|
|
export function triggerRuntimeImpactFeedback(
|
|
style: 'light' | 'medium' | 'heavy' = 'light',
|
|
fallbackVibrationPatternMs?: number,
|
|
) {
|
|
void requestHostHapticsImpact({ style })
|
|
.then((handled) => {
|
|
if (!handled && fallbackVibrationPatternMs !== undefined) {
|
|
triggerBrowserRuntimeVibration(fallbackVibrationPatternMs);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (fallbackVibrationPatternMs !== undefined) {
|
|
triggerBrowserRuntimeVibration(fallbackVibrationPatternMs);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function triggerRuntimeClickFeedback(
|
|
source = DEFAULT_RUNTIME_CLICK_SOUND_SRC,
|
|
volume = 0.6,
|
|
options: {
|
|
hapticsStyle?: 'light' | 'medium' | 'heavy';
|
|
fallbackVibrationPatternMs?: number;
|
|
} = {},
|
|
) {
|
|
triggerRuntimeImpactFeedback(
|
|
options.hapticsStyle ?? 'light',
|
|
options.fallbackVibrationPatternMs,
|
|
);
|
|
playRuntimeClickSound(source, volume);
|
|
}
|
|
|
|
export function resolveRuntimeCountdownSecondBucket(remainingMs: number) {
|
|
if (
|
|
!Number.isFinite(remainingMs) ||
|
|
remainingMs <= 0 ||
|
|
remainingMs > DEFAULT_RUNTIME_COUNTDOWN_WARNING_THRESHOLD_MS
|
|
) {
|
|
return null;
|
|
}
|
|
return Math.max(1, Math.ceil(remainingMs / 1000));
|
|
}
|