[feat] add dedicated privacy screen controls

This commit is contained in:
dijunkun
2026-09-12 22:24:09 +08:00
parent 99ae0fc811
commit 56ebfdf8e7
8 changed files with 168 additions and 84 deletions
@@ -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;
+1
View File
@@ -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}";
+7 -6
View File
@@ -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; } }
+62 -43
View File
@@ -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]));