diff --git a/OpenNet/Core/P2PManager.cpp b/OpenNet/Core/P2PManager.cpp index 67c006f..781d1ca 100644 --- a/OpenNet/Core/P2PManager.cpp +++ b/OpenNet/Core/P2PManager.cpp @@ -27,9 +27,28 @@ namespace OpenNet::Core co_await winrt::resume_background(); { std::scoped_lock lk(m_torrentMutex); + + // Initialize state manager first + if (!m_stateManager) + { + m_stateManager = std::make_unique(); + if (!m_stateManager->Initialize()) + { + OutputDebugStringA("Failed to initialize TorrentStateManager\n"); + // Continue anyway, persistence will just be disabled + } + } + if (!m_torrentCore) { m_torrentCore = std::make_unique(); + + // Set state manager before initialization + if (m_stateManager) + { + m_torrentCore->SetStateManager(m_stateManager.get()); + } + if (!m_torrentCore->Initialize()) { m_torrentCore.reset(); @@ -42,6 +61,9 @@ namespace OpenNet::Core } m_isTorrentCoreInitialized.store(true); m_initializing.store(false); + + // Load and resume saved tasks + co_await LoadAndResumeSavedTasksAsync(); } IAsyncOperation P2PManager::AddMagnetAsync(std::string magnetUri, std::string savePath) @@ -52,6 +74,66 @@ namespace OpenNet::Core co_return m_torrentCore->AddMagnet(magnetUri, savePath); } + IAsyncAction P2PManager::LoadAndResumeSavedTasksAsync() + { + co_await winrt::resume_background(); + + std::scoped_lock lk(m_torrentMutex); + if (!m_stateManager || !m_torrentCore) co_return; + + auto tasks = m_stateManager->LoadAllTasks(); + for (auto const& task : tasks) + { + // Only resume non-completed, non-failed tasks + if (task.status == 1 || task.status == 2) // Downloading or Paused + { + std::string resumedId = m_torrentCore->AddTorrentFromResumeData(task.taskId); + if (!resumedId.empty()) + { + OutputDebugStringA(("Resumed task: " + task.taskId + "\n").c_str()); + } + } + } + } + + std::vector<::OpenNet::Core::Torrent::TaskMetadata> P2PManager::GetAllTasks() + { + std::scoped_lock lk(m_torrentMutex); + if (!m_stateManager) return {}; + return m_stateManager->LoadAllTasks(); + } + + IAsyncOperation P2PManager::ExportTasksAsync(std::wstring filePath) + { + co_await winrt::resume_background(); + std::scoped_lock lk(m_torrentMutex); + if (!m_stateManager) co_return false; + co_return m_stateManager->ExportToFile(filePath); + } + + IAsyncOperation P2PManager::ImportTasksAsync(std::wstring filePath) + { + co_await winrt::resume_background(); + std::scoped_lock lk(m_torrentMutex); + if (!m_stateManager) co_return false; + bool result = m_stateManager->ImportFromFile(filePath); + + // Resume imported tasks + if (result && m_torrentCore) + { + auto tasks = m_stateManager->LoadAllTasks(); + for (auto const& task : tasks) + { + if (task.status == 1 || task.status == 2) + { + m_torrentCore->AddTorrentFromResumeData(task.taskId); + } + } + } + + co_return result; + } + void P2PManager::SetProgressCallback(ProgressCb cb) { std::scoped_lock lk(m_cbMutex); diff --git a/OpenNet/Core/P2PManager.h b/OpenNet/Core/P2PManager.h index 3da1e24..e72ae92 100644 --- a/OpenNet/Core/P2PManager.h +++ b/OpenNet/Core/P2PManager.h @@ -6,9 +6,11 @@ #include #include #include +#include // Include torrent core so nested ProgressEvent is known #include "Core/torrentCore/libtorrentHandle.h" +#include "Core/torrentCore/TorrentStateManager.h" namespace OpenNet::Core { @@ -35,9 +37,25 @@ namespace OpenNet::Core return m_torrentCore.get(); } + // State manager access + ::OpenNet::Core::Torrent::TorrentStateManager* StateManager() noexcept + { + return m_stateManager.get(); + } + // Torrent operations winrt::Windows::Foundation::IAsyncOperation AddMagnetAsync(std::string magnetUri, std::string savePath); + // Load all saved tasks and resume them + winrt::Windows::Foundation::IAsyncAction LoadAndResumeSavedTasksAsync(); + + // Get all saved task metadata + std::vector<::OpenNet::Core::Torrent::TaskMetadata> GetAllTasks(); + + // Import/Export task data + winrt::Windows::Foundation::IAsyncOperation ExportTasksAsync(std::wstring filePath); + winrt::Windows::Foundation::IAsyncOperation ImportTasksAsync(std::wstring filePath); + // Callback registration using ProgressCb = std::function; using FinishedCb = std::function; @@ -54,6 +72,7 @@ namespace OpenNet::Core void WireCoreCallbacks(); std::unique_ptr<::OpenNet::Core::Torrent::LibtorrentHandle> m_torrentCore; + std::unique_ptr<::OpenNet::Core::Torrent::TorrentStateManager> m_stateManager; std::mutex m_torrentMutex; std::atomic m_isTorrentCoreInitialized{ false }; std::atomic m_initializing{ false }; diff --git a/OpenNet/Core/torrentCore/TorrentStateManager.cpp b/OpenNet/Core/torrentCore/TorrentStateManager.cpp new file mode 100644 index 0000000..f696e64 --- /dev/null +++ b/OpenNet/Core/torrentCore/TorrentStateManager.cpp @@ -0,0 +1,738 @@ +#include "pch.h" +#include "TorrentStateManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "ThirdParty/Sqlite/sqlite3.h" + +#include +#include +#include +#include +#include + +namespace lt = libtorrent; + +namespace OpenNet::Core::Torrent +{ + TorrentStateManager::TorrentStateManager() = default; + + TorrentStateManager::~TorrentStateManager() + { + CloseDatabase(); + } + + bool TorrentStateManager::Initialize(std::wstring const& basePath) + { + std::lock_guard lk(m_dbMutex); + if (m_initialized) return true; + + try + { + if (basePath.empty()) + { + // Use WinUI3 recommended LocalFolder + auto localFolder = winrt::Windows::Storage::ApplicationData::Current().LocalFolder(); + m_storagePath = localFolder.Path().c_str(); + } + else + { + m_storagePath = basePath; + } + + // Ensure the storage path ends with a separator + if (!m_storagePath.empty() && m_storagePath.back() != L'\\' && m_storagePath.back() != L'/') + { + m_storagePath += L"\\"; + } + + // Create resume data subfolder + std::wstring resumeFolder = m_storagePath + L"resume_data"; + std::filesystem::create_directories(resumeFolder); + + m_dbPath = m_storagePath + DATABASE_FILENAME; + + if (!InitializeDatabase()) + { + return false; + } + + m_initialized = true; + return true; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("TorrentStateManager::Initialize error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + std::wstring TorrentStateManager::GetStoragePath() const + { + return m_storagePath; + } + + bool TorrentStateManager::InitializeDatabase() + { + std::string dbPathUtf8 = winrt::to_string(m_dbPath); + + sqlite3* db = nullptr; + int rc = sqlite3_open(dbPathUtf8.c_str(), &db); + if (rc != SQLITE_OK) + { + if (db) sqlite3_close(db); + OutputDebugStringA("Failed to open SQLite database\n"); + return false; + } + + m_db = db; + + if (!CreateTables()) + { + CloseDatabase(); + return false; + } + + return true; + } + + bool TorrentStateManager::CreateTables() + { + if (!m_db) return false; + + const char* createTasksTable = R"( + CREATE TABLE IF NOT EXISTS tasks ( + task_id TEXT PRIMARY KEY, + magnet_uri TEXT NOT NULL, + save_path TEXT NOT NULL, + name TEXT, + added_timestamp INTEGER NOT NULL, + total_size INTEGER DEFAULT 0, + downloaded_size INTEGER DEFAULT 0, + status INTEGER DEFAULT 0, + resume_data BLOB + ); + )"; + + const char* createSessionTable = R"( + CREATE TABLE IF NOT EXISTS session_state ( + id INTEGER PRIMARY KEY CHECK (id = 1), + state_data BLOB + ); + )"; + + char* errMsg = nullptr; + int rc = sqlite3_exec(static_cast(m_db), createTasksTable, nullptr, nullptr, &errMsg); + if (rc != SQLITE_OK) + { + if (errMsg) + { + OutputDebugStringA(("SQLite error creating tasks table: " + std::string(errMsg) + "\n").c_str()); + sqlite3_free(errMsg); + } + return false; + } + + rc = sqlite3_exec(static_cast(m_db), createSessionTable, nullptr, nullptr, &errMsg); + if (rc != SQLITE_OK) + { + if (errMsg) + { + OutputDebugStringA(("SQLite error creating session_state table: " + std::string(errMsg) + "\n").c_str()); + sqlite3_free(errMsg); + } + return false; + } + + return true; + } + + bool TorrentStateManager::CloseDatabase() + { + if (m_db) + { + sqlite3_close(static_cast(m_db)); + m_db = nullptr; + } + return true; + } + + bool TorrentStateManager::SaveSessionState(lt::session& session) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return false; + + try + { + // Get session state and serialize it + lt::session_params params = session.session_state(); + std::vector buf = lt::write_session_params_buf(params, lt::session::save_dht_state); + + const char* sql = R"( + INSERT OR REPLACE INTO session_state (id, state_data) VALUES (1, ?); + )"; + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + sqlite3_bind_blob(stmt, 1, buf.data(), static_cast(buf.size()), SQLITE_TRANSIENT); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return rc == SQLITE_DONE; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("SaveSessionState error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + bool TorrentStateManager::LoadSessionState(lt::session& session) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return false; + + try + { + const char* sql = "SELECT state_data FROM session_state WHERE id = 1;"; + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) + { + const void* data = sqlite3_column_blob(stmt, 0); + int size = sqlite3_column_bytes(stmt, 0); + + if (data && size > 0) + { + lt::span buf(static_cast(data), size); + lt::session_params params = lt::read_session_params(buf); + session.apply_settings(params.settings); + // DHT state is applied automatically + } + } + + sqlite3_finalize(stmt); + return true; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("LoadSessionState error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + bool TorrentStateManager::SaveTaskResumeData(std::string const& taskId, lt::add_torrent_params const& params) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return false; + + try + { + // Serialize resume data using libtorrent's write_resume_data + std::vector buf = lt::write_resume_data_buf(params); + + const char* sql = "UPDATE tasks SET resume_data = ? WHERE task_id = ?;"; + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + sqlite3_bind_blob(stmt, 1, buf.data(), static_cast(buf.size()), SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, taskId.c_str(), -1, SQLITE_TRANSIENT); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return rc == SQLITE_DONE; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("SaveTaskResumeData error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + std::optional TorrentStateManager::LoadTaskResumeData(std::string const& taskId) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return std::nullopt; + + try + { + const char* sql = "SELECT resume_data FROM tasks WHERE task_id = ?;"; + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return std::nullopt; + + sqlite3_bind_text(stmt, 1, taskId.c_str(), -1, SQLITE_TRANSIENT); + + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) + { + const void* data = sqlite3_column_blob(stmt, 0); + int size = sqlite3_column_bytes(stmt, 0); + + if (data && size > 0) + { + lt::span buf(static_cast(data), size); + lt::error_code ec; + lt::add_torrent_params params = lt::read_resume_data(buf, ec); + sqlite3_finalize(stmt); + + if (!ec) + { + return params; + } + } + } + + sqlite3_finalize(stmt); + return std::nullopt; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("LoadTaskResumeData error: " + std::string(ex.what()) + "\n").c_str()); + return std::nullopt; + } + } + + bool TorrentStateManager::SaveTaskMetadata(TaskMetadata const& metadata) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return false; + + try + { + const char* sql = R"( + INSERT OR REPLACE INTO tasks + (task_id, magnet_uri, save_path, name, added_timestamp, total_size, downloaded_size, status, resume_data) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + )"; + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + sqlite3_bind_text(stmt, 1, metadata.taskId.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, metadata.magnetUri.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, metadata.savePath.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 4, metadata.name.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int64(stmt, 5, metadata.addedTimestamp); + sqlite3_bind_int64(stmt, 6, metadata.totalSize); + sqlite3_bind_int64(stmt, 7, metadata.downloadedSize); + sqlite3_bind_int(stmt, 8, metadata.status); + + if (metadata.resumeData.empty()) + { + sqlite3_bind_null(stmt, 9); + } + else + { + sqlite3_bind_blob(stmt, 9, metadata.resumeData.data(), + static_cast(metadata.resumeData.size()), SQLITE_TRANSIENT); + } + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return rc == SQLITE_DONE; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("SaveTaskMetadata error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + std::optional TorrentStateManager::LoadTaskMetadata(std::string const& taskId) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return std::nullopt; + + try + { + const char* sql = R"( + SELECT task_id, magnet_uri, save_path, name, added_timestamp, + total_size, downloaded_size, status, resume_data + FROM tasks WHERE task_id = ?; + )"; + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return std::nullopt; + + sqlite3_bind_text(stmt, 1, taskId.c_str(), -1, SQLITE_TRANSIENT); + + rc = sqlite3_step(stmt); + if (rc == SQLITE_ROW) + { + TaskMetadata metadata; + metadata.taskId = reinterpret_cast(sqlite3_column_text(stmt, 0)); + metadata.magnetUri = reinterpret_cast(sqlite3_column_text(stmt, 1)); + metadata.savePath = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + auto namePtr = sqlite3_column_text(stmt, 3); + metadata.name = namePtr ? reinterpret_cast(namePtr) : ""; + + metadata.addedTimestamp = sqlite3_column_int64(stmt, 4); + metadata.totalSize = sqlite3_column_int64(stmt, 5); + metadata.downloadedSize = sqlite3_column_int64(stmt, 6); + metadata.status = sqlite3_column_int(stmt, 7); + + const void* blobData = sqlite3_column_blob(stmt, 8); + int blobSize = sqlite3_column_bytes(stmt, 8); + if (blobData && blobSize > 0) + { + metadata.resumeData.assign( + static_cast(blobData), + static_cast(blobData) + blobSize + ); + } + + sqlite3_finalize(stmt); + return metadata; + } + + sqlite3_finalize(stmt); + return std::nullopt; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("LoadTaskMetadata error: " + std::string(ex.what()) + "\n").c_str()); + return std::nullopt; + } + } + + std::vector TorrentStateManager::LoadAllTasks() + { + std::lock_guard lk(m_dbMutex); + std::vector tasks; + if (!m_db) return tasks; + + try + { + const char* sql = R"( + SELECT task_id, magnet_uri, save_path, name, added_timestamp, + total_size, downloaded_size, status, resume_data + FROM tasks ORDER BY added_timestamp DESC; + )"; + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return tasks; + + while (sqlite3_step(stmt) == SQLITE_ROW) + { + TaskMetadata metadata; + metadata.taskId = reinterpret_cast(sqlite3_column_text(stmt, 0)); + metadata.magnetUri = reinterpret_cast(sqlite3_column_text(stmt, 1)); + metadata.savePath = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + auto namePtr = sqlite3_column_text(stmt, 3); + metadata.name = namePtr ? reinterpret_cast(namePtr) : ""; + + metadata.addedTimestamp = sqlite3_column_int64(stmt, 4); + metadata.totalSize = sqlite3_column_int64(stmt, 5); + metadata.downloadedSize = sqlite3_column_int64(stmt, 6); + metadata.status = sqlite3_column_int(stmt, 7); + + const void* blobData = sqlite3_column_blob(stmt, 8); + int blobSize = sqlite3_column_bytes(stmt, 8); + if (blobData && blobSize > 0) + { + metadata.resumeData.assign( + static_cast(blobData), + static_cast(blobData) + blobSize + ); + } + + tasks.push_back(std::move(metadata)); + } + + sqlite3_finalize(stmt); + } + catch (std::exception const& ex) + { + OutputDebugStringA(("LoadAllTasks error: " + std::string(ex.what()) + "\n").c_str()); + } + + return tasks; + } + + bool TorrentStateManager::DeleteTask(std::string const& taskId) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return false; + + try + { + const char* sql = "DELETE FROM tasks WHERE task_id = ?;"; + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + sqlite3_bind_text(stmt, 1, taskId.c_str(), -1, SQLITE_TRANSIENT); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return rc == SQLITE_DONE; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("DeleteTask error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + bool TorrentStateManager::UpdateTaskStatus(std::string const& taskId, int status) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return false; + + try + { + const char* sql = "UPDATE tasks SET status = ? WHERE task_id = ?;"; + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + sqlite3_bind_int(stmt, 1, status); + sqlite3_bind_text(stmt, 2, taskId.c_str(), -1, SQLITE_TRANSIENT); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return rc == SQLITE_DONE; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("UpdateTaskStatus error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + bool TorrentStateManager::UpdateTaskProgress(std::string const& taskId, int64_t downloadedSize) + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return false; + + try + { + const char* sql = "UPDATE tasks SET downloaded_size = ? WHERE task_id = ?;"; + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + sqlite3_bind_int64(stmt, 1, downloadedSize); + sqlite3_bind_text(stmt, 2, taskId.c_str(), -1, SQLITE_TRANSIENT); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return rc == SQLITE_DONE; + } + catch (std::exception const& ex) + { + OutputDebugStringA(("UpdateTaskProgress error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + bool TorrentStateManager::ExportToFile(std::wstring const& filePath) + { + // Don't hold lock while doing I/O - load data first, then write + std::vector tasks; + + // Load all tasks with lock held + { + std::lock_guard lk(m_dbMutex); + if (!m_db) return false; + + // Inline query to avoid recursive locking + const char* sql = R"( + SELECT task_id, magnet_uri, save_path, name, added_timestamp, + total_size, downloaded_size, status, resume_data + FROM tasks ORDER BY added_timestamp DESC; + )"; + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(static_cast(m_db), sql, -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + while (sqlite3_step(stmt) == SQLITE_ROW) + { + TaskMetadata metadata; + metadata.taskId = reinterpret_cast(sqlite3_column_text(stmt, 0)); + metadata.magnetUri = reinterpret_cast(sqlite3_column_text(stmt, 1)); + metadata.savePath = reinterpret_cast(sqlite3_column_text(stmt, 2)); + + auto namePtr = sqlite3_column_text(stmt, 3); + metadata.name = namePtr ? reinterpret_cast(namePtr) : ""; + + metadata.addedTimestamp = sqlite3_column_int64(stmt, 4); + metadata.totalSize = sqlite3_column_int64(stmt, 5); + metadata.downloadedSize = sqlite3_column_int64(stmt, 6); + metadata.status = sqlite3_column_int(stmt, 7); + + const void* blobData = sqlite3_column_blob(stmt, 8); + int blobSize = sqlite3_column_bytes(stmt, 8); + if (blobData && blobSize > 0) + { + metadata.resumeData.assign( + static_cast(blobData), + static_cast(blobData) + blobSize + ); + } + + tasks.push_back(std::move(metadata)); + } + sqlite3_finalize(stmt); + } + + // Now write to file without lock + try + { + lt::entry exportData; + lt::entry::list_type& taskList = exportData["tasks"].list(); + + for (auto const& task : tasks) + { + lt::entry taskEntry; + taskEntry["task_id"] = task.taskId; + taskEntry["magnet_uri"] = task.magnetUri; + taskEntry["save_path"] = task.savePath; + taskEntry["name"] = task.name; + taskEntry["added_timestamp"] = task.addedTimestamp; + taskEntry["total_size"] = task.totalSize; + taskEntry["downloaded_size"] = task.downloadedSize; + taskEntry["status"] = task.status; + + if (!task.resumeData.empty()) + { + taskEntry["resume_data"] = std::string( + reinterpret_cast(task.resumeData.data()), + task.resumeData.size() + ); + } + + taskList.push_back(std::move(taskEntry)); + } + + std::vector buf; + lt::bencode(std::back_inserter(buf), exportData); + + std::ofstream file(filePath, std::ios::binary); + if (!file) return false; + + file.write(buf.data(), buf.size()); + return file.good(); + } + catch (std::exception const& ex) + { + OutputDebugStringA(("ExportToFile error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + } + + bool TorrentStateManager::ImportFromFile(std::wstring const& filePath) + { + // Read file first without lock, then save to database + std::vector tasksToImport; + + try + { + std::ifstream file(filePath, std::ios::binary); + if (!file) return false; + + std::vector buf((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + + lt::error_code ec; + lt::bdecode_node node = lt::bdecode(buf, ec); + if (ec) return false; + + auto tasksNode = node.dict_find_list("tasks"); + if (!tasksNode) return false; + + for (int i = 0; i < tasksNode.list_size(); ++i) + { + auto taskNode = tasksNode.list_at(i); + if (taskNode.type() != lt::bdecode_node::dict_t) continue; + + TaskMetadata metadata; + metadata.taskId = taskNode.dict_find_string_value("task_id").to_string(); + metadata.magnetUri = taskNode.dict_find_string_value("magnet_uri").to_string(); + metadata.savePath = taskNode.dict_find_string_value("save_path").to_string(); + metadata.name = taskNode.dict_find_string_value("name").to_string(); + metadata.addedTimestamp = taskNode.dict_find_int_value("added_timestamp"); + metadata.totalSize = taskNode.dict_find_int_value("total_size"); + metadata.downloadedSize = taskNode.dict_find_int_value("downloaded_size"); + metadata.status = static_cast(taskNode.dict_find_int_value("status")); + + auto resumeStr = taskNode.dict_find_string_value("resume_data"); + if (!resumeStr.empty()) + { + metadata.resumeData.assign( + reinterpret_cast(resumeStr.data()), + reinterpret_cast(resumeStr.data()) + resumeStr.size() + ); + } + + // Generate new task ID if empty + if (metadata.taskId.empty()) + { + metadata.taskId = GenerateTaskId(); + } + + tasksToImport.push_back(std::move(metadata)); + } + } + catch (std::exception const& ex) + { + OutputDebugStringA(("ImportFromFile read error: " + std::string(ex.what()) + "\n").c_str()); + return false; + } + + // Now save all tasks to database + for (auto const& metadata : tasksToImport) + { + SaveTaskMetadata(metadata); + } + + return true; + } + + std::string TorrentStateManager::GenerateTaskId() + { + auto now = std::chrono::system_clock::now(); + auto timestamp = std::chrono::duration_cast( + now.time_since_epoch()).count(); + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> dis(0, 0xFFFF); + + std::ostringstream oss; + oss << std::hex << std::setfill('0') << std::setw(12) << timestamp + << std::setw(4) << dis(gen); + + return oss.str(); + } + +} // namespace OpenNet::Core::Torrent diff --git a/OpenNet/Core/torrentCore/TorrentStateManager.h b/OpenNet/Core/torrentCore/TorrentStateManager.h new file mode 100644 index 0000000..ed51f36 --- /dev/null +++ b/OpenNet/Core/torrentCore/TorrentStateManager.h @@ -0,0 +1,86 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace OpenNet::Core::Torrent +{ + // Task metadata stored in SQLite + struct TaskMetadata + { + std::string taskId; // Unique task identifier + std::string magnetUri; // Original magnet URI + std::string savePath; // Download save path + std::string name; // Torrent name + int64_t addedTimestamp{}; // When the task was added + int64_t totalSize{}; // Total size in bytes + int64_t downloadedSize{}; // Downloaded size in bytes + int status{}; // Task status: 0=pending, 1=downloading, 2=paused, 3=completed, 4=failed + std::vector resumeData; // libtorrent resume data blob + }; + + // Manages libtorrent session state and resume data persistence + // Uses SQLite for metadata and WinUI3 LocalFolder for storage + class TorrentStateManager + { + public: + TorrentStateManager(); + ~TorrentStateManager(); + + TorrentStateManager(TorrentStateManager const&) = delete; + TorrentStateManager& operator=(TorrentStateManager const&) = delete; + + // Initialize the state manager (creates database, loads existing state) + bool Initialize(std::wstring const& basePath = L""); + + // Get the storage base path (LocalFolder) + std::wstring GetStoragePath() const; + + // Session state operations + bool SaveSessionState(libtorrent::session& session); + bool LoadSessionState(libtorrent::session& session); + + // Task resume data operations + bool SaveTaskResumeData(std::string const& taskId, libtorrent::add_torrent_params const& params); + std::optional LoadTaskResumeData(std::string const& taskId); + + // Task metadata operations + bool SaveTaskMetadata(TaskMetadata const& metadata); + std::optional LoadTaskMetadata(std::string const& taskId); + std::vector LoadAllTasks(); + bool DeleteTask(std::string const& taskId); + bool UpdateTaskStatus(std::string const& taskId, int status); + bool UpdateTaskProgress(std::string const& taskId, int64_t downloadedSize); + + // Import/Export functionality + bool ExportToFile(std::wstring const& filePath); + bool ImportFromFile(std::wstring const& filePath); + + // Generate a unique task ID + static std::string GenerateTaskId(); + + private: + bool InitializeDatabase(); + bool CreateTables(); + bool CloseDatabase(); + + std::wstring m_storagePath; + std::wstring m_dbPath; + void* m_db{ nullptr }; // sqlite3* + std::mutex m_dbMutex; + bool m_initialized{ false }; + + // File paths + static constexpr const wchar_t* DATABASE_FILENAME = L"torrent_state.db"; + static constexpr const wchar_t* SESSION_STATE_FILENAME = L"session_state.dat"; + static constexpr const char* RESUME_DATA_FOLDER = "resume_data"; + }; + +} // namespace OpenNet::Core::Torrent diff --git a/OpenNet/Core/torrentCore/libtorrentHandle.cpp b/OpenNet/Core/torrentCore/libtorrentHandle.cpp index ac1f2cf..7cc477f 100644 --- a/OpenNet/Core/torrentCore/libtorrentHandle.cpp +++ b/OpenNet/Core/torrentCore/libtorrentHandle.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "libtorrentHandle.h" +#include "TorrentStateManager.h" #include #include @@ -8,6 +9,8 @@ #include #include #include +#include +#include #include #include @@ -21,6 +24,8 @@ namespace OpenNet::Core::Torrent LibtorrentHandle::LibtorrentHandle() {} LibtorrentHandle::~LibtorrentHandle() { + // Save all resume data before stopping + SaveAllResumeData(); Stop(); } @@ -32,6 +37,12 @@ namespace OpenNet::Core::Torrent auto pack = std::make_unique(); ConfigureDefaultSettings(*pack); m_session = std::make_unique(*pack); + + // Load saved session state if state manager is available + if (m_stateManager) + { + m_stateManager->LoadSessionState(*m_session); + } } catch (std::exception const& ex) { @@ -53,7 +64,11 @@ namespace OpenNet::Core::Torrent pack.set_bool(lt::settings_pack::enable_outgoing_tcp, true); pack.set_bool(lt::settings_pack::enable_incoming_utp, true); pack.set_bool(lt::settings_pack::enable_outgoing_utp, true); - // 可根据需要增加更多设置 + // Enable alerts for resume data + pack.set_int(lt::settings_pack::alert_mask, + lt::alert_category::status | + lt::alert_category::error | + lt::alert_category::storage); } void LibtorrentHandle::Start() @@ -67,6 +82,12 @@ namespace OpenNet::Core::Torrent void LibtorrentHandle::Stop() { + // Save session state before stopping + if (m_session && m_stateManager) + { + m_stateManager->SaveSessionState(*m_session); + } + m_stopRequested = true; if (m_running.exchange(false)) { @@ -86,8 +107,34 @@ namespace OpenNet::Core::Torrent { lt::add_torrent_params atp = lt::parse_magnet_uri(magnetUri); atp.save_path = savePath; // 目标目录 - atp.flags |= lt::torrent_flags::seed_mode; // 可根据需要调整 - m_session->async_add_torrent(atp); + // Remove seed_mode flag for downloads + atp.flags &= ~lt::torrent_flags::seed_mode; + + // Generate task ID and save metadata + std::string taskId = TorrentStateManager::GenerateTaskId(); + + if (m_stateManager) + { + TaskMetadata metadata; + metadata.taskId = taskId; + metadata.magnetUri = magnetUri; + metadata.savePath = savePath; + metadata.name = ""; // Will be updated when metadata is received + metadata.addedTimestamp = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + metadata.status = 1; // Downloading + m_stateManager->SaveTaskMetadata(metadata); + } + + lt::torrent_handle handle = m_session->add_torrent(atp); + + // Store mapping + { + std::lock_guard lk(m_torrentMapMutex); + m_taskIdToHandle[taskId] = handle; + m_handleToTaskId[handle] = taskId; + } + return true; } catch (std::exception const& ex) @@ -98,6 +145,133 @@ namespace OpenNet::Core::Torrent } } + std::string LibtorrentHandle::AddTorrentFromResumeData(std::string const& taskId) + { + if (!Initialize()) return ""; + if (!m_stateManager) return ""; + + try + { + auto paramsOpt = m_stateManager->LoadTaskResumeData(taskId); + if (!paramsOpt.has_value()) + { + // Try to load from metadata + auto metadataOpt = m_stateManager->LoadTaskMetadata(taskId); + if (!metadataOpt.has_value() || metadataOpt->magnetUri.empty()) + { + return ""; + } + + // Re-add using magnet URI + lt::add_torrent_params atp = lt::parse_magnet_uri(metadataOpt->magnetUri); + atp.save_path = metadataOpt->savePath; + atp.flags &= ~lt::torrent_flags::seed_mode; + + lt::torrent_handle handle = m_session->add_torrent(atp); + + { + std::lock_guard lk(m_torrentMapMutex); + m_taskIdToHandle[taskId] = handle; + m_handleToTaskId[handle] = taskId; + } + + return taskId; + } + + lt::add_torrent_params atp = paramsOpt.value(); + lt::torrent_handle handle = m_session->add_torrent(atp); + + { + std::lock_guard lk(m_torrentMapMutex); + m_taskIdToHandle[taskId] = handle; + m_handleToTaskId[handle] = taskId; + } + + return taskId; + } + catch (std::exception const& ex) + { + std::lock_guard lk(m_cbMutex); + if (m_errorCb) m_errorCb(std::string("AddTorrentFromResumeData error: ") + ex.what()); + return ""; + } + } + + void LibtorrentHandle::PauseTorrent(std::string const& taskId) + { + std::lock_guard lk(m_torrentMapMutex); + auto it = m_taskIdToHandle.find(taskId); + if (it != m_taskIdToHandle.end() && it->second.is_valid()) + { + it->second.pause(); + if (m_stateManager) + { + m_stateManager->UpdateTaskStatus(taskId, 2); // Paused + } + } + } + + void LibtorrentHandle::ResumeTorrent(std::string const& taskId) + { + std::lock_guard lk(m_torrentMapMutex); + auto it = m_taskIdToHandle.find(taskId); + if (it != m_taskIdToHandle.end() && it->second.is_valid()) + { + it->second.resume(); + if (m_stateManager) + { + m_stateManager->UpdateTaskStatus(taskId, 1); // Downloading + } + } + } + + void LibtorrentHandle::RemoveTorrent(std::string const& taskId, bool deleteFiles) + { + lt::torrent_handle handle; + { + std::lock_guard lk(m_torrentMapMutex); + auto it = m_taskIdToHandle.find(taskId); + if (it == m_taskIdToHandle.end()) return; + handle = it->second; + m_handleToTaskId.erase(handle); + m_taskIdToHandle.erase(it); + } + + if (m_session && handle.is_valid()) + { + lt::remove_flags_t flags = {}; + if (deleteFiles) + { + flags = lt::session::delete_files; + } + m_session->remove_torrent(handle, flags); + } + + if (m_stateManager) + { + m_stateManager->DeleteTask(taskId); + } + } + + void LibtorrentHandle::SaveAllResumeData() + { + if (!m_session) return; + + std::lock_guard lk(m_torrentMapMutex); + for (auto const& [taskId, handle] : m_taskIdToHandle) + { + if (handle.is_valid()) + { + RequestResumeDataForTorrent(handle); + } + } + } + + void LibtorrentHandle::SetStateManager(TorrentStateManager* stateManager) + { + m_stateManager = stateManager; + } + void LibtorrentHandle::SetProgressCallback(ProgressCallback cb) { std::lock_guard lk(m_cbMutex); @@ -114,6 +288,27 @@ namespace OpenNet::Core::Torrent m_errorCb = std::move(cb); } + std::string LibtorrentHandle::GetTaskIdByName(std::string const& name) const + { + std::lock_guard lk(m_torrentMapMutex); + for (auto const& [taskId, handle] : m_taskIdToHandle) + { + if (handle.is_valid()) + { + try + { + auto status = handle.status(); + if (status.name == name) + { + return taskId; + } + } + catch (...) {} + } + } + return ""; + } + void LibtorrentHandle::AlertLoop() { while (!m_stopRequested.load()) @@ -149,10 +344,24 @@ namespace OpenNet::Core::Torrent evt.uploadRateKB = static_cast(s.upload_rate / 1000); evt.name = s.name; m_progressCb(evt); + + // Update progress in database + if (m_stateManager) + { + std::lock_guard mapLk(m_torrentMapMutex); + auto it = m_handleToTaskId.find(s.handle); + if (it != m_handleToTaskId.end()) + { + m_stateManager->UpdateTaskProgress(it->second, s.total_done); + } + } } } else if (auto tf = lt::alert_cast(a)) { + // Request resume data when torrent finishes + RequestResumeDataForTorrent(tf->handle); + std::lock_guard lk(m_cbMutex); if (m_finishedCb) { @@ -160,10 +369,29 @@ namespace OpenNet::Core::Torrent { auto status = tf->handle.status(); m_finishedCb(status.name); + + // Update status in database + if (m_stateManager) + { + std::lock_guard mapLk(m_torrentMapMutex); + auto it = m_handleToTaskId.find(tf->handle); + if (it != m_handleToTaskId.end()) + { + m_stateManager->UpdateTaskStatus(it->second, 3); // Completed + } + } } catch (...) {} } } + else if (auto srd = lt::alert_cast(a)) + { + HandleSaveResumeDataAlert(srd); + } + else if (auto srdf = lt::alert_cast(a)) + { + HandleSaveResumeDataFailedAlert(srdf); + } else if (auto se = lt::alert_cast(a)) { std::lock_guard lk(m_cbMutex); @@ -171,6 +399,17 @@ namespace OpenNet::Core::Torrent } else if (auto te = lt::alert_cast(a)) { + // Update status in database + if (m_stateManager) + { + std::lock_guard mapLk(m_torrentMapMutex); + auto it = m_handleToTaskId.find(te->handle); + if (it != m_handleToTaskId.end()) + { + m_stateManager->UpdateTaskStatus(it->second, 4); // Failed + } + } + std::lock_guard lk(m_cbMutex); if (m_errorCb) m_errorCb(te->message()); } @@ -179,6 +418,71 @@ namespace OpenNet::Core::Torrent std::lock_guard lk(m_cbMutex); if (m_errorCb) m_errorCb(fe->message()); } + else if (auto ma = lt::alert_cast(a)) + { + // Update task name when metadata is received + if (m_stateManager && ma->handle.is_valid()) + { + try + { + auto status = ma->handle.status(); + std::lock_guard mapLk(m_torrentMapMutex); + auto it = m_handleToTaskId.find(ma->handle); + if (it != m_handleToTaskId.end()) + { + auto metaOpt = m_stateManager->LoadTaskMetadata(it->second); + if (metaOpt.has_value()) + { + TaskMetadata meta = metaOpt.value(); + meta.name = status.name; + meta.totalSize = status.total_wanted; + m_stateManager->SaveTaskMetadata(meta); + } + } + } + catch (...) {} + } + } + } + } + + void LibtorrentHandle::HandleSaveResumeDataAlert(lt::save_resume_data_alert const* alert) + { + if (!m_stateManager) return; + if (!alert || !alert->handle.is_valid()) return; + + try + { + std::lock_guard lk(m_torrentMapMutex); + auto it = m_handleToTaskId.find(alert->handle); + if (it != m_handleToTaskId.end()) + { + m_stateManager->SaveTaskResumeData(it->second, alert->params); + } + } + catch (std::exception const& ex) + { + OutputDebugStringA(("HandleSaveResumeDataAlert error: " + std::string(ex.what()) + "\n").c_str()); + } + } + + void LibtorrentHandle::HandleSaveResumeDataFailedAlert(lt::save_resume_data_failed_alert const* alert) + { + if (alert) + { + OutputDebugStringA(("Save resume data failed: " + alert->message() + "\n").c_str()); + } + } + + void LibtorrentHandle::RequestResumeDataForTorrent(lt::torrent_handle const& handle) + { + if (handle.is_valid()) + { + try + { + handle.save_resume_data(lt::torrent_handle::save_info_dict); + } + catch (...) {} } } } \ No newline at end of file diff --git a/OpenNet/Core/torrentCore/libtorrentHandle.h b/OpenNet/Core/torrentCore/libtorrentHandle.h index 5312991..b64e6a1 100644 --- a/OpenNet/Core/torrentCore/libtorrentHandle.h +++ b/OpenNet/Core/torrentCore/libtorrentHandle.h @@ -6,6 +6,7 @@ #include #include #include +#include // 直接包含 libtorrent 头,避免与 inline namespace 冲突 //forward declarations 前置声明 @@ -17,6 +18,10 @@ #include #include #include +#include + +// Forward declaration +namespace OpenNet::Core::Torrent { class TorrentStateManager; } namespace OpenNet::Core::Torrent { @@ -48,16 +53,40 @@ namespace OpenNet::Core::Torrent void Stop(); bool AddMagnet(std::string const& magnetUri, std::string const& savePath); + // Resume torrent from saved state (returns task ID if successful) + std::string AddTorrentFromResumeData(std::string const& taskId); + + // Pause a specific torrent by task ID + void PauseTorrent(std::string const& taskId); + + // Resume a specific torrent by task ID + void ResumeTorrent(std::string const& taskId); + + // Remove a torrent by task ID + void RemoveTorrent(std::string const& taskId, bool deleteFiles = false); + + // Save resume data for all active torrents + void SaveAllResumeData(); + + // Set the state manager for persistence + void SetStateManager(TorrentStateManager* stateManager); + void SetProgressCallback(ProgressCallback cb); void SetFinishedCallback(FinishedCallback cb); void SetErrorCallback(ErrorCallback cb); bool IsRunning() const noexcept { return m_running.load(); } + // Get the task ID for a torrent name + std::string GetTaskIdByName(std::string const& name) const; + private: void AlertLoop(); void DispatchAlerts(std::vector const& alerts); void ConfigureDefaultSettings(libtorrent::settings_pack& pack); + void HandleSaveResumeDataAlert(libtorrent::save_resume_data_alert const* alert); + void HandleSaveResumeDataFailedAlert(libtorrent::save_resume_data_failed_alert const* alert); + void RequestResumeDataForTorrent(libtorrent::torrent_handle const& handle); std::unique_ptr m_session; std::atomic m_running{ false }; @@ -69,6 +98,14 @@ namespace OpenNet::Core::Torrent ErrorCallback m_errorCb; std::atomic m_stopRequested{ false }; + + // State manager for persistence (not owned) + TorrentStateManager* m_stateManager{ nullptr }; + + // Mapping from task ID to torrent handle + mutable std::mutex m_torrentMapMutex; + std::unordered_map m_taskIdToHandle; + std::unordered_map m_handleToTaskId; }; } \ No newline at end of file diff --git a/OpenNet/OpenNet.vcxproj b/OpenNet/OpenNet.vcxproj index 52ec898..bd48548 100644 --- a/OpenNet/OpenNet.vcxproj +++ b/OpenNet/OpenNet.vcxproj @@ -151,6 +151,7 @@ + Helpers\NavItemIconHelper.idl Code @@ -330,6 +331,7 @@ + diff --git a/OpenNet/OpenNet.vcxproj.filters b/OpenNet/OpenNet.vcxproj.filters index d409b0a..22d7c5a 100644 --- a/OpenNet/OpenNet.vcxproj.filters +++ b/OpenNet/OpenNet.vcxproj.filters @@ -148,6 +148,9 @@ Core\torrentCore + + Core\torrentCore + ViewModels @@ -231,6 +234,9 @@ Core\torrentCore + + Core\torrentCore + ViewModels diff --git a/OpenNet/ViewModels/TasksViewModel.cpp b/OpenNet/ViewModels/TasksViewModel.cpp index 8edc74e..eba85b7 100644 --- a/OpenNet/ViewModels/TasksViewModel.cpp +++ b/OpenNet/ViewModels/TasksViewModel.cpp @@ -2,9 +2,13 @@ #include "ViewModels/TasksViewModel.h" #include #include +#include #include "Core/P2PManager.h" +#include "Core/torrentCore/TorrentStateManager.h" #include "mvvm_framework/mvvm_hresult_helper.h" +#include + using namespace std::string_literals; namespace winrt::OpenNet::ViewModels::implementation @@ -79,6 +83,49 @@ namespace winrt::OpenNet::ViewModels::implementation }) .Build(); + // ExportCommand: Export tasks to a file + m_exportCommand = mvvm::AsyncCommandBuilder(*this) + .ExecuteAsync([](winrt::Windows::Foundation::IInspectable const&) -> winrt::Windows::Foundation::IAsyncAction + { + co_await winrt::resume_background(); + auto& mgr = ::OpenNet::Core::P2PManager::Instance(); + if (mgr.StateManager()) + { + // Use a default export path in LocalFolder + auto localFolder = winrt::Windows::Storage::ApplicationData::Current().LocalFolder(); + std::wstring exportPath = localFolder.Path().c_str(); + exportPath += L"\\tasks_export.dat"; + co_await mgr.ExportTasksAsync(exportPath); + } + co_return; + }) + .Build(); + + // ImportCommand: Import tasks from a file + m_importCommand = mvvm::AsyncCommandBuilder(*this) + .ExecuteAsync([weak = get_weak()](winrt::Windows::Foundation::IInspectable const&) -> winrt::Windows::Foundation::IAsyncAction + { + co_await winrt::resume_background(); + auto& mgr = ::OpenNet::Core::P2PManager::Instance(); + if (mgr.StateManager()) + { + // Use a default import path in LocalFolder + auto localFolder = winrt::Windows::Storage::ApplicationData::Current().LocalFolder(); + std::wstring importPath = localFolder.Path().c_str(); + importPath += L"\\tasks_export.dat"; + bool result = co_await mgr.ImportTasksAsync(importPath); + if (result) + { + if (auto self = weak.get()) + { + self->LoadSavedTasks(); + } + } + } + co_return; + }) + .Build(); + // 注册回调 ::OpenNet::Core::P2PManager::Instance().SetProgressCallback([weak = get_weak()](const ::OpenNet::Core::Torrent::LibtorrentHandle::ProgressEvent& e) { @@ -99,6 +146,7 @@ namespace winrt::OpenNet::ViewModels::implementation void TasksViewModel::Initialize() { (void)::OpenNet::Core::P2PManager::Instance().EnsureTorrentCoreInitializedAsync(); + LoadSavedTasks(); RebuildFiltered(); } @@ -106,6 +154,91 @@ namespace winrt::OpenNet::ViewModels::implementation { } + // Helper function to format timestamp to date string + static winrt::hstring FormatTimestamp(int64_t timestamp) + { + if (timestamp <= 0) return L"-"; + + std::time_t time = static_cast(timestamp); + std::tm tm_buf{}; +#ifdef _WIN32 + localtime_s(&tm_buf, &time); +#else + localtime_r(&time, &tm_buf); +#endif + wchar_t buf[64]; + swprintf(buf, 64, L"%04d-%02d-%02d %02d:%02d", + tm_buf.tm_year + 1900, tm_buf.tm_mon + 1, tm_buf.tm_mday, + tm_buf.tm_hour, tm_buf.tm_min); + return winrt::hstring{ buf }; + } + + void TasksViewModel::LoadSavedTasks() + { + auto dispatcher = m_dispatcher; + if (!dispatcher) return; + + auto tasks = ::OpenNet::Core::P2PManager::Instance().GetAllTasks(); + + dispatcher.TryEnqueue([weak = get_weak(), tasks = std::move(tasks)]() + { + if (auto self = weak.get()) + { + for (auto const& task : tasks) + { + winrt::hstring name = task.name.empty() + ? winrt::to_hstring(task.magnetUri.substr(0, 40) + "...") + : winrt::to_hstring(task.name); + + auto vm = self->FindOrCreateItemByTaskId(task.taskId, name); + + // Set add date from timestamp + vm.AddDate(FormatTimestamp(task.addedTimestamp)); + + // Set progress based on status + if (task.status == 3) // Completed + { + vm.Progress(L"100%"); + vm.DownloadRate(L"0 KB/s"); + } + else if (task.totalSize > 0 && task.downloadedSize > 0) + { + int percent = static_cast((task.downloadedSize * 100) / task.totalSize); + wchar_t buf[32]; + swprintf(buf, 32, L"%d%%", percent); + vm.Progress(buf); + } + else + { + vm.Progress(L"0%"); + } + + // Format size + if (task.totalSize > 0) + { + wchar_t sizeBuf[64]; + double sizeGB = task.totalSize / (1024.0 * 1024.0 * 1024.0); + if (sizeGB >= 1.0) + { + swprintf(sizeBuf, 64, L"%.2f GB", sizeGB); + } + else + { + double sizeMB = task.totalSize / (1024.0 * 1024.0); + swprintf(sizeBuf, 64, L"%.2f MB", sizeMB); + } + vm.Size(sizeBuf); + } + else + { + vm.Size(L"-"); + } + } + self->RebuildFiltered(); + } + }); + } + winrt::OpenNet::ViewModels::TaskViewModel TasksViewModel::FindOrCreateItem(winrt::hstring const& name) { for (auto const& item : m_tasks) @@ -117,7 +250,28 @@ namespace winrt::OpenNet::ViewModels::implementation } auto vm = winrt::make(); vm.Name(name); - vm.AddDate(winrt::clock().now().time_since_epoch().count() ? L"" : L""); // placeholder + // Set current time as add date for new items + auto now = std::chrono::system_clock::now(); + auto timestamp = std::chrono::duration_cast(now.time_since_epoch()).count(); + vm.AddDate(FormatTimestamp(timestamp)); + m_tasks.Append(vm); + return vm; + } + + winrt::OpenNet::ViewModels::TaskViewModel TasksViewModel::FindOrCreateItemByTaskId(std::string const& taskId, winrt::hstring const& name) + { + // First try to find by name (for display purposes) + for (auto const& item : m_tasks) + { + if (item.Name() == name) + { + return item; + } + } + + auto vm = winrt::make(); + vm.Name(name); + // Date will be set by the caller with the correct timestamp m_tasks.Append(vm); return vm; } diff --git a/OpenNet/ViewModels/TasksViewModel.h b/OpenNet/ViewModels/TasksViewModel.h index bf45e70..aea0591 100644 --- a/OpenNet/ViewModels/TasksViewModel.h +++ b/OpenNet/ViewModels/TasksViewModel.h @@ -28,6 +28,8 @@ namespace winrt::OpenNet::ViewModels::implementation winrt::Microsoft::UI::Xaml::Input::ICommand StartCommand() const { return m_startCommand; } winrt::Microsoft::UI::Xaml::Input::ICommand PauseCommand() const { return m_pauseCommand; } winrt::Microsoft::UI::Xaml::Input::ICommand DeleteCommand()const { return m_deleteCommand; } + winrt::Microsoft::UI::Xaml::Input::ICommand ExportCommand() const { return m_exportCommand; } + winrt::Microsoft::UI::Xaml::Input::ICommand ImportCommand() const { return m_importCommand; } void Initialize(); void Shutdown(); @@ -49,6 +51,8 @@ namespace winrt::OpenNet::ViewModels::implementation winrt::Microsoft::UI::Xaml::Input::ICommand m_startCommand{ nullptr }; winrt::Microsoft::UI::Xaml::Input::ICommand m_pauseCommand{ nullptr }; winrt::Microsoft::UI::Xaml::Input::ICommand m_deleteCommand{ nullptr }; + winrt::Microsoft::UI::Xaml::Input::ICommand m_exportCommand{ nullptr }; + winrt::Microsoft::UI::Xaml::Input::ICommand m_importCommand{ nullptr }; // 缺失的调度器字段(原 cpp 使用但未声明) winrt::Microsoft::UI::Dispatching::DispatcherQueue m_dispatcher{ nullptr }; @@ -57,9 +61,11 @@ namespace winrt::OpenNet::ViewModels::implementation winrt::hstring m_currentFilter{ L"AllTasks" }; winrt::OpenNet::ViewModels::TaskViewModel FindOrCreateItem(winrt::hstring const& name); + winrt::OpenNet::ViewModels::TaskViewModel FindOrCreateItemByTaskId(std::string const& taskId, winrt::hstring const& name); void OnProgress(const struct ::OpenNet::Core::Torrent::LibtorrentHandle::ProgressEvent& e); void OnFinished(std::string const& name); void OnError(std::string const& msg); + void LoadSavedTasks(); // Rebuild filtered view from full list according to current filter void RebuildFiltered(); diff --git a/OpenNet/ViewModels/TasksViewModel.idl b/OpenNet/ViewModels/TasksViewModel.idl index d008d8a..1af884c 100644 --- a/OpenNet/ViewModels/TasksViewModel.idl +++ b/OpenNet/ViewModels/TasksViewModel.idl @@ -17,6 +17,8 @@ namespace OpenNet.ViewModels Microsoft.UI.Xaml.Input.ICommand StartCommand { get; }; Microsoft.UI.Xaml.Input.ICommand PauseCommand { get; }; Microsoft.UI.Xaml.Input.ICommand DeleteCommand{ get; }; + Microsoft.UI.Xaml.Input.ICommand ExportCommand { get; }; + Microsoft.UI.Xaml.Input.ICommand ImportCommand { get; }; // Apply filter by tag: "AllTasks", "Downloading", "Completed", "Failed" void ApplyFilter(String tag);