mirror of
https://github.com/kunkundi/crossdesk.git
synced 2025-12-26 02:36:45 +08:00
[feat] add file transfer window to show file sending info
This commit is contained in:
@@ -123,6 +123,19 @@ class Render {
|
||||
int frame_count_ = 0;
|
||||
std::chrono::steady_clock::time_point last_time_;
|
||||
XNetTrafficStats net_traffic_stats_;
|
||||
|
||||
// File transfer progress
|
||||
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; // bytes per second
|
||||
std::string file_sending_name_ = "";
|
||||
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;
|
||||
bool file_transfer_completed_ = false;
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -173,6 +186,7 @@ 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);
|
||||
|
||||
private:
|
||||
int ConnectTo(const std::string& remote_id, const char* password,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
@@ -206,17 +209,134 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
// Send selected file over file data channel in a background thread.
|
||||
auto peer = props->peer_;
|
||||
props->file_sending_ = true;
|
||||
std::filesystem::path file_path = std::filesystem::path(path);
|
||||
std::string file_label = file_label_;
|
||||
auto props_weak = std::weak_ptr<SubStreamWindowProperties>(props);
|
||||
|
||||
std::thread([peer, file_path, file_label, props_weak]() {
|
||||
auto props_locked = props_weak.lock();
|
||||
if (!props_locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize file transfer progress
|
||||
std::error_code ec;
|
||||
uint64_t total_size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) {
|
||||
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Set file transfer status (atomic variables don't need mutex)
|
||||
props_locked->file_sending_ = true;
|
||||
props_locked->file_sent_bytes_ = 0;
|
||||
props_locked->file_total_bytes_ = total_size;
|
||||
props_locked->file_send_rate_bps_ = 0;
|
||||
props_locked->file_transfer_window_visible_ = true;
|
||||
props_locked->file_transfer_completed_ = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
props_locked->file_transfer_mutex_);
|
||||
props_locked->file_sending_name_ = file_path.filename().string();
|
||||
}
|
||||
props_locked->file_send_start_time_ =
|
||||
std::chrono::steady_clock::now();
|
||||
props_locked->file_send_last_update_time_ =
|
||||
props_locked->file_send_start_time_;
|
||||
props_locked->file_send_last_bytes_ = 0;
|
||||
|
||||
LOG_INFO(
|
||||
"File transfer started: {} ({} bytes), file_sending_={}, "
|
||||
"total_bytes_={}",
|
||||
file_path.filename().string(), total_size,
|
||||
props_locked->file_sending_.load(),
|
||||
props_locked->file_total_bytes_.load());
|
||||
|
||||
std::thread([peer, file_path, file_label]() {
|
||||
FileSender sender;
|
||||
auto last_progress_update = std::chrono::steady_clock::now();
|
||||
auto last_rate_update = std::chrono::steady_clock::now();
|
||||
uint64_t last_actual_sent_bytes = 0;
|
||||
|
||||
int ret = sender.SendFile(
|
||||
file_path, file_path.filename().string(),
|
||||
[peer, file_label](const char* buf, size_t sz) -> int {
|
||||
return SendReliableDataFrame(peer, buf, sz, file_label.c_str());
|
||||
[peer, file_label, props_weak, &last_progress_update,
|
||||
&last_rate_update, &last_actual_sent_bytes,
|
||||
total_size](const char* buf, size_t sz) -> int {
|
||||
int send_ret =
|
||||
SendReliableDataFrame(peer, buf, sz, file_label.c_str());
|
||||
if (send_ret == 0) {
|
||||
// Update progress periodically (every 50ms) by querying
|
||||
// actual sent bytes
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed_progress =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - last_progress_update)
|
||||
.count();
|
||||
|
||||
if (elapsed_progress >= 50) {
|
||||
// Query actual sent bytes from the transport layer
|
||||
uint64_t actual_sent_bytes =
|
||||
GetDataChannelSentBytes(peer, file_label.c_str());
|
||||
|
||||
auto props_locked = props_weak.lock();
|
||||
if (props_locked) {
|
||||
props_locked->file_sent_bytes_ = actual_sent_bytes;
|
||||
|
||||
// Update rate every 100ms
|
||||
auto elapsed_rate =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - last_rate_update)
|
||||
.count();
|
||||
|
||||
if (elapsed_rate >= 100) {
|
||||
std::lock_guard<std::mutex> lock(
|
||||
props_locked->file_transfer_mutex_);
|
||||
uint64_t bytes_sent_since_last =
|
||||
actual_sent_bytes - last_actual_sent_bytes;
|
||||
// Calculate rate in bits per second
|
||||
uint32_t rate_bps = static_cast<uint32_t>(
|
||||
(bytes_sent_since_last * 8 * 1000) / elapsed_rate);
|
||||
props_locked->file_send_rate_bps_ = rate_bps;
|
||||
last_actual_sent_bytes = actual_sent_bytes;
|
||||
last_rate_update = now;
|
||||
}
|
||||
}
|
||||
last_progress_update = now;
|
||||
}
|
||||
}
|
||||
return send_ret;
|
||||
});
|
||||
|
||||
// Reset file transfer progress and show completion
|
||||
auto props_locked_final = props_weak.lock();
|
||||
if (props_locked_final) {
|
||||
// Reset atomic variables first
|
||||
props_locked_final->file_sending_ = false;
|
||||
props_locked_final->file_sent_bytes_ = 0;
|
||||
props_locked_final->file_total_bytes_ = 0;
|
||||
props_locked_final->file_send_rate_bps_ = 0;
|
||||
|
||||
// Show completion window
|
||||
if (ret == 0) {
|
||||
props_locked_final->file_transfer_completed_ = true;
|
||||
props_locked_final->file_transfer_window_visible_ = true;
|
||||
} else {
|
||||
props_locked_final->file_transfer_completed_ = false;
|
||||
props_locked_final->file_transfer_window_visible_ = false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(
|
||||
props_locked_final->file_transfer_mutex_);
|
||||
// Keep file name for completion message
|
||||
if (ret != 0) {
|
||||
props_locked_final->file_sending_name_ = "";
|
||||
}
|
||||
}
|
||||
LOG_INFO("File transfer progress reset, completed={}", ret == 0);
|
||||
}
|
||||
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
|
||||
file_path.string().c_str(), ret);
|
||||
@@ -330,6 +450,16 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
|
||||
NetTrafficStats(props);
|
||||
} else {
|
||||
// Debug: log why NetTrafficStats is not being called
|
||||
static bool logged_once = false;
|
||||
if (!logged_once && props->file_sending_.load()) {
|
||||
LOG_INFO(
|
||||
"NetTrafficStats not called: button_pressed={}, "
|
||||
"control_bar_expand={}",
|
||||
props->net_traffic_stats_button_pressed_, props->control_bar_expand_);
|
||||
logged_once = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
169
src/gui/windows/file_transfer_window.cpp
Normal file
169
src/gui/windows/file_transfer_window.cpp
Normal file
@@ -0,0 +1,169 @@
|
||||
#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) {
|
||||
if (!props->file_transfer_window_visible_ &&
|
||||
!props->file_transfer_completed_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Position window at bottom-left of stream window
|
||||
float window_width = 400.0f;
|
||||
float window_height = 120.0f;
|
||||
float pos_x = 10.0f;
|
||||
float pos_y = 10.0f;
|
||||
|
||||
// Get stream window size and position
|
||||
ImVec2 stream_window_size =
|
||||
ImVec2(stream_window_width_, stream_window_height_);
|
||||
if (fullscreen_button_pressed_) {
|
||||
pos_y = stream_window_size.y - window_height - 10.0f;
|
||||
} else {
|
||||
pos_y = stream_window_size.y - window_height - 10.0f - title_bar_height_;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.2f, 0.2f, 0.2f, 0.95f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_TitleBg, ImVec4(0.3f, 0.3f, 0.3f, 1.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
|
||||
|
||||
std::string window_title = "File Transfer";
|
||||
bool window_opened = true;
|
||||
|
||||
if (ImGui::Begin("FileTransferWindow", &window_opened,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoSavedSettings)) {
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
// Close button handling
|
||||
if (!window_opened) {
|
||||
props->file_transfer_window_visible_ = false;
|
||||
props->file_transfer_completed_ = false;
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
|
||||
bool is_sending = props->file_sending_.load();
|
||||
|
||||
if (props->file_transfer_completed_ && !is_sending) {
|
||||
// Show completion message
|
||||
ImGui::SetCursorPos(ImVec2(10, 30));
|
||||
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f),
|
||||
"%s File transfer completed!", ICON_FA_CHECK);
|
||||
|
||||
std::string file_name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_mutex_);
|
||||
file_name = props->file_sending_name_;
|
||||
}
|
||||
if (!file_name.empty()) {
|
||||
ImGui::SetCursorPos(ImVec2(10, 50));
|
||||
ImGui::Text("File: %s", file_name.c_str());
|
||||
}
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(10, 70));
|
||||
if (ImGui::Button("OK", ImVec2(80, 25))) {
|
||||
props->file_transfer_completed_ = false;
|
||||
props->file_transfer_window_visible_ = false;
|
||||
}
|
||||
} else if (is_sending) {
|
||||
// Show transfer progress
|
||||
uint64_t sent = props->file_sent_bytes_.load();
|
||||
uint64_t total = props->file_total_bytes_.load();
|
||||
float progress =
|
||||
total > 0 ? static_cast<float>(sent) / static_cast<float>(total)
|
||||
: 0.0f;
|
||||
progress = (std::max)(0.0f, (std::min)(1.0f, progress));
|
||||
|
||||
// File name
|
||||
std::string file_name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->file_transfer_mutex_);
|
||||
file_name = props->file_sending_name_;
|
||||
}
|
||||
if (file_name.empty()) {
|
||||
file_name = "Sending...";
|
||||
}
|
||||
|
||||
ImGui::SetCursorPos(ImVec2(10, 30));
|
||||
ImGui::Text("File: %s", file_name.c_str());
|
||||
|
||||
// Progress bar
|
||||
ImGui::SetCursorPos(ImVec2(10, 50));
|
||||
ImGui::ProgressBar(progress, ImVec2(window_width - 40, 0), "");
|
||||
ImGui::SameLine(0, 5);
|
||||
ImGui::Text("%.1f%%", progress * 100.0f);
|
||||
|
||||
// Transfer rate and size info
|
||||
ImGui::SetCursorPos(ImVec2(10, 75));
|
||||
uint32_t rate_bps = props->file_send_rate_bps_.load();
|
||||
ImGui::Text("Speed: ");
|
||||
ImGui::SameLine();
|
||||
BitrateDisplay(static_cast<int>(rate_bps));
|
||||
|
||||
ImGui::SameLine(0, 20);
|
||||
// Format size display
|
||||
char size_str[64];
|
||||
if (total < 1024) {
|
||||
snprintf(size_str, sizeof(size_str), "%llu B",
|
||||
(unsigned long long)total);
|
||||
} else if (total < 1024 * 1024) {
|
||||
snprintf(size_str, sizeof(size_str), "%.2f KB", total / 1024.0f);
|
||||
} else {
|
||||
snprintf(size_str, sizeof(size_str), "%.2f MB",
|
||||
total / (1024.0f * 1024.0f));
|
||||
}
|
||||
ImGui::Text("Size: %s", size_str);
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::End();
|
||||
} else {
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
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_) {
|
||||
|
||||
Reference in New Issue
Block a user