Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/web/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ class App {
private streamGame: { appId: string; title: string } | null = null;
/** What the host said when End game was refused mid-stream, for a few seconds. */
private streamNotice: string | undefined;
/** When the live stream began, while its exit hint is up; `0` once the hint is gone. */
private hintSince = 0;
private hintTimer = 0;
private wasStreaming = false;
private streamNoticeTimer = 0;
/** The origin whose running title is being polled. */
private pollingFor: string | null = null;
Expand Down Expand Up @@ -237,6 +241,19 @@ class App {
this.menuOpen = false;
this.streamGame = null;
}
// The exit hint: armed on the edge into a stream, dropped by its timer or the stream's end.
const streaming = s.kind === "streaming";
if (streaming && !this.wasStreaming && this.prefs.exitHint) {
this.hintSince = Date.now();
clearTimeout(this.hintTimer);
this.hintTimer = window.setTimeout(() => {
this.hintSince = 0;
this.render(this.engine.current);
}, EXIT_HINT_MS);
} else if (!streaming) {
this.hintSince = 0;
}
this.wasStreaming = streaming;
if (this.settingsOpen) {
return this.show({ kind: "settings", values: this.prefs, streaming: s.kind === "streaming" });
}
Expand Down Expand Up @@ -337,6 +354,9 @@ class App {
stats: { origin: s.origin, ...s.stats },
diagnostics: s.stats.statsTier !== "off",
menu: this.menuOpen,
corner: this.prefs.hudPlacement,
scale: this.prefs.statsScalePct / 100,
...(this.hintSince ? { exitHint: exitHintText() } : {}),
...(this.streamGame ? { endGame: this.streamGame.title } : {}),
...(this.streamNotice ? { notice: this.streamNotice } : {}),
});
Expand Down Expand Up @@ -1085,3 +1105,14 @@ try {
text: sentence(e instanceof Error ? e.message : String(e)),
});
}

/** How long the exit hint stays up; the page's keyframes fade it out over the last tenth. */
const EXIT_HINT_MS = 6000;

/** How to leave with the input in hand: the pad chord with a controller connected, the dial's
* End stream on a touchscreen, the key otherwise. */
function exitHintText(): string {
if (navigator.getGamepads?.().some((g) => g?.mapping === "standard")) return "Hold L1 + R1 + Start + Select to leave";
if (matchMedia("(pointer: coarse)").matches) return "Menu, then End stream, to leave";
return "Ctrl+Alt+Shift+D to leave";
}
11 changes: 11 additions & 0 deletions apps/web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,14 @@
radial-gradient(50rem 36rem at 90% 110%, rgb(108 91 243 / 0.14), transparent 62%),
var(--background);
}

/* The exit hint: six seconds on screen, the last 0.6 fading, as on every client. */
@keyframes pf-exit-hint {
0%,
90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
10 changes: 7 additions & 3 deletions apps/web/src/ui/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const hosts: HostCard[] = [
];
const shelves = hosts.filter((h) => h.fingerprint);
const origin = "https://192.168.1.25:47990";
/** The statistics where a fresh browser puts them. */
const overlay = { corner: "topTrailing", scale: 1 } as const;

export const SCREENS = {
"home": { kind: "home", adding: false, hosts },
Expand All @@ -78,14 +80,16 @@ export const SCREENS = {
"library-loading": { kind: "library", origin, shelves, host: "living-room-pc", entries: [], art: new Map(), busy: true },
"library-offline": { kind: "library", origin, shelves, host: "living-room-pc", entries: [], art: new Map(), offline: true },
"library-none": { kind: "library", origin: null, shelves: [], entries: [], art: new Map(), offline: true },
"streaming": { kind: "streaming", stats, diagnostics: false, menu: false },
"streaming-diagnostics": { kind: "streaming", stats, diagnostics: true, menu: false },
"streaming-menu": { kind: "streaming", stats, diagnostics: false, menu: true },
"streaming": { kind: "streaming", stats, diagnostics: false, menu: false, ...overlay },
"streaming-hint": { kind: "streaming", stats, diagnostics: false, menu: false, ...overlay, exitHint: "Ctrl+Alt+Shift+D to leave" },
"streaming-diagnostics": { kind: "streaming", stats, diagnostics: true, menu: false, ...overlay },
"streaming-menu": { kind: "streaming", stats, diagnostics: false, menu: true, ...overlay },
"streaming-captured": {
kind: "streaming",
stats: { ...stats, pointerCaptured: true },
diagnostics: false,
menu: false,
...overlay,
},
"settings": { kind: "settings", values: DEFAULTS, streaming: false },
"settings-streaming": { kind: "settings", values: DEFAULTS, streaming: true },
Expand Down
32 changes: 27 additions & 5 deletions apps/web/src/ui/hud.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// The overlay over a live picture. Out of the way by default — this is the screen someone came
// for — and brought back by a pointer, a key or a tap, then hidden again.

import type { HudLine } from "@punktfunk/stream";
import type { HudCorner, HudLine } from "@punktfunk/stream";
import { cn } from "@unom/ui/lib/utils";
import { Maximize, Menu, MousePointer2, Settings } from "lucide-react";
import { type JSX, useEffect, useState } from "react";
Expand Down Expand Up @@ -100,7 +100,13 @@ export function Hud({ screen, actions }: { screen: Extract<Screen, { kind: "stre
</Button>
</div>
{screen.menu && <QuickMenu screen={screen} actions={actions} />}
{screen.diagnostics && <Diagnostics stats={stats} />}
{screen.diagnostics && <Diagnostics stats={stats} corner={screen.corner} scale={screen.scale} />}
{/* How to leave, once, as the stream starts; the captured-pointer line below says it then. */}
{screen.exitHint && !stats.pointerCaptured && (
<div className="pointer-events-none fixed inset-x-0 bottom-[6%] flex justify-center px-inset animate-[pf-exit-hint_6s_forwards]">
<span className="rounded-full bg-card/95 px-4 py-2 text-sm">{screen.exitHint}</span>
</div>
)}
{/* The host's word on a launch that did not give the player their game, or on this
device's access changing or about to end. */}
{(screen.notice ?? stats.launchNotice ?? stats.accessNotice) && (
Expand Down Expand Up @@ -213,9 +219,18 @@ const ROLE_CLASS: Record<HudLine["role"], string> = {
warn: "text-amber-500",
};

/** Where each corner puts the panel. The top ones clear the toolbar. */
const CORNER_CLASS: Record<HudCorner, string> = {
topLeading: "top-[calc(3.5rem+var(--pf-inset))] left-inset",
topTrailing: "top-[calc(3.5rem+var(--pf-inset))] right-inset",
bottomLeading: "bottom-inset left-inset",
bottomTrailing: "bottom-inset right-inset",
};

/** The stats overlay every client draws, in this session's tier and vocabulary, then what only a
* browser measures. Lines come from the engine formatted; this panel only paints them. */
function Diagnostics({ stats: s }: { stats: SessionStats }): JSX.Element {
* browser measures. Lines come from the engine formatted; this panel only paints them, in
* `corner` and at `scale` times its size. */
function Diagnostics({ stats: s, corner, scale }: { stats: SessionStats; corner: HudCorner; scale: number }): JSX.Element {
const lines = s.hud ?? [];
const browser = [
s.backend ?? "",
Expand All @@ -228,7 +243,14 @@ function Diagnostics({ stats: s }: { stats: SessionStats }): JSX.Element {
return (
<Card
aria-label="Statistics"
className="pointer-events-auto fixed top-[calc(3.5rem+var(--pf-inset))] right-inset bg-card/95 backdrop-blur-none max-h-[60dvh] w-[min(34rem,calc(100vw-2*var(--pf-inset)))] overflow-y-auto px-5 py-4 font-mono text-sm leading-relaxed"
className={cn(
"pointer-events-auto fixed bg-card/95 backdrop-blur-none max-h-[60dvh] overflow-y-auto px-5 py-4 font-mono leading-relaxed",
CORNER_CLASS[corner],
)}
style={{
fontSize: `${0.875 * scale}rem`,
width: `min(${34 * scale}rem, calc(100vw - 2 * var(--pf-inset)))`,
}}
>
{lines.map((l, i) => (
// Lines have no identity beyond their place in the list.
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/ui/screens.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const LibraryLoading = of(SCREENS["library-loading"]);
export const LibraryOffline = of(SCREENS["library-offline"]);
export const LibraryNone = of(SCREENS["library-none"]);
export const Streaming = of(SCREENS["streaming"]);
export const StreamingHint = of(SCREENS["streaming-hint"]);
export const StreamingDiagnostics = of(SCREENS["streaming-diagnostics"]);
export const StreamingCaptured = of(SCREENS["streaming-captured"]);
export const StreamingMenu = of(SCREENS["streaming-menu"]);
Expand Down
Loading
Loading