diff --git a/src/gui/render.h b/src/gui/render.h index 31f8da8..0a2084b 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -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 file_sending_ = false; + std::atomic file_sent_bytes_ = 0; + std::atomic file_total_bytes_ = 0; + std::atomic 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& props); private: int ConnectTo(const std::string& remote_id, const char* password, diff --git a/src/gui/toolbars/control_bar.cpp b/src/gui/toolbars/control_bar.cpp index c326444..3751982 100644 --- a/src/gui/toolbars/control_bar.cpp +++ b/src/gui/toolbars/control_bar.cpp @@ -1,3 +1,6 @@ +#include +#include +#include #include #include #include @@ -206,17 +209,134 @@ int Render::ControlBar(std::shared_ptr& 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(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 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( + 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( + now - last_rate_update) + .count(); + + if (elapsed_rate >= 100) { + std::lock_guard 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( + (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 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& 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(); diff --git a/src/gui/windows/file_transfer_window.cpp b/src/gui/windows/file_transfer_window.cpp new file mode 100644 index 0000000..0264858 --- /dev/null +++ b/src/gui/windows/file_transfer_window.cpp @@ -0,0 +1,169 @@ +#include +#include + +#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& 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 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(sent) / static_cast(total) + : 0.0f; + progress = (std::max)(0.0f, (std::min)(1.0f, progress)); + + // File name + std::string file_name; + { + std::lock_guard 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(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 diff --git a/src/gui/windows/stream_window.cpp b/src/gui/windows/stream_window.cpp index ade804e..b6f3a4a 100644 --- a/src/gui/windows/stream_window.cpp +++ b/src/gui/windows/stream_window.cpp @@ -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_) { diff --git a/submodules/minirtc b/submodules/minirtc index 95d2027..235b86b 160000 --- a/submodules/minirtc +++ b/submodules/minirtc @@ -1 +1 @@ -Subproject commit 95d2027835f2fdea1b7db3947cd0a41bd0c7d726 +Subproject commit 235b86b5bf84e8359d513dbf91512a02e7bd7a86