Files
rustdesk-server/web_client/src/ui/style.css
T
mike 4308a2f112 feat: M6 web client QoL — clipboard, multi-monitor, quality, reconnect, H.264, docs
Builds on the M6a-g MVP (d07e98e) with five user-facing features that
take the in-browser remote-control client from "demo toy" to "workable
for daily use", plus user-facing documentation.

M6h — Text clipboard sync (both directions)
- Host → browser via Clipboard{format: Text} → navigator.clipboard.writeText.
- Browser → host via Cmd-V/Ctrl-V keydown intercept → navigator.clipboard
  .readText → Message{clipboard}, sent BEFORE the V keystroke so the
  host's paste hotkey lands on the freshly-synced text.
- Handles both single-format Clipboard (older peers) and MultiClipboards
  wrapper (peers ≥ 1.3.0; gated by clipboard.rs:is_support_multi_clipboard).
- Switched printable-with-modifier hotkeys (Ctrl-C etc.) from Unicode to
  Chr payload — host's process_unicode does key_sequence(char) which
  ignores modifiers, breaking copy/cut; process_chr respects them.
- Firefox refuses navigator.clipboard.readText() by default — accepted as
  a known browser limitation, host → browser direction works regardless.

M6i — Multi-monitor switching
- HUD picker shown when peer_info.displays > 1.
- On change: SwitchDisplay + CaptureDisplays{set:[idx]} two-message dance
  — required for clients ≥ 1.2.4 (we send "1.4.0"). Without the follow-
  up, switch_display_to leaves both video services subscribed and
  switching display 0 → 1 → 0 doesn't restore display 0.
- Mouse coords offset by the active display's virtual-desktop origin
  (DisplayInfo.x, .y). Without this, clicks on display 2 landed on
  display 1 because both share canvas (0,0) but only display 1 has
  origin (0,0) in virtual-desktop space.

M6j — Quality / FPS / mute controls
- Image quality preset (Low/Balanced/Best) → Misc{option: {image_quality}}.
- Custom FPS (15/30/60) → Misc{option: {custom_fps}}; host caps at 30
  unless allow_more_fps is advertised.
- Mute toggle additionally sends Misc{option: {disable_audio: Yes/No}}
  so the peer stops encoding audio while muted (saves CPU + bandwidth).

M6k — Auto-reconnect on transient drops
- session.recv() throw → reconnect with exponential backoff: 1s, 2s, 4s,
  8s, 16s, 30s, 30s, capped at 30s, max 10 attempts.
- Dim overlay sits on top of the canvas during retry; canvas keeps
  last-known frame for visual continuity.
- Auth errors (password/signature) bail immediately — no point retrying.
- User options (mute, image_quality, custom_fps, current display)
  re-applied to host on each successful reconnect, since host treats
  every session as fresh and resets to defaults.
- Architecture: `session` is a let-binding mutated on reconnect; HUD
  button closures read it at click-time so they automatically retarget.
  Input modules (mouse/keyboard/clipboard) get a Proxy that forwards
  method calls to whatever session is current — avoids re-binding
  window/canvas listeners on each reconnect.

M6l — H.264 video decode (Annex-B + SPS-derived codec string)
- decode/bitstream.ts: iterate Annex-B NAL units, derive avc1.PPCCLL
  from the keyframe's inline SPS (host's hwcodec defaults to high
  profile; a hardcoded baseline string would make WebCodecs refuse the
  stream).
- Defer H.264 decoder configure until first keyframe arrives.
- VP9 codec string corrected from level 1.0 (vp09.00.10.08) to level
  5.0 (vp09.00.50.08) — wrong level was probably forcing software
  decode in some browsers.
- Default prefer flipped to VP8 (cheapest software encoder; H.264 path
  stays implemented for hosts with hwcodec/nvenc).

M6m — docs/CONFIGURATION.md "Web client" section: routes, browser
matrix, network requirements (relay reachability + reverse-proxy WS
upgrade), feature status table, codec selection rationale, the
recv/dec/draw HUD diagnostic, build commands.

Bundle: 535 KB / ~75 KB gzipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:43:23 +02:00

183 lines
3.3 KiB
CSS

/* RustDesk web client — minimal, dark theme to match the admin dashboard. */
html, body {
margin: 0;
padding: 0;
height: 100%;
background: #0f172a;
color: #e2e8f0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
#root {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.placeholder {
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 32px 40px;
max-width: 540px;
text-align: center;
}
.placeholder h1 {
margin: 0 0 16px;
font-size: 20px;
font-weight: 600;
}
.placeholder p {
margin: 8px 0;
font-size: 14px;
color: #cbd5e1;
}
.placeholder code {
background: #0f172a;
padding: 2px 6px;
border-radius: 3px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
color: #7dd3fc;
}
.muted { color: #64748b !important; font-size: 12px !important; }
.pw-form {
display: flex;
gap: 8px;
margin-top: 16px;
align-items: stretch;
}
.pw-form input[type="password"] {
flex: 1;
background: #0f172a;
border: 1px solid #334155;
color: #e2e8f0;
padding: 8px 10px;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
}
.pw-form input[type="password"]:focus {
outline: none;
border-color: #38bdf8;
}
.pw-form button {
background: #0284c7;
border: 0;
color: #f0f9ff;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.pw-form button:hover { background: #0369a1; }
.error-inline {
background: rgba(220, 38, 38, 0.15);
border: 1px solid rgba(220, 38, 38, 0.4);
color: #fca5a5;
padding: 8px 10px;
border-radius: 4px;
font-size: 13px;
margin-top: 12px;
}
/* ------- Live session ------- */
.session {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
background: #000;
}
.rd-canvas {
flex: 1;
width: 100%;
height: 100%;
/* Letterbox: keep aspect ratio while fitting the browser viewport. */
object-fit: contain;
display: block;
background: #000;
}
.hud {
position: fixed;
top: 8px;
left: 8px;
background: rgba(0, 0, 0, 0.55);
color: #cbd5e1;
font-size: 11px;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
padding: 4px 8px;
border-radius: 4px;
z-index: 10;
display: flex;
align-items: center;
gap: 8px;
}
.hud-fps {
pointer-events: none;
}
.hud-btn {
background: #334155;
border: 0;
color: #e2e8f0;
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-family: inherit;
cursor: pointer;
}
.hud-btn:hover { background: #475569; }
.hud-select {
background: #334155;
border: 0;
color: #e2e8f0;
padding: 2px 4px;
border-radius: 3px;
font-size: 11px;
font-family: inherit;
cursor: pointer;
}
.hud-select:hover { background: #475569; }
.reconnect-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(15, 23, 42, 0.65);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
color: #e2e8f0;
font-family: inherit;
font-size: 15px;
z-index: 20;
pointer-events: none;
}
.error {
background: rgba(220, 38, 38, 0.15);
border: 1px solid rgba(220, 38, 38, 0.4);
border-radius: 8px;
padding: 24px 32px;
color: #fca5a5;
max-width: 640px;
}
.error h1 { margin: 0 0 12px; font-size: 18px; }
.error pre { white-space: pre-wrap; font-size: 13px; }