mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
Compare commits
2
Commits
v1.5.2
...
c661319890
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c661319890 | ||
|
|
e0cd4c0567 |
@@ -12,12 +12,6 @@ bool IsValidTurnModeValue(long value) {
|
||||
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
|
||||
}
|
||||
|
||||
bool IsValidVideoAdaptationPolicyValue(long value) {
|
||||
using Policy = ConfigCenter::VIDEO_ADAPTATION_POLICY;
|
||||
return value >= static_cast<long>(Policy::FRAME_RATE_PRIORITY) &&
|
||||
value <= static_cast<long>(Policy::BALANCED);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigCenter::ConfigCenter(const std::string& config_path)
|
||||
@@ -36,6 +30,11 @@ int ConfigCenter::Load() {
|
||||
}
|
||||
|
||||
bool persist_config_migration = false;
|
||||
// These options now belong only to the active remote connection. Discard
|
||||
// legacy local defaults so they cannot affect this host or new connections.
|
||||
persist_config_migration |= ini_.Delete(section_, "video_quality");
|
||||
persist_config_migration |= ini_.Delete(section_, "video_frame_rate");
|
||||
persist_config_migration |= ini_.Delete(section_, "video_adaptation_policy");
|
||||
persist_config_migration |= ini_.Delete(section_, "video_content_type");
|
||||
persist_config_migration |= ini_.Delete(section_, "screen_content");
|
||||
persist_config_migration |=
|
||||
@@ -52,12 +51,6 @@ int ConfigCenter::Load() {
|
||||
language_ = static_cast<LANGUAGE>(language_value);
|
||||
}
|
||||
|
||||
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
|
||||
section_, "video_quality", static_cast<long>(video_quality_)));
|
||||
|
||||
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
|
||||
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
|
||||
|
||||
const long screen_capture_method_value = ini_.GetLongValue(
|
||||
section_, "screen_capture_method",
|
||||
static_cast<long>(ScreenCaptureMethod::Auto));
|
||||
@@ -66,18 +59,6 @@ int ConfigCenter::Load() {
|
||||
screen_capture_method_value)
|
||||
: ScreenCaptureMethod::Auto;
|
||||
|
||||
const long video_adaptation_policy_value = ini_.GetLongValue(
|
||||
section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
if (IsValidVideoAdaptationPolicyValue(video_adaptation_policy_value)) {
|
||||
video_adaptation_policy_ =
|
||||
static_cast<VIDEO_ADAPTATION_POLICY>(video_adaptation_policy_value);
|
||||
} else {
|
||||
LOG_WARN("Invalid video adaptation policy [{}], using quality priority",
|
||||
video_adaptation_policy_value);
|
||||
video_adaptation_policy_ = VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
|
||||
}
|
||||
|
||||
video_encode_format_ = static_cast<VIDEO_ENCODE_FORMAT>(
|
||||
ini_.GetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_)));
|
||||
@@ -156,14 +137,8 @@ int ConfigCenter::Load() {
|
||||
|
||||
int ConfigCenter::Save() {
|
||||
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
|
||||
ini_.SetLongValue(section_, "video_quality",
|
||||
static_cast<long>(video_quality_));
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
ini_.SetLongValue(section_, "screen_capture_method",
|
||||
static_cast<long>(screen_capture_method_));
|
||||
ini_.SetLongValue(section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
@@ -210,28 +185,6 @@ int ConfigCenter::SetLanguage(LANGUAGE language) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoQuality(VIDEO_QUALITY video_quality) {
|
||||
video_quality_ = video_quality;
|
||||
ini_.SetLongValue(section_, "video_quality",
|
||||
static_cast<long>(video_quality_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
|
||||
video_frame_rate_ = video_frame_rate;
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
|
||||
if (!IsValidScreenCaptureMethod(static_cast<long>(method))) {
|
||||
return -1;
|
||||
@@ -246,18 +199,6 @@ int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoAdaptationPolicy(
|
||||
VIDEO_ADAPTATION_POLICY policy) {
|
||||
video_adaptation_policy_ = policy;
|
||||
ini_.SetLongValue(section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoEncodeFormat(
|
||||
VIDEO_ENCODE_FORMAT video_encode_format) {
|
||||
video_encode_format_ = video_encode_format;
|
||||
@@ -426,23 +367,10 @@ int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
|
||||
|
||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
||||
|
||||
ConfigCenter::VIDEO_QUALITY ConfigCenter::GetVideoQuality() const {
|
||||
return video_quality_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
|
||||
return video_frame_rate_;
|
||||
}
|
||||
|
||||
ScreenCaptureMethod ConfigCenter::GetScreenCaptureMethod() const {
|
||||
return screen_capture_method_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ADAPTATION_POLICY
|
||||
ConfigCenter::GetVideoAdaptationPolicy() const {
|
||||
return video_adaptation_policy_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
|
||||
return video_encode_format_;
|
||||
}
|
||||
|
||||
@@ -18,13 +18,6 @@ namespace crossdesk {
|
||||
class ConfigCenter {
|
||||
public:
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 2 };
|
||||
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
|
||||
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
|
||||
enum class VIDEO_ADAPTATION_POLICY {
|
||||
FRAME_RATE_PRIORITY = 0,
|
||||
QUALITY_PRIORITY = 1,
|
||||
BALANCED = 2
|
||||
};
|
||||
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||
enum class TURN_MODE {
|
||||
DISABLED = 0,
|
||||
@@ -39,10 +32,7 @@ class ConfigCenter {
|
||||
|
||||
// write config
|
||||
int SetLanguage(LANGUAGE language);
|
||||
int SetVideoQuality(VIDEO_QUALITY video_quality);
|
||||
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
|
||||
int SetScreenCaptureMethod(ScreenCaptureMethod method);
|
||||
int SetVideoAdaptationPolicy(VIDEO_ADAPTATION_POLICY policy);
|
||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||
int SetHardwareVideoCodec(bool hardware_video_codec);
|
||||
int SetTurnMode(TURN_MODE turn_mode);
|
||||
@@ -59,10 +49,7 @@ class ConfigCenter {
|
||||
// read config
|
||||
|
||||
LANGUAGE GetLanguage() const;
|
||||
VIDEO_QUALITY GetVideoQuality() const;
|
||||
VIDEO_FRAME_RATE GetVideoFrameRate() const;
|
||||
ScreenCaptureMethod GetScreenCaptureMethod() const;
|
||||
VIDEO_ADAPTATION_POLICY GetVideoAdaptationPolicy() const;
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||
// Whether this build includes a hardware video codec backend.
|
||||
static bool IsHardwareVideoCodecAvailable();
|
||||
@@ -91,11 +78,7 @@ class ConfigCenter {
|
||||
const char* section_ = "Settings";
|
||||
|
||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
|
||||
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
|
||||
ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
|
||||
VIDEO_ADAPTATION_POLICY video_adaptation_policy_ =
|
||||
VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||
bool hardware_video_codec_ = false;
|
||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
@@ -47,7 +46,6 @@ struct InteractionState {
|
||||
};
|
||||
|
||||
struct UpdateState {
|
||||
nlohmann::json latest_version_info_ = nlohmann::json{};
|
||||
bool update_available_ = false;
|
||||
std::string latest_version_;
|
||||
std::string release_name_;
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
#include "rd_log.h"
|
||||
#include "server_window_state.h"
|
||||
#include "ui/ui_localization.h"
|
||||
#include "version_checker.h"
|
||||
#include "update_checker.h"
|
||||
#include "window_geometry.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -989,33 +989,6 @@ int GuiApplication::Run() {
|
||||
"%d", signal_port);
|
||||
}
|
||||
|
||||
latest_version_info_ = CheckUpdate();
|
||||
if (!latest_version_info_.empty()) {
|
||||
std::string version;
|
||||
if (latest_version_info_.contains("latest_version") &&
|
||||
latest_version_info_["latest_version"].is_string()) {
|
||||
version = latest_version_info_["latest_version"].get<std::string>();
|
||||
} else if (latest_version_info_.contains("version") &&
|
||||
latest_version_info_["version"].is_string()) {
|
||||
version = latest_version_info_["version"].get<std::string>();
|
||||
}
|
||||
latest_version_ = version.empty() ? std::string{} : "v" + version;
|
||||
if (latest_version_info_.contains("releaseNotes") &&
|
||||
latest_version_info_["releaseNotes"].is_string()) {
|
||||
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
|
||||
}
|
||||
if (latest_version_info_.contains("releaseName") &&
|
||||
latest_version_info_["releaseName"].is_string()) {
|
||||
release_name_ = latest_version_info_["releaseName"].get<std::string>();
|
||||
}
|
||||
if (latest_version_info_.contains("releaseDate") &&
|
||||
latest_version_info_["releaseDate"].is_string()) {
|
||||
release_date_ = latest_version_info_["releaseDate"].get<std::string>();
|
||||
}
|
||||
update_available_ =
|
||||
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
||||
}
|
||||
|
||||
InitializeSettings();
|
||||
if (!InitializeSDL()) {
|
||||
return -1;
|
||||
@@ -1030,6 +1003,7 @@ int GuiApplication::Run() {
|
||||
LOG_INFO("Slint backend: {}; {}", backend.backend, backend.diagnostic);
|
||||
#endif
|
||||
InitializeModules();
|
||||
update_checker_ = std::make_unique<UpdateChecker>(CROSSDESK_VERSION);
|
||||
InitializeUi();
|
||||
|
||||
ui_->timer.start(slint::TimerMode::Repeated, 16ms, [this] { Tick(); });
|
||||
@@ -1155,14 +1129,6 @@ void GuiApplication::InitializeUi() {
|
||||
ui_->main->set_custom_titlebar(use_x11_custom_titlebar_);
|
||||
ui_->main->set_wayland_titlebar(use_xwayland_gui_);
|
||||
#endif
|
||||
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
|
||||
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
|
||||
ui::ReleaseNoteBlock block;
|
||||
block.content = parsed.content;
|
||||
block.section_gap = parsed.section_gap;
|
||||
release_note_blocks.push_back(std::move(block));
|
||||
}
|
||||
ui_->release_note_blocks_model->set_vector(std::move(release_note_blocks));
|
||||
ui_->main->set_release_note_blocks(ui_->release_note_blocks_model);
|
||||
RegisterFontAwesome(ui_->main->window());
|
||||
ui_->main->set_recent_connections(ui_->recent_model);
|
||||
@@ -1250,10 +1216,6 @@ void GuiApplication::ResetSettingsUi() {
|
||||
return;
|
||||
}
|
||||
ui_->main->set_language_index(language_button_value_);
|
||||
ui_->main->set_video_quality_index(video_quality_button_value_);
|
||||
ui_->main->set_frame_rate_index(video_frame_rate_button_value_);
|
||||
ui_->main->set_adaptation_policy_index(
|
||||
video_adaptation_policy_button_value_);
|
||||
ui_->main->set_codec_index(video_encode_format_button_value_);
|
||||
const bool hardware_codec_available =
|
||||
ConfigCenter::IsHardwareVideoCodecAvailable();
|
||||
@@ -1402,6 +1364,10 @@ void GuiApplication::BindMainCallbacks() {
|
||||
: slint::SharedString{});
|
||||
});
|
||||
main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); });
|
||||
main->on_check_for_updates([this] {
|
||||
update_checker_->RequestCheck();
|
||||
SyncMainWindow();
|
||||
});
|
||||
main->on_connection_cancel([this] {
|
||||
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
|
||||
if (!props) {
|
||||
@@ -1623,6 +1589,32 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
}
|
||||
});
|
||||
stream->on_change_video_settings([this](int quality, int frame_rate_index,
|
||||
int preference) {
|
||||
auto props = SelectedSession();
|
||||
if (!props || !props->peer_ ||
|
||||
props->connection_status_.load() != ConnectionStatus::Connected)
|
||||
return;
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::video_settings;
|
||||
action.vs = {quality, frame_rate_index == 0 ? 30 : 60, preference};
|
||||
if (frame_rate_index < 0 || frame_rate_index > 1 ||
|
||||
!ValidVideoSettings(action.vs))
|
||||
return;
|
||||
{
|
||||
std::lock_guard lock(props->video_settings_mutex_);
|
||||
if (!props->video_settings_supported_) return;
|
||||
action.vs.request_id = props->video_settings_.request_id + 1;
|
||||
const auto message = action.to_json();
|
||||
if (SendReliableDataFrame(props->peer_, message.data(), message.size(),
|
||||
props->control_data_label_.c_str()) != 0)
|
||||
return;
|
||||
props->video_settings_ = action.vs;
|
||||
props->video_settings_pending_ = true;
|
||||
props->video_settings_failed_ = false;
|
||||
props->video_settings_request_tick_ = SDL_GetTicks();
|
||||
}
|
||||
});
|
||||
stream->on_toggle_audio([this] {
|
||||
auto props = SelectedSession();
|
||||
if (!props || !props->connection_established_) {
|
||||
@@ -1761,6 +1753,7 @@ void GuiApplication::Tick() {
|
||||
slint::quit_event_loop();
|
||||
return;
|
||||
}
|
||||
PollUpdateCheck();
|
||||
HandleSessionCleanup();
|
||||
HandlePasswordChangeResult();
|
||||
HandleCredentialRecovery();
|
||||
@@ -1835,6 +1828,29 @@ void GuiApplication::Tick() {
|
||||
SyncServerWindow();
|
||||
}
|
||||
|
||||
void GuiApplication::PollUpdateCheck() {
|
||||
// The first tick runs after show()/event-loop startup. Poll never waits for
|
||||
// networking and all application/model changes remain on this UI thread.
|
||||
if (!update_checker_->Poll() ||
|
||||
update_checker_->status() == UpdateChecker::Status::Failed) {
|
||||
return;
|
||||
}
|
||||
const auto& info = *update_checker_->latest();
|
||||
latest_version_ = "v" + info.version;
|
||||
release_name_ = info.release_name;
|
||||
release_notes_ = info.release_notes;
|
||||
release_date_ = info.release_date;
|
||||
update_available_ = update_checker_->update_available();
|
||||
std::vector<ui::ReleaseNoteBlock> blocks;
|
||||
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
|
||||
ui::ReleaseNoteBlock block;
|
||||
block.content = parsed.content;
|
||||
block.section_gap = parsed.section_gap;
|
||||
blocks.push_back(std::move(block));
|
||||
}
|
||||
ui_->release_note_blocks_model->set_vector(std::move(blocks));
|
||||
}
|
||||
|
||||
void GuiApplication::ShareLocalCursorState() {
|
||||
constexpr auto kCursorEchoSuppressionInterval = 300ms;
|
||||
constexpr auto kCursorStateHeartbeatInterval = 500ms;
|
||||
@@ -2231,6 +2247,23 @@ void GuiApplication::SyncMainWindow() {
|
||||
ui_->main->set_signal_tls_error(signal_status_ ==
|
||||
SignalStatus::SignalTlsCertError);
|
||||
ui_->main->set_update_available(update_available_);
|
||||
switch (update_checker_->status()) {
|
||||
case UpdateChecker::Status::Idle:
|
||||
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Idle);
|
||||
break;
|
||||
case UpdateChecker::Status::Checking:
|
||||
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Checking);
|
||||
break;
|
||||
case UpdateChecker::Status::UpToDate:
|
||||
ui_->main->set_update_check_status(ui::UpdateCheckStatus::UpToDate);
|
||||
break;
|
||||
case UpdateChecker::Status::Available:
|
||||
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Available);
|
||||
break;
|
||||
case UpdateChecker::Status::Failed:
|
||||
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Failed);
|
||||
break;
|
||||
}
|
||||
ui_->main->set_current_version(CROSSDESK_VERSION);
|
||||
ui_->main->set_latest_version(UiText(latest_version_));
|
||||
ui_->main->set_release_name(UiText(release_name_));
|
||||
@@ -2672,6 +2705,33 @@ void GuiApplication::SyncStreamWindow() {
|
||||
(*ui_->stream)->set_remote_cursor_active(remote_cursor_active);
|
||||
(*ui_->stream)->set_remote_cursor_shape(remote_cursor_shape);
|
||||
(*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
|
||||
{
|
||||
std::lock_guard lock(props->video_settings_mutex_);
|
||||
if (props->video_settings_pending_ &&
|
||||
SDL_GetTicks() - props->video_settings_request_tick_ > 5000) {
|
||||
props->video_settings_pending_ = false;
|
||||
props->video_settings_failed_ = true;
|
||||
const auto request_id = props->video_settings_.request_id;
|
||||
props->video_settings_ = props->applied_video_settings_;
|
||||
props->video_settings_.request_id = request_id;
|
||||
}
|
||||
(*ui_->stream)
|
||||
->set_video_settings_enabled(status == ConnectionStatus::Connected &&
|
||||
props->video_settings_supported_);
|
||||
(*ui_->stream)
|
||||
->set_video_settings_feedback(UiText(
|
||||
props->video_settings_failed_ ? localization::video_settings_failed
|
||||
[localization_language_index_]
|
||||
: props->video_settings_pending_
|
||||
? localization::video_settings_pending
|
||||
[localization_language_index_]
|
||||
: ""));
|
||||
(*ui_->stream)->set_video_quality_index(props->video_settings_.quality);
|
||||
(*ui_->stream)
|
||||
->set_frame_rate_index(props->video_settings_.frame_rate == 30 ? 0 : 1);
|
||||
(*ui_->stream)
|
||||
->set_adaptation_policy_index(props->video_settings_.preference);
|
||||
}
|
||||
#if defined(__APPLE__)
|
||||
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
|
||||
#else
|
||||
@@ -3062,12 +3122,6 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
auto& main = ui_->main;
|
||||
language_button_value_ =
|
||||
localization::detail::ClampLanguageIndex(main->get_language_index());
|
||||
video_quality_button_value_ =
|
||||
std::clamp(main->get_video_quality_index(), 0, 2);
|
||||
video_frame_rate_button_value_ =
|
||||
std::clamp(main->get_frame_rate_index(), 0, 1);
|
||||
video_adaptation_policy_button_value_ =
|
||||
std::clamp(main->get_adaptation_policy_index(), 0, 2);
|
||||
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
||||
enable_hardware_video_codec_ = ConfigCenter::IsHardwareVideoCodecAvailable() &&
|
||||
main->get_hardware_codec_enabled();
|
||||
@@ -3080,15 +3134,8 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
|
||||
localization_language_index_ = language_button_value_;
|
||||
config_center_->SetLanguage(localization_language_);
|
||||
privacy_.SetText({
|
||||
localization::privacy_screen_unlock_hint[localization_language_index_]});
|
||||
config_center_->SetVideoQuality(
|
||||
static_cast<ConfigCenter::VIDEO_QUALITY>(video_quality_button_value_));
|
||||
config_center_->SetVideoFrameRate(static_cast<ConfigCenter::VIDEO_FRAME_RATE>(
|
||||
video_frame_rate_button_value_));
|
||||
config_center_->SetVideoAdaptationPolicy(
|
||||
static_cast<ConfigCenter::VIDEO_ADAPTATION_POLICY>(
|
||||
video_adaptation_policy_button_value_));
|
||||
privacy_.SetText(
|
||||
{localization::privacy_screen_unlock_hint[localization_language_index_]});
|
||||
config_center_->SetVideoEncodeFormat(
|
||||
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
|
||||
video_encode_format_button_value_));
|
||||
@@ -3133,10 +3180,6 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
}
|
||||
|
||||
language_button_value_last_ = language_button_value_;
|
||||
video_quality_button_value_last_ = video_quality_button_value_;
|
||||
video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
|
||||
video_adaptation_policy_button_value_last_ =
|
||||
video_adaptation_policy_button_value_;
|
||||
video_encode_format_button_value_last_ = video_encode_format_button_value_;
|
||||
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
|
||||
enable_turn_last_ = enable_turn_;
|
||||
@@ -3463,6 +3506,9 @@ void GuiApplication::Cleanup() {
|
||||
}
|
||||
#endif
|
||||
ui_.reset();
|
||||
if (update_checker_) {
|
||||
update_checker_->Stop();
|
||||
}
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class UpdateChecker;
|
||||
|
||||
// Slint application shell. GuiRuntime remains the owner of transport, media,
|
||||
// device, clipboard, transfer and settings behavior.
|
||||
class GuiApplication final : private GuiRuntime {
|
||||
@@ -48,6 +50,7 @@ private:
|
||||
void BindStreamCallbacks();
|
||||
void BindServerCallbacks();
|
||||
void Tick();
|
||||
void PollUpdateCheck();
|
||||
void ShareLocalCursorState();
|
||||
bool RequestPasswordChange(const std::string &password);
|
||||
void HandlePasswordChangeResult();
|
||||
@@ -88,6 +91,7 @@ private:
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
std::unique_ptr<UpdateChecker> update_checker_;
|
||||
CursorStateProvider cursor_state_provider_;
|
||||
std::unordered_map<std::string, CursorDeliveryState>
|
||||
cursor_delivery_states_;
|
||||
|
||||
@@ -131,6 +131,10 @@ struct TranslationRow {
|
||||
X(screen_capture_method, u8"采集方式:", "Capture Method:", \
|
||||
u8"Способ захвата:") \
|
||||
X(screen_capture_method_auto, u8"自动", "Auto", u8"Авто") \
|
||||
X(video_settings, u8"画面设置", "Video settings", u8"Настройки видео") \
|
||||
X(video_settings_pending, u8"正在应用…", "Applying…", u8"Применение…") \
|
||||
X(video_settings_failed, u8"调整失败,请重试。", "Could not apply settings. Try again.", u8"Не удалось применить настройки. Повторите попытку.") \
|
||||
X(video_settings_unavailable, u8"连接建立后可调整,需被控端支持。", "Available when connected to a supported remote device.", u8"Доступно при подключении к поддерживаемому устройству.") \
|
||||
X(video_quality, u8"画面质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_frame_rate, u8"画面采集帧率:", \
|
||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||
@@ -195,6 +199,15 @@ struct TranslationRow {
|
||||
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
||||
X(new_version_available, u8"新版本可用", "New Version Available", \
|
||||
u8"Доступна новая версия") \
|
||||
X(check_for_updates, u8"检查更新", "Check for updates", \
|
||||
u8"Проверить обновления") \
|
||||
X(checking_for_updates, u8"正在检查更新…", "Checking for updates…", \
|
||||
u8"Проверка обновлений…") \
|
||||
X(up_to_date, u8"已是最新版本", "You're up to date", \
|
||||
u8"Установлена последняя версия") \
|
||||
X(update_check_failed, u8"检查失败,请稍后重试", \
|
||||
"Check failed. Please try again later.", \
|
||||
u8"Не удалось проверить обновления. Повторите позже.") \
|
||||
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
|
||||
X(version, u8"版本", "Version", u8"Версия") \
|
||||
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
||||
|
||||
@@ -142,10 +142,10 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
capture_secure_callbacks_ = 0;
|
||||
capture_send_errors_ = 0;
|
||||
capture_max_gap_us_ = capture_send_us_ = capture_max_send_us_ = 0;
|
||||
const int fps = owner_.config_center_->GetVideoFrameRate() ==
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
// Keep the shared source ready for either live rate. MiniRTC admits 30/60
|
||||
// frames independently per viewer, so switching never restarts native
|
||||
// capture.
|
||||
const int fps = 60;
|
||||
LOG_INFO("Init screen capturer with {} fps", fps);
|
||||
SetDisplayInfoList({});
|
||||
registered_display_stream_count_ = 0;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -297,12 +298,6 @@ int SettingsManager::Load() {
|
||||
|
||||
owner_.language_button_value_ = localization::detail::ClampLanguageIndex(
|
||||
static_cast<int>(owner_.config_center_->GetLanguage()));
|
||||
owner_.video_quality_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoQuality());
|
||||
owner_.video_frame_rate_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoFrameRate());
|
||||
owner_.video_adaptation_policy_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoAdaptationPolicy());
|
||||
owner_.video_encode_format_button_value_ =
|
||||
static_cast<int>(owner_.config_center_->GetVideoEncodeFormat());
|
||||
owner_.enable_hardware_video_codec_ =
|
||||
@@ -324,11 +319,6 @@ int SettingsManager::Load() {
|
||||
owner_.file_transfer_save_path_last_ = saved_path;
|
||||
|
||||
owner_.language_button_value_last_ = owner_.language_button_value_;
|
||||
owner_.video_quality_button_value_last_ = owner_.video_quality_button_value_;
|
||||
owner_.video_frame_rate_button_value_last_ =
|
||||
owner_.video_frame_rate_button_value_;
|
||||
owner_.video_adaptation_policy_button_value_last_ =
|
||||
owner_.video_adaptation_policy_button_value_;
|
||||
owner_.video_encode_format_button_value_last_ =
|
||||
owner_.video_encode_format_button_value_;
|
||||
owner_.enable_hardware_video_codec_last_ =
|
||||
|
||||
@@ -58,8 +58,9 @@ private:
|
||||
struct CacheV1 {
|
||||
char client_id_with_password[17];
|
||||
int language;
|
||||
int video_quality;
|
||||
int video_frame_rate;
|
||||
// Reserved legacy slots: retain the on-disk cache layout.
|
||||
int legacy_video_quality;
|
||||
int legacy_video_frame_rate;
|
||||
int video_encode_format;
|
||||
bool enable_hardware_video_codec;
|
||||
bool enable_turn;
|
||||
@@ -71,8 +72,9 @@ private:
|
||||
struct CacheV2 {
|
||||
char client_id_with_password[17];
|
||||
int language;
|
||||
int video_quality;
|
||||
int video_frame_rate;
|
||||
// Reserved legacy slots: retain the on-disk cache layout.
|
||||
int legacy_video_quality;
|
||||
int legacy_video_frame_rate;
|
||||
int video_encode_format;
|
||||
bool enable_hardware_video_codec;
|
||||
bool enable_turn;
|
||||
|
||||
@@ -136,28 +136,12 @@ int GuiRuntime::CreateConnectionPeer() {
|
||||
params_.turn_mode = static_cast<TurnMode>(config_center_->GetTurnMode());
|
||||
params_.enable_srtp = config_center_->IsEnableSrtp();
|
||||
params_.video_content_type = VideoContentType::ScreenContent;
|
||||
params_.video_quality =
|
||||
static_cast<VideoQuality>(config_center_->GetVideoQuality());
|
||||
params_.video_frame_rate =
|
||||
config_center_->GetVideoFrameRate() ==
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
switch (config_center_->GetVideoAdaptationPolicy()) {
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::FRAME_RATE_PRIORITY:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
break;
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::BALANCED:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::Balanced;
|
||||
break;
|
||||
case ConfigCenter::VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY:
|
||||
default:
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainResolution;
|
||||
break;
|
||||
}
|
||||
// Bootstrap outgoing media with fixed defaults. Live overrides come only
|
||||
// from the authorized controller and apply to its connection on this host.
|
||||
params_.video_quality = VideoQuality::QualityHigh;
|
||||
params_.video_frame_rate = 60;
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainResolution;
|
||||
params_.on_receive_video_buffer = nullptr;
|
||||
params_.on_receive_audio_buffer = PeerEventHandler::OnReceiveAudioBuffer;
|
||||
params_.on_receive_data_buffer = PeerEventHandler::OnReceiveDataBuffer;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -146,6 +147,47 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
if (remote_action.type == ControlType::video_settings_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
if (source_id != props->control_data_label_) return;
|
||||
std::lock_guard lock(props->video_settings_mutex_);
|
||||
if (!props->video_settings_supported_) return;
|
||||
if (remote_action.vs.accepted &&
|
||||
static_cast<int32_t>(remote_action.vs.request_id -
|
||||
props->applied_video_settings_.request_id) >= 0) {
|
||||
props->applied_video_settings_ = remote_action.vs;
|
||||
}
|
||||
if (remote_action.vs.request_id != props->video_settings_.request_id) return;
|
||||
props->video_settings_pending_ = false;
|
||||
props->video_settings_failed_ = !remote_action.vs.accepted;
|
||||
if (remote_action.vs.accepted) {
|
||||
props->video_settings_ = remote_action.vs;
|
||||
props->applied_video_settings_ = remote_action.vs;
|
||||
} else {
|
||||
const auto request_id = props->video_settings_.request_id;
|
||||
props->video_settings_ = props->applied_video_settings_;
|
||||
props->video_settings_.request_id = request_id;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (remote_action.type == ControlType::video_settings) {
|
||||
if (source_id != runtime->control_data_label_ ||
|
||||
!runtime->IsAuthorizedController(remote_id))
|
||||
return;
|
||||
const auto& settings = remote_action.vs;
|
||||
remote_action.vs.accepted =
|
||||
UpdateVideoSettings(
|
||||
runtime->peer_, remote_id.data(), remote_id.size(),
|
||||
static_cast<VideoQuality>(settings.quality), settings.frame_rate,
|
||||
static_cast<VideoDegradationPreference>(settings.preference)) == 0;
|
||||
remote_action.type = ControlType::video_settings_status;
|
||||
const auto message = remote_action.to_json();
|
||||
SendReliableDataFrameToPeer(runtime->peer_, message.data(), message.size(),
|
||||
runtime->control_data_label_.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
return;
|
||||
}
|
||||
if (remote_action.type == ControlType::privacy_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
if (source_id != props->control_data_label_) return;
|
||||
@@ -274,6 +316,25 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
}
|
||||
|
||||
if (is_client_mode) {
|
||||
// Negotiate live settings only on the reliable control stream. Old
|
||||
// hosts omit this capability and retain their existing video behavior.
|
||||
if (props && source_id == props->control_data_label_) {
|
||||
std::lock_guard lock(props->video_settings_mutex_);
|
||||
const bool first_advertisement = !props->video_settings_supported_;
|
||||
props->video_settings_supported_ =
|
||||
remote_action.i.supports_video_settings;
|
||||
if (first_advertisement && props->video_settings_supported_) {
|
||||
RemoteAction settings{};
|
||||
settings.type = ControlType::video_settings;
|
||||
++props->video_settings_.request_id;
|
||||
props->video_settings_pending_ = true;
|
||||
props->video_settings_request_tick_ = SDL_GetTicks();
|
||||
settings.vs = props->video_settings_;
|
||||
const auto message = settings.to_json();
|
||||
SendReliableDataFrame(props->peer_, message.data(), message.size(),
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
// client mode
|
||||
if (props && props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -267,6 +268,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
RemoteAction remote_action = MakeHostInformation(
|
||||
GetHostName(), runtime->devices_.host_display_list(),
|
||||
/*supports_privacy_screen=*/true);
|
||||
remote_action.i.supports_video_settings = true;
|
||||
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
remote_action.i.display_list[i]);
|
||||
@@ -297,6 +299,12 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
props->connection_established_ = false;
|
||||
props->enable_mouse_control_ = false;
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
{
|
||||
std::lock_guard lock(props->video_settings_mutex_);
|
||||
props->video_settings_supported_ = false;
|
||||
props->video_settings_pending_ = false;
|
||||
props->video_settings_failed_ = false;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||
props->remote_cursor_state_ = {};
|
||||
@@ -382,6 +390,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
RemoteAction remote_action = MakeHostInformation(
|
||||
GetHostName(), runtime->devices_.host_display_list(),
|
||||
/*supports_privacy_screen=*/true);
|
||||
remote_action.i.supports_video_settings = true;
|
||||
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
remote_action.i.display_list[i]);
|
||||
|
||||
@@ -119,6 +119,14 @@ struct RemoteSession {
|
||||
bool tab_opened_ = true;
|
||||
std::string remote_host_name_;
|
||||
bool remote_service_status_received_ = false;
|
||||
std::mutex video_settings_mutex_;
|
||||
// Requested and confirmed settings for this remote session only.
|
||||
VideoSettings video_settings_{2, 60, 1};
|
||||
bool video_settings_supported_ = false;
|
||||
VideoSettings applied_video_settings_{2, 60, 1, 0, false};
|
||||
bool video_settings_pending_ = false;
|
||||
bool video_settings_failed_ = false;
|
||||
uint64_t video_settings_request_tick_ = 0;
|
||||
std::mutex privacy_status_mutex_;
|
||||
PrivacyStatus privacy_status_{};
|
||||
bool privacy_status_received_ = false;
|
||||
|
||||
@@ -128,9 +128,6 @@ struct UserSettingsState {
|
||||
char self_hosted_user_id_[17] = "";
|
||||
char connection_login_identity_[17] = "";
|
||||
int language_button_value_ = 0;
|
||||
int video_quality_button_value_ = 2;
|
||||
int video_frame_rate_button_value_ = 1;
|
||||
int video_adaptation_policy_button_value_ = 1;
|
||||
int video_encode_format_button_value_ = 0;
|
||||
bool enable_hardware_video_codec_ = true;
|
||||
bool enable_turn_ = true;
|
||||
@@ -138,9 +135,6 @@ struct UserSettingsState {
|
||||
char signal_server_port_[6] = "9099";
|
||||
bool enable_self_hosted_ = false;
|
||||
int language_button_value_last_ = 0;
|
||||
int video_quality_button_value_last_ = 0;
|
||||
int video_frame_rate_button_value_last_ = 0;
|
||||
int video_adaptation_policy_button_value_last_ = 1;
|
||||
int video_encode_format_button_value_last_ = 0;
|
||||
bool enable_hardware_video_codec_last_ = false;
|
||||
bool enable_turn_last_ = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Single AOT entry point. Generating one header for the entire UI avoids
|
||||
// duplicate imported-component definitions when C++ uses all three windows.
|
||||
export { MainWindow, RecentConnection, UiStrings } from "main_window.slint";
|
||||
export { MainWindow, RecentConnection, UpdateCheckStatus, UiStrings } from "main_window.slint";
|
||||
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
||||
export { ServerWindow, ControllerEntry } from "server_window.slint";
|
||||
|
||||
@@ -14,6 +14,8 @@ export struct ReleaseNoteBlock {
|
||||
section-gap: bool,
|
||||
}
|
||||
|
||||
export enum UpdateCheckStatus { idle, checking, up-to-date, available, failed }
|
||||
|
||||
export global UiStrings {
|
||||
in-out property <string> local-desktop: "Local Desktop";
|
||||
in-out property <string> remote-desktop: "Remote Desktop";
|
||||
@@ -38,14 +40,8 @@ export global UiStrings {
|
||||
in-out property <string> delete-connection: "Delete connection";
|
||||
in-out property <string> confirm-delete: "Delete this recent connection?";
|
||||
in-out property <string> language: "Language";
|
||||
in-out property <string> video-quality: "Video quality";
|
||||
in-out property <string> frame-rate: "Frame rate";
|
||||
in-out property <string> capture-method: "Capture method";
|
||||
in-out property <string> capture-method-auto: "Auto";
|
||||
in-out property <string> adaptation-policy: "Video preference";
|
||||
in-out property <string> priority-frame-rate: "Frame rate";
|
||||
in-out property <string> priority-quality: "Quality";
|
||||
in-out property <string> priority-balanced: "Balanced";
|
||||
in-out property <string> codec: "Video codec";
|
||||
in-out property <string> hardware-codec: "Hardware video codec";
|
||||
in-out property <string> turn-relay: "TURN relay";
|
||||
@@ -65,6 +61,10 @@ export global UiStrings {
|
||||
in-out property <string> signal-disconnected: "Signal server disconnected";
|
||||
in-out property <string> tls-error: "Signal server TLS certificate error";
|
||||
in-out property <string> update-available: "A new version is available";
|
||||
in-out property <string> check-for-updates: "Check for updates";
|
||||
in-out property <string> checking-for-updates: "Checking for updates…";
|
||||
in-out property <string> up-to-date: "You’re up to date";
|
||||
in-out property <string> update-check-failed: "Check failed. Please try again later.";
|
||||
in-out property <string> release-notes: "Release notes";
|
||||
in-out property <string> later: "Later";
|
||||
in-out property <string> download: "Download";
|
||||
@@ -84,9 +84,6 @@ export global UiStrings {
|
||||
in-out property <string> do-not-remind: "Do not remind again";
|
||||
in-out property <string> notification: "Notification";
|
||||
in-out property <string> service-suppressed-message: "You will not be reminded again. You can install the service later in Settings.";
|
||||
in-out property <string> quality-low: "Low";
|
||||
in-out property <string> quality-medium: "Medium";
|
||||
in-out property <string> quality-high: "High";
|
||||
in-out property <string> codec-h264: "H.264";
|
||||
in-out property <string> codec-av1: "AV1";
|
||||
in-out property <string> self-hosted-settings: "Self-hosted server settings";
|
||||
@@ -266,6 +263,11 @@ export component MainWindow inherits Window {
|
||||
in property <bool> signal-tls-error: false;
|
||||
in property <[RecentConnection]> recent-connections;
|
||||
in property <bool> update-available: false;
|
||||
in property <UpdateCheckStatus> update-check-status: idle;
|
||||
private property <string> update-status-text:
|
||||
root.update-check-status == UpdateCheckStatus.checking ? UiStrings.checking-for-updates :
|
||||
root.update-check-status == UpdateCheckStatus.up-to-date ? UiStrings.up-to-date :
|
||||
root.update-check-status == UpdateCheckStatus.failed ? UiStrings.update-check-failed : "";
|
||||
in property <string> current-version: "";
|
||||
in property <string> latest-version: "";
|
||||
in property <string> release-name: "";
|
||||
@@ -292,11 +294,8 @@ export component MainWindow inherits Window {
|
||||
in-out property <bool> portable-service-do-not-remind: false;
|
||||
|
||||
in-out property <int> language-index: 0;
|
||||
in-out property <int> video-quality-index: 2;
|
||||
in-out property <int> frame-rate-index: 1;
|
||||
in property <bool> capture-method-visible: false;
|
||||
in-out property <int> capture-method-index: 0;
|
||||
in-out property <int> adaptation-policy-index: 1;
|
||||
in-out property <int> codec-index: 0;
|
||||
in-out property <bool> hardware-codec-enabled: true;
|
||||
in-out property <bool> turn-enabled: true;
|
||||
@@ -333,6 +332,7 @@ export component MainWindow inherits Window {
|
||||
callback cancel-self-hosted-settings;
|
||||
callback browse-save-path;
|
||||
callback open-download;
|
||||
callback check-for-updates;
|
||||
callback connection-cancel;
|
||||
callback connection-acknowledge;
|
||||
callback connection-submit-password(string, bool);
|
||||
@@ -352,7 +352,7 @@ export component MainWindow inherits Window {
|
||||
private property <length> settings-scroll-y: 0px;
|
||||
in-out property <bool> self-host-settings-open: false;
|
||||
in-out property <bool> about-open: false;
|
||||
in-out property <bool> update-open: root.update-available;
|
||||
in-out property <bool> update-open: false;
|
||||
in-out property <bool> reset-password-open: false;
|
||||
in-out property <bool> reset-password-invalid: false;
|
||||
in-out property <string> new-password-input: "";
|
||||
@@ -1493,21 +1493,6 @@ export component MainWindow inherits Window {
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.capture-method; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.adaptation-policy; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 91px : 160px; y: 2px; width: root.compact-language ? 88px : 87px; enabled: !root.settings-session-active; model: [UiStrings.priority-frame-rate, UiStrings.priority-quality, UiStrings.priority-balanced]; current-index <=> root.adaptation-policy-index; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
@@ -1621,10 +1606,10 @@ export component MainWindow inherits Window {
|
||||
z: 46;
|
||||
TouchArea { clicked => { focus-sink.focus(); } }
|
||||
DialogSurface {
|
||||
x: 170px;
|
||||
y: 127px;
|
||||
width: 300px;
|
||||
height: 196px;
|
||||
x: (root.width - self.width) / 2;
|
||||
y: (root.height - self.height) / 2;
|
||||
width: 320px;
|
||||
height: 216px;
|
||||
border-radius: 7px;
|
||||
|
||||
Rectangle {
|
||||
@@ -1638,19 +1623,81 @@ export component MainWindow inherits Window {
|
||||
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; }
|
||||
Text { x: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
|
||||
}
|
||||
Text { x: 30px; y: 40px; width: 240px; height: 20px; text: UiStrings.version + ": CrossDesk " + root.current-version; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
if root.update-available: Text { x: 30px; y: 60px; width: 240px; height: 20px; text: UiStrings.update-available + ":"; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
if root.update-available: Rectangle {
|
||||
x: 30px;
|
||||
y: 60px;
|
||||
width: 240px;
|
||||
height: 20px;
|
||||
version-link := Text { text: root.latest-version; color: version-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; horizontal-alignment: center; vertical-alignment: center; }
|
||||
version-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
|
||||
// Keep the dialog/footer stable while visible text rows determine
|
||||
// each group's size and the layout redistributes the space between.
|
||||
about-content := VerticalLayout {
|
||||
padding: 16px;
|
||||
padding-top: 35px;
|
||||
spacing: 12px;
|
||||
alignment: space-between;
|
||||
|
||||
VerticalLayout {
|
||||
vertical-stretch: 0;
|
||||
spacing: 4px;
|
||||
Text {
|
||||
text: "CrossDesk " + root.current-version;
|
||||
color: #202124;
|
||||
font-size: ImGuiFontStyle.base;
|
||||
font-weight: 600;
|
||||
min-height: 18px;
|
||||
overflow: elide;
|
||||
}
|
||||
if root.update-available: Rectangle {
|
||||
height: max(18px, version-link.preferred-height);
|
||||
accessible-role: button;
|
||||
accessible-label: UiStrings.release-notes + " " + root.latest-version;
|
||||
accessible-action-default => { version-touch.clicked(); }
|
||||
version-link := Text {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
text: UiStrings.update-available + ": " + root.latest-version;
|
||||
color: version-touch.has-hover ? #174ea6 : #334fd1;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
vertical-alignment: center;
|
||||
horizontal-alignment: left;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
version-touch := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.about-open = false; root.update-open = true; }
|
||||
}
|
||||
}
|
||||
if root.update-status-text != "": Text {
|
||||
text: root.update-status-text;
|
||||
color: root.update-check-status == UpdateCheckStatus.failed ? #b3261e : #61656c;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
min-height: 18px;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
vertical-stretch: 0;
|
||||
spacing: 3px;
|
||||
Rectangle { height: 1px; background: #e6e8eb; }
|
||||
Text { height: 16px; text: UiStrings.copyright; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
Text { height: 16px; text: UiStrings.license; color: #61656c; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
vertical-stretch: 0;
|
||||
alignment: end;
|
||||
spacing: 8px;
|
||||
CompactButton {
|
||||
width: max(76px, check-button-metrics.preferred-width + 20px);
|
||||
text: UiStrings.check-for-updates;
|
||||
enabled: root.update-check-status != UpdateCheckStatus.checking;
|
||||
accessible-role: button;
|
||||
accessible-label: self.text;
|
||||
accessible-action-default => { if self.enabled { root.check-for-updates(); } }
|
||||
clicked => { root.check-for-updates(); }
|
||||
check-button-metrics := Text { visible: false; text: UiStrings.check-for-updates; font-size: ImGuiFontStyle.body; }
|
||||
}
|
||||
CompactButton { width: 48px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
|
||||
}
|
||||
}
|
||||
Text { x: 30px; y: 85px; width: 240px; height: 20px; text: UiStrings.copyright; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
Text { x: 30px; y: 105px; width: 240px; height: 20px; text: UiStrings.license; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
CompactButton { x: 133px; y: 145px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,18 @@ export struct NetworkStatsRow {
|
||||
}
|
||||
|
||||
export global StreamStrings {
|
||||
in-out property <string> video-settings: "Video settings";
|
||||
in-out property <string> video-settings-unavailable: "The remote device does not support live video settings";
|
||||
in-out property <string> video-quality: "Video quality";
|
||||
in-out property <string> frame-rate: "Capture frame rate";
|
||||
in-out property <string> adaptation-policy: "Video preference";
|
||||
in-out property <string> quality-low: "Low";
|
||||
in-out property <string> quality-medium: "Medium";
|
||||
in-out property <string> quality-high: "High";
|
||||
in-out property <string> priority-frame-rate: "Frame rate";
|
||||
in-out property <string> priority-quality: "Quality";
|
||||
in-out property <string> priority-balanced: "Balanced";
|
||||
|
||||
in-out property <string> select-display: "Select Display";
|
||||
in-out property <string> send-shortcut: "Send Shortcut";
|
||||
in-out property <string> privacy-enable: "Enable privacy screen";
|
||||
@@ -128,6 +140,35 @@ component ControlBarButton inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
component VideoSettingRow inherits VerticalLayout {
|
||||
in property <string> label;
|
||||
in property <[string]> options;
|
||||
in property <int> selected;
|
||||
in property <bool> enabled;
|
||||
callback choose(int);
|
||||
callback pressed;
|
||||
spacing: 4px;
|
||||
Text { text: root.label; color: #5c626c; font-size: 11px; height: 18px; }
|
||||
HorizontalLayout {
|
||||
spacing: 4px;
|
||||
for option[index] in root.options: Rectangle {
|
||||
height: 27px;
|
||||
background: !root.enabled ? #f1f1f1 : root.selected == index ? #d7eaff
|
||||
: option-touch.has-hover ? #eaf3ff : #f5f6f8;
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-color: root.enabled && root.selected == index ? #4296fa : #dce0e6;
|
||||
Text { text: option; color: root.enabled ? #30343b : #8b8f95;
|
||||
font-size: 11px; horizontal-alignment: center; vertical-alignment: center; }
|
||||
option-touch := TouchArea {
|
||||
pointer-event(event) => { if event.kind == PointerEventKind.down { root.pressed(); } }
|
||||
clicked => { if root.enabled { root.choose(index); } }
|
||||
scroll-event(event) => { accept }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component StreamWindow inherits Window {
|
||||
title: "CrossDesk";
|
||||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||
@@ -168,6 +209,12 @@ export component StreamWindow inherits Window {
|
||||
in property <bool> remote-cursor-active: false;
|
||||
in property <int> remote-cursor-shape: 0;
|
||||
in property <bool> audio-enabled: true;
|
||||
in property <bool> video-settings-enabled: false;
|
||||
in property <string> video-settings-feedback: "";
|
||||
in-out property <int> video-quality-index: 2;
|
||||
in-out property <int> frame-rate-index: 1;
|
||||
in-out property <int> adaptation-policy-index: 1;
|
||||
callback change-video-settings(int, int, int);
|
||||
in property <bool> fullscreen-enabled: false;
|
||||
in property <bool> stats-visible: false;
|
||||
in property <[NetworkStatsRow]> stats-rows;
|
||||
@@ -213,16 +260,26 @@ export component StreamWindow inherits Window {
|
||||
private property <bool> display-menu-animation-enabled: false;
|
||||
private property <float> display-menu-progress: 0.0;
|
||||
private property <bool> shortcut-menu-open: false;
|
||||
in-out property <bool> video-menu-open: false;
|
||||
private property <length> video-menu-width: min(320px, video.width - 8px);
|
||||
private property <length> video-menu-height: root.video-settings-enabled && root.video-settings-feedback == "" ? 182px : 220px;
|
||||
private property <length> video-menu-x: min(max(4px, control.x + video-button.x), video.width - root.video-menu-width - 4px);
|
||||
private property <length> video-menu-y: control.y + root.control-toolbar-height + root.video-menu-height <= video.height
|
||||
? control.y + root.control-toolbar-height - 1px : max(0px, control.y - root.video-menu-height);
|
||||
changed selected-tab => { root.video-menu-open = false; }
|
||||
changed control-expanded => { if !root.control-expanded { root.video-menu-open = false; } }
|
||||
changed window-active => { if !root.window-active { root.video-menu-open = false; } }
|
||||
private property <bool> control-tooltip-visible: false;
|
||||
private property <ControlBarTooltip> hovered-control: {
|
||||
if root.window-active && !root.control-dragging
|
||||
&& !root.display-menu-open && !root.shortcut-menu-open {
|
||||
&& !root.display-menu-open && !root.shortcut-menu-open && !root.video-menu-open {
|
||||
if collapse-button.has-hover { return collapse-button.tooltip-info; }
|
||||
if root.control-expanded {
|
||||
if display-button.has-hover { return display-button.tooltip-info; }
|
||||
if shortcut-button.has-hover { return shortcut-button.tooltip-info; }
|
||||
if mouse-button.has-hover { return mouse-button.tooltip-info; }
|
||||
if privacy-button.has-hover { return privacy-button.tooltip-info; }
|
||||
if video-button.has-hover { return video-button.tooltip-info; }
|
||||
if audio-button.has-hover { return audio-button.tooltip-info; }
|
||||
if file-button.has-hover { return file-button.tooltip-info; }
|
||||
if stats-button.has-hover { return stats-button.tooltip-info; }
|
||||
@@ -305,7 +362,10 @@ export component StreamWindow inherits Window {
|
||||
&& pointer-x <= shortcut-menu-x + 180px
|
||||
&& pointer-y >= shortcut-menu-y
|
||||
&& pointer-y <= shortcut-menu-y + 68px;
|
||||
return over-control || over-display-menu || over-shortcut-menu;
|
||||
let over-video-menu = root.video-menu-open
|
||||
&& pointer-x >= root.video-menu-x && pointer-x <= root.video-menu-x + root.video-menu-width
|
||||
&& pointer-y >= root.video-menu-y && pointer-y <= root.video-menu-y + root.video-menu-height;
|
||||
return over-control || over-display-menu || over-shortcut-menu || over-video-menu;
|
||||
}
|
||||
|
||||
begin-control-drag(press-x, press-y) => {
|
||||
@@ -324,6 +384,7 @@ export component StreamWindow inherits Window {
|
||||
root.display-menu-progress = 0.0;
|
||||
root.display-menu-open = false;
|
||||
root.shortcut-menu-open = false;
|
||||
root.video-menu-open = false;
|
||||
root.control-dragging = true;
|
||||
}
|
||||
move-control-drag(pointer-x, pointer-y) => {
|
||||
@@ -697,7 +758,7 @@ export component StreamWindow inherits Window {
|
||||
control := Rectangle {
|
||||
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
|
||||
y: min(max(0px, root.control-pos-y), parent.height - self.height);
|
||||
width: root.control-expanded ? min(76px + 8 * root.control-button-step, parent.width) : 20px;
|
||||
width: root.control-expanded ? min(76px + 9 * root.control-button-step, parent.width) : 20px;
|
||||
height: root.stats-visible && root.control-expanded
|
||||
? min(root.control-toolbar-height + root.stats-content-height + 8px, parent.height)
|
||||
: root.control-toolbar-height;
|
||||
@@ -729,6 +790,7 @@ export component StreamWindow inherits Window {
|
||||
root.display-menu-progress = 0.0;
|
||||
root.display-menu-open = !root.display-menu-open;
|
||||
root.shortcut-menu-open = false;
|
||||
root.video-menu-open = false;
|
||||
if root.display-menu-open {
|
||||
display-menu-open-timer.running = true;
|
||||
}
|
||||
@@ -744,6 +806,7 @@ export component StreamWindow inherits Window {
|
||||
root.display-menu-progress = 0.0;
|
||||
root.shortcut-menu-open = !root.shortcut-menu-open;
|
||||
root.display-menu-open = false;
|
||||
root.video-menu-open = false;
|
||||
}
|
||||
}
|
||||
mouse-button := ControlBarButton {
|
||||
@@ -766,23 +829,34 @@ export component StreamWindow inherits Window {
|
||||
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
|
||||
clicked => { root.select-file(); }
|
||||
}
|
||||
stats-button := ControlBarButton {
|
||||
video-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: (root.control-docked-left ? 9px : 42px) + 6 * root.control-button-step; y: 7px;
|
||||
icon: "\u{f1de}"; selected: root.video-menu-open;
|
||||
tooltip: StreamStrings.video-settings;
|
||||
clicked => {
|
||||
root.video-menu-open = !root.video-menu-open;
|
||||
root.display-menu-open = false;
|
||||
root.shortcut-menu-open = false;
|
||||
}
|
||||
}
|
||||
stats-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: (root.control-docked-left ? 9px : 42px) + 7 * root.control-button-step; y: 7px;
|
||||
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
|
||||
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
|
||||
clicked => { root.toggle-stats(); }
|
||||
}
|
||||
fullscreen-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: (root.control-docked-left ? 9px : 42px) + 7 * root.control-button-step; y: 7px;
|
||||
x: (root.control-docked-left ? 9px : 42px) + 8 * root.control-button-step; y: 7px;
|
||||
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
|
||||
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
|
||||
clicked => { root.toggle-fullscreen(); }
|
||||
}
|
||||
close-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: (root.control-docked-left ? 9px : 42px) + 8 * root.control-button-step; y: 7px;
|
||||
x: (root.control-docked-left ? 9px : 42px) + 9 * root.control-button-step; y: 7px;
|
||||
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
|
||||
clicked => { root.disconnect(); }
|
||||
}
|
||||
@@ -1010,6 +1084,59 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
// While the menu is open, outside gestures dismiss it locally.
|
||||
// Keep the backdrop through pointer-up; otherwise Slint's implicit
|
||||
// pointer move or release can reach the remote input surface.
|
||||
if root.video-menu-open: Rectangle {
|
||||
width: parent.width; height: parent.height; z: 9;
|
||||
TouchArea {
|
||||
pointer-event(event) => {
|
||||
if event.kind == PointerEventKind.down {
|
||||
root.suppress-next-remote-pointer-release = true;
|
||||
}
|
||||
}
|
||||
clicked => { root.video-menu-open = false; }
|
||||
scroll-event(event) => { accept }
|
||||
}
|
||||
}
|
||||
if root.video-menu-open: Rectangle {
|
||||
x: root.video-menu-x; y: root.video-menu-y;
|
||||
width: root.video-menu-width; height: root.video-menu-height;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border;
|
||||
border-radius: 5px; z: 11;
|
||||
TouchArea {
|
||||
pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } }
|
||||
scroll-event(event) => { accept }
|
||||
}
|
||||
VerticalLayout {
|
||||
padding: 10px; spacing: 8px;
|
||||
VideoSettingRow {
|
||||
label: StreamStrings.video-quality;
|
||||
options: [StreamStrings.quality-low, StreamStrings.quality-medium, StreamStrings.quality-high];
|
||||
selected: root.video-quality-index; enabled: root.video-settings-enabled;
|
||||
pressed => { root.suppress-next-remote-pointer-release = true; }
|
||||
choose(index) => { root.change-video-settings(index, root.frame-rate-index, root.adaptation-policy-index); }
|
||||
}
|
||||
VideoSettingRow {
|
||||
label: StreamStrings.frame-rate; options: ["30 fps", "60 fps"];
|
||||
selected: root.frame-rate-index; enabled: root.video-settings-enabled;
|
||||
pressed => { root.suppress-next-remote-pointer-release = true; }
|
||||
choose(index) => { root.change-video-settings(root.video-quality-index, index, root.adaptation-policy-index); }
|
||||
}
|
||||
VideoSettingRow {
|
||||
label: StreamStrings.adaptation-policy;
|
||||
options: [StreamStrings.priority-frame-rate, StreamStrings.priority-quality, StreamStrings.priority-balanced];
|
||||
selected: root.adaptation-policy-index; enabled: root.video-settings-enabled;
|
||||
pressed => { root.suppress-next-remote-pointer-release = true; }
|
||||
choose(index) => { root.change-video-settings(root.video-quality-index, root.frame-rate-index, index); }
|
||||
}
|
||||
if !root.video-settings-enabled || root.video-settings-feedback != "": Text {
|
||||
text: root.video-settings-enabled ? root.video-settings-feedback : StreamStrings.video-settings-unavailable; wrap: word-wrap;
|
||||
color: #8b8f95; font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root.display-menu-open: display-menu := Rectangle {
|
||||
private property <length> expanded-height: min(180px, root.displays.length * 30px + 8px);
|
||||
private property <int> hovered-index:
|
||||
|
||||
@@ -53,19 +53,9 @@ inline int ApplyMainWindowStrings(
|
||||
strings.set_confirm_delete(
|
||||
Text(localization::confirm_delete_connection[language]));
|
||||
strings.set_language(Text(localization::language[language]));
|
||||
strings.set_video_quality(Text(localization::video_quality[language]));
|
||||
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
|
||||
strings.set_capture_method(Text(localization::screen_capture_method[language]));
|
||||
strings.set_capture_method_auto(
|
||||
Text(localization::screen_capture_method_auto[language]));
|
||||
strings.set_adaptation_policy(
|
||||
Text(localization::video_adaptation_policy[language]));
|
||||
strings.set_priority_frame_rate(
|
||||
Text(localization::video_priority_frame_rate[language]));
|
||||
strings.set_priority_quality(
|
||||
Text(localization::video_priority_quality[language]));
|
||||
strings.set_priority_balanced(
|
||||
Text(localization::video_priority_balanced[language]));
|
||||
strings.set_codec(Text(localization::video_encode_format[language]));
|
||||
strings.set_hardware_codec(
|
||||
Text(localization::enable_hardware_video_codec[language]));
|
||||
@@ -90,6 +80,12 @@ inline int ApplyMainWindowStrings(
|
||||
strings.set_tls_error(Text(localization::signal_tls_cert_error[language]));
|
||||
strings.set_update_available(
|
||||
Text(localization::new_version_available[language]));
|
||||
strings.set_check_for_updates(Text(localization::check_for_updates[language]));
|
||||
strings.set_checking_for_updates(
|
||||
Text(localization::checking_for_updates[language]));
|
||||
strings.set_up_to_date(Text(localization::up_to_date[language]));
|
||||
strings.set_update_check_failed(
|
||||
Text(localization::update_check_failed[language]));
|
||||
strings.set_release_notes(Text(localization::release_notes[language]));
|
||||
strings.set_download(Text(localization::update[language]));
|
||||
strings.set_input_password(Text(localization::input_password[language]));
|
||||
@@ -121,10 +117,6 @@ inline int ApplyMainWindowStrings(
|
||||
strings.set_notification(Text(localization::notification[language]));
|
||||
strings.set_service_suppressed_message(
|
||||
Text(localization::windows_service_prompt_suppressed_message[language]));
|
||||
strings.set_quality_low(Text(localization::video_quality_low[language]));
|
||||
strings.set_quality_medium(
|
||||
Text(localization::video_quality_medium[language]));
|
||||
strings.set_quality_high(Text(localization::video_quality_high[language]));
|
||||
strings.set_codec_h264(Text(localization::h264[language]));
|
||||
strings.set_codec_av1(Text(localization::av1[language]));
|
||||
strings.set_self_hosted_settings(
|
||||
@@ -147,6 +139,23 @@ inline int ApplyStreamWindowStrings(
|
||||
strings.set_privacy_disable(Text(localization::privacy_disable[language]));
|
||||
strings.set_control_mouse(Text(localization::control_mouse[language]));
|
||||
strings.set_release_mouse(Text(localization::release_mouse[language]));
|
||||
strings.set_video_settings(Text(localization::video_settings[language]));
|
||||
strings.set_video_settings_unavailable(
|
||||
Text(localization::video_settings_unavailable[language]));
|
||||
strings.set_video_quality(Text(localization::video_quality[language]));
|
||||
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
|
||||
strings.set_adaptation_policy(
|
||||
Text(localization::video_adaptation_policy[language]));
|
||||
strings.set_quality_low(Text(localization::video_quality_low[language]));
|
||||
strings.set_quality_medium(
|
||||
Text(localization::video_quality_medium[language]));
|
||||
strings.set_quality_high(Text(localization::video_quality_high[language]));
|
||||
strings.set_priority_frame_rate(
|
||||
Text(localization::video_priority_frame_rate[language]));
|
||||
strings.set_priority_quality(
|
||||
Text(localization::video_priority_quality[language]));
|
||||
strings.set_priority_balanced(
|
||||
Text(localization::video_priority_balanced[language]));
|
||||
strings.set_audio(Text(localization::audio_capture[language]));
|
||||
strings.set_mute(Text(localization::mute[language]));
|
||||
strings.set_select_file(Text(localization::select_file[language]));
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "update_checker.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
UpdateChecker::UpdateChecker(std::string current_version, Fetch fetch)
|
||||
: current_version_(std::move(current_version)), fetch_(std::move(fetch)) {}
|
||||
|
||||
UpdateChecker::~UpdateChecker() { Stop(); }
|
||||
|
||||
bool UpdateChecker::RequestCheck(TimePoint now) {
|
||||
if (stopped_ || pending_.valid()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
pending_ = std::async(std::launch::async, fetch_);
|
||||
status_ = Status::Checking;
|
||||
return true;
|
||||
} catch (const std::exception& error) {
|
||||
LOG_WARN("Could not start update check: {}", error.what());
|
||||
ScheduleRetry(now);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateChecker::Poll(TimePoint now) {
|
||||
if (stopped_) {
|
||||
return false;
|
||||
}
|
||||
if (!pending_.valid()) {
|
||||
if (now >= next_check_) {
|
||||
RequestCheck(now);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (pending_.wait_for(0ms) != std::future_status::ready) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<VersionInfo> result;
|
||||
try {
|
||||
result = pending_.get();
|
||||
} catch (const std::exception& error) {
|
||||
LOG_WARN("Update check failed: {}", error.what());
|
||||
} catch (...) {
|
||||
LOG_WARN("Update check failed with an unknown error");
|
||||
}
|
||||
if (!result) {
|
||||
ScheduleRetry(now);
|
||||
return true;
|
||||
}
|
||||
latest_ = std::move(result);
|
||||
update_available_ =
|
||||
IsNewerVersionWithMetadata(current_version_, latest_->version,
|
||||
latest_->release_date, latest_->patch);
|
||||
status_ = update_available_ ? Status::Available : Status::UpToDate;
|
||||
failures_ = 0;
|
||||
next_check_ = now + 6h;
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateChecker::ScheduleRetry(TimePoint now) {
|
||||
constexpr std::chrono::minutes delays[] = {5min, 30min, 60min};
|
||||
next_check_ = now + delays[std::min(failures_, 2u)];
|
||||
failures_ = std::min(failures_ + 1, 3u);
|
||||
status_ = Status::Failed;
|
||||
}
|
||||
|
||||
void UpdateChecker::Stop() {
|
||||
stopped_ = true;
|
||||
// Join before application/logging teardown. The HTTP client uses finite
|
||||
// connection/read/write and overall request timeouts; no worker is detached.
|
||||
if (pending_.valid()) {
|
||||
pending_.wait();
|
||||
pending_ = {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-24
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _UPDATE_CHECKER_H_
|
||||
#define _UPDATE_CHECKER_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "version_checker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Owned and polled by the UI thread. Only the fetch function runs on a worker;
|
||||
// it returns a value and never accesses application or UI state.
|
||||
class UpdateChecker {
|
||||
public:
|
||||
// Wall time includes suspend time, so an overdue check runs on the first UI
|
||||
// tick after wake, including on platforms whose steady clock pauses in sleep.
|
||||
using Clock = std::chrono::system_clock;
|
||||
using TimePoint = Clock::time_point;
|
||||
using Fetch = std::function<std::optional<VersionInfo>()>;
|
||||
enum class Status { Idle, Checking, UpToDate, Available, Failed };
|
||||
|
||||
explicit UpdateChecker(std::string current_version,
|
||||
Fetch fetch = CheckUpdate);
|
||||
~UpdateChecker();
|
||||
|
||||
// Manual requests bypass the schedule, but share an already running check.
|
||||
bool RequestCheck(TimePoint now = Clock::now());
|
||||
// Starts due checks and consumes ready results without blocking. Returns true
|
||||
// when a result was consumed; failure leaves the last successful info intact.
|
||||
bool Poll(TimePoint now = Clock::now());
|
||||
void Stop();
|
||||
|
||||
Status status() const { return status_; }
|
||||
bool update_available() const { return update_available_; }
|
||||
const std::optional<VersionInfo>& latest() const { return latest_; }
|
||||
|
||||
private:
|
||||
void ScheduleRetry(TimePoint now);
|
||||
|
||||
std::string current_version_;
|
||||
Fetch fetch_;
|
||||
std::future<std::optional<VersionInfo>> pending_;
|
||||
std::optional<VersionInfo> latest_;
|
||||
TimePoint next_check_{};
|
||||
Status status_ = Status::Idle;
|
||||
unsigned int failures_ = 0;
|
||||
bool update_available_ = false;
|
||||
bool stopped_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -23,10 +23,6 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static std::string latest_release_date_ = "";
|
||||
static bool latest_patch_available_ = false;
|
||||
static int latest_patch_ = 0;
|
||||
|
||||
std::vector<int> SplitVersion(const std::string& ver);
|
||||
|
||||
namespace {
|
||||
@@ -202,18 +198,20 @@ int CompareNumericVersion(const std::vector<int>& current,
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ResetLatestMetadata() {
|
||||
latest_release_date_ = "";
|
||||
latest_patch_available_ = false;
|
||||
latest_patch_ = 0;
|
||||
}
|
||||
|
||||
bool ReadPatchField(const nlohmann::json& json, int* patch) {
|
||||
if (!json.contains("patch")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& patch_value = json["patch"];
|
||||
if (patch_value.is_number_unsigned()) {
|
||||
const auto parsed = patch_value.get<unsigned long long>();
|
||||
if (parsed > static_cast<unsigned long long>(std::numeric_limits<int>::max())) {
|
||||
return false;
|
||||
}
|
||||
*patch = static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
if (patch_value.is_number_integer()) {
|
||||
const long long parsed = patch_value.get<long long>();
|
||||
if (parsed < 0 || parsed > std::numeric_limits<int>::max()) {
|
||||
@@ -353,9 +351,7 @@ bool IsNewerDate(const std::string& date1, const std::string& date2) {
|
||||
}
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest) {
|
||||
return IsNewerVersionWithMetadata(
|
||||
current, latest, latest_release_date_,
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
return IsNewerVersionWithMetadata(current, latest, "", -1);
|
||||
}
|
||||
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
@@ -391,11 +387,54 @@ bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json CheckUpdate() {
|
||||
std::optional<VersionInfo> ParseVersionInfo(const nlohmann::json& json) {
|
||||
if (!json.is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto read_string = [&](const char* key) -> std::string {
|
||||
const auto it = json.find(key);
|
||||
return it != json.end() && it->is_string() ? it->get<std::string>()
|
||||
: std::string{};
|
||||
};
|
||||
VersionInfo info;
|
||||
info.version = read_string("latest_version");
|
||||
if (info.version.empty()) {
|
||||
info.version = read_string("version");
|
||||
}
|
||||
if (!info.version.empty() &&
|
||||
(info.version.front() == 'v' || info.version.front() == 'V')) {
|
||||
info.version.erase(0, 1);
|
||||
}
|
||||
// Invalid/missing version data is a failed check, never "up to date".
|
||||
const auto numeric_end = FindNumericEnd(info.version, 0);
|
||||
if (numeric_end == 0 || info.version[numeric_end - 1] == '.' ||
|
||||
(numeric_end < info.version.size() &&
|
||||
(numeric_end + 1 == info.version.size() ||
|
||||
(info.version[numeric_end] != '-' && info.version[numeric_end] != '+')))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::istringstream parts(info.version.substr(0, numeric_end));
|
||||
std::string part;
|
||||
int number = 0;
|
||||
while (std::getline(parts, part, '.')) {
|
||||
if (!TryParseNonNegativeInt(part, &number)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
info.release_name = read_string("releaseName");
|
||||
info.release_notes = read_string("releaseNotes");
|
||||
info.release_date = read_string("releaseDate");
|
||||
ReadPatchField(json, &info.patch);
|
||||
return info;
|
||||
}
|
||||
|
||||
std::optional<VersionInfo> CheckUpdate() {
|
||||
httplib::Client cli("https://version.crossdesk.cn");
|
||||
|
||||
cli.set_connection_timeout(5);
|
||||
cli.set_read_timeout(5);
|
||||
cli.set_write_timeout(5);
|
||||
cli.set_max_timeout(15000);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
|
||||
@@ -406,33 +445,25 @@ nlohmann::json CheckUpdate() {
|
||||
if (res) {
|
||||
if (res->status == 200) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(res->body);
|
||||
if (j.contains("releaseDate") && j["releaseDate"].is_string()) {
|
||||
latest_release_date_ = j["releaseDate"];
|
||||
auto info = ParseVersionInfo(nlohmann::json::parse(res->body));
|
||||
if (info) {
|
||||
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
|
||||
info->version, info->release_date, info->patch);
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
LOG_WARN("version.json does not contain a valid version");
|
||||
}
|
||||
latest_patch_ = 0;
|
||||
latest_patch_available_ = ReadPatchField(j, &latest_patch_);
|
||||
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
|
||||
j.value("latest_version", j.value("version", "")),
|
||||
j.value("releaseDate", ""),
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
return j;
|
||||
return info;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARN("Failed to parse version.json: {}", e.what());
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
return std::nullopt;
|
||||
}
|
||||
} else {
|
||||
LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
return std::nullopt;
|
||||
}
|
||||
} else {
|
||||
LogHttpError(res);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,21 @@
|
||||
#define _VERSION_CHECKER_H_
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
nlohmann::json CheckUpdate();
|
||||
struct VersionInfo {
|
||||
std::string version;
|
||||
std::string release_name;
|
||||
std::string release_notes;
|
||||
std::string release_date;
|
||||
int patch = -1;
|
||||
};
|
||||
|
||||
std::optional<VersionInfo> ParseVersionInfo(const nlohmann::json& json);
|
||||
std::optional<VersionInfo> CheckUpdate();
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest);
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ void ResetMainCaptureState(
|
||||
window->set_about_open(false);
|
||||
window->set_update_open(false);
|
||||
window->set_update_available(false);
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::Idle);
|
||||
window->set_reset_password_open(false);
|
||||
window->set_reset_password_invalid(false);
|
||||
window->set_new_password_input("");
|
||||
@@ -213,9 +214,20 @@ void ConfigureMainCapturePage(
|
||||
} else if (options.page == "self-hosted") {
|
||||
window->set_settings_open(true);
|
||||
window->set_self_host_settings_open(true);
|
||||
} else if (options.page == "about") {
|
||||
window->set_current_version("0.0.0");
|
||||
} else if (options.page == "about" || options.page == "about-checking" ||
|
||||
options.page == "about-failed" || options.page == "about-update") {
|
||||
window->set_current_version("v1.3.5-1-20260924");
|
||||
window->set_about_open(true);
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::UpToDate);
|
||||
if (options.page == "about-checking") {
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::Checking);
|
||||
} else if (options.page == "about-failed" || options.page == "about-update") {
|
||||
window->set_update_available(true);
|
||||
window->set_latest_version("v1.3.6-1-20260924");
|
||||
window->set_update_check_status(options.page == "about-failed"
|
||||
? crossdesk::ui::UpdateCheckStatus::Failed
|
||||
: crossdesk::ui::UpdateCheckStatus::Available);
|
||||
}
|
||||
} else if (options.page == "update") {
|
||||
window->set_update_available(true);
|
||||
window->set_latest_version("v0.0.1");
|
||||
@@ -444,6 +456,24 @@ int main() {
|
||||
window->set_about_open(true);
|
||||
assert(window->get_settings_open());
|
||||
assert(window->get_about_open());
|
||||
window->set_update_available(true);
|
||||
if (window->get_update_open()) {
|
||||
std::cerr << "Automatic update checks must not open the update dialog\n";
|
||||
return 8;
|
||||
}
|
||||
bool update_check_requested = false;
|
||||
window->on_check_for_updates([&] {
|
||||
update_check_requested = true;
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::Checking);
|
||||
});
|
||||
window->invoke_check_for_updates();
|
||||
if (!update_check_requested || window->get_update_check_status() !=
|
||||
crossdesk::ui::UpdateCheckStatus::Checking) {
|
||||
std::cerr << "Manual update check must expose the checking state\n";
|
||||
return 8;
|
||||
}
|
||||
window->set_update_available(false);
|
||||
window->set_update_check_status(crossdesk::ui::UpdateCheckStatus::Idle);
|
||||
if (const char *snapshot_path =
|
||||
std::getenv("CROSSDESK_SETTINGS_UI_SNAPSHOT")) {
|
||||
window->set_about_open(false);
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "update_checker.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
using namespace std::chrono_literals;
|
||||
using crossdesk::UpdateChecker;
|
||||
using crossdesk::VersionInfo;
|
||||
using Status = UpdateChecker::Status;
|
||||
using TimePoint = UpdateChecker::TimePoint;
|
||||
|
||||
bool Expect(const char* name, bool condition) {
|
||||
if (!condition) {
|
||||
std::cerr << "Failed: " << name << '\n';
|
||||
}
|
||||
return condition;
|
||||
}
|
||||
|
||||
bool Complete(UpdateChecker& checker, TimePoint now) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + 5s;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (checker.Poll(now)) {
|
||||
return true;
|
||||
}
|
||||
std::this_thread::sleep_for(1ms);
|
||||
}
|
||||
return Expect("worker finishes without blocking UI polling", false);
|
||||
}
|
||||
|
||||
bool TestAsyncAndSchedule() {
|
||||
std::promise<void> release;
|
||||
const auto gate = release.get_future().share();
|
||||
std::promise<void> started;
|
||||
auto started_future = started.get_future();
|
||||
std::atomic<int> calls{0};
|
||||
UpdateChecker checker("1.0.0", [&]() -> std::optional<VersionInfo> {
|
||||
if (++calls == 1) {
|
||||
started.set_value();
|
||||
gate.wait();
|
||||
}
|
||||
return VersionInfo{"1.0.0"};
|
||||
});
|
||||
const TimePoint now{};
|
||||
bool ok = Expect("initial state has no result", !checker.latest());
|
||||
checker.Poll(now);
|
||||
ok &= Expect("startup check starts asynchronously",
|
||||
checker.status() == Status::Checking &&
|
||||
started_future.wait_for(5s) == std::future_status::ready);
|
||||
ok &= Expect("manual request shares running startup check",
|
||||
!checker.RequestCheck(now));
|
||||
checker.Poll(now + 8h);
|
||||
ok &= Expect("overdue timer cannot overlap active request", calls == 1);
|
||||
release.set_value();
|
||||
ok &= Complete(checker, now);
|
||||
ok &= Expect(
|
||||
"successful current version result",
|
||||
checker.status() == Status::UpToDate && !checker.update_available());
|
||||
checker.Poll(now + 6h - 1ms);
|
||||
ok &=
|
||||
Expect("no check before six hours", checker.status() == Status::UpToDate);
|
||||
checker.Poll(now + 6h);
|
||||
ok &= Complete(checker, now + 6h);
|
||||
ok &= Expect("periodic check runs once", calls == 2);
|
||||
// Simulate the process receiving no ticks while the machine is asleep.
|
||||
checker.Poll(now + 20h);
|
||||
ok &= Complete(checker, now + 20h);
|
||||
ok &= Expect("wake starts one overdue check without replaying missed checks",
|
||||
calls == 3);
|
||||
ok &= Expect("manual check bypasses six-hour interval",
|
||||
checker.RequestCheck(now + 20h + 1min));
|
||||
ok &= Complete(checker, now + 20h + 1min);
|
||||
checker.Stop();
|
||||
checker.Poll(now + 48h);
|
||||
ok &= Expect("stopped checker cannot restart",
|
||||
!checker.RequestCheck(now + 48h) && calls == 4);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestRetryAndCachedResult() {
|
||||
int calls = 0;
|
||||
UpdateChecker checker(
|
||||
"1.3.5-1-20260529", [&]() -> std::optional<VersionInfo> {
|
||||
++calls;
|
||||
if (calls <= 4 || calls == 7) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (calls == 5) {
|
||||
return VersionInfo{"1.3.5", "Patch release", "Fixes", "2026-05-29",
|
||||
2};
|
||||
}
|
||||
if (calls == 6) {
|
||||
throw std::runtime_error("simulated network failure");
|
||||
}
|
||||
return VersionInfo{"1.3.5", "", "", "", 1};
|
||||
});
|
||||
TimePoint now{};
|
||||
checker.Poll(now);
|
||||
bool ok = Complete(checker, now);
|
||||
ok &= Expect("startup failure is not up to date",
|
||||
checker.status() == Status::Failed && !checker.latest());
|
||||
for (const auto delay : {5min, 30min, 60min, 60min}) {
|
||||
checker.Poll(now + delay - 1ms);
|
||||
ok &= Expect("failure does not retry early",
|
||||
checker.status() == Status::Failed);
|
||||
now += delay;
|
||||
checker.Poll(now);
|
||||
ok &= Complete(checker, now);
|
||||
}
|
||||
ok &= Expect("metadata patch detects update after retries",
|
||||
checker.status() == Status::Available &&
|
||||
checker.update_available() && calls == 5);
|
||||
checker.RequestCheck(now + 1min);
|
||||
now += 1min;
|
||||
ok &= Complete(checker, now);
|
||||
ok &= Expect("exception is a failure and preserves previous update details",
|
||||
checker.status() == Status::Failed &&
|
||||
checker.update_available() && checker.latest() &&
|
||||
checker.latest()->release_name == "Patch release" &&
|
||||
checker.latest()->release_notes == "Fixes" &&
|
||||
checker.latest()->patch == 2);
|
||||
checker.Poll(now + 5min - 1ms);
|
||||
ok &= Expect("successful check resets backoff",
|
||||
checker.status() == Status::Failed);
|
||||
now += 5min;
|
||||
checker.Poll(now);
|
||||
ok &= Complete(checker, now);
|
||||
ok &= Expect("retry is due five minutes after failure", calls == 7);
|
||||
checker.RequestCheck(now + 1min);
|
||||
ok &= Complete(checker, now + 1min);
|
||||
ok &= Expect("new successful result replaces stale metadata",
|
||||
checker.status() == Status::UpToDate &&
|
||||
!checker.update_available() &&
|
||||
checker.latest()->release_notes.empty() &&
|
||||
checker.latest()->patch == 1);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool TestStopDuringFetch() {
|
||||
std::atomic<bool> finished{false};
|
||||
UpdateChecker checker("1.0.0", [&]() -> std::optional<VersionInfo> {
|
||||
std::this_thread::sleep_for(10ms);
|
||||
finished = true;
|
||||
return VersionInfo{"2.0.0"};
|
||||
});
|
||||
checker.RequestCheck();
|
||||
checker.Stop();
|
||||
return Expect("shutdown joins the worker and discards pending UI results",
|
||||
finished && !checker.Poll() && !checker.latest() &&
|
||||
!checker.RequestCheck());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
bool ok = TestAsyncAndSchedule();
|
||||
ok &= TestRetryAndCachedResult();
|
||||
ok &= TestStopDuringFetch();
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "version_checker.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -50,5 +52,49 @@ int main() {
|
||||
"v1.3.5-9-20260529", "v1.3.6-1-20260529", "", -1),
|
||||
true);
|
||||
|
||||
const auto info =
|
||||
crossdesk::ParseVersionInfo({{"latest_version", "v1.3.5-20260529"},
|
||||
{"patch", "10"},
|
||||
{"releaseName", "Release"},
|
||||
{"releaseNotes", "Changes"},
|
||||
{"releaseDate", "2026-05-29"}});
|
||||
ok &= ExpectEqual("valid metadata is normalized and retained",
|
||||
info && info->version == "1.3.5-20260529" &&
|
||||
info->patch == 10 && info->release_name == "Release" &&
|
||||
info->release_notes == "Changes" &&
|
||||
info->release_date == "2026-05-29",
|
||||
true);
|
||||
const auto legacy = crossdesk::ParseVersionInfo({{"version", "1.3.6"}});
|
||||
ok &= ExpectEqual("legacy version field is supported",
|
||||
legacy && legacy->version == "1.3.6" && legacy->patch == -1,
|
||||
true);
|
||||
for (const auto& json :
|
||||
std::vector<nlohmann::json>{nullptr,
|
||||
nlohmann::json::array(),
|
||||
nlohmann::json::object(),
|
||||
{{"version", false}},
|
||||
{{"version", ""}},
|
||||
{{"version", "unknown"}},
|
||||
{{"version", "1..2"}},
|
||||
{{"version", ".1.2"}},
|
||||
{{"version", "1.2."}},
|
||||
{{"version", "1.2-"}},
|
||||
{{"version", "999999999999999999999.2"}}}) {
|
||||
ok &= ExpectEqual("invalid response is a failed check: " + json.dump(),
|
||||
crossdesk::ParseVersionInfo(json).has_value(), false);
|
||||
}
|
||||
const auto malformed_optional = crossdesk::ParseVersionInfo(
|
||||
{{"version", "1.3.5"},
|
||||
{"patch", std::numeric_limits<uint64_t>::max()},
|
||||
{"releaseName", nullptr},
|
||||
{"releaseNotes", 123}});
|
||||
ok &= ExpectEqual("bad optional metadata does not leak into later checks",
|
||||
malformed_optional && malformed_optional->patch == -1 &&
|
||||
malformed_optional->release_name.empty() &&
|
||||
malformed_optional->release_notes.empty(),
|
||||
true);
|
||||
ok &= ExpectEqual("plain comparison has no hidden metadata from prior checks",
|
||||
crossdesk::IsNewerVersion("1.3.5", "1.3.5"), false);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -197,6 +197,12 @@ function setup_targets()
|
||||
add_frameworks("Security", "CoreFoundation")
|
||||
end
|
||||
|
||||
target("update_checker_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_deps("version_checker")
|
||||
add_files("apps/desktop/tests/update_checker_test.cpp")
|
||||
|
||||
target("privacy")
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "crossdesk_wire")
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: a25a3b4c4a...000ab78890
@@ -29,6 +29,8 @@ enum ControlType {
|
||||
cursor_state = 8,
|
||||
privacy_command = 9,
|
||||
privacy_status = 10,
|
||||
video_settings = 11,
|
||||
video_settings_status = 12,
|
||||
};
|
||||
|
||||
enum MouseFlag {
|
||||
@@ -125,6 +127,7 @@ struct HostInfo {
|
||||
int* bottom;
|
||||
// Optional capability; absent in legacy desktop and web host information.
|
||||
bool supports_privacy_screen;
|
||||
bool supports_video_settings;
|
||||
};
|
||||
|
||||
struct ServiceStatus {
|
||||
@@ -153,6 +156,20 @@ struct PrivacyStatus {
|
||||
char reason[256];
|
||||
};
|
||||
|
||||
struct VideoSettings {
|
||||
int quality; // 0: low, 1: medium, 2: high
|
||||
int frame_rate; // 30 or 60
|
||||
int preference; // 0: frame rate, 1: quality, 2: balanced
|
||||
uint32_t request_id;
|
||||
bool accepted; // Only meaningful in video_settings_status.
|
||||
};
|
||||
|
||||
inline bool ValidVideoSettings(const VideoSettings& settings) {
|
||||
return settings.quality >= 0 && settings.quality <= 2 &&
|
||||
(settings.frame_rate == 30 || settings.frame_rate == 60) &&
|
||||
settings.preference >= 0 && settings.preference <= 2;
|
||||
}
|
||||
|
||||
struct RemoteAction {
|
||||
ControlType type = ControlType::invalid;
|
||||
union {
|
||||
@@ -167,6 +184,7 @@ struct RemoteAction {
|
||||
ServiceCommand c;
|
||||
PrivacyCommand pc;
|
||||
PrivacyStatus ps;
|
||||
VideoSettings vs;
|
||||
};
|
||||
|
||||
std::string to_json() const;
|
||||
|
||||
@@ -23,6 +23,7 @@ void ResetHostInfo(HostInfo& info) {
|
||||
info.right = nullptr;
|
||||
info.bottom = nullptr;
|
||||
info.supports_privacy_screen = false;
|
||||
info.supports_video_settings = false;
|
||||
}
|
||||
|
||||
bool AllocateHostDisplays(HostInfo& info, std::size_t count) {
|
||||
@@ -134,6 +135,15 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
|
||||
{"render_mode", static_cast<int>(action.cs.render_mode)},
|
||||
{"hidden_reason", static_cast<int>(action.cs.hidden_reason)}};
|
||||
break;
|
||||
case ControlType::video_settings:
|
||||
case ControlType::video_settings_status:
|
||||
if (!ValidVideoSettings(action.vs)) return {};
|
||||
object["video_settings"] = {{"quality", action.vs.quality},
|
||||
{"frame_rate", action.vs.frame_rate},
|
||||
{"preference", action.vs.preference},
|
||||
{"request_id", action.vs.request_id},
|
||||
{"accepted", action.vs.accepted}};
|
||||
break;
|
||||
case ControlType::audio_capture:
|
||||
object["audio_capture"] = action.a;
|
||||
break;
|
||||
@@ -172,11 +182,12 @@ std::string RemoteAction::ToJson(const RemoteAction& action) {
|
||||
{"right", action.i.right ? action.i.right[index] : 0},
|
||||
{"bottom", action.i.bottom ? action.i.bottom[index] : 0}});
|
||||
}
|
||||
object["host_info"] = {{"host_name", action.i.host_name},
|
||||
{"display_num", action.i.display_num},
|
||||
{"displays", displays},
|
||||
{"supports_privacy_screen",
|
||||
action.i.supports_privacy_screen}};
|
||||
object["host_info"] = {
|
||||
{"host_name", action.i.host_name},
|
||||
{"display_num", action.i.display_num},
|
||||
{"displays", displays},
|
||||
{"supports_privacy_screen", action.i.supports_privacy_screen},
|
||||
{"supports_video_settings", action.i.supports_video_settings}};
|
||||
break;
|
||||
}
|
||||
case ControlType::invalid:
|
||||
@@ -277,6 +288,26 @@ bool RemoteAction::FromJson(const std::string& json_string,
|
||||
std::clamp(output.cs.visual_offset_y, -1.0f, 1.0f);
|
||||
break;
|
||||
}
|
||||
case ControlType::video_settings:
|
||||
case ControlType::video_settings_status: {
|
||||
const auto& settings = object.at("video_settings");
|
||||
for (const auto* field : {"quality", "frame_rate", "preference"}) {
|
||||
if (!settings.at(field).is_number_integer()) return false;
|
||||
const auto value = settings.at(field).get<int64_t>();
|
||||
if (value < 0 || value > 60) return false;
|
||||
}
|
||||
output.vs = {settings.at("quality").get<int>(),
|
||||
settings.at("frame_rate").get<int>(),
|
||||
settings.at("preference").get<int>()};
|
||||
if (!ValidVideoSettings(output.vs)) return false;
|
||||
const auto& request_id = settings.at("request_id");
|
||||
if (!request_id.is_number_integer() || request_id.get<int64_t>() < 0 ||
|
||||
request_id.get<uint64_t>() > UINT32_MAX)
|
||||
return false;
|
||||
output.vs.request_id = request_id.get<uint32_t>();
|
||||
output.vs.accepted = settings.value("accepted", false);
|
||||
break;
|
||||
}
|
||||
case ControlType::audio_capture:
|
||||
output.a = object.at("audio_capture").get<bool>();
|
||||
break;
|
||||
@@ -334,6 +365,8 @@ bool RemoteAction::FromJson(const std::string& json_string,
|
||||
output.i.supports_privacy_screen =
|
||||
privacy_support != host_info_object.end() &&
|
||||
privacy_support->is_boolean() && privacy_support->get<bool>();
|
||||
output.i.supports_video_settings =
|
||||
host_info_object.value("supports_video_settings", false);
|
||||
const std::string host_name =
|
||||
host_info_object.at("host_name").get<std::string>();
|
||||
std::strncpy(output.i.host_name, host_name.c_str(),
|
||||
|
||||
Reference in New Issue
Block a user