mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
Two field-reported gaps, unrelated except that both are about being able to read something off a screen. A PANEL'S IPv6 WAS NEVER COLLECTED, LET ALONE SHOWN. DeviceInfo.getLocalIp() filters to Inet4Address, so a v6-only panel reported no address at all and the dashboard rendered a dash for a screen that was perfectly reachable. It now reports both stacks in their own fields: a dual-stack panel genuinely has two addresses and either may be the one you need, so collapsing them into one column would make it mean "whichever interface enumerated first". Link-local (fe80::/10) is deliberately excluded. Every interface has one, they tend to enumerate first, and none can be dialled without also knowing the zone index — so admitting them would fill the field with a string nobody can paste anywhere and hide the address that works. Any %iface suffix is trimmed for the same reason. The 45-char cap the writer already applied is exactly the longest legitimate IPv6 text form, so it needed no change. The dashboard card renders only when a panel actually has a v6 address, rather than showing an empty row to the overwhelmingly v4 fleet. THE PAIRING CODE DID NOT SCALE, WHICH IS WORST WHERE IT MATTERS MOST. Every size on the pre-playback screens was a hard-coded pixel value. A CSS pixel covers a quarter of the screen area on a 4K panel that it does on 1080p, and a sixteenth on 8K — so the 72px code that fills a 1080p screen is a smudge on the 4K wall it was installed on, which is where signage actually goes. What has to stay constant is ANGULAR size, so the root font size is now viewport-proportional and everything on those screens is a rem against it. The code holds 6.67% of screen height at every resolution: 72px at 1080p — bit for bit what it renders today, so nothing changes for the existing fleet — 144px at 4K, 288px at 8K. Verified in a browser rather than by arithmetic: at a 1409px viewport the root computes to 13.0473px, which is 0.926vmin to four decimals. vmin, not vw, because portrait-mounted panels are common here and vw would render a 1080x1920 screen at half size. Clamped at both ends so the dashboard's preview iframe stays legible instead of microscopic and an ultrawide does not get silly. Applied to the web player (which BrightSign also runs) and to Tizen, where a 1920x1080 logical viewport makes it arithmetically identical to the values it replaces — the point being the panels where it is not. A test asserts the scaling cannot reach playback content: the whole safety argument is that only the chrome uses rem, and a stage or zone rule adopting it would start resizing CONTENT, which is a worse bug than the one being fixed. Android is untouched — its pairing code already autosizes within a dp-scaled layout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS
106 lines
4.1 KiB
CSS
106 lines
4.1 KiB
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
/*
|
|
* ONE KNOB for every pre-playback screen, matching server/player/index.html.
|
|
*
|
|
* A pairing code is read from across a room, so what must stay constant is ANGULAR size, not
|
|
* pixel size — and a CSS pixel covers a sixteenth of the screen area on an 8K panel that it does
|
|
* on 1080p. Sizes below are rem against this root: 1rem = 10px at a 1080-tall viewport, and it
|
|
* scales from there. A Tizen web app is handed a 1920x1080 logical viewport on most panels, where
|
|
* this is arithmetically identical to the pixel values it replaces — the point is the panels where
|
|
* it is not.
|
|
*
|
|
* vmin rather than vw: portrait-mounted panels are common and vw would halve everything on one.
|
|
* Only the setup/pairing/toast chrome uses rem; the stage and PiP layers are laid out in %/vw/vh
|
|
* and are untouched.
|
|
*/
|
|
html { font-size: clamp(7px, 0.926vmin, 56px); }
|
|
|
|
html, body {
|
|
width: 100%; height: 100%;
|
|
background: #000; color: #f1f5f9;
|
|
font-family: "Samsung One", "Tizen Sans", Arial, sans-serif;
|
|
overflow: hidden;
|
|
cursor: none;
|
|
}
|
|
|
|
.screen {
|
|
position: absolute; top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.hidden { display: none !important; }
|
|
|
|
/* Setup / pairing card */
|
|
.card {
|
|
background: #111827;
|
|
border: 1px solid #1f2937;
|
|
border-radius: 1.8rem;
|
|
padding: 4.8rem 6.4rem;
|
|
text-align: center;
|
|
max-width: 76rem;
|
|
}
|
|
.card h1 { color: #3b82f6; font-size: 4.4rem; margin-bottom: 0.6rem; }
|
|
.sub { color: #94a3b8; font-size: 2.2rem; margin-bottom: 3.6rem; }
|
|
.card label { display: block; text-align: left; color: #94a3b8; font-size: 1.8rem; margin-bottom: 0.8rem; }
|
|
|
|
#serverUrl {
|
|
width: 100%; font-size: 2.6rem; padding: 1.6rem 2rem;
|
|
border-radius: 1rem; border: 0.2rem solid #334155;
|
|
background: #0b1220; color: #f1f5f9; margin-bottom: 2.4rem;
|
|
}
|
|
#serverUrl:focus { outline: none; border-color: #3b82f6; }
|
|
|
|
button {
|
|
font-size: 2.4rem; font-weight: bold; color: #fff;
|
|
background: #3b82f6; border: none; border-radius: 1rem;
|
|
padding: 1.6rem 4rem; cursor: pointer;
|
|
}
|
|
button:focus { outline: 0.3rem solid #93c5fd; }
|
|
button.ghost { background: transparent; color: #64748b; font-size: 1.8rem; margin-top: 2.4rem; padding: 0.8rem; }
|
|
|
|
.status { color: #64748b; font-size: 1.8rem; margin-top: 2rem; min-height: 2.4rem; }
|
|
.status.error { color: #ef4444; }
|
|
|
|
/* Pairing code */
|
|
.code {
|
|
font-size: 9.6rem; font-weight: bold; letter-spacing: 1.6rem;
|
|
color: #22c55e; margin: 2.4rem 0; font-family: monospace;
|
|
}
|
|
.hint { color: #94a3b8; font-size: 2rem; line-height: 1.5; }
|
|
|
|
/* Playback stage */
|
|
.stage { background: #000; }
|
|
.stage img, .stage video, .stage iframe {
|
|
position: absolute; top: 0; left: 0;
|
|
width: 100%; height: 100%; border: 0;
|
|
}
|
|
.stage img.contain, .stage video.contain { object-fit: contain; }
|
|
.stage img.cover, .stage video.cover { object-fit: cover; }
|
|
.stage img.fill, .stage video.fill { object-fit: fill; }
|
|
|
|
/* Video wall mode: the stage is positioned (in vw/vh) as this screen's slice of the
|
|
wall's player rect; media stretches to fill (object-fit:fill) so a given source row
|
|
lands on the same physical line across every screen that shares a viewport height. */
|
|
.stage.wall-mode img, .stage.wall-mode video, .stage.wall-mode iframe { object-fit: fill; }
|
|
|
|
/* #109: PiP overlay layer. Sits above #stage and fills the same viewport box so a
|
|
child positioned to a corner lands in the corner of the visible content. app.js
|
|
applies the SAME orientation transform here as on #stage (portrait/flipped). It is
|
|
pointer-transparent and empty (invisible) until PipOverlay renders into it. */
|
|
#pip {
|
|
position: fixed; top: 0; left: 0;
|
|
width: 100vw; height: 100vh;
|
|
pointer-events: none;
|
|
z-index: 50;
|
|
}
|
|
#pip > div { box-shadow: 0 6px 28px rgba(0,0,0,0.55); }
|
|
|
|
/* Toast */
|
|
.toast {
|
|
position: absolute; bottom: 2.4rem; left: 50%; transform: translateX(-50%);
|
|
background: rgba(17,24,39,0.92); color: #f1f5f9;
|
|
padding: 1.2rem 2.4rem; border-radius: 1rem; font-size: 1.8rem;
|
|
border: 0.1rem solid #334155;
|
|
}
|