mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-03-22 07:37:29 +08:00
Compare commits
40 Commits
e09243f1ec
...
v1.2.1-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e8ce6a2f0 | ||
|
|
9927a56b78 | ||
|
|
db3da52f83 | ||
|
|
19a7c6978a | ||
|
|
b5e9ba03a1 | ||
|
|
cb5f8b91ad | ||
|
|
f627f60f1a | ||
|
|
e9fce5b8b8 | ||
|
|
a7820a79db | ||
|
|
b6a52dbcd4 | ||
|
|
7bbd10a50c | ||
|
|
ee08b231db | ||
|
|
619e54dc0e | ||
|
|
9b69596af1 | ||
|
|
f6e169c013 | ||
|
|
fd242d50c1 | ||
|
|
d6d8ecd6c5 | ||
|
|
669fac7f50 | ||
|
|
92d670916e | ||
|
|
0155413c12 | ||
|
|
8468be6532 | ||
|
|
78c82f778a | ||
|
|
ab89a3d41a | ||
|
|
5f320af6e6 | ||
|
|
17b7ba6b72 | ||
|
|
c70ebdfe15 | ||
|
|
a3e564f160 | ||
|
|
eea37424c9 | ||
|
|
b322181853 | ||
|
|
3ad66f5e0b | ||
|
|
4035e0dd13 | ||
|
|
832b820096 | ||
|
|
d337971de0 | ||
|
|
a967dc72d7 | ||
|
|
5066fcda48 | ||
|
|
e7bdf42694 | ||
|
|
875fea88ee | ||
|
|
b2654ea9db | ||
|
|
8f8e415262 | ||
|
|
5ff624f7b2 |
16
README.md
16
README.md
@@ -229,21 +229,19 @@ sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
```
|
||||
|
||||
## 证书文件
|
||||
在宿主机的 `/var/lib/crossdesk/certs` 路径下可找到证书文件 `crossdesk.cn_root.crt`,下载到你的客户端主机,并在客户端的**自托管服务器设置**中选择相应的**证书文件路径**。
|
||||
|
||||
### 客户端
|
||||
1. 点击右上角设置进入设置页面。<br>
|
||||
1. 点击右上角设置进入设置页面。<br><br>
|
||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
|
||||
|
||||
3. 点击点击**自托管服务器配置**。<br><br>
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
|
||||
2. 点击点击`自托管服务器配置`按钮。<br><br>
|
||||
<img width="600" height="140" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
|
||||
|
||||
5. 在**证书文件路径**选择框中找到 **crossdesk.cn_root.crt** 的存放路径,选中 **crossdesk.cn_root.crt**,点击确认。<br><br>
|
||||
<img width="600" height="220" alt="image" src="https://github.com/user-attachments/assets/4af7cd3a-c72e-44fb-b032-30e050019c2a" /><br><br>
|
||||
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**)。<br><br>
|
||||
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><br><br>
|
||||
|
||||
7. 勾选使用**自托管服务器配置**,点击确认配置生效。<br><br>
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/1e455dc3-4087-4f37-a544-1ff9f8789383" /><br><br>
|
||||
4. 后续如果自托管服务器被重置或因其他原因导致证书更换,可以点击`重置证书指纹`按钮重置客户端保存的证书指纹。<br><br>
|
||||
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><br><br>
|
||||
|
||||
### Web 客户端
|
||||
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
16
README_EN.md
16
README_EN.md
@@ -238,25 +238,21 @@ sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
```
|
||||
|
||||
### Certificate Files
|
||||
You can find the certificate file `crossdesk.cn_root.crt` at `/var/lib/crossdesk/certs` on the host machine.
|
||||
Download it to your client device and select it in the **Certificate File Path** field under the CrossDesk client’s **Self-Hosted Server Settings**.
|
||||
|
||||
### Server Side
|
||||
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
|
||||
|
||||
### Client Side
|
||||
1. Click the settings icon in the top-right corner to enter the settings page.<br>
|
||||
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
|
||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br><br>
|
||||
|
||||
2. Click **Self-Hosted Server Configuration**.<br><br>
|
||||
2. Click `Self-Hosted Server Configuration` button.<br><br>
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br><br>
|
||||
|
||||
3. In the **Certificate File Path** selection, locate and select the **crossdesk.cn_root.crt** file.<br><br>
|
||||
<img width="600" height="220" alt="image" src="https://github.com/user-attachments/assets/4af7cd3a-c72e-44fb-b032-30e050019c2a" /><br><br>
|
||||
3. Enter the `Server Address` (**EXTERNAL_IP**), `Signaling Service Port` (**CROSSDESK_SERVER_PORT**), and `Relay Service Port` (**COTURN_PORT**).<br><br>
|
||||
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/9a32ddd5-37f8-4bee-9a51-eae295820f9a" /><br><br>
|
||||
|
||||
4. Check the option to use **Self-Hosted Server Configuration**.<br><br>
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/1e455dc3-4087-4f37-a544-1ff9f8789383" /><br><br>
|
||||
4. If the self-hosted server is later reset or the certificate is replaced for any reason, you can click the `Reset Certificate Fingerprint` button to clear the certificate fingerprint saved on the client.<br><br>
|
||||
<img width="600" height="200" alt="image" src="https://github.com/user-attachments/assets/d9e423ab-0c2b-4fab-b132-4dc27462d704" /><br><br>
|
||||
|
||||
### Web Client
|
||||
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
25
src/common/window_util_mac.h
Normal file
25
src/common/window_util_mac.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-01-20
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_UTIL_MAC_H_
|
||||
#define _WINDOW_UTIL_MAC_H_
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Best-effort: keep an SDL window above normal windows on macOS.
|
||||
// No-op on non-macOS builds.
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top);
|
||||
|
||||
// Best-effort: exclude an SDL window from the Window menu and window cycling.
|
||||
// Note: Cmd-Tab switches apps (not individual windows), so this primarily
|
||||
// affects the Window menu and Cmd-` window cycling.
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
64
src/common/window_util_mac.mm
Normal file
64
src/common/window_util_mac.mm
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "window_util_mac.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
static NSWindow* GetNSWindowFromSDL(::SDL_Window* window) {
|
||||
if (!window) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_COCOA_WINDOW_POINTER)
|
||||
return nil;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
void* cocoa_window_ptr =
|
||||
SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, NULL);
|
||||
if (!cocoa_window_ptr) {
|
||||
return nil;
|
||||
}
|
||||
return (__bridge NSWindow*)cocoa_window_ptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MacSetWindowAlwaysOnTop(::SDL_Window* window, bool always_on_top) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)always_on_top;
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep above normal windows.
|
||||
const NSInteger level = always_on_top ? NSFloatingWindowLevel : NSNormalWindowLevel;
|
||||
[ns_window setLevel:level];
|
||||
|
||||
// Optional: keep visible across Spaces/fullscreen. Safe as best-effort.
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorCanJoinAllSpaces;
|
||||
behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
void MacSetWindowExcludedFromWindowMenu(::SDL_Window* window, bool excluded) {
|
||||
NSWindow* ns_window = GetNSWindowFromSDL(window);
|
||||
if (!ns_window) {
|
||||
(void)excluded;
|
||||
return;
|
||||
}
|
||||
|
||||
[ns_window setExcludedFromWindowsMenu:excluded];
|
||||
|
||||
NSWindowCollectionBehavior behavior = [ns_window collectionBehavior];
|
||||
behavior |= NSWindowCollectionBehaviorIgnoresCycle;
|
||||
behavior |= NSWindowCollectionBehaviorTransient;
|
||||
[ns_window setCollectionBehavior:behavior];
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -77,6 +77,64 @@ int ConfigCenter::Load() {
|
||||
} else {
|
||||
cert_file_path_ = "";
|
||||
}
|
||||
const char* cert_fingerprint_value =
|
||||
ini_.GetValue(section_, "cert_fingerprint", nullptr);
|
||||
if (cert_fingerprint_value != nullptr && strlen(cert_fingerprint_value) > 0) {
|
||||
cert_fingerprint_ = cert_fingerprint_value;
|
||||
} else {
|
||||
cert_fingerprint_ = "";
|
||||
}
|
||||
const char* cert_fingerprint_server_host_value =
|
||||
ini_.GetValue(section_, "cert_fingerprint_server_host", nullptr);
|
||||
if (cert_fingerprint_server_host_value != nullptr &&
|
||||
strlen(cert_fingerprint_server_host_value) > 0) {
|
||||
cert_fingerprint_server_host_ = cert_fingerprint_server_host_value;
|
||||
} else {
|
||||
cert_fingerprint_server_host_ = "";
|
||||
}
|
||||
|
||||
const char* default_cert_fingerprint_value =
|
||||
ini_.GetValue(section_, "default_cert_fingerprint", nullptr);
|
||||
if (default_cert_fingerprint_value != nullptr &&
|
||||
strlen(default_cert_fingerprint_value) > 0) {
|
||||
default_cert_fingerprint_ = default_cert_fingerprint_value;
|
||||
} else {
|
||||
default_cert_fingerprint_ = "";
|
||||
}
|
||||
const char* default_cert_fingerprint_server_host_value =
|
||||
ini_.GetValue(section_, "default_cert_fingerprint_server_host", nullptr);
|
||||
if (default_cert_fingerprint_server_host_value != nullptr &&
|
||||
strlen(default_cert_fingerprint_server_host_value) > 0) {
|
||||
default_cert_fingerprint_server_host_ =
|
||||
default_cert_fingerprint_server_host_value;
|
||||
} else {
|
||||
default_cert_fingerprint_server_host_ = "";
|
||||
}
|
||||
|
||||
if (enable_self_hosted_ && !cert_fingerprint_.empty() &&
|
||||
!cert_fingerprint_server_host_.empty() &&
|
||||
signal_server_host_ != cert_fingerprint_server_host_) {
|
||||
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
|
||||
cert_fingerprint_server_host_, signal_server_host_);
|
||||
cert_fingerprint_.clear();
|
||||
cert_fingerprint_server_host_.clear();
|
||||
ini_.Delete(section_, "cert_fingerprint", false);
|
||||
ini_.Delete(section_, "cert_fingerprint_server_host", false);
|
||||
ini_.SaveFile(config_path_.c_str());
|
||||
}
|
||||
|
||||
if (!enable_self_hosted_ && !default_cert_fingerprint_.empty() &&
|
||||
!default_cert_fingerprint_server_host_.empty() &&
|
||||
signal_server_host_default_ != default_cert_fingerprint_server_host_) {
|
||||
LOG_INFO(
|
||||
"Default server IP changed from {} to {}, clearing old fingerprint",
|
||||
default_cert_fingerprint_server_host_, signal_server_host_default_);
|
||||
default_cert_fingerprint_.clear();
|
||||
default_cert_fingerprint_server_host_.clear();
|
||||
ini_.Delete(section_, "default_cert_fingerprint", false);
|
||||
ini_.Delete(section_, "default_cert_fingerprint_server_host", false);
|
||||
ini_.SaveFile(config_path_.c_str());
|
||||
}
|
||||
|
||||
enable_autostart_ =
|
||||
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
@@ -108,6 +166,18 @@ int ConfigCenter::Save() {
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
|
||||
if (!cert_fingerprint_.empty()) {
|
||||
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
|
||||
ini_.SetValue(section_, "cert_fingerprint_server_host",
|
||||
cert_fingerprint_server_host_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!default_cert_fingerprint_.empty()) {
|
||||
ini_.SetValue(section_, "default_cert_fingerprint",
|
||||
default_cert_fingerprint_.c_str());
|
||||
ini_.SetValue(section_, "default_cert_fingerprint_server_host",
|
||||
default_cert_fingerprint_server_host_.c_str());
|
||||
}
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
@@ -200,6 +270,15 @@ int ConfigCenter::SetSrtp(bool enable_srtp) {
|
||||
}
|
||||
|
||||
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
|
||||
if (enable_self_hosted_ && !cert_fingerprint_.empty() &&
|
||||
signal_server_host != signal_server_host_) {
|
||||
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
|
||||
signal_server_host_, signal_server_host);
|
||||
cert_fingerprint_.clear();
|
||||
cert_fingerprint_server_host_.clear();
|
||||
ini_.Delete(section_, "cert_fingerprint", false);
|
||||
ini_.Delete(section_, "cert_fingerprint_server_host", false);
|
||||
}
|
||||
signal_server_host_ = signal_server_host;
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
@@ -241,6 +320,57 @@ int ConfigCenter::SetCertFilePath(const std::string& cert_file_path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetCertFingerprint(const std::string& fingerprint) {
|
||||
cert_fingerprint_ = fingerprint;
|
||||
cert_fingerprint_server_host_ = signal_server_host_;
|
||||
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
|
||||
ini_.SetValue(section_, "cert_fingerprint_server_host",
|
||||
cert_fingerprint_server_host_.c_str());
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetDefaultCertFingerprint(const std::string& fingerprint) {
|
||||
default_cert_fingerprint_ = fingerprint;
|
||||
default_cert_fingerprint_server_host_ = signal_server_host_default_;
|
||||
ini_.SetValue(section_, "default_cert_fingerprint",
|
||||
default_cert_fingerprint_.c_str());
|
||||
ini_.SetValue(section_, "default_cert_fingerprint_server_host",
|
||||
default_cert_fingerprint_server_host_.c_str());
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::ClearCertFingerprint() {
|
||||
cert_fingerprint_.clear();
|
||||
cert_fingerprint_server_host_.clear();
|
||||
ini_.Delete(section_, "cert_fingerprint", false);
|
||||
ini_.Delete(section_, "cert_fingerprint_server_host", false);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::ClearDefaultCertFingerprint() {
|
||||
default_cert_fingerprint_.clear();
|
||||
default_cert_fingerprint_server_host_.clear();
|
||||
ini_.Delete(section_, "default_cert_fingerprint", false);
|
||||
ini_.Delete(section_, "default_cert_fingerprint_server_host", false);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
||||
enable_self_hosted_ = enable_self_hosted;
|
||||
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
@@ -272,6 +402,28 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
||||
if (cert_file_path_value != nullptr && strlen(cert_file_path_value) > 0) {
|
||||
cert_file_path_ = cert_file_path_value;
|
||||
}
|
||||
const char* cert_fingerprint_value =
|
||||
ini_.GetValue(section_, "cert_fingerprint", nullptr);
|
||||
if (cert_fingerprint_value != nullptr &&
|
||||
strlen(cert_fingerprint_value) > 0) {
|
||||
cert_fingerprint_ = cert_fingerprint_value;
|
||||
}
|
||||
const char* cert_fingerprint_server_host_value =
|
||||
ini_.GetValue(section_, "cert_fingerprint_server_host", nullptr);
|
||||
if (cert_fingerprint_server_host_value != nullptr &&
|
||||
strlen(cert_fingerprint_server_host_value) > 0) {
|
||||
cert_fingerprint_server_host_ = cert_fingerprint_server_host_value;
|
||||
}
|
||||
|
||||
if (!cert_fingerprint_.empty() && !cert_fingerprint_server_host_.empty() &&
|
||||
signal_server_host_ != cert_fingerprint_server_host_) {
|
||||
LOG_INFO("Server IP changed from {} to {}, clearing old fingerprint",
|
||||
cert_fingerprint_server_host_, signal_server_host_);
|
||||
cert_fingerprint_.clear();
|
||||
cert_fingerprint_server_host_.clear();
|
||||
ini_.Delete(section_, "cert_fingerprint", false);
|
||||
ini_.Delete(section_, "cert_fingerprint_server_host", false);
|
||||
}
|
||||
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
@@ -279,6 +431,11 @@ int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
|
||||
if (!cert_fingerprint_.empty()) {
|
||||
ini_.SetValue(section_, "cert_fingerprint", cert_fingerprint_.c_str());
|
||||
ini_.SetValue(section_, "cert_fingerprint_server_host",
|
||||
cert_fingerprint_server_host_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
@@ -368,6 +525,14 @@ int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
|
||||
|
||||
std::string ConfigCenter::GetCertFilePath() const { return cert_file_path_; }
|
||||
|
||||
std::string ConfigCenter::GetCertFingerprint() const {
|
||||
return cert_fingerprint_;
|
||||
}
|
||||
|
||||
std::string ConfigCenter::GetDefaultCertFingerprint() const {
|
||||
return default_cert_fingerprint_;
|
||||
}
|
||||
|
||||
std::string ConfigCenter::GetDefaultServerHost() const {
|
||||
return signal_server_host_default_;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ class ConfigCenter {
|
||||
int SetServerPort(int signal_server_port);
|
||||
int SetCoturnServerPort(int coturn_server_port);
|
||||
int SetCertFilePath(const std::string& cert_file_path);
|
||||
int SetCertFingerprint(const std::string& fingerprint);
|
||||
int SetDefaultCertFingerprint(const std::string& fingerprint);
|
||||
int ClearCertFingerprint();
|
||||
int ClearDefaultCertFingerprint();
|
||||
int SetSelfHosted(bool enable_self_hosted);
|
||||
int SetMinimizeToTray(bool enable_minimize_to_tray);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
@@ -56,6 +60,8 @@ class ConfigCenter {
|
||||
int GetSignalServerPort() const;
|
||||
int GetCoturnServerPort() const;
|
||||
std::string GetCertFilePath() const;
|
||||
std::string GetCertFingerprint() const;
|
||||
std::string GetDefaultCertFingerprint() const;
|
||||
std::string GetDefaultServerHost() const;
|
||||
int GetDefaultSignalServerPort() const;
|
||||
int GetDefaultCoturnServerPort() const;
|
||||
@@ -88,6 +94,10 @@ class ConfigCenter {
|
||||
int coturn_server_port_default_ = 3478;
|
||||
std::string cert_file_path_ = "";
|
||||
std::string cert_file_path_default_ = "";
|
||||
std::string cert_fingerprint_ = "";
|
||||
std::string cert_fingerprint_server_host_ = "";
|
||||
std::string default_cert_fingerprint_ = "";
|
||||
std::string default_cert_fingerprint_server_host_ = "";
|
||||
bool enable_self_hosted_ = false;
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -116,6 +116,9 @@ static std::vector<std::string> self_hosted_server_certificate_path = {
|
||||
reinterpret_cast<const char*>(u8"证书文件路径:"), "Certificate File Path:"};
|
||||
static std::vector<std::string> select_a_file = {
|
||||
reinterpret_cast<const char*>(u8"请选择文件"), "Please select a file"};
|
||||
static std::vector<std::string> reset_cert_fingerprint = {
|
||||
reinterpret_cast<const char*>(u8"重置证书指纹"),
|
||||
"Reset Certificate Fingerprint"};
|
||||
static std::vector<std::string> ok = {reinterpret_cast<const char*>(u8"确认"),
|
||||
"OK"};
|
||||
static std::vector<std::string> cancel = {
|
||||
@@ -182,6 +185,25 @@ static std::vector<std::string> enable_daemon = {
|
||||
static std::vector<std::string> takes_effect_after_restart = {
|
||||
reinterpret_cast<const char*>(u8"重启后生效"),
|
||||
"Takes effect after restart"};
|
||||
static std::vector<std::string> select_file = {
|
||||
reinterpret_cast<const char*>(u8"选择文件"), "Select File"};
|
||||
static std::vector<std::string> file_transfer_progress = {
|
||||
reinterpret_cast<const char*>(u8"文件传输进度"), "File Transfer Progress"};
|
||||
static std::vector<std::string> queued = {
|
||||
reinterpret_cast<const char*>(u8"队列中"), "Queued"};
|
||||
static std::vector<std::string> sending = {
|
||||
reinterpret_cast<const char*>(u8"正在传输"), "Sending"};
|
||||
static std::vector<std::string> completed = {
|
||||
reinterpret_cast<const char*>(u8"已完成"), "Completed"};
|
||||
static std::vector<std::string> failed = {
|
||||
reinterpret_cast<const char*>(u8"失败"), "Failed"};
|
||||
static std::vector<std::string> controller = {
|
||||
reinterpret_cast<const char*>(u8"控制端:"), "Controller:"};
|
||||
static std::vector<std::string> file_transfer = {
|
||||
reinterpret_cast<const char*>(u8"文件传输:"), "File Transfer:"};
|
||||
static std::vector<std::string> connection_status = {
|
||||
reinterpret_cast<const char*>(u8"连接状态:"), "Connection Status:"};
|
||||
|
||||
#if _WIN32
|
||||
static std::vector<std::string> minimize_to_tray = {
|
||||
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),
|
||||
|
||||
@@ -30,6 +30,7 @@ int Render::LocalWindow() {
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.9f);
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||
localization::local_desktop[localization_language_index_].c_str());
|
||||
|
||||
@@ -26,6 +26,7 @@ int Render::RecentConnectionsWindow() {
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.02f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.9f);
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||
localization::recent_connections[localization_language_index_].c_str());
|
||||
|
||||
@@ -31,6 +31,7 @@ int Render::RemoteWindow() {
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(io.DisplaySize.x * 0.057f, io.DisplaySize.y * 0.02f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.9f);
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.0f, 0.0f, 0.0f, 0.5f), "%s",
|
||||
localization::remote_desktop[localization_language_index_].c_str());
|
||||
@@ -189,11 +190,11 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 8.0f;
|
||||
props->control_window_width_ = title_bar_height_ * 9.0f;
|
||||
props->control_window_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_min_width_ = title_bar_height_ * 0.65f;
|
||||
props->control_window_min_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_max_width_ = title_bar_height_ * 8.0f;
|
||||
props->control_window_max_width_ = title_bar_height_ * 9.0f;
|
||||
props->control_window_max_height_ = title_bar_height_ * 6.0f;
|
||||
|
||||
if (!props->peer_) {
|
||||
@@ -205,7 +206,11 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
AddVideoStream(props->peer_, display_info.name.c_str());
|
||||
}
|
||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||
AddDataStream(props->peer_, props->data_label_.c_str());
|
||||
AddDataStream(props->peer_, props->data_label_.c_str(), false);
|
||||
AddDataStream(props->peer_, props->control_data_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
|
||||
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
162
src/gui/render.h
162
src/gui/render.h
@@ -11,10 +11,12 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -40,11 +42,50 @@
|
||||
namespace crossdesk {
|
||||
class Render {
|
||||
public:
|
||||
struct FileTransferState {
|
||||
std::atomic<bool> file_sending_ = false;
|
||||
std::atomic<uint64_t> file_sent_bytes_ = 0;
|
||||
std::atomic<uint64_t> file_total_bytes_ = 0;
|
||||
std::atomic<uint32_t> file_send_rate_bps_ = 0;
|
||||
std::mutex file_transfer_mutex_;
|
||||
std::chrono::steady_clock::time_point file_send_start_time_;
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
std::filesystem::path file_path;
|
||||
std::string file_label;
|
||||
std::string remote_id;
|
||||
};
|
||||
std::queue<QueuedFile> file_send_queue_;
|
||||
std::mutex file_queue_mutex_;
|
||||
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
|
||||
|
||||
struct FileTransferInfo {
|
||||
std::string file_name;
|
||||
std::filesystem::path file_path;
|
||||
uint64_t file_size = 0;
|
||||
FileTransferStatus status = FileTransferStatus::Queued;
|
||||
uint64_t sent_bytes = 0;
|
||||
uint32_t file_id = 0;
|
||||
uint32_t rate_bps = 0;
|
||||
};
|
||||
std::vector<FileTransferInfo> file_transfer_list_;
|
||||
std::mutex file_transfer_list_mutex_;
|
||||
};
|
||||
|
||||
struct SubStreamWindowProperties {
|
||||
Params params_;
|
||||
PeerPtr* peer_ = nullptr;
|
||||
std::string audio_label_ = "control_audio";
|
||||
std::string data_label_ = "control_data";
|
||||
std::string data_label_ = "data";
|
||||
std::string file_label_ = "file";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_feedback_label_ = "file_feedback";
|
||||
std::string clipboard_label_ = "clipboard";
|
||||
std::string local_id_ = "";
|
||||
std::string remote_id_ = "";
|
||||
bool exit_ = false;
|
||||
@@ -82,8 +123,13 @@ class Render {
|
||||
float mouse_diff_control_bar_pos_y_ = 0;
|
||||
double control_bar_button_pressed_time_ = 0;
|
||||
double net_traffic_stats_button_pressed_time_ = 0;
|
||||
unsigned char* dst_buffer_ = nullptr;
|
||||
size_t dst_buffer_capacity_ = 0;
|
||||
// Double-buffered NV12 frame storage. Written by decode callback thread,
|
||||
// consumed by SDL main thread.
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
bool render_rect_dirty_ = false;
|
||||
bool stream_cleanup_pending_ = false;
|
||||
float mouse_pos_x_ = 0;
|
||||
float mouse_pos_y_ = 0;
|
||||
float mouse_pos_x_last_ = 0;
|
||||
@@ -122,6 +168,11 @@ class Render {
|
||||
int frame_count_ = 0;
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
XNetTrafficStats net_traffic_stats_;
|
||||
|
||||
using QueuedFile = FileTransferState::QueuedFile;
|
||||
using FileTransferStatus = FileTransferState::FileTransferStatus;
|
||||
using FileTransferInfo = FileTransferState::FileTransferInfo;
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -142,6 +193,7 @@ class Render {
|
||||
void UpdateInteractions();
|
||||
void HandleRecentConnections();
|
||||
void HandleStreamWindow();
|
||||
void HandleServerWindow();
|
||||
void Cleanup();
|
||||
void CleanupFactories();
|
||||
void CleanupPeer(std::shared_ptr<SubStreamWindowProperties> props);
|
||||
@@ -151,12 +203,24 @@ class Render {
|
||||
void UpdateRenderRect();
|
||||
void ProcessSdlEvent(const SDL_Event& event);
|
||||
|
||||
void ProcessFileDropEvent(const SDL_Event& event);
|
||||
|
||||
void ProcessSelectedFile(
|
||||
const std::string& path,
|
||||
const std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label, const std::string& remote_id = "");
|
||||
|
||||
std::shared_ptr<SubStreamWindowProperties>
|
||||
GetSubStreamWindowPropertiesByRemoteId(const std::string& remote_id);
|
||||
|
||||
private:
|
||||
int CreateStreamRenderWindow();
|
||||
int TitleBar(bool main_window);
|
||||
int MainWindow();
|
||||
int UpdateNotificationWindow();
|
||||
int StreamWindow();
|
||||
int ServerWindow();
|
||||
int RemoteClientInfoWindow();
|
||||
int LocalWindow();
|
||||
int RemoteWindow();
|
||||
int RecentConnectionsWindow();
|
||||
@@ -172,6 +236,8 @@ class Render {
|
||||
int ShowRecentConnections();
|
||||
void Hyperlink(const std::string& label, const std::string& url,
|
||||
const float window_width);
|
||||
int FileTransferWindow(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
std::string OpenFileDialog(std::string title);
|
||||
|
||||
private:
|
||||
int ConnectTo(const std::string& remote_id, const char* password,
|
||||
@@ -180,11 +246,15 @@ class Render {
|
||||
int DestroyMainWindow();
|
||||
int CreateStreamWindow();
|
||||
int DestroyStreamWindow();
|
||||
int SetupFontAndStyle(bool main_window);
|
||||
int CreateServerWindow();
|
||||
int DestroyServerWindow();
|
||||
int SetupFontAndStyle(ImFont** system_chinese_font_out);
|
||||
int DestroyMainWindowContext();
|
||||
int DestroyStreamWindowContext();
|
||||
int DestroyServerWindowContext();
|
||||
int DrawMainWindow();
|
||||
int DrawStreamWindow();
|
||||
int DrawServerWindow();
|
||||
int ConfirmDeleteConnection();
|
||||
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
|
||||
void DrawConnectionStatusText(
|
||||
@@ -201,14 +271,17 @@ class Render {
|
||||
public:
|
||||
static void OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data);
|
||||
|
||||
static void OnReceiveAudioBufferCb(const char* data, size_t size,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data);
|
||||
|
||||
static void OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data);
|
||||
|
||||
static void OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
@@ -217,11 +290,11 @@ class Render {
|
||||
static void OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
size_t user_id_size, void* user_data);
|
||||
|
||||
static void NetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data);
|
||||
static void OnNetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data);
|
||||
|
||||
static SDL_HitTestResult HitTestCallback(SDL_Window* window,
|
||||
const SDL_Point* area, void* data);
|
||||
@@ -259,6 +332,13 @@ class Render {
|
||||
|
||||
int CreateConnectionPeer();
|
||||
|
||||
// File transfer helper functions
|
||||
void StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
|
||||
const std::filesystem::path& file_path,
|
||||
const std::string& file_label,
|
||||
const std::string& remote_id = "");
|
||||
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
|
||||
|
||||
int AudioDeviceInit();
|
||||
int AudioDeviceDestroy();
|
||||
|
||||
@@ -334,6 +414,7 @@ class Render {
|
||||
ImGuiContext* main_ctx_ = nullptr;
|
||||
ImFont* main_windows_system_chinese_font_ = nullptr;
|
||||
ImFont* stream_windows_system_chinese_font_ = nullptr;
|
||||
ImFont* server_windows_system_chinese_font_ = nullptr;
|
||||
bool exit_ = false;
|
||||
const int sdl_refresh_ms_ = 16; // ~60 FPS
|
||||
#if _WIN32
|
||||
@@ -356,6 +437,9 @@ class Render {
|
||||
bool keyboard_capturer_is_started_ = false;
|
||||
bool foucs_on_main_window_ = false;
|
||||
bool foucs_on_stream_window_ = false;
|
||||
bool main_window_minimized_ = false;
|
||||
uint32_t last_main_minimize_request_tick_ = 0;
|
||||
uint32_t last_stream_minimize_request_tick_ = 0;
|
||||
bool audio_capture_ = false;
|
||||
int main_window_width_real_ = 720;
|
||||
int main_window_height_real_ = 540;
|
||||
@@ -405,7 +489,7 @@ class Render {
|
||||
bool just_created_ = false;
|
||||
std::string controlled_remote_id_ = "";
|
||||
std::string focused_remote_id_ = "";
|
||||
bool need_to_send_host_info_ = false;
|
||||
std::string remote_client_id_ = "";
|
||||
SDL_Event last_mouse_event;
|
||||
SDL_AudioStream* output_stream_;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
@@ -432,6 +516,42 @@ class Render {
|
||||
float stream_window_dpi_scaling_w_ = 1.0f;
|
||||
float stream_window_dpi_scaling_h_ = 1.0f;
|
||||
|
||||
// server window render
|
||||
SDL_Window* server_window_ = nullptr;
|
||||
SDL_Renderer* server_renderer_ = nullptr;
|
||||
ImGuiContext* server_ctx_ = nullptr;
|
||||
|
||||
// server window properties
|
||||
bool need_to_create_server_window_ = false;
|
||||
bool need_to_destroy_server_window_ = false;
|
||||
bool server_window_created_ = false;
|
||||
bool server_window_inited_ = false;
|
||||
int server_window_width_default_ = 250;
|
||||
int server_window_height_default_ = 150;
|
||||
float server_window_width_ = 250;
|
||||
float server_window_height_ = 150;
|
||||
float server_window_title_bar_height_ = 30.0f;
|
||||
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
float server_window_dpi_scaling_h_ = 1.0f;
|
||||
|
||||
// server window collapsed mode
|
||||
bool server_window_collapsed_ = false;
|
||||
bool server_window_collapsed_dragging_ = false;
|
||||
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_collapsed_drag_start_win_x_ = 0;
|
||||
int server_window_collapsed_drag_start_win_y_ = 0;
|
||||
|
||||
// server window drag normal mode
|
||||
bool server_window_dragging_ = false;
|
||||
float server_window_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_drag_start_win_x_ = 0;
|
||||
int server_window_drag_start_win_y_ = 0;
|
||||
|
||||
bool label_inited_ = false;
|
||||
bool connect_button_pressed_ = false;
|
||||
bool password_validating_ = false;
|
||||
@@ -448,6 +568,7 @@ class Render {
|
||||
bool fullscreen_button_pressed_ = false;
|
||||
bool focus_on_input_widget_ = true;
|
||||
bool is_client_mode_ = false;
|
||||
bool is_server_mode_ = false;
|
||||
bool reload_recent_connections_ = true;
|
||||
bool show_confirm_delete_connection_ = false;
|
||||
bool delete_connection_ = false;
|
||||
@@ -464,7 +585,20 @@ class Render {
|
||||
std::string video_secondary_label_ = "secondary_display";
|
||||
std::string audio_label_ = "audio";
|
||||
std::string data_label_ = "data";
|
||||
std::string info_label_ = "info";
|
||||
std::string control_data_label_ = "control_data";
|
||||
std::string file_label_ = "file";
|
||||
std::string file_feedback_label_ = "file_feedback";
|
||||
std::string clipboard_label_ = "clipboard";
|
||||
Params params_;
|
||||
// Map file_id to props for tracking file transfer progress via ACK
|
||||
std::unordered_map<uint32_t, std::weak_ptr<SubStreamWindowProperties>>
|
||||
file_id_to_props_;
|
||||
std::shared_mutex file_id_to_props_mutex_;
|
||||
|
||||
// Map file_id to FileTransferState for global file transfer (props == null)
|
||||
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
|
||||
std::shared_mutex file_id_to_transfer_state_mutex_;
|
||||
SDL_AudioDeviceID input_dev_;
|
||||
SDL_AudioDeviceID output_dev_;
|
||||
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
|
||||
@@ -478,8 +612,8 @@ class Render {
|
||||
uint64_t last_frame_time_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
uint64_t new_version_icon_last_trigger_time_ = 0;
|
||||
uint64_t new_version_icon_render_start_time_ = 0;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
double new_version_icon_render_start_time_ = 0.0;
|
||||
#ifdef __APPLE__
|
||||
bool show_request_permission_window_ = true;
|
||||
#endif
|
||||
@@ -539,6 +673,10 @@ class Render {
|
||||
|
||||
/* ------ server mode ------ */
|
||||
std::unordered_map<std::string, ConnectionStatus> connection_status_;
|
||||
std::unordered_map<std::string, std::string> connection_host_names_;
|
||||
std::string selected_server_remote_id_ = "";
|
||||
std::string selected_server_remote_hostname_ = "";
|
||||
FileTransferState file_transfer_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -1,8 +1,17 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "clipboard.h"
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "minirtc.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
@@ -212,6 +221,7 @@ void Render::SdlCaptureAudioOut([[maybe_unused]] void* userdata,
|
||||
|
||||
void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data) {
|
||||
Render* render = (Render*)user_data;
|
||||
if (!render) {
|
||||
@@ -228,31 +238,31 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||
render->client_properties_.find(remote_id)->second.get();
|
||||
|
||||
if (props->connection_established_) {
|
||||
if (!props->dst_buffer_) {
|
||||
props->dst_buffer_capacity_ = video_frame->size;
|
||||
props->dst_buffer_ = new unsigned char[video_frame->size];
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
|
||||
if (props->dst_buffer_capacity_ < video_frame->size) {
|
||||
delete props->dst_buffer_;
|
||||
props->dst_buffer_capacity_ = video_frame->size;
|
||||
props->dst_buffer_ = new unsigned char[video_frame->size];
|
||||
}
|
||||
if (!props->back_frame_) {
|
||||
props->back_frame_ =
|
||||
std::make_shared<std::vector<unsigned char>>(video_frame->size);
|
||||
}
|
||||
if (props->back_frame_->size() != video_frame->size) {
|
||||
props->back_frame_->resize(video_frame->size);
|
||||
}
|
||||
|
||||
memcpy(props->dst_buffer_, video_frame->data, video_frame->size);
|
||||
bool need_to_update_render_rect = false;
|
||||
if (props->video_width_ != props->video_width_last_ ||
|
||||
props->video_height_ != props->video_height_last_) {
|
||||
need_to_update_render_rect = true;
|
||||
props->video_width_last_ = props->video_width_;
|
||||
props->video_height_last_ = props->video_height_;
|
||||
}
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
std::memcpy(props->back_frame_->data(), video_frame->data,
|
||||
video_frame->size);
|
||||
|
||||
if (need_to_update_render_rect) {
|
||||
render->UpdateRenderRect();
|
||||
const bool size_changed = (props->video_width_ != video_frame->width) ||
|
||||
(props->video_height_ != video_frame->height);
|
||||
if (size_changed) {
|
||||
props->render_rect_dirty_ = true;
|
||||
}
|
||||
|
||||
props->video_width_ = video_frame->width;
|
||||
props->video_height_ = video_frame->height;
|
||||
props->video_size_ = video_frame->size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
@@ -279,6 +289,7 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
|
||||
|
||||
void Render::OnReceiveAudioBufferCb(const char* data, size_t size,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data) {
|
||||
Render* render = (Render*)user_data;
|
||||
if (!render) {
|
||||
@@ -298,12 +309,219 @@ void Render::OnReceiveAudioBufferCb(const char* data, size_t size,
|
||||
|
||||
void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size,
|
||||
void* user_data) {
|
||||
Render* render = (Render*)user_data;
|
||||
if (!render) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string source_id = std::string(src_id, src_id_size);
|
||||
if (source_id == render->file_label_) {
|
||||
std::string remote_user_id = std::string(user_id, user_id_size);
|
||||
|
||||
static FileReceiver receiver;
|
||||
receiver.SetOnSendAck([render,
|
||||
remote_user_id](const FileTransferAck& ack) -> int {
|
||||
bool is_server_sending = remote_user_id.rfind("C-", 0) != 0;
|
||||
if (is_server_sending) {
|
||||
auto props =
|
||||
render->GetSubStreamWindowPropertiesByRemoteId(remote_user_id);
|
||||
if (props) {
|
||||
PeerPtr* peer = props->peer_;
|
||||
return SendReliableDataFrame(
|
||||
peer, reinterpret_cast<const char*>(&ack),
|
||||
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return SendReliableDataFrame(
|
||||
render->peer_, reinterpret_cast<const char*>(&ack),
|
||||
sizeof(FileTransferAck), render->file_feedback_label_.c_str());
|
||||
});
|
||||
|
||||
receiver.OnData(data, size);
|
||||
return;
|
||||
} else if (source_id == render->clipboard_label_) {
|
||||
if (size > 0) {
|
||||
std::string clipboard_text(data, size);
|
||||
if (!Clipboard::SetText(clipboard_text)) {
|
||||
LOG_ERROR("Failed to set clipboard content from remote");
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if (source_id == render->file_feedback_label_) {
|
||||
if (size < sizeof(FileTransferAck)) {
|
||||
LOG_ERROR("FileTransferAck: buffer too small, size={}", size);
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferAck ack{};
|
||||
memcpy(&ack, data, sizeof(FileTransferAck));
|
||||
|
||||
if (ack.magic != kFileAckMagic) {
|
||||
LOG_ERROR(
|
||||
"FileTransferAck: invalid magic, got 0x{:08X}, expected 0x{:08X}",
|
||||
ack.magic, kFileAckMagic);
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<SubStreamWindowProperties> props = nullptr;
|
||||
{
|
||||
std::shared_lock lock(render->file_id_to_props_mutex_);
|
||||
auto it = render->file_id_to_props_.find(ack.file_id);
|
||||
if (it != render->file_id_to_props_.end()) {
|
||||
props = it->second.lock();
|
||||
}
|
||||
}
|
||||
|
||||
Render::FileTransferState* state = nullptr;
|
||||
if (!props) {
|
||||
{
|
||||
std::shared_lock lock(render->file_id_to_transfer_state_mutex_);
|
||||
auto it = render->file_id_to_transfer_state_.find(ack.file_id);
|
||||
if (it != render->file_id_to_transfer_state_.end()) {
|
||||
state = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
LOG_WARN("FileTransferAck: no props/state found for file_id={}",
|
||||
ack.file_id);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
state = &props->file_transfer_;
|
||||
}
|
||||
|
||||
// Update progress based on ACK
|
||||
state->file_sent_bytes_ = ack.acked_offset;
|
||||
state->file_total_bytes_ = ack.total_size;
|
||||
|
||||
uint32_t rate_bps = 0;
|
||||
{
|
||||
if (props) {
|
||||
uint32_t data_channel_bitrate =
|
||||
props->net_traffic_stats_.data_outbound_stats.bitrate;
|
||||
|
||||
if (data_channel_bitrate > 0 && state->file_sending_.load()) {
|
||||
rate_bps = static_cast<uint32_t>(data_channel_bitrate * 0.99f);
|
||||
|
||||
uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
if (current_rate > 0) {
|
||||
// 70% old + 30% new for smoother display
|
||||
rate_bps =
|
||||
static_cast<uint32_t>(current_rate * 0.7 + rate_bps * 0.3);
|
||||
}
|
||||
} else {
|
||||
rate_bps = state->file_send_rate_bps_.load();
|
||||
}
|
||||
} else {
|
||||
// Global transfer: no per-connection bitrate available.
|
||||
// Estimate send rate from ACKed bytes delta over time.
|
||||
const uint32_t current_rate = state->file_send_rate_bps_.load();
|
||||
uint32_t estimated_rate_bps = 0;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
|
||||
uint64_t last_bytes = 0;
|
||||
std::chrono::steady_clock::time_point last_time;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_mutex_);
|
||||
last_bytes = state->file_send_last_bytes_;
|
||||
last_time = state->file_send_last_update_time_;
|
||||
}
|
||||
|
||||
if (state->file_sending_.load() && ack.acked_offset >= last_bytes) {
|
||||
const uint64_t delta_bytes = ack.acked_offset - last_bytes;
|
||||
const double delta_seconds =
|
||||
std::chrono::duration<double>(now - last_time).count();
|
||||
|
||||
if (delta_seconds > 0.0 && delta_bytes > 0) {
|
||||
const double bps =
|
||||
(static_cast<double>(delta_bytes) * 8.0) / delta_seconds;
|
||||
if (bps > 0.0) {
|
||||
const double capped = (std::min)(
|
||||
bps,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)()));
|
||||
estimated_rate_bps = static_cast<uint32_t>(capped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (estimated_rate_bps > 0 && current_rate > 0) {
|
||||
// 70% old + 30% new for smoother display
|
||||
rate_bps = static_cast<uint32_t>(current_rate * 0.7 +
|
||||
estimated_rate_bps * 0.3);
|
||||
} else if (estimated_rate_bps > 0) {
|
||||
rate_bps = estimated_rate_bps;
|
||||
} else {
|
||||
rate_bps = current_rate;
|
||||
}
|
||||
}
|
||||
|
||||
state->file_send_rate_bps_ = rate_bps;
|
||||
state->file_send_last_bytes_ = ack.acked_offset;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
state->file_send_last_update_time_ = now;
|
||||
}
|
||||
|
||||
// Update file transfer list: update progress and rate
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto& info : state->file_transfer_list_) {
|
||||
if (info.file_id == ack.file_id) {
|
||||
info.sent_bytes = ack.acked_offset;
|
||||
info.file_size = ack.total_size;
|
||||
info.rate_bps = rate_bps;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if transfer is completed
|
||||
if ((ack.flags & 0x01) != 0) {
|
||||
// Transfer completed - receiver has finished receiving the file
|
||||
// Reopen window if it was closed by user
|
||||
state->file_transfer_window_visible_ = true;
|
||||
state->file_sending_ = false; // Mark sending as finished
|
||||
LOG_INFO(
|
||||
"File transfer completed via ACK, file_id={}, total_size={}, "
|
||||
"acked_offset={}",
|
||||
ack.file_id, ack.total_size, ack.acked_offset);
|
||||
|
||||
// Update file transfer list: mark as completed
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
for (auto& info : state->file_transfer_list_) {
|
||||
if (info.file_id == ack.file_id) {
|
||||
info.status =
|
||||
Render::FileTransferState::FileTransferStatus::Completed;
|
||||
info.sent_bytes = ack.total_size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister file_id mapping after completion
|
||||
{
|
||||
if (props) {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render->file_id_to_props_mutex_);
|
||||
render->file_id_to_props_.erase(ack.file_id);
|
||||
} else {
|
||||
std::lock_guard<std::shared_mutex> lock(
|
||||
render->file_id_to_transfer_state_mutex_);
|
||||
render->file_id_to_transfer_state_.erase(ack.file_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Process next file in queue
|
||||
render->ProcessFileQueue(props);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
std::string json_str(data, size);
|
||||
RemoteAction remote_action;
|
||||
|
||||
@@ -316,24 +534,39 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(render->client_properties_mutex_);
|
||||
if (render->client_properties_.find(remote_id) !=
|
||||
render->client_properties_.end()) {
|
||||
// local
|
||||
auto props = render->client_properties_.find(remote_id)->second;
|
||||
if (remote_action.type == ControlType::host_infomation &&
|
||||
props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
|
||||
if (remote_action.type == ControlType::host_infomation) {
|
||||
if (render->client_properties_.find(remote_id) !=
|
||||
render->client_properties_.end()) {
|
||||
// client mode
|
||||
auto props = render->client_properties_.find(remote_id)->second;
|
||||
if (props && props->remote_host_name_.empty()) {
|
||||
props->remote_host_name_ = std::string(remote_action.i.host_name,
|
||||
remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]", props->remote_host_name_);
|
||||
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
props->display_info_list_.push_back(
|
||||
DisplayInfo(remote_action.i.display_list[i],
|
||||
remote_action.i.left[i], remote_action.i.top[i],
|
||||
remote_action.i.right[i], remote_action.i.bottom[i]));
|
||||
}
|
||||
}
|
||||
FreeRemoteAction(remote_action);
|
||||
} else {
|
||||
// server mode
|
||||
render->connection_host_names_[remote_id] = std::string(
|
||||
remote_action.i.host_name, remote_action.i.host_name_size);
|
||||
LOG_INFO("Remote hostname: [{}]",
|
||||
render->connection_host_names_[remote_id]);
|
||||
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
props->display_info_list_.push_back(
|
||||
render->display_info_list_.push_back(
|
||||
DisplayInfo(remote_action.i.display_list[i],
|
||||
remote_action.i.left[i], remote_action.i.top[i],
|
||||
remote_action.i.right[i], remote_action.i.bottom[i]));
|
||||
}
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
FreeRemoteAction(remote_action);
|
||||
} else {
|
||||
// remote
|
||||
if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
|
||||
@@ -380,6 +613,10 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
render->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalServerClosed == status) {
|
||||
render->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalFingerprintMismatch == status) {
|
||||
render->signal_connected_ = false;
|
||||
LOG_ERROR("[{}] signal server fingerprint mismatch", client_id);
|
||||
render->config_center_->ClearDefaultCertFingerprint();
|
||||
}
|
||||
} else {
|
||||
if (client_id.rfind("C-", 0) != 0) {
|
||||
@@ -407,6 +644,9 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalServerClosed == status) {
|
||||
props->signal_connected_ = false;
|
||||
} else if (SignalStatus::SignalFingerprintMismatch == status) {
|
||||
props->signal_connected_ = false;
|
||||
LOG_ERROR("[{}] signal server fingerprint mismatch", remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,6 +668,49 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = render->display_info_list_.size();
|
||||
remote_action.i.display_list =
|
||||
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||
remote_action.i.left =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
render->display_info_list_[i].name);
|
||||
remote_action.i.display_list[i] =
|
||||
(char*)malloc(render->display_info_list_[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
render->display_info_list_[i].name.c_str(),
|
||||
render->display_info_list_[i].name.length());
|
||||
remote_action.i
|
||||
.display_list[i][render->display_info_list_[i].name.length()] =
|
||||
'\0';
|
||||
remote_action.i.left[i] = render->display_info_list_[i].left;
|
||||
remote_action.i.top[i] = render->display_info_list_[i].top;
|
||||
remote_action.i.right[i] = render->display_info_list_[i].right;
|
||||
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||
host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
||||
render->control_data_label_.c_str());
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
|
||||
if (!render->need_to_create_stream_window_ &&
|
||||
!render->client_properties_.empty()) {
|
||||
render->need_to_create_stream_window_ = true;
|
||||
@@ -444,12 +727,22 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
case ConnectionStatus::Closed: {
|
||||
props->connection_established_ = false;
|
||||
props->mouse_control_button_pressed_ = false;
|
||||
if (props->dst_buffer_ && props->stream_texture_) {
|
||||
memset(props->dst_buffer_, 0, props->dst_buffer_capacity_);
|
||||
SDL_UpdateTexture(props->stream_texture_, NULL, props->dst_buffer_,
|
||||
props->texture_width_);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = true;
|
||||
}
|
||||
render->CleanSubStreamWindowProperties(props);
|
||||
|
||||
SDL_Event event;
|
||||
event.type = render->STREAM_REFRESH_EVENT;
|
||||
event.user.data1 = props.get();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -482,9 +775,54 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
render->need_to_send_host_info_ = true;
|
||||
{
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num = render->display_info_list_.size();
|
||||
remote_action.i.display_list =
|
||||
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||
remote_action.i.left =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.top =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.right =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
remote_action.i.bottom =
|
||||
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||
render->display_info_list_[i].name);
|
||||
remote_action.i.display_list[i] =
|
||||
(char*)malloc(render->display_info_list_[i].name.length() + 1);
|
||||
strncpy(remote_action.i.display_list[i],
|
||||
render->display_info_list_[i].name.c_str(),
|
||||
render->display_info_list_[i].name.length());
|
||||
remote_action.i
|
||||
.display_list[i][render->display_info_list_[i].name.length()] =
|
||||
'\0';
|
||||
remote_action.i.left[i] = render->display_info_list_[i].left;
|
||||
remote_action.i.top[i] = render->display_info_list_[i].top;
|
||||
remote_action.i.right[i] = render->display_info_list_[i].right;
|
||||
remote_action.i.bottom[i] = render->display_info_list_[i].bottom;
|
||||
}
|
||||
|
||||
std::string host_name = GetHostName();
|
||||
remote_action.type = ControlType::host_infomation;
|
||||
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||
host_name.size());
|
||||
remote_action.i.host_name[host_name.size()] = '\0';
|
||||
remote_action.i.host_name_size = host_name.size();
|
||||
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(render->peer_, msg.data(), msg.size(),
|
||||
render->control_data_label_.c_str());
|
||||
FreeRemoteAction(remote_action);
|
||||
}
|
||||
|
||||
render->need_to_create_server_window_ = true;
|
||||
render->is_server_mode_ = true;
|
||||
render->start_screen_capturer_ = true;
|
||||
render->start_speaker_capturer_ = true;
|
||||
render->remote_client_id_ = remote_id;
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
@@ -507,11 +845,13 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
kv.second == ConnectionStatus::Failed ||
|
||||
kv.second == ConnectionStatus::Disconnected;
|
||||
})) {
|
||||
render->need_to_destroy_server_window_ = true;
|
||||
render->is_server_mode_ = false;
|
||||
render->start_screen_capturer_ = false;
|
||||
render->start_speaker_capturer_ = false;
|
||||
render->start_mouse_controller_ = false;
|
||||
render->start_keyboard_capturer_ = false;
|
||||
render->need_to_send_host_info_ = false;
|
||||
render->remote_client_id_ = "";
|
||||
if (props) props->connection_established_ = false;
|
||||
if (render->audio_capture_) {
|
||||
render->StopSpeakerCapturer();
|
||||
@@ -536,11 +876,11 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
|
||||
}
|
||||
}
|
||||
|
||||
void Render::NetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data) {
|
||||
void Render::OnNetStatusReport(const char* client_id, size_t client_id_size,
|
||||
TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats,
|
||||
const char* user_id, const size_t user_id_size,
|
||||
void* user_data) {
|
||||
Render* render = (Render*)user_data;
|
||||
if (!render) {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "file_transfer.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "tinyfiledialogs.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -31,6 +41,97 @@ int LossRateDisplay(float loss_rate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string Render::OpenFileDialog(std::string title) {
|
||||
const char* path = tinyfd_openFileDialog(title.c_str(),
|
||||
"", // default path
|
||||
0, // number of filters
|
||||
nullptr, // filters
|
||||
nullptr, // filter description
|
||||
0 // no multiple selection
|
||||
);
|
||||
|
||||
return path ? path : "";
|
||||
}
|
||||
|
||||
void Render::ProcessSelectedFile(
|
||||
const std::string& path,
|
||||
const std::shared_ptr<SubStreamWindowProperties>& props,
|
||||
const std::string& file_label, const std::string& remote_id) {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileTransferState* file_transfer_state =
|
||||
props ? &props->file_transfer_ : &file_transfer_;
|
||||
|
||||
LOG_INFO("Selected file: {}", path.c_str());
|
||||
|
||||
std::filesystem::path file_path = std::filesystem::u8path(path);
|
||||
|
||||
// Get file size
|
||||
std::error_code ec;
|
||||
uint64_t file_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||
file_size = 0;
|
||||
}
|
||||
|
||||
// Add file to transfer list
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_state->file_transfer_list_mutex_);
|
||||
FileTransferState::FileTransferInfo info;
|
||||
info.file_name = file_path.filename().u8string();
|
||||
info.file_path = file_path; // Store full path for precise matching
|
||||
info.file_size = file_size;
|
||||
info.status = FileTransferState::FileTransferStatus::Queued;
|
||||
info.sent_bytes = 0;
|
||||
info.file_id = 0;
|
||||
info.rate_bps = 0;
|
||||
file_transfer_state->file_transfer_list_.push_back(info);
|
||||
}
|
||||
file_transfer_state->file_transfer_window_visible_ = true;
|
||||
|
||||
if (file_transfer_state->file_sending_.load()) {
|
||||
// Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(file_transfer_state->file_queue_mutex_);
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
queued_file.remote_id = remote_id;
|
||||
file_transfer_state->file_send_queue_.push(queued_file);
|
||||
queue_size = file_transfer_state->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO("File added to queue: {} ({} files in queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
} else {
|
||||
StartFileTransfer(props, file_path, file_label, remote_id);
|
||||
|
||||
if (file_transfer_state->file_sending_.load()) {
|
||||
} else {
|
||||
// Failed to start (race condition: another file started between
|
||||
// check and call) Add to queue
|
||||
size_t queue_size = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_state->file_queue_mutex_);
|
||||
FileTransferState::QueuedFile queued_file;
|
||||
queued_file.file_path = file_path;
|
||||
queued_file.file_label = file_label;
|
||||
queued_file.remote_id = remote_id;
|
||||
file_transfer_state->file_send_queue_.push(queued_file);
|
||||
queue_size = file_transfer_state->file_send_queue_.size();
|
||||
}
|
||||
LOG_INFO(
|
||||
"File added to queue after race condition: {} ({} files in "
|
||||
"queue)",
|
||||
file_path.filename().string().c_str(), queue_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
float button_width = title_bar_height_ * 0.8f;
|
||||
float button_height = title_bar_height_ * 0.8f;
|
||||
@@ -41,14 +142,14 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
if (props->control_bar_expand_) {
|
||||
ImGui::SetCursorPosX(props->is_control_bar_in_left_
|
||||
? props->control_window_width_ * 1.03f
|
||||
: props->control_window_width_ * 0.2f);
|
||||
: props->control_window_width_ * 0.17f);
|
||||
|
||||
ImDrawList* draw_list = ImGui::GetWindowDrawList();
|
||||
if (!props->is_control_bar_in_left_) {
|
||||
draw_list->AddLine(
|
||||
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.56f,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
|
||||
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
|
||||
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.56f,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
|
||||
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
|
||||
IM_COL32(178, 178, 178, 255), 2.0f);
|
||||
}
|
||||
@@ -73,8 +174,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
remote_action.d = i;
|
||||
if (props->connection_status_ == ConnectionStatus::Connected) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
props->display_selectable_hovered_ = ImGui::IsWindowHovered();
|
||||
@@ -154,8 +255,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
remote_action.type = ControlType::audio_capture;
|
||||
remote_action.a = props->audio_capture_button_pressed_;
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->data_label_.c_str());
|
||||
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +276,16 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
line_thickness);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
std::string open_folder = ICON_FA_FOLDER_OPEN;
|
||||
if (ImGui::Button(open_folder.c_str(),
|
||||
ImVec2(button_width, button_height))) {
|
||||
std::string title =
|
||||
localization::select_file[localization_language_index_];
|
||||
std::string path = OpenFileDialog(title);
|
||||
ProcessSelectedFile(path, props, file_label_);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
// net traffic stats button
|
||||
bool button_color_style_pushed = false;
|
||||
@@ -238,9 +349,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
if (props->is_control_bar_in_left_) {
|
||||
draw_list->AddLine(
|
||||
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.2f,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
|
||||
ImGui::GetCursorScreenPos().y + button_height * 0.2f),
|
||||
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.2f,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x + button_height * 0.13f,
|
||||
ImGui::GetCursorScreenPos().y + button_height * 0.8f),
|
||||
IM_COL32(178, 178, 178, 255), 2.0f);
|
||||
}
|
||||
@@ -250,7 +361,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
float expand_button_pos_x =
|
||||
props->control_bar_expand_ ? (props->is_control_bar_in_left_
|
||||
? props->control_window_width_ * 1.91f
|
||||
? props->control_window_width_ * 1.917f
|
||||
: props->control_window_width_ * 0.03f)
|
||||
: (props->is_control_bar_in_left_
|
||||
? props->control_window_width_ * 1.02f
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
#define NEW_VERSION_ICON_RENDER_TIME_INTERVAL 2000
|
||||
constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
|
||||
constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -15,14 +16,28 @@ int Render::TitleBar(bool main_window) {
|
||||
float title_bar_button_width = title_bar_button_width_;
|
||||
float title_bar_button_height = title_bar_button_height_;
|
||||
if (main_window) {
|
||||
title_bar_width = io.DisplaySize.x;
|
||||
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
|
||||
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f);
|
||||
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
|
||||
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
|
||||
title_bar_height_ = title_bar_height;
|
||||
title_bar_button_width_ = title_bar_button_width;
|
||||
title_bar_button_height_ = title_bar_button_height;
|
||||
// When the main window is minimized, Dear ImGui may report DisplaySize as
|
||||
// (0, 0). Do not overwrite shared title-bar metrics with zeros, otherwise
|
||||
// stream/server windows (which reuse these metrics) will lose their title
|
||||
// bars and appear collapsed.
|
||||
if (io.DisplaySize.x > 0.0f && io.DisplaySize.y > 0.0f) {
|
||||
title_bar_width = io.DisplaySize.x;
|
||||
title_bar_height = io.DisplaySize.y * TITLE_BAR_HEIGHT;
|
||||
title_bar_height_padding = io.DisplaySize.y * (TITLE_BAR_HEIGHT + 0.01f);
|
||||
title_bar_button_width = io.DisplaySize.x * TITLE_BAR_BUTTON_WIDTH;
|
||||
title_bar_button_height = io.DisplaySize.y * TITLE_BAR_BUTTON_HEIGHT;
|
||||
|
||||
title_bar_height_ = title_bar_height;
|
||||
title_bar_button_width_ = title_bar_button_width;
|
||||
title_bar_button_height_ = title_bar_button_height;
|
||||
} else {
|
||||
// Keep using last known good values.
|
||||
title_bar_width = title_bar_width_;
|
||||
title_bar_height = title_bar_height_;
|
||||
title_bar_height_padding = title_bar_height_;
|
||||
title_bar_button_width = title_bar_button_width_;
|
||||
title_bar_button_height = title_bar_button_height_;
|
||||
}
|
||||
} else {
|
||||
title_bar_width = io.DisplaySize.x;
|
||||
title_bar_height = title_bar_button_height_;
|
||||
@@ -92,13 +107,11 @@ int Render::TitleBar(bool main_window) {
|
||||
|
||||
std::string about_str = localization::about[localization_language_index_];
|
||||
if (update_available_) {
|
||||
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const double now_time = ImGui::GetTime();
|
||||
|
||||
// every 2 seconds
|
||||
if (now_time - new_version_icon_last_trigger_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
|
||||
kNewVersionIconBlinkIntervalSec) {
|
||||
show_new_version_icon_ = true;
|
||||
new_version_icon_render_start_time_ = now_time;
|
||||
new_version_icon_last_trigger_time_ = now_time;
|
||||
@@ -106,9 +119,9 @@ int Render::TitleBar(bool main_window) {
|
||||
|
||||
// render for 1 second
|
||||
if (show_new_version_icon_) {
|
||||
about_str = about_str + " " + ICON_FA_TRIANGLE_EXCLAMATION;
|
||||
about_str = about_str + " " + ICON_FA_CIRCLE_ARROW_UP;
|
||||
if (now_time - new_version_icon_render_start_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
|
||||
kNewVersionIconBlinkOnTimeSec) {
|
||||
show_new_version_icon_ = false;
|
||||
}
|
||||
} else {
|
||||
@@ -137,13 +150,11 @@ int Render::TitleBar(bool main_window) {
|
||||
}
|
||||
|
||||
if (update_available_ && show_new_version_icon_in_menu_) {
|
||||
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const double now_time = ImGui::GetTime();
|
||||
|
||||
// every 2 seconds
|
||||
if (now_time - new_version_icon_last_trigger_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL) {
|
||||
kNewVersionIconBlinkIntervalSec) {
|
||||
show_new_version_icon_ = true;
|
||||
new_version_icon_render_start_time_ = now_time;
|
||||
new_version_icon_last_trigger_time_ = now_time;
|
||||
@@ -152,14 +163,13 @@ int Render::TitleBar(bool main_window) {
|
||||
// render for 1 second
|
||||
if (show_new_version_icon_) {
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
ImGui::SetCursorPos(
|
||||
ImVec2(bar_pos_x + title_bar_button_width * 0.15f,
|
||||
bar_pos_y - title_bar_button_width * 0.325f));
|
||||
ImGui::Text(ICON_FA_TRIANGLE_EXCLAMATION);
|
||||
ImGui::SetCursorPos(ImVec2(bar_pos_x + title_bar_button_width * 0.21f,
|
||||
bar_pos_y - title_bar_button_width * 0.24f));
|
||||
ImGui::Text(ICON_FA_CIRCLE_ARROW_UP);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
if (now_time - new_version_icon_render_start_time_ >=
|
||||
NEW_VERSION_ICON_RENDER_TIME_INTERVAL / 2) {
|
||||
kNewVersionIconBlinkOnTimeSec) {
|
||||
show_new_version_icon_ = false;
|
||||
}
|
||||
}
|
||||
@@ -187,6 +197,11 @@ int Render::TitleBar(bool main_window) {
|
||||
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
|
||||
if (ImGui::Button(window_minimize_button.c_str(),
|
||||
ImVec2(title_bar_button_width, title_bar_button_height))) {
|
||||
if (main_window) {
|
||||
last_main_minimize_request_tick_ = SDL_GetTicks();
|
||||
} else {
|
||||
last_stream_minimize_request_tick_ = SDL_GetTicks();
|
||||
}
|
||||
SDL_MinimizeWindow(main_window ? main_window_ : stream_window_);
|
||||
}
|
||||
draw_list->AddLine(
|
||||
|
||||
244
src/gui/windows/file_transfer_window.cpp
Normal file
244
src/gui/windows/file_transfer_window.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "IconsFontAwesome6.h"
|
||||
#include "layout.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
}
|
||||
|
||||
int BitrateDisplay(int bitrate) {
|
||||
int num_of_digits = CountDigits(bitrate);
|
||||
if (num_of_digits <= 3) {
|
||||
ImGui::Text("%d bps", bitrate);
|
||||
} else if (num_of_digits > 3 && num_of_digits <= 6) {
|
||||
ImGui::Text("%d kbps", bitrate / 1000);
|
||||
} else {
|
||||
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int Render::FileTransferWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
|
||||
|
||||
// Only show window if there are files in transfer list or currently
|
||||
// transferring
|
||||
std::vector<SubStreamWindowProperties::FileTransferInfo> file_list;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
|
||||
file_list = state->file_transfer_list_;
|
||||
}
|
||||
|
||||
// Sort file list: Sending first, then Completed, then Queued, then Failed
|
||||
std::sort(
|
||||
file_list.begin(), file_list.end(),
|
||||
[](const SubStreamWindowProperties::FileTransferInfo& a,
|
||||
const SubStreamWindowProperties::FileTransferInfo& b) {
|
||||
// Priority: Sending > Completed > Queued > Failed
|
||||
auto get_priority =
|
||||
[](SubStreamWindowProperties::FileTransferStatus status) {
|
||||
switch (status) {
|
||||
case SubStreamWindowProperties::FileTransferStatus::Sending:
|
||||
return 0;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Completed:
|
||||
return 1;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Queued:
|
||||
return 2;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Failed:
|
||||
return 3;
|
||||
}
|
||||
return 3;
|
||||
};
|
||||
return get_priority(a.status) < get_priority(b.status);
|
||||
});
|
||||
|
||||
// Only show window if file_transfer_window_visible_ is true
|
||||
// Window can be closed by user even during transfer
|
||||
// It will be reopened automatically when:
|
||||
// 1. A file transfer completes (in render_callback.cpp)
|
||||
// 2. A new file starts sending from queue (in render.cpp)
|
||||
if (!state->file_transfer_window_visible_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Position window at bottom-left of stream window
|
||||
// Adjust window size based on number of files
|
||||
float file_transfer_window_width = main_window_width_ * 0.6f;
|
||||
float file_transfer_window_height =
|
||||
main_window_height_ * 0.3f; // Dynamic height
|
||||
float pos_x = file_transfer_window_width * 0.05f;
|
||||
float pos_y = stream_window_height_ - file_transfer_window_height -
|
||||
file_transfer_window_width * 0.05;
|
||||
float same_line_width = file_transfer_window_width * 0.1f;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(file_transfer_window_width, file_transfer_window_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
// Set Chinese font for proper display
|
||||
if (stream_windows_system_chinese_font_) {
|
||||
ImGui::PushFont(stream_windows_system_chinese_font_);
|
||||
}
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 0.9f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
bool window_opened = true;
|
||||
if (ImGui::Begin(
|
||||
localization::file_transfer_progress[localization_language_index_]
|
||||
.c_str(),
|
||||
&window_opened,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoScrollbar)) {
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
// Close button handling
|
||||
if (!window_opened) {
|
||||
state->file_transfer_window_visible_ = false;
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Display file list
|
||||
if (file_list.empty()) {
|
||||
ImGui::Text("No files in transfer queue");
|
||||
} else {
|
||||
// Use a scrollable child window for the file list
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::BeginChild(
|
||||
"FileList", ImVec2(0, file_transfer_window_height * 0.75f),
|
||||
ImGuiChildFlags_Border, ImGuiWindowFlags_HorizontalScrollbar);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
|
||||
for (size_t i = 0; i < file_list.size(); ++i) {
|
||||
const auto& info = file_list[i];
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
// Status icon and file name
|
||||
const char* status_icon = "";
|
||||
ImVec4 status_color(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
const char* status_text = "";
|
||||
|
||||
switch (info.status) {
|
||||
case SubStreamWindowProperties::FileTransferStatus::Queued:
|
||||
status_icon = ICON_FA_CLOCK;
|
||||
status_color =
|
||||
ImVec4(0.5f, 0.6f, 0.7f, 1.0f); // Common blue-gray for queued
|
||||
status_text =
|
||||
localization::queued[localization_language_index_].c_str();
|
||||
break;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Sending:
|
||||
status_icon = ICON_FA_ARROW_UP;
|
||||
status_color = ImVec4(0.2f, 0.6f, 1.0f, 1.0f);
|
||||
status_text =
|
||||
localization::sending[localization_language_index_].c_str();
|
||||
break;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Completed:
|
||||
status_icon = ICON_FA_CHECK;
|
||||
status_color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f);
|
||||
status_text =
|
||||
localization::completed[localization_language_index_].c_str();
|
||||
break;
|
||||
case SubStreamWindowProperties::FileTransferStatus::Failed:
|
||||
status_icon = ICON_FA_XMARK;
|
||||
status_color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
|
||||
status_text =
|
||||
localization::failed[localization_language_index_].c_str();
|
||||
break;
|
||||
}
|
||||
|
||||
ImGui::TextColored(status_color, "%s", status_icon);
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s", info.file_name.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(status_color, "%s", status_text);
|
||||
|
||||
// Progress bar for sending files
|
||||
if (info.status ==
|
||||
SubStreamWindowProperties::FileTransferStatus::Sending &&
|
||||
info.file_size > 0) {
|
||||
float progress = static_cast<float>(info.sent_bytes) /
|
||||
static_cast<float>(info.file_size);
|
||||
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
|
||||
|
||||
float text_height = ImGui::GetTextLineHeight();
|
||||
ImGui::ProgressBar(
|
||||
progress, ImVec2(file_transfer_window_width * 0.5f, text_height),
|
||||
"");
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::Text("%.1f%%", progress * 100.0f);
|
||||
ImGui::SameLine();
|
||||
|
||||
float speed_x_pos = file_transfer_window_width * 0.65f;
|
||||
ImGui::SetCursorPosX(speed_x_pos);
|
||||
BitrateDisplay(static_cast<int>(info.rate_bps));
|
||||
} else if (info.status ==
|
||||
SubStreamWindowProperties::FileTransferStatus::Completed) {
|
||||
// Show completed size
|
||||
char size_str[64];
|
||||
if (info.file_size < 1024) {
|
||||
snprintf(size_str, sizeof(size_str), "%llu B",
|
||||
(unsigned long long)info.file_size);
|
||||
} else if (info.file_size < 1024 * 1024) {
|
||||
snprintf(size_str, sizeof(size_str), "%.2f KB",
|
||||
info.file_size / 1024.0f);
|
||||
} else {
|
||||
snprintf(size_str, sizeof(size_str), "%.2f MB",
|
||||
info.file_size / (1024.0f * 1024.0f));
|
||||
}
|
||||
ImGui::Text("Size: %s", size_str);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::EndChild();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::End();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
// Pop Chinese font if it was pushed
|
||||
if (stream_windows_system_chinese_font_) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
} else {
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -214,20 +214,31 @@ int Render::SelfHostedServerWindow() {
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// {
|
||||
// ImGui::AlignTextToFramePadding();
|
||||
// ImGui::Text(
|
||||
// "%s",
|
||||
// localization::reset_cert_fingerprint[localization_language_index_]
|
||||
// .c_str());
|
||||
// ImGui::SameLine();
|
||||
// if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
// ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
|
||||
// } else {
|
||||
// ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
|
||||
// }
|
||||
// ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
|
||||
|
||||
// ShowSimpleFileBrowser();
|
||||
// }
|
||||
|
||||
{
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s", localization::self_hosted_server_certificate_path
|
||||
if (ImGui::Button(localization::reset_cert_fingerprint
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 2.5f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.43f);
|
||||
.c_str())) {
|
||||
config_center_->ClearCertFingerprint();
|
||||
LOG_INFO("Certificate fingerprint cleared by user");
|
||||
}
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 3.8f);
|
||||
|
||||
ShowSimpleFileBrowser();
|
||||
}
|
||||
|
||||
if (stream_window_inited_) {
|
||||
|
||||
368
src/gui/windows/server_window.cpp
Normal file
368
src/gui/windows/server_window.cpp
Normal file
@@ -0,0 +1,368 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
}
|
||||
|
||||
void BitrateDisplay(uint32_t bitrate) {
|
||||
const int num_of_digits = CountDigits(static_cast<int>(bitrate));
|
||||
if (num_of_digits <= 3) {
|
||||
ImGui::Text("%u bps", bitrate);
|
||||
} else if (num_of_digits > 3 && num_of_digits <= 6) {
|
||||
ImGui::Text("%u kbps", bitrate / 1000);
|
||||
} else {
|
||||
ImGui::Text("%.1f mbps", bitrate / 1000000.0f);
|
||||
}
|
||||
}
|
||||
|
||||
std::string FormatBytes(uint64_t bytes) {
|
||||
char buf[64];
|
||||
if (bytes < 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%llu B", (unsigned long long)bytes);
|
||||
} else if (bytes < 1024ULL * 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f KB", bytes / 1024.0);
|
||||
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
} else {
|
||||
std::snprintf(buf, sizeof(buf), "%.2f GB",
|
||||
bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int Render::ServerWindow() {
|
||||
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
|
||||
ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
|
||||
ImGui::Begin("##server_window", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoScrollbar |
|
||||
ImGuiWindowFlags_NoScrollWithMouse);
|
||||
|
||||
server_window_title_bar_height_ = title_bar_height_;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::BeginChild(
|
||||
"ServerTitleBar",
|
||||
ImVec2(server_window_width_, server_window_title_bar_height_),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
|
||||
float server_title_bar_button_width = server_window_title_bar_height_;
|
||||
float server_title_bar_button_height = server_window_title_bar_height_;
|
||||
|
||||
// Collapse/expand toggle button (FontAwesome icon).
|
||||
{
|
||||
ImGui::SetCursorPos(ImVec2(0.0f, 0.0f));
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.1f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||||
ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const char* icon =
|
||||
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
|
||||
std::string toggle_label = std::string(icon) + "##server_toggle";
|
||||
if (ImGui::Button(toggle_label.c_str(),
|
||||
ImVec2(server_title_bar_button_width,
|
||||
server_title_bar_button_height))) {
|
||||
if (server_window_) {
|
||||
int w = 0;
|
||||
int h = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
SDL_GetWindowSize(server_window_, &w, &h);
|
||||
SDL_GetWindowPosition(server_window_, &x, &y);
|
||||
|
||||
if (server_window_collapsed_) {
|
||||
const int normal_h = server_window_normal_height_;
|
||||
SDL_SetWindowSize(server_window_, w, normal_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = false;
|
||||
} else {
|
||||
const int collapsed_h = (int)server_window_title_bar_height_;
|
||||
// Collapse upward: keep top edge stable.
|
||||
SDL_SetWindowSize(server_window_, w, collapsed_h);
|
||||
SDL_SetWindowPosition(server_window_, x, y);
|
||||
server_window_collapsed_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
RemoteClientInfoWindow();
|
||||
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Render::RemoteClientInfoWindow() {
|
||||
float remote_client_info_window_width = server_window_width_ * 0.8f;
|
||||
float remote_client_info_window_height =
|
||||
(server_window_height_ - server_window_title_bar_height_) * 0.9f;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f);
|
||||
ImGui::BeginChild(
|
||||
"RemoteClientInfoWindow",
|
||||
ImVec2(remote_client_info_window_width, remote_client_info_window_height),
|
||||
ImGuiChildFlags_Border,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
|
||||
|
||||
std::vector<std::string> remote_hostnames;
|
||||
remote_hostnames.reserve(connection_host_names_.size());
|
||||
for (const auto& kv : connection_host_names_) {
|
||||
remote_hostnames.push_back(kv.second);
|
||||
}
|
||||
|
||||
auto find_remote_id_by_hostname =
|
||||
[this](const std::string& hostname) -> std::string {
|
||||
for (const auto& kv : connection_host_names_) {
|
||||
if (kv.second == hostname) {
|
||||
return kv.first;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
if (!selected_server_remote_hostname_.empty()) {
|
||||
if (std::find(remote_hostnames.begin(), remote_hostnames.end(),
|
||||
selected_server_remote_hostname_) == remote_hostnames.end()) {
|
||||
selected_server_remote_hostname_.clear();
|
||||
selected_server_remote_id_.clear();
|
||||
}
|
||||
}
|
||||
if (selected_server_remote_hostname_.empty() && !remote_hostnames.empty()) {
|
||||
selected_server_remote_hostname_ = remote_hostnames.front();
|
||||
selected_server_remote_id_ =
|
||||
find_remote_id_by_hostname(selected_server_remote_hostname_);
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s",
|
||||
localization::controller[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
|
||||
const char* selected_preview = "-";
|
||||
if (!selected_server_remote_hostname_.empty()) {
|
||||
selected_preview = selected_server_remote_hostname_.c_str();
|
||||
} else if (!remote_client_id_.empty()) {
|
||||
selected_preview = remote_client_id_.c_str();
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(remote_client_info_window_width *
|
||||
(localization_language_index_ == 0 ? 0.68f : 0.62f));
|
||||
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
if (ImGui::BeginCombo("##server_remote_id", selected_preview)) {
|
||||
ImGui::SetWindowFontScale(localization_language_index_ == 0 ? 0.45f : 0.4f);
|
||||
for (int i = 0; i < static_cast<int>(remote_hostnames.size()); i++) {
|
||||
const bool selected =
|
||||
(remote_hostnames[i] == selected_server_remote_hostname_);
|
||||
if (ImGui::Selectable(remote_hostnames[i].c_str(), selected)) {
|
||||
selected_server_remote_hostname_ = remote_hostnames[i];
|
||||
selected_server_remote_id_ =
|
||||
find_remote_id_by_hostname(selected_server_remote_hostname_);
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
|
||||
if (!selected_server_remote_id_.empty()) {
|
||||
auto it = connection_status_.find(selected_server_remote_id_);
|
||||
const ConnectionStatus status = (it == connection_status_.end())
|
||||
? ConnectionStatus::Closed
|
||||
: it->second;
|
||||
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::connection_status[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_connected[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Connecting:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_connecting[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Disconnected:
|
||||
ImGui::Text("%s",
|
||||
localization::p2p_disconnected[localization_language_index_]
|
||||
.c_str());
|
||||
break;
|
||||
case ConnectionStatus::Failed:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_failed[localization_language_index_].c_str());
|
||||
break;
|
||||
case ConnectionStatus::Closed:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_closed[localization_language_index_].c_str());
|
||||
break;
|
||||
default:
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::p2p_failed[localization_language_index_].c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text(
|
||||
"%s", localization::file_transfer[localization_language_index_].c_str());
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button(
|
||||
localization::select_file[localization_language_index_].c_str())) {
|
||||
std::string title = localization::select_file[localization_language_index_];
|
||||
std::string path = OpenFileDialog(title);
|
||||
LOG_INFO("Selected file path: {}", path.c_str());
|
||||
|
||||
ProcessSelectedFile(path, nullptr, file_label_, selected_server_remote_id_);
|
||||
}
|
||||
|
||||
if (file_transfer_.file_transfer_window_visible_) {
|
||||
ImGui::SameLine();
|
||||
const bool is_sending = file_transfer_.file_sending_.load();
|
||||
|
||||
if (is_sending) {
|
||||
// Simple animation: cycle icon every 0.5s while sending.
|
||||
static const char* kFileTransferIcons[] = {ICON_FA_ANGLE_UP,
|
||||
ICON_FA_ANGLES_UP};
|
||||
const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) %
|
||||
(static_cast<int>(sizeof(kFileTransferIcons) /
|
||||
sizeof(kFileTransferIcons[0])));
|
||||
ImGui::Text("%s", kFileTransferIcons[icon_index]);
|
||||
} else {
|
||||
// Completed.
|
||||
ImGui::Text("%s", ICON_FA_CHECK);
|
||||
}
|
||||
|
||||
if (ImGui::IsItemHovered()) {
|
||||
const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load();
|
||||
const uint64_t total_bytes = file_transfer_.file_total_bytes_.load();
|
||||
const uint32_t rate_bps = file_transfer_.file_send_rate_bps_.load();
|
||||
|
||||
float progress = 0.0f;
|
||||
if (total_bytes > 0) {
|
||||
progress =
|
||||
static_cast<float>(sent_bytes) / static_cast<float>(total_bytes);
|
||||
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
|
||||
}
|
||||
|
||||
std::string current_file_name;
|
||||
const uint32_t current_file_id = file_transfer_.current_file_id_.load();
|
||||
if (current_file_id != 0) {
|
||||
std::lock_guard<std::mutex> lock(
|
||||
file_transfer_.file_transfer_list_mutex_);
|
||||
for (const auto& info : file_transfer_.file_transfer_list_) {
|
||||
if (info.file_id == current_file_id) {
|
||||
current_file_name = info.file_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
if (server_windows_system_chinese_font_) {
|
||||
ImGui::PushFont(server_windows_system_chinese_font_);
|
||||
}
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (!current_file_name.empty()) {
|
||||
ImGui::Text("%s", current_file_name.c_str());
|
||||
}
|
||||
if (total_bytes > 0) {
|
||||
const std::string sent_str = FormatBytes(sent_bytes);
|
||||
const std::string total_str = FormatBytes(total_bytes);
|
||||
ImGui::Text("%s / %s", sent_str.c_str(), total_str.c_str());
|
||||
}
|
||||
|
||||
const float text_height = ImGui::GetTextLineHeight();
|
||||
char overlay[32];
|
||||
std::snprintf(overlay, sizeof(overlay), "%.1f%%", progress * 100.0f);
|
||||
ImGui::ProgressBar(progress, ImVec2(180.0f, text_height), overlay);
|
||||
BitrateDisplay(rate_bps);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
if (server_windows_system_chinese_font_) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
float close_connection_button_width = server_window_width_ * 0.1f;
|
||||
float close_connection_button_height = remote_client_info_window_height;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1.0f, 0.5f, 0.5f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 5.0f);
|
||||
ImGui::SetWindowFontScale(font_scale);
|
||||
if (ImGui::Button(ICON_FA_XMARK, ImVec2(close_connection_button_width,
|
||||
close_connection_button_height))) {
|
||||
if (peer_ && !selected_server_remote_id_.empty()) {
|
||||
LeaveConnection(peer_, selected_server_remote_id_.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::PopStyleColor(3);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -138,6 +138,9 @@ int Render::StreamWindow() {
|
||||
UpdateRenderRect();
|
||||
|
||||
ControlWindow(props);
|
||||
|
||||
// Show file transfer window if needed
|
||||
FileTransferWindow(props);
|
||||
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
|
||||
@@ -233,6 +236,10 @@ int Render::StreamWindow() {
|
||||
UpdateRenderRect();
|
||||
|
||||
ControlWindow(props);
|
||||
|
||||
// Show file transfer window if needed
|
||||
FileTransferWindow(props);
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (!props->peer_) {
|
||||
|
||||
534
src/tools/clipboard.cpp
Normal file
534
src/tools/clipboard.cpp
Normal file
@@ -0,0 +1,534 @@
|
||||
|
||||
#include "clipboard.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif __linux__
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::atomic<bool> g_monitoring{false};
|
||||
std::thread g_monitor_thread;
|
||||
std::mutex g_monitor_mutex;
|
||||
std::string g_last_clipboard_text;
|
||||
int g_check_interval_ms = 100;
|
||||
Clipboard::OnClipboardChanged g_on_clipboard_changed;
|
||||
|
||||
#ifdef _WIN32
|
||||
HWND g_clipboard_wnd = nullptr;
|
||||
const char* g_clipboard_class_name = "CrossDeskClipboardMonitor";
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
Display* g_x11_display = nullptr;
|
||||
Atom g_clipboard_atom = None;
|
||||
Atom g_xfixes_selection_notify = None;
|
||||
#endif
|
||||
} // namespace crossdesk
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
#ifdef _WIN32
|
||||
std::string Clipboard::GetText() {
|
||||
if (!OpenClipboard(nullptr)) {
|
||||
LOG_ERROR("Clipboard::GetText: failed to open clipboard");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string result;
|
||||
HANDLE hData = GetClipboardData(CF_UNICODETEXT);
|
||||
if (hData != nullptr) {
|
||||
wchar_t* pszText = static_cast<wchar_t*>(GlobalLock(hData));
|
||||
if (pszText != nullptr) {
|
||||
int size_needed = WideCharToMultiByte(CP_UTF8, 0, pszText, -1, nullptr, 0,
|
||||
nullptr, nullptr);
|
||||
if (size_needed > 0) {
|
||||
// -1 because WideCharToMultiByte contains '\0'
|
||||
result.resize(size_needed - 1);
|
||||
WideCharToMultiByte(CP_UTF8, 0, pszText, -1, &result[0], size_needed,
|
||||
nullptr, nullptr);
|
||||
}
|
||||
GlobalUnlock(hData);
|
||||
}
|
||||
}
|
||||
|
||||
CloseClipboard();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Clipboard::SetText(const std::string& text) {
|
||||
if (!OpenClipboard(nullptr)) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to open clipboard");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EmptyClipboard()) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to empty clipboard");
|
||||
CloseClipboard();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert UTF-8 string to wide char
|
||||
int size_needed =
|
||||
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0);
|
||||
if (size_needed <= 0) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to convert to wide char");
|
||||
CloseClipboard();
|
||||
return false;
|
||||
}
|
||||
|
||||
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size_needed * sizeof(wchar_t));
|
||||
if (hMem == nullptr) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to allocate memory");
|
||||
CloseClipboard();
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t* pszText = static_cast<wchar_t*>(GlobalLock(hMem));
|
||||
if (pszText == nullptr) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to lock memory");
|
||||
GlobalFree(hMem);
|
||||
CloseClipboard();
|
||||
return false;
|
||||
}
|
||||
|
||||
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, pszText, size_needed);
|
||||
GlobalUnlock(hMem);
|
||||
|
||||
if (SetClipboardData(CF_UNICODETEXT, hMem) == nullptr) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to set clipboard data");
|
||||
GlobalFree(hMem);
|
||||
CloseClipboard();
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseClipboard();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Clipboard::HasText() {
|
||||
if (!OpenClipboard(nullptr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool has_text = IsClipboardFormatAvailable(CF_UNICODETEXT) ||
|
||||
IsClipboardFormatAvailable(CF_TEXT);
|
||||
CloseClipboard();
|
||||
return has_text;
|
||||
}
|
||||
|
||||
#elif __APPLE__
|
||||
// macOS implementation is in clipboard_mac.mm
|
||||
#elif __linux__
|
||||
|
||||
std::string Clipboard::GetText() {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (display == nullptr) {
|
||||
LOG_ERROR("Clipboard::GetText: failed to open X display");
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string result;
|
||||
Window owner = XGetSelectionOwner(display, XA_PRIMARY);
|
||||
if (owner == None) {
|
||||
// Try using CLIPBOARD
|
||||
owner =
|
||||
XGetSelectionOwner(display, XInternAtom(display, "CLIPBOARD", False));
|
||||
if (owner == None) {
|
||||
XCloseDisplay(display);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Atom selection = XA_PRIMARY;
|
||||
Atom target = XInternAtom(display, "UTF8_STRING", False);
|
||||
if (target == None) {
|
||||
target = XA_STRING;
|
||||
}
|
||||
|
||||
XEvent event;
|
||||
Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0, 0,
|
||||
1, 1, 0, 0, 0);
|
||||
XSelectInput(display, window, PropertyChangeMask);
|
||||
|
||||
XConvertSelection(display, selection, target, XA_PRIMARY, window,
|
||||
CurrentTime);
|
||||
|
||||
// Wait for selection conversion to complete
|
||||
bool done = false;
|
||||
while (!done) {
|
||||
XNextEvent(display, &event);
|
||||
if (event.type == SelectionNotify) {
|
||||
if (event.xselection.property == None) {
|
||||
// Try using CLIPBOARD
|
||||
if (selection == XA_PRIMARY) {
|
||||
selection = XInternAtom(display, "CLIPBOARD", False);
|
||||
XConvertSelection(display, selection, target, XA_PRIMARY, window,
|
||||
CurrentTime);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Atom actual_type;
|
||||
int actual_format;
|
||||
unsigned long nitems;
|
||||
unsigned long bytes_after;
|
||||
unsigned char* data = nullptr;
|
||||
|
||||
if (XGetWindowProperty(display, window, XA_PRIMARY, 0, LONG_MAX / 4,
|
||||
False, AnyPropertyType, &actual_type,
|
||||
&actual_format, &nitems, &bytes_after,
|
||||
&data) == Success) {
|
||||
if (data != nullptr) {
|
||||
result = std::string(reinterpret_cast<char*>(data), nitems);
|
||||
XFree(data);
|
||||
}
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Clipboard::SetText(const std::string& text) {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (display == nullptr) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to open X display");
|
||||
return false;
|
||||
}
|
||||
|
||||
Window window = XCreateSimpleWindow(display, DefaultRootWindow(display), 0, 0,
|
||||
1, 1, 0, 0, 0);
|
||||
Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
|
||||
Atom utf8_string = XInternAtom(display, "UTF8_STRING", False);
|
||||
Atom targets = XInternAtom(display, "TARGETS", False);
|
||||
Atom xa_string = XA_STRING;
|
||||
|
||||
XSetSelectionOwner(display, clipboard, window, CurrentTime);
|
||||
if (XGetSelectionOwner(display, clipboard) != window) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to set selection owner");
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
return false;
|
||||
}
|
||||
|
||||
XChangeProperty(display, window, XA_PRIMARY, utf8_string, 8, PropModeReplace,
|
||||
reinterpret_cast<const unsigned char*>(text.c_str()),
|
||||
static_cast<int>(text.length()));
|
||||
|
||||
XEvent event;
|
||||
while (true) {
|
||||
XNextEvent(display, &event);
|
||||
if (event.type == SelectionRequest) {
|
||||
XSelectionRequestEvent* req = &event.xselectionrequest;
|
||||
XSelectionEvent se;
|
||||
se.type = SelectionNotify;
|
||||
se.display = req->display;
|
||||
se.requestor = req->requestor;
|
||||
se.selection = req->selection;
|
||||
se.time = req->time;
|
||||
se.target = req->target;
|
||||
se.property = req->property;
|
||||
|
||||
if (req->target == targets) {
|
||||
// Return supported formats
|
||||
Atom supported[] = {utf8_string, xa_string, targets};
|
||||
XChangeProperty(display, req->requestor, req->property, XA_ATOM, 32,
|
||||
PropModeReplace,
|
||||
reinterpret_cast<unsigned char*>(supported), 3);
|
||||
se.property = req->property;
|
||||
} else if (req->target == utf8_string || req->target == xa_string) {
|
||||
// Return text data
|
||||
XChangeProperty(display, req->requestor, req->property, req->target, 8,
|
||||
PropModeReplace,
|
||||
reinterpret_cast<const unsigned char*>(text.c_str()),
|
||||
static_cast<int>(text.length()));
|
||||
se.property = req->property;
|
||||
} else {
|
||||
se.property = None;
|
||||
}
|
||||
|
||||
XSendEvent(display, req->requestor, False, 0,
|
||||
reinterpret_cast<XEvent*>(&se));
|
||||
XSync(display, False);
|
||||
} else if (event.type == SelectionClear) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XDestroyWindow(display, window);
|
||||
XCloseDisplay(display);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Clipboard::HasText() {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
if (display == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Atom clipboard = XInternAtom(display, "CLIPBOARD", False);
|
||||
Window owner = XGetSelectionOwner(display, clipboard);
|
||||
if (owner == None) {
|
||||
owner = XGetSelectionOwner(display, XA_PRIMARY);
|
||||
}
|
||||
|
||||
XCloseDisplay(display);
|
||||
return owner != None;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
std::string Clipboard::GetText() {
|
||||
LOG_ERROR("Clipboard::GetText: unsupported platform");
|
||||
return "";
|
||||
}
|
||||
|
||||
bool Clipboard::SetText(const std::string& text) {
|
||||
LOG_ERROR("Clipboard::SetText: unsupported platform");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Clipboard::HasText() {
|
||||
LOG_ERROR("Clipboard::HasText: unsupported platform");
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void HandleClipboardChange() {
|
||||
if (!Clipboard::HasText()) {
|
||||
std::lock_guard<std::mutex> lock(g_monitor_mutex);
|
||||
if (!g_last_clipboard_text.empty()) {
|
||||
g_last_clipboard_text.clear();
|
||||
LOG_INFO("Clipboard content cleared");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::string current_text = Clipboard::GetText();
|
||||
|
||||
// Check if the content has changed
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_monitor_mutex);
|
||||
if (current_text != g_last_clipboard_text) {
|
||||
g_last_clipboard_text = current_text;
|
||||
if (!current_text.empty()) {
|
||||
if (g_on_clipboard_changed) {
|
||||
int ret = g_on_clipboard_changed(current_text.c_str(),
|
||||
current_text.length());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Clipboard callback returned error: {}", ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
LRESULT CALLBACK ClipboardWndProc(HWND hwnd, UINT uMsg, WPARAM wParam,
|
||||
LPARAM lParam) {
|
||||
if (uMsg == WM_CLIPBOARDUPDATE) {
|
||||
HandleClipboardChange();
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
|
||||
static void MonitorThreadFunc() {
|
||||
// Create a hidden window to receive clipboard messages
|
||||
WNDCLASSA wc = {0};
|
||||
wc.lpfnWndProc = ClipboardWndProc;
|
||||
wc.hInstance = GetModuleHandle(nullptr);
|
||||
wc.lpszClassName = g_clipboard_class_name;
|
||||
RegisterClassA(&wc);
|
||||
|
||||
g_clipboard_wnd = CreateWindowA(g_clipboard_class_name, nullptr, 0, 0, 0, 0,
|
||||
0, HWND_MESSAGE, nullptr, nullptr, nullptr);
|
||||
if (!g_clipboard_wnd) {
|
||||
LOG_ERROR("Failed to create clipboard monitor window");
|
||||
g_monitoring.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Register clipboard format listener
|
||||
if (!AddClipboardFormatListener(g_clipboard_wnd)) {
|
||||
LOG_ERROR("Failed to add clipboard format listener");
|
||||
DestroyWindow(g_clipboard_wnd);
|
||||
g_clipboard_wnd = nullptr;
|
||||
g_monitoring.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Clipboard event monitoring started (Windows)");
|
||||
|
||||
MSG msg;
|
||||
while (g_monitoring.load()) {
|
||||
BOOL ret = GetMessage(&msg, nullptr, 0, 0);
|
||||
if (ret == 0 || ret == -1) {
|
||||
break;
|
||||
}
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
RemoveClipboardFormatListener(g_clipboard_wnd);
|
||||
if (g_clipboard_wnd) {
|
||||
DestroyWindow(g_clipboard_wnd);
|
||||
g_clipboard_wnd = nullptr;
|
||||
}
|
||||
UnregisterClassA(g_clipboard_class_name, GetModuleHandle(nullptr));
|
||||
}
|
||||
|
||||
#elif __APPLE__
|
||||
// macOS use notification mechanism, need external functions
|
||||
extern void StartMacOSClipboardMonitoring();
|
||||
extern void StopMacOSClipboardMonitoring();
|
||||
|
||||
static void MonitorThreadFunc() { StartMacOSClipboardMonitoring(); }
|
||||
|
||||
#elif __linux__
|
||||
static void MonitorThreadFunc() {
|
||||
g_x11_display = XOpenDisplay(nullptr);
|
||||
if (!g_x11_display) {
|
||||
LOG_ERROR("Failed to open X display for clipboard monitoring");
|
||||
g_monitoring.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if XFixes extension is available
|
||||
int event_base, error_base;
|
||||
if (!XFixesQueryExtension(g_x11_display, &event_base, &error_base)) {
|
||||
LOG_WARN("XFixes extension not available, falling back to polling");
|
||||
XCloseDisplay(g_x11_display);
|
||||
g_x11_display = nullptr;
|
||||
// fallback to polling mode
|
||||
while (g_monitoring.load()) {
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(g_check_interval_ms));
|
||||
if (!g_monitoring.load()) {
|
||||
break;
|
||||
}
|
||||
HandleClipboardChange();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
g_clipboard_atom = XInternAtom(g_x11_display, "CLIPBOARD", False);
|
||||
g_xfixes_selection_notify =
|
||||
XInternAtom(g_x11_display, "XFIXES_SELECTION_NOTIFY", False);
|
||||
|
||||
// Create event window
|
||||
Window root = DefaultRootWindow(g_x11_display);
|
||||
Window event_window =
|
||||
XCreateSimpleWindow(g_x11_display, root, 0, 0, 1, 1, 0, 0, 0);
|
||||
|
||||
// Select events to monitor
|
||||
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom,
|
||||
XFixesSetSelectionOwnerNotifyMask |
|
||||
XFixesSelectionWindowDestroyNotifyMask |
|
||||
XFixesSelectionClientCloseNotifyMask);
|
||||
|
||||
LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
|
||||
|
||||
XEvent event;
|
||||
while (g_monitoring.load()) {
|
||||
XNextEvent(g_x11_display, &event);
|
||||
if (event.type == event_base + XFixesSelectionNotify) {
|
||||
HandleClipboardChange();
|
||||
}
|
||||
}
|
||||
|
||||
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0);
|
||||
XDestroyWindow(g_x11_display, event_window);
|
||||
XCloseDisplay(g_x11_display);
|
||||
g_x11_display = nullptr;
|
||||
}
|
||||
|
||||
#else
|
||||
// Fallback to polling mode for other platforms
|
||||
static void MonitorThreadFunc() {
|
||||
while (g_monitoring.load()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(g_check_interval_ms));
|
||||
if (!g_monitoring.load()) {
|
||||
break;
|
||||
}
|
||||
HandleClipboardChange();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void Clipboard::StartMonitoring(int check_interval_ms,
|
||||
OnClipboardChanged on_changed) {
|
||||
if (g_monitoring.load()) {
|
||||
LOG_WARN("Clipboard monitoring is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
g_check_interval_ms = check_interval_ms > 0 ? check_interval_ms : 100;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_monitor_mutex);
|
||||
g_on_clipboard_changed = on_changed;
|
||||
if (HasText()) {
|
||||
g_last_clipboard_text = GetText();
|
||||
} else {
|
||||
g_last_clipboard_text.clear();
|
||||
}
|
||||
}
|
||||
g_monitoring.store(true);
|
||||
|
||||
g_monitor_thread = std::thread(MonitorThreadFunc);
|
||||
LOG_INFO("Clipboard event monitoring started");
|
||||
}
|
||||
|
||||
void Clipboard::StopMonitoring() {
|
||||
if (!g_monitoring.load()) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_monitoring.store(false);
|
||||
|
||||
#ifdef _WIN32
|
||||
if (g_clipboard_wnd) {
|
||||
PostMessage(g_clipboard_wnd, WM_QUIT, 0, 0);
|
||||
}
|
||||
#elif __APPLE__
|
||||
StopMacOSClipboardMonitoring();
|
||||
#endif
|
||||
|
||||
if (g_monitor_thread.joinable()) {
|
||||
g_monitor_thread.join();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_monitor_mutex);
|
||||
g_last_clipboard_text.clear();
|
||||
g_on_clipboard_changed = nullptr;
|
||||
}
|
||||
|
||||
LOG_INFO("Clipboard monitoring stopped");
|
||||
}
|
||||
|
||||
bool Clipboard::IsMonitoring() { return g_monitoring.load(); }
|
||||
|
||||
} // namespace crossdesk
|
||||
38
src/tools/clipboard.h
Normal file
38
src/tools/clipboard.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-12-28
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CLIPBOARD_H_
|
||||
#define _CLIPBOARD_H_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class Clipboard {
|
||||
public:
|
||||
using OnClipboardChanged = std::function<int(const char* data, size_t size)>;
|
||||
|
||||
Clipboard() = default;
|
||||
~Clipboard() = default;
|
||||
|
||||
static std::string GetText();
|
||||
|
||||
static bool SetText(const std::string& text);
|
||||
|
||||
static bool HasText();
|
||||
|
||||
static void StartMonitoring(int check_interval_ms = 100,
|
||||
OnClipboardChanged on_changed = nullptr);
|
||||
|
||||
static void StopMonitoring();
|
||||
|
||||
static bool IsMonitoring();
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
120
src/tools/clipboard_mac.mm
Normal file
120
src/tools/clipboard_mac.mm
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-12-18
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "clipboard.h"
|
||||
|
||||
#include <AppKit/AppKit.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
extern std::atomic<bool> g_monitoring;
|
||||
extern std::mutex g_monitor_mutex;
|
||||
extern std::string g_last_clipboard_text;
|
||||
extern Clipboard::OnClipboardChanged g_on_clipboard_changed;
|
||||
|
||||
static CFRunLoopRef g_monitor_runloop = nullptr;
|
||||
|
||||
std::string Clipboard::GetText() {
|
||||
@autoreleasepool {
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
NSString* string = [pasteboard stringForType:NSPasteboardTypeString];
|
||||
if (string == nil) {
|
||||
return "";
|
||||
}
|
||||
return std::string([string UTF8String]);
|
||||
}
|
||||
}
|
||||
|
||||
bool Clipboard::SetText(const std::string& text) {
|
||||
@autoreleasepool {
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
[pasteboard clearContents];
|
||||
NSString* string = [NSString stringWithUTF8String:text.c_str()];
|
||||
if (string == nil) {
|
||||
LOG_ERROR("Clipboard::SetText: failed to create NSString");
|
||||
return false;
|
||||
}
|
||||
BOOL success = [pasteboard setString:string forType:NSPasteboardTypeString];
|
||||
return success == YES;
|
||||
}
|
||||
}
|
||||
|
||||
bool Clipboard::HasText() {
|
||||
@autoreleasepool {
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
NSArray* types = [pasteboard types];
|
||||
return [types containsObject:NSPasteboardTypeString];
|
||||
}
|
||||
}
|
||||
|
||||
extern void HandleClipboardChange();
|
||||
|
||||
void StartMacOSClipboardMonitoring() {
|
||||
@autoreleasepool {
|
||||
NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
|
||||
|
||||
// Store RunLoop reference for waking up
|
||||
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
|
||||
g_monitor_runloop = [runLoop getCFRunLoop];
|
||||
if (g_monitor_runloop) {
|
||||
CFRetain(g_monitor_runloop);
|
||||
}
|
||||
|
||||
// Track changeCount to detect clipboard changes
|
||||
// Use __block to allow modification inside the block
|
||||
__block NSInteger lastChangeCount = [pasteboard changeCount];
|
||||
|
||||
LOG_INFO("Clipboard event monitoring started (macOS)");
|
||||
|
||||
// Use a timer to periodically check changeCount
|
||||
// This is more reliable than NSPasteboardDidChangeNotification which may not be available
|
||||
NSTimer* timer =
|
||||
[NSTimer scheduledTimerWithTimeInterval:0.1
|
||||
repeats:YES
|
||||
block:^(NSTimer* timer) {
|
||||
if (!g_monitoring.load()) {
|
||||
[timer invalidate];
|
||||
return;
|
||||
}
|
||||
NSInteger currentChangeCount = [pasteboard changeCount];
|
||||
if (currentChangeCount != lastChangeCount) {
|
||||
lastChangeCount = currentChangeCount;
|
||||
HandleClipboardChange();
|
||||
}
|
||||
}];
|
||||
|
||||
while (g_monitoring.load()) {
|
||||
@autoreleasepool {
|
||||
NSDate* date = [NSDate dateWithTimeIntervalSinceNow:0.1];
|
||||
[runLoop runMode:NSDefaultRunLoopMode beforeDate:date];
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
[timer invalidate];
|
||||
if (g_monitor_runloop) {
|
||||
CFRelease(g_monitor_runloop);
|
||||
g_monitor_runloop = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StopMacOSClipboardMonitoring() {
|
||||
// Wake up the RunLoop immediately so it can check g_monitoring and exit
|
||||
// This ensures the RunLoop exits promptly instead of waiting up to 0.1 seconds
|
||||
if (g_monitor_runloop) {
|
||||
CFRunLoopWakeUp(g_monitor_runloop);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
307
src/tools/file_transfer.cpp
Normal file
307
src/tools/file_transfer.cpp
Normal file
@@ -0,0 +1,307 @@
|
||||
#include "file_transfer.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
std::atomic<uint32_t> g_next_file_id{1};
|
||||
} // namespace
|
||||
|
||||
uint32_t FileSender::NextFileId() { return g_next_file_id.fetch_add(1); }
|
||||
|
||||
int FileSender::SendFile(const std::filesystem::path& path,
|
||||
const std::string& label, const SendFunc& send,
|
||||
std::size_t chunk_size, uint32_t file_id) {
|
||||
if (!send) {
|
||||
LOG_ERROR("FileSender::SendFile: send function is empty");
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(path, ec) ||
|
||||
!std::filesystem::is_regular_file(path, ec)) {
|
||||
LOG_ERROR("FileSender::SendFile: file [{}] not found or not regular",
|
||||
path.string().c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t total_size = std::filesystem::file_size(path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("FileSender::SendFile: failed to get size of [{}]: {}",
|
||||
path.string().c_str(), ec.message().c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::ifstream ifs(path, std::ios::binary);
|
||||
if (!ifs.is_open()) {
|
||||
LOG_ERROR("FileSender::SendFile: failed to open [{}]",
|
||||
path.string().c_str());
|
||||
return -1;
|
||||
}
|
||||
LOG_INFO("FileSender send file {}, total size {}, file_id={}",
|
||||
path.string().c_str(), total_size, file_id);
|
||||
|
||||
if (file_id == 0) {
|
||||
file_id = NextFileId();
|
||||
}
|
||||
const uint32_t final_file_id = file_id;
|
||||
uint64_t offset = 0;
|
||||
bool is_first = true;
|
||||
std::string file_name = label.empty() ? path.filename().string() : label;
|
||||
|
||||
std::vector<char> buffer;
|
||||
buffer.resize(chunk_size);
|
||||
|
||||
while (ifs && offset < total_size) {
|
||||
uint64_t remaining = total_size - offset;
|
||||
uint32_t to_read =
|
||||
static_cast<uint32_t>(std::min<uint64_t>(remaining, chunk_size));
|
||||
|
||||
ifs.read(buffer.data(), static_cast<std::streamsize>(to_read));
|
||||
std::streamsize bytes_read = ifs.gcount();
|
||||
if (bytes_read <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
bool is_last = (offset + static_cast<uint64_t>(bytes_read) >= total_size);
|
||||
const std::string* name_ptr = is_first ? &file_name : nullptr;
|
||||
|
||||
std::vector<char> chunk = BuildChunk(
|
||||
final_file_id, offset, total_size, buffer.data(),
|
||||
static_cast<uint32_t>(bytes_read), name_ptr, is_first, is_last);
|
||||
|
||||
int ret = send(chunk.data(), chunk.size());
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("FileSender::SendFile: send failed for [{}], ret={}",
|
||||
path.string().c_str(), ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
offset += static_cast<uint64_t>(bytes_read);
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<char> FileSender::BuildChunk(uint32_t file_id, uint64_t offset,
|
||||
uint64_t total_size, const char* data,
|
||||
uint32_t data_size,
|
||||
const std::string* file_name,
|
||||
bool is_first, bool is_last) {
|
||||
FileChunkHeader header{};
|
||||
header.magic = kFileChunkMagic;
|
||||
header.file_id = file_id;
|
||||
header.offset = offset;
|
||||
header.total_size = total_size;
|
||||
header.chunk_size = data_size;
|
||||
header.name_len =
|
||||
(file_name && is_first) ? static_cast<uint16_t>(file_name->size()) : 0;
|
||||
header.flags = 0;
|
||||
if (is_first) header.flags |= 0x01;
|
||||
if (is_last) header.flags |= 0x02;
|
||||
|
||||
std::size_t total_size_bytes =
|
||||
sizeof(FileChunkHeader) + header.name_len + header.chunk_size;
|
||||
|
||||
std::vector<char> buffer;
|
||||
buffer.resize(total_size_bytes);
|
||||
|
||||
std::size_t offset_bytes = 0;
|
||||
memcpy(buffer.data() + offset_bytes, &header, sizeof(FileChunkHeader));
|
||||
offset_bytes += sizeof(FileChunkHeader);
|
||||
|
||||
if (header.name_len > 0 && file_name) {
|
||||
memcpy(buffer.data() + offset_bytes, file_name->data(), header.name_len);
|
||||
offset_bytes += header.name_len;
|
||||
}
|
||||
|
||||
if (header.chunk_size > 0 && data) {
|
||||
memcpy(buffer.data() + offset_bytes, data, header.chunk_size);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// ---------- FileReceiver ----------
|
||||
|
||||
FileReceiver::FileReceiver() : output_dir_(GetDefaultDesktopPath()) {}
|
||||
|
||||
FileReceiver::FileReceiver(const std::filesystem::path& output_dir)
|
||||
: output_dir_(output_dir) {
|
||||
std::error_code ec;
|
||||
if (!output_dir_.empty()) {
|
||||
std::filesystem::create_directories(output_dir_, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("FileReceiver: failed to create output dir [{}]: {}",
|
||||
output_dir_.string().c_str(), ec.message().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path FileReceiver::GetDefaultDesktopPath() {
|
||||
#ifdef _WIN32
|
||||
const char* home_env = std::getenv("USERPROFILE");
|
||||
#else
|
||||
const char* home_env = std::getenv("HOME");
|
||||
#endif
|
||||
if (!home_env) {
|
||||
return std::filesystem::path{};
|
||||
}
|
||||
|
||||
std::filesystem::path desktop_path =
|
||||
std::filesystem::path(home_env) / "Desktop";
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(desktop_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("FileReceiver: failed to create desktop directory [{}]: {}",
|
||||
desktop_path.string().c_str(), ec.message().c_str());
|
||||
}
|
||||
|
||||
return desktop_path;
|
||||
}
|
||||
|
||||
bool FileReceiver::OnData(const char* data, size_t size) {
|
||||
if (!data || size < sizeof(FileChunkHeader)) {
|
||||
LOG_ERROR("FileReceiver::OnData: invalid buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
FileChunkHeader header{};
|
||||
memcpy(&header, data, sizeof(FileChunkHeader));
|
||||
|
||||
if (header.magic != kFileChunkMagic) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t header_and_name =
|
||||
sizeof(FileChunkHeader) + static_cast<std::size_t>(header.name_len);
|
||||
if (size < header_and_name ||
|
||||
size < header_and_name + static_cast<std::size_t>(header.chunk_size)) {
|
||||
LOG_ERROR("FileReceiver::OnData: buffer too small for header + payload");
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* name_ptr = data + sizeof(FileChunkHeader);
|
||||
std::string file_name;
|
||||
const std::string* file_name_ptr = nullptr;
|
||||
if (header.name_len > 0) {
|
||||
file_name.assign(name_ptr,
|
||||
name_ptr + static_cast<std::size_t>(header.name_len));
|
||||
file_name_ptr = &file_name;
|
||||
}
|
||||
|
||||
const char* payload = data + header_and_name;
|
||||
std::size_t payload_size =
|
||||
static_cast<std::size_t>(header.chunk_size); // may be 0
|
||||
|
||||
return HandleChunk(header, payload, payload_size, file_name_ptr);
|
||||
}
|
||||
|
||||
bool FileReceiver::HandleChunk(const FileChunkHeader& header,
|
||||
const char* payload, size_t payload_size,
|
||||
const std::string* file_name) {
|
||||
auto it = contexts_.find(header.file_id);
|
||||
if (it == contexts_.end()) {
|
||||
// new file context must start with first chunk.
|
||||
if ((header.flags & 0x01) == 0) {
|
||||
LOG_ERROR("FileReceiver: received non-first chunk for unknown file_id={}",
|
||||
header.file_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
FileContext ctx;
|
||||
ctx.total_size = header.total_size;
|
||||
|
||||
std::string filename;
|
||||
if (file_name && !file_name->empty()) {
|
||||
filename = *file_name;
|
||||
} else {
|
||||
filename = "received_" + std::to_string(header.file_id);
|
||||
}
|
||||
|
||||
ctx.file_name = filename;
|
||||
|
||||
std::filesystem::path save_path = output_dir_.empty()
|
||||
? std::filesystem::path(filename)
|
||||
: output_dir_ / filename;
|
||||
|
||||
// if file exists, append timestamp.
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(save_path, ec)) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto ts = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch())
|
||||
.count();
|
||||
save_path = save_path.parent_path() /
|
||||
(save_path.stem().string() + "_" + std::to_string(ts) +
|
||||
save_path.extension().string());
|
||||
}
|
||||
|
||||
ctx.ofs.open(save_path, std::ios::binary | std::ios::trunc);
|
||||
if (!ctx.ofs.is_open()) {
|
||||
LOG_ERROR("FileReceiver: failed to open [{}] for writing",
|
||||
save_path.string().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
contexts_.emplace(header.file_id, std::move(ctx));
|
||||
it = contexts_.find(header.file_id);
|
||||
}
|
||||
|
||||
FileContext& ctx = it->second;
|
||||
|
||||
if (payload_size > 0 && payload) {
|
||||
ctx.ofs.seekp(static_cast<std::streamoff>(header.offset), std::ios::beg);
|
||||
ctx.ofs.write(payload, static_cast<std::streamsize>(payload_size));
|
||||
if (!ctx.ofs.good()) {
|
||||
LOG_ERROR("FileReceiver: write failed for file_id={}", header.file_id);
|
||||
return false;
|
||||
}
|
||||
ctx.received += static_cast<uint64_t>(payload_size);
|
||||
}
|
||||
|
||||
// Send ACK after processing chunk
|
||||
if (on_send_ack_) {
|
||||
FileTransferAck ack{};
|
||||
ack.magic = kFileAckMagic;
|
||||
ack.file_id = header.file_id;
|
||||
ack.acked_offset = header.offset + static_cast<uint64_t>(payload_size);
|
||||
ack.total_size = header.total_size;
|
||||
ack.flags = 0;
|
||||
|
||||
bool is_last = (header.flags & 0x02) != 0;
|
||||
if (is_last || ctx.received >= ctx.total_size) {
|
||||
ack.flags |= 0x01; // completed
|
||||
}
|
||||
|
||||
int ret = on_send_ack_(ack);
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("FileReceiver: failed to send ACK for file_id={}, ret={}",
|
||||
header.file_id, ret);
|
||||
}
|
||||
}
|
||||
|
||||
bool is_last = (header.flags & 0x02) != 0;
|
||||
if (is_last || ctx.received >= ctx.total_size) {
|
||||
ctx.ofs.close();
|
||||
|
||||
std::filesystem::path saved_path =
|
||||
output_dir_.empty() ? std::filesystem::path(ctx.file_name)
|
||||
: output_dir_ / ctx.file_name;
|
||||
|
||||
LOG_INFO("FileReceiver: file received complete, file_id={}, size={}",
|
||||
header.file_id, ctx.received);
|
||||
|
||||
contexts_.erase(header.file_id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
114
src/tools/file_transfer.h
Normal file
114
src/tools/file_transfer.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-12-18
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _FILE_TRANSFER_H_
|
||||
#define _FILE_TRANSFER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Magic constants for file transfer protocol
|
||||
constexpr uint32_t kFileChunkMagic = 0x4A4E544D; // 'JNTM'
|
||||
constexpr uint32_t kFileAckMagic = 0x4A4E5443; // 'JNTC'
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct FileChunkHeader {
|
||||
uint32_t magic; // magic to identify file-transfer chunks
|
||||
uint32_t file_id; // unique id per file transfer
|
||||
uint64_t offset; // offset in file
|
||||
uint64_t total_size; // total file size
|
||||
uint32_t chunk_size; // payload size in this chunk
|
||||
uint16_t name_len; // filename length (bytes), only set on first chunk
|
||||
uint8_t flags; // bit0: is_first, bit1: is_last, others reserved
|
||||
};
|
||||
|
||||
struct FileTransferAck {
|
||||
uint32_t magic; // magic to identify file-transfer ack
|
||||
uint32_t file_id; // must match FileChunkHeader.file_id
|
||||
uint64_t acked_offset; // received offset
|
||||
uint64_t total_size; // total file size
|
||||
uint32_t flags; // bit0: completed, bit1: error
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
class FileSender {
|
||||
public:
|
||||
using SendFunc = std::function<int(const char* data, size_t size)>;
|
||||
|
||||
public:
|
||||
FileSender() = default;
|
||||
|
||||
// generate a new file id
|
||||
static uint32_t NextFileId();
|
||||
|
||||
// synchronously send a file using the provided send function.
|
||||
// `path` : full path to the local file.
|
||||
// `label` : logical filename to send (usually path.filename()).
|
||||
// `send` : callback that pushes one encoded chunk into the data channel.
|
||||
// `file_id` : file id to use (0 means auto-generate).
|
||||
// Return 0 on success, <0 on error.
|
||||
int SendFile(const std::filesystem::path& path, const std::string& label,
|
||||
const SendFunc& send, std::size_t chunk_size = 64 * 1024,
|
||||
uint32_t file_id = 0);
|
||||
|
||||
// build a single encoded chunk buffer according to FileChunkHeader protocol.
|
||||
static std::vector<char> BuildChunk(uint32_t file_id, uint64_t offset,
|
||||
uint64_t total_size, const char* data,
|
||||
uint32_t data_size,
|
||||
const std::string* file_name,
|
||||
bool is_first, bool is_last);
|
||||
};
|
||||
|
||||
class FileReceiver {
|
||||
public:
|
||||
struct FileContext {
|
||||
std::string file_name;
|
||||
uint64_t total_size = 0;
|
||||
uint64_t received = 0;
|
||||
std::ofstream ofs;
|
||||
};
|
||||
|
||||
using OnFileComplete =
|
||||
std::function<void(const std::filesystem::path& saved_path)>;
|
||||
using OnSendAck = std::function<int(const FileTransferAck& ack)>;
|
||||
|
||||
public:
|
||||
// save to default desktop directory.
|
||||
FileReceiver();
|
||||
|
||||
// save to a specified directory.
|
||||
explicit FileReceiver(const std::filesystem::path& output_dir);
|
||||
|
||||
// process one received data buffer (one chunk).
|
||||
// return true if parsed and processed successfully, false otherwise.
|
||||
bool OnData(const char* data, size_t size);
|
||||
|
||||
void SetOnSendAck(OnSendAck cb) { on_send_ack_ = cb; }
|
||||
|
||||
const std::filesystem::path& OutputDir() const { return output_dir_; }
|
||||
|
||||
private:
|
||||
static std::filesystem::path GetDefaultDesktopPath();
|
||||
|
||||
bool HandleChunk(const FileChunkHeader& header, const char* payload,
|
||||
size_t payload_size, const std::string* file_name);
|
||||
|
||||
private:
|
||||
std::filesystem::path output_dir_;
|
||||
std::unordered_map<uint32_t, FileContext> contexts_;
|
||||
OnSendAck on_send_ack_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
Submodule submodules/minirtc updated: 9eebd7c4f3...27f721015b
17
xmake.lua
17
xmake.lua
@@ -33,6 +33,7 @@ add_requires("imgui v1.92.1-docking", {configs = {sdl3 = true, sdl3_renderer = t
|
||||
add_requires("openssl3 3.3.2", {system = false})
|
||||
add_requires("nlohmann_json 3.11.3")
|
||||
add_requires("cpp-httplib v0.26.0", {configs = {ssl = true}})
|
||||
add_requires("tinyfiledialogs 3.15.1")
|
||||
|
||||
if is_os("windows") then
|
||||
add_requires("libyuv", "miniaudio 0.11.21")
|
||||
@@ -69,6 +70,9 @@ target("common")
|
||||
set_kind("object")
|
||||
add_deps("rd_log")
|
||||
add_files("src/common/*.cpp")
|
||||
if is_os("macosx") then
|
||||
add_files("src/common/*.mm")
|
||||
end
|
||||
add_includedirs("src/common", {public = true})
|
||||
|
||||
target("path_manager")
|
||||
@@ -168,13 +172,22 @@ target("version_checker")
|
||||
add_files("src/version_checker/*.cpp")
|
||||
add_includedirs("src/version_checker", {public = true})
|
||||
|
||||
target("tools")
|
||||
set_kind("object")
|
||||
add_deps("rd_log")
|
||||
add_files("src/tools/*.cpp")
|
||||
if is_os("macosx") then
|
||||
add_files("src/tools/*.mm")
|
||||
end
|
||||
add_includedirs("src/tools", {public = true})
|
||||
|
||||
target("gui")
|
||||
set_kind("object")
|
||||
add_packages("libyuv")
|
||||
add_packages("libyuv", "tinyfiledialogs")
|
||||
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
|
||||
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
|
||||
"path_manager", "screen_capturer", "speaker_capturer",
|
||||
"device_controller", "thumbnail", "version_checker")
|
||||
"device_controller", "thumbnail", "version_checker", "tools")
|
||||
add_files("src/gui/*.cpp", "src/gui/panels/*.cpp", "src/gui/toolbars/*.cpp",
|
||||
"src/gui/windows/*.cpp")
|
||||
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
|
||||
|
||||
Reference in New Issue
Block a user