fix: improve update notes display

This commit is contained in:
dijunkun
2026-07-22 03:03:32 +08:00
parent f5c4e2ba4a
commit 2c20599800
5 changed files with 593 additions and 552 deletions
+271 -216
View File
@@ -96,54 +96,94 @@ std::string FormatRemotePeerIdInput(std::string_view input) {
} }
std::string CompactPeerId(std::string id) { std::string CompactPeerId(std::string id) {
id.erase(std::remove_if(id.begin(), id.end(), [](unsigned char ch) { id.erase(
return std::isspace(ch) != 0; std::remove_if(id.begin(), id.end(),
}), [](unsigned char ch) { return std::isspace(ch) != 0; }),
id.end()); id.end());
return id; return id;
} }
std::string Trim(std::string value) { std::string Trim(std::string value) {
const auto first = std::find_if_not(value.begin(), value.end(), const auto first =
[](unsigned char ch) { std::find_if_not(value.begin(), value.end(),
[](unsigned char ch) { return std::isspace(ch) != 0; });
const auto last =
std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
return std::isspace(ch) != 0; return std::isspace(ch) != 0;
}); }).base();
const auto last = std::find_if_not(value.rbegin(), value.rend(),
[](unsigned char ch) {
return std::isspace(ch) != 0;
})
.base();
if (first >= last) { if (first >= last) {
return {}; return {};
} }
return std::string(first, last); return std::string(first, last);
} }
std::string CleanReleaseNotesForUi(std::string markdown) { struct ParsedReleaseNoteBlock {
slint::StyledText content;
bool section_gap = false;
};
std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
std::string_view markdown) {
std::vector<ParsedReleaseNoteBlock> blocks;
bool section_gap = false;
size_t line_start = 0; size_t line_start = 0;
while (line_start < markdown.size()) { while (line_start < markdown.size()) {
size_t content_start = line_start;
while (content_start < markdown.size() && markdown[content_start] == '#') {
++content_start;
}
if (content_start > line_start && content_start < markdown.size() &&
markdown[content_start] == ' ') {
++content_start;
}
if (content_start > line_start) {
markdown.erase(line_start, content_start - line_start);
}
const size_t newline = markdown.find('\n', line_start); const size_t newline = markdown.find('\n', line_start);
if (newline == std::string::npos) { const size_t line_end = newline == std::string_view::npos
? markdown.size()
: newline;
std::string_view line = markdown.substr(line_start, line_end - line_start);
if (!line.empty() && line.back() == '\r') {
line.remove_suffix(1);
}
if (std::all_of(line.begin(), line.end(), [](unsigned char ch) {
return std::isspace(ch) != 0;
})) {
section_gap = !blocks.empty();
if (newline == std::string_view::npos) {
break;
}
line_start = newline + 1;
continue;
}
size_t heading_end = 0;
while (heading_end < line.size() && heading_end < 6 &&
line[heading_end] == '#') {
++heading_end;
}
const bool is_heading =
heading_end > 0 && heading_end < line.size() &&
std::isspace(static_cast<unsigned char>(line[heading_end])) != 0;
if (is_heading) {
if (newline == std::string_view::npos) {
break;
}
line_start = newline + 1;
continue;
}
const std::string block_markdown(line);
ParsedReleaseNoteBlock block;
if (const auto parsed =
slint::StyledText::from_markdown(block_markdown)) {
block.content = *parsed;
} else {
LOG_WARN("Failed to parse a release-note Markdown block; using plain text");
block.content = slint::StyledText::from_plain_text(block_markdown);
}
block.section_gap = section_gap;
blocks.push_back(std::move(block));
section_gap = false;
if (newline == std::string_view::npos) {
break; break;
} }
line_start = newline + 1; line_start = newline + 1;
} }
size_t marker = 0; return blocks;
while ((marker = markdown.find("**", marker)) != std::string::npos) {
markdown.erase(marker, 2);
}
return markdown;
} }
std::string DecodeDroppedPath(std::string text) { std::string DecodeDroppedPath(std::string text) {
@@ -232,8 +272,7 @@ std::string FormatBitrate(uint32_t bits_per_second) {
} else if (bits_per_second < 1000000) { } else if (bits_per_second < 1000000) {
std::snprintf(text, sizeof(text), "%u kbps", bits_per_second / 1000); std::snprintf(text, sizeof(text), "%u kbps", bits_per_second / 1000);
} else { } else {
std::snprintf(text, sizeof(text), "%.1f mbps", std::snprintf(text, sizeof(text), "%.1f mbps", bits_per_second / 1000000.0);
bits_per_second / 1000000.0);
} }
return text; return text;
} }
@@ -259,8 +298,7 @@ std::string FormatFileSize(uint64_t bytes) {
} else if (bytes < 1024 * 1024) { } else if (bytes < 1024 * 1024) {
std::snprintf(text, sizeof(text), "%.2f KB", bytes / 1024.0); std::snprintf(text, sizeof(text), "%.2f KB", bytes / 1024.0);
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) { } else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
std::snprintf(text, sizeof(text), "%.2f MB", std::snprintf(text, sizeof(text), "%.2f MB", bytes / (1024.0 * 1024.0));
bytes / (1024.0 * 1024.0));
} else { } else {
std::snprintf(text, sizeof(text), "%.2f GB", std::snprintf(text, sizeof(text), "%.2f GB",
bytes / (1024.0 * 1024.0 * 1024.0)); bytes / (1024.0 * 1024.0 * 1024.0));
@@ -464,16 +502,15 @@ SDL_DisplayID DisplayForSlintWindow(const slint::Window &window) {
const auto position = window.position(); const auto position = window.position();
const auto size = window.size(); const auto size = window.size();
const SDL_Point center{ const SDL_Point center{
static_cast<int>(std::lround(position.x / scale + static_cast<int>(
size.width / (2.0f * scale))), std::lround(position.x / scale + size.width / (2.0f * scale))),
static_cast<int>(std::lround(position.y / scale + static_cast<int>(
size.height / (2.0f * scale)))}; std::lround(position.y / scale + size.height / (2.0f * scale)))};
const SDL_DisplayID display = SDL_GetDisplayForPoint(&center); const SDL_DisplayID display = SDL_GetDisplayForPoint(&center);
return display != 0 ? display : SDL_GetPrimaryDisplay(); return display != 0 ? display : SDL_GetPrimaryDisplay();
} }
bool PositionWindowAtCenter(slint::Window &window, bool PositionWindowAtCenter(slint::Window& window, const slint::Window& anchor,
const slint::Window &anchor,
float logical_width, float logical_height) { float logical_width, float logical_height) {
if (!CanUseGlobalPointerPosition()) { if (!CanUseGlobalPointerPosition()) {
return false; return false;
@@ -481,21 +518,20 @@ bool PositionWindowAtCenter(slint::Window &window,
const SDL_DisplayID display = DisplayForSlintWindow(anchor); const SDL_DisplayID display = DisplayForSlintWindow(anchor);
SDL_Rect usable_bounds{}; SDL_Rect usable_bounds{};
if (display == 0 || if (display == 0 || !SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
!SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
LOG_WARN("Unable to obtain usable display bounds for stream window: {}", LOG_WARN("Unable to obtain usable display bounds for stream window: {}",
SDL_GetError()); SDL_GetError());
return false; return false;
} }
const float target_x = static_cast<float>(usable_bounds.x) + const float target_x =
(static_cast<float>(usable_bounds.w) - logical_width) / static_cast<float>(usable_bounds.x) +
2.0f; (static_cast<float>(usable_bounds.w) - logical_width) / 2.0f;
const float target_y = static_cast<float>(usable_bounds.y) + const float target_y =
(static_cast<float>(usable_bounds.h) - logical_height) / static_cast<float>(usable_bounds.y) +
2.0f; (static_cast<float>(usable_bounds.h) - logical_height) / 2.0f;
window.set_position(slint::LogicalPosition( window.set_position(
slint::Point<float>{target_x, target_y})); slint::LogicalPosition(slint::Point<float>{target_x, target_y}));
return true; return true;
} }
@@ -510,18 +546,17 @@ bool PositionWindowAtBottomRight(slint::Window &window, float logical_width,
const auto position = window.position(); const auto position = window.position();
const auto size = window.size(); const auto size = window.size();
const SDL_Point center{ const SDL_Point center{
static_cast<int>(std::lround(position.x / scale + static_cast<int>(
size.width / (2.0f * scale))), std::lround(position.x / scale + size.width / (2.0f * scale))),
static_cast<int>(std::lround(position.y / scale + static_cast<int>(
size.height / (2.0f * scale)))}; std::lround(position.y / scale + size.height / (2.0f * scale)))};
display = SDL_GetDisplayForPoint(&center); display = SDL_GetDisplayForPoint(&center);
if (display == 0) { if (display == 0) {
display = SDL_GetPrimaryDisplay(); display = SDL_GetPrimaryDisplay();
} }
SDL_Rect usable_bounds{}; SDL_Rect usable_bounds{};
if (display == 0 || if (display == 0 || !SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
!SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
LOG_WARN("Unable to obtain usable display bounds for server window: {}", LOG_WARN("Unable to obtain usable display bounds for server window: {}",
SDL_GetError()); SDL_GetError());
return false; return false;
@@ -533,8 +568,8 @@ bool PositionWindowAtBottomRight(slint::Window &window, float logical_width,
const float target_y = const float target_y =
static_cast<float>(usable_bounds.y) + static_cast<float>(usable_bounds.y) +
std::max(0.0f, static_cast<float>(usable_bounds.h) - logical_height); std::max(0.0f, static_cast<float>(usable_bounds.h) - logical_height);
window.set_position(slint::LogicalPosition( window.set_position(
slint::Point<float>{target_x, target_y})); slint::LogicalPosition(slint::Point<float>{target_x, target_y}));
return true; return true;
} }
@@ -585,11 +620,9 @@ void DragWindow(WindowHandle &component, int phase, float mouse_x,
float pointer_y = 0; float pointer_y = 0;
SDL_GetGlobalMouseState(&pointer_x, &pointer_y); SDL_GetGlobalMouseState(&pointer_x, &pointer_y);
const int delta_x = static_cast<int>(std::lround( const int delta_x = static_cast<int>(std::lround(
(pointer_x - state.pointer_start_x) * (pointer_x - state.pointer_start_x) * state.global_to_physical_scale));
state.global_to_physical_scale));
const int delta_y = static_cast<int>(std::lround( const int delta_y = static_cast<int>(std::lround(
(pointer_y - state.pointer_start_y) * (pointer_y - state.pointer_start_y) * state.global_to_physical_scale));
state.global_to_physical_scale));
const slint::PhysicalPosition target(slint::Point<int32_t>{ const slint::PhysicalPosition target(slint::Point<int32_t>{
state.window_start.x + delta_x, state.window_start.y + delta_y}); state.window_start.x + delta_x, state.window_start.y + delta_y});
if (target.x != state.last_target.x || target.y != state.last_target.y) { if (target.x != state.last_target.x || target.y != state.last_target.y) {
@@ -602,14 +635,13 @@ void DragWindow(WindowHandle &component, int phase, float mouse_x,
// Retain the previous behavior for backends without global pointer access. // Retain the previous behavior for backends without global pointer access.
const float scale = window.scale_factor(); const float scale = window.scale_factor();
const auto position = window.position(); const auto position = window.position();
const int delta_x = static_cast<int>( const int delta_x =
std::lround((mouse_x - state.last_local_x) * scale)); static_cast<int>(std::lround((mouse_x - state.last_local_x) * scale));
const int delta_y = static_cast<int>( const int delta_y =
std::lround((mouse_y - state.last_local_y) * scale)); static_cast<int>(std::lround((mouse_y - state.last_local_y) * scale));
if (delta_x != 0 || delta_y != 0) { if (delta_x != 0 || delta_y != 0) {
window.set_position( window.set_position(slint::PhysicalPosition(
slint::PhysicalPosition(slint::Point<int32_t>{ slint::Point<int32_t>{position.x + delta_x, position.y + delta_y}));
position.x + delta_x, position.y + delta_y}));
} }
state.last_local_x = mouse_x; state.last_local_x = mouse_x;
state.last_local_y = mouse_y; state.last_local_y = mouse_y;
@@ -619,6 +651,9 @@ void DragWindow(WindowHandle &component, int phase, float mouse_x,
struct GuiApplication::SlintUi { struct GuiApplication::SlintUi {
slint::ComponentHandle<ui::MainWindow> main = ui::MainWindow::create(); slint::ComponentHandle<ui::MainWindow> main = ui::MainWindow::create();
std::shared_ptr<slint::VectorModel<ui::ReleaseNoteBlock>>
release_note_blocks_model =
std::make_shared<slint::VectorModel<ui::ReleaseNoteBlock>>();
std::optional<slint::ComponentHandle<ui::StreamWindow>> stream; std::optional<slint::ComponentHandle<ui::StreamWindow>> stream;
std::optional<slint::ComponentHandle<ui::ServerWindow>> server; std::optional<slint::ComponentHandle<ui::ServerWindow>> server;
std::shared_ptr<slint::VectorModel<ui::RecentConnection>> recent_model = std::shared_ptr<slint::VectorModel<ui::RecentConnection>> recent_model =
@@ -633,7 +668,8 @@ struct GuiApplication::SlintUi {
std::make_shared<slint::VectorModel<ui::NetworkStatsRow>>(); std::make_shared<slint::VectorModel<ui::NetworkStatsRow>>();
std::shared_ptr<slint::VectorModel<ui::ControllerEntry>> controller_model = std::shared_ptr<slint::VectorModel<ui::ControllerEntry>> controller_model =
std::make_shared<slint::VectorModel<ui::ControllerEntry>>(); std::make_shared<slint::VectorModel<ui::ControllerEntry>>();
std::shared_ptr<slint::VectorModel<slint::SharedString>> controller_name_model = std::shared_ptr<slint::VectorModel<slint::SharedString>>
controller_name_model =
std::make_shared<slint::VectorModel<slint::SharedString>>(); std::make_shared<slint::VectorModel<slint::SharedString>>();
std::unordered_map<std::string, uint64_t> displayed_frame_sequence; std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
std::vector<std::string> tab_order; std::vector<std::string> tab_order;
@@ -674,8 +710,7 @@ int GuiApplication::Run() {
exec_log_path_ = path_manager_->GetLogPath().string(); exec_log_path_ = path_manager_->GetLogPath().string();
dll_log_path_ = exec_log_path_; dll_log_path_ = exec_log_path_;
cache_path_ = path_manager_->GetCachePath().string(); cache_path_ = path_manager_->GetCachePath().string();
config_center_ = config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
InitializeLogger(); InitializeLogger();
LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION); LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
@@ -701,8 +736,7 @@ int GuiApplication::Run() {
latest_version_ = version.empty() ? std::string{} : "v" + version; latest_version_ = version.empty() ? std::string{} : "v" + version;
if (latest_version_info_.contains("releaseNotes") && if (latest_version_info_.contains("releaseNotes") &&
latest_version_info_["releaseNotes"].is_string()) { latest_version_info_["releaseNotes"].is_string()) {
release_notes_ = release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
latest_version_info_["releaseNotes"].get<std::string>();
} }
if (latest_version_info_.contains("releaseName") && if (latest_version_info_.contains("releaseName") &&
latest_version_info_["releaseName"].is_string()) { latest_version_info_["releaseName"].is_string()) {
@@ -782,6 +816,16 @@ void GuiApplication::InitializeModules() {
void GuiApplication::InitializeUi() { void GuiApplication::InitializeUi() {
ui_ = std::make_unique<SlintUi>(); ui_ = std::make_unique<SlintUi>();
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()); RegisterFontAwesome(ui_->main->window());
#ifdef CROSSDESK_DEBUG #ifdef CROSSDESK_DEBUG
auto read_capture_override = [](const char* path) -> std::string { auto read_capture_override = [](const char* path) -> std::string {
@@ -798,13 +842,13 @@ void GuiApplication::InitializeUi() {
} }
std::string capture_language_override; std::string capture_language_override;
if (ui_->capture_mode) { if (ui_->capture_mode) {
if (const auto page = read_capture_override( if (const auto page =
"/tmp/crossdesk-ui-capture-page"); read_capture_override("/tmp/crossdesk-ui-capture-page");
!page.empty()) { !page.empty()) {
ui_->capture_page = page; ui_->capture_page = page;
} }
capture_language_override = read_capture_override( capture_language_override =
"/tmp/crossdesk-ui-capture-language"); read_capture_override("/tmp/crossdesk-ui-capture-language");
} }
const char* language = capture_language_override.empty() const char* language = capture_language_override.empty()
? std::getenv("CROSSDESK_UI_CAPTURE_LANGUAGE") ? std::getenv("CROSSDESK_UI_CAPTURE_LANGUAGE")
@@ -817,8 +861,8 @@ void GuiApplication::InitializeUi() {
localization_language_index_ = localization_language_index_ =
localization::detail::ClampLanguageIndex(capture_language); localization::detail::ClampLanguageIndex(capture_language);
language_button_value_ = localization_language_index_; language_button_value_ = localization_language_index_;
localization_language_ = static_cast<ConfigCenter::LANGUAGE>( localization_language_ =
localization_language_index_); static_cast<ConfigCenter::LANGUAGE>(localization_language_index_);
} }
} }
#endif #endif
@@ -889,7 +933,8 @@ void GuiApplication::InitializeUi() {
ui_->display_model->set_vector({slint::SharedString("Display 1")}); ui_->display_model->set_vector({slint::SharedString("Display 1")});
ui_->localized_language = -1; ui_->localized_language = -1;
UpdateLocalization(); UpdateLocalization();
(*ui_->stream)->set_status_text( (*ui_->stream)
->set_status_text(
UiText(localization::p2p_connected[localization_language_index_])); UiText(localization::p2p_connected[localization_language_index_]));
#ifdef CROSSDESK_DEBUG #ifdef CROSSDESK_DEBUG
// A bordered 16:9 frame makes stream scaling/cropping regressions visible // A bordered 16:9 frame makes stream scaling/cropping regressions visible
@@ -898,20 +943,16 @@ void GuiApplication::InitializeUi() {
constexpr int preview_height = 360; constexpr int preview_height = 360;
slint::SharedPixelBuffer<slint::Rgb8Pixel> preview_pixels(preview_width, slint::SharedPixelBuffer<slint::Rgb8Pixel> preview_pixels(preview_width,
preview_height); preview_height);
auto *preview_data = auto* preview_data = reinterpret_cast<uint8_t*>(preview_pixels.begin());
reinterpret_cast<uint8_t *>(preview_pixels.begin());
for (int y = 0; y < preview_height; ++y) { for (int y = 0; y < preview_height; ++y) {
for (int x = 0; x < preview_width; ++x) { for (int x = 0; x < preview_width; ++x) {
const bool border = x < 8 || y < 8 || x >= preview_width - 8 || const bool border =
y >= preview_height - 8; x < 8 || y < 8 || x >= preview_width - 8 || y >= preview_height - 8;
const size_t offset = const size_t offset = (static_cast<size_t>(y) * preview_width + x) * 3;
(static_cast<size_t>(y) * preview_width + x) * 3; preview_data[offset] =
preview_data[offset] = border ? 240 : static_cast<uint8_t>( border ? 240 : static_cast<uint8_t>(35 + 150 * x / preview_width);
35 + 150 * x / preview_data[offset + 1] =
preview_width); border ? 70 : static_cast<uint8_t>(35 + 150 * y / preview_height);
preview_data[offset + 1] = border ? 70 : static_cast<uint8_t>(
35 + 150 * y /
preview_height);
preview_data[offset + 2] = border ? 70 : 90; preview_data[offset + 2] = border ? 70 : 90;
} }
} }
@@ -934,22 +975,29 @@ void GuiApplication::InitializeUi() {
const float server_width = const float server_width =
ServerWindowLogicalWidth(localization_language_index_); ServerWindowLogicalWidth(localization_language_index_);
const float server_height = ServerWindowLogicalHeight(false); const float server_height = ServerWindowLogicalHeight(false);
(*ui_->server)->window().set_size(slint::LogicalSize( (*ui_->server)
->window()
.set_size(slint::LogicalSize(
slint::Size<float>{server_width, server_height})); slint::Size<float>{server_width, server_height}));
ui::ControllerEntry preview_controller; ui::ControllerEntry preview_controller;
preview_controller.remote_id = "589173341"; preview_controller.remote_id = "589173341";
preview_controller.display_name = "Mac"; preview_controller.display_name = "Mac";
ui_->controller_model->set_vector({preview_controller}); ui_->controller_model->set_vector({preview_controller});
ui_->controller_name_model->set_vector({slint::SharedString("Mac")}); ui_->controller_name_model->set_vector({slint::SharedString("Mac")});
(*ui_->server)->set_controller_label( (*ui_->server)
->set_controller_label(
UiText(localization::controller[localization_language_index_])); UiText(localization::controller[localization_language_index_]));
(*ui_->server)->set_connection_label( (*ui_->server)
UiText(localization::connection_status[localization_language_index_])); ->set_connection_label(UiText(
(*ui_->server)->set_connection_status( localization::connection_status[localization_language_index_]));
(*ui_->server)
->set_connection_status(
UiText(localization::p2p_connected[localization_language_index_])); UiText(localization::p2p_connected[localization_language_index_]));
(*ui_->server)->set_file_transfer_label( (*ui_->server)
->set_file_transfer_label(
UiText(localization::file_transfer[localization_language_index_])); UiText(localization::file_transfer[localization_language_index_]));
(*ui_->server)->set_select_file_label( (*ui_->server)
->set_select_file_label(
UiText(localization::select_file[localization_language_index_])); UiText(localization::select_file[localization_language_index_]));
BindServerCallbacks(); BindServerCallbacks();
(*ui_->server)->show(); (*ui_->server)->show();
@@ -1002,9 +1050,8 @@ void GuiApplication::InitializeSystemTray() {
LoadSlintTrayIcon(), L"CrossDesk", localization_language_index_); LoadSlintTrayIcon(), L"CrossDesk", localization_language_index_);
#elif defined(__APPLE__) #elif defined(__APPLE__)
ui_->tray = std::make_unique<MacTray>( ui_->tray = std::make_unique<MacTray>(
std::move(show_window), std::move(hide_window), std::move(show_window), std::move(hide_window), std::move(open_settings),
std::move(open_settings), std::move(exit_app), "CrossDesk", std::move(exit_app), "CrossDesk", localization_language_index_);
localization_language_index_);
#elif defined(__linux__) #elif defined(__linux__)
ui_->tray = std::make_unique<LinuxTray>( ui_->tray = std::make_unique<LinuxTray>(
std::move(show_window), std::move(hide_window), std::move(exit_app), std::move(show_window), std::move(hide_window), std::move(exit_app),
@@ -1106,15 +1153,13 @@ void GuiApplication::BindMainCallbacks() {
} }
return true; return true;
}); });
main->on_connect_requested([this](slint::SharedString id) { main->on_connect_requested(
ConnectFromUi(std::string(id)); [this](slint::SharedString id) { ConnectFromUi(std::string(id)); });
});
main->on_format_remote_id([](slint::SharedString value) { main->on_format_remote_id([](slint::SharedString value) {
return UiText(FormatRemotePeerIdInput(std::string_view(value))); return UiText(FormatRemotePeerIdInput(std::string_view(value)));
}); });
main->on_recent_connect([this](slint::SharedString id) { main->on_recent_connect(
ConnectFromUi(std::string(id)); [this](slint::SharedString id) { ConnectFromUi(std::string(id)); });
});
main->on_recent_edit_alias( main->on_recent_edit_alias(
[this](slint::SharedString id, slint::SharedString alias) { [this](slint::SharedString id, slint::SharedString alias) {
const std::string remote_id(id); const std::string remote_id(id);
@@ -1143,7 +1188,8 @@ void GuiApplication::BindMainCallbacks() {
const char* folder = tinyfd_selectFolderDialog( const char* folder = tinyfd_selectFolderDialog(
localization::file_transfer_save_path[localization_language_index_] localization::file_transfer_save_path[localization_language_index_]
.c_str(), .c_str(),
file_transfer_save_path_buf_[0] ? file_transfer_save_path_buf_ : nullptr); file_transfer_save_path_buf_[0] ? file_transfer_save_path_buf_
: nullptr);
if (folder) { if (folder) {
ui_->main->set_file_save_path(folder); ui_->main->set_file_save_path(folder);
} }
@@ -1172,8 +1218,8 @@ void GuiApplication::BindMainCallbacks() {
} }
if (coturn_port) { if (coturn_port) {
config_center_->SetCoturnServerPort(*coturn_port); config_center_->SetCoturnServerPort(*coturn_port);
std::snprintf(coturn_server_port_self_, std::snprintf(coturn_server_port_self_, sizeof(coturn_server_port_self_),
sizeof(coturn_server_port_self_), "%d", *coturn_port); "%d", *coturn_port);
std::snprintf(coturn_server_port_, sizeof(coturn_server_port_), "%d", std::snprintf(coturn_server_port_, sizeof(coturn_server_port_), "%d",
*coturn_port); *coturn_port);
} }
@@ -1183,11 +1229,11 @@ void GuiApplication::BindMainCallbacks() {
ui_->main->set_server_host(UiText(host)); ui_->main->set_server_host(UiText(host));
const int signal_port = config_center_->GetSignalServerPort(); const int signal_port = config_center_->GetSignalServerPort();
const int coturn_port = config_center_->GetCoturnServerPort(); const int coturn_port = config_center_->GetCoturnServerPort();
ui_->main->set_server_port( ui_->main->set_server_port(signal_port > 0
signal_port > 0 ? UiText(std::to_string(signal_port)) ? UiText(std::to_string(signal_port))
: slint::SharedString{}); : slint::SharedString{});
ui_->main->set_coturn_port( ui_->main->set_coturn_port(coturn_port > 0
coturn_port > 0 ? UiText(std::to_string(coturn_port)) ? UiText(std::to_string(coturn_port))
: slint::SharedString{}); : slint::SharedString{});
}); });
main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); }); main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); });
@@ -1219,8 +1265,8 @@ void GuiApplication::BindMainCallbacks() {
} }
show_connection_status_window_ = false; show_connection_status_window_ = false;
}); });
main->on_connection_submit_password( main->on_connection_submit_password([this](slint::SharedString value,
[this](slint::SharedString value, bool remember) { bool remember) {
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id); const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
const std::string password(value); const std::string password(value);
if (!props || password.empty() || password.size() > 6) { if (!props || password.empty() || password.size() > 6) {
@@ -1308,9 +1354,8 @@ void GuiApplication::BindStreamCallbacks() {
stream->on_reorder_tab([this](int from, float drop_x, float tab_width) { stream->on_reorder_tab([this](int from, float drop_x, float tab_width) {
ReorderStreamTab(from, drop_x, tab_width); ReorderStreamTab(from, drop_x, tab_width);
}); });
stream->on_close_tab([this](slint::SharedString id) { stream->on_close_tab(
CloseStreamTab(std::string(id)); [this](slint::SharedString id) { CloseStreamTab(std::string(id)); });
});
stream->on_switch_display([this](int index) { stream->on_switch_display([this](int index) {
auto props = SelectedSession(); auto props = SelectedSession();
if (!props || index < 0 || if (!props || index < 0 ||
@@ -1379,9 +1424,8 @@ void GuiApplication::BindStreamCallbacks() {
props->file_transfer_.file_transfer_window_visible_ = false; props->file_transfer_.file_transfer_window_visible_ = false;
} }
}); });
stream->on_can_drop_file([](const slint::DataTransfer &data) { stream->on_can_drop_file(
return data.has_plain_text(); [](const slint::DataTransfer& data) { return data.has_plain_text(); });
});
stream->on_file_dropped([this](const slint::DataTransfer& data) { stream->on_file_dropped([this](const slint::DataTransfer& data) {
const auto text = data.plain_text(); const auto text = data.plain_text();
auto props = SelectedSession(); auto props = SelectedSession();
@@ -1436,9 +1480,8 @@ void GuiApplication::BindStreamCallbacks() {
stream->on_scroll_input([this](float dx, float dy, float x, float y) { stream->on_scroll_input([this](float dx, float dy, float x, float y) {
SendScrollInput(dx, dy, x, y); SendScrollInput(dx, dy, x, y);
}); });
stream->on_key_input( stream->on_key_input([this](slint::SharedString text, bool pressed,
[this](slint::SharedString text, bool pressed, bool control, bool alt, bool control, bool alt, bool shift, bool meta) {
bool shift, bool meta) {
SendKeyInput(std::string(text), pressed, control, alt, shift, meta); SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
}); });
} }
@@ -1456,9 +1499,10 @@ void GuiApplication::BindServerCallbacks() {
server_window_collapsed_ = collapsed; server_window_collapsed_ = collapsed;
const int language = (*ui_->server)->get_language_index(); const int language = (*ui_->server)->get_language_index();
const float width = ServerWindowLogicalWidth(language); const float width = ServerWindowLogicalWidth(language);
(*ui_->server)->window().set_size( (*ui_->server)
slint::LogicalSize(slint::Size<float>{ ->window()
width, ServerWindowLogicalHeight(collapsed)})); .set_size(slint::LogicalSize(
slint::Size<float>{width, ServerWindowLogicalHeight(collapsed)}));
}); });
server->on_controller_selected([this](int index) { server->on_controller_selected([this](int index) {
if (index < 0 || index >= static_cast<int>(ui_->controller_ids.size())) { if (index < 0 || index >= static_cast<int>(ui_->controller_ids.size())) {
@@ -1580,7 +1624,8 @@ void GuiApplication::UpdateLocalization() {
strings.set_ok(UiText(localization::ok[language])); strings.set_ok(UiText(localization::ok[language]));
strings.set_cancel(UiText(localization::cancel[language])); strings.set_cancel(UiText(localization::cancel[language]));
strings.set_new_password(UiText(localization::new_password[language])); strings.set_new_password(UiText(localization::new_password[language]));
strings.set_invalid_password(UiText(localization::max_password_len[language])); strings.set_invalid_password(
UiText(localization::max_password_len[language]));
strings.set_edit_alias( strings.set_edit_alias(
UiText(localization::input_connection_alias[language])); UiText(localization::input_connection_alias[language]));
strings.set_delete_connection( strings.set_delete_connection(
@@ -1618,6 +1663,7 @@ void GuiApplication::UpdateLocalization() {
strings.set_tls_error(UiText(localization::signal_tls_cert_error[language])); strings.set_tls_error(UiText(localization::signal_tls_cert_error[language]));
strings.set_update_available( strings.set_update_available(
UiText(localization::new_version_available[language])); UiText(localization::new_version_available[language]));
strings.set_release_notes(UiText(localization::release_notes[language]));
strings.set_download(UiText(localization::update[language])); strings.set_download(UiText(localization::update[language]));
strings.set_input_password(UiText(localization::input_password[language])); strings.set_input_password(UiText(localization::input_password[language]));
strings.set_reinput_password( strings.set_reinput_password(
@@ -1647,8 +1693,8 @@ void GuiApplication::UpdateLocalization() {
strings.set_do_not_remind( strings.set_do_not_remind(
UiText(localization::do_not_remind_again[language])); UiText(localization::do_not_remind_again[language]));
strings.set_notification(UiText(localization::notification[language])); strings.set_notification(UiText(localization::notification[language]));
strings.set_service_suppressed_message( strings.set_service_suppressed_message(UiText(
UiText(localization::windows_service_prompt_suppressed_message[language])); localization::windows_service_prompt_suppressed_message[language]));
strings.set_quality_low(UiText(localization::video_quality_low[language])); strings.set_quality_low(UiText(localization::video_quality_low[language]));
strings.set_quality_medium( strings.set_quality_medium(
UiText(localization::video_quality_medium[language])); UiText(localization::video_quality_medium[language]));
@@ -1671,8 +1717,7 @@ void GuiApplication::UpdateLocalization() {
UiText(localization::release_mouse[language])); UiText(localization::release_mouse[language]));
stream_strings.set_audio(UiText(localization::audio_capture[language])); stream_strings.set_audio(UiText(localization::audio_capture[language]));
stream_strings.set_mute(UiText(localization::mute[language])); stream_strings.set_mute(UiText(localization::mute[language]));
stream_strings.set_select_file( stream_strings.set_select_file(UiText(localization::select_file[language]));
UiText(localization::select_file[language]));
stream_strings.set_show_stats( stream_strings.set_show_stats(
UiText(localization::show_net_traffic_stats[language])); UiText(localization::show_net_traffic_stats[language]));
stream_strings.set_hide_stats( stream_strings.set_hide_stats(
@@ -1680,8 +1725,7 @@ void GuiApplication::UpdateLocalization() {
stream_strings.set_fullscreen(UiText(localization::fullscreen[language])); stream_strings.set_fullscreen(UiText(localization::fullscreen[language]));
stream_strings.set_exit_fullscreen( stream_strings.set_exit_fullscreen(
UiText(localization::exit_fullscreen[language])); UiText(localization::exit_fullscreen[language]));
stream_strings.set_disconnect( stream_strings.set_disconnect(UiText(localization::disconnect[language]));
UiText(localization::disconnect[language]));
stream_strings.set_file_transfer( stream_strings.set_file_transfer(
UiText(localization::file_transfer_progress[language])); UiText(localization::file_transfer_progress[language]));
stream_strings.set_expand_control( stream_strings.set_expand_control(
@@ -1700,8 +1744,7 @@ void GuiApplication::UpdateLocalization() {
auto& main_stream_strings = ui_->main->global<ui::StreamStrings>(); auto& main_stream_strings = ui_->main->global<ui::StreamStrings>();
apply_stream_strings(main_stream_strings); apply_stream_strings(main_stream_strings);
if (ui_->stream) { if (ui_->stream) {
auto &active_stream_strings = auto& active_stream_strings = (*ui_->stream)->global<ui::StreamStrings>();
(*ui_->stream)->global<ui::StreamStrings>();
apply_stream_strings(active_stream_strings); apply_stream_strings(active_stream_strings);
} }
ui_->localized_language = language; ui_->localized_language = language;
@@ -1728,13 +1771,12 @@ void GuiApplication::SyncMainWindow() {
ui_->main->set_local_password(password_saved_); ui_->main->set_local_password(password_saved_);
ui_->main->set_password_visible(show_password_); ui_->main->set_password_visible(show_password_);
ui_->main->set_signal_connected(signal_connected_); ui_->main->set_signal_connected(signal_connected_);
ui_->main->set_signal_tls_error( ui_->main->set_signal_tls_error(signal_status_ ==
signal_status_ == SignalStatus::SignalTlsCertError); SignalStatus::SignalTlsCertError);
ui_->main->set_update_available(update_available_); ui_->main->set_update_available(update_available_);
ui_->main->set_current_version(CROSSDESK_VERSION); ui_->main->set_current_version(CROSSDESK_VERSION);
ui_->main->set_latest_version(UiText(latest_version_)); ui_->main->set_latest_version(UiText(latest_version_));
ui_->main->set_release_name(UiText(release_name_)); ui_->main->set_release_name(UiText(release_name_));
ui_->main->set_release_notes(UiText(CleanReleaseNotesForUi(release_notes_)));
ui_->main->set_release_date(UiText(release_date_)); ui_->main->set_release_date(UiText(release_date_));
ui_->main->set_settings_session_active(stream_window_inited_); ui_->main->set_settings_session_active(stream_window_inited_);
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
@@ -1873,8 +1915,8 @@ void GuiApplication::SyncPlatformDialogs() {
state == PortableServiceInstallState::succeeded); state == PortableServiceInstallState::succeeded);
const std::string* status = nullptr; const std::string* status = nullptr;
if (state == PortableServiceInstallState::installing) { if (state == PortableServiceInstallState::installing) {
status = &localization::installing_windows_service status =
[localization_language_index_]; &localization::installing_windows_service[localization_language_index_];
} else if (state == PortableServiceInstallState::succeeded) { } else if (state == PortableServiceInstallState::succeeded) {
status = &localization::windows_service_install_success status = &localization::windows_service_install_success
[localization_language_index_]; [localization_language_index_];
@@ -1882,8 +1924,8 @@ void GuiApplication::SyncPlatformDialogs() {
status = &localization::windows_service_install_failed status = &localization::windows_service_install_failed
[localization_language_index_]; [localization_language_index_];
} }
ui_->main->set_portable_service_status( ui_->main->set_portable_service_status(status ? UiText(*status)
status ? UiText(*status) : slint::SharedString{}); : slint::SharedString{});
if (show_portable_service_install_window_) { if (show_portable_service_install_window_) {
if (!ui_->portable_service_dialog_initialized) { if (!ui_->portable_service_dialog_initialized) {
ui_->main->set_portable_service_do_not_remind( ui_->main->set_portable_service_do_not_remind(
@@ -1907,14 +1949,12 @@ void GuiApplication::SyncPlatformDialogs() {
return; return;
} }
RefreshMacPermissionStatus(false); RefreshMacPermissionStatus(false);
show_request_permission_window_ = show_request_permission_window_ = !mac_screen_recording_permission_granted_ ||
!mac_screen_recording_permission_granted_ ||
!mac_accessibility_permission_granted_; !mac_accessibility_permission_granted_;
ui_->main->set_permission_dialog_open(show_request_permission_window_); ui_->main->set_permission_dialog_open(show_request_permission_window_);
ui_->main->set_screen_recording_granted( ui_->main->set_screen_recording_granted(
mac_screen_recording_permission_granted_); mac_screen_recording_permission_granted_);
ui_->main->set_accessibility_granted( ui_->main->set_accessibility_granted(mac_accessibility_permission_granted_);
mac_accessibility_permission_granted_);
#else #else
ui_->main->set_permission_dialog_open(false); ui_->main->set_permission_dialog_open(false);
#endif #endif
@@ -1999,11 +2039,10 @@ void GuiApplication::SyncStreamWindow() {
} }
ui::StreamTab tab; ui::StreamTab tab;
tab.remote_id = UiText(id); tab.remote_id = UiText(id);
tab.title = UiText(props->remote_host_name_.empty() tab.title = UiText(
? id props->remote_host_name_.empty() ? id : props->remote_host_name_);
: props->remote_host_name_); tab.connected =
tab.connected = props->connection_status_.load() == props->connection_status_.load() == ConnectionStatus::Connected;
ConnectionStatus::Connected;
tabs_by_id.emplace(id, std::move(tab)); tabs_by_id.emplace(id, std::move(tab));
} }
} }
@@ -2053,12 +2092,14 @@ void GuiApplication::SyncStreamWindow() {
// Once the peer is connected, the frame-waiting hint replaces the // Once the peer is connected, the frame-waiting hint replaces the
// connection status. Keeping these states mutually exclusive also avoids a // connection status. Keeping these states mutually exclusive also avoids a
// transient overlap when both properties are synchronized in one tick. // transient overlap when both properties are synchronized in one tick.
(*ui_->stream)->set_status_text(UiText( (*ui_->stream)
->set_status_text(UiText(
status == ConnectionStatus::Connected status == ConnectionStatus::Connected
? std::string{} ? std::string{}
: ConnectionStatusText(status, localization_language_index_))); : ConnectionStatusText(status, localization_language_index_)));
(*ui_->stream)->set_receiving_text( (*ui_->stream)
UiText(status == ConnectionStatus::Connected ->set_receiving_text(UiText(
status == ConnectionStatus::Connected
? localization::receiving_screen[localization_language_index_] ? localization::receiving_screen[localization_language_index_]
: std::string{})); : std::string{}));
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_); (*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
@@ -2069,8 +2110,7 @@ void GuiApplication::SyncStreamWindow() {
fullscreen_button_pressed_ = (*ui_->stream)->window().is_fullscreen(); fullscreen_button_pressed_ = (*ui_->stream)->window().is_fullscreen();
#endif #endif
(*ui_->stream)->set_fullscreen_enabled(fullscreen_button_pressed_); (*ui_->stream)->set_fullscreen_enabled(fullscreen_button_pressed_);
(*ui_->stream)->set_stats_visible( (*ui_->stream)->set_stats_visible(props->net_traffic_stats_button_pressed_);
props->net_traffic_stats_button_pressed_);
std::vector<slint::SharedString> displays; std::vector<slint::SharedString> displays;
displays.reserve(props->display_info_list_.size()); displays.reserve(props->display_info_list_.size());
@@ -2084,8 +2124,7 @@ void GuiApplication::SyncStreamWindow() {
std::vector<ui::NetworkStatsRow> stats_rows; std::vector<ui::NetworkStatsRow> stats_rows;
stats_rows.reserve(4); stats_rows.reserve(4);
const auto append_stats_row = [&](const std::string& label, const auto append_stats_row = [&](const std::string& label,
const auto &inbound, const auto& inbound, const auto& outbound) {
const auto &outbound) {
ui::NetworkStatsRow row; ui::NetworkStatsRow row;
row.label = UiText(label); row.label = UiText(label);
row.inbound = UiText(FormatBitrate(inbound.bitrate)); row.inbound = UiText(FormatBitrate(inbound.bitrate));
@@ -2103,13 +2142,16 @@ void GuiApplication::SyncStreamWindow() {
net.total_inbound_stats, net.total_outbound_stats); net.total_inbound_stats, net.total_outbound_stats);
ui_->stats_model->set_vector(std::move(stats_rows)); ui_->stats_model->set_vector(std::move(stats_rows));
(*ui_->stream)->set_stats_fps(UiText(std::to_string(props->fps_))); (*ui_->stream)->set_stats_fps(UiText(std::to_string(props->fps_)));
(*ui_->stream)->set_stats_resolution(UiText( (*ui_->stream)
std::to_string(props->video_width_) + "x" + ->set_stats_resolution(UiText(std::to_string(props->video_width_) + "x" +
std::to_string(props->video_height_))); std::to_string(props->video_height_)));
(*ui_->stream)->set_stats_connection_mode(UiText( (*ui_->stream)
props->traversal_mode_ == TraversalMode::P2P ->set_stats_connection_mode(
? localization::connection_mode_direct[localization_language_index_] UiText(props->traversal_mode_ == TraversalMode::P2P
: localization::connection_mode_relay[localization_language_index_])); ? localization::connection_mode_direct
[localization_language_index_]
: localization::connection_mode_relay
[localization_language_index_]));
std::vector<RemoteSession::FileTransferInfo> file_list; std::vector<RemoteSession::FileTransferInfo> file_list;
{ {
@@ -2146,15 +2188,14 @@ void GuiApplication::SyncStreamWindow() {
if (status_index == 1) if (status_index == 1)
item.status = UiText(localization::sending[localization_language_index_]); item.status = UiText(localization::sending[localization_language_index_]);
if (status_index == 2) if (status_index == 2)
item.status = UiText(localization::completed[localization_language_index_]); item.status =
UiText(localization::completed[localization_language_index_]);
if (status_index == 3) if (status_index == 3)
item.status = UiText(localization::failed[localization_language_index_]); item.status = UiText(localization::failed[localization_language_index_]);
item.progress = status_index == 2 item.progress = status_index == 2 ? 1.0f
? 1.0f
: info.file_size == 0 : info.file_size == 0
? 0.0f ? 0.0f
: std::clamp( : std::clamp(static_cast<float>(info.sent_bytes) /
static_cast<float>(info.sent_bytes) /
static_cast<float>(info.file_size), static_cast<float>(info.file_size),
0.0f, 1.0f); 0.0f, 1.0f);
item.speed = UiText(FormatTransferRate(info.rate_bps)); item.speed = UiText(FormatTransferRate(info.rate_bps));
@@ -2162,7 +2203,8 @@ void GuiApplication::SyncStreamWindow() {
transfers.push_back(std::move(item)); transfers.push_back(std::move(item));
} }
ui_->transfer_model->set_vector(std::move(transfers)); ui_->transfer_model->set_vector(std::move(transfers));
(*ui_->stream)->set_file_transfer_visible( (*ui_->stream)
->set_file_transfer_visible(
props->file_transfer_.file_transfer_window_visible_); props->file_transfer_.file_transfer_window_visible_);
std::shared_ptr<std::vector<unsigned char>> frame; std::shared_ptr<std::vector<unsigned char>> frame;
@@ -2181,9 +2223,9 @@ void GuiApplication::SyncStreamWindow() {
ui_->displayed_frame_sequence[props->remote_id_] != sequence) { ui_->displayed_frame_sequence[props->remote_id_] != sequence) {
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(width, height); slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(width, height);
const int result = libyuv::NV12ToRAW( const int result = libyuv::NV12ToRAW(
frame->data(), width, frame->data() + static_cast<size_t>(width) * height, frame->data(), width,
width, reinterpret_cast<uint8_t *>(pixels.begin()), width * 3, width, frame->data() + static_cast<size_t>(width) * height, width,
height); reinterpret_cast<uint8_t*>(pixels.begin()), width * 3, width, height);
if (result == 0) { if (result == 0) {
(*ui_->stream)->set_frame(slint::Image(std::move(pixels))); (*ui_->stream)->set_frame(slint::Image(std::move(pixels)));
(*ui_->stream)->set_has_frame(true); (*ui_->stream)->set_has_frame(true);
@@ -2203,7 +2245,9 @@ void GuiApplication::SyncServerWindow() {
(*ui_->server)->set_controller_names(ui_->controller_name_model); (*ui_->server)->set_controller_names(ui_->controller_name_model);
(*ui_->server)->set_language_index(localization_language_index_); (*ui_->server)->set_language_index(localization_language_index_);
server_window_collapsed_ = false; server_window_collapsed_ = false;
(*ui_->server)->window().set_size(slint::LogicalSize(slint::Size<float>{ (*ui_->server)
->window()
.set_size(slint::LogicalSize(slint::Size<float>{
ServerWindowLogicalWidth(localization_language_index_), ServerWindowLogicalWidth(localization_language_index_),
ServerWindowLogicalHeight(false)})); ServerWindowLogicalHeight(false)}));
BindServerCallbacks(); BindServerCallbacks();
@@ -2267,7 +2311,9 @@ void GuiApplication::SyncServerWindow() {
ServerWindowLogicalWidth(localization_language_index_); ServerWindowLogicalWidth(localization_language_index_);
const float server_height = const float server_height =
ServerWindowLogicalHeight(server_window_collapsed_); ServerWindowLogicalHeight(server_window_collapsed_);
(*ui_->server)->window().set_size(slint::LogicalSize( (*ui_->server)
->window()
.set_size(slint::LogicalSize(
slint::Size<float>{server_width, server_height})); slint::Size<float>{server_width, server_height}));
if (PositionWindowAtBottomRight((*ui_->server)->window(), server_width, if (PositionWindowAtBottomRight((*ui_->server)->window(), server_width,
server_height)) { server_height)) {
@@ -2276,13 +2322,17 @@ void GuiApplication::SyncServerWindow() {
ui_->server_initial_position_attempts = 0; ui_->server_initial_position_attempts = 0;
} }
} }
(*ui_->server)->set_controller_label( (*ui_->server)
->set_controller_label(
UiText(localization::controller[localization_language_index_])); UiText(localization::controller[localization_language_index_]));
(*ui_->server)->set_connection_label( (*ui_->server)
UiText(localization::connection_status[localization_language_index_])); ->set_connection_label(UiText(
(*ui_->server)->set_file_transfer_label( localization::connection_status[localization_language_index_]));
(*ui_->server)
->set_file_transfer_label(
UiText(localization::file_transfer[localization_language_index_])); UiText(localization::file_transfer[localization_language_index_]));
(*ui_->server)->set_select_file_label( (*ui_->server)
->set_select_file_label(
UiText(localization::select_file[localization_language_index_])); UiText(localization::select_file[localization_language_index_]));
ConnectionStatus status = ConnectionStatus::Closed; ConnectionStatus status = ConnectionStatus::Closed;
@@ -2293,21 +2343,23 @@ void GuiApplication::SyncServerWindow() {
status = found->second; status = found->second;
} }
} }
(*ui_->server)->set_connection_status(UiText( (*ui_->server)
ConnectionStatusText(status, localization_language_index_))); ->set_connection_status(
UiText(ConnectionStatusText(status, localization_language_index_)));
auto& transfer = transfers_.global_state(); auto& transfer = transfers_.global_state();
const auto sent = transfer.file_sent_bytes_.load(); const auto sent = transfer.file_sent_bytes_.load();
const auto total = transfer.file_total_bytes_.load(); const auto total = transfer.file_total_bytes_.load();
(*ui_->server)->set_file_transfer_visible( (*ui_->server)
transfer.file_transfer_window_visible_); ->set_file_transfer_visible(transfer.file_transfer_window_visible_);
(*ui_->server)->set_sending_file(transfer.file_sending_.load()); (*ui_->server)->set_sending_file(transfer.file_sending_.load());
(*ui_->server)->set_file_progress( (*ui_->server)
total == 0 ? 0.0f ->set_file_progress(total == 0 ? 0.0f
: std::clamp(static_cast<float>(sent) / : std::clamp(static_cast<float>(sent) /
static_cast<float>(total), static_cast<float>(total),
0.0f, 1.0f)); 0.0f, 1.0f));
(*ui_->server)->set_file_progress_text( (*ui_->server)
->set_file_progress_text(
UiText(FormatTransferRate(transfer.file_send_rate_bps_.load()))); UiText(FormatTransferRate(transfer.file_send_rate_bps_.load())));
std::string current_file_name; std::string current_file_name;
const uint32_t current_file_id = transfer.current_file_id_.load(); const uint32_t current_file_id = transfer.current_file_id_.load();
@@ -2321,22 +2373,21 @@ void GuiApplication::SyncServerWindow() {
} }
} }
(*ui_->server)->set_current_file_name(UiText(current_file_name)); (*ui_->server)->set_current_file_name(UiText(current_file_name));
(*ui_->server)->set_file_size_text( (*ui_->server)
total == 0 ? slint::SharedString{} ->set_file_size_text(total == 0 ? slint::SharedString{}
: UiText(FormatFileSize(sent) + " / " + : UiText(FormatFileSize(sent) + " / " +
FormatFileSize(total))); FormatFileSize(total)));
} }
void GuiApplication::SaveSettingsFromUi() { void GuiApplication::SaveSettingsFromUi() {
auto& main = ui_->main; auto& main = ui_->main;
language_button_value_ = localization::detail::ClampLanguageIndex( language_button_value_ =
main->get_language_index()); localization::detail::ClampLanguageIndex(main->get_language_index());
video_quality_button_value_ = video_quality_button_value_ =
std::clamp(main->get_video_quality_index(), 0, 2); std::clamp(main->get_video_quality_index(), 0, 2);
video_frame_rate_button_value_ = video_frame_rate_button_value_ =
std::clamp(main->get_frame_rate_index(), 0, 1); std::clamp(main->get_frame_rate_index(), 0, 1);
video_encode_format_button_value_ = video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
std::clamp(main->get_codec_index(), 0, 1);
enable_hardware_video_codec_ = main->get_hardware_codec_enabled(); enable_hardware_video_codec_ = main->get_hardware_codec_enabled();
enable_turn_ = main->get_turn_enabled(); enable_turn_ = main->get_turn_enabled();
enable_srtp_ = main->get_srtp_enabled(); enable_srtp_ = main->get_srtp_enabled();
@@ -2385,8 +2436,7 @@ void GuiApplication::SaveSettingsFromUi() {
language_button_value_last_ = language_button_value_; language_button_value_last_ = language_button_value_;
video_quality_button_value_last_ = video_quality_button_value_; video_quality_button_value_last_ = video_quality_button_value_;
video_frame_rate_button_value_last_ = video_frame_rate_button_value_; video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
video_encode_format_button_value_last_ = video_encode_format_button_value_last_ = video_encode_format_button_value_;
video_encode_format_button_value_;
enable_hardware_video_codec_last_ = enable_hardware_video_codec_; enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
enable_turn_last_ = enable_turn_; enable_turn_last_ = enable_turn_;
enable_srtp_last_ = enable_srtp_; enable_srtp_last_ = enable_srtp_;
@@ -2439,15 +2489,14 @@ void GuiApplication::SelectStreamTab(int index) {
} }
} }
void GuiApplication::ReorderStreamTab(int from, float drop_x, void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
float tab_width) { if (!ui_ || from < 0 || from >= static_cast<int>(ui_->tab_order.size()) ||
if (!ui_ || from < 0 || tab_width <= 0.0f) {
from >= static_cast<int>(ui_->tab_order.size()) || tab_width <= 0.0f) {
return; return;
} }
const float slot_width = tab_width + 1.0f; const float slot_width = tab_width + 1.0f;
const int target = std::clamp(static_cast<int>(std::floor(drop_x / slot_width)), const int target =
0, std::clamp(static_cast<int>(std::floor(drop_x / slot_width)), 0,
static_cast<int>(ui_->tab_order.size()) - 1); static_cast<int>(ui_->tab_order.size()) - 1);
if (target == from) { if (target == from) {
return; return;
@@ -2513,8 +2562,7 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
const float scale = (*ui_->stream)->window().scale_factor(); const float scale = (*ui_->stream)->window().scale_factor();
const float available_width = size.width / scale; const float available_width = size.width / scale;
const float available_height = const float available_height =
size.height / scale - size.height / scale - (fullscreen_button_pressed_
(fullscreen_button_pressed_
? 0.0f ? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f)); : (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
float render_width = available_width; float render_width = available_width;
@@ -2548,15 +2596,23 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
if (kind == 3) { if (kind == 3) {
action.m.flag = MouseFlag::move; action.m.flag = MouseFlag::move;
} else if (kind == 1) { } else if (kind == 1) {
if (button == 1) action.m.flag = MouseFlag::left_down; if (button == 1)
else if (button == 2) action.m.flag = MouseFlag::right_down; action.m.flag = MouseFlag::left_down;
else if (button == 3) action.m.flag = MouseFlag::middle_down; else if (button == 2)
else return; action.m.flag = MouseFlag::right_down;
else if (button == 3)
action.m.flag = MouseFlag::middle_down;
else
return;
} else if (kind == 2) { } else if (kind == 2) {
if (button == 1) action.m.flag = MouseFlag::left_up; if (button == 1)
else if (button == 2) action.m.flag = MouseFlag::right_up; action.m.flag = MouseFlag::left_up;
else if (button == 3) action.m.flag = MouseFlag::middle_up; else if (button == 2)
else return; action.m.flag = MouseFlag::right_up;
else if (button == 3)
action.m.flag = MouseFlag::middle_up;
else
return;
} else { } else {
return; return;
} }
@@ -2577,8 +2633,7 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
const float scale = (*ui_->stream)->window().scale_factor(); const float scale = (*ui_->stream)->window().scale_factor();
const float width = size.width / scale; const float width = size.width / scale;
const float height = const float height =
size.height / scale - size.height / scale - (fullscreen_button_pressed_
(fullscreen_button_pressed_
? 0.0f ? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f)); : (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
if (width <= 0 || height <= 0) { if (width <= 0 || height <= 0) {
@@ -179,6 +179,7 @@ struct TranslationRow {
X(notification, u8"通知", "Notification", u8"Уведомление") \ X(notification, u8"通知", "Notification", u8"Уведомление") \
X(new_version_available, u8"新版本可用", "New Version Available", \ X(new_version_available, u8"新版本可用", "New Version Available", \
u8"Доступна новая версия") \ u8"Доступна новая версия") \
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
X(version, u8"版本", "Version", u8"Версия") \ X(version, u8"版本", "Version", u8"Версия") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \ X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
X(access_website, u8"访问官网: ", \ X(access_website, u8"访问官网: ", \
+52 -23
View File
@@ -9,6 +9,11 @@ export struct RecentConnection {
thumbnail: image, thumbnail: image,
} }
export struct ReleaseNoteBlock {
content: styled-text,
section-gap: bool,
}
export global UiStrings { export global UiStrings {
in-out property <string> local-desktop: "Local Desktop"; in-out property <string> local-desktop: "Local Desktop";
in-out property <string> remote-desktop: "Remote Desktop"; in-out property <string> remote-desktop: "Remote Desktop";
@@ -48,8 +53,8 @@ export global UiStrings {
in-out property <string> server-port: "Signal port"; in-out property <string> server-port: "Signal port";
in-out property <string> coturn-port: "TURN port"; in-out property <string> coturn-port: "TURN port";
in-out property <string> version: "Version"; in-out property <string> version: "Version";
in-out property <string> copyright: "Copyright © CrossDesk contributors"; in-out property <string> copyright: "© 2026 by JUNKUN DI. All right reserved.";
in-out property <string> license: "Licensed under GPL-3.0-only"; in-out property <string> license: "Licensed under GNU GPL v3.";
in-out property <string> signal-connected: "Signal server connected"; in-out property <string> signal-connected: "Signal server connected";
in-out property <string> signal-disconnected: "Signal server disconnected"; in-out property <string> signal-disconnected: "Signal server disconnected";
in-out property <string> tls-error: "Signal server TLS certificate error"; in-out property <string> tls-error: "Signal server TLS certificate error";
@@ -199,7 +204,7 @@ export component MainWindow inherits Window {
in property <string> current-version: ""; in property <string> current-version: "";
in property <string> latest-version: ""; in property <string> latest-version: "";
in property <string> release-name: ""; in property <string> release-name: "";
in property <string> release-notes: ""; in property <[ReleaseNoteBlock]> release-note-blocks;
in property <string> release-date: ""; in property <string> release-date: "";
in-out property <string> offline-warning: ""; in-out property <string> offline-warning: "";
in property <bool> connection-dialog-open: false; in property <bool> connection-dialog-open: false;
@@ -1341,19 +1346,19 @@ export component MainWindow inherits Window {
Rectangle { y: parent.height - 7px; height: 7px; background: #d3d3d3; } 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: 6px; text: UiStrings.about; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
} }
Text { x: 30px; y: 29px; width: 240px; height: 20px; text: UiStrings.version + ": CrossDesk " + root.current-version; color: #202124; font-size: ImGuiFontStyle.body; 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: 51px; width: 240px; height: 20px; text: UiStrings.update-available + ":"; 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 { if root.update-available: Rectangle {
x: 30px; x: 30px;
y: 70px; y: 60px;
width: 240px; width: 240px;
height: 20px; 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-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(); } } version-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
} }
Text { x: 30px; y: 96px; width: 240px; height: 20px; text: UiStrings.copyright; color: #202124; font-size: ImGuiFontStyle.body; vertical-alignment: center; } 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: 118px; width: 240px; height: 20px; text: UiStrings.license; 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: 157px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } } CompactButton { x: 133px; y: 145px; primary: true; text: UiStrings.ok; clicked => { root.about-open = false; } }
} }
} }
@@ -1364,24 +1369,43 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } } TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface { DialogSurface {
x: 120px; x: 120px;
y: 65px; y: 52px;
width: 400px; width: 400px;
height: 320px; height: 346px;
border-radius: 7px; border-radius: 7px;
Text { x: 40px; y: 22px; width: 320px; height: 24px; text: UiStrings.update-available + ": " + root.latest-version; color: #202124; font-size: ImGuiFontStyle.prominent; vertical-alignment: center; } Text { x: 30px; y: 22px; width: 320px; height: 24px; text: UiStrings.update-available + ": " + root.latest-version; color: #202124; font-size: ImGuiFontStyle.prominent; vertical-alignment: center; }
Rectangle { Rectangle {
x: 40px; x: 30px;
y: 47px; y: 47px;
width: 320px; width: 320px;
height: 22px; height: 22px;
Text { text: UiStrings.access-website + "https://crossdesk.cn"; color: website-touch.has-hover ? #174ea6 : #334fd1; font-size: ImGuiFontStyle.body; vertical-alignment: center; } Text {
x: 0px;
width: parent.width;
height: parent.height;
text: UiStrings.access-website + "https://crossdesk.cn";
color: website-touch.has-hover ? #174ea6 : #334fd1;
font-size: ImGuiFontStyle.body;
horizontal-alignment: left;
vertical-alignment: center;
}
website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } } website-touch := TouchArea { mouse-cursor: pointer; clicked => { root.open-download(); } }
} }
Text {
x: 30px;
y: 75px;
width: 320px;
height: 20px;
text: UiStrings.release-notes;
color: #202124;
font-size: 12px;
vertical-alignment: center;
}
Rectangle { Rectangle {
x: 20px; x: 30px;
y: 74px; y: 105px;
width: 360px; width: 360px;
height: 200px; height: 190px;
background: white; background: white;
border-width: 1px; border-width: 1px;
border-color: #b9bdc3; border-color: #b9bdc3;
@@ -1392,19 +1416,24 @@ export component MainWindow inherits Window {
height: parent.height - 2px; height: parent.height - 2px;
VerticalLayout { VerticalLayout {
width: 340px; width: 340px;
padding-left: 19px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
padding-top: 8px; padding-top: 8px;
padding-bottom: 8px; padding-bottom: 8px;
spacing: 7px; spacing: 7px;
if root.release-name != "": Text { width: 312px; text: root.release-name; color: #202124; font-size: ImGuiFontStyle.body; wrap: word-wrap; } VerticalLayout {
Text { width: 312px; text: root.release-notes; color: #202124; font-size: ImGuiFontStyle.body; wrap: word-wrap; } width: 312px;
if root.release-date != "": Text { width: 312px; text: UiStrings.release-date-label + root.release-date; color: #4f555e; font-size: ImGuiFontStyle.small; wrap: word-wrap; } spacing: 10px;
for block in root.release-note-blocks: VerticalLayout {
padding-top: block.section-gap ? 10px : 0px;
StyledText { width: 312px; text: block.content; default-color: #202124; default-font-size: ImGuiFontStyle.body; }
} }
} }
} }
CompactButton { x: root.compact-language ? 163px : 130px; y: 283px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 283px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } } }
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } }
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } }
} }
} }
@@ -1,4 +1,3 @@
#include <algorithm>
#include <string> #include <string>
#include "application/gui_application.h" #include "application/gui_application.h"
@@ -8,48 +7,6 @@
namespace crossdesk { namespace crossdesk {
std::string CleanMarkdown(const std::string &markdown) {
std::string result = markdown;
// remove # title mark
size_t pos = 0;
while (pos < result.length()) {
if (result[pos] == '\n' || pos == 0) {
size_t line_start = (result[pos] == '\n') ? pos + 1 : pos;
if (line_start < result.length() && result[line_start] == '#') {
size_t hash_end = line_start;
while (hash_end < result.length() &&
(result[hash_end] == '#' || result[hash_end] == ' ')) {
hash_end++;
}
result.erase(line_start, hash_end - line_start);
pos = line_start;
continue;
}
}
pos++;
}
// remove ** bold mark
pos = 0;
while ((pos = result.find("**", pos)) != std::string::npos) {
result.erase(pos, 2);
}
// remove all spaces
result.erase(std::remove(result.begin(), result.end(), ' '), result.end());
// replace . with 、
pos = 0;
while ((pos = result.find('.', pos)) != std::string::npos) {
result.replace(pos, 1, "");
pos += 1; // Move to next position after the replacement
}
return result;
}
int GuiApplication::UpdateNotificationWindow() { int GuiApplication::UpdateNotificationWindow() {
if (show_update_notification_window_ && update_available_) { if (show_update_notification_window_ && update_available_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport* viewport = ImGui::GetMainViewport();
@@ -142,8 +99,7 @@ int GuiApplication::UpdateNotificationWindow() {
// release notes // release notes
if (!release_notes_.empty()) { if (!release_notes_.empty()) {
ImGui::SetCursorPosX(update_notification_window_width * 0.05f); ImGui::SetCursorPosX(update_notification_window_width * 0.05f);
std::string cleaned_notes = CleanMarkdown(release_notes_); ImGui::TextWrapped("%s", release_notes_.c_str());
ImGui::TextWrapped("%s", cleaned_notes.c_str());
ImGui::Spacing(); ImGui::Spacing();
} }