[feat] support multi-file transfer, refs #45

This commit is contained in:
dijunkun
2025-12-28 18:19:26 +08:00
parent eea37424c9
commit a3e564f160
8 changed files with 2835 additions and 2479 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -187,6 +187,17 @@ static std::vector<std::string> takes_effect_after_restart = {
"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"};
#if _WIN32
static std::vector<std::string> minimize_to_tray = {
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),

View File

@@ -12,6 +12,7 @@
#include "device_controller_factory.h"
#include "fa_regular_400.h"
#include "fa_solid_900.h"
#include "file_transfer.h"
#include "layout_relative.h"
#include "localization.h"
#include "platform.h"
@@ -1544,6 +1545,166 @@ void Render::CleanSubStreamWindowProperties(
}
}
void Render::StartFileTransfer(std::shared_ptr<SubStreamWindowProperties> props,
const std::filesystem::path& file_path,
const std::string& file_label) {
if (!props || !props->peer_) {
LOG_ERROR("StartFileTransfer: invalid props or peer");
return;
}
bool expected = false;
if (!props->file_sending_.compare_exchange_strong(expected, true)) {
// Already sending, this should not happen if called correctly
LOG_WARN(
"StartFileTransfer called but file_sending_ is already true, "
"file should have been queued: {}",
file_path.filename().string().c_str());
return;
}
auto peer = props->peer_;
auto props_weak = std::weak_ptr<SubStreamWindowProperties>(props);
Render* render_ptr = this;
std::thread([peer, file_path, file_label, props_weak, render_ptr]() {
auto props_locked = props_weak.lock();
if (!props_locked) {
return;
}
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());
props_locked->file_sending_ = false;
return;
}
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;
{
std::lock_guard<std::mutex> lock(props_locked->file_transfer_mutex_);
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());
FileSender sender;
uint32_t file_id = FileSender::NextFileId();
{
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_props_mutex_);
render_ptr->file_id_to_props_[file_id] = props_weak;
}
props_locked->current_file_id_ = file_id;
// Update file transfer list: mark as sending
// Find the queued file that matches the exact file path
{
std::lock_guard<std::mutex> lock(props_locked->file_transfer_list_mutex_);
for (auto& info : props_locked->file_transfer_list_) {
if (info.file_path == file_path &&
info.status ==
SubStreamWindowProperties::FileTransferStatus::Queued) {
info.status = SubStreamWindowProperties::FileTransferStatus::Sending;
info.file_id = file_id;
info.file_size = total_size;
info.sent_bytes = 0;
break;
}
}
}
props_locked->file_transfer_window_visible_ = true;
// Progress will be updated via ACK from receiver
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());
},
64 * 1024, file_id);
// file_sending_ should remain true until we receive the final ACK from
// receiver
auto props_locked_final = props_weak.lock();
if (props_locked_final) {
// On error, set file_sending_ to false immediately to allow next file
if (ret != 0) {
props_locked_final->file_sending_ = false;
props_locked_final->file_transfer_window_visible_ = false;
props_locked_final->file_sent_bytes_ = 0;
props_locked_final->file_total_bytes_ = 0;
props_locked_final->file_send_rate_bps_ = 0;
props_locked_final->current_file_id_ = 0;
// Unregister file_id mapping on error
{
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_props_mutex_);
render_ptr->file_id_to_props_.erase(file_id);
}
// Update file transfer list: mark as failed
{
std::lock_guard<std::mutex> lock(
props_locked_final->file_transfer_list_mutex_);
for (auto& info : props_locked_final->file_transfer_list_) {
if (info.file_id == file_id) {
info.status =
SubStreamWindowProperties::FileTransferStatus::Failed;
break;
}
}
}
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
file_path.string().c_str(), ret);
render_ptr->ProcessFileQueue(props_locked_final);
}
}
}).detach();
}
void Render::ProcessFileQueue(
std::shared_ptr<SubStreamWindowProperties> props) {
if (!props) {
return;
}
if (props->file_sending_.load()) {
return;
}
SubStreamWindowProperties::QueuedFile queued_file;
{
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
if (props->file_send_queue_.empty()) {
return;
}
queued_file = props->file_send_queue_.front();
props->file_send_queue_.pop();
}
LOG_INFO("Processing next file in queue: {}",
queued_file.file_path.string().c_str());
StartFileTransfer(props, queued_file.file_path, queued_file.file_label);
}
void Render::UpdateRenderRect() {
// std::shared_lock lock(client_properties_mutex_);
for (auto& [_, props] : client_properties_) {

View File

@@ -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>
@@ -129,16 +131,34 @@ class Render {
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::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;
bool file_transfer_completed_ = false;
std::atomic<uint32_t> current_file_id_{
0}; // Track current file transfer ID
std::atomic<uint32_t> current_file_id_{0};
struct QueuedFile {
std::filesystem::path file_path;
std::string file_label;
};
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_;
};
public:
@@ -280,6 +300,12 @@ 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);
void ProcessFileQueue(std::shared_ptr<SubStreamWindowProperties> props);
int AudioDeviceInit();
int AudioDeviceDestroy();

View File

@@ -361,27 +361,9 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
props->file_sent_bytes_ = ack.acked_offset;
props->file_total_bytes_ = ack.total_size;
// Check if transfer is completed
if ((ack.flags & 0x01) != 0) {
// Transfer completed - receiver has finished receiving the file
props->file_transfer_completed_ = true;
props->file_transfer_window_visible_ = true;
props->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);
// Unregister file_id mapping after completion
{
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_props_mutex_);
render->file_id_to_props_.erase(ack.file_id);
}
}
// Update rate calculation
auto now = std::chrono::steady_clock::now();
uint32_t rate_bps = 0;
{
std::lock_guard<std::mutex> lock(props->file_transfer_mutex_);
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
@@ -391,14 +373,64 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
if (elapsed >= 100) {
uint64_t bytes_sent_since_last =
ack.acked_offset - props->file_send_last_bytes_;
uint32_t rate_bps =
rate_bps =
static_cast<uint32_t>((bytes_sent_since_last * 8 * 1000) / elapsed);
props->file_send_rate_bps_ = rate_bps;
props->file_send_last_bytes_ = ack.acked_offset;
props->file_send_last_update_time_ = now;
} else {
rate_bps = props->file_send_rate_bps_.load();
}
}
// Update file transfer list: update progress and rate
{
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
for (auto& info : props->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
props->file_transfer_window_visible_ = true;
props->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(props->file_transfer_list_mutex_);
for (auto& info : props->file_transfer_list_) {
if (info.file_id == ack.file_id) {
info.status =
SubStreamWindowProperties::FileTransferStatus::Completed;
info.sent_bytes = ack.total_size;
break;
}
}
}
// Unregister file_id mapping after completion
{
std::lock_guard<std::shared_mutex> lock(
render->file_id_to_props_mutex_);
render->file_id_to_props_.erase(ack.file_id);
}
// Process next file in queue
render->ProcessFileQueue(props);
}
return;
}

View File

@@ -207,113 +207,67 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
if (!path.empty()) {
LOG_INFO("Selected file: {}", path.c_str());
// 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);
Render* render_ptr = this;
std::thread([peer, file_path, file_label, props_weak, render_ptr]() {
auto props_locked = props_weak.lock();
if (!props_locked) {
return;
}
// 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;
}
// 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;
}
// Add file to transfer list
{
std::lock_guard<std::mutex> lock(props->file_transfer_list_mutex_);
SubStreamWindowProperties::FileTransferInfo info;
info.file_name = file_path.filename().string();
info.file_path = file_path; // Store full path for precise matching
info.file_size = file_size;
info.status = SubStreamWindowProperties::FileTransferStatus::Queued;
info.sent_bytes = 0;
info.file_id = 0;
info.rate_bps = 0;
props->file_transfer_list_.push_back(info);
}
props->file_transfer_window_visible_ = true;
// 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;
if (props->file_sending_.load()) {
// Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(
props_locked->file_transfer_mutex_);
props_locked->file_sending_name_ = file_path.filename().string();
std::lock_guard<std::mutex> lock(props->file_queue_mutex_);
SubStreamWindowProperties::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
props->file_send_queue_.push(queued_file);
queue_size = props->file_send_queue_.size();
}
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 added to queue: {} ({} files in queue)",
file_path.filename().string().c_str(), queue_size);
} else {
StartFileTransfer(props, file_path, file_label);
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());
FileSender sender;
uint32_t file_id = FileSender::NextFileId();
{
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_props_mutex_);
render_ptr->file_id_to_props_[file_id] = props_weak;
}
props_locked->current_file_id_ = file_id;
// Progress will be updated via ACK from receiver
// Don't update file_sent_bytes_ here, let ACK control the progress
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());
},
64 * 1024, // chunk_size
file_id); // file_id
// Mark sending thread as finished, but don't set completion flag yet
// Completion will be set when we receive the final ACK from receiver
auto props_locked_final = props_weak.lock();
if (props_locked_final) {
props_locked_final->file_sending_ = false;
if (ret != 0) {
// On error, clean up immediately
props_locked_final->file_transfer_completed_ = false;
props_locked_final->file_transfer_window_visible_ = false;
props_locked_final->file_sent_bytes_ = 0;
props_locked_final->file_total_bytes_ = 0;
props_locked_final->file_send_rate_bps_ = 0;
props_locked_final->current_file_id_ = 0;
// Unregister file_id mapping on error
{
std::lock_guard<std::shared_mutex> lock(
render_ptr->file_id_to_props_mutex_);
render_ptr->file_id_to_props_.erase(file_id);
}
{
std::lock_guard<std::mutex> lock(
props_locked_final->file_transfer_mutex_);
props_locked_final->file_sending_name_ = "";
}
LOG_ERROR("FileSender::SendFile failed for [{}], ret={}",
file_path.string().c_str(), ret);
} else {
// On success, keep file_id mapping and wait for final ACK
// Don't set completion flag here - wait for ACK with completed
// flag
LOG_INFO("File send finished (waiting for ACK): {}",
file_path.string().c_str());
if (props->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(props->file_queue_mutex_);
SubStreamWindowProperties::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
props->file_send_queue_.push(queued_file);
queue_size = props->file_send_queue_.size();
}
LOG_INFO(
"File added to queue after race condition: {} ({} files in "
"queue)",
file_path.filename().string().c_str(), queue_size);
}
}).detach();
}
}
}

View File

@@ -30,47 +30,80 @@ int BitrateDisplay(int bitrate) {
int Render::FileTransferWindow(
std::shared_ptr<SubStreamWindowProperties>& props) {
if (!props->file_transfer_window_visible_ &&
!props->file_transfer_completed_) {
// 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(props->file_transfer_list_mutex_);
file_list = props->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 (!props->file_transfer_window_visible_) {
return 0;
}
ImGuiIO& io = ImGui::GetIO();
// Position window at bottom-left of stream window
float window_width = main_window_width_ * 0.35f;
float window_height = main_window_height_ * 0.25f;
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_;
}
// 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(window_width, window_height),
ImGuiCond_Always);
ImGui::SetNextWindowSize(
ImVec2(file_transfer_window_width, file_transfer_window_height),
ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 0.8f));
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(0.4f, 0.4f, 0.4f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
std::string window_title = "File Transfer";
ImGui::SetWindowFontScale(0.5f);
bool window_opened = true;
// ImGui::SetWindowFontScale(0.5f);
if (ImGui::Begin("FileTransferWindow", &window_opened,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings)) {
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);
@@ -78,89 +111,115 @@ int Render::FileTransferWindow(
// Close button handling
if (!window_opened) {
props->file_transfer_window_visible_ = false;
props->file_transfer_completed_ = false;
ImGui::End();
return 0;
}
bool is_sending = props->file_sending_.load();
uint64_t total = props->file_total_bytes_.load();
bool has_transfer = total > 0; // Check if there's an active transfer
// 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);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
if (props->file_transfer_completed_ && !is_sending) {
// Show completion message
ImGui::SetCursorPos(ImVec2(10, 30));
ImGui::TextColored(ImVec4(0.0f, 0.0f, 1.0f, 1.0f),
"%s File transfer completed!", ICON_FA_CHECK);
for (size_t i = 0; i < file_list.size(); ++i) {
const auto& info = file_list[i];
ImGui::PushID(static_cast<int>(i));
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());
// 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::SetCursorPos(ImVec2(10, 70));
if (ImGui::Button("OK", ImVec2(80, 25))) {
props->file_transfer_completed_ = false;
props->file_transfer_window_visible_ = false;
}
} else if (has_transfer && !props->file_transfer_completed_) {
// Show transfer progress (either sending or waiting for ACK)
uint64_t sent = props->file_sent_bytes_.load();
// Re-read total in case it was updated
uint64_t current_total = props->file_total_bytes_.load();
float progress = current_total > 0 ? static_cast<float>(sent) /
static_cast<float>(current_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 (current_total < 1024) {
snprintf(size_str, sizeof(size_str), "%llu B",
(unsigned long long)current_total);
} else if (current_total < 1024 * 1024) {
snprintf(size_str, sizeof(size_str), "%.2f KB",
current_total / 1024.0f);
} else {
snprintf(size_str, sizeof(size_str), "%.2f MB",
current_total / (1024.0f * 1024.0f));
}
ImGui::Text("Size: %s", size_str);
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);
} else {
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2);