Merge the typing game into the music player as a tab, with lock/unlock UI
Moves tippen from a standalone app into web/ as a third tab (audio player / smarthome / typing), replacing the old single room-toggle corner button with a vertical icon tab rail. Curriculum and progress now come from the backend (musicmouse/tippen/*) instead of a build-time YAML import and localStorage. Adds reward-driven lock rendering: Cover/BrowseView/AlbumModal show a question mark for locked albums/tracks with a hint on what unlocks them, and ResultSheet gets a new unlock-animation block alongside the existing lesson-unlock and aquarium-creature celebrations. CSS from the two apps is merged carefully: identical rules (bubble/card/ key-cap/view-enter/backdrop-enter and their keyframes) are shared as-is, while rules that bake in each app's own hue are kept separate under a `tp-` prefix and scoped to the typing tab's own .tp-stage wrapper, so neither app's look bleeds into the other's. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,11 @@ import type {
|
||||
RemoteMapping,
|
||||
RemoteSlotInput,
|
||||
Settings,
|
||||
TippenCurriculum,
|
||||
TippenProgress,
|
||||
TippenRunInput,
|
||||
TippenRunResult,
|
||||
TippenSettings,
|
||||
TrackDetail,
|
||||
} from "./types";
|
||||
|
||||
@@ -53,6 +58,11 @@ const fetchHaConfig = (): Promise<HaConfig | null> => fetchOrNullOn404<HaConfig>
|
||||
/** `null` means the IR remote isn't configured, not an error. */
|
||||
const fetchLircConfig = (): Promise<LircConfig | null> => fetchOrNullOn404<LircConfig>("/lirc");
|
||||
|
||||
/** `null` means the typing game isn't configured, not an error - the tab still shows,
|
||||
* just with a "not set up" placeholder instead of a lesson map. */
|
||||
const fetchTippenCurriculum = (): Promise<TippenCurriculum | null> =>
|
||||
fetchOrNullOn404<TippenCurriculum>("/tippen/curriculum");
|
||||
|
||||
/** `null` covers both "unknown to Home Assistant" and "Home Assistant unreachable
|
||||
* right now" (the backend answers the latter with a 502) - the room page treats a
|
||||
* device with no state the same way either way, rather than crashing on a poll. */
|
||||
@@ -103,6 +113,13 @@ export const api = {
|
||||
remoteMapping: () => request<RemoteMapping>("/remote/mapping"),
|
||||
saveRemoteMapping: (slots: Record<string, RemoteSlotInput>) =>
|
||||
request<RemoteMapping>("/remote/mapping", { method: "PUT", body: JSON.stringify({ slots }) }),
|
||||
|
||||
tippenCurriculum: fetchTippenCurriculum,
|
||||
tippenProgress: () => request<TippenProgress>("/tippen/progress"),
|
||||
saveTippenSettings: (settings: TippenSettings) =>
|
||||
request<TippenSettings>("/tippen/settings", { method: "PUT", body: JSON.stringify(settings) }),
|
||||
recordTippenRun: (body: TippenRunInput) =>
|
||||
request<TippenRunResult>("/tippen/runs", { method: "POST", body: JSON.stringify(body) }),
|
||||
};
|
||||
|
||||
export const coverUrl = (albumId: string) => `/api/albums/${albumId}/cover`;
|
||||
|
||||
Reference in New Issue
Block a user