mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 01:27:11 +00:00
feat(audit): align with RustDesk 1.4.8 and update language files
- Conducted an audit against upstream RustDesk tag 1.4.8, confirming no breaking changes to the wire format. - Implemented various fixes including client version string, supported encoding handshake, and multi-session handling. - Updated language files for Arabic, Czech, Danish, German, English, and Spanish to reflect new UI elements and messages. - Enhanced clipboard and file transfer functionalities to match upstream behavior.
This commit is contained in:
@@ -86,3 +86,32 @@ This does **not** replace the RustDesk relay-based pipeline — it provides a *f
|
||||
- `betterdesk-agent-client/src-tauri/src/bd_signal.rs` — agent signal/control channel
|
||||
- `betterdesk-agent-client/src-tauri/src/cdap_client.rs` — CDAP gateway client
|
||||
- Upstream RustDesk: <https://github.com/rustdesk/rustdesk>
|
||||
|
||||
## RustDesk 1.4.8 alignment (2026-07-06)
|
||||
|
||||
Audit against upstream tag **1.4.8** (June 2026). No breaking wire-format changes vs our `message.proto` / `rendezvous.proto` baseline — proto fields `supported_encoding`, `selected_sid`, `windows_sessions`, and `PeerInfo.encoding` were already present locally.
|
||||
|
||||
| RustDesk 1.4.8 item | RdClient status | Files | Notes |
|
||||
|---|---|---|---|
|
||||
| Client version string | **Fixed** — `BetterDesk-Web/1.4.8` | `protocol.js` | Peers see current baseline |
|
||||
| `Misc.supported_encoding` handshake | **Fixed** — advertise on session start; parse inbound | `protocol.js`, `client.js` | H265 ability now probed when WebCodecs available |
|
||||
| `windows_sessions` / `selected_sid` | **Fixed** — parse + Actions menu UI | `client.js`, `remote.js`, `lang/*.json` | Session picker when peer reports ≥2 sessions |
|
||||
| Monitor quick-switch toolbar (#15342) | **Fixed** — `.toolbar-monitor-strip` for 2–4 displays | `remote.ejs`, `remote.css`, `remote.js` | Dropdown kept for virtual displays / >4 monitors |
|
||||
| View-only clipboard disabled (#15224) | **Fixed** — guards on send + inbound apply + sync | `client.js`, `remote.js` | Matches upstream behaviour |
|
||||
| Pause key Windows (#15351) | **Fixed** — `Pause` → `0xE046`; Linux `Delete`≠`Pause` | `keyboard-scancode.js`, tests | |
|
||||
| Privacy mode multi-monitor (#15321) | **Partial** — toggle via `togglePrivacyMode` | `client.js` | Peer-side 1.4.8 refactor; no display param in web client |
|
||||
| Clipboard DIB alpha (#15296) | **Partial** — RGBA path via `ImageData` | `clipboard.js` | PNG inbound preferred; alpha preserved on canvas path |
|
||||
| Windows ARM64 host | n/a | — | Native peer feature, not rdclient |
|
||||
| Hole-punch / mDNS | n/a | — | Browser limitation |
|
||||
| Microphone → peer | **Missing** | `audio.js` | Phase 67 |
|
||||
| File transfer resume | **Missing** | `filetransfer.js` | Phase 67 |
|
||||
| Terminal PTY | **Missing** | — | bd-signal one-shot only |
|
||||
|
||||
### Verify with RustDesk 1.4.8 peer
|
||||
|
||||
1. Panel update → open `/remote/:id` — notch toolbar attached to tab bar; hover shows fullscreen + expand.
|
||||
2. Password overlay — remember checkbox aligned under password field.
|
||||
3. Multi-monitor peer — numbered strip in expanded notch; dropdown for virtual displays.
|
||||
4. Windows multi-session host — Actions → Windows sessions.
|
||||
5. View-only — clipboard paste and auto-sync blocked.
|
||||
6. HTTPS + WebCodecs — H265 negotiates when peer supports it.
|
||||
|
||||
+4222
-4221
File diff suppressed because it is too large
Load Diff
+4215
-4214
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4215
-4214
File diff suppressed because it is too large
Load Diff
+4219
-4218
File diff suppressed because it is too large
Load Diff
+4215
-4214
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4215
-4214
File diff suppressed because it is too large
Load Diff
+4215
-4214
File diff suppressed because it is too large
Load Diff
+4215
-4214
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4215
-4214
File diff suppressed because it is too large
Load Diff
+4217
-4216
File diff suppressed because it is too large
Load Diff
+4215
-4214
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4221
-4220
File diff suppressed because it is too large
Load Diff
+4217
-4216
File diff suppressed because it is too large
Load Diff
@@ -272,85 +272,176 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- Toolbar (floating RustDesk-style collapsible pill) ---- */
|
||||
.viewer-toolbar {
|
||||
position: fixed;
|
||||
top: 44px;
|
||||
.password-form-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.session-remember-peer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.session-remember-peer-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--accent-blue, #388bfd);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-remember-peer span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.password-form-footer .session-btn-authenticate {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* ---- Toolbar (notch integrated in session tab bar) ---- */
|
||||
.toolbar-notch-slot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
z-index: 230;
|
||||
pointer-events: none;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.viewer-toolbar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
z-index: 50;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Expanded action pill — only visible when the toolbar is expanded */
|
||||
/* Expanded action bar — hangs from tab bar as a longer notch */
|
||||
.toolbar-expanded {
|
||||
display: none;
|
||||
position: relative;
|
||||
/* Sit above the compact handle so dropdown menus (which extend downward
|
||||
into the handle's region) are not covered by it. */
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 3;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
max-width: calc(100vw - 24px);
|
||||
padding: 6px 10px;
|
||||
background: linear-gradient(180deg, rgba(22, 27, 34, 0.97) 0%, rgba(22, 27, 34, 0.92) 100%);
|
||||
gap: 6px;
|
||||
max-width: calc(100vw - 120px);
|
||||
padding: 5px 12px 8px;
|
||||
background: linear-gradient(180deg, var(--bg-secondary, #161b22) 0%, rgba(22, 27, 34, 0.97) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border-primary, #30363d);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
|
||||
/* overflow must stay visible so dropdown menus (top:100%) can escape the pill */
|
||||
border-top: none;
|
||||
border-radius: 0 0 14px 14px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.viewer-toolbar.expanded .toolbar-expanded {
|
||||
.viewer-toolbar.expanded .toolbar-expanded,
|
||||
.viewer-toolbar.pinned .toolbar-expanded {
|
||||
display: flex;
|
||||
animation: toolbar-pop 0.18s ease;
|
||||
animation: notch-expand 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes toolbar-pop {
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
@keyframes notch-expand {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(-4px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
/* Compact handle — always visible */
|
||||
/* Compact notch handle — droplet attached to tab bar bottom edge */
|
||||
.toolbar-handle {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 3px 5px;
|
||||
background: linear-gradient(180deg, rgba(22, 27, 34, 0.95) 0%, rgba(22, 27, 34, 0.88) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
min-width: 36px;
|
||||
height: 14px;
|
||||
padding: 0 4px 2px;
|
||||
background: var(--bg-secondary, #161b22);
|
||||
border: 1px solid var(--border-primary, #30363d);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4);
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s;
|
||||
border-top: none;
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
|
||||
opacity: 0.55;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.18s ease, min-width 0.2s ease, height 0.2s ease, padding 0.2s ease, gap 0.2s ease;
|
||||
}
|
||||
|
||||
.toolbar-handle:hover,
|
||||
.toolbar-handle .handle-btn {
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
transition: width 0.18s ease, min-width 0.18s ease, height 0.18s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.viewer-toolbar.hover-preview .toolbar-handle,
|
||||
.viewer-toolbar.expanded .toolbar-handle,
|
||||
.viewer-toolbar.dragging .toolbar-handle {
|
||||
.viewer-toolbar.pinned .toolbar-handle,
|
||||
.toolbar-handle:hover,
|
||||
.toolbar-handle:focus-within {
|
||||
opacity: 1;
|
||||
min-width: 72px;
|
||||
height: 26px;
|
||||
padding: 2px 6px 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.viewer-toolbar.hover-preview .toolbar-handle .handle-btn,
|
||||
.viewer-toolbar.expanded .toolbar-handle .handle-btn,
|
||||
.viewer-toolbar.pinned .toolbar-handle .handle-btn,
|
||||
.toolbar-handle:hover .handle-btn,
|
||||
.toolbar-handle:focus-within .handle-btn {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
height: 22px;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.viewer-toolbar.expanded .toolbar-handle,
|
||||
.viewer-toolbar.pinned .toolbar-handle {
|
||||
border-radius: 0;
|
||||
border-bottom: none;
|
||||
box-shadow: none;
|
||||
min-width: 48px;
|
||||
height: 4px;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.viewer-toolbar.expanded .toolbar-handle .handle-btn,
|
||||
.viewer-toolbar.pinned .toolbar-handle .handle-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.handle-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
cursor: pointer;
|
||||
@@ -363,15 +454,7 @@
|
||||
}
|
||||
|
||||
.handle-btn .material-icons {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.handle-drag {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.viewer-toolbar.dragging .handle-drag {
|
||||
cursor: grabbing;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.handle-expand.active {
|
||||
@@ -379,15 +462,52 @@
|
||||
}
|
||||
|
||||
.toolbar-left,
|
||||
.toolbar-center,
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-center {
|
||||
.toolbar-monitor-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 4px;
|
||||
border-left: 1px solid var(--border-primary, #30363d);
|
||||
border-right: 1px solid var(--border-primary, #30363d);
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.toolbar-monitor-strip[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.toolbar-monitor-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
padding: 0 6px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.toolbar-monitor-btn:hover {
|
||||
background: var(--bg-tertiary, #21262d);
|
||||
color: var(--text-primary, #e6edf3);
|
||||
}
|
||||
|
||||
.toolbar-monitor-btn.active {
|
||||
background: rgba(88, 166, 255, 0.15);
|
||||
color: var(--accent-blue, #58a6ff);
|
||||
}
|
||||
|
||||
/* Toolbar button */
|
||||
@@ -444,13 +564,6 @@
|
||||
color: var(--text-secondary, #8b949e);
|
||||
}
|
||||
|
||||
/* Stats in toolbar center */
|
||||
.toolbar-stats {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #8b949e);
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
/* ---- Toolbar Dropdowns ---- */
|
||||
.toolbar-dropdown {
|
||||
position: relative;
|
||||
@@ -613,10 +726,6 @@
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Pinned toolbar — keeps the expanded pill open (no auto-collapse) */
|
||||
.viewer-toolbar.pinned .toolbar-expanded {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ---- Chat Panel ---- */
|
||||
.chat-panel {
|
||||
@@ -1354,18 +1463,10 @@
|
||||
|
||||
/* ---- Responsive ---- */
|
||||
@media (max-width: 768px) {
|
||||
.toolbar-center {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar-device-info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.viewer-toolbar {
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.toolbar-expanded {
|
||||
max-width: calc(100vw - 16px);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ class RDClient {
|
||||
|
||||
// Codec / quality control
|
||||
this._codecAbilities = null; // probed VideoDecoder support map
|
||||
this._peerEncoding = null; // peer SupportedEncoding from handshake
|
||||
this._windowsSessions = { sessions: [], currentSid: 0 };
|
||||
this._preferCodec = opts.preferCodec || 'Auto';
|
||||
this._adaptivePaused = false; // true once the user picks codec/quality manually
|
||||
this._codecFallbackDone = false; // one automatic downgrade per session
|
||||
@@ -1064,6 +1066,7 @@ class RDClient {
|
||||
}
|
||||
|
||||
async _applyRemoteClipboard(clipboards) {
|
||||
if (this._viewOnly) return;
|
||||
const list = clipboards || [];
|
||||
if (!list.length) return;
|
||||
|
||||
@@ -1137,6 +1140,11 @@ class RDClient {
|
||||
this._emit('switch_display', misc.switchDisplay);
|
||||
return;
|
||||
}
|
||||
if (misc.supportedEncoding || misc.supported_encoding) {
|
||||
this._peerEncoding = misc.supportedEncoding || misc.supported_encoding;
|
||||
this._emit('peer_encoding', this._peerEncoding);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Session Management ----
|
||||
@@ -1208,6 +1216,8 @@ class RDClient {
|
||||
// immediately even if we joined an already-running stream on a delta.
|
||||
this._sendPeerMessage(this.proto.buildMisc('refreshVideo', true));
|
||||
|
||||
this._advertiseSupportedEncoding();
|
||||
|
||||
// Start ping interval
|
||||
this._pingInterval = setInterval(() => {
|
||||
if (this._state === 'streaming') {
|
||||
@@ -1445,6 +1455,7 @@ class RDClient {
|
||||
*/
|
||||
sendClipboard(text) {
|
||||
if (this._state !== 'streaming') return;
|
||||
if (this._viewOnly) return;
|
||||
void this._sendClipboard(text);
|
||||
}
|
||||
|
||||
@@ -1663,11 +1674,64 @@ class RDClient {
|
||||
this._currentDisplay = 0;
|
||||
}
|
||||
this._virtualDisplay = this._parseVirtualDisplaySupport(info);
|
||||
if (info.encoding) {
|
||||
this._peerEncoding = info.encoding;
|
||||
}
|
||||
this._windowsSessions = this._parseWindowsSessions(info);
|
||||
if (this.input && info.platform) {
|
||||
this.input.setPeerPlatform(info.platform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} info
|
||||
* @returns {{sessions: Array<{sid:number,name:string}>, currentSid: number}}
|
||||
*/
|
||||
_parseWindowsSessions(info) {
|
||||
const raw = info.windowsSessions || info.windows_sessions;
|
||||
if (!raw) return { sessions: [], currentSid: 0 };
|
||||
const list = Array.isArray(raw.sessions) ? raw.sessions : [];
|
||||
const sessions = list.map(function (s) {
|
||||
return {
|
||||
sid: s.sid,
|
||||
name: s.name || ('Session ' + s.sid)
|
||||
};
|
||||
});
|
||||
const currentSid = raw.currentSid != null ? raw.currentSid : (raw.current_sid || 0);
|
||||
return { sessions: sessions, currentSid: currentSid };
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows session list reported by the peer (multi-session hosts).
|
||||
* @returns {{sessions: Array<{sid:number,name:string}>, currentSid: number}}
|
||||
*/
|
||||
getWindowsSessions() {
|
||||
return this._windowsSessions || { sessions: [], currentSid: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the controlled Windows session (RustDesk selected_sid).
|
||||
* @param {number} sid
|
||||
*/
|
||||
selectWindowsSession(sid) {
|
||||
if (this._state !== 'streaming') return;
|
||||
const n = Number(sid);
|
||||
if (!Number.isFinite(n) || n < 0) return;
|
||||
this._sendPeerMessage(this.proto.buildMisc('selectedSid', n));
|
||||
if (this._windowsSessions) this._windowsSessions.currentSid = n;
|
||||
this._emit('windows_session_selected', n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advertise operator decode capabilities to the peer (Misc.supported_encoding).
|
||||
* @private
|
||||
*/
|
||||
_advertiseSupportedEncoding() {
|
||||
if (!this.proto || typeof this.proto.buildSupportedEncoding !== 'function') return;
|
||||
const enc = this.proto.buildSupportedEncoding(this._codecAbilities);
|
||||
this._sendPeerMessage(this.proto.buildMisc('supportedEncoding', enc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Release stuck remote modifiers / keys (toolbar recovery).
|
||||
*/
|
||||
|
||||
@@ -81,7 +81,7 @@ const CODE_TO_LINUX_SCAN = {
|
||||
Home: 110, ArrowUp: 111, PageUp: 112,
|
||||
ArrowLeft: 113, ArrowRight: 114,
|
||||
End: 115, ArrowDown: 116, PageDown: 117,
|
||||
Insert: 118, Delete: 119,
|
||||
Insert: 118, Delete: 111,
|
||||
MetaLeft: 133, MetaRight: 134, ContextMenu: 135,
|
||||
NumpadEnter: 104, NumpadDivide: 106, PrintScreen: 107, Pause: 119,
|
||||
};
|
||||
|
||||
@@ -153,7 +153,8 @@ class RDProtocol {
|
||||
return this.types.Message.decode(protoBytes);
|
||||
}
|
||||
|
||||
// ---- Protocol message builders ----
|
||||
// Client version advertised to RustDesk peers (keep in sync with product baseline)
|
||||
static CLIENT_VERSION = 'BetterDesk-Web/1.4.8';
|
||||
|
||||
/**
|
||||
* Build PunchHoleRequest for connecting to a device
|
||||
@@ -169,7 +170,7 @@ class RDProtocol {
|
||||
licenceKey: serverKey || '',
|
||||
connType: ct,
|
||||
token: '',
|
||||
version: 'BetterDesk-Web/1.3.9',
|
||||
version: RDProtocol.CLIENT_VERSION,
|
||||
forceRelay: true // Browser must use relay
|
||||
}
|
||||
};
|
||||
@@ -238,7 +239,7 @@ class RDProtocol {
|
||||
myId: opts.myId || 'web-client-ft',
|
||||
myName: opts.myName || 'BetterDesk Web',
|
||||
myPlatform: 'Web',
|
||||
version: 'BetterDesk-Web/1.3.9',
|
||||
version: RDProtocol.CLIENT_VERSION,
|
||||
sessionId: Date.now(),
|
||||
fileTransfer: {
|
||||
dir: opts.dir != null ? opts.dir : '',
|
||||
@@ -263,7 +264,7 @@ class RDProtocol {
|
||||
myId: opts.myId || 'web-client',
|
||||
myName: opts.myName || 'BetterDesk Web',
|
||||
myPlatform: 'Web',
|
||||
version: 'BetterDesk-Web/1.3.9',
|
||||
version: RDProtocol.CLIENT_VERSION,
|
||||
sessionId: Date.now(),
|
||||
option: {
|
||||
imageQuality: this.enums.ImageQuality.values[quality] || this.enums.ImageQuality.values.Best,
|
||||
@@ -312,13 +313,38 @@ class RDProtocol {
|
||||
return {
|
||||
abilityVp9: can('vp9', hasWebCodecs),
|
||||
abilityH264: can('h264', hasWebCodecs || hasJMuxer),
|
||||
abilityH265: can('h265', false),
|
||||
abilityH265: can('h265', hasWebCodecs),
|
||||
abilityAv1: can('av1', hasWebCodecs),
|
||||
abilityVp8: can('vp8', hasWebCodecs),
|
||||
prefer: this.preferCodecValue(o.prefer || (hasWebCodecs ? 'Auto' : 'H264'))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SupportedEncoding for Misc.supported_encoding (RustDesk 1.4.x negotiation).
|
||||
* @param {Object} [abilities] - probed { h264, h265, vp8, av1 } booleans
|
||||
* @returns {Object}
|
||||
*/
|
||||
buildSupportedEncoding(abilities) {
|
||||
const a = abilities || {};
|
||||
const has = (key, fallback) => (a[key] != null ? !!a[key] : !!fallback);
|
||||
const hasWebCodecs = typeof VideoDecoder !== 'undefined';
|
||||
const hasJMuxer = typeof JMuxer !== 'undefined';
|
||||
return {
|
||||
h264: has('h264', hasWebCodecs || hasJMuxer),
|
||||
h265: has('h265', hasWebCodecs),
|
||||
vp8: has('vp8', hasWebCodecs),
|
||||
av1: has('av1', hasWebCodecs),
|
||||
i444: {
|
||||
vp8: false,
|
||||
vp9: false,
|
||||
av1: false,
|
||||
h264: false,
|
||||
h265: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build MouseEvent message
|
||||
*/
|
||||
|
||||
+115
-93
@@ -177,17 +177,18 @@
|
||||
const viewerContainer = document.getElementById('viewer-container');
|
||||
const viewerShell = document.getElementById('rd-viewer-shell');
|
||||
const toolbar = document.getElementById('viewer-toolbar');
|
||||
const toolbarHandle = document.getElementById('toolbar-handle');
|
||||
const toolbarStatus = document.getElementById('toolbar-status');
|
||||
const toolbarStats = document.getElementById('toolbar-stats');
|
||||
const toolbarDeviceId = document.getElementById('toolbar-device-id');
|
||||
const tabBar = document.getElementById('session-tabs');
|
||||
|
||||
// ---- Floating toolbar (collapsible RustDesk-style pill) ----
|
||||
// The compact handle is always visible; the action pill expands only on an
|
||||
// explicit click of the expand button. There is NO hover-to-open behaviour.
|
||||
// ---- Notch toolbar (integrated in session tab bar) ----
|
||||
// Compact droplet is always visible; hover reveals handle icons; expand
|
||||
// button opens the full action bar as a longer notch.
|
||||
let toolbarPinned = false;
|
||||
|
||||
function expandToolbar() {
|
||||
toolbar.classList.remove('hover-preview');
|
||||
toolbar.classList.add('expanded');
|
||||
const exp = document.getElementById('btn-toolbar-expand');
|
||||
if (exp) {
|
||||
@@ -201,6 +202,7 @@
|
||||
if (toolbarPinned) return;
|
||||
if (document.querySelector('.toolbar-dropdown-menu.open')) return;
|
||||
toolbar.classList.remove('expanded');
|
||||
toolbar.classList.remove('hover-preview');
|
||||
const exp = document.getElementById('btn-toolbar-expand');
|
||||
if (exp) {
|
||||
exp.classList.remove('active');
|
||||
@@ -211,8 +213,9 @@
|
||||
|
||||
function toggleToolbar() {
|
||||
if (toolbar.classList.contains('expanded')) {
|
||||
// Force-collapse (ignore pin) when the user explicitly clicks.
|
||||
toolbar.classList.remove('expanded');
|
||||
toolbarPinned = false;
|
||||
document.getElementById('btn-pin')?.classList.remove('active');
|
||||
toolbar.classList.remove('expanded', 'hover-preview');
|
||||
const exp = document.getElementById('btn-toolbar-expand');
|
||||
if (exp) {
|
||||
exp.classList.remove('active');
|
||||
@@ -224,6 +227,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (toolbarHandle && toolbar) {
|
||||
toolbarHandle.addEventListener('mouseenter', () => {
|
||||
if (!toolbar.classList.contains('expanded') && !toolbarPinned) {
|
||||
toolbar.classList.add('hover-preview');
|
||||
}
|
||||
});
|
||||
toolbarHandle.addEventListener('mouseleave', () => {
|
||||
if (!toolbar.classList.contains('expanded')) {
|
||||
toolbar.classList.remove('hover-preview');
|
||||
}
|
||||
});
|
||||
toolbarHandle.addEventListener('focusin', () => {
|
||||
if (!toolbar.classList.contains('expanded') && !toolbarPinned) {
|
||||
toolbar.classList.add('hover-preview');
|
||||
}
|
||||
});
|
||||
toolbarHandle.addEventListener('focusout', () => {
|
||||
if (!toolbar.classList.contains('expanded')) {
|
||||
toolbar.classList.remove('hover-preview');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy compatibility shims — older code paths call these to surface the
|
||||
// status while overlays are visible. They now drive expand/collapse only,
|
||||
// never an auto-hide timer.
|
||||
@@ -289,6 +315,7 @@
|
||||
async function syncLocalClipboardToRemote() {
|
||||
const session = getActiveSession();
|
||||
if (!session || !session.client || session.state !== 'streaming') return;
|
||||
if (session.client.viewOnly) return;
|
||||
if (!navigator.clipboard || !navigator.clipboard.readText) return;
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
@@ -719,7 +746,6 @@
|
||||
|
||||
c.on('stats', (stats) => {
|
||||
session.lastStats = stats;
|
||||
if (isActive(session)) updateStats(stats, session.latency);
|
||||
});
|
||||
|
||||
c.on('latency', (rtt) => { session.latency = rtt; });
|
||||
@@ -742,6 +768,7 @@
|
||||
c.on('peer_info', () => {
|
||||
if (isActive(session)) {
|
||||
try { refreshMonitorButton(session); } catch { /* not ready */ }
|
||||
try { updateWindowsSessionMenu(session); } catch { /* not ready */ }
|
||||
}
|
||||
});
|
||||
c.on('display_switched', () => {
|
||||
@@ -929,12 +956,6 @@
|
||||
}
|
||||
document.querySelector('.toolbar-status-sep')?.style.setProperty('display', isStreaming ? 'none' : '');
|
||||
|
||||
if (session.lastStats) {
|
||||
updateStats(session.lastStats, session.latency);
|
||||
} else {
|
||||
toolbarStats.textContent = '';
|
||||
}
|
||||
|
||||
// Audio icon
|
||||
const audioBtn = document.getElementById('btn-audio');
|
||||
if (audioBtn) {
|
||||
@@ -967,25 +988,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Stats display ----
|
||||
|
||||
function updateStats(stats, latency) {
|
||||
if (!stats) return;
|
||||
const parts = [];
|
||||
if (stats.video) {
|
||||
const fps = stats.video.videoFps || 0;
|
||||
parts.push(fps + ' FPS');
|
||||
}
|
||||
if (stats.video && stats.video.displayWidth && stats.video.displayHeight) {
|
||||
parts.push(stats.video.displayWidth + 'x' + stats.video.displayHeight);
|
||||
} else if (stats.renderer && stats.renderer.remoteWidth && stats.renderer.remoteHeight) {
|
||||
parts.push(stats.renderer.remoteWidth + 'x' + stats.renderer.remoteHeight);
|
||||
}
|
||||
if (latency > 0) parts.push(latency + 'ms');
|
||||
if (stats.video && stats.video.codec) parts.push(stats.video.codec.toUpperCase());
|
||||
toolbarStats.textContent = parts.join(' | ');
|
||||
}
|
||||
|
||||
// ---- Autoplay blocked overlay ----
|
||||
|
||||
function showAutoplayOverlay(session) {
|
||||
@@ -1076,11 +1078,6 @@
|
||||
withClient(c => c.disconnect());
|
||||
});
|
||||
|
||||
// Fullscreen
|
||||
document.getElementById('btn-fullscreen')?.addEventListener('click', () => {
|
||||
toggleViewerShellFullscreen();
|
||||
});
|
||||
|
||||
// Audio toggle
|
||||
document.getElementById('btn-audio')?.addEventListener('click', function () {
|
||||
const session = getActiveSession();
|
||||
@@ -1134,6 +1131,11 @@
|
||||
document.getElementById('btn-clipboard-paste')?.addEventListener('click', async () => {
|
||||
const session = getActiveSession();
|
||||
if (!session || !session.client) return;
|
||||
if (session.client.viewOnly) {
|
||||
showToast(_('remote.view_only') || 'View only mode', 'warning');
|
||||
closeAllDropdowns();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) {
|
||||
@@ -1260,9 +1262,81 @@
|
||||
? session.client.getVirtualDisplaySupport()
|
||||
: { supported: false };
|
||||
} catch { vd = { supported: false }; }
|
||||
const visible = (monitors.length > 1) || (vd && vd.supported);
|
||||
btn.style.display = visible ? '' : 'none';
|
||||
if (visible) updateMonitorMenu();
|
||||
const useStrip = monitors.length >= 2 && monitors.length <= 4;
|
||||
const showDropdown = (monitors.length > 1 && !useStrip) || (vd && vd.supported);
|
||||
btn.style.display = showDropdown ? '' : 'none';
|
||||
updateMonitorStrip(session, monitors, useStrip);
|
||||
if (showDropdown) updateMonitorMenu();
|
||||
}
|
||||
|
||||
function updateMonitorStrip(session, monitors, useStrip) {
|
||||
const strip = document.getElementById('toolbar-monitor-strip');
|
||||
if (!strip) return;
|
||||
strip.innerHTML = '';
|
||||
if (!useStrip || !session || !session.client || monitors.length < 2) {
|
||||
strip.hidden = true;
|
||||
return;
|
||||
}
|
||||
strip.hidden = false;
|
||||
monitors.forEach(function (m, i) {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'toolbar-monitor-btn' + (m.current ? ' active' : '');
|
||||
item.title = m.name || ('Monitor ' + (i + 1));
|
||||
item.textContent = String(i + 1);
|
||||
item.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
session.client.switchMonitor(m.idx);
|
||||
strip.querySelectorAll('.toolbar-monitor-btn').forEach(function (b) {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
item.classList.add('active');
|
||||
updateMonitorMenu();
|
||||
});
|
||||
strip.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function updateWindowsSessionMenu(session) {
|
||||
session = session || getActiveSession();
|
||||
const menu = document.getElementById('actions-menu');
|
||||
if (!menu || !session || !session.client) return;
|
||||
menu.querySelectorAll('.windows-session-item, .windows-session-sep').forEach(function (el) {
|
||||
el.remove();
|
||||
});
|
||||
let ws = { sessions: [], currentSid: 0 };
|
||||
try {
|
||||
ws = (typeof session.client.getWindowsSessions === 'function')
|
||||
? session.client.getWindowsSessions()
|
||||
: { sessions: [], currentSid: 0 };
|
||||
} catch { ws = { sessions: [], currentSid: 0 }; }
|
||||
if (!ws.sessions || ws.sessions.length < 2) return;
|
||||
|
||||
const sep = document.createElement('div');
|
||||
sep.className = 'dropdown-separator windows-session-sep';
|
||||
menu.appendChild(sep);
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'dropdown-label windows-session-sep';
|
||||
label.textContent = t('remote.windows_sessions', 'Windows sessions');
|
||||
menu.appendChild(label);
|
||||
|
||||
ws.sessions.forEach(function (s) {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'dropdown-item windows-session-item' +
|
||||
(s.sid === ws.currentSid ? ' active' : '');
|
||||
item.innerHTML = '<span class="material-icons">desktop_windows</span> ' + escapeHtml(s.name);
|
||||
item.addEventListener('click', function () {
|
||||
session.client.selectWindowsSession(s.sid);
|
||||
menu.querySelectorAll('.windows-session-item').forEach(function (b) {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
item.classList.add('active');
|
||||
closeAllDropdowns();
|
||||
});
|
||||
menu.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function updateMonitorMenu() {
|
||||
@@ -1303,6 +1377,7 @@
|
||||
session.client.switchMonitor(m.idx);
|
||||
menu.querySelectorAll('.monitor-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
refreshMonitorButton(session);
|
||||
});
|
||||
menu.appendChild(item);
|
||||
});
|
||||
@@ -1478,57 +1553,6 @@
|
||||
toggleViewerShellFullscreen();
|
||||
});
|
||||
|
||||
// ---- Compact handle: drag the floating toolbar horizontally ----
|
||||
(function setupToolbarDrag() {
|
||||
const dragBtn = document.getElementById('btn-toolbar-drag');
|
||||
if (!dragBtn) return;
|
||||
let dragging = false;
|
||||
let startX = 0;
|
||||
let startLeft = 0;
|
||||
|
||||
function clampLeft(px) {
|
||||
const w = toolbar.offsetWidth || 200;
|
||||
const min = 8 + w / 2;
|
||||
const max = window.innerWidth - 8 - w / 2;
|
||||
return Math.max(min, Math.min(max, px));
|
||||
}
|
||||
|
||||
function onMove(ev) {
|
||||
if (!dragging) return;
|
||||
ev.preventDefault();
|
||||
const clientX = ev.touches ? ev.touches[0].clientX : ev.clientX;
|
||||
const next = clampLeft(startLeft + (clientX - startX));
|
||||
toolbar.style.left = next + 'px';
|
||||
toolbar.style.transform = 'translateX(-50%)';
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
toolbar.classList.remove('dragging');
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', onUp);
|
||||
}
|
||||
|
||||
function onDown(ev) {
|
||||
ev.preventDefault();
|
||||
dragging = true;
|
||||
toolbar.classList.add('dragging');
|
||||
const rect = toolbar.getBoundingClientRect();
|
||||
startLeft = rect.left + rect.width / 2;
|
||||
startX = ev.touches ? ev.touches[0].clientX : ev.clientX;
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
document.addEventListener('touchmove', onMove, { passive: false });
|
||||
document.addEventListener('touchend', onUp);
|
||||
}
|
||||
|
||||
dragBtn.addEventListener('mousedown', onDown);
|
||||
dragBtn.addEventListener('touchstart', onDown, { passive: false });
|
||||
})();
|
||||
|
||||
// ---- Back to devices — close all sessions first ----
|
||||
document.getElementById('btn-back-devices')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
@@ -1566,8 +1590,6 @@
|
||||
// Fullscreen handler
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
const fsIcon = document.fullscreenElement ? 'fullscreen_exit' : 'fullscreen';
|
||||
const icon = document.getElementById('btn-fullscreen')?.querySelector('.material-icons');
|
||||
if (icon) icon.textContent = fsIcon;
|
||||
const handleIcon = document.getElementById('btn-handle-fullscreen')?.querySelector('.material-icons');
|
||||
if (handleIcon) handleIcon.textContent = fsIcon;
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -597,4 +597,24 @@ describe('RDInput keyboard parity (RustDesk contract)', () => {
|
||||
expect(sent[0].keyEvent.modifiers).toContain(3);
|
||||
expect(sent[0].keyEvent.modifiers).not.toContain(29);
|
||||
});
|
||||
|
||||
it('maps Pause to Windows extended scancode 0xE046', () => {
|
||||
const sent = [];
|
||||
const input = makeInput((msg) => sent.push(msg));
|
||||
input.setPeerPlatform('Windows');
|
||||
input.setKeyboardMode('Map');
|
||||
input.start();
|
||||
|
||||
input._handleKeyDown(keyEvt({ code: 'Pause', key: 'Pause' }));
|
||||
|
||||
expect(sent[0].keyEvent.mode).toBe('Map');
|
||||
expect(sent[0].keyEvent.chr).toBe(0xE046);
|
||||
});
|
||||
|
||||
it('maps Delete and Pause to distinct Linux scancodes', () => {
|
||||
const sc = loadBrowserScripts(['public/js/rdclient/keyboard-scancode.js']);
|
||||
const codeToScancode = sc.RDKeyboardScancode.codeToScancode;
|
||||
expect(codeToScancode('Delete', 'Linux')).toBe(111);
|
||||
expect(codeToScancode('Pause', 'Linux')).toBe(119);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -284,3 +284,42 @@ describe('RDClient KeyEvent protobuf encoding', () => {
|
||||
expect(decoded.keyEvent.controlKey).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RDClient SupportedEncoding protobuf encoding', () => {
|
||||
let Message;
|
||||
let SupportedEncoding;
|
||||
|
||||
beforeAll(async () => {
|
||||
const root = await protobuf.load([
|
||||
path.join(__dirname, '../protos/message.proto')
|
||||
]);
|
||||
Message = root.lookupType('hbb.Message');
|
||||
SupportedEncoding = root.lookupType('hbb.SupportedEncoding');
|
||||
});
|
||||
|
||||
it('encodes Misc.supported_encoding with codec flags', () => {
|
||||
const enc = SupportedEncoding.fromObject({
|
||||
h264: true,
|
||||
h265: true,
|
||||
vp8: false,
|
||||
av1: true,
|
||||
i444: { h264: false, h265: false, vp8: false, vp9: false, av1: false }
|
||||
});
|
||||
const buf = Message.encode(Message.fromObject({
|
||||
misc: { supportedEncoding: enc }
|
||||
})).finish();
|
||||
expect(buf.length).toBeGreaterThan(0);
|
||||
const decoded = Message.decode(buf).toJSON();
|
||||
expect(decoded.misc.supportedEncoding.h264).toBe(true);
|
||||
expect(decoded.misc.supportedEncoding.h265).toBe(true);
|
||||
expect(decoded.misc.supportedEncoding.av1).toBe(true);
|
||||
});
|
||||
|
||||
it('encodes Misc.selected_sid for Windows session switch', () => {
|
||||
const buf = Message.encode(Message.fromObject({
|
||||
misc: { selectedSid: 2 }
|
||||
})).finish();
|
||||
const decoded = Message.decode(buf).toJSON();
|
||||
expect(decoded.misc.selectedSid).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
+168
-197
@@ -27,6 +27,164 @@
|
||||
<span class="material-icons">arrow_back</span>
|
||||
</button>
|
||||
<div class="session-tabs" id="session-tabs"></div>
|
||||
<div class="toolbar-notch-slot" id="toolbar-notch-slot">
|
||||
<div class="viewer-toolbar" id="viewer-toolbar">
|
||||
<div class="toolbar-expanded" id="toolbar-expanded">
|
||||
<div class="toolbar-left">
|
||||
<span class="toolbar-device-info">
|
||||
<span class="toolbar-device-id" id="toolbar-device-id"></span>
|
||||
<span class="toolbar-separator toolbar-status-sep">|</span>
|
||||
<span class="toolbar-status" id="toolbar-status">${_('remote.connecting')}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="toolbar-monitor-strip" id="toolbar-monitor-strip" hidden></div>
|
||||
<div class="toolbar-right">
|
||||
<div class="toolbar-dropdown">
|
||||
<button class="toolbar-btn" id="btn-actions" title="${_('remote.actions')}">
|
||||
<span class="material-icons">flash_on</span>
|
||||
</button>
|
||||
<div class="toolbar-dropdown-menu" id="actions-menu">
|
||||
<button class="dropdown-item" id="btn-cad">
|
||||
<span class="material-icons">security</span> Ctrl+Alt+Del
|
||||
</button>
|
||||
<button class="dropdown-item" id="btn-lock">
|
||||
<span class="material-icons">lock</span> ${_('remote.lock_screen')}
|
||||
</button>
|
||||
<button class="dropdown-item" id="btn-restart-remote">
|
||||
<span class="material-icons">restart_alt</span> ${_('remote.restart_remote')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<button class="dropdown-item" id="btn-refresh-screen">
|
||||
<span class="material-icons">refresh</span> ${_('remote.refresh_screen')}
|
||||
</button>
|
||||
<button class="dropdown-item" id="btn-clipboard-paste">
|
||||
<span class="material-icons">content_paste</span> ${_('remote.clipboard_paste')}
|
||||
</button>
|
||||
<button class="dropdown-item" id="btn-reset-keyboard">
|
||||
<span class="material-icons">keyboard</span> ${_('remote.reset_keyboard')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<button class="dropdown-item toggle-item" id="btn-block-input" data-active="false">
|
||||
<span class="material-icons">block</span> ${_('remote.block_input')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-dropdown">
|
||||
<button class="toolbar-btn" id="btn-display" title="${_('remote.display_settings')}">
|
||||
<span class="material-icons">tune</span>
|
||||
</button>
|
||||
<div class="toolbar-dropdown-menu toolbar-dropdown-wide" id="display-menu">
|
||||
<div class="dropdown-label">${_('remote.image_quality')}</div>
|
||||
<button class="dropdown-item quality-item active" data-quality="Best">
|
||||
<span class="material-icons">hd</span> ${_('remote.quality_best')}
|
||||
</button>
|
||||
<button class="dropdown-item quality-item" data-quality="Balanced">
|
||||
<span class="material-icons">tune</span> ${_('remote.quality_balanced')}
|
||||
</button>
|
||||
<button class="dropdown-item quality-item" data-quality="Low">
|
||||
<span class="material-icons">speed</span> ${_('remote.quality_low')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<div class="dropdown-label">${_('remote.codec')}</div>
|
||||
<button class="dropdown-item codec-item active" data-codec="Auto">
|
||||
<span class="material-icons">auto_awesome</span> ${_('remote.codec_auto')}
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="VP9">
|
||||
<span class="material-icons">memory</span> VP9
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="AV1">
|
||||
<span class="material-icons">memory</span> AV1
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="H264">
|
||||
<span class="material-icons">memory</span> H264
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="VP8">
|
||||
<span class="material-icons">memory</span> VP8
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="H265">
|
||||
<span class="material-icons">memory</span> H265
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<div class="dropdown-label">${_('remote.keyboard_mode')}</div>
|
||||
<button class="dropdown-item keyboard-mode-item active" data-keyboard-mode="Auto">
|
||||
<span class="material-icons">auto_awesome</span> ${_('remote.keyboard_mode_auto')}
|
||||
</button>
|
||||
<button class="dropdown-item keyboard-mode-item" data-keyboard-mode="Legacy">
|
||||
<span class="material-icons">translate</span> ${_('remote.keyboard_mode_legacy')}
|
||||
</button>
|
||||
<button class="dropdown-item keyboard-mode-item" data-keyboard-mode="Map">
|
||||
<span class="material-icons">keyboard</span> ${_('remote.keyboard_mode_map')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<div class="dropdown-label">${_('remote.scale_mode')}</div>
|
||||
<button class="dropdown-item scale-item active" data-scale="fit">
|
||||
<span class="material-icons">fit_screen</span> ${_('remote.scale_fit')}
|
||||
</button>
|
||||
<button class="dropdown-item scale-item" data-scale="fill">
|
||||
<span class="material-icons">crop_free</span> ${_('remote.scale_fill')}
|
||||
</button>
|
||||
<button class="dropdown-item scale-item" data-scale="1:1">
|
||||
<span class="material-icons">filter_1</span> ${_('remote.scale_original')}
|
||||
</button>
|
||||
<button class="dropdown-item scale-item" data-scale="stretch">
|
||||
<span class="material-icons">open_in_full</span> ${_('remote.scale_stretch')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<button class="dropdown-item toggle-item" id="btn-show-cursor" data-active="true">
|
||||
<span class="material-icons">mouse</span> ${_('remote.show_cursor')}
|
||||
</button>
|
||||
<button class="dropdown-item toggle-item" id="btn-lock-session" data-active="false">
|
||||
<span class="material-icons">lock_clock</span> ${_('remote.lock_after_session')}
|
||||
</button>
|
||||
<button class="dropdown-item toggle-item" id="btn-privacy-mode" data-active="false">
|
||||
<span class="material-icons">visibility_off</span> ${_('remote.privacy_mode')}
|
||||
</button>
|
||||
<button class="dropdown-item toggle-item" id="btn-disable-clipboard" data-active="false">
|
||||
<span class="material-icons">content_paste_off</span> ${_('remote.disable_clipboard')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="toolbar-btn" id="btn-audio" title="${_('remote.toggle_audio')}">
|
||||
<span class="material-icons">volume_up</span>
|
||||
</button>
|
||||
<div class="toolbar-dropdown">
|
||||
<button class="toolbar-btn" id="btn-monitors" title="${_('remote.monitors') || 'Monitors'}" style="display:none">
|
||||
<span class="material-icons">desktop_windows</span>
|
||||
</button>
|
||||
<div class="toolbar-dropdown-menu" id="monitors-menu">
|
||||
<div class="dropdown-label">${_('remote.select_monitor') || 'Select Monitor'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="toolbar-btn" id="btn-chat" title="${_('remote.chat')}">
|
||||
<span class="material-icons">chat</span>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="btn-file-transfer" title="${_('remote.file_transfer') || 'File Transfer'}">
|
||||
<span class="material-icons">folder_open</span>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="btn-record" title="${_('remote.record')}">
|
||||
<span class="material-icons">fiber_manual_record</span>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="btn-viewonly" title="${_('remote.view_only')}">
|
||||
<span class="material-icons">visibility</span>
|
||||
</button>
|
||||
<button class="toolbar-btn" id="btn-pin" title="${_('remote.pin_toolbar')}">
|
||||
<span class="material-icons">push_pin</span>
|
||||
</button>
|
||||
<button class="toolbar-btn toolbar-btn-danger" id="btn-disconnect" title="${_('remote.disconnect')}">
|
||||
<span class="material-icons">power_settings_new</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-handle" id="toolbar-handle">
|
||||
<button type="button" class="handle-btn" id="btn-handle-fullscreen" title="${_('remote.fullscreen')}">
|
||||
<span class="material-icons">fullscreen</span>
|
||||
</button>
|
||||
<button type="button" class="handle-btn handle-expand" id="btn-toolbar-expand" title="${_('remote.toggle_menu')}">
|
||||
<span class="material-icons">expand_more</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="tab-bar-add" id="btn-add-session" title="${_('remote.new_connection') || 'New Connection'}">
|
||||
<span class="material-icons">add</span>
|
||||
</button>
|
||||
@@ -139,195 +297,6 @@
|
||||
<!-- Main Viewer Container (session panels are inserted here) -->
|
||||
<div class="viewer-container" id="viewer-container"></div>
|
||||
|
||||
<!-- Shared floating toolbar (collapsible RustDesk-style pill, applies to active session) -->
|
||||
<div class="viewer-toolbar" id="viewer-toolbar">
|
||||
<!-- Expanded action pill (shown only when the toolbar is expanded) -->
|
||||
<div class="toolbar-expanded" id="toolbar-expanded">
|
||||
<div class="toolbar-left">
|
||||
<span class="toolbar-device-info">
|
||||
<span class="toolbar-device-id" id="toolbar-device-id"></span>
|
||||
<span class="toolbar-separator toolbar-status-sep">|</span>
|
||||
<span class="toolbar-status" id="toolbar-status">${_('remote.connecting')}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="toolbar-center">
|
||||
<span class="toolbar-stats" id="toolbar-stats"></span>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<!-- Actions Menu -->
|
||||
<div class="toolbar-dropdown">
|
||||
<button class="toolbar-btn" id="btn-actions" title="${_('remote.actions')}">
|
||||
<span class="material-icons">flash_on</span>
|
||||
</button>
|
||||
<div class="toolbar-dropdown-menu" id="actions-menu">
|
||||
<button class="dropdown-item" id="btn-cad">
|
||||
<span class="material-icons">security</span> Ctrl+Alt+Del
|
||||
</button>
|
||||
<button class="dropdown-item" id="btn-lock">
|
||||
<span class="material-icons">lock</span> ${_('remote.lock_screen')}
|
||||
</button>
|
||||
<button class="dropdown-item" id="btn-restart-remote">
|
||||
<span class="material-icons">restart_alt</span> ${_('remote.restart_remote')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<button class="dropdown-item" id="btn-refresh-screen">
|
||||
<span class="material-icons">refresh</span> ${_('remote.refresh_screen')}
|
||||
</button>
|
||||
<button class="dropdown-item" id="btn-clipboard-paste">
|
||||
<span class="material-icons">content_paste</span> ${_('remote.clipboard_paste')}
|
||||
</button>
|
||||
<button class="dropdown-item" id="btn-reset-keyboard">
|
||||
<span class="material-icons">keyboard</span> ${_('remote.reset_keyboard')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<button class="dropdown-item toggle-item" id="btn-block-input" data-active="false">
|
||||
<span class="material-icons">block</span> ${_('remote.block_input')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Menu -->
|
||||
<div class="toolbar-dropdown">
|
||||
<button class="toolbar-btn" id="btn-display" title="${_('remote.display_settings')}">
|
||||
<span class="material-icons">tune</span>
|
||||
</button>
|
||||
<div class="toolbar-dropdown-menu toolbar-dropdown-wide" id="display-menu">
|
||||
<div class="dropdown-label">${_('remote.image_quality')}</div>
|
||||
<button class="dropdown-item quality-item active" data-quality="Best">
|
||||
<span class="material-icons">hd</span> ${_('remote.quality_best')}
|
||||
</button>
|
||||
<button class="dropdown-item quality-item" data-quality="Balanced">
|
||||
<span class="material-icons">tune</span> ${_('remote.quality_balanced')}
|
||||
</button>
|
||||
<button class="dropdown-item quality-item" data-quality="Low">
|
||||
<span class="material-icons">speed</span> ${_('remote.quality_low')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<div class="dropdown-label">${_('remote.codec')}</div>
|
||||
<button class="dropdown-item codec-item active" data-codec="Auto">
|
||||
<span class="material-icons">auto_awesome</span> ${_('remote.codec_auto')}
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="VP9">
|
||||
<span class="material-icons">memory</span> VP9
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="AV1">
|
||||
<span class="material-icons">memory</span> AV1
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="H264">
|
||||
<span class="material-icons">memory</span> H264
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="VP8">
|
||||
<span class="material-icons">memory</span> VP8
|
||||
</button>
|
||||
<button class="dropdown-item codec-item" data-codec="H265">
|
||||
<span class="material-icons">memory</span> H265
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<div class="dropdown-label">${_('remote.keyboard_mode')}</div>
|
||||
<button class="dropdown-item keyboard-mode-item active" data-keyboard-mode="Auto">
|
||||
<span class="material-icons">auto_awesome</span> ${_('remote.keyboard_mode_auto')}
|
||||
</button>
|
||||
<button class="dropdown-item keyboard-mode-item" data-keyboard-mode="Legacy">
|
||||
<span class="material-icons">translate</span> ${_('remote.keyboard_mode_legacy')}
|
||||
</button>
|
||||
<button class="dropdown-item keyboard-mode-item" data-keyboard-mode="Map">
|
||||
<span class="material-icons">keyboard</span> ${_('remote.keyboard_mode_map')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<div class="dropdown-label">${_('remote.scale_mode')}</div>
|
||||
<button class="dropdown-item scale-item active" data-scale="fit">
|
||||
<span class="material-icons">fit_screen</span> ${_('remote.scale_fit')}
|
||||
</button>
|
||||
<button class="dropdown-item scale-item" data-scale="fill">
|
||||
<span class="material-icons">crop_free</span> ${_('remote.scale_fill')}
|
||||
</button>
|
||||
<button class="dropdown-item scale-item" data-scale="1:1">
|
||||
<span class="material-icons">filter_1</span> ${_('remote.scale_original')}
|
||||
</button>
|
||||
<button class="dropdown-item scale-item" data-scale="stretch">
|
||||
<span class="material-icons">open_in_full</span> ${_('remote.scale_stretch')}
|
||||
</button>
|
||||
<div class="dropdown-separator"></div>
|
||||
<button class="dropdown-item toggle-item" id="btn-show-cursor" data-active="true">
|
||||
<span class="material-icons">mouse</span> ${_('remote.show_cursor')}
|
||||
</button>
|
||||
<button class="dropdown-item toggle-item" id="btn-lock-session" data-active="false">
|
||||
<span class="material-icons">lock_clock</span> ${_('remote.lock_after_session')}
|
||||
</button>
|
||||
<button class="dropdown-item toggle-item" id="btn-privacy-mode" data-active="false">
|
||||
<span class="material-icons">visibility_off</span> ${_('remote.privacy_mode')}
|
||||
</button>
|
||||
<button class="dropdown-item toggle-item" id="btn-disable-clipboard" data-active="false">
|
||||
<span class="material-icons">content_paste_off</span> ${_('remote.disable_clipboard')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio -->
|
||||
<button class="toolbar-btn" id="btn-audio" title="${_('remote.toggle_audio')}">
|
||||
<span class="material-icons">volume_up</span>
|
||||
</button>
|
||||
|
||||
<!-- Monitor Selector -->
|
||||
<div class="toolbar-dropdown">
|
||||
<button class="toolbar-btn" id="btn-monitors" title="${_('remote.monitors') || 'Monitors'}" style="display:none">
|
||||
<span class="material-icons">desktop_windows</span>
|
||||
</button>
|
||||
<div class="toolbar-dropdown-menu" id="monitors-menu">
|
||||
<div class="dropdown-label">${_('remote.select_monitor') || 'Select Monitor'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat -->
|
||||
<button class="toolbar-btn" id="btn-chat" title="${_('remote.chat')}">
|
||||
<span class="material-icons">chat</span>
|
||||
</button>
|
||||
|
||||
<!-- File Transfer -->
|
||||
<button class="toolbar-btn" id="btn-file-transfer" title="${_('remote.file_transfer') || 'File Transfer'}">
|
||||
<span class="material-icons">folder_open</span>
|
||||
</button>
|
||||
|
||||
<!-- Recording -->
|
||||
<button class="toolbar-btn" id="btn-record" title="${_('remote.record')}">
|
||||
<span class="material-icons">fiber_manual_record</span>
|
||||
</button>
|
||||
|
||||
<!-- View Only -->
|
||||
<button class="toolbar-btn" id="btn-viewonly" title="${_('remote.view_only')}">
|
||||
<span class="material-icons">visibility</span>
|
||||
</button>
|
||||
|
||||
<!-- Pin Toolbar -->
|
||||
<button class="toolbar-btn" id="btn-pin" title="${_('remote.pin_toolbar')}">
|
||||
<span class="material-icons">push_pin</span>
|
||||
</button>
|
||||
|
||||
<!-- Fullscreen -->
|
||||
<button class="toolbar-btn" id="btn-fullscreen" title="${_('remote.fullscreen')}">
|
||||
<span class="material-icons">fullscreen</span>
|
||||
</button>
|
||||
|
||||
<!-- Disconnect -->
|
||||
<button class="toolbar-btn toolbar-btn-danger" id="btn-disconnect" title="${_('remote.disconnect')}">
|
||||
<span class="material-icons">power_settings_new</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Always-visible compact handle: [move] [fullscreen] [expand/collapse] -->
|
||||
<div class="toolbar-handle" id="toolbar-handle">
|
||||
<button type="button" class="handle-btn handle-drag" id="btn-toolbar-drag" title="${_('remote.move_toolbar')}">
|
||||
<span class="material-icons">drag_indicator</span>
|
||||
</button>
|
||||
<button type="button" class="handle-btn" id="btn-handle-fullscreen" title="${_('remote.fullscreen')}">
|
||||
<span class="material-icons">fullscreen</span>
|
||||
</button>
|
||||
<button type="button" class="handle-btn handle-expand" id="btn-toolbar-expand" title="${_('remote.toggle_menu')}">
|
||||
<span class="material-icons">expand_more</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /#rd-viewer-shell -->
|
||||
<nav class="rd-mobile-toolbar" id="rd-mobile-toolbar" aria-label="${_('remote.mobile_toolbar') || 'Remote controls'}">
|
||||
<button type="button" class="rd-mobile-toolbar-btn active" id="rd-mob-input-touch" data-mode="direct" title="${_('remote.input_mode_touch')}">
|
||||
@@ -399,14 +368,16 @@
|
||||
placeholder="${_('remote.password_placeholder')}" autocomplete="off">
|
||||
</div>
|
||||
<p class="login-error session-login-error" style="display:none;"></p>
|
||||
<label class="form-check session-remember-peer" style="display:flex;align-items:center;gap:8px;margin:10px 0 0;font-size:0.875rem;">
|
||||
<input type="checkbox" class="session-remember-peer-checkbox">
|
||||
<span>${_('remote.remember_peer_password') || 'Remember device password on this device'}</span>
|
||||
</label>
|
||||
<button class="btn btn-primary btn-full session-btn-authenticate">
|
||||
<span class="material-icons">login</span>
|
||||
${_('remote.connect')}
|
||||
</button>
|
||||
<div class="password-form-footer">
|
||||
<label class="session-remember-peer">
|
||||
<input type="checkbox" class="session-remember-peer-checkbox">
|
||||
<span>${_('remote.remember_peer_password') || 'Remember device password on this device'}</span>
|
||||
</label>
|
||||
<button class="btn btn-primary btn-full session-btn-authenticate">
|
||||
<span class="material-icons">login</span>
|
||||
${_('remote.connect')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user