phase 5: transfer service

This commit is contained in:
2026-04-27 01:21:16 +03:00
parent b5c506cf03
commit 895fee6235
49 changed files with 905 additions and 838 deletions
+17 -2
View File
@@ -1,4 +1,5 @@
.DS_Store .DS_Store
.serena
# Prerequisites # Prerequisites
*.d *.d
@@ -32,5 +33,19 @@
*.exe *.exe
*.out *.out
*.app *.app
server
client # Switch build artifacts
*.nro
*.nso
*.pfs0
*.nacp
*.elf
*.lst
*.map
# CMake
build/
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
compile_commands.json
+104
View File
@@ -0,0 +1,104 @@
cmake_minimum_required(VERSION 3.20)
project(NXST
LANGUAGES CXX
VERSION 0.1.0
)
# ── C++ standard and flags ────────────────────────────────────────────────────
# Arch/linker/libnx flags are already injected by the Switch.cmake toolchain.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS ON) # gnu++17
add_compile_options(
-fno-rtti
-fno-exceptions
-O2
-g
-D_GNU_SOURCE=1
)
# Export compilation database (enables clangd / clang-tidy on the host)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ── Sources ───────────────────────────────────────────────────────────────────
file(GLOB_RECURSE NXST_SOURCES
src/app/*.cpp
src/domain/*.cpp
src/infra/net/*.cpp
src/infra/fs/*.cpp
src/infra/sys/*.cpp
src/service/*.cpp
src/ui/*.cpp
)
file(GLOB_RECURSE PLUTONIUM_SOURCES
lib/Plutonium/source/*.cpp
)
add_executable(NXST ${NXST_SOURCES} ${PLUTONIUM_SOURCES})
# ── Include paths ─────────────────────────────────────────────────────────────
target_include_directories(NXST PRIVATE
include
lib/Plutonium/include
)
# ── pkg-config (uses aarch64-none-elf-pkg-config set by Switch.cmake) ─────────
find_package(PkgConfig REQUIRED)
set(NXST_PKG_MODULES
SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer
freetype2 harfbuzz minizip libpng libjpeg libwebp
glesv2 egl glapi zlib
)
pkg_check_modules(PORTLIBS REQUIRED IMPORTED_TARGET ${NXST_PKG_MODULES})
target_include_directories(NXST PRIVATE ${PORTLIBS_INCLUDE_DIRS})
# ── Link libraries ────────────────────────────────────────────────────────────
# Order matters for static linking: put most dependent libs first.
# libpu.a first (contains C wrappers not in Plutonium source).
# drm_nouveau, harfbuzz, freetype, z appended explicitly after pkg-config output
# to fix the freetype→harfbuzz static link order (see build notes from libnx update).
target_link_libraries(NXST PRIVATE
${CMAKE_SOURCE_DIR}/lib/Plutonium/lib/libpu.a
PkgConfig::PORTLIBS
drm_nouveau
harfbuzz
freetype
z
)
# ── NACP + NRO ────────────────────────────────────────────────────────────────
set(NXST_NACP ${CMAKE_CURRENT_BINARY_DIR}/NXST.nacp)
nx_generate_nacp(
OUTPUT ${NXST_NACP}
NAME "NXST"
AUTHOR "DragonSpirit"
VERSION "04.26.2026"
)
nx_create_nro(NXST
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro
ICON ${CMAKE_SOURCE_DIR}/icon.png
NACP ${NXST_NACP}
)
# ── Convenience targets ────────────────────────────────────────────────────────
find_program(NXLINK_EXE nxlink HINTS ${DEVKITPRO}/tools/bin)
if(NXLINK_EXE)
add_custom_target(send
COMMAND ${NXLINK_EXE} ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro
DEPENDS NXST_nro
COMMENT "Sending NXST.nro via nxlink"
)
add_custom_target(debug
COMMAND ${NXLINK_EXE} -s ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro
DEPENDS NXST_nro
COMMENT "Sending NXST.nro with stdio bridge"
)
endif()
+2 -2
View File
@@ -32,8 +32,8 @@ include $(DEVKITPRO)/libnx/switch_rules
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
TARGET := NXST TARGET := NXST
BUILD := build BUILD := build
SOURCES := source lib/Plutonium/source SOURCES := src/app src/domain src/infra/net src/infra/fs src/infra/sys src/service src/ui lib/Plutonium/source
INCLUDES := include include/net lib/Plutonium/include INCLUDES := include lib/Plutonium/include
EXEFS_SRC := exefs_src EXEFS_SRC := exefs_src
APP_TITLE := NXST APP_TITLE := NXST
APP_AUTHOR := DragonSpirit APP_AUTHOR := DragonSpirit
+3 -3
View File
@@ -7,14 +7,14 @@
| 0 | Tooling & ground rules | ✅ Done | S (~2h) | `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes` | | 0 | Tooling & ground rules | ✅ Done | S (~2h) | `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes` |
| 1 | Bug fixes & dead code | ✅ Done | S (~3h) | logger rewrite, `mkdir 0777`, RU comment, dead code | | 1 | Bug fixes & dead code | ✅ Done | S (~3h) | logger rewrite, `mkdir 0777`, RU comment, dead code |
| 2 | File renames + `#pragma once` | ✅ Done | S (~2h) | snake_case filenames, unify guards | | 2 | File renames + `#pragma once` | ✅ Done | S (~2h) | snake_case filenames, unify guards |
| 3 | Directory restructure | ☐ Not started | M (~1d) | `src/` + `include/nxst/` layered tree | | 3 | Directory restructure | ✅ Done | M (~1d) | `src/` + `include/nxst/` layered tree |
| 4 | Make → CMake migration | ☐ Not started | M (~1d) | devkitpro `Switch.cmake` toolchain | | 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain |
| 5 | TransferService extraction | ☐ Not started | L (~2d) | kill globals, sever UI ↔ net coupling | | 5 | TransferService extraction | ☐ Not started | L (~2d) | kill globals, sever UI ↔ net coupling |
| 6 | `Result<T>` + RAII | ☐ Not started | M (~1d) | tagged union, OS handle wrappers, split `restore()` | | 6 | `Result<T>` + RAII | ☐ Not started | M (~1d) | tagged union, OS handle wrappers, split `restore()` |
| 7 | Documentation + license | ☐ Not started | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE | | 7 | Documentation + license | ☐ Not started | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE |
| 8 | CI | ☐ Not started | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check | | 8 | CI | ☐ Not started | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check |
**Active phase:** Phase 3Directory restructure. **Active phase:** Phase 5TransferService extraction.
**Last updated:** 2026-04-26. **Last updated:** 2026-04-26.
Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth. Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth.
-13
View File
@@ -1,13 +0,0 @@
#include <string>
#include <switch.h>
int transfer_files(size_t index, AccountUid uid);
bool isClientTransferDone();
bool isClientTransferCancelled();
bool isClientConnectionFailed();
bool isClientProgressKnown();
bool isClientWorkersIdle();
void cancelClientTransfer();
double getClientProgress();
std::string getClientStatusText();
std::string getClientFailReason();
@@ -1,16 +1,15 @@
#pragma once #pragma once
#include <const.h> #include <nxst/ui/const.h>
#include "account.hpp" #include <nxst/domain/account.hpp>
#include "title.hpp" #include <nxst/domain/title.hpp>
#include "util.hpp" #include <nxst/domain/util.hpp>
#include <memory> #include <memory>
#include <switch.h> #include <switch.h>
#include "logger.hpp" #include <nxst/infra/sys/logger.hpp>
typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t; typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t;
inline float g_currentTime = 0; inline float g_currentTime = 0;
inline AccountUid g_currentUId;
inline bool g_backupScrollEnabled = 0; inline bool g_backupScrollEnabled = 0;
inline bool g_notificationLedAvailable = false; inline bool g_notificationLedAvailable = false;
inline bool g_shouldExitNetworkLoop = false; inline bool g_shouldExitNetworkLoop = false;
@@ -1,21 +1,22 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <users_layout.hpp> #include <nxst/service/transfer_service.hpp>
#include <titles_layout.hpp> #include <nxst/ui/users_layout.hpp>
#include <nxst/ui/titles_layout.hpp>
namespace ui {
namespace ui {
class MainApplication : public pu::ui::Application {
class MainApplication : public pu::ui::Application {
public:
using Application::Application; public:
PU_SMART_CTOR(MainApplication) using Application::Application;
PU_SMART_CTOR(MainApplication)
void OnLoad() override;
void OnLoad() override;
// Layout instance
UsersLayout::Ref usersLayout; UsersLayout::Ref users_layout;
TitlesLayout::Ref titlesLayout; TitlesLayout::Ref titles_layout;
}; nxst::TransferService transfer;
};
} }
@@ -25,9 +25,9 @@
*/ */
#pragma once #pragma once
#include "account.hpp" #include <nxst/domain/account.hpp>
#include "filesystem.hpp" #include <nxst/infra/fs/filesystem.hpp>
#include "io.hpp" #include <nxst/infra/fs/io.hpp>
#include <algorithm> #include <algorithm>
#include <stdlib.h> #include <stdlib.h>
#include <string> #include <string>
@@ -25,9 +25,9 @@
*/ */
#pragma once #pragma once
#include "account.hpp" #include <nxst/domain/account.hpp>
#include "common.hpp" #include <nxst/domain/common.hpp>
#include "io.hpp" #include <nxst/infra/fs/io.hpp>
#include <switch.h> #include <switch.h>
#include <sys/stat.h> #include <sys/stat.h>
@@ -25,7 +25,7 @@
*/ */
#pragma once #pragma once
#include "account.hpp" #include <nxst/domain/account.hpp>
#include <switch.h> #include <switch.h>
namespace FileSystem { namespace FileSystem {
@@ -25,10 +25,10 @@
*/ */
#pragma once #pragma once
#include "account.hpp" #include <nxst/domain/account.hpp>
#include "directory.hpp" #include <nxst/infra/fs/directory.hpp>
#include "title.hpp" #include <nxst/domain/title.hpp>
#include "util.hpp" #include <nxst/domain/util.hpp>
#include <dirent.h> #include <dirent.h>
#include <switch.h> #include <switch.h>
#include <sys/stat.h> #include <sys/stat.h>
@@ -0,0 +1 @@
#pragma once
@@ -0,0 +1 @@
#pragma once
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include <atomic>
#include <pthread.h>
#include <string>
#include <switch.h>
#include <nxst/domain/transfer_state.hpp>
namespace nxst {
class TransferService {
public:
int startSend(size_t title_index, AccountUid uid);
void cancelSend();
bool isSendDone() const { return sender_state.done.load(); }
bool isSendCancelled() const { return sender_state.cancelled.load(); }
bool isSendConnectionFailed() const { return sender_state.connection_failed.load(); }
bool isSendProgressKnown() const { return sender_state.bytes_total.load() > 0; }
bool isSendWorkersIdle() const { return !sender_active.load(); }
double sendProgress() const { return sender_state.progress(); }
std::string sendStatusText() const { return sender_state.getStatus(); }
std::string sendFailReason() const { return sender_state.fail_reason; }
int startReceive(size_t title_index, AccountUid uid, std::string title_name);
void cancelReceive();
bool isReceiveDone() const { return receiver_state.done.load(); }
bool isReceiveCancelled() const { return receiver_state.cancelled.load(); }
bool isReceiveWorkersIdle() const {
return !receiver_accept_active.load() && !receiver_broadcast_active.load();
}
double receiveProgress() const { return receiver_state.progress(); }
std::string receiveStatusText() const { return receiver_state.getStatus(); }
bool restoreSucceeded() const { return restore_ok; }
std::string restoreError() const { return restore_error; }
private:
// Sender
TransferState sender_state;
std::atomic<int> sender_udp_sock{-1};
std::atomic<int> sender_tcp_sock{-1};
std::atomic<bool> sender_active{false};
// Receiver
TransferState receiver_state;
std::atomic<int> receiver_client_sock{-1};
std::atomic<int> receiver_listen_sock{-1};
std::atomic<int> receiver_bcast_sock{-1};
std::atomic<bool> receiver_accept_active{false};
std::atomic<bool> receiver_broadcast_active{false};
pthread_t receiver_bcast_thread{};
// Stored at startReceive, read after network transfer completes
size_t restore_title_index{0};
AccountUid restore_uid{};
std::string restore_title_name;
bool restore_ok{false};
std::string restore_error;
// Sender thread
struct SenderArgs { TransferService* svc; size_t title_index; AccountUid uid; };
static void* senderEntry(void* arg);
void runSender(size_t title_index, AccountUid uid);
void failSend(const std::string& reason);
int findServer(char* out_ip);
// Receiver threads
struct AcceptArgs { TransferService* svc; int server_fd; };
static void* broadcastEntry(void* arg);
static void* acceptEntry(void* arg);
void runBroadcast();
void runAccept(int server_fd);
std::string replaceUsername(const std::string& file_path) const;
};
} // namespace nxst
@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <theme.hpp> #include <nxst/ui/theme.hpp>
namespace ui { namespace ui {
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <theme.hpp> #include <nxst/ui/theme.hpp>
#define COLOR(hex) pu::ui::Color::FromHex(hex) #define COLOR(hex) pu::ui::Color::FromHex(hex)
#define BACKGROUND_COLOR theme::color::BgBase #define BACKGROUND_COLOR theme::color::BgBase
@@ -1,8 +1,8 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <theme.hpp> #include <nxst/ui/theme.hpp>
#include <ui/ui_context.hpp> #include <nxst/ui/ui_context.hpp>
#include <account.hpp> #include <nxst/domain/account.hpp>
namespace ui { namespace ui {
@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <theme.hpp> #include <nxst/ui/theme.hpp>
#include <vector> #include <vector>
#include <string> #include <string>
@@ -1,12 +1,13 @@
#pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <const.h> #include <nxst/ui/const.h>
#include <title.hpp> #include <nxst/domain/title.hpp>
#include <account.hpp> #include <nxst/domain/account.hpp>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <ui/header_bar.hpp> #include <nxst/ui/header_bar.hpp>
#include <ui/hint_bar.hpp> #include <nxst/ui/hint_bar.hpp>
namespace ui { namespace ui {
@@ -33,6 +34,7 @@ namespace ui {
pu::ui::elm::TextBlock::Ref emptyText; pu::ui::elm::TextBlock::Ref emptyText;
pu::ui::elm::TextBlock::Ref emptySub; pu::ui::elm::TextBlock::Ref emptySub;
AccountUid current_uid{};
TitlesFocus focus = TitlesFocus::List; TitlesFocus focus = TitlesFocus::List;
TitlesAction action = TitlesAction::Transfer; TitlesAction action = TitlesAction::Transfer;
int lockedListIndex = 0; int lockedListIndex = 0;
@@ -46,7 +48,7 @@ namespace ui {
public: public:
TitlesLayout(); TitlesLayout();
void InitTitles(); void InitTitles(AccountUid uid);
void LockInput() { m_inputLocked = true; } void LockInput() { m_inputLocked = true; }
void UnlockInput() { m_inputLocked = false; } void UnlockInput() { m_inputLocked = false; }
@@ -1,7 +1,7 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <theme.hpp> #include <nxst/ui/theme.hpp>
#include <util.hpp> #include <nxst/domain/util.hpp>
namespace ui { namespace ui {
@@ -2,7 +2,7 @@
#include <string> #include <string>
#include <optional> #include <optional>
#include <switch.h> #include <switch.h>
#include <account.hpp> #include <nxst/domain/account.hpp>
namespace ui { namespace ui {
struct UiContext { struct UiContext {
@@ -1,7 +1,7 @@
#include <pu/Plutonium> #include <pu/Plutonium>
#include <const.h> #include <nxst/ui/const.h>
#include <ui/header_bar.hpp> #include <nxst/ui/header_bar.hpp>
#include <ui/hint_bar.hpp> #include <nxst/ui/hint_bar.hpp>
#include <memory> #include <memory>
namespace ui { namespace ui {
-8
View File
@@ -1,8 +0,0 @@
#include <string>
int startSendingThread();
bool isServerTransferDone();
bool isServerTransferCancelled();
bool isServerWorkersIdle();
void cancelServerTransfer();
double getServerProgress();
std::string getServerStatusText();
-249
View File
@@ -1,249 +0,0 @@
#include <arpa/inet.h>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <sys/select.h>
#include <fstream>
#include <iostream>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#ifdef __SWITCH__
#include <client.hpp>
#include <io.hpp>
#include <switch.h>
#endif
#include <protocol.hpp>
#include <transfer_state.hpp>
namespace fs = std::filesystem;
using path = fs::path;
static TransferState g_client_state;
static std::atomic<int> g_client_udp_sock{-1};
static std::atomic<int> g_client_tcp_sock{-1};
static std::atomic<bool> g_client_thread_active{false};
bool isClientTransferDone() { return g_client_state.done.load(); }
bool isClientTransferCancelled() { return g_client_state.cancelled.load(); }
bool isClientConnectionFailed() { return g_client_state.connection_failed.load(); }
bool isClientProgressKnown() { return g_client_state.bytes_total.load() > 0; }
bool isClientWorkersIdle() { return !g_client_thread_active.load(); }
double getClientProgress() { return g_client_state.progress(); }
std::string getClientStatusText() { return g_client_state.getStatus(); }
std::string getClientFailReason() { return g_client_state.fail_reason; }
void cancelClientTransfer() {
g_client_state.cancelled.store(true);
int udp = g_client_udp_sock.exchange(-1);
if (udp >= 0) {
shutdown(udp, SHUT_RDWR);
close(udp);
}
int tcp = g_client_tcp_sock.exchange(-1);
if (tcp >= 0) {
shutdown(tcp, SHUT_RDWR);
close(tcp);
}
}
static bool send_all(int sock, const void* buf, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0);
if (n <= 0) return false;
sent += n;
}
return true;
}
static bool sendFile(int sock, const fs::path& filepath) {
std::ifstream infile(filepath, std::ios::binary | std::ios::ate);
if (!infile.is_open()) {
std::cerr << "File not found: " << filepath << std::endl;
return false;
}
uint32_t filename_len = (uint32_t)filepath.string().size();
uint64_t file_size = (uint64_t)infile.tellg();
infile.seekg(0, std::ios::beg);
std::cout << "Sending: " << filepath << " (" << file_size << " bytes)" << std::endl;
if (!send_all(sock, &filename_len, sizeof(filename_len))) return false;
if (!send_all(sock, filepath.c_str(), filename_len)) return false;
if (!send_all(sock, &file_size, sizeof(file_size))) return false;
std::vector<char> buffer(proto::BUF_SIZE);
uint64_t remaining = file_size;
while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
infile.read(buffer.data(), (std::streamsize)to_read);
std::streamsize count = infile.gcount();
if (count <= 0) break;
if (!send_all(sock, buffer.data(), (size_t)count)) {
std::cerr << "Failed to send data for: " << filepath << std::endl;
return false;
}
g_client_state.bytes_done.fetch_add((uint64_t)count);
remaining -= (uint64_t)count;
}
return true;
}
struct ThreadArgs { size_t index; AccountUid uid; };
static int find_server(char* server_ip);
static void fail_connect(const std::string& reason) {
g_client_state.fail_reason = reason;
g_client_state.connection_failed.store(true);
g_client_state.done.store(true);
}
static void* discovery_and_send_thread(void* arg) {
g_client_thread_active.store(true);
ThreadArgs* targs = static_cast<ThreadArgs*>(arg);
size_t index = targs->index;
AccountUid uid = targs->uid;
delete targs;
auto finish = [](void*) {
g_client_state.done.store(true);
g_client_thread_active.store(false);
return (void*)nullptr;
};
char server_ip[INET_ADDRSTRLEN];
if (find_server(server_ip) != 0) {
if (!g_client_state.cancelled.load())
fail_connect("No receiver found.\nMake sure the other Switch is in Receive mode.");
return finish(nullptr);
}
if (g_client_state.cancelled.load()) return finish(nullptr);
g_client_state.setStatus("Creating backup...");
auto backupResult = io::backup(index, uid);
if (!std::get<0>(backupResult)) {
fail_connect("Failed to create backup:\n" + std::get<2>(backupResult));
return finish(nullptr);
}
fs::path directory = std::get<2>(backupResult);
if (g_client_state.cancelled.load()) return finish(nullptr);
g_client_state.setStatus("Connecting...");
int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_fd < 0) { fail_connect("Failed to open socket."); return finish(nullptr); }
g_client_tcp_sock.store(tcp_fd);
auto release_tcp = [&]() {
int owned = g_client_tcp_sock.exchange(-1);
if (owned == tcp_fd) close(tcp_fd);
};
sockaddr_in serv{};
serv.sin_family = AF_INET;
serv.sin_port = htons(proto::TCP_PORT);
if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0 ||
connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) {
if (!g_client_state.cancelled.load())
fail_connect("Failed to connect to receiver.");
release_tcp();
return finish(nullptr);
}
uint64_t total = 0;
for (const auto& entry : fs::recursive_directory_iterator(directory))
if (fs::is_regular_file(entry.path()))
total += fs::file_size(entry.path());
g_client_state.bytes_total.store(total);
for (const auto& entry : fs::recursive_directory_iterator(directory)) {
if (g_client_state.cancelled.load()) break;
const path& p = entry.path();
if (fs::is_regular_file(p)) {
g_client_state.setStatus(p.filename().string());
if (!sendFile(tcp_fd, p)) break;
}
}
uint32_t sentinel = proto::EOF_SENTINEL;
send_all(tcp_fd, &sentinel, sizeof(sentinel));
release_tcp();
g_client_state.setStatus("");
return finish(nullptr);
}
static int find_server(char* server_ip) {
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_fd < 0) return -1;
g_client_udp_sock.store(udp_fd);
auto release_udp = [&]() {
int owned = g_client_udp_sock.exchange(-1);
if (owned == udp_fd) close(udp_fd);
};
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(proto::MULTICAST_PORT);
addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP);
const char* msg = "DISCOVER_SERVER";
if (sendto(udp_fd, msg, strlen(msg), 0, (sockaddr*)&addr, sizeof(addr)) < 0) {
release_udp();
return -1;
}
// Poll in 100ms slices so we can react to cancel within 100ms
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < deadline) {
if (g_client_state.cancelled.load()) {
release_udp();
return -1;
}
struct timeval tv{0, 100000};
fd_set fds;
FD_ZERO(&fds);
FD_SET(udp_fd, &fds);
if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) {
sockaddr_in from{};
socklen_t fromlen = sizeof(from);
char buf[256];
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n > 0) {
buf[n] = '\0';
if (strcmp(buf, "SERVER_HERE") == 0) {
inet_ntop(AF_INET, &from.sin_addr, server_ip, INET_ADDRSTRLEN);
release_udp();
return 0;
}
}
}
}
release_udp();
return -1;
}
int transfer_files(size_t index, AccountUid uid) {
g_client_state.reset();
g_client_state.setStatus("Searching for receiver...");
ThreadArgs* arg = new ThreadArgs{index, uid};
pthread_t thread;
if (pthread_create(&thread, nullptr, discovery_and_send_thread, arg) != 0) {
delete arg;
return -1;
}
pthread_detach(thread);
return 0;
}
-21
View File
@@ -1,21 +0,0 @@
#include <string>
#include <switch.h>
#include <switch/services/hid.h>
#include <vector>
#include <main_application.hpp>
namespace ui {
MainApplication *mainApp;
void MainApplication::OnLoad() {
mainApp = this;
this->usersLayout = UsersLayout::New();
this->titlesLayout = TitlesLayout::New();
this->usersLayout->SetOnInput(
std::bind(&UsersLayout::onInput, this->usersLayout, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->titlesLayout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titlesLayout,std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->LoadLayout(this->usersLayout);
}
}
-345
View File
@@ -1,345 +0,0 @@
#include <arpa/inet.h>
#include <atomic>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <netinet/in.h>
#include <pthread.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#ifdef __SWITCH__
#include <server.hpp>
#include <switch.h>
#include <main.hpp>
#endif
#include <protocol.hpp>
#include <transfer_state.hpp>
#include <net/socket.hpp>
static TransferState g_server_state;
static std::atomic<int> g_server_client_sock{-1};
static std::atomic<int> g_server_listen_sock{-1};
static std::atomic<int> g_broadcast_sock{-1};
static std::atomic<bool> g_accept_thread_active{false};
static std::atomic<bool> g_broadcast_thread_active{false};
static pthread_t g_broadcast_thread{};
bool isServerTransferDone() { return g_server_state.done.load(); }
bool isServerTransferCancelled() { return g_server_state.cancelled.load(); }
bool isServerWorkersIdle() { return !g_accept_thread_active.load() && !g_broadcast_thread_active.load(); }
double getServerProgress() { return g_server_state.progress(); }
std::string getServerStatusText() { return g_server_state.getStatus(); }
void cancelServerTransfer() {
g_server_state.cancelled.store(true);
int sock = g_server_client_sock.exchange(-1);
if (sock >= 0) {
shutdown(sock, SHUT_RDWR);
close(sock);
}
int lsock = g_server_listen_sock.exchange(-1);
if (lsock >= 0) {
shutdown(lsock, SHUT_RDWR);
close(lsock);
}
int bsock = g_broadcast_sock.exchange(-1);
if (bsock >= 0) {
shutdown(bsock, SHUT_RDWR);
close(bsock);
}
if (g_broadcast_thread_active.load()) {
pthread_cancel(g_broadcast_thread);
}
}
#ifdef __SWITCH__
static std::string replaceUsername(const std::string& path) {
std::string username = StringUtils::removeNotAscii(
StringUtils::removeAccents(Account::username(g_currentUId)));
size_t lastSlash = path.rfind('/');
if (lastSlash == std::string::npos) return path;
size_t prevSlash = path.rfind('/', lastSlash - 1);
if (prevSlash == std::string::npos)
return username + path.substr(lastSlash);
return path.substr(0, prevSlash + 1) + username + path.substr(lastSlash);
}
#endif
static bool recv_all(int sock, void* buf, size_t len) {
size_t received = 0;
while (received < len) {
ssize_t n = read(sock, static_cast<char*>(buf) + received, len - received);
if (n <= 0) return false;
received += n;
}
return true;
}
static void mkdirs(const std::string& path) {
for (size_t i = 1; i < path.size(); i++) {
if (path[i] == '/') {
std::string component = path.substr(0, i);
mkdir(component.c_str(), 0777);
}
}
mkdir(path.c_str(), 0777);
}
static void receive_file(int sock, const std::string& relative_path, uint64_t file_size) {
std::cout << "Receiving: " << relative_path << " (" << file_size << " bytes)" << std::endl;
size_t last_slash = relative_path.rfind('/');
if (last_slash != std::string::npos) {
std::string dir = relative_path.substr(0, last_slash);
if (!dir.empty()) mkdirs(dir);
}
FILE* outfile = fopen(relative_path.c_str(), "wb");
if (!outfile) {
std::cerr << "Failed to open for writing: " << relative_path
<< " errno=" << errno << std::endl;
// Drain so sender doesn't hang
std::vector<char> drain(proto::BUF_SIZE);
uint64_t remaining = file_size;
while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
ssize_t n = read(sock, drain.data(), to_read);
if (n <= 0) break;
remaining -= (uint64_t)n;
}
return;
}
g_server_state.bytes_total.store(file_size);
g_server_state.bytes_done.store(0);
std::vector<char> buffer(proto::BUF_SIZE);
uint64_t total_received = 0;
while (total_received < file_size) {
size_t to_read = (size_t)std::min(file_size - total_received, (uint64_t)proto::BUF_SIZE);
ssize_t n = read(sock, buffer.data(), to_read);
if (n <= 0) {
std::cerr << "Read error receiving: " << relative_path << std::endl;
break;
}
fwrite(buffer.data(), 1, (size_t)n, outfile);
total_received += (uint64_t)n;
g_server_state.bytes_done.store(total_received);
}
fclose(outfile);
std::cout << "Received: " << relative_path << std::endl;
}
static void* handle_client(void* socket_desc) {
int client_socket = *(int*)socket_desc;
delete static_cast<int*>(socket_desc);
while (true) {
uint32_t filename_len = 0;
if (!recv_all(client_socket, &filename_len, sizeof(filename_len)))
break;
if (filename_len == proto::EOF_SENTINEL) {
std::cout << "End of transfer." << std::endl;
break;
}
if (filename_len > proto::MAX_FILENAME) {
std::cerr << "filename_len=" << filename_len << " exceeds MAX_FILENAME, aborting." << std::endl;
break;
}
std::vector<char> filename(filename_len + 1, '\0');
if (!recv_all(client_socket, filename.data(), filename_len)) {
std::cerr << "Short read on filename, aborting." << std::endl;
break;
}
std::string filename_str(filename.data(), filename_len);
#ifdef __SWITCH__
filename_str = replaceUsername(filename_str);
#endif
{
size_t sl = filename_str.rfind('/');
g_server_state.setStatus(
sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str);
}
uint64_t file_size = 0;
if (!recv_all(client_socket, &file_size, sizeof(file_size))) {
std::cerr << "Short read on file_size, aborting." << std::endl;
break;
}
receive_file(client_socket, filename_str, file_size);
}
int owned_client = g_server_client_sock.exchange(-1);
if (owned_client == client_socket) {
close(client_socket);
}
return nullptr;
}
struct AcceptArgs { int server_fd; };
static void* accept_and_handle(void* arg) {
g_accept_thread_active.store(true);
int server_fd = static_cast<AcceptArgs*>(arg)->server_fd;
delete static_cast<AcceptArgs*>(arg);
g_server_listen_sock.store(server_fd);
sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int client_socket = accept(server_fd, (sockaddr*)&client_addr, &client_len);
int owned_listen = g_server_listen_sock.exchange(-1);
if (owned_listen == server_fd) {
close(server_fd);
}
if (client_socket >= 0) {
g_server_client_sock.store(client_socket);
int* pclient = new (std::nothrow) int(client_socket);
if (pclient) {
handle_client(pclient);
} else {
close(client_socket);
}
}
g_server_state.done.store(true);
g_accept_thread_active.store(false);
return nullptr;
}
static void* broadcast_listener(void* arg) {
g_broadcast_thread_active.store(true);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr);
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
int udp = socket(AF_INET, SOCK_DGRAM, 0);
if (udp < 0) {
perror("broadcast_listener: socket");
g_broadcast_thread_active.store(false);
return nullptr;
}
g_broadcast_sock.store(udp);
struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit
setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(proto::MULTICAST_PORT);
if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) {
perror("broadcast_listener: bind");
int owned = g_broadcast_sock.exchange(-1);
if (owned == udp) close(udp);
g_broadcast_thread_active.store(false);
return nullptr;
}
ip_mreq group{};
group.imr_multiaddr.s_addr = inet_addr(proto::MULTICAST_GROUP);
group.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) {
perror("broadcast_listener: setsockopt");
int owned = g_broadcast_sock.exchange(-1);
if (owned == udp) close(udp);
g_broadcast_thread_active.store(false);
return nullptr;
}
std::cout << "Broadcast listener started" << std::endl;
char buf[256];
sockaddr_in from{};
socklen_t fromlen = sizeof(from);
while (true) {
ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n < 0) {
if (g_server_state.cancelled.load()) break;
continue;
}
buf[n] = '\0';
if (strcmp(buf, "DISCOVER_SERVER") == 0) {
const char* reply = "SERVER_HERE";
sendto(udp, reply, strlen(reply), 0, (sockaddr*)&from, fromlen);
std::cout << "Discovery replied." << std::endl;
break;
}
}
int owned = g_broadcast_sock.exchange(-1);
if (owned == udp) close(udp);
g_broadcast_thread_active.store(false);
return nullptr;
}
int startSendingThread() {
g_server_state.reset();
g_server_state.setStatus("Waiting for connection...");
pthread_t broadcast_thread;
if (pthread_create(&broadcast_thread, nullptr, broadcast_listener, nullptr) != 0) {
perror("startSendingThread: broadcast thread");
return 1;
}
g_broadcast_thread = broadcast_thread;
pthread_detach(broadcast_thread);
Socket server(socket(AF_INET, SOCK_STREAM, 0));
if (!server.valid()) {
perror("startSendingThread: socket");
cancelServerTransfer();
return 1;
}
int yes = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(proto::TCP_PORT);
if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0) {
perror("startSendingThread: bind");
cancelServerTransfer();
return 1;
}
if (listen(server, 3) < 0) {
perror("startSendingThread: listen");
cancelServerTransfer();
return 1;
}
AcceptArgs* acc_args = new AcceptArgs{server.fd};
pthread_t accept_thread;
if (pthread_create(&accept_thread, nullptr, accept_and_handle, acc_args) != 0) {
delete acc_args;
cancelServerTransfer();
return 1;
}
pthread_detach(accept_thread);
server.release(); // accepted by accept_and_handle
return 0;
}
#ifndef __SWITCH__
int main() {
if (startSendingThread() != 0) return 1;
while (!isServerTransferDone()) usleep(16000);
return 0;
}
#endif
+82 -78
View File
@@ -1,78 +1,82 @@
#include <main_application.hpp> #include <nxst/app/main_application.hpp>
#include "util.hpp" #include <nxst/domain/util.hpp>
#include "main.hpp" #include <nxst/app/main.hpp>
#include <server.hpp> #include <unistd.h>
#include <client.hpp>
#include <unistd.h> namespace ui { extern MainApplication* mainApp; }
static int nxlink_sock = -1; static int nxlink_sock = -1;
extern "C" void userAppInit() { extern "C" void userAppInit() {
appletInitialize(); appletInitialize();
hidInitialize(); hidInitialize();
nsInitialize(); nsInitialize();
setsysInitialize(); setsysInitialize();
setInitialize(); setInitialize();
accountInitialize(AccountServiceType_Administrator); accountInitialize(AccountServiceType_Administrator);
pmshellInitialize(); pmshellInitialize();
socketInitializeDefault(); socketInitializeDefault();
pdmqryInitialize(); pdmqryInitialize();
nxlink_sock = nxlinkStdio(); nxlink_sock = nxlinkStdio();
printf("userAppInit\n"); printf("userAppInit\n");
} }
extern "C" void userAppExit() { extern "C" void userAppExit() {
cancelServerTransfer(); if (ui::mainApp) {
cancelClientTransfer(); ui::mainApp->transfer.cancelReceive();
for (int i = 0; i < 150 && (!isServerWorkersIdle() || !isClientWorkersIdle()); i++) { ui::mainApp->transfer.cancelSend();
usleep(10000); for (int i = 0; i < 150 &&
} (!ui::mainApp->transfer.isReceiveWorkersIdle() ||
if (nxlink_sock != -1) { !ui::mainApp->transfer.isSendWorkersIdle()); i++) {
close(nxlink_sock); usleep(10000);
} }
appletExit(); }
hidExit(); if (nxlink_sock != -1) {
nsExit(); close(nxlink_sock);
setsysExit(); }
setExit(); appletExit();
accountExit(); hidExit();
pmshellExit(); nsExit();
socketExit(); setsysExit();
pdmqryExit(); setExit();
} accountExit();
pmshellExit();
// Main entrypoint socketExit();
int main() { pdmqryExit();
Result res = servicesInit(); }
if (R_FAILED(res)) {
servicesExit(); // Main entrypoint
exit(res); int main() {
} Result res = servicesInit();
if (R_FAILED(res)) {
printf("main"); servicesExit();
exit(res);
// First create our renderer, where one can customize SDL or other stuff's }
// initialization.
auto renderer_opts = pu::ui::render::RendererInitOptions( printf("main");
SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags);
renderer_opts.UseImage(pu::ui::render::IMGAllFlags); // First create our renderer, where one can customize SDL or other stuff's
renderer_opts.UseAudio(pu::ui::render::MixerAllFlags); // initialization.
renderer_opts.UseTTF(); auto renderer_opts = pu::ui::render::RendererInitOptions(
renderer_opts.SetExtraDefaultFontSize(theme::type::Caption); SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags);
renderer_opts.SetExtraDefaultFontSize(theme::type::Label); renderer_opts.UseImage(pu::ui::render::IMGAllFlags);
renderer_opts.SetExtraDefaultFontSize(theme::type::Body); renderer_opts.UseAudio(pu::ui::render::MixerAllFlags);
renderer_opts.SetExtraDefaultFontSize(theme::type::Title); renderer_opts.UseTTF();
renderer_opts.SetExtraDefaultFontSize(theme::type::Display); renderer_opts.SetExtraDefaultFontSize(theme::type::Caption);
renderer_opts.SetExtraDefaultFontSize(theme::type::Label);
auto renderer = pu::ui::render::Renderer::New(renderer_opts); renderer_opts.SetExtraDefaultFontSize(theme::type::Body);
renderer_opts.SetExtraDefaultFontSize(theme::type::Title);
// Create our main application from the renderer renderer_opts.SetExtraDefaultFontSize(theme::type::Display);
auto main = ui::MainApplication::New(renderer);
auto renderer = pu::ui::render::Renderer::New(renderer_opts);
main->Prepare();
// Create our main application from the renderer
main->Show(); auto main = ui::MainApplication::New(renderer);
servicesExit(); main->Prepare();
return 0;
} main->Show();
servicesExit();
return 0;
}
+21
View File
@@ -0,0 +1,21 @@
#include <string>
#include <switch.h>
#include <switch/services/hid.h>
#include <vector>
#include <nxst/app/main_application.hpp>
namespace ui {
MainApplication *mainApp;
void MainApplication::OnLoad() {
mainApp = this;
this->users_layout = UsersLayout::New();
this->titles_layout = TitlesLayout::New();
this->users_layout->SetOnInput(
std::bind(&UsersLayout::onInput, this->users_layout, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->titles_layout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titles_layout, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->LoadLayout(this->users_layout);
}
}
@@ -24,8 +24,7 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include "account.hpp" #include <nxst/domain/account.hpp>
#include <main.hpp>
#include <sys/stat.h> #include <sys/stat.h>
#include <cstdio> #include <cstdio>
@@ -138,5 +137,5 @@ AccountUid Account::selectAccount(void)
return uid; return uid;
} }
return g_currentUId; return AccountUid{};
} }
+1 -1
View File
@@ -24,7 +24,7 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include "common.hpp" #include <nxst/domain/common.hpp>
std::string DateTime::timeStr(void) std::string DateTime::timeStr(void)
{ {
+2 -2
View File
@@ -24,8 +24,8 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include "title.hpp" #include <nxst/domain/title.hpp>
#include "main.hpp" #include <nxst/app/main.hpp>
static std::unordered_map<AccountUid, std::vector<Title>> titles; static std::unordered_map<AccountUid, std::vector<Title>> titles;
static bool s_titlesLoaded = false; static bool s_titlesLoaded = false;
+4 -4
View File
@@ -24,10 +24,10 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include "util.hpp" #include <nxst/domain/util.hpp>
#include <logger.hpp> #include <nxst/infra/sys/logger.hpp>
#include <main_application.hpp> #include <nxst/app/main_application.hpp>
#include "main.hpp" #include <nxst/app/main.hpp>
void servicesExit(void) void servicesExit(void)
{ {
@@ -24,7 +24,7 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include "directory.hpp" #include <nxst/infra/fs/directory.hpp>
Directory::Directory(const std::string& root) Directory::Directory(const std::string& root)
{ {
@@ -24,7 +24,7 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include "filesystem.hpp" #include <nxst/infra/fs/filesystem.hpp>
Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID)
{ {
+3 -3
View File
@@ -24,9 +24,9 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include "io.hpp" #include <nxst/infra/fs/io.hpp>
#include "main.hpp" #include <nxst/app/main.hpp>
#include <logger.hpp> #include <nxst/infra/sys/logger.hpp>
bool io::fileExists(const std::string& path) bool io::fileExists(const std::string& path)
{ {
+1
View File
@@ -0,0 +1 @@
// Logic moved to src/service/transfer_service.cpp
+1
View File
@@ -0,0 +1 @@
// Logic moved to src/service/transfer_service.cpp
@@ -1,4 +1,4 @@
#include <logger.hpp> #include <nxst/infra/sys/logger.hpp>
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
+483
View File
@@ -0,0 +1,483 @@
#include <nxst/service/transfer_service.hpp>
#include <arpa/inet.h>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
#ifdef __SWITCH__
#include <switch.h>
#include <nxst/infra/fs/io.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/domain/account.hpp>
#endif
#include <nxst/domain/protocol.hpp>
#include <nxst/infra/net/socket.hpp>
namespace fs = std::filesystem;
namespace nxst {
// ─── File-transfer helpers ────────────────────────────────────────────────────
static bool sendAll(int sock, const void* buf, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0);
if (n <= 0) return false;
sent += n;
}
return true;
}
static bool recvAll(int sock, void* buf, size_t len) {
size_t got = 0;
while (got < len) {
ssize_t n = read(sock, static_cast<char*>(buf) + got, len - got);
if (n <= 0) return false;
got += n;
}
return true;
}
static bool sendFile(int sock, const fs::path& filepath, TransferState& state) {
std::ifstream infile(filepath, std::ios::binary | std::ios::ate);
if (!infile.is_open()) return false;
uint32_t filename_len = (uint32_t)filepath.string().size();
uint64_t file_size = (uint64_t)infile.tellg();
infile.seekg(0, std::ios::beg);
if (!sendAll(sock, &filename_len, sizeof(filename_len))) return false;
if (!sendAll(sock, filepath.c_str(), filename_len)) return false;
if (!sendAll(sock, &file_size, sizeof(file_size))) return false;
std::vector<char> buffer(proto::BUF_SIZE);
uint64_t remaining = file_size;
while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
infile.read(buffer.data(), (std::streamsize)to_read);
std::streamsize count = infile.gcount();
if (count <= 0) break;
if (!sendAll(sock, buffer.data(), (size_t)count)) return false;
state.bytes_done.fetch_add((uint64_t)count);
remaining -= (uint64_t)count;
}
return true;
}
static void mkdirs(const std::string& path) {
for (size_t i = 1; i < path.size(); i++) {
if (path[i] == '/') {
std::string component = path.substr(0, i);
mkdir(component.c_str(), 0777);
}
}
mkdir(path.c_str(), 0777);
}
static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size,
TransferState& state) {
size_t last_slash = rel_path.rfind('/');
if (last_slash != std::string::npos) {
std::string dir = rel_path.substr(0, last_slash);
if (!dir.empty()) mkdirs(dir);
}
FILE* outfile = fopen(rel_path.c_str(), "wb");
if (!outfile) {
std::vector<char> drain(proto::BUF_SIZE);
uint64_t remaining = file_size;
while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
ssize_t n = read(sock, drain.data(), to_read);
if (n <= 0) break;
remaining -= (uint64_t)n;
}
return;
}
state.bytes_total.store(file_size);
state.bytes_done.store(0);
std::vector<char> buffer(proto::BUF_SIZE);
uint64_t total = 0;
while (total < file_size) {
size_t to_read = (size_t)std::min(file_size - total, (uint64_t)proto::BUF_SIZE);
ssize_t n = read(sock, buffer.data(), to_read);
if (n <= 0) break;
fwrite(buffer.data(), 1, (size_t)n, outfile);
total += (uint64_t)n;
state.bytes_done.store(total);
}
fclose(outfile);
}
// ─── Sender ──────────────────────────────────────────────────────────────────
void TransferService::failSend(const std::string& reason) {
sender_state.fail_reason = reason;
sender_state.connection_failed.store(true);
sender_state.done.store(true);
}
int TransferService::findServer(char* out_ip) {
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_fd < 0) return -1;
sender_udp_sock.store(udp_fd);
auto releaseUdp = [&]() {
int owned = sender_udp_sock.exchange(-1);
if (owned == udp_fd) close(udp_fd);
};
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(proto::MULTICAST_PORT);
addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP);
if (sendto(udp_fd, "DISCOVER_SERVER", 15, 0, (sockaddr*)&addr, sizeof(addr)) < 0) {
releaseUdp();
return -1;
}
// Poll in 100ms slices so cancel races within 100ms
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < deadline) {
if (sender_state.cancelled.load()) { releaseUdp(); return -1; }
struct timeval tv{0, 100000};
fd_set fds;
FD_ZERO(&fds);
FD_SET(udp_fd, &fds);
if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) {
sockaddr_in from{};
socklen_t fromlen = sizeof(from);
char buf[256];
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n > 0) {
buf[n] = '\0';
if (strcmp(buf, "SERVER_HERE") == 0) {
inet_ntop(AF_INET, &from.sin_addr, out_ip, INET_ADDRSTRLEN);
releaseUdp();
return 0;
}
}
}
}
releaseUdp();
return -1;
}
void* TransferService::senderEntry(void* arg) {
auto* a = static_cast<SenderArgs*>(arg);
TransferService* svc = a->svc;
size_t idx = a->title_index;
AccountUid uid = a->uid;
delete a;
svc->runSender(idx, uid);
return nullptr;
}
void TransferService::runSender(size_t title_index, AccountUid uid) {
sender_active.store(true);
auto finish = [this]() {
sender_state.done.store(true);
sender_active.store(false);
};
char server_ip[INET_ADDRSTRLEN];
if (findServer(server_ip) != 0) {
if (!sender_state.cancelled.load())
failSend("No receiver found.\nMake sure the other Switch is in Receive mode.");
return finish();
}
if (sender_state.cancelled.load()) return finish();
sender_state.setStatus("Creating backup...");
#ifdef __SWITCH__
auto backup_result = io::backup(title_index, uid);
if (!std::get<0>(backup_result)) {
failSend("Failed to create backup:\n" + std::get<2>(backup_result));
return finish();
}
fs::path directory = std::get<2>(backup_result);
#else
fs::path directory = ".";
(void)title_index; (void)uid;
#endif
if (sender_state.cancelled.load()) return finish();
sender_state.setStatus("Connecting...");
int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_fd < 0) { failSend("Failed to open socket."); return finish(); }
sender_tcp_sock.store(tcp_fd);
auto releaseTcp = [&]() {
int owned = sender_tcp_sock.exchange(-1);
if (owned == tcp_fd) close(tcp_fd);
};
sockaddr_in serv{};
serv.sin_family = AF_INET;
serv.sin_port = htons(proto::TCP_PORT);
if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0 ||
connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) {
if (!sender_state.cancelled.load())
failSend("Failed to connect to receiver.");
releaseTcp();
return finish();
}
uint64_t total = 0;
for (const auto& entry : fs::recursive_directory_iterator(directory))
if (fs::is_regular_file(entry.path()))
total += fs::file_size(entry.path());
sender_state.bytes_total.store(total);
for (const auto& entry : fs::recursive_directory_iterator(directory)) {
if (sender_state.cancelled.load()) break;
const fs::path& p = entry.path();
if (fs::is_regular_file(p)) {
sender_state.setStatus(p.filename().string());
if (!sendFile(tcp_fd, p, sender_state)) break;
}
}
uint32_t sentinel = proto::EOF_SENTINEL;
sendAll(tcp_fd, &sentinel, sizeof(sentinel));
releaseTcp();
sender_state.setStatus("");
return finish();
}
int TransferService::startSend(size_t title_index, AccountUid uid) {
sender_state.reset();
sender_state.setStatus("Searching for receiver...");
auto* arg = new SenderArgs{this, title_index, uid};
pthread_t thread;
if (pthread_create(&thread, nullptr, senderEntry, arg) != 0) {
delete arg;
return -1;
}
pthread_detach(thread);
return 0;
}
void TransferService::cancelSend() {
sender_state.cancelled.store(true);
int udp = sender_udp_sock.exchange(-1);
if (udp >= 0) { shutdown(udp, SHUT_RDWR); close(udp); }
int tcp = sender_tcp_sock.exchange(-1);
if (tcp >= 0) { shutdown(tcp, SHUT_RDWR); close(tcp); }
}
// ─── Receiver ────────────────────────────────────────────────────────────────
std::string TransferService::replaceUsername(const std::string& file_path) const {
#ifdef __SWITCH__
std::string username = StringUtils::removeNotAscii(
StringUtils::removeAccents(Account::username(restore_uid)));
size_t last_slash = file_path.rfind('/');
if (last_slash == std::string::npos) return file_path;
size_t prev_slash = file_path.rfind('/', last_slash - 1);
if (prev_slash == std::string::npos)
return username + file_path.substr(last_slash);
return file_path.substr(0, prev_slash + 1) + username + file_path.substr(last_slash);
#else
return file_path;
#endif
}
void* TransferService::broadcastEntry(void* arg) {
static_cast<TransferService*>(arg)->runBroadcast();
return nullptr;
}
void TransferService::runBroadcast() {
receiver_broadcast_active.store(true);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr);
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
int udp = socket(AF_INET, SOCK_DGRAM, 0);
if (udp < 0) { receiver_broadcast_active.store(false); return; }
receiver_bcast_sock.store(udp);
auto releaseUdp = [&]() {
int owned = receiver_bcast_sock.exchange(-1);
if (owned == udp) close(udp);
};
struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit
setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(proto::MULTICAST_PORT);
if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) {
releaseUdp();
receiver_broadcast_active.store(false);
return;
}
ip_mreq group{};
group.imr_multiaddr.s_addr = inet_addr(proto::MULTICAST_GROUP);
group.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) {
releaseUdp();
receiver_broadcast_active.store(false);
return;
}
char buf[256];
sockaddr_in from{};
socklen_t fromlen = sizeof(from);
while (true) {
ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n < 0) {
if (receiver_state.cancelled.load()) break;
continue;
}
buf[n] = '\0';
if (strcmp(buf, "DISCOVER_SERVER") == 0) {
sendto(udp, "SERVER_HERE", 11, 0, (sockaddr*)&from, fromlen);
break;
}
}
releaseUdp();
receiver_broadcast_active.store(false);
}
void* TransferService::acceptEntry(void* arg) {
auto* a = static_cast<AcceptArgs*>(arg);
TransferService* svc = a->svc;
int server_fd = a->server_fd;
delete a;
svc->runAccept(server_fd);
return nullptr;
}
void TransferService::runAccept(int server_fd) {
receiver_accept_active.store(true);
receiver_listen_sock.store(server_fd);
sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len);
int owned_listen = receiver_listen_sock.exchange(-1);
if (owned_listen == server_fd) close(server_fd);
if (client_sock >= 0) {
receiver_client_sock.store(client_sock);
while (true) {
uint32_t filename_len = 0;
if (!recvAll(client_sock, &filename_len, sizeof(filename_len))) break;
if (filename_len == proto::EOF_SENTINEL) break;
if (filename_len > proto::MAX_FILENAME) break;
std::vector<char> filename_buf(filename_len + 1, '\0');
if (!recvAll(client_sock, filename_buf.data(), filename_len)) break;
std::string filename_str(filename_buf.data(), filename_len);
filename_str = replaceUsername(filename_str);
{
size_t sl = filename_str.rfind('/');
receiver_state.setStatus(
sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str);
}
uint64_t file_size = 0;
if (!recvAll(client_sock, &file_size, sizeof(file_size))) break;
receiveFile(client_sock, filename_str, file_size, receiver_state);
}
int owned = receiver_client_sock.exchange(-1);
if (owned == client_sock) close(client_sock);
if (!receiver_state.cancelled.load()) {
#ifdef __SWITCH__
receiver_state.setStatus("Restoring...");
auto result = io::restore(restore_title_index, restore_uid, 0, restore_title_name);
restore_ok = std::get<0>(result);
restore_error = restore_ok ? "" : std::get<2>(result);
#else
restore_ok = true;
#endif
}
}
receiver_state.done.store(true);
receiver_accept_active.store(false);
}
int TransferService::startReceive(size_t title_index, AccountUid uid, std::string title_name) {
receiver_state.reset();
receiver_state.setStatus("Waiting for connection...");
restore_title_index = title_index;
restore_uid = uid;
restore_title_name = std::move(title_name);
restore_ok = false;
restore_error.clear();
pthread_t bcast_thread;
if (pthread_create(&bcast_thread, nullptr, broadcastEntry, this) != 0) return 1;
receiver_bcast_thread = bcast_thread;
pthread_detach(bcast_thread);
Socket server(socket(AF_INET, SOCK_STREAM, 0));
if (!server.valid()) { cancelReceive(); return 1; }
int yes = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(proto::TCP_PORT);
if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 ||
listen(server, 3) < 0) {
cancelReceive();
return 1;
}
auto* acc_args = new AcceptArgs{this, server.fd};
pthread_t accept_thread;
if (pthread_create(&accept_thread, nullptr, acceptEntry, acc_args) != 0) {
delete acc_args;
cancelReceive();
return 1;
}
pthread_detach(accept_thread);
server.release();
return 0;
}
void TransferService::cancelReceive() {
receiver_state.cancelled.store(true);
int sock = receiver_client_sock.exchange(-1);
if (sock >= 0) { shutdown(sock, SHUT_RDWR); close(sock); }
int lsock = receiver_listen_sock.exchange(-1);
if (lsock >= 0) { shutdown(lsock, SHUT_RDWR); close(lsock); }
int bsock = receiver_bcast_sock.exchange(-1);
if (bsock >= 0) { shutdown(bsock, SHUT_RDWR); close(bsock); }
if (receiver_broadcast_active.load()) pthread_cancel(receiver_bcast_thread);
}
} // namespace nxst
@@ -1,10 +1,7 @@
#include <main_application.hpp> #include <nxst/app/main_application.hpp>
#include <stdio.h> #include <nxst/domain/util.hpp>
#include <main.hpp> #include <nxst/ui/transfer_overlay.hpp>
#include <const.h> #include <nxst/ui/const.h>
#include <client.hpp>
#include <server.hpp>
#include <transfer_overlay.hpp>
namespace ui { namespace ui {
extern MainApplication *mainApp; extern MainApplication *mainApp;
@@ -98,24 +95,24 @@ namespace ui {
this->updateHints(); this->updateHints();
} }
void TitlesLayout::InitTitles() { void TitlesLayout::InitTitles(AccountUid uid) {
using namespace theme; using namespace theme;
Logger::getInstance().log(Logger::INFO, "InitTitles"); this->current_uid = uid;
auto it = this->menuCache.find(g_currentUId); auto it = this->menuCache.find(uid);
std::vector<pu::ui::elm::MenuItem::Ref>* items; std::vector<pu::ui::elm::MenuItem::Ref>* items;
if (it != this->menuCache.end()) { if (it != this->menuCache.end()) {
items = &it->second; items = &it->second;
} else { } else {
std::vector<pu::ui::elm::MenuItem::Ref> built; std::vector<pu::ui::elm::MenuItem::Ref> built;
for (size_t i = 0; i < getTitleCount(g_currentUId); i++) { for (size_t i = 0; i < getTitleCount(uid); i++) {
Title title; Title title;
getTitle(title, g_currentUId, i); getTitle(title, uid, i);
auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str());
titleItem->SetColor(color::TextPrimary); titleItem->SetColor(color::TextPrimary);
built.push_back(titleItem); built.push_back(titleItem);
} }
auto inserted = this->menuCache.emplace(g_currentUId, std::move(built)); auto inserted = this->menuCache.emplace(uid, std::move(built));
items = &inserted.first->second; items = &inserted.first->second;
} }
@@ -144,14 +141,14 @@ namespace ui {
this->refreshButtons(); this->refreshButtons();
this->updateHints(); this->updateHints();
this->header->SetUser(g_currentUId, Account::username(g_currentUId)); this->header->SetUser(uid, Account::username(uid));
} }
void TitlesLayout::refreshPanel() { void TitlesLayout::refreshPanel() {
if (this->titlesMenu->GetItems().empty()) return; if (this->titlesMenu->GetItems().empty()) return;
int idx = this->titlesMenu->GetSelectedIndex(); int idx = this->titlesMenu->GetSelectedIndex();
Title title; Title title;
getTitle(title, g_currentUId, idx); getTitle(title, this->current_uid, idx);
this->panelTitle->SetText(StringUtils::elide(title.name(), 24)); this->panelTitle->SetText(StringUtils::elide(title.name(), 24));
} }
@@ -185,24 +182,25 @@ namespace ui {
} }
void TitlesLayout::runTransfer(int index, Title& title) { void TitlesLayout::runTransfer(int index, Title& title) {
(void)title;
auto ovl = TransferOverlay::New("Transferring save data..."); auto ovl = TransferOverlay::New("Transferring save data...");
this->titlesMenu->SetVisible(false); this->titlesMenu->SetVisible(false);
mainApp->StartOverlay(ovl); mainApp->StartOverlay(ovl);
this->LockInput(); this->LockInput();
if (transfer_files(index, g_currentUId) != 0) { if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) {
mainApp->EndOverlay(); mainApp->EndOverlay();
this->titlesMenu->SetVisible(true); this->titlesMenu->SetVisible(true);
this->UnlockInput(); this->UnlockInput();
mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true);
return; return;
} }
while (!isClientTransferDone()) { while (!mainApp->transfer.isSendDone()) {
ovl->SetStatus(getClientStatusText()); ovl->SetStatus(mainApp->transfer.sendStatusText());
ovl->SetProgressVisible(isClientProgressKnown()); ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown());
ovl->SetProgress(getClientProgress()); ovl->SetProgress(mainApp->transfer.sendProgress());
mainApp->CallForRender(); mainApp->CallForRender();
if (mainApp->GetButtonsDown() & HidNpadButton_B) { if (mainApp->GetButtonsDown() & HidNpadButton_B) {
cancelClientTransfer(); mainApp->transfer.cancelSend();
} }
svcSleepThread(16666666LL); svcSleepThread(16666666LL);
} }
@@ -210,9 +208,9 @@ namespace ui {
this->titlesMenu->SetVisible(true); this->titlesMenu->SetVisible(true);
this->UnlockInput(); this->UnlockInput();
if (isClientConnectionFailed()) { if (mainApp->transfer.isSendConnectionFailed()) {
mainApp->CreateShowDialog("Transfer", getClientFailReason(), {"OK"}, true); mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true);
} else if (isClientTransferCancelled()) { } else if (mainApp->transfer.isSendCancelled()) {
mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true);
} else { } else {
mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true);
@@ -220,7 +218,7 @@ namespace ui {
} }
void TitlesLayout::runReceive(int index, Title& title) { void TitlesLayout::runReceive(int index, Title& title) {
if (startSendingThread() != 0) { if (mainApp->transfer.startReceive((size_t)index, this->current_uid, title.name()) != 0) {
mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true); mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true);
return; return;
} }
@@ -228,12 +226,12 @@ namespace ui {
this->titlesMenu->SetVisible(false); this->titlesMenu->SetVisible(false);
mainApp->StartOverlay(ovl); mainApp->StartOverlay(ovl);
this->LockInput(); this->LockInput();
while (!isServerTransferDone()) { while (!mainApp->transfer.isReceiveDone()) {
ovl->SetStatus(getServerStatusText()); ovl->SetStatus(mainApp->transfer.receiveStatusText());
ovl->SetProgress(getServerProgress()); ovl->SetProgress(mainApp->transfer.receiveProgress());
mainApp->CallForRender(); mainApp->CallForRender();
if (mainApp->GetButtonsDown() & HidNpadButton_B) { if (mainApp->GetButtonsDown() & HidNpadButton_B) {
cancelServerTransfer(); mainApp->transfer.cancelReceive();
} }
svcSleepThread(16666666LL); svcSleepThread(16666666LL);
} }
@@ -241,24 +239,22 @@ namespace ui {
this->titlesMenu->SetVisible(true); this->titlesMenu->SetVisible(true);
this->UnlockInput(); this->UnlockInput();
if (isServerTransferCancelled()) { if (mainApp->transfer.isReceiveCancelled()) {
mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true);
return; } else if (mainApp->transfer.restoreSucceeded()) {
}
auto restoreResult = io::restore(index, g_currentUId, 0, title.name());
if (std::get<0>(restoreResult)) {
mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true);
} else { } else {
mainApp->CreateShowDialog("Receive", "Restore failed:\n" + std::get<2>(restoreResult), {"OK"}, true); mainApp->CreateShowDialog("Receive", "Restore failed:\n" + mainApp->transfer.restoreError(), {"OK"}, true);
} }
} }
void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
(void)Up; (void)Held; (void)Pos;
if (m_inputLocked) return; if (m_inputLocked) return;
if (Down & HidNpadButton_Plus) { if (Down & HidNpadButton_Plus) {
cancelClientTransfer(); mainApp->transfer.cancelSend();
cancelServerTransfer(); mainApp->transfer.cancelReceive();
mainApp->Close(); mainApp->Close();
return; return;
} }
@@ -266,7 +262,7 @@ namespace ui {
if (focus == TitlesFocus::List) { if (focus == TitlesFocus::List) {
if (Down & HidNpadButton_B) { if (Down & HidNpadButton_B) {
this->header->SetUser(std::nullopt, ""); this->header->SetUser(std::nullopt, "");
mainApp->LoadLayout(mainApp->usersLayout); mainApp->LoadLayout(mainApp->users_layout);
return; return;
} }
if (Down & HidNpadButton_A) { if (Down & HidNpadButton_A) {
@@ -298,7 +294,7 @@ namespace ui {
if (Down & HidNpadButton_A) { if (Down & HidNpadButton_A) {
int idx = this->titlesMenu->GetSelectedIndex(); int idx = this->titlesMenu->GetSelectedIndex();
Title title; Title title;
getTitle(title, g_currentUId, idx); getTitle(title, this->current_uid, idx);
TitlesAction chosen = action; TitlesAction chosen = action;
this->focus = TitlesFocus::List; this->focus = TitlesFocus::List;
this->refreshButtons(); this->refreshButtons();
@@ -1,6 +1,4 @@
#include <cstdio> #include <nxst/app/main_application.hpp>
#include <main_application.hpp>
#include "main.hpp"
namespace ui { namespace ui {
extern MainApplication *mainApp; extern MainApplication *mainApp;
@@ -50,11 +48,12 @@ namespace ui {
void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
if (Down & HidNpadButton_Plus) { if (Down & HidNpadButton_Plus) {
svcExitProcess(); mainApp->Close();
return;
} }
if (Down & HidNpadButton_A) { if (Down & HidNpadButton_A) {
g_currentUId = Account::ids().at(this->usersMenu->GetSelectedIndex()); AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex());
if (!areTitlesLoaded()) { if (!areTitlesLoaded()) {
this->usersMenu->SetVisible(false); this->usersMenu->SetVisible(false);
@@ -69,8 +68,8 @@ namespace ui {
this->usersMenu->SetVisible(true); this->usersMenu->SetVisible(true);
} }
mainApp->titlesLayout->InitTitles(); mainApp->titles_layout->InitTitles(uid);
mainApp->LoadLayout(mainApp->titlesLayout); mainApp->LoadLayout(mainApp->titles_layout);
} }
} }
} }