Rework navigation for a consistent music/room toggle and back button
- Replace the "Mein Zimmer"/"Musik" text pills and the help button with
two fixed circular corner buttons: a music/room toggle (top-right)
and a single "back one level" button (top-left) used everywhere -
search, categories, and the play view.
- Split `view` (browse/play) from a new `page` (music/room) in UiState
so toggling to the room and back leaves the music side - search,
selection, even the full-screen play view - exactly as it was.
- Back peels one step at a time (search, then track-search mode, then
group+category together, since a root shelf tile sets both at once
and undoing that jump should be one step too).
- Album cards: click the cover to open the track list, click the title
to start playing immediately (matching what Enter already did from
the keyboard). Audiobook cards drop the artist line and clamp the
title to a fixed height so covers stay aligned across a row.
- Root shelf group headings ("Musik", "Hörbücher", "Podcasts") are now
the link into that group's full category grid, replacing the
separate search-icon button.
- "Taste zuweisen" is now an icon-only round button, moved to the
bottom-right so it doesn't sit under the new toggle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
147
web/src/App.tsx
147
web/src/App.tsx
@@ -6,6 +6,7 @@
|
||||
* the mouse, a figure on the reader, Home Assistant - and this UI has to follow them.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { api } from "./api/client";
|
||||
@@ -27,7 +28,7 @@ import { usePlayerState } from "./hooks/usePlayerState";
|
||||
import { DEFAULT_MANUAL_CONTROL, DEFAULT_TUNABLES } from "./lib/ambienceTunables";
|
||||
import type { AmbienceTunables, ManualControl } from "./lib/ambienceTunables";
|
||||
import type { Action, UiState } from "./lib/keyboard";
|
||||
import { handleKey, initialUiState } from "./lib/keyboard";
|
||||
import { browseBackActions, handleKey, initialUiState, isRootShelf } from "./lib/keyboard";
|
||||
import { playPop } from "./lib/pop";
|
||||
import { targetForAlbum } from "./lib/remote";
|
||||
import type { Group, Results, SongHit } from "./lib/search";
|
||||
@@ -285,11 +286,6 @@ export function App() {
|
||||
setUi((previous) => ({ ...previous, group, category, search: "", selIndex: 0 }));
|
||||
};
|
||||
|
||||
const onBackToRoot = () => {
|
||||
playPop(380);
|
||||
setUi((previous) => ({ ...previous, group: null, category: null, selIndex: 0 }));
|
||||
};
|
||||
|
||||
const onCategory = (key: string | null) => {
|
||||
if (key) playPop(440);
|
||||
setUi((previous) => ({ ...previous, category: key, selIndex: 0 }));
|
||||
@@ -307,6 +303,13 @@ export function App() {
|
||||
setUi((previous) => ({ ...previous, openAlbumId: album.id, selIndex: navIndex }));
|
||||
};
|
||||
|
||||
/** Clicking the title text (as opposed to the cover) starts the album right away -
|
||||
* the same thing ENTER does from the keyboard - instead of opening the track list. */
|
||||
const onPlayAlbum = (album: Album, navIndex: number) => {
|
||||
setUi((previous) => ({ ...previous, selIndex: navIndex }));
|
||||
play(album.id, 0);
|
||||
};
|
||||
|
||||
const onOpenCurrentAlbum = () => {
|
||||
if (!currentAlbum) return;
|
||||
playPop(420);
|
||||
@@ -320,6 +323,21 @@ export function App() {
|
||||
};
|
||||
const onMute = () => setVolume(state && state.volume > 0 ? 0 : UNMUTE_PERCENT);
|
||||
|
||||
// The single top-left "back" button, shared by every level of the music page -
|
||||
// search, categories, play view - so it always means "one step out" no matter what
|
||||
// is on screen. Nothing to back out of on the room page (it has no sub-levels), and
|
||||
// the toggle button is what moves between the two pages.
|
||||
const canGoBack = ui.page === "music" && (ui.view === "play" || !isRootShelf(ui));
|
||||
const onBack = () => {
|
||||
if (ui.view === "play") {
|
||||
setUi((previous) => ({ ...previous, view: "browse" }));
|
||||
return;
|
||||
}
|
||||
run(browseBackActions(ui));
|
||||
};
|
||||
const onToggleRoom = () =>
|
||||
setUi((previous) => ({ ...previous, page: previous.page === "room" ? "music" : "room" }));
|
||||
|
||||
return (
|
||||
<div className="stage">
|
||||
{state && (
|
||||
@@ -332,7 +350,7 @@ export function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{ui.view === "browse" && state && (
|
||||
{ui.page === "music" && ui.view === "browse" && state && (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
@@ -342,17 +360,7 @@ export function App() {
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<AppHeader
|
||||
status={<ConnectionDot online={connection.online} state={state} />}
|
||||
link={
|
||||
haConfig
|
||||
? {
|
||||
label: "💡 Mein Zimmer",
|
||||
onClick: () => setUi((previous) => ({ ...previous, view: "room" })),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<AppHeader status={<ConnectionDot online={connection.online} state={state} />} />
|
||||
<BrowseView
|
||||
results={results}
|
||||
group={ui.group}
|
||||
@@ -364,15 +372,15 @@ export function App() {
|
||||
currentAlbumId={state.album_id}
|
||||
gridRef={gridRef}
|
||||
onEnterGroup={onEnterGroup}
|
||||
onBackToRoot={onBackToRoot}
|
||||
onCategory={onCategory}
|
||||
onOpenAlbum={onOpenAlbum}
|
||||
onPlayAlbum={onPlayAlbum}
|
||||
onPlaySong={onPlaySong}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ui.view === "play" && state && (
|
||||
{ui.page === "music" && ui.view === "play" && state && (
|
||||
<PlayView
|
||||
state={state}
|
||||
album={currentAlbum}
|
||||
@@ -382,7 +390,6 @@ export function App() {
|
||||
onSeek={onSeek}
|
||||
onVolume={setVolume}
|
||||
onMute={onMute}
|
||||
onBrowse={() => setUi((previous) => ({ ...previous, view: "browse" }))}
|
||||
onOpenAlbum={onOpenCurrentAlbum}
|
||||
onOpenAssign={() => setAssignPopupOpen(true)}
|
||||
assignPending={ui.assignPending}
|
||||
@@ -421,12 +428,7 @@ export function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ui.view === "room" && haConfig && (
|
||||
<RoomView
|
||||
config={haConfig}
|
||||
onBrowse={() => setUi((previous) => ({ ...previous, view: "browse" }))}
|
||||
/>
|
||||
)}
|
||||
{ui.page === "room" && haConfig && <RoomView config={haConfig} />}
|
||||
|
||||
{openAlbum && state && (
|
||||
<AlbumModal
|
||||
@@ -438,34 +440,35 @@ export function App() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setUi((previous) => ({ ...previous, showHelp: !previous.showHelp }))}
|
||||
title="Zaubertasten (F1)"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 18,
|
||||
right: 24,
|
||||
zIndex: 3,
|
||||
width: 44,
|
||||
height: 44,
|
||||
background: "oklch(97% 0.01 210 / .95)",
|
||||
border: "none",
|
||||
borderRadius: 999,
|
||||
boxShadow: "0 6px 18px oklch(15% 0.05 210 / .4)",
|
||||
fontSize: 20,
|
||||
fontWeight: 900,
|
||||
color: "var(--ink)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
?
|
||||
</button>
|
||||
{canGoBack && (
|
||||
<CornerButton side="left" onClick={onBack} label="Zurück">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M12.5 4.5 6.5 10l6 5.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.4"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</CornerButton>
|
||||
)}
|
||||
|
||||
{haConfig && (
|
||||
<CornerButton
|
||||
side="right"
|
||||
onClick={onToggleRoom}
|
||||
label={ui.page === "room" ? "Musik" : "Mein Zimmer"}
|
||||
>
|
||||
{ui.page === "room" ? "🎵" : "💡"}
|
||||
</CornerButton>
|
||||
)}
|
||||
|
||||
{ui.showHelp && (
|
||||
<HelpOverlay onClose={() => setUi((previous) => ({ ...previous, showHelp: false }))} />
|
||||
)}
|
||||
|
||||
{ui.view === "browse" && state && (
|
||||
{ui.page === "music" && ui.view === "browse" && state && (
|
||||
<PlayerBar
|
||||
state={state}
|
||||
album={currentAlbum}
|
||||
@@ -497,6 +500,50 @@ export function App() {
|
||||
);
|
||||
}
|
||||
|
||||
/** The one fixed round button in each top corner: back (left) and the music/room
|
||||
* toggle (right). Same position and shape everywhere they appear, so "top left" and
|
||||
* "top right" always mean the same thing. */
|
||||
function CornerButton({
|
||||
side,
|
||||
onClick,
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
side: "left" | "right";
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 18,
|
||||
[side]: 24,
|
||||
zIndex: 3,
|
||||
width: 44,
|
||||
height: 44,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "oklch(97% 0.01 210 / .95)",
|
||||
border: "none",
|
||||
borderRadius: 999,
|
||||
boxShadow: "0 6px 18px oklch(15% 0.05 210 / .4)",
|
||||
fontSize: 20,
|
||||
fontWeight: 900,
|
||||
color: "var(--ink)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectionDot({ online, state }: { online: boolean; state: { active_figure: string | null } }) {
|
||||
const label = !online ? "Keine Verbindung" : state.active_figure ? `🧸 ${state.active_figure}` : null;
|
||||
if (!label) return null;
|
||||
|
||||
Reference in New Issue
Block a user