diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..5081159 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + pull_request: + +jobs: + nro: + name: Build NRO + runs-on: ubuntu-latest + + steps: + - name: Checkout + run: | + AUTH_URL=$(echo "$GITHUB_SERVER_URL" | sed "s|://|://oauth2:${GITHUB_TOKEN}@|") + git clone --recurse-submodules "${AUTH_URL}/${GITHUB_REPOSITORY}" . + git checkout "$GITHUB_SHA" + + - name: Build + run: | + docker run --rm \ + -v "$GITHUB_WORKSPACE:/workspace" -w /workspace \ + devkitpro/devkita64:latest \ + bash -c "ls -la && cmake --preset switch && cmake --build build -j\$(nproc)" + + - 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 @build/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/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