mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-19 18:33:00 +08:00
[feat] unify persistent file transfer notifications
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -34,10 +35,12 @@
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
#include "fa_solid_900.h"
|
||||
#include "features/file_transfer/transfer_notification_layout.h"
|
||||
#include "features/gallery/gallery_fullscreen_transition.h"
|
||||
#include "features/gallery/gallery_window_state.h"
|
||||
#include "features/gallery/gallery_workspace.h"
|
||||
#include "features/gallery/preview_bandwidth.h"
|
||||
#include "filesystem_utf8.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#ifndef __OHOS__
|
||||
@@ -961,7 +964,7 @@ struct GuiApplication::SlintUi {
|
||||
bool remember_password = false;
|
||||
enum class State { Queued, Running, AwaitingPassword, Finished };
|
||||
State state = State::Queued;
|
||||
size_t file_index = 0;
|
||||
uint32_t file_id = 0;
|
||||
std::weak_ptr<RemoteSession> session;
|
||||
std::chrono::steady_clock::time_point started{};
|
||||
};
|
||||
@@ -994,8 +997,13 @@ struct GuiApplication::SlintUi {
|
||||
std::make_shared<slint::VectorModel<ui::StreamTab>>();
|
||||
std::shared_ptr<slint::VectorModel<slint::SharedString>> display_model =
|
||||
std::make_shared<slint::VectorModel<slint::SharedString>>();
|
||||
std::shared_ptr<slint::VectorModel<ui::FileTransferEntry>> transfer_model =
|
||||
std::make_shared<slint::VectorModel<ui::FileTransferEntry>>();
|
||||
struct TransferNotification {
|
||||
std::optional<slint::ComponentHandle<ui::FileTransferNotification>> window;
|
||||
RemoteSession::FileTransferInfo info;
|
||||
bool dismissed = false;
|
||||
};
|
||||
std::map<uint32_t, TransferNotification> transfer_notifications;
|
||||
SDL_DisplayID transfer_display = 0;
|
||||
std::shared_ptr<slint::VectorModel<ui::NetworkStatsRow>> stats_model =
|
||||
std::make_shared<slint::VectorModel<ui::NetworkStatsRow>>();
|
||||
std::shared_ptr<slint::VectorModel<ui::ControllerEntry>> controller_model =
|
||||
@@ -1015,6 +1023,8 @@ struct GuiApplication::SlintUi {
|
||||
std::vector<std::string> recent_filter_groups;
|
||||
slint::Timer timer;
|
||||
slint::Timer server_file_dialog_timer;
|
||||
std::shared_ptr<void> file_dialog_lifetime = std::make_shared<int>(0);
|
||||
bool file_dialog_open = false;
|
||||
WindowDragState main_drag, gallery_drag;
|
||||
WindowDragState stream_drag;
|
||||
WindowDragState server_drag;
|
||||
@@ -1141,6 +1151,7 @@ int GuiApplication::Run() {
|
||||
ConfigureWindowsWindowIcons(ui_->main->window().win32_hwnd());
|
||||
#elif defined(__APPLE__)
|
||||
ConfigureMainWindowChrome(ui_->main->window().appkit_view());
|
||||
PrepareNativeFileDialog();
|
||||
#endif
|
||||
slint::run_event_loop(slint::EventLoopMode::RunUntilQuit);
|
||||
Cleanup();
|
||||
@@ -2017,14 +2028,12 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
transfers_.ProcessSelectedFile(
|
||||
OpenFileDialog(localization::select_file[localization_language_index_]),
|
||||
props, props->file_label_);
|
||||
});
|
||||
stream->on_close_file_transfer([this] {
|
||||
if (auto props = SelectedSession()) {
|
||||
props->file_transfer_.file_transfer_window_visible_ = false;
|
||||
}
|
||||
OpenFileDialog(localization::select_file[localization_language_index_],
|
||||
[this, props](std::string path) {
|
||||
if (!path.empty() && !props->closing_ &&
|
||||
props->connection_status_ == ConnectionStatus::Connected)
|
||||
transfers_.ProcessSelectedFile(path, props, props->file_label_);
|
||||
});
|
||||
});
|
||||
stream->on_can_drop_file(
|
||||
[](const slint::DataTransfer& data) { return data.has_plain_text(); });
|
||||
@@ -2140,32 +2149,37 @@ void GuiApplication::BindServerCallbacks() {
|
||||
if (selected_server_remote_id_.empty() || ui_->server_file_dialog_open) {
|
||||
return;
|
||||
}
|
||||
// A native file picker runs a nested event loop. Keep the recipient stable
|
||||
// if another controller connects or leaves while it is open.
|
||||
// Keep the recipient stable if another controller connects or leaves while
|
||||
// the asynchronous native picker is open.
|
||||
const std::string recipient = selected_server_remote_id_;
|
||||
ui_->server_file_dialog_open = true;
|
||||
(*ui_->server)->window().request_redraw();
|
||||
// TouchArea clears pressed only after its clicked callback returns. Allow
|
||||
// that release and a repaint before entering the blocking native picker.
|
||||
ui_->server_file_dialog_timer.start(
|
||||
slint::TimerMode::SingleShot, std::chrono::milliseconds(32),
|
||||
[this, recipient] {
|
||||
const auto connected = [this, &recipient] {
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
const auto found = connection_status_.find(recipient);
|
||||
return found != connection_status_.end() &&
|
||||
found->second == ConnectionStatus::Connected;
|
||||
};
|
||||
if (!connected()) {
|
||||
const auto select_file = [this, recipient] {
|
||||
const auto connected = [this, recipient] {
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
const auto found = connection_status_.find(recipient);
|
||||
return found != connection_status_.end() &&
|
||||
found->second == ConnectionStatus::Connected;
|
||||
};
|
||||
if (!connected()) {
|
||||
ui_->server_file_dialog_open = false;
|
||||
return;
|
||||
}
|
||||
OpenFileDialog(localization::select_file[localization_language_index_],
|
||||
[this, recipient, connected](std::string path) {
|
||||
ui_->server_file_dialog_open = false;
|
||||
return;
|
||||
}
|
||||
const auto path = OpenFileDialog(
|
||||
localization::select_file[localization_language_index_]);
|
||||
ui_->server_file_dialog_open = false;
|
||||
if (connected() && !path.empty())
|
||||
transfers_.ProcessSelectedFile(path, nullptr, file_label_, recipient);
|
||||
});
|
||||
if (connected() && !path.empty())
|
||||
transfers_.ProcessSelectedFile(path, nullptr, file_label_, recipient);
|
||||
});
|
||||
};
|
||||
#if defined(__APPLE__)
|
||||
// The native picker is already dispatched outside the clicked callback.
|
||||
select_file();
|
||||
#else
|
||||
// Let TouchArea release and repaint before the blocking native picker.
|
||||
ui_->server_file_dialog_timer.start(
|
||||
slint::TimerMode::SingleShot, std::chrono::milliseconds(32), select_file);
|
||||
#endif
|
||||
});
|
||||
server->on_disconnect_controller([this](slint::SharedString id) {
|
||||
CloseServerController(std::string(id));
|
||||
@@ -2287,6 +2301,7 @@ void GuiApplication::Tick() {
|
||||
SyncPlatformDialogs();
|
||||
SyncStreamWindow();
|
||||
SyncServerWindow();
|
||||
SyncFileTransferNotifications();
|
||||
}
|
||||
|
||||
void GuiApplication::PollUpdateCheck() {
|
||||
@@ -3240,7 +3255,6 @@ void GuiApplication::SyncStreamWindow() {
|
||||
ui_->localized_language = -1;
|
||||
(*ui_->stream)->set_tabs(ui_->tab_model);
|
||||
(*ui_->stream)->set_displays(ui_->display_model);
|
||||
(*ui_->stream)->set_file_transfers(ui_->transfer_model);
|
||||
(*ui_->stream)->set_stats_rows(ui_->stats_model);
|
||||
BindStreamCallbacks();
|
||||
(*ui_->stream)->show();
|
||||
@@ -3535,60 +3549,6 @@ void GuiApplication::SyncStreamWindow() {
|
||||
: localization::connection_mode_relay
|
||||
[localization_language_index_]));
|
||||
|
||||
std::vector<RemoteSession::FileTransferInfo> file_list;
|
||||
{
|
||||
std::lock_guard lock(props->file_transfer_.file_transfer_list_mutex_);
|
||||
file_list = props->file_transfer_.file_transfer_list_;
|
||||
}
|
||||
const auto transfer_priority = [](RemoteSession::FileTransferStatus status) {
|
||||
switch (status) {
|
||||
case RemoteSession::FileTransferStatus::Sending:
|
||||
return 0;
|
||||
case RemoteSession::FileTransferStatus::Completed:
|
||||
return 1;
|
||||
case RemoteSession::FileTransferStatus::Queued:
|
||||
return 2;
|
||||
case RemoteSession::FileTransferStatus::Failed:
|
||||
return 3;
|
||||
}
|
||||
return 3;
|
||||
};
|
||||
std::stable_sort(file_list.begin(), file_list.end(),
|
||||
[&](const auto& left, const auto& right) {
|
||||
return transfer_priority(left.status) <
|
||||
transfer_priority(right.status);
|
||||
});
|
||||
|
||||
std::vector<ui::FileTransferEntry> transfers;
|
||||
transfers.reserve(file_list.size());
|
||||
for (const auto& info : file_list) {
|
||||
ui::FileTransferEntry item;
|
||||
item.name = UiText(info.file_name);
|
||||
const auto status_index = static_cast<int>(info.status);
|
||||
if (status_index == 0)
|
||||
item.status = UiText(localization::queued[localization_language_index_]);
|
||||
if (status_index == 1)
|
||||
item.status = UiText(localization::sending[localization_language_index_]);
|
||||
if (status_index == 2)
|
||||
item.status =
|
||||
UiText(localization::completed[localization_language_index_]);
|
||||
if (status_index == 3)
|
||||
item.status = UiText(localization::failed[localization_language_index_]);
|
||||
item.progress = status_index == 2 ? 1.0f
|
||||
: info.file_size == 0
|
||||
? 0.0f
|
||||
: std::clamp(static_cast<float>(info.sent_bytes) /
|
||||
static_cast<float>(info.file_size),
|
||||
0.0f, 1.0f);
|
||||
item.speed = UiText(FormatTransferRate(info.rate_bps));
|
||||
item.size = UiText(FormatFileSize(info.file_size));
|
||||
transfers.push_back(std::move(item));
|
||||
}
|
||||
ui::UpdateModelRows(*ui_->transfer_model, std::move(transfers));
|
||||
(*ui_->stream)
|
||||
->set_file_transfer_visible(
|
||||
props->file_transfer_.file_transfer_window_visible_);
|
||||
|
||||
const auto fps_now = std::chrono::steady_clock::now();
|
||||
bool refresh_stats = false;
|
||||
if (!props->net_traffic_stats_button_pressed_) {
|
||||
@@ -3872,7 +3832,7 @@ void GuiApplication::SyncServerWindow() {
|
||||
if (!show_controller_window && ui_->server) {
|
||||
(*ui_->server)->hide();
|
||||
(*ui_->server_bar)->hide();
|
||||
// Keep handles alive through a native file dialog's nested event loop.
|
||||
// Keep handles alive until the pending native file dialog completes.
|
||||
if (ui_->server_file_dialog_open) {
|
||||
server_window_collapsed_ = true;
|
||||
(*ui_->server_bar)->set_expanded(false);
|
||||
@@ -3940,37 +3900,177 @@ void GuiApplication::SyncServerWindow() {
|
||||
--ui_->server_native_window_attempts;
|
||||
}
|
||||
#endif
|
||||
auto& transfer = transfers_.global_state();
|
||||
const auto sent = transfer.file_sent_bytes_.load();
|
||||
const auto total = transfer.file_total_bytes_.load();
|
||||
(*ui_->server)
|
||||
->set_file_transfer_visible(transfer.file_transfer_window_visible_);
|
||||
(*ui_->server)->set_sending_file(transfer.file_sending_.load());
|
||||
(*ui_->server)
|
||||
->set_file_progress(total == 0 ? 0.0f
|
||||
: std::clamp(static_cast<float>(sent) /
|
||||
static_cast<float>(total),
|
||||
0.0f, 1.0f));
|
||||
(*ui_->server)
|
||||
->set_file_progress_text(
|
||||
UiText(FormatTransferRate(transfer.file_send_rate_bps_.load())));
|
||||
std::string current_file_name;
|
||||
const uint32_t current_file_id = transfer.current_file_id_.load();
|
||||
if (current_file_id != 0) {
|
||||
std::lock_guard lock(transfer.file_transfer_list_mutex_);
|
||||
for (const auto& info : transfer.file_transfer_list_) {
|
||||
if (info.file_id == current_file_id) {
|
||||
current_file_name = info.file_name;
|
||||
break;
|
||||
}
|
||||
(*ui_->server)->set_sending_file(transfers_.global_state().file_sending_.load());
|
||||
PositionServerWindows();
|
||||
}
|
||||
|
||||
void GuiApplication::SyncFileTransferNotifications() {
|
||||
using Status = RemoteSession::FileTransferStatus;
|
||||
using namespace transfer_notification;
|
||||
std::map<uint32_t, RemoteSession::FileTransferInfo> files;
|
||||
const auto collect = [&](FileTransferState& state) {
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
for (const auto& info : state.file_transfer_list_) files.emplace(info.file_id, info);
|
||||
};
|
||||
collect(transfers_.global_state());
|
||||
collect(transfers_.received_state());
|
||||
{
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (const auto& [_, props] : remote_sessions_) {
|
||||
if (props) collect(props->file_transfer_);
|
||||
}
|
||||
}
|
||||
(*ui_->server)->set_current_file_name(UiText(current_file_name));
|
||||
(*ui_->server)
|
||||
->set_file_size_text(total == 0 ? slint::SharedString{}
|
||||
: UiText(FormatFileSize(sent) + " / " +
|
||||
FormatFileSize(total)));
|
||||
PositionServerWindows();
|
||||
// DeskHub reserves each recipient's notification before a transfer slot is
|
||||
// available. Keep its queued window (and dismissal) alive until dispatched.
|
||||
std::set<uint32_t> pending_files;
|
||||
for (const auto& operation : ui_->batch_operations)
|
||||
if (operation.file_id &&
|
||||
operation.state != SlintUi::BatchOperation::State::Finished)
|
||||
pending_files.insert(operation.file_id);
|
||||
// Retain the last progress independently of session lifetime. Results stay
|
||||
// visible until dismissed, including after the connection is closed.
|
||||
for (const auto& [id, info] : files)
|
||||
ui_->transfer_notifications[id].info = info;
|
||||
std::erase_if(ui_->transfer_notifications, [&](auto& item) {
|
||||
if (!item.second.dismissed || files.count(item.first) ||
|
||||
pending_files.count(item.first)) return false;
|
||||
if (item.second.window) (*item.second.window)->hide();
|
||||
return true;
|
||||
});
|
||||
if (ui_->transfer_notifications.empty()) {
|
||||
ui_->transfer_display = 0;
|
||||
return;
|
||||
}
|
||||
if (!ui_->transfer_display) {
|
||||
const auto& anchor = ui_->stream && (*ui_->stream)->window().is_visible()
|
||||
? (*ui_->stream)->window()
|
||||
: ui_->gallery_shown ? ui_->wall->window() : ui_->main->window();
|
||||
ui_->transfer_display = DisplayForSlintWindow(anchor);
|
||||
}
|
||||
SDL_Rect bounds{};
|
||||
if (!SDL_GetDisplayUsableBounds(ui_->transfer_display, &bounds)) {
|
||||
ui_->transfer_display = SDL_GetPrimaryDisplay();
|
||||
if (!SDL_GetDisplayUsableBounds(ui_->transfer_display, &bounds)) return;
|
||||
}
|
||||
#if defined(__APPLE__)
|
||||
const float desktop_scale = 1;
|
||||
#else
|
||||
const float desktop_scale = std::max(0.01f, SDL_GetDisplayContentScale(ui_->transfer_display));
|
||||
#endif
|
||||
const float screen_x = bounds.x / desktop_scale;
|
||||
const float screen_y = bounds.y / desktop_scale;
|
||||
const float screen_width = bounds.w / desktop_scale;
|
||||
const float screen_height = bounds.h / desktop_scale;
|
||||
const auto layout = Fit(screen_width, screen_height);
|
||||
size_t slot = 0;
|
||||
for (auto& [id, notification] : ui_->transfer_notifications) {
|
||||
auto& info = notification.info;
|
||||
if (!files.count(id) && !pending_files.count(id) && info.status != Status::Completed &&
|
||||
info.status != Status::Failed) {
|
||||
info.status = Status::Failed;
|
||||
info.rate_bps = 0;
|
||||
}
|
||||
const bool completed = info.status == Status::Completed;
|
||||
const bool failed = info.status == Status::Failed;
|
||||
if (notification.dismissed || slot >= layout.capacity()) {
|
||||
if (notification.window) {
|
||||
(*notification.window)->hide();
|
||||
notification.window.reset();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const bool created = !notification.window;
|
||||
if (created) {
|
||||
notification.window.emplace(ui::FileTransferNotification::create());
|
||||
RegisterFontAwesome((*notification.window)->window());
|
||||
const auto dismiss = [this, id] {
|
||||
const auto found = ui_->transfer_notifications.find(id);
|
||||
if (found == ui_->transfer_notifications.end()) return;
|
||||
found->second.dismissed = true;
|
||||
if (found->second.window) (*found->second.window)->hide();
|
||||
};
|
||||
(*notification.window)->on_dismissed(dismiss);
|
||||
(*notification.window)->window().on_close_requested([dismiss] {
|
||||
dismiss();
|
||||
return slint::CloseRequestResponse::KeepWindowShown;
|
||||
});
|
||||
}
|
||||
auto& component = *notification.window;
|
||||
ui::FileTransferEntry entry;
|
||||
entry.name = UiText(info.file_name);
|
||||
switch (info.status) {
|
||||
case Status::Queued: entry.status = UiText(localization::queued[localization_language_index_]); break;
|
||||
case Status::Sending: entry.status = UiText(localization::sending[localization_language_index_]); break;
|
||||
case Status::Receiving: entry.status = UiText(localization::receiving[localization_language_index_]); break;
|
||||
case Status::Completed: entry.status = UiText(localization::completed[localization_language_index_]); break;
|
||||
case Status::Failed: entry.status = UiText(localization::failed[localization_language_index_]); break;
|
||||
}
|
||||
entry.progress = completed ? 1.0f : info.file_size == 0 ? 0.0f
|
||||
: std::clamp(static_cast<float>(info.sent_bytes) / info.file_size, 0.0f, 1.0f);
|
||||
entry.speed = UiText(FormatTransferRate(info.rate_bps));
|
||||
entry.size = UiText(FormatFileSize(info.sent_bytes) + " / " + FormatFileSize(info.file_size));
|
||||
component->set_entry(entry);
|
||||
auto display_id = info.remote_id;
|
||||
std::erase_if(display_id, [](unsigned char c) { return c < '0' || c > '9'; });
|
||||
component->set_remote_id(UiText(display_id));
|
||||
component->set_heading(UiText(info.receiving
|
||||
? localization::file_receiving_progress[localization_language_index_]
|
||||
: localization::file_sending_progress[localization_language_index_]));
|
||||
component->set_transferring(info.status == Status::Sending || info.status == Status::Receiving);
|
||||
component->set_completed(completed);
|
||||
component->set_failed(failed);
|
||||
auto& window = component->window();
|
||||
const auto size = window.size();
|
||||
const float scale = std::max(window.scale_factor(), 0.01f);
|
||||
if (created || std::abs(size.width / scale - layout.width) > 0.5f ||
|
||||
std::abs(size.height / scale - layout.height) > 0.5f) {
|
||||
window.set_size(slint::LogicalSize({layout.width, layout.height}));
|
||||
}
|
||||
const auto position = Place(layout, screen_x, screen_y, screen_width, screen_height, slot++);
|
||||
if (CanUseGlobalPointerPosition()) {
|
||||
const auto current = window.position();
|
||||
#if defined(__APPLE__)
|
||||
if (created || std::abs(current.x / scale - position.x) > 0.5f ||
|
||||
std::abs(current.y / scale - position.y) > 0.5f)
|
||||
window.set_position(slint::LogicalPosition({position.x, position.y}));
|
||||
#else
|
||||
const auto x = static_cast<int32_t>(std::lround(position.x * desktop_scale));
|
||||
const auto y = static_cast<int32_t>(std::lround(position.y * desktop_scale));
|
||||
if (created || current.x != x || current.y != y)
|
||||
window.set_position(slint::PhysicalPosition({x, y}));
|
||||
#endif
|
||||
}
|
||||
if (!window.is_visible()) {
|
||||
#if defined(__APPLE__)
|
||||
// Winit creates native windows on the next event-loop turn. Wait until
|
||||
// it exists so show() and focus restoration happen synchronously.
|
||||
if (!window.appkit_view()) continue;
|
||||
#elif _WIN32
|
||||
if (!window.win32_hwnd()) continue;
|
||||
#endif
|
||||
#if _WIN32
|
||||
// Tool windows stay out of Alt-Tab and must not interrupt remote input.
|
||||
const auto configure = [&] {
|
||||
const HWND hwnd = window.win32_hwnd();
|
||||
if (!hwnd) return;
|
||||
SetWindowLongPtr(hwnd, GWL_EXSTYLE,
|
||||
(GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) & ~WS_EX_APPWINDOW);
|
||||
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
|
||||
};
|
||||
configure();
|
||||
#endif
|
||||
#if defined(__APPLE__)
|
||||
ShowFileTransferNotification(window.appkit_view(), [&window] { window.show(); });
|
||||
#else
|
||||
component->show();
|
||||
#endif
|
||||
#if _WIN32
|
||||
configure();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (slot == 0) ui_->transfer_display = 0;
|
||||
}
|
||||
|
||||
bool GuiApplication::ValidateSelfHostedSettingsFromUi(bool gallery_window) {
|
||||
@@ -4490,13 +4590,27 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
|
||||
}
|
||||
}
|
||||
|
||||
std::string GuiApplication::OpenFileDialog(const std::string& title) {
|
||||
void GuiApplication::OpenFileDialog(const std::string& title,
|
||||
std::function<void(std::string)> completion) {
|
||||
if (!ui_ || exit_) return;
|
||||
if (ui_->file_dialog_open) {
|
||||
completion({});
|
||||
return;
|
||||
}
|
||||
ui_->file_dialog_open = true;
|
||||
std::weak_ptr<void> lifetime = ui_->file_dialog_lifetime;
|
||||
auto finished = [this, lifetime, completion = std::move(completion)](std::string path) {
|
||||
// Native callbacks can arrive after cancellation during application exit.
|
||||
if (lifetime.expired()) return;
|
||||
ui_->file_dialog_open = false;
|
||||
if (!exit_) completion(std::move(path));
|
||||
};
|
||||
#if defined(__APPLE__)
|
||||
return OpenNativeFileDialog(title);
|
||||
OpenNativeFileDialog(title, std::move(finished));
|
||||
#else
|
||||
const char* path =
|
||||
tinyfd_openFileDialog(title.c_str(), "", 0, nullptr, nullptr, 0);
|
||||
return path ? path : "";
|
||||
finished(path ? path : "");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -5080,36 +5194,66 @@ void GuiApplication::GalleryAction(const std::string& action,
|
||||
SyncGallery();
|
||||
return;
|
||||
}
|
||||
// Retain IDs before the modal file picker can process another UI event.
|
||||
std::string file;
|
||||
if (action == "file") {
|
||||
file = OpenFileDialog(
|
||||
std::string(tr("选择分发文件", "Select a file", "Выберите файл")));
|
||||
if (file.empty()) return;
|
||||
}
|
||||
for (const auto& id : ids) {
|
||||
if (action == "connect" && !value.empty()) {
|
||||
if (auto props = FindRemoteSession(id);
|
||||
props && props->connection_auth_.Get().password_required) {
|
||||
ui_->gallery_auth_id = id;
|
||||
show_connection_status_window_ = true;
|
||||
// Capture the selection before the asynchronous file picker opens.
|
||||
const auto enqueue = [this, ids, action, value](const std::string& file) {
|
||||
RemoteSession::FileTransferInfo queued_file;
|
||||
if (action == "file") {
|
||||
queued_file.file_path = PathFromUtf8(file);
|
||||
const auto name = queued_file.file_path.filename().u8string();
|
||||
queued_file.file_name.assign(name.begin(), name.end());
|
||||
std::error_code ec;
|
||||
if (std::filesystem::is_regular_file(queued_file.file_path, ec)) {
|
||||
const auto size = std::filesystem::file_size(queued_file.file_path, ec);
|
||||
if (!ec) queued_file.file_size = size;
|
||||
} else {
|
||||
queued_file.status = RemoteSession::FileTransferStatus::Failed;
|
||||
}
|
||||
if (ec) queued_file.status = RemoteSession::FileTransferStatus::Failed;
|
||||
}
|
||||
const bool pending =
|
||||
std::any_of(ui_->batch_operations.begin(),
|
||||
ui_->batch_operations.end(), [&](const auto& t) {
|
||||
return t.id == id && t.action == action &&
|
||||
(t.state == OperationState::Queued ||
|
||||
t.state == OperationState::Running ||
|
||||
t.state == OperationState::AwaitingPassword);
|
||||
});
|
||||
if (pending) continue;
|
||||
SlintUi::BatchOperation operation;
|
||||
operation.id = id;
|
||||
operation.action = action;
|
||||
operation.file = file;
|
||||
ui_->batch_operations.push_back(std::move(operation));
|
||||
for (const auto& id : ids) {
|
||||
if (action == "connect" && !value.empty()) {
|
||||
if (auto props = FindRemoteSession(id);
|
||||
props && props->connection_auth_.Get().password_required) {
|
||||
ui_->gallery_auth_id = id;
|
||||
show_connection_status_window_ = true;
|
||||
}
|
||||
}
|
||||
const bool pending =
|
||||
std::any_of(ui_->batch_operations.begin(),
|
||||
ui_->batch_operations.end(), [&](const auto& t) {
|
||||
return t.id == id && t.action == action &&
|
||||
(t.state == OperationState::Queued ||
|
||||
t.state == OperationState::Running ||
|
||||
t.state == OperationState::AwaitingPassword);
|
||||
});
|
||||
// Repeated file selections are distinct transfers, even to a busy peer.
|
||||
if (pending && action != "file") continue;
|
||||
SlintUi::BatchOperation operation;
|
||||
operation.id = id;
|
||||
operation.action = action;
|
||||
operation.file = file;
|
||||
if (action == "file") {
|
||||
operation.file_id = file_transfer::Session::NextFileId();
|
||||
auto& notification = ui_->transfer_notifications[operation.file_id];
|
||||
notification.info = queued_file;
|
||||
notification.info.file_id = operation.file_id;
|
||||
notification.info.remote_id = id;
|
||||
if (queued_file.status == RemoteSession::FileTransferStatus::Failed)
|
||||
operation.state = OperationState::Finished;
|
||||
}
|
||||
ui_->batch_operations.push_back(std::move(operation));
|
||||
}
|
||||
SyncGallery();
|
||||
};
|
||||
if (action == "file") {
|
||||
OpenFileDialog(std::string(tr("选择分发文件", "Select a file", "Выберите файл")),
|
||||
[enqueue](std::string file) {
|
||||
if (!file.empty()) enqueue(file);
|
||||
});
|
||||
} else {
|
||||
enqueue({});
|
||||
}
|
||||
return;
|
||||
}
|
||||
SyncGallery();
|
||||
}
|
||||
@@ -5525,19 +5669,9 @@ void GuiApplication::SyncGallery() {
|
||||
props->connection_status_ != ConnectionStatus::Connected) {
|
||||
operation.state = OperationState::Finished;
|
||||
} else if (operation.action == "file") {
|
||||
{
|
||||
std::lock_guard lock(props->file_transfer_.file_transfer_list_mutex_);
|
||||
operation.file_index =
|
||||
props->file_transfer_.file_transfer_list_.size();
|
||||
}
|
||||
transfers_.ProcessSelectedFile(operation.file, props,
|
||||
props->file_label_);
|
||||
{
|
||||
std::lock_guard lock(props->file_transfer_.file_transfer_list_mutex_);
|
||||
if (operation.file_index >=
|
||||
props->file_transfer_.file_transfer_list_.size())
|
||||
operation.state = OperationState::Finished;
|
||||
}
|
||||
if (!transfers_.ProcessSelectedFile(operation.file, props,
|
||||
props->file_label_, {}, operation.file_id))
|
||||
operation.state = OperationState::Finished;
|
||||
}
|
||||
operation.session = props;
|
||||
}
|
||||
@@ -5574,13 +5708,12 @@ void GuiApplication::SyncGallery() {
|
||||
else if (operation.action == "file") {
|
||||
std::lock_guard lock(props->file_transfer_.file_transfer_list_mutex_);
|
||||
const auto& files = props->file_transfer_.file_transfer_list_;
|
||||
if (operation.file_index >= files.size())
|
||||
operation.state = OperationState::Finished;
|
||||
else if (files[operation.file_index].status ==
|
||||
gui_detail::FileTransferState::FileTransferStatus::Completed)
|
||||
operation.state = OperationState::Finished;
|
||||
else if (files[operation.file_index].status ==
|
||||
gui_detail::FileTransferState::FileTransferStatus::Failed)
|
||||
const auto file = std::find_if(files.begin(), files.end(), [&](const auto& info) {
|
||||
return info.file_id == operation.file_id;
|
||||
});
|
||||
if (file == files.end() ||
|
||||
file->status == RemoteSession::FileTransferStatus::Completed ||
|
||||
file->status == RemoteSession::FileTransferStatus::Failed)
|
||||
operation.state = OperationState::Finished;
|
||||
}
|
||||
}
|
||||
@@ -5914,11 +6047,18 @@ void GuiApplication::Cleanup() {
|
||||
SDL_Quit();
|
||||
return;
|
||||
}
|
||||
ui_->file_dialog_lifetime.reset();
|
||||
#if defined(__APPLE__)
|
||||
CancelNativeFileDialog();
|
||||
#endif
|
||||
SaveGalleryWindowState();
|
||||
#if defined(__APPLE__)
|
||||
SetGalleryWindowFullscreen(false);
|
||||
#endif
|
||||
ui_->wall->hide();
|
||||
for (auto& [_, notification] : ui_->transfer_notifications)
|
||||
if (notification.window) (*notification.window)->hide();
|
||||
ui_->transfer_notifications.clear();
|
||||
ui_->timer.stop();
|
||||
ui_->server_file_dialog_timer.stop();
|
||||
ui_->video_timer.stop();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#define _GUI_APPLICATION_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -90,6 +91,7 @@ private:
|
||||
void SyncStreamKeyboardFocus();
|
||||
void SetStreamKeyboardFocus(bool focused);
|
||||
void SyncServerWindow();
|
||||
void SyncFileTransferNotifications();
|
||||
void SetServerPanelExpanded(bool expanded);
|
||||
void PositionServerWindows();
|
||||
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
|
||||
@@ -117,7 +119,8 @@ private:
|
||||
void Cleanup();
|
||||
|
||||
std::shared_ptr<RemoteSession> SelectedSession();
|
||||
std::string OpenFileDialog(const std::string &title);
|
||||
void OpenFileDialog(const std::string& title,
|
||||
std::function<void(std::string)> completion);
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
|
||||
@@ -330,8 +330,13 @@ struct TranslationRow {
|
||||
u8"Выбрать файл для отправки") \
|
||||
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
|
||||
u8"Прогресс передачи файлов") \
|
||||
X(file_sending_progress, u8"文件发送进度", "File Sending Progress", \
|
||||
u8"Отправка файла") \
|
||||
X(file_receiving_progress, u8"文件接收进度", "File Receiving Progress", \
|
||||
u8"Получение файла") \
|
||||
X(queued, u8"队列中", "Queued", u8"В очереди") \
|
||||
X(sending, u8"正在传输", "Sending", u8"Передача") \
|
||||
X(receiving, u8"正在接收", "Receiving", u8"Получение") \
|
||||
X(completed, u8"已完成", "Completed", u8"Завершено") \
|
||||
X(failed, u8"失败", "Failed", u8"Ошибка") \
|
||||
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
|
||||
|
||||
@@ -32,35 +32,42 @@ void FileTransferManager::Shutdown() {
|
||||
}
|
||||
|
||||
FileTransferManager::FileTransferState& FileTransferManager::global_state() { return global_state_; }
|
||||
FileTransferManager::FileTransferState& FileTransferManager::received_state() { return received_state_; }
|
||||
FileTransferManager::FileTransferState& FileTransferManager::state_for(const std::shared_ptr<RemoteSession>& props) {
|
||||
return props ? props->file_transfer_ : global_state_;
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessSelectedFile(const std::string& path,
|
||||
bool FileTransferManager::ProcessSelectedFile(const std::string& path,
|
||||
const std::shared_ptr<RemoteSession>& props, const std::string& file_label,
|
||||
const std::string& remote_id) {
|
||||
if (path.empty() || stopping_ || (props && props->closing_)) return;
|
||||
const std::string& remote_id, uint32_t file_id) {
|
||||
if (path.empty() || stopping_ || (props && props->closing_)) return false;
|
||||
const auto file_path = PathFromUtf8(path);
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) return;
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) return false;
|
||||
const auto size = std::filesystem::file_size(file_path, ec);
|
||||
if (ec) return;
|
||||
if (ec) return false;
|
||||
auto& state = state_for(props);
|
||||
// Allocate once at enqueue time so queued and active notifications keep the
|
||||
// same identity, including repeated sends of the same path.
|
||||
const auto id = file_id ? file_id : file_transfer::Session::NextFileId();
|
||||
{
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
FileTransferState::FileTransferInfo info;
|
||||
const auto name = file_path.filename().u8string();
|
||||
info.file_name.assign(name.begin(), name.end());
|
||||
info.remote_id = props ? props->remote_id_ : remote_id;
|
||||
info.file_path = file_path; info.file_size = size;
|
||||
info.file_id = id;
|
||||
info.status = FileTransferState::FileTransferStatus::Queued;
|
||||
state.file_transfer_list_.push_back(std::move(info));
|
||||
}
|
||||
state.file_transfer_window_visible_ = true;
|
||||
{
|
||||
std::lock_guard lock(state.file_queue_mutex_);
|
||||
state.file_send_queue_.push({file_path, file_label, props ? props->remote_id_ : remote_id});
|
||||
state.file_send_queue_.push({file_path, file_label, props ? props->remote_id_ : remote_id, id});
|
||||
}
|
||||
ProcessQueue(props);
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
|
||||
@@ -79,7 +86,7 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
|
||||
});
|
||||
workers_.push_back(std::async(std::launch::async, [this, props, file] {
|
||||
auto& state = state_for(props);
|
||||
const auto id = file_transfer::Session::NextFileId();
|
||||
const auto id = file.file_id;
|
||||
PeerPtr* peer;
|
||||
std::shared_ptr<PeerEventHandler> events;
|
||||
{
|
||||
@@ -97,15 +104,17 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
|
||||
}, file_transfer::Session::Progress{});
|
||||
state.file_sent_bytes_ = 0; state.file_total_bytes_ = 0; state.file_send_rate_bps_ = 0;
|
||||
state.current_file_id_ = id;
|
||||
const auto started_at = std::chrono::steady_clock::now();
|
||||
auto next_rate_update = started_at + std::chrono::seconds(1);
|
||||
{
|
||||
std::lock_guard lock(state.file_transfer_mutex_);
|
||||
state.file_send_start_time_ = state.file_send_last_update_time_ = std::chrono::steady_clock::now();
|
||||
state.file_send_start_time_ = state.file_send_last_update_time_ = started_at;
|
||||
state.file_send_last_bytes_ = 0;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
for (auto& info : state.file_transfer_list_) {
|
||||
if (info.file_path == file.file_path && info.status == FileTransferState::FileTransferStatus::Queued) {
|
||||
if (info.file_id == id) {
|
||||
info.status = FileTransferState::FileTransferStatus::Sending;
|
||||
info.file_id = id; info.sent_bytes = 0;
|
||||
break;
|
||||
@@ -133,8 +142,13 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
|
||||
if (stopping_ || !events->IsActive() || (props && props->closing_)) session->Cancel();
|
||||
const bool pumped = session->Pump();
|
||||
progress = session->Snapshot();
|
||||
if (progress.acknowledged != previous || progress.status != file_transfer::Status::Sending) {
|
||||
UpdateProgress(state, props, progress); previous = progress.acknowledged;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const bool rate_due = now >= next_rate_update;
|
||||
if (progress.acknowledged != previous || rate_due ||
|
||||
progress.status != file_transfer::Status::Sending) {
|
||||
UpdateProgress(state, progress, now);
|
||||
previous = progress.acknowledged;
|
||||
if (rate_due) next_rate_update = now + std::chrono::seconds(1);
|
||||
}
|
||||
if (progress.status != file_transfer::Status::Sending) break;
|
||||
if (!pumped) {
|
||||
@@ -168,29 +182,27 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
|
||||
}
|
||||
|
||||
void FileTransferManager::UpdateProgress(FileTransferState& state,
|
||||
const std::shared_ptr<RemoteSession>& props, const file_transfer::SendState& progress) {
|
||||
const file_transfer::SendState& progress,
|
||||
std::chrono::steady_clock::time_point now) {
|
||||
state.file_sent_bytes_ = progress.acknowledged; state.file_total_bytes_ = progress.total;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const uint32_t current = state.file_send_rate_bps_.load();
|
||||
uint32_t rate = current;
|
||||
uint32_t rate = state.file_send_rate_bps_.load();
|
||||
{
|
||||
std::lock_guard lock(state.file_transfer_mutex_);
|
||||
if (props) {
|
||||
const auto bitrate = props->net_traffic_stats_.DataOutboundBitrate();
|
||||
if (bitrate) rate = current ? static_cast<uint32_t>(current * .7 + bitrate * .99 * .3)
|
||||
: static_cast<uint32_t>(bitrate * .99);
|
||||
} else {
|
||||
const double seconds = std::chrono::duration<double>(now - state.file_send_last_update_time_).count();
|
||||
if (seconds > 0 && progress.acknowledged > state.file_send_last_bytes_) {
|
||||
const double measured = (progress.acknowledged - state.file_send_last_bytes_) * 8.0 / seconds;
|
||||
const auto value = static_cast<uint32_t>((std::min)(measured,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
|
||||
rate = current ? static_cast<uint32_t>(current * .7 + value * .3) : value;
|
||||
}
|
||||
const auto elapsed = now - state.file_send_last_update_time_;
|
||||
// Progress follows every ACK; speed uses one full sample per second for
|
||||
// this file. Sample even without new ACKs so a stalled transfer reads zero.
|
||||
if (elapsed >= std::chrono::seconds(1)) {
|
||||
const double seconds = std::chrono::duration<double>(elapsed).count();
|
||||
const auto bytes = progress.acknowledged >= state.file_send_last_bytes_
|
||||
? progress.acknowledged - state.file_send_last_bytes_ : 0;
|
||||
const double measured = bytes * 8.0 / seconds;
|
||||
rate = static_cast<uint32_t>((std::min)(measured,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
|
||||
state.file_send_rate_bps_ = rate;
|
||||
state.file_send_last_bytes_ = progress.acknowledged;
|
||||
state.file_send_last_update_time_ = now;
|
||||
}
|
||||
state.file_send_last_bytes_ = progress.acknowledged; state.file_send_last_update_time_ = now;
|
||||
}
|
||||
state.file_send_rate_bps_ = rate;
|
||||
std::lock_guard lock(state.file_transfer_list_mutex_);
|
||||
for (auto& info : state.file_transfer_list_) if (info.file_id == progress.id) {
|
||||
info.sent_bytes = progress.acknowledged; info.file_size = progress.total; info.rate_bps = rate;
|
||||
@@ -213,6 +225,41 @@ void FileTransferManager::HandleAck(const char* data, size_t size,
|
||||
wake_.notify_all();
|
||||
}
|
||||
|
||||
void FileTransferManager::UpdateReceiveProgress(
|
||||
const file_transfer::ReceiveState& progress,
|
||||
std::unordered_map<uint32_t, size_t>& notifications,
|
||||
const std::string& remote_id) {
|
||||
using Status = FileTransferState::FileTransferStatus;
|
||||
std::lock_guard lock(received_state_.file_transfer_list_mutex_);
|
||||
auto& files = received_state_.file_transfer_list_;
|
||||
auto found = notifications.find(progress.id);
|
||||
if (found == notifications.end()) {
|
||||
FileTransferState::FileTransferInfo info;
|
||||
// Wire IDs can collide between peers or with outgoing transfers. Give each
|
||||
// reception a local notification identity from the same allocator as sends.
|
||||
info.file_id = file_transfer::Session::NextFileId();
|
||||
info.remote_id = remote_id;
|
||||
info.receiving = true;
|
||||
receive_rate_samples_.emplace(info.file_id,
|
||||
ReceiveRateSample{std::chrono::steady_clock::now(), 0});
|
||||
found = notifications.emplace(progress.id, files.size()).first;
|
||||
files.push_back(std::move(info));
|
||||
}
|
||||
auto& info = files[found->second];
|
||||
info.file_name = progress.name;
|
||||
info.file_size = progress.total;
|
||||
info.sent_bytes = progress.received;
|
||||
switch (progress.status) {
|
||||
case file_transfer::ReceiveStatus::Receiving: info.status = Status::Receiving; break;
|
||||
case file_transfer::ReceiveStatus::Completed: info.status = Status::Completed; break;
|
||||
case file_transfer::ReceiveStatus::Failed: info.status = Status::Failed; break;
|
||||
}
|
||||
if (info.status != Status::Receiving) {
|
||||
receive_rate_samples_.erase(info.file_id);
|
||||
notifications.erase(found);
|
||||
}
|
||||
}
|
||||
|
||||
void FileTransferManager::Receive(const char* data, size_t size,
|
||||
PeerEventHandler* handler, const std::string& remote) {
|
||||
const auto props = owner_.FindRemoteSession(remote);
|
||||
@@ -235,6 +282,12 @@ void FileTransferManager::Receive(const char* data, size_t size,
|
||||
if (!entry.session) {
|
||||
entry.session = std::make_shared<FileReceiver>(); entry.handler = events;
|
||||
entry.default_directory = entry.session->OutputDir();
|
||||
// The transfer engine serializes progress callbacks for this receiver.
|
||||
entry.session->SetOnReceiveProgress(
|
||||
[this, remote, notifications = std::unordered_map<uint32_t, size_t>{}]
|
||||
(const file_transfer::ReceiveState& progress) mutable {
|
||||
UpdateReceiveProgress(progress, notifications, remote);
|
||||
});
|
||||
entry.session->SetOnSendAck([peer, events, remote](const FileTransferAck& ack) {
|
||||
auto callback = events->EnterCallback();
|
||||
if (!callback) return -1;
|
||||
@@ -285,6 +338,20 @@ void FileTransferManager::Poll() {
|
||||
for (const auto& [remote, receiver] : peers) receivers.push_back(receiver.session);
|
||||
}
|
||||
for (const auto& receiver : receivers) receiver->Poll();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
std::lock_guard lock(received_state_.file_transfer_list_mutex_);
|
||||
for (auto& info : received_state_.file_transfer_list_) {
|
||||
const auto sample = receive_rate_samples_.find(info.file_id);
|
||||
if (sample == receive_rate_samples_.end()) continue;
|
||||
const auto elapsed = now - sample->second.updated;
|
||||
if (elapsed < std::chrono::seconds(1)) continue;
|
||||
const double seconds = std::chrono::duration<double>(elapsed).count();
|
||||
const auto bytes = info.sent_bytes >= sample->second.bytes
|
||||
? info.sent_bytes - sample->second.bytes : 0;
|
||||
info.rate_bps = static_cast<uint32_t>((std::min)(bytes * 8.0 / seconds,
|
||||
static_cast<double>((std::numeric_limits<uint32_t>::max)())));
|
||||
sample->second = {now, info.sent_bytes};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
@@ -26,9 +27,12 @@ class FileTransferManager {
|
||||
explicit FileTransferManager(GuiRuntime& owner);
|
||||
~FileTransferManager();
|
||||
FileTransferState& global_state();
|
||||
FileTransferState& received_state();
|
||||
FileTransferState& state_for(const std::shared_ptr<RemoteSession>& props);
|
||||
void ProcessSelectedFile(const std::string& path, const std::shared_ptr<RemoteSession>& props,
|
||||
const std::string& file_label, const std::string& remote_id = "");
|
||||
// A reserved ID keeps a DeskHub notification stable while waiting for a slot.
|
||||
bool ProcessSelectedFile(const std::string& path, const std::shared_ptr<RemoteSession>& props,
|
||||
const std::string& file_label, const std::string& remote_id = "",
|
||||
uint32_t file_id = 0);
|
||||
void HandleAck(const char* data, size_t size, PeerEventHandler* handler, const std::string& remote);
|
||||
void Receive(const char* data, size_t size, PeerEventHandler* handler, const std::string& remote);
|
||||
void Connected(PeerEventHandler* handler, const std::string& remote);
|
||||
@@ -48,8 +52,16 @@ class FileTransferManager {
|
||||
std::filesystem::path default_directory;
|
||||
};
|
||||
void ProcessQueue(std::shared_ptr<RemoteSession> props);
|
||||
void UpdateProgress(FileTransferState& state, const std::shared_ptr<RemoteSession>& props,
|
||||
const file_transfer::SendState& progress);
|
||||
void UpdateProgress(FileTransferState& state,
|
||||
const file_transfer::SendState& progress,
|
||||
std::chrono::steady_clock::time_point now);
|
||||
void UpdateReceiveProgress(const file_transfer::ReceiveState& progress,
|
||||
std::unordered_map<uint32_t, size_t>& notifications,
|
||||
const std::string& remote_id);
|
||||
struct ReceiveRateSample {
|
||||
std::chrono::steady_clock::time_point updated;
|
||||
uint64_t bytes = 0;
|
||||
};
|
||||
std::mutex workers_mutex_, sessions_mutex_, wake_mutex_;
|
||||
std::condition_variable wake_;
|
||||
std::vector<std::future<void>> workers_;
|
||||
@@ -59,6 +71,10 @@ class FileTransferManager {
|
||||
std::unordered_map<PeerEventHandler*, std::set<std::string>> disconnected_;
|
||||
GuiRuntime& owner_;
|
||||
FileTransferState global_state_;
|
||||
// Keep received files separate from outgoing queues.
|
||||
FileTransferState received_state_;
|
||||
// Guarded by received_state_.file_transfer_list_mutex_.
|
||||
std::unordered_map<uint32_t, ReceiveRateSample> receive_rate_samples_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-19
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _TRANSFER_NOTIFICATION_LAYOUT_H_
|
||||
#define _TRANSFER_NOTIFICATION_LAYOUT_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
|
||||
namespace crossdesk::transfer_notification {
|
||||
|
||||
constexpr float kWidth = 360;
|
||||
constexpr float kHeight = 132;
|
||||
constexpr float kMargin = 16;
|
||||
constexpr float kGap = 10;
|
||||
|
||||
struct Layout {
|
||||
float width, height;
|
||||
size_t rows, columns;
|
||||
size_t capacity() const { return rows * columns; }
|
||||
};
|
||||
|
||||
inline Layout Fit(float width, float height) {
|
||||
const float card_width = std::min(kWidth, std::max(1.0f, width - 2 * kMargin));
|
||||
const float card_height = std::min(kHeight, std::max(1.0f, height - 2 * kMargin));
|
||||
const auto count = [](float available, float card) {
|
||||
return static_cast<size_t>(std::max(1.0f, (available - 2 * kMargin + kGap) / (card + kGap)));
|
||||
};
|
||||
return {card_width, card_height, count(height, card_height), count(width, card_width)};
|
||||
}
|
||||
|
||||
struct Position { float x, y; };
|
||||
|
||||
inline Position Place(const Layout& layout, float x, float y,
|
||||
float width, float height, size_t index) {
|
||||
// Fill upward from the bottom-right, then continue in a column to the left.
|
||||
return {x + std::max(0.0f, width - kMargin - layout.width) -
|
||||
(index / layout.rows) * (layout.width + kGap),
|
||||
y + std::max(0.0f, height - kMargin - layout.height) -
|
||||
(index % layout.rows) * (layout.height + kGap)};
|
||||
}
|
||||
|
||||
} // namespace crossdesk::transfer_notification
|
||||
|
||||
#endif
|
||||
@@ -51,20 +51,23 @@ struct FileTransferState {
|
||||
std::filesystem::path file_path;
|
||||
std::string file_label;
|
||||
std::string remote_id;
|
||||
uint32_t file_id = 0;
|
||||
};
|
||||
std::queue<QueuedFile> file_send_queue_;
|
||||
std::mutex file_queue_mutex_;
|
||||
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed };
|
||||
enum class FileTransferStatus { Queued, Sending, Completed, Failed, Receiving };
|
||||
|
||||
struct FileTransferInfo {
|
||||
std::string file_name;
|
||||
std::string remote_id;
|
||||
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;
|
||||
bool receiving = false;
|
||||
};
|
||||
std::vector<FileTransferInfo> file_transfer_list_;
|
||||
std::mutex file_transfer_list_mutex_;
|
||||
|
||||
@@ -4,6 +4,7 @@ export { AnnouncementItem, MessageStrings } from "message_panel.slint";
|
||||
export { MainWindow, RecentConnection, UpdateCheckStatus, AutostartStatus, UiStrings } from "main_window.slint";
|
||||
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
||||
export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "server_window.slint";
|
||||
export { FileTransferNotification } from "file_transfer_notification.slint";
|
||||
|
||||
export { GalleryTile, GalleryRecentDevice, GalleryActionDevice } from "gallery_view.slint";
|
||||
export { GalleryStrings } from "device_strings.slint";
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { FileTransferEntry } from "stream_window.slint";
|
||||
import { DialogSurface, FontAwesomeIcons, IconButton, ImGuiFontStyle } from "common.slint";
|
||||
import { WorkbenchStyle } from "workbench_controls.slint";
|
||||
|
||||
export component FileTransferNotification inherits Window {
|
||||
title: "CrossDesk";
|
||||
icon: @image-url("../../../resources/linux/crossdesk_256x256.png");
|
||||
preferred-width: 360px;
|
||||
preferred-height: 132px;
|
||||
no-frame: true;
|
||||
always-on-top: true;
|
||||
background: transparent;
|
||||
default-font-size: ImGuiFontStyle.base;
|
||||
in property <FileTransferEntry> entry;
|
||||
in property <string> heading;
|
||||
in property <string> remote-id;
|
||||
in property <bool> completed;
|
||||
in property <bool> failed;
|
||||
in property <bool> transferring;
|
||||
callback dismissed;
|
||||
|
||||
DialogSurface {
|
||||
width: parent.width; height: parent.height;
|
||||
background: WorkbenchStyle.surface;
|
||||
border-color: WorkbenchStyle.separator;
|
||||
Text {
|
||||
x: 16px; y: 8px;
|
||||
width: parent.width - 64px - (root.remote-id == "" ? 0px : 108px);
|
||||
height: 28px;
|
||||
text: root.heading; color: WorkbenchStyle.text;
|
||||
font-size: ImGuiFontStyle.prominent; font-weight: 600;
|
||||
vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
if root.remote-id != "": Text {
|
||||
x: parent.width - 148px; y: 8px; width: 100px; height: 28px;
|
||||
text: root.remote-id;
|
||||
color: WorkbenchStyle.muted;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
horizontal-alignment: right; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
IconButton {
|
||||
x: parent.width - 40px; y: 8px; width: 28px; height: 28px;
|
||||
icon: FontAwesomeIcons.xmark; icon-size: 14px;
|
||||
icon-color: WorkbenchStyle.muted;
|
||||
hover-background: WorkbenchStyle.hover-surface;
|
||||
pressed-background: WorkbenchStyle.pressed-surface;
|
||||
clicked => { root.dismissed(); }
|
||||
}
|
||||
Rectangle {
|
||||
x: 16px; y: 40px; width: parent.width - 32px; height: 1px;
|
||||
background: WorkbenchStyle.separator;
|
||||
}
|
||||
Text {
|
||||
x: 16px; y: 47px; width: parent.width - 32px; height: 18px;
|
||||
text: root.entry.name; color: WorkbenchStyle.text;
|
||||
font-size: ImGuiFontStyle.base; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
HorizontalLayout {
|
||||
x: 16px; y: 68px; width: parent.width - 32px; height: 16px; spacing: 8px;
|
||||
Text {
|
||||
text: root.entry.status;
|
||||
color: root.failed ? #d92d20 : root.completed ? WorkbenchStyle.success
|
||||
: root.transferring ? WorkbenchStyle.accent : WorkbenchStyle.muted;
|
||||
font-size: ImGuiFontStyle.body; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
Text {
|
||||
text: root.transferring ? round(root.entry.progress * 100) + "% " + root.entry.speed : root.completed ? "100%" : "";
|
||||
color: WorkbenchStyle.muted; font-size: ImGuiFontStyle.body;
|
||||
horizontal-alignment: right; vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
x: 16px; y: 90px; width: parent.width - 32px; height: 6px;
|
||||
background: WorkbenchStyle.separator; border-radius: 3px;
|
||||
Rectangle {
|
||||
x: 0px; y: 0px;
|
||||
width: parent.width * max(0, min(1, root.entry.progress)); height: parent.height;
|
||||
background: root.failed ? #d92d20 : root.completed ? WorkbenchStyle.success : WorkbenchStyle.accent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
Text {
|
||||
x: 16px; y: 103px; width: parent.width - 32px; height: 17px;
|
||||
text: root.entry.size; color: WorkbenchStyle.muted;
|
||||
font-size: ImGuiFontStyle.body; vertical-alignment: center; overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
@interface CrossDeskStreamFullscreenTarget : NSObject
|
||||
- (void)toggleStreamFullscreen:(id)sender;
|
||||
@@ -40,6 +43,24 @@ BOOL stream_restore_zoom_hidden = NO;
|
||||
NSApplicationPresentationOptions stream_restore_presentation_options =
|
||||
NSApplicationPresentationDefault;
|
||||
|
||||
struct FileDialogRequest {
|
||||
std::string title;
|
||||
std::function<void(std::string)> completion;
|
||||
};
|
||||
std::shared_ptr<FileDialogRequest> file_dialog_request;
|
||||
NSOpenPanel *file_dialog = nil;
|
||||
NSOpenPanel *prepared_file_dialog = nil;
|
||||
uint64_t file_dialog_prepare_generation = 0;
|
||||
|
||||
NSOpenPanel *CreateFileDialog() {
|
||||
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
||||
panel.canChooseFiles = YES;
|
||||
panel.canChooseDirectories = NO;
|
||||
panel.allowsMultipleSelection = NO;
|
||||
panel.resolvesAliases = YES;
|
||||
return panel;
|
||||
}
|
||||
|
||||
void InstallStreamFullscreenButton(NSWindow *window) {
|
||||
if (window == nil) {
|
||||
return;
|
||||
@@ -358,41 +379,109 @@ bool IsStreamWindowFullscreen() {
|
||||
return stream_fullscreen;
|
||||
}
|
||||
|
||||
std::string OpenNativeFileDialog(const std::string &title) {
|
||||
void ShowFileTransferNotification(void* slint_view,
|
||||
const std::function<void()>& show) {
|
||||
@autoreleasepool {
|
||||
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
||||
panel.canChooseFiles = YES;
|
||||
panel.canChooseDirectories = NO;
|
||||
panel.allowsMultipleSelection = NO;
|
||||
panel.resolvesAliases = YES;
|
||||
NSWindow *previous_key = NSApp.keyWindow;
|
||||
NSView *view = (__bridge NSView *)slint_view;
|
||||
NSWindow *notification = view.window;
|
||||
notification.excludedFromWindowsMenu = YES;
|
||||
notification.hidesOnDeactivate = NO;
|
||||
notification.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary |
|
||||
NSWindowCollectionBehaviorIgnoresCycle;
|
||||
show();
|
||||
// Winit's show path calls makeKeyAndOrderFront even for frameless windows.
|
||||
// Restore the input owner synchronously before the next event-loop turn.
|
||||
if (notification.isKeyWindow) {
|
||||
if (previous_key && previous_key != notification) [previous_key makeKeyWindow];
|
||||
else [notification resignKeyWindow];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PrepareNativeFileDialog() {
|
||||
const auto generation = ++file_dialog_prepare_generation;
|
||||
// Creating NSOpenPanel initializes the system file browser and can take
|
||||
// hundreds of milliseconds. Do that once after the main window is visible,
|
||||
// without showing a panel, changing focus, or blocking the initial frame.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC),
|
||||
dispatch_get_main_queue(), ^{
|
||||
if (generation != file_dialog_prepare_generation ||
|
||||
file_dialog_request || prepared_file_dialog) return;
|
||||
NSOpenPanel *panel = CreateFileDialog();
|
||||
if (generation == file_dialog_prepare_generation &&
|
||||
!file_dialog_request && !prepared_file_dialog)
|
||||
prepared_file_dialog = panel;
|
||||
});
|
||||
}
|
||||
|
||||
void OpenNativeFileDialog(const std::string& title,
|
||||
std::function<void(std::string)> completion) {
|
||||
if (file_dialog_request) {
|
||||
completion({});
|
||||
return;
|
||||
}
|
||||
const auto request = std::make_shared<FileDialogRequest>(
|
||||
FileDialogRequest{title, std::move(completion)});
|
||||
file_dialog_request = request;
|
||||
// Leave the current Slint callback before AppKit creates or shows the panel.
|
||||
// runModal would let display-link callbacks reenter Slint's timer dispatch.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (file_dialog_request != request) return;
|
||||
NSOpenPanel *panel = prepared_file_dialog ?: CreateFileDialog();
|
||||
prepared_file_dialog = nil;
|
||||
file_dialog = panel;
|
||||
|
||||
NSString *prompt =
|
||||
[[NSString alloc] initWithBytes:title.data()
|
||||
length:title.size()
|
||||
[[NSString alloc] initWithBytes:request->title.data()
|
||||
length:request->title.size()
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if (prompt != nil && prompt.length > 0) {
|
||||
panel.title = prompt;
|
||||
panel.message = prompt;
|
||||
}
|
||||
panel.title = prompt ?: @"";
|
||||
panel.message = prompt ?: @"";
|
||||
|
||||
// tinyfiledialogs uses an external osascript process on macOS, so its
|
||||
// chooser can appear without becoming the active window. Keep the picker
|
||||
// inside CrossDesk and explicitly activate both the app and panel.
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
NSWindow *owner = stream_window ?: NSApp.keyWindow ?: NSApp.mainWindow;
|
||||
NSWindow *owner = NSApp.keyWindow ?: NSApp.mainWindow;
|
||||
if (owner != nil) {
|
||||
[owner makeKeyAndOrderFront:nil];
|
||||
}
|
||||
NSScreen *target_screen = owner.screen ?: NSScreen.mainScreen;
|
||||
CenterWindowOnScreen(panel, target_screen);
|
||||
[panel makeKeyAndOrderFront:nil];
|
||||
__weak NSOpenPanel *weak_panel = panel;
|
||||
[panel beginWithCompletionHandler:^(NSModalResponse result) {
|
||||
// Cancellation can complete a request before AppKit calls this block.
|
||||
if (file_dialog_request != request) return;
|
||||
NSOpenPanel *finished_panel = weak_panel;
|
||||
const char *selected = result == NSModalResponseOK
|
||||
? finished_panel.URL.fileSystemRepresentation : nullptr;
|
||||
const std::string path = selected ? selected : "";
|
||||
[finished_panel orderOut:nil];
|
||||
file_dialog = nil;
|
||||
// AppKit has returned the panel to its configuration phase. Keep the
|
||||
// browser alive for the next selection instead of rebuilding it.
|
||||
prepared_file_dialog = finished_panel;
|
||||
file_dialog_request.reset();
|
||||
auto finished = std::move(request->completion);
|
||||
finished(path);
|
||||
}];
|
||||
if (file_dialog_request == request) [panel makeKeyAndOrderFront:nil];
|
||||
});
|
||||
}
|
||||
|
||||
if ([panel runModal] != NSModalResponseOK || panel.URL == nil) {
|
||||
return {};
|
||||
}
|
||||
const char *path = panel.URL.fileSystemRepresentation;
|
||||
return path != nullptr ? std::string(path) : std::string{};
|
||||
}
|
||||
void CancelNativeFileDialog() {
|
||||
++file_dialog_prepare_generation;
|
||||
prepared_file_dialog = nil;
|
||||
const auto request = std::move(file_dialog_request);
|
||||
if (!request) return;
|
||||
NSOpenPanel *panel = file_dialog;
|
||||
file_dialog = nil;
|
||||
[panel cancel:nil];
|
||||
[panel orderOut:nil];
|
||||
auto finished = std::move(request->completion);
|
||||
finished({});
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -42,8 +43,20 @@ bool IsStreamWindowActive();
|
||||
bool SetStreamWindowFullscreen(bool fullscreen);
|
||||
bool IsStreamWindowFullscreen();
|
||||
|
||||
// Opens an application-modal macOS file picker. The panel is made key before
|
||||
// entering its modal loop so it receives keyboard input immediately.
|
||||
std::string OpenNativeFileDialog(const std::string &title);
|
||||
// Shows a desktop transfer notification without replacing the current key
|
||||
// window, including while the stream is fullscreen.
|
||||
// |slint_view| must be the notification's existing AppKit NSView*.
|
||||
void ShowFileTransferNotification(void* slint_view,
|
||||
const std::function<void()>& show);
|
||||
|
||||
// Schedule hidden initialization after the main window's initial display.
|
||||
void PrepareNativeFileDialog();
|
||||
// Opens a macOS file picker asynchronously on the main thread, without a
|
||||
// nested modal loop. Completion receives an empty path on cancel or failure.
|
||||
void OpenNativeFileDialog(const std::string& title,
|
||||
std::function<void(std::string)> completion);
|
||||
// Cancels an open or pending picker, completes it once with an empty path, and
|
||||
// releases the cached panel and pending initialization during application exit.
|
||||
void CancelNativeFileDialog();
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -16,7 +16,9 @@ FileReceiver::FileReceiver(const std::filesystem::path& output_dir)
|
||||
FileTransferAck ack{};
|
||||
return on_send_ack_ && DecodeFileTransferAck(data, size, &ack)
|
||||
? on_send_ack_(ack) : 0;
|
||||
}, {}) {
|
||||
}, {}, {}, [this](const file_transfer::ReceiveState& progress) {
|
||||
if (on_receive_progress_) on_receive_progress_(progress);
|
||||
}) {
|
||||
session_.SetDirectory(output_dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace crossdesk {
|
||||
class FileReceiver {
|
||||
public:
|
||||
using OnSendAck = std::function<int(const FileTransferAck&)>;
|
||||
using OnReceiveProgress = file_transfer::Session::ReceiveProgress;
|
||||
FileReceiver();
|
||||
explicit FileReceiver(const std::filesystem::path& output_dir);
|
||||
bool OnData(const char* data, size_t size) { return session_.Receive(data, size); }
|
||||
@@ -25,6 +26,7 @@ class FileReceiver {
|
||||
void Cancel() { session_.Cancel(); }
|
||||
// Set once, before the receiver is published to transport callbacks.
|
||||
void SetOnSendAck(OnSendAck cb) { on_send_ack_ = std::move(cb); }
|
||||
void SetOnReceiveProgress(OnReceiveProgress cb) { on_receive_progress_ = std::move(cb); }
|
||||
std::filesystem::path OutputDir() const {
|
||||
std::lock_guard lock(directory_mutex_);
|
||||
return output_dir_;
|
||||
@@ -40,6 +42,7 @@ class FileReceiver {
|
||||
mutable std::mutex directory_mutex_;
|
||||
std::filesystem::path output_dir_;
|
||||
OnSendAck on_send_ack_;
|
||||
OnReceiveProgress on_receive_progress_;
|
||||
file_transfer::Session session_;
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,14 @@ struct SendState {
|
||||
Status status = Status::Idle;
|
||||
};
|
||||
|
||||
enum class ReceiveStatus { Receiving, Completed, Failed };
|
||||
struct ReceiveState {
|
||||
uint32_t id = 0;
|
||||
std::string name;
|
||||
uint64_t total = 0, received = 0;
|
||||
ReceiveStatus status = ReceiveStatus::Receiving;
|
||||
};
|
||||
|
||||
class Session {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
@@ -48,8 +56,12 @@ class Session {
|
||||
// outside its mutex, allowing immediate/reentrant ACKs and concurrent peers.
|
||||
using Progress = std::function<void(const std::string&, double, bool,
|
||||
const std::string&)>;
|
||||
Session(Send send, Progress progress, Options options = {})
|
||||
: send_(std::move(send)), progress_(std::move(progress)), options_(options) {}
|
||||
// Exact accepted bytes and terminal state, keyed by this peer's wire ID.
|
||||
using ReceiveProgress = std::function<void(const ReceiveState&)>;
|
||||
Session(Send send, Progress progress, Options options = {},
|
||||
ReceiveProgress receive_progress = {})
|
||||
: send_(std::move(send)), progress_(std::move(progress)),
|
||||
receive_progress_(std::move(receive_progress)), options_(options) {}
|
||||
~Session() { Clear(); }
|
||||
|
||||
static uint32_t NextFileId() {
|
||||
@@ -92,6 +104,7 @@ class Session {
|
||||
for (auto it = incoming_.begin(); it != incoming_.end();) {
|
||||
if (now - it->second.updated < timeout_) { ++it; continue; }
|
||||
expired.push_back({kFileAckMagic, it->first, it->second.received, it->second.total, 2});
|
||||
NotifyReceive(it->first, it->second, ReceiveStatus::Failed);
|
||||
Notify(it->second.name, -1, false, "");
|
||||
Remove(it->second); it = incoming_.erase(it);
|
||||
}
|
||||
@@ -169,6 +182,8 @@ class Session {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
closed_ = true;
|
||||
if (outgoing_) FinishSend(false);
|
||||
for (const auto& [id, file] : incoming_)
|
||||
NotifyReceive(id, file, ReceiveStatus::Failed);
|
||||
Clear();
|
||||
}
|
||||
|
||||
@@ -193,13 +208,23 @@ class Session {
|
||||
int percent = -1;
|
||||
Clock::time_point updated;
|
||||
};
|
||||
void NotifyReceive(uint32_t id, const Incoming& file, ReceiveStatus status) {
|
||||
if (receive_progress_)
|
||||
receive_progress_({id, file.name, file.total, file.received, status});
|
||||
}
|
||||
FileTransferAck ReceiveLocked(const FileChunkView& chunk, size_t size, Clock::time_point now) {
|
||||
const auto& h = chunk.header;
|
||||
auto it = incoming_.find(h.file_id);
|
||||
auto reject = [&]() {
|
||||
if (it != incoming_.end()) {
|
||||
NotifyReceive(h.file_id, it->second, ReceiveStatus::Failed);
|
||||
Notify(it->second.name, -1, false, ""); Remove(it->second); incoming_.erase(it);
|
||||
} else Notify(SafeName(chunk.file_name), -1, false, "");
|
||||
} else {
|
||||
if (receive_progress_)
|
||||
receive_progress_({h.file_id, SafeName(chunk.file_name), h.total_size,
|
||||
0, ReceiveStatus::Failed});
|
||||
Notify(SafeName(chunk.file_name), -1, false, "");
|
||||
}
|
||||
return FileTransferAck{kFileAckMagic, h.file_id, h.offset, h.total_size, 2};
|
||||
};
|
||||
const bool first = h.flags & 1, last = h.flags & 2;
|
||||
@@ -248,6 +273,7 @@ class Session {
|
||||
if (file.output.fail() || (complete && !Publish(file))) return reject();
|
||||
file.updated = now;
|
||||
const FileTransferAck ack{kFileAckMagic, h.file_id, file.received, file.total, complete ? 1u : 0u};
|
||||
NotifyReceive(h.file_id, file, complete ? ReceiveStatus::Completed : ReceiveStatus::Receiving);
|
||||
if (complete) {
|
||||
const auto path = file.path.u8string();
|
||||
Notify(file.name, 1, false, std::string(path.begin(), path.end()));
|
||||
@@ -375,6 +401,7 @@ class Session {
|
||||
mutable std::mutex mutex_;
|
||||
Send send_;
|
||||
Progress progress_;
|
||||
ReceiveProgress receive_progress_;
|
||||
Options options_;
|
||||
bool closed_ = false;
|
||||
SendState state_;
|
||||
|
||||
Reference in New Issue
Block a user