Files
BetterDesk/web-nodejs/views/remote.ejs
T
UNITRONIX 2e01955f6c feat(rdclient): enhance desktop client with server validation, LAN discovery, and settings management
- Introduced server URL validation via `GET /api/bd/server-info` and `probe_server_url`.
- Added LAN discovery capabilities using UDP and optional mDNS for local network panel detection.
- Implemented a local settings window for managing URL, TLS settings, and user preferences.
- Enhanced dashboard with a unified sidebar and improved scrolling behavior.
- Updated documentation and pre-release checklist to reflect new features and requirements.
2026-06-14 21:12:41 +02:00

349 lines
18 KiB
HTML

<%- include('layouts/viewer', {
title: title || _('remote.title'),
pageScripts: ['billing-report', 'remote'],
body: `
<!-- Session Tab Bar -->
<div class="session-tab-bar" id="session-tab-bar">
<button type="button" class="tab-bar-back" id="btn-back-devices" title="${_('remote.back_to_devices')}">
<span class="material-icons">arrow_back</span>
</button>
<div class="session-tabs" id="session-tabs"></div>
<button class="tab-bar-add" id="btn-add-session" title="${_('remote.new_connection') || 'New Connection'}">
<span class="material-icons">add</span>
</button>
</div>
<!-- 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">|</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">
<select id="viewer-language-select" class="toolbar-lang-select" data-language-select aria-label="${_('settings.language')}"></select>
<!-- 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>
<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.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>
<!-- Add Session Dialog -->
<div class="viewer-overlay" id="add-session-overlay" style="display:none;">
<div class="overlay-card">
<div class="overlay-icon"><span class="material-icons">add_circle</span></div>
<h2 class="overlay-title">${_('remote.new_connection') || 'New Connection'}</h2>
<div class="password-form">
<div class="form-group">
<input type="text" class="form-input" id="new-session-id"
placeholder="${_('remote.device_id_placeholder') || 'Enter Device ID'}" autocomplete="off">
</div>
<button class="btn btn-primary btn-full" id="btn-connect-new">
<span class="material-icons">login</span>
${_('remote.connect')}
</button>
<button class="btn btn-secondary btn-full" id="btn-cancel-new" style="margin-top:8px;">
${_('actions.cancel')}
</button>
</div>
</div>
</div>
<!-- Session Panel Template (cloned for each session) -->
<template id="session-panel-template">
<div class="session-panel">
<div class="viewer-overlay session-connection-overlay">
<div class="overlay-card">
<div class="overlay-icon"><span class="material-icons">connected_tv</span></div>
<h2 class="overlay-title">${_('remote.title')}</h2>
<p class="overlay-device-id session-device-label"></p>
<p class="overlay-device-name session-device-name"></p>
<div class="overlay-status">
<div class="spinner"></div>
<span class="session-status-text">${_('remote.connecting')}</span>
</div>
<div class="overlay-actions session-overlay-actions" style="display:none;">
<button class="btn btn-primary session-btn-reconnect">
<span class="material-icons">refresh</span>
${_('remote.reconnect')}
</button>
<button class="btn btn-secondary session-btn-cdap-fallback" style="display:none;margin-top:8px;" title="${_('remote.cdap_fallback_hint')}">
<span class="material-icons">photo_camera</span>
${_('remote.use_cdap_fallback')}
</button>
</div>
</div>
</div>
<div class="viewer-overlay session-password-overlay" style="display:none;">
<div class="overlay-card">
<div class="overlay-icon"><span class="material-icons">lock</span></div>
<h2 class="overlay-title">${_('remote.password_required')}</h2>
<p class="overlay-device-id session-password-label"></p>
<div class="password-form">
<div class="form-group">
<input type="password" class="form-input session-password-input"
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>
</div>
</div>
<div class="viewer-overlay session-2fa-overlay" style="display:none;">
<div class="overlay-card">
<div class="overlay-icon"><span class="material-icons">verified_user</span></div>
<h2 class="overlay-title">${_('remote.2fa_required') || '2FA Verification'}</h2>
<p class="overlay-subtitle">${_('remote.2fa_hint') || 'Enter the 6-digit code from your authenticator app'}</p>
<div class="password-form">
<div class="form-group">
<input type="text" class="form-input session-2fa-input"
placeholder="000000" autocomplete="one-time-code"
maxlength="6" inputmode="numeric" pattern="[0-9]*"
style="text-align:center;font-size:1.5rem;letter-spacing:0.5em;">
</div>
<p class="login-error session-2fa-error" style="display:none;"></p>
<button class="btn btn-primary btn-full session-btn-verify-2fa">
<span class="material-icons">verified</span>
${_('remote.verify') || 'Verify'}
</button>
</div>
</div>
</div>
<canvas class="session-canvas" tabindex="0"></canvas>
<div class="chat-panel session-chat-panel" style="display:none;">
<div class="chat-header">
<span>${_('remote.chat')}</span>
<button class="chat-close session-btn-chat-close"><span class="material-icons">close</span></button>
</div>
<div class="chat-messages session-chat-messages"></div>
<div class="chat-input-area">
<input type="text" class="chat-input session-chat-input" placeholder="${_('remote.chat_placeholder')}" />
<button class="chat-send session-btn-chat-send"><span class="material-icons">send</span></button>
</div>
</div>
<!-- File Transfer Panel -->
<div class="file-panel session-file-panel" style="display:none;">
<div class="file-panel-header">
<span class="material-icons">folder_open</span>
<span>${_('remote.file_transfer') || 'File Transfer'}</span>
<div class="file-panel-actions">
<button class="file-btn-icon session-file-btn-hidden" title="${_('remote.file_show_hidden') || 'Show Hidden'}">
<span class="material-icons">visibility</span>
</button>
<button class="file-btn-icon session-file-btn-newdir" title="${_('remote.file_new_folder') || 'New Folder'}">
<span class="material-icons">create_new_folder</span>
</button>
<button class="file-btn-icon session-file-btn-upload" title="${_('remote.file_upload') || 'Upload'}">
<span class="material-icons">upload_file</span>
</button>
<button class="file-btn-icon session-file-btn-close">
<span class="material-icons">close</span>
</button>
</div>
</div>
<div class="file-panel-path">
<button class="file-btn-icon session-file-btn-up" title="${_('remote.file_parent') || 'Parent Directory'}">
<span class="material-icons">arrow_upward</span>
</button>
<button class="file-btn-icon session-file-btn-home" title="${_('remote.file_root') || 'Root'}">
<span class="material-icons">home</span>
</button>
<span class="file-path-text session-file-path-text">/</span>
</div>
<div class="file-panel-list session-file-list">
<div class="file-empty">${_('remote.file_empty') || 'No files'}</div>
</div>
<div class="file-panel-transfers session-file-transfers" style="display:none;">
<div class="file-transfers-header">
<span class="material-icons">swap_vert</span>
<span>${_('remote.file_transfers') || 'Transfers'}</span>
</div>
<div class="file-transfers-list session-file-transfers-list"></div>
</div>
<input type="file" class="session-file-upload-input" multiple style="display:none;" />
</div>
</div>
</template>
<!-- Initial session data (from server route) -->
<script nonce="${cspNonce}">
window.__initialDeviceId = ` + JSON.stringify(deviceId) + `;
window.__initialDeviceName = ` + JSON.stringify(device && device.hostname ? device.hostname : '') + `;
window.__capabilities = ` + JSON.stringify(capabilities || { transport: 'rd' }) + `;
</script>
`
}) %>