mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-26 15:28:53 +08:00
[feat] integrate IP direct mode with desktop controls and history
This commit is contained in:
@@ -68,6 +68,13 @@ int ConfigCenter::Load() {
|
|||||||
language_ = static_cast<LANGUAGE>(language_value);
|
language_ = static_cast<LANGUAGE>(language_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enable_ip_direct_ = ini_->GetBoolValue(section_, "enable_ip_direct", true);
|
||||||
|
connection_mode_ =
|
||||||
|
enable_ip_direct_ &&
|
||||||
|
std::strcmp(ini_->GetValue(section_, "connection_mode", "id"), "ip") == 0
|
||||||
|
? CONNECTION_MODE::DIRECT_IP
|
||||||
|
: CONNECTION_MODE::DEVICE_ID;
|
||||||
|
|
||||||
const long screen_capture_method_value = ini_->GetLongValue(
|
const long screen_capture_method_value = ini_->GetLongValue(
|
||||||
section_, "screen_capture_method",
|
section_, "screen_capture_method",
|
||||||
static_cast<long>(ScreenCaptureMethod::Auto));
|
static_cast<long>(ScreenCaptureMethod::Auto));
|
||||||
@@ -201,6 +208,9 @@ int ConfigCenter::StoreValues(
|
|||||||
|
|
||||||
int ConfigCenter::Save() {
|
int ConfigCenter::Save() {
|
||||||
ini_->SetLongValue(section_, "language", static_cast<long>(language_));
|
ini_->SetLongValue(section_, "language", static_cast<long>(language_));
|
||||||
|
ini_->SetValue(section_, "connection_mode",
|
||||||
|
connection_mode_ == CONNECTION_MODE::DIRECT_IP ? "ip" : "id");
|
||||||
|
ini_->SetBoolValue(section_, "enable_ip_direct", enable_ip_direct_);
|
||||||
ini_->SetLongValue(section_, "screen_capture_method",
|
ini_->SetLongValue(section_, "screen_capture_method",
|
||||||
static_cast<long>(screen_capture_method_));
|
static_cast<long>(screen_capture_method_));
|
||||||
ini_->SetLongValue(section_, "video_encode_format",
|
ini_->SetLongValue(section_, "video_encode_format",
|
||||||
@@ -249,6 +259,34 @@ int ConfigCenter::SetLanguage(LANGUAGE language) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ConfigCenter::SetConnectionMode(CONNECTION_MODE mode) {
|
||||||
|
if (mode != CONNECTION_MODE::DEVICE_ID && mode != CONNECTION_MODE::DIRECT_IP) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (mode == CONNECTION_MODE::DIRECT_IP && !enable_ip_direct_) return -1;
|
||||||
|
if (mode == connection_mode_) return 0;
|
||||||
|
if (StoreValues({{"connection_mode",
|
||||||
|
mode == CONNECTION_MODE::DIRECT_IP ? "ip" : "id"}}) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
connection_mode_ = mode;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ConfigCenter::SetIpDirectEnabled(bool enabled) {
|
||||||
|
if (enabled == enable_ip_direct_) return 0;
|
||||||
|
const auto mode = enabled ? connection_mode_ : CONNECTION_MODE::DEVICE_ID;
|
||||||
|
// Persist availability and the fallback mode together, so a failed save
|
||||||
|
// leaves both the current runtime and the next launch unchanged.
|
||||||
|
if (StoreValues({{"enable_ip_direct", enabled ? "true" : "false"},
|
||||||
|
{"connection_mode", mode == CONNECTION_MODE::DIRECT_IP
|
||||||
|
? "ip" : "id"}}) != 0)
|
||||||
|
return -1;
|
||||||
|
enable_ip_direct_ = enabled;
|
||||||
|
connection_mode_ = mode;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
|
int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
|
||||||
if (!IsValidScreenCaptureMethod(static_cast<long>(method))) {
|
if (!IsValidScreenCaptureMethod(static_cast<long>(method))) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -409,6 +447,12 @@ int ConfigCenter::SetSponsorshipPromptDismissedVersion(
|
|||||||
|
|
||||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
||||||
|
|
||||||
|
ConfigCenter::CONNECTION_MODE ConfigCenter::GetConnectionMode() const {
|
||||||
|
return connection_mode_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConfigCenter::IsIpDirectEnabled() const { return enable_ip_direct_; }
|
||||||
|
|
||||||
ScreenCaptureMethod ConfigCenter::GetScreenCaptureMethod() const {
|
ScreenCaptureMethod ConfigCenter::GetScreenCaptureMethod() const {
|
||||||
return screen_capture_method_;
|
return screen_capture_method_;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace crossdesk {
|
|||||||
class ConfigCenter {
|
class ConfigCenter {
|
||||||
public:
|
public:
|
||||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
|
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
|
||||||
|
enum class CONNECTION_MODE { DEVICE_ID = 0, DIRECT_IP = 1 };
|
||||||
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||||
enum class TURN_MODE {
|
enum class TURN_MODE {
|
||||||
DISABLED = 0,
|
DISABLED = 0,
|
||||||
@@ -35,6 +36,8 @@ class ConfigCenter {
|
|||||||
|
|
||||||
// write config
|
// write config
|
||||||
int SetLanguage(LANGUAGE language);
|
int SetLanguage(LANGUAGE language);
|
||||||
|
int SetConnectionMode(CONNECTION_MODE mode);
|
||||||
|
int SetIpDirectEnabled(bool enabled);
|
||||||
int SetScreenCaptureMethod(ScreenCaptureMethod method);
|
int SetScreenCaptureMethod(ScreenCaptureMethod method);
|
||||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||||
int SetTurnMode(TURN_MODE turn_mode);
|
int SetTurnMode(TURN_MODE turn_mode);
|
||||||
@@ -54,6 +57,8 @@ class ConfigCenter {
|
|||||||
// read config
|
// read config
|
||||||
|
|
||||||
LANGUAGE GetLanguage() const;
|
LANGUAGE GetLanguage() const;
|
||||||
|
CONNECTION_MODE GetConnectionMode() const;
|
||||||
|
bool IsIpDirectEnabled() const;
|
||||||
ScreenCaptureMethod GetScreenCaptureMethod() const;
|
ScreenCaptureMethod GetScreenCaptureMethod() const;
|
||||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||||
TURN_MODE GetTurnMode() const;
|
TURN_MODE GetTurnMode() const;
|
||||||
@@ -86,6 +91,8 @@ class ConfigCenter {
|
|||||||
const char* section_ = "Settings";
|
const char* section_ = "Settings";
|
||||||
|
|
||||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||||
|
CONNECTION_MODE connection_mode_ = CONNECTION_MODE::DEVICE_ID;
|
||||||
|
bool enable_ip_direct_ = true;
|
||||||
ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
|
ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
|
||||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||||
|
|||||||
@@ -1162,6 +1162,7 @@ int GuiApplication::Run() {
|
|||||||
updater_ = std::make_unique<updater::Controller>(path_manager_->GetCachePath());
|
updater_ = std::make_unique<updater::Controller>(path_manager_->GetCachePath());
|
||||||
#endif
|
#endif
|
||||||
InitializeUi();
|
InitializeUi();
|
||||||
|
InitializeLan();
|
||||||
#ifndef __OHOS__
|
#ifndef __OHOS__
|
||||||
ui_->activation_receiver = std::move(activation_receiver);
|
ui_->activation_receiver = std::move(activation_receiver);
|
||||||
#endif
|
#endif
|
||||||
@@ -1296,6 +1297,10 @@ void GuiApplication::InitializeUi() {
|
|||||||
slint::set_xdg_app_id("crossdesk");
|
slint::set_xdg_app_id("crossdesk");
|
||||||
#endif
|
#endif
|
||||||
ui_ = std::make_unique<SlintUi>();
|
ui_ = std::make_unique<SlintUi>();
|
||||||
|
for (const auto& window : {ui_->main, ui_->wall})
|
||||||
|
window->set_ip_direct_enabled(config_center_->IsIpDirectEnabled());
|
||||||
|
ui_->main->set_lan_mode(config_center_->GetConnectionMode() ==
|
||||||
|
ConfigCenter::CONNECTION_MODE::DIRECT_IP);
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
ui_->shutdown_guard =
|
ui_->shutdown_guard =
|
||||||
std::make_unique<ShutdownSessionGuard>(windows_rdp_handoff_connected_);
|
std::make_unique<ShutdownSessionGuard>(windows_rdp_handoff_connected_);
|
||||||
@@ -1436,7 +1441,6 @@ void GuiApplication::InitializeSystemTray() {
|
|||||||
};
|
};
|
||||||
auto exit_app = [this] {
|
auto exit_app = [this] {
|
||||||
exit_ = true;
|
exit_ = true;
|
||||||
slint::quit_event_loop();
|
|
||||||
};
|
};
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
ui_->tray = std::make_unique<WinTray>(
|
ui_->tray = std::make_unique<WinTray>(
|
||||||
@@ -1485,6 +1489,8 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
|
|||||||
main->set_language_index(language_button_value_);
|
main->set_language_index(language_button_value_);
|
||||||
main->set_codec_index(video_encode_format_button_value_);
|
main->set_codec_index(video_encode_format_button_value_);
|
||||||
main->set_turn_enabled(enable_turn_);
|
main->set_turn_enabled(enable_turn_);
|
||||||
|
main->set_ip_direct_enabled_draft(config_center_->IsIpDirectEnabled());
|
||||||
|
main->set_settings_save_error("");
|
||||||
main->set_force_relay_enabled(config_center_->IsForceRelay());
|
main->set_force_relay_enabled(config_center_->IsForceRelay());
|
||||||
main->set_self_hosted_enabled(enable_self_hosted_);
|
main->set_self_hosted_enabled(enable_self_hosted_);
|
||||||
main->set_autostart_enabled(enable_autostart_);
|
main->set_autostart_enabled(enable_autostart_);
|
||||||
@@ -1544,6 +1550,19 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
|
|||||||
SendKeyInput(std::string(text), pressed, ctrl, alt, shift, meta);
|
SendKeyInput(std::string(text), pressed, ctrl, alt, shift, meta);
|
||||||
});
|
});
|
||||||
main->on_open_gallery([this] { ShowGalleryWindow(); });
|
main->on_open_gallery([this] { ShowGalleryWindow(); });
|
||||||
|
main->on_connection_mode_changed([this, main](bool lan_mode) {
|
||||||
|
const auto mode = lan_mode ? ConfigCenter::CONNECTION_MODE::DIRECT_IP
|
||||||
|
: ConfigCenter::CONNECTION_MODE::DEVICE_ID;
|
||||||
|
if (config_center_->SetConnectionMode(mode) != 0) {
|
||||||
|
LOG_WARN("Could not save connection mode preference");
|
||||||
|
main->set_offline_warning(UiText(
|
||||||
|
localization_language_index_ == 0
|
||||||
|
? "无法保存连接模式,下次启动可能不会恢复当前模式。"
|
||||||
|
: localization_language_index_ == 2
|
||||||
|
? "Не удалось сохранить режим подключения. При следующем запуске он может не восстановиться."
|
||||||
|
: "Could not save the connection mode. It may not be restored on the next launch."));
|
||||||
|
}
|
||||||
|
});
|
||||||
main->on_toggle_gallery_fullscreen(
|
main->on_toggle_gallery_fullscreen(
|
||||||
[this] { ToggleGalleryFullscreen(); });
|
[this] { ToggleGalleryFullscreen(); });
|
||||||
main->on_gallery_action(
|
main->on_gallery_action(
|
||||||
@@ -1678,8 +1697,19 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
|
|||||||
show_password_ = !show_password_;
|
show_password_ = !show_password_;
|
||||||
main->set_password_visible(show_password_);
|
main->set_password_visible(show_password_);
|
||||||
});
|
});
|
||||||
main->on_reset_password([this](slint::SharedString value) {
|
main->on_reset_password([this, main](slint::SharedString value) {
|
||||||
return RequestPasswordChange(std::string(value));
|
const std::string password(value);
|
||||||
|
if (!main->get_lan_mode()) return RequestPasswordChange(password);
|
||||||
|
if (!lan::ValidPassword(password)) return false;
|
||||||
|
if (!LanAvailable()) return true; // The initialization error is shown on the main view.
|
||||||
|
try {
|
||||||
|
lan_error_.clear();
|
||||||
|
lan_->SetPassword(password);
|
||||||
|
main->set_lan_password_pending(lan_->password_pending());
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
lan_error_ = error.what();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
main->on_refresh_password([this] {
|
main->on_refresh_password([this] {
|
||||||
const auto password = GenerateRandomPassword(password_saved_);
|
const auto password = GenerateRandomPassword(password_saved_);
|
||||||
@@ -1691,6 +1721,106 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
|
|||||||
}
|
}
|
||||||
RequestPasswordChange(*password);
|
RequestPasswordChange(*password);
|
||||||
});
|
});
|
||||||
|
main->on_lan_select_address([this, main](slint::SharedString address) {
|
||||||
|
if (!LanAvailable()) {
|
||||||
|
main->set_lan_endpoint_error(UiText(lan_error_.empty() ? "Busy" : lan_error_));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
lan_->identity().SelectAddress(std::string(address));
|
||||||
|
const auto port =
|
||||||
|
UiText(std::to_string(lan_->identity().PortFor(std::string(address))));
|
||||||
|
main->set_lan_listener_ip(address);
|
||||||
|
main->set_lan_listener_port(port);
|
||||||
|
main->set_lan_local_port(port);
|
||||||
|
main->set_lan_port_dirty(false);
|
||||||
|
main->set_lan_endpoint_error("");
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
main->set_lan_endpoint_error(UiText(error.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
main->on_lan_configure([this, main](slint::SharedString address,
|
||||||
|
slint::SharedString port) {
|
||||||
|
if (!LanAvailable()) {
|
||||||
|
main->set_lan_endpoint_error(UiText(lan_error_.empty() ? "Busy" : lan_error_));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const auto port_number = lan::ParsePort(std::string(port));
|
||||||
|
lan_->Configure(std::string(address), port_number);
|
||||||
|
const auto normalized_port = UiText(std::to_string(port_number));
|
||||||
|
main->set_lan_listener_ip(address);
|
||||||
|
main->set_lan_listener_port(normalized_port);
|
||||||
|
main->set_lan_local_port(normalized_port);
|
||||||
|
main->set_lan_port_dirty(false);
|
||||||
|
lan_error_.clear();
|
||||||
|
main->set_lan_endpoint_error("");
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
main->set_lan_endpoint_error(UiText(error.what()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
main->on_lan_reset_password([this] {
|
||||||
|
if (LanAvailable()) {
|
||||||
|
lan_error_.clear();
|
||||||
|
lan_->ResetPassword();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
main->on_lan_copy_text([this](slint::SharedString value) {
|
||||||
|
if (!SDL_SetClipboardText(std::string(value).c_str())) {
|
||||||
|
lan_error_ = "Cannot copy text";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
main->on_lan_prepare_connect([this, main](slint::SharedString endpoint) {
|
||||||
|
try {
|
||||||
|
const auto [address, port] = lan::ParseEndpoint(Trim(std::string(endpoint)), 52888);
|
||||||
|
if (!LanAvailable()) return UiText(lan_error_.empty() ? "Busy" : lan_error_);
|
||||||
|
main->set_lan_remote_address(UiText(address));
|
||||||
|
main->set_lan_remote_port(UiText(std::to_string(port)));
|
||||||
|
main->set_lan_retry_after(lan_->RetryAfter(address, port));
|
||||||
|
// History is newest first. Match both address and port, and only use
|
||||||
|
// the latest matching record when an endpoint has been reused.
|
||||||
|
for (const auto& [_, recent] : recent_connections_) {
|
||||||
|
if (recent.lan.device_id.empty() || recent.lan.address != address ||
|
||||||
|
recent.lan.port != port)
|
||||||
|
continue;
|
||||||
|
if (recent.remember_password && lan::ValidPassword(recent.password)) {
|
||||||
|
main->set_lan_access_password(UiText(recent.password));
|
||||||
|
main->set_lan_remember_password(true);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lan_error_.clear();
|
||||||
|
main->set_lan_message("");
|
||||||
|
return slint::SharedString{};
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
return UiText(error.what());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
main->on_lan_cancel_connect([this, main] {
|
||||||
|
if (lan_ && lan_connecting_) lan_->Close(lan_connecting_);
|
||||||
|
lan_connecting_.reset();
|
||||||
|
main->set_lan_connecting(false);
|
||||||
|
lan_error_.clear();
|
||||||
|
main->set_lan_message("");
|
||||||
|
});
|
||||||
|
main->on_lan_connect([this, main](slint::SharedString address,
|
||||||
|
slint::SharedString port,
|
||||||
|
slint::SharedString password, bool remember) {
|
||||||
|
try {
|
||||||
|
ConnectLan(std::string(address), lan::ParsePort(std::string(port)),
|
||||||
|
std::string(password), remember);
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
lan_error_ = error.what();
|
||||||
|
}
|
||||||
|
main->set_lan_connecting(lan_connecting_ != nullptr);
|
||||||
|
main->set_lan_message(UiText(lan_error_));
|
||||||
|
});
|
||||||
main->on_connect_requested(
|
main->on_connect_requested(
|
||||||
[this](slint::SharedString id) { ConnectFromUi(std::string(id)); });
|
[this](slint::SharedString id) { ConnectFromUi(std::string(id)); });
|
||||||
main->on_format_remote_id([](slint::SharedString value) {
|
main->on_format_remote_id([](slint::SharedString value) {
|
||||||
@@ -1703,7 +1833,7 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
|
|||||||
if (auto props = FindRemoteSession(remote_id);
|
if (auto props = FindRemoteSession(remote_id);
|
||||||
props && props->connection_status_ == ConnectionStatus::Connected &&
|
props && props->connection_status_ == ConnectionStatus::Connected &&
|
||||||
!props->closing_) {
|
!props->closing_) {
|
||||||
CloseStreamTab(remote_id);
|
CloseStreamTab(props->remote_id_);
|
||||||
SyncMainWindow();
|
SyncMainWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2266,9 +2396,101 @@ void GuiApplication::BindServerCallbacks() {
|
|||||||
|
|
||||||
void GuiApplication::Tick() {
|
void GuiApplication::Tick() {
|
||||||
if (exit_) {
|
if (exit_) {
|
||||||
slint::quit_event_loop();
|
BeginShutdown();
|
||||||
|
if (ShutdownDrained()) slint::quit_event_loop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
TickLan();
|
||||||
|
if (LanAvailable()) {
|
||||||
|
const auto address = lan_->identity().settings().value("address", "");
|
||||||
|
ui_->main->set_lan_password_pending(lan_->password_pending());
|
||||||
|
ui_->main->set_lan_listening(lan_->listening());
|
||||||
|
if (lan_connecting_ && lan_connecting_->authorized) {
|
||||||
|
ui_->main->set_lan_open(false);
|
||||||
|
lan_connecting_.reset();
|
||||||
|
} else if (lan_connecting_ && lan_connecting_->closing) {
|
||||||
|
lan_connecting_.reset();
|
||||||
|
}
|
||||||
|
ui_->main->set_lan_connecting(lan_connecting_ != nullptr);
|
||||||
|
int retry_after = 0;
|
||||||
|
if (ui_->main->get_lan_open()) {
|
||||||
|
try {
|
||||||
|
retry_after = lan_->RetryAfter(
|
||||||
|
std::string(ui_->main->get_lan_remote_address()),
|
||||||
|
lan::ParsePort(std::string(ui_->main->get_lan_remote_port())));
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
// An incomplete address has no cooldown; input validation handles it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui_->main->set_lan_retry_after(retry_after);
|
||||||
|
if (auto password = lan_->TakeNewPassword(); !password.empty())
|
||||||
|
ui_->main->set_lan_new_password(UiText(password));
|
||||||
|
const auto& available_interfaces = lan_->interfaces();
|
||||||
|
const auto previous = ui_->main->get_lan_interfaces();
|
||||||
|
bool changed = !previous || previous->row_count() != available_interfaces.size();
|
||||||
|
for (size_t i = 0; !changed && i < available_interfaces.size(); ++i)
|
||||||
|
changed = std::string_view(previous->row_data(i).value()) !=
|
||||||
|
available_interfaces[i].address;
|
||||||
|
if (changed) {
|
||||||
|
std::vector<slint::SharedString> addresses;
|
||||||
|
addresses.reserve(available_interfaces.size());
|
||||||
|
for (const auto& interface : available_interfaces)
|
||||||
|
addresses.push_back(UiText(interface.address));
|
||||||
|
ui_->main->set_lan_interfaces(
|
||||||
|
std::make_shared<slint::VectorModel<slint::SharedString>>(std::move(addresses)));
|
||||||
|
}
|
||||||
|
// Keep the current displayed address while it remains available, including
|
||||||
|
// when a previously missing preferred address returns during a port edit.
|
||||||
|
const auto previous_address = std::string(ui_->main->get_lan_listener_ip());
|
||||||
|
auto display_address = previous_address.empty() ? address : previous_address;
|
||||||
|
int interface_index = -1;
|
||||||
|
for (size_t i = 0; i < available_interfaces.size(); ++i)
|
||||||
|
if (available_interfaces[i].address == display_address) {
|
||||||
|
interface_index = static_cast<int>(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (interface_index < 0) {
|
||||||
|
interface_index = available_interfaces.empty() ? -1 : 0;
|
||||||
|
display_address = interface_index == 0
|
||||||
|
? available_interfaces.front().address : "";
|
||||||
|
}
|
||||||
|
const auto port =
|
||||||
|
UiText(std::to_string(lan_->identity().PortFor(display_address)));
|
||||||
|
if (previous_address != display_address) {
|
||||||
|
// A disappearing address must not transfer its unfinished port edit to
|
||||||
|
// another interface. Explicit selection commits the old edit first.
|
||||||
|
if (!previous_address.empty() && ui_->main->get_lan_port_dirty()) {
|
||||||
|
ui_->main->set_lan_access_warning_endpoint(UiText(previous_address));
|
||||||
|
ui_->main->set_lan_access_warning("Selected LAN interface is unavailable");
|
||||||
|
}
|
||||||
|
ui_->main->set_lan_port_dirty(false);
|
||||||
|
ui_->main->set_lan_endpoint_error("");
|
||||||
|
ui_->main->set_lan_local_port(port);
|
||||||
|
}
|
||||||
|
ui_->main->set_lan_listener_ip(UiText(display_address));
|
||||||
|
ui_->main->set_lan_listener_port(port);
|
||||||
|
ui_->main->set_lan_interface_index(interface_index);
|
||||||
|
ui_->main->set_lan_interface_draft_index(interface_index);
|
||||||
|
if (!ui_->main->get_lan_port_editing() && !ui_->main->get_lan_port_dirty())
|
||||||
|
ui_->main->set_lan_local_port(port);
|
||||||
|
ui_->main->set_lan_listener_error(UiText(
|
||||||
|
display_address.empty() ? "Selected LAN interface is unavailable"
|
||||||
|
: lan_->ListenerError(display_address)));
|
||||||
|
// Show failures one address at a time, without interrupting edits or another
|
||||||
|
// dialog. A working listener must not clear another address's error.
|
||||||
|
if (ui_->main->get_lan_mode() && ui_->main->get_can_show_sponsorship() &&
|
||||||
|
!ui_->main->get_lan_port_editing() && !ui_->main->get_lan_port_dirty()) {
|
||||||
|
if (auto issue = lan_->TakeListenerWarning()) {
|
||||||
|
ui_->main->set_lan_access_warning_endpoint(
|
||||||
|
UiText(issue->address + ":" + std::to_string(issue->port)));
|
||||||
|
ui_->main->set_lan_access_warning(UiText(issue->error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto lan_message = lan_error_.rfind("ConnectionFailed", 0) == 0
|
||||||
|
? std::string("ConnectionFailed")
|
||||||
|
: lan_error_;
|
||||||
|
ui_->main->set_lan_message(UiText(lan_message));
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
if (ui_->shutdown_guard) {
|
if (ui_->shutdown_guard) {
|
||||||
ui_->shutdown_guard->Update(ui_->shutdown_reason);
|
ui_->shutdown_guard->Update(ui_->shutdown_reason);
|
||||||
@@ -2461,7 +2683,7 @@ void GuiApplication::ShareLocalCursorState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!is_server_mode_ || !peer_ || connected_controllers.empty()) {
|
if (!is_server_mode_ || connected_controllers.empty()) {
|
||||||
cursor_delivery_states_.clear();
|
cursor_delivery_states_.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2587,9 +2809,8 @@ void GuiApplication::ShareLocalCursorState() {
|
|||||||
action.type = ControlType::cursor_state;
|
action.type = ControlType::cursor_state;
|
||||||
action.cs = outgoing;
|
action.cs = outgoing;
|
||||||
const std::string message = action.to_json();
|
const std::string message = action.to_json();
|
||||||
const int result = SendReliableDataFrameToPeer(
|
const int result = SendControllerData(recipient.remote_id, message.c_str(),
|
||||||
peer_, message.c_str(), message.size(), mouse_label_.c_str(),
|
message.size(), mouse_label_.c_str());
|
||||||
recipient.remote_id.c_str(), recipient.remote_id.size());
|
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
LOG_WARN("Send cursor state to [{}] failed, ret={}",
|
LOG_WARN("Send cursor state to [{}] failed, ret={}",
|
||||||
recipient.remote_id, result);
|
recipient.remote_id, result);
|
||||||
@@ -3008,8 +3229,9 @@ void GuiApplication::SyncMainWindow() {
|
|||||||
std::vector<slint::SharedString> recent_groups{
|
std::vector<slint::SharedString> recent_groups{
|
||||||
tr_group("全部设备", "All devices", "Все устройства"),
|
tr_group("全部设备", "All devices", "Все устройства"),
|
||||||
tr_group("未分组", "Ungrouped", "Без группы")};
|
tr_group("未分组", "Ungrouped", "Без группы")};
|
||||||
|
const bool lan_mode = ui_->main->get_lan_mode();
|
||||||
int recent_group_index = 0;
|
int recent_group_index = 0;
|
||||||
if (ui_->recent_group_filter) {
|
if (!lan_mode && ui_->recent_group_filter) {
|
||||||
if (ui_->recent_group_filter->empty()) recent_group_index = 1;
|
if (ui_->recent_group_filter->empty()) recent_group_index = 1;
|
||||||
else {
|
else {
|
||||||
const auto found = std::find(ui_->gallery.groups.begin(),
|
const auto found = std::find(ui_->gallery.groups.begin(),
|
||||||
@@ -3048,10 +3270,12 @@ void GuiApplication::SyncMainWindow() {
|
|||||||
};
|
};
|
||||||
std::ostringstream signature_builder;
|
std::ostringstream signature_builder;
|
||||||
signature_builder << config_center_->IsSaveRemotePreviews() << '\n'
|
signature_builder << config_center_->IsSaveRemotePreviews() << '\n'
|
||||||
|
<< lan_mode << '\n'
|
||||||
<< bool(ui_->recent_group_filter) << '\0'
|
<< bool(ui_->recent_group_filter) << '\0'
|
||||||
<< ui_->recent_group_filter.value_or("") << '\n';
|
<< ui_->recent_group_filter.value_or("") << '\n';
|
||||||
for (const auto& [key, connection] : recent_connections_) {
|
for (const auto& [key, connection] : recent_connections_) {
|
||||||
signature_builder << key << '\0' << connection.remote_id << '\0'
|
signature_builder << key << '\0' << connection.remote_id << '\0'
|
||||||
|
<< connection.lan.address << ':' << connection.lan.port << '\0'
|
||||||
<< connection.remote_host_name << '\0'
|
<< connection.remote_host_name << '\0'
|
||||||
<< static_cast<int>(connection.platform) << '\0'
|
<< static_cast<int>(connection.platform) << '\0'
|
||||||
<< settings_.RecentConnectionDisplayName(connection)
|
<< settings_.RecentConnectionDisplayName(connection)
|
||||||
@@ -3072,7 +3296,9 @@ void GuiApplication::SyncMainWindow() {
|
|||||||
std::vector<ui::RecentConnection> model;
|
std::vector<ui::RecentConnection> model;
|
||||||
model.reserve(recent_connections_.size());
|
model.reserve(recent_connections_.size());
|
||||||
for (const auto& [_, connection] : recent_connections_) {
|
for (const auto& [_, connection] : recent_connections_) {
|
||||||
if (!ui_->gallery.MatchesGroup(connection.remote_id, ui_->recent_group_filter))
|
if (lan_mode != !connection.lan.device_id.empty()) continue;
|
||||||
|
if (!lan_mode &&
|
||||||
|
!ui_->gallery.MatchesGroup(connection.remote_id, ui_->recent_group_filter))
|
||||||
continue;
|
continue;
|
||||||
// Slint-generated scalar fields have no defaults. With no image,
|
// Slint-generated scalar fields have no defaults. With no image,
|
||||||
// has_thumbnail must remain false so the remote platform is rendered.
|
// has_thumbnail must remain false so the remote platform is rendered.
|
||||||
@@ -3082,7 +3308,12 @@ void GuiApplication::SyncMainWindow() {
|
|||||||
item.group_names = UiText(group_labels[connection.remote_id]);
|
item.group_names = UiText(group_labels[connection.remote_id]);
|
||||||
item.display_name =
|
item.display_name =
|
||||||
UiText(settings_.RecentConnectionDisplayName(connection));
|
UiText(settings_.RecentConnectionDisplayName(connection));
|
||||||
item.online = device_presence_cache_.IsOnline(connection.remote_id);
|
item.lan = !connection.lan.device_id.empty();
|
||||||
|
item.endpoint = UiText(item.lan ? connection.lan.address + ":" +
|
||||||
|
std::to_string(connection.lan.port)
|
||||||
|
: "");
|
||||||
|
item.online =
|
||||||
|
!item.lan && device_presence_cache_.IsOnline(connection.remote_id);
|
||||||
item.connected = is_connected(connection.remote_id);
|
item.connected = is_connected(connection.remote_id);
|
||||||
item.platform = UiText(HostPlatformName(connection.platform));
|
item.platform = UiText(HostPlatformName(connection.platform));
|
||||||
std::vector<unsigned char> rgba;
|
std::vector<unsigned char> rgba;
|
||||||
@@ -3163,7 +3394,9 @@ void GuiApplication::SyncConnectionDialog() {
|
|||||||
} else if (auth.error == minirtc::JoinFailureKind::NotAuthenticated) {
|
} else if (auth.error == minirtc::JoinFailureKind::NotAuthenticated) {
|
||||||
text = localization::connection_not_authenticated[localization_language_index_];
|
text = localization::connection_not_authenticated[localization_language_index_];
|
||||||
} else {
|
} else {
|
||||||
text = ConnectionStatusText(status, localization_language_index_);
|
text = props->lan_session_ && !props->lan_session_->error.empty()
|
||||||
|
? props->lan_session_->error
|
||||||
|
: ConnectionStatusText(status, localization_language_index_);
|
||||||
}
|
}
|
||||||
if (auth.retry_after > 0) {
|
if (auth.retry_after > 0) {
|
||||||
char retry_text[256];
|
char retry_text[256];
|
||||||
@@ -4260,6 +4493,7 @@ bool GuiApplication::ValidateSelfHostedSettingsFromUi(bool gallery_window) {
|
|||||||
bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
|
bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
|
||||||
const auto previous_server = GetSignalServerSettings(*config_center_);
|
const auto previous_server = GetSignalServerSettings(*config_center_);
|
||||||
auto& main = ui_->Window(gallery_window);
|
auto& main = ui_->Window(gallery_window);
|
||||||
|
main->set_settings_save_error("");
|
||||||
const bool has_endpoint = main->get_self_hosted_enabled() ||
|
const bool has_endpoint = main->get_self_hosted_enabled() ||
|
||||||
!std::string(main->get_server_host()).empty() ||
|
!std::string(main->get_server_host()).empty() ||
|
||||||
!std::string(main->get_server_port()).empty();
|
!std::string(main->get_server_port()).empty();
|
||||||
@@ -4298,6 +4532,15 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
|
|||||||
main->set_self_host_settings_open(true);
|
main->set_self_host_settings_open(true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (config_center_->SetIpDirectEnabled(main->get_ip_direct_enabled_draft()) != 0) {
|
||||||
|
if (wall_changed && !ui_->gallery.Save(ui_->gallery_path))
|
||||||
|
LOG_ERROR("Failed to restore DeskHub settings after IP direct save failure");
|
||||||
|
main->set_settings_save_error(UiText(
|
||||||
|
localization_language_index_ == 0 ? "无法保存 IP 直连设置,请重试。"
|
||||||
|
: localization_language_index_ == 2 ? "Не удалось сохранить настройки IP. Повторите попытку."
|
||||||
|
: "Could not save direct IP settings. Try again."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
language_button_value_ =
|
language_button_value_ =
|
||||||
localization::detail::ClampLanguageIndex(main->get_language_index());
|
localization::detail::ClampLanguageIndex(main->get_language_index());
|
||||||
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
||||||
@@ -4407,9 +4650,12 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
|
|||||||
#endif
|
#endif
|
||||||
if (ui_) {
|
if (ui_) {
|
||||||
ui_->localized_language = -1;
|
ui_->localized_language = -1;
|
||||||
|
for (const auto& window : {ui_->main, ui_->wall})
|
||||||
|
window->set_ip_direct_enabled(config_center_->IsIpDirectEnabled());
|
||||||
if (!ui_->main->get_settings_open()) ResetSettingsUi();
|
if (!ui_->main->get_settings_open()) ResetSettingsUi();
|
||||||
if (!ui_->wall->get_settings_open()) ResetSettingsUi(true);
|
if (!ui_->wall->get_settings_open()) ResetSettingsUi(true);
|
||||||
}
|
}
|
||||||
|
ApplyLanSettings();
|
||||||
if (reconnect && !HasActiveSession()) {
|
if (reconnect && !HasActiveSession()) {
|
||||||
// Tick recreates the peer after asynchronous cleanup, using the committed
|
// Tick recreates the peer after asynchronous cleanup, using the committed
|
||||||
// configuration. Keep the application and its console running throughout.
|
// configuration. Keep the application and its console running throughout.
|
||||||
@@ -4434,6 +4680,26 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
|
|||||||
|
|
||||||
void GuiApplication::ConnectFromUi(const std::string& input) {
|
void GuiApplication::ConnectFromUi(const std::string& input) {
|
||||||
ui_->gallery_auth_id.clear();
|
ui_->gallery_auth_id.clear();
|
||||||
|
if (input.rfind("lan-", 0) == 0) {
|
||||||
|
if (!LanAvailable()) return;
|
||||||
|
if (auto existing = FindRemoteSession(input);
|
||||||
|
existing && existing->connection_established_) {
|
||||||
|
existing->tab_opened_ = true;
|
||||||
|
focused_remote_id_ = existing->remote_id_;
|
||||||
|
need_to_create_stream_window_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto& [_, recent] : recent_connections_)
|
||||||
|
if (recent.remote_id == input && !recent.lan.device_id.empty()) {
|
||||||
|
ui_->main->invoke_request_lan_connection(
|
||||||
|
UiText(recent.lan.address +
|
||||||
|
(recent.lan.port == 52888
|
||||||
|
? ""
|
||||||
|
: ":" + std::to_string(recent.lan.port))));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const std::string remote_id = CompactPeerId(input);
|
const std::string remote_id = CompactPeerId(input);
|
||||||
if (!gallery::Workspace::ValidId(remote_id)) {
|
if (!gallery::Workspace::ValidId(remote_id)) {
|
||||||
ui_->main->set_connection_input_error(
|
ui_->main->set_connection_input_error(
|
||||||
@@ -5209,6 +5475,7 @@ void GuiApplication::GalleryAction(const std::string& action,
|
|||||||
std::set<std::string> seen;
|
std::set<std::string> seen;
|
||||||
int available = 0;
|
int available = 0;
|
||||||
for (const auto& [_, recent] : recent_connections_) {
|
for (const auto& [_, recent] : recent_connections_) {
|
||||||
|
if (!recent.lan.device_id.empty()) continue;
|
||||||
if (!gallery::Workspace::ValidId(recent.remote_id) ||
|
if (!gallery::Workspace::ValidId(recent.remote_id) ||
|
||||||
!seen.insert(recent.remote_id).second)
|
!seen.insert(recent.remote_id).second)
|
||||||
continue;
|
continue;
|
||||||
@@ -5697,8 +5964,9 @@ void GuiApplication::SyncGallery() {
|
|||||||
};
|
};
|
||||||
const auto name = [this](const std::string& id) {
|
const auto name = [this](const std::string& id) {
|
||||||
for (const auto& [_, recent] : recent_connections_)
|
for (const auto& [_, recent] : recent_connections_)
|
||||||
if (recent.remote_id == id)
|
if (recent.lan.device_id.empty())
|
||||||
return settings_.RecentConnectionDisplayName(recent);
|
if (recent.remote_id == id)
|
||||||
|
return settings_.RecentConnectionDisplayName(recent);
|
||||||
Thumbnail::RecentConnection device{};
|
Thumbnail::RecentConnection device{};
|
||||||
device.remote_id = id;
|
device.remote_id = id;
|
||||||
if (auto props = FindRemoteSession(id)) {
|
if (auto props = FindRemoteSession(id)) {
|
||||||
@@ -5716,7 +5984,7 @@ void GuiApplication::SyncGallery() {
|
|||||||
{
|
{
|
||||||
std::shared_lock lock(remote_sessions_mutex_);
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
for (const auto& [id, props] : remote_sessions_)
|
for (const auto& [id, props] : remote_sessions_)
|
||||||
if (props && !props->closing_ &&
|
if (props && !props->lan_session_ && !props->closing_ &&
|
||||||
props->connection_status_ == ConnectionStatus::Connected)
|
props->connection_status_ == ConnectionStatus::Connected)
|
||||||
connected_ids.push_back(id);
|
connected_ids.push_back(id);
|
||||||
}
|
}
|
||||||
@@ -5816,11 +6084,12 @@ void GuiApplication::SyncGallery() {
|
|||||||
bool remember = operation.remember_password;
|
bool remember = operation.remember_password;
|
||||||
if (!operation.password)
|
if (!operation.password)
|
||||||
for (const auto& [_, recent] : recent_connections_)
|
for (const auto& [_, recent] : recent_connections_)
|
||||||
if (recent.remote_id == operation.id) {
|
if (recent.lan.device_id.empty())
|
||||||
password = recent.password;
|
if (recent.remote_id == operation.id) {
|
||||||
remember = recent.remember_password;
|
password = recent.password;
|
||||||
break;
|
remember = recent.remember_password;
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
// Join itself determines availability. Background probes must never
|
// Join itself determines availability. Background probes must never
|
||||||
// overwrite the interactive page's single pending presence probe.
|
// overwrite the interactive page's single pending presence probe.
|
||||||
ConnectTo(operation.id, password.c_str(), remember, true, true);
|
ConnectTo(operation.id, password.c_str(), remember, true, true);
|
||||||
@@ -5970,6 +6239,7 @@ void GuiApplication::SyncGallery() {
|
|||||||
tile.selected = wall.selected.count(id);
|
tile.selected = wall.selected.count(id);
|
||||||
auto props = FindRemoteSession(id);
|
auto props = FindRemoteSession(id);
|
||||||
for (const auto& [_, recent] : recent_connections_) {
|
for (const auto& [_, recent] : recent_connections_) {
|
||||||
|
if (!recent.lan.device_id.empty()) continue;
|
||||||
if (recent.remote_id != id) continue;
|
if (recent.remote_id != id) continue;
|
||||||
tile.host_name = UiText(recent.remote_host_name);
|
tile.host_name = UiText(recent.remote_host_name);
|
||||||
break;
|
break;
|
||||||
@@ -6205,6 +6475,9 @@ void GuiApplication::SyncGallery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::Cleanup() {
|
void GuiApplication::Cleanup() {
|
||||||
|
BeginShutdown();
|
||||||
|
while (!ShutdownDrained())
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||||
remote_input_.Stop();
|
remote_input_.Stop();
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed);
|
windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed);
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ struct TranslationRow {
|
|||||||
X(console_cli_help, u8"Linux 无头运行选项:\n --headless 自动选择已有桌面并启用控制台\n --headless-display DISPLAY 高级:指定已有显示会话\n --headless-size WIDTHxHEIGHT 新建虚拟屏幕;默认1920x1080,偶数尺寸320..8192\n --headless-session EXECUTABLE 启动桌面环境(startxfce4;none 仅启动裸屏幕)\n --no-headless 禁用自动回退\n正常启动会自动选择已有桌面,无需填写显示编号。控制台输入 settings 管理设置。\n", "Linux headless options:\n --headless Select an existing desktop and enable the console\n --headless-display DISPLAY Advanced: override desktop selection\n --headless-size WIDTHxHEIGHT New virtual display; default 1920x1080, even 320..8192\n --headless-session EXECUTABLE Start a desktop (startxfce4; none for a bare display)\n --no-headless Disable automatic fallback\nNormal startup selects your existing desktop automatically. Enter settings in the console to configure it.\n", u8"Параметры запуска без монитора:\n --headless Выбрать рабочий стол и включить консоль\n --headless-display DISPLAY Дополнительно: указать сеанс дисплея\n --headless-size WIDTHxHEIGHT Новый виртуальный экран; 1920x1080 по умолчанию, чётные размеры 320..8192\n --headless-session EXECUTABLE Запустить рабочий стол (startxfce4; none — только экран)\n --no-headless Отключить автоматический резервный запуск\nРабочий стол выбирается автоматически. Команда settings открывает настройки консоли.\n") \
|
X(console_cli_help, u8"Linux 无头运行选项:\n --headless 自动选择已有桌面并启用控制台\n --headless-display DISPLAY 高级:指定已有显示会话\n --headless-size WIDTHxHEIGHT 新建虚拟屏幕;默认1920x1080,偶数尺寸320..8192\n --headless-session EXECUTABLE 启动桌面环境(startxfce4;none 仅启动裸屏幕)\n --no-headless 禁用自动回退\n正常启动会自动选择已有桌面,无需填写显示编号。控制台输入 settings 管理设置。\n", "Linux headless options:\n --headless Select an existing desktop and enable the console\n --headless-display DISPLAY Advanced: override desktop selection\n --headless-size WIDTHxHEIGHT New virtual display; default 1920x1080, even 320..8192\n --headless-session EXECUTABLE Start a desktop (startxfce4; none for a bare display)\n --no-headless Disable automatic fallback\nNormal startup selects your existing desktop automatically. Enter settings in the console to configure it.\n", u8"Параметры запуска без монитора:\n --headless Выбрать рабочий стол и включить консоль\n --headless-display DISPLAY Дополнительно: указать сеанс дисплея\n --headless-size WIDTHxHEIGHT Новый виртуальный экран; 1920x1080 по умолчанию, чётные размеры 320..8192\n --headless-session EXECUTABLE Запустить рабочий стол (startxfce4; none — только экран)\n --no-headless Отключить автоматический резервный запуск\nРабочий стол выбирается автоматически. Команда settings открывает настройки консоли.\n") \
|
||||||
X(console_title, u8"CrossDesk 无头控制台", "CrossDesk headless console", u8"Консоль CrossDesk") \
|
X(console_title, u8"CrossDesk 无头控制台", "CrossDesk headless console", u8"Консоль CrossDesk") \
|
||||||
X(console_log_file, u8"日志文件", "Log file", u8"Файл журнала") \
|
X(console_log_file, u8"日志文件", "Log file", u8"Файл журнала") \
|
||||||
X(console_help, u8"命令: status 查看ID/密码/状态;settings 设置;password 修改密码;random 随机密码;help 帮助;quit 退出", "Commands: status, settings, password, random, help, quit", u8"Команды: status, settings, password, random, help, quit") \
|
X(console_help, u8"命令: status 查看 ID/密码/状态;settings 设置;password 修改密码;random 随机密码;help 帮助;quit 退出", "Commands: status, settings, password, random, help, quit", u8"Команды: status, settings, password, random, help, quit") \
|
||||||
X(console_status, u8"状态", "Status", u8"Состояние") \
|
X(console_status, u8"状态", "Status", u8"Состояние") \
|
||||||
X(console_online, u8"在线", "Online", u8"В сети") \
|
X(console_online, u8"在线", "Online", u8"В сети") \
|
||||||
X(console_offline, u8"未连接服务器", "Disconnected from server", u8"Нет подключения к серверу") \
|
X(console_offline, u8"未连接服务器", "Disconnected from server", u8"Нет подключения к серверу") \
|
||||||
@@ -100,7 +100,7 @@ struct TranslationRow {
|
|||||||
X(privacy_off, u8"隐私屏:已关闭", "Privacy screen: off", u8"Приватный экран: выключен") \
|
X(privacy_off, u8"隐私屏:已关闭", "Privacy screen: off", u8"Приватный экран: выключен") \
|
||||||
X(privacy_unsupported, u8"隐私屏:不可用", "Privacy screen: unavailable", u8"Приватный экран: недоступен") \
|
X(privacy_unsupported, u8"隐私屏:不可用", "Privacy screen: unavailable", u8"Приватный экран: недоступен") \
|
||||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
X(local_id, u8"本机 ID", "Local ID", u8"Локальный ID") \
|
||||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||||
u8"Скопировано в буфер обмена") \
|
u8"Скопировано в буфер обмена") \
|
||||||
X(password, u8"密码", "Password", u8"Пароль") \
|
X(password, u8"密码", "Password", u8"Пароль") \
|
||||||
@@ -110,7 +110,7 @@ struct TranslationRow {
|
|||||||
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
||||||
u8"Удаленный рабочий стол") \
|
u8"Удаленный рабочий стол") \
|
||||||
X(device_name, u8"设备名称", "Device Name", u8"Имя устройства") \
|
X(device_name, u8"设备名称", "Device Name", u8"Имя устройства") \
|
||||||
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
|
X(remote_id, u8"对端 ID", "Remote ID", u8"Удаленный ID") \
|
||||||
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
||||||
X(recent_connections, u8"近期连接", "Recent Connections", \
|
X(recent_connections, u8"近期连接", "Recent Connections", \
|
||||||
u8"Недавние подключения") \
|
u8"Недавние подключения") \
|
||||||
@@ -145,16 +145,16 @@ struct TranslationRow {
|
|||||||
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
|
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
|
||||||
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
||||||
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
||||||
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
X(send_sas, u8"发送 SAS", "Send SAS", u8"Отправить SAS") \
|
||||||
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
||||||
X(remote_password_box_visible, u8"远端密码框已出现", \
|
X(remote_password_box_visible, u8"远端密码框已出现", \
|
||||||
"Remote password box visible", u8"Окно ввода пароля видно") \
|
"Remote password box visible", u8"Окно ввода пароля видно") \
|
||||||
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送 SAS", \
|
||||||
"Remote lock screen visible, send SAS", \
|
"Remote lock screen visible, send SAS", \
|
||||||
u8"Видна блокировка, отправьте SAS") \
|
u8"Видна блокировка, отправьте SAS") \
|
||||||
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
||||||
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
|
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
|
||||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
X(remote_service_unavailable, u8"远端 Windows 服务不可用", \
|
||||||
"Remote Windows service unavailable", \
|
"Remote Windows service unavailable", \
|
||||||
u8"Служба Windows на удаленной стороне недоступна") \
|
u8"Служба Windows на удаленной стороне недоступна") \
|
||||||
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
||||||
@@ -288,7 +288,7 @@ struct TranslationRow {
|
|||||||
X(connection_not_authenticated, u8"登录状态已失效,请重新连接。", "Signaling login expired. Please reconnect.", u8"Сеанс входа истёк. Подключитесь заново.") \
|
X(connection_not_authenticated, u8"登录状态已失效,请重新连接。", "Signaling login expired. Please reconnect.", u8"Сеанс входа истёк. Подключитесь заново.") \
|
||||||
X(connection_retry_after, u8"请在 %d 秒后重试。", "Retry in %d seconds.", u8"Повторите через %d сек.") \
|
X(connection_retry_after, u8"请在 %d 秒后重试。", "Retry in %d seconds.", u8"Повторите через %d сек.") \
|
||||||
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
||||||
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
X(no_such_id, u8"无此 ID", "No such ID", u8"ID не найден") \
|
||||||
X(about, u8"关于", "About", u8"О программе") \
|
X(about, u8"关于", "About", u8"О программе") \
|
||||||
X(sponsorship_title, u8"赞助支持", "Sponsorship", u8"Поддержка проекта") \
|
X(sponsorship_title, u8"赞助支持", "Sponsorship", u8"Поддержка проекта") \
|
||||||
X(sponsorship_message, u8"如果 CrossDesk 对你有帮助,欢迎赞助支持项目的持续开发、跨平台测试与日常维护。感谢你的支持!", \
|
X(sponsorship_message, u8"如果 CrossDesk 对你有帮助,欢迎赞助支持项目的持续开发、跨平台测试与日常维护。感谢你的支持!", \
|
||||||
|
|||||||
@@ -226,15 +226,11 @@ int ClipboardController::SendToPeers(const std::string &text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (owner_.peer_) {
|
lock.unlock();
|
||||||
const int ret =
|
for (const auto& id : owner_.ConnectedControllers()) {
|
||||||
SendReliableDataFrame(owner_.peer_, text.data(), text.size(),
|
const int ret = owner_.SendControllerData(id, text.data(), text.size(),
|
||||||
owner_.clipboard_label_.c_str());
|
owner_.clipboard_label_.c_str());
|
||||||
if (ret != 0) {
|
if (ret != 0) return ret;
|
||||||
LOG_WARN("Failed to send clipboard data to peer [{}], ret={}",
|
|
||||||
owner_.remote_id_display_, ret);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -43,18 +43,17 @@ SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
|
|||||||
SpeakerCapturerFactory().Create());
|
SpeakerCapturerFactory().Create());
|
||||||
},
|
},
|
||||||
[this](unsigned char *data, size_t size, const char *) {
|
[this](unsigned char *data, size_t size, const char *) {
|
||||||
if (!owner_.peer_) return;
|
|
||||||
MiniRtcAudioFrame frame{};
|
MiniRtcAudioFrame frame{};
|
||||||
frame.data = reinterpret_cast<const char *>(data);
|
frame.data = reinterpret_cast<const char *>(data);
|
||||||
frame.size = size;
|
frame.size = size;
|
||||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
for (const auto& id : owner_.ConnectedControllers())
|
||||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
owner_.WithController(
|
||||||
for (const auto& [id, status] : owner_.connection_status_) {
|
id, [&](PeerPtr* peer, const std::string& wire) {
|
||||||
if (status == ConnectionStatus::Connected)
|
frame.captured_timestamp = GetSystemTimeMicros(peer);
|
||||||
SendAudioFrameToPeer(owner_.peer_, &frame,
|
return SendAudioFrameToPeer(peer, &frame,
|
||||||
owner_.audio_label_.c_str(), id.data(),
|
owner_.audio_label_.c_str(),
|
||||||
id.size());
|
wire.data(), wire.size());
|
||||||
}
|
});
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); }
|
SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); }
|
||||||
@@ -132,6 +131,10 @@ void SessionDeviceManager::Initialize() {
|
|||||||
DeviceControllerFactory::Device::Keyboard));
|
DeviceControllerFactory::Device::Keyboard));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SessionDeviceManager::EnsureCaptureReady() {
|
||||||
|
return screen_capturer_ ? 0 : InitializeScreenCapturer();
|
||||||
|
}
|
||||||
|
|
||||||
int SessionDeviceManager::InitializeScreenCapturer() {
|
int SessionDeviceManager::InitializeScreenCapturer() {
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
if (!owner_.EnsureMacScreenRecordingPermission()) {
|
if (!owner_.EnsureMacScreenRecordingPermission()) {
|
||||||
@@ -193,16 +196,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
|||||||
.count()
|
.count()
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
std::vector<std::string> connected_remote_ids;
|
const auto connected_remote_ids = owner_.ConnectedControllers();
|
||||||
{
|
|
||||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
|
||||||
connected_remote_ids.reserve(owner_.connection_status_.size());
|
|
||||||
for (const auto &[remote_id, status] : owner_.connection_status_) {
|
|
||||||
if (status == ConnectionStatus::Connected) {
|
|
||||||
connected_remote_ids.push_back(remote_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Capture can still deliver frames while ICE is gathering or after
|
// Capture can still deliver frames while ICE is gathering or after
|
||||||
// the final controller disconnects. Do not broadcast those frames to
|
// the final controller disconnects. Do not broadcast those frames to
|
||||||
@@ -232,28 +226,27 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
|||||||
has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs;
|
has_previous_frame && duration_ms >= kCaptureResumeKeyFrameGapMs;
|
||||||
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
const bool stream_changed = !last_video_frame_stream_id_.empty() &&
|
||||||
last_video_frame_stream_id_ != stream_id;
|
last_video_frame_stream_id_ != stream_id;
|
||||||
if (resumed_after_gap || stream_changed) {
|
|
||||||
if (RequestVideoKeyFrame(owner_.peer_, stream_id.c_str()) == 0) {
|
|
||||||
LOG_DEBUG("Request video key frame before sending captured frame, "
|
|
||||||
"stream='{}', gap_ms={}, stream_changed={}",
|
|
||||||
stream_id, duration_ms, stream_changed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MiniRtcVideoFrame frame{};
|
MiniRtcVideoFrame frame{};
|
||||||
frame.data = reinterpret_cast<const char *>(data);
|
frame.data = reinterpret_cast<const char *>(data);
|
||||||
frame.size = size;
|
frame.size = size;
|
||||||
frame.width = width;
|
frame.width = width;
|
||||||
frame.height = height;
|
frame.height = height;
|
||||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
|
||||||
frame.native_frame = native_frame;
|
frame.native_frame = native_frame;
|
||||||
const auto send_started = std::chrono::steady_clock::now();
|
const auto send_started = std::chrono::steady_clock::now();
|
||||||
uint64_t send_errors = 0;
|
uint64_t send_errors = 0;
|
||||||
for (const std::string &remote_id : connected_remote_ids) {
|
for (const std::string &remote_id : connected_remote_ids) {
|
||||||
if (!owner_.preview_subscriptions_.ShouldSend(remote_id, now_time,
|
if (!owner_.preview_subscriptions_.ShouldSend(remote_id, now_time,
|
||||||
stream_id)) continue;
|
stream_id)) continue;
|
||||||
if (SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
if (owner_.WithController(
|
||||||
remote_id.data(), remote_id.size()) != 0)
|
remote_id, [&](PeerPtr* peer, const std::string& wire) {
|
||||||
|
if (resumed_after_gap || stream_changed)
|
||||||
|
RequestVideoKeyFrame(peer, stream_id.c_str());
|
||||||
|
frame.captured_timestamp = GetSystemTimeMicros(peer);
|
||||||
|
return SendVideoFrameToPeer(peer, &frame, stream_id.c_str(),
|
||||||
|
wire.data(), wire.size());
|
||||||
|
}) != 0)
|
||||||
++send_errors;
|
++send_errors;
|
||||||
}
|
}
|
||||||
const auto send_us =
|
const auto send_us =
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public:
|
|||||||
void DestroyFactories();
|
void DestroyFactories();
|
||||||
|
|
||||||
int InitializeScreenCapturer();
|
int InitializeScreenCapturer();
|
||||||
|
int EnsureCaptureReady();
|
||||||
int StartScreenCapturer();
|
int StartScreenCapturer();
|
||||||
int StopScreenCapturer();
|
int StopScreenCapturer();
|
||||||
bool ReleaseRemoteMouseButtons();
|
bool ReleaseRemoteMouseButtons();
|
||||||
|
|||||||
@@ -94,13 +94,19 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
|
|||||||
peer = props ? props->peer_ : owner_.peer_;
|
peer = props ? props->peer_ : owner_.peer_;
|
||||||
events = props ? props->peer_events_ : owner_.peer_events_;
|
events = props ? props->peer_events_ : owner_.peer_events_;
|
||||||
}
|
}
|
||||||
|
if (!props && file.remote_id.rfind("lan/", 0) == 0) {
|
||||||
|
events = owner_.ControllerEvents(file.remote_id);
|
||||||
|
peer = events ? events->DirectPeer() : nullptr;
|
||||||
|
}
|
||||||
auto session = std::make_shared<file_transfer::Session>(
|
auto session = std::make_shared<file_transfer::Session>(
|
||||||
[peer, events, file, props](const char*, const char* data, size_t size) {
|
[peer, events, file, props](const char*, const char* data, size_t size) {
|
||||||
if (!peer || !events || (props && props->closing_)) return -1;
|
if (!peer || !events || (props && props->closing_)) return -1;
|
||||||
auto callback = events->EnterCallback();
|
auto callback = events->EnterCallback();
|
||||||
if (!callback) return -1;
|
if (!callback || !events->DataAllowed()) return -1;
|
||||||
return SendReliableDataFrameToPeer(peer, data, size, file.file_label.c_str(),
|
const auto wire_id = events->WireId(file.remote_id);
|
||||||
file.remote_id.c_str(), file.remote_id.size());
|
return SendReliableDataFrameToPeer(
|
||||||
|
peer, data, size, file.file_label.c_str(),
|
||||||
|
wire_id.data(), wire_id.size());
|
||||||
}, file_transfer::Session::Progress{});
|
}, file_transfer::Session::Progress{});
|
||||||
state.file_sent_bytes_ = 0; state.file_total_bytes_ = 0; state.file_send_rate_bps_ = 0;
|
state.file_sent_bytes_ = 0; state.file_total_bytes_ = 0; state.file_send_rate_bps_ = 0;
|
||||||
state.current_file_id_ = id;
|
state.current_file_id_ = id;
|
||||||
@@ -313,6 +319,10 @@ void FileTransferManager::Receive(const char* data, size_t size,
|
|||||||
peer = props ? props->peer_ : owner_.peer_;
|
peer = props ? props->peer_ : owner_.peer_;
|
||||||
events = props ? props->peer_events_ : owner_.peer_events_;
|
events = props ? props->peer_events_ : owner_.peer_events_;
|
||||||
}
|
}
|
||||||
|
if (!props && remote.rfind("lan/", 0) == 0) {
|
||||||
|
events = owner_.ControllerEvents(remote);
|
||||||
|
peer = events ? events->DirectPeer() : nullptr;
|
||||||
|
}
|
||||||
if (!peer || !events || events.get() != handler || (props && props->closing_)) return;
|
if (!peer || !events || events.get() != handler || (props && props->closing_)) return;
|
||||||
std::shared_ptr<FileReceiver> receiver;
|
std::shared_ptr<FileReceiver> receiver;
|
||||||
std::filesystem::path default_directory;
|
std::filesystem::path default_directory;
|
||||||
@@ -333,10 +343,12 @@ void FileTransferManager::Receive(const char* data, size_t size,
|
|||||||
});
|
});
|
||||||
entry.session->SetOnSendAck([peer, events, remote](const FileTransferAck& ack) {
|
entry.session->SetOnSendAck([peer, events, remote](const FileTransferAck& ack) {
|
||||||
auto callback = events->EnterCallback();
|
auto callback = events->EnterCallback();
|
||||||
if (!callback) return -1;
|
if (!callback || !events->DataAllowed()) return -1;
|
||||||
const auto bytes = EncodeFileTransferAck(ack);
|
const auto bytes = EncodeFileTransferAck(ack);
|
||||||
return SendReliableDataFrameToPeer(peer, bytes.data(), bytes.size(), kFileFeedbackStream,
|
const auto wire_id = events->WireId(remote);
|
||||||
remote.c_str(), remote.size());
|
return SendReliableDataFrameToPeer(
|
||||||
|
peer, bytes.data(), bytes.size(), kFileFeedbackStream,
|
||||||
|
wire_id.data(), wire_id.size());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
receiver = entry.session;
|
receiver = entry.session;
|
||||||
|
|||||||
@@ -744,6 +744,8 @@ std::string SettingsManager::RecentConnectionDisplayName(
|
|||||||
connection.remote_host_name != "unknown") {
|
connection.remote_host_name != "unknown") {
|
||||||
return connection.remote_host_name;
|
return connection.remote_host_name;
|
||||||
}
|
}
|
||||||
|
if (!connection.lan.device_id.empty())
|
||||||
|
return connection.lan.address + ":" + std::to_string(connection.lan.port);
|
||||||
return connection.remote_id;
|
return connection.remote_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ constexpr auto kPresenceRetryInterval = std::chrono::seconds(1);
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool GuiRuntime::HasActiveSession() {
|
bool GuiRuntime::HasActiveSession() {
|
||||||
|
if (lan_ && !lan_->Quiescent()) return true;
|
||||||
if (stream_window_inited_) return true;
|
if (stream_window_inited_) return true;
|
||||||
std::shared_lock lock(connection_status_mutex_);
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
return std::any_of(connection_status_.begin(), connection_status_.end(),
|
return std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||||
@@ -188,7 +189,8 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
|
|||||||
|
|
||||||
void GuiRuntime::HandleServerControllerDisconnected(
|
void GuiRuntime::HandleServerControllerDisconnected(
|
||||||
const std::string& remote_id, const char* reason) {
|
const std::string& remote_id, const char* reason) {
|
||||||
transfers_.Cancel(peer_events_.get(), remote_id);
|
if (auto events = ControllerEvents(remote_id))
|
||||||
|
transfers_.Cancel(events.get(), remote_id);
|
||||||
preview_subscriptions_.Remove(remote_id);
|
preview_subscriptions_.Remove(remote_id);
|
||||||
|
|
||||||
bool has_connected_controller = false;
|
bool has_connected_controller = false;
|
||||||
@@ -243,7 +245,15 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
|||||||
#endif
|
#endif
|
||||||
devices_.StopSpeakerCapturer();
|
devices_.StopSpeakerCapturer();
|
||||||
start_mouse_controller_ = false;
|
start_mouse_controller_ = false;
|
||||||
start_keyboard_capturer_ = false;
|
{
|
||||||
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
|
start_keyboard_capturer_ = std::any_of(
|
||||||
|
remote_sessions_.begin(), remote_sessions_.end(), [](const auto& item) {
|
||||||
|
const auto& session = item.second;
|
||||||
|
return session && !session->closing_ &&
|
||||||
|
session->connection_established_ && !session->wall_managed_;
|
||||||
|
});
|
||||||
|
}
|
||||||
remote_client_id_.clear();
|
remote_client_id_.clear();
|
||||||
audio_capture_ = false;
|
audio_capture_ = false;
|
||||||
}
|
}
|
||||||
@@ -294,10 +304,15 @@ int GuiRuntime::ForceSignalReconnect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||||
if (!props || props->closing_.exchange(true)) return;
|
if (!props) return;
|
||||||
|
if (props->lan_session_ && !props->lan_session_->closing) {
|
||||||
|
if (lan_) lan_->Close(props->lan_session_);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props->closing_.exchange(true)) return;
|
||||||
MuteRemoteAudio(props);
|
MuteRemoteAudio(props);
|
||||||
if (props->peer_events_) {
|
if (props->peer_events_) {
|
||||||
props->peer_events_->Deactivate();
|
props->peer_events_->StopAccepting();
|
||||||
transfers_.Cancel(props->peer_events_.get());
|
transfers_.Cancel(props->peer_events_.get());
|
||||||
}
|
}
|
||||||
props->connection_established_ = false;
|
props->connection_established_ = false;
|
||||||
@@ -350,7 +365,8 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask(
|
session_cleanup_tasks_[props->remote_id_] = session_cleanup_queue_.PostTask(
|
||||||
[props, peer, frame_snapshot, video_width, video_height, platform,
|
[props, peer, frame_snapshot, video_width, video_height, platform,
|
||||||
thumbnail = thumbnail_, queued_at]() mutable {
|
thumbnail = thumbnail_, queued_at]() mutable {
|
||||||
if (peer) {
|
if (props->peer_events_) props->peer_events_->Deactivate();
|
||||||
|
if (peer && !props->lan_session_) {
|
||||||
LOG_INFO("[{}] Background leave connection [{}]", props->local_id_,
|
LOG_INFO("[{}] Background leave connection [{}]", props->local_id_,
|
||||||
props->remote_id_);
|
props->remote_id_);
|
||||||
LeaveConnection(peer, props->remote_id_.c_str());
|
LeaveConnection(peer, props->remote_id_.c_str());
|
||||||
@@ -359,11 +375,20 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
if (thumbnail && props->was_connected_) {
|
if (thumbnail && props->was_connected_) {
|
||||||
const char* nv12 = frame_snapshot && !frame_snapshot->empty()
|
const char* nv12 = frame_snapshot && !frame_snapshot->empty()
|
||||||
? reinterpret_cast<const char*>(frame_snapshot->data()) : nullptr;
|
? reinterpret_cast<const char*>(frame_snapshot->data()) : nullptr;
|
||||||
|
const auto direct = props->lan_session_;
|
||||||
|
const Thumbnail::LanTarget target =
|
||||||
|
direct ? Thumbnail::LanTarget{direct->target.address,
|
||||||
|
direct->target.device_id,
|
||||||
|
direct->target.port}
|
||||||
|
: Thumbnail::LanTarget{};
|
||||||
|
const auto record_id = direct
|
||||||
|
? Thumbnail::LanRecordId(target.device_id)
|
||||||
|
: props->remote_id_;
|
||||||
if (thumbnail->SaveToThumbnail(
|
if (thumbnail->SaveToThumbnail(
|
||||||
nv12, video_width, video_height, props->remote_id_,
|
nv12, video_width, video_height, record_id,
|
||||||
props->remote_host_name_,
|
props->remote_host_name_,
|
||||||
props->remember_password_ ? props->remote_password_ : "",
|
props->remember_password_ ? props->remote_password_ : "",
|
||||||
platform) != 0) {
|
platform, direct ? &target : nullptr) != 0) {
|
||||||
LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_);
|
LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,6 +400,17 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiRuntime::CloseServerController(const std::string& remote_id) {
|
void GuiRuntime::CloseServerController(const std::string& remote_id) {
|
||||||
|
if (remote_id.rfind("lan/", 0) == 0) {
|
||||||
|
std::shared_ptr<lan::Session> session;
|
||||||
|
{
|
||||||
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
|
if (auto it = lan_controllers_.find(remote_id);
|
||||||
|
it != lan_controllers_.end())
|
||||||
|
session = it->second.session;
|
||||||
|
}
|
||||||
|
if (session && lan_) lan_->Close(session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id))
|
if (!peer_ || remote_id.empty() || controller_cleanup_tasks_.count(remote_id))
|
||||||
return;
|
return;
|
||||||
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
|
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
|
||||||
@@ -392,20 +428,17 @@ void GuiRuntime::CloseConnectionPeer() {
|
|||||||
announcements_.Reset();
|
announcements_.Reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
peer_events_->Deactivate();
|
peer_events_->StopAccepting();
|
||||||
transfers_.Cancel(peer_events_.get());
|
transfers_.Cancel(peer_events_.get());
|
||||||
announcements_.Reset();
|
announcements_.Reset();
|
||||||
std::vector<std::string> controllers;
|
std::vector<std::string> controllers;
|
||||||
{
|
{
|
||||||
std::shared_lock lock(connection_status_mutex_);
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
for (const auto& [id, status] : connection_status_) controllers.push_back(id);
|
for (const auto& [id, status] : connection_status_)
|
||||||
|
if (id.rfind("lan/", 0) != 0) controllers.push_back(id);
|
||||||
}
|
}
|
||||||
for (const auto& id : controllers)
|
for (const auto& id : controllers)
|
||||||
HandleServerControllerDisconnected(id, "peer_closed");
|
HandleServerControllerDisconnected(id, "peer_closed");
|
||||||
devices_.StopMouseController();
|
|
||||||
devices_.StopScreenCapturer();
|
|
||||||
devices_.StopSpeakerCapturer();
|
|
||||||
devices_.StopKeyboardCapturer();
|
|
||||||
signal_connected_ = false;
|
signal_connected_ = false;
|
||||||
device_presence_cache_.SetSignalConnected(false);
|
device_presence_cache_.SetSignalConnected(false);
|
||||||
signal_status_ = SignalStatus::SignalClosed;
|
signal_status_ = SignalStatus::SignalClosed;
|
||||||
@@ -418,6 +451,7 @@ void GuiRuntime::CloseConnectionPeer() {
|
|||||||
// these finish before the shared peer is destroyed.
|
// these finish before the shared peer is destroyed.
|
||||||
connection_peer_cleanup_ = session_cleanup_queue_.PostTask(
|
connection_peer_cleanup_ = session_cleanup_queue_.PostTask(
|
||||||
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
|
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
|
||||||
|
handler->Deactivate();
|
||||||
LOG_INFO("[{}] Background connection peer cleanup", id);
|
LOG_INFO("[{}] Background connection peer cleanup", id);
|
||||||
LeaveConnection(peer, id.c_str());
|
LeaveConnection(peer, id.c_str());
|
||||||
DestroyPeer(&peer);
|
DestroyPeer(&peer);
|
||||||
@@ -551,6 +585,12 @@ std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
|||||||
std::shared_lock lock(remote_sessions_mutex_);
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
auto it = remote_sessions_.find(remote_id);
|
auto it = remote_sessions_.find(remote_id);
|
||||||
if (it == remote_sessions_.end()) {
|
if (it == remote_sessions_.end()) {
|
||||||
|
if (remote_id.rfind("lan-", 0) == 0)
|
||||||
|
for (const auto& [_, props] : remote_sessions_)
|
||||||
|
if (props->lan_session_ && !props->closing_ &&
|
||||||
|
Thumbnail::LanRecordId(props->lan_session_->target.device_id) ==
|
||||||
|
remote_id)
|
||||||
|
return props;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return it->second;
|
return it->second;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ inline nlohmann::json BuildDevicePresenceRequest(
|
|||||||
const auto append = [&](const std::string& id) {
|
const auto append = [&](const std::string& id) {
|
||||||
// Recent connection keys contain Y/N plus display settings after the ID.
|
// Recent connection keys contain Y/N plus display settings after the ID.
|
||||||
const std::string pure_id = id.substr(0, id.find_first_of("YN"));
|
const std::string pure_id = id.substr(0, id.find_first_of("YN"));
|
||||||
|
if (pure_id.rfind("lan-", 0) == 0 || pure_id.rfind("lan/", 0) == 0) return;
|
||||||
if (!pure_id.empty() &&
|
if (!pure_id.empty() &&
|
||||||
std::find(devices.begin(), devices.end(), pure_id) == devices.end()) {
|
std::find(devices.begin(), devices.end(), pure_id) == devices.end()) {
|
||||||
devices.push_back(pure_id);
|
devices.push_back(pure_id);
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ GuiRuntime::~GuiRuntime() {
|
|||||||
windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed);
|
windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed);
|
||||||
if (windows_service_worker_) windows_service_worker_->Reset();
|
if (windows_service_worker_) windows_service_worker_->Reset();
|
||||||
#endif
|
#endif
|
||||||
|
lan_.reset();
|
||||||
transfers_.Shutdown();
|
transfers_.Shutdown();
|
||||||
WaitForSessionCleanup();
|
WaitForSessionCleanup();
|
||||||
}
|
}
|
||||||
@@ -292,7 +293,7 @@ int GuiRuntime::CreateConnectionPeer() {
|
|||||||
LOG_INFO("Create peer [{}] instance failed", client_id_);
|
LOG_INFO("Create peer [{}] instance failed", client_id_);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (0 == devices_.InitializeScreenCapturer()) {
|
if (0 == devices_.EnsureCaptureReady()) {
|
||||||
const auto &displays = devices_.display_info_list();
|
const auto &displays = devices_.display_info_list();
|
||||||
for (size_t index = 0; index < displays.size(); ++index) {
|
for (size_t index = 0; index < displays.size(); ++index) {
|
||||||
const std::string stream_id = MakeDisplayStreamId(index);
|
const std::string stream_id = MakeDisplayStreamId(index);
|
||||||
@@ -334,10 +335,18 @@ void GuiRuntime::HandleRecentConnections() {
|
|||||||
// Record a successful connection before a preview or host info arrives.
|
// Record a successful connection before a preview or host info arrives.
|
||||||
// The close path still updates its final thumbnail. Remember password
|
// The close path still updates its final thumbnail. Remember password
|
||||||
// controls only credential storage, not whether this device is listed.
|
// controls only credential storage, not whether this device is listed.
|
||||||
|
const auto direct = props->lan_session_;
|
||||||
|
const Thumbnail::LanTarget target =
|
||||||
|
direct ? Thumbnail::LanTarget{direct->target.address,
|
||||||
|
direct->target.device_id,
|
||||||
|
direct->target.port}
|
||||||
|
: Thumbnail::LanTarget{};
|
||||||
|
const auto record_id =
|
||||||
|
direct ? Thumbnail::LanRecordId(target.device_id) : props->remote_id_;
|
||||||
if (thumbnail_->SaveToThumbnail(
|
if (thumbnail_->SaveToThumbnail(
|
||||||
nullptr, 0, 0, props->remote_id_, host_name,
|
nullptr, 0, 0, record_id, host_name,
|
||||||
props->remember_password_ ? props->remote_password_ : "",
|
props->remember_password_ ? props->remote_password_ : "",
|
||||||
platform) == 0)
|
platform, direct ? &target : nullptr) == 0)
|
||||||
reload_recent_connections_ = true;
|
reload_recent_connections_ = true;
|
||||||
else
|
else
|
||||||
LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_);
|
LOG_ERROR("[{}] Failed to save recent connection", props->remote_id_);
|
||||||
@@ -356,7 +365,8 @@ void GuiRuntime::HandleRecentConnections() {
|
|||||||
|
|
||||||
recent_connection_ids_.clear();
|
recent_connection_ids_.clear();
|
||||||
for (const auto &conn : recent_connections_) {
|
for (const auto &conn : recent_connections_) {
|
||||||
recent_connection_ids_.push_back(conn.first);
|
if (conn.second.lan.device_id.empty())
|
||||||
|
recent_connection_ids_.push_back(conn.first);
|
||||||
}
|
}
|
||||||
need_to_send_recent_connections_ = true;
|
need_to_send_recent_connections_ = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "features/input/keyboard_controller.h"
|
#include "features/input/keyboard_controller.h"
|
||||||
#include "features/input/remote_input_dispatcher.h"
|
#include "features/input/remote_input_dispatcher.h"
|
||||||
#include "features/settings/settings_manager.h"
|
#include "features/settings/settings_manager.h"
|
||||||
|
#include "network/lan/lan_session_manager.h"
|
||||||
#include "privacy_controller.h"
|
#include "privacy_controller.h"
|
||||||
#include "runtime/gui_state.h"
|
#include "runtime/gui_state.h"
|
||||||
#include "runtime/announcement_inbox.h"
|
#include "runtime/announcement_inbox.h"
|
||||||
@@ -50,6 +51,35 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
static void SdlCaptureAudioIn(void* userdata, uint8_t* stream, int len);
|
static void SdlCaptureAudioIn(void* userdata, uint8_t* stream, int len);
|
||||||
static void SdlCaptureAudioOut(void* userdata, uint8_t* stream, int len);
|
static void SdlCaptureAudioOut(void* userdata, uint8_t* stream, int len);
|
||||||
|
|
||||||
|
void InitializeLan();
|
||||||
|
void ApplyLanSettings();
|
||||||
|
bool LanAvailable() const;
|
||||||
|
void TickLan();
|
||||||
|
void BeginShutdown();
|
||||||
|
bool ShutdownDrained();
|
||||||
|
void RegisterStreams(PeerPtr* peer, size_t display_count);
|
||||||
|
void ConnectLan(const std::string& address, unsigned port,
|
||||||
|
std::string password, bool remember_password = false);
|
||||||
|
int WithController(
|
||||||
|
const std::string& id,
|
||||||
|
const std::function<int(PeerPtr*, const std::string&)>& operation);
|
||||||
|
int SendControllerData(const std::string& id, const char* data, size_t size,
|
||||||
|
const char* label);
|
||||||
|
std::shared_ptr<PeerEventHandler> ControllerEvents(const std::string& id);
|
||||||
|
std::vector<std::string> ConnectedControllers();
|
||||||
|
std::unique_ptr<lan::SessionManager> lan_;
|
||||||
|
std::string lan_error_;
|
||||||
|
std::shared_ptr<lan::Session> lan_connecting_;
|
||||||
|
bool lan_stopping_ = false;
|
||||||
|
bool stopping_ = false;
|
||||||
|
std::chrono::steady_clock::time_point shutdown_report_at_{};
|
||||||
|
struct LanController {
|
||||||
|
std::shared_ptr<lan::Session> session;
|
||||||
|
std::shared_ptr<PeerEventHandler> events;
|
||||||
|
};
|
||||||
|
// Protected by remote_sessions_mutex_; snapshots acquire callback leases
|
||||||
|
// before releasing that lock, so removal cannot race a raw Peer use.
|
||||||
|
std::map<std::string, LanController> lan_controllers_;
|
||||||
int CreateConnectionPeer();
|
int CreateConnectionPeer();
|
||||||
int ConnectTo(const std::string& remote_id, const char* password,
|
int ConnectTo(const std::string& remote_id, const char* password,
|
||||||
bool remember_password, bool bypass_presence_check = false,
|
bool remember_password, bool bypass_presence_check = false,
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
#include <display_stream_id.h>
|
||||||
|
|
||||||
|
#include "rd_log.h"
|
||||||
|
#include "runtime/gui_runtime.h"
|
||||||
|
#if defined(__linux__) && !defined(__OHOS__)
|
||||||
|
#include "platform/linux/headless/headless_console.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
void GuiRuntime::RegisterStreams(PeerPtr* peer, size_t displays) {
|
||||||
|
for (size_t i = 0; i < displays; ++i) {
|
||||||
|
const auto id = MakeDisplayStreamId(i);
|
||||||
|
AddVideoStream(peer, id.c_str());
|
||||||
|
}
|
||||||
|
AddAudioStream(peer, audio_label_.c_str());
|
||||||
|
AddDataStream(peer, data_label_.c_str(), false);
|
||||||
|
for (const auto& label :
|
||||||
|
{mouse_label_, keyboard_label_, control_data_label_, file_label_,
|
||||||
|
file_feedback_label_, clipboard_label_})
|
||||||
|
AddDataStream(peer, label.c_str(), true);
|
||||||
|
}
|
||||||
|
void GuiRuntime::InitializeLan() {
|
||||||
|
if (unattended_host_ || stopping_ || lan_ ||
|
||||||
|
!config_center_->IsIpDirectEnabled()) return;
|
||||||
|
#if defined(__linux__) && !defined(__OHOS__)
|
||||||
|
if (HeadlessConsole::Instance().active()) return;
|
||||||
|
#endif
|
||||||
|
lan_error_.clear();
|
||||||
|
lan::SessionManager::Hooks hooks;
|
||||||
|
hooks.error = [this](const std::string& error) {
|
||||||
|
if (!lan_stopping_ && !stopping_) lan_error_ = error;
|
||||||
|
};
|
||||||
|
hooks.prepare = [this](const std::shared_ptr<lan::Session>& session) {
|
||||||
|
if (stopping_ || lan_stopping_ || devices_.EnsureCaptureReady() != 0)
|
||||||
|
throw std::runtime_error("Capture initialization failed");
|
||||||
|
const size_t displays = devices_.display_info_list().size();
|
||||||
|
auto events = std::make_shared<PeerEventHandler>(*this);
|
||||||
|
events->BindDirect(session);
|
||||||
|
// Direct initialization must not depend on cloud login/configuration having
|
||||||
|
// successfully populated the managed peer's Params snapshot.
|
||||||
|
Params params{};
|
||||||
|
std::snprintf(params.log_path, sizeof(params.log_path), "%s",
|
||||||
|
dll_log_path_.c_str());
|
||||||
|
params.hardware_acceleration = true;
|
||||||
|
params.native_video_output = true;
|
||||||
|
params.av1_encoding = config_center_->GetVideoEncodeFormat() ==
|
||||||
|
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1;
|
||||||
|
params.enable_srtp = true;
|
||||||
|
params.turn_mode = TurnMode::TurnDisabled;
|
||||||
|
params.video_content_type = VideoContentType::ScreenContent;
|
||||||
|
params.video_quality = VideoQuality::QualityHigh;
|
||||||
|
params.video_frame_rate = 60;
|
||||||
|
params.video_degradation_preference =
|
||||||
|
VideoDegradationPreference::MaintainResolution;
|
||||||
|
params.on_receive_video_frame = PeerEventHandler::OnReceiveVideoBuffer;
|
||||||
|
params.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
||||||
|
params.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
||||||
|
params.on_connection_status = PeerEventHandler::OnConnectionStatus;
|
||||||
|
params.on_net_status_report = PeerEventHandler::OnNetStatusReport;
|
||||||
|
params.user_id = session->local_id.c_str();
|
||||||
|
params.user_data = events.get();
|
||||||
|
if (!session->incoming) {
|
||||||
|
auto props = FindRemoteSession(session->key);
|
||||||
|
if (!props) throw std::runtime_error("LAN session cancelled");
|
||||||
|
props->peer_events_ = events;
|
||||||
|
props->local_id_ = session->local_id;
|
||||||
|
}
|
||||||
|
return [this, session, events, params, displays] {
|
||||||
|
lan::SessionManager::Prepared result;
|
||||||
|
result.callback_owner = events;
|
||||||
|
result.drain_callbacks = [events] { events->Deactivate(); };
|
||||||
|
result.peer = CreatePeer(¶ms);
|
||||||
|
events->SetDirectPeer(result.peer);
|
||||||
|
if (result.peer) RegisterStreams(result.peer, displays);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
hooks.attached = [this](const std::shared_ptr<lan::Session>& session) {
|
||||||
|
auto events =
|
||||||
|
std::static_pointer_cast<PeerEventHandler>(session->callback_owner);
|
||||||
|
std::unique_lock lock(remote_sessions_mutex_);
|
||||||
|
if (session->incoming)
|
||||||
|
lan_controllers_[session->key] = {session, events};
|
||||||
|
else if (auto it = remote_sessions_.find(session->key);
|
||||||
|
it != remote_sessions_.end())
|
||||||
|
it->second->peer_ = session->peer;
|
||||||
|
};
|
||||||
|
hooks.connected = [this](const std::shared_ptr<lan::Session>& session) {
|
||||||
|
auto events =
|
||||||
|
std::static_pointer_cast<PeerEventHandler>(session->callback_owner);
|
||||||
|
events->ApplyConnectionStatus(ConnectionStatus::Connected, session->key);
|
||||||
|
events->FlushEarlyData();
|
||||||
|
};
|
||||||
|
hooks.closed = [this](const std::shared_ptr<lan::Session>& session) {
|
||||||
|
auto events =
|
||||||
|
std::static_pointer_cast<PeerEventHandler>(session->callback_owner);
|
||||||
|
bool attached = !session->incoming;
|
||||||
|
if (session->incoming) {
|
||||||
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
|
attached = lan_controllers_.count(session->key) != 0;
|
||||||
|
}
|
||||||
|
if (events) {
|
||||||
|
if (attached)
|
||||||
|
events->ApplyConnectionStatus(ConnectionStatus::Closed, session->key);
|
||||||
|
events->StopAccepting();
|
||||||
|
transfers_.Cancel(events.get());
|
||||||
|
}
|
||||||
|
if (session->incoming) {
|
||||||
|
std::unique_lock lock(remote_sessions_mutex_);
|
||||||
|
lan_controllers_.erase(session->key);
|
||||||
|
} else if (auto props = FindRemoteSession(session->key))
|
||||||
|
CloseRemoteSession(props);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
lan_ = std::make_unique<lan::SessionManager>(
|
||||||
|
path_manager_->GetConfigPath() / "lan", std::move(hooks));
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
lan_error_ = error.what();
|
||||||
|
LOG_ERROR("LAN initialization failed: {}", error.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void GuiRuntime::ApplyLanSettings() {
|
||||||
|
if (config_center_->IsIpDirectEnabled()) {
|
||||||
|
InitializeLan();
|
||||||
|
} else {
|
||||||
|
lan_connecting_.reset();
|
||||||
|
lan_error_.clear();
|
||||||
|
if (lan_ && !lan_stopping_) {
|
||||||
|
lan_stopping_ = true;
|
||||||
|
lan_->Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool GuiRuntime::LanAvailable() const {
|
||||||
|
return lan_ && !lan_stopping_ && !stopping_;
|
||||||
|
}
|
||||||
|
void GuiRuntime::TickLan() {
|
||||||
|
if (lan_) lan_->Tick();
|
||||||
|
if (lan_ && lan_stopping_ && lan_->Quiescent()) {
|
||||||
|
// Drain async authentication, media preparation and callbacks before
|
||||||
|
// releasing the manager. A quick off/on waits here before rebinding.
|
||||||
|
lan_.reset();
|
||||||
|
lan_stopping_ = false;
|
||||||
|
InitializeLan();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void GuiRuntime::ConnectLan(const std::string& address, unsigned port,
|
||||||
|
std::string password, bool remember_password) {
|
||||||
|
if (!LanAvailable() || (lan_connecting_ && !lan_connecting_->closing))
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
auto props = std::make_shared<RemoteSession>();
|
||||||
|
props->remember_password_ = remember_password;
|
||||||
|
if (remember_password && lan::ValidPassword(password))
|
||||||
|
std::memcpy(props->remote_password_, password.data(), password.size());
|
||||||
|
auto session = lan_->Connect(address, port, std::move(password));
|
||||||
|
lan_connecting_ = session;
|
||||||
|
props->lan_session_ = session;
|
||||||
|
props->remote_id_ = session->key;
|
||||||
|
props->connection_status_ = ConnectionStatus::Connecting;
|
||||||
|
props->connection_auth_.Complete();
|
||||||
|
{
|
||||||
|
std::unique_lock lock(remote_sessions_mutex_);
|
||||||
|
remote_sessions_[session->key] = props;
|
||||||
|
}
|
||||||
|
focused_remote_id_ = session->key;
|
||||||
|
show_connection_status_window_ = false;
|
||||||
|
lan_error_.clear();
|
||||||
|
} catch (const std::exception& error) {
|
||||||
|
lan_error_ = error.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::shared_ptr<PeerEventHandler> GuiRuntime::ControllerEvents(
|
||||||
|
const std::string& id) {
|
||||||
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
|
auto it = lan_controllers_.find(id);
|
||||||
|
return it == lan_controllers_.end()
|
||||||
|
? (id.rfind("lan/", 0) == 0 ? nullptr : peer_events_)
|
||||||
|
: it->second.events;
|
||||||
|
}
|
||||||
|
int GuiRuntime::WithController(
|
||||||
|
const std::string& id,
|
||||||
|
const std::function<int(PeerPtr*, const std::string&)>& operation) {
|
||||||
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
|
auto it = lan_controllers_.find(id);
|
||||||
|
auto events = it == lan_controllers_.end() ? peer_events_ : it->second.events;
|
||||||
|
if (it == lan_controllers_.end() && id.rfind("lan/", 0) == 0) return -1;
|
||||||
|
PeerPtr* peer =
|
||||||
|
it == lan_controllers_.end() ? peer_ : it->second.session->peer;
|
||||||
|
if (!peer || !events || !events->DataAllowed()) return -1;
|
||||||
|
auto callback = events->EnterCallback();
|
||||||
|
if (!callback) return -1;
|
||||||
|
const auto wire_id = events->WireId(id);
|
||||||
|
lock.unlock();
|
||||||
|
return operation(peer, wire_id);
|
||||||
|
}
|
||||||
|
int GuiRuntime::SendControllerData(const std::string& id, const char* data,
|
||||||
|
size_t size, const char* label) {
|
||||||
|
return WithController(id, [&](PeerPtr* peer, const std::string& wire) {
|
||||||
|
return SendReliableDataFrameToPeer(peer, data, size, label, wire.data(),
|
||||||
|
wire.size());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
std::vector<std::string> GuiRuntime::ConnectedControllers() {
|
||||||
|
std::vector<std::string> ids;
|
||||||
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
|
ids.reserve(connection_status_.size());
|
||||||
|
for (const auto& [id, status] : connection_status_)
|
||||||
|
if (status == ConnectionStatus::Connected) ids.push_back(id);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
void GuiRuntime::BeginShutdown() {
|
||||||
|
if (stopping_) return;
|
||||||
|
stopping_ = true;
|
||||||
|
shutdown_report_at_ =
|
||||||
|
std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||||
|
if (lan_) lan_->Stop();
|
||||||
|
CloseAllRemoteSessions();
|
||||||
|
}
|
||||||
|
bool GuiRuntime::ShutdownDrained() {
|
||||||
|
TickLan();
|
||||||
|
HandleSessionCleanup();
|
||||||
|
const bool drained =
|
||||||
|
(!lan_ || lan_->Quiescent()) && !connection_peer_cleanup_.valid() &&
|
||||||
|
session_cleanup_tasks_.empty() && controller_cleanup_tasks_.empty();
|
||||||
|
if (!drained && std::chrono::steady_clock::now() >= shutdown_report_at_) {
|
||||||
|
LOG_WARN(
|
||||||
|
"Shutdown waiting: LAN [{}], cloud peer={}, viewers={}, controllers={}",
|
||||||
|
lan_ ? lan_->PendingWork() : "none", connection_peer_cleanup_.valid(),
|
||||||
|
session_cleanup_tasks_.size(), controller_cleanup_tasks_.size());
|
||||||
|
shutdown_report_at_ =
|
||||||
|
std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||||
|
}
|
||||||
|
return drained;
|
||||||
|
}
|
||||||
|
} // namespace crossdesk
|
||||||
@@ -84,17 +84,52 @@ void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *side,
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool PeerEventHandler::QueueEarlyData(const char* data, size_t size,
|
||||||
|
const char* label, size_t label_size) {
|
||||||
|
if (!direct_bound_) return false;
|
||||||
|
auto session = direct_.lock();
|
||||||
|
if (!session || session->closing) return true;
|
||||||
|
std::lock_guard lock(early_data_mutex_);
|
||||||
|
if (session->authorized) return false;
|
||||||
|
// Keep bounded reliable metadata until the manager commits Connected. Mouse,
|
||||||
|
// keyboard, clipboard and files never enter this pre-authorization queue.
|
||||||
|
if (std::string_view(label, label_size) != owner_.control_data_label_)
|
||||||
|
return true;
|
||||||
|
if (size > 8192 || early_data_.size() >= 16 ||
|
||||||
|
early_data_bytes_ + size > 65536) {
|
||||||
|
session->failed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
early_data_bytes_ += size;
|
||||||
|
early_data_.emplace_back(data, size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void PeerEventHandler::FlushEarlyData() {
|
||||||
|
std::vector<std::string> queued;
|
||||||
|
{
|
||||||
|
std::lock_guard lock(early_data_mutex_);
|
||||||
|
queued.swap(early_data_);
|
||||||
|
early_data_bytes_ = 0;
|
||||||
|
}
|
||||||
|
const auto session = direct_.lock();
|
||||||
|
if (!session || !session->authorized || session->closing) return;
|
||||||
|
const auto& label = owner_.control_data_label_;
|
||||||
|
for (const auto& data : queued)
|
||||||
|
OnReceiveDataBuffer(data.data(), data.size(), session->remote_id.data(),
|
||||||
|
session->remote_id.size(), label.data(), label.size(),
|
||||||
|
this);
|
||||||
|
}
|
||||||
|
|
||||||
void PeerEventHandler::OnReceiveDataBuffer(
|
void PeerEventHandler::OnReceiveDataBuffer(
|
||||||
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
const char *data, size_t size, const char *user_id, size_t user_id_size,
|
||||||
const char *src_id, size_t src_id_size, void *user_data) {
|
const char *src_id, size_t src_id_size, void *user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
auto callback = handler->EnterCallback();
|
auto callback = handler->EnterCallback();
|
||||||
if (!callback) return;
|
if (!callback || handler->QueueEarlyData(data, size, src_id, src_id_size) ||
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
!handler->DataAllowed())
|
||||||
if (!runtime) {
|
|
||||||
return;
|
return;
|
||||||
}
|
GuiRuntime* runtime = &handler->owner_;
|
||||||
|
|
||||||
std::string source_id = std::string(src_id, src_id_size);
|
std::string source_id = std::string(src_id, src_id_size);
|
||||||
// The service-owned host runs as SYSTEM. Do not expose filesystem or
|
// The service-owned host runs as SYSTEM. Do not expose filesystem or
|
||||||
@@ -105,11 +140,13 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
source_id == runtime->clipboard_label_))
|
source_id == runtime->clipboard_label_))
|
||||||
return;
|
return;
|
||||||
if (source_id == runtime->file_label_) {
|
if (source_id == runtime->file_label_) {
|
||||||
runtime->transfers_.Receive(data, size, handler, std::string(user_id, user_id_size));
|
runtime->transfers_.Receive(data, size, handler,
|
||||||
|
handler->ConnectionKey(user_id, user_id_size));
|
||||||
return;
|
return;
|
||||||
} else if (source_id == runtime->clipboard_label_) {
|
} else if (source_id == runtime->clipboard_label_) {
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
std::string remote_user_id(user_id, user_id_size);
|
std::string remote_user_id =
|
||||||
|
handler->ConnectionKey(user_id, user_id_size);
|
||||||
auto props =
|
auto props =
|
||||||
runtime->FindRemoteSession(remote_user_id);
|
runtime->FindRemoteSession(remote_user_id);
|
||||||
if (props && !props->clipboard_allowed_.load()) {
|
if (props && !props->clipboard_allowed_.load()) {
|
||||||
@@ -121,11 +158,15 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else if (source_id == runtime->file_feedback_label_) {
|
} else if (source_id == runtime->file_feedback_label_) {
|
||||||
runtime->transfers_.HandleAck(data, size, handler, std::string(user_id, user_id_size));
|
runtime->transfers_.HandleAck(
|
||||||
|
data, size, handler, handler->ConnectionKey(user_id, user_id_size));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string remote_id(user_id, user_id_size);
|
const std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
|
||||||
|
const std::string wire_id = handler->WireId(remote_id);
|
||||||
|
PeerPtr* bound_peer =
|
||||||
|
handler->IsDirect() ? handler->DirectPeer() : runtime->peer_;
|
||||||
std::string json_str(data, size);
|
std::string json_str(data, size);
|
||||||
const auto preview_json =
|
const auto preview_json =
|
||||||
json_str.find("\"deskhub_preview") != std::string::npos
|
json_str.find("\"deskhub_preview") != std::string::npos
|
||||||
@@ -145,29 +186,31 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
if (!previous || subscription->IsNewerThan(*previous)) {
|
if (!previous || subscription->IsNewerThan(*previous)) {
|
||||||
if (subscription->fps > 0)
|
if (subscription->fps > 0)
|
||||||
accepted = UpdateVideoSettings(
|
accepted = UpdateVideoSettings(
|
||||||
runtime->peer_, remote_id.data(), remote_id.size(),
|
bound_peer, wire_id.data(), wire_id.size(),
|
||||||
static_cast<VideoQuality>(subscription->quality),
|
static_cast<VideoQuality>(subscription->quality),
|
||||||
subscription->frame_rate,
|
subscription->frame_rate,
|
||||||
static_cast<VideoDegradationPreference>(
|
static_cast<VideoDegradationPreference>(
|
||||||
subscription->EncoderPreference())) == 0;
|
subscription->EncoderPreference())) == 0;
|
||||||
if (accepted && (!previous || previous->max_bitrate_bps.value_or(0) !=
|
if (accepted && (!previous || previous->max_bitrate_bps.value_or(0) !=
|
||||||
subscription->max_bitrate_bps.value_or(0)))
|
subscription->max_bitrate_bps.value_or(0)))
|
||||||
accepted = UpdateVideoBitrateLimit(
|
accepted = UpdateVideoBitrateLimit(
|
||||||
runtime->peer_, remote_id.data(), remote_id.size(),
|
bound_peer, wire_id.data(), wire_id.size(),
|
||||||
subscription->max_bitrate_bps.value_or(0)) == 0;
|
subscription->max_bitrate_bps.value_or(0)) == 0;
|
||||||
// Rebalancing bandwidth alone must not force every device to send a
|
// Rebalancing bandwidth alone must not force every device to send a
|
||||||
// keyframe. The pause API requests its own keyframes on resume.
|
// keyframe. The pause API requests its own keyframes on resume.
|
||||||
if (accepted && (!previous || previous->width != subscription->width ||
|
if (accepted && (!previous || previous->width != subscription->width ||
|
||||||
previous->height != subscription->height))
|
previous->height != subscription->height))
|
||||||
accepted = UpdateVideoResolution(
|
accepted = UpdateVideoResolution(bound_peer, wire_id.data(),
|
||||||
runtime->peer_, remote_id.data(), remote_id.size(),
|
wire_id.size(), subscription->width,
|
||||||
subscription->width, subscription->height) == 0;
|
subscription->height) == 0;
|
||||||
if (accepted)
|
if (accepted)
|
||||||
accepted = SetVideoSendPaused(runtime->peer_, remote_id.data(),
|
accepted =
|
||||||
remote_id.size(), subscription->fps == 0) == 0;
|
SetVideoSendPaused(bound_peer, wire_id.data(), wire_id.size(),
|
||||||
|
subscription->fps == 0) == 0;
|
||||||
if (accepted)
|
if (accepted)
|
||||||
accepted = SetAudioSendPaused(runtime->peer_, remote_id.data(),
|
accepted =
|
||||||
remote_id.size(), !subscription->audio) == 0;
|
SetAudioSendPaused(bound_peer, wire_id.data(), wire_id.size(),
|
||||||
|
!subscription->audio) == 0;
|
||||||
if (accepted && subscription->audio)
|
if (accepted && subscription->audio)
|
||||||
runtime->devices_.StartSpeakerCapturer();
|
runtime->devices_.StartSpeakerCapturer();
|
||||||
}
|
}
|
||||||
@@ -178,9 +221,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
{"request_id", subscription->request_id},
|
{"request_id", subscription->request_id},
|
||||||
{"accepted",
|
{"accepted",
|
||||||
accepted}}.dump();
|
accepted}}.dump();
|
||||||
SendReliableDataFrameToPeer(runtime->peer_, reply.data(), reply.size(),
|
SendReliableDataFrameToPeer(bound_peer, reply.data(), reply.size(),
|
||||||
runtime->control_data_label_.c_str(),
|
runtime->control_data_label_.c_str(),
|
||||||
remote_id.data(), remote_id.size());
|
wire_id.data(), wire_id.size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (preview_json.is_object() &&
|
if (preview_json.is_object() &&
|
||||||
@@ -253,7 +296,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
const auto& settings = remote_action.vs;
|
const auto& settings = remote_action.vs;
|
||||||
remote_action.vs.accepted =
|
remote_action.vs.accepted =
|
||||||
UpdateVideoSettings(
|
UpdateVideoSettings(
|
||||||
runtime->peer_, remote_id.data(), remote_id.size(),
|
bound_peer, wire_id.data(), wire_id.size(),
|
||||||
static_cast<VideoQuality>(settings.quality), settings.frame_rate,
|
static_cast<VideoQuality>(settings.quality), settings.frame_rate,
|
||||||
static_cast<VideoDegradationPreference>(settings.preference)) == 0;
|
static_cast<VideoDegradationPreference>(settings.preference)) == 0;
|
||||||
LOG_INFO("Video settings: peer={} source={} request={} quality={} fps={} "
|
LOG_INFO("Video settings: peer={} source={} request={} quality={} fps={} "
|
||||||
@@ -262,9 +305,9 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
settings.frame_rate, settings.preference, remote_action.vs.accepted);
|
settings.frame_rate, settings.preference, remote_action.vs.accepted);
|
||||||
remote_action.type = ControlType::video_settings_status;
|
remote_action.type = ControlType::video_settings_status;
|
||||||
const auto message = remote_action.to_json();
|
const auto message = remote_action.to_json();
|
||||||
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(),
|
SendReliableDataFrameToPeer(bound_peer, message.data(), message.size(),
|
||||||
runtime->control_data_label_.c_str(),
|
runtime->control_data_label_.c_str(),
|
||||||
remote_id.data(), remote_id.size());
|
wire_id.data(), wire_id.size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (remote_action.type == ControlType::privacy_status) {
|
if (remote_action.type == ControlType::privacy_status) {
|
||||||
@@ -407,8 +450,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
props->video_settings_request_tick_ = SDL_GetTicks();
|
props->video_settings_request_tick_ = SDL_GetTicks();
|
||||||
settings.vs = props->video_settings_;
|
settings.vs = props->video_settings_;
|
||||||
const auto message = settings.to_json();
|
const auto message = settings.to_json();
|
||||||
SendReliableDataFrame(props->peer_, message.data(), message.size(),
|
SendReliableDataFrame(
|
||||||
props->control_data_label_.c_str());
|
handler->IsDirect() ? handler->DirectPeer() : props->peer_,
|
||||||
|
message.data(), message.size(),
|
||||||
|
props->control_data_label_.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// client mode
|
// client mode
|
||||||
@@ -456,8 +501,8 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
// command arrives after it (e.g. when unmuting an inactive window).
|
// command arrives after it (e.g. when unmuting an inactive window).
|
||||||
const auto subscription = runtime->preview_subscriptions_.Get(remote_id);
|
const auto subscription = runtime->preview_subscriptions_.Get(remote_id);
|
||||||
const bool paused = subscription ? !subscription->audio : !remote_action.a;
|
const bool paused = subscription ? !subscription->audio : !remote_action.a;
|
||||||
const int ret = SetAudioSendPaused(runtime->peer_, remote_id.data(),
|
const int ret = SetAudioSendPaused(bound_peer, wire_id.data(),
|
||||||
remote_id.size(), paused);
|
wire_id.size(), paused);
|
||||||
if (remote_action.a)
|
if (remote_action.a)
|
||||||
runtime->devices_.StartSpeakerCapturer();
|
runtime->devices_.StartSpeakerCapturer();
|
||||||
else if (ret != 0)
|
else if (ret != 0)
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ constexpr HostPlatform kClientPlatform = HostPlatform::Unknown;
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::string PeerEventHandler::ConnectionKey(const char* id, size_t size) const {
|
||||||
|
if (auto session = direct_.lock()) return session->key;
|
||||||
|
return direct_bound_ ? std::string{} : std::string(id, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PeerEventHandler::WireId(const std::string& key) const {
|
||||||
|
if (auto session = direct_.lock()) return session->remote_id;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PeerEventHandler::DataAllowed() const {
|
||||||
|
if (!direct_bound_) return true;
|
||||||
|
auto session = direct_.lock();
|
||||||
|
return session && session->authorized && !session->closing;
|
||||||
|
}
|
||||||
|
|
||||||
void PeerEventHandler::SendClientInfo(PeerPtr* peer,
|
void PeerEventHandler::SendClientInfo(PeerPtr* peer,
|
||||||
const std::string& client_id) {
|
const std::string& client_id) {
|
||||||
if (!peer) {
|
if (!peer) {
|
||||||
@@ -74,8 +90,8 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
|||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
auto callback = handler->EnterCallback();
|
auto callback = handler->EnterCallback();
|
||||||
if (!callback) return;
|
if (!callback) return;
|
||||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = &handler->owner_;
|
||||||
if (!runtime || !message || size == 0) {
|
if (!message || size == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::string s(message, size);
|
std::string s(message, size);
|
||||||
@@ -179,10 +195,7 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
|||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
auto callback = handler->EnterCallback();
|
auto callback = handler->EnterCallback();
|
||||||
if (!callback) return;
|
if (!callback) return;
|
||||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = &handler->owner_;
|
||||||
if (!runtime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string client_id(user_id, user_id_size);
|
std::string client_id(user_id, user_id_size);
|
||||||
if (client_id == runtime->client_id_) {
|
if (client_id == runtime->client_id_) {
|
||||||
@@ -251,17 +264,30 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||||
const char* user_id,
|
const char* user_id, size_t size,
|
||||||
const size_t user_id_size,
|
void* context) {
|
||||||
void* user_data) {
|
auto* handler = static_cast<PeerEventHandler*>(context);
|
||||||
|
if (!handler) return;
|
||||||
|
if (handler->direct_bound_) {
|
||||||
|
auto callback = handler->EnterCallback();
|
||||||
|
if (callback)
|
||||||
|
if (auto session = handler->direct_.lock()) session->Status(status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DispatchConnectionStatus(status, user_id, size, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PeerEventHandler::DispatchConnectionStatus(ConnectionStatus status,
|
||||||
|
const char* user_id,
|
||||||
|
const size_t user_id_size,
|
||||||
|
void* user_data) {
|
||||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
auto callback = handler->EnterCallback();
|
auto callback = handler->EnterCallback();
|
||||||
if (!callback) return;
|
if (!callback) return;
|
||||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = &handler->owner_;
|
||||||
if (!runtime) return;
|
|
||||||
|
|
||||||
std::string remote_id(user_id, user_id_size);
|
std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
|
||||||
if (status == ConnectionStatus::Connected) runtime->transfers_.Connected(handler, remote_id);
|
if (status == ConnectionStatus::Connected) runtime->transfers_.Connected(handler, remote_id);
|
||||||
else if (status == ConnectionStatus::Disconnected || status == ConnectionStatus::Failed ||
|
else if (status == ConnectionStatus::Disconnected || status == ConnectionStatus::Failed ||
|
||||||
status == ConnectionStatus::Closed) runtime->transfers_.Cancel(handler, remote_id);
|
status == ConnectionStatus::Closed) runtime->transfers_.Cancel(handler, remote_id);
|
||||||
@@ -474,8 +500,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
}
|
}
|
||||||
std::string msg = remote_action.to_json();
|
std::string msg = remote_action.to_json();
|
||||||
const int ret =
|
const int ret =
|
||||||
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
|
runtime->SendControllerData(remote_id, msg.data(), msg.size(),
|
||||||
runtime->control_data_label_.c_str());
|
runtime->control_data_label_.c_str());
|
||||||
if (ret != 0) {
|
if (ret != 0) {
|
||||||
LOG_WARN("[{}] host information send failed, ret={}", remote_id,
|
LOG_WARN("[{}] host information send failed, ret={}", remote_id,
|
||||||
ret);
|
ret);
|
||||||
@@ -516,21 +542,12 @@ void PeerEventHandler::OnNetStatusReport(
|
|||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
auto callback = handler->EnterCallback();
|
auto callback = handler->EnterCallback();
|
||||||
if (!callback) return;
|
if (!callback) return;
|
||||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = &handler->owner_;
|
||||||
if (!runtime) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
|
if (const char* at_pos = handler->IsDirect() ? nullptr : strchr(client_id, '@');
|
||||||
std::string id, password;
|
at_pos && strchr(user_id, '-') == nullptr) {
|
||||||
const char* at_pos = strchr(client_id, '@');
|
const std::string id(client_id, at_pos - client_id);
|
||||||
if (at_pos == nullptr) {
|
const std::string password(at_pos + 1);
|
||||||
id = client_id;
|
|
||||||
password.clear();
|
|
||||||
} else {
|
|
||||||
id.assign(client_id, at_pos - client_id);
|
|
||||||
password = at_pos + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_self_hosted = runtime->config_center_->IsSelfHosted();
|
bool is_self_hosted = runtime->config_center_->IsSelfHosted();
|
||||||
|
|
||||||
@@ -578,7 +595,7 @@ void PeerEventHandler::OnNetStatusReport(
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string remote_id(user_id, user_id_size);
|
std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
|
||||||
auto props = runtime->FindRemoteSession(remote_id);
|
auto props = runtime->FindRemoteSession(remote_id);
|
||||||
if (!props) {
|
if (!props) {
|
||||||
bool report_codecs = false;
|
bool report_codecs = false;
|
||||||
@@ -597,9 +614,8 @@ void PeerEventHandler::OnNetStatusReport(
|
|||||||
net_traffic_stats->video_encoder);
|
net_traffic_stats->video_encoder);
|
||||||
const auto message = action.to_json();
|
const auto message = action.to_json();
|
||||||
// Report only to this controller, after releasing the status lock.
|
// Report only to this controller, after releasing the status lock.
|
||||||
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(),
|
runtime->SendControllerData(remote_id, message.data(), message.size(),
|
||||||
runtime->control_data_label_.c_str(),
|
runtime->control_data_label_.c_str());
|
||||||
remote_id.data(), remote_id.size());
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
#define _PEER_EVENT_HANDLER_H_
|
#define _PEER_EVENT_HANDLER_H_
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "minirtc.h"
|
#include "minirtc.h"
|
||||||
#include "runtime/session_lifecycle.h"
|
#include "runtime/session_lifecycle.h"
|
||||||
@@ -16,12 +19,30 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
class GuiRuntime;
|
class GuiRuntime;
|
||||||
|
namespace lan {
|
||||||
|
struct Session;
|
||||||
|
}
|
||||||
|
|
||||||
// Adapts MiniRTC C callbacks to the GUI runtime.
|
// Adapts MiniRTC C callbacks to the GUI runtime.
|
||||||
class PeerEventHandler {
|
class PeerEventHandler {
|
||||||
public:
|
public:
|
||||||
explicit PeerEventHandler(GuiRuntime &owner);
|
explicit PeerEventHandler(GuiRuntime &owner);
|
||||||
|
|
||||||
|
void BindDirect(const std::shared_ptr<lan::Session>& session) {
|
||||||
|
direct_ = session;
|
||||||
|
direct_bound_ = true;
|
||||||
|
}
|
||||||
|
std::string ConnectionKey(const char* id, size_t size) const;
|
||||||
|
std::string WireId(const std::string& key) const;
|
||||||
|
bool DataAllowed() const;
|
||||||
|
bool IsDirect() const { return direct_bound_; }
|
||||||
|
void SetDirectPeer(PeerPtr* peer) { direct_peer_ = peer; }
|
||||||
|
PeerPtr* DirectPeer() const { return direct_peer_; }
|
||||||
|
void FlushEarlyData();
|
||||||
|
void ApplyConnectionStatus(ConnectionStatus status, const std::string& id) {
|
||||||
|
DispatchConnectionStatus(status, id.data(), id.size(), this);
|
||||||
|
}
|
||||||
|
void StopAccepting() { callbacks_.StopAccepting(); }
|
||||||
bool IsActive() const { return callbacks_.IsActive(); }
|
bool IsActive() const { return callbacks_.IsActive(); }
|
||||||
auto EnterCallback() { return callbacks_.Enter(); }
|
auto EnterCallback() { return callbacks_.Enter(); }
|
||||||
void Deactivate() { callbacks_.Close(); }
|
void Deactivate() { callbacks_.Close(); }
|
||||||
@@ -51,9 +72,19 @@ public:
|
|||||||
void *user_data);
|
void *user_data);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
|
static void DispatchConnectionStatus(ConnectionStatus status, const char* id,
|
||||||
|
size_t size, void* context);
|
||||||
|
static void SendClientInfo(PeerPtr* peer, const std::string& client_id);
|
||||||
|
bool QueueEarlyData(const char* data, size_t size, const char* label,
|
||||||
|
size_t label_size);
|
||||||
|
std::weak_ptr<lan::Session> direct_;
|
||||||
|
bool direct_bound_ = false;
|
||||||
|
PeerPtr* direct_peer_ = nullptr;
|
||||||
|
std::mutex early_data_mutex_;
|
||||||
|
std::vector<std::string> early_data_;
|
||||||
|
size_t early_data_bytes_ = 0;
|
||||||
|
|
||||||
GuiRuntime &owner_;
|
GuiRuntime& owner_;
|
||||||
gui_detail::SessionCallbackGate callbacks_;
|
gui_detail::SessionCallbackGate callbacks_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,20 +38,21 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
|||||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
auto callback = handler->EnterCallback();
|
auto callback = handler->EnterCallback();
|
||||||
if (!callback) return;
|
if (!callback || !handler->DataAllowed()) return;
|
||||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = &handler->owner_;
|
||||||
if (!runtime || !video_frame) {
|
if (!video_frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string remote_id(user_id, user_id_size);
|
std::string remote_id = handler->ConnectionKey(user_id, user_id_size);
|
||||||
auto props = runtime->FindRemoteSession(remote_id);
|
auto props = runtime->FindRemoteSession(remote_id);
|
||||||
if (!props) return;
|
if (!props) return;
|
||||||
|
|
||||||
if (props->connection_established_) {
|
if (props->connection_established_) {
|
||||||
const auto timing_now = VideoLatencyFrame::Clock::now();
|
const auto timing_now = VideoLatencyFrame::Clock::now();
|
||||||
// Pair the clocks before preview conversion so its cost is included.
|
// Pair the clocks before preview conversion so its cost is included.
|
||||||
const auto local_now_us = GetSystemTimeMicros(props->peer_);
|
const auto local_now_us = GetSystemTimeMicros(
|
||||||
|
handler->IsDirect() ? handler->DirectPeer() : props->peer_);
|
||||||
const auto* received_native = GetNativeVideoFrame(video_frame);
|
const auto* received_native = GetNativeVideoFrame(video_frame);
|
||||||
props->received_video_stats_.Record(
|
props->received_video_stats_.Record(
|
||||||
received_native ? received_native->width : video_frame->width,
|
received_native ? received_native->width : video_frame->width,
|
||||||
@@ -274,15 +275,15 @@ void PeerEventHandler::OnReceiveAudioBuffer(
|
|||||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
if (!handler) return;
|
if (!handler) return;
|
||||||
auto callback = handler->EnterCallback();
|
auto callback = handler->EnterCallback();
|
||||||
if (!callback) return;
|
if (!callback || !handler->DataAllowed()) return;
|
||||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = &handler->owner_;
|
||||||
if (!runtime || runtime->unattended_host_) {
|
if (runtime->unattended_host_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard delivery_lock(runtime->audio_delivery_mutex_);
|
std::lock_guard delivery_lock(runtime->audio_delivery_mutex_);
|
||||||
const auto session =
|
const auto session =
|
||||||
runtime->FindRemoteSession(std::string(user_id, user_id_size));
|
runtime->FindRemoteSession(handler->ConnectionKey(user_id, user_id_size));
|
||||||
if (!session || session->closing_ || !session->audio_allowed_.load()) return;
|
if (!session || session->closing_ || !session->audio_allowed_.load()) return;
|
||||||
runtime->audio_buffer_fresh_ = true;
|
runtime->audio_buffer_fresh_ = true;
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
|
bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
|
||||||
|
if (remote_id.rfind("lan/", 0) == 0) {
|
||||||
|
const auto events = ControllerEvents(remote_id);
|
||||||
|
if (!events || !events->DataAllowed()) return false;
|
||||||
|
}
|
||||||
// Connections appear here only after the existing password/signalling/RTC
|
// Connections appear here only after the existing password/signalling/RTC
|
||||||
// admission path. Outgoing/viewer sessions live in remote_sessions_ instead.
|
// admission path. Outgoing/viewer sessions live in remote_sessions_ instead.
|
||||||
std::shared_lock lock(connection_status_mutex_);
|
std::shared_lock lock(connection_status_mutex_);
|
||||||
@@ -109,9 +113,9 @@ void GuiRuntime::HandlePrivacy() {
|
|||||||
"A connected controller does not support privacy screen");
|
"A connected controller does not support privacy screen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!peer_ || (last_privacy_status_tick_ != 0 &&
|
if (last_privacy_status_tick_ != 0 &&
|
||||||
status.revision == last_privacy_revision_ &&
|
status.revision == last_privacy_revision_ &&
|
||||||
now - last_privacy_status_tick_ < 1000))
|
now - last_privacy_status_tick_ < 1000)
|
||||||
return;
|
return;
|
||||||
RemoteAction action{};
|
RemoteAction action{};
|
||||||
action.type = ControlType::privacy_status;
|
action.type = ControlType::privacy_status;
|
||||||
@@ -119,9 +123,8 @@ void GuiRuntime::HandlePrivacy() {
|
|||||||
const auto message = action.to_json();
|
const auto message = action.to_json();
|
||||||
bool sent = true;
|
bool sent = true;
|
||||||
for (const auto& id : controllers) {
|
for (const auto& id : controllers) {
|
||||||
const int result = SendReliableDataFrameToPeer(
|
const int result = SendControllerData(id, message.data(), message.size(),
|
||||||
peer_, message.data(), message.size(), control_data_label_.c_str(),
|
control_data_label_.c_str());
|
||||||
id.data(), id.size());
|
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
sent = false;
|
sent = false;
|
||||||
LOG_WARN("Privacy status send failed, peer={}, ret={}", id, result);
|
LOG_WARN("Privacy status send failed, peer={}, ret={}", id, result);
|
||||||
|
|||||||
@@ -31,7 +31,12 @@
|
|||||||
#include "runtime/received_video_stats.h"
|
#include "runtime/received_video_stats.h"
|
||||||
#include "runtime/connection_auth_state.h"
|
#include "runtime/connection_auth_state.h"
|
||||||
|
|
||||||
namespace crossdesk { class PeerEventHandler; }
|
namespace crossdesk {
|
||||||
|
class PeerEventHandler;
|
||||||
|
namespace lan {
|
||||||
|
struct Session;
|
||||||
|
}
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
namespace crossdesk::gui_detail {
|
namespace crossdesk::gui_detail {
|
||||||
|
|
||||||
@@ -105,6 +110,7 @@ struct RemoteSession {
|
|||||||
// Decoder-thread scratch memory, never shared with the UI.
|
// Decoder-thread scratch memory, never shared with the UI.
|
||||||
std::mutex gallery_conversion_mutex_;
|
std::mutex gallery_conversion_mutex_;
|
||||||
std::vector<unsigned char> gallery_nv12_, gallery_scaled_, gallery_scratch_;
|
std::vector<unsigned char> gallery_nv12_, gallery_scaled_, gallery_scratch_;
|
||||||
|
std::shared_ptr<lan::Session> lan_session_;
|
||||||
Params params_;
|
Params params_;
|
||||||
PeerPtr* peer_ = nullptr;
|
PeerPtr* peer_ = nullptr;
|
||||||
std::shared_ptr<PeerEventHandler> peer_events_;
|
std::shared_ptr<PeerEventHandler> peer_events_;
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ void SessionCallbackGate::EndCallback() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SessionCallbackGate::StopAccepting() {
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
accepting_callbacks_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
void SessionCallbackGate::Close() {
|
void SessionCallbackGate::Close() {
|
||||||
std::unique_lock lock(mutex_);
|
std::unique_lock lock(mutex_);
|
||||||
accepting_callbacks_ = false;
|
accepting_callbacks_ = false;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class SessionCallbackGate {
|
|||||||
CallbackGuard Enter();
|
CallbackGuard Enter();
|
||||||
|
|
||||||
// Called by the session owner, never from within this session's callbacks.
|
// Called by the session owner, never from within this session's callbacks.
|
||||||
|
void StopAccepting();
|
||||||
void Close();
|
void Close();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -7,10 +7,19 @@ export component GroupFilter inherits Rectangle {
|
|||||||
in property <[string]> model;
|
in property <[string]> model;
|
||||||
in property <int> system-count: 2;
|
in property <int> system-count: 2;
|
||||||
in property <bool> enabled: true;
|
in property <bool> enabled: true;
|
||||||
|
// An embedded picker shares its enclosing field's border and focus ring.
|
||||||
|
in property <bool> embedded: false;
|
||||||
|
in property <bool> show-indicator: true;
|
||||||
|
in property <length> popup-min-width: 0px;
|
||||||
|
in property <length> text-size: 12px;
|
||||||
|
in property <int> text-weight: 400;
|
||||||
|
in property <color> text-color: #30343b;
|
||||||
in property <color> normal-border-color: #c9cdd2;
|
in property <color> normal-border-color: #c9cdd2;
|
||||||
in property <color> hover-border-color: root.normal-border-color;
|
in property <color> hover-border-color: root.normal-border-color;
|
||||||
in property <color> focus-border-color: #8db5df;
|
in property <color> focus-border-color: #8db5df;
|
||||||
in-out property <int> current-index: 0;
|
in-out property <int> current-index: 0;
|
||||||
|
out property <bool> has-focus: trigger-focus.has-focus || popup.is-open;
|
||||||
|
out property <bool> has-hover: root.enabled && trigger-touch.has-hover && !popup.is-open;
|
||||||
callback selected(int);
|
callback selected(int);
|
||||||
private property <bool> has-divider: root.system-count > 0 && root.model.length > root.system-count;
|
private property <bool> has-divider: root.system-count > 0 && root.model.length > root.system-count;
|
||||||
private property <length> list-height: root.model.length * 26px + (root.has-divider ? 6px : 0px);
|
private property <length> list-height: root.model.length * 26px + (root.has-divider ? 6px : 0px);
|
||||||
@@ -25,8 +34,9 @@ export component GroupFilter inherits Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
width: 155px; height: 28px;
|
width: 155px; height: 28px;
|
||||||
background: !root.enabled ? #f3f4f5 : trigger-touch.has-hover ? #f8f9fb : white;
|
background: root.embedded ? (root.enabled && trigger-touch.has-hover ? #eef3fb : transparent)
|
||||||
border-width: 1px;
|
: !root.enabled ? #f3f4f5 : trigger-touch.has-hover ? #f8f9fb : white;
|
||||||
|
border-width: root.embedded ? 0px : 1px;
|
||||||
border-color: popup.is-open || trigger-focus.has-focus ? root.focus-border-color
|
border-color: popup.is-open || trigger-focus.has-focus ? root.focus-border-color
|
||||||
: trigger-touch.has-hover ? root.hover-border-color : root.normal-border-color;
|
: trigger-touch.has-hover ? root.hover-border-color : root.normal-border-color;
|
||||||
border-radius: UiRadius.control;
|
border-radius: UiRadius.control;
|
||||||
@@ -52,16 +62,18 @@ export component GroupFilter inherits Rectangle {
|
|||||||
root.reveal-selection();
|
root.reveal-selection();
|
||||||
root.selected(root.current-index);
|
root.selected(root.current-index);
|
||||||
}
|
}
|
||||||
function open-menu() {
|
public function open-menu() {
|
||||||
if root.enabled && root.model.length > 0 {
|
if root.enabled && root.model.length > 0 {
|
||||||
popup-open-timer.running = false;
|
popup-open-timer.running = false;
|
||||||
root.popup-animation-enabled = false;
|
root.popup-animation-enabled = false;
|
||||||
root.popup-progress = 0;
|
root.popup-progress = 0;
|
||||||
root.reveal-selection();
|
root.reveal-selection();
|
||||||
|
trigger-focus.focus();
|
||||||
popup.show();
|
popup.show();
|
||||||
popup-open-timer.running = true;
|
popup-open-timer.running = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public function close-menu() { popup.close(); }
|
||||||
// Defer until the popup has its initial frame; animate its contents only,
|
// Defer until the popup has its initial frame; animate its contents only,
|
||||||
// keeping the native popup bounds and scrolling geometry stable.
|
// keeping the native popup bounds and scrolling geometry stable.
|
||||||
popup-open-timer := Timer {
|
popup-open-timer := Timer {
|
||||||
@@ -87,14 +99,19 @@ export component GroupFilter inherits Rectangle {
|
|||||||
return reject;
|
return reject;
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
x: 10px; width: parent.width - 36px; height: parent.height;
|
x: root.embedded ? 8px : 10px;
|
||||||
|
width: parent.width - (root.show-indicator
|
||||||
|
? (root.embedded ? 26px : 36px) : (root.embedded ? 16px : 20px));
|
||||||
|
height: parent.height;
|
||||||
text: root.model[root.current-index];
|
text: root.model[root.current-index];
|
||||||
color: root.enabled ? #30343b : #92979f;
|
color: root.enabled ? root.text-color : #92979f;
|
||||||
font-size: 12px; overflow: elide; vertical-alignment: center;
|
font-size: root.text-size; font-weight: root.text-weight;
|
||||||
|
overflow: elide; vertical-alignment: center;
|
||||||
accessible-role: none;
|
accessible-role: none;
|
||||||
}
|
}
|
||||||
Text {
|
if root.show-indicator: Text {
|
||||||
x: parent.width - 25px; width: 16px; height: parent.height;
|
x: parent.width - (root.embedded ? 16px : 25px);
|
||||||
|
width: root.embedded ? 10px : 16px; height: parent.height;
|
||||||
text: FontAwesomeIcons.angle-down; font-family: "Font Awesome 6 Free";
|
text: FontAwesomeIcons.angle-down; font-family: "Font Awesome 6 Free";
|
||||||
font-weight: 900; font-size: 10px; color: #858c96;
|
font-weight: 900; font-size: 10px; color: #858c96;
|
||||||
horizontal-alignment: center; vertical-alignment: center;
|
horizontal-alignment: center; vertical-alignment: center;
|
||||||
@@ -107,12 +124,13 @@ export component GroupFilter inherits Rectangle {
|
|||||||
key-pressed(event) => { return root.handle-key(event); }
|
key-pressed(event) => { return root.handle-key(event); }
|
||||||
trigger-touch := TouchArea {
|
trigger-touch := TouchArea {
|
||||||
enabled: root.enabled;
|
enabled: root.enabled;
|
||||||
clicked => { trigger-focus.focus(); root.open-menu(); }
|
mouse-cursor: pointer;
|
||||||
|
clicked => { root.open-menu(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
popup := PopupWindow {
|
popup := PopupWindow {
|
||||||
x: 0px; y: root.height + 4px;
|
x: 0px; y: root.height + 4px;
|
||||||
width: root.width; height: root.visible-height + 8px;
|
width: max(root.width, root.popup-min-width); height: root.visible-height + 8px;
|
||||||
close-policy: close-on-click-outside;
|
close-policy: close-on-click-outside;
|
||||||
forward-focus: popup-focus;
|
forward-focus: popup-focus;
|
||||||
popup-focus := FocusScope {
|
popup-focus := FocusScope {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { GalleryStrings } from "device_strings.slint";
|
||||||
|
|
||||||
|
export global LanStrings {
|
||||||
|
// Short localized lines let the warning dialog add leading even though
|
||||||
|
// Slint Text does not expose a line-height property.
|
||||||
|
public pure function access-error-lines(code: string) -> [string] {
|
||||||
|
if code == "LanPortInUse" { return [
|
||||||
|
GalleryStrings.tr("此端口已被占用,", "This port is in use.", "Порт занят."),
|
||||||
|
GalleryStrings.tr("请更换端口。", "Choose another port.", "Выберите другой порт."),
|
||||||
|
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
|
||||||
|
]; }
|
||||||
|
if code == "LanPortCheckFailed" { return [
|
||||||
|
GalleryStrings.tr("无法确认此端口是否可用,", "Could not check this port.", "Не удалось проверить порт."),
|
||||||
|
GalleryStrings.tr("请重试或检查本机防火墙。", "Retry or check the local firewall.", "Повторите попытку или проверьте брандмауэр."),
|
||||||
|
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
|
||||||
|
]; }
|
||||||
|
if code == "LanPortPermissionDenied" { return [
|
||||||
|
GalleryStrings.tr("无法使用此端口,", "Cannot use this port.", "Порт недоступен."),
|
||||||
|
GalleryStrings.tr("请选择其他端口或检查系统权限。", "Choose another port or check system permissions.", "Смените порт или проверьте разрешения."),
|
||||||
|
GalleryStrings.tr("原设置已保留。", "Your previous settings are unchanged.", "Прежние настройки сохранены.")
|
||||||
|
]; }
|
||||||
|
if code == "Selected LAN interface is unavailable" { return [
|
||||||
|
GalleryStrings.tr("未找到可用的局域网连接。", "Connect to Wi-Fi or Ethernet,", "Подключитесь к Wi-Fi или Ethernet"),
|
||||||
|
GalleryStrings.tr("请连接 Wi-Fi 或网线,", "or select a local IP", "либо выберите IP"),
|
||||||
|
GalleryStrings.tr("或在主界面选择本机 IP。", "on the main screen.", "в главном окне.")
|
||||||
|
]; }
|
||||||
|
// Keep all branches the same size for Slint's C++ model generator.
|
||||||
|
return ["", "", ""];
|
||||||
|
}
|
||||||
|
public pure function error-message(code: string) -> string {
|
||||||
|
let lines = LanStrings.access-error-lines(code);
|
||||||
|
if lines[0] != "" {
|
||||||
|
let separator = GalleryStrings.tr("", " ", " ");
|
||||||
|
return lines[0] + separator + lines[1] + separator + lines[2];
|
||||||
|
}
|
||||||
|
if code == "InvalidCredentials" { return GalleryStrings.tr("密码不正确,或对方密码已更新。请查看对方的局域网访问密码后重试。", "Check the other desktop's current LAN password and try again.", "Проверьте текущий пароль LAN другого компьютера и повторите попытку."); }
|
||||||
|
if code == "AuthenticationRateLimited" { return GalleryStrings.tr("尝试次数过多,请等待倒计时结束后重试。", "Too many attempts. Try again when the countdown ends.", "Слишком много попыток. Дождитесь окончания обратного отсчёта."); }
|
||||||
|
if code == "CredentialsChanged" { return GalleryStrings.tr("对方密码已更新,请使用新密码重试。", "The password changed. Try again with the new password.", "Пароль изменился. Повторите с новым паролем."); }
|
||||||
|
if code == "Busy" { return GalleryStrings.tr("设备正忙,请稍后重试。", "The desktop is busy. Try again shortly.", "Компьютер занят. Повторите позже."); }
|
||||||
|
if code == "LAN password must contain exactly 6 ASCII letters or digits" { return GalleryStrings.tr("访问密码必须为 6 位英文字母或数字,区分大小写。", "Use exactly 6 letters (A–Z, a–z) or digits (0–9). Case-sensitive.", "Пароль: ровно 6 латинских букв или цифр. Регистр учитывается."); }
|
||||||
|
if code == "ConnectionFailed" || code == "ConnectionTimeout" || code == "Timeout" || code == "ConnectionClosed" { return GalleryStrings.tr("无法连接,请检查对方 IP、程序是否运行,以及防火墙是否放行。", "Check the IP, whether the other desktop is running CrossDesk, and its firewall settings.", "Проверьте IP, работу CrossDesk на другом компьютере и брандмауэр."); }
|
||||||
|
if code == "Enter a numeric IPv4 address or IPv4:port" { return GalleryStrings.tr("请输入有效的 IP,例如 192.168.1.20;指定端口可用 IP:端口,端口范围 1–65535。", "Enter a valid IP such as 192.168.1.20, or IP:port (1–65535).", "Введите IP, например 192.168.1.20, или IP:порт (1–65535)."); }
|
||||||
|
if code == "Invalid LAN port" { return GalleryStrings.tr("请输入 1–65535 之间的整数端口。", "Enter a port number from 1 to 65535.", "Введите целый номер порта от 1 до 65535."); }
|
||||||
|
if code == "LAN password is not ready" { return GalleryStrings.tr("请点击主界面密码旁的修改按钮设置密码。", "Set a password using Edit next to the password on the main screen.", "Задайте пароль кнопкой редактирования в главном окне."); }
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LanStrings } from "lan_strings.slint";
|
||||||
import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint";
|
import { AboutPanel, AboutPage, AboutComponent, AboutStrings, AboutNavigationRow, AboutNavigationGroup, AboutRowSeparator } from "about_panel.slint";
|
||||||
import { UiRadius } from "ui_radius.slint";
|
import { UiRadius } from "ui_radius.slint";
|
||||||
import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint";
|
import { WorkbenchStyle, WorkbenchButton, WorkbenchCard, WorkbenchNotice } from "workbench_controls.slint";
|
||||||
@@ -22,6 +23,8 @@ export struct RecentConnection {
|
|||||||
host-name: string,
|
host-name: string,
|
||||||
group-names: string,
|
group-names: string,
|
||||||
online: bool,
|
online: bool,
|
||||||
|
lan: bool,
|
||||||
|
endpoint: string,
|
||||||
connected: bool,
|
connected: bool,
|
||||||
thumbnail: image,
|
thumbnail: image,
|
||||||
has-thumbnail: bool,
|
has-thumbnail: bool,
|
||||||
@@ -372,6 +375,172 @@ export component MainWindow inherits Window {
|
|||||||
private property <length> wayland-corner-radius: 12px;
|
private property <length> wayland-corner-radius: 12px;
|
||||||
in property <string> local-id: "";
|
in property <string> local-id: "";
|
||||||
in property <string> local-password: "";
|
in property <string> local-password: "";
|
||||||
|
in property <bool> ip-direct-enabled: true;
|
||||||
|
changed ip-direct-enabled => {
|
||||||
|
if !root.ip-direct-enabled {
|
||||||
|
root.lan-port-dirty = false;
|
||||||
|
root.lan-port-editing = false;
|
||||||
|
root.lan-apply-pending = false;
|
||||||
|
root.lan-local-port = root.lan-listener-port;
|
||||||
|
root.lan-endpoint-error = "";
|
||||||
|
root.lan-access-warning = "";
|
||||||
|
root.lan-access-warning-endpoint = "";
|
||||||
|
root.lan-listener-error = "";
|
||||||
|
root.lan-open = false;
|
||||||
|
root.lan-connecting = false;
|
||||||
|
root.lan-listening = false;
|
||||||
|
root.lan-password-pending = false;
|
||||||
|
root.lan-retry-after = 0;
|
||||||
|
root.lan-access-password = "";
|
||||||
|
root.lan-new-password = "";
|
||||||
|
root.lan-message = "";
|
||||||
|
lan-ip-picker.close-menu();
|
||||||
|
if root.lan-mode {
|
||||||
|
root.reset-password-open = false;
|
||||||
|
root.set-connection-mode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in-out property <bool> lan-open;
|
||||||
|
changed lan-open => {
|
||||||
|
if !root.lan-open { root.lan-access-password = ""; }
|
||||||
|
if root.lan-open { root.gallery-action("release", ""); }
|
||||||
|
}
|
||||||
|
in-out property <bool> lan-mode;
|
||||||
|
callback connection-mode-changed(bool);
|
||||||
|
changed lan-mode => {
|
||||||
|
root.recent-tooltip-visible = false;
|
||||||
|
recent-scroll.viewport-x = 0px;
|
||||||
|
if !root.lan-mode {
|
||||||
|
root.lan-port-editing = false;
|
||||||
|
if root.lan-port-dirty && root.lan-endpoint-error == "" { root.lan-apply-pending = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private property <string> id-connection-draft;
|
||||||
|
private property <string> ip-connection-draft;
|
||||||
|
private property <bool> lan-password-visible: true;
|
||||||
|
private property <bool> active-password-visible: root.lan-mode ? root.lan-password-visible : root.password-visible;
|
||||||
|
private property <string> active-password: root.lan-mode ? root.lan-new-password : root.local-password;
|
||||||
|
function set-connection-mode(lan: bool) {
|
||||||
|
recent-group-filter.close-menu();
|
||||||
|
if root.lan-mode { root.ip-connection-draft = root.remote-id-input; }
|
||||||
|
else { root.id-connection-draft = root.remote-id-input; }
|
||||||
|
root.lan-mode = lan;
|
||||||
|
root.remote-id-input = root.lan-mode ? root.ip-connection-draft : root.id-connection-draft;
|
||||||
|
root.connection-input-error = "";
|
||||||
|
root.copy-feedback = CopyFeedback.idle;
|
||||||
|
root.menu-open = false;
|
||||||
|
focus-sink.focus();
|
||||||
|
}
|
||||||
|
function switch-connection-mode() {
|
||||||
|
if !root.ip-direct-enabled { return; }
|
||||||
|
root.set-connection-mode(!root.lan-mode);
|
||||||
|
root.connection-mode-changed(root.lan-mode);
|
||||||
|
}
|
||||||
|
function submit-connection() {
|
||||||
|
if root.connection-pending || root.lan-connecting { return; }
|
||||||
|
if root.lan-mode {
|
||||||
|
root.request-lan-connection(root.remote-id-input);
|
||||||
|
} else { root.connect-requested(root.remote-id-input); }
|
||||||
|
}
|
||||||
|
public function request-lan-connection(address: string) {
|
||||||
|
if !root.ip-direct-enabled || root.connection-pending || root.lan-connecting || root.lan-open { return; }
|
||||||
|
root.lan-access-password = "";
|
||||||
|
root.lan-remember-password = false;
|
||||||
|
let error = root.lan-prepare-connect(address);
|
||||||
|
root.connection-input-error = error == "Enter a numeric IPv4 address or IPv4:port"
|
||||||
|
? GalleryStrings.tr("请输入有效的 IPv4 或 IPv4:端口", "Enter a valid IPv4 or IPv4:port", "Укажите IPv4 или IPv4:порт")
|
||||||
|
: LanStrings.error-message(error);
|
||||||
|
if error == "" {
|
||||||
|
root.lan-open = true;
|
||||||
|
if root.lan-remember-password && root.lan-access-password != "" && root.lan-retry-after == 0 {
|
||||||
|
root.submit-connection-password();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in-out property <bool> lan-listening;
|
||||||
|
in-out property <string> lan-listener-ip;
|
||||||
|
in-out property <string> lan-listener-port: "52888";
|
||||||
|
out property <string> lan-listener-address: root.lan-listener-ip == "" ? "" : root.lan-listener-ip + ":" + root.lan-listener-port;
|
||||||
|
in-out property <[string]> lan-interfaces;
|
||||||
|
in-out property <int> lan-interface-index: 0;
|
||||||
|
in-out property <int> lan-interface-draft-index: 0;
|
||||||
|
in-out property <string> lan-local-port: "52888";
|
||||||
|
in-out property <bool> lan-port-editing;
|
||||||
|
in-out property <bool> lan-port-dirty;
|
||||||
|
in-out property <string> lan-endpoint-error;
|
||||||
|
in-out property <string> lan-access-warning;
|
||||||
|
in-out property <string> lan-access-warning-endpoint;
|
||||||
|
in-out property <string> lan-listener-error;
|
||||||
|
function dismiss-lan-access-warning() {
|
||||||
|
root.lan-access-warning = "";
|
||||||
|
root.lan-access-warning-endpoint = "";
|
||||||
|
// Keep the failed draft ready to edit and validate it on every new blur.
|
||||||
|
if root.lan-mode && root.lan-port-dirty && root.sponsorship-unobstructed {
|
||||||
|
lan-port-editor.focus();
|
||||||
|
} else { focus-sink.focus(); }
|
||||||
|
}
|
||||||
|
private property <bool> lan-apply-pending;
|
||||||
|
callback lan-configure(string, string) -> bool;
|
||||||
|
callback lan-select-address(string) -> bool;
|
||||||
|
function apply-lan-endpoint() -> bool {
|
||||||
|
root.lan-apply-pending = false;
|
||||||
|
if !root.ip-direct-enabled || root.lan-password-pending { return false; }
|
||||||
|
if root.lan-configure(root.lan-listener-ip, root.lan-local-port) {
|
||||||
|
root.lan-port-dirty = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
root.lan-access-warning-endpoint = root.lan-listener-ip
|
||||||
|
+ (root.lan-endpoint-error == "Invalid LAN port" ? "" : ":" + root.lan-local-port);
|
||||||
|
root.lan-access-warning = root.lan-endpoint-error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
interval: 1ms;
|
||||||
|
running: root.lan-apply-pending;
|
||||||
|
triggered => {
|
||||||
|
root.lan-apply-pending = false;
|
||||||
|
if root.lan-port-dirty { root.apply-lan-endpoint(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in-out property <string> lan-remote-address;
|
||||||
|
in-out property <string> lan-remote-port: "52888";
|
||||||
|
in-out property <bool> lan-password-pending;
|
||||||
|
in-out property <bool> lan-connecting;
|
||||||
|
in-out property <int> lan-retry-after;
|
||||||
|
in-out property <string> lan-access-password;
|
||||||
|
in-out property <bool> lan-remember-password;
|
||||||
|
in-out property <string> lan-new-password;
|
||||||
|
in-out property <string> lan-message;
|
||||||
|
callback lan-reset-password;
|
||||||
|
callback lan-copy-text(string) -> bool;
|
||||||
|
callback lan-cancel-connect;
|
||||||
|
callback lan-prepare-connect(string) -> string;
|
||||||
|
callback lan-connect(string, string, string, bool);
|
||||||
|
private property <bool> prompt-password-required: root.lan-open || root.connection-password-required;
|
||||||
|
private property <bool> prompt-validating: root.lan-open ? root.lan-connecting : root.connection-validating;
|
||||||
|
private property <bool> prompt-pending: root.lan-open ? root.lan-connecting : root.connection-pending;
|
||||||
|
private property <string> prompt-status-text: !root.lan-open ? root.connection-status-text
|
||||||
|
: root.lan-connecting ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…")
|
||||||
|
: root.lan-retry-after > 0 ? GalleryStrings.tr("请在 " + root.lan-retry-after + " 秒后重试", "Try again in " + root.lan-retry-after + "s", "Повторите через " + root.lan-retry-after + " с")
|
||||||
|
: root.lan-message == "InvalidCredentials" ? UiStrings.reinput-password
|
||||||
|
: root.lan-message == "" || root.lan-message == "AuthenticationRateLimited" ? UiStrings.input-password
|
||||||
|
: LanStrings.error-message(root.lan-message);
|
||||||
|
function submit-connection-password() {
|
||||||
|
if root.lan-open {
|
||||||
|
if root.lan-connecting || root.lan-retry-after > 0 || root.lan-access-password.character-count != 6 { return; }
|
||||||
|
root.lan-connect(root.lan-remote-address, root.lan-remote-port, root.lan-access-password, root.lan-remember-password);
|
||||||
|
root.lan-access-password = "";
|
||||||
|
} else { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
|
||||||
|
}
|
||||||
|
function cancel-connection-prompt() {
|
||||||
|
if root.lan-open {
|
||||||
|
root.lan-cancel-connect();
|
||||||
|
root.lan-access-password = "";
|
||||||
|
root.lan-open = false;
|
||||||
|
focus-sink.focus();
|
||||||
|
} else { root.connection-cancel(); }
|
||||||
|
}
|
||||||
in-out property <string> remote-id-input: "";
|
in-out property <string> remote-id-input: "";
|
||||||
in property <bool> password-visible: true;
|
in property <bool> password-visible: true;
|
||||||
in property <bool> password-change-pending: false;
|
in property <bool> password-change-pending: false;
|
||||||
@@ -456,6 +625,8 @@ export component MainWindow inherits Window {
|
|||||||
in-out property <int> capture-method-index: 0;
|
in-out property <int> capture-method-index: 0;
|
||||||
in-out property <int> codec-index: 0;
|
in-out property <int> codec-index: 0;
|
||||||
in-out property <bool> turn-enabled: true;
|
in-out property <bool> turn-enabled: true;
|
||||||
|
in-out property <bool> ip-direct-enabled-draft: true;
|
||||||
|
in property <string> settings-save-error;
|
||||||
in-out property <bool> force-relay-enabled: false;
|
in-out property <bool> force-relay-enabled: false;
|
||||||
changed turn-enabled => {
|
changed turn-enabled => {
|
||||||
if !root.turn-enabled { root.force-relay-enabled = false; }
|
if !root.turn-enabled { root.force-relay-enabled = false; }
|
||||||
@@ -555,11 +726,11 @@ export component MainWindow inherits Window {
|
|||||||
&& !root.update-confirm-open
|
&& !root.update-confirm-open
|
||||||
&& !root.reset-password-open && !root.alias-open && !root.delete-open
|
&& !root.reset-password-open && !root.alias-open && !root.delete-open
|
||||||
&& !root.disconnect-open
|
&& !root.disconnect-open
|
||||||
&& !root.device-group-open && !root.connection-dialog-open
|
&& !root.device-group-open && !root.connection-dialog-open && !root.lan-open
|
||||||
&& !root.permission-dialog-open && !root.wayland-notice-open
|
&& !root.permission-dialog-open && !root.wayland-notice-open
|
||||||
&& !root.portable-service-dialog-open
|
&& !root.portable-service-dialog-open
|
||||||
&& !root.portable-service-suppressed-notice-open
|
&& !root.portable-service-suppressed-notice-open
|
||||||
&& root.offline-warning == "";
|
&& root.offline-warning == "" && root.lan-access-warning == "";
|
||||||
out property <bool> can-show-sponsorship: !root.sponsorship-open
|
out property <bool> can-show-sponsorship: !root.sponsorship-open
|
||||||
&& root.sponsorship-unobstructed;
|
&& root.sponsorship-unobstructed;
|
||||||
// Follow the first available update. Dismissing the dialog replaces this
|
// Follow the first available update. Dismissing the dialog replaces this
|
||||||
@@ -570,6 +741,10 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
in-out property <bool> update-confirm-open: false;
|
in-out property <bool> update-confirm-open: false;
|
||||||
in-out property <bool> reset-password-open: false;
|
in-out property <bool> reset-password-open: false;
|
||||||
|
changed reset-password-open => {
|
||||||
|
root.new-password-input = "";
|
||||||
|
root.reset-password-invalid = false;
|
||||||
|
}
|
||||||
in-out property <bool> reset-password-invalid: false;
|
in-out property <bool> reset-password-invalid: false;
|
||||||
in-out property <string> new-password-input: "";
|
in-out property <string> new-password-input: "";
|
||||||
in-out property <bool> alias-open: false;
|
in-out property <bool> alias-open: false;
|
||||||
@@ -926,9 +1101,21 @@ export component MainWindow inherits Window {
|
|||||||
|
|
||||||
// A compact application toolbar keeps global actions outside device cards.
|
// A compact application toolbar keeps global actions outside device cards.
|
||||||
// Center the 28px buttons above the cards with 6px of space on each side.
|
// Center the 28px buttons above the cards with 6px of space on each side.
|
||||||
|
connection-mode-button := WorkbenchButton {
|
||||||
|
visible: !root.gallery-mode && root.ip-direct-enabled;
|
||||||
|
x: 20px; y: 6px;
|
||||||
|
width: 120px; height: 28px;
|
||||||
|
text: root.lan-mode ? GalleryStrings.tr("IP 直连模式", "Direct IP mode", "Режим IP") : GalleryStrings.tr("ID 连接模式", "ID mode", "Режим ID");
|
||||||
|
tooltip: root.lan-mode ? GalleryStrings.tr("切换到 ID 连接模式", "Switch to ID mode", "Переключить в режим ID") : GalleryStrings.tr("切换到 IP 直连模式", "Switch to direct IP mode", "Переключить в режим IP");
|
||||||
|
accessible-label: self.text + " · " + self.tooltip;
|
||||||
|
enabled: !root.connection-pending && !root.lan-connecting;
|
||||||
|
z: 8;
|
||||||
|
clicked => { root.switch-connection-mode(); }
|
||||||
|
}
|
||||||
open-gallery-button := WorkbenchButton {
|
open-gallery-button := WorkbenchButton {
|
||||||
visible: !root.gallery-mode;
|
visible: !root.gallery-mode;
|
||||||
x: 20px; y: 6px;
|
x: root.ip-direct-enabled ? connection-mode-button.x + connection-mode-button.width + 8px : 20px;
|
||||||
|
y: connection-mode-button.y;
|
||||||
width: 80px; height: 28px;
|
width: 80px; height: 28px;
|
||||||
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
|
text: GalleryStrings.tr("桌面墙", "DeskHub", "DeskHub");
|
||||||
z: 8;
|
z: 8;
|
||||||
@@ -1024,22 +1211,166 @@ export component MainWindow inherits Window {
|
|||||||
x: 0px; y: 0px;
|
x: 0px; y: 0px;
|
||||||
width: (parent.width - 16px) / 2; height: parent.height;
|
width: (parent.width - 16px) / 2; height: parent.height;
|
||||||
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.local-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
|
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.local-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
|
||||||
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: GalleryStrings.tr("将 ID 和密码分享给连接方", "Share your ID and password to get connected", "Передайте ID и пароль для подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
|
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("将 IP 和密码分享给连接方", "Share your IP and password to get connected", "Передайте IP и пароль для управления") : GalleryStrings.tr("将 ID 和密码分享给连接方", "Share your ID and password to get connected", "Передайте ID и пароль для подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
|
||||||
Text { x: 16px; y: 68px; height: 18px; text: UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; }
|
local-address-label := Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("本机 IP / 端口", "Local IP / Port", "IP / Порт") : UiStrings.local-id; font-size: 12px; color: WorkbenchStyle.muted; vertical-alignment: top; }
|
||||||
Rectangle {
|
lan-switch-ip-metrics := Text {
|
||||||
|
visible: false; accessible-role: none;
|
||||||
|
text: GalleryStrings.tr("切换 IP", "Switch IP", "Сменить IP");
|
||||||
|
font-size: 12px; font-weight: 500;
|
||||||
|
}
|
||||||
|
lan-edit-port-metrics := Text {
|
||||||
|
visible: false; accessible-role: none;
|
||||||
|
text: GalleryStrings.tr("修改端口", "Edit port", "Изменить порт");
|
||||||
|
font-size: 12px; font-weight: 500;
|
||||||
|
}
|
||||||
|
lan-switch-ip-action := WorkbenchButton {
|
||||||
|
visible: root.lan-mode;
|
||||||
|
x: local-address-label.x + local-address-label.preferred-width + 4px;
|
||||||
|
y: local-address-label.y - 5px;
|
||||||
|
width: lan-switch-ip-metrics.preferred-width + 20px; height: 22px;
|
||||||
|
quiet: true; text: lan-switch-ip-metrics.text;
|
||||||
|
text-size: 12px; text-color: WorkbenchStyle.accent;
|
||||||
|
enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0;
|
||||||
|
clicked => {
|
||||||
|
if root.lan-port-dirty && !root.apply-lan-endpoint() { return; }
|
||||||
|
lan-ip-picker.open-menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WorkbenchButton {
|
||||||
|
visible: root.lan-mode;
|
||||||
|
x: lan-switch-ip-action.x + lan-switch-ip-action.width + 2px;
|
||||||
|
y: lan-switch-ip-action.y;
|
||||||
|
width: lan-edit-port-metrics.preferred-width + 20px; height: 22px;
|
||||||
|
quiet: true; text: lan-edit-port-metrics.text;
|
||||||
|
text-size: 12px; text-color: WorkbenchStyle.accent;
|
||||||
|
enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0;
|
||||||
|
clicked => {
|
||||||
|
// Moving from the editor to this action is still editing.
|
||||||
|
root.lan-apply-pending = false;
|
||||||
|
lan-port-editor.focus();
|
||||||
|
lan-port-editor.select-all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local-address-field := Rectangle {
|
||||||
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
|
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
|
||||||
|
// Size the endpoint as one line; reserve five port digits so
|
||||||
|
// editing never shifts the IP or changes the font size.
|
||||||
|
private property <float> lan-font-scale: min(1,
|
||||||
|
(self.width - 80px) / (lan-ip-metrics.preferred-width + lan-port-metrics.preferred-width));
|
||||||
|
lan-ip-metrics := Text {
|
||||||
|
visible: false; accessible-role: none;
|
||||||
|
text: root.lan-interfaces[root.lan-interface-draft-index];
|
||||||
|
font-size: 18px; font-weight: 600;
|
||||||
|
}
|
||||||
|
lan-port-metrics := Text {
|
||||||
|
visible: false; accessible-role: none;
|
||||||
|
text: "65535"; font-size: 18px; font-weight: 600;
|
||||||
|
}
|
||||||
// Raise the whole field: a child's z cannot escape its
|
// Raise the whole field: a child's z cannot escape its
|
||||||
// parent's stacking order above the following password row.
|
// parent's stacking order above the following password row.
|
||||||
z: 1;
|
z: 1;
|
||||||
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
|
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
|
||||||
border-width: 1px; border-color: WorkbenchStyle.border;
|
border-width: 1px;
|
||||||
Text { x: 12px; width: parent.width - 64px; height: parent.height; text: root.local-id; font-size: 24px; font-weight: 600; letter-spacing: 1px; color: WorkbenchStyle.text; vertical-alignment: center; }
|
border-color: !root.lan-mode ? WorkbenchStyle.border
|
||||||
|
: root.lan-endpoint-error != "" ? #d92d20
|
||||||
|
: lan-ip-picker.has-focus || root.lan-port-editing ? WorkbenchStyle.accent
|
||||||
|
: WorkbenchStyle.border;
|
||||||
|
animate border-color { duration: 120ms; }
|
||||||
|
if !root.lan-mode: Text { x: 12px; width: parent.width - 64px; height: parent.height; text: root.local-id; font-size: 24px; font-weight: 600; letter-spacing: 1px; overflow: elide; color: WorkbenchStyle.text; vertical-alignment: center; }
|
||||||
Rectangle { x: parent.width - 48px; y: 10px; width: 1px; height: parent.height - 20px; background: WorkbenchStyle.border; }
|
Rectangle { x: parent.width - 48px; y: 10px; width: 1px; height: parent.height - 20px; background: WorkbenchStyle.border; }
|
||||||
|
lan-ip-picker := GroupFilter {
|
||||||
|
visible: root.lan-mode;
|
||||||
|
x: 4px; y: 2px;
|
||||||
|
width: lan-ip-metrics.preferred-width * local-address-field.lan-font-scale + 16px;
|
||||||
|
height: parent.height - 4px;
|
||||||
|
embedded: true; text-size: 18px * local-address-field.lan-font-scale;
|
||||||
|
text-weight: 600; text-color: WorkbenchStyle.text;
|
||||||
|
show-indicator: false;
|
||||||
|
popup-min-width: 160px;
|
||||||
|
model: root.lan-interfaces; system-count: 0;
|
||||||
|
current-index <=> root.lan-interface-draft-index;
|
||||||
|
enabled: root.lan-mode && !root.lan-password-pending && root.lan-interfaces.length > 0;
|
||||||
|
normal-border-color: WorkbenchStyle.control-border;
|
||||||
|
hover-border-color: WorkbenchStyle.hover-border;
|
||||||
|
focus-border-color: WorkbenchStyle.accent;
|
||||||
|
accessible-label: GalleryStrings.tr("切换本机 IP", "Switch local IP", "Сменить локальный IP");
|
||||||
|
selected(index) => {
|
||||||
|
root.copy-feedback = CopyFeedback.idle;
|
||||||
|
// Save the draft for its original IP before changing
|
||||||
|
// the display. Selecting an IP never rebinds sockets.
|
||||||
|
if root.lan-port-dirty && !root.apply-lan-endpoint() {
|
||||||
|
root.lan-interface-draft-index = root.lan-interface-index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if root.lan-select-address(root.lan-interfaces[index]) {
|
||||||
|
root.lan-interface-index = index;
|
||||||
|
} else {
|
||||||
|
root.lan-access-warning-endpoint = root.lan-interfaces[index];
|
||||||
|
root.lan-access-warning = root.lan-endpoint-error;
|
||||||
|
root.lan-interface-draft-index = root.lan-interface-index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if root.lan-mode: Text {
|
||||||
|
x: lan-ip-picker.x + lan-ip-picker.width - 4px; width: 6px; height: parent.height;
|
||||||
|
text: ":"; font-size: 18px * local-address-field.lan-font-scale;
|
||||||
|
font-weight: 600; color: WorkbenchStyle.text;
|
||||||
|
horizontal-alignment: center; vertical-alignment: center;
|
||||||
|
}
|
||||||
|
lan-port-field := Rectangle {
|
||||||
|
visible: root.lan-mode;
|
||||||
|
x: lan-ip-picker.x + lan-ip-picker.width + 2px; y: 2px;
|
||||||
|
width: lan-port-metrics.preferred-width * local-address-field.lan-font-scale + 4px;
|
||||||
|
height: parent.height - 4px;
|
||||||
|
background: lan-port-editor.has-focus ? WorkbenchStyle.surface
|
||||||
|
: lan-port-hover.has-hover ? WorkbenchStyle.hover-surface : transparent;
|
||||||
|
border-radius: UiRadius.control;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: lan-port-editor.has-focus ? WorkbenchStyle.accent : transparent;
|
||||||
|
animate background, border-color { duration: 120ms; }
|
||||||
|
lan-port-hover := TouchArea {
|
||||||
|
enabled: root.lan-mode && !root.lan-password-pending;
|
||||||
|
mouse-cursor: text;
|
||||||
|
FocusScope {
|
||||||
|
width: parent.width; height: parent.height;
|
||||||
|
key-pressed(event) => {
|
||||||
|
if event.text == Key.Escape {
|
||||||
|
root.lan-apply-pending = false;
|
||||||
|
root.lan-port-dirty = false;
|
||||||
|
root.lan-local-port = root.lan-listener-port;
|
||||||
|
root.lan-endpoint-error = "";
|
||||||
|
focus-sink.focus();
|
||||||
|
return accept;
|
||||||
|
}
|
||||||
|
return reject;
|
||||||
|
}
|
||||||
|
lan-port-editor := TextInput {
|
||||||
|
x: 2px; width: parent.width - 4px; height: parent.height;
|
||||||
|
text <=> root.lan-local-port;
|
||||||
|
font-size: 18px * local-address-field.lan-font-scale; font-weight: 600;
|
||||||
|
input-type: InputType.number; color: WorkbenchStyle.text;
|
||||||
|
enabled: root.lan-mode && !root.lan-password-pending;
|
||||||
|
accessible-label: GalleryStrings.tr("局域网端口", "LAN port", "Порт ЛВС");
|
||||||
|
vertical-alignment: center; horizontal-alignment: left;
|
||||||
|
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
|
||||||
|
changed has-focus => {
|
||||||
|
root.lan-port-editing = self.has-focus;
|
||||||
|
// Opening the warning also moves focus: do not
|
||||||
|
// treat that as another attempt to save.
|
||||||
|
if !self.has-focus && root.lan-port-dirty && root.lan-access-warning == "" { root.lan-apply-pending = true; }
|
||||||
|
}
|
||||||
|
edited => { root.lan-port-dirty = true; root.lan-endpoint-error = ""; root.copy-feedback = CopyFeedback.idle; }
|
||||||
|
accepted => { if root.apply-lan-endpoint() { focus-sink.focus(); } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
copy-button := WorkbenchButton {
|
copy-button := WorkbenchButton {
|
||||||
x: parent.width - 42px; y: 2px; width: 36px; quiet: true;
|
x: parent.width - 42px; y: 2px; width: 36px; quiet: true;
|
||||||
icon: root.copy-feedback == CopyFeedback.success ? FontAwesomeIcons.check : FontAwesomeIcons.copy;
|
icon: root.copy-feedback == CopyFeedback.success ? FontAwesomeIcons.check : FontAwesomeIcons.copy;
|
||||||
icon-color: root.copy-feedback == CopyFeedback.success ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
icon-color: root.copy-feedback == CopyFeedback.success ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
||||||
tooltip: GalleryStrings.tr("复制本机 ID", "Copy local ID", "Копировать ID");
|
tooltip: root.lan-mode ? GalleryStrings.tr("复制 IP 和端口", "Copy IP and port", "Копировать IP и порт") : GalleryStrings.tr("复制本机 ID", "Copy local ID", "Копировать ID");
|
||||||
|
enabled: !root.lan-mode || root.lan-listener-address != "";
|
||||||
tooltip-suppressed: root.copy-feedback != CopyFeedback.idle;
|
tooltip-suppressed: root.copy-feedback != CopyFeedback.idle;
|
||||||
// Let other controls receive the original click; moving
|
// Let other controls receive the original click; moving
|
||||||
// focus away from Copy dismisses its transient feedback.
|
// focus away from Copy dismisses its transient feedback.
|
||||||
@@ -1047,35 +1378,61 @@ export component MainWindow inherits Window {
|
|||||||
if !self.has-focus { root.copy-feedback = CopyFeedback.idle; }
|
if !self.has-focus { root.copy-feedback = CopyFeedback.idle; }
|
||||||
}
|
}
|
||||||
clicked => {
|
clicked => {
|
||||||
root.copy-feedback = root.copy-local-id() ? CopyFeedback.success : CopyFeedback.failed;
|
if root.lan-mode && root.lan-port-dirty && !root.apply-lan-endpoint() { return; }
|
||||||
|
let copied = root.lan-mode ? root.lan-copy-text(root.lan-listener-address) : root.copy-local-id();
|
||||||
|
root.copy-feedback = copied ? CopyFeedback.success : CopyFeedback.failed;
|
||||||
copy-timer.restart();
|
copy-timer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private property <string> edit-hint: !root.lan-mode || !root.sponsorship-unobstructed
|
||||||
|
|| root.lan-password-pending ? ""
|
||||||
|
: lan-ip-picker.has-hover ? GalleryStrings.tr("点击切换本机 IP", "Click to switch the local IP", "Нажмите, чтобы сменить IP")
|
||||||
|
: lan-port-hover.has-hover && !lan-port-editor.has-focus ? GalleryStrings.tr("点击修改端口", "Click to edit the port", "Нажмите, чтобы изменить порт") : "";
|
||||||
|
private property <bool> edit-hint-visible;
|
||||||
|
changed edit-hint => { local-address-field.edit-hint-visible = false; }
|
||||||
|
Timer {
|
||||||
|
interval: 450ms;
|
||||||
|
running: local-address-field.edit-hint != "" && !local-address-field.edit-hint-visible;
|
||||||
|
triggered => { local-address-field.edit-hint-visible = true; }
|
||||||
|
}
|
||||||
|
if local-address-field.edit-hint != "" && local-address-field.edit-hint-visible: Rectangle {
|
||||||
|
x: clamp((lan-ip-picker.has-hover ? lan-ip-picker.x + lan-ip-picker.width / 2
|
||||||
|
: lan-port-field.x + lan-port-field.width / 2) - self.width / 2, 0px, parent.width - self.width);
|
||||||
|
y: parent.height + 6px;
|
||||||
|
width: min(parent.width, edit-hint-text.preferred-width + 20px);
|
||||||
|
height: max(28px, edit-hint-text.preferred-height + 12px);
|
||||||
|
background: #24324a; border-radius: UiRadius.control; z: 20;
|
||||||
|
edit-hint-text := Text {
|
||||||
|
x: 10px; y: 6px; width: parent.width - 20px; height: parent.height - 12px;
|
||||||
|
text: local-address-field.edit-hint; font-size: 12px; color: white;
|
||||||
|
wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Text { x: 16px; y: 138px; height: 18px; text: UiStrings.password; font-size: 12px; color: WorkbenchStyle.muted; }
|
Text { x: 16px; y: 138px; height: 18px; text: UiStrings.password; font-size: 12px; color: WorkbenchStyle.muted; }
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
|
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
|
||||||
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
|
background: WorkbenchStyle.field-surface; border-radius: UiRadius.control;
|
||||||
border-width: 1px; border-color: WorkbenchStyle.border;
|
border-width: 1px; border-color: WorkbenchStyle.border;
|
||||||
Text { x: 12px; width: parent.width - 132px; height: parent.height; text: root.password-visible ? root.local-password : "••••••"; font-size: 20px; font-weight: 500; letter-spacing: 2px; color: WorkbenchStyle.text; vertical-alignment: center; }
|
Text { x: 12px; width: parent.width - 132px; height: parent.height; text: root.active-password == "" ? "—" : root.active-password-visible ? root.active-password : "••••••"; font-size: 20px; font-weight: 500; letter-spacing: 2px; color: WorkbenchStyle.text; vertical-alignment: center; }
|
||||||
Rectangle { x: parent.width - 118px; y: 8px; width: 1px; height: parent.height - 16px; background: WorkbenchStyle.border; }
|
Rectangle { x: parent.width - 118px; y: 8px; width: 1px; height: parent.height - 16px; background: WorkbenchStyle.border; }
|
||||||
WorkbenchButton {
|
WorkbenchButton {
|
||||||
x: parent.width - 112px; y: 0px; width: 36px; quiet: true;
|
x: parent.width - 112px; y: 0px; width: 36px; quiet: true;
|
||||||
icon: root.password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
|
icon: root.active-password-visible ? FontAwesomeIcons.eye : FontAwesomeIcons.eye-slash;
|
||||||
tooltip: root.password-visible ? GalleryStrings.tr("隐藏密码", "Hide password", "Скрыть пароль") : GalleryStrings.tr("显示密码", "Show password", "Показать пароль");
|
tooltip: root.active-password-visible ? GalleryStrings.tr("隐藏密码", "Hide password", "Скрыть пароль") : GalleryStrings.tr("显示密码", "Show password", "Показать пароль");
|
||||||
clicked => { root.toggle-password-visibility(); }
|
clicked => { if root.lan-mode { root.lan-password-visible = !root.lan-password-visible; } else { root.toggle-password-visibility(); } }
|
||||||
}
|
}
|
||||||
WorkbenchButton {
|
WorkbenchButton {
|
||||||
x: parent.width - 74px; y: 0px; width: 36px; quiet: true;
|
x: parent.width - 74px; y: 0px; width: 36px; quiet: true;
|
||||||
icon: FontAwesomeIcons.rotate; tooltip: UiStrings.refresh-password;
|
icon: FontAwesomeIcons.rotate; tooltip: UiStrings.refresh-password;
|
||||||
enabled: !root.password-change-pending;
|
enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending;
|
||||||
clicked => { root.refresh-password(); }
|
clicked => { if root.lan-mode { root.lan-reset-password(); } else { root.refresh-password(); } }
|
||||||
}
|
}
|
||||||
WorkbenchButton {
|
WorkbenchButton {
|
||||||
x: parent.width - 36px; y: 0px; width: 36px; quiet: true;
|
x: parent.width - 36px; y: 0px; width: 36px; quiet: true;
|
||||||
icon: FontAwesomeIcons.pen;
|
icon: FontAwesomeIcons.pen;
|
||||||
tooltip: GalleryStrings.tr("修改密码", "Change password", "Изменить пароль");
|
tooltip: GalleryStrings.tr("修改密码", "Change password", "Изменить пароль");
|
||||||
enabled: !root.password-change-pending;
|
enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending;
|
||||||
clicked => { root.reset-password-open = true; }
|
clicked => { root.reset-password-open = true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1083,8 +1440,8 @@ export component MainWindow inherits Window {
|
|||||||
WorkbenchCard {
|
WorkbenchCard {
|
||||||
x: local-card.width + 16px; y: 0px; width: local-card.width; height: parent.height;
|
x: local-card.width + 16px; y: 0px; width: local-card.width; height: parent.height;
|
||||||
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.remote-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
|
Text { x: 16px; y: 14px; width: parent.width - 32px; height: 24px; overflow: elide; text: UiStrings.remote-desktop; font-size: 16px; font-weight: 600; color: WorkbenchStyle.text; }
|
||||||
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: GalleryStrings.tr("输入设备 ID,连接远程桌面", "Enter a device ID to start a remote session", "Введите ID для удалённого подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
|
Text { x: 16px; y: 38px; width: parent.width - 32px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("输入 IP:端口,通过局域网连接", "Enter IP:port to connect over LAN", "Введите IP:порт для подключения по LAN") : GalleryStrings.tr("输入设备 ID,连接远程桌面", "Enter a device ID to start a remote session", "Введите ID для удалённого подключения"); font-size: 11px; color: WorkbenchStyle.muted; overflow: elide; }
|
||||||
Text { x: 16px; y: 68px; height: 18px; text: UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; }
|
Text { x: 16px; y: 68px; height: 18px; text: root.lan-mode ? GalleryStrings.tr("远端 IP / 端口", "Remote IP / Port", "Удалённый IP / Порт") : UiStrings.remote-id; font-size: 12px; color: WorkbenchStyle.muted; }
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
|
x: 16px; y: 90px; width: parent.width - 32px; height: 40px;
|
||||||
background: white; border-radius: UiRadius.control; border-width: 1px;
|
background: white; border-radius: UiRadius.control; border-width: 1px;
|
||||||
@@ -1092,40 +1449,42 @@ export component MainWindow inherits Window {
|
|||||||
animate border-color { duration: 120ms; }
|
animate border-color { duration: 120ms; }
|
||||||
if root.remote-id-input == "": Text {
|
if root.remote-id-input == "": Text {
|
||||||
x: 12px; width: parent.width - 24px; height: parent.height;
|
x: 12px; width: parent.width - 24px; height: parent.height;
|
||||||
text: GalleryStrings.tr("请输入 9 位设备 ID", "Enter a 9-digit device ID", "Введите ID из 9 цифр");
|
text: root.lan-mode ? "192.168.1.20:52888" : GalleryStrings.tr("请输入 9 位设备 ID", "Enter a 9-digit device ID", "Введите ID из 9 цифр");
|
||||||
font-size: 13px; color: #8290a4; vertical-alignment: center;
|
font-size: 13px; color: #8290a4; vertical-alignment: center;
|
||||||
}
|
}
|
||||||
remote-id-editor := TextInput {
|
remote-id-editor := TextInput {
|
||||||
x: 12px; width: parent.width - 24px; height: parent.height;
|
x: 12px; width: parent.width - 24px; height: parent.height;
|
||||||
text <=> root.remote-id-input; font-size: 24px; font-weight: 600; letter-spacing: 1px;
|
text <=> root.remote-id-input; font-size: root.lan-mode ? 18px : 24px; font-weight: 600; letter-spacing: root.lan-mode ? 0px : 1px;
|
||||||
input-type: InputType.number; color: WorkbenchStyle.text;
|
input-type: root.lan-mode ? InputType.text : InputType.number; color: WorkbenchStyle.text;
|
||||||
accessible-label: UiStrings.remote-id;
|
accessible-label: root.lan-mode ? GalleryStrings.tr("远端 IP / 端口", "Remote IP / Port", "Удалённый IP / Порт") : UiStrings.remote-id;
|
||||||
|
enabled: !root.connection-pending && !root.lan-connecting;
|
||||||
vertical-alignment: center;
|
vertical-alignment: center;
|
||||||
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
|
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
|
||||||
edited => {
|
edited => {
|
||||||
root.connection-input-error = "";
|
root.connection-input-error = "";
|
||||||
// Enforce the digit limit before the next frame can
|
// Enforce the digit limit before the next frame can
|
||||||
// render the text from typing, paste, or replacement.
|
// render the text from typing, paste, or replacement.
|
||||||
let formatted = root.format-remote-id(self.text);
|
let formatted = root.lan-mode ? self.text : root.format-remote-id(self.text);
|
||||||
self.text = formatted;
|
self.text = formatted;
|
||||||
// Formatted IDs are ASCII, so these are UTF-8 offsets.
|
// Formatted IDs are ASCII, so these are UTF-8 offsets.
|
||||||
self.set-selection-offsets(
|
self.set-selection-offsets(
|
||||||
formatted.character-count, formatted.character-count);
|
formatted.character-count, formatted.character-count);
|
||||||
}
|
}
|
||||||
accepted => { if !root.connection-pending { root.connect-requested(root.remote-id-input); } }
|
accepted => { root.submit-connection(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
x: 16px; y: 133px; width: parent.width - 32px; height: 24px;
|
x: 16px; y: 133px; width: parent.width - 32px; height: 24px;
|
||||||
text: root.connection-input-error;
|
text: root.connection-input-error != "" ? root.connection-input-error
|
||||||
font-size: 11px; color: #be3d42; wrap: word-wrap;
|
: root.lan-mode ? GalleryStrings.tr("不填端口时,默认使用 52888", "Port defaults to 52888 if omitted", "Без порта используется 52888") : "";
|
||||||
|
font-size: 11px; color: root.connection-input-error != "" ? #be3d42 : WorkbenchStyle.muted; wrap: word-wrap;
|
||||||
}
|
}
|
||||||
WorkbenchButton {
|
WorkbenchButton {
|
||||||
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
|
x: 16px; y: 158px; width: parent.width - 32px; height: 36px;
|
||||||
primary: true; icon: FontAwesomeIcons.arrow-right-long;
|
primary: true; icon: FontAwesomeIcons.arrow-right-long;
|
||||||
text: root.connection-pending ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…") : UiStrings.connect;
|
text: root.connection-pending || root.lan-connecting ? GalleryStrings.tr("正在连接…", "Connecting…", "Подключение…") : UiStrings.connect;
|
||||||
enabled: !root.connection-pending;
|
enabled: !root.connection-pending && !root.lan-connecting;
|
||||||
clicked => { root.connect-requested(root.remote-id-input); }
|
clicked => { root.submit-connection(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1137,6 +1496,7 @@ export component MainWindow inherits Window {
|
|||||||
|
|
||||||
Text { x: 20px; y: 0px; width: recent-group-filter.x - self.x - 12px; height: 32px; text: UiStrings.recent-connections; font-size: 15px; font-weight: 600; color: WorkbenchStyle.text; vertical-alignment: center; }
|
Text { x: 20px; y: 0px; width: recent-group-filter.x - self.x - 12px; height: 32px; text: UiStrings.recent-connections; font-size: 15px; font-weight: 600; color: WorkbenchStyle.text; vertical-alignment: center; }
|
||||||
recent-group-filter := GroupFilter {
|
recent-group-filter := GroupFilter {
|
||||||
|
visible: !root.lan-mode;
|
||||||
x: parent.width - self.width - 20px; y: 0px;
|
x: parent.width - self.width - 20px; y: 0px;
|
||||||
width: 155px; height: 32px;
|
width: 155px; height: 32px;
|
||||||
normal-border-color: WorkbenchStyle.control-border;
|
normal-border-color: WorkbenchStyle.control-border;
|
||||||
@@ -1169,14 +1529,18 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
x: 84px; y: 36px; width: parent.width - 104px; height: 24px;
|
x: 84px; y: 36px; width: parent.width - 104px; height: 24px;
|
||||||
text: root.recent-group-index == 0
|
text: root.lan-mode
|
||||||
|
? GalleryStrings.tr("暂无 IP 直连设备", "No recent IP connections", "Нет недавних IP-подключений")
|
||||||
|
: root.recent-group-index == 0
|
||||||
? GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений")
|
? GalleryStrings.tr("暂无近期连接", "No recent connections", "Нет недавних подключений")
|
||||||
: GalleryStrings.tr("该分组暂无近期连接", "No recent connections in this group", "В этой группе нет недавних подключений");
|
: GalleryStrings.tr("该分组暂无近期连接", "No recent connections in this group", "В этой группе нет недавних подключений");
|
||||||
color: WorkbenchStyle.text; font-size: 14px; font-weight: 600;
|
color: WorkbenchStyle.text; font-size: 14px; font-weight: 600;
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
x: 84px; y: 68px; width: parent.width - 104px; height: 20px;
|
x: 84px; y: 68px; width: parent.width - 104px; height: 20px;
|
||||||
text: root.recent-group-index == 0
|
text: root.lan-mode
|
||||||
|
? GalleryStrings.tr("通过 IP 连接过的设备会显示在这里。", "Devices you connect to by IP will appear here.", "Здесь появятся устройства, подключённые по IP.")
|
||||||
|
: root.recent-group-index == 0
|
||||||
? GalleryStrings.tr("连接过的设备会显示在这里,方便下次快速访问。", "Devices you connect to will appear here for quick access.", "Устройства появятся здесь после первого подключения.")
|
? GalleryStrings.tr("连接过的设备会显示在这里,方便下次快速访问。", "Devices you connect to will appear here for quick access.", "Устройства появятся здесь после первого подключения.")
|
||||||
: GalleryStrings.tr("切换设备分组,查看其他连接记录。", "Choose another group to see your other connections.", "Выберите другую группу для просмотра подключений.");
|
: GalleryStrings.tr("切换设备分组,查看其他连接记录。", "Choose another group to see your other connections.", "Выберите другую группу для просмотра подключений.");
|
||||||
color: WorkbenchStyle.muted; font-size: 12px;
|
color: WorkbenchStyle.muted; font-size: 12px;
|
||||||
@@ -1202,7 +1566,7 @@ export component MainWindow inherits Window {
|
|||||||
width: (recent-scroll.width - 24px) / 3;
|
width: (recent-scroll.width - 24px) / 3;
|
||||||
height: 126px;
|
height: 126px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
private property <bool> available: connection.online || connection.connected;
|
private property <bool> available: connection.lan || connection.online || connection.connected;
|
||||||
private property <length> content-width: self.width;
|
private property <length> content-width: self.width;
|
||||||
private property <length> scroll-offset: recent-scroll.viewport-x;
|
private property <length> scroll-offset: recent-scroll.viewport-x;
|
||||||
private property <string> device-id: connection.remote-id;
|
private property <string> device-id: connection.remote-id;
|
||||||
@@ -1263,7 +1627,7 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
presence-label := Text {
|
presence-label := Text {
|
||||||
x: 17px; width: self.preferred-width; height: parent.height;
|
x: 17px; width: self.preferred-width; height: parent.height;
|
||||||
text: connection-card.available ? UiStrings.online : UiStrings.offline;
|
text: connection.lan ? "LAN" : connection-card.available ? UiStrings.online : UiStrings.offline;
|
||||||
color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
color: connection-card.available ? WorkbenchStyle.success : WorkbenchStyle.muted;
|
||||||
font-size: 10px; font-weight: 500; vertical-alignment: center;
|
font-size: 10px; font-weight: 500; vertical-alignment: center;
|
||||||
}
|
}
|
||||||
@@ -1286,7 +1650,7 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
x: 12px; y: 24px; height: 16px; width: parent.width - 56px;
|
x: 12px; y: 24px; height: 16px; width: parent.width - 56px;
|
||||||
text: connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted;
|
text: connection.lan ? connection.endpoint : connection.remote-id; font-size: 11px; color: WorkbenchStyle.muted;
|
||||||
overflow: elide;
|
overflow: elide;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1367,7 +1731,7 @@ export component MainWindow inherits Window {
|
|||||||
menu-width: root.compact-language ? 116px : root.language-index == 2 ? 156px : 128px;
|
menu-width: root.compact-language ? 116px : root.language-index == 2 ? 156px : 128px;
|
||||||
items: [
|
items: [
|
||||||
{label: GalleryStrings.tr("修改名称", "Rename", "Переименовать"), enabled: true},
|
{label: GalleryStrings.tr("修改名称", "Rename", "Переименовать"), enabled: true},
|
||||||
{label: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"), enabled: true},
|
{label: GalleryStrings.tr("修改分组", "Edit groups", "Изменить группы"), enabled: !connection.lan},
|
||||||
{label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись"), enabled: true},
|
{label: GalleryStrings.tr("删除记录", "Delete record", "Удалить запись"), enabled: true},
|
||||||
];
|
];
|
||||||
item-icons: [FontAwesomeIcons.pen, FontAwesomeIcons.tags, FontAwesomeIcons.trash-can];
|
item-icons: [FontAwesomeIcons.pen, FontAwesomeIcons.tags, FontAwesomeIcons.trash-can];
|
||||||
@@ -1465,7 +1829,7 @@ export component MainWindow inherits Window {
|
|||||||
root.delete-remote-id = id; root.delete-from-gallery = true; root.delete-open = true;
|
root.delete-remote-id = id; root.delete-from-gallery = true; root.delete-open = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Server status and reconnect actions belong to the connection window.
|
// Show availability for the selected connection mode.
|
||||||
if !root.gallery-mode: Rectangle {
|
if !root.gallery-mode: Rectangle {
|
||||||
y: parent.height - 28px;
|
y: parent.height - 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@@ -1477,13 +1841,16 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: 20px; y: 11px; width: 6px; height: 6px; border-radius: 3px;
|
x: 20px; y: 11px; width: 6px; height: 6px; border-radius: 3px;
|
||||||
background: root.signal-tls-error || root.signal-reconnecting ? #d6932b : root.signal-connected ? WorkbenchStyle.success : #d65252;
|
background: root.lan-mode ? (root.lan-message != "" || root.lan-listener-error != "" ? #d6932b : root.lan-listening ? WorkbenchStyle.success : WorkbenchStyle.muted) : root.signal-tls-error || root.signal-reconnecting ? #d6932b : root.signal-connected ? WorkbenchStyle.success : #d65252;
|
||||||
}
|
}
|
||||||
Text {
|
Text {
|
||||||
x: 34px;
|
x: 34px;
|
||||||
y: 0px;
|
y: 0px;
|
||||||
height: parent.height;
|
height: parent.height;
|
||||||
text: root.signal-tls-error ? UiStrings.tls-error
|
width: parent.width - 160px;
|
||||||
|
overflow: elide;
|
||||||
|
text: root.lan-mode ? (root.lan-message != "" ? LanStrings.error-message(root.lan-message) : root.lan-listener-error != "" ? (root.lan-listener-address == "" ? "" : root.lan-listener-address + " · ") + LanStrings.error-message(root.lan-listener-error) : root.lan-listening ? GalleryStrings.tr("局域网模式已启用", "LAN mode enabled", "Режим LAN включён") : root.lan-password-pending ? GalleryStrings.tr("局域网模式准备中…", "Preparing LAN mode…", "Подготовка режима LAN…") : GalleryStrings.tr("局域网模式未就绪", "LAN mode not ready", "Режим LAN не готов"))
|
||||||
|
: root.signal-tls-error ? UiStrings.tls-error
|
||||||
: root.signal-connected ? UiStrings.signal-connected
|
: root.signal-connected ? UiStrings.signal-connected
|
||||||
: root.signal-reconnecting ? UiStrings.signal-reconnecting
|
: root.signal-reconnecting ? UiStrings.signal-reconnecting
|
||||||
: UiStrings.signal-disconnected;
|
: UiStrings.signal-disconnected;
|
||||||
@@ -1491,12 +1858,12 @@ export component MainWindow inherits Window {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
vertical-alignment: center;
|
vertical-alignment: center;
|
||||||
}
|
}
|
||||||
if root.signal-connected && !root.signal-tls-error: Text {
|
if root.lan-mode || (root.signal-connected && !root.signal-tls-error): Text {
|
||||||
x: parent.width - self.preferred-width - 20px; height: parent.height;
|
x: parent.width - self.preferred-width - 20px; height: parent.height;
|
||||||
text: root.current-version;
|
text: root.current-version;
|
||||||
font-size: 11px; color: WorkbenchStyle.muted; vertical-alignment: center;
|
font-size: 11px; color: WorkbenchStyle.muted; vertical-alignment: center;
|
||||||
}
|
}
|
||||||
if !root.signal-connected && !root.signal-tls-error: WorkbenchButton {
|
if !root.lan-mode && !root.signal-connected && !root.signal-tls-error: WorkbenchButton {
|
||||||
x: parent.width - self.width - 20px;
|
x: parent.width - self.width - 20px;
|
||||||
y: (parent.height - self.height) / 2;
|
y: (parent.height - self.height) / 2;
|
||||||
width: max(64px, reconnect-label-metrics.preferred-width + 22px);
|
width: max(64px, reconnect-label-metrics.preferred-width + 22px);
|
||||||
@@ -1521,7 +1888,7 @@ export component MainWindow inherits Window {
|
|||||||
// Shared device details sit outside the clipped recent list and wall rail,
|
// Shared device details sit outside the clipped recent list and wall rail,
|
||||||
// below the modal layer (z >= 39).
|
// below the modal layer (z >= 39).
|
||||||
if (root.gallery-mode ? gallery.hovered-device.id != "" : root.recent-tooltip-visible)
|
if (root.gallery-mode ? gallery.hovered-device.id != "" : root.recent-tooltip-visible)
|
||||||
&& !root.connection-dialog-open: device-tip := Rectangle {
|
&& !root.connection-dialog-open && !root.lan-open: device-tip := Rectangle {
|
||||||
private property <string> device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name;
|
private property <string> device-name: root.gallery-mode ? gallery.hovered-device.name : root.recent-tooltip-name;
|
||||||
private property <string> host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host;
|
private property <string> host-name: root.gallery-mode ? gallery.hovered-device.host-name : root.recent-tooltip-host;
|
||||||
private property <string> device-id: root.gallery-mode ? gallery.hovered-device.id : root.recent-tooltip-id;
|
private property <string> device-id: root.gallery-mode ? gallery.hovered-device.id : root.recent-tooltip-id;
|
||||||
@@ -1824,7 +2191,7 @@ export component MainWindow inherits Window {
|
|||||||
icon: self.error ? FontAwesomeIcons.xmark : FontAwesomeIcons.check;
|
icon: self.error ? FontAwesomeIcons.xmark : FontAwesomeIcons.check;
|
||||||
text: self.error
|
text: self.error
|
||||||
? GalleryStrings.tr("复制失败,请重试", "Couldn’t copy. Try again", "Не удалось скопировать. Повторите попытку")
|
? GalleryStrings.tr("复制失败,请重试", "Couldn’t copy. Try again", "Не удалось скопировать. Повторите попытку")
|
||||||
: UiStrings.copied;
|
: root.lan-mode ? GalleryStrings.tr("IP 和端口已复制", "IP and port copied", "IP и порт скопированы") : UiStrings.copied;
|
||||||
z: 30;
|
z: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1854,6 +2221,7 @@ export component MainWindow inherits Window {
|
|||||||
alignment: center; spacing: 8px;
|
alignment: center; spacing: 8px;
|
||||||
PrimaryButton {
|
PrimaryButton {
|
||||||
text: UiStrings.ok;
|
text: UiStrings.ok;
|
||||||
|
enabled: root.lan-mode ? !root.lan-password-pending : !root.password-change-pending;
|
||||||
clicked => {
|
clicked => {
|
||||||
if (root.reset-password(root.new-password-input)) {
|
if (root.reset-password(root.new-password-input)) {
|
||||||
root.new-password-input = "";
|
root.new-password-input = "";
|
||||||
@@ -2047,32 +2415,41 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connection feedback follows the workbench spacing and action hierarchy.
|
// Connection feedback follows the workbench spacing and action hierarchy.
|
||||||
if root.connection-dialog-open: Rectangle {
|
if root.connection-dialog-open || root.lan-open: Rectangle {
|
||||||
background: #15223850;
|
background: #15223850;
|
||||||
z: 48;
|
z: 48;
|
||||||
TouchArea { clicked => { focus-sink.focus(); } }
|
TouchArea { clicked => { if !root.lan-open { focus-sink.focus(); } } }
|
||||||
WorkbenchCard {
|
WorkbenchCard {
|
||||||
x: (parent.width - self.width) / 2;
|
x: (parent.width - self.width) / 2;
|
||||||
y: (parent.height - self.height) / 2;
|
y: (parent.height - self.height) / 2;
|
||||||
width: 340px;
|
width: 340px;
|
||||||
height: root.connection-password-required && !root.connection-validating ? 236px : 168px;
|
height: (root.prompt-password-required && !root.prompt-validating ? 236px : 168px)
|
||||||
Text {
|
+ max(0px, connection-prompt-text.preferred-height - 46px);
|
||||||
x: 24px; y: 24px; width: parent.width - 48px; height: 46px;
|
connection-prompt-text := Text {
|
||||||
text: root.connection-status-text;
|
x: 24px; y: 24px; width: parent.width - 48px; height: max(46px, self.preferred-height);
|
||||||
|
text: root.prompt-status-text;
|
||||||
color: WorkbenchStyle.text; font-size: 14px;
|
color: WorkbenchStyle.text; font-size: 14px;
|
||||||
wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center;
|
wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center;
|
||||||
}
|
}
|
||||||
if root.connection-password-required && !root.connection-validating: PasswordInput {
|
if !root.lan-open && root.connection-password-required && !root.connection-validating: PasswordInput {
|
||||||
x: (parent.width - self.width) / 2; y: 86px;
|
x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 16px;
|
||||||
width: 164px; height: 36px; text-size: 16px;
|
width: 164px; height: 36px; text-size: 16px;
|
||||||
border-radius: UiRadius.control; border-color: WorkbenchStyle.control-border;
|
border-radius: UiRadius.control; border-color: WorkbenchStyle.control-border;
|
||||||
text <=> root.connection-password;
|
text <=> root.connection-password;
|
||||||
accepted => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
|
accepted => { root.submit-connection-password(); }
|
||||||
}
|
}
|
||||||
if root.connection-password-required && !root.connection-validating: Rectangle {
|
if root.lan-open && !root.lan-connecting: PasswordInput {
|
||||||
x: (parent.width - self.width) / 2; y: 134px;
|
x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 16px;
|
||||||
|
width: 164px; height: 36px; text-size: 16px; max-length: 6;
|
||||||
|
border-radius: UiRadius.control; border-color: WorkbenchStyle.control-border;
|
||||||
|
text <=> root.lan-access-password;
|
||||||
|
accepted => { root.submit-connection-password(); }
|
||||||
|
}
|
||||||
|
if root.prompt-password-required && !root.prompt-validating: Rectangle {
|
||||||
|
x: (parent.width - self.width) / 2; y: connection-prompt-text.y + connection-prompt-text.height + 64px;
|
||||||
width: remember-label.preferred-width + 29px; height: 21px;
|
width: remember-label.preferred-width + 29px; height: 21px;
|
||||||
ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.connection-remember-password; }
|
if root.lan-open: ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.lan-remember-password; }
|
||||||
|
if !root.lan-open: ImGuiCheckBox { x: 0px; y: 0px; checked <=> root.connection-remember-password; }
|
||||||
remember-label := Text {
|
remember-label := Text {
|
||||||
x: 29px; height: parent.height;
|
x: 29px; height: parent.height;
|
||||||
text: UiStrings.remember-password;
|
text: UiStrings.remember-password;
|
||||||
@@ -2080,21 +2457,25 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
TouchArea {
|
TouchArea {
|
||||||
x: 29px; width: parent.width - 29px; height: parent.height;
|
x: 29px; width: parent.width - 29px; height: parent.height;
|
||||||
clicked => { root.connection-remember-password = !root.connection-remember-password; }
|
clicked => {
|
||||||
|
if root.lan-open { root.lan-remember-password = !root.lan-remember-password; }
|
||||||
|
else { root.connection-remember-password = !root.connection-remember-password; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
x: 24px; y: parent.height - 60px; width: parent.width - 48px; height: 36px;
|
x: 24px; y: parent.height - 60px; width: parent.width - 48px; height: 36px;
|
||||||
spacing: 12px; alignment: center;
|
spacing: 12px; alignment: center;
|
||||||
if root.connection-password-required || root.connection-pending: WorkbenchButton {
|
if root.prompt-password-required || root.prompt-pending: WorkbenchButton {
|
||||||
text: UiStrings.cancel;
|
text: UiStrings.cancel;
|
||||||
clicked => { root.connection-cancel(); }
|
clicked => { root.cancel-connection-prompt(); }
|
||||||
}
|
}
|
||||||
if root.connection-password-required && !root.connection-validating: WorkbenchButton {
|
if root.prompt-password-required && !root.prompt-validating: WorkbenchButton {
|
||||||
primary: true; text: UiStrings.ok;
|
primary: true; text: UiStrings.ok;
|
||||||
clicked => { root.connection-submit-password(root.connection-password, root.connection-remember-password); }
|
enabled: !root.lan-open || (root.lan-retry-after == 0 && root.lan-access-password.character-count == 6);
|
||||||
|
clicked => { root.submit-connection-password(); }
|
||||||
}
|
}
|
||||||
if !root.connection-password-required && !root.connection-pending: WorkbenchButton {
|
if !root.prompt-password-required && !root.prompt-pending: WorkbenchButton {
|
||||||
primary: true; text: UiStrings.ok;
|
primary: true; text: UiStrings.ok;
|
||||||
clicked => { root.connection-acknowledge(); }
|
clicked => { root.connection-acknowledge(); }
|
||||||
}
|
}
|
||||||
@@ -2253,7 +2634,7 @@ export component MainWindow inherits Window {
|
|||||||
if root.settings-tab == 0: settings-scroll := ScrollView {
|
if root.settings-tab == 0: settings-scroll := ScrollView {
|
||||||
x: settings-dialog.content-inset; y: 108px;
|
x: settings-dialog.content-inset; y: 108px;
|
||||||
width: parent.width - 2 * settings-dialog.content-inset + settings-dialog.scrollbar-gutter;
|
width: parent.width - 2 * settings-dialog.content-inset + settings-dialog.scrollbar-gutter;
|
||||||
height: parent.height - 174px;
|
height: parent.height - 174px - (root.settings-save-error == "" ? 0px : 28px);
|
||||||
viewport-width: self.visible-width;
|
viewport-width: self.visible-width;
|
||||||
viewport-height: settings-content.preferred-height;
|
viewport-height: settings-content.preferred-height;
|
||||||
viewport-y <=> root.settings-scroll-y;
|
viewport-y <=> root.settings-scroll-y;
|
||||||
@@ -2288,7 +2669,11 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingsGroup {
|
SettingsGroup {
|
||||||
title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 3;
|
title: GalleryStrings.tr("网络连接", "Network", "Сеть"); rows: 4;
|
||||||
|
SettingsToggleRow {
|
||||||
|
label: GalleryStrings.tr("启用 IP 直连", "Enable direct IP", "Прямое IP-подключение");
|
||||||
|
checked <=> root.ip-direct-enabled-draft;
|
||||||
|
}
|
||||||
SettingsToggleRow { label: UiStrings.turn-relay; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
|
SettingsToggleRow { label: UiStrings.turn-relay; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
|
||||||
SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: root.turn-enabled && !root.settings-session-active; }
|
SettingsToggleRow { label: UiStrings.force-relay; checked <=> root.force-relay-enabled; enabled: root.turn-enabled && !root.settings-session-active; }
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
@@ -2296,6 +2681,10 @@ export component MainWindow inherits Window {
|
|||||||
SettingsButton { x: 0px; y: 2px; width: parent.width - 60px; height: 32px; quiet: true; align-left: true; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
|
SettingsButton { x: 0px; y: 2px; width: parent.width - 60px; height: 32px; quiet: true; align-left: true; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Text {
|
||||||
|
text: GalleryStrings.tr("关闭 IP 直连将隐藏模式按钮,并断开 IP 直连会话。", "Disabling direct IP hides the mode button and disconnects direct IP sessions.", "Отключение скроет кнопку режима и завершит прямые IP-сеансы.");
|
||||||
|
font-size: 11px; color: WorkbenchStyle.muted; wrap: word-wrap;
|
||||||
|
}
|
||||||
SettingsGroup {
|
SettingsGroup {
|
||||||
title: GalleryStrings.tr("隐私保护", "Privacy", "Приватность"); rows: 1;
|
title: GalleryStrings.tr("隐私保护", "Privacy", "Приватность"); rows: 1;
|
||||||
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; divider: false; }
|
SettingsToggleRow { label: UiStrings.privacy-on-connect; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; divider: false; }
|
||||||
@@ -2324,12 +2713,18 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
if root.settings-tab == 1: GallerySettings {
|
if root.settings-tab == 1: GallerySettings {
|
||||||
x: settings-dialog.content-inset; y: 108px;
|
x: settings-dialog.content-inset; y: 108px;
|
||||||
width: parent.width - 2 * settings-dialog.content-inset; height: parent.height - 174px;
|
width: parent.width - 2 * settings-dialog.content-inset;
|
||||||
|
height: parent.height - 174px - (root.settings-save-error == "" ? 0px : 28px);
|
||||||
resolution-index <=> root.gallery-resolution-draft;
|
resolution-index <=> root.gallery-resolution-draft;
|
||||||
frame-rate-index <=> root.gallery-frame-rate-draft;
|
frame-rate-index <=> root.gallery-frame-rate-draft;
|
||||||
bandwidth-index <=> root.gallery-bandwidth-draft;
|
bandwidth-index <=> root.gallery-bandwidth-draft;
|
||||||
error: root.gallery-settings-error;
|
error: root.gallery-settings-error;
|
||||||
}
|
}
|
||||||
|
if root.settings-save-error != "": Text {
|
||||||
|
x: settings-dialog.content-inset; y: parent.height - 92px;
|
||||||
|
width: parent.width - 2 * settings-dialog.content-inset; height: 24px;
|
||||||
|
text: root.settings-save-error; font-size: 11px; color: #c53e3e; wrap: word-wrap;
|
||||||
|
}
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: settings-dialog.content-inset; y: parent.height - 64px;
|
x: settings-dialog.content-inset; y: parent.height - 64px;
|
||||||
width: parent.width - 2 * settings-dialog.content-inset; height: 1px; background: #e6ebf2;
|
width: parent.width - 2 * settings-dialog.content-inset; height: 1px; background: #e6ebf2;
|
||||||
@@ -3104,4 +3499,79 @@ export component MainWindow inherits Window {
|
|||||||
z: 200;
|
z: 200;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Each explicit validation failure opens one modal, without a focus loop.
|
||||||
|
if root.lan-access-warning != "": Rectangle {
|
||||||
|
width: parent.width; height: parent.height;
|
||||||
|
background: #00000042;
|
||||||
|
z: 300;
|
||||||
|
TouchArea { scroll-event(event) => { accept } }
|
||||||
|
lan-warning-focus := FocusScope {
|
||||||
|
width: parent.width; height: parent.height;
|
||||||
|
key-pressed(event) => {
|
||||||
|
if event.text == Key.Escape || event.text == Key.Return || event.text == " " {
|
||||||
|
root.dismiss-lan-access-warning();
|
||||||
|
} else if event.text == Key.Tab {
|
||||||
|
self.focus();
|
||||||
|
}
|
||||||
|
return accept;
|
||||||
|
}
|
||||||
|
Timer {
|
||||||
|
interval: 1ms;
|
||||||
|
running: true;
|
||||||
|
triggered => {
|
||||||
|
self.running = false;
|
||||||
|
// Defer focus until after Slint's accessibility update.
|
||||||
|
root.menu-open = false;
|
||||||
|
lan-ip-picker.close-menu();
|
||||||
|
lan-warning-focus.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DialogSurface {
|
||||||
|
x: (parent.width - self.width) / 2;
|
||||||
|
y: (parent.height - self.height) / 2;
|
||||||
|
width: min(root.compact-language ? 270px : 320px, parent.width - 40px);
|
||||||
|
// Let short messages stay compact; longer translations grow
|
||||||
|
// naturally without stretching the gap above the action.
|
||||||
|
height: lan-warning-content.preferred-height;
|
||||||
|
TouchArea { scroll-event(event) => { accept } }
|
||||||
|
lan-warning-content := VerticalLayout {
|
||||||
|
padding: 20px; spacing: 20px;
|
||||||
|
padding-top: 28px; padding-bottom: 28px;
|
||||||
|
if root.lan-access-warning-endpoint != "": Text {
|
||||||
|
text: root.lan-access-warning-endpoint;
|
||||||
|
font-size: 12px; font-weight: 600; color: WorkbenchStyle.text;
|
||||||
|
horizontal-alignment: center; wrap: word-wrap;
|
||||||
|
}
|
||||||
|
lan-warning-message := Rectangle {
|
||||||
|
private property <[string]> lines: LanStrings.access-error-lines(root.lan-access-warning);
|
||||||
|
height: warning-lines.preferred-height;
|
||||||
|
vertical-stretch: 0;
|
||||||
|
accessible-role: text;
|
||||||
|
accessible-label: LanStrings.error-message(root.lan-access-warning);
|
||||||
|
warning-lines := VerticalLayout {
|
||||||
|
width: parent.width;
|
||||||
|
height: self.preferred-height;
|
||||||
|
spacing: 6px;
|
||||||
|
for line in (lan-warning-message.lines[0] != "" ? lan-warning-message.lines : [LanStrings.error-message(root.lan-access-warning)]): Text {
|
||||||
|
text: line; font-size: ImGuiFontStyle.base; color: #30343b;
|
||||||
|
horizontal-alignment: center; wrap: word-wrap;
|
||||||
|
accessible-role: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalLayout {
|
||||||
|
height: 30px; alignment: center;
|
||||||
|
PrimaryButton {
|
||||||
|
text: UiStrings.ok;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: UiStrings.ok;
|
||||||
|
accessible-action-default => { root.dismiss-lan-access-warning(); }
|
||||||
|
clicked => { root.dismiss-lan-access-warning(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,8 +197,9 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
optimistic.available = local_service_available_;
|
optimistic.available = local_service_available_;
|
||||||
optimistic.interactive_stage = local_interactive_stage_;
|
optimistic.interactive_stage = local_interactive_stage_;
|
||||||
const auto message = BuildWindowsServiceStatusAction(optimistic).to_json();
|
const auto message = BuildWindowsServiceStatusAction(optimistic).to_json();
|
||||||
SendReliableDataFrame(peer_, message.data(), message.size(),
|
for (const auto& id : ConnectedControllers())
|
||||||
control_data_label_.c_str());
|
SendControllerData(id, message.data(), message.size(),
|
||||||
|
control_data_label_.c_str());
|
||||||
}
|
}
|
||||||
result.reset();
|
result.reset();
|
||||||
}
|
}
|
||||||
@@ -293,10 +294,11 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
RemoteAction remote_action =
|
RemoteAction remote_action =
|
||||||
BuildWindowsServiceStatusAction(broadcast_status);
|
BuildWindowsServiceStatusAction(broadcast_status);
|
||||||
std::string msg = remote_action.to_json();
|
std::string msg = remote_action.to_json();
|
||||||
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
|
for (const auto& id : ConnectedControllers()) {
|
||||||
control_data_label_.c_str());
|
const int ret = SendControllerData(id, msg.data(), msg.size(),
|
||||||
if (ret != 0) {
|
control_data_label_.c_str());
|
||||||
LOG_WARN("Broadcast Windows service status failed, ret={}", ret);
|
if (ret != 0)
|
||||||
|
LOG_WARN("Windows service status send failed, peer={}, ret={}", id, ret);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,14 @@ bool IsRemoteId(const std::string& id) {
|
|||||||
[](unsigned char c) { return c >= '0' && c <= '9'; });
|
[](unsigned char c) { return c >= '0' && c <= '9'; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsLanRecordId(const std::string& id) {
|
||||||
|
return id.size() == 68 && id.rfind("lan-", 0) == 0 &&
|
||||||
|
std::all_of(id.begin() + 4, id.end(), [](char c) {
|
||||||
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
|
||||||
|
});
|
||||||
|
}
|
||||||
std::string ConnectionKey(const Thumbnail::RecentConnection& connection) {
|
std::string ConnectionKey(const Thumbnail::RecentConnection& connection) {
|
||||||
|
if (!connection.lan.device_id.empty()) return connection.remote_id;
|
||||||
return connection.remote_id + (connection.remember_password ? "Y" : "N") +
|
return connection.remote_id + (connection.remember_password ? "Y" : "N") +
|
||||||
connection.remote_host_name +
|
connection.remote_host_name +
|
||||||
(connection.remember_password ? "@" + connection.password : "");
|
(connection.remember_password ? "@" + connection.password : "");
|
||||||
@@ -126,17 +133,37 @@ std::string ConnectionKey(const Thumbnail::RecentConnection& connection) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::string Thumbnail::LanRecordId(const std::string& device_id) {
|
||||||
|
unsigned char digest[32]{};
|
||||||
|
unsigned int size = 0;
|
||||||
|
const std::string input = "lan_direct/" + device_id;
|
||||||
|
if (EVP_Digest(input.data(), input.size(), digest, &size, EVP_sha256(),
|
||||||
|
nullptr) != 1 ||
|
||||||
|
size != 32)
|
||||||
|
return {};
|
||||||
|
std::ostringstream text;
|
||||||
|
text << "lan-" << std::hex << std::setfill('0');
|
||||||
|
for (const auto byte : digest) text << std::setw(2) << unsigned(byte);
|
||||||
|
return text.str();
|
||||||
|
}
|
||||||
|
|
||||||
bool Thumbnail::WriteRecord(const RecentConnection& connection) {
|
bool Thumbnail::WriteRecord(const RecentConnection& connection) {
|
||||||
|
const auto password = connection.remember_password
|
||||||
|
? AES_encrypt(connection.password, aes128_key_, aes128_iv_) : "";
|
||||||
|
if (connection.remember_password && password.empty()) return false;
|
||||||
const auto path =
|
const auto path =
|
||||||
std::filesystem::path(save_path_) / (connection.remote_id + ".json");
|
std::filesystem::path(save_path_) / (connection.remote_id + ".json");
|
||||||
const auto temporary = path.string() + ".tmp";
|
const auto temporary = path.string() + ".tmp";
|
||||||
const nlohmann::json record = {
|
const nlohmann::json record = {
|
||||||
|
{"version", 2},
|
||||||
|
{"kind", connection.lan.device_id.empty() ? "cloud_id" : "lan_direct"},
|
||||||
|
{"lan",
|
||||||
|
{{"address", connection.lan.address},
|
||||||
|
{"port", connection.lan.port},
|
||||||
|
{"device_id", connection.lan.device_id}}},
|
||||||
{"remote_id", connection.remote_id},
|
{"remote_id", connection.remote_id},
|
||||||
{"host_name", connection.remote_host_name},
|
{"host_name", connection.remote_host_name},
|
||||||
{"password",
|
{"password", password},
|
||||||
connection.remember_password
|
|
||||||
? AES_encrypt(connection.password, aes128_key_, aes128_iv_)
|
|
||||||
: ""},
|
|
||||||
{"platform", HostPlatformName(connection.platform)}};
|
{"platform", HostPlatformName(connection.platform)}};
|
||||||
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
|
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
|
||||||
output << record.dump();
|
output << record.dump();
|
||||||
@@ -167,14 +194,25 @@ bool Thumbnail::ReadRecord(const std::filesystem::path& path,
|
|||||||
if (!record.is_object()) return false;
|
if (!record.is_object()) return false;
|
||||||
try {
|
try {
|
||||||
connection->remote_id = record.value("remote_id", "");
|
connection->remote_id = record.value("remote_id", "");
|
||||||
if (!IsRemoteId(connection->remote_id) ||
|
const auto kind = record.value("kind", "cloud_id");
|
||||||
path.stem() != connection->remote_id)
|
if (kind == "lan_direct") {
|
||||||
|
const auto& target = record.at("lan");
|
||||||
|
connection->lan = {target.at("address"), target.at("device_id"),
|
||||||
|
target.at("port")};
|
||||||
|
if (!IsLanRecordId(connection->remote_id) ||
|
||||||
|
connection->lan.device_id.size() != 32 ||
|
||||||
|
connection->remote_id != LanRecordId(connection->lan.device_id) ||
|
||||||
|
!connection->lan.port || connection->lan.port > 65535 ||
|
||||||
|
connection->lan.address.size() > 15)
|
||||||
|
return false;
|
||||||
|
} else if (kind != "cloud_id" || !IsRemoteId(connection->remote_id))
|
||||||
return false;
|
return false;
|
||||||
|
if (path.stem() != connection->remote_id) return false;
|
||||||
connection->remote_host_name = record.value("host_name", "");
|
connection->remote_host_name = record.value("host_name", "");
|
||||||
const auto password = record.value("password", "");
|
const auto password = record.value("password", "");
|
||||||
connection->remember_password = !password.empty();
|
|
||||||
connection->password =
|
connection->password =
|
||||||
password.empty() ? "" : AES_decrypt(password, aes128_key_, aes128_iv_);
|
password.empty() ? "" : AES_decrypt(password, aes128_key_, aes128_iv_);
|
||||||
|
connection->remember_password = !connection->password.empty();
|
||||||
connection->platform =
|
connection->platform =
|
||||||
ParseHostPlatform(record.value("platform", "unknown"));
|
ParseHostPlatform(record.value("platform", "unknown"));
|
||||||
const auto image = path.parent_path() / (connection->remote_id + ".png");
|
const auto image = path.parent_path() / (connection->remote_id + ".png");
|
||||||
@@ -201,17 +239,21 @@ int Thumbnail::SaveToThumbnail(const char* nv12, int width, int height,
|
|||||||
const std::string& remote_id,
|
const std::string& remote_id,
|
||||||
const std::string& host_name,
|
const std::string& host_name,
|
||||||
const std::string& password,
|
const std::string& password,
|
||||||
HostPlatform platform) {
|
HostPlatform platform, const LanTarget* lan) {
|
||||||
std::lock_guard lock(mutex_);
|
std::lock_guard lock(mutex_);
|
||||||
// Successful connections are history even without saved credentials.
|
// Successful connections are history even without saved credentials.
|
||||||
if (!IsRemoteId(remote_id)) return -1;
|
if (lan ? (!IsLanRecordId(remote_id) ||
|
||||||
|
remote_id != LanRecordId(lan->device_id))
|
||||||
|
: !IsRemoteId(remote_id))
|
||||||
|
return -1;
|
||||||
RecentConnection connection;
|
RecentConnection connection;
|
||||||
ReadRecord(std::filesystem::path(save_path_) / (remote_id + ".json"),
|
ReadRecord(std::filesystem::path(save_path_) / (remote_id + ".json"),
|
||||||
&connection);
|
&connection);
|
||||||
connection.remote_id = remote_id;
|
connection.remote_id = remote_id;
|
||||||
if (!host_name.empty()) connection.remote_host_name = host_name;
|
if (!host_name.empty()) connection.remote_host_name = host_name;
|
||||||
|
connection.lan = lan ? *lan : LanTarget{};
|
||||||
connection.password = password;
|
connection.password = password;
|
||||||
connection.remember_password = !password.empty();
|
connection.remember_password = !connection.password.empty();
|
||||||
if (platform != HostPlatform::Unknown) connection.platform = platform;
|
if (platform != HostPlatform::Unknown) connection.platform = platform;
|
||||||
if (!WriteRecord(connection)) return -1;
|
if (!WriteRecord(connection)) return -1;
|
||||||
|
|
||||||
@@ -349,6 +391,12 @@ bool Thumbnail::DecodeImage(const std::filesystem::path& image_path,
|
|||||||
|
|
||||||
int Thumbnail::DeleteThumbnail(const std::string& filename_keyword) {
|
int Thumbnail::DeleteThumbnail(const std::string& filename_keyword) {
|
||||||
std::lock_guard lock(mutex_);
|
std::lock_guard lock(mutex_);
|
||||||
|
if (IsLanRecordId(filename_keyword)) {
|
||||||
|
for (const auto* extension : {".json", ".png"})
|
||||||
|
std::filesystem::remove(std::filesystem::path(save_path_) /
|
||||||
|
(filename_keyword + extension));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
const auto remote_id = filename_keyword.substr(0, 9);
|
const auto remote_id = filename_keyword.substr(0, 9);
|
||||||
if (!IsRemoteId(remote_id)) return -1;
|
if (!IsRemoteId(remote_id)) return -1;
|
||||||
for (const auto& entry : std::filesystem::directory_iterator(save_path_)) {
|
for (const auto& entry : std::filesystem::directory_iterator(save_path_)) {
|
||||||
@@ -411,14 +459,14 @@ std::string Thumbnail::AES_encrypt(const std::string& plaintext,
|
|||||||
ctx = EVP_CIPHER_CTX_new();
|
ctx = EVP_CIPHER_CTX_new();
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
LOG_ERROR("Error in EVP_CIPHER_CTX_new");
|
LOG_ERROR("Error in EVP_CIPHER_CTX_new");
|
||||||
return plaintext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv);
|
ret = EVP_EncryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv);
|
||||||
if (1 != ret) {
|
if (1 != ret) {
|
||||||
LOG_ERROR("Error in EVP_EncryptInit_ex");
|
LOG_ERROR("Error in EVP_EncryptInit_ex");
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
return plaintext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = EVP_EncryptUpdate(
|
ret = EVP_EncryptUpdate(
|
||||||
@@ -428,7 +476,7 @@ std::string Thumbnail::AES_encrypt(const std::string& plaintext,
|
|||||||
if (1 != ret) {
|
if (1 != ret) {
|
||||||
LOG_ERROR("Error in EVP_EncryptUpdate");
|
LOG_ERROR("Error in EVP_EncryptUpdate");
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
return plaintext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ciphertext_len = len;
|
ciphertext_len = len;
|
||||||
@@ -436,7 +484,7 @@ std::string Thumbnail::AES_encrypt(const std::string& plaintext,
|
|||||||
if (1 != ret) {
|
if (1 != ret) {
|
||||||
LOG_ERROR("Error in EVP_EncryptFinal_ex");
|
LOG_ERROR("Error in EVP_EncryptFinal_ex");
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
return plaintext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ciphertext_len += len;
|
ciphertext_len += len;
|
||||||
@@ -448,7 +496,7 @@ std::string Thumbnail::AES_encrypt(const std::string& plaintext,
|
|||||||
if (1 != ret) {
|
if (1 != ret) {
|
||||||
LOG_ERROR("Error in OPENSSL_buf2hexstr_ex");
|
LOG_ERROR("Error in OPENSSL_buf2hexstr_ex");
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
return plaintext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
@@ -471,13 +519,13 @@ std::string Thumbnail::AES_decrypt(const std::string& ciphertext,
|
|||||||
&ciphertext_buf_len, ciphertext.c_str(), '\0');
|
&ciphertext_buf_len, ciphertext.c_str(), '\0');
|
||||||
if (1 != ret) {
|
if (1 != ret) {
|
||||||
LOG_ERROR("Error in OPENSSL_hexstr2buf_ex");
|
LOG_ERROR("Error in OPENSSL_hexstr2buf_ex");
|
||||||
return ciphertext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = EVP_CIPHER_CTX_new();
|
ctx = EVP_CIPHER_CTX_new();
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
LOG_ERROR("Error in EVP_CIPHER_CTX_new");
|
LOG_ERROR("Error in EVP_CIPHER_CTX_new");
|
||||||
return ciphertext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv);
|
ret = EVP_DecryptInit_ex(ctx, EVP_aes_128_cbc(), NULL, key, iv);
|
||||||
@@ -485,7 +533,7 @@ std::string Thumbnail::AES_decrypt(const std::string& ciphertext,
|
|||||||
LOG_ERROR("Error in EVP_DecryptInit_ex");
|
LOG_ERROR("Error in EVP_DecryptInit_ex");
|
||||||
|
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
return ciphertext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = EVP_DecryptUpdate(ctx, plaintext, &plaintext_len, ciphertext_buf,
|
ret = EVP_DecryptUpdate(ctx, plaintext, &plaintext_len, ciphertext_buf,
|
||||||
@@ -494,7 +542,7 @@ std::string Thumbnail::AES_decrypt(const std::string& ciphertext,
|
|||||||
LOG_ERROR("Error in EVP_DecryptUpdate");
|
LOG_ERROR("Error in EVP_DecryptUpdate");
|
||||||
|
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
return ciphertext;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
ret =
|
ret =
|
||||||
@@ -503,7 +551,7 @@ std::string Thumbnail::AES_decrypt(const std::string& ciphertext,
|
|||||||
LOG_ERROR("Error in EVP_DecryptFinal_ex");
|
LOG_ERROR("Error in EVP_DecryptFinal_ex");
|
||||||
|
|
||||||
EVP_CIPHER_CTX_free(ctx);
|
EVP_CIPHER_CTX_free(ctx);
|
||||||
return ciphertext;
|
return {};
|
||||||
}
|
}
|
||||||
plaintext_len += plaintext_final_len;
|
plaintext_len += plaintext_final_len;
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,18 @@ namespace crossdesk {
|
|||||||
|
|
||||||
class Thumbnail {
|
class Thumbnail {
|
||||||
public:
|
public:
|
||||||
|
struct LanTarget {
|
||||||
|
std::string address, device_id;
|
||||||
|
unsigned port = 52888;
|
||||||
|
};
|
||||||
|
static std::string LanRecordId(const std::string& device_id);
|
||||||
struct RecentConnection {
|
struct RecentConnection {
|
||||||
std::filesystem::path image_path;
|
std::filesystem::path image_path;
|
||||||
std::string remote_id;
|
std::string remote_id;
|
||||||
std::string remote_host_name;
|
std::string remote_host_name;
|
||||||
std::string password;
|
std::string password;
|
||||||
bool remember_password = false;
|
bool remember_password = false;
|
||||||
|
LanTarget lan;
|
||||||
HostPlatform platform = HostPlatform::Unknown;
|
HostPlatform platform = HostPlatform::Unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,9 +51,9 @@ class Thumbnail {
|
|||||||
// credentials (and clears any remembered password from a previous session).
|
// credentials (and clears any remembered password from a previous session).
|
||||||
int SaveToThumbnail(const char* nv12, int width, int height,
|
int SaveToThumbnail(const char* nv12, int width, int height,
|
||||||
const std::string& remote_id,
|
const std::string& remote_id,
|
||||||
const std::string& host_name,
|
const std::string& host_name, const std::string& password,
|
||||||
const std::string& password,
|
HostPlatform platform = HostPlatform::Unknown,
|
||||||
HostPlatform platform = HostPlatform::Unknown);
|
const LanTarget* lan = nullptr);
|
||||||
|
|
||||||
int LoadThumbnail(
|
int LoadThumbnail(
|
||||||
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>&
|
std::vector<std::pair<std::string, Thumbnail::RecentConnection>>&
|
||||||
|
|||||||
Reference in New Issue
Block a user