mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[feat] add dedicated privacy screen controls
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
#include "platform.h"
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rendering/slint_video_presenter.h"
|
||||
#include "runtime/privacy_control_state.h"
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
@@ -1230,13 +1231,15 @@ void GuiApplication::ResetSettingsUi() {
|
||||
ui_->main->set_self_hosted_enabled(enable_self_hosted_);
|
||||
ui_->main->set_autostart_enabled(enable_autostart_);
|
||||
ui_->main->set_daemon_enabled(enable_daemon_);
|
||||
ui_->main->set_privacy_on_connect_enabled(
|
||||
config_center_->IsEnablePrivacyScreen());
|
||||
#ifdef _WIN32
|
||||
ui_->main->set_capture_method_visible(true);
|
||||
ui_->main->set_capture_method_index(
|
||||
static_cast<int>(config_center_->GetScreenCaptureMethod()));
|
||||
ui_->main->set_privacy_setting_visible(true);
|
||||
ui_->main->set_privacy_on_connect_enabled(
|
||||
config_center_->IsEnablePrivacyScreen());
|
||||
ui_->main->set_privacy_setting_available(true);
|
||||
#else
|
||||
ui_->main->set_privacy_setting_available(false);
|
||||
#endif
|
||||
ui_->main->set_file_save_path(file_transfer_save_path_buf_);
|
||||
ui_->main->set_server_host(signal_server_ip_self_);
|
||||
@@ -1607,11 +1610,16 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
{
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
const auto& status = props->privacy_status_;
|
||||
action.pc.flag = status.overlay_active || status.remote_paused || status.state == PrivacyState::on ||
|
||||
status.state == PrivacyState::failed ? PrivacyCommandFlag::disable : PrivacyCommandFlag::enable;
|
||||
action.pc.block_local_input = props->privacy_block_local_input_;
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
const auto controls = GetPrivacyControlState(
|
||||
status, props->privacy_status_received_, props->privacy_command_pending_,
|
||||
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
|
||||
if (!controls.can_toggle) return;
|
||||
action.pc.flag = controls.active ? PrivacyCommandFlag::disable
|
||||
: PrivacyCommandFlag::enable;
|
||||
action.pc.block_local_input = true;
|
||||
props->privacy_command_pending_ = true;
|
||||
props->privacy_command_tick_ = SDL_GetTicks();
|
||||
props->privacy_command_tick_ = now;
|
||||
props->privacy_request_revision_ = status.revision;
|
||||
}
|
||||
const auto message = action.to_json();
|
||||
@@ -1623,12 +1631,6 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_);
|
||||
}
|
||||
});
|
||||
stream->on_toggle_privacy_input([this] {
|
||||
if (auto props = SelectedSession()) {
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
props->privacy_block_local_input_ = !props->privacy_block_local_input_;
|
||||
}
|
||||
});
|
||||
stream->on_toggle_mouse_control([this] {
|
||||
auto props = SelectedSession();
|
||||
if (!props || !props->connection_established_) {
|
||||
@@ -2554,23 +2556,27 @@ void GuiApplication::SyncStreamWindow() {
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
const auto& status = props->privacy_status_;
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
const bool timeout = props->privacy_command_pending_ && now - props->privacy_command_tick_ > 15000;
|
||||
const bool stale = props->privacy_status_received_ && now - props->privacy_status_tick_ > 5000;
|
||||
const bool pending = props->privacy_command_pending_ && !timeout;
|
||||
const bool active = status.overlay_active || status.remote_paused || status.state == PrivacyState::on || status.state == PrivacyState::failed;
|
||||
(*ui_->stream)->set_privacy_active(active);
|
||||
(*ui_->stream)->set_privacy_can_toggle(props->control_mouse_ &&
|
||||
props->connection_status_.load() == ConnectionStatus::Connected && !pending &&
|
||||
(active || (props->privacy_status_received_ && status.supported && !stale)));
|
||||
(*ui_->stream)->set_privacy_can_change_input(props->control_mouse_ && !active && !pending && status.input_block_supported);
|
||||
(*ui_->stream)->set_privacy_block_input(active ? status.input_blocked : props->privacy_block_local_input_);
|
||||
(*ui_->stream)->set_privacy_remote_paused(status.remote_paused || (active && (stale || timeout)));
|
||||
const auto controls = GetPrivacyControlState(
|
||||
status, props->privacy_status_received_, props->privacy_command_pending_,
|
||||
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
|
||||
const bool connected =
|
||||
props->connection_status_.load() == ConnectionStatus::Connected;
|
||||
(*ui_->stream)->set_privacy_active(controls.active);
|
||||
(*ui_->stream)->set_privacy_off(connected && controls.off);
|
||||
(*ui_->stream)->set_privacy_unsupported(controls.unsupported);
|
||||
(*ui_->stream)->set_privacy_can_toggle(
|
||||
props->control_mouse_ && connected && controls.can_toggle);
|
||||
const int language = localization_language_index_;
|
||||
std::string text = !props->privacy_status_received_ ? localization::privacy_unknown[language] :
|
||||
(timeout || stale) ? localization::privacy_timeout[language] :
|
||||
pending ? localization::privacy_pending[language] :
|
||||
status.remote_paused ? localization::privacy_paused[language] : status.reason;
|
||||
if (status.remote_paused && !pending) text += std::string("\n") + status.reason;
|
||||
std::string text = !connected || !props->privacy_status_received_
|
||||
? localization::privacy_screen[language]
|
||||
: controls.timeout || controls.stale ? localization::privacy_timeout[language]
|
||||
: controls.warning ? localization::privacy_paused[language]
|
||||
: controls.enabled ? localization::privacy_on[language]
|
||||
: !status.supported ? localization::privacy_unsupported[language]
|
||||
: controls.off ? localization::privacy_off[language]
|
||||
: localization::privacy_screen[language];
|
||||
if (connected && controls.warning && status.reason[0] != '\0')
|
||||
text += std::string("\n") + status.reason;
|
||||
(*ui_->stream)->set_privacy_status_text(UiText(text));
|
||||
}
|
||||
int remote_cursor_shape =
|
||||
|
||||
@@ -20,13 +20,14 @@ struct TranslationRow {
|
||||
|
||||
// Single source of truth for all UI strings.
|
||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||
X(privacy_screen, u8"隐私屏", "Privacy screen", u8"Приватный экран") \
|
||||
X(privacy_enable, u8"开启隐私屏", "Enable privacy screen", u8"Включить приватный экран") \
|
||||
X(privacy_disable, u8"关闭隐私屏 / 恢复操作", "Turn privacy off / recover", u8"Выключить / восстановить") \
|
||||
X(privacy_block_input, u8"开启时屏蔽本地输入", "Block local input on enable", u8"Блокировать локальный ввод") \
|
||||
X(privacy_unknown, u8"隐私屏:未收到被控端确认(旧版本可能不支持)", "Privacy: no host confirmation (older hosts may not support it)", u8"Приватность: нет подтверждения от узла") \
|
||||
X(privacy_pending, u8"隐私屏:等待被控端完成验证", "Privacy: waiting for host verification", u8"Приватность: ожидание проверки") \
|
||||
X(privacy_on, u8"隐私屏:已开启", "Privacy screen: on", u8"Приватный экран: включён") \
|
||||
X(privacy_off, u8"隐私屏:已关闭", "Privacy screen: off", u8"Приватный экран: выключен") \
|
||||
X(privacy_unsupported, u8"隐私屏:被控端暂不支持", "Privacy screen: unavailable on this host", u8"Приватный экран: недоступен на узле") \
|
||||
X(privacy_timeout, u8"隐私屏状态未知:被控端确认超时,可关闭后重试", "Privacy state unknown: host confirmation timed out; turn off to recover", u8"Статус неизвестен: время ожидания истекло") \
|
||||
X(privacy_paused, u8"隐私保护未就绪,远程操作已暂停。可在快捷菜单关闭隐私屏恢复。", "Privacy protection is not ready; remote operation paused. Turn privacy off in the shortcut menu to recover.", u8"Приватность не готова; удалённое управление приостановлено.") \
|
||||
X(privacy_paused, u8"隐私保护未就绪,远程操作已暂停。点击隐私屏按钮关闭并恢复。", "Privacy protection is not ready; remote operation paused. Click the privacy screen button to turn it off and recover.", u8"Приватность не готова; нажмите кнопку приватного экрана для выключения и восстановления управления.") \
|
||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-12
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_CONTROL_STATE_H_
|
||||
#define _PRIVACY_CONTROL_STATE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "remote_action.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct PrivacyControlState {
|
||||
bool active = false;
|
||||
bool enabled = false;
|
||||
bool off = false;
|
||||
bool unsupported = false;
|
||||
bool warning = false;
|
||||
bool stale = false;
|
||||
bool timeout = false;
|
||||
bool can_toggle = false;
|
||||
};
|
||||
|
||||
// Use the same host-confirmed state for presentation and command admission.
|
||||
// An engaged or failed screen can still be disabled when its status goes stale.
|
||||
inline PrivacyControlState GetPrivacyControlState(
|
||||
const PrivacyStatus& status, bool received, bool command_pending,
|
||||
uint64_t status_age_ms, uint64_t command_age_ms) {
|
||||
PrivacyControlState result;
|
||||
result.timeout = command_pending && command_age_ms > 15000;
|
||||
result.stale = received && status_age_ms > 5000;
|
||||
const bool request_pending = command_pending && !result.timeout;
|
||||
const bool fresh = received && !result.stale;
|
||||
result.active = status.overlay_active || status.remote_paused ||
|
||||
status.state == PrivacyState::on ||
|
||||
status.state == PrivacyState::starting ||
|
||||
status.state == PrivacyState::stopping ||
|
||||
status.state == PrivacyState::failed;
|
||||
// A command in flight only gates repeated clicks; it adds no display state.
|
||||
result.enabled = fresh && !result.timeout &&
|
||||
status.state == PrivacyState::on && status.overlay_active &&
|
||||
!status.remote_paused;
|
||||
result.off = fresh && !result.timeout &&
|
||||
status.state == PrivacyState::off && !result.active;
|
||||
result.unsupported = received && !status.supported;
|
||||
result.warning = result.timeout || result.stale ||
|
||||
(received && status.state == PrivacyState::failed);
|
||||
result.can_toggle = !request_pending &&
|
||||
(result.active || (fresh && status.supported));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -161,7 +161,6 @@ struct RemoteSession {
|
||||
PrivacyStatus privacy_status_{};
|
||||
bool privacy_status_received_ = false;
|
||||
bool privacy_command_pending_ = false;
|
||||
bool privacy_block_local_input_ = true;
|
||||
uint64_t privacy_command_tick_ = 0;
|
||||
uint32_t privacy_request_revision_ = 0;
|
||||
uint64_t privacy_status_tick_ = 0;
|
||||
|
||||
@@ -45,6 +45,7 @@ export global FontAwesomeIcons {
|
||||
out property <string> shield-halved: "\u{f3ed}";
|
||||
out property <string> square-full: "\u{f45c}";
|
||||
out property <string> trash-can: "\u{f2ed}";
|
||||
out property <string> user-secret: "\u{f21b}";
|
||||
out property <string> volume-high: "\u{f028}";
|
||||
out property <string> volume-xmark: "\u{f6a9}";
|
||||
out property <string> xmark: "\u{f00d}";
|
||||
|
||||
@@ -251,7 +251,8 @@ export component MainWindow inherits Window {
|
||||
in-out property <bool> self-hosted-enabled: false;
|
||||
in-out property <bool> autostart-enabled: false;
|
||||
in-out property <bool> daemon-enabled: false;
|
||||
in-out property <bool> privacy-setting-visible: false;
|
||||
// Show the preference on every platform; only adapted hosts can change it.
|
||||
in property <bool> privacy-setting-available: false;
|
||||
in-out property <bool> privacy-on-connect-enabled: true;
|
||||
in-out property <string> file-save-path: "";
|
||||
in-out property <string> server-host: "";
|
||||
@@ -1457,11 +1458,6 @@ export component MainWindow inherits Window {
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.capture-method; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
|
||||
}
|
||||
if root.privacy-setting-visible: Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.privacy-on-connect-enabled; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
@@ -1497,6 +1493,11 @@ export component MainWindow inherits Window {
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; color: root.privacy-setting-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
CompactButton { x: 8px; y: 3px; width: root.compact-language ? 70px : 206px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
|
||||
|
||||
@@ -27,7 +27,6 @@ export global StreamStrings {
|
||||
in-out property <string> send-shortcut: "Send Shortcut";
|
||||
in-out property <string> privacy-enable: "Enable privacy screen";
|
||||
in-out property <string> privacy-disable: "Turn privacy off / recover";
|
||||
in-out property <string> privacy-block-input: "Block local input on enable";
|
||||
in-out property <string> control-mouse: "Control Mouse";
|
||||
in-out property <string> release-mouse: "Release Mouse";
|
||||
in-out property <string> audio: "Audio Capture";
|
||||
@@ -57,19 +56,23 @@ component ControlBarButton inherits Rectangle {
|
||||
in property <string> tooltip: "";
|
||||
in property <bool> selected: false;
|
||||
in property <bool> slashed: false;
|
||||
in property <bool> action-enabled: true;
|
||||
// Capability and temporary command availability have different visuals.
|
||||
in property <bool> muted: false;
|
||||
in property <length> icon-size: 16px;
|
||||
out property <bool> has-hover: touch.has-hover;
|
||||
callback clicked;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: touch.pressed ? #0f87fa
|
||||
background: root.muted ? #eef0f3 : touch.pressed && root.action-enabled ? #0f87fa
|
||||
: touch.has-hover ? #4296fa
|
||||
: root.selected ? #4296fa : #4296fa66;
|
||||
border-radius: 3px;
|
||||
|
||||
Text {
|
||||
text: root.icon;
|
||||
color: #24272d;
|
||||
color: root.muted ? #8a8f96 : #24272d;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: root.icon-size;
|
||||
@@ -108,7 +111,8 @@ component ControlBarButton inherits Rectangle {
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => { root.clicked(); }
|
||||
// Keep disabled controls hit-testable so clicks cannot reach the peer.
|
||||
clicked => { if root.action-enabled { root.clicked(); } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +143,10 @@ export component StreamWindow inherits Window {
|
||||
in property <bool> mouse-control-enabled: true;
|
||||
in property <bool> privacy-can-toggle: false;
|
||||
in property <bool> privacy-active: false;
|
||||
in property <bool> privacy-block-input: true;
|
||||
in property <bool> privacy-can-change-input: false;
|
||||
in property <bool> privacy-remote-paused: false;
|
||||
in property <string> privacy-status-text: "Privacy screen: waiting for host";
|
||||
in property <bool> privacy-off: false;
|
||||
in property <bool> privacy-unsupported: false;
|
||||
// Privacy feedback stays on its toolbar button and hover tooltip.
|
||||
in property <string> privacy-status-text: "Privacy screen";
|
||||
// Numeric values mirror crossdesk::RemoteCursorShape and Slint's MouseCursor.
|
||||
in property <bool> remote-cursor-active: false;
|
||||
in property <int> remote-cursor-shape: 0;
|
||||
@@ -164,7 +168,6 @@ export component StreamWindow inherits Window {
|
||||
callback send-shortcut(string);
|
||||
callback toggle-mouse-control;
|
||||
callback toggle-privacy;
|
||||
callback toggle-privacy-input;
|
||||
callback toggle-audio;
|
||||
callback select-file;
|
||||
callback close-file-transfer;
|
||||
@@ -187,6 +190,13 @@ export component StreamWindow inherits Window {
|
||||
private property <bool> display-menu-animation-enabled: false;
|
||||
private property <float> display-menu-progress: 0.0;
|
||||
private property <bool> shortcut-menu-open: false;
|
||||
private property <bool> privacy-tooltip-visible: false;
|
||||
private property <bool> privacy-hovered: root.control-expanded
|
||||
&& privacy-button.has-hover && !root.control-dragging;
|
||||
private property <length> privacy-tooltip-width: min(320px, video.width);
|
||||
private property <length> privacy-tooltip-x: min(max(0px,
|
||||
control.x + (root.control-docked-left ? 105px : 138px)),
|
||||
video.width - root.privacy-tooltip-width);
|
||||
// A popup can remove itself from the scene in its clicked callback before
|
||||
// the video input layer observes the same pointer release. Remember that
|
||||
// the press belonged to local UI so that release cannot reach the peer.
|
||||
@@ -226,6 +236,13 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
changed privacy-hovered => { root.privacy-tooltip-visible = false; }
|
||||
privacy-tooltip-timer := Timer {
|
||||
interval: 450ms;
|
||||
running: root.privacy-hovered && !root.privacy-tooltip-visible;
|
||||
triggered => { root.privacy-tooltip-visible = true; }
|
||||
}
|
||||
|
||||
is-local-control-area(pointer-x, pointer-y) => {
|
||||
let over-control = pointer-x >= control.x
|
||||
&& pointer-x <= control.x + control.width
|
||||
@@ -243,9 +260,9 @@ export component StreamWindow inherits Window {
|
||||
let shortcut-menu-y = control.y + 40px;
|
||||
let over-shortcut-menu = root.shortcut-menu-open
|
||||
&& pointer-x >= shortcut-menu-x
|
||||
&& pointer-x <= shortcut-menu-x + 360px
|
||||
&& pointer-x <= shortcut-menu-x + 180px
|
||||
&& pointer-y >= shortcut-menu-y
|
||||
&& pointer-y <= shortcut-menu-y + 200px;
|
||||
&& pointer-y <= shortcut-menu-y + 68px;
|
||||
return over-control || over-display-menu || over-shortcut-menu;
|
||||
}
|
||||
|
||||
@@ -650,7 +667,7 @@ export component StreamWindow inherits Window {
|
||||
control := Rectangle {
|
||||
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
|
||||
y: min(max(0px, root.control-pos-y), parent.height - self.height);
|
||||
width: root.control-expanded ? min(300px, parent.width) : 20px;
|
||||
width: root.control-expanded ? min(332px, parent.width) : 20px;
|
||||
height: root.stats-visible && root.control-expanded ? min(180px, parent.height) : 38px;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
@@ -702,40 +719,40 @@ export component StreamWindow inherits Window {
|
||||
clicked => { root.toggle-mouse-control(); }
|
||||
}
|
||||
if root.control-expanded: audio-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 105px : 138px; y: 7px;
|
||||
x: root.control-docked-left ? 137px : 170px; y: 7px;
|
||||
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
|
||||
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
|
||||
clicked => { root.toggle-audio(); }
|
||||
}
|
||||
if root.control-expanded: file-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 137px : 170px; y: 7px;
|
||||
x: root.control-docked-left ? 169px : 202px; y: 7px;
|
||||
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
|
||||
clicked => { root.select-file(); }
|
||||
}
|
||||
if root.control-expanded: stats-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 169px : 202px; y: 7px;
|
||||
x: root.control-docked-left ? 201px : 234px; y: 7px;
|
||||
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
|
||||
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
|
||||
clicked => { root.toggle-stats(); }
|
||||
}
|
||||
if root.control-expanded: fullscreen-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 201px : 234px; y: 7px;
|
||||
x: root.control-docked-left ? 233px : 266px; y: 7px;
|
||||
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
|
||||
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
|
||||
clicked => { root.toggle-fullscreen(); }
|
||||
}
|
||||
if root.control-expanded: close-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 233px : 266px; y: 7px;
|
||||
x: root.control-docked-left ? 265px : 298px; y: 7px;
|
||||
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
|
||||
clicked => { root.disconnect(); }
|
||||
}
|
||||
|
||||
if root.control-expanded && root.control-docked-left: Rectangle {
|
||||
x: 267px; y: 12px; width: 2px; height: 15px;
|
||||
x: 299px; y: 12px; width: 2px; height: 15px;
|
||||
background: ImGuiLineStyle.control-separator;
|
||||
}
|
||||
collapse-button := ControlBarButton {
|
||||
x: root.control-expanded ? (root.control-docked-left ? 275px : 10px) : 2px;
|
||||
x: root.control-expanded ? (root.control-docked-left ? 307px : 10px) : 2px;
|
||||
y: 7px;
|
||||
width: 15px;
|
||||
icon: root.control-expanded
|
||||
@@ -745,6 +762,17 @@ export component StreamWindow inherits Window {
|
||||
clicked => { root.control-expanded = !root.control-expanded; }
|
||||
}
|
||||
|
||||
privacy-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: root.control-docked-left ? 105px : 138px; y: 7px;
|
||||
icon: FontAwesomeIcons.user-secret;
|
||||
slashed: root.privacy-off && !root.privacy-unsupported;
|
||||
muted: root.privacy-unsupported;
|
||||
action-enabled: root.privacy-can-toggle;
|
||||
tooltip: root.privacy-status-text;
|
||||
clicked => { root.toggle-privacy(); }
|
||||
}
|
||||
|
||||
if root.stats-visible && root.control-expanded: stats-table := Rectangle {
|
||||
x: 14px;
|
||||
y: 38px;
|
||||
@@ -954,7 +982,7 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
|
||||
if root.shortcut-menu-open: Rectangle {
|
||||
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 360px; height: 200px;
|
||||
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 180px; height: 68px;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
|
||||
VerticalLayout {
|
||||
padding: 4px; spacing: 1px;
|
||||
@@ -970,32 +998,23 @@ export component StreamWindow inherits Window {
|
||||
clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
height: 29px; background: privacy-touch.has-hover ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: root.privacy-active ? StreamStrings.privacy-disable : StreamStrings.privacy-enable; color: root.privacy-can-toggle ? #30343b : #8a8f96; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
privacy-touch := TouchArea {
|
||||
enabled: root.privacy-can-toggle;
|
||||
pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } }
|
||||
clicked => { root.toggle-privacy(); }
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
height: 29px; background: privacy-input-touch.has-hover ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: (root.privacy-block-input ? "[✓] " : "[ ] ") + StreamStrings.privacy-block-input; color: root.privacy-can-change-input ? #30343b : #8a8f96; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
privacy-input-touch := TouchArea {
|
||||
enabled: root.privacy-can-change-input;
|
||||
pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } }
|
||||
clicked => { root.toggle-privacy-input(); }
|
||||
}
|
||||
}
|
||||
Text { height: 68px; text: root.privacy-status-text; wrap: word-wrap; color: #555b66; font-size: ImGuiFontStyle.body; }
|
||||
}
|
||||
}
|
||||
|
||||
if root.privacy-remote-paused: Rectangle {
|
||||
x: 0px; y: 65px; width: parent.width; height: parent.height - 65px;
|
||||
background: #16181d; z: 6;
|
||||
Text { x: 30px; width: parent.width - 60px; text: root.privacy-status-text; color: #eeeeee; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; }
|
||||
if root.privacy-hovered && root.privacy-tooltip-visible: Rectangle {
|
||||
x: root.privacy-tooltip-x;
|
||||
width: root.privacy-tooltip-width;
|
||||
height: privacy-tip.preferred-height + 16px;
|
||||
y: control.y + control.height + self.height <= video.height
|
||||
? control.y + control.height : max(0px, control.y - self.height);
|
||||
background: #30343bf5; border-radius: 5px; z: 12;
|
||||
privacy-tip := Text {
|
||||
x: 8px; y: 8px; width: parent.width - 16px;
|
||||
text: root.privacy-status-text
|
||||
+ (root.privacy-can-toggle ? "\n" + (root.privacy-active
|
||||
? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : "");
|
||||
color: white; font-size: ImGuiFontStyle.body; wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer progress overlay at bottom-left.
|
||||
|
||||
@@ -144,7 +144,6 @@ inline int ApplyStreamWindowStrings(
|
||||
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
|
||||
strings.set_privacy_enable(Text(localization::privacy_enable[language]));
|
||||
strings.set_privacy_disable(Text(localization::privacy_disable[language]));
|
||||
strings.set_privacy_block_input(Text(localization::privacy_block_input[language]));
|
||||
strings.set_control_mouse(Text(localization::control_mouse[language]));
|
||||
strings.set_release_mouse(Text(localization::release_mouse[language]));
|
||||
strings.set_audio(Text(localization::audio_capture[language]));
|
||||
|
||||
Reference in New Issue
Block a user