From b5c506cf035b5f19f6e6b71dfbf9c73931691736 Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov Date: Sun, 26 Apr 2026 20:02:15 +0300 Subject: [PATCH 01/10] refactor: phase 0 & 1 --- .clang-format | 28 ++ .clang-tidy | 47 ++ .editorconfig | 22 + .gitattributes | 14 + PLAN.md | 434 ++++++++++++++++++ include/account.hpp | 5 +- include/common.hpp | 5 +- include/const.h | 2 +- include/directory.hpp | 5 +- include/filesystem.hpp | 5 +- include/io.hpp | 5 +- include/logger.hpp | 114 ++--- include/main.hpp | 4 +- ...inApplication.hpp => main_application.hpp} | 4 +- include/net/{Socket.hpp => socket.hpp} | 0 include/{Theme.hpp => theme.hpp} | 0 include/title.hpp | 5 +- .../{TitlesLayout.hpp => titles_layout.hpp} | 4 +- ...ansferOverlay.hpp => transfer_overlay.hpp} | 2 +- .../{TransferState.hpp => transfer_state.hpp} | 0 include/ui/{Card.hpp => card.hpp} | 2 +- include/ui/{HeaderBar.hpp => header_bar.hpp} | 4 +- include/ui/{HintBar.hpp => hint_bar.hpp} | 2 +- include/ui/{UiContext.hpp => ui_context.hpp} | 0 include/{UsersLayout.hpp => users_layout.hpp} | 4 +- include/util.hpp | 6 +- source/client.cpp | 2 +- source/io.cpp | 6 +- source/logger.cpp | 64 +++ source/{Main.cpp => main.cpp} | 2 +- ...inApplication.cpp => main_application.cpp} | 2 +- source/server.cpp | 4 +- .../{TitlesLayout.cpp => titles_layout.cpp} | 4 +- source/{UsersLayout.cpp => users_layout.cpp} | 2 +- source/util.cpp | 12 +- tools/format.sh | 6 + 36 files changed, 690 insertions(+), 137 deletions(-) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 PLAN.md rename include/{MainApplication.hpp => main_application.hpp} (81%) rename include/net/{Socket.hpp => socket.hpp} (100%) rename include/{Theme.hpp => theme.hpp} (100%) rename include/{TitlesLayout.hpp => titles_layout.hpp} (96%) rename include/{TransferOverlay.hpp => transfer_overlay.hpp} (99%) rename include/{TransferState.hpp => transfer_state.hpp} (100%) rename include/ui/{Card.hpp => card.hpp} (95%) rename include/ui/{HeaderBar.hpp => header_bar.hpp} (98%) rename include/ui/{HintBar.hpp => hint_bar.hpp} (98%) rename include/ui/{UiContext.hpp => ui_context.hpp} (100%) rename include/{UsersLayout.hpp => users_layout.hpp} (90%) create mode 100644 source/logger.cpp rename source/{Main.cpp => main.cpp} (94%) rename source/{MainApplication.cpp => main_application.cpp} (93%) rename source/{TitlesLayout.cpp => titles_layout.cpp} (99%) rename source/{UsersLayout.cpp => users_layout.cpp} (98%) create mode 100755 tools/format.sh diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..c3485b3 --- /dev/null +++ b/.clang-format @@ -0,0 +1,28 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 4 +ColumnLimit: 110 +PointerAlignment: Left +AlignAfterOpenBracket: Align +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +BreakBeforeBraces: Attach +SortIncludes: true +IncludeBlocks: Regroup +IncludeCategories: + # Project headers: nxst/ + - Regex: '^(<|")(nxst/)' + Priority: 3 + SortPriority: 3 + # Third-party: Plutonium, libnx, SDL, switch.h + - Regex: '^(<|")(pu/|switch\.h|libnx|SDL|freetype|harfbuzz|zlib)' + Priority: 2 + SortPriority: 2 + # System / C++ standard library + - Regex: '^<' + Priority: 1 + SortPriority: 1 +SpacesBeforeTrailingComments: 2 +Cpp11BracedListStyle: true +Standard: c++17 diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..d397413 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,47 @@ +--- +Checks: > + bugprone-*, + readability-*, + modernize-*, + cppcoreguidelines-pro-type-cstyle-cast, + -fuchsia-*, + -google-*, + -llvm-*, + -readability-magic-numbers, + -readability-named-parameter, + -modernize-use-trailing-return-type, + -modernize-use-nodiscard, + -modernize-avoid-c-arrays, + -bugprone-easily-swappable-parameters + +WarningsAsErrors: '' + +HeaderFilterRegex: 'include/nxst/.*' + +CheckOptions: + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: camelBack + - key: readability-identifier-naming.MethodCase + value: camelBack + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ConstexprVariablePrefix + value: 'k' + - key: readability-identifier-naming.ConstexprVariableCase + value: CamelCase + - key: readability-identifier-naming.EnumCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-braces-around-statements.ShortStatementLines + value: '2' diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c1bae59 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{cpp,hpp,h,c}] +indent_style = space +indent_size = 4 + +[Makefile] +indent_style = tab + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.{json,md}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c006aab --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Normalize line endings +* text=auto eol=lf + +# Vendor: do not count toward language stats +lib/Plutonium/** linguist-vendored=true +deps/** linguist-vendored=true + +# Binary assets +*.png binary +*.jpg binary +*.nro binary +*.nso binary +*.pfs0 binary +*.elf binary diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..2c61461 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,434 @@ +# NXST Architectural Refactor Plan + +## Current Phase Tracker + +| Phase | Title | Status | Effort | Notes | +|-------|-------|--------|--------|-------| +| 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 | +| 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 | +| 4 | Make → CMake migration | ☐ Not started | M (~1d) | devkitpro `Switch.cmake` toolchain | +| 5 | TransferService extraction | ☐ Not started | L (~2d) | kill globals, sever UI ↔ net coupling | +| 6 | `Result` + 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 | +| 8 | CI | ☐ Not started | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check | + +**Active phase:** Phase 3 — Directory restructure. +**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. + +--- + +## Context + +NXST is a Nintendo Switch homebrew save-transfer app (~3.2K LOC across 13 .cpp + 24 .hpp files, plus the vendored Plutonium UI submodule). The current codebase works but reads as a prototype: + +- Flat `source/` + mostly flat `include/` with mixed `PascalCase` and `lowercase` filenames. +- Mixed identifier style (`recv_all` next to `isServerTransferDone`). +- 11 headers use `#pragma once`, 9 use legacy `#ifndef X_HPP` guards. +- **Severe layering violation**: UI layouts (`TitlesLayout::runTransfer`, `runReceive`) call into raw socket code and `io::restore` directly. +- Globals: 6 in `server.cpp`, 4 in `client.cpp`, 2 in `io.cpp`, plus `g_currentUId` everywhere. +- **Logger is silently broken** (`include/logger.hpp:54`): `printf(StringUtils::format(...).c_str(), args...)` already substitutes the format, then passes leftover variadic args to a string with no remaining `%` specifiers — every error log loses its arguments. +- **Permission bug** in `source/io.cpp:119`: `mkdir(path.c_str(), 777)` — decimal 777 is octal 1411 (no read for owner). Has been wrong since day one. +- One monster function: `io::restore` is 112 lines mixing mount, create, clear, copy, commit. +- Russian comment in `io.cpp:231` (mixed languages). +- Dead commented-out code in `util.cpp:50-58` and `logger.hpp:51-54`. +- No `.clang-format`, no `.clang-tidy`, no `.editorconfig`, no `.gitattributes`, no CI, no `README.md`, no `ARCHITECTURE.md`, no `LICENSE` file (despite `io.cpp` carrying GPLv3 from Checkpoint). + +Goal: transform NXST into a layered, conventionally-named, tooled-up project that builds on Switch via CMake and is readable by a stranger in under ten minutes. Solo developer; bar is "professional indie", not enterprise. + +User decisions (already made): +- **Full 8-phase plan**. +- **Migrate Make → CMake** with devkitpro toolchain file. +- **camelCase for functions, PascalCase for classes**, snake_case for files and locals, `kPascalCase` for constants. +- **No tests** (Switch homebrew rarely has them; pure-logic surface is small). + +--- + +## Target Directory Tree + +``` +NXST/ +├── .clang-format # LLVM-derived, 4-space, 110 col +├── .clang-tidy # bugprone-*, readability-*, modernize-* +├── .editorconfig # LF, UTF-8, trim WS, final newline +├── .gitattributes # eol=lf; lib/Plutonium linguist-vendored +├── .github/workflows/build.yml # CI: devkitpro container, cmake, .nro upload +├── docs/ +│ ├── ARCHITECTURE.md +│ ├── PROTOCOL.md +│ └── screenshots/ +├── cmake/ +│ └── modules/ # FindPlutonium.cmake (if needed) +├── include/nxst/ +│ ├── app/ # MainApplication, AppContext +│ ├── domain/ # Title, Account, TransferState, Result +│ ├── infra/ +│ │ ├── fs/ # filesystem, directory, RAII handles +│ │ ├── net/ # socket, sendAll/recvAll +│ │ └── sys/ # logger, threading, switch services +│ ├── service/ # TransferService +│ └── ui/ # layouts, widgets, theme +├── src/ # mirrors include/nxst/ +│ ├── app/ +│ ├── domain/ +│ ├── infra/{fs,net,sys}/ +│ ├── service/ +│ ├── ui/ +│ └── main.cpp +├── tools/format.sh # one-line clang-format helper +├── lib/Plutonium/ # submodule, untouched +├── deps/asprintf/ # untouched +├── CMakeLists.txt # top-level +├── README.md +├── ARCHITECTURE.md # → docs/ARCHITECTURE.md +├── CHANGELOG.md +├── LICENSE # GPLv3 (forced by Checkpoint inheritance) +└── icon.png +``` + +--- + +## Naming Convention Spec + +| Axis | Rule | Examples | +|---|---|---| +| Files | `snake_case.{cpp,hpp}` | `transfer_service.cpp`, `socket.hpp` | +| Classes / structs | `PascalCase` | `TransferService`, `AccountProfile`, `Socket` | +| Functions / methods | `camelCase` | `sendAll()`, `recvAll()`, `replaceUsername()` | +| Locals / params | `snake_case` | `int sock_fd`, `std::string file_name` | +| Members | `snake_case`, no prefix | `bytes_done`, `client_sock` | +| Constants / constexpr | `kPascalCase` | `kTcpPort`, `kBufSize`, `kMulticastGroup` | +| Enums | `enum class Foo { Bar, Baz }` | `enum class TransferKind { Send, Receive }` | +| Macros (avoid) | `NXST_UPPER_SNAKE` | `NXST_UNREACHABLE` | +| Namespaces | lowercase, short | `nxst`, `nxst::net`, `nxst::ui` | + +--- + +## Layering (dependency direction) + +``` + ┌────────────────────────┐ + │ Presentation (ui/) │ + │ layouts, widgets │ + └───────────┬────────────┘ + │ + ┌───────────▼────────────┐ + │ Service (svc/) │ + │ TransferService │ + └───────────┬────────────┘ + │ + ┌────────────────────┼────────────────────┐ + │ │ │ +┌──────▼──────┐ ┌────────▼──────┐ ┌────────▼──────┐ +│ Domain │ │ Infra/net │ │ Infra/fs │ Infra/sys +│ Title, │ │ Socket, │ │ Directory, │ Logger, +│ Account, │ │ sendAll, │ │ FsHandle, │ Thread, +│ Result │ │ recvAll │ │ FileHandle │ Account svc +└─────────────┘ └───────────────┘ └───────────────┘ + │ + libnx, SDL, Plutonium +``` + +**Hard rules.** `domain/` depends on nothing in the project. `infra/` depends only on `domain/`. `service/` depends on `domain/` + `infra/`. `ui/` depends only on `service/` + `domain/` — **must not include ``, ``, `pthread.h`, or call `recv`/`send` directly**. `app/` wires everything. + +CI enforcement: a 20-line shell step greps `src/ui/**` for forbidden headers. + +--- + +## Phase 0 — Tooling and ground rules (S, ~2h) + +**Goal.** Lay down conventions before touching code. + +**Files created.** +- `.clang-format`: BasedOnStyle LLVM, IndentWidth 4, ColumnLimit 110, PointerAlignment Left, IncludeBlocks Regroup with three categories (system, third-party, project). +- `.clang-tidy`: enable `bugprone-*`, `readability-*` (minus `magic-numbers`), `modernize-*` (minus `use-trailing-return-type`, `use-nodiscard`), `cppcoreguidelines-pro-type-cstyle-cast`. Disable `fuchsia-*`, `google-*`, `llvm-header-guard`. +- `.editorconfig`: LF, UTF-8, 4-space, trim trailing whitespace, final newline. Tab indent for `Makefile` (only while it still exists). +- `.gitattributes`: `* text=auto eol=lf`, `lib/Plutonium/** linguist-vendored=true`, `deps/** linguist-vendored=true`. Stops GitHub mis-classifying the repo language. +- `tools/format.sh`: `clang-format -i` over `src/` + `include/`. + +**Risk.** None — no compiled changes. + +**Buildable?** Unaffected. + +--- + +## Phase 1 — Bug fixes and dead code (S, ~3h) + +**Goal.** Fix concrete bugs before any structural change can mask them. + +**Tasks.** +1. Replace broken `Logger` (`include/logger.hpp`) with free-function API in `include/nxst/infra/sys/log.hpp`: + ```cpp + namespace nxst::log { + enum class Level { Debug, Info, Warn, Error }; + void write(Level, const char* fmt, ...) __attribute__((format(printf, 2, 3))); + void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2))); + void info (const char* fmt, ...) __attribute__((format(printf, 1, 2))); + void warn (const char* fmt, ...) __attribute__((format(printf, 1, 2))); + void error(const char* fmt, ...) __attribute__((format(printf, 1, 2))); + } + ``` + Implementation: `vsnprintf` into stack buffer, prefix with `[YYYY-MM-DD HH:MM:SS][LEVEL]`, append to `/switch/NXST/log.log` and stderr. TU-local `std::mutex`. No singleton class. Drop the GPLv3 Checkpoint header (the new file is original). + - `format(printf, ...)` attribute gives compile-time format-string checking — that is the actual reason this rewrite matters. + - Leave thin shim `LOG_INFO(...)`/`LOG_ERROR(...)` macros so existing call sites keep compiling during phase migration. +2. Fix `source/io.cpp:119` — `mkdir(path.c_str(), 777)` → `mkdir(path.c_str(), 0777)`. +3. Translate Russian comment at `source/io.cpp:231` to English. +4. Delete dead commented-out code in `source/util.cpp:50-58` and old `include/logger.hpp:51-54` block. + +**Risk.** Logger API change ripples to <30 call sites. Macros bridge the gap. + +**Buildable?** Yes after each file. + +--- + +## Phase 2 — File renames + `#pragma once` (S, ~2h) + +**Goal.** Land the filename convention before the directory move so git history sees rename + move as separate commits. + +**Tasks.** +- Rename to `snake_case` via `git mv` (use two-step on case-insensitive macOS): `Main.cpp`→`main.cpp`, `MainApplication.{cpp,hpp}`→`main_application.{cpp,hpp}`, `TitlesLayout.{cpp,hpp}`→`titles_layout.{cpp,hpp}`, `UsersLayout.{cpp,hpp}`→`users_layout.{cpp,hpp}`, plus matching headers under `include/`. +- Convert all `#ifndef X_HPP / #define X_HPP / #endif` guards to `#pragma once`. Affected: `logger.hpp`, `account.hpp`, and the 7 other headers using legacy guards. +- Update affected `#include` lines. +- `Makefile` `SOURCES` globs `*.cpp`, so no Makefile change needed for renames. + +**Risk.** macOS case-insensitive FS hides case-only renames — use `git mv Foo.cpp tmp.cpp && git mv tmp.cpp foo.cpp`. + +**Buildable?** Yes after each pair. + +--- + +## Phase 3 — Directory restructure (M, ~1 day) + +**Goal.** Move flat `source/` and `include/` into the layered tree. **No code changes**, just relocation + include-path updates. + +**Tasks.** +1. Create the `src/{app,domain,infra/{net,fs,sys},service,ui}` and `include/nxst/{...}` skeleton. +2. Move files: + - `protocol.hpp`, `transfer_state.hpp`, `account.hpp` (the `AccountUid` struct + ordering only), `title.hpp/cpp`, `common.hpp/cpp`, `util.hpp/cpp` → `domain/`. + - `client.{hpp,cpp}` → `infra/net/transfer_sender.{hpp,cpp}`, `server.{hpp,cpp}` → `infra/net/transfer_receiver.{hpp,cpp}`. Renaming is intentional: `CLAUDE.md` already documents that "server = receiver, client = sender" is inverted from typical usage; renaming kills the confusion permanently. + - `net/socket.hpp` → `infra/net/socket.hpp`. + - `io.hpp/cpp`, `directory.hpp/cpp`, `filesystem.hpp/cpp` → `infra/fs/`. + - new `log.hpp/cpp`, switch-service init pieces from `account.cpp` → `infra/sys/`. + - `main_application.{hpp,cpp}`, `main.{hpp,cpp}` → `app/`. + - `titles_layout.*`, `users_layout.*`, `transfer_overlay.hpp`, `theme.hpp`, `ui/*.hpp`, `header_bar.hpp` → `ui/`. +3. Update `Makefile` (still in use — CMake migration is Phase 4): + ```make + SOURCES := src src/app src/domain src/infra/net src/infra/fs src/infra/sys src/service src/ui lib/Plutonium/source + INCLUDES := include lib/Plutonium/include + ``` + Remove the now-defunct `include/net` from `INCLUDES`. +4. Update **every** `#include` to `#include ` form: `#include `, `#include `. The single root `include/` keeps the prefix mandatory and grepable. + +**Risk.** Big disruptive phase. Do on a feature branch, build after every directory's worth of moves. `service/` directory stays empty for now — scaffolding only. + +**Buildable?** Yes if you move-and-rebuild incrementally. + +--- + +## Phase 4 — Migrate Make → CMake (M, ~1 day) + +**Goal.** Replace `Makefile` with `CMakeLists.txt` using devkitpro's bundled `Switch.cmake` toolchain file. Project compiles via `cmake -B build -DCMAKE_TOOLCHAIN_FILE=$DEVKITPRO/cmake/Switch.cmake && cmake --build build`. + +**Tasks.** +1. Create top-level `CMakeLists.txt`: + - `cmake_minimum_required(VERSION 3.20)`, `project(NXST CXX)`. + - C++17, `-fno-rtti -fno-exceptions` (matches current `CXXFLAGS`). + - ARM flags `-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE` mirror current `ARCH`. + - `find_package(PkgConfig REQUIRED)` + `pkg_check_modules(SWITCH_LIBS REQUIRED SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer freetype2 harfbuzz minizip libpng libjpeg libwebp glesv2 egl glapi zlib)` against `aarch64-none-elf-pkg-config` (already wired up per memory ID 101). + - Manual link: `pu` (Plutonium), `drm_nouveau`, plus the trailing `-lharfbuzz -lfreetype -lz` static-link-order workaround (memory IDs 97, 98, 100 — required after recent libnx update). + - Add Plutonium as `add_subdirectory(lib/Plutonium)` if its CMake exists, else as an INTERFACE library wrapping its prebuilt `.a`. Inspection during this phase decides. + - Use devkitpro's `nx_create_nro` / `nx_generate_nacp` helpers from the toolchain file to emit `NXST.nro` with icon + NACP metadata (`APP_TITLE=NXST`, `APP_AUTHOR=DragonSpirit`, `APP_VERSION=04.26.2026`). +2. Create `cmake/modules/` if any custom Find module proves necessary (likely not). +3. Add convenience targets: `cmake --build build --target send` to invoke `nxlink NXST.nro`. +4. Delete old `Makefile` only after CMake build produces a working `.nro` matching the Make output. +5. Update `.gitignore`: replace `build/` Make artifacts with CMake's `build/` directory rules and `CMakeCache.txt`, `CMakeFiles/`, `compile_commands.json`. + +**Risk.** Highest in the plan. Mitigation: +- Keep `Makefile` alongside CMake during the phase. Verify `.nro` from both is identical (or at minimum boots and runs on hardware). +- If the devkitpro `Switch.cmake` toolchain proves brittle for Plutonium's transitive headers, fall back to keeping Make and document why CMake was deferred. +- The pkg-config tweaks captured in memory IDs 96–101 are non-obvious — re-apply them in the CMake config explicitly, not via `pkg_check_modules` defaults. + +**Buildable?** Yes — both build systems coexist until CMake is verified. + +--- + +## Phase 5 — TransferService extraction, kill globals (L, ~2 days) + +**Goal.** Sever the UI → net coupling. Single biggest "this looks like real software" change. + +**Tasks.** +1. Create `src/service/transfer_service.{hpp,cpp}`. Class owns: + - one `TransferState` per direction (sender, receiver), + - all listen / accept / broadcast threads (currently 6 globals in `server.cpp`, 4 in `client.cpp`), + - public API: `start(TransferKind, AccountUid, std::vector)`, `cancel()`, `progress()`, `statusText()`, `isDone()`, `failureReason()`, `setOnComplete(std::function)`. +2. Move file globals (`g_server_state`, `g_client_state`, all socket/pthread atomics) into `TransferService` private members. pthread C trampolines: `static void* threadEntry(void* self)` immediately calls `*static_cast(self)`. +3. Refactor `titles_layout.cpp` `runTransfer`/`runReceive` to call `app_ctx.transfer.start(...)` / `cancel()` instead of free `transfer_files()` / `start_listening()`. Inject via `AppContext&` (already partially present per `include/ui/UiContext.hpp`). +4. Move `g_currentUId` out of global scope into `AppContext::current_user`. Pass `AppContext&` down to layouts at construction. There is exactly one `AppContext` and its lifetime equals the app — safe and cheap. +5. UI's `runReceive` currently calls `io::restore` directly after server work completes. Move into the service's completion callback so UI never touches FS directly. + +**Strategy.** Keep old `transfer_files()` / `start_listening()` free functions as thin wrappers around `TransferService` for the duration of the phase. Convert UI call sites one at a time. Delete wrappers only after both sites are on the service. + +**Buildable?** Yes via the wrappers. + +--- + +## Phase 6 — `Result` and RAII (M, ~1 day) + +**Goal.** Replace bool/out-param/silent-failure patterns with `Result`; wrap raw OS handles in RAII. + +**Tasks.** +1. `include/nxst/domain/result.hpp` — minimal `Result` with tagged union (no `std::variant` because `-fno-exceptions` rules out monostate-on-throw): + ```cpp + template + class Result { + bool ok_; + union { T val_; E err_; }; + public: + static Result success(T v); + static Result failure(E e); + bool isOk() const noexcept; + const T& value() const; // UB if !isOk + const E& error() const; // UB if isOk + template auto map(F&&) const; + }; + ``` + Resist importing `tl::expected`. 60 lines of in-house code beats a vendored dep at this scale. +2. RAII wrappers in `include/nxst/infra/`: + - `FdHandle` — owns `int fd`, closes on dtor, move-only. + - `FsFileSystemHandle` — owns `FsFileSystem`, calls `fsFsClose`. Plugs the "save"-mount leak from memory ID 59. + - `AccountProfileHandle` — owns `AccountProfile`, calls `accountProfileClose`. Plugs the bug from memory ID 64. + - `FileHandle` — owns `FILE*`, `fclose` on dtor. +3. Replace `io.cpp:57` `new u8[BUFFER_SIZE] / delete[]` and `io.cpp:234` `malloc/free` with `std::vector` (already the pattern in `client.cpp`/`server.cpp`). +4. Split `io::restore` (the 112-line monster, `io.cpp:221-332`) into `restoreSaveForTitle`, `extractAndCommit`, `clearStaleSaveFiles`. Each returns `Result`. +5. Convert `recvAll` / `sendAll` / socket setup paths in `infra/net` to return `Result` so error paths compose under `-fno-exceptions`. + +**Strategy.** Adopt incrementally — start at IO boundaries, leave `bool` returns untouched in modules not yet refactored. + +**Buildable?** Yes per file. + +--- + +## Phase 7 — Documentation + license (S, ~half day) + +**Goal.** A stranger can build, run, and understand this in under 15 minutes. + +**Files created.** +- `README.md`: what NXST does, screenshot, install (drop `.nro` into `/switch/`), build (devkitpro + `cmake --build`), credit Plutonium and Checkpoint, link to `docs/`. +- `docs/ARCHITECTURE.md`: layering diagram from this plan, threading model (one accept thread, one broadcast thread, one transfer thread per direction; cancellation via `shutdown(2)` + atomic flag), pointers to key files. +- `docs/PROTOCOL.md`: promote the 9-line wire-format comment from `protocol.hpp` into a real spec — byte-level diagrams, multicast discovery on `239.0.0.1:8081`, TCP transfer on `:8080`, EOF sentinel = `filename_len == 0`, `kBufSize=65536`, `kMaxFilename=4096`. +- `CHANGELOG.md`: Keep-a-Changelog format. Version `0.x` until protocol stabilizes. +- `LICENSE`: GPLv3 — forced by `io.cpp` carrying Checkpoint's GPLv3 header. Document credit + license inheritance in README. + +**Risk.** None. + +**Buildable?** Unaffected. + +--- + +## Phase 8 — CI (S, ~2h) + +**Goal.** Every push verifies `.nro` builds. One green badge tells visitors the repo is alive. + +**Tasks.** +- `.github/workflows/build.yml`: + ```yaml + on: [push, pull_request] + jobs: + nro: + runs-on: ubuntu-latest + container: devkitpro/devkita64:latest + steps: + - uses: actions/checkout@v4 + with: { submodules: recursive } + - run: | + cmake -B build \ + -DCMAKE_TOOLCHAIN_FILE=$DEVKITPRO/cmake/Switch.cmake \ + -DCMAKE_BUILD_TYPE=Release + cmake --build build -j$(nproc) + - uses: actions/upload-artifact@v4 + with: + name: NXST-${{ github.sha }} + path: build/NXST.nro + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: | + sudo apt-get install -y clang-format + find src include -name '*.cpp' -o -name '*.hpp' \ + | xargs clang-format --dry-run --Werror + layering: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: | + ! grep -rE '^#include\s*[<"](arpa/|sys/socket|pthread\.h)' src/ui/ + ``` +- Optional release job on tag `v*`: same CMake build, attach `.nro` to GitHub Release. + +**Risk.** First run typically fails on submodule init or container path — iterate on a branch. + +**Buildable?** Pass or fix. + +--- + +## Critical files to modify + +- `Makefile` — Phase 3 SOURCES/INCLUDES update; deleted in Phase 4 after CMake parity. +- `include/logger.hpp` — Phase 1 replace with `nxst::log` free-function API. +- `source/io.cpp` — Phase 1 mkdir + comment fix; Phase 6 split `restore()` + RAII; Phase 5 stop being called from UI. +- `source/server.cpp` — Phase 3 rename to `transfer_receiver.cpp`; Phase 5 globals migrate into `TransferService`. +- `source/client.cpp` — Phase 3 rename to `transfer_sender.cpp`; Phase 5 globals migrate into `TransferService`. +- `source/TitlesLayout.cpp` — Phase 2 rename; Phase 5 sever direct calls into network and `io::restore`. +- `source/Main.cpp` — Phase 2 rename; Phase 5 construct `AppContext` with `TransferService`. +- `source/util.cpp` — Phase 1 delete dead block. + +New files (not yet present): +- `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes` — Phase 0. +- `CMakeLists.txt` — Phase 4. +- `include/nxst/domain/result.hpp` — Phase 6. +- `include/nxst/service/transfer_service.hpp` + `src/service/transfer_service.cpp` — Phase 5. +- `README.md`, `docs/ARCHITECTURE.md`, `docs/PROTOCOL.md`, `CHANGELOG.md`, `LICENSE` — Phase 7. +- `.github/workflows/build.yml` — Phase 8. + +--- + +## What is deliberately NOT in this plan + +1. **C++20.** Devkitpro's `gnu++17` is fine; nothing in the codebase wants concepts/ranges/coroutines, and coroutines under `-fno-exceptions` are a maze. Stay on `gnu++17`. +2. **Abstracting Plutonium.** It is *the* UI framework; you will not port it. UI calls Plutonium directly. +3. **`tl::expected` / `std::expected`.** 60-line in-house `Result` is enough. +4. **DI containers, factories, abstract logger interfaces.** Solo developer, one service, one logger sink. Add complexity only when a second sink actually appears. +5. **Integration tests against the network.** No bug class is meaningfully prevented; cost is high; value comes from running on real hardware. +6. **Migrating from pthread to `std::thread`.** Libnx's pthread is the supported path. A single `Thread` RAII wrapper inside `infra/sys/` is the most you should add. +7. **PImpl broadly.** At 2.3 KLOC compile time is sub-second; PImpl just adds indirection. +8. **Re-vendoring or modifying Plutonium.** Submodule, untouched. `linguist-vendored` keeps it out of GitHub language stats. +9. **UPPER_SNAKE constants.** Visually clash with macros; `kPascalCase` is the modern norm. + +--- + +## Recommended phasing for a solo developer + +- **Weekend 1:** Phase 0 + 1 + 2. Tooled, bugs fixed, files renamed, builds. Already feels different. +- **Weekend 2:** Phase 3. Big move, one sitting on a branch. +- **Weekend 3:** Phase 4 (CMake). Build system swap. Highest individual phase risk — schedule when fresh. +- **Weekend 4 (longest):** Phase 5 (TransferService). UI no longer touches sockets. The win. +- **Weekend 5:** Phase 6. `Result`, RAII, `restore()` split. +- **Evening:** Phase 7. README + ARCHITECTURE + LICENSE. +- **Evening:** Phase 8. CI. + +Stopping after Weekend 4 still leaves the project visibly more serious than it started. Each weekend produces a defensible "ship it" state. + +--- + +## Verification (end-to-end test plan) + +After each phase: +1. `cmake --build build` (Phase 4+) or `make` produces `NXST.nro` with no warnings. +2. Copy `NXST.nro` to Switch SD card `/switch/NXST/`, launch via hbmenu, verify the title list and user list render (regression tests for memory IDs 73, 78). +3. Run a save transfer between two Switches: select a title on the sender, "Receive" on the receiver, verify the file lands and `io::restore` replaces it on the receiver. Verify cancel works mid-transfer (regression for memory IDs 60, 103, 104). +4. Verify the log file at `/switch/NXST/log.log` contains formatted entries with timestamps and levels (regression for the broken-Logger bug). +5. Phase 8 only: confirm a green CI run on a PR; download the `NXST.nro` artifact; install it on Switch; run the same flow. diff --git a/include/account.hpp b/include/account.hpp index ec43644..693a7e1 100644 --- a/include/account.hpp +++ b/include/account.hpp @@ -24,9 +24,7 @@ * reasonable ways as different from the original version. */ -#ifndef ACCOUNT_HPP -#define ACCOUNT_HPP - +#pragma once #include #include #include @@ -73,4 +71,3 @@ namespace Account { std::string iconPath(AccountUid id); } -#endif \ No newline at end of file diff --git a/include/common.hpp b/include/common.hpp index b80a8f6..a652f84 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -24,9 +24,7 @@ * reasonable ways as different from the original version. */ -#ifndef COMMON_HPP -#define COMMON_HPP - +#pragma once #include #include #include @@ -62,4 +60,3 @@ namespace StringUtils { char* getConsoleIP(void); -#endif \ No newline at end of file diff --git a/include/const.h b/include/const.h index 0cf33f4..935f045 100644 --- a/include/const.h +++ b/include/const.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #define COLOR(hex) pu::ui::Color::FromHex(hex) #define BACKGROUND_COLOR theme::color::BgBase diff --git a/include/directory.hpp b/include/directory.hpp index eefe165..2ba7a75 100644 --- a/include/directory.hpp +++ b/include/directory.hpp @@ -24,9 +24,7 @@ * reasonable ways as different from the original version. */ -#ifndef DIRECTORY_HPP -#define DIRECTORY_HPP - +#pragma once #include #include #include @@ -55,4 +53,3 @@ private: bool mGood; }; -#endif \ No newline at end of file diff --git a/include/filesystem.hpp b/include/filesystem.hpp index a940a3e..6b93c4c 100644 --- a/include/filesystem.hpp +++ b/include/filesystem.hpp @@ -24,9 +24,7 @@ * reasonable ways as different from the original version. */ -#ifndef FILESYSTEM_HPP -#define FILESYSTEM_HPP - +#pragma once #include "account.hpp" #include @@ -36,4 +34,3 @@ namespace FileSystem { void unmount(void); } -#endif \ No newline at end of file diff --git a/include/io.hpp b/include/io.hpp index 3693f5b..37f95c6 100644 --- a/include/io.hpp +++ b/include/io.hpp @@ -24,9 +24,7 @@ * reasonable ways as different from the original version. */ -#ifndef IO_HPP -#define IO_HPP - +#pragma once #include "account.hpp" #include "directory.hpp" #include "title.hpp" @@ -52,4 +50,3 @@ namespace io { bool fileExists(const std::string& path); } -#endif \ No newline at end of file diff --git a/include/logger.hpp b/include/logger.hpp index 177694e..d14be5a 100644 --- a/include/logger.hpp +++ b/include/logger.hpp @@ -1,85 +1,55 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ +#pragma once -#ifndef LOGGER_HPP -#define LOGGER_HPP - -#include "common.hpp" - -#include +#include #include -class Logger { -public: - static Logger& getInstance(void) +// New API — use these going forward. +namespace nxst::log { + +enum class Level { Debug, Info, Warn, Error }; + +void write(Level level, const char* fmt, ...) __attribute__((format(printf, 2, 3))); +void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2))); +void info (const char* fmt, ...) __attribute__((format(printf, 1, 2))); +void warn (const char* fmt, ...) __attribute__((format(printf, 1, 2))); +void error(const char* fmt, ...) __attribute__((format(printf, 1, 2))); + +// No-op: writes are immediate. Kept for source compatibility during migration. +inline void flush() {} + +} // namespace nxst::log + +// Backward-compat shim — existing Logger::getInstance().log(...) call sites compile +// unchanged. Format args are dropped (same behavior as broken original). Migrate +// call sites to nxst::log::* in Phase 3. +struct Logger { + static Logger& getInstance() { - static Logger mLogger; - return mLogger; + static Logger instance; + return instance; } - inline static const std::string INFO = "[INFO]"; - inline static const std::string DEBUG = "[DEBUG]"; - inline static const std::string ERROR = "[ERROR]"; - inline static const std::string WARN = "[WARN]"; + // clang-tidy naming suppressed: these must match existing call sites during migration. + static constexpr const char* INFO = "[INFO]"; // NOLINT(readability-identifier-naming) + static constexpr const char* DEBUG = "[DEBUG]"; // NOLINT(readability-identifier-naming) + static constexpr const char* ERROR = "[ERROR]"; // NOLINT(readability-identifier-naming) + static constexpr const char* WARN = "[WARN]"; // NOLINT(readability-identifier-naming) + static void flush() { nxst::log::flush(); } + + // Args intentionally dropped — format string still logged for visibility. template - void log(const std::string& level, const std::string& format = {}, Args... args) + void log(const std::string& level, const std::string& fmt, Args&&... /*args*/) { - // buffer += StringUtils::format(("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...); - // buffer += StringUtils::format("%s\n", format.c_str()); - // buffer += StringUtils::format("%s\n",StringUtils::format("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...); - printf(StringUtils::format("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...); + if (level == ERROR) nxst::log::error("%s", fmt.c_str()); + else if (level == WARN) nxst::log::warn("%s", fmt.c_str()); + else if (level == DEBUG) nxst::log::debug("%s", fmt.c_str()); + else nxst::log::info("%s", fmt.c_str()); } - void flush(void) - { - mFile = fopen(mPath.c_str(), "a"); - if (mFile != NULL) { - fprintf(mFile, buffer.c_str()); - fprintf(stderr, buffer.c_str()); - fclose(mFile); - } - } + Logger() = default; + ~Logger() = default; -private: - Logger(void) { buffer = ""; } - ~Logger(void) {} - - Logger(Logger const&) = delete; - void operator=(Logger const&) = delete; - -#if defined(__SWITCH__) - const std::string mPath = "/switch/NXST/log.log"; -#else - const std::string mPath = "log.log"; -#endif - - FILE* mFile; - - std::string buffer; + Logger(const Logger&) = delete; // NOLINT(modernize-use-equals-delete) + Logger& operator=(const Logger&) = delete; // NOLINT(modernize-use-equals-delete) }; - -#endif \ No newline at end of file diff --git a/include/main.hpp b/include/main.hpp index a46e5d2..cdff2fc 100644 --- a/include/main.hpp +++ b/include/main.hpp @@ -1,5 +1,4 @@ -#ifndef MAIN_HPP -#define MAIN_HPP +#pragma once #include #include "account.hpp" #include "title.hpp" @@ -23,4 +22,3 @@ inline std::string g_currentFile = ""; inline bool g_isTransferringFile = false; inline const std::string g_emptySave = "New..."; -#endif \ No newline at end of file diff --git a/include/MainApplication.hpp b/include/main_application.hpp similarity index 81% rename from include/MainApplication.hpp rename to include/main_application.hpp index c510dea..342e2a2 100644 --- a/include/MainApplication.hpp +++ b/include/main_application.hpp @@ -1,8 +1,8 @@ #pragma once #include -#include -#include +#include +#include namespace ui { diff --git a/include/net/Socket.hpp b/include/net/socket.hpp similarity index 100% rename from include/net/Socket.hpp rename to include/net/socket.hpp diff --git a/include/Theme.hpp b/include/theme.hpp similarity index 100% rename from include/Theme.hpp rename to include/theme.hpp diff --git a/include/title.hpp b/include/title.hpp index 31e88b2..acbb900 100644 --- a/include/title.hpp +++ b/include/title.hpp @@ -24,9 +24,7 @@ * reasonable ways as different from the original version. */ -#ifndef TITLE_HPP -#define TITLE_HPP - +#pragma once #include "account.hpp" #include "filesystem.hpp" #include "io.hpp" @@ -88,4 +86,3 @@ void rotateSortMode(void); void refreshDirectories(u64 id); std::unordered_map getCompleteTitleList(void); -#endif \ No newline at end of file diff --git a/include/TitlesLayout.hpp b/include/titles_layout.hpp similarity index 96% rename from include/TitlesLayout.hpp rename to include/titles_layout.hpp index b8b6c39..1903589 100644 --- a/include/TitlesLayout.hpp +++ b/include/titles_layout.hpp @@ -5,8 +5,8 @@ #include #include #include -#include -#include +#include +#include namespace ui { diff --git a/include/TransferOverlay.hpp b/include/transfer_overlay.hpp similarity index 99% rename from include/TransferOverlay.hpp rename to include/transfer_overlay.hpp index ab9d5a6..ddfe16b 100644 --- a/include/TransferOverlay.hpp +++ b/include/transfer_overlay.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include namespace ui { diff --git a/include/TransferState.hpp b/include/transfer_state.hpp similarity index 100% rename from include/TransferState.hpp rename to include/transfer_state.hpp diff --git a/include/ui/Card.hpp b/include/ui/card.hpp similarity index 95% rename from include/ui/Card.hpp rename to include/ui/card.hpp index c923f4d..5861a32 100644 --- a/include/ui/Card.hpp +++ b/include/ui/card.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include namespace ui { diff --git a/include/ui/HeaderBar.hpp b/include/ui/header_bar.hpp similarity index 98% rename from include/ui/HeaderBar.hpp rename to include/ui/header_bar.hpp index 59b1a20..54f5c2f 100644 --- a/include/ui/HeaderBar.hpp +++ b/include/ui/header_bar.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include -#include +#include +#include #include namespace ui { diff --git a/include/ui/HintBar.hpp b/include/ui/hint_bar.hpp similarity index 98% rename from include/ui/HintBar.hpp rename to include/ui/hint_bar.hpp index 76107ed..f67769c 100644 --- a/include/ui/HintBar.hpp +++ b/include/ui/hint_bar.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include diff --git a/include/ui/UiContext.hpp b/include/ui/ui_context.hpp similarity index 100% rename from include/ui/UiContext.hpp rename to include/ui/ui_context.hpp diff --git a/include/UsersLayout.hpp b/include/users_layout.hpp similarity index 90% rename from include/UsersLayout.hpp rename to include/users_layout.hpp index e61f191..2529ca8 100644 --- a/include/UsersLayout.hpp +++ b/include/users_layout.hpp @@ -1,7 +1,7 @@ #include #include -#include -#include +#include +#include #include namespace ui { diff --git a/include/util.hpp b/include/util.hpp index 5fafd97..b2b865d 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -24,9 +24,7 @@ * reasonable ways as different from the original version. */ -#ifndef UTIL_HPP -#define UTIL_HPP - +#pragma once #include "account.hpp" #include "common.hpp" #include "io.hpp" @@ -50,4 +48,4 @@ namespace StringUtils { std::string elide(const std::string& s, size_t maxChars); } -#endif + diff --git a/source/client.cpp b/source/client.cpp index 0ea5350..f84577c 100644 --- a/source/client.cpp +++ b/source/client.cpp @@ -19,7 +19,7 @@ #endif #include -#include +#include namespace fs = std::filesystem; using path = fs::path; diff --git a/source/io.cpp b/source/io.cpp index 4364d8c..d303384 100644 --- a/source/io.cpp +++ b/source/io.cpp @@ -116,7 +116,7 @@ Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) Result io::createDirectory(const std::string& path) { - mkdir(path.c_str(), 777); + mkdir(path.c_str(), 0777); return 0; } @@ -228,8 +228,8 @@ std::tuple io::restore(size_t index, AccountUid uid, Logger::getInstance().log(Logger::INFO, "Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(), title.userId().uid[1], title.userId().uid[0]); - // Если сейв ещё не существует (игра не запускалась) — создаём его через NACP. - // fsCreateSaveDataFileSystem возвращает ошибку если сейв уже есть — это нормально. + // If save data does not yet exist (game was never launched), create it via NACP. + // fsCreateSaveDataFileSystem returns an error if the save already exists — this is expected. { NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData)); if (nsacd != NULL) { diff --git a/source/logger.cpp b/source/logger.cpp new file mode 100644 index 0000000..10c5bca --- /dev/null +++ b/source/logger.cpp @@ -0,0 +1,64 @@ +#include + +#include +#include +#include +#include + +namespace { + +std::mutex g_log_mutex; + +#if defined(__SWITCH__) +constexpr const char* kLogPath = "/switch/NXST/log.log"; +#else +constexpr const char* kLogPath = "nxst.log"; +#endif + +void writeEntry(const char* tag, const char* fmt, va_list args) +{ + char msg[2048]; + vsnprintf(msg, sizeof(msg), fmt, args); + + time_t now = time(nullptr); + struct tm tm_buf; + localtime_r(&now, &tm_buf); + char time_str[16]; + strftime(time_str, sizeof(time_str), "%H:%M:%S", &tm_buf); + + std::lock_guard lock(g_log_mutex); + + fprintf(stderr, "[%s]%s %s\n", time_str, tag, msg); + + FILE* log_file = fopen(kLogPath, "a"); + if (log_file != nullptr) { + fprintf(log_file, "[%s]%s %s\n", time_str, tag, msg); + fclose(log_file); + } +} + +} // namespace + +namespace nxst::log { + +void write(Level level, const char* fmt, ...) +{ + const char* tag = "[INFO] "; + switch (level) { + case Level::Debug: tag = "[DEBUG]"; break; + case Level::Info: tag = "[INFO] "; break; + case Level::Warn: tag = "[WARN] "; break; + case Level::Error: tag = "[ERROR]"; break; + } + va_list args; + va_start(args, fmt); + writeEntry(tag, fmt, args); + va_end(args); +} + +void debug(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[DEBUG]", fmt, args); va_end(args); } +void info (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[INFO] ", fmt, args); va_end(args); } +void warn (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[WARN] ", fmt, args); va_end(args); } +void error(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[ERROR]", fmt, args); va_end(args); } + +} // namespace nxst::log diff --git a/source/Main.cpp b/source/main.cpp similarity index 94% rename from source/Main.cpp rename to source/main.cpp index 47748d1..9eb33dd 100644 --- a/source/Main.cpp +++ b/source/main.cpp @@ -1,4 +1,4 @@ -#include +#include #include "util.hpp" #include "main.hpp" #include diff --git a/source/MainApplication.cpp b/source/main_application.cpp similarity index 93% rename from source/MainApplication.cpp rename to source/main_application.cpp index c2fa9de..d9e93bc 100644 --- a/source/MainApplication.cpp +++ b/source/main_application.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include namespace ui { MainApplication *mainApp; diff --git a/source/server.cpp b/source/server.cpp index a864df3..9bc3a7b 100644 --- a/source/server.cpp +++ b/source/server.cpp @@ -19,8 +19,8 @@ #endif #include -#include -#include +#include +#include static TransferState g_server_state; static std::atomic g_server_client_sock{-1}; diff --git a/source/TitlesLayout.cpp b/source/titles_layout.cpp similarity index 99% rename from source/TitlesLayout.cpp rename to source/titles_layout.cpp index 3af2d07..68e5d55 100644 --- a/source/TitlesLayout.cpp +++ b/source/titles_layout.cpp @@ -1,10 +1,10 @@ -#include +#include #include #include #include #include #include -#include +#include namespace ui { extern MainApplication *mainApp; diff --git a/source/UsersLayout.cpp b/source/users_layout.cpp similarity index 98% rename from source/UsersLayout.cpp rename to source/users_layout.cpp index 6231d70..96f0d26 100644 --- a/source/UsersLayout.cpp +++ b/source/users_layout.cpp @@ -1,5 +1,5 @@ #include -#include +#include #include "main.hpp" namespace ui { diff --git a/source/util.cpp b/source/util.cpp index 81f4219..79b1f8b 100644 --- a/source/util.cpp +++ b/source/util.cpp @@ -26,7 +26,7 @@ #include "util.hpp" #include -#include +#include #include "main.hpp" void servicesExit(void) @@ -47,16 +47,6 @@ Result servicesInit(void) Logger::getInstance().log(Logger::WARN, "Please do not run NXST in applet mode."); } - // Result socinit = 0; - // if ((socinit = socketInitializeDefault()) == 0) { - // nxlinkStdio(); - // } - // else { - // Logger::getInstance().log(Logger::INFO, "Unable to initialize socket. Result code 0x%08lX.", socinit); - // } - - // g_shouldExitNetworkLoop = R_FAILED(socinit); - Result res = 0; romfsInit(); diff --git a/tools/format.sh b/tools/format.sh new file mode 100755 index 0000000..2717ef7 --- /dev/null +++ b/tools/format.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +find src include -name '*.cpp' -o -name '*.hpp' | xargs clang-format -i +echo "Formatted." -- 2.39.2 From 895fee623580e0f750a2fdddf1156aad1e3ab5b6 Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov Date: Mon, 27 Apr 2026 01:21:16 +0300 Subject: [PATCH 02/10] phase 5: transfer service --- .gitignore | 19 +- CMakeLists.txt | 104 ++++ Makefile | 4 +- PLAN.md | 6 +- include/client.hpp | 13 - include/{ => nxst/app}/main.hpp | 11 +- include/{ => nxst/app}/main_application.hpp | 41 +- include/{ => nxst/domain}/account.hpp | 0 include/{ => nxst/domain}/common.hpp | 0 include/{ => nxst/domain}/protocol.hpp | 0 include/{ => nxst/domain}/title.hpp | 6 +- include/{ => nxst/domain}/transfer_state.hpp | 0 include/{ => nxst/domain}/util.hpp | 6 +- include/{ => nxst/infra/fs}/directory.hpp | 0 include/{ => nxst/infra/fs}/filesystem.hpp | 2 +- include/{ => nxst/infra/fs}/io.hpp | 8 +- include/{ => nxst/infra}/net/socket.hpp | 0 include/nxst/infra/net/transfer_receiver.hpp | 1 + include/nxst/infra/net/transfer_sender.hpp | 1 + include/{ => nxst/infra/sys}/logger.hpp | 0 include/nxst/service/transfer_service.hpp | 76 +++ include/{ => nxst}/ui/card.hpp | 2 +- include/{ => nxst/ui}/const.h | 2 +- include/{ => nxst}/ui/header_bar.hpp | 6 +- include/{ => nxst}/ui/hint_bar.hpp | 2 +- include/{ => nxst/ui}/theme.hpp | 0 include/{ => nxst/ui}/titles_layout.hpp | 14 +- include/{ => nxst/ui}/transfer_overlay.hpp | 4 +- include/{ => nxst}/ui/ui_context.hpp | 2 +- include/{ => nxst/ui}/users_layout.hpp | 6 +- include/server.hpp | 8 - source/client.cpp | 249 ---------- source/main_application.cpp | 21 - source/server.cpp | 345 ------------- {source => src/app}/main.cpp | 160 +++--- src/app/main_application.cpp | 21 + {source => src/domain}/account.cpp | 5 +- {source => src/domain}/common.cpp | 2 +- {source => src/domain}/title.cpp | 4 +- {source => src/domain}/util.cpp | 8 +- {source => src/infra/fs}/directory.cpp | 2 +- {source => src/infra/fs}/filesystem.cpp | 2 +- {source => src/infra/fs}/io.cpp | 6 +- src/infra/net/transfer_receiver.cpp | 1 + src/infra/net/transfer_sender.cpp | 1 + {source => src/infra/sys}/logger.cpp | 2 +- src/service/transfer_service.cpp | 483 +++++++++++++++++++ {source => src/ui}/titles_layout.cpp | 74 ++- {source => src/ui}/users_layout.cpp | 13 +- 49 files changed, 905 insertions(+), 838 deletions(-) create mode 100644 CMakeLists.txt delete mode 100644 include/client.hpp rename include/{ => nxst/app}/main.hpp (79%) rename include/{ => nxst/app}/main_application.hpp (50%) rename include/{ => nxst/domain}/account.hpp (100%) rename include/{ => nxst/domain}/common.hpp (100%) rename include/{ => nxst/domain}/protocol.hpp (100%) rename include/{ => nxst/domain}/title.hpp (96%) rename include/{ => nxst/domain}/transfer_state.hpp (100%) rename include/{ => nxst/domain}/util.hpp (94%) rename include/{ => nxst/infra/fs}/directory.hpp (100%) rename include/{ => nxst/infra/fs}/filesystem.hpp (97%) rename include/{ => nxst/infra/fs}/io.hpp (93%) rename include/{ => nxst/infra}/net/socket.hpp (100%) create mode 100644 include/nxst/infra/net/transfer_receiver.hpp create mode 100644 include/nxst/infra/net/transfer_sender.hpp rename include/{ => nxst/infra/sys}/logger.hpp (100%) create mode 100644 include/nxst/service/transfer_service.hpp rename include/{ => nxst}/ui/card.hpp (93%) rename include/{ => nxst/ui}/const.h (78%) rename include/{ => nxst}/ui/header_bar.hpp (96%) rename include/{ => nxst}/ui/hint_bar.hpp (98%) rename include/{ => nxst/ui}/theme.hpp (100%) rename include/{ => nxst/ui}/titles_layout.hpp (86%) rename include/{ => nxst/ui}/transfer_overlay.hpp (98%) rename include/{ => nxst}/ui/ui_context.hpp (85%) rename include/{ => nxst/ui}/users_layout.hpp (85%) delete mode 100644 include/server.hpp delete mode 100644 source/client.cpp delete mode 100644 source/main_application.cpp delete mode 100644 source/server.cpp rename {source => src/app}/main.cpp (78%) create mode 100644 src/app/main_application.cpp rename {source => src/domain}/account.cpp (98%) rename {source => src/domain}/common.cpp (99%) rename {source => src/domain}/title.cpp (99%) rename {source => src/domain}/util.cpp (97%) rename {source => src/infra/fs}/directory.cpp (98%) rename {source => src/infra/fs}/filesystem.cpp (97%) rename {source => src/infra/fs}/io.cpp (99%) create mode 100644 src/infra/net/transfer_receiver.cpp create mode 100644 src/infra/net/transfer_sender.cpp rename {source => src/infra/sys}/logger.cpp (97%) create mode 100644 src/service/transfer_service.cpp rename {source => src/ui}/titles_layout.cpp (85%) rename {source => src/ui}/users_layout.cpp (88%) diff --git a/.gitignore b/.gitignore index 4d54a32..3c9059b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.serena # Prerequisites *.d @@ -32,5 +33,19 @@ *.exe *.out *.app -server -client + +# Switch build artifacts +*.nro +*.nso +*.pfs0 +*.nacp +*.elf +*.lst +*.map + +# CMake +build/ +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..888c756 --- /dev/null +++ b/CMakeLists.txt @@ -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() diff --git a/Makefile b/Makefile index 356daa7..2f23ac4 100644 --- a/Makefile +++ b/Makefile @@ -32,8 +32,8 @@ include $(DEVKITPRO)/libnx/switch_rules #--------------------------------------------------------------------------------- TARGET := NXST BUILD := build -SOURCES := source lib/Plutonium/source -INCLUDES := include include/net lib/Plutonium/include +SOURCES := src/app src/domain src/infra/net src/infra/fs src/infra/sys src/service src/ui lib/Plutonium/source +INCLUDES := include lib/Plutonium/include EXEFS_SRC := exefs_src APP_TITLE := NXST APP_AUTHOR := DragonSpirit diff --git a/PLAN.md b/PLAN.md index 2c61461..80f5349 100644 --- a/PLAN.md +++ b/PLAN.md @@ -7,14 +7,14 @@ | 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 | | 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 | -| 4 | Make → CMake migration | ☐ Not started | M (~1d) | devkitpro `Switch.cmake` toolchain | +| 3 | Directory restructure | ✅ Done | M (~1d) | `src/` + `include/nxst/` layered tree | +| 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain | | 5 | TransferService extraction | ☐ Not started | L (~2d) | kill globals, sever UI ↔ net coupling | | 6 | `Result` + 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 | | 8 | CI | ☐ Not started | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check | -**Active phase:** Phase 3 — Directory restructure. +**Active phase:** Phase 5 — TransferService extraction. **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. diff --git a/include/client.hpp b/include/client.hpp deleted file mode 100644 index 582b8a6..0000000 --- a/include/client.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include - -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(); diff --git a/include/main.hpp b/include/nxst/app/main.hpp similarity index 79% rename from include/main.hpp rename to include/nxst/app/main.hpp index cdff2fc..44d004e 100644 --- a/include/main.hpp +++ b/include/nxst/app/main.hpp @@ -1,16 +1,15 @@ #pragma once -#include -#include "account.hpp" -#include "title.hpp" -#include "util.hpp" +#include +#include +#include +#include #include #include -#include "logger.hpp" +#include typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t; inline float g_currentTime = 0; -inline AccountUid g_currentUId; inline bool g_backupScrollEnabled = 0; inline bool g_notificationLedAvailable = false; inline bool g_shouldExitNetworkLoop = false; diff --git a/include/main_application.hpp b/include/nxst/app/main_application.hpp similarity index 50% rename from include/main_application.hpp rename to include/nxst/app/main_application.hpp index 342e2a2..77a0a7c 100644 --- a/include/main_application.hpp +++ b/include/nxst/app/main_application.hpp @@ -1,21 +1,22 @@ -#pragma once - -#include -#include -#include - -namespace ui { - - class MainApplication : public pu::ui::Application { - - public: - using Application::Application; - PU_SMART_CTOR(MainApplication) - - void OnLoad() override; - - // Layout instance - UsersLayout::Ref usersLayout; - TitlesLayout::Ref titlesLayout; - }; +#pragma once + +#include +#include +#include +#include + +namespace ui { + + class MainApplication : public pu::ui::Application { + + public: + using Application::Application; + PU_SMART_CTOR(MainApplication) + + void OnLoad() override; + + UsersLayout::Ref users_layout; + TitlesLayout::Ref titles_layout; + nxst::TransferService transfer; + }; } \ No newline at end of file diff --git a/include/account.hpp b/include/nxst/domain/account.hpp similarity index 100% rename from include/account.hpp rename to include/nxst/domain/account.hpp diff --git a/include/common.hpp b/include/nxst/domain/common.hpp similarity index 100% rename from include/common.hpp rename to include/nxst/domain/common.hpp diff --git a/include/protocol.hpp b/include/nxst/domain/protocol.hpp similarity index 100% rename from include/protocol.hpp rename to include/nxst/domain/protocol.hpp diff --git a/include/title.hpp b/include/nxst/domain/title.hpp similarity index 96% rename from include/title.hpp rename to include/nxst/domain/title.hpp index acbb900..b257872 100644 --- a/include/title.hpp +++ b/include/nxst/domain/title.hpp @@ -25,9 +25,9 @@ */ #pragma once -#include "account.hpp" -#include "filesystem.hpp" -#include "io.hpp" +#include +#include +#include #include #include #include diff --git a/include/transfer_state.hpp b/include/nxst/domain/transfer_state.hpp similarity index 100% rename from include/transfer_state.hpp rename to include/nxst/domain/transfer_state.hpp diff --git a/include/util.hpp b/include/nxst/domain/util.hpp similarity index 94% rename from include/util.hpp rename to include/nxst/domain/util.hpp index b2b865d..89db34c 100644 --- a/include/util.hpp +++ b/include/nxst/domain/util.hpp @@ -25,9 +25,9 @@ */ #pragma once -#include "account.hpp" -#include "common.hpp" -#include "io.hpp" +#include +#include +#include #include #include diff --git a/include/directory.hpp b/include/nxst/infra/fs/directory.hpp similarity index 100% rename from include/directory.hpp rename to include/nxst/infra/fs/directory.hpp diff --git a/include/filesystem.hpp b/include/nxst/infra/fs/filesystem.hpp similarity index 97% rename from include/filesystem.hpp rename to include/nxst/infra/fs/filesystem.hpp index 6b93c4c..0b17f8d 100644 --- a/include/filesystem.hpp +++ b/include/nxst/infra/fs/filesystem.hpp @@ -25,7 +25,7 @@ */ #pragma once -#include "account.hpp" +#include #include namespace FileSystem { diff --git a/include/io.hpp b/include/nxst/infra/fs/io.hpp similarity index 93% rename from include/io.hpp rename to include/nxst/infra/fs/io.hpp index 37f95c6..4d542b7 100644 --- a/include/io.hpp +++ b/include/nxst/infra/fs/io.hpp @@ -25,10 +25,10 @@ */ #pragma once -#include "account.hpp" -#include "directory.hpp" -#include "title.hpp" -#include "util.hpp" +#include +#include +#include +#include #include #include #include diff --git a/include/net/socket.hpp b/include/nxst/infra/net/socket.hpp similarity index 100% rename from include/net/socket.hpp rename to include/nxst/infra/net/socket.hpp diff --git a/include/nxst/infra/net/transfer_receiver.hpp b/include/nxst/infra/net/transfer_receiver.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/include/nxst/infra/net/transfer_receiver.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/include/nxst/infra/net/transfer_sender.hpp b/include/nxst/infra/net/transfer_sender.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/include/nxst/infra/net/transfer_sender.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/include/logger.hpp b/include/nxst/infra/sys/logger.hpp similarity index 100% rename from include/logger.hpp rename to include/nxst/infra/sys/logger.hpp diff --git a/include/nxst/service/transfer_service.hpp b/include/nxst/service/transfer_service.hpp new file mode 100644 index 0000000..677b206 --- /dev/null +++ b/include/nxst/service/transfer_service.hpp @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include +#include +#include + +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 sender_udp_sock{-1}; + std::atomic sender_tcp_sock{-1}; + std::atomic sender_active{false}; + + // Receiver + TransferState receiver_state; + std::atomic receiver_client_sock{-1}; + std::atomic receiver_listen_sock{-1}; + std::atomic receiver_bcast_sock{-1}; + std::atomic receiver_accept_active{false}; + std::atomic 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 diff --git a/include/ui/card.hpp b/include/nxst/ui/card.hpp similarity index 93% rename from include/ui/card.hpp rename to include/nxst/ui/card.hpp index 5861a32..ddc9517 100644 --- a/include/ui/card.hpp +++ b/include/nxst/ui/card.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include namespace ui { diff --git a/include/const.h b/include/nxst/ui/const.h similarity index 78% rename from include/const.h rename to include/nxst/ui/const.h index 935f045..6d9908f 100644 --- a/include/const.h +++ b/include/nxst/ui/const.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #define COLOR(hex) pu::ui::Color::FromHex(hex) #define BACKGROUND_COLOR theme::color::BgBase diff --git a/include/ui/header_bar.hpp b/include/nxst/ui/header_bar.hpp similarity index 96% rename from include/ui/header_bar.hpp rename to include/nxst/ui/header_bar.hpp index 54f5c2f..fbb19ae 100644 --- a/include/ui/header_bar.hpp +++ b/include/nxst/ui/header_bar.hpp @@ -1,8 +1,8 @@ #pragma once #include -#include -#include -#include +#include +#include +#include namespace ui { diff --git a/include/ui/hint_bar.hpp b/include/nxst/ui/hint_bar.hpp similarity index 98% rename from include/ui/hint_bar.hpp rename to include/nxst/ui/hint_bar.hpp index f67769c..0cb5ab5 100644 --- a/include/ui/hint_bar.hpp +++ b/include/nxst/ui/hint_bar.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include diff --git a/include/theme.hpp b/include/nxst/ui/theme.hpp similarity index 100% rename from include/theme.hpp rename to include/nxst/ui/theme.hpp diff --git a/include/titles_layout.hpp b/include/nxst/ui/titles_layout.hpp similarity index 86% rename from include/titles_layout.hpp rename to include/nxst/ui/titles_layout.hpp index 1903589..15a220e 100644 --- a/include/titles_layout.hpp +++ b/include/nxst/ui/titles_layout.hpp @@ -1,12 +1,13 @@ +#pragma once #include -#include -#include -#include +#include +#include +#include #include #include #include -#include -#include +#include +#include namespace ui { @@ -33,6 +34,7 @@ namespace ui { pu::ui::elm::TextBlock::Ref emptyText; pu::ui::elm::TextBlock::Ref emptySub; + AccountUid current_uid{}; TitlesFocus focus = TitlesFocus::List; TitlesAction action = TitlesAction::Transfer; int lockedListIndex = 0; @@ -46,7 +48,7 @@ namespace ui { public: TitlesLayout(); - void InitTitles(); + void InitTitles(AccountUid uid); void LockInput() { m_inputLocked = true; } void UnlockInput() { m_inputLocked = false; } diff --git a/include/transfer_overlay.hpp b/include/nxst/ui/transfer_overlay.hpp similarity index 98% rename from include/transfer_overlay.hpp rename to include/nxst/ui/transfer_overlay.hpp index ddfe16b..a7b02b5 100644 --- a/include/transfer_overlay.hpp +++ b/include/nxst/ui/transfer_overlay.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include -#include +#include +#include namespace ui { diff --git a/include/ui/ui_context.hpp b/include/nxst/ui/ui_context.hpp similarity index 85% rename from include/ui/ui_context.hpp rename to include/nxst/ui/ui_context.hpp index 3d444e8..8f1b6d7 100644 --- a/include/ui/ui_context.hpp +++ b/include/nxst/ui/ui_context.hpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include namespace ui { struct UiContext { diff --git a/include/users_layout.hpp b/include/nxst/ui/users_layout.hpp similarity index 85% rename from include/users_layout.hpp rename to include/nxst/ui/users_layout.hpp index 2529ca8..ca46b94 100644 --- a/include/users_layout.hpp +++ b/include/nxst/ui/users_layout.hpp @@ -1,7 +1,7 @@ #include -#include -#include -#include +#include +#include +#include #include namespace ui { diff --git a/include/server.hpp b/include/server.hpp deleted file mode 100644 index 5e917e0..0000000 --- a/include/server.hpp +++ /dev/null @@ -1,8 +0,0 @@ -#include -int startSendingThread(); -bool isServerTransferDone(); -bool isServerTransferCancelled(); -bool isServerWorkersIdle(); -void cancelServerTransfer(); -double getServerProgress(); -std::string getServerStatusText(); \ No newline at end of file diff --git a/source/client.cpp b/source/client.cpp deleted file mode 100644 index f84577c..0000000 --- a/source/client.cpp +++ /dev/null @@ -1,249 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __SWITCH__ -#include -#include -#include -#endif - -#include -#include - -namespace fs = std::filesystem; -using path = fs::path; - -static TransferState g_client_state; -static std::atomic g_client_udp_sock{-1}; -static std::atomic g_client_tcp_sock{-1}; -static std::atomic 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(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 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(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; -} diff --git a/source/main_application.cpp b/source/main_application.cpp deleted file mode 100644 index d9e93bc..0000000 --- a/source/main_application.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include -#include -#include -#include -#include - -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); - } -} \ No newline at end of file diff --git a/source/server.cpp b/source/server.cpp deleted file mode 100644 index 9bc3a7b..0000000 --- a/source/server.cpp +++ /dev/null @@ -1,345 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __SWITCH__ -#include -#include -#include -#endif - -#include -#include -#include - -static TransferState g_server_state; -static std::atomic g_server_client_sock{-1}; -static std::atomic g_server_listen_sock{-1}; -static std::atomic g_broadcast_sock{-1}; -static std::atomic g_accept_thread_active{false}; -static std::atomic 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(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 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 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(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 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(arg)->server_fd; - delete static_cast(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 diff --git a/source/main.cpp b/src/app/main.cpp similarity index 78% rename from source/main.cpp rename to src/app/main.cpp index 9eb33dd..89ac179 100644 --- a/source/main.cpp +++ b/src/app/main.cpp @@ -1,78 +1,82 @@ -#include -#include "util.hpp" -#include "main.hpp" -#include -#include -#include - -static int nxlink_sock = -1; - -extern "C" void userAppInit() { - appletInitialize(); - hidInitialize(); - nsInitialize(); - setsysInitialize(); - setInitialize(); - accountInitialize(AccountServiceType_Administrator); - pmshellInitialize(); - socketInitializeDefault(); - pdmqryInitialize(); - nxlink_sock = nxlinkStdio(); - printf("userAppInit\n"); -} - -extern "C" void userAppExit() { - cancelServerTransfer(); - cancelClientTransfer(); - for (int i = 0; i < 150 && (!isServerWorkersIdle() || !isClientWorkersIdle()); i++) { - usleep(10000); - } - if (nxlink_sock != -1) { - close(nxlink_sock); - } - appletExit(); - hidExit(); - nsExit(); - setsysExit(); - setExit(); - accountExit(); - pmshellExit(); - socketExit(); - pdmqryExit(); -} - -// Main entrypoint -int main() { - Result res = servicesInit(); - if (R_FAILED(res)) { - servicesExit(); - exit(res); - } - - printf("main"); - - // First create our renderer, where one can customize SDL or other stuff's - // initialization. - auto renderer_opts = pu::ui::render::RendererInitOptions( - SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags); - renderer_opts.UseImage(pu::ui::render::IMGAllFlags); - renderer_opts.UseAudio(pu::ui::render::MixerAllFlags); - renderer_opts.UseTTF(); - renderer_opts.SetExtraDefaultFontSize(theme::type::Caption); - renderer_opts.SetExtraDefaultFontSize(theme::type::Label); - renderer_opts.SetExtraDefaultFontSize(theme::type::Body); - renderer_opts.SetExtraDefaultFontSize(theme::type::Title); - renderer_opts.SetExtraDefaultFontSize(theme::type::Display); - - auto renderer = pu::ui::render::Renderer::New(renderer_opts); - - // Create our main application from the renderer - auto main = ui::MainApplication::New(renderer); - - main->Prepare(); - - main->Show(); - - servicesExit(); - return 0; -} +#include +#include +#include +#include + +namespace ui { extern MainApplication* mainApp; } + +static int nxlink_sock = -1; + +extern "C" void userAppInit() { + appletInitialize(); + hidInitialize(); + nsInitialize(); + setsysInitialize(); + setInitialize(); + accountInitialize(AccountServiceType_Administrator); + pmshellInitialize(); + socketInitializeDefault(); + pdmqryInitialize(); + nxlink_sock = nxlinkStdio(); + printf("userAppInit\n"); +} + +extern "C" void userAppExit() { + if (ui::mainApp) { + ui::mainApp->transfer.cancelReceive(); + ui::mainApp->transfer.cancelSend(); + for (int i = 0; i < 150 && + (!ui::mainApp->transfer.isReceiveWorkersIdle() || + !ui::mainApp->transfer.isSendWorkersIdle()); i++) { + usleep(10000); + } + } + if (nxlink_sock != -1) { + close(nxlink_sock); + } + appletExit(); + hidExit(); + nsExit(); + setsysExit(); + setExit(); + accountExit(); + pmshellExit(); + socketExit(); + pdmqryExit(); +} + +// Main entrypoint +int main() { + Result res = servicesInit(); + if (R_FAILED(res)) { + servicesExit(); + exit(res); + } + + printf("main"); + + // First create our renderer, where one can customize SDL or other stuff's + // initialization. + auto renderer_opts = pu::ui::render::RendererInitOptions( + SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags); + renderer_opts.UseImage(pu::ui::render::IMGAllFlags); + renderer_opts.UseAudio(pu::ui::render::MixerAllFlags); + renderer_opts.UseTTF(); + renderer_opts.SetExtraDefaultFontSize(theme::type::Caption); + renderer_opts.SetExtraDefaultFontSize(theme::type::Label); + renderer_opts.SetExtraDefaultFontSize(theme::type::Body); + renderer_opts.SetExtraDefaultFontSize(theme::type::Title); + renderer_opts.SetExtraDefaultFontSize(theme::type::Display); + + auto renderer = pu::ui::render::Renderer::New(renderer_opts); + + // Create our main application from the renderer + auto main = ui::MainApplication::New(renderer); + + main->Prepare(); + + main->Show(); + + servicesExit(); + return 0; +} diff --git a/src/app/main_application.cpp b/src/app/main_application.cpp new file mode 100644 index 0000000..a80be72 --- /dev/null +++ b/src/app/main_application.cpp @@ -0,0 +1,21 @@ +#include +#include +#include +#include +#include + +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); + } +} \ No newline at end of file diff --git a/source/account.cpp b/src/domain/account.cpp similarity index 98% rename from source/account.cpp rename to src/domain/account.cpp index 12d3967..e92e37f 100644 --- a/source/account.cpp +++ b/src/domain/account.cpp @@ -24,8 +24,7 @@ * reasonable ways as different from the original version. */ -#include "account.hpp" -#include +#include #include #include @@ -138,5 +137,5 @@ AccountUid Account::selectAccount(void) return uid; } - return g_currentUId; + return AccountUid{}; } \ No newline at end of file diff --git a/source/common.cpp b/src/domain/common.cpp similarity index 99% rename from source/common.cpp rename to src/domain/common.cpp index dbd2cea..e5ddf47 100644 --- a/source/common.cpp +++ b/src/domain/common.cpp @@ -24,7 +24,7 @@ * reasonable ways as different from the original version. */ -#include "common.hpp" +#include std::string DateTime::timeStr(void) { diff --git a/source/title.cpp b/src/domain/title.cpp similarity index 99% rename from source/title.cpp rename to src/domain/title.cpp index 190cb1c..b48139f 100644 --- a/source/title.cpp +++ b/src/domain/title.cpp @@ -24,8 +24,8 @@ * reasonable ways as different from the original version. */ -#include "title.hpp" -#include "main.hpp" +#include +#include static std::unordered_map> titles; static bool s_titlesLoaded = false; diff --git a/source/util.cpp b/src/domain/util.cpp similarity index 97% rename from source/util.cpp rename to src/domain/util.cpp index 79b1f8b..c4e6b8d 100644 --- a/source/util.cpp +++ b/src/domain/util.cpp @@ -24,10 +24,10 @@ * reasonable ways as different from the original version. */ -#include "util.hpp" -#include -#include -#include "main.hpp" +#include +#include +#include +#include void servicesExit(void) { diff --git a/source/directory.cpp b/src/infra/fs/directory.cpp similarity index 98% rename from source/directory.cpp rename to src/infra/fs/directory.cpp index b6acbe3..d97610b 100644 --- a/source/directory.cpp +++ b/src/infra/fs/directory.cpp @@ -24,7 +24,7 @@ * reasonable ways as different from the original version. */ -#include "directory.hpp" +#include Directory::Directory(const std::string& root) { diff --git a/source/filesystem.cpp b/src/infra/fs/filesystem.cpp similarity index 97% rename from source/filesystem.cpp rename to src/infra/fs/filesystem.cpp index 7df86a1..28c991e 100644 --- a/source/filesystem.cpp +++ b/src/infra/fs/filesystem.cpp @@ -24,7 +24,7 @@ * reasonable ways as different from the original version. */ -#include "filesystem.hpp" +#include Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) { diff --git a/source/io.cpp b/src/infra/fs/io.cpp similarity index 99% rename from source/io.cpp rename to src/infra/fs/io.cpp index d303384..d792c61 100644 --- a/source/io.cpp +++ b/src/infra/fs/io.cpp @@ -24,9 +24,9 @@ * reasonable ways as different from the original version. */ -#include "io.hpp" -#include "main.hpp" -#include +#include +#include +#include bool io::fileExists(const std::string& path) { diff --git a/src/infra/net/transfer_receiver.cpp b/src/infra/net/transfer_receiver.cpp new file mode 100644 index 0000000..bfd75ba --- /dev/null +++ b/src/infra/net/transfer_receiver.cpp @@ -0,0 +1 @@ +// Logic moved to src/service/transfer_service.cpp diff --git a/src/infra/net/transfer_sender.cpp b/src/infra/net/transfer_sender.cpp new file mode 100644 index 0000000..bfd75ba --- /dev/null +++ b/src/infra/net/transfer_sender.cpp @@ -0,0 +1 @@ +// Logic moved to src/service/transfer_service.cpp diff --git a/source/logger.cpp b/src/infra/sys/logger.cpp similarity index 97% rename from source/logger.cpp rename to src/infra/sys/logger.cpp index 10c5bca..3837579 100644 --- a/source/logger.cpp +++ b/src/infra/sys/logger.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/service/transfer_service.cpp b/src/service/transfer_service.cpp new file mode 100644 index 0000000..2c59c8c --- /dev/null +++ b/src/service/transfer_service.cpp @@ -0,0 +1,483 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __SWITCH__ +#include +#include +#include +#include +#endif + +#include +#include + +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(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(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 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 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 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(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(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(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 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 diff --git a/source/titles_layout.cpp b/src/ui/titles_layout.cpp similarity index 85% rename from source/titles_layout.cpp rename to src/ui/titles_layout.cpp index 68e5d55..8b98a78 100644 --- a/source/titles_layout.cpp +++ b/src/ui/titles_layout.cpp @@ -1,10 +1,7 @@ -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include namespace ui { extern MainApplication *mainApp; @@ -98,24 +95,24 @@ namespace ui { this->updateHints(); } - void TitlesLayout::InitTitles() { + void TitlesLayout::InitTitles(AccountUid uid) { 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* items; if (it != this->menuCache.end()) { items = &it->second; } else { std::vector built; - for (size_t i = 0; i < getTitleCount(g_currentUId); i++) { + for (size_t i = 0; i < getTitleCount(uid); i++) { Title title; - getTitle(title, g_currentUId, i); + getTitle(title, uid, i); auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); titleItem->SetColor(color::TextPrimary); 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; } @@ -144,14 +141,14 @@ namespace ui { this->refreshButtons(); this->updateHints(); - this->header->SetUser(g_currentUId, Account::username(g_currentUId)); + this->header->SetUser(uid, Account::username(uid)); } void TitlesLayout::refreshPanel() { if (this->titlesMenu->GetItems().empty()) return; int idx = this->titlesMenu->GetSelectedIndex(); Title title; - getTitle(title, g_currentUId, idx); + getTitle(title, this->current_uid, idx); this->panelTitle->SetText(StringUtils::elide(title.name(), 24)); } @@ -185,24 +182,25 @@ namespace ui { } void TitlesLayout::runTransfer(int index, Title& title) { + (void)title; auto ovl = TransferOverlay::New("Transferring save data..."); this->titlesMenu->SetVisible(false); mainApp->StartOverlay(ovl); this->LockInput(); - if (transfer_files(index, g_currentUId) != 0) { + if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) { mainApp->EndOverlay(); this->titlesMenu->SetVisible(true); this->UnlockInput(); mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); return; } - while (!isClientTransferDone()) { - ovl->SetStatus(getClientStatusText()); - ovl->SetProgressVisible(isClientProgressKnown()); - ovl->SetProgress(getClientProgress()); + while (!mainApp->transfer.isSendDone()) { + ovl->SetStatus(mainApp->transfer.sendStatusText()); + ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown()); + ovl->SetProgress(mainApp->transfer.sendProgress()); mainApp->CallForRender(); if (mainApp->GetButtonsDown() & HidNpadButton_B) { - cancelClientTransfer(); + mainApp->transfer.cancelSend(); } svcSleepThread(16666666LL); } @@ -210,9 +208,9 @@ namespace ui { this->titlesMenu->SetVisible(true); this->UnlockInput(); - if (isClientConnectionFailed()) { - mainApp->CreateShowDialog("Transfer", getClientFailReason(), {"OK"}, true); - } else if (isClientTransferCancelled()) { + if (mainApp->transfer.isSendConnectionFailed()) { + mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true); + } else if (mainApp->transfer.isSendCancelled()) { mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); } else { mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); @@ -220,7 +218,7 @@ namespace ui { } 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); return; } @@ -228,12 +226,12 @@ namespace ui { this->titlesMenu->SetVisible(false); mainApp->StartOverlay(ovl); this->LockInput(); - while (!isServerTransferDone()) { - ovl->SetStatus(getServerStatusText()); - ovl->SetProgress(getServerProgress()); + while (!mainApp->transfer.isReceiveDone()) { + ovl->SetStatus(mainApp->transfer.receiveStatusText()); + ovl->SetProgress(mainApp->transfer.receiveProgress()); mainApp->CallForRender(); if (mainApp->GetButtonsDown() & HidNpadButton_B) { - cancelServerTransfer(); + mainApp->transfer.cancelReceive(); } svcSleepThread(16666666LL); } @@ -241,24 +239,22 @@ namespace ui { this->titlesMenu->SetVisible(true); this->UnlockInput(); - if (isServerTransferCancelled()) { + if (mainApp->transfer.isReceiveCancelled()) { mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); - return; - } - auto restoreResult = io::restore(index, g_currentUId, 0, title.name()); - if (std::get<0>(restoreResult)) { + } else if (mainApp->transfer.restoreSucceeded()) { mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); } 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)Up; (void)Held; (void)Pos; if (m_inputLocked) return; if (Down & HidNpadButton_Plus) { - cancelClientTransfer(); - cancelServerTransfer(); + mainApp->transfer.cancelSend(); + mainApp->transfer.cancelReceive(); mainApp->Close(); return; } @@ -266,7 +262,7 @@ namespace ui { if (focus == TitlesFocus::List) { if (Down & HidNpadButton_B) { this->header->SetUser(std::nullopt, ""); - mainApp->LoadLayout(mainApp->usersLayout); + mainApp->LoadLayout(mainApp->users_layout); return; } if (Down & HidNpadButton_A) { @@ -298,7 +294,7 @@ namespace ui { if (Down & HidNpadButton_A) { int idx = this->titlesMenu->GetSelectedIndex(); Title title; - getTitle(title, g_currentUId, idx); + getTitle(title, this->current_uid, idx); TitlesAction chosen = action; this->focus = TitlesFocus::List; this->refreshButtons(); diff --git a/source/users_layout.cpp b/src/ui/users_layout.cpp similarity index 88% rename from source/users_layout.cpp rename to src/ui/users_layout.cpp index 96f0d26..c290d2d 100644 --- a/source/users_layout.cpp +++ b/src/ui/users_layout.cpp @@ -1,6 +1,4 @@ -#include -#include -#include "main.hpp" +#include namespace ui { extern MainApplication *mainApp; @@ -50,11 +48,12 @@ namespace ui { void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { if (Down & HidNpadButton_Plus) { - svcExitProcess(); + mainApp->Close(); + return; } if (Down & HidNpadButton_A) { - g_currentUId = Account::ids().at(this->usersMenu->GetSelectedIndex()); + AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex()); if (!areTitlesLoaded()) { this->usersMenu->SetVisible(false); @@ -69,8 +68,8 @@ namespace ui { this->usersMenu->SetVisible(true); } - mainApp->titlesLayout->InitTitles(); - mainApp->LoadLayout(mainApp->titlesLayout); + mainApp->titles_layout->InitTitles(uid); + mainApp->LoadLayout(mainApp->titles_layout); } } } -- 2.39.2 From dc65a4c8a95a6e3e7816e0f408ef1437d35ac7fd Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov Date: Mon, 27 Apr 2026 01:27:16 +0300 Subject: [PATCH 03/10] =?UTF-8?q?refactor:=20phases=205=20&=206=20?= =?UTF-8?q?=E2=80=94=20TransferService,=20Result,=20RAII?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 — TransferService extraction: - New nxst::TransferService owns all sender/receiver state, threads, and atomics (previously 11 file-statics across transfer_sender.cpp + transfer_receiver.cpp) - startReceive() calls io::restore internally; UI never touches infra/fs or infra/net - TitlesLayout routes all transfer ops through mainApp->transfer - g_currentUId removed from global scope; TitlesLayout::InitTitles(AccountUid) - MainApplication gains transfer member; layout refs renamed to snake_case - userAppExit() updated to cancel via service Phase 6 — Result + RAII: - include/nxst/domain/result.hpp: 85-line tagged-union Result + Result - include/nxst/infra/fs/handles.hpp: FsFileSystemHandle (auto fsFsClose), FileHandle (auto fclose) — eliminates manual close on every error path - io::backup / io::restore return nxst::Result (was tuple) - new u8[] + malloc in copyFile/restore replaced with std::vector - NACP save-creation extracted to createSaveIfNeeded() helper in io.cpp Co-Authored-By: Claude Sonnet 4.6 --- PLAN.md | 8 +- include/nxst/domain/result.hpp | 85 ++++++++++ include/nxst/infra/fs/handles.hpp | 37 ++++ include/nxst/infra/fs/io.hpp | 13 +- src/infra/fs/io.cpp | 269 +++++++++++++++--------------- src/service/transfer_service.cpp | 10 +- 6 files changed, 270 insertions(+), 152 deletions(-) create mode 100644 include/nxst/domain/result.hpp create mode 100644 include/nxst/infra/fs/handles.hpp diff --git a/PLAN.md b/PLAN.md index 80f5349..5a02885 100644 --- a/PLAN.md +++ b/PLAN.md @@ -9,13 +9,13 @@ | 2 | File renames + `#pragma once` | ✅ Done | S (~2h) | snake_case filenames, unify guards | | 3 | Directory restructure | ✅ Done | M (~1d) | `src/` + `include/nxst/` layered tree | | 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain | -| 5 | TransferService extraction | ☐ Not started | L (~2d) | kill globals, sever UI ↔ net coupling | -| 6 | `Result` + RAII | ☐ Not started | M (~1d) | tagged union, OS handle wrappers, split `restore()` | +| 5 | TransferService extraction | ✅ Done | L (~2d) | kill globals, sever UI ↔ net coupling | +| 6 | `Result` + RAII | ✅ Done | M (~1d) | tagged union, OS handle wrappers, fix raw memory | | 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 | -**Active phase:** Phase 5 — TransferService extraction. -**Last updated:** 2026-04-26. +**Active phase:** Phase 7 — Documentation + license. +**Last updated:** 2026-04-27. Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth. diff --git a/include/nxst/domain/result.hpp b/include/nxst/domain/result.hpp new file mode 100644 index 0000000..f3d4956 --- /dev/null +++ b/include/nxst/domain/result.hpp @@ -0,0 +1,85 @@ +#pragma once +#include +#include + +namespace nxst { + +template +class Result { + bool ok; + alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)]; + + Result() = default; + +public: + static Result success(T val) { + Result res; + res.ok = true; + new (res.storage) T(std::move(val)); + return res; + } + + static Result failure(E err) { + Result res; + res.ok = false; + new (res.storage) E(std::move(err)); + return res; + } + + ~Result() { + if (ok) reinterpret_cast(storage)->~T(); + else reinterpret_cast(storage)->~E(); + } + + Result(const Result& other) : ok(other.ok) { + if (ok) new (storage) T(*reinterpret_cast(other.storage)); + else new (storage) E(*reinterpret_cast(other.storage)); + } + + Result(Result&& other) : ok(other.ok) { + if (ok) new (storage) T(std::move(*reinterpret_cast(other.storage))); + else new (storage) E(std::move(*reinterpret_cast(other.storage))); + } + + Result& operator=(const Result&) = delete; + + bool isOk() const noexcept { return ok; } + const T& value() const { return *reinterpret_cast(storage); } + const E& error() const { return *reinterpret_cast(storage); } +}; + +// Specialisation for Result +template +class Result { + bool ok; + alignas(E) unsigned char storage[sizeof(E)]; + + Result() = default; + +public: + static Result success() { Result res; res.ok = true; return res; } + + static Result failure(E err) { + Result res; + res.ok = false; + new (res.storage) E(std::move(err)); + return res; + } + + ~Result() { if (!ok) reinterpret_cast(storage)->~E(); } + + Result(const Result& other) : ok(other.ok) { + if (!ok) new (storage) E(*reinterpret_cast(other.storage)); + } + + Result(Result&& other) : ok(other.ok) { + if (!ok) new (storage) E(std::move(*reinterpret_cast(other.storage))); + } + + Result& operator=(const Result&) = delete; + + bool isOk() const noexcept { return ok; } + const E& error() const { return *reinterpret_cast(storage); } +}; + +} // namespace nxst diff --git a/include/nxst/infra/fs/handles.hpp b/include/nxst/infra/fs/handles.hpp new file mode 100644 index 0000000..be99c43 --- /dev/null +++ b/include/nxst/infra/fs/handles.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include + +namespace nxst { + +// RAII wrapper for FsFileSystem — auto-closes on destruction. +struct FsFileSystemHandle { + FsFileSystem fs{}; + bool valid{false}; + + FsFileSystemHandle() = default; + ~FsFileSystemHandle() { if (valid) fsFsClose(&fs); } // NOLINT(modernize-use-equals-default) + + FsFileSystemHandle(const FsFileSystemHandle&) = delete; + FsFileSystemHandle& operator=(const FsFileSystemHandle&) = delete; + + FsFileSystem* get() { return &fs; } + + void release() { valid = false; } // transfer ownership to devfs +}; + +// RAII wrapper for FILE* — auto-fclose on destruction. +struct FileHandle { + FILE* ptr{nullptr}; + + explicit FileHandle(FILE* file) : ptr(file) {} + ~FileHandle() { if (ptr != nullptr) fclose(ptr); } // NOLINT(modernize-use-equals-default) + + FileHandle(const FileHandle&) = delete; + FileHandle& operator=(const FileHandle&) = delete; + + explicit operator bool() const { return ptr != nullptr; } + FILE* get() const { return ptr; } +}; + +} // namespace nxst diff --git a/include/nxst/infra/fs/io.hpp b/include/nxst/infra/fs/io.hpp index 4d542b7..6ecea44 100644 --- a/include/nxst/infra/fs/io.hpp +++ b/include/nxst/infra/fs/io.hpp @@ -26,27 +26,26 @@ #pragma once #include +#include #include #include #include #include #include #include -#include #include -#include #define BUFFER_SIZE 0x80000 namespace io { - std::tuple backup(size_t index, AccountUid uid); - std::tuple restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell); + nxst::Result backup(size_t index, AccountUid uid); + nxst::Result restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell); Result copyDirectory(const std::string& srcPath, const std::string& dstPath); - void copyFile(const std::string& srcPath, const std::string& dstPath); + void copyFile(const std::string& srcPath, const std::string& dstPath); Result createDirectory(const std::string& path); Result deleteFolderRecursively(const std::string& path); - bool directoryExists(const std::string& path); - bool fileExists(const std::string& path); + bool directoryExists(const std::string& path); + bool fileExists(const std::string& path); } diff --git a/src/infra/fs/io.cpp b/src/infra/fs/io.cpp index d792c61..c038256 100644 --- a/src/infra/fs/io.cpp +++ b/src/infra/fs/io.cpp @@ -25,8 +25,10 @@ */ #include +#include #include #include +#include bool io::fileExists(const std::string& path) { @@ -38,42 +40,43 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath) { g_isTransferringFile = true; - FILE* src = fopen(srcPath.c_str(), "rb"); - if (src == NULL) { - Logger::getInstance().log(Logger::ERROR, "Failed to open source file " + srcPath + " during copy with errno %d. Skipping...", errno); + nxst::FileHandle src(fopen(srcPath.c_str(), "rb")); + if (!src) { + nxst::log::error("Failed to open source file %s during copy with errno %d. Skipping...", + srcPath.c_str(), errno); + g_isTransferringFile = false; return; } - FILE* dst = fopen(dstPath.c_str(), "wb"); - if (dst == NULL) { - Logger::getInstance().log(Logger::ERROR, "Failed to open destination file " + dstPath + " during copy with errno " + std::to_string(errno) + ". Skipping..."); - fclose(src); + nxst::FileHandle dst(fopen(dstPath.c_str(), "wb")); + if (!dst) { + nxst::log::error("Failed to open destination file %s during copy with errno %d. Skipping...", + dstPath.c_str(), errno); + g_isTransferringFile = false; return; } - fseek(src, 0, SEEK_END); - u64 sz = ftell(src); - rewind(src); + fseek(src.get(), 0, SEEK_END); + u64 sz = (u64)ftell(src.get()); + rewind(src.get()); - u8* buf = new u8[BUFFER_SIZE]; + std::vector buf(BUFFER_SIZE); u64 offset = 0; - size_t slashpos = srcPath.rfind("/"); + size_t slashpos = srcPath.rfind('/'); g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1); while (offset < sz) { - u32 count = fread((char*)buf, 1, BUFFER_SIZE, src); + u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get()); if (count == 0) { - Logger::getInstance().log(Logger::ERROR, "fread returned 0 for file {} at offset {}/{} with errno {}. Aborting copy.", srcPath, offset, sz, errno); + nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.", + srcPath.c_str(), (unsigned long long)offset, + (unsigned long long)sz, errno); break; } - fwrite((char*)buf, 1, count, dst); + fwrite(buf.data(), 1, count, dst.get()); offset += count; } - delete[] buf; - fclose(src); - fclose(dst); - if (dstPath.rfind("save:/", 0) == 0) { fsdevCommitDevice("save"); } @@ -150,154 +153,150 @@ Result io::deleteFolderRecursively(const std::string& path) return 0; } -std::tuple io::backup(size_t index, AccountUid uid) +nxst::Result io::backup(size_t index, AccountUid uid) { - Result res = 0; - std::tuple ret = std::make_tuple(false, -1, ""); Title title; getTitle(title, uid, index); - Logger::getInstance().log(Logger::INFO, "Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(), - title.userId().uid[1], title.userId().uid[0]); + nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", + title.name().c_str(), title.id(), + title.userId().uid[1], title.userId().uid[0]); - FsFileSystem fileSystem; - res = FileSystem::mount(&fileSystem, title.id(), title.userId()); - if (R_SUCCEEDED(res)) { - int rc = FileSystem::mount(fileSystem); - if (rc == -1) { - fsFsClose(&fileSystem); - FileSystem::unmount(); - Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during backup. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(), - title.userId().uid[1], title.userId().uid[0]); - return std::make_tuple(false, -2, "Failed to mount save."); - } + nxst::FsFileSystemHandle fsHandle; + Result res = FileSystem::mount(fsHandle.get(), title.id(), title.userId()); + if (R_FAILED(res)) { + nxst::log::error("Failed to mount filesystem during backup with result 0x%08lX. " + "Title id: 0x%016lX; User id: 0x%lX%lX.", + res, title.id(), title.userId().uid[1], title.userId().uid[0]); + return nxst::Result::failure("Failed to mount save."); } - else { - Logger::getInstance().log(Logger::ERROR, - "Failed to mount filesystem during backup with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(), - title.userId().uid[1], title.userId().uid[0]); - return std::make_tuple(false, res, "Failed to mount save."); + fsHandle.valid = true; + + if (FileSystem::mount(*fsHandle.get()) == -1) { + nxst::log::error("Failed to mount devfs during backup. Title id: 0x%016lX; User id: 0x%lX%lX.", + title.id(), title.userId().uid[1], title.userId().uid[0]); + FileSystem::unmount(); + return nxst::Result::failure("Failed to mount save."); } + fsHandle.release(); // devfs now owns the kernel handle std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId()))); io::createDirectory(title.path()); - std::string dstPath = title.path() + "/" + suggestion; + std::string dst_path = title.path() + "/" + suggestion; + std::string tmp_path = dst_path + ".tmp"; - // Write to a temp dir first; rename on success so the existing backup - // is never destroyed if the copy is interrupted mid-way. - std::string tmpPath = dstPath + ".tmp"; - if (io::directoryExists(tmpPath)) { - io::deleteFolderRecursively((tmpPath + "/").c_str()); + if (io::directoryExists(tmp_path)) { + io::deleteFolderRecursively(tmp_path + "/"); } - io::createDirectory(tmpPath); - res = io::copyDirectory("save:/", tmpPath + "/"); + io::createDirectory(tmp_path); + res = io::copyDirectory("save:/", tmp_path + "/"); if (R_FAILED(res)) { FileSystem::unmount(); - io::deleteFolderRecursively((tmpPath + "/").c_str()); - Logger::getInstance().log(Logger::ERROR, "Failed to copy directory to " + tmpPath + " with result 0x%08lX.", res); - return std::make_tuple(false, res, "Failed to backup save."); + io::deleteFolderRecursively(tmp_path + "/"); + nxst::log::error("Failed to copy directory to %s with result 0x%08lX.", tmp_path.c_str(), res); + return nxst::Result::failure("Failed to backup save."); } - // Swap: delete old backup only after new one is fully written. - if (io::directoryExists(dstPath)) { - io::deleteFolderRecursively((dstPath + "/").c_str()); + if (io::directoryExists(dst_path)) { + io::deleteFolderRecursively(dst_path + "/"); } - if (rename(tmpPath.c_str(), dstPath.c_str()) != 0) { + if (rename(tmp_path.c_str(), dst_path.c_str()) != 0) { FileSystem::unmount(); - Logger::getInstance().log(Logger::ERROR, "Failed to rename temp backup to " + dstPath); - return std::make_tuple(false, (Result)-1, "Failed to finalise backup."); + nxst::log::error("Failed to rename temp backup to %s.", dst_path.c_str()); + return nxst::Result::failure("Failed to finalise backup."); } refreshDirectories(title.id()); - FileSystem::unmount(); - ret = std::make_tuple(true, 0, dstPath); - Logger::getInstance().log(Logger::INFO, "Backup succeeded."); - return ret; + nxst::log::info("Backup succeeded."); + return nxst::Result::success(dst_path); } -std::tuple io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell) +// Creates the save data filesystem for a title if it doesn't exist yet. +static void createSaveIfNeeded(u64 title_id, AccountUid uid) { - Result res = 0; - std::tuple ret = std::make_tuple(false, -1, ""); + std::vector nsacd_buf(sizeof(NsApplicationControlData), 0); + auto* nsacd = reinterpret_cast(nsacd_buf.data()); + + size_t outsize = 0; + if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, + title_id, nsacd, + sizeof(NsApplicationControlData), &outsize))) { + return; + } + + static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; + + FsSaveDataAttribute attr = {}; + attr.application_id = title_id; + attr.uid = uid; + attr.save_data_type = FsSaveDataType_Account; + attr.save_data_rank = FsSaveDataRank_Primary; + + FsSaveDataCreationInfo create_info = {}; + create_info.save_data_size = (s64)nsacd->nacp.user_account_save_data_size; + create_info.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size; + create_info.available_size = 0x4000; + create_info.owner_id = nsacd->nacp.save_data_owner_id; + create_info.save_data_space_id = FsSaveDataSpaceId_User; + + fsCreateSaveDataFileSystem(&attr, &create_info, &meta); +} + +nxst::Result io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell) +{ + (void)cellIndex; + Title title; getTitle(title, uid, index); - Logger::getInstance().log(Logger::INFO, "Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(), - title.userId().uid[1], title.userId().uid[0]); + nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", + title.name().c_str(), title.id(), + title.userId().uid[1], title.userId().uid[0]); - // If save data does not yet exist (game was never launched), create it via NACP. - // fsCreateSaveDataFileSystem returns an error if the save already exists — this is expected. - { - NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData)); - if (nsacd != NULL) { - memset(nsacd, 0, sizeof(NsApplicationControlData)); - size_t outsize = 0; - if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title.id(), nsacd, sizeof(NsApplicationControlData), &outsize))) { - static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; + createSaveIfNeeded(title.id(), uid); - FsSaveDataAttribute attr = {}; - attr.application_id = title.id(); - attr.uid = uid; - attr.save_data_type = FsSaveDataType_Account; - attr.save_data_rank = FsSaveDataRank_Primary; - - FsSaveDataCreationInfo createInfo = {}; - createInfo.save_data_size = (s64)nsacd->nacp.user_account_save_data_size; - createInfo.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size; - createInfo.available_size = 0x4000; - createInfo.owner_id = nsacd->nacp.save_data_owner_id; - createInfo.save_data_space_id = FsSaveDataSpaceId_User; - - fsCreateSaveDataFileSystem(&attr, &createInfo, &meta); - } - free(nsacd); - } + nxst::FsFileSystemHandle fsHandle; + Result res = FileSystem::mount(fsHandle.get(), title.id(), uid); + if (R_FAILED(res)) { + nxst::log::error("Failed to mount filesystem during restore with result 0x%08lX. " + "Title id: 0x%016lX; User id: 0x%lX%lX.", + res, title.id(), uid.uid[1], uid.uid[0]); + return nxst::Result::failure("Failed to mount save."); } + fsHandle.valid = true; - FsFileSystem fileSystem; - res = FileSystem::mount(&fileSystem, title.id(), uid); - if (R_SUCCEEDED(res)) { - int rc = FileSystem::mount(fileSystem); - if (rc == -1) { - fsFsClose(&fileSystem); - FileSystem::unmount(); - Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(), - uid.uid[1], uid.uid[0]); - return std::make_tuple(false, -2, "Failed to mount save."); - } - } - else { - Logger::getInstance().log(Logger::ERROR, - "Failed to mount filesystem during restore with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(), - uid.uid[1], uid.uid[0]); - return std::make_tuple(false, res, "Failed to mount save."); + if (FileSystem::mount(*fsHandle.get()) == -1) { + nxst::log::error("Failed to mount devfs during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", + title.id(), uid.uid[1], uid.uid[0]); + FileSystem::unmount(); + return nxst::Result::failure("Failed to mount save."); } + fsHandle.release(); // devfs now owns the kernel handle std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid))); - std::string srcPath = title.path() + "/" + suggestion + "/"; - std::string dstPath = "save:/"; + std::string src_path = title.path() + "/" + suggestion + "/"; + const std::string dst_path = "save:/"; - // Validate source exists and is non-empty before touching live save data. { - Directory srcCheck(srcPath); - if (!srcCheck.good() || srcCheck.size() == 0) { + Directory src_check(src_path); + if (!src_check.good() || src_check.size() == 0) { FileSystem::unmount(); - Logger::getInstance().log(Logger::ERROR, "Restore source is empty or missing: " + srcPath); - return std::make_tuple(false, (Result)-1, "Restore source is empty or missing."); + nxst::log::error("Restore source is empty or missing: %s", src_path.c_str()); + return nxst::Result::failure("Restore source is empty or missing."); } } { - Directory saveRoot(dstPath); - for (size_t i = 0, sz = saveRoot.size(); i < sz; i++) { - if (saveRoot.folder(i)) { - io::deleteFolderRecursively((dstPath + saveRoot.entry(i) + "/").c_str()); - rmdir((dstPath + saveRoot.entry(i)).c_str()); + Directory save_root(dst_path); + for (size_t i = 0, sz = save_root.size(); i < sz; i++) { + if (save_root.folder(i)) { + io::deleteFolderRecursively(dst_path + save_root.entry(i) + "/"); + rmdir((dst_path + save_root.entry(i)).c_str()); } else { - std::remove((dstPath + saveRoot.entry(i)).c_str()); + std::remove((dst_path + save_root.entry(i)).c_str()); } } } @@ -305,29 +304,27 @@ std::tuple io::restore(size_t index, AccountUid uid, res = fsdevCommitDevice("save"); if (R_FAILED(res)) { FileSystem::unmount(); - Logger::getInstance().log(Logger::ERROR, "Failed to commit save after clearing with result 0x%08lX.", res); - return std::make_tuple(false, res, "Failed to commit save after delete."); + nxst::log::error("Failed to commit save after clearing with result 0x%08lX.", res); + return nxst::Result::failure("Failed to commit save after delete."); } - res = io::copyDirectory(srcPath, dstPath); + res = io::copyDirectory(src_path, dst_path); if (R_FAILED(res)) { FileSystem::unmount(); - Logger::getInstance().log(Logger::ERROR, "Failed to copy directory " + srcPath + " to " + dstPath + " with result 0x%08lX. Skipping...", res); - return std::make_tuple(false, res, "Failed to restore save."); + nxst::log::error("Failed to copy %s to save:/ with result 0x%08lX.", src_path.c_str(), res); + return nxst::Result::failure("Failed to restore save."); } res = fsdevCommitDevice("save"); if (R_FAILED(res)) { - Logger::getInstance().log(Logger::ERROR, "Failed to commit save with result 0x%08lX.", res); - return std::make_tuple(false, res, "Failed to commit to save device."); - } - else { - blinkLed(4); - ret = std::make_tuple(true, 0, nameFromCell + "\nhas been restored successfully."); + FileSystem::unmount(); + nxst::log::error("Failed to commit save with result 0x%08lX.", res); + return nxst::Result::failure("Failed to commit to save device."); } + blinkLed(4); FileSystem::unmount(); - Logger::getInstance().log(Logger::INFO, "Restore succeeded."); - return ret; -} \ No newline at end of file + nxst::log::info("Restore succeeded."); + return nxst::Result::success(nameFromCell + "\nhas been restored successfully."); +} diff --git a/src/service/transfer_service.cpp b/src/service/transfer_service.cpp index 2c59c8c..3fb4e79 100644 --- a/src/service/transfer_service.cpp +++ b/src/service/transfer_service.cpp @@ -205,11 +205,11 @@ void TransferService::runSender(size_t title_index, AccountUid uid) { 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)); + if (!backup_result.isOk()) { + failSend("Failed to create backup:\n" + backup_result.error()); return finish(); } - fs::path directory = std::get<2>(backup_result); + fs::path directory = backup_result.value(); #else fs::path directory = "."; (void)title_index; (void)uid; @@ -414,8 +414,8 @@ void TransferService::runAccept(int server_fd) { #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); + restore_ok = result.isOk(); + restore_error = result.isOk() ? "" : result.error(); #else restore_ok = true; #endif -- 2.39.2 From 9339e7dbfec9aa30cb28f61c0c27f9a24417f6b1 Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov Date: Mon, 27 Apr 2026 01:49:41 +0300 Subject: [PATCH 04/10] finish refactor, add docs and CI --- .gitea/workflows/build.yml | 79 +++ .github/workflows/build.yml | 76 +++ CHANGELOG.md | 34 ++ CMakeLists.txt | 20 +- CMakePresets.json | 17 + LICENSE | 30 ++ Makefile | 204 -------- PLAN.md | 6 +- README.md | 88 ++++ docs/ARCHITECTURE.md | 163 +++++++ docs/PROTOCOL.md | 95 ++++ include/nxst/app/main.hpp | 17 +- include/nxst/app/main_application.hpp | 23 +- include/nxst/domain/account.hpp | 41 +- include/nxst/domain/common.hpp | 27 +- include/nxst/domain/protocol.hpp | 24 +- include/nxst/domain/result.hpp | 67 ++- include/nxst/domain/title.hpp | 18 +- include/nxst/domain/transfer_state.hpp | 18 +- include/nxst/domain/util.hpp | 18 +- include/nxst/infra/fs/directory.hpp | 8 +- include/nxst/infra/fs/filesystem.hpp | 12 +- include/nxst/infra/fs/handles.hpp | 33 +- include/nxst/infra/fs/io.hpp | 34 +- include/nxst/infra/net/socket.hpp | 23 +- include/nxst/infra/sys/logger.hpp | 40 +- include/nxst/service/transfer_service.hpp | 115 +++-- include/nxst/ui/card.hpp | 22 +- include/nxst/ui/header_bar.hpp | 127 +++-- include/nxst/ui/hint_bar.hpp | 95 ++-- include/nxst/ui/theme.hpp | 127 ++--- include/nxst/ui/titles_layout.hpp | 92 ++-- include/nxst/ui/transfer_overlay.hpp | 136 +++--- include/nxst/ui/ui_context.hpp | 14 +- include/nxst/ui/users_layout.hpp | 35 +- src/app/main.cpp | 25 +- src/app/main_application.cpp | 31 +- src/domain/account.cpp | 51 +- src/domain/common.cpp | 53 +- src/domain/title.cpp | 201 ++++---- src/domain/util.cpp | 66 ++- src/infra/fs/directory.cpp | 27 +- src/infra/fs/filesystem.cpp | 9 +- src/infra/fs/io.cpp | 81 ++-- src/infra/sys/logger.cpp | 58 ++- src/service/transfer_service.cpp | 199 +++++--- src/ui/titles_layout.cpp | 560 +++++++++++----------- src/ui/users_layout.cpp | 116 +++-- 48 files changed, 1979 insertions(+), 1476 deletions(-) create mode 100644 .gitea/workflows/build.yml create mode 100644 .github/workflows/build.yml create mode 100644 CHANGELOG.md create mode 100644 CMakePresets.json create mode 100644 LICENSE delete mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/PROTOCOL.md diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..e6ae9f5 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,79 @@ +name: CI + +on: + push: + pull_request: + +jobs: + nro: + name: Build NRO + runs-on: ubuntu-latest + + steps: + - name: Build + run: | + AUTH_URL=$(echo "$GITHUB_SERVER_URL" | sed "s|://|://oauth2:${GITHUB_TOKEN}@|") + docker rm -f nxst-build 2>/dev/null || true + docker run --name nxst-build devkitpro/devkita64:latest \ + bash -c " + git clone --recurse-submodules '${AUTH_URL}/${GITHUB_REPOSITORY}' /src && + cd /src && git checkout '${GITHUB_SHA}' && + cmake --preset switch && + cmake --build build -j\$(nproc) + " + docker cp nxst-build:/src/build/NXST.nro ./NXST.nro + docker rm nxst-build + + - name: Publish release + if: startsWith(github.ref, 'refs/tags/v') + env: + GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }} + run: | + release_id=$(curl -sf -X POST "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\": \"${GITHUB_REF_NAME}\", \"name\": \"${GITHUB_REF_NAME}\"}" \ + | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) + + curl -sf -X POST "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets?name=NXST.nro" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + --data-binary @./NXST.nro + + format: + name: Format check + runs-on: ubuntu-latest + + steps: + - name: Checkout + run: | + AUTH_URL=$(echo "$GITHUB_SERVER_URL" | sed "s|://|://oauth2:${GITHUB_TOKEN}@|") + git clone "${AUTH_URL}/${GITHUB_REPOSITORY}" . + git checkout "$GITHUB_SHA" + + - name: Install clang-format + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm.list + sudo apt-get update && sudo apt-get install -y clang-format-22 + sudo ln -sf /usr/bin/clang-format-22 /usr/local/bin/clang-format + + - name: Check formatting + run: | + find src include \( -name '*.cpp' -o -name '*.hpp' \) \ + | xargs clang-format --dry-run --Werror + + layering: + name: Layering check + runs-on: ubuntu-latest + + steps: + - name: Checkout + run: | + AUTH_URL=$(echo "$GITHUB_SERVER_URL" | sed "s|://|://oauth2:${GITHUB_TOKEN}@|") + git clone "${AUTH_URL}/${GITHUB_REPOSITORY}" . + git checkout "$GITHUB_SHA" + + - name: UI must not include net/sys headers + run: | + ! grep -rE '^#include\s*[<"](arpa/inet|sys/socket|pthread)' src/ui/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d7123e2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,76 @@ +name: CI + +on: + push: + pull_request: + +jobs: + nro: + name: Build NRO + runs-on: ubuntu-latest + container: + image: devkitpro/devkita64:latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure + run: cmake --preset switch + + - name: Build + run: cmake --build build -j$(nproc) + + - uses: actions/upload-artifact@v4 + with: + name: NXST-${{ github.sha }} + path: build/NXST.nro + + release: + name: Upload release asset + runs-on: ubuntu-latest + needs: nro + if: startsWith(github.ref, 'refs/tags/v') + + permissions: + contents: write + + steps: + - uses: actions/download-artifact@v4 + with: + name: NXST-${{ github.sha }} + + - uses: softprops/action-gh-release@v2 + with: + files: NXST.nro + + format: + name: Format check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install clang-format + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm.list + sudo apt-get update && sudo apt-get install -y clang-format-22 + sudo ln -sf /usr/bin/clang-format-22 /usr/local/bin/clang-format + + - name: Check formatting + run: | + find src include \( -name '*.cpp' -o -name '*.hpp' \) \ + | xargs clang-format --dry-run --Werror + + layering: + name: Layering check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: UI must not include net/sys headers + run: | + ! grep -rE '^#include\s*[<"](arpa/inet|sys/socket|pthread)' src/ui/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e12dae0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to NXST follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +Version numbers are calendar-based (`MM.DD.YYYY`) until the protocol stabilises. + +--- + +## [Unreleased] + +### Changed +- Full architectural refactor (8 phases): + - Layered directory structure: `src/` + `include/nxst/` mirroring domain / infra / service / ui + - CMake build system replacing Makefile + - `nxst::TransferService` — all network state and threads moved out of UI layer + - `nxst::Result` tagged-union type replacing `std::tuple` + - RAII handles: `FsFileSystemHandle`, `FileHandle` + - Logger rewritten with `nxst::log` namespace and compile-time format checking + - snake_case filenames and identifiers throughout + +### Fixed +- `mkdir(path, 777)` was decimal (octal `01411`) — corrected to `0777` +- Logger format args were silently dropped (double-substitution bug) +- `new u8[]` / `malloc` in IO paths replaced with `std::vector` + +--- + +## [04.26.2026] + +### Added +- Initial public release: save transfer between two Nintendo Switch consoles over local Wi-Fi +- Multicast discovery (UDP `239.0.0.1:8081`) + TCP file transfer (`:8080`) +- Send and Receive modes with progress overlay and cancel support +- Automatic save backup before transfer +- Restore replaces live save data and commits to the save device diff --git a/CMakeLists.txt b/CMakeLists.txt index 888c756..14a2d30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,11 +32,7 @@ file(GLOB_RECURSE NXST_SOURCES src/ui/*.cpp ) -file(GLOB_RECURSE PLUTONIUM_SOURCES - lib/Plutonium/source/*.cpp -) - -add_executable(NXST ${NXST_SOURCES} ${PLUTONIUM_SOURCES}) +add_executable(NXST ${NXST_SOURCES}) # ── Include paths ───────────────────────────────────────────────────────────── target_include_directories(NXST PRIVATE @@ -56,13 +52,25 @@ pkg_check_modules(PORTLIBS REQUIRED IMPORTED_TARGET ${NXST_PKG_MODULES}) target_include_directories(NXST PRIVATE ${PORTLIBS_INCLUDE_DIRS}) +# ── Link libraries ──────────────────────────────────────────────────────────── +# ── Plutonium static library ────────────────────────────────────────────────── +set(LIBPU_A ${CMAKE_SOURCE_DIR}/lib/Plutonium/lib/libpu.a) + +add_custom_command( + OUTPUT ${LIBPU_A} + COMMAND make -C ${CMAKE_SOURCE_DIR}/lib/Plutonium -j + COMMENT "Building Plutonium (libpu.a)" +) +add_custom_target(plutonium DEPENDS ${LIBPU_A}) +add_dependencies(NXST plutonium) + # ── 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 + ${LIBPU_A} PkgConfig::PORTLIBS drm_nouveau harfbuzz diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..7a816f8 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,17 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "switch", + "displayName": "Nintendo Switch", + "toolchainFile": "$env{DEVKITPRO}/cmake/Switch.cmake", + "binaryDir": "${sourceDir}/build" + } + ], + "buildPresets": [ + { + "name": "switch", + "configurePreset": "switch" + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fa6ac14 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + For the full license text see . + +------------------------------------------------------------------------------- + +NXST includes code derived from Checkpoint by Bernardo Giordano / FlagBrew, +licensed under GPLv3. The derived files are: + + src/infra/fs/io.cpp + src/domain/account.cpp + src/domain/title.cpp + include/nxst/infra/fs/filesystem.hpp + src/infra/fs/filesystem.cpp + src/infra/fs/directory.cpp + +All original NXST code is also released under the GNU General Public +License v3 or later, to satisfy the license inheritance requirement. + +Original Checkpoint repository: https://github.com/BernardoGiordano/Checkpoint diff --git a/Makefile b/Makefile deleted file mode 100644 index 2f23ac4..0000000 --- a/Makefile +++ /dev/null @@ -1,204 +0,0 @@ -#--------------------------------------------------------------------------------- -.SUFFIXES: -#--------------------------------------------------------------------------------- - -ifeq ($(strip $(DEVKITPRO)),) -$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") -endif - -TOPDIR ?= $(CURDIR) -include $(DEVKITPRO)/libnx/switch_rules - -#--------------------------------------------------------------------------------- -# TARGET is the name of the output -# BUILD is the directory where object files & intermediate files will be placed -# SOURCES is a list of directories containing source code -# DATA is a list of directories containing data files -# INCLUDES is a list of directories containing header files -# EXEFS_SRC is the optional input directory containing data copied into exefs, if anything this normally should only contain "main.npdm". -# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) -# -# NO_ICON: if set to anything, do not use icon. -# NO_NACP: if set to anything, no .nacp file is generated. -# APP_TITLE is the name of the app stored in the .nacp file (Optional) -# APP_AUTHOR is the author of the app stored in the .nacp file (Optional) -# APP_VERSION is the version of the app stored in the .nacp file (Optional) -# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) -# ICON is the filename of the icon (.jpg), relative to the project folder. -# If not set, it attempts to use one of the following (in this order): -# - .jpg -# - icon.jpg -# - /default_icon.jpg -#--------------------------------------------------------------------------------- -TARGET := NXST -BUILD := build -SOURCES := src/app src/domain src/infra/net src/infra/fs src/infra/sys src/service src/ui lib/Plutonium/source -INCLUDES := include lib/Plutonium/include -EXEFS_SRC := exefs_src -APP_TITLE := NXST -APP_AUTHOR := DragonSpirit -APP_VERSION := 04.26.2026 -ICON := icon.png - -#--------------------------------------------------------------------------------- -# options for code generation -#--------------------------------------------------------------------------------- -ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE - -CFLAGS += -g -O2 -ffunction-sections $(ARCH) $(DEFINES) - -CFLAGS += $(INCLUDE) -D__SWITCH__ -D_GNU_SOURCE=1 - -CXXFLAGS:= $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17 -g - -ASFLAGS := -g $(ARCH) -LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) - -PKGCONF := $(DEVKITPRO)/portlibs/switch/bin/aarch64-none-elf-pkg-config -PKG_LIBS := SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer freetype2 harfbuzz minizip libpng libjpeg libwebp glesv2 egl glapi zlib -LIBS := -lpu $(shell $(PKGCONF) --libs $(PKG_LIBS)) -ldrm_nouveau -lharfbuzz -lfreetype -lz - -#--------------------------------------------------------------------------------- -# list of directories containing libraries, this must be the top level containing -# include and lib -#--------------------------------------------------------------------------------- -LIBDIRS := $(PORTLIBS) $(LIBNX) $(CURDIR)/lib/Plutonium - - -#--------------------------------------------------------------------------------- -# no real need to edit anything past this point unless you need to add additional -# rules for different file extensions -#--------------------------------------------------------------------------------- -ifneq ($(BUILD),$(notdir $(CURDIR))) -#--------------------------------------------------------------------------------- - -export OUTPUT := $(CURDIR)/$(TARGET) -export TOPDIR := $(CURDIR) - -export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ - $(foreach dir,$(DATA),$(CURDIR)/$(dir)) - -export DEPSDIR := $(CURDIR)/$(BUILD) - -CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) -CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) -SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) -BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) - -#--------------------------------------------------------------------------------- -# use CXX for linking C++ projects, CC for standard C -#--------------------------------------------------------------------------------- -ifeq ($(strip $(CPPFILES)),) -#--------------------------------------------------------------------------------- - export LD := $(CC) -#--------------------------------------------------------------------------------- -else -#--------------------------------------------------------------------------------- - export LD := $(CXX) -#--------------------------------------------------------------------------------- -endif -#--------------------------------------------------------------------------------- - -export OFILES_BIN := $(addsuffix .o,$(BINFILES)) -export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) -export OFILES := $(OFILES_BIN) $(OFILES_SRC) -export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) - -export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ - $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ - -I$(CURDIR)/$(BUILD) - -export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) - -export BUILD_EXEFS_SRC := $(TOPDIR)/$(EXEFS_SRC) - -ifeq ($(strip $(ICON)),) - icons := $(wildcard *.jpg) - ifneq (,$(findstring $(TARGET).jpg,$(icons))) - export APP_ICON := $(TOPDIR)/$(TARGET).jpg - else - ifneq (,$(findstring icon.jpg,$(icons))) - export APP_ICON := $(TOPDIR)/icon.jpg - endif - endif -else - export APP_ICON := $(TOPDIR)/$(ICON) -endif - -ifeq ($(strip $(NO_ICON)),) - export NROFLAGS += --icon=$(APP_ICON) -endif - -ifeq ($(strip $(NO_NACP)),) - export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp -endif - -ifneq ($(APP_TITLEID),) - export NACPFLAGS += --titleid=$(APP_TITLEID) -endif - -ifneq ($(ROMFS),) - export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) -endif - -.PHONY: $(BUILD) clean all - -#--------------------------------------------------------------------------------- -all: $(BUILD) - -$(BUILD): - @[ -d $@ ] || mkdir -p $@ - @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile - -#--------------------------------------------------------------------------------- -clean: - @echo clean ... - @rm -fr $(BUILD) $(TARGET).pfs0 $(TARGET).nso $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst - -cleanbuild: clean all - -#--------------------------------------------------------------------------------- -send: $(BUILD) - @nxlink $(TARGET).nro - -debug: $(BUILD) - @nxlink -s $(TARGET).nro - - -else -.PHONY: all - -DEPENDS := $(OFILES:.o=.d) - -#--------------------------------------------------------------------------------- -# main targets -#--------------------------------------------------------------------------------- -all : $(OUTPUT).pfs0 $(OUTPUT).nro - -$(OUTPUT).pfs0 : $(OUTPUT).nso - -$(OUTPUT).nso : $(OUTPUT).elf - -ifeq ($(strip $(NO_NACP)),) -$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp -else -$(OUTPUT).nro : $(OUTPUT).elf -endif - -$(OUTPUT).elf : $(OFILES) - -$(OFILES_SRC) : $(HFILES_BIN) - -#--------------------------------------------------------------------------------- -# you need a rule like this for each extension you use as binary data -#--------------------------------------------------------------------------------- -%.bin.o %_bin.h : %.bin -#--------------------------------------------------------------------------------- - @echo $(notdir $<) - @$(bin2o) - --include $(DEPENDS) - -#--------------------------------------------------------------------------------------- -endif -#--------------------------------------------------------------------------------------- diff --git a/PLAN.md b/PLAN.md index 5a02885..e93e5e7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -11,10 +11,10 @@ | 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain | | 5 | TransferService extraction | ✅ Done | L (~2d) | kill globals, sever UI ↔ net coupling | | 6 | `Result` + RAII | ✅ Done | M (~1d) | tagged union, OS handle wrappers, fix raw memory | -| 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 | +| 7 | Documentation + license | ✅ Done | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE | +| 8 | CI | ✅ Done | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check | -**Active phase:** Phase 7 — Documentation + license. +**Active phase:** — All phases complete. **Last updated:** 2026-04-27. Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c33eea --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# NXST + +Transfer save data between two Nintendo Switch consoles over local Wi-Fi. + +Pick a game, pick a user. One Switch sends; the other receives. The sender backs up its own save first, so you can never lose data in transit. + +--- + +## Install + +1. Download `NXST.nro` from [Releases](../../releases). +2. Copy it to `/switch/NXST/NXST.nro` on your SD card. +3. Launch via hbmenu (hold R while starting any game, or Album). + +Both Switches must be on the same Wi-Fi network. No router configuration needed — discovery uses UDP multicast. + +--- + +## Usage + +**Sender** (the Switch whose save you want to copy): + +1. Open NXST → select a title → press **A** → **Transfer**. +2. Wait for "Waiting for receiver…" to change to "Transferring…". + +**Receiver** (the Switch that will receive the save): + +1. Open NXST → select the same title → press **A** → **Receive**. +2. Wait. The save is restored automatically when the transfer finishes. + +Press **B** on either side to cancel mid-transfer. + +Logs are written to `/switch/NXST/log.log`. + +--- + +## Build + +**Prerequisites:** [devkitPro](https://devkitpro.org/wiki/Getting_Started) with `switch-dev` and `switch-portlibs` packages, plus `cmake ≥ 3.20`. + +```bash +# Clone with submodules (Plutonium UI) +git clone --recurse-submodules https://github.com/your-username/NXST.git +cd NXST + +# Configure (once) +cmake --preset switch + +# Build +cmake --build build + +# Send to Switch via nxlink (Switch must be on same network, nxlink running) +cmake --build build --target send +``` + +Output: `build/NXST.nro` + +--- + +## Architecture + +See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the layer diagram, threading model, and key types. + +See [`docs/PROTOCOL.md`](docs/PROTOCOL.md) for the wire protocol (UDP multicast discovery + TCP file stream). + +``` +ui/ — TitlesLayout, UsersLayout, TransferOverlay, HeaderBar +service/ — TransferService (all network threads and state) +infra/net/ — Socket RAII, sendAll/recvAll +infra/fs/ — io::backup, io::restore, directory iterator, RAII handles +infra/sys/ — nxst::log (printf-checked, timestamped) +domain/ — Title, Account, Result, TransferState, protocol constants +``` + +--- + +## Credits + +- **[Plutonium](https://github.com/XorTroll/Plutonium)** — Switch UI framework by XorTroll +- **[Checkpoint](https://github.com/BernardoGiordano/Checkpoint)** — save management library by Bernardo Giordano / FlagBrew; several files in `src/infra/fs/` and `src/domain/` are derived from Checkpoint + +--- + +## License + +GPLv3 — see [`LICENSE`](LICENSE). + +NXST includes code derived from Checkpoint (GPLv3). All original NXST code is released under the same license to satisfy the GPL inheritance requirement. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..0d0f748 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,163 @@ +# NXST Architecture + +## Layer Diagram + +``` + ┌──────────────────────────────┐ + │ ui/ │ + │ TitlesLayout, UsersLayout │ + │ TransferOverlay, HeaderBar │ + └──────────────┬───────────────┘ + │ calls + ┌──────────────▼───────────────┐ + │ service/ │ + │ TransferService │ + └──────┬───────────────┬───────┘ + │ │ + ┌────────────▼──┐ ┌───────▼────────┐ + │ infra/net/ │ │ infra/fs/ │ + │ TransferService│ │ io, directory │ + │ (sender thread │ │ filesystem │ + │ recv threads) │ │ handles │ + └────────────┬──┘ └───────┬────────┘ + │ │ + ┌────────────▼───────────────▼────────┐ + │ domain/ │ + │ Title, Account, TransferState │ + │ Result, protocol constants │ + └─────────────────────────────────────┘ + │ + ┌────────────────────▼────────────────┐ + │ libnx, Plutonium, SDL2, devkitpro│ + └─────────────────────────────────────┘ +``` + +**Hard layering rules:** +- `domain/` depends on nothing in the project. +- `infra/` depends only on `domain/`. +- `service/` depends on `domain/` + `infra/`. +- `ui/` depends only on `service/` + `domain/` — must not include ``, ``, ``, or call `recv`/`send` directly. +- `app/` (MainApplication, main.cpp) wires everything together. + +--- + +## Key Types + +### `nxst::TransferService` (`include/nxst/service/transfer_service.hpp`) + +Owns all network state for both transfer directions. One instance lives in `MainApplication`. + +| Method | Description | +|--------|-------------| +| `startSend(index, uid)` | Backup save, discover receiver, stream files | +| `startReceive(index, uid, title_name)` | Listen for sender, receive files, restore save | +| `cancelSend()` / `cancelReceive()` | Interrupt in-flight transfer (socket shutdown) | +| `isSendDone()` / `isReceiveDone()` | Polled by UI render loop | +| `restoreSucceeded()` / `restoreError()` | Restore outcome after receive | + +**Threading model:** + +``` +UI thread (Plutonium event loop) + └─ startSend() + └─ senderEntry [pthread, detached] + ├─ findServer() — UDP multicast, 100 ms poll slices + ├─ io::backup() — creates local save backup + └─ TCP send loop + + └─ startReceive() + ├─ broadcastEntry [pthread, detached] — UDP multicast listener + └─ acceptEntry [pthread, detached] + ├─ TCP receive loop + └─ io::restore() — mounts save, clears, copies, commits +``` + +Cancellation: `cancelSend()` / `cancelReceive()` call `shutdown(SHUT_RDWR)` on all open sockets. Blocking `read`/`recvfrom`/`accept` return errors immediately. Atomic flags are checked at loop boundaries. + +### `nxst::Result` (`include/nxst/domain/result.hpp`) + +85-line tagged-union result type. No exceptions, no `std::variant`. Used for `io::backup` and `io::restore` return values. + +```cpp +auto result = io::backup(title_index, uid); +if (!result.isOk()) { + failSend("Backup failed: " + result.error()); + return; +} +fs::path dir = result.value(); +``` + +`Result` specialisation available for operations that succeed without a value. + +### `nxst::FsFileSystemHandle` / `nxst::FileHandle` (`include/nxst/infra/fs/handles.hpp`) + +RAII wrappers ensuring `fsFsClose` / `fclose` are called on all exit paths. + +--- + +## Threading Invariants + +- All `std::atomic` members in `TransferService` use sequential-consistency (default). No explicit `memory_order` needed at this concurrency level. +- `TransferState::status` is a `std::string` protected by `std::mutex status_mutex`. All other `TransferState` fields are atomics. +- `pthread_create` + `pthread_detach` is used throughout (libnx's supported path). Threads are never joined — they signal completion via `state.done = true` and set their active flag to false. +- Cancel is safe to call from any thread. + +--- + +## File Map + +``` +include/nxst/ +├── app/ +│ ├── main.hpp — global state (g_currentTime, g_sortMode, …) +│ └── main_application.hpp — MainApplication : pu::ui::Application +├── domain/ +│ ├── account.hpp — AccountUid, Account::ids(), Account::username() +│ ├── common.hpp — StringUtils (UTF-8/16, elide, accents) +│ ├── protocol.hpp — wire constants (ports, sentinel, buffer size) +│ ├── result.hpp — Result tagged union +│ ├── title.hpp — Title, getTitle(), getTitleCount() +│ ├── transfer_state.hpp — TransferState (atomics + mutex-guarded status) +│ └── util.hpp — StringUtils helpers, blinkLed +├── infra/ +│ ├── fs/ +│ │ ├── directory.hpp — Directory iterator (libnx FsDir) +│ │ ├── filesystem.hpp — FileSystem::mount/unmount wrappers +│ │ ├── handles.hpp — FsFileSystemHandle, FileHandle (RAII) +│ │ └── io.hpp — io::backup, io::restore, io::copyFile, … +│ ├── net/ +│ │ └── socket.hpp — Socket RAII wrapper (int fd) +│ └── sys/ +│ └── logger.hpp — nxst::log::info/warn/error (printf-checked) +├── service/ +│ └── transfer_service.hpp — TransferService +└── ui/ + ├── card.hpp — Card UI component + ├── const.h — layout/color/font constants + ├── header_bar.hpp — HeaderBar (title + user avatar row) + ├── hint_bar.hpp — HintBar (button legend) + ├── theme.hpp — color, radius, spacing, type tokens + ├── titles_layout.hpp — TitlesLayout + ├── transfer_overlay.hpp — TransferOverlay (progress modal) + ├── ui_context.hpp — UiContext (selected user state) + └── users_layout.hpp — UsersLayout +``` + +--- + +## Build + +```bash +# Configure (once — toolchain preset reads $DEVKITPRO automatically) +cmake --preset switch + +# Build +cmake --build build + +# Send to Switch via nxlink +cmake --build build --target send +``` + +Output: `build/NXST.nro` + +See `README.md` for full build prerequisites. diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md new file mode 100644 index 0000000..896c863 --- /dev/null +++ b/docs/PROTOCOL.md @@ -0,0 +1,95 @@ +# NXST Wire Protocol + +## Overview + +NXST uses two sockets per transfer session: + +| Socket | Purpose | +|--------|---------| +| UDP multicast | Receiver advertisement (discovery) | +| TCP | File data stream | + +Both sides must be on the same local network segment. The sender (Transfer mode) initiates; the receiver (Receive mode) listens. + +--- + +## Discovery — UDP Multicast + +| Parameter | Value | +|-----------|-------| +| Group | `239.0.0.1` | +| Port | `8081` | +| Direction | sender → receiver | + +**Flow:** + +1. Receiver joins multicast group and binds `0.0.0.0:8081`. +2. Sender sends `"DISCOVER_SERVER"` (15 bytes, no null terminator) to `239.0.0.1:8081`. +3. Receiver replies `"SERVER_HERE"` (11 bytes) to the sender's source address. +4. Sender extracts the receiver's IP from the reply source and closes the UDP socket. + +Sender polls in 100 ms slices for up to 3 seconds. Cancel is checked each slice. + +--- + +## File Transfer — TCP + +| Parameter | Value | +|-----------|-------| +| Port | `8080` | +| Direction | sender connects → receiver listens | +| Buffer size | 65 536 bytes (`proto::BUF_SIZE`) | + +**Connection:** + +1. Receiver listens on `0.0.0.0:8080` (started concurrently with multicast listener). +2. Sender connects after receiving `"SERVER_HERE"`. + +**Wire layout — one file:** + +``` +┌─────────────────────────────────┐ +│ filename_len │ uint32_t LE │ 4 bytes +├─────────────────────────────────┤ +│ filename │ filename_len │ bytes, no null terminator +│ │ bytes │ +├─────────────────────────────────┤ +│ file_size │ uint64_t LE │ 8 bytes +├─────────────────────────────────┤ +│ file_data │ file_size │ bytes +│ │ bytes │ +└─────────────────────────────────┘ +``` + +Files are sent sequentially. The stream ends with a sentinel frame: + +``` +filename_len == 0 (proto::EOF_SENTINEL) +``` + +No `filename` or `file_size` field follows the sentinel. + +**Constraints:** + +- `filename_len > proto::MAX_FILENAME` (4 096) is treated as a protocol error; the receiver aborts. +- Filenames are full paths as produced by `io::backup` (e.g. `/switch/NXST//<user>/...`). +- On the receiver, the username path component is rewritten to match the local user's nickname before writing to disk. + +--- + +## Post-Transfer + +After the TCP stream closes (sentinel received), the receiver calls `io::restore`: + +1. Mounts the title's save filesystem. +2. Clears existing save data. +3. Copies received files into `save:/`. +4. Commits via `fsdevCommitDevice("save")`. + +The sender creates a local backup via `io::backup` before connecting, so the sender's own save is never at risk. + +--- + +## Cancellation + +Either side can cancel at any time by closing the relevant socket (`shutdown` + `close`). The other side's blocking read/write will return an error and the transfer loop exits cleanly. The receiver's accept thread sets `receiver_state.done = true` regardless of how the connection ends, so the UI poll loop always terminates. diff --git a/include/nxst/app/main.hpp b/include/nxst/app/main.hpp index 44d004e..c7337b9 100644 --- a/include/nxst/app/main.hpp +++ b/include/nxst/app/main.hpp @@ -1,23 +1,24 @@ #pragma once -#include <nxst/ui/const.h> +#include <memory> + +#include <switch.h> + #include <nxst/domain/account.hpp> #include <nxst/domain/title.hpp> #include <nxst/domain/util.hpp> -#include <memory> -#include <switch.h> #include <nxst/infra/sys/logger.hpp> +#include <nxst/ui/const.h> typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t; inline float g_currentTime = 0; -inline bool g_backupScrollEnabled = 0; -inline bool g_notificationLedAvailable = false; -inline bool g_shouldExitNetworkLoop = false; +inline bool g_backupScrollEnabled = 0; +inline bool g_notificationLedAvailable = false; +inline bool g_shouldExitNetworkLoop = false; inline std::string g_selectedCheatKey; inline std::vector<std::string> g_selectedCheatCodes; inline u32 g_username_dotsize; -inline sort_t g_sortMode = SORT_ALPHA; +inline sort_t g_sortMode = SORT_ALPHA; inline std::string g_currentFile = ""; inline bool g_isTransferringFile = false; inline const std::string g_emptySave = "New..."; - diff --git a/include/nxst/app/main_application.hpp b/include/nxst/app/main_application.hpp index 77a0a7c..52ca05d 100644 --- a/include/nxst/app/main_application.hpp +++ b/include/nxst/app/main_application.hpp @@ -1,22 +1,23 @@ #pragma once #include <pu/Plutonium> + #include <nxst/service/transfer_service.hpp> -#include <nxst/ui/users_layout.hpp> #include <nxst/ui/titles_layout.hpp> +#include <nxst/ui/users_layout.hpp> namespace ui { - class MainApplication : public pu::ui::Application { +class MainApplication : public pu::ui::Application { - public: - using Application::Application; - PU_SMART_CTOR(MainApplication) + public: + using Application::Application; + PU_SMART_CTOR(MainApplication) - void OnLoad() override; + void OnLoad() override; - UsersLayout::Ref users_layout; - TitlesLayout::Ref titles_layout; - nxst::TransferService transfer; - }; -} \ No newline at end of file + UsersLayout::Ref users_layout; + TitlesLayout::Ref titles_layout; + nxst::TransferService transfer; +}; +} // namespace ui \ No newline at end of file diff --git a/include/nxst/domain/account.hpp b/include/nxst/domain/account.hpp index 693a7e1..f3eda2e 100644 --- a/include/nxst/domain/account.hpp +++ b/include/nxst/domain/account.hpp @@ -28,31 +28,31 @@ #include <map> #include <string.h> #include <string> -#include <switch.h> #include <vector> +#include <switch.h> + #define USER_ICON_SIZE 64 namespace std { - template <> - struct hash<AccountUid> { - size_t operator()(const AccountUid& a) const { return ((hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1); } - }; -} +template <> struct hash<AccountUid> { + size_t operator()(const AccountUid& a) const { + return ((hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1); + } +}; +} // namespace std -inline bool operator==(const AccountUid& x, const AccountUid& y) -{ +inline bool operator==(const AccountUid& x, const AccountUid& y) { return x.uid[0] == y.uid[0] && x.uid[1] == y.uid[1]; } -inline bool operator==(const AccountUid& x, u64 y) -{ +inline bool operator==(const AccountUid& x, u64 y) { return x.uid[0] == y && x.uid[1] == y; } -inline bool operator<(const AccountUid& x, const AccountUid& y) -{ - if (x.uid[0] != y.uid[0]) return x.uid[0] < y.uid[0]; +inline bool operator<(const AccountUid& x, const AccountUid& y) { + if (x.uid[0] != y.uid[0]) + return x.uid[0] < y.uid[0]; return x.uid[1] < y.uid[1]; } @@ -62,12 +62,11 @@ struct User { }; namespace Account { - Result init(void); - void exit(void); - - std::vector<AccountUid> ids(void); - AccountUid selectAccount(void); - std::string username(AccountUid id); - std::string iconPath(AccountUid id); -} +Result init(void); +void exit(void); +std::vector<AccountUid> ids(void); +AccountUid selectAccount(void); +std::string username(AccountUid id); +std::string iconPath(AccountUid id); +} // namespace Account diff --git a/include/nxst/domain/common.hpp b/include/nxst/domain/common.hpp index a652f84..8bebd12 100644 --- a/include/nxst/domain/common.hpp +++ b/include/nxst/domain/common.hpp @@ -42,21 +42,20 @@ #define ATEXIT(func) atexit((void (*)())func) namespace DateTime { - std::string timeStr(void); - std::string dateTimeStr(void); - std::string logDateTime(void); -} +std::string timeStr(void); +std::string dateTimeStr(void); +std::string logDateTime(void); +} // namespace DateTime namespace StringUtils { - bool containsInvalidChar(const std::string& str); - std::string escapeJson(const std::string& s); - std::string format(const std::string fmt_str, ...); - std::string removeForbiddenCharacters(std::string src); - std::string UTF16toUTF8(const std::u16string& src); - void ltrim(std::string& s); - void rtrim(std::string& s); - void trim(std::string& s); -} +bool containsInvalidChar(const std::string& str); +std::string escapeJson(const std::string& s); +std::string format(const std::string fmt_str, ...); +std::string removeForbiddenCharacters(std::string src); +std::string UTF16toUTF8(const std::u16string& src); +void ltrim(std::string& s); +void rtrim(std::string& s); +void trim(std::string& s); +} // namespace StringUtils char* getConsoleIP(void); - diff --git a/include/nxst/domain/protocol.hpp b/include/nxst/domain/protocol.hpp index 48cd021..245a777 100644 --- a/include/nxst/domain/protocol.hpp +++ b/include/nxst/domain/protocol.hpp @@ -2,16 +2,16 @@ #include <cstdint> namespace proto { - constexpr uint16_t TCP_PORT = 8080; - constexpr uint16_t MULTICAST_PORT = 8081; - constexpr char MULTICAST_GROUP[] = "239.0.0.1"; - constexpr size_t BUF_SIZE = 65536; - constexpr uint32_t MAX_FILENAME = 4096; - constexpr uint32_t EOF_SENTINEL = 0; +constexpr uint16_t TCP_PORT = 8080; +constexpr uint16_t MULTICAST_PORT = 8081; +constexpr char MULTICAST_GROUP[] = "239.0.0.1"; +constexpr size_t BUF_SIZE = 65536; +constexpr uint32_t MAX_FILENAME = 4096; +constexpr uint32_t EOF_SENTINEL = 0; - // Wire layout per file: - // [filename_len : uint32_t LE] — 0 == end-of-stream - // [filename : filename_len bytes] - // [file_size : uint64_t LE] - // [file_data : file_size bytes] -} +// Wire layout per file: +// [filename_len : uint32_t LE] — 0 == end-of-stream +// [filename : filename_len bytes] +// [file_size : uint64_t LE] +// [file_data : file_size bytes] +} // namespace proto diff --git a/include/nxst/domain/result.hpp b/include/nxst/domain/result.hpp index f3d4956..f146450 100644 --- a/include/nxst/domain/result.hpp +++ b/include/nxst/domain/result.hpp @@ -4,14 +4,13 @@ namespace nxst { -template <class T, class E = std::string> -class Result { +template <class T, class E = std::string> class Result { bool ok; alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)]; Result() = default; -public: + public: static Result success(T val) { Result res; res.ok = true; @@ -27,37 +26,52 @@ public: } ~Result() { - if (ok) reinterpret_cast<T*>(storage)->~T(); - else reinterpret_cast<E*>(storage)->~E(); + if (ok) + reinterpret_cast<T*>(storage)->~T(); + else + reinterpret_cast<E*>(storage)->~E(); } Result(const Result& other) : ok(other.ok) { - if (ok) new (storage) T(*reinterpret_cast<const T*>(other.storage)); - else new (storage) E(*reinterpret_cast<const E*>(other.storage)); + if (ok) + new (storage) T(*reinterpret_cast<const T*>(other.storage)); + else + new (storage) E(*reinterpret_cast<const E*>(other.storage)); } Result(Result&& other) : ok(other.ok) { - if (ok) new (storage) T(std::move(*reinterpret_cast<T*>(other.storage))); - else new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); + if (ok) + new (storage) T(std::move(*reinterpret_cast<T*>(other.storage))); + else + new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); } Result& operator=(const Result&) = delete; - bool isOk() const noexcept { return ok; } - const T& value() const { return *reinterpret_cast<const T*>(storage); } - const E& error() const { return *reinterpret_cast<const E*>(storage); } + bool isOk() const noexcept { + return ok; + } + const T& value() const { + return *reinterpret_cast<const T*>(storage); + } + const E& error() const { + return *reinterpret_cast<const E*>(storage); + } }; // Specialisation for Result<void> -template <class E> -class Result<void, E> { +template <class E> class Result<void, E> { bool ok; alignas(E) unsigned char storage[sizeof(E)]; Result() = default; -public: - static Result success() { Result res; res.ok = true; return res; } + public: + static Result success() { + Result res; + res.ok = true; + return res; + } static Result failure(E err) { Result res; @@ -66,20 +80,29 @@ public: return res; } - ~Result() { if (!ok) reinterpret_cast<E*>(storage)->~E(); } + ~Result() { + if (!ok) + reinterpret_cast<E*>(storage)->~E(); + } Result(const Result& other) : ok(other.ok) { - if (!ok) new (storage) E(*reinterpret_cast<const E*>(other.storage)); + if (!ok) + new (storage) E(*reinterpret_cast<const E*>(other.storage)); } Result(Result&& other) : ok(other.ok) { - if (!ok) new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); + if (!ok) + new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); } Result& operator=(const Result&) = delete; - bool isOk() const noexcept { return ok; } - const E& error() const { return *reinterpret_cast<const E*>(storage); } + bool isOk() const noexcept { + return ok; + } + const E& error() const { + return *reinterpret_cast<const E*>(storage); + } }; -} // namespace nxst +} // namespace nxst diff --git a/include/nxst/domain/title.hpp b/include/nxst/domain/title.hpp index b257872..2a22410 100644 --- a/include/nxst/domain/title.hpp +++ b/include/nxst/domain/title.hpp @@ -25,20 +25,23 @@ */ #pragma once -#include <nxst/domain/account.hpp> -#include <nxst/infra/fs/filesystem.hpp> -#include <nxst/infra/fs/io.hpp> #include <algorithm> #include <stdlib.h> #include <string> -#include <switch.h> #include <unordered_map> #include <utility> #include <vector> +#include <switch.h> + +#include <nxst/domain/account.hpp> +#include <nxst/infra/fs/filesystem.hpp> +#include <nxst/infra/fs/io.hpp> + class Title { -public: - void init(u8 saveDataType, u64 titleid, AccountUid userID, const std::string& name, const std::string& author); + public: + void init(u8 saveDataType, u64 titleid, AccountUid userID, const std::string& name, + const std::string& author); ~Title() = default; std::string author(void); @@ -60,7 +63,7 @@ public: AccountUid userId(void); std::string userName(void); -private: + private: u64 mId; u64 mSaveId; AccountUid mUserId; @@ -85,4 +88,3 @@ void sortTitles(void); void rotateSortMode(void); void refreshDirectories(u64 id); std::unordered_map<std::string, std::string> getCompleteTitleList(void); - diff --git a/include/nxst/domain/transfer_state.hpp b/include/nxst/domain/transfer_state.hpp index d3adc61..5255891 100644 --- a/include/nxst/domain/transfer_state.hpp +++ b/include/nxst/domain/transfer_state.hpp @@ -4,22 +4,22 @@ #include <string> struct TransferState { - std::atomic<bool> done{false}; - std::atomic<bool> cancelled{false}; - std::atomic<bool> connection_failed{false}; + std::atomic<bool> done{false}; + std::atomic<bool> cancelled{false}; + std::atomic<bool> connection_failed{false}; std::atomic<uint64_t> bytes_done{0}; std::atomic<uint64_t> bytes_total{0}; - std::string status; - std::string fail_reason; + std::string status; + std::string fail_reason; mutable std::mutex status_mutex; void reset() { - done = false; - cancelled = false; + done = false; + cancelled = false; connection_failed = false; - bytes_done = 0; - bytes_total = 0; + bytes_done = 0; + bytes_total = 0; fail_reason.clear(); std::lock_guard<std::mutex> lock(status_mutex); status.clear(); diff --git a/include/nxst/domain/util.hpp b/include/nxst/domain/util.hpp index 89db34c..e9b4aa6 100644 --- a/include/nxst/domain/util.hpp +++ b/include/nxst/domain/util.hpp @@ -25,11 +25,13 @@ */ #pragma once +#include <sys/stat.h> + +#include <switch.h> + #include <nxst/domain/account.hpp> #include <nxst/domain/common.hpp> #include <nxst/infra/fs/io.hpp> -#include <switch.h> -#include <sys/stat.h> // debug #include <arpa/inet.h> @@ -42,10 +44,8 @@ HidsysNotificationLedPattern blinkLedPattern(u8 times); void blinkLed(u8 times); namespace StringUtils { - std::string removeAccents(std::string str); - std::string removeNotAscii(std::string str); - std::u16string UTF8toUTF16(const char* src); - std::string elide(const std::string& s, size_t maxChars); -} - - +std::string removeAccents(std::string str); +std::string removeNotAscii(std::string str); +std::u16string UTF8toUTF16(const char* src); +std::string elide(const std::string& s, size_t maxChars); +} // namespace StringUtils diff --git a/include/nxst/infra/fs/directory.hpp b/include/nxst/infra/fs/directory.hpp index 2ba7a75..826f4b0 100644 --- a/include/nxst/infra/fs/directory.hpp +++ b/include/nxst/infra/fs/directory.hpp @@ -28,16 +28,17 @@ #include <dirent.h> #include <errno.h> #include <string> -#include <switch.h> #include <vector> +#include <switch.h> + struct DirectoryEntry { std::string name; bool directory; }; class Directory { -public: + public: Directory(const std::string& root); ~Directory() = default; @@ -47,9 +48,8 @@ public: bool good(void); size_t size(void); -private: + private: std::vector<struct DirectoryEntry> mList; Result mError; bool mGood; }; - diff --git a/include/nxst/infra/fs/filesystem.hpp b/include/nxst/infra/fs/filesystem.hpp index 0b17f8d..77f3827 100644 --- a/include/nxst/infra/fs/filesystem.hpp +++ b/include/nxst/infra/fs/filesystem.hpp @@ -25,12 +25,12 @@ */ #pragma once -#include <nxst/domain/account.hpp> #include <switch.h> -namespace FileSystem { - Result mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID); - int mount(FsFileSystem fs); - void unmount(void); -} +#include <nxst/domain/account.hpp> +namespace FileSystem { +Result mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID); +int mount(FsFileSystem fs); +void unmount(void); +} // namespace FileSystem diff --git a/include/nxst/infra/fs/handles.hpp b/include/nxst/infra/fs/handles.hpp index be99c43..b2cefc8 100644 --- a/include/nxst/infra/fs/handles.hpp +++ b/include/nxst/infra/fs/handles.hpp @@ -1,5 +1,6 @@ #pragma once #include <cstdio> + #include <switch.h> namespace nxst { @@ -10,14 +11,21 @@ struct FsFileSystemHandle { bool valid{false}; FsFileSystemHandle() = default; - ~FsFileSystemHandle() { if (valid) fsFsClose(&fs); } // NOLINT(modernize-use-equals-default) + ~FsFileSystemHandle() { + if (valid) + fsFsClose(&fs); + } // NOLINT(modernize-use-equals-default) - FsFileSystemHandle(const FsFileSystemHandle&) = delete; + FsFileSystemHandle(const FsFileSystemHandle&) = delete; FsFileSystemHandle& operator=(const FsFileSystemHandle&) = delete; - FsFileSystem* get() { return &fs; } + FsFileSystem* get() { + return &fs; + } - void release() { valid = false; } // transfer ownership to devfs + void release() { + valid = false; + } // transfer ownership to devfs }; // RAII wrapper for FILE* — auto-fclose on destruction. @@ -25,13 +33,20 @@ struct FileHandle { FILE* ptr{nullptr}; explicit FileHandle(FILE* file) : ptr(file) {} - ~FileHandle() { if (ptr != nullptr) fclose(ptr); } // NOLINT(modernize-use-equals-default) + ~FileHandle() { + if (ptr != nullptr) + fclose(ptr); + } // NOLINT(modernize-use-equals-default) - FileHandle(const FileHandle&) = delete; + FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; - explicit operator bool() const { return ptr != nullptr; } - FILE* get() const { return ptr; } + explicit operator bool() const { + return ptr != nullptr; + } + FILE* get() const { + return ptr; + } }; -} // namespace nxst +} // namespace nxst diff --git a/include/nxst/infra/fs/io.hpp b/include/nxst/infra/fs/io.hpp index 6ecea44..920d73f 100644 --- a/include/nxst/infra/fs/io.hpp +++ b/include/nxst/infra/fs/io.hpp @@ -25,27 +25,29 @@ */ #pragma once -#include <nxst/domain/account.hpp> -#include <nxst/domain/result.hpp> -#include <nxst/infra/fs/directory.hpp> -#include <nxst/domain/title.hpp> -#include <nxst/domain/util.hpp> #include <dirent.h> -#include <switch.h> #include <sys/stat.h> #include <unistd.h> +#include <switch.h> + +#include <nxst/domain/account.hpp> +#include <nxst/domain/result.hpp> +#include <nxst/domain/title.hpp> +#include <nxst/domain/util.hpp> +#include <nxst/infra/fs/directory.hpp> + #define BUFFER_SIZE 0x80000 namespace io { - nxst::Result<std::string> backup(size_t index, AccountUid uid); - nxst::Result<std::string> restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell); - - Result copyDirectory(const std::string& srcPath, const std::string& dstPath); - void copyFile(const std::string& srcPath, const std::string& dstPath); - Result createDirectory(const std::string& path); - Result deleteFolderRecursively(const std::string& path); - bool directoryExists(const std::string& path); - bool fileExists(const std::string& path); -} +nxst::Result<std::string> backup(size_t index, AccountUid uid); +nxst::Result<std::string> restore(size_t index, AccountUid uid, size_t cellIndex, + const std::string& nameFromCell); +Result copyDirectory(const std::string& srcPath, const std::string& dstPath); +void copyFile(const std::string& srcPath, const std::string& dstPath); +Result createDirectory(const std::string& path); +Result deleteFolderRecursively(const std::string& path); +bool directoryExists(const std::string& path); +bool fileExists(const std::string& path); +} // namespace io diff --git a/include/nxst/infra/net/socket.hpp b/include/nxst/infra/net/socket.hpp index 0dc2371..5fdf054 100644 --- a/include/nxst/infra/net/socket.hpp +++ b/include/nxst/infra/net/socket.hpp @@ -6,13 +6,24 @@ struct Socket { Socket() = default; explicit Socket(int fd) : fd(fd) {} - ~Socket() { if (fd >= 0) close(fd); } + ~Socket() { + if (fd >= 0) + close(fd); + } - Socket(const Socket&) = delete; + Socket(const Socket&) = delete; Socket& operator=(const Socket&) = delete; - Socket(Socket&& o) : fd(o.fd) { o.fd = -1; } + Socket(Socket&& o) : fd(o.fd) { + o.fd = -1; + } - operator int() const { return fd; } - bool valid() const { return fd >= 0; } - void release() { fd = -1; } + operator int() const { + return fd; + } + bool valid() const { + return fd >= 0; + } + void release() { + fd = -1; + } }; diff --git a/include/nxst/infra/sys/logger.hpp b/include/nxst/infra/sys/logger.hpp index d14be5a..f8f6879 100644 --- a/include/nxst/infra/sys/logger.hpp +++ b/include/nxst/infra/sys/logger.hpp @@ -9,47 +9,51 @@ namespace nxst::log { enum class Level { Debug, Info, Warn, Error }; void write(Level level, const char* fmt, ...) __attribute__((format(printf, 2, 3))); -void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2))); -void info (const char* fmt, ...) __attribute__((format(printf, 1, 2))); -void warn (const char* fmt, ...) __attribute__((format(printf, 1, 2))); -void error(const char* fmt, ...) __attribute__((format(printf, 1, 2))); +void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2))); +void info(const char* fmt, ...) __attribute__((format(printf, 1, 2))); +void warn(const char* fmt, ...) __attribute__((format(printf, 1, 2))); +void error(const char* fmt, ...) __attribute__((format(printf, 1, 2))); // No-op: writes are immediate. Kept for source compatibility during migration. inline void flush() {} -} // namespace nxst::log +} // namespace nxst::log // Backward-compat shim — existing Logger::getInstance().log(...) call sites compile // unchanged. Format args are dropped (same behavior as broken original). Migrate // call sites to nxst::log::* in Phase 3. struct Logger { - static Logger& getInstance() - { + static Logger& getInstance() { static Logger instance; return instance; } // clang-tidy naming suppressed: these must match existing call sites during migration. - static constexpr const char* INFO = "[INFO]"; // NOLINT(readability-identifier-naming) + static constexpr const char* INFO = "[INFO]"; // NOLINT(readability-identifier-naming) static constexpr const char* DEBUG = "[DEBUG]"; // NOLINT(readability-identifier-naming) static constexpr const char* ERROR = "[ERROR]"; // NOLINT(readability-identifier-naming) - static constexpr const char* WARN = "[WARN]"; // NOLINT(readability-identifier-naming) + static constexpr const char* WARN = "[WARN]"; // NOLINT(readability-identifier-naming) - static void flush() { nxst::log::flush(); } + static void flush() { + nxst::log::flush(); + } // Args intentionally dropped — format string still logged for visibility. template <typename... Args> - void log(const std::string& level, const std::string& fmt, Args&&... /*args*/) - { - if (level == ERROR) nxst::log::error("%s", fmt.c_str()); - else if (level == WARN) nxst::log::warn("%s", fmt.c_str()); - else if (level == DEBUG) nxst::log::debug("%s", fmt.c_str()); - else nxst::log::info("%s", fmt.c_str()); + void log(const std::string& level, const std::string& fmt, Args&&... /*args*/) { + if (level == ERROR) + nxst::log::error("%s", fmt.c_str()); + else if (level == WARN) + nxst::log::warn("%s", fmt.c_str()); + else if (level == DEBUG) + nxst::log::debug("%s", fmt.c_str()); + else + nxst::log::info("%s", fmt.c_str()); } - Logger() = default; + Logger() = default; ~Logger() = default; - Logger(const Logger&) = delete; // NOLINT(modernize-use-equals-delete) + Logger(const Logger&) = delete; // NOLINT(modernize-use-equals-delete) Logger& operator=(const Logger&) = delete; // NOLINT(modernize-use-equals-delete) }; diff --git a/include/nxst/service/transfer_service.hpp b/include/nxst/service/transfer_service.hpp index 677b206..8093374 100644 --- a/include/nxst/service/transfer_service.hpp +++ b/include/nxst/service/transfer_service.hpp @@ -2,75 +2,112 @@ #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); + 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; } + 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); + 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 { + 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; } + 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: + private: // Sender - TransferState sender_state; - std::atomic<int> sender_udp_sock{-1}; - std::atomic<int> sender_tcp_sock{-1}; + 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}; + 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{}; + pthread_t receiver_bcast_thread{}; // Stored at startReceive, read after network transfer completes - size_t restore_title_index{0}; - AccountUid restore_uid{}; + size_t restore_title_index{0}; + AccountUid restore_uid{}; std::string restore_title_name; - bool restore_ok{false}; + bool restore_ok{false}; std::string restore_error; // Sender thread - struct SenderArgs { TransferService* svc; size_t title_index; AccountUid uid; }; + 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); + 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; }; + 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; + void runBroadcast(); + void runAccept(int server_fd); + std::string replaceUsername(const std::string& file_path) const; }; -} // namespace nxst +} // namespace nxst diff --git a/include/nxst/ui/card.hpp b/include/nxst/ui/card.hpp index ddc9517..6d62e86 100644 --- a/include/nxst/ui/card.hpp +++ b/include/nxst/ui/card.hpp @@ -1,18 +1,18 @@ #pragma once #include <pu/Plutonium> + #include <nxst/ui/theme.hpp> namespace ui { - class Card { - public: - pu::ui::elm::Rectangle::Ref bg; +class Card { + public: + pu::ui::elm::Rectangle::Ref bg; - Card(pu::ui::Layout* parent, int x, int y, int w, int h, - pu::ui::Color color = theme::color::BgSurface, - int rad = theme::radius::lg) { - bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad); - parent->Add(bg); - } - }; -} + Card(pu::ui::Layout* parent, int x, int y, int w, int h, pu::ui::Color color = theme::color::BgSurface, + int rad = theme::radius::lg) { + bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad); + parent->Add(bg); + } +}; +} // namespace ui diff --git a/include/nxst/ui/header_bar.hpp b/include/nxst/ui/header_bar.hpp index fbb19ae..6a79a41 100644 --- a/include/nxst/ui/header_bar.hpp +++ b/include/nxst/ui/header_bar.hpp @@ -1,86 +1,83 @@ #pragma once #include <pu/Plutonium> + +#include <nxst/domain/account.hpp> #include <nxst/ui/theme.hpp> #include <nxst/ui/ui_context.hpp> -#include <nxst/domain/account.hpp> namespace ui { - class HeaderBar { - private: - pu::ui::elm::Rectangle::Ref bg; - pu::ui::elm::Rectangle::Ref divider; - pu::ui::elm::TextBlock::Ref appName; - pu::ui::elm::TextBlock::Ref subtitle; - pu::ui::elm::Rectangle::Ref chipBg; - pu::ui::elm::Image::Ref avatar; - pu::ui::elm::TextBlock::Ref userName; +class HeaderBar { + private: + pu::ui::elm::Rectangle::Ref bg; + pu::ui::elm::Rectangle::Ref divider; + pu::ui::elm::TextBlock::Ref appName; + pu::ui::elm::TextBlock::Ref subtitle; + pu::ui::elm::Rectangle::Ref chipBg; + pu::ui::elm::Image::Ref avatar; + pu::ui::elm::TextBlock::Ref userName; - public: - HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") { - using namespace theme; + public: + HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") { + using namespace theme; - bg = pu::ui::elm::Rectangle::New( - 0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface); - divider = pu::ui::elm::Rectangle::New( - 0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider); + bg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface); + divider = pu::ui::elm::Rectangle::New(0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider); - appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST"); - appName->SetFont(type::font(type::Title)); - appName->SetColor(color::TextPrimary); + appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST"); + appName->SetFont(type::font(type::Title)); + appName->SetColor(color::TextPrimary); - subtitle = pu::ui::elm::TextBlock::New(space::lg, 46, sub); - subtitle->SetFont(type::font(type::Caption)); - subtitle->SetColor(color::TextMuted); + subtitle = pu::ui::elm::TextBlock::New(space::lg, 46, sub); + subtitle->SetFont(type::font(type::Caption)); + subtitle->SetColor(color::TextMuted); - const int chipW = 280; - const int chipX = layout::ScreenW - chipW - space::lg; - chipBg = pu::ui::elm::Rectangle::New( - chipX, 16, chipW, 40, - color::BgSurface2, radius::pill); - chipBg->SetVisible(false); + const int chipW = 280; + const int chipX = layout::ScreenW - chipW - space::lg; + chipBg = pu::ui::elm::Rectangle::New(chipX, 16, chipW, 40, color::BgSurface2, radius::pill); + chipBg->SetVisible(false); - avatar = pu::ui::elm::Image::New(chipX + 4, 20, ""); - avatar->SetWidth(32); - avatar->SetHeight(32); - avatar->SetVisible(false); + avatar = pu::ui::elm::Image::New(chipX + 4, 20, ""); + avatar->SetWidth(32); + avatar->SetHeight(32); + avatar->SetVisible(false); - userName = pu::ui::elm::TextBlock::New(chipX + 44, 24, ""); - userName->SetFont(type::font(type::Body)); - userName->SetColor(color::TextPrimary); - userName->SetVisible(false); + userName = pu::ui::elm::TextBlock::New(chipX + 44, 24, ""); + userName->SetFont(type::font(type::Body)); + userName->SetColor(color::TextPrimary); + userName->SetVisible(false); - parent->Add(bg); - parent->Add(divider); - parent->Add(appName); - parent->Add(subtitle); - parent->Add(chipBg); - parent->Add(avatar); - parent->Add(userName); - } + parent->Add(bg); + parent->Add(divider); + parent->Add(appName); + parent->Add(subtitle); + parent->Add(chipBg); + parent->Add(avatar); + parent->Add(userName); + } - void SetUser(const std::optional<AccountUid>& uid, const std::string& name) { - const bool show = uid.has_value(); - chipBg->SetVisible(show); - userName->SetVisible(show); - if (show) { - userName->SetText(name); - std::string path = Account::iconPath(*uid); - if (!path.empty()) { - avatar->SetImage(path); - avatar->SetWidth(32); - avatar->SetHeight(32); - avatar->SetVisible(avatar->IsImageValid()); - } else { - avatar->SetVisible(false); - } + void SetUser(const std::optional<AccountUid>& uid, const std::string& name) { + const bool show = uid.has_value(); + chipBg->SetVisible(show); + userName->SetVisible(show); + if (show) { + userName->SetText(name); + std::string path = Account::iconPath(*uid); + if (!path.empty()) { + avatar->SetImage(path); + avatar->SetWidth(32); + avatar->SetHeight(32); + avatar->SetVisible(avatar->IsImageValid()); } else { avatar->SetVisible(false); } + } else { + avatar->SetVisible(false); } + } - void SetSubtitle(const std::string& text) { - subtitle->SetText(text); - } - }; -} + void SetSubtitle(const std::string& text) { + subtitle->SetText(text); + } +}; +} // namespace ui diff --git a/include/nxst/ui/hint_bar.hpp b/include/nxst/ui/hint_bar.hpp index 0cb5ab5..89a90d3 100644 --- a/include/nxst/ui/hint_bar.hpp +++ b/include/nxst/ui/hint_bar.hpp @@ -1,55 +1,56 @@ #pragma once -#include <pu/Plutonium> -#include <nxst/ui/theme.hpp> -#include <vector> #include <string> +#include <vector> + +#include <pu/Plutonium> + +#include <nxst/ui/theme.hpp> namespace ui { - struct Hint { - std::string glyph; - std::string label; - }; +struct Hint { + std::string glyph; + std::string label; +}; - class HintBar { - private: - pu::ui::Layout* parent; - pu::ui::elm::Rectangle::Ref bg; - pu::ui::elm::Rectangle::Ref divider; - std::vector<pu::ui::elm::TextBlock::Ref> labels; +class HintBar { + private: + pu::ui::Layout* parent; + pu::ui::elm::Rectangle::Ref bg; + pu::ui::elm::Rectangle::Ref divider; + std::vector<pu::ui::elm::TextBlock::Ref> labels; - public: - HintBar(pu::ui::Layout* p) : parent(p) { - using namespace theme; - bg = pu::ui::elm::Rectangle::New( - 0, layout::ScreenH - layout::HintH, - layout::ScreenW, layout::HintH, color::BgSurface); - divider = pu::ui::elm::Rectangle::New( - 0, layout::ScreenH - layout::HintH, - layout::ScreenW, 1, color::Divider); - parent->Add(bg); - parent->Add(divider); + public: + HintBar(pu::ui::Layout* p) : parent(p) { + using namespace theme; + bg = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, layout::HintH, + color::BgSurface); + divider = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, 1, + color::Divider); + parent->Add(bg); + parent->Add(divider); + } + + void SetHints(const std::vector<Hint>& hints) { + using namespace theme; + for (auto& l : labels) + l->SetVisible(false); + labels.clear(); + + int x = layout::ScreenW - space::lg; + int y = layout::ScreenH - layout::HintH + 18; + for (auto it = hints.rbegin(); it != hints.rend(); ++it) { + std::string text = it->glyph + " " + it->label; + auto tb = pu::ui::elm::TextBlock::New(0, y, text); + tb->SetFont(type::font(type::Label)); + tb->SetColor(color::TextSecondary); + int w = tb->GetWidth(); + x -= w; + tb->SetX(x); + x -= space::xl; + parent->Add(tb); + labels.push_back(tb); } - - void SetHints(const std::vector<Hint>& hints) { - using namespace theme; - for (auto& l : labels) l->SetVisible(false); - labels.clear(); - - int x = layout::ScreenW - space::lg; - int y = layout::ScreenH - layout::HintH + 18; - for (auto it = hints.rbegin(); it != hints.rend(); ++it) { - std::string text = it->glyph + " " + it->label; - auto tb = pu::ui::elm::TextBlock::New(0, y, text); - tb->SetFont(type::font(type::Label)); - tb->SetColor(color::TextSecondary); - int w = tb->GetWidth(); - x -= w; - tb->SetX(x); - x -= space::xl; - parent->Add(tb); - labels.push_back(tb); - } - } - }; -} + } +}; +} // namespace ui diff --git a/include/nxst/ui/theme.hpp b/include/nxst/ui/theme.hpp index f32f31a..7a3b677 100644 --- a/include/nxst/ui/theme.hpp +++ b/include/nxst/ui/theme.hpp @@ -1,78 +1,79 @@ #pragma once -#include <pu/Plutonium> #include <string> +#include <pu/Plutonium> + namespace theme { - using pu::ui::Color; +using pu::ui::Color; - namespace color { - constexpr Color BgBase{0x10, 0x14, 0x1C, 0xFF}; - constexpr Color BgSurface{0x18, 0x1F, 0x2A, 0xFF}; - constexpr Color BgSurface2{0x22, 0x2B, 0x39, 0xFF}; - constexpr Color Scrim{0x00, 0x00, 0x00, 0xB8}; +namespace color { +constexpr Color BgBase{0x10, 0x14, 0x1C, 0xFF}; +constexpr Color BgSurface{0x18, 0x1F, 0x2A, 0xFF}; +constexpr Color BgSurface2{0x22, 0x2B, 0x39, 0xFF}; +constexpr Color Scrim{0x00, 0x00, 0x00, 0xB8}; - constexpr Color Primary{0xE2, 0x4B, 0x55, 0xFF}; - constexpr Color PrimaryDim{0x9C, 0x33, 0x3A, 0xFF}; - constexpr Color Accent{0x4A, 0xC2, 0xE0, 0xFF}; +constexpr Color Primary{0xE2, 0x4B, 0x55, 0xFF}; +constexpr Color PrimaryDim{0x9C, 0x33, 0x3A, 0xFF}; +constexpr Color Accent{0x4A, 0xC2, 0xE0, 0xFF}; - constexpr Color TextPrimary{0xF2, 0xF4, 0xF8, 0xFF}; - constexpr Color TextSecondary{0xB6, 0xBE, 0xCB, 0xFF}; - constexpr Color TextMuted{0x70, 0x7A, 0x8C, 0xFF}; +constexpr Color TextPrimary{0xF2, 0xF4, 0xF8, 0xFF}; +constexpr Color TextSecondary{0xB6, 0xBE, 0xCB, 0xFF}; +constexpr Color TextMuted{0x70, 0x7A, 0x8C, 0xFF}; - constexpr Color Success{0x55, 0xC8, 0x8A, 0xFF}; - constexpr Color Error{0xE0, 0x6C, 0x6C, 0xFF}; - constexpr Color Warning{0xE6, 0xB4, 0x55, 0xFF}; +constexpr Color Success{0x55, 0xC8, 0x8A, 0xFF}; +constexpr Color Error{0xE0, 0x6C, 0x6C, 0xFF}; +constexpr Color Warning{0xE6, 0xB4, 0x55, 0xFF}; - constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF}; - constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF}; - } +constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF}; +constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF}; +} // namespace color - namespace space { - constexpr int xs = 4; - constexpr int sm = 8; - constexpr int md = 16; - constexpr int lg = 24; - constexpr int xl = 32; - constexpr int xxl = 48; - } +namespace space { +constexpr int xs = 4; +constexpr int sm = 8; +constexpr int md = 16; +constexpr int lg = 24; +constexpr int xl = 32; +constexpr int xxl = 48; +} // namespace space - namespace radius { - constexpr int sm = 6; - constexpr int md = 12; - constexpr int lg = 20; - constexpr int pill = 9999; - } +namespace radius { +constexpr int sm = 6; +constexpr int md = 12; +constexpr int lg = 20; +constexpr int pill = 9999; +} // namespace radius - namespace type { - constexpr int Display = 38; - constexpr int Title = 30; - constexpr int Body = 25; - constexpr int Label = 20; - constexpr int Caption = 18; +namespace type { +constexpr int Display = 38; +constexpr int Title = 30; +constexpr int Body = 25; +constexpr int Label = 20; +constexpr int Caption = 18; - inline std::string font(int size) { - return "DefaultFont@" + std::to_string(size); - } - } - - namespace layout { - constexpr int ScreenW = 1280; - constexpr int ScreenH = 720; - constexpr int HeaderH = 72; - constexpr int HintH = 56; - constexpr int ContentTop = HeaderH; - constexpr int ContentH = ScreenH - HeaderH - HintH; - } - - namespace motion { - constexpr int FadeFrames = 20; - constexpr int SlideFrames = 14; - constexpr int SpinnerFrames = 72; - } - - namespace font { - constexpr const char* Default = "Inter"; - constexpr const char* Medium = "InterMedium"; - } +inline std::string font(int size) { + return "DefaultFont@" + std::to_string(size); } +} // namespace type + +namespace layout { +constexpr int ScreenW = 1280; +constexpr int ScreenH = 720; +constexpr int HeaderH = 72; +constexpr int HintH = 56; +constexpr int ContentTop = HeaderH; +constexpr int ContentH = ScreenH - HeaderH - HintH; +} // namespace layout + +namespace motion { +constexpr int FadeFrames = 20; +constexpr int SlideFrames = 14; +constexpr int SpinnerFrames = 72; +} // namespace motion + +namespace font { +constexpr const char* Default = "Inter"; +constexpr const char* Medium = "InterMedium"; +} // namespace font +} // namespace theme diff --git a/include/nxst/ui/titles_layout.hpp b/include/nxst/ui/titles_layout.hpp index 15a220e..f20dbae 100644 --- a/include/nxst/ui/titles_layout.hpp +++ b/include/nxst/ui/titles_layout.hpp @@ -1,59 +1,63 @@ #pragma once -#include <pu/Plutonium> -#include <nxst/ui/const.h> -#include <nxst/domain/title.hpp> -#include <nxst/domain/account.hpp> +#include <memory> #include <unordered_map> #include <vector> -#include <memory> + +#include <pu/Plutonium> + +#include <nxst/domain/account.hpp> +#include <nxst/domain/title.hpp> +#include <nxst/ui/const.h> #include <nxst/ui/header_bar.hpp> #include <nxst/ui/hint_bar.hpp> namespace ui { - enum class TitlesFocus { List, Actions }; - enum class TitlesAction { Transfer, Receive }; +enum class TitlesFocus { List, Actions }; +enum class TitlesAction { Transfer, Receive }; - class TitlesLayout : public pu::ui::Layout { - private: +class TitlesLayout : public pu::ui::Layout { + private: + pu::ui::elm::Menu::Ref titlesMenu; + std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache; + bool m_inputLocked = false; + std::unique_ptr<HeaderBar> header; + std::unique_ptr<HintBar> hints; - pu::ui::elm::Menu::Ref titlesMenu; - std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache; - bool m_inputLocked = false; - std::unique_ptr<HeaderBar> header; - std::unique_ptr<HintBar> hints; + pu::ui::elm::Rectangle::Ref panelBg; + pu::ui::elm::TextBlock::Ref panelTitle; + pu::ui::elm::TextBlock::Ref panelHint; + pu::ui::elm::Rectangle::Ref btnTransferBg; + pu::ui::elm::TextBlock::Ref btnTransferText; + pu::ui::elm::Rectangle::Ref btnReceiveBg; + pu::ui::elm::TextBlock::Ref btnReceiveText; + pu::ui::elm::TextBlock::Ref panelFooter; + pu::ui::elm::TextBlock::Ref emptyText; + pu::ui::elm::TextBlock::Ref emptySub; - pu::ui::elm::Rectangle::Ref panelBg; - pu::ui::elm::TextBlock::Ref panelTitle; - pu::ui::elm::TextBlock::Ref panelHint; - pu::ui::elm::Rectangle::Ref btnTransferBg; - pu::ui::elm::TextBlock::Ref btnTransferText; - pu::ui::elm::Rectangle::Ref btnReceiveBg; - pu::ui::elm::TextBlock::Ref btnReceiveText; - pu::ui::elm::TextBlock::Ref panelFooter; - pu::ui::elm::TextBlock::Ref emptyText; - pu::ui::elm::TextBlock::Ref emptySub; + AccountUid current_uid{}; + TitlesFocus focus = TitlesFocus::List; + TitlesAction action = TitlesAction::Transfer; + int lockedListIndex = 0; - AccountUid current_uid{}; - TitlesFocus focus = TitlesFocus::List; - TitlesAction action = TitlesAction::Transfer; - int lockedListIndex = 0; + void refreshPanel(); + void refreshButtons(); + void updateHints(); + void runTransfer(int index, Title& title); + void runReceive(int index, Title& title); - void refreshPanel(); - void refreshButtons(); - void updateHints(); - void runTransfer(int index, Title& title); - void runReceive(int index, Title& title); + public: + TitlesLayout(); + void InitTitles(AccountUid uid); + void LockInput() { + m_inputLocked = true; + } + void UnlockInput() { + m_inputLocked = false; + } - public: + void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); - TitlesLayout(); - void InitTitles(AccountUid uid); - void LockInput() { m_inputLocked = true; } - void UnlockInput() { m_inputLocked = false; } - - void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); - - PU_SMART_CTOR(TitlesLayout) - }; -} + PU_SMART_CTOR(TitlesLayout) +}; +} // namespace ui diff --git a/include/nxst/ui/transfer_overlay.hpp b/include/nxst/ui/transfer_overlay.hpp index a7b02b5..16db4e0 100644 --- a/include/nxst/ui/transfer_overlay.hpp +++ b/include/nxst/ui/transfer_overlay.hpp @@ -1,94 +1,84 @@ #pragma once #include <pu/Plutonium> -#include <nxst/ui/theme.hpp> + #include <nxst/domain/util.hpp> +#include <nxst/ui/theme.hpp> namespace ui { - class TransferOverlay : public pu::ui::Overlay { - private: - pu::ui::elm::Rectangle::Ref card; - pu::ui::elm::TextBlock::Ref titleText; - pu::ui::elm::TextBlock::Ref statusText; - pu::ui::elm::Rectangle::Ref progressTrack; - pu::ui::elm::ProgressBar::Ref progressBar; - pu::ui::elm::TextBlock::Ref indeterminateText; - pu::ui::elm::TextBlock::Ref hintText; +class TransferOverlay : public pu::ui::Overlay { + private: + pu::ui::elm::Rectangle::Ref card; + pu::ui::elm::TextBlock::Ref titleText; + pu::ui::elm::TextBlock::Ref statusText; + pu::ui::elm::Rectangle::Ref progressTrack; + pu::ui::elm::ProgressBar::Ref progressBar; + pu::ui::elm::TextBlock::Ref indeterminateText; + pu::ui::elm::TextBlock::Ref hintText; - static constexpr int CardW = 720; - static constexpr int CardH = 360; - static constexpr int CardX = (theme::layout::ScreenW - CardW) / 2; - static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2; + static constexpr int CardW = 720; + static constexpr int CardH = 360; + static constexpr int CardX = (theme::layout::ScreenW - CardW) / 2; + static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2; - public: - TransferOverlay(const std::string &title) - : Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) - { - using namespace theme; + public: + TransferOverlay(const std::string& title) + : Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) { + using namespace theme; - card = pu::ui::elm::Rectangle::New( - CardX, CardY, CardW, CardH, color::BgSurface, radius::lg); + card = pu::ui::elm::Rectangle::New(CardX, CardY, CardW, CardH, color::BgSurface, radius::lg); - titleText = pu::ui::elm::TextBlock::New( - CardX + space::lg, CardY + space::lg, title); - titleText->SetFont(type::font(type::Title)); - titleText->SetColor(color::TextPrimary); + titleText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg, title); + titleText->SetFont(type::font(type::Title)); + titleText->SetColor(color::TextPrimary); - statusText = pu::ui::elm::TextBlock::New( - CardX + space::lg, - CardY + space::lg + 56, - ""); - statusText->SetFont(type::font(type::Body)); - statusText->SetColor(color::TextSecondary); + statusText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg + 56, ""); + statusText->SetFont(type::font(type::Body)); + statusText->SetColor(color::TextSecondary); - int barX = CardX + space::lg; - int barY = CardY + space::lg + 56 + 56; - int barW = CardW - 2 * space::lg; + int barX = CardX + space::lg; + int barY = CardY + space::lg + 56 + 56; + int barW = CardW - 2 * space::lg; - progressTrack = pu::ui::elm::Rectangle::New( - barX, barY, barW, 8, color::Divider, radius::sm); + progressTrack = pu::ui::elm::Rectangle::New(barX, barY, barW, 8, color::Divider, radius::sm); - progressBar = pu::ui::elm::ProgressBar::New( - barX, barY, barW, 8, 100.0); - progressBar->SetProgressColor(color::Primary); - progressBar->SetBackgroundColor(color::Divider); + progressBar = pu::ui::elm::ProgressBar::New(barX, barY, barW, 8, 100.0); + progressBar->SetProgressColor(color::Primary); + progressBar->SetBackgroundColor(color::Divider); - indeterminateText = pu::ui::elm::TextBlock::New( - barX, barY - 4, "Preparing transfer..."); - indeterminateText->SetFont(type::font(type::Body)); - indeterminateText->SetColor(color::TextMuted); - indeterminateText->SetVisible(false); + indeterminateText = pu::ui::elm::TextBlock::New(barX, barY - 4, "Preparing transfer..."); + indeterminateText->SetFont(type::font(type::Body)); + indeterminateText->SetColor(color::TextMuted); + indeterminateText->SetVisible(false); - hintText = pu::ui::elm::TextBlock::New( - CardX + space::lg, - CardY + CardH - space::lg - 18, - "B to cancel"); - hintText->SetFont(type::font(type::Caption)); - hintText->SetColor(color::TextMuted); + hintText = + pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + CardH - space::lg - 18, "B to cancel"); + hintText->SetFont(type::font(type::Caption)); + hintText->SetColor(color::TextMuted); - this->Add(card); - this->Add(titleText); - this->Add(statusText); - this->Add(progressTrack); - this->Add(progressBar); - this->Add(indeterminateText); - this->Add(hintText); - } - PU_SMART_CTOR(TransferOverlay) + this->Add(card); + this->Add(titleText); + this->Add(statusText); + this->Add(progressTrack); + this->Add(progressBar); + this->Add(indeterminateText); + this->Add(hintText); + } + PU_SMART_CTOR(TransferOverlay) - void SetStatus(const std::string &status) { - statusText->SetText(StringUtils::elide(status, 56)); - } + void SetStatus(const std::string& status) { + statusText->SetText(StringUtils::elide(status, 56)); + } - void SetProgress(double val) { - progressBar->SetProgress(val); - } + void SetProgress(double val) { + progressBar->SetProgress(val); + } - void SetProgressVisible(bool visible) { - progressTrack->SetVisible(visible); - progressBar->SetVisible(visible); - indeterminateText->SetVisible(!visible); - } - }; + void SetProgressVisible(bool visible) { + progressTrack->SetVisible(visible); + progressBar->SetVisible(visible); + indeterminateText->SetVisible(!visible); + } +}; -} +} // namespace ui diff --git a/include/nxst/ui/ui_context.hpp b/include/nxst/ui/ui_context.hpp index 8f1b6d7..18e2f99 100644 --- a/include/nxst/ui/ui_context.hpp +++ b/include/nxst/ui/ui_context.hpp @@ -1,12 +1,14 @@ #pragma once -#include <string> #include <optional> +#include <string> + #include <switch.h> + #include <nxst/domain/account.hpp> namespace ui { - struct UiContext { - std::optional<AccountUid> selectedUser; - std::string selectedUserName; - }; -} +struct UiContext { + std::optional<AccountUid> selectedUser; + std::string selectedUserName; +}; +} // namespace ui diff --git a/include/nxst/ui/users_layout.hpp b/include/nxst/ui/users_layout.hpp index ca46b94..e01072d 100644 --- a/include/nxst/ui/users_layout.hpp +++ b/include/nxst/ui/users_layout.hpp @@ -1,29 +1,28 @@ +#include <memory> + #include <pu/Plutonium> + #include <nxst/ui/const.h> #include <nxst/ui/header_bar.hpp> #include <nxst/ui/hint_bar.hpp> -#include <memory> namespace ui { - class UsersLayout : public pu::ui::Layout { - private: +class UsersLayout : public pu::ui::Layout { + private: + pu::ui::elm::Menu::Ref usersMenu; + pu::ui::elm::Rectangle::Ref loadingBg; + pu::ui::elm::TextBlock::Ref loadingText; + std::unique_ptr<HeaderBar> header; + std::unique_ptr<HintBar> hints; - pu::ui::elm::Menu::Ref usersMenu; - pu::ui::elm::Rectangle::Ref loadingBg; - pu::ui::elm::TextBlock::Ref loadingText; - std::unique_ptr<HeaderBar> header; - std::unique_ptr<HintBar> hints; + public: + UsersLayout(); - public: + void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); - UsersLayout(); + int32_t GetCurrentIndex(); - void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); - - int32_t GetCurrentIndex(); - - PU_SMART_CTOR(UsersLayout) - - }; -} + PU_SMART_CTOR(UsersLayout) +}; +} // namespace ui diff --git a/src/app/main.cpp b/src/app/main.cpp index 89ac179..12654c2 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,9 +1,12 @@ -#include <nxst/app/main_application.hpp> -#include <nxst/domain/util.hpp> -#include <nxst/app/main.hpp> #include <unistd.h> -namespace ui { extern MainApplication* mainApp; } +#include <nxst/app/main.hpp> +#include <nxst/app/main_application.hpp> +#include <nxst/domain/util.hpp> + +namespace ui { +extern MainApplication* mainApp; +} static int nxlink_sock = -1; @@ -25,9 +28,9 @@ extern "C" void userAppExit() { if (ui::mainApp) { ui::mainApp->transfer.cancelReceive(); ui::mainApp->transfer.cancelSend(); - for (int i = 0; i < 150 && - (!ui::mainApp->transfer.isReceiveWorkersIdle() || - !ui::mainApp->transfer.isSendWorkersIdle()); i++) { + for (int i = 0; i < 150 && (!ui::mainApp->transfer.isReceiveWorkersIdle() || + !ui::mainApp->transfer.isSendWorkersIdle()); + i++) { usleep(10000); } } @@ -57,8 +60,8 @@ int main() { // First create our renderer, where one can customize SDL or other stuff's // initialization. - auto renderer_opts = pu::ui::render::RendererInitOptions( - SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags); + auto renderer_opts = + pu::ui::render::RendererInitOptions(SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags); renderer_opts.UseImage(pu::ui::render::IMGAllFlags); renderer_opts.UseAudio(pu::ui::render::MixerAllFlags); renderer_opts.UseTTF(); @@ -68,7 +71,7 @@ int main() { renderer_opts.SetExtraDefaultFontSize(theme::type::Title); renderer_opts.SetExtraDefaultFontSize(theme::type::Display); - auto renderer = pu::ui::render::Renderer::New(renderer_opts); + auto renderer = pu::ui::render::Renderer::New(renderer_opts); // Create our main application from the renderer auto main = ui::MainApplication::New(renderer); @@ -78,5 +81,5 @@ int main() { main->Show(); servicesExit(); - return 0; + return 0; } diff --git a/src/app/main_application.cpp b/src/app/main_application.cpp index a80be72..dcf627a 100644 --- a/src/app/main_application.cpp +++ b/src/app/main_application.cpp @@ -1,21 +1,24 @@ #include <string> -#include <switch.h> #include <switch/services/hid.h> #include <vector> + +#include <switch.h> + #include <nxst/app/main_application.hpp> namespace ui { - MainApplication *mainApp; +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); - } -} \ No newline at end of file +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); +} +} // namespace ui \ No newline at end of file diff --git a/src/domain/account.cpp b/src/domain/account.cpp index e92e37f..3ff62c7 100644 --- a/src/domain/account.cpp +++ b/src/domain/account.cpp @@ -24,16 +24,17 @@ * reasonable ways as different from the original version. */ -#include <nxst/domain/account.hpp> -#include <sys/stat.h> #include <cstdio> +#include <sys/stat.h> + +#include <nxst/domain/account.hpp> static std::map<AccountUid, User> mUsers; -Result Account::init(void) -{ +Result Account::init(void) { Result res = accountInitialize(AccountServiceType_Application); - if (R_FAILED(res)) return res; + if (R_FAILED(res)) + return res; AccountUid uids[8]; s32 count = 0; @@ -44,13 +45,11 @@ Result Account::init(void) return 0; } -void Account::exit(void) -{ +void Account::exit(void) { accountExit(); } -std::vector<AccountUid> Account::ids(void) -{ +std::vector<AccountUid> Account::ids(void) { std::vector<AccountUid> v; for (auto& value : mUsers) { v.push_back(value.second.id); @@ -58,8 +57,7 @@ std::vector<AccountUid> Account::ids(void) return v; } -static User getUser(AccountUid id) -{ +static User getUser(AccountUid id) { User user{id, ""}; AccountProfile profile; AccountProfileBase profilebase; @@ -74,8 +72,7 @@ static User getUser(AccountUid id) return user; } -std::string Account::username(AccountUid id) -{ +std::string Account::username(AccountUid id) { std::map<AccountUid, User>::const_iterator got = mUsers.find(id); if (got == mUsers.end()) { User user = getUser(id); @@ -86,21 +83,21 @@ std::string Account::username(AccountUid id) return got->second.name; } -std::string Account::iconPath(AccountUid id) -{ +std::string Account::iconPath(AccountUid id) { char path[128]; - snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg", - id.uid[0], id.uid[1]); + snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg", id.uid[0], id.uid[1]); struct stat st; - if (stat(path, &st) == 0 && st.st_size > 0) return std::string(path); + if (stat(path, &st) == 0 && st.st_size > 0) + return std::string(path); mkdir("sdmc:/switch", 0755); mkdir("sdmc:/switch/NXST", 0755); mkdir("sdmc:/switch/NXST/cache", 0755); AccountProfile profile; - if (R_FAILED(accountGetProfile(&profile, id))) return ""; + if (R_FAILED(accountGetProfile(&profile, id))) + return ""; u32 imgSize = 0; if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) { @@ -112,26 +109,28 @@ std::string Account::iconPath(AccountUid id) u32 outSize = 0; Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize); accountProfileClose(&profile); - if (R_FAILED(r) || outSize == 0) return ""; + if (R_FAILED(r) || outSize == 0) + return ""; FILE* f = fopen(path, "wb"); - if (!f) return ""; + if (!f) + return ""; fwrite(buf.data(), 1, outSize, f); fclose(f); return std::string(path); } -AccountUid Account::selectAccount(void) -{ +AccountUid Account::selectAccount(void) { LibAppletArgs args; libappletArgsCreate(&args, 0x10000); - u8 st_in[0xA0] = {0}; + u8 st_in[0xA0] = {0}; u8 st_out[0x18] = {0}; size_t repsz; - Result res = libappletLaunch(AppletId_LibraryAppletPlayerSelect, &args, st_in, 0xA0, st_out, 0x18, &repsz); + Result res = + libappletLaunch(AppletId_LibraryAppletPlayerSelect, &args, st_in, 0xA0, st_out, 0x18, &repsz); if (R_SUCCEEDED(res)) { - u64 lres = *(u64*)st_out; + u64 lres = *(u64*)st_out; AccountUid uid = *(AccountUid*)&st_out[8]; if (lres == 0) return uid; diff --git a/src/domain/common.cpp b/src/domain/common.cpp index e5ddf47..b59ca95 100644 --- a/src/domain/common.cpp +++ b/src/domain/common.cpp @@ -26,8 +26,7 @@ #include <nxst/domain/common.hpp> -std::string DateTime::timeStr(void) -{ +std::string DateTime::timeStr(void) { time_t unixTime; struct tm timeStruct; time(&unixTime); @@ -35,35 +34,32 @@ std::string DateTime::timeStr(void) return StringUtils::format("%02i:%02i:%02i", timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec); } -std::string DateTime::dateTimeStr(void) -{ +std::string DateTime::dateTimeStr(void) { time_t unixTime; struct tm timeStruct; time(&unixTime); localtime_r(&unixTime, &timeStruct); - return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday, timeStruct.tm_hour, - timeStruct.tm_min, timeStruct.tm_sec); + return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, + timeStruct.tm_mday, timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec); } -std::string DateTime::logDateTime(void) -{ +std::string DateTime::logDateTime(void) { time_t unixTime; struct tm timeStruct; time(&unixTime); localtime_r(&unixTime, &timeStruct); - return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday, - timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec); + return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900, + timeStruct.tm_mon + 1, timeStruct.tm_mday, timeStruct.tm_hour, + timeStruct.tm_min, timeStruct.tm_sec); } -std::string StringUtils::UTF16toUTF8(const std::u16string& src) -{ +std::string StringUtils::UTF16toUTF8(const std::u16string& src) { static std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert; std::string dst = convert.to_bytes(src); return dst; } -std::string StringUtils::removeForbiddenCharacters(std::string src) -{ +std::string StringUtils::removeForbiddenCharacters(std::string src) { static const std::string illegalChars = ".,!\\/:?*\"<>|"; for (size_t i = 0, sz = src.length(); i < sz; i++) { if (illegalChars.find(src[i]) != std::string::npos) { @@ -79,8 +75,7 @@ std::string StringUtils::removeForbiddenCharacters(std::string src) return src; } -std::string StringUtils::format(const std::string fmt_str, ...) -{ +std::string StringUtils::format(const std::string fmt_str, ...) { va_list ap; char* fp = NULL; va_start(ap, fmt_str); @@ -90,8 +85,7 @@ std::string StringUtils::format(const std::string fmt_str, ...) return std::string(formatted.get()); } -bool StringUtils::containsInvalidChar(const std::string& str) -{ +bool StringUtils::containsInvalidChar(const std::string& str) { for (size_t i = 0, sz = str.length(); i < sz; i++) { if (!isascii(str[i])) { return true; @@ -100,24 +94,27 @@ bool StringUtils::containsInvalidChar(const std::string& str) return false; } -void StringUtils::ltrim(std::string& s) -{ - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); })); +void StringUtils::ltrim(std::string& s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { + return !std::isspace(ch); + })); } -void StringUtils::rtrim(std::string& s) -{ - s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end()); +void StringUtils::rtrim(std::string& s) { + s.erase(std::find_if(s.rbegin(), s.rend(), + [](int ch) { + return !std::isspace(ch); + }) + .base(), + s.end()); } -void StringUtils::trim(std::string& s) -{ +void StringUtils::trim(std::string& s) { ltrim(s); rtrim(s); } -char* getConsoleIP(void) -{ +char* getConsoleIP(void) { struct in_addr in; in.s_addr = gethostid(); return inet_ntoa(in); diff --git a/src/domain/title.cpp b/src/domain/title.cpp index b48139f..38aacc4 100644 --- a/src/domain/title.cpp +++ b/src/domain/title.cpp @@ -24,40 +24,39 @@ * reasonable ways as different from the original version. */ -#include <nxst/domain/title.hpp> #include <nxst/app/main.hpp> +#include <nxst/domain/title.hpp> static std::unordered_map<AccountUid, std::vector<Title>> titles; static bool s_titlesLoaded = false; -bool areTitlesLoaded(void) -{ +bool areTitlesLoaded(void) { return s_titlesLoaded; } -void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name, const std::string& author) -{ - mId = id; - mUserId = userID; +void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name, + const std::string& author) { + mId = id; + mUserId = userID; mSaveDataType = saveDataType; - mUserName = Account::username(userID); - mAuthor = author; - mName = name; - mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId) : StringUtils::removeForbiddenCharacters(name); - mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName; + mUserName = Account::username(userID); + mAuthor = author; + mName = name; + mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId) + : StringUtils::removeForbiddenCharacters(name); + mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName; std::string aname = StringUtils::removeAccents(mName); - size_t pos = aname.rfind(":"); - mDisplayName = std::make_pair(aname, ""); + size_t pos = aname.rfind(":"); + mDisplayName = std::make_pair(aname, ""); if (pos != std::string::npos) { std::string name1 = aname.substr(0, pos); std::string name2 = aname.substr(pos + 1); StringUtils::trim(name1); StringUtils::trim(name2); - mDisplayName.first = name1; + mDisplayName.first = name1; mDisplayName.second = name2; - } - else { + } else { // check for parenthesis size_t pos1 = aname.rfind("("); size_t pos2 = aname.rfind(")"); @@ -66,7 +65,7 @@ void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& std::string name2 = aname.substr(pos1 + 1, pos2 - 1 - pos1); StringUtils::trim(name1); StringUtils::trim(name2); - mDisplayName.first = name1; + mDisplayName.first = name1; mDisplayName.second = name2; } } @@ -74,94 +73,77 @@ void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& refreshDirectories(); } -u8 Title::saveDataType(void) -{ +u8 Title::saveDataType(void) { return mSaveDataType; } -u64 Title::id(void) -{ +u64 Title::id(void) { return mId; } -u64 Title::saveId(void) -{ +u64 Title::saveId(void) { return mSaveId; } -void Title::saveId(u64 saveId) -{ +void Title::saveId(u64 saveId) { mSaveId = saveId; } -AccountUid Title::userId(void) -{ +AccountUid Title::userId(void) { return mUserId; } -std::string Title::userName(void) -{ +std::string Title::userName(void) { return mUserName; } -std::string Title::author(void) -{ +std::string Title::author(void) { return mAuthor; } -std::string Title::name(void) -{ +std::string Title::name(void) { return mName; } -std::pair<std::string, std::string> Title::displayName(void) -{ +std::pair<std::string, std::string> Title::displayName(void) { return mDisplayName; } -std::string Title::path(void) -{ +std::string Title::path(void) { return mPath; } -std::string Title::fullPath(size_t index) -{ +std::string Title::fullPath(size_t index) { return mFullSavePaths.at(index); } -std::vector<std::string> Title::saves() -{ +std::vector<std::string> Title::saves() { return mSaves; } -u64 Title::playTimeNanoseconds(void) -{ +u64 Title::playTimeNanoseconds(void) { return mPlayTimeNanoseconds; } -std::string Title::playTime(void) -{ +std::string Title::playTime(void) { const u64 playTimeMinutes = mPlayTimeNanoseconds / 60000000000; - return StringUtils::format("%d", playTimeMinutes / 60) + ":" + StringUtils::format("%02d", playTimeMinutes % 60) + " hours"; + return StringUtils::format("%d", playTimeMinutes / 60) + ":" + + StringUtils::format("%02d", playTimeMinutes % 60) + " hours"; } -void Title::playTimeNanoseconds(u64 playTimeNanoseconds) -{ +void Title::playTimeNanoseconds(u64 playTimeNanoseconds) { mPlayTimeNanoseconds = playTimeNanoseconds; } -u32 Title::lastPlayedTimestamp(void) -{ +u32 Title::lastPlayedTimestamp(void) { return mLastPlayedTimestamp; } -void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp) -{ +void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp) { mLastPlayedTimestamp = lastPlayedTimestamp; } -void Title::refreshDirectories(void) -{ +void Title::refreshDirectories(void) { mSaves.clear(); mFullSavePaths.clear(); @@ -178,15 +160,15 @@ void Title::refreshDirectories(void) std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend()); mSaves.insert(mSaves.begin(), g_emptySave); mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave); - } - else { - Logger::getInstance().log(Logger::ERROR, "Couldn't retrieve the extdata directory list for the title " + name()); + } else { + Logger::getInstance().log(Logger::ERROR, + "Couldn't retrieve the extdata directory list for the title " + name()); } } -void loadTitles(void) -{ - if (s_titlesLoaded) return; +void loadTitles(void) { + if (s_titlesLoaded) + return; s_titlesLoaded = true; titles.clear(); @@ -194,9 +176,9 @@ void loadTitles(void) FsSaveDataInfoReader reader; FsSaveDataInfo info; s64 total_entries = 0; - size_t outsize = 0; + size_t outsize = 0; - NacpLanguageEntry* nle = NULL; + NacpLanguageEntry* nle = NULL; NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData)); if (nsacd == NULL) { return; @@ -216,43 +198,44 @@ void loadTitles(void) } if (info.save_data_type == FsSaveDataType_Account) { - u64 tid = info.application_id; - u64 sid = info.save_data_id; + u64 tid = info.application_id; + u64 sid = info.save_data_id; AccountUid uid = info.uid; // if (mFilterIds.find(tid) == mFilterIds.end()) { - res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd, sizeof(NsApplicationControlData), &outsize); - if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) { - res = nacpGetLanguageEntry(&nsacd->nacp, &nle); - if (R_SUCCEEDED(res) && nle != NULL) { - Title title; - title.init(info.save_data_type, tid, uid, std::string(nle->name), std::string(nle->author)); - title.saveId(sid); + res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd, + sizeof(NsApplicationControlData), &outsize); + if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) { + res = nacpGetLanguageEntry(&nsacd->nacp, &nle); + if (R_SUCCEEDED(res) && nle != NULL) { + Title title; + title.init(info.save_data_type, tid, uid, std::string(nle->name), + std::string(nle->author)); + title.saveId(sid); - // load play statistics - PdmPlayStatistics stats; - res = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats); - if (R_SUCCEEDED(res)) { - title.playTimeNanoseconds(stats.playtime); - title.lastPlayedTimestamp(stats.last_timestamp_user); - } + // load play statistics + PdmPlayStatistics stats; + res = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats); + if (R_SUCCEEDED(res)) { + title.playTimeNanoseconds(stats.playtime); + title.lastPlayedTimestamp(stats.last_timestamp_user); + } - // loadIcon(tid, nsacd, outsize - sizeof(nsacd->nacp)); + // loadIcon(tid, nsacd, outsize - sizeof(nsacd->nacp)); - // check if the vector is already created - std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); - if (it != titles.end()) { - // found - it->second.push_back(title); - } - else { - // not found, insert into map - std::vector<Title> v; - v.push_back(title); - titles.emplace(uid, v); - } + // check if the vector is already created + std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); + if (it != titles.end()) { + // found + it->second.push_back(title); + } else { + // not found, insert into map + std::vector<Title> v; + v.push_back(title); + titles.emplace(uid, v); } } - nle = NULL; + } + nle = NULL; // } } } @@ -263,45 +246,40 @@ void loadTitles(void) sortTitles(); } -void sortTitles(void) -{ +void sortTitles(void) { for (auto& vect : titles) { std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) { switch (g_sortMode) { - case SORT_LAST_PLAYED: - return l.lastPlayedTimestamp() > r.lastPlayedTimestamp(); - case SORT_PLAY_TIME: - return l.playTimeNanoseconds() > r.playTimeNanoseconds(); - case SORT_ALPHA: - default: - return l.name() < r.name(); + case SORT_LAST_PLAYED: + return l.lastPlayedTimestamp() > r.lastPlayedTimestamp(); + case SORT_PLAY_TIME: + return l.playTimeNanoseconds() > r.playTimeNanoseconds(); + case SORT_ALPHA: + default: + return l.name() < r.name(); } }); } } -void rotateSortMode(void) -{ +void rotateSortMode(void) { g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT); sortTitles(); } -void getTitle(Title& dst, AccountUid uid, size_t i) -{ +void getTitle(Title& dst, AccountUid uid, size_t i) { std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); if (it != titles.end() && i < getTitleCount(uid)) { dst = it->second.at(i); } } -size_t getTitleCount(AccountUid uid) -{ +size_t getTitleCount(AccountUid uid) { std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); return it != titles.end() ? it->second.size() : 0; } -void refreshDirectories(u64 id) -{ +void refreshDirectories(u64 id) { for (auto& pair : titles) { for (size_t i = 0; i < pair.second.size(); i++) { if (pair.second.at(i).id() == id) { @@ -311,8 +289,7 @@ void refreshDirectories(u64 id) } } -std::unordered_map<std::string, std::string> getCompleteTitleList(void) -{ +std::unordered_map<std::string, std::string> getCompleteTitleList(void) { std::unordered_map<std::string, std::string> map; for (const auto& pair : titles) { for (auto value : pair.second) { diff --git a/src/domain/util.cpp b/src/domain/util.cpp index c4e6b8d..dd7d601 100644 --- a/src/domain/util.cpp +++ b/src/domain/util.cpp @@ -24,21 +24,19 @@ * reasonable ways as different from the original version. */ +#include <nxst/app/main.hpp> +#include <nxst/app/main_application.hpp> #include <nxst/domain/util.hpp> #include <nxst/infra/sys/logger.hpp> -#include <nxst/app/main_application.hpp> -#include <nxst/app/main.hpp> -void servicesExit(void) -{ +void servicesExit(void) { Logger::getInstance().flush(); Account::exit(); plExit(); romfsExit(); } -Result servicesInit(void) -{ +Result servicesInit(void) { io::createDirectory("sdmc:/switch"); io::createDirectory("sdmc:/switch/NXST"); io::createDirectory("sdmc:/switch/NXST/saves"); @@ -71,30 +69,28 @@ Result servicesInit(void) if (R_SUCCEEDED(res = hidsysInitialize())) { g_notificationLedAvailable = true; - } - else { + } else { Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res); } - Logger::getInstance().log(Logger::INFO, "NXST loading completed!"); return 0; } -std::u16string StringUtils::UTF8toUTF16(const char* src) -{ +std::u16string StringUtils::UTF8toUTF16(const char* src) { char16_t tmp[256] = {0}; utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256); return std::u16string(tmp); } // https://stackoverflow.com/questions/14094621/change-all-accented-letters-to-normal-letters-in-c -std::string StringUtils::removeAccents(std::string str) -{ +std::string StringUtils::removeAccents(std::string str) { std::u16string src = UTF8toUTF16(str.c_str()); - const std::u16string illegal = UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ"); - const std::u16string fixed = UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy"); + const std::u16string illegal = + UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ"); + const std::u16string fixed = + UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy"); for (size_t i = 0, sz = src.length(); i < sz; i++) { size_t index = illegal.find(src[i]); @@ -106,8 +102,7 @@ std::string StringUtils::removeAccents(std::string str) return UTF16toUTF8(src); } -std::string StringUtils::removeNotAscii(std::string str) -{ +std::string StringUtils::removeNotAscii(std::string str) { for (size_t i = 0, sz = str.length(); i < sz; i++) { if (!isascii(str[i])) { str[i] = ' '; @@ -116,9 +111,9 @@ std::string StringUtils::removeNotAscii(std::string str) return str; } -std::string StringUtils::elide(const std::string& s, size_t maxChars) -{ - if (s.size() <= maxChars || maxChars < 6) return s; +std::string StringUtils::elide(const std::string& s, size_t maxChars) { + if (s.size() <= maxChars || maxChars < 6) + return s; constexpr const char* dots = "..."; size_t budget = maxChars - 3; size_t head = (budget + 1) / 2; @@ -126,36 +121,35 @@ std::string StringUtils::elide(const std::string& s, size_t maxChars) return s.substr(0, head) + dots + s.substr(s.size() - tail); } -HidsysNotificationLedPattern blinkLedPattern(u8 times) -{ +HidsysNotificationLedPattern blinkLedPattern(u8 times) { HidsysNotificationLedPattern pattern; memset(&pattern, 0, sizeof(pattern)); - pattern.baseMiniCycleDuration = 0x1; // 12.5ms. - pattern.totalMiniCycles = 0x2; // 2 mini cycles. - pattern.totalFullCycles = times; // Repeat n times. - pattern.startIntensity = 0x0; // 0%. + pattern.baseMiniCycleDuration = 0x1; // 12.5ms. + pattern.totalMiniCycles = 0x2; // 2 mini cycles. + pattern.totalFullCycles = times; // Repeat n times. + pattern.startIntensity = 0x0; // 0%. - pattern.miniCycles[0].ledIntensity = 0xF; // 100%. - pattern.miniCycles[0].transitionSteps = 0xF; // 15 steps. Total 187.5ms. - pattern.miniCycles[0].finalStepDuration = 0x0; // Forced 12.5ms. - pattern.miniCycles[1].ledIntensity = 0x0; // 0%. - pattern.miniCycles[1].transitionSteps = 0xF; // 15 steps. Total 187.5ms. - pattern.miniCycles[1].finalStepDuration = 0x0; // Forced 12.5ms. + pattern.miniCycles[0].ledIntensity = 0xF; // 100%. + pattern.miniCycles[0].transitionSteps = 0xF; // 15 steps. Total 187.5ms. + pattern.miniCycles[0].finalStepDuration = 0x0; // Forced 12.5ms. + pattern.miniCycles[1].ledIntensity = 0x0; // 0%. + pattern.miniCycles[1].transitionSteps = 0xF; // 15 steps. Total 187.5ms. + pattern.miniCycles[1].finalStepDuration = 0x0; // Forced 12.5ms. return pattern; } -void blinkLed(u8 times) -{ +void blinkLed(u8 times) { if (g_notificationLedAvailable) { PadState pad; padInitializeDefault(&pad); s32 n; - HidsysUniquePadId uniquePadIds[2] = {0}; + HidsysUniquePadId uniquePadIds[2] = {0}; HidsysNotificationLedPattern pattern = blinkLedPattern(times); memset(uniquePadIds, 0, sizeof(uniquePadIds)); - Result res = hidsysGetUniquePadsFromNpad(padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, uniquePadIds, 2, &n); + Result res = hidsysGetUniquePadsFromNpad( + padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, uniquePadIds, 2, &n); if (R_SUCCEEDED(res)) { for (s32 i = 0; i < n; i++) { hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]); diff --git a/src/infra/fs/directory.cpp b/src/infra/fs/directory.cpp index d97610b..d5e4507 100644 --- a/src/infra/fs/directory.cpp +++ b/src/infra/fs/directory.cpp @@ -26,9 +26,8 @@ #include <nxst/infra/fs/directory.hpp> -Directory::Directory(const std::string& root) -{ - mGood = false; +Directory::Directory(const std::string& root) { + mGood = false; mError = 0; mList.clear(); @@ -37,11 +36,10 @@ Directory::Directory(const std::string& root) if (dir == NULL) { mError = (Result)errno; - } - else { + } else { while ((ent = readdir(dir))) { - std::string name = std::string(ent->d_name); - bool directory = ent->d_type == DT_DIR; + std::string name = std::string(ent->d_name); + bool directory = ent->d_type == DT_DIR; struct DirectoryEntry de = {name, directory}; mList.push_back(de); } @@ -50,27 +48,22 @@ Directory::Directory(const std::string& root) } } -Result Directory::error(void) -{ +Result Directory::error(void) { return mError; } -bool Directory::good(void) -{ +bool Directory::good(void) { return mGood; } -std::string Directory::entry(size_t index) -{ +std::string Directory::entry(size_t index) { return index < mList.size() ? mList.at(index).name : ""; } -bool Directory::folder(size_t index) -{ +bool Directory::folder(size_t index) { return index < mList.size() ? mList.at(index).directory : false; } -size_t Directory::size(void) -{ +size_t Directory::size(void) { return mList.size(); } \ No newline at end of file diff --git a/src/infra/fs/filesystem.cpp b/src/infra/fs/filesystem.cpp index 28c991e..69138f1 100644 --- a/src/infra/fs/filesystem.cpp +++ b/src/infra/fs/filesystem.cpp @@ -26,17 +26,14 @@ #include <nxst/infra/fs/filesystem.hpp> -Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) -{ +Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) { return fsOpen_SaveData(fileSystem, titleID, userID); } -int FileSystem::mount(FsFileSystem fs) -{ +int FileSystem::mount(FsFileSystem fs) { return fsdevMountDevice("save", fs); } -void FileSystem::unmount(void) -{ +void FileSystem::unmount(void) { fsdevUnmountDevice("save"); } \ No newline at end of file diff --git a/src/infra/fs/io.cpp b/src/infra/fs/io.cpp index c038256..4769beb 100644 --- a/src/infra/fs/io.cpp +++ b/src/infra/fs/io.cpp @@ -24,20 +24,19 @@ * reasonable ways as different from the original version. */ -#include <nxst/infra/fs/io.hpp> -#include <nxst/infra/fs/handles.hpp> -#include <nxst/app/main.hpp> -#include <nxst/infra/sys/logger.hpp> #include <vector> -bool io::fileExists(const std::string& path) -{ +#include <nxst/app/main.hpp> +#include <nxst/infra/fs/handles.hpp> +#include <nxst/infra/fs/io.hpp> +#include <nxst/infra/sys/logger.hpp> + +bool io::fileExists(const std::string& path) { struct stat buffer; return (stat(path.c_str(), &buffer) == 0); } -void io::copyFile(const std::string& srcPath, const std::string& dstPath) -{ +void io::copyFile(const std::string& srcPath, const std::string& dstPath) { g_isTransferringFile = true; nxst::FileHandle src(fopen(srcPath.c_str(), "rb")); @@ -63,14 +62,13 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath) u64 offset = 0; size_t slashpos = srcPath.rfind('/'); - g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1); + g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1); while (offset < sz) { u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get()); if (count == 0) { nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.", - srcPath.c_str(), (unsigned long long)offset, - (unsigned long long)sz, errno); + srcPath.c_str(), (unsigned long long)offset, (unsigned long long)sz, errno); break; } fwrite(buf.data(), 1, count, dst.get()); @@ -84,10 +82,9 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath) g_isTransferringFile = false; } -Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) -{ +Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) { Result res = 0; - bool quit = false; + bool quit = false; Directory items(srcPath); if (!items.good()) { @@ -104,12 +101,10 @@ Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) newsrc += "/"; newdst += "/"; res = io::copyDirectory(newsrc, newdst); - } - else { + } else { quit = true; } - } - else { + } else { io::copyFile(newsrc, newdst); } } @@ -117,20 +112,17 @@ Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) return 0; } -Result io::createDirectory(const std::string& path) -{ +Result io::createDirectory(const std::string& path) { mkdir(path.c_str(), 0777); return 0; } -bool io::directoryExists(const std::string& path) -{ +bool io::directoryExists(const std::string& path) { struct stat sb; return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode)); } -Result io::deleteFolderRecursively(const std::string& path) -{ +Result io::deleteFolderRecursively(const std::string& path) { Directory dir(path); if (!dir.good()) { return dir.error(); @@ -142,8 +134,7 @@ Result io::deleteFolderRecursively(const std::string& path) deleteFolderRecursively(newpath); newpath = path + dir.entry(i); rmdir(newpath.c_str()); - } - else { + } else { std::string newpath = path + dir.entry(i); std::remove(newpath.c_str()); } @@ -153,14 +144,12 @@ Result io::deleteFolderRecursively(const std::string& path) return 0; } -nxst::Result<std::string> io::backup(size_t index, AccountUid uid) -{ +nxst::Result<std::string> io::backup(size_t index, AccountUid uid) { Title title; getTitle(title, uid, index); - nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", - title.name().c_str(), title.id(), - title.userId().uid[1], title.userId().uid[0]); + nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), + title.id(), title.userId().uid[1], title.userId().uid[0]); nxst::FsFileSystemHandle fsHandle; Result res = FileSystem::mount(fsHandle.get(), title.id(), title.userId()); @@ -180,7 +169,8 @@ nxst::Result<std::string> io::backup(size_t index, AccountUid uid) } fsHandle.release(); // devfs now owns the kernel handle - std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId()))); + std::string suggestion = + StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId()))); io::createDirectory(title.path()); std::string dst_path = title.path() + "/" + suggestion; @@ -215,15 +205,13 @@ nxst::Result<std::string> io::backup(size_t index, AccountUid uid) } // Creates the save data filesystem for a title if it doesn't exist yet. -static void createSaveIfNeeded(u64 title_id, AccountUid uid) -{ +static void createSaveIfNeeded(u64 title_id, AccountUid uid) { std::vector<u8> nsacd_buf(sizeof(NsApplicationControlData), 0); auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nsacd_buf.data()); size_t outsize = 0; - if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, - title_id, nsacd, - sizeof(NsApplicationControlData), &outsize))) { + if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title_id, nsacd, + sizeof(NsApplicationControlData), &outsize))) { return; } @@ -231,30 +219,29 @@ static void createSaveIfNeeded(u64 title_id, AccountUid uid) FsSaveDataAttribute attr = {}; attr.application_id = title_id; - attr.uid = uid; + attr.uid = uid; attr.save_data_type = FsSaveDataType_Account; attr.save_data_rank = FsSaveDataRank_Primary; FsSaveDataCreationInfo create_info = {}; - create_info.save_data_size = (s64)nsacd->nacp.user_account_save_data_size; - create_info.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size; - create_info.available_size = 0x4000; - create_info.owner_id = nsacd->nacp.save_data_owner_id; + create_info.save_data_size = (s64)nsacd->nacp.user_account_save_data_size; + create_info.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size; + create_info.available_size = 0x4000; + create_info.owner_id = nsacd->nacp.save_data_owner_id; create_info.save_data_space_id = FsSaveDataSpaceId_User; fsCreateSaveDataFileSystem(&attr, &create_info, &meta); } -nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell) -{ +nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, + const std::string& nameFromCell) { (void)cellIndex; Title title; getTitle(title, uid, index); - nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", - title.name().c_str(), title.id(), - title.userId().uid[1], title.userId().uid[0]); + nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), + title.id(), title.userId().uid[1], title.userId().uid[0]); createSaveIfNeeded(title.id(), uid); diff --git a/src/infra/sys/logger.cpp b/src/infra/sys/logger.cpp index 3837579..7e61d9f 100644 --- a/src/infra/sys/logger.cpp +++ b/src/infra/sys/logger.cpp @@ -1,10 +1,10 @@ -#include <nxst/infra/sys/logger.hpp> - #include <cstdarg> #include <cstdio> #include <ctime> #include <mutex> +#include <nxst/infra/sys/logger.hpp> + namespace { std::mutex g_log_mutex; @@ -15,8 +15,7 @@ constexpr const char* kLogPath = "/switch/NXST/log.log"; constexpr const char* kLogPath = "nxst.log"; #endif -void writeEntry(const char* tag, const char* fmt, va_list args) -{ +void writeEntry(const char* tag, const char* fmt, va_list args) { char msg[2048]; vsnprintf(msg, sizeof(msg), fmt, args); @@ -37,18 +36,25 @@ void writeEntry(const char* tag, const char* fmt, va_list args) } } -} // namespace +} // namespace namespace nxst::log { -void write(Level level, const char* fmt, ...) -{ +void write(Level level, const char* fmt, ...) { const char* tag = "[INFO] "; switch (level) { - case Level::Debug: tag = "[DEBUG]"; break; - case Level::Info: tag = "[INFO] "; break; - case Level::Warn: tag = "[WARN] "; break; - case Level::Error: tag = "[ERROR]"; break; + case Level::Debug: + tag = "[DEBUG]"; + break; + case Level::Info: + tag = "[INFO] "; + break; + case Level::Warn: + tag = "[WARN] "; + break; + case Level::Error: + tag = "[ERROR]"; + break; } va_list args; va_start(args, fmt); @@ -56,9 +62,29 @@ void write(Level level, const char* fmt, ...) va_end(args); } -void debug(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[DEBUG]", fmt, args); va_end(args); } -void info (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[INFO] ", fmt, args); va_end(args); } -void warn (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[WARN] ", fmt, args); va_end(args); } -void error(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[ERROR]", fmt, args); va_end(args); } +void debug(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeEntry("[DEBUG]", fmt, args); + va_end(args); +} +void info(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeEntry("[INFO] ", fmt, args); + va_end(args); +} +void warn(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeEntry("[WARN] ", fmt, args); + va_end(args); +} +void error(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeEntry("[ERROR]", fmt, args); + va_end(args); +} -} // namespace nxst::log +} // namespace nxst::log diff --git a/src/service/transfer_service.cpp b/src/service/transfer_service.cpp index 3fb4e79..6d0b978 100644 --- a/src/service/transfer_service.cpp +++ b/src/service/transfer_service.cpp @@ -1,5 +1,3 @@ -#include <nxst/service/transfer_service.hpp> - #include <arpa/inet.h> #include <chrono> #include <cstring> @@ -12,11 +10,14 @@ #include <unistd.h> #include <vector> +#include <nxst/service/transfer_service.hpp> + #ifdef __SWITCH__ #include <switch.h> -#include <nxst/infra/fs/io.hpp> -#include <nxst/domain/util.hpp> + #include <nxst/domain/account.hpp> +#include <nxst/domain/util.hpp> +#include <nxst/infra/fs/io.hpp> #endif #include <nxst/domain/protocol.hpp> @@ -32,7 +33,8 @@ 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; + if (n <= 0) + return false; sent += n; } return true; @@ -42,7 +44,8 @@ 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; + if (n <= 0) + return false; got += n; } return true; @@ -50,15 +53,19 @@ static bool recvAll(int sock, void* buf, size_t len) { 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; + if (!infile.is_open()) + return false; uint32_t filename_len = (uint32_t)filepath.string().size(); - uint64_t file_size = (uint64_t)infile.tellg(); + 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; + 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; @@ -66,8 +73,10 @@ static bool sendFile(int sock, const fs::path& filepath, TransferState& state) { 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; + 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; } @@ -84,12 +93,12 @@ static void mkdirs(const std::string& path) { mkdir(path.c_str(), 0777); } -static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size, - TransferState& state) { +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); + if (!dir.empty()) + mkdirs(dir); } FILE* outfile = fopen(rel_path.c_str(), "wb"); @@ -99,7 +108,8 @@ static void receiveFile(int sock, const std::string& rel_path, uint64_t file_siz 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; + if (n <= 0) + break; remaining -= (uint64_t)n; } return; @@ -113,7 +123,8 @@ static void receiveFile(int sock, const std::string& rel_path, uint64_t file_siz 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; + if (n <= 0) + break; fwrite(buffer.data(), 1, (size_t)n, outfile); total += (uint64_t)n; state.bytes_done.store(total); @@ -131,17 +142,19 @@ void TransferService::failSend(const std::string& reason) { int TransferService::findServer(char* out_ip) { int udp_fd = socket(AF_INET, SOCK_DGRAM, 0); - if (udp_fd < 0) return -1; + 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); + if (owned == udp_fd) + close(udp_fd); }; sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(proto::MULTICAST_PORT); + 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) { @@ -152,16 +165,19 @@ int TransferService::findServer(char* out_ip) { // 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; } + 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); + 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) { @@ -179,8 +195,8 @@ int TransferService::findServer(char* out_ip) { 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; + size_t idx = a->title_index; + AccountUid uid = a->uid; delete a; svc->runSender(idx, uid); return nullptr; @@ -200,7 +216,8 @@ void TransferService::runSender(size_t title_index, AccountUid uid) { failSend("No receiver found.\nMake sure the other Switch is in Receive mode."); return finish(); } - if (sender_state.cancelled.load()) return finish(); + if (sender_state.cancelled.load()) + return finish(); sender_state.setStatus("Creating backup..."); #ifdef __SWITCH__ @@ -212,24 +229,30 @@ void TransferService::runSender(size_t title_index, AccountUid uid) { fs::path directory = backup_result.value(); #else fs::path directory = "."; - (void)title_index; (void)uid; + (void)title_index; + (void)uid; #endif - if (sender_state.cancelled.load()) return finish(); + 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(); } + 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); + if (owned == tcp_fd) + close(tcp_fd); }; sockaddr_in serv{}; serv.sin_family = AF_INET; - serv.sin_port = htons(proto::TCP_PORT); + 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()) @@ -245,11 +268,13 @@ void TransferService::runSender(size_t title_index, AccountUid uid) { sender_state.bytes_total.store(total); for (const auto& entry : fs::recursive_directory_iterator(directory)) { - if (sender_state.cancelled.load()) break; + 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; + if (!sendFile(tcp_fd, p, sender_state)) + break; } } @@ -278,19 +303,26 @@ int TransferService::startSend(size_t title_index, AccountUid uid) { void TransferService::cancelSend() { sender_state.cancelled.store(true); int udp = sender_udp_sock.exchange(-1); - if (udp >= 0) { shutdown(udp, SHUT_RDWR); close(udp); } + 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); } + 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))); + 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; + 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); @@ -311,21 +343,25 @@ void TransferService::runBroadcast() { pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr); int udp = socket(AF_INET, SOCK_DGRAM, 0); - if (udp < 0) { receiver_broadcast_active.store(false); return; } + 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); + 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_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons(proto::MULTICAST_PORT); + addr.sin_port = htons(proto::MULTICAST_PORT); if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) { releaseUdp(); @@ -348,7 +384,8 @@ void TransferService::runBroadcast() { while (true) { ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen); if (n < 0) { - if (receiver_state.cancelled.load()) break; + if (receiver_state.cancelled.load()) + break; continue; } buf[n] = '\0'; @@ -365,7 +402,7 @@ void TransferService::runBroadcast() { void* TransferService::acceptEntry(void* arg) { auto* a = static_cast<AcceptArgs*>(arg); TransferService* svc = a->svc; - int server_fd = a->server_fd; + int server_fd = a->server_fd; delete a; svc->runAccept(server_fd); return nullptr; @@ -380,41 +417,48 @@ void TransferService::runAccept(int server_fd) { 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 (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; + 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; + 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); + 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; + 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 (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 = result.isOk(); + restore_ok = result.isOk(); restore_error = result.isOk() ? "" : result.error(); #else restore_ok = true; @@ -430,29 +474,32 @@ int TransferService::startReceive(size_t title_index, AccountUid uid, std::strin 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_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; + 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; } + 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_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_port = htons(proto::TCP_PORT); + addr.sin_port = htons(proto::TCP_PORT); - if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 || - listen(server, 3) < 0) { + if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 || listen(server, 3) < 0) { cancelReceive(); return 1; } @@ -472,12 +519,22 @@ int TransferService::startReceive(size_t title_index, AccountUid uid, std::strin void TransferService::cancelReceive() { receiver_state.cancelled.store(true); int sock = receiver_client_sock.exchange(-1); - if (sock >= 0) { shutdown(sock, SHUT_RDWR); close(sock); } + 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); } + 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); + if (bsock >= 0) { + shutdown(bsock, SHUT_RDWR); + close(bsock); + } + if (receiver_broadcast_active.load()) + pthread_cancel(receiver_bcast_thread); } -} // namespace nxst +} // namespace nxst diff --git a/src/ui/titles_layout.cpp b/src/ui/titles_layout.cpp index 8b98a78..4df3ead 100644 --- a/src/ui/titles_layout.cpp +++ b/src/ui/titles_layout.cpp @@ -1,310 +1,312 @@ #include <nxst/app/main_application.hpp> #include <nxst/domain/util.hpp> -#include <nxst/ui/transfer_overlay.hpp> #include <nxst/ui/const.h> +#include <nxst/ui/transfer_overlay.hpp> namespace ui { - extern MainApplication *mainApp; +extern MainApplication* mainApp; - namespace { - constexpr int ListX = theme::space::lg; - constexpr int ListW = 760; - constexpr int PanelX = ListX + ListW + theme::space::xl; - constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg; - constexpr int ContentY = theme::layout::ContentTop + theme::space::md; - constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md; - constexpr int BtnH = 56; - constexpr int BtnW = PanelW - 2 * theme::space::lg; - } +namespace { +constexpr int ListX = theme::space::lg; +constexpr int ListW = 760; +constexpr int PanelX = ListX + ListW + theme::space::xl; +constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg; +constexpr int ContentY = theme::layout::ContentTop + theme::space::md; +constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md; +constexpr int BtnH = 56; +constexpr int BtnW = PanelW - 2 * theme::space::lg; +} // namespace - TitlesLayout::TitlesLayout() : Layout::Layout() { - using namespace theme; +TitlesLayout::TitlesLayout() : Layout::Layout() { + using namespace theme; - this->titlesMenu = pu::ui::elm::Menu::New( - ListX, ContentY, ListW, - color::BgBase, color::BgSurface2, - 88, 6); - this->titlesMenu->SetScrollbarColor(color::Primary); - this->titlesMenu->SetItemsFocusColor(color::BgSurface2); - this->titlesMenu->SetOnSelectionChanged([this]() { this->refreshPanel(); }); - this->SetBackgroundColor(color::BgBase); - this->Add(this->titlesMenu); - - this->panelBg = pu::ui::elm::Rectangle::New( - PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg); - this->Add(this->panelBg); - - this->panelTitle = pu::ui::elm::TextBlock::New( - PanelX + space::lg, ContentY + space::lg, ""); - this->panelTitle->SetFont(type::font(type::Title)); - this->panelTitle->SetColor(color::TextPrimary); - this->Add(this->panelTitle); - - this->panelHint = pu::ui::elm::TextBlock::New( - PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:"); - this->panelHint->SetFont(type::font(type::Body)); - this->panelHint->SetColor(color::TextSecondary); - this->Add(this->panelHint); - - int btnY = ContentY + 200; - this->btnTransferBg = pu::ui::elm::Rectangle::New( - PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md); - this->Add(this->btnTransferBg); - this->btnTransferText = pu::ui::elm::TextBlock::New( - PanelX + space::lg + space::md, btnY + 14, "Transfer to another device"); - this->btnTransferText->SetFont(type::font(type::Body)); - this->btnTransferText->SetColor(color::TextSecondary); - this->Add(this->btnTransferText); - - int btnY2 = btnY + BtnH + space::md; - this->btnReceiveBg = pu::ui::elm::Rectangle::New( - PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md); - this->Add(this->btnReceiveBg); - this->btnReceiveText = pu::ui::elm::TextBlock::New( - PanelX + space::lg + space::md, btnY2 + 14, "Receive from another device"); - this->btnReceiveText->SetFont(type::font(type::Body)); - this->btnReceiveText->SetColor(color::TextSecondary); - this->Add(this->btnReceiveText); - - this->panelFooter = pu::ui::elm::TextBlock::New( - PanelX + space::lg, - ContentY + ContentH - space::lg - 18, - "Save data only"); - this->panelFooter->SetFont(type::font(type::Caption)); - this->panelFooter->SetColor(color::TextMuted); - this->Add(this->panelFooter); - - this->emptyText = pu::ui::elm::TextBlock::New( - ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40, - "No save data on this profile"); - this->emptyText->SetFont(type::font(type::Display)); - this->emptyText->SetColor(color::TextPrimary); - this->emptyText->SetVisible(false); - this->Add(this->emptyText); - - this->emptySub = pu::ui::elm::TextBlock::New( - ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16, - "Play something first, then come back."); - this->emptySub->SetFont(type::font(type::Body)); - this->emptySub->SetColor(color::TextMuted); - this->emptySub->SetVisible(false); - this->Add(this->emptySub); - - this->header = std::make_unique<HeaderBar>(this, "Save Transfer"); - this->hints = std::make_unique<HintBar>(this); - this->updateHints(); - } - - void TitlesLayout::InitTitles(AccountUid uid) { - using namespace theme; - this->current_uid = uid; - - auto it = this->menuCache.find(uid); - std::vector<pu::ui::elm::MenuItem::Ref>* items; - if (it != this->menuCache.end()) { - items = &it->second; - } else { - std::vector<pu::ui::elm::MenuItem::Ref> built; - for (size_t i = 0; i < getTitleCount(uid); i++) { - Title title; - getTitle(title, uid, i); - auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); - titleItem->SetColor(color::TextPrimary); - built.push_back(titleItem); - } - auto inserted = this->menuCache.emplace(uid, std::move(built)); - items = &inserted.first->second; - } - - this->titlesMenu->ClearItems(); - for (auto& item : *items) { - this->titlesMenu->AddItem(item); - } - this->titlesMenu->SetSelectedIndex(0); - - const bool empty = items->empty(); - this->titlesMenu->SetVisible(!empty); - this->panelBg->SetVisible(!empty); - this->panelTitle->SetVisible(!empty); - this->panelHint->SetVisible(!empty); - this->btnTransferBg->SetVisible(!empty); - this->btnTransferText->SetVisible(!empty); - this->btnReceiveBg->SetVisible(!empty); - this->btnReceiveText->SetVisible(!empty); - this->panelFooter->SetVisible(!empty); - this->emptyText->SetVisible(empty); - this->emptySub->SetVisible(empty); - - this->focus = TitlesFocus::List; - this->action = TitlesAction::Transfer; + this->titlesMenu = + pu::ui::elm::Menu::New(ListX, ContentY, ListW, color::BgBase, color::BgSurface2, 88, 6); + this->titlesMenu->SetScrollbarColor(color::Primary); + this->titlesMenu->SetItemsFocusColor(color::BgSurface2); + this->titlesMenu->SetOnSelectionChanged([this]() { this->refreshPanel(); - this->refreshButtons(); - this->updateHints(); + }); + this->SetBackgroundColor(color::BgBase); + this->Add(this->titlesMenu); - this->header->SetUser(uid, Account::username(uid)); + this->panelBg = + pu::ui::elm::Rectangle::New(PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg); + this->Add(this->panelBg); + + this->panelTitle = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg, ""); + this->panelTitle->SetFont(type::font(type::Title)); + this->panelTitle->SetColor(color::TextPrimary); + this->Add(this->panelTitle); + + this->panelHint = + pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:"); + this->panelHint->SetFont(type::font(type::Body)); + this->panelHint->SetColor(color::TextSecondary); + this->Add(this->panelHint); + + int btnY = ContentY + 200; + this->btnTransferBg = + pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md); + this->Add(this->btnTransferBg); + this->btnTransferText = + pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY + 14, "Transfer to another device"); + this->btnTransferText->SetFont(type::font(type::Body)); + this->btnTransferText->SetColor(color::TextSecondary); + this->Add(this->btnTransferText); + + int btnY2 = btnY + BtnH + space::md; + this->btnReceiveBg = + pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md); + this->Add(this->btnReceiveBg); + this->btnReceiveText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY2 + 14, + "Receive from another device"); + this->btnReceiveText->SetFont(type::font(type::Body)); + this->btnReceiveText->SetColor(color::TextSecondary); + this->Add(this->btnReceiveText); + + this->panelFooter = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + ContentH - space::lg - 18, + "Save data only"); + this->panelFooter->SetFont(type::font(type::Caption)); + this->panelFooter->SetColor(color::TextMuted); + this->Add(this->panelFooter); + + this->emptyText = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40, + "No save data on this profile"); + this->emptyText->SetFont(type::font(type::Display)); + this->emptyText->SetColor(color::TextPrimary); + this->emptyText->SetVisible(false); + this->Add(this->emptyText); + + this->emptySub = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16, + "Play something first, then come back."); + this->emptySub->SetFont(type::font(type::Body)); + this->emptySub->SetColor(color::TextMuted); + this->emptySub->SetVisible(false); + this->Add(this->emptySub); + + this->header = std::make_unique<HeaderBar>(this, "Save Transfer"); + this->hints = std::make_unique<HintBar>(this); + this->updateHints(); +} + +void TitlesLayout::InitTitles(AccountUid uid) { + using namespace theme; + this->current_uid = uid; + + auto it = this->menuCache.find(uid); + std::vector<pu::ui::elm::MenuItem::Ref>* items; + if (it != this->menuCache.end()) { + items = &it->second; + } else { + std::vector<pu::ui::elm::MenuItem::Ref> built; + for (size_t i = 0; i < getTitleCount(uid); i++) { + Title title; + getTitle(title, uid, i); + auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); + titleItem->SetColor(color::TextPrimary); + built.push_back(titleItem); + } + auto inserted = this->menuCache.emplace(uid, std::move(built)); + items = &inserted.first->second; } - void TitlesLayout::refreshPanel() { - if (this->titlesMenu->GetItems().empty()) return; - int idx = this->titlesMenu->GetSelectedIndex(); - Title title; - getTitle(title, this->current_uid, idx); - this->panelTitle->SetText(StringUtils::elide(title.name(), 24)); + this->titlesMenu->ClearItems(); + for (auto& item : *items) { + this->titlesMenu->AddItem(item); } + this->titlesMenu->SetSelectedIndex(0); - void TitlesLayout::refreshButtons() { - using namespace theme; - const bool active = (focus == TitlesFocus::Actions); - if (active && action == TitlesAction::Transfer) { - this->btnTransferBg->SetColor(color::Primary); - this->btnTransferText->SetColor(color::TextPrimary); - this->btnReceiveBg->SetColor(color::BgSurface2); - this->btnReceiveText->SetColor(color::TextSecondary); - } else if (active && action == TitlesAction::Receive) { - this->btnTransferBg->SetColor(color::BgSurface2); - this->btnTransferText->SetColor(color::TextSecondary); - this->btnReceiveBg->SetColor(color::Accent); - this->btnReceiveText->SetColor(color::BgBase); - } else { - this->btnTransferBg->SetColor(color::BgSurface2); - this->btnTransferText->SetColor(color::TextSecondary); - this->btnReceiveBg->SetColor(color::BgSurface2); - this->btnReceiveText->SetColor(color::TextSecondary); - } + const bool empty = items->empty(); + this->titlesMenu->SetVisible(!empty); + this->panelBg->SetVisible(!empty); + this->panelTitle->SetVisible(!empty); + this->panelHint->SetVisible(!empty); + this->btnTransferBg->SetVisible(!empty); + this->btnTransferText->SetVisible(!empty); + this->btnReceiveBg->SetVisible(!empty); + this->btnReceiveText->SetVisible(!empty); + this->panelFooter->SetVisible(!empty); + this->emptyText->SetVisible(empty); + this->emptySub->SetVisible(empty); + + this->focus = TitlesFocus::List; + this->action = TitlesAction::Transfer; + this->refreshPanel(); + this->refreshButtons(); + this->updateHints(); + + this->header->SetUser(uid, Account::username(uid)); +} + +void TitlesLayout::refreshPanel() { + if (this->titlesMenu->GetItems().empty()) + return; + int idx = this->titlesMenu->GetSelectedIndex(); + Title title; + getTitle(title, this->current_uid, idx); + this->panelTitle->SetText(StringUtils::elide(title.name(), 24)); +} + +void TitlesLayout::refreshButtons() { + using namespace theme; + const bool active = (focus == TitlesFocus::Actions); + if (active && action == TitlesAction::Transfer) { + this->btnTransferBg->SetColor(color::Primary); + this->btnTransferText->SetColor(color::TextPrimary); + this->btnReceiveBg->SetColor(color::BgSurface2); + this->btnReceiveText->SetColor(color::TextSecondary); + } else if (active && action == TitlesAction::Receive) { + this->btnTransferBg->SetColor(color::BgSurface2); + this->btnTransferText->SetColor(color::TextSecondary); + this->btnReceiveBg->SetColor(color::Accent); + this->btnReceiveText->SetColor(color::BgBase); + } else { + this->btnTransferBg->SetColor(color::BgSurface2); + this->btnTransferText->SetColor(color::TextSecondary); + this->btnReceiveBg->SetColor(color::BgSurface2); + this->btnReceiveText->SetColor(color::TextSecondary); } +} - void TitlesLayout::updateHints() { - if (focus == TitlesFocus::List) { - this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}}); - } else { - this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}}); - } +void TitlesLayout::updateHints() { + if (focus == TitlesFocus::List) { + this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}}); + } else { + this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}}); } +} - void TitlesLayout::runTransfer(int index, Title& title) { - (void)title; - auto ovl = TransferOverlay::New("Transferring save data..."); - this->titlesMenu->SetVisible(false); - mainApp->StartOverlay(ovl); - this->LockInput(); - if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) { - mainApp->EndOverlay(); - this->titlesMenu->SetVisible(true); - this->UnlockInput(); - mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); - return; - } - while (!mainApp->transfer.isSendDone()) { - ovl->SetStatus(mainApp->transfer.sendStatusText()); - ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown()); - ovl->SetProgress(mainApp->transfer.sendProgress()); - mainApp->CallForRender(); - if (mainApp->GetButtonsDown() & HidNpadButton_B) { - mainApp->transfer.cancelSend(); - } - svcSleepThread(16666666LL); - } +void TitlesLayout::runTransfer(int index, Title& title) { + (void)title; + auto ovl = TransferOverlay::New("Transferring save data..."); + this->titlesMenu->SetVisible(false); + mainApp->StartOverlay(ovl); + this->LockInput(); + if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) { mainApp->EndOverlay(); this->titlesMenu->SetVisible(true); this->UnlockInput(); - - if (mainApp->transfer.isSendConnectionFailed()) { - mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true); - } else if (mainApp->transfer.isSendCancelled()) { - mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); - } else { - mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); - } + mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); + return; } - - void TitlesLayout::runReceive(int index, Title& title) { - 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); - return; - } - auto ovl = TransferOverlay::New("Receiving save data..."); - this->titlesMenu->SetVisible(false); - mainApp->StartOverlay(ovl); - this->LockInput(); - while (!mainApp->transfer.isReceiveDone()) { - ovl->SetStatus(mainApp->transfer.receiveStatusText()); - ovl->SetProgress(mainApp->transfer.receiveProgress()); - mainApp->CallForRender(); - if (mainApp->GetButtonsDown() & HidNpadButton_B) { - mainApp->transfer.cancelReceive(); - } - svcSleepThread(16666666LL); - } - mainApp->EndOverlay(); - this->titlesMenu->SetVisible(true); - this->UnlockInput(); - - if (mainApp->transfer.isReceiveCancelled()) { - mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); - } else if (mainApp->transfer.restoreSucceeded()) { - mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); - } else { - 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)Up; (void)Held; (void)Pos; - if (m_inputLocked) return; - - if (Down & HidNpadButton_Plus) { + while (!mainApp->transfer.isSendDone()) { + ovl->SetStatus(mainApp->transfer.sendStatusText()); + ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown()); + ovl->SetProgress(mainApp->transfer.sendProgress()); + mainApp->CallForRender(); + if (mainApp->GetButtonsDown() & HidNpadButton_B) { mainApp->transfer.cancelSend(); + } + svcSleepThread(16666666LL); + } + mainApp->EndOverlay(); + this->titlesMenu->SetVisible(true); + this->UnlockInput(); + + if (mainApp->transfer.isSendConnectionFailed()) { + mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true); + } else if (mainApp->transfer.isSendCancelled()) { + mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); + } else { + mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); + } +} + +void TitlesLayout::runReceive(int index, Title& title) { + 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); + return; + } + auto ovl = TransferOverlay::New("Receiving save data..."); + this->titlesMenu->SetVisible(false); + mainApp->StartOverlay(ovl); + this->LockInput(); + while (!mainApp->transfer.isReceiveDone()) { + ovl->SetStatus(mainApp->transfer.receiveStatusText()); + ovl->SetProgress(mainApp->transfer.receiveProgress()); + mainApp->CallForRender(); + if (mainApp->GetButtonsDown() & HidNpadButton_B) { mainApp->transfer.cancelReceive(); - mainApp->Close(); + } + svcSleepThread(16666666LL); + } + mainApp->EndOverlay(); + this->titlesMenu->SetVisible(true); + this->UnlockInput(); + + if (mainApp->transfer.isReceiveCancelled()) { + mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); + } else if (mainApp->transfer.restoreSucceeded()) { + mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); + } else { + 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)Up; + (void)Held; + (void)Pos; + if (m_inputLocked) + return; + + if (Down & HidNpadButton_Plus) { + mainApp->transfer.cancelSend(); + mainApp->transfer.cancelReceive(); + mainApp->Close(); + return; + } + + if (focus == TitlesFocus::List) { + if (Down & HidNpadButton_B) { + this->header->SetUser(std::nullopt, ""); + mainApp->LoadLayout(mainApp->users_layout); return; } - - if (focus == TitlesFocus::List) { - if (Down & HidNpadButton_B) { - this->header->SetUser(std::nullopt, ""); - mainApp->LoadLayout(mainApp->users_layout); + if (Down & HidNpadButton_A) { + if (this->titlesMenu->GetItems().empty()) return; - } - if (Down & HidNpadButton_A) { - if (this->titlesMenu->GetItems().empty()) return; - this->lockedListIndex = this->titlesMenu->GetSelectedIndex(); - this->focus = TitlesFocus::Actions; - this->action = TitlesAction::Transfer; - this->refreshButtons(); - this->updateHints(); - return; - } - } else { - if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) { - this->titlesMenu->SetSelectedIndex(this->lockedListIndex); - } - if (Down & HidNpadButton_B) { - this->focus = TitlesFocus::List; - this->refreshButtons(); - this->updateHints(); - return; - } - if (Down & (HidNpadButton_Up | HidNpadButton_Down | - HidNpadButton_StickLUp | HidNpadButton_StickLDown)) { - this->action = (action == TitlesAction::Transfer) - ? TitlesAction::Receive : TitlesAction::Transfer; - this->refreshButtons(); - return; - } - if (Down & HidNpadButton_A) { - int idx = this->titlesMenu->GetSelectedIndex(); - Title title; - getTitle(title, this->current_uid, idx); - TitlesAction chosen = action; - this->focus = TitlesFocus::List; - this->refreshButtons(); - this->updateHints(); - if (chosen == TitlesAction::Transfer) { - this->runTransfer(idx, title); - } else { - this->runReceive(idx, title); - } + this->lockedListIndex = this->titlesMenu->GetSelectedIndex(); + this->focus = TitlesFocus::Actions; + this->action = TitlesAction::Transfer; + this->refreshButtons(); + this->updateHints(); + return; + } + } else { + if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) { + this->titlesMenu->SetSelectedIndex(this->lockedListIndex); + } + if (Down & HidNpadButton_B) { + this->focus = TitlesFocus::List; + this->refreshButtons(); + this->updateHints(); + return; + } + if (Down & + (HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_StickLUp | HidNpadButton_StickLDown)) { + this->action = + (action == TitlesAction::Transfer) ? TitlesAction::Receive : TitlesAction::Transfer; + this->refreshButtons(); + return; + } + if (Down & HidNpadButton_A) { + int idx = this->titlesMenu->GetSelectedIndex(); + Title title; + getTitle(title, this->current_uid, idx); + TitlesAction chosen = action; + this->focus = TitlesFocus::List; + this->refreshButtons(); + this->updateHints(); + if (chosen == TitlesAction::Transfer) { + this->runTransfer(idx, title); + } else { + this->runReceive(idx, title); } } } } +} // namespace ui diff --git a/src/ui/users_layout.cpp b/src/ui/users_layout.cpp index c290d2d..3991655 100644 --- a/src/ui/users_layout.cpp +++ b/src/ui/users_layout.cpp @@ -1,75 +1,69 @@ #include <nxst/app/main_application.hpp> namespace ui { - extern MainApplication *mainApp; +extern MainApplication* mainApp; - UsersLayout::UsersLayout() : Layout::Layout() { - using namespace theme; +UsersLayout::UsersLayout() : Layout::Layout() { + using namespace theme; - this->usersMenu = pu::ui::elm::Menu::New( - 0, layout::ContentTop + space::md, - layout::ScreenW, - color::BgBase, color::BgSurface2, - 88, 6); - this->usersMenu->SetScrollbarColor(color::Primary); - this->usersMenu->SetItemsFocusColor(color::BgSurface2); + this->usersMenu = pu::ui::elm::Menu::New(0, layout::ContentTop + space::md, layout::ScreenW, + color::BgBase, color::BgSurface2, 88, 6); + this->usersMenu->SetScrollbarColor(color::Primary); + this->usersMenu->SetItemsFocusColor(color::BgSurface2); - for (AccountUid const& uid : Account::ids()) { - auto item = pu::ui::elm::MenuItem::New(Account::username(uid)); - item->SetColor(color::TextPrimary); - this->usersMenu->AddItem(item); - } - - this->loadingBg = pu::ui::elm::Rectangle::New( - 0, 0, layout::ScreenW, layout::ScreenH, color::Scrim); - this->loadingBg->SetVisible(false); - - this->loadingText = pu::ui::elm::TextBlock::New( - layout::ScreenW / 2 - 120, - layout::ScreenH / 2 - 12, - "Loading saves..."); - this->loadingText->SetFont(type::font(type::Body)); - this->loadingText->SetColor(color::TextSecondary); - this->loadingText->SetVisible(false); - - this->SetBackgroundColor(color::BgBase); - this->Add(this->usersMenu); - this->Add(this->loadingBg); - this->Add(this->loadingText); - - this->header = std::make_unique<HeaderBar>(this, "Select a user"); - this->hints = std::make_unique<HintBar>(this); - this->hints->SetHints({{"A", "Select"}, {"+", "Quit"}}); + for (AccountUid const& uid : Account::ids()) { + auto item = pu::ui::elm::MenuItem::New(Account::username(uid)); + item->SetColor(color::TextPrimary); + this->usersMenu->AddItem(item); } - int32_t UsersLayout::GetCurrentIndex() { - return this->usersMenu->GetSelectedIndex(); + this->loadingBg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::ScreenH, color::Scrim); + this->loadingBg->SetVisible(false); + + this->loadingText = + pu::ui::elm::TextBlock::New(layout::ScreenW / 2 - 120, layout::ScreenH / 2 - 12, "Loading saves..."); + this->loadingText->SetFont(type::font(type::Body)); + this->loadingText->SetColor(color::TextSecondary); + this->loadingText->SetVisible(false); + + this->SetBackgroundColor(color::BgBase); + this->Add(this->usersMenu); + this->Add(this->loadingBg); + this->Add(this->loadingText); + + this->header = std::make_unique<HeaderBar>(this, "Select a user"); + this->hints = std::make_unique<HintBar>(this); + this->hints->SetHints({{"A", "Select"}, {"+", "Quit"}}); +} + +int32_t UsersLayout::GetCurrentIndex() { + return this->usersMenu->GetSelectedIndex(); +} + +void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { + if (Down & HidNpadButton_Plus) { + mainApp->Close(); + return; } - void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { - if (Down & HidNpadButton_Plus) { - mainApp->Close(); - return; + if (Down & HidNpadButton_A) { + AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex()); + + if (!areTitlesLoaded()) { + this->usersMenu->SetVisible(false); + this->loadingBg->SetVisible(true); + this->loadingText->SetVisible(true); + mainApp->CallForRender(); + + loadTitles(); + + this->loadingBg->SetVisible(false); + this->loadingText->SetVisible(false); + this->usersMenu->SetVisible(true); } - if (Down & HidNpadButton_A) { - AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex()); - - if (!areTitlesLoaded()) { - this->usersMenu->SetVisible(false); - this->loadingBg->SetVisible(true); - this->loadingText->SetVisible(true); - mainApp->CallForRender(); - - loadTitles(); - - this->loadingBg->SetVisible(false); - this->loadingText->SetVisible(false); - this->usersMenu->SetVisible(true); - } - - mainApp->titles_layout->InitTitles(uid); - mainApp->LoadLayout(mainApp->titles_layout); - } + mainApp->titles_layout->InitTitles(uid); + mainApp->LoadLayout(mainApp->titles_layout); } } +} // namespace ui -- 2.39.2 From 33a1ce73af0cb9a7a4f15380764fbb34c1402c3c Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov <mail@nfedorov.dev> Date: Sat, 2 May 2026 20:49:42 +0300 Subject: [PATCH 05/10] chore: remove vendored deps/asprintf vasprintf/asprintf are provided by devkitpro newlib (<stdio.h>). deps/asprintf was never compiled by CMake (GLOB_RECURSE covers src/ only). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- deps/asprintf/asprintf.c | 37 ------------------------------------- deps/asprintf/asprintf.h | 10 ---------- deps/asprintf/clib.json | 9 --------- 3 files changed, 56 deletions(-) delete mode 100644 deps/asprintf/asprintf.c delete mode 100644 deps/asprintf/asprintf.h delete mode 100644 deps/asprintf/clib.json diff --git a/deps/asprintf/asprintf.c b/deps/asprintf/asprintf.c deleted file mode 100644 index 1d25035..0000000 --- a/deps/asprintf/asprintf.c +++ /dev/null @@ -1,37 +0,0 @@ -#include <stdio.h> -#include <stdlib.h> - -#include "asprintf.h" - -int vasprintf(char **strp, const char *fmt, va_list ap) { - int size, res; - - va_list cp; - va_copy(cp, ap); - size = vsnprintf(NULL, 0, fmt, cp); - va_end(cp); - if (size < 0) - return -1; - - *strp = (char *)malloc(size + 1); - if (*strp == NULL) - return -1; - - res = vsnprintf(*strp, size + 1, fmt, ap); - if (res < 0) { - free(*strp); - return -1; - } - - return res; -} - -int asprintf(char **s, const char *fmt, ...) { - int ret; - - va_list ap; - va_start(ap, fmt); - ret = vasprintf(s, fmt, ap); - va_end(ap); - return ret; -} diff --git a/deps/asprintf/asprintf.h b/deps/asprintf/asprintf.h deleted file mode 100644 index 3b40a04..0000000 --- a/deps/asprintf/asprintf.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef HAVE_ASPRINTF -#define HAVE_ASPRINTF 1 - -#include <stdarg.h> - -int vasprintf(char **strp, const char *fmt, va_list ap); - -int asprintf(char **s, const char *fmt, ...); - -#endif diff --git a/deps/asprintf/clib.json b/deps/asprintf/clib.json deleted file mode 100644 index 4e2b86d..0000000 --- a/deps/asprintf/clib.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "asprintf", - "version": "1.0.0", - "repo": "Neved4/asprintf", - "description": "asprintf, vasprintf - print to allocated string", - "license": "MIT", - "keywords": ["asprintf", "sprintf", "alloc", "string"], - "src": ["asprintf.c", "asprintf.h"] -} -- 2.39.2 From 82df796a4a469f615763769817d1ea5167e81753 Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov <mail@nfedorov.dev> Date: Sat, 2 May 2026 20:57:27 +0300 Subject: [PATCH 06/10] build: migrate Plutonium to ExternalProject_Add + IMPORTED target Replaces add_custom_command/add_custom_target with ExternalProject_Add, which correctly declares BUILD_BYPRODUCTS for Ninja and propagates include paths via INTERFACE_INCLUDE_DIRECTORIES on the IMPORTED STATIC target. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- CMakeLists.txt | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 14a2d30..64d4a4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,10 +35,7 @@ file(GLOB_RECURSE NXST_SOURCES add_executable(NXST ${NXST_SOURCES}) # ── Include paths ───────────────────────────────────────────────────────────── -target_include_directories(NXST PRIVATE - include - lib/Plutonium/include -) +target_include_directories(NXST PRIVATE include) # ── pkg-config (uses aarch64-none-elf-pkg-config set by Switch.cmake) ───────── find_package(PkgConfig REQUIRED) @@ -52,25 +49,33 @@ pkg_check_modules(PORTLIBS REQUIRED IMPORTED_TARGET ${NXST_PKG_MODULES}) target_include_directories(NXST PRIVATE ${PORTLIBS_INCLUDE_DIRS}) -# ── Link libraries ──────────────────────────────────────────────────────────── -# ── Plutonium static library ────────────────────────────────────────────────── +# ── Plutonium ───────────────────────────────────────────────────────────────── +include(ExternalProject) + set(LIBPU_A ${CMAKE_SOURCE_DIR}/lib/Plutonium/lib/libpu.a) -add_custom_command( - OUTPUT ${LIBPU_A} - COMMAND make -C ${CMAKE_SOURCE_DIR}/lib/Plutonium -j - COMMENT "Building Plutonium (libpu.a)" +ExternalProject_Add(plutonium_ep + SOURCE_DIR ${CMAKE_SOURCE_DIR}/lib/Plutonium + CONFIGURE_COMMAND "" + BUILD_COMMAND make -j -C ${CMAKE_SOURCE_DIR}/lib/Plutonium + INSTALL_COMMAND "" + BUILD_IN_SOURCE 1 + BUILD_BYPRODUCTS ${LIBPU_A} +) + +add_library(plutonium STATIC IMPORTED GLOBAL) +add_dependencies(plutonium plutonium_ep) +set_target_properties(plutonium PROPERTIES + IMPORTED_LOCATION ${LIBPU_A} + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/lib/Plutonium/include ) -add_custom_target(plutonium DEPENDS ${LIBPU_A}) -add_dependencies(NXST plutonium) # ── 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 - ${LIBPU_A} + plutonium PkgConfig::PORTLIBS drm_nouveau harfbuzz -- 2.39.2 From 9b18a32b0c9671db31c502e436ec28a94d6ad4f6 Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov <mail@nfedorov.dev> Date: Sun, 3 May 2026 13:00:36 +0300 Subject: [PATCH 07/10] build: replace Plutonium submodule with ExternalProject_Add fetch Plutonium is now downloaded at build time via ExternalProject_Add (GIT_REPOSITORY + GIT_TAG) instead of being a git submodule. Removes lib/, .gitmodules, and the linguist-vendored entry for it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- .gitattributes | 1 - .gitignore | 1 + .gitmodules | 3 --- CMakeLists.txt | 14 +++++++++----- lib | 1 - 5 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 .gitmodules delete mode 160000 lib diff --git a/.gitattributes b/.gitattributes index c006aab..efc0677 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,7 +2,6 @@ * text=auto eol=lf # Vendor: do not count toward language stats -lib/Plutonium/** linguist-vendored=true deps/** linguist-vendored=true # Binary assets diff --git a/.gitignore b/.gitignore index 3c9059b..7275bfa 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ CMakeCache.txt CMakeFiles/ cmake_install.cmake compile_commands.json +.cache/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 4312018..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "lib"] - path = lib - url = https://github.com/XorTroll/Plutonium.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 64d4a4b..43f8420 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,12 +52,16 @@ target_include_directories(NXST PRIVATE ${PORTLIBS_INCLUDE_DIRS}) # ── Plutonium ───────────────────────────────────────────────────────────────── include(ExternalProject) -set(LIBPU_A ${CMAKE_SOURCE_DIR}/lib/Plutonium/lib/libpu.a) +set(PLUTONIUM_SOURCE_DIR ${CMAKE_BINARY_DIR}/plutonium) +set(LIBPU_A ${PLUTONIUM_SOURCE_DIR}/Plutonium/lib/libpu.a) ExternalProject_Add(plutonium_ep - SOURCE_DIR ${CMAKE_SOURCE_DIR}/lib/Plutonium + GIT_REPOSITORY https://github.com/XorTroll/Plutonium.git + GIT_TAG b56564b70d038c59eef875f2c3cf436859c827f2 + GIT_PROGRESS ON + SOURCE_DIR ${PLUTONIUM_SOURCE_DIR} CONFIGURE_COMMAND "" - BUILD_COMMAND make -j -C ${CMAKE_SOURCE_DIR}/lib/Plutonium + BUILD_COMMAND make -j -C ${PLUTONIUM_SOURCE_DIR}/Plutonium INSTALL_COMMAND "" BUILD_IN_SOURCE 1 BUILD_BYPRODUCTS ${LIBPU_A} @@ -66,9 +70,9 @@ ExternalProject_Add(plutonium_ep add_library(plutonium STATIC IMPORTED GLOBAL) add_dependencies(plutonium plutonium_ep) set_target_properties(plutonium PROPERTIES - IMPORTED_LOCATION ${LIBPU_A} - INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/lib/Plutonium/include + IMPORTED_LOCATION ${LIBPU_A} ) +target_include_directories(NXST PRIVATE ${PLUTONIUM_SOURCE_DIR}/Plutonium/include) # ── Link libraries ──────────────────────────────────────────────────────────── # Order matters for static linking: put most dependent libs first. diff --git a/lib b/lib deleted file mode 160000 index b56564b..0000000 --- a/lib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b56564b70d038c59eef875f2c3cf436859c827f2 -- 2.39.2 From 836956394a5124650d996d063c45b1d92e1979fb Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov <mail@nfedorov.dev> Date: Sun, 3 May 2026 13:02:30 +0300 Subject: [PATCH 08/10] chore: remove stale deps/** linguist-vendored from .gitattributes deps/ directory was removed with asprintf cleanup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- .gitattributes | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index efc0677..d41ff5e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,6 @@ # Normalize line endings * text=auto eol=lf -# Vendor: do not count toward language stats -deps/** linguist-vendored=true - # Binary assets *.png binary *.jpg binary -- 2.39.2 From 6f8ede035f3246418add1709186a27ff7100c55a Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov <mail@nfedorov.dev> Date: Sun, 3 May 2026 14:56:38 +0300 Subject: [PATCH 09/10] =?UTF-8?q?refactor:=20phase=206=20cleanup=20?= =?UTF-8?q?=E2=80=94=20RAII=20handles=20and=20io::restore=20split?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AccountProfileHandle RAII wrapper in handles.hpp; applied in account.cpp (getUser, iconPath) replacing manual accountProfileClose - FILE* in iconPath replaced with existing FileHandle RAII wrapper - io::restore split into clearSaveRoot + extractAndCommit static helpers; public signature unchanged - sendAll/recvAll kept as bool — callers don't propagate the error string, Result<void> would add noise without benefit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- ISSUES/phase6-cleanup.md | 115 ++++++++++++++++++++++++++++++ include/nxst/infra/fs/handles.hpp | 19 +++++ src/domain/account.cpp | 27 ++++--- src/infra/fs/io.cpp | 69 ++++++++++-------- 4 files changed, 187 insertions(+), 43 deletions(-) create mode 100644 ISSUES/phase6-cleanup.md diff --git a/ISSUES/phase6-cleanup.md b/ISSUES/phase6-cleanup.md new file mode 100644 index 0000000..b330732 --- /dev/null +++ b/ISSUES/phase6-cleanup.md @@ -0,0 +1,115 @@ +# Phase 6 cleanup — unfinished items + +Leftover from the architectural refactor. All items are small, contained, and build on +the already-present `Result<T>` + RAII infrastructure. + +## Status + +| # | Item | File(s) | Status | +|---|------|---------|--------| +| 1 | `AccountProfileHandle` RAII + apply in `account.cpp` | `handles.hpp`, `account.cpp` | [x] | +| 2 | `FILE*` in `account.cpp::iconPath` → `FileHandle` | `account.cpp` | [x] | +| 3 | Split `io::restore` into three focused functions | `io.cpp`, `io.hpp` | [x] | +| 4 | `sendAll`/`recvAll` → `nxst::Result<void>` | `transfer_service.cpp` | dropped — callers don't propagate error, `bool` is correct | + +--- + +## Item 1 — `AccountProfileHandle` RAII + +**File:** `include/nxst/infra/fs/handles.hpp` + +Add after `FileHandle`: + +```cpp +// RAII wrapper for AccountProfile — auto-closes on destruction. +struct AccountProfileHandle { + AccountProfile profile{}; + bool valid{false}; + + AccountProfileHandle() = default; + ~AccountProfileHandle() { + if (valid) + accountProfileClose(&profile); + } + AccountProfileHandle(const AccountProfileHandle&) = delete; + AccountProfileHandle& operator=(const AccountProfileHandle&) = delete; + + AccountProfile* get() { return &profile; } +}; +``` + +**Apply in `account.cpp`:** + +`getUser()` (line ~62): replace raw `AccountProfile profile` + manual `accountProfileClose` with `AccountProfileHandle`. + +`iconPath()` (line ~98): same — wrap `AccountProfile profile` → `AccountProfileHandle`. + +--- + +## Item 2 — `FILE*` in `iconPath` → `FileHandle` + +**File:** `src/domain/account.cpp`, lines ~115-119 + +```cpp +// before +FILE* f = fopen(path, "wb"); +if (!f) return ""; +fwrite(buf.data(), 1, outSize, f); +fclose(f); + +// after +nxst::FileHandle f(fopen(path, "wb")); +if (!f) return ""; +fwrite(buf.data(), 1, outSize, f.get()); +``` + +No `fclose` needed — `FileHandle` dtor handles it. + +--- + +## Item 3 — Split `io::restore` + +**Current:** `io.cpp:236-317` — one 80-line function doing mount, validate, clear, copy, commit. + +**Split into three `static` helpers in `io.cpp` (not exposed in header):** + +``` +static nxst::Result<void> clearSaveRoot(const std::string& dst_path) + — removes all files/dirs under save:/ + +static nxst::Result<void> extractAndCommit(const std::string& src_path, const std::string& dst_path) + — copyDirectory + fsdevCommitDevice + +io::restore (public) + — mount, validate src, call clearSaveRoot, call extractAndCommit, return success +``` + +`io.hpp` declaration unchanged — `io::restore` signature stays the same. + +--- + +## Item 4 — `sendAll`/`recvAll` → `nxst::Result<void>` + +**File:** `src/service/transfer_service.cpp`, lines 32-55 + +```cpp +// before +static bool sendAll(int sock, const void* buf, size_t len) { ... return true/false; } +static bool recvAll(int sock, void* buf, size_t len) { ... return true/false; } + +// after +static nxst::Result<void> sendAll(int sock, const void* buf, size_t len); +static nxst::Result<void> recvAll(int sock, void* buf, size_t len); +``` + +All callers: `if (!sendAll(...))` → `if (!sendAll(...).isOk())`. + +--- + +## Verification + +``` +cmake --build build/ # must produce NXST.elf + NXST.nro with zero warnings +``` + +No observable behavior change — pure refactor. diff --git a/include/nxst/infra/fs/handles.hpp b/include/nxst/infra/fs/handles.hpp index b2cefc8..ef8d1b8 100644 --- a/include/nxst/infra/fs/handles.hpp +++ b/include/nxst/infra/fs/handles.hpp @@ -49,4 +49,23 @@ struct FileHandle { } }; +// RAII wrapper for AccountProfile — auto-closes on destruction. +struct AccountProfileHandle { + AccountProfile profile{}; + bool valid{false}; + + AccountProfileHandle() = default; + ~AccountProfileHandle() { + if (valid) + accountProfileClose(&profile); + } // NOLINT(modernize-use-equals-default) + + AccountProfileHandle(const AccountProfileHandle&) = delete; + AccountProfileHandle& operator=(const AccountProfileHandle&) = delete; + + AccountProfile* get() { + return &profile; + } +}; + } // namespace nxst diff --git a/src/domain/account.cpp b/src/domain/account.cpp index 3ff62c7..40627f1 100644 --- a/src/domain/account.cpp +++ b/src/domain/account.cpp @@ -28,6 +28,7 @@ #include <sys/stat.h> #include <nxst/domain/account.hpp> +#include <nxst/infra/fs/handles.hpp> static std::map<AccountUid, User> mUsers; @@ -59,15 +60,15 @@ std::vector<AccountUid> Account::ids(void) { static User getUser(AccountUid id) { User user{id, ""}; - AccountProfile profile; + nxst::AccountProfileHandle profile; AccountProfileBase profilebase; memset(&profilebase, 0, sizeof(profilebase)); - if (R_SUCCEEDED(accountGetProfile(&profile, id))) { - if (R_SUCCEEDED(accountProfileGet(&profile, NULL, &profilebase))) { + if (R_SUCCEEDED(accountGetProfile(profile.get(), id))) { + profile.valid = true; + if (R_SUCCEEDED(accountProfileGet(profile.get(), NULL, &profilebase))) { user.name = std::string(profilebase.nickname); } - accountProfileClose(&profile); } return user; } @@ -95,28 +96,24 @@ std::string Account::iconPath(AccountUid id) { mkdir("sdmc:/switch/NXST", 0755); mkdir("sdmc:/switch/NXST/cache", 0755); - AccountProfile profile; - if (R_FAILED(accountGetProfile(&profile, id))) + nxst::AccountProfileHandle profile; + if (R_FAILED(accountGetProfile(profile.get(), id))) return ""; + profile.valid = true; u32 imgSize = 0; - if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) { - accountProfileClose(&profile); + if (R_FAILED(accountProfileGetImageSize(profile.get(), &imgSize)) || imgSize == 0) return ""; - } std::vector<u8> buf(imgSize); u32 outSize = 0; - Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize); - accountProfileClose(&profile); - if (R_FAILED(r) || outSize == 0) + if (R_FAILED(accountProfileLoadImage(profile.get(), buf.data(), imgSize, &outSize)) || outSize == 0) return ""; - FILE* f = fopen(path, "wb"); + nxst::FileHandle f(fopen(path, "wb")); if (!f) return ""; - fwrite(buf.data(), 1, outSize, f); - fclose(f); + fwrite(buf.data(), 1, outSize, f.get()); return std::string(path); } diff --git a/src/infra/fs/io.cpp b/src/infra/fs/io.cpp index 4769beb..9c86721 100644 --- a/src/infra/fs/io.cpp +++ b/src/infra/fs/io.cpp @@ -233,6 +233,40 @@ static void createSaveIfNeeded(u64 title_id, AccountUid uid) { fsCreateSaveDataFileSystem(&attr, &create_info, &meta); } +static nxst::Result<void> clearSaveRoot(const std::string& dst_path) { + Directory save_root(dst_path); + for (size_t i = 0, sz = save_root.size(); i < sz; i++) { + if (save_root.folder(i)) { + io::deleteFolderRecursively(dst_path + save_root.entry(i) + "/"); + rmdir((dst_path + save_root.entry(i)).c_str()); + } else { + std::remove((dst_path + save_root.entry(i)).c_str()); + } + } + + Result res = fsdevCommitDevice("save"); + if (R_FAILED(res)) { + nxst::log::error("Failed to commit save after clearing with result 0x%08lX.", res); + return nxst::Result<void>::failure("Failed to commit save after delete."); + } + return nxst::Result<void>::success(); +} + +static nxst::Result<void> extractAndCommit(const std::string& src_path, const std::string& dst_path) { + Result res = io::copyDirectory(src_path, dst_path); + if (R_FAILED(res)) { + nxst::log::error("Failed to copy %s to save:/ with result 0x%08lX.", src_path.c_str(), res); + return nxst::Result<void>::failure("Failed to restore save."); + } + + res = fsdevCommitDevice("save"); + if (R_FAILED(res)) { + nxst::log::error("Failed to commit save with result 0x%08lX.", res); + return nxst::Result<void>::failure("Failed to commit to save device."); + } + return nxst::Result<void>::success(); +} + nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell) { (void)cellIndex; @@ -276,37 +310,16 @@ nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellI } } - { - Directory save_root(dst_path); - for (size_t i = 0, sz = save_root.size(); i < sz; i++) { - if (save_root.folder(i)) { - io::deleteFolderRecursively(dst_path + save_root.entry(i) + "/"); - rmdir((dst_path + save_root.entry(i)).c_str()); - } else { - std::remove((dst_path + save_root.entry(i)).c_str()); - } - } + auto clear_res = clearSaveRoot(dst_path); + if (!clear_res.isOk()) { + FileSystem::unmount(); + return nxst::Result<std::string>::failure(clear_res.error()); } - res = fsdevCommitDevice("save"); - if (R_FAILED(res)) { + auto extract_res = extractAndCommit(src_path, dst_path); + if (!extract_res.isOk()) { FileSystem::unmount(); - nxst::log::error("Failed to commit save after clearing with result 0x%08lX.", res); - return nxst::Result<std::string>::failure("Failed to commit save after delete."); - } - - res = io::copyDirectory(src_path, dst_path); - if (R_FAILED(res)) { - FileSystem::unmount(); - nxst::log::error("Failed to copy %s to save:/ with result 0x%08lX.", src_path.c_str(), res); - return nxst::Result<std::string>::failure("Failed to restore save."); - } - - res = fsdevCommitDevice("save"); - if (R_FAILED(res)) { - FileSystem::unmount(); - nxst::log::error("Failed to commit save with result 0x%08lX.", res); - return nxst::Result<std::string>::failure("Failed to commit to save device."); + return nxst::Result<std::string>::failure(extract_res.error()); } blinkLed(4); -- 2.39.2 From 49efcde301ed31649c056437122f11fad58e800d Mon Sep 17 00:00:00 2001 From: "n.fedorov" <mail@nfedorov.dev> Date: Tue, 12 May 2026 09:59:43 +0300 Subject: [PATCH 10/10] another stage of refactoring --- .clang-format | 1 + .gitignore | 1 + ISSUES/phase6-cleanup.md | 115 --- README.md | 4 +- docs/PROTOCOL.md | 6 +- include/nxst/app/main.hpp | 24 - include/nxst/app/main_application.hpp | 18 +- include/nxst/domain/account.hpp | 66 +- include/nxst/domain/common.hpp | 61 -- include/nxst/domain/protocol.hpp | 23 +- include/nxst/domain/result.hpp | 166 ++-- include/nxst/domain/title.hpp | 108 +-- include/nxst/domain/util.hpp | 61 +- include/nxst/infra/fs/directory.hpp | 63 +- include/nxst/infra/fs/filesystem.hpp | 39 +- include/nxst/infra/fs/handles.hpp | 100 +-- include/nxst/infra/fs/io.hpp | 54 +- include/nxst/infra/net/transfer_receiver.hpp | 1 - include/nxst/infra/net/transfer_sender.hpp | 1 - include/nxst/infra/sys/logger.hpp | 59 +- include/nxst/service/transfer_service.hpp | 184 ++-- include/nxst/ui/card.hpp | 18 - include/nxst/ui/const.h | 6 - include/nxst/ui/header_bar.hpp | 119 ++- include/nxst/ui/hint_bar.hpp | 84 +- include/nxst/ui/theme.hpp | 118 +-- include/nxst/ui/titles_layout.hpp | 81 +- include/nxst/ui/transfer_overlay.hpp | 120 +-- include/nxst/ui/ui_context.hpp | 14 - include/nxst/ui/users_layout.hpp | 27 +- src/app/main.cpp | 3 +- src/app/main_application.cpp | 30 +- src/domain/account.cpp | 106 +-- src/domain/common.cpp | 121 --- src/domain/title.cpp | 393 ++++---- src/domain/util.cpp | 297 ++++--- src/infra/fs/directory.cpp | 78 +- src/infra/fs/filesystem.cpp | 37 +- src/infra/fs/io.cpp | 163 ++-- src/infra/net/transfer_receiver.cpp | 1 - src/infra/net/transfer_sender.cpp | 1 - src/infra/sys/logger.cpp | 124 +-- src/service/transfer_service.cpp | 890 +++++++++---------- src/ui/titles_layout.cpp | 542 +++++------ src/ui/users_layout.cpp | 118 +-- 45 files changed, 1982 insertions(+), 2664 deletions(-) delete mode 100644 ISSUES/phase6-cleanup.md delete mode 100644 include/nxst/app/main.hpp delete mode 100644 include/nxst/domain/common.hpp delete mode 100644 include/nxst/infra/net/transfer_receiver.hpp delete mode 100644 include/nxst/infra/net/transfer_sender.hpp delete mode 100644 include/nxst/ui/card.hpp delete mode 100644 include/nxst/ui/const.h delete mode 100644 include/nxst/ui/ui_context.hpp delete mode 100644 src/domain/common.cpp delete mode 100644 src/infra/net/transfer_receiver.cpp delete mode 100644 src/infra/net/transfer_sender.cpp diff --git a/.clang-format b/.clang-format index c3485b3..9fcdcf4 100644 --- a/.clang-format +++ b/.clang-format @@ -26,3 +26,4 @@ IncludeCategories: SpacesBeforeTrailingComments: 2 Cpp11BracedListStyle: true Standard: c++17 +NamespaceIndentation: All diff --git a/.gitignore b/.gitignore index 7275bfa..5c2b8e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .serena +.idea/ # Prerequisites *.d diff --git a/ISSUES/phase6-cleanup.md b/ISSUES/phase6-cleanup.md deleted file mode 100644 index b330732..0000000 --- a/ISSUES/phase6-cleanup.md +++ /dev/null @@ -1,115 +0,0 @@ -# Phase 6 cleanup — unfinished items - -Leftover from the architectural refactor. All items are small, contained, and build on -the already-present `Result<T>` + RAII infrastructure. - -## Status - -| # | Item | File(s) | Status | -|---|------|---------|--------| -| 1 | `AccountProfileHandle` RAII + apply in `account.cpp` | `handles.hpp`, `account.cpp` | [x] | -| 2 | `FILE*` in `account.cpp::iconPath` → `FileHandle` | `account.cpp` | [x] | -| 3 | Split `io::restore` into three focused functions | `io.cpp`, `io.hpp` | [x] | -| 4 | `sendAll`/`recvAll` → `nxst::Result<void>` | `transfer_service.cpp` | dropped — callers don't propagate error, `bool` is correct | - ---- - -## Item 1 — `AccountProfileHandle` RAII - -**File:** `include/nxst/infra/fs/handles.hpp` - -Add after `FileHandle`: - -```cpp -// RAII wrapper for AccountProfile — auto-closes on destruction. -struct AccountProfileHandle { - AccountProfile profile{}; - bool valid{false}; - - AccountProfileHandle() = default; - ~AccountProfileHandle() { - if (valid) - accountProfileClose(&profile); - } - AccountProfileHandle(const AccountProfileHandle&) = delete; - AccountProfileHandle& operator=(const AccountProfileHandle&) = delete; - - AccountProfile* get() { return &profile; } -}; -``` - -**Apply in `account.cpp`:** - -`getUser()` (line ~62): replace raw `AccountProfile profile` + manual `accountProfileClose` with `AccountProfileHandle`. - -`iconPath()` (line ~98): same — wrap `AccountProfile profile` → `AccountProfileHandle`. - ---- - -## Item 2 — `FILE*` in `iconPath` → `FileHandle` - -**File:** `src/domain/account.cpp`, lines ~115-119 - -```cpp -// before -FILE* f = fopen(path, "wb"); -if (!f) return ""; -fwrite(buf.data(), 1, outSize, f); -fclose(f); - -// after -nxst::FileHandle f(fopen(path, "wb")); -if (!f) return ""; -fwrite(buf.data(), 1, outSize, f.get()); -``` - -No `fclose` needed — `FileHandle` dtor handles it. - ---- - -## Item 3 — Split `io::restore` - -**Current:** `io.cpp:236-317` — one 80-line function doing mount, validate, clear, copy, commit. - -**Split into three `static` helpers in `io.cpp` (not exposed in header):** - -``` -static nxst::Result<void> clearSaveRoot(const std::string& dst_path) - — removes all files/dirs under save:/ - -static nxst::Result<void> extractAndCommit(const std::string& src_path, const std::string& dst_path) - — copyDirectory + fsdevCommitDevice - -io::restore (public) - — mount, validate src, call clearSaveRoot, call extractAndCommit, return success -``` - -`io.hpp` declaration unchanged — `io::restore` signature stays the same. - ---- - -## Item 4 — `sendAll`/`recvAll` → `nxst::Result<void>` - -**File:** `src/service/transfer_service.cpp`, lines 32-55 - -```cpp -// before -static bool sendAll(int sock, const void* buf, size_t len) { ... return true/false; } -static bool recvAll(int sock, void* buf, size_t len) { ... return true/false; } - -// after -static nxst::Result<void> sendAll(int sock, const void* buf, size_t len); -static nxst::Result<void> recvAll(int sock, void* buf, size_t len); -``` - -All callers: `if (!sendAll(...))` → `if (!sendAll(...).isOk())`. - ---- - -## Verification - -``` -cmake --build build/ # must produce NXST.elf + NXST.nro with zero warnings -``` - -No observable behavior change — pure refactor. diff --git a/README.md b/README.md index 9c33eea..3b28446 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ Logs are written to `/switch/NXST/log.log`. **Prerequisites:** [devkitPro](https://devkitpro.org/wiki/Getting_Started) with `switch-dev` and `switch-portlibs` packages, plus `cmake ≥ 3.20`. ```bash -# Clone with submodules (Plutonium UI) -git clone --recurse-submodules https://github.com/your-username/NXST.git +# Clone (Plutonium UI fetched automatically by CMake) +git clone https://github.com/your-username/NXST.git cd NXST # Configure (once) diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 896c863..83e737c 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -38,7 +38,7 @@ Sender polls in 100 ms slices for up to 3 seconds. Cancel is checked each slice. |-----------|-------| | Port | `8080` | | Direction | sender connects → receiver listens | -| Buffer size | 65 536 bytes (`proto::BUF_SIZE`) | +| Buffer size | 65 536 bytes (`proto::kBufSize`) | **Connection:** @@ -64,14 +64,14 @@ Sender polls in 100 ms slices for up to 3 seconds. Cancel is checked each slice. Files are sent sequentially. The stream ends with a sentinel frame: ``` -filename_len == 0 (proto::EOF_SENTINEL) +filename_len == 0 (proto::kEofSentinel) ``` No `filename` or `file_size` field follows the sentinel. **Constraints:** -- `filename_len > proto::MAX_FILENAME` (4 096) is treated as a protocol error; the receiver aborts. +- `filename_len > proto::kMaxFilename` (4 096) is treated as a protocol error; the receiver aborts. - Filenames are full paths as produced by `io::backup` (e.g. `/switch/NXST/<title>/<user>/...`). - On the receiver, the username path component is rewritten to match the local user's nickname before writing to disk. diff --git a/include/nxst/app/main.hpp b/include/nxst/app/main.hpp deleted file mode 100644 index c7337b9..0000000 --- a/include/nxst/app/main.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include <memory> - -#include <switch.h> - -#include <nxst/domain/account.hpp> -#include <nxst/domain/title.hpp> -#include <nxst/domain/util.hpp> -#include <nxst/infra/sys/logger.hpp> -#include <nxst/ui/const.h> - -typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t; - -inline float g_currentTime = 0; -inline bool g_backupScrollEnabled = 0; -inline bool g_notificationLedAvailable = false; -inline bool g_shouldExitNetworkLoop = false; -inline std::string g_selectedCheatKey; -inline std::vector<std::string> g_selectedCheatCodes; -inline u32 g_username_dotsize; -inline sort_t g_sortMode = SORT_ALPHA; -inline std::string g_currentFile = ""; -inline bool g_isTransferringFile = false; -inline const std::string g_emptySave = "New..."; diff --git a/include/nxst/app/main_application.hpp b/include/nxst/app/main_application.hpp index 52ca05d..e3ebdce 100644 --- a/include/nxst/app/main_application.hpp +++ b/include/nxst/app/main_application.hpp @@ -8,16 +8,16 @@ namespace ui { -class MainApplication : public pu::ui::Application { + class MainApplication : public pu::ui::Application { - public: - using Application::Application; - PU_SMART_CTOR(MainApplication) + public: + using Application::Application; + PU_SMART_CTOR(MainApplication) - void OnLoad() override; + void OnLoad() override; - UsersLayout::Ref users_layout; - TitlesLayout::Ref titles_layout; - nxst::TransferService transfer; -}; + UsersLayout::Ref users_layout; + TitlesLayout::Ref titles_layout; + nxst::TransferService transfer; + }; } // namespace ui \ No newline at end of file diff --git a/include/nxst/domain/account.hpp b/include/nxst/domain/account.hpp index f3eda2e..ca7200a 100644 --- a/include/nxst/domain/account.hpp +++ b/include/nxst/domain/account.hpp @@ -1,59 +1,25 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors #pragma once -#include <map> -#include <string.h> #include <string> #include <vector> #include <switch.h> -#define USER_ICON_SIZE 64 - +// Hash and comparison support for AccountUid as map/unordered_map key. namespace std { -template <> struct hash<AccountUid> { - size_t operator()(const AccountUid& a) const { - return ((hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1); - } -}; + template <> struct hash<AccountUid> { + size_t operator()(const AccountUid& a) const noexcept { + return (hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1; + } + }; } // namespace std inline bool operator==(const AccountUid& x, const AccountUid& y) { return x.uid[0] == y.uid[0] && x.uid[1] == y.uid[1]; } -inline bool operator==(const AccountUid& x, u64 y) { - return x.uid[0] == y && x.uid[1] == y; -} - inline bool operator<(const AccountUid& x, const AccountUid& y) { - if (x.uid[0] != y.uid[0]) - return x.uid[0] < y.uid[0]; - return x.uid[1] < y.uid[1]; + return x.uid[0] != y.uid[0] ? x.uid[0] < y.uid[0] : x.uid[1] < y.uid[1]; } struct User { @@ -61,12 +27,10 @@ struct User { std::string name; }; -namespace Account { -Result init(void); -void exit(void); - -std::vector<AccountUid> ids(void); -AccountUid selectAccount(void); -std::string username(AccountUid id); -std::string iconPath(AccountUid id); -} // namespace Account +namespace account { + Result init(); + void exit(); + std::vector<AccountUid> ids(); + std::string username(AccountUid id); + std::string iconPath(AccountUid id); +} // namespace account diff --git a/include/nxst/domain/common.hpp b/include/nxst/domain/common.hpp deleted file mode 100644 index 8bebd12..0000000 --- a/include/nxst/domain/common.hpp +++ /dev/null @@ -1,61 +0,0 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - -#pragma once -#include <algorithm> -#include <arpa/inet.h> -#include <codecvt> -#include <cstdio> -#include <locale> -#include <memory> -#include <netinet/in.h> -#include <stdarg.h> -#include <string.h> -#include <string> -#include <sys/socket.h> -#include <time.h> -#include <unistd.h> - -#define ATEXIT(func) atexit((void (*)())func) - -namespace DateTime { -std::string timeStr(void); -std::string dateTimeStr(void); -std::string logDateTime(void); -} // namespace DateTime - -namespace StringUtils { -bool containsInvalidChar(const std::string& str); -std::string escapeJson(const std::string& s); -std::string format(const std::string fmt_str, ...); -std::string removeForbiddenCharacters(std::string src); -std::string UTF16toUTF8(const std::u16string& src); -void ltrim(std::string& s); -void rtrim(std::string& s); -void trim(std::string& s); -} // namespace StringUtils - -char* getConsoleIP(void); diff --git a/include/nxst/domain/protocol.hpp b/include/nxst/domain/protocol.hpp index 245a777..adafb5d 100644 --- a/include/nxst/domain/protocol.hpp +++ b/include/nxst/domain/protocol.hpp @@ -1,17 +1,16 @@ #pragma once -#include <cstdint> namespace proto { -constexpr uint16_t TCP_PORT = 8080; -constexpr uint16_t MULTICAST_PORT = 8081; -constexpr char MULTICAST_GROUP[] = "239.0.0.1"; -constexpr size_t BUF_SIZE = 65536; -constexpr uint32_t MAX_FILENAME = 4096; -constexpr uint32_t EOF_SENTINEL = 0; + constexpr uint16_t kTcpPort = 8080; + constexpr uint16_t kMulticastPort = 8081; + constexpr char kMulticastGroup[] = "239.0.0.1"; + constexpr size_t kBufSize = 65536; + constexpr uint32_t kMaxFilename = 4096; + constexpr uint32_t kEofSentinel = 0; -// Wire layout per file: -// [filename_len : uint32_t LE] — 0 == end-of-stream -// [filename : filename_len bytes] -// [file_size : uint64_t LE] -// [file_data : file_size bytes] + // Wire layout per file: + // [filename_len : uint32_t LE] — 0 == end-of-stream + // [filename : filename_len bytes] + // [file_size : uint64_t LE] + // [file_data : file_size bytes] } // namespace proto diff --git a/include/nxst/domain/result.hpp b/include/nxst/domain/result.hpp index f146450..f92279d 100644 --- a/include/nxst/domain/result.hpp +++ b/include/nxst/domain/result.hpp @@ -4,105 +4,105 @@ namespace nxst { -template <class T, class E = std::string> class Result { - bool ok; - alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)]; + template <class T, class E = std::string> class Result { + bool ok; + alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)]; - Result() = default; + Result() = default; - public: - static Result success(T val) { - Result res; - res.ok = true; - new (res.storage) T(std::move(val)); - return res; - } + public: + static Result success(T val) { + Result res; + res.ok = true; + new (res.storage) T(std::move(val)); + return res; + } - static Result failure(E err) { - Result res; - res.ok = false; - new (res.storage) E(std::move(err)); - return res; - } + static Result failure(E err) { + Result res; + res.ok = false; + new (res.storage) E(std::move(err)); + return res; + } - ~Result() { - if (ok) - reinterpret_cast<T*>(storage)->~T(); - else - reinterpret_cast<E*>(storage)->~E(); - } + ~Result() { + if (ok) + reinterpret_cast<T*>(storage)->~T(); + else + reinterpret_cast<E*>(storage)->~E(); + } - Result(const Result& other) : ok(other.ok) { - if (ok) - new (storage) T(*reinterpret_cast<const T*>(other.storage)); - else - new (storage) E(*reinterpret_cast<const E*>(other.storage)); - } + Result(const Result& other) : ok(other.ok) { + if (ok) + new (storage) T(*reinterpret_cast<const T*>(other.storage)); + else + new (storage) E(*reinterpret_cast<const E*>(other.storage)); + } - Result(Result&& other) : ok(other.ok) { - if (ok) - new (storage) T(std::move(*reinterpret_cast<T*>(other.storage))); - else - new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); - } + Result(Result&& other) : ok(other.ok) { + if (ok) + new (storage) T(std::move(*reinterpret_cast<T*>(other.storage))); + else + new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); + } - Result& operator=(const Result&) = delete; + Result& operator=(const Result&) = delete; - bool isOk() const noexcept { - return ok; - } - const T& value() const { - return *reinterpret_cast<const T*>(storage); - } - const E& error() const { - return *reinterpret_cast<const E*>(storage); - } -}; + bool isOk() const noexcept { + return ok; + } + const T& value() const { + return *reinterpret_cast<const T*>(storage); + } + const E& error() const { + return *reinterpret_cast<const E*>(storage); + } + }; -// Specialisation for Result<void> -template <class E> class Result<void, E> { - bool ok; - alignas(E) unsigned char storage[sizeof(E)]; + // Specialisation for Result<void> + template <class E> class Result<void, E> { + bool ok; + alignas(E) unsigned char storage[sizeof(E)]; - Result() = default; + Result() = default; - public: - static Result success() { - Result res; - res.ok = true; - return res; - } + public: + static Result success() { + Result res; + res.ok = true; + return res; + } - static Result failure(E err) { - Result res; - res.ok = false; - new (res.storage) E(std::move(err)); - return res; - } + static Result failure(E err) { + Result res; + res.ok = false; + new (res.storage) E(std::move(err)); + return res; + } - ~Result() { - if (!ok) - reinterpret_cast<E*>(storage)->~E(); - } + ~Result() { + if (!ok) + reinterpret_cast<E*>(storage)->~E(); + } - Result(const Result& other) : ok(other.ok) { - if (!ok) - new (storage) E(*reinterpret_cast<const E*>(other.storage)); - } + Result(const Result& other) : ok(other.ok) { + if (!ok) + new (storage) E(*reinterpret_cast<const E*>(other.storage)); + } - Result(Result&& other) : ok(other.ok) { - if (!ok) - new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); - } + Result(Result&& other) : ok(other.ok) { + if (!ok) + new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); + } - Result& operator=(const Result&) = delete; + Result& operator=(const Result&) = delete; - bool isOk() const noexcept { - return ok; - } - const E& error() const { - return *reinterpret_cast<const E*>(storage); - } -}; + bool isOk() const noexcept { + return ok; + } + const E& error() const { + return *reinterpret_cast<const E*>(storage); + } + }; } // namespace nxst diff --git a/include/nxst/domain/title.hpp b/include/nxst/domain/title.hpp index 2a22410..7363451 100644 --- a/include/nxst/domain/title.hpp +++ b/include/nxst/domain/title.hpp @@ -1,32 +1,5 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors #pragma once -#include <algorithm> -#include <stdlib.h> #include <string> #include <unordered_map> #include <utility> @@ -34,57 +7,52 @@ #include <switch.h> -#include <nxst/domain/account.hpp> -#include <nxst/infra/fs/filesystem.hpp> -#include <nxst/infra/fs/io.hpp> - class Title { public: - void init(u8 saveDataType, u64 titleid, AccountUid userID, const std::string& name, + void init(u8 save_data_type, u64 title_id, AccountUid uid, const std::string& name, const std::string& author); - ~Title() = default; - std::string author(void); - std::pair<std::string, std::string> displayName(void); - u64 id(void); - std::string name(void); - std::string path(void); - u64 playTimeNanoseconds(void); - std::string playTime(void); - void playTimeNanoseconds(u64 playTimeNanoseconds); - u32 lastPlayedTimestamp(void); - void lastPlayedTimestamp(u32 lastPlayedTimestamp); - std::string fullPath(size_t index); - void refreshDirectories(void); - u64 saveId(); + std::string author() const; + std::pair<std::string, std::string> displayName() const; + u64 id() const; + std::string name() const; + std::string path() const; + u64 playTimeNanoseconds() const; + std::string playTime() const; + void playTimeNanoseconds(u64 ns); + u32 lastPlayedTimestamp() const; + void lastPlayedTimestamp(u32 ts); + std::string fullPath(size_t index) const; + void refreshDirectories(); + u64 saveId() const; void saveId(u64 id); - std::vector<std::string> saves(void); - u8 saveDataType(void); - AccountUid userId(void); - std::string userName(void); + std::vector<std::string> saves() const; + u8 saveDataType() const; + AccountUid userId() const; + std::string userName() const; private: - u64 mId; - u64 mSaveId; - AccountUid mUserId; - std::string mUserName; - std::string mName; - std::string mSafeName; - std::string mAuthor; - std::string mPath; - std::vector<std::string> mSaves; - std::vector<std::string> mFullSavePaths; - u8 mSaveDataType; - std::pair<std::string, std::string> mDisplayName; - u64 mPlayTimeNanoseconds; - u32 mLastPlayedTimestamp; + u64 m_id{0}; + u64 m_save_id{0}; + AccountUid m_uid{}; + std::string m_user_name; + std::string m_name; + std::string m_safe_name; + std::string m_author; + std::string m_path; + std::vector<std::string> m_saves; + std::vector<std::string> m_full_save_paths; + u8 m_save_data_type{0}; + std::pair<std::string, std::string> m_display_name; + u64 m_play_time_ns{0}; + u32 m_last_played_ts{0}; }; +bool areTitlesLoaded(); +void loadTitles(); +void sortTitles(); +void rotateSortMode(); void getTitle(Title& dst, AccountUid uid, size_t i); size_t getTitleCount(AccountUid uid); -void loadTitles(void); -bool areTitlesLoaded(void); -void sortTitles(void); -void rotateSortMode(void); void refreshDirectories(u64 id); -std::unordered_map<std::string, std::string> getCompleteTitleList(void); +std::unordered_map<std::string, std::string> getCompleteTitleList(); diff --git a/include/nxst/domain/util.hpp b/include/nxst/domain/util.hpp index e9b4aa6..6642618 100644 --- a/include/nxst/domain/util.hpp +++ b/include/nxst/domain/util.hpp @@ -1,51 +1,22 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors #pragma once -#include <sys/stat.h> #include <switch.h> -#include <nxst/domain/account.hpp> -#include <nxst/domain/common.hpp> -#include <nxst/infra/fs/io.hpp> - -// debug -#include <arpa/inet.h> -#include <sys/errno.h> -#include <sys/socket.h> - -void servicesExit(void); -Result servicesInit(void); -HidsysNotificationLedPattern blinkLedPattern(u8 times); +void servicesExit(); +Result servicesInit(); void blinkLed(u8 times); -namespace StringUtils { -std::string removeAccents(std::string str); -std::string removeNotAscii(std::string str); -std::u16string UTF8toUTF16(const char* src); -std::string elide(const std::string& s, size_t maxChars); -} // namespace StringUtils +namespace string_utils { + bool containsInvalidChar(const std::string& str); + std::string format(const char* fmt, ...) __attribute__((format(printf, 1, 2))); + std::string removeForbiddenCharacters(std::string src); + std::string UTF16toUTF8(const std::u16string& src); + void ltrim(std::string& s); + void rtrim(std::string& s); + void trim(std::string& s); + std::string removeAccents(std::string str); + std::string removeNotAscii(std::string str); + std::u16string UTF8toUTF16(const char* src); + std::string elide(const std::string& s, size_t max_chars); +} // namespace string_utils diff --git a/include/nxst/infra/fs/directory.hpp b/include/nxst/infra/fs/directory.hpp index 826f4b0..a51d404 100644 --- a/include/nxst/infra/fs/directory.hpp +++ b/include/nxst/infra/fs/directory.hpp @@ -1,55 +1,32 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors #pragma once -#include <dirent.h> -#include <errno.h> #include <string> #include <vector> #include <switch.h> -struct DirectoryEntry { - std::string name; - bool directory; -}; - class Directory { public: - Directory(const std::string& root); - ~Directory() = default; + explicit Directory(const std::string& path); - Result error(void); - std::string entry(size_t index); - bool folder(size_t index); - bool good(void); - size_t size(void); + bool good() const { + return m_good; + } + Result error() const { + return m_error; + } + size_t size() const { + return m_entries.size(); + } + std::string entry(size_t i) const; + bool folder(size_t i) const; private: - std::vector<struct DirectoryEntry> mList; - Result mError; - bool mGood; + struct Entry { + std::string name; + bool is_dir; + }; + std::vector<Entry> m_entries; + Result m_error{0}; + bool m_good{false}; }; diff --git a/include/nxst/infra/fs/filesystem.hpp b/include/nxst/infra/fs/filesystem.hpp index 77f3827..22eb421 100644 --- a/include/nxst/infra/fs/filesystem.hpp +++ b/include/nxst/infra/fs/filesystem.hpp @@ -1,36 +1,9 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors #pragma once #include <switch.h> -#include <nxst/domain/account.hpp> - -namespace FileSystem { -Result mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID); -int mount(FsFileSystem fs); -void unmount(void); -} // namespace FileSystem +namespace file_system { + Result mount(FsFileSystem* fs, u64 title_id, AccountUid uid); + int mount(FsFileSystem fs); + void unmount(); +} // namespace file_system diff --git a/include/nxst/infra/fs/handles.hpp b/include/nxst/infra/fs/handles.hpp index ef8d1b8..ae41240 100644 --- a/include/nxst/infra/fs/handles.hpp +++ b/include/nxst/infra/fs/handles.hpp @@ -5,67 +5,67 @@ namespace nxst { -// RAII wrapper for FsFileSystem — auto-closes on destruction. -struct FsFileSystemHandle { - FsFileSystem fs{}; - bool valid{false}; + // RAII wrapper for FsFileSystem — auto-closes on destruction. + struct FsFileSystemHandle { + FsFileSystem fs{}; + bool valid{false}; - FsFileSystemHandle() = default; - ~FsFileSystemHandle() { - if (valid) - fsFsClose(&fs); - } // NOLINT(modernize-use-equals-default) + FsFileSystemHandle() = default; + ~FsFileSystemHandle() { + if (valid) + fsFsClose(&fs); + } // NOLINT(modernize-use-equals-default) - FsFileSystemHandle(const FsFileSystemHandle&) = delete; - FsFileSystemHandle& operator=(const FsFileSystemHandle&) = delete; + FsFileSystemHandle(const FsFileSystemHandle&) = delete; + FsFileSystemHandle& operator=(const FsFileSystemHandle&) = delete; - FsFileSystem* get() { - return &fs; - } + FsFileSystem* get() { + return &fs; + } - void release() { - valid = false; - } // transfer ownership to devfs -}; + void release() { + valid = false; + } // transfer ownership to devfs + }; -// RAII wrapper for FILE* — auto-fclose on destruction. -struct FileHandle { - FILE* ptr{nullptr}; + // RAII wrapper for FILE* — auto-fclose on destruction. + struct FileHandle { + FILE* ptr{nullptr}; - explicit FileHandle(FILE* file) : ptr(file) {} - ~FileHandle() { - if (ptr != nullptr) - fclose(ptr); - } // NOLINT(modernize-use-equals-default) + explicit FileHandle(FILE* file) : ptr(file) {} + ~FileHandle() { + if (ptr != nullptr) + fclose(ptr); + } // NOLINT(modernize-use-equals-default) - FileHandle(const FileHandle&) = delete; - FileHandle& operator=(const FileHandle&) = delete; + FileHandle(const FileHandle&) = delete; + FileHandle& operator=(const FileHandle&) = delete; - explicit operator bool() const { - return ptr != nullptr; - } - FILE* get() const { - return ptr; - } -}; + explicit operator bool() const { + return ptr != nullptr; + } + FILE* get() const { + return ptr; + } + }; -// RAII wrapper for AccountProfile — auto-closes on destruction. -struct AccountProfileHandle { - AccountProfile profile{}; - bool valid{false}; + // RAII wrapper for AccountProfile — auto-closes on destruction. + struct AccountProfileHandle { + AccountProfile profile{}; + bool valid{false}; - AccountProfileHandle() = default; - ~AccountProfileHandle() { - if (valid) - accountProfileClose(&profile); - } // NOLINT(modernize-use-equals-default) + AccountProfileHandle() = default; + ~AccountProfileHandle() { + if (valid) + accountProfileClose(&profile); + } // NOLINT(modernize-use-equals-default) - AccountProfileHandle(const AccountProfileHandle&) = delete; - AccountProfileHandle& operator=(const AccountProfileHandle&) = delete; + AccountProfileHandle(const AccountProfileHandle&) = delete; + AccountProfileHandle& operator=(const AccountProfileHandle&) = delete; - AccountProfile* get() { - return &profile; - } -}; + AccountProfile* get() { + return &profile; + } + }; } // namespace nxst diff --git a/include/nxst/infra/fs/io.hpp b/include/nxst/infra/fs/io.hpp index 920d73f..418f52d 100644 --- a/include/nxst/infra/fs/io.hpp +++ b/include/nxst/infra/fs/io.hpp @@ -1,53 +1,19 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors #pragma once -#include <dirent.h> -#include <sys/stat.h> -#include <unistd.h> +#include <string> #include <switch.h> -#include <nxst/domain/account.hpp> #include <nxst/domain/result.hpp> -#include <nxst/domain/title.hpp> -#include <nxst/domain/util.hpp> -#include <nxst/infra/fs/directory.hpp> - -#define BUFFER_SIZE 0x80000 namespace io { -nxst::Result<std::string> backup(size_t index, AccountUid uid); -nxst::Result<std::string> restore(size_t index, AccountUid uid, size_t cellIndex, - const std::string& nameFromCell); + nxst::Result<std::string> backup(size_t index, AccountUid uid); + nxst::Result<std::string> restore(size_t index, AccountUid uid, const std::string& title_name); -Result copyDirectory(const std::string& srcPath, const std::string& dstPath); -void copyFile(const std::string& srcPath, const std::string& dstPath); -Result createDirectory(const std::string& path); -Result deleteFolderRecursively(const std::string& path); -bool directoryExists(const std::string& path); -bool fileExists(const std::string& path); + Result copyDirectory(const std::string& src, const std::string& dst); + void copyFile(const std::string& src, const std::string& dst); + Result createDirectory(const std::string& path); + Result deleteFolderRecursively(const std::string& path); + bool directoryExists(const std::string& path); + bool fileExists(const std::string& path); } // namespace io diff --git a/include/nxst/infra/net/transfer_receiver.hpp b/include/nxst/infra/net/transfer_receiver.hpp deleted file mode 100644 index 6f70f09..0000000 --- a/include/nxst/infra/net/transfer_receiver.hpp +++ /dev/null @@ -1 +0,0 @@ -#pragma once diff --git a/include/nxst/infra/net/transfer_sender.hpp b/include/nxst/infra/net/transfer_sender.hpp deleted file mode 100644 index 6f70f09..0000000 --- a/include/nxst/infra/net/transfer_sender.hpp +++ /dev/null @@ -1 +0,0 @@ -#pragma once diff --git a/include/nxst/infra/sys/logger.hpp b/include/nxst/infra/sys/logger.hpp index f8f6879..7fca98b 100644 --- a/include/nxst/infra/sys/logger.hpp +++ b/include/nxst/infra/sys/logger.hpp @@ -1,59 +1,14 @@ #pragma once -#include <cstring> -#include <string> - // New API — use these going forward. namespace nxst::log { -enum class Level { Debug, Info, Warn, Error }; + enum class Level { Debug, Info, Warn, Error }; -void write(Level level, const char* fmt, ...) __attribute__((format(printf, 2, 3))); -void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2))); -void info(const char* fmt, ...) __attribute__((format(printf, 1, 2))); -void warn(const char* fmt, ...) __attribute__((format(printf, 1, 2))); -void error(const char* fmt, ...) __attribute__((format(printf, 1, 2))); + void write(Level level, const char* fmt, ...) __attribute__((format(printf, 2, 3))); + void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2))); + void info(const char* fmt, ...) __attribute__((format(printf, 1, 2))); + void warn(const char* fmt, ...) __attribute__((format(printf, 1, 2))); + void error(const char* fmt, ...) __attribute__((format(printf, 1, 2))); -// No-op: writes are immediate. Kept for source compatibility during migration. -inline void flush() {} - -} // namespace nxst::log - -// Backward-compat shim — existing Logger::getInstance().log(...) call sites compile -// unchanged. Format args are dropped (same behavior as broken original). Migrate -// call sites to nxst::log::* in Phase 3. -struct Logger { - static Logger& getInstance() { - static Logger instance; - return instance; - } - - // clang-tidy naming suppressed: these must match existing call sites during migration. - static constexpr const char* INFO = "[INFO]"; // NOLINT(readability-identifier-naming) - static constexpr const char* DEBUG = "[DEBUG]"; // NOLINT(readability-identifier-naming) - static constexpr const char* ERROR = "[ERROR]"; // NOLINT(readability-identifier-naming) - static constexpr const char* WARN = "[WARN]"; // NOLINT(readability-identifier-naming) - - static void flush() { - nxst::log::flush(); - } - - // Args intentionally dropped — format string still logged for visibility. - template <typename... Args> - void log(const std::string& level, const std::string& fmt, Args&&... /*args*/) { - if (level == ERROR) - nxst::log::error("%s", fmt.c_str()); - else if (level == WARN) - nxst::log::warn("%s", fmt.c_str()); - else if (level == DEBUG) - nxst::log::debug("%s", fmt.c_str()); - else - nxst::log::info("%s", fmt.c_str()); - } - - Logger() = default; - ~Logger() = default; - - Logger(const Logger&) = delete; // NOLINT(modernize-use-equals-delete) - Logger& operator=(const Logger&) = delete; // NOLINT(modernize-use-equals-delete) -}; +} // namespace nxst::log \ No newline at end of file diff --git a/include/nxst/service/transfer_service.hpp b/include/nxst/service/transfer_service.hpp index 8093374..886adce 100644 --- a/include/nxst/service/transfer_service.hpp +++ b/include/nxst/service/transfer_service.hpp @@ -9,105 +9,105 @@ namespace nxst { -class TransferService { - public: - int startSend(size_t title_index, AccountUid uid); - void cancelSend(); + 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; - } + 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(); + 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; - } + 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}; + 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{}; + // 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; + // 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; + // 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; }; - 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 diff --git a/include/nxst/ui/card.hpp b/include/nxst/ui/card.hpp deleted file mode 100644 index 6d62e86..0000000 --- a/include/nxst/ui/card.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include <pu/Plutonium> - -#include <nxst/ui/theme.hpp> - -namespace ui { - -class Card { - public: - pu::ui::elm::Rectangle::Ref bg; - - Card(pu::ui::Layout* parent, int x, int y, int w, int h, pu::ui::Color color = theme::color::BgSurface, - int rad = theme::radius::lg) { - bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad); - parent->Add(bg); - } -}; -} // namespace ui diff --git a/include/nxst/ui/const.h b/include/nxst/ui/const.h deleted file mode 100644 index 6d9908f..0000000 --- a/include/nxst/ui/const.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include <nxst/ui/theme.hpp> - -#define COLOR(hex) pu::ui::Color::FromHex(hex) -#define BACKGROUND_COLOR theme::color::BgBase diff --git a/include/nxst/ui/header_bar.hpp b/include/nxst/ui/header_bar.hpp index 6a79a41..40ce919 100644 --- a/include/nxst/ui/header_bar.hpp +++ b/include/nxst/ui/header_bar.hpp @@ -3,81 +3,80 @@ #include <nxst/domain/account.hpp> #include <nxst/ui/theme.hpp> -#include <nxst/ui/ui_context.hpp> namespace ui { -class HeaderBar { - private: - pu::ui::elm::Rectangle::Ref bg; - pu::ui::elm::Rectangle::Ref divider; - pu::ui::elm::TextBlock::Ref appName; - pu::ui::elm::TextBlock::Ref subtitle; - pu::ui::elm::Rectangle::Ref chipBg; - pu::ui::elm::Image::Ref avatar; - pu::ui::elm::TextBlock::Ref userName; + class HeaderBar { + private: + pu::ui::elm::Rectangle::Ref bg; + pu::ui::elm::Rectangle::Ref divider; + pu::ui::elm::TextBlock::Ref appName; + pu::ui::elm::TextBlock::Ref subtitle; + pu::ui::elm::Rectangle::Ref chipBg; + pu::ui::elm::Image::Ref avatar; + pu::ui::elm::TextBlock::Ref userName; - public: - HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") { - using namespace theme; + public: + HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") { + using namespace theme; - bg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface); - divider = pu::ui::elm::Rectangle::New(0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider); + bg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface); + divider = pu::ui::elm::Rectangle::New(0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider); - appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST"); - appName->SetFont(type::font(type::Title)); - appName->SetColor(color::TextPrimary); + appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST"); + appName->SetFont(type::font(type::Title)); + appName->SetColor(color::TextPrimary); - subtitle = pu::ui::elm::TextBlock::New(space::lg, 46, sub); - subtitle->SetFont(type::font(type::Caption)); - subtitle->SetColor(color::TextMuted); + subtitle = pu::ui::elm::TextBlock::New(space::lg, 46, sub); + subtitle->SetFont(type::font(type::Caption)); + subtitle->SetColor(color::TextMuted); - const int chipW = 280; - const int chipX = layout::ScreenW - chipW - space::lg; - chipBg = pu::ui::elm::Rectangle::New(chipX, 16, chipW, 40, color::BgSurface2, radius::pill); - chipBg->SetVisible(false); + const int chipW = 280; + const int chipX = layout::ScreenW - chipW - space::lg; + chipBg = pu::ui::elm::Rectangle::New(chipX, 16, chipW, 40, color::BgSurface2, radius::pill); + chipBg->SetVisible(false); - avatar = pu::ui::elm::Image::New(chipX + 4, 20, ""); - avatar->SetWidth(32); - avatar->SetHeight(32); - avatar->SetVisible(false); + avatar = pu::ui::elm::Image::New(chipX + 4, 20, ""); + avatar->SetWidth(32); + avatar->SetHeight(32); + avatar->SetVisible(false); - userName = pu::ui::elm::TextBlock::New(chipX + 44, 24, ""); - userName->SetFont(type::font(type::Body)); - userName->SetColor(color::TextPrimary); - userName->SetVisible(false); + userName = pu::ui::elm::TextBlock::New(chipX + 44, 24, ""); + userName->SetFont(type::font(type::Body)); + userName->SetColor(color::TextPrimary); + userName->SetVisible(false); - parent->Add(bg); - parent->Add(divider); - parent->Add(appName); - parent->Add(subtitle); - parent->Add(chipBg); - parent->Add(avatar); - parent->Add(userName); - } + parent->Add(bg); + parent->Add(divider); + parent->Add(appName); + parent->Add(subtitle); + parent->Add(chipBg); + parent->Add(avatar); + parent->Add(userName); + } - void SetUser(const std::optional<AccountUid>& uid, const std::string& name) { - const bool show = uid.has_value(); - chipBg->SetVisible(show); - userName->SetVisible(show); - if (show) { - userName->SetText(name); - std::string path = Account::iconPath(*uid); - if (!path.empty()) { - avatar->SetImage(path); - avatar->SetWidth(32); - avatar->SetHeight(32); - avatar->SetVisible(avatar->IsImageValid()); + void SetUser(const std::optional<AccountUid>& uid, const std::string& name) { + const bool show = uid.has_value(); + chipBg->SetVisible(show); + userName->SetVisible(show); + if (show) { + userName->SetText(name); + std::string path = account::iconPath(*uid); + if (!path.empty()) { + avatar->SetImage(path); + avatar->SetWidth(32); + avatar->SetHeight(32); + avatar->SetVisible(avatar->IsImageValid()); + } else { + avatar->SetVisible(false); + } } else { avatar->SetVisible(false); } - } else { - avatar->SetVisible(false); } - } - void SetSubtitle(const std::string& text) { - subtitle->SetText(text); - } -}; + void SetSubtitle(const std::string& text) { + subtitle->SetText(text); + } + }; } // namespace ui diff --git a/include/nxst/ui/hint_bar.hpp b/include/nxst/ui/hint_bar.hpp index 89a90d3..322ecd3 100644 --- a/include/nxst/ui/hint_bar.hpp +++ b/include/nxst/ui/hint_bar.hpp @@ -8,49 +8,49 @@ namespace ui { -struct Hint { - std::string glyph; - std::string label; -}; + struct Hint { + std::string glyph; + std::string label; + }; -class HintBar { - private: - pu::ui::Layout* parent; - pu::ui::elm::Rectangle::Ref bg; - pu::ui::elm::Rectangle::Ref divider; - std::vector<pu::ui::elm::TextBlock::Ref> labels; + class HintBar { + private: + pu::ui::Layout* parent; + pu::ui::elm::Rectangle::Ref bg; + pu::ui::elm::Rectangle::Ref divider; + std::vector<pu::ui::elm::TextBlock::Ref> labels; - public: - HintBar(pu::ui::Layout* p) : parent(p) { - using namespace theme; - bg = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, layout::HintH, - color::BgSurface); - divider = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, 1, - color::Divider); - parent->Add(bg); - parent->Add(divider); - } - - void SetHints(const std::vector<Hint>& hints) { - using namespace theme; - for (auto& l : labels) - l->SetVisible(false); - labels.clear(); - - int x = layout::ScreenW - space::lg; - int y = layout::ScreenH - layout::HintH + 18; - for (auto it = hints.rbegin(); it != hints.rend(); ++it) { - std::string text = it->glyph + " " + it->label; - auto tb = pu::ui::elm::TextBlock::New(0, y, text); - tb->SetFont(type::font(type::Label)); - tb->SetColor(color::TextSecondary); - int w = tb->GetWidth(); - x -= w; - tb->SetX(x); - x -= space::xl; - parent->Add(tb); - labels.push_back(tb); + public: + HintBar(pu::ui::Layout* p) : parent(p) { + using namespace theme; + bg = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, + layout::HintH, color::BgSurface); + divider = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, 1, + color::Divider); + parent->Add(bg); + parent->Add(divider); } - } -}; + + void SetHints(const std::vector<Hint>& hints) { + using namespace theme; + for (auto& l : labels) + l->SetVisible(false); + labels.clear(); + + int x = layout::ScreenW - space::lg; + int y = layout::ScreenH - layout::HintH + 18; + for (auto it = hints.rbegin(); it != hints.rend(); ++it) { + std::string text = it->glyph + " " + it->label; + auto tb = pu::ui::elm::TextBlock::New(0, y, text); + tb->SetFont(type::font(type::Label)); + tb->SetColor(color::TextSecondary); + int w = tb->GetWidth(); + x -= w; + tb->SetX(x); + x -= space::xl; + parent->Add(tb); + labels.push_back(tb); + } + } + }; } // namespace ui diff --git a/include/nxst/ui/theme.hpp b/include/nxst/ui/theme.hpp index 7a3b677..a974c8a 100644 --- a/include/nxst/ui/theme.hpp +++ b/include/nxst/ui/theme.hpp @@ -5,75 +5,75 @@ #include <pu/Plutonium> namespace theme { -using pu::ui::Color; + using pu::ui::Color; -namespace color { -constexpr Color BgBase{0x10, 0x14, 0x1C, 0xFF}; -constexpr Color BgSurface{0x18, 0x1F, 0x2A, 0xFF}; -constexpr Color BgSurface2{0x22, 0x2B, 0x39, 0xFF}; -constexpr Color Scrim{0x00, 0x00, 0x00, 0xB8}; + namespace color { + constexpr Color BgBase{0x10, 0x14, 0x1C, 0xFF}; + constexpr Color BgSurface{0x18, 0x1F, 0x2A, 0xFF}; + constexpr Color BgSurface2{0x22, 0x2B, 0x39, 0xFF}; + constexpr Color Scrim{0x00, 0x00, 0x00, 0xB8}; -constexpr Color Primary{0xE2, 0x4B, 0x55, 0xFF}; -constexpr Color PrimaryDim{0x9C, 0x33, 0x3A, 0xFF}; -constexpr Color Accent{0x4A, 0xC2, 0xE0, 0xFF}; + constexpr Color Primary{0xE2, 0x4B, 0x55, 0xFF}; + constexpr Color PrimaryDim{0x9C, 0x33, 0x3A, 0xFF}; + constexpr Color Accent{0x4A, 0xC2, 0xE0, 0xFF}; -constexpr Color TextPrimary{0xF2, 0xF4, 0xF8, 0xFF}; -constexpr Color TextSecondary{0xB6, 0xBE, 0xCB, 0xFF}; -constexpr Color TextMuted{0x70, 0x7A, 0x8C, 0xFF}; + constexpr Color TextPrimary{0xF2, 0xF4, 0xF8, 0xFF}; + constexpr Color TextSecondary{0xB6, 0xBE, 0xCB, 0xFF}; + constexpr Color TextMuted{0x70, 0x7A, 0x8C, 0xFF}; -constexpr Color Success{0x55, 0xC8, 0x8A, 0xFF}; -constexpr Color Error{0xE0, 0x6C, 0x6C, 0xFF}; -constexpr Color Warning{0xE6, 0xB4, 0x55, 0xFF}; + constexpr Color Success{0x55, 0xC8, 0x8A, 0xFF}; + constexpr Color Error{0xE0, 0x6C, 0x6C, 0xFF}; + constexpr Color Warning{0xE6, 0xB4, 0x55, 0xFF}; -constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF}; -constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF}; -} // namespace color + constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF}; + constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF}; + } // namespace color -namespace space { -constexpr int xs = 4; -constexpr int sm = 8; -constexpr int md = 16; -constexpr int lg = 24; -constexpr int xl = 32; -constexpr int xxl = 48; -} // namespace space + namespace space { + constexpr int xs = 4; + constexpr int sm = 8; + constexpr int md = 16; + constexpr int lg = 24; + constexpr int xl = 32; + constexpr int xxl = 48; + } // namespace space -namespace radius { -constexpr int sm = 6; -constexpr int md = 12; -constexpr int lg = 20; -constexpr int pill = 9999; -} // namespace radius + namespace radius { + constexpr int sm = 6; + constexpr int md = 12; + constexpr int lg = 20; + constexpr int pill = 9999; + } // namespace radius -namespace type { -constexpr int Display = 38; -constexpr int Title = 30; -constexpr int Body = 25; -constexpr int Label = 20; -constexpr int Caption = 18; + namespace type { + constexpr int Display = 38; + constexpr int Title = 30; + constexpr int Body = 25; + constexpr int Label = 20; + constexpr int Caption = 18; -inline std::string font(int size) { - return "DefaultFont@" + std::to_string(size); -} -} // namespace type + inline std::string font(int size) { + return "DefaultFont@" + std::to_string(size); + } + } // namespace type -namespace layout { -constexpr int ScreenW = 1280; -constexpr int ScreenH = 720; -constexpr int HeaderH = 72; -constexpr int HintH = 56; -constexpr int ContentTop = HeaderH; -constexpr int ContentH = ScreenH - HeaderH - HintH; -} // namespace layout + namespace layout { + constexpr int ScreenW = 1280; + constexpr int ScreenH = 720; + constexpr int HeaderH = 72; + constexpr int HintH = 56; + constexpr int ContentTop = HeaderH; + constexpr int ContentH = ScreenH - HeaderH - HintH; + } // namespace layout -namespace motion { -constexpr int FadeFrames = 20; -constexpr int SlideFrames = 14; -constexpr int SpinnerFrames = 72; -} // namespace motion + namespace motion { + constexpr int FadeFrames = 20; + constexpr int SlideFrames = 14; + constexpr int SpinnerFrames = 72; + } // namespace motion -namespace font { -constexpr const char* Default = "Inter"; -constexpr const char* Medium = "InterMedium"; -} // namespace font + namespace font { + constexpr const char* Default = "Inter"; + constexpr const char* Medium = "InterMedium"; + } // namespace font } // namespace theme diff --git a/include/nxst/ui/titles_layout.hpp b/include/nxst/ui/titles_layout.hpp index f20dbae..b7b507c 100644 --- a/include/nxst/ui/titles_layout.hpp +++ b/include/nxst/ui/titles_layout.hpp @@ -7,57 +7,56 @@ #include <nxst/domain/account.hpp> #include <nxst/domain/title.hpp> -#include <nxst/ui/const.h> #include <nxst/ui/header_bar.hpp> #include <nxst/ui/hint_bar.hpp> namespace ui { -enum class TitlesFocus { List, Actions }; -enum class TitlesAction { Transfer, Receive }; + enum class TitlesFocus { List, Actions }; + enum class TitlesAction { Transfer, Receive }; -class TitlesLayout : public pu::ui::Layout { - private: - pu::ui::elm::Menu::Ref titlesMenu; - std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache; - bool m_inputLocked = false; - std::unique_ptr<HeaderBar> header; - std::unique_ptr<HintBar> hints; + class TitlesLayout : public pu::ui::Layout { + private: + pu::ui::elm::Menu::Ref titlesMenu; + std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache; + bool m_inputLocked = false; + std::unique_ptr<HeaderBar> header; + std::unique_ptr<HintBar> hints; - pu::ui::elm::Rectangle::Ref panelBg; - pu::ui::elm::TextBlock::Ref panelTitle; - pu::ui::elm::TextBlock::Ref panelHint; - pu::ui::elm::Rectangle::Ref btnTransferBg; - pu::ui::elm::TextBlock::Ref btnTransferText; - pu::ui::elm::Rectangle::Ref btnReceiveBg; - pu::ui::elm::TextBlock::Ref btnReceiveText; - pu::ui::elm::TextBlock::Ref panelFooter; - pu::ui::elm::TextBlock::Ref emptyText; - pu::ui::elm::TextBlock::Ref emptySub; + pu::ui::elm::Rectangle::Ref panelBg; + pu::ui::elm::TextBlock::Ref panelTitle; + pu::ui::elm::TextBlock::Ref panelHint; + pu::ui::elm::Rectangle::Ref btnTransferBg; + pu::ui::elm::TextBlock::Ref btnTransferText; + pu::ui::elm::Rectangle::Ref btnReceiveBg; + pu::ui::elm::TextBlock::Ref btnReceiveText; + pu::ui::elm::TextBlock::Ref panelFooter; + pu::ui::elm::TextBlock::Ref emptyText; + pu::ui::elm::TextBlock::Ref emptySub; - AccountUid current_uid{}; - TitlesFocus focus = TitlesFocus::List; - TitlesAction action = TitlesAction::Transfer; - int lockedListIndex = 0; + AccountUid current_uid{}; + TitlesFocus focus = TitlesFocus::List; + TitlesAction action = TitlesAction::Transfer; + int lockedListIndex = 0; - void refreshPanel(); - void refreshButtons(); - void updateHints(); - void runTransfer(int index, Title& title); - void runReceive(int index, Title& title); + void refreshPanel(); + void refreshButtons(); + void updateHints(); + void runTransfer(int index, Title& title); + void runReceive(int index, Title& title); - public: - TitlesLayout(); - void InitTitles(AccountUid uid); - void LockInput() { - m_inputLocked = true; - } - void UnlockInput() { - m_inputLocked = false; - } + public: + TitlesLayout(); + void InitTitles(AccountUid uid); + void LockInput() { + m_inputLocked = true; + } + void UnlockInput() { + m_inputLocked = false; + } - void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); + void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); - PU_SMART_CTOR(TitlesLayout) -}; + PU_SMART_CTOR(TitlesLayout) + }; } // namespace ui diff --git a/include/nxst/ui/transfer_overlay.hpp b/include/nxst/ui/transfer_overlay.hpp index 16db4e0..1d17b33 100644 --- a/include/nxst/ui/transfer_overlay.hpp +++ b/include/nxst/ui/transfer_overlay.hpp @@ -6,79 +6,79 @@ namespace ui { -class TransferOverlay : public pu::ui::Overlay { - private: - pu::ui::elm::Rectangle::Ref card; - pu::ui::elm::TextBlock::Ref titleText; - pu::ui::elm::TextBlock::Ref statusText; - pu::ui::elm::Rectangle::Ref progressTrack; - pu::ui::elm::ProgressBar::Ref progressBar; - pu::ui::elm::TextBlock::Ref indeterminateText; - pu::ui::elm::TextBlock::Ref hintText; + class TransferOverlay : public pu::ui::Overlay { + private: + pu::ui::elm::Rectangle::Ref card; + pu::ui::elm::TextBlock::Ref titleText; + pu::ui::elm::TextBlock::Ref statusText; + pu::ui::elm::Rectangle::Ref progressTrack; + pu::ui::elm::ProgressBar::Ref progressBar; + pu::ui::elm::TextBlock::Ref indeterminateText; + pu::ui::elm::TextBlock::Ref hintText; - static constexpr int CardW = 720; - static constexpr int CardH = 360; - static constexpr int CardX = (theme::layout::ScreenW - CardW) / 2; - static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2; + static constexpr int CardW = 720; + static constexpr int CardH = 360; + static constexpr int CardX = (theme::layout::ScreenW - CardW) / 2; + static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2; - public: - TransferOverlay(const std::string& title) - : Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) { - using namespace theme; + public: + TransferOverlay(const std::string& title) + : Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) { + using namespace theme; - card = pu::ui::elm::Rectangle::New(CardX, CardY, CardW, CardH, color::BgSurface, radius::lg); + card = pu::ui::elm::Rectangle::New(CardX, CardY, CardW, CardH, color::BgSurface, radius::lg); - titleText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg, title); - titleText->SetFont(type::font(type::Title)); - titleText->SetColor(color::TextPrimary); + titleText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg, title); + titleText->SetFont(type::font(type::Title)); + titleText->SetColor(color::TextPrimary); - statusText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg + 56, ""); - statusText->SetFont(type::font(type::Body)); - statusText->SetColor(color::TextSecondary); + statusText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg + 56, ""); + statusText->SetFont(type::font(type::Body)); + statusText->SetColor(color::TextSecondary); - int barX = CardX + space::lg; - int barY = CardY + space::lg + 56 + 56; - int barW = CardW - 2 * space::lg; + int barX = CardX + space::lg; + int barY = CardY + space::lg + 56 + 56; + int barW = CardW - 2 * space::lg; - progressTrack = pu::ui::elm::Rectangle::New(barX, barY, barW, 8, color::Divider, radius::sm); + progressTrack = pu::ui::elm::Rectangle::New(barX, barY, barW, 8, color::Divider, radius::sm); - progressBar = pu::ui::elm::ProgressBar::New(barX, barY, barW, 8, 100.0); - progressBar->SetProgressColor(color::Primary); - progressBar->SetBackgroundColor(color::Divider); + progressBar = pu::ui::elm::ProgressBar::New(barX, barY, barW, 8, 100.0); + progressBar->SetProgressColor(color::Primary); + progressBar->SetBackgroundColor(color::Divider); - indeterminateText = pu::ui::elm::TextBlock::New(barX, barY - 4, "Preparing transfer..."); - indeterminateText->SetFont(type::font(type::Body)); - indeterminateText->SetColor(color::TextMuted); - indeterminateText->SetVisible(false); + indeterminateText = pu::ui::elm::TextBlock::New(barX, barY - 4, "Preparing transfer..."); + indeterminateText->SetFont(type::font(type::Body)); + indeterminateText->SetColor(color::TextMuted); + indeterminateText->SetVisible(false); - hintText = - pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + CardH - space::lg - 18, "B to cancel"); - hintText->SetFont(type::font(type::Caption)); - hintText->SetColor(color::TextMuted); + hintText = + pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + CardH - space::lg - 18, "B to cancel"); + hintText->SetFont(type::font(type::Caption)); + hintText->SetColor(color::TextMuted); - this->Add(card); - this->Add(titleText); - this->Add(statusText); - this->Add(progressTrack); - this->Add(progressBar); - this->Add(indeterminateText); - this->Add(hintText); - } - PU_SMART_CTOR(TransferOverlay) + this->Add(card); + this->Add(titleText); + this->Add(statusText); + this->Add(progressTrack); + this->Add(progressBar); + this->Add(indeterminateText); + this->Add(hintText); + } + PU_SMART_CTOR(TransferOverlay) - void SetStatus(const std::string& status) { - statusText->SetText(StringUtils::elide(status, 56)); - } + void SetStatus(const std::string& status) { + statusText->SetText(string_utils::elide(status, 56)); + } - void SetProgress(double val) { - progressBar->SetProgress(val); - } + void SetProgress(double val) { + progressBar->SetProgress(val); + } - void SetProgressVisible(bool visible) { - progressTrack->SetVisible(visible); - progressBar->SetVisible(visible); - indeterminateText->SetVisible(!visible); - } -}; + void SetProgressVisible(bool visible) { + progressTrack->SetVisible(visible); + progressBar->SetVisible(visible); + indeterminateText->SetVisible(!visible); + } + }; } // namespace ui diff --git a/include/nxst/ui/ui_context.hpp b/include/nxst/ui/ui_context.hpp deleted file mode 100644 index 18e2f99..0000000 --- a/include/nxst/ui/ui_context.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include <optional> -#include <string> - -#include <switch.h> - -#include <nxst/domain/account.hpp> - -namespace ui { -struct UiContext { - std::optional<AccountUid> selectedUser; - std::string selectedUserName; -}; -} // namespace ui diff --git a/include/nxst/ui/users_layout.hpp b/include/nxst/ui/users_layout.hpp index e01072d..c037621 100644 --- a/include/nxst/ui/users_layout.hpp +++ b/include/nxst/ui/users_layout.hpp @@ -2,27 +2,26 @@ #include <pu/Plutonium> -#include <nxst/ui/const.h> #include <nxst/ui/header_bar.hpp> #include <nxst/ui/hint_bar.hpp> namespace ui { -class UsersLayout : public pu::ui::Layout { - private: - pu::ui::elm::Menu::Ref usersMenu; - pu::ui::elm::Rectangle::Ref loadingBg; - pu::ui::elm::TextBlock::Ref loadingText; - std::unique_ptr<HeaderBar> header; - std::unique_ptr<HintBar> hints; + class UsersLayout : public pu::ui::Layout { + private: + pu::ui::elm::Menu::Ref usersMenu; + pu::ui::elm::Rectangle::Ref loadingBg; + pu::ui::elm::TextBlock::Ref loadingText; + std::unique_ptr<HeaderBar> header; + std::unique_ptr<HintBar> hints; - public: - UsersLayout(); + public: + UsersLayout(); - void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); + void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); - int32_t GetCurrentIndex(); + int32_t GetCurrentIndex(); - PU_SMART_CTOR(UsersLayout) -}; + PU_SMART_CTOR(UsersLayout) + }; } // namespace ui diff --git a/src/app/main.cpp b/src/app/main.cpp index 12654c2..d95d70b 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,11 +1,10 @@ #include <unistd.h> -#include <nxst/app/main.hpp> #include <nxst/app/main_application.hpp> #include <nxst/domain/util.hpp> namespace ui { -extern MainApplication* mainApp; + extern MainApplication* mainApp; } static int nxlink_sock = -1; diff --git a/src/app/main_application.cpp b/src/app/main_application.cpp index dcf627a..593a214 100644 --- a/src/app/main_application.cpp +++ b/src/app/main_application.cpp @@ -1,24 +1,20 @@ -#include <string> -#include <switch/services/hid.h> -#include <vector> - #include <switch.h> #include <nxst/app/main_application.hpp> namespace ui { -MainApplication* mainApp; + 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); -} + 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); + } } // namespace ui \ No newline at end of file diff --git a/src/domain/account.cpp b/src/domain/account.cpp index 40627f1..adcb23b 100644 --- a/src/domain/account.cpp +++ b/src/domain/account.cpp @@ -1,38 +1,16 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors #include <cstdio> +#include <cstring> +#include <map> #include <sys/stat.h> +#include <vector> #include <nxst/domain/account.hpp> #include <nxst/infra/fs/handles.hpp> -static std::map<AccountUid, User> mUsers; +static std::map<AccountUid, User> s_users; -Result Account::init(void) { +Result account::init() { Result res = accountInitialize(AccountServiceType_Application); if (R_FAILED(res)) return res; @@ -40,51 +18,50 @@ Result Account::init(void) { AccountUid uids[8]; s32 count = 0; accountListAllUsers(uids, 8, &count); - for (s32 i = 0; i < count; i++) { - Account::username(uids[i]); // populates mUsers as side effect + for (s32 i = 0; i < count; ++i) { + username(uids[i]); // populate cache } return 0; } -void Account::exit(void) { +void account::exit() { accountExit(); } -std::vector<AccountUid> Account::ids(void) { - std::vector<AccountUid> v; - for (auto& value : mUsers) { - v.push_back(value.second.id); +std::vector<AccountUid> account::ids() { + std::vector<AccountUid> result; + result.reserve(s_users.size()); + for (const auto& pair : s_users) { + result.push_back(pair.second.id); } - return v; + return result; } -static User getUser(AccountUid id) { +static User fetchUser(AccountUid id) { User user{id, ""}; nxst::AccountProfileHandle profile; - AccountProfileBase profilebase; - memset(&profilebase, 0, sizeof(profilebase)); + AccountProfileBase base{}; if (R_SUCCEEDED(accountGetProfile(profile.get(), id))) { profile.valid = true; - if (R_SUCCEEDED(accountProfileGet(profile.get(), NULL, &profilebase))) { - user.name = std::string(profilebase.nickname); + if (R_SUCCEEDED(accountProfileGet(profile.get(), nullptr, &base))) { + user.name = std::string(base.nickname); } } return user; } -std::string Account::username(AccountUid id) { - std::map<AccountUid, User>::const_iterator got = mUsers.find(id); - if (got == mUsers.end()) { - User user = getUser(id); - mUsers.insert({id, user}); +std::string account::username(AccountUid id) { + auto it = s_users.find(id); + if (it == s_users.end()) { + User user = fetchUser(id); + s_users.emplace(id, user); return user.name; } - - return got->second.name; + return it->second.name; } -std::string Account::iconPath(AccountUid id) { +std::string account::iconPath(AccountUid id) { char path[128]; snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg", id.uid[0], id.uid[1]); @@ -101,37 +78,18 @@ std::string Account::iconPath(AccountUid id) { return ""; profile.valid = true; - u32 imgSize = 0; - if (R_FAILED(accountProfileGetImageSize(profile.get(), &imgSize)) || imgSize == 0) + u32 img_size = 0; + if (R_FAILED(accountProfileGetImageSize(profile.get(), &img_size)) || img_size == 0) return ""; - std::vector<u8> buf(imgSize); - u32 outSize = 0; - if (R_FAILED(accountProfileLoadImage(profile.get(), buf.data(), imgSize, &outSize)) || outSize == 0) + std::vector<u8> buf(img_size); + u32 out_size = 0; + if (R_FAILED(accountProfileLoadImage(profile.get(), buf.data(), img_size, &out_size)) || out_size == 0) return ""; nxst::FileHandle f(fopen(path, "wb")); if (!f) return ""; - fwrite(buf.data(), 1, outSize, f.get()); + fwrite(buf.data(), 1, out_size, f.get()); return std::string(path); } - -AccountUid Account::selectAccount(void) { - LibAppletArgs args; - libappletArgsCreate(&args, 0x10000); - u8 st_in[0xA0] = {0}; - u8 st_out[0x18] = {0}; - size_t repsz; - - Result res = - libappletLaunch(AppletId_LibraryAppletPlayerSelect, &args, st_in, 0xA0, st_out, 0x18, &repsz); - if (R_SUCCEEDED(res)) { - u64 lres = *(u64*)st_out; - AccountUid uid = *(AccountUid*)&st_out[8]; - if (lres == 0) - return uid; - } - - return AccountUid{}; -} \ No newline at end of file diff --git a/src/domain/common.cpp b/src/domain/common.cpp deleted file mode 100644 index b59ca95..0000000 --- a/src/domain/common.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - -#include <nxst/domain/common.hpp> - -std::string DateTime::timeStr(void) { - time_t unixTime; - struct tm timeStruct; - time(&unixTime); - localtime_r(&unixTime, &timeStruct); - return StringUtils::format("%02i:%02i:%02i", timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec); -} - -std::string DateTime::dateTimeStr(void) { - time_t unixTime; - struct tm timeStruct; - time(&unixTime); - localtime_r(&unixTime, &timeStruct); - return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, - timeStruct.tm_mday, timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec); -} - -std::string DateTime::logDateTime(void) { - time_t unixTime; - struct tm timeStruct; - time(&unixTime); - localtime_r(&unixTime, &timeStruct); - return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900, - timeStruct.tm_mon + 1, timeStruct.tm_mday, timeStruct.tm_hour, - timeStruct.tm_min, timeStruct.tm_sec); -} - -std::string StringUtils::UTF16toUTF8(const std::u16string& src) { - static std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert; - std::string dst = convert.to_bytes(src); - return dst; -} - -std::string StringUtils::removeForbiddenCharacters(std::string src) { - static const std::string illegalChars = ".,!\\/:?*\"<>|"; - for (size_t i = 0, sz = src.length(); i < sz; i++) { - if (illegalChars.find(src[i]) != std::string::npos) { - src[i] = ' '; - } - } - - size_t i; - for (i = src.length() - 1; i > 0 && src[i] == ' '; i--) - ; - src.erase(i + 1, src.length() - i); - - return src; -} - -std::string StringUtils::format(const std::string fmt_str, ...) { - va_list ap; - char* fp = NULL; - va_start(ap, fmt_str); - vasprintf(&fp, fmt_str.c_str(), ap); - va_end(ap); - std::unique_ptr<char[]> formatted(fp); - return std::string(formatted.get()); -} - -bool StringUtils::containsInvalidChar(const std::string& str) { - for (size_t i = 0, sz = str.length(); i < sz; i++) { - if (!isascii(str[i])) { - return true; - } - } - return false; -} - -void StringUtils::ltrim(std::string& s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { - return !std::isspace(ch); - })); -} - -void StringUtils::rtrim(std::string& s) { - s.erase(std::find_if(s.rbegin(), s.rend(), - [](int ch) { - return !std::isspace(ch); - }) - .base(), - s.end()); -} - -void StringUtils::trim(std::string& s) { - ltrim(s); - rtrim(s); -} - -char* getConsoleIP(void) { - struct in_addr in; - in.s_addr = gethostid(); - return inet_ntoa(in); -} \ No newline at end of file diff --git a/src/domain/title.cpp b/src/domain/title.cpp index 38aacc4..c86616a 100644 --- a/src/domain/title.cpp +++ b/src/domain/title.cpp @@ -1,260 +1,205 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ +// Copyright (C) 2024-2026 NXST contributors +#include <algorithm> +#include <cstring> +#include <vector> -#include <nxst/app/main.hpp> +#include "nxst/domain/account.hpp" #include <nxst/domain/title.hpp> +#include <nxst/domain/util.hpp> +#include <nxst/infra/fs/directory.hpp> +#include <nxst/infra/sys/logger.hpp> + +using sort_t = enum { SortAlpha, SortLastPlayed, SortPlayTime, SortModesCount }; +static constexpr const char* kEmptySave = "New..."; +static sort_t s_sort_mode = SortAlpha; static std::unordered_map<AccountUid, std::vector<Title>> titles; -static bool s_titlesLoaded = false; +static bool s_titles_loaded = false; -bool areTitlesLoaded(void) { - return s_titlesLoaded; -} - -void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name, +void Title::init(u8 save_data_type, u64 title_id, AccountUid uid, const std::string& name, const std::string& author) { - mId = id; - mUserId = userID; - mSaveDataType = saveDataType; - mUserName = Account::username(userID); - mAuthor = author; - mName = name; - mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId) - : StringUtils::removeForbiddenCharacters(name); - mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName; + m_id = title_id; + m_uid = uid; + m_save_data_type = save_data_type; + m_user_name = account::username(uid); + m_author = author; + m_name = name; + m_safe_name = string_utils::containsInvalidChar(name) ? string_utils::format("0x%016llX", m_id) + : string_utils::removeForbiddenCharacters(name); + m_path = "sdmc:/switch/NXST/saves/" + string_utils::format("0x%016llX", m_id) + " " + m_safe_name; - std::string aname = StringUtils::removeAccents(mName); - size_t pos = aname.rfind(":"); - mDisplayName = std::make_pair(aname, ""); - if (pos != std::string::npos) { - std::string name1 = aname.substr(0, pos); - std::string name2 = aname.substr(pos + 1); - StringUtils::trim(name1); - StringUtils::trim(name2); - mDisplayName.first = name1; - mDisplayName.second = name2; + std::string aname = string_utils::removeAccents(m_name); + m_display_name = {aname, ""}; + + size_t colon = aname.rfind(':'); + if (colon != std::string::npos) { + std::string head = aname.substr(0, colon); + std::string tail = aname.substr(colon + 1); + string_utils::trim(head); + string_utils::trim(tail); + m_display_name = {head, tail}; } else { - // check for parenthesis - size_t pos1 = aname.rfind("("); - size_t pos2 = aname.rfind(")"); - if (pos1 != std::string::npos && pos2 != std::string::npos) { - std::string name1 = aname.substr(0, pos1); - std::string name2 = aname.substr(pos1 + 1, pos2 - 1 - pos1); - StringUtils::trim(name1); - StringUtils::trim(name2); - mDisplayName.first = name1; - mDisplayName.second = name2; + size_t open = aname.rfind('('); + size_t close = aname.rfind(')'); + if (open != std::string::npos && close != std::string::npos && close > open) { + std::string head = aname.substr(0, open); + std::string paren = aname.substr(open + 1, close - open - 1); + string_utils::trim(head); + string_utils::trim(paren); + m_display_name = {head, paren}; } } refreshDirectories(); } -u8 Title::saveDataType(void) { - return mSaveDataType; +u8 Title::saveDataType() const { + return m_save_data_type; +} +u64 Title::id() const { + return m_id; +} +u64 Title::saveId() const { + return m_save_id; +} +void Title::saveId(u64 id) { + m_save_id = id; +} +AccountUid Title::userId() const { + return m_uid; +} +std::string Title::userName() const { + return m_user_name; +} +std::string Title::author() const { + return m_author; +} +std::string Title::name() const { + return m_name; +} +std::pair<std::string, std::string> Title::displayName() const { + return m_display_name; +} +std::string Title::path() const { + return m_path; +} +std::string Title::fullPath(size_t index) const { + return m_full_save_paths.at(index); +} +std::vector<std::string> Title::saves() const { + return m_saves; +} +u64 Title::playTimeNanoseconds() const { + return m_play_time_ns; +} +void Title::playTimeNanoseconds(u64 ns) { + m_play_time_ns = ns; +} +u32 Title::lastPlayedTimestamp() const { + return m_last_played_ts; +} +void Title::lastPlayedTimestamp(u32 ts) { + m_last_played_ts = ts; } -u64 Title::id(void) { - return mId; +std::string Title::playTime() const { + const u64 minutes = m_play_time_ns / 60000000000ULL; + return string_utils::format("%d", minutes / 60) + ":" + string_utils::format("%02d", minutes % 60) + + " hours"; } -u64 Title::saveId(void) { - return mSaveId; -} +void Title::refreshDirectories() { + m_saves.clear(); + m_full_save_paths.clear(); -void Title::saveId(u64 saveId) { - mSaveId = saveId; -} - -AccountUid Title::userId(void) { - return mUserId; -} - -std::string Title::userName(void) { - return mUserName; -} - -std::string Title::author(void) { - return mAuthor; -} - -std::string Title::name(void) { - return mName; -} - -std::pair<std::string, std::string> Title::displayName(void) { - return mDisplayName; -} - -std::string Title::path(void) { - return mPath; -} - -std::string Title::fullPath(size_t index) { - return mFullSavePaths.at(index); -} - -std::vector<std::string> Title::saves() { - return mSaves; -} - -u64 Title::playTimeNanoseconds(void) { - return mPlayTimeNanoseconds; -} - -std::string Title::playTime(void) { - const u64 playTimeMinutes = mPlayTimeNanoseconds / 60000000000; - return StringUtils::format("%d", playTimeMinutes / 60) + ":" + - StringUtils::format("%02d", playTimeMinutes % 60) + " hours"; -} - -void Title::playTimeNanoseconds(u64 playTimeNanoseconds) { - mPlayTimeNanoseconds = playTimeNanoseconds; -} - -u32 Title::lastPlayedTimestamp(void) { - return mLastPlayedTimestamp; -} - -void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp) { - mLastPlayedTimestamp = lastPlayedTimestamp; -} - -void Title::refreshDirectories(void) { - mSaves.clear(); - mFullSavePaths.clear(); - - Directory savelist(mPath); + Directory savelist(m_path); if (savelist.good()) { - for (size_t i = 0, sz = savelist.size(); i < sz; i++) { + for (size_t i = 0; i < savelist.size(); ++i) { if (savelist.folder(i)) { - mSaves.push_back(savelist.entry(i)); - mFullSavePaths.push_back(mPath + "/" + savelist.entry(i)); + m_saves.push_back(savelist.entry(i)); + m_full_save_paths.push_back(m_path + "/" + savelist.entry(i)); } } - - std::sort(mSaves.rbegin(), mSaves.rend()); - std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend()); - mSaves.insert(mSaves.begin(), g_emptySave); - mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave); + std::sort(m_saves.rbegin(), m_saves.rend()); + std::sort(m_full_save_paths.rbegin(), m_full_save_paths.rend()); + m_saves.insert(m_saves.begin(), kEmptySave); + m_full_save_paths.insert(m_full_save_paths.begin(), kEmptySave); } else { - Logger::getInstance().log(Logger::ERROR, - "Couldn't retrieve the extdata directory list for the title " + name()); + nxst::log::error("Could not read save directory for title %s", m_name.c_str()); } } -void loadTitles(void) { - if (s_titlesLoaded) - return; - s_titlesLoaded = true; +bool areTitlesLoaded() { + return s_titles_loaded; +} +void loadTitles() { + if (s_titles_loaded) + return; + s_titles_loaded = true; titles.clear(); FsSaveDataInfoReader reader; - FsSaveDataInfo info; - s64 total_entries = 0; - size_t outsize = 0; - - NacpLanguageEntry* nle = NULL; - NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData)); - if (nsacd == NULL) { - return; - } - memset(nsacd, 0, sizeof(NsApplicationControlData)); - Result res = fsOpenSaveDataInfoReader(&reader, FsSaveDataSpaceId_User); - if (R_FAILED(res)) { - free(nsacd); + if (R_FAILED(res)) return; - } - while (1) { - res = fsSaveDataInfoReaderRead(&reader, &info, 1, &total_entries); - if (R_FAILED(res) || total_entries == 0) { + std::vector<u8> nacp_buf(sizeof(NsApplicationControlData), 0); + auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nacp_buf.data()); + + FsSaveDataInfo info{}; + s64 count = 0; + while (true) { + res = fsSaveDataInfoReaderRead(&reader, &info, 1, &count); + if (R_FAILED(res) || count == 0) break; + if (info.save_data_type != FsSaveDataType_Account) + continue; + + u64 tid = info.application_id; + AccountUid uid = info.uid; + size_t outsize = 0; + NacpLanguageEntry* nle = nullptr; + + memset(nsacd, 0, sizeof(NsApplicationControlData)); + res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd, + sizeof(NsApplicationControlData), &outsize); + if (R_FAILED(res) || outsize < sizeof(nsacd->nacp)) + continue; + if (R_FAILED(nacpGetLanguageEntry(&nsacd->nacp, &nle)) || !nle) + continue; + + Title title; + title.init(info.save_data_type, tid, uid, std::string(nle->name), std::string(nle->author)); + title.saveId(info.save_data_id); + + PdmPlayStatistics stats{}; + if (R_SUCCEEDED(pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats))) { + title.playTimeNanoseconds(stats.playtime); + title.lastPlayedTimestamp(stats.last_timestamp_user); } - if (info.save_data_type == FsSaveDataType_Account) { - u64 tid = info.application_id; - u64 sid = info.save_data_id; - AccountUid uid = info.uid; - // if (mFilterIds.find(tid) == mFilterIds.end()) { - res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd, - sizeof(NsApplicationControlData), &outsize); - if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) { - res = nacpGetLanguageEntry(&nsacd->nacp, &nle); - if (R_SUCCEEDED(res) && nle != NULL) { - Title title; - title.init(info.save_data_type, tid, uid, std::string(nle->name), - std::string(nle->author)); - title.saveId(sid); - - // load play statistics - PdmPlayStatistics stats; - res = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats); - if (R_SUCCEEDED(res)) { - title.playTimeNanoseconds(stats.playtime); - title.lastPlayedTimestamp(stats.last_timestamp_user); - } - - // loadIcon(tid, nsacd, outsize - sizeof(nsacd->nacp)); - - // check if the vector is already created - std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); - if (it != titles.end()) { - // found - it->second.push_back(title); - } else { - // not found, insert into map - std::vector<Title> v; - v.push_back(title); - titles.emplace(uid, v); - } - } - } - nle = NULL; - // } + auto it = titles.find(uid); + if (it != titles.end()) { + it->second.push_back(title); + } else { + titles.emplace(uid, std::vector<Title>{title}); } } - free(nsacd); fsSaveDataInfoReaderClose(&reader); - sortTitles(); } -void sortTitles(void) { - for (auto& vect : titles) { - std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) { - switch (g_sortMode) { - case SORT_LAST_PLAYED: +void sortTitles() { + for (auto& pair : titles) { + std::sort(pair.second.begin(), pair.second.end(), [](const Title& l, const Title& r) { + switch (s_sort_mode) { + case SortLastPlayed: return l.lastPlayedTimestamp() > r.lastPlayedTimestamp(); - case SORT_PLAY_TIME: + case SortPlayTime: return l.playTimeNanoseconds() > r.playTimeNanoseconds(); - case SORT_ALPHA: + case SortAlpha: default: return l.name() < r.name(); } @@ -262,38 +207,36 @@ void sortTitles(void) { } } -void rotateSortMode(void) { - g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT); +void rotateSortMode() { + s_sort_mode = static_cast<sort_t>((s_sort_mode + 1) % SortModesCount); sortTitles(); } void getTitle(Title& dst, AccountUid uid, size_t i) { - std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); - if (it != titles.end() && i < getTitleCount(uid)) { - dst = it->second.at(i); - } + auto it = titles.find(uid); + if (it != titles.end() && i < it->second.size()) + dst = it->second[i]; } size_t getTitleCount(AccountUid uid) { - std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); + auto it = titles.find(uid); return it != titles.end() ? it->second.size() : 0; } void refreshDirectories(u64 id) { for (auto& pair : titles) { - for (size_t i = 0; i < pair.second.size(); i++) { - if (pair.second.at(i).id() == id) { - pair.second.at(i).refreshDirectories(); - } + for (auto& title : pair.second) { + if (title.id() == id) + title.refreshDirectories(); } } } -std::unordered_map<std::string, std::string> getCompleteTitleList(void) { +std::unordered_map<std::string, std::string> getCompleteTitleList() { std::unordered_map<std::string, std::string> map; for (const auto& pair : titles) { - for (auto value : pair.second) { - map.insert({StringUtils::format("0x%016llX", value.id()), value.name()}); + for (const auto& title : pair.second) { + map.emplace(string_utils::format("0x%016llX", title.id()), title.name()); } } return map; diff --git a/src/domain/util.cpp b/src/domain/util.cpp index dd7d601..80d4f30 100644 --- a/src/domain/util.cpp +++ b/src/domain/util.cpp @@ -1,159 +1,250 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ +// Copyright (C) 2024-2026 NXST contributors +#include <algorithm> +#include <cctype> +#include <cstdarg> +#include <cstdio> +#include <string> +#include <unordered_map> -#include <nxst/app/main.hpp> -#include <nxst/app/main_application.hpp> +#include <nxst/domain/account.hpp> #include <nxst/domain/util.hpp> +#include <nxst/infra/fs/io.hpp> #include <nxst/infra/sys/logger.hpp> -void servicesExit(void) { - Logger::getInstance().flush(); - Account::exit(); +static bool s_notification_led_available = false; + +void servicesExit() { + account::exit(); plExit(); romfsExit(); } -Result servicesInit(void) { +Result servicesInit() { io::createDirectory("sdmc:/switch"); io::createDirectory("sdmc:/switch/NXST"); io::createDirectory("sdmc:/switch/NXST/saves"); if (appletGetAppletType() != AppletType_Application) { - Logger::getInstance().log(Logger::WARN, "Please do not run NXST in applet mode."); + nxst::log::warn("Please do not run NXST in applet mode."); } Result res = 0; - romfsInit(); - padConfigureInput(1, HidNpadStyleSet_NpadStandard); hidInitializeTouchScreen(); if (R_FAILED(res = plInitialize(PlServiceType_User))) { - Logger::getInstance().log(Logger::ERROR, "plInitialize failed. Result code 0x%08lX.", res); + nxst::log::error("plInitialize failed. Result code 0x%08X.", res); return res; } - - if (R_FAILED(res = Account::init())) { - Logger::getInstance().log(Logger::ERROR, "Account::init failed. Result code 0x%08lX.", res); + if (R_FAILED(res = account::init())) { + nxst::log::error("account::init failed. Result code 0x%08X.", res); return res; } - if (R_FAILED(res = nsInitialize())) { - Logger::getInstance().log(Logger::ERROR, "nsInitialize failed. Result code 0x{:08X}.", res); + nxst::log::error("nsInitialize failed. Result code 0x%08X.", res); return res; } - - if (R_SUCCEEDED(res = hidsysInitialize())) { - g_notificationLedAvailable = true; + if (R_SUCCEEDED(hidsysInitialize())) { + s_notification_led_available = true; } else { - Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res); + nxst::log::info("Notification LED not available."); } - Logger::getInstance().log(Logger::INFO, "NXST loading completed!"); - + nxst::log::info("NXST loading completed."); return 0; } -std::u16string StringUtils::UTF8toUTF16(const char* src) { - char16_t tmp[256] = {0}; - utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256); - return std::u16string(tmp); +bool string_utils::containsInvalidChar(const std::string& str) { + for (unsigned char c : str) { + if (!isascii(c)) + return true; + } + return false; } -// https://stackoverflow.com/questions/14094621/change-all-accented-letters-to-normal-letters-in-c -std::string StringUtils::removeAccents(std::string str) { - std::u16string src = UTF8toUTF16(str.c_str()); - const std::u16string illegal = - UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ"); - const std::u16string fixed = - UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy"); +std::string string_utils::format(const char* fmt, ...) { + va_list a1, a2; + va_start(a1, fmt); + va_copy(a2, a1); + int n = vsnprintf(nullptr, 0, fmt, a1); + va_end(a1); + if (n < 0) { + va_end(a2); + return {}; + } + std::string buf(static_cast<size_t>(n), '\0'); + vsnprintf(buf.data(), static_cast<size_t>(n) + 1, fmt, a2); + va_end(a2); + return buf; +} - for (size_t i = 0, sz = src.length(); i < sz; i++) { - size_t index = illegal.find(src[i]); - if (index != std::string::npos) { - src[i] = fixed[index]; +std::string string_utils::removeForbiddenCharacters(std::string src) { + static constexpr std::string_view kForbidden = ".,!\\/:?*\"<>|"; + for (char& c : src) { + if (kForbidden.find(c) != std::string_view::npos) + c = ' '; + } + auto last = src.find_last_not_of(' '); + if (last != std::string::npos) + src.erase(last + 1); + return src; +} + +static size_t encodeUtf8(char* out, char32_t cp) { + if (cp < 0x80) { + out[0] = static_cast<char>(cp); + return 1; + } + if (cp < 0x800) { + out[0] = static_cast<char>(0xC0 | (cp >> 6)); + out[1] = static_cast<char>(0x80 | (cp & 0x3F)); + return 2; + } + if (cp < 0x10000) { + out[0] = static_cast<char>(0xE0 | (cp >> 12)); + out[1] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); + out[2] = static_cast<char>(0x80 | (cp & 0x3F)); + return 3; + } + out[0] = static_cast<char>(0xF0 | (cp >> 18)); + out[1] = static_cast<char>(0x80 | ((cp >> 12) & 0x3F)); + out[2] = static_cast<char>(0x80 | ((cp >> 6) & 0x3F)); + out[3] = static_cast<char>(0x80 | (cp & 0x3F)); + return 4; +} + +std::string string_utils::UTF16toUTF8(const std::u16string& src) { + std::string result; + result.reserve(src.size() * 2); + for (size_t i = 0; i < src.size(); ++i) { + char32_t cp = src[i]; + if (cp >= 0xD800 && cp <= 0xDBFF && i + 1 < src.size()) { + cp = 0x10000 + ((cp - 0xD800) << 10) + (src[++i] - 0xDC00); + } + char buf[4]; + result.append(buf, encodeUtf8(buf, cp)); + } + return result; +} + +void string_utils::ltrim(std::string& s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char c) { + return !std::isspace(c); + })); +} + +void string_utils::rtrim(std::string& s) { + s.erase(std::find_if(s.rbegin(), s.rend(), + [](unsigned char c) { + return !std::isspace(c); + }) + .base(), + s.end()); +} + +void string_utils::trim(std::string& s) { + ltrim(s); + rtrim(s); +} + +// Decodes a UTF-8 string to UTF-16, handling surrogate pairs for codepoints > U+FFFF. +std::u16string string_utils::UTF8toUTF16(const char* src) { + std::u16string result; + while (*src != '\0') { + char32_t cp = 0; + unsigned char c = static_cast<unsigned char>(*src); + if (c < 0x80) { + cp = c; + ++src; + } else if (c < 0xE0) { + cp = static_cast<char32_t>(c & 0x1F) << 6 | (src[1] & 0x3F); + src += 2; + } else if (c < 0xF0) { + cp = static_cast<char32_t>(c & 0x0F) << 12 | static_cast<char32_t>(src[1] & 0x3F) << 6 | + (src[2] & 0x3F); + src += 3; + } else { + cp = static_cast<char32_t>(c & 0x07) << 18 | static_cast<char32_t>(src[1] & 0x3F) << 12 | + static_cast<char32_t>(src[2] & 0x3F) << 6 | (src[3] & 0x3F); + src += 4; + } + if (cp < 0x10000) { + result += static_cast<char16_t>(cp); + } else { + cp -= 0x10000; + result += static_cast<char16_t>(0xD800 | (cp >> 10)); + result += static_cast<char16_t>(0xDC00 | (cp & 0x3FF)); } } - - return UTF16toUTF8(src); + return result; } -std::string StringUtils::removeNotAscii(std::string str) { - for (size_t i = 0, sz = str.length(); i < sz; i++) { - if (!isascii(str[i])) { - str[i] = ' '; - } +// Replaces Latin characters with diacritics with their ASCII base equivalents. +std::string string_utils::removeAccents(std::string str) { + static const std::unordered_map<char16_t, char16_t> kMap = { + {u'À', u'A'}, {u'Á', u'A'}, {u'Â', u'A'}, {u'Ã', u'A'}, {u'Ä', u'A'}, {u'Å', u'A'}, {u'Æ', u'E'}, + {u'Ç', u'C'}, {u'È', u'E'}, {u'É', u'E'}, {u'Ê', u'E'}, {u'Ë', u'E'}, {u'Ì', u'I'}, {u'Í', u'I'}, + {u'Î', u'I'}, {u'Ï', u'I'}, {u'Ð', u'D'}, {u'Ñ', u'N'}, {u'Ò', u'O'}, {u'Ó', u'O'}, {u'Ô', u'O'}, + {u'Õ', u'O'}, {u'Ö', u'O'}, {u'Ø', u'O'}, {u'Ù', u'U'}, {u'Ú', u'U'}, {u'Û', u'U'}, {u'Ü', u'U'}, + {u'Ý', u'Y'}, {u'ß', u's'}, {u'à', u'a'}, {u'á', u'a'}, {u'â', u'a'}, {u'ã', u'a'}, {u'ä', u'a'}, + {u'å', u'a'}, {u'æ', u'e'}, {u'ç', u'c'}, {u'è', u'e'}, {u'é', u'e'}, {u'ê', u'e'}, {u'ë', u'e'}, + {u'ì', u'i'}, {u'í', u'i'}, {u'î', u'i'}, {u'ï', u'i'}, {u'ñ', u'n'}, {u'ò', u'o'}, {u'ó', u'o'}, + {u'ô', u'o'}, {u'õ', u'o'}, {u'ö', u'o'}, {u'ø', u'o'}, {u'ù', u'u'}, {u'ú', u'u'}, {u'û', u'u'}, + {u'ü', u'u'}, {u'ū', u'u'}, {u'ý', u'y'}, {u'ÿ', u'y'}, + }; + std::u16string wide = UTF8toUTF16(str.c_str()); + for (char16_t& ch : wide) { + auto it = kMap.find(ch); + if (it != kMap.end()) + ch = it->second; + } + return string_utils::UTF16toUTF8(wide); +} + +std::string string_utils::removeNotAscii(std::string str) { + for (char& c : str) { + if (!isascii(static_cast<unsigned char>(c))) + c = ' '; } return str; } -std::string StringUtils::elide(const std::string& s, size_t maxChars) { - if (s.size() <= maxChars || maxChars < 6) +std::string string_utils::elide(const std::string& s, size_t max_chars) { + if (s.size() <= max_chars || max_chars < 6) return s; - constexpr const char* dots = "..."; - size_t budget = maxChars - 3; + size_t budget = max_chars - 3; size_t head = (budget + 1) / 2; size_t tail = budget - head; - return s.substr(0, head) + dots + s.substr(s.size() - tail); + return s.substr(0, head) + "..." + s.substr(s.size() - tail); } -HidsysNotificationLedPattern blinkLedPattern(u8 times) { - HidsysNotificationLedPattern pattern; - memset(&pattern, 0, sizeof(pattern)); - - pattern.baseMiniCycleDuration = 0x1; // 12.5ms. - pattern.totalMiniCycles = 0x2; // 2 mini cycles. - pattern.totalFullCycles = times; // Repeat n times. - pattern.startIntensity = 0x0; // 0%. - - pattern.miniCycles[0].ledIntensity = 0xF; // 100%. - pattern.miniCycles[0].transitionSteps = 0xF; // 15 steps. Total 187.5ms. - pattern.miniCycles[0].finalStepDuration = 0x0; // Forced 12.5ms. - pattern.miniCycles[1].ledIntensity = 0x0; // 0%. - pattern.miniCycles[1].transitionSteps = 0xF; // 15 steps. Total 187.5ms. - pattern.miniCycles[1].finalStepDuration = 0x0; // Forced 12.5ms. - - return pattern; +static HidsysNotificationLedPattern makeLedPattern(u8 times) { + HidsysNotificationLedPattern p{}; + p.baseMiniCycleDuration = 0x1; + p.totalMiniCycles = 0x2; + p.totalFullCycles = times; + p.startIntensity = 0x0; + p.miniCycles[0] = {0xF, 0xF, 0x0}; + p.miniCycles[1] = {0x0, 0xF, 0x0}; + return p; } void blinkLed(u8 times) { - if (g_notificationLedAvailable) { - PadState pad; - padInitializeDefault(&pad); - s32 n; - HidsysUniquePadId uniquePadIds[2] = {0}; - HidsysNotificationLedPattern pattern = blinkLedPattern(times); - memset(uniquePadIds, 0, sizeof(uniquePadIds)); - Result res = hidsysGetUniquePadsFromNpad( - padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, uniquePadIds, 2, &n); - if (R_SUCCEEDED(res)) { - for (s32 i = 0; i < n; i++) { - hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]); - } + if (!s_notification_led_available) + return; + + PadState pad; + padInitializeDefault(&pad); + s32 n = 0; + HidsysUniquePadId pads[2]{}; + HidsysNotificationLedPattern pattern = makeLedPattern(times); + + Result res = hidsysGetUniquePadsFromNpad(padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, + pads, 2, &n); + if (R_SUCCEEDED(res)) { + for (s32 i = 0; i < n; ++i) { + hidsysSetNotificationLedPattern(&pattern, pads[i]); } } } diff --git a/src/infra/fs/directory.cpp b/src/infra/fs/directory.cpp index d5e4507..d551179 100644 --- a/src/infra/fs/directory.cpp +++ b/src/infra/fs/directory.cpp @@ -1,69 +1,27 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ +// Copyright (C) 2024-2026 NXST contributors +#include <cerrno> +#include <dirent.h> #include <nxst/infra/fs/directory.hpp> -Directory::Directory(const std::string& root) { - mGood = false; - mError = 0; - mList.clear(); - - DIR* dir = opendir(root.c_str()); - struct dirent* ent; - - if (dir == NULL) { - mError = (Result)errno; - } else { - while ((ent = readdir(dir))) { - std::string name = std::string(ent->d_name); - bool directory = ent->d_type == DT_DIR; - struct DirectoryEntry de = {name, directory}; - mList.push_back(de); - } - closedir(dir); - mGood = true; +Directory::Directory(const std::string& path) { + DIR* d = opendir(path.c_str()); + if (!d) { + m_error = static_cast<Result>(errno); + return; } + struct dirent* ent; + while ((ent = readdir(d)) != nullptr) { + m_entries.push_back({ent->d_name, ent->d_type == DT_DIR}); + } + closedir(d); + m_good = true; } -Result Directory::error(void) { - return mError; +std::string Directory::entry(size_t i) const { + return i < m_entries.size() ? m_entries[i].name : ""; } -bool Directory::good(void) { - return mGood; +bool Directory::folder(size_t i) const { + return i < m_entries.size() && m_entries[i].is_dir; } - -std::string Directory::entry(size_t index) { - return index < mList.size() ? mList.at(index).name : ""; -} - -bool Directory::folder(size_t index) { - return index < mList.size() ? mList.at(index).directory : false; -} - -size_t Directory::size(void) { - return mList.size(); -} \ No newline at end of file diff --git a/src/infra/fs/filesystem.cpp b/src/infra/fs/filesystem.cpp index 69138f1..9abd9ea 100644 --- a/src/infra/fs/filesystem.cpp +++ b/src/infra/fs/filesystem.cpp @@ -1,39 +1,14 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors #include <nxst/infra/fs/filesystem.hpp> -Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) { - return fsOpen_SaveData(fileSystem, titleID, userID); +Result file_system::mount(FsFileSystem* fs, u64 title_id, AccountUid uid) { + return fsOpen_SaveData(fs, title_id, uid); } -int FileSystem::mount(FsFileSystem fs) { +int file_system::mount(FsFileSystem fs) { return fsdevMountDevice("save", fs); } -void FileSystem::unmount(void) { +void file_system::unmount() { fsdevUnmountDevice("save"); -} \ No newline at end of file +} diff --git a/src/infra/fs/io.cpp b/src/infra/fs/io.cpp index 9c86721..721ea24 100644 --- a/src/infra/fs/io.cpp +++ b/src/infra/fs/io.cpp @@ -1,56 +1,36 @@ -/* - * This file is part of Checkpoint - * Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - * Additional Terms 7.b and 7.c of GPLv3 apply to this file: - * * Requiring preservation of specified reasonable legal notices or - * author attributions in that material or in the Appropriate Legal - * Notices displayed by works containing it. - * * Prohibiting misrepresentation of the origin of that material, - * or requiring that modified versions of such material be marked in - * reasonable ways as different from the original version. - */ - +// Copyright (C) 2024-2026 NXST contributors +#include <cstdio> +#include <sys/stat.h> +#include <unistd.h> #include <vector> -#include <nxst/app/main.hpp> +#include <nxst/domain/account.hpp> +#include <nxst/domain/title.hpp> +#include <nxst/domain/util.hpp> +#include <nxst/infra/fs/directory.hpp> +#include <nxst/infra/fs/filesystem.hpp> #include <nxst/infra/fs/handles.hpp> #include <nxst/infra/fs/io.hpp> #include <nxst/infra/sys/logger.hpp> +static constexpr size_t kBufSize = 0x80000; + bool io::fileExists(const std::string& path) { struct stat buffer; return (stat(path.c_str(), &buffer) == 0); } void io::copyFile(const std::string& srcPath, const std::string& dstPath) { - g_isTransferringFile = true; - nxst::FileHandle src(fopen(srcPath.c_str(), "rb")); if (!src) { nxst::log::error("Failed to open source file %s during copy with errno %d. Skipping...", srcPath.c_str(), errno); - g_isTransferringFile = false; return; } nxst::FileHandle dst(fopen(dstPath.c_str(), "wb")); if (!dst) { nxst::log::error("Failed to open destination file %s during copy with errno %d. Skipping...", dstPath.c_str(), errno); - g_isTransferringFile = false; return; } @@ -58,28 +38,28 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath) { u64 sz = (u64)ftell(src.get()); rewind(src.get()); - std::vector<u8> buf(BUFFER_SIZE); + std::vector<u8> buf(kBufSize); u64 offset = 0; - size_t slashpos = srcPath.rfind('/'); - g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1); - while (offset < sz) { - u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get()); + u32 count = (u32)fread(buf.data(), 1, kBufSize, src.get()); if (count == 0) { nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.", srcPath.c_str(), (unsigned long long)offset, (unsigned long long)sz, errno); break; } - fwrite(buf.data(), 1, count, dst.get()); + size_t written = fwrite(buf.data(), 1, count, dst.get()); + if (written != count) { + nxst::log::error("fwrite incomplete for %s (%zu/%u bytes). Aborting.", dstPath.c_str(), written, + count); + break; + } offset += count; } if (dstPath.rfind("save:/", 0) == 0) { fsdevCommitDevice("save"); } - - g_isTransferringFile = false; } Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) { @@ -96,24 +76,27 @@ Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) std::string newdst = dstPath + items.entry(i); if (items.folder(i)) { - res = io::createDirectory(newdst); + res = createDirectory(newdst); if (R_SUCCEEDED(res)) { newsrc += "/"; newdst += "/"; - res = io::copyDirectory(newsrc, newdst); + res = copyDirectory(newsrc, newdst); } else { quit = true; } } else { - io::copyFile(newsrc, newdst); + copyFile(newsrc, newdst); } } - return 0; + return res; } Result io::createDirectory(const std::string& path) { - mkdir(path.c_str(), 0777); + if (mkdir(path.c_str(), 0777) != 0 && errno != EEXIST) { + nxst::log::error("mkdir failed for %s (errno %d).", path.c_str(), errno); + return 1; + } return 0; } @@ -128,20 +111,21 @@ Result io::deleteFolderRecursively(const std::string& path) { return dir.error(); } + Result res = 0; for (size_t i = 0, sz = dir.size(); i < sz; i++) { if (dir.folder(i)) { - std::string newpath = path + "/" + dir.entry(i) + "/"; - deleteFolderRecursively(newpath); - newpath = path + dir.entry(i); - rmdir(newpath.c_str()); + std::string sub = path + "/" + dir.entry(i); + Result sub_res = deleteFolderRecursively(sub + "/"); + if (R_FAILED(sub_res)) + res = sub_res; + rmdir(sub.c_str()); } else { - std::string newpath = path + dir.entry(i); - std::remove(newpath.c_str()); + std::remove((path + dir.entry(i)).c_str()); } } rmdir(path.c_str()); - return 0; + return res; } nxst::Result<std::string> io::backup(size_t index, AccountUid uid) { @@ -151,54 +135,61 @@ nxst::Result<std::string> io::backup(size_t index, AccountUid uid) { nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(), title.userId().uid[1], title.userId().uid[0]); - nxst::FsFileSystemHandle fsHandle; - Result res = FileSystem::mount(fsHandle.get(), title.id(), title.userId()); + nxst::FsFileSystemHandle fs_handle; + Result res = file_system::mount(fs_handle.get(), title.id(), title.userId()); if (R_FAILED(res)) { - nxst::log::error("Failed to mount filesystem during backup with result 0x%08lX. " + nxst::log::error("Failed to mount filesystem during backup with result 0x%08X. " "Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(), title.userId().uid[1], title.userId().uid[0]); return nxst::Result<std::string>::failure("Failed to mount save."); } - fsHandle.valid = true; + fs_handle.valid = true; - if (FileSystem::mount(*fsHandle.get()) == -1) { + if (file_system::mount(*fs_handle.get()) == -1) { nxst::log::error("Failed to mount devfs during backup. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(), title.userId().uid[1], title.userId().uid[0]); - FileSystem::unmount(); + file_system::unmount(); return nxst::Result<std::string>::failure("Failed to mount save."); } - fsHandle.release(); // devfs now owns the kernel handle + fs_handle.release(); // devfs now owns the kernel handle std::string suggestion = - StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId()))); + string_utils::removeNotAscii(string_utils::removeAccents(account::username(title.userId()))); io::createDirectory(title.path()); std::string dst_path = title.path() + "/" + suggestion; std::string tmp_path = dst_path + ".tmp"; if (io::directoryExists(tmp_path)) { - io::deleteFolderRecursively(tmp_path + "/"); + if (R_FAILED(io::deleteFolderRecursively(tmp_path + "/"))) + nxst::log::warn("Failed to clean up stale tmp dir %s.", tmp_path.c_str()); } - io::createDirectory(tmp_path); - res = io::copyDirectory("save:/", tmp_path + "/"); + res = io::createDirectory(tmp_path); if (R_FAILED(res)) { - FileSystem::unmount(); + file_system::unmount(); + nxst::log::error("Failed to create tmp dir %s.", tmp_path.c_str()); + return nxst::Result<std::string>::failure("Failed to create tmp directory."); + } + res = copyDirectory("save:/", tmp_path + "/"); + if (R_FAILED(res)) { + file_system::unmount(); io::deleteFolderRecursively(tmp_path + "/"); - nxst::log::error("Failed to copy directory to %s with result 0x%08lX.", tmp_path.c_str(), res); + nxst::log::error("Failed to copy directory to %s with result 0x%08X.", tmp_path.c_str(), res); return nxst::Result<std::string>::failure("Failed to backup save."); } if (io::directoryExists(dst_path)) { - io::deleteFolderRecursively(dst_path + "/"); + if (R_FAILED(io::deleteFolderRecursively(dst_path + "/"))) + nxst::log::warn("Failed to remove old backup at %s.", dst_path.c_str()); } if (rename(tmp_path.c_str(), dst_path.c_str()) != 0) { - FileSystem::unmount(); + file_system::unmount(); nxst::log::error("Failed to rename temp backup to %s.", dst_path.c_str()); return nxst::Result<std::string>::failure("Failed to finalise backup."); } refreshDirectories(title.id()); - FileSystem::unmount(); + file_system::unmount(); nxst::log::info("Backup succeeded."); return nxst::Result<std::string>::success(dst_path); @@ -246,7 +237,7 @@ static nxst::Result<void> clearSaveRoot(const std::string& dst_path) { Result res = fsdevCommitDevice("save"); if (R_FAILED(res)) { - nxst::log::error("Failed to commit save after clearing with result 0x%08lX.", res); + nxst::log::error("Failed to commit save after clearing with result 0x%08X.", res); return nxst::Result<void>::failure("Failed to commit save after delete."); } return nxst::Result<void>::success(); @@ -255,22 +246,19 @@ static nxst::Result<void> clearSaveRoot(const std::string& dst_path) { static nxst::Result<void> extractAndCommit(const std::string& src_path, const std::string& dst_path) { Result res = io::copyDirectory(src_path, dst_path); if (R_FAILED(res)) { - nxst::log::error("Failed to copy %s to save:/ with result 0x%08lX.", src_path.c_str(), res); + nxst::log::error("Failed to copy %s to save:/ with result 0x%08X.", src_path.c_str(), res); return nxst::Result<void>::failure("Failed to restore save."); } res = fsdevCommitDevice("save"); if (R_FAILED(res)) { - nxst::log::error("Failed to commit save with result 0x%08lX.", res); + nxst::log::error("Failed to commit save with result 0x%08X.", res); return nxst::Result<void>::failure("Failed to commit to save device."); } return nxst::Result<void>::success(); } -nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, - const std::string& nameFromCell) { - (void)cellIndex; - +nxst::Result<std::string> io::restore(size_t index, AccountUid uid, const std::string& title_name) { Title title; getTitle(title, uid, index); @@ -279,32 +267,33 @@ nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellI createSaveIfNeeded(title.id(), uid); - nxst::FsFileSystemHandle fsHandle; - Result res = FileSystem::mount(fsHandle.get(), title.id(), uid); + nxst::FsFileSystemHandle fs_handle; + Result res = file_system::mount(fs_handle.get(), title.id(), uid); if (R_FAILED(res)) { - nxst::log::error("Failed to mount filesystem during restore with result 0x%08lX. " + nxst::log::error("Failed to mount filesystem during restore with result 0x%08X. " "Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(), uid.uid[1], uid.uid[0]); return nxst::Result<std::string>::failure("Failed to mount save."); } - fsHandle.valid = true; + fs_handle.valid = true; - if (FileSystem::mount(*fsHandle.get()) == -1) { + if (file_system::mount(*fs_handle.get()) == -1) { nxst::log::error("Failed to mount devfs during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(), uid.uid[1], uid.uid[0]); - FileSystem::unmount(); + file_system::unmount(); return nxst::Result<std::string>::failure("Failed to mount save."); } - fsHandle.release(); // devfs now owns the kernel handle + fs_handle.release(); // devfs now owns the kernel handle - std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid))); + std::string suggestion = + string_utils::removeNotAscii(string_utils::removeAccents(account::username(uid))); std::string src_path = title.path() + "/" + suggestion + "/"; const std::string dst_path = "save:/"; { Directory src_check(src_path); if (!src_check.good() || src_check.size() == 0) { - FileSystem::unmount(); + file_system::unmount(); nxst::log::error("Restore source is empty or missing: %s", src_path.c_str()); return nxst::Result<std::string>::failure("Restore source is empty or missing."); } @@ -312,19 +301,19 @@ nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellI auto clear_res = clearSaveRoot(dst_path); if (!clear_res.isOk()) { - FileSystem::unmount(); + file_system::unmount(); return nxst::Result<std::string>::failure(clear_res.error()); } auto extract_res = extractAndCommit(src_path, dst_path); if (!extract_res.isOk()) { - FileSystem::unmount(); + file_system::unmount(); return nxst::Result<std::string>::failure(extract_res.error()); } blinkLed(4); - FileSystem::unmount(); + file_system::unmount(); nxst::log::info("Restore succeeded."); - return nxst::Result<std::string>::success(nameFromCell + "\nhas been restored successfully."); + return nxst::Result<std::string>::success(title_name + "\nhas been restored successfully."); } diff --git a/src/infra/net/transfer_receiver.cpp b/src/infra/net/transfer_receiver.cpp deleted file mode 100644 index bfd75ba..0000000 --- a/src/infra/net/transfer_receiver.cpp +++ /dev/null @@ -1 +0,0 @@ -// Logic moved to src/service/transfer_service.cpp diff --git a/src/infra/net/transfer_sender.cpp b/src/infra/net/transfer_sender.cpp deleted file mode 100644 index bfd75ba..0000000 --- a/src/infra/net/transfer_sender.cpp +++ /dev/null @@ -1 +0,0 @@ -// Logic moved to src/service/transfer_service.cpp diff --git a/src/infra/sys/logger.cpp b/src/infra/sys/logger.cpp index 7e61d9f..985dea8 100644 --- a/src/infra/sys/logger.cpp +++ b/src/infra/sys/logger.cpp @@ -7,84 +7,84 @@ namespace { -std::mutex g_log_mutex; + std::mutex g_log_mutex; #if defined(__SWITCH__) -constexpr const char* kLogPath = "/switch/NXST/log.log"; + constexpr const char* kLogPath = "/switch/NXST/log.log"; #else -constexpr const char* kLogPath = "nxst.log"; + constexpr const char* kLogPath = "nxst.log"; #endif -void writeEntry(const char* tag, const char* fmt, va_list args) { - char msg[2048]; - vsnprintf(msg, sizeof(msg), fmt, args); + void writeEntry(const char* tag, const char* fmt, va_list args) { + char msg[2048]; + vsnprintf(msg, sizeof(msg), fmt, args); - time_t now = time(nullptr); - struct tm tm_buf; - localtime_r(&now, &tm_buf); - char time_str[16]; - strftime(time_str, sizeof(time_str), "%H:%M:%S", &tm_buf); + time_t now = time(nullptr); + struct tm tm_buf; + localtime_r(&now, &tm_buf); + char time_str[16]; + strftime(time_str, sizeof(time_str), "%H:%M:%S", &tm_buf); - std::lock_guard<std::mutex> lock(g_log_mutex); + std::lock_guard<std::mutex> lock(g_log_mutex); - fprintf(stderr, "[%s]%s %s\n", time_str, tag, msg); + fprintf(stderr, "[%s]%s %s\n", time_str, tag, msg); - FILE* log_file = fopen(kLogPath, "a"); - if (log_file != nullptr) { - fprintf(log_file, "[%s]%s %s\n", time_str, tag, msg); - fclose(log_file); + FILE* log_file = fopen(kLogPath, "a"); + if (log_file != nullptr) { + fprintf(log_file, "[%s]%s %s\n", time_str, tag, msg); + fclose(log_file); + } } -} } // namespace namespace nxst::log { -void write(Level level, const char* fmt, ...) { - const char* tag = "[INFO] "; - switch (level) { - case Level::Debug: - tag = "[DEBUG]"; - break; - case Level::Info: - tag = "[INFO] "; - break; - case Level::Warn: - tag = "[WARN] "; - break; - case Level::Error: - tag = "[ERROR]"; - break; + void write(Level level, const char* fmt, ...) { + const char* tag = "[INFO] "; + switch (level) { + case Level::Debug: + tag = "[DEBUG]"; + break; + case Level::Info: + tag = "[INFO] "; + break; + case Level::Warn: + tag = "[WARN] "; + break; + case Level::Error: + tag = "[ERROR]"; + break; + } + va_list args; + va_start(args, fmt); + writeEntry(tag, fmt, args); + va_end(args); } - va_list args; - va_start(args, fmt); - writeEntry(tag, fmt, args); - va_end(args); -} -void debug(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - writeEntry("[DEBUG]", fmt, args); - va_end(args); -} -void info(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - writeEntry("[INFO] ", fmt, args); - va_end(args); -} -void warn(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - writeEntry("[WARN] ", fmt, args); - va_end(args); -} -void error(const char* fmt, ...) { - va_list args; - va_start(args, fmt); - writeEntry("[ERROR]", fmt, args); - va_end(args); -} + void debug(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeEntry("[DEBUG]", fmt, args); + va_end(args); + } + void info(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeEntry("[INFO] ", fmt, args); + va_end(args); + } + void warn(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeEntry("[WARN] ", fmt, args); + va_end(args); + } + void error(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + writeEntry("[ERROR]", fmt, args); + va_end(args); + } } // namespace nxst::log diff --git a/src/service/transfer_service.cpp b/src/service/transfer_service.cpp index 6d0b978..440c529 100644 --- a/src/service/transfer_service.cpp +++ b/src/service/transfer_service.cpp @@ -27,514 +27,514 @@ namespace fs = std::filesystem; namespace nxst { -// ─── File-transfer helpers ──────────────────────────────────────────────────── + // ─── 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); + 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; } - } - 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); + return true; } - FILE* outfile = fopen(rel_path.c_str(), "wb"); - if (!outfile) { - std::vector<char> drain(proto::BUF_SIZE); + 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::kBufSize); 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); + size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::kBufSize); + 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::kBufSize); + uint64_t remaining = file_size; + while (remaining > 0) { + size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::kBufSize); + 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::kBufSize); + uint64_t total = 0; + while (total < file_size) { + size_t to_read = (size_t)std::min(file_size - total, (uint64_t)proto::kBufSize); + ssize_t n = read(sock, buffer.data(), to_read); if (n <= 0) break; - remaining -= (uint64_t)n; + fwrite(buffer.data(), 1, (size_t)n, outfile); + total += (uint64_t)n; + state.bytes_done.store(total); } - return; + fclose(outfile); } - state.bytes_total.store(file_size); - state.bytes_done.store(0); + // ─── Sender ────────────────────────────────────────────────────────────────── - 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; + void TransferService::failSend(const std::string& reason) { + sender_state.fail_reason = reason; + sender_state.connection_failed.store(true); + sender_state.done.store(true); } - // 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()) { + 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::kMulticastPort); + addr.sin_addr.s_addr = inet_addr(proto::kMulticastGroup); + + if (sendto(udp_fd, "DISCOVER_SERVER", 15, 0, (sockaddr*)&addr, sizeof(addr)) < 0) { 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; + + // 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 (!backup_result.isOk()) { - failSend("Failed to create backup:\n" + backup_result.error()); - return finish(); - } - fs::path directory = backup_result.value(); -#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; + releaseUdp(); 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); + 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; } - int tcp = sender_tcp_sock.exchange(-1); - if (tcp >= 0) { - shutdown(tcp, SHUT_RDWR); - close(tcp); - } -} -// ─── Receiver ──────────────────────────────────────────────────────────────── + void TransferService::runSender(size_t title_index, AccountUid uid) { + sender_active.store(true); -std::string TransferService::replaceUsername(const std::string& file_path) const { + 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__ - 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); + auto backup_result = io::backup(title_index, uid); + if (!backup_result.isOk()) { + failSend("Failed to create backup:\n" + backup_result.error()); + return finish(); + } + fs::path directory = backup_result.value(); #else - return file_path; + fs::path directory = "."; + (void)title_index; + (void)uid; #endif -} -void* TransferService::broadcastEntry(void* arg) { - static_cast<TransferService*>(arg)->runBroadcast(); - return nullptr; -} + if (sender_state.cancelled.load()) + return finish(); -void TransferService::runBroadcast() { - receiver_broadcast_active.store(true); - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr); - pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr); + 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); - int udp = socket(AF_INET, SOCK_DGRAM, 0); - if (udp < 0) { - receiver_broadcast_active.store(false); - return; + 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::kTcpPort); + 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::kEofSentinel; + sendAll(tcp_fd, &sentinel, sizeof(sentinel)); + + releaseTcp(); + sender_state.setStatus(""); + return finish(); } - receiver_bcast_sock.store(udp); - auto releaseUdp = [&]() { - int owned = receiver_bcast_sock.exchange(-1); - if (owned == udp) + 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); - }; - - 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; + int tcp = sender_tcp_sock.exchange(-1); + if (tcp >= 0) { + shutdown(tcp, SHUT_RDWR); + close(tcp); } } - releaseUdp(); - receiver_broadcast_active.store(false); -} + // ─── Receiver ──────────────────────────────────────────────────────────────── -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; -} + std::string TransferService::replaceUsername(const std::string& file_path) const { +#ifdef __SWITCH__ + std::string username = + string_utils::removeNotAscii(string_utils::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::runAccept(int server_fd) { - receiver_accept_active.store(true); - receiver_listen_sock.store(server_fd); + void* TransferService::broadcastEntry(void* arg) { + static_cast<TransferService*>(arg)->runBroadcast(); + return nullptr; + } - sockaddr_in client_addr{}; - socklen_t client_len = sizeof(client_addr); - int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len); + void TransferService::runBroadcast() { + receiver_broadcast_active.store(true); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr); + pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr); - int owned_listen = receiver_listen_sock.exchange(-1); - if (owned_listen == server_fd) - close(server_fd); + int udp = socket(AF_INET, SOCK_DGRAM, 0); + if (udp < 0) { + receiver_broadcast_active.store(false); + return; + } + receiver_bcast_sock.store(udp); - if (client_sock >= 0) { - receiver_client_sock.store(client_sock); + 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::kMulticastPort); + + 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::kMulticastGroup); + 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) { - 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) + 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; + } + } - 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); + releaseUdp(); + receiver_broadcast_active.store(false); + } - { - size_t sl = filename_str.rfind('/'); - receiver_state.setStatus(sl != std::string::npos ? filename_str.substr(sl + 1) - : filename_str); + 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::kEofSentinel) + break; + if (filename_len > proto::kMaxFilename) + 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); } - 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); - int owned = receiver_client_sock.exchange(-1); - if (owned == client_sock) - close(client_sock); - - if (!receiver_state.cancelled.load()) { + 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 = result.isOk(); - restore_error = result.isOk() ? "" : result.error(); + receiver_state.setStatus("Restoring..."); + auto result = io::restore(restore_title_index, restore_uid, restore_title_name); + restore_ok = result.isOk(); + restore_error = result.isOk() ? "" : result.error(); #else - restore_ok = true; + restore_ok = true; #endif + } } + + receiver_state.done.store(true); + receiver_accept_active.store(false); } - 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(); -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); - 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; + } - 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::kTcpPort); + + 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; } - 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; + 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); } - 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 diff --git a/src/ui/titles_layout.cpp b/src/ui/titles_layout.cpp index 4df3ead..6b8c11f 100644 --- a/src/ui/titles_layout.cpp +++ b/src/ui/titles_layout.cpp @@ -1,312 +1,312 @@ #include <nxst/app/main_application.hpp> #include <nxst/domain/util.hpp> -#include <nxst/ui/const.h> #include <nxst/ui/transfer_overlay.hpp> namespace ui { -extern MainApplication* mainApp; + extern MainApplication* mainApp; -namespace { -constexpr int ListX = theme::space::lg; -constexpr int ListW = 760; -constexpr int PanelX = ListX + ListW + theme::space::xl; -constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg; -constexpr int ContentY = theme::layout::ContentTop + theme::space::md; -constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md; -constexpr int BtnH = 56; -constexpr int BtnW = PanelW - 2 * theme::space::lg; -} // namespace + namespace { + constexpr int ListX = theme::space::lg; + constexpr int ListW = 760; + constexpr int PanelX = ListX + ListW + theme::space::xl; + constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg; + constexpr int ContentY = theme::layout::ContentTop + theme::space::md; + constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md; + constexpr int BtnH = 56; + constexpr int BtnW = PanelW - 2 * theme::space::lg; + } // namespace -TitlesLayout::TitlesLayout() : Layout::Layout() { - using namespace theme; + TitlesLayout::TitlesLayout() { + using namespace theme; - this->titlesMenu = - pu::ui::elm::Menu::New(ListX, ContentY, ListW, color::BgBase, color::BgSurface2, 88, 6); - this->titlesMenu->SetScrollbarColor(color::Primary); - this->titlesMenu->SetItemsFocusColor(color::BgSurface2); - this->titlesMenu->SetOnSelectionChanged([this]() { - this->refreshPanel(); - }); - this->SetBackgroundColor(color::BgBase); - this->Add(this->titlesMenu); + this->titlesMenu = + pu::ui::elm::Menu::New(ListX, ContentY, ListW, color::BgBase, color::BgSurface2, 88, 6); + this->titlesMenu->SetScrollbarColor(color::Primary); + this->titlesMenu->SetItemsFocusColor(color::BgSurface2); + this->titlesMenu->SetOnSelectionChanged([this]() { + this->refreshPanel(); + }); + this->SetBackgroundColor(color::BgBase); + this->Add(this->titlesMenu); - this->panelBg = - pu::ui::elm::Rectangle::New(PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg); - this->Add(this->panelBg); + this->panelBg = + pu::ui::elm::Rectangle::New(PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg); + this->Add(this->panelBg); - this->panelTitle = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg, ""); - this->panelTitle->SetFont(type::font(type::Title)); - this->panelTitle->SetColor(color::TextPrimary); - this->Add(this->panelTitle); + this->panelTitle = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg, ""); + this->panelTitle->SetFont(type::font(type::Title)); + this->panelTitle->SetColor(color::TextPrimary); + this->Add(this->panelTitle); - this->panelHint = - pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:"); - this->panelHint->SetFont(type::font(type::Body)); - this->panelHint->SetColor(color::TextSecondary); - this->Add(this->panelHint); + this->panelHint = + pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:"); + this->panelHint->SetFont(type::font(type::Body)); + this->panelHint->SetColor(color::TextSecondary); + this->Add(this->panelHint); - int btnY = ContentY + 200; - this->btnTransferBg = - pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md); - this->Add(this->btnTransferBg); - this->btnTransferText = - pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY + 14, "Transfer to another device"); - this->btnTransferText->SetFont(type::font(type::Body)); - this->btnTransferText->SetColor(color::TextSecondary); - this->Add(this->btnTransferText); + int btnY = ContentY + 200; + this->btnTransferBg = + pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md); + this->Add(this->btnTransferBg); + this->btnTransferText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY + 14, + "Transfer to another device"); + this->btnTransferText->SetFont(type::font(type::Body)); + this->btnTransferText->SetColor(color::TextSecondary); + this->Add(this->btnTransferText); - int btnY2 = btnY + BtnH + space::md; - this->btnReceiveBg = - pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md); - this->Add(this->btnReceiveBg); - this->btnReceiveText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY2 + 14, - "Receive from another device"); - this->btnReceiveText->SetFont(type::font(type::Body)); - this->btnReceiveText->SetColor(color::TextSecondary); - this->Add(this->btnReceiveText); + int btnY2 = btnY + BtnH + space::md; + this->btnReceiveBg = + pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md); + this->Add(this->btnReceiveBg); + this->btnReceiveText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY2 + 14, + "Receive from another device"); + this->btnReceiveText->SetFont(type::font(type::Body)); + this->btnReceiveText->SetColor(color::TextSecondary); + this->Add(this->btnReceiveText); - this->panelFooter = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + ContentH - space::lg - 18, - "Save data only"); - this->panelFooter->SetFont(type::font(type::Caption)); - this->panelFooter->SetColor(color::TextMuted); - this->Add(this->panelFooter); + this->panelFooter = pu::ui::elm::TextBlock::New( + PanelX + space::lg, ContentY + ContentH - space::lg - 18, "Save data only"); + this->panelFooter->SetFont(type::font(type::Caption)); + this->panelFooter->SetColor(color::TextMuted); + this->Add(this->panelFooter); - this->emptyText = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40, - "No save data on this profile"); - this->emptyText->SetFont(type::font(type::Display)); - this->emptyText->SetColor(color::TextPrimary); - this->emptyText->SetVisible(false); - this->Add(this->emptyText); + this->emptyText = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40, + "No save data on this profile"); + this->emptyText->SetFont(type::font(type::Display)); + this->emptyText->SetColor(color::TextPrimary); + this->emptyText->SetVisible(false); + this->Add(this->emptyText); - this->emptySub = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16, - "Play something first, then come back."); - this->emptySub->SetFont(type::font(type::Body)); - this->emptySub->SetColor(color::TextMuted); - this->emptySub->SetVisible(false); - this->Add(this->emptySub); + this->emptySub = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16, + "Play something first, then come back."); + this->emptySub->SetFont(type::font(type::Body)); + this->emptySub->SetColor(color::TextMuted); + this->emptySub->SetVisible(false); + this->Add(this->emptySub); - this->header = std::make_unique<HeaderBar>(this, "Save Transfer"); - this->hints = std::make_unique<HintBar>(this); - this->updateHints(); -} + this->header = std::make_unique<HeaderBar>(this, "Save Transfer"); + this->hints = std::make_unique<HintBar>(this); + this->updateHints(); + } -void TitlesLayout::InitTitles(AccountUid uid) { - using namespace theme; - this->current_uid = uid; + void TitlesLayout::InitTitles(AccountUid uid) { + using namespace theme; + this->current_uid = uid; - auto it = this->menuCache.find(uid); - std::vector<pu::ui::elm::MenuItem::Ref>* items; - if (it != this->menuCache.end()) { - items = &it->second; - } else { - std::vector<pu::ui::elm::MenuItem::Ref> built; - for (size_t i = 0; i < getTitleCount(uid); i++) { - Title title; - getTitle(title, uid, i); - auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); - titleItem->SetColor(color::TextPrimary); - built.push_back(titleItem); + auto it = this->menuCache.find(uid); + std::vector<pu::ui::elm::MenuItem::Ref>* items; + if (it != this->menuCache.end()) { + items = &it->second; + } else { + std::vector<pu::ui::elm::MenuItem::Ref> built; + for (size_t i = 0; i < getTitleCount(uid); i++) { + Title title; + getTitle(title, uid, i); + auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); + titleItem->SetColor(color::TextPrimary); + built.push_back(titleItem); + } + auto inserted = this->menuCache.emplace(uid, std::move(built)); + items = &inserted.first->second; } - auto inserted = this->menuCache.emplace(uid, std::move(built)); - items = &inserted.first->second; + + this->titlesMenu->ClearItems(); + for (auto& item : *items) { + this->titlesMenu->AddItem(item); + } + this->titlesMenu->SetSelectedIndex(0); + + const bool empty = items->empty(); + this->titlesMenu->SetVisible(!empty); + this->panelBg->SetVisible(!empty); + this->panelTitle->SetVisible(!empty); + this->panelHint->SetVisible(!empty); + this->btnTransferBg->SetVisible(!empty); + this->btnTransferText->SetVisible(!empty); + this->btnReceiveBg->SetVisible(!empty); + this->btnReceiveText->SetVisible(!empty); + this->panelFooter->SetVisible(!empty); + this->emptyText->SetVisible(empty); + this->emptySub->SetVisible(empty); + + this->focus = TitlesFocus::List; + this->action = TitlesAction::Transfer; + this->refreshPanel(); + this->refreshButtons(); + this->updateHints(); + + this->header->SetUser(uid, account::username(uid)); } - this->titlesMenu->ClearItems(); - for (auto& item : *items) { - this->titlesMenu->AddItem(item); + void TitlesLayout::refreshPanel() { + if (this->titlesMenu->GetItems().empty()) + return; + int idx = this->titlesMenu->GetSelectedIndex(); + Title title; + getTitle(title, this->current_uid, idx); + this->panelTitle->SetText(string_utils::elide(title.name(), 24)); } - this->titlesMenu->SetSelectedIndex(0); - const bool empty = items->empty(); - this->titlesMenu->SetVisible(!empty); - this->panelBg->SetVisible(!empty); - this->panelTitle->SetVisible(!empty); - this->panelHint->SetVisible(!empty); - this->btnTransferBg->SetVisible(!empty); - this->btnTransferText->SetVisible(!empty); - this->btnReceiveBg->SetVisible(!empty); - this->btnReceiveText->SetVisible(!empty); - this->panelFooter->SetVisible(!empty); - this->emptyText->SetVisible(empty); - this->emptySub->SetVisible(empty); - - this->focus = TitlesFocus::List; - this->action = TitlesAction::Transfer; - this->refreshPanel(); - this->refreshButtons(); - this->updateHints(); - - this->header->SetUser(uid, Account::username(uid)); -} - -void TitlesLayout::refreshPanel() { - if (this->titlesMenu->GetItems().empty()) - return; - int idx = this->titlesMenu->GetSelectedIndex(); - Title title; - getTitle(title, this->current_uid, idx); - this->panelTitle->SetText(StringUtils::elide(title.name(), 24)); -} - -void TitlesLayout::refreshButtons() { - using namespace theme; - const bool active = (focus == TitlesFocus::Actions); - if (active && action == TitlesAction::Transfer) { - this->btnTransferBg->SetColor(color::Primary); - this->btnTransferText->SetColor(color::TextPrimary); - this->btnReceiveBg->SetColor(color::BgSurface2); - this->btnReceiveText->SetColor(color::TextSecondary); - } else if (active && action == TitlesAction::Receive) { - this->btnTransferBg->SetColor(color::BgSurface2); - this->btnTransferText->SetColor(color::TextSecondary); - this->btnReceiveBg->SetColor(color::Accent); - this->btnReceiveText->SetColor(color::BgBase); - } else { - this->btnTransferBg->SetColor(color::BgSurface2); - this->btnTransferText->SetColor(color::TextSecondary); - this->btnReceiveBg->SetColor(color::BgSurface2); - this->btnReceiveText->SetColor(color::TextSecondary); + void TitlesLayout::refreshButtons() { + using namespace theme; + const bool active = (focus == TitlesFocus::Actions); + if (active && action == TitlesAction::Transfer) { + this->btnTransferBg->SetColor(color::Primary); + this->btnTransferText->SetColor(color::TextPrimary); + this->btnReceiveBg->SetColor(color::BgSurface2); + this->btnReceiveText->SetColor(color::TextSecondary); + } else if (active && action == TitlesAction::Receive) { + this->btnTransferBg->SetColor(color::BgSurface2); + this->btnTransferText->SetColor(color::TextSecondary); + this->btnReceiveBg->SetColor(color::Accent); + this->btnReceiveText->SetColor(color::BgBase); + } else { + this->btnTransferBg->SetColor(color::BgSurface2); + this->btnTransferText->SetColor(color::TextSecondary); + this->btnReceiveBg->SetColor(color::BgSurface2); + this->btnReceiveText->SetColor(color::TextSecondary); + } } -} -void TitlesLayout::updateHints() { - if (focus == TitlesFocus::List) { - this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}}); - } else { - this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}}); + void TitlesLayout::updateHints() { + if (focus == TitlesFocus::List) { + this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}}); + } else { + this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}}); + } } -} -void TitlesLayout::runTransfer(int index, Title& title) { - (void)title; - auto ovl = TransferOverlay::New("Transferring save data..."); - this->titlesMenu->SetVisible(false); - mainApp->StartOverlay(ovl); - this->LockInput(); - if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) { + void TitlesLayout::runTransfer(int index, Title& title) { + (void)title; + auto ovl = TransferOverlay::New("Transferring save data..."); + this->titlesMenu->SetVisible(false); + mainApp->StartOverlay(ovl); + this->LockInput(); + if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) { + mainApp->EndOverlay(); + this->titlesMenu->SetVisible(true); + this->UnlockInput(); + mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); + return; + } + while (!mainApp->transfer.isSendDone()) { + ovl->SetStatus(mainApp->transfer.sendStatusText()); + ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown()); + ovl->SetProgress(mainApp->transfer.sendProgress()); + mainApp->CallForRender(); + if (mainApp->GetButtonsDown() & HidNpadButton_B) { + mainApp->transfer.cancelSend(); + } + svcSleepThread(16666666LL); + } mainApp->EndOverlay(); this->titlesMenu->SetVisible(true); this->UnlockInput(); - mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); - return; + + if (mainApp->transfer.isSendConnectionFailed()) { + mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true); + } else if (mainApp->transfer.isSendCancelled()) { + mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); + } else { + mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); + } } - while (!mainApp->transfer.isSendDone()) { - ovl->SetStatus(mainApp->transfer.sendStatusText()); - ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown()); - ovl->SetProgress(mainApp->transfer.sendProgress()); - mainApp->CallForRender(); - if (mainApp->GetButtonsDown() & HidNpadButton_B) { + + void TitlesLayout::runReceive(int index, Title& title) { + 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); + return; + } + auto ovl = TransferOverlay::New("Receiving save data..."); + this->titlesMenu->SetVisible(false); + mainApp->StartOverlay(ovl); + this->LockInput(); + while (!mainApp->transfer.isReceiveDone()) { + ovl->SetStatus(mainApp->transfer.receiveStatusText()); + ovl->SetProgress(mainApp->transfer.receiveProgress()); + mainApp->CallForRender(); + if (mainApp->GetButtonsDown() & HidNpadButton_B) { + mainApp->transfer.cancelReceive(); + } + svcSleepThread(16666666LL); + } + mainApp->EndOverlay(); + this->titlesMenu->SetVisible(true); + this->UnlockInput(); + + if (mainApp->transfer.isReceiveCancelled()) { + mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); + } else if (mainApp->transfer.restoreSucceeded()) { + mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, + true); + } else { + 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)Up; + (void)Held; + (void)Pos; + if (m_inputLocked) + return; + + if (Down & HidNpadButton_Plus) { mainApp->transfer.cancelSend(); - } - svcSleepThread(16666666LL); - } - mainApp->EndOverlay(); - this->titlesMenu->SetVisible(true); - this->UnlockInput(); - - if (mainApp->transfer.isSendConnectionFailed()) { - mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true); - } else if (mainApp->transfer.isSendCancelled()) { - mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); - } else { - mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); - } -} - -void TitlesLayout::runReceive(int index, Title& title) { - 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); - return; - } - auto ovl = TransferOverlay::New("Receiving save data..."); - this->titlesMenu->SetVisible(false); - mainApp->StartOverlay(ovl); - this->LockInput(); - while (!mainApp->transfer.isReceiveDone()) { - ovl->SetStatus(mainApp->transfer.receiveStatusText()); - ovl->SetProgress(mainApp->transfer.receiveProgress()); - mainApp->CallForRender(); - if (mainApp->GetButtonsDown() & HidNpadButton_B) { mainApp->transfer.cancelReceive(); - } - svcSleepThread(16666666LL); - } - mainApp->EndOverlay(); - this->titlesMenu->SetVisible(true); - this->UnlockInput(); - - if (mainApp->transfer.isReceiveCancelled()) { - mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); - } else if (mainApp->transfer.restoreSucceeded()) { - mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); - } else { - 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)Up; - (void)Held; - (void)Pos; - if (m_inputLocked) - return; - - if (Down & HidNpadButton_Plus) { - mainApp->transfer.cancelSend(); - mainApp->transfer.cancelReceive(); - mainApp->Close(); - return; - } - - if (focus == TitlesFocus::List) { - if (Down & HidNpadButton_B) { - this->header->SetUser(std::nullopt, ""); - mainApp->LoadLayout(mainApp->users_layout); + mainApp->Close(); return; } - if (Down & HidNpadButton_A) { - if (this->titlesMenu->GetItems().empty()) + + if (focus == TitlesFocus::List) { + if (Down & HidNpadButton_B) { + this->header->SetUser(std::nullopt, ""); + mainApp->LoadLayout(mainApp->users_layout); return; - this->lockedListIndex = this->titlesMenu->GetSelectedIndex(); - this->focus = TitlesFocus::Actions; - this->action = TitlesAction::Transfer; - this->refreshButtons(); - this->updateHints(); - return; - } - } else { - if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) { - this->titlesMenu->SetSelectedIndex(this->lockedListIndex); - } - if (Down & HidNpadButton_B) { - this->focus = TitlesFocus::List; - this->refreshButtons(); - this->updateHints(); - return; - } - if (Down & - (HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_StickLUp | HidNpadButton_StickLDown)) { - this->action = - (action == TitlesAction::Transfer) ? TitlesAction::Receive : TitlesAction::Transfer; - this->refreshButtons(); - return; - } - if (Down & HidNpadButton_A) { - int idx = this->titlesMenu->GetSelectedIndex(); - Title title; - getTitle(title, this->current_uid, idx); - TitlesAction chosen = action; - this->focus = TitlesFocus::List; - this->refreshButtons(); - this->updateHints(); - if (chosen == TitlesAction::Transfer) { - this->runTransfer(idx, title); - } else { - this->runReceive(idx, title); + } + if (Down & HidNpadButton_A) { + if (this->titlesMenu->GetItems().empty()) + return; + this->lockedListIndex = this->titlesMenu->GetSelectedIndex(); + this->focus = TitlesFocus::Actions; + this->action = TitlesAction::Transfer; + this->refreshButtons(); + this->updateHints(); + return; + } + } else { + if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) { + this->titlesMenu->SetSelectedIndex(this->lockedListIndex); + } + if (Down & HidNpadButton_B) { + this->focus = TitlesFocus::List; + this->refreshButtons(); + this->updateHints(); + return; + } + if (Down & + (HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_StickLUp | HidNpadButton_StickLDown)) { + this->action = + (action == TitlesAction::Transfer) ? TitlesAction::Receive : TitlesAction::Transfer; + this->refreshButtons(); + return; + } + if (Down & HidNpadButton_A) { + int idx = this->titlesMenu->GetSelectedIndex(); + Title title; + getTitle(title, this->current_uid, idx); + TitlesAction chosen = action; + this->focus = TitlesFocus::List; + this->refreshButtons(); + this->updateHints(); + if (chosen == TitlesAction::Transfer) { + this->runTransfer(idx, title); + } else { + this->runReceive(idx, title); + } } } } -} } // namespace ui diff --git a/src/ui/users_layout.cpp b/src/ui/users_layout.cpp index 3991655..22a315e 100644 --- a/src/ui/users_layout.cpp +++ b/src/ui/users_layout.cpp @@ -1,69 +1,69 @@ #include <nxst/app/main_application.hpp> namespace ui { -extern MainApplication* mainApp; + extern MainApplication* mainApp; -UsersLayout::UsersLayout() : Layout::Layout() { - using namespace theme; + UsersLayout::UsersLayout() : Layout::Layout() { + using namespace theme; - this->usersMenu = pu::ui::elm::Menu::New(0, layout::ContentTop + space::md, layout::ScreenW, - color::BgBase, color::BgSurface2, 88, 6); - this->usersMenu->SetScrollbarColor(color::Primary); - this->usersMenu->SetItemsFocusColor(color::BgSurface2); + this->usersMenu = pu::ui::elm::Menu::New(0, layout::ContentTop + space::md, layout::ScreenW, + color::BgBase, color::BgSurface2, 88, 6); + this->usersMenu->SetScrollbarColor(color::Primary); + this->usersMenu->SetItemsFocusColor(color::BgSurface2); - for (AccountUid const& uid : Account::ids()) { - auto item = pu::ui::elm::MenuItem::New(Account::username(uid)); - item->SetColor(color::TextPrimary); - this->usersMenu->AddItem(item); - } - - this->loadingBg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::ScreenH, color::Scrim); - this->loadingBg->SetVisible(false); - - this->loadingText = - pu::ui::elm::TextBlock::New(layout::ScreenW / 2 - 120, layout::ScreenH / 2 - 12, "Loading saves..."); - this->loadingText->SetFont(type::font(type::Body)); - this->loadingText->SetColor(color::TextSecondary); - this->loadingText->SetVisible(false); - - this->SetBackgroundColor(color::BgBase); - this->Add(this->usersMenu); - this->Add(this->loadingBg); - this->Add(this->loadingText); - - this->header = std::make_unique<HeaderBar>(this, "Select a user"); - this->hints = std::make_unique<HintBar>(this); - this->hints->SetHints({{"A", "Select"}, {"+", "Quit"}}); -} - -int32_t UsersLayout::GetCurrentIndex() { - return this->usersMenu->GetSelectedIndex(); -} - -void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { - if (Down & HidNpadButton_Plus) { - mainApp->Close(); - return; - } - - if (Down & HidNpadButton_A) { - AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex()); - - if (!areTitlesLoaded()) { - this->usersMenu->SetVisible(false); - this->loadingBg->SetVisible(true); - this->loadingText->SetVisible(true); - mainApp->CallForRender(); - - loadTitles(); - - this->loadingBg->SetVisible(false); - this->loadingText->SetVisible(false); - this->usersMenu->SetVisible(true); + for (AccountUid const& uid : account::ids()) { + auto item = pu::ui::elm::MenuItem::New(account::username(uid)); + item->SetColor(color::TextPrimary); + this->usersMenu->AddItem(item); } - mainApp->titles_layout->InitTitles(uid); - mainApp->LoadLayout(mainApp->titles_layout); + this->loadingBg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::ScreenH, color::Scrim); + this->loadingBg->SetVisible(false); + + this->loadingText = pu::ui::elm::TextBlock::New(layout::ScreenW / 2 - 120, layout::ScreenH / 2 - 12, + "Loading saves..."); + this->loadingText->SetFont(type::font(type::Body)); + this->loadingText->SetColor(color::TextSecondary); + this->loadingText->SetVisible(false); + + this->SetBackgroundColor(color::BgBase); + this->Add(this->usersMenu); + this->Add(this->loadingBg); + this->Add(this->loadingText); + + this->header = std::make_unique<HeaderBar>(this, "Select a user"); + this->hints = std::make_unique<HintBar>(this); + this->hints->SetHints({{"A", "Select"}, {"+", "Quit"}}); + } + + int32_t UsersLayout::GetCurrentIndex() { + return this->usersMenu->GetSelectedIndex(); + } + + void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { + if (Down & HidNpadButton_Plus) { + mainApp->Close(); + return; + } + + if (Down & HidNpadButton_A) { + AccountUid uid = account::ids().at(this->usersMenu->GetSelectedIndex()); + + if (!areTitlesLoaded()) { + this->usersMenu->SetVisible(false); + this->loadingBg->SetVisible(true); + this->loadingText->SetVisible(true); + mainApp->CallForRender(); + + loadTitles(); + + this->loadingBg->SetVisible(false); + this->loadingText->SetVisible(false); + this->usersMenu->SetVisible(true); + } + + mainApp->titles_layout->InitTitles(uid); + mainApp->LoadLayout(mainApp->titles_layout); + } } -} } // namespace ui -- 2.39.2