Volume: quieter/louder buttons that take the remote's own step
Next to the five volume bars, a − and a + button move the volume by
`volume_increment` device units, the same step as the IR remote's volume keys and
the rotary encoder, via a new `{steps}` form of POST /api/volume. The bars still
jump to a percentage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,12 @@ export function App() {
|
||||
[connection],
|
||||
);
|
||||
|
||||
/** The IR remote's own volume step (the backend's `volume_increment`), not a
|
||||
* percentage - so no optimistic patch: the state push brings the new level. */
|
||||
const stepVolume = useCallback((steps: number) => {
|
||||
void api.stepVolume(steps);
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
if (!state) return;
|
||||
if (state.album_id === null) {
|
||||
@@ -576,6 +582,7 @@ export function App() {
|
||||
onPrevious={onPrevious}
|
||||
onSeek={seekTo}
|
||||
onVolume={setVolume}
|
||||
onVolumeStep={stepVolume}
|
||||
onMute={onMute}
|
||||
onOpenAlbum={onOpenCurrentAlbum}
|
||||
onOpenAssign={() => setAssignPopupOpen(true)}
|
||||
@@ -685,6 +692,7 @@ export function App() {
|
||||
onPrevious={onPrevious}
|
||||
onSeek={seekTo}
|
||||
onVolume={setVolume}
|
||||
onVolumeStep={stepVolume}
|
||||
onMute={onMute}
|
||||
onOpenPlayView={() => setUi((previous) => ({ ...previous, view: "play" }))}
|
||||
/>
|
||||
|
||||
@@ -97,6 +97,8 @@ export const api = {
|
||||
seek: (position: number) => post("/seek", { position }),
|
||||
setVolume: (percent: number) => post("/volume", { percent }),
|
||||
nudgeVolume: (deltaPercent: number) => post("/volume", { delta_percent: deltaPercent }),
|
||||
/** One step of the IR remote's volume keys per unit, in the device's own increment. */
|
||||
stepVolume: (steps: number) => post("/volume", { steps }),
|
||||
toggleScreen: () => post("/screen/toggle"),
|
||||
/** Someone is at the kiosk: holds off the screen's idle auto-off. */
|
||||
screenActivity: () => post("/screen/activity"),
|
||||
|
||||
@@ -20,6 +20,8 @@ interface Props {
|
||||
onPrevious: () => void;
|
||||
onSeek: (position: number) => void;
|
||||
onVolume: (percent: number) => void;
|
||||
/** One IR-remote-sized step up (+1) or down (-1). */
|
||||
onVolumeStep: (steps: number) => void;
|
||||
onMute: () => void;
|
||||
onOpenAlbum: () => void;
|
||||
/** Assigning the current album/show to a remote key. `assignPending` is true right
|
||||
@@ -42,6 +44,7 @@ export function PlayView({
|
||||
onPrevious,
|
||||
onSeek,
|
||||
onVolume,
|
||||
onVolumeStep,
|
||||
onMute,
|
||||
onOpenAlbum,
|
||||
onOpenAssign,
|
||||
@@ -221,6 +224,7 @@ export function PlayView({
|
||||
<VolumeBars
|
||||
volume={state.volume}
|
||||
onChange={onVolume}
|
||||
onStep={onVolumeStep}
|
||||
onMute={onMute}
|
||||
barWidth={18}
|
||||
barHeight="clamp(26px, 4.6vh, 44px)"
|
||||
|
||||
@@ -18,6 +18,8 @@ interface Props {
|
||||
onPrevious: () => void;
|
||||
onSeek: (position: number) => void;
|
||||
onVolume: (percent: number) => void;
|
||||
/** One IR-remote-sized step up (+1) or down (-1). */
|
||||
onVolumeStep: (steps: number) => void;
|
||||
onMute: () => void;
|
||||
onOpenPlayView: () => void;
|
||||
}
|
||||
@@ -30,6 +32,7 @@ export function PlayerBar({
|
||||
onPrevious,
|
||||
onSeek,
|
||||
onVolume,
|
||||
onVolumeStep,
|
||||
onMute,
|
||||
onOpenPlayView,
|
||||
}: Props) {
|
||||
@@ -153,6 +156,7 @@ export function PlayerBar({
|
||||
<VolumeBars
|
||||
volume={state.volume}
|
||||
onChange={onVolume}
|
||||
onStep={onVolumeStep}
|
||||
onMute={onMute}
|
||||
barWidth={11}
|
||||
barHeight="30px"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/** Five bars, not a slider. The device's real range never reaches the browser: 100 %
|
||||
* here means whatever ceiling a parent configured. */
|
||||
/** Five bars, not a slider, flanked by a quieter and a louder button. The device's real
|
||||
* range never reaches the browser: 100 % here means whatever ceiling a parent
|
||||
* configured. The bars jump to a level; the buttons take the IR remote's own step. */
|
||||
|
||||
interface Props {
|
||||
/** 0..100. */
|
||||
volume: number;
|
||||
onChange: (percent: number) => void;
|
||||
/** +1 or -1: one press of the IR remote's volume key. */
|
||||
onStep: (steps: number) => void;
|
||||
onMute: () => void;
|
||||
barWidth: number;
|
||||
barHeight: string;
|
||||
@@ -12,9 +15,51 @@ interface Props {
|
||||
iconSize: number;
|
||||
}
|
||||
|
||||
function StepButton({
|
||||
label,
|
||||
symbol,
|
||||
size,
|
||||
onClick,
|
||||
}: {
|
||||
label: string;
|
||||
symbol: string;
|
||||
size: number;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
style={{
|
||||
border: "none",
|
||||
padding: 0,
|
||||
cursor: "pointer",
|
||||
width: size * 1.4,
|
||||
height: size * 1.4,
|
||||
borderRadius: 999,
|
||||
flexShrink: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
// The active bars' colour, so the pair reads on the light play view and the
|
||||
// dark player bar alike.
|
||||
background: "var(--accent)",
|
||||
color: "#fff",
|
||||
fontSize: size,
|
||||
fontWeight: 900,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{symbol}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function VolumeBars({
|
||||
volume,
|
||||
onChange,
|
||||
onStep,
|
||||
onMute,
|
||||
barWidth,
|
||||
barHeight,
|
||||
@@ -37,6 +82,7 @@ export function VolumeBars({
|
||||
>
|
||||
{volume === 0 ? "🔇" : "🔊"}
|
||||
</button>
|
||||
<StepButton label="Leiser" symbol="−" size={iconSize} onClick={() => onStep(-1)} />
|
||||
<div style={{ display: "flex", gap }}>
|
||||
{[20, 40, 60, 80, 100].map((level) => (
|
||||
<button
|
||||
@@ -57,6 +103,7 @@ export function VolumeBars({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<StepButton label="Lauter" symbol="+" size={iconSize} onClick={() => onStep(1)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user