Files
pgames-manager/frontend/src/player/volume-store.ts
T
mrleo1nidandClaude Sonnet 5.5 a93f33c93d
CI / frontend (push) Successful in 41s
CI / backend (push) Successful in 1m41s
feat: shared volume settings with a per-game override
- Music, sound and voice volumes are shared by all games by default; a game that
  has never been launched with its own volumes plays with the shared ones
- The sound section of the player settings has a "own volume for this game" switch:
  turning it on starts from the current values and then the sliders change only this
  game, turning it off removes the override and returns to the shared volumes
- The override belongs to the game (game_id from /api/versions/{id}), so forks and mods
  share it; it falls back to the version id when the id is unavailable
- Reading speed and auto delay stay shared
- volume-store.ts keeps the storage logic (damaged data ignored, values clamped,
  unavailable storage tolerated) and has its own tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 18:27:39 +03:00

52 lines
1.4 KiB
TypeScript

// Volumes of a game: shared by all games unless the game has been given its own.
import type { Volumes } from "./audio";
type Store = Pick<Storage, "getItem" | "setItem" | "removeItem">;
function usable(v: any): v is number {
return typeof v === "number" && Number.isFinite(v);
}
const clamp = (v: number) => Math.max(0, Math.min(1, v));
export class VolumeStore {
constructor(private key: string | null, private storage: Store = localStorage) {}
get available() {
return this.key !== null;
}
/** This game's own volumes, or null when it has none (it then plays with the shared ones). */
read(): Volumes | null {
if (!this.key) return null;
try {
const data = JSON.parse(this.storage.getItem(this.key) ?? "null");
if (data && usable(data.music) && usable(data.sound) && usable(data.voice)) {
return { music: clamp(data.music), sound: clamp(data.sound), voice: clamp(data.voice) };
}
} catch {
/* storage unavailable or damaged: fall back to the shared volumes */
}
return null;
}
write({ music, sound, voice }: Volumes) {
if (!this.key) return;
try {
this.storage.setItem(this.key, JSON.stringify({ music, sound, voice }));
} catch {
/* the change still applies for this session */
}
}
clear() {
if (!this.key) return;
try {
this.storage.removeItem(this.key);
} catch {
/* nothing to remove */
}
}
}