Number keys: the keyboard's digits work like the remote's; a key can hold one song
The IR remote's digits and the kiosk keyboard's number keys now both emit
PlayRemoteSlotRequested, and one reaction resolves the slot for either (a podcast
show to its newest episode, an album to the album). POST /api/remote/press/{digit}
is the keyboard's way in. Mid-search a digit is still part of the query.
A remote slot can start one song instead of a whole album: `track_index` on an
album slot plays from that track, then the rest of the album. Shift+A on the play
screen arms "this song" where A arms "this album", and the assign popup offers
both. A series slot cannot name a song, and an index past the album's end is
rejected when saving.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ const SECTIONS: Array<[title: string, rows: Array<[caps: string[], label: string
|
||||
[["⇧J", "⇧K"], "Leiser / lauter"],
|
||||
[["⇧M"], "Stumm"],
|
||||
[["⇧←", "⇧→"], "15 Sekunden zurück / vor"],
|
||||
[["0-9"], "Wie die Zahlen auf der Fernbedienung"],
|
||||
],
|
||||
],
|
||||
[
|
||||
@@ -35,7 +36,8 @@ const SECTIONS: Array<[title: string, rows: Array<[caps: string[], label: string
|
||||
[
|
||||
"Sonstiges",
|
||||
[
|
||||
[["A", "0-9"], "Titel auf Fernbedienungs-Taste legen"],
|
||||
[["A", "0-9"], "Album auf Fernbedienungs-Taste legen"],
|
||||
[["⇧A", "0-9"], "Nur dieses Lied auf die Taste legen"],
|
||||
[["ENTF"], "Bildschirm aus / an"],
|
||||
[["F1"], "Diese Hilfe"],
|
||||
[["ESC"], "Schließen / Suche löschen"],
|
||||
|
||||
@@ -233,7 +233,7 @@ export function PlayView({
|
||||
<button
|
||||
onClick={onOpenAssign}
|
||||
aria-label={assignPending ? "Zahl drücken …" : "Taste zuweisen"}
|
||||
title="Einer Fernbedienungs-Taste zuweisen (oder: A und dann eine Zahl)"
|
||||
title="Einer Fernbedienungs-Taste zuweisen (oder: A und dann eine Zahl, Shift+A für nur dieses Lied)"
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 28,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
/** "Which remote key?" - a simple 0-9 grid for assigning the currently playing album
|
||||
* or podcast show to a number key on the IR remote. Same job as pressing "A" then a
|
||||
* digit; this is the tap-only way in, for a viewer with no keyboard. */
|
||||
* or podcast show to a number key on the IR remote. Same job as pressing "A" (or
|
||||
* Shift+"A" for the song) then a digit; this is the tap-only way in, for a viewer with
|
||||
* no keyboard. */
|
||||
|
||||
import { useState, type ReactNode } from "react";
|
||||
|
||||
import type { Album, RemoteMapping } from "../api/types";
|
||||
import { remoteSlotView } from "../lib/remote";
|
||||
import { canAssignSong, remoteSlotView } from "../lib/remote";
|
||||
import { Cover } from "./Cover";
|
||||
|
||||
const DIGITS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
|
||||
@@ -11,15 +14,63 @@ const ZERO_COLUMN = 2;
|
||||
|
||||
interface Props {
|
||||
album: Album;
|
||||
/** The track playing now - what "ab diesem Lied" assigns. */
|
||||
currentTrackIndex: number;
|
||||
mapping: RemoteMapping | null;
|
||||
albums: Album[];
|
||||
onAssign: (digit: string) => void;
|
||||
onAssign: (digit: string, fromTrack: boolean) => void;
|
||||
onClear: (digit: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function RemoteAssignPopup({ album, mapping, albums, onAssign, onClear, onClose }: Props) {
|
||||
function ChoiceButton({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
aria-pressed={active}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: "8px 10px",
|
||||
borderRadius: 10,
|
||||
fontSize: 13,
|
||||
fontWeight: 800,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
background: active ? "var(--accent)" : "transparent",
|
||||
color: active ? "#fff" : "var(--ink)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function RemoteAssignPopup({
|
||||
album,
|
||||
currentTrackIndex,
|
||||
mapping,
|
||||
albums,
|
||||
onAssign,
|
||||
onClear,
|
||||
onClose,
|
||||
}: Props) {
|
||||
const byDigit = new Map(mapping?.slots.map((slot) => [slot.digit, slot]));
|
||||
const songChoice = canAssignSong(album);
|
||||
const currentTrack = album.tracks[currentTrackIndex];
|
||||
const [fromTrack, setFromTrack] = useState(false);
|
||||
const assignsSong = songChoice && fromTrack;
|
||||
|
||||
return (
|
||||
<div className="overlay" style={{ zIndex: 6 }} onClick={onClose}>
|
||||
@@ -39,9 +90,29 @@ export function RemoteAssignPopup({ album, mapping, albums, onAssign, onClear, o
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
„{album.title}" antippen, welche Taste?
|
||||
„{assignsSong && currentTrack ? currentTrack.title : album.title}" – welche Taste?
|
||||
</div>
|
||||
|
||||
{songChoice && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 4,
|
||||
padding: 4,
|
||||
marginBottom: 16,
|
||||
borderRadius: 14,
|
||||
background: "oklch(94% 0.01 var(--hue))",
|
||||
}}
|
||||
>
|
||||
<ChoiceButton active={!fromTrack} onClick={() => setFromTrack(false)}>
|
||||
Ganzes Album
|
||||
</ChoiceButton>
|
||||
<ChoiceButton active={fromTrack} onClick={() => setFromTrack(true)}>
|
||||
Ab diesem Lied{currentTrack ? `: ${currentTrack.title}` : ""}
|
||||
</ChoiceButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
@@ -60,10 +131,10 @@ export function RemoteAssignPopup({ album, mapping, albums, onAssign, onClear, o
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => onAssign(digit)}
|
||||
onClick={() => onAssign(digit, assignsSong)}
|
||||
title={
|
||||
view.state === "ok"
|
||||
? `Taste ${digit}: ${view.album.title} → überschreiben`
|
||||
? `Taste ${digit}: ${view.track?.title ?? view.album.title} → überschreiben`
|
||||
: `Taste ${digit} zuweisen`
|
||||
}
|
||||
style={{
|
||||
@@ -98,6 +169,11 @@ export function RemoteAssignPopup({ album, mapping, albums, onAssign, onClear, o
|
||||
)}
|
||||
<span style={{ fontSize: 13, fontWeight: 900, color: "var(--ink)" }}>
|
||||
{digit}
|
||||
{view.state === "ok" && view.track && (
|
||||
<span style={{ fontWeight: 700, opacity: 0.7 }}>
|
||||
{" "}· Lied {view.slot.track_index + 1}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
{view.state !== "empty" && (
|
||||
|
||||
Reference in New Issue
Block a user