finish refactor, add docs and CI
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nro:
|
||||||
|
name: Build NRO
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
docker run --rm \
|
||||||
|
-v "$PWD:/workspace" -w /workspace \
|
||||||
|
devkitpro/devkita64:latest \
|
||||||
|
bash -c "cmake --preset switch && cmake --build build -j\$(nproc)"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: NXST-${{ gitea.sha }}
|
||||||
|
path: build/NXST.nro
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Upload release asset
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: nro
|
||||||
|
if: startsWith(gitea.ref, 'refs/tags/v')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: NXST-${{ gitea.sha }}
|
||||||
|
|
||||||
|
- name: Create Gitea release and upload NRO
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
TAG: ${{ gitea.ref_name }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1
|
||||||
|
REPO: ${{ gitea.repository }}
|
||||||
|
run: |
|
||||||
|
release_id=$(curl -sf -X POST "$API/repos/$REPO/releases" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\"}" \
|
||||||
|
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||||
|
|
||||||
|
curl -sf -X POST "$API/repos/$REPO/releases/$release_id/assets?name=NXST.nro" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @NXST.nro
|
||||||
|
|
||||||
|
format:
|
||||||
|
name: Format check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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/
|
||||||
@@ -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/
|
||||||
@@ -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<T>` tagged-union type replacing `std::tuple<bool, Result, std::string>`
|
||||||
|
- 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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
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 <https://www.gnu.org/licenses/gpl-3.0.txt>.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
#---------------------------------------------------------------------------------
|
|
||||||
.SUFFIXES:
|
|
||||||
#---------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
ifeq ($(strip $(DEVKITPRO)),)
|
|
||||||
$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/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):
|
|
||||||
# - <Project name>.jpg
|
|
||||||
# - icon.jpg
|
|
||||||
# - <libnx folder>/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
|
|
||||||
#---------------------------------------------------------------------------------------
|
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
| 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain |
|
| 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain |
|
||||||
| 5 | TransferService extraction | ✅ Done | L (~2d) | kill globals, sever UI ↔ net coupling |
|
| 5 | TransferService extraction | ✅ Done | L (~2d) | kill globals, sever UI ↔ net coupling |
|
||||||
| 6 | `Result<T>` + RAII | ✅ Done | M (~1d) | tagged union, OS handle wrappers, fix raw memory |
|
| 6 | `Result<T>` + 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 |
|
| 7 | Documentation + license | ✅ Done | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE |
|
||||||
| 8 | CI | ☐ Not started | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check |
|
| 8 | CI | ✅ 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.
|
**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.
|
Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth.
|
||||||
|
|||||||
@@ -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<T>, 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.
|
||||||
@@ -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<T>, 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 `<arpa/inet.h>`, `<sys/socket.h>`, `<pthread.h>`, 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<T, E>` (`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<void, E>` 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<T, E> 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.
|
||||||
@@ -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/<title>/<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.
|
||||||
@@ -1,23 +1,24 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <nxst/ui/const.h>
|
#include <memory>
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
#include <nxst/domain/account.hpp>
|
#include <nxst/domain/account.hpp>
|
||||||
#include <nxst/domain/title.hpp>
|
#include <nxst/domain/title.hpp>
|
||||||
#include <nxst/domain/util.hpp>
|
#include <nxst/domain/util.hpp>
|
||||||
#include <memory>
|
|
||||||
#include <switch.h>
|
|
||||||
#include <nxst/infra/sys/logger.hpp>
|
#include <nxst/infra/sys/logger.hpp>
|
||||||
|
#include <nxst/ui/const.h>
|
||||||
|
|
||||||
typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t;
|
typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t;
|
||||||
|
|
||||||
inline float g_currentTime = 0;
|
inline float g_currentTime = 0;
|
||||||
inline bool g_backupScrollEnabled = 0;
|
inline bool g_backupScrollEnabled = 0;
|
||||||
inline bool g_notificationLedAvailable = false;
|
inline bool g_notificationLedAvailable = false;
|
||||||
inline bool g_shouldExitNetworkLoop = false;
|
inline bool g_shouldExitNetworkLoop = false;
|
||||||
inline std::string g_selectedCheatKey;
|
inline std::string g_selectedCheatKey;
|
||||||
inline std::vector<std::string> g_selectedCheatCodes;
|
inline std::vector<std::string> g_selectedCheatCodes;
|
||||||
inline u32 g_username_dotsize;
|
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 std::string g_currentFile = "";
|
||||||
inline bool g_isTransferringFile = false;
|
inline bool g_isTransferringFile = false;
|
||||||
inline const std::string g_emptySave = "New...";
|
inline const std::string g_emptySave = "New...";
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <pu/Plutonium>
|
#include <pu/Plutonium>
|
||||||
|
|
||||||
#include <nxst/service/transfer_service.hpp>
|
#include <nxst/service/transfer_service.hpp>
|
||||||
#include <nxst/ui/users_layout.hpp>
|
|
||||||
#include <nxst/ui/titles_layout.hpp>
|
#include <nxst/ui/titles_layout.hpp>
|
||||||
|
#include <nxst/ui/users_layout.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
class MainApplication : public pu::ui::Application {
|
class MainApplication : public pu::ui::Application {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
using Application::Application;
|
using Application::Application;
|
||||||
PU_SMART_CTOR(MainApplication)
|
PU_SMART_CTOR(MainApplication)
|
||||||
|
|
||||||
void OnLoad() override;
|
void OnLoad() override;
|
||||||
|
|
||||||
UsersLayout::Ref users_layout;
|
UsersLayout::Ref users_layout;
|
||||||
TitlesLayout::Ref titles_layout;
|
TitlesLayout::Ref titles_layout;
|
||||||
nxst::TransferService transfer;
|
nxst::TransferService transfer;
|
||||||
};
|
};
|
||||||
}
|
} // namespace ui
|
||||||
@@ -28,31 +28,31 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <switch.h>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
#define USER_ICON_SIZE 64
|
#define USER_ICON_SIZE 64
|
||||||
|
|
||||||
namespace std {
|
namespace std {
|
||||||
template <>
|
template <> struct hash<AccountUid> {
|
||||||
struct hash<AccountUid> {
|
size_t operator()(const AccountUid& a) const {
|
||||||
size_t operator()(const AccountUid& a) const { return ((hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1); }
|
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];
|
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;
|
return x.uid[0] == y && x.uid[1] == y;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool operator<(const AccountUid& x, const AccountUid& y)
|
inline bool operator<(const AccountUid& x, const AccountUid& y) {
|
||||||
{
|
if (x.uid[0] != y.uid[0])
|
||||||
if (x.uid[0] != y.uid[0]) return x.uid[0] < y.uid[0];
|
return x.uid[0] < y.uid[0];
|
||||||
return x.uid[1] < y.uid[1];
|
return x.uid[1] < y.uid[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +62,11 @@ struct User {
|
|||||||
};
|
};
|
||||||
|
|
||||||
namespace Account {
|
namespace Account {
|
||||||
Result init(void);
|
Result init(void);
|
||||||
void exit(void);
|
void exit(void);
|
||||||
|
|
||||||
std::vector<AccountUid> ids(void);
|
|
||||||
AccountUid selectAccount(void);
|
|
||||||
std::string username(AccountUid id);
|
|
||||||
std::string iconPath(AccountUid id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
std::vector<AccountUid> ids(void);
|
||||||
|
AccountUid selectAccount(void);
|
||||||
|
std::string username(AccountUid id);
|
||||||
|
std::string iconPath(AccountUid id);
|
||||||
|
} // namespace Account
|
||||||
|
|||||||
@@ -42,21 +42,20 @@
|
|||||||
#define ATEXIT(func) atexit((void (*)())func)
|
#define ATEXIT(func) atexit((void (*)())func)
|
||||||
|
|
||||||
namespace DateTime {
|
namespace DateTime {
|
||||||
std::string timeStr(void);
|
std::string timeStr(void);
|
||||||
std::string dateTimeStr(void);
|
std::string dateTimeStr(void);
|
||||||
std::string logDateTime(void);
|
std::string logDateTime(void);
|
||||||
}
|
} // namespace DateTime
|
||||||
|
|
||||||
namespace StringUtils {
|
namespace StringUtils {
|
||||||
bool containsInvalidChar(const std::string& str);
|
bool containsInvalidChar(const std::string& str);
|
||||||
std::string escapeJson(const std::string& s);
|
std::string escapeJson(const std::string& s);
|
||||||
std::string format(const std::string fmt_str, ...);
|
std::string format(const std::string fmt_str, ...);
|
||||||
std::string removeForbiddenCharacters(std::string src);
|
std::string removeForbiddenCharacters(std::string src);
|
||||||
std::string UTF16toUTF8(const std::u16string& src);
|
std::string UTF16toUTF8(const std::u16string& src);
|
||||||
void ltrim(std::string& s);
|
void ltrim(std::string& s);
|
||||||
void rtrim(std::string& s);
|
void rtrim(std::string& s);
|
||||||
void trim(std::string& s);
|
void trim(std::string& s);
|
||||||
}
|
} // namespace StringUtils
|
||||||
|
|
||||||
char* getConsoleIP(void);
|
char* getConsoleIP(void);
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace proto {
|
namespace proto {
|
||||||
constexpr uint16_t TCP_PORT = 8080;
|
constexpr uint16_t TCP_PORT = 8080;
|
||||||
constexpr uint16_t MULTICAST_PORT = 8081;
|
constexpr uint16_t MULTICAST_PORT = 8081;
|
||||||
constexpr char MULTICAST_GROUP[] = "239.0.0.1";
|
constexpr char MULTICAST_GROUP[] = "239.0.0.1";
|
||||||
constexpr size_t BUF_SIZE = 65536;
|
constexpr size_t BUF_SIZE = 65536;
|
||||||
constexpr uint32_t MAX_FILENAME = 4096;
|
constexpr uint32_t MAX_FILENAME = 4096;
|
||||||
constexpr uint32_t EOF_SENTINEL = 0;
|
constexpr uint32_t EOF_SENTINEL = 0;
|
||||||
|
|
||||||
// Wire layout per file:
|
// Wire layout per file:
|
||||||
// [filename_len : uint32_t LE] — 0 == end-of-stream
|
// [filename_len : uint32_t LE] — 0 == end-of-stream
|
||||||
// [filename : filename_len bytes]
|
// [filename : filename_len bytes]
|
||||||
// [file_size : uint64_t LE]
|
// [file_size : uint64_t LE]
|
||||||
// [file_data : file_size bytes]
|
// [file_data : file_size bytes]
|
||||||
}
|
} // namespace proto
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
|
|
||||||
namespace nxst {
|
namespace nxst {
|
||||||
|
|
||||||
template <class T, class E = std::string>
|
template <class T, class E = std::string> class Result {
|
||||||
class Result {
|
|
||||||
bool ok;
|
bool ok;
|
||||||
alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)];
|
alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)];
|
||||||
|
|
||||||
Result() = default;
|
Result() = default;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static Result success(T val) {
|
static Result success(T val) {
|
||||||
Result res;
|
Result res;
|
||||||
res.ok = true;
|
res.ok = true;
|
||||||
@@ -27,37 +26,52 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
~Result() {
|
~Result() {
|
||||||
if (ok) reinterpret_cast<T*>(storage)->~T();
|
if (ok)
|
||||||
else reinterpret_cast<E*>(storage)->~E();
|
reinterpret_cast<T*>(storage)->~T();
|
||||||
|
else
|
||||||
|
reinterpret_cast<E*>(storage)->~E();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result(const Result& other) : ok(other.ok) {
|
Result(const Result& other) : ok(other.ok) {
|
||||||
if (ok) new (storage) T(*reinterpret_cast<const T*>(other.storage));
|
if (ok)
|
||||||
else new (storage) E(*reinterpret_cast<const E*>(other.storage));
|
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) {
|
Result(Result&& other) : ok(other.ok) {
|
||||||
if (ok) new (storage) T(std::move(*reinterpret_cast<T*>(other.storage)));
|
if (ok)
|
||||||
else new (storage) E(std::move(*reinterpret_cast<E*>(other.storage)));
|
new (storage) T(std::move(*reinterpret_cast<T*>(other.storage)));
|
||||||
|
else
|
||||||
|
new (storage) E(std::move(*reinterpret_cast<E*>(other.storage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Result& operator=(const Result&) = delete;
|
Result& operator=(const Result&) = delete;
|
||||||
|
|
||||||
bool isOk() const noexcept { return ok; }
|
bool isOk() const noexcept {
|
||||||
const T& value() const { return *reinterpret_cast<const T*>(storage); }
|
return ok;
|
||||||
const E& error() const { return *reinterpret_cast<const E*>(storage); }
|
}
|
||||||
|
const T& value() const {
|
||||||
|
return *reinterpret_cast<const T*>(storage);
|
||||||
|
}
|
||||||
|
const E& error() const {
|
||||||
|
return *reinterpret_cast<const E*>(storage);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Specialisation for Result<void>
|
// Specialisation for Result<void>
|
||||||
template <class E>
|
template <class E> class Result<void, E> {
|
||||||
class Result<void, E> {
|
|
||||||
bool ok;
|
bool ok;
|
||||||
alignas(E) unsigned char storage[sizeof(E)];
|
alignas(E) unsigned char storage[sizeof(E)];
|
||||||
|
|
||||||
Result() = default;
|
Result() = default;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static Result success() { Result res; res.ok = true; return res; }
|
static Result success() {
|
||||||
|
Result res;
|
||||||
|
res.ok = true;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
static Result failure(E err) {
|
static Result failure(E err) {
|
||||||
Result res;
|
Result res;
|
||||||
@@ -66,20 +80,29 @@ public:
|
|||||||
return res;
|
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) {
|
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) {
|
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;
|
Result& operator=(const Result&) = delete;
|
||||||
|
|
||||||
bool isOk() const noexcept { return ok; }
|
bool isOk() const noexcept {
|
||||||
const E& error() const { return *reinterpret_cast<const E*>(storage); }
|
return ok;
|
||||||
|
}
|
||||||
|
const E& error() const {
|
||||||
|
return *reinterpret_cast<const E*>(storage);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace nxst
|
} // namespace nxst
|
||||||
|
|||||||
@@ -25,20 +25,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <nxst/domain/account.hpp>
|
|
||||||
#include <nxst/infra/fs/filesystem.hpp>
|
|
||||||
#include <nxst/infra/fs/io.hpp>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <switch.h>
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
#include <nxst/domain/account.hpp>
|
||||||
|
#include <nxst/infra/fs/filesystem.hpp>
|
||||||
|
#include <nxst/infra/fs/io.hpp>
|
||||||
|
|
||||||
class Title {
|
class Title {
|
||||||
public:
|
public:
|
||||||
void init(u8 saveDataType, u64 titleid, AccountUid userID, const std::string& name, const std::string& author);
|
void init(u8 saveDataType, u64 titleid, AccountUid userID, const std::string& name,
|
||||||
|
const std::string& author);
|
||||||
~Title() = default;
|
~Title() = default;
|
||||||
|
|
||||||
std::string author(void);
|
std::string author(void);
|
||||||
@@ -60,7 +63,7 @@ public:
|
|||||||
AccountUid userId(void);
|
AccountUid userId(void);
|
||||||
std::string userName(void);
|
std::string userName(void);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
u64 mId;
|
u64 mId;
|
||||||
u64 mSaveId;
|
u64 mSaveId;
|
||||||
AccountUid mUserId;
|
AccountUid mUserId;
|
||||||
@@ -85,4 +88,3 @@ void sortTitles(void);
|
|||||||
void rotateSortMode(void);
|
void rotateSortMode(void);
|
||||||
void refreshDirectories(u64 id);
|
void refreshDirectories(u64 id);
|
||||||
std::unordered_map<std::string, std::string> getCompleteTitleList(void);
|
std::unordered_map<std::string, std::string> getCompleteTitleList(void);
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
struct TransferState {
|
struct TransferState {
|
||||||
std::atomic<bool> done{false};
|
std::atomic<bool> done{false};
|
||||||
std::atomic<bool> cancelled{false};
|
std::atomic<bool> cancelled{false};
|
||||||
std::atomic<bool> connection_failed{false};
|
std::atomic<bool> connection_failed{false};
|
||||||
std::atomic<uint64_t> bytes_done{0};
|
std::atomic<uint64_t> bytes_done{0};
|
||||||
std::atomic<uint64_t> bytes_total{0};
|
std::atomic<uint64_t> bytes_total{0};
|
||||||
|
|
||||||
std::string status;
|
std::string status;
|
||||||
std::string fail_reason;
|
std::string fail_reason;
|
||||||
mutable std::mutex status_mutex;
|
mutable std::mutex status_mutex;
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
done = false;
|
done = false;
|
||||||
cancelled = false;
|
cancelled = false;
|
||||||
connection_failed = false;
|
connection_failed = false;
|
||||||
bytes_done = 0;
|
bytes_done = 0;
|
||||||
bytes_total = 0;
|
bytes_total = 0;
|
||||||
fail_reason.clear();
|
fail_reason.clear();
|
||||||
std::lock_guard<std::mutex> lock(status_mutex);
|
std::lock_guard<std::mutex> lock(status_mutex);
|
||||||
status.clear();
|
status.clear();
|
||||||
|
|||||||
@@ -25,11 +25,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
#include <nxst/domain/account.hpp>
|
#include <nxst/domain/account.hpp>
|
||||||
#include <nxst/domain/common.hpp>
|
#include <nxst/domain/common.hpp>
|
||||||
#include <nxst/infra/fs/io.hpp>
|
#include <nxst/infra/fs/io.hpp>
|
||||||
#include <switch.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
|
|
||||||
// debug
|
// debug
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
@@ -42,10 +44,8 @@ HidsysNotificationLedPattern blinkLedPattern(u8 times);
|
|||||||
void blinkLed(u8 times);
|
void blinkLed(u8 times);
|
||||||
|
|
||||||
namespace StringUtils {
|
namespace StringUtils {
|
||||||
std::string removeAccents(std::string str);
|
std::string removeAccents(std::string str);
|
||||||
std::string removeNotAscii(std::string str);
|
std::string removeNotAscii(std::string str);
|
||||||
std::u16string UTF8toUTF16(const char* src);
|
std::u16string UTF8toUTF16(const char* src);
|
||||||
std::string elide(const std::string& s, size_t maxChars);
|
std::string elide(const std::string& s, size_t maxChars);
|
||||||
}
|
} // namespace StringUtils
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,16 +28,17 @@
|
|||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <switch.h>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
struct DirectoryEntry {
|
struct DirectoryEntry {
|
||||||
std::string name;
|
std::string name;
|
||||||
bool directory;
|
bool directory;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Directory {
|
class Directory {
|
||||||
public:
|
public:
|
||||||
Directory(const std::string& root);
|
Directory(const std::string& root);
|
||||||
~Directory() = default;
|
~Directory() = default;
|
||||||
|
|
||||||
@@ -47,9 +48,8 @@ public:
|
|||||||
bool good(void);
|
bool good(void);
|
||||||
size_t size(void);
|
size_t size(void);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<struct DirectoryEntry> mList;
|
std::vector<struct DirectoryEntry> mList;
|
||||||
Result mError;
|
Result mError;
|
||||||
bool mGood;
|
bool mGood;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <nxst/domain/account.hpp>
|
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
namespace FileSystem {
|
#include <nxst/domain/account.hpp>
|
||||||
Result mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID);
|
|
||||||
int mount(FsFileSystem fs);
|
|
||||||
void unmount(void);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
namespace FileSystem {
|
||||||
|
Result mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID);
|
||||||
|
int mount(FsFileSystem fs);
|
||||||
|
void unmount(void);
|
||||||
|
} // namespace FileSystem
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
namespace nxst {
|
namespace nxst {
|
||||||
@@ -10,14 +11,21 @@ struct FsFileSystemHandle {
|
|||||||
bool valid{false};
|
bool valid{false};
|
||||||
|
|
||||||
FsFileSystemHandle() = default;
|
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;
|
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.
|
// RAII wrapper for FILE* — auto-fclose on destruction.
|
||||||
@@ -25,13 +33,20 @@ struct FileHandle {
|
|||||||
FILE* ptr{nullptr};
|
FILE* ptr{nullptr};
|
||||||
|
|
||||||
explicit FileHandle(FILE* file) : ptr(file) {}
|
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;
|
FileHandle& operator=(const FileHandle&) = delete;
|
||||||
|
|
||||||
explicit operator bool() const { return ptr != nullptr; }
|
explicit operator bool() const {
|
||||||
FILE* get() const { return ptr; }
|
return ptr != nullptr;
|
||||||
|
}
|
||||||
|
FILE* get() const {
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace nxst
|
} // namespace nxst
|
||||||
|
|||||||
@@ -25,27 +25,29 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#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 <dirent.h>
|
||||||
#include <switch.h>
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.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
|
#define BUFFER_SIZE 0x80000
|
||||||
|
|
||||||
namespace io {
|
namespace io {
|
||||||
nxst::Result<std::string> backup(size_t index, AccountUid uid);
|
nxst::Result<std::string> backup(size_t index, AccountUid uid);
|
||||||
nxst::Result<std::string> restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell);
|
nxst::Result<std::string> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -6,13 +6,24 @@ struct Socket {
|
|||||||
|
|
||||||
Socket() = default;
|
Socket() = default;
|
||||||
explicit Socket(int fd) : fd(fd) {}
|
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& 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; }
|
operator int() const {
|
||||||
bool valid() const { return fd >= 0; }
|
return fd;
|
||||||
void release() { fd = -1; }
|
}
|
||||||
|
bool valid() const {
|
||||||
|
return fd >= 0;
|
||||||
|
}
|
||||||
|
void release() {
|
||||||
|
fd = -1;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,47 +9,51 @@ namespace nxst::log {
|
|||||||
enum class Level { Debug, Info, Warn, Error };
|
enum class Level { Debug, Info, Warn, Error };
|
||||||
|
|
||||||
void write(Level level, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
|
void write(Level level, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||||
void debug(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 info(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
void warn (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 error(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
|
|
||||||
// No-op: writes are immediate. Kept for source compatibility during migration.
|
// No-op: writes are immediate. Kept for source compatibility during migration.
|
||||||
inline void flush() {}
|
inline void flush() {}
|
||||||
|
|
||||||
} // namespace nxst::log
|
} // namespace nxst::log
|
||||||
|
|
||||||
// Backward-compat shim — existing Logger::getInstance().log(...) call sites compile
|
// Backward-compat shim — existing Logger::getInstance().log(...) call sites compile
|
||||||
// unchanged. Format args are dropped (same behavior as broken original). Migrate
|
// unchanged. Format args are dropped (same behavior as broken original). Migrate
|
||||||
// call sites to nxst::log::* in Phase 3.
|
// call sites to nxst::log::* in Phase 3.
|
||||||
struct Logger {
|
struct Logger {
|
||||||
static Logger& getInstance()
|
static Logger& getInstance() {
|
||||||
{
|
|
||||||
static Logger instance;
|
static Logger instance;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// clang-tidy naming suppressed: these must match existing call sites during migration.
|
// 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* DEBUG = "[DEBUG]"; // NOLINT(readability-identifier-naming)
|
||||||
static constexpr const char* ERROR = "[ERROR]"; // 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.
|
// Args intentionally dropped — format string still logged for visibility.
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void log(const std::string& level, const std::string& fmt, Args&&... /*args*/)
|
void log(const std::string& level, const std::string& fmt, Args&&... /*args*/) {
|
||||||
{
|
if (level == ERROR)
|
||||||
if (level == ERROR) nxst::log::error("%s", fmt.c_str());
|
nxst::log::error("%s", fmt.c_str());
|
||||||
else if (level == WARN) nxst::log::warn("%s", fmt.c_str());
|
else if (level == WARN)
|
||||||
else if (level == DEBUG) nxst::log::debug("%s", fmt.c_str());
|
nxst::log::warn("%s", fmt.c_str());
|
||||||
else nxst::log::info("%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() = 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)
|
Logger& operator=(const Logger&) = delete; // NOLINT(modernize-use-equals-delete)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,75 +2,112 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
#include <nxst/domain/transfer_state.hpp>
|
#include <nxst/domain/transfer_state.hpp>
|
||||||
|
|
||||||
namespace nxst {
|
namespace nxst {
|
||||||
|
|
||||||
class TransferService {
|
class TransferService {
|
||||||
public:
|
public:
|
||||||
int startSend(size_t title_index, AccountUid uid);
|
int startSend(size_t title_index, AccountUid uid);
|
||||||
void cancelSend();
|
void cancelSend();
|
||||||
|
|
||||||
bool isSendDone() const { return sender_state.done.load(); }
|
bool isSendDone() const {
|
||||||
bool isSendCancelled() const { return sender_state.cancelled.load(); }
|
return sender_state.done.load();
|
||||||
bool isSendConnectionFailed() const { return sender_state.connection_failed.load(); }
|
}
|
||||||
bool isSendProgressKnown() const { return sender_state.bytes_total.load() > 0; }
|
bool isSendCancelled() const {
|
||||||
bool isSendWorkersIdle() const { return !sender_active.load(); }
|
return sender_state.cancelled.load();
|
||||||
double sendProgress() const { return sender_state.progress(); }
|
}
|
||||||
std::string sendStatusText() const { return sender_state.getStatus(); }
|
bool isSendConnectionFailed() const {
|
||||||
std::string sendFailReason() const { return sender_state.fail_reason; }
|
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();
|
void cancelReceive();
|
||||||
|
|
||||||
bool isReceiveDone() const { return receiver_state.done.load(); }
|
bool isReceiveDone() const {
|
||||||
bool isReceiveCancelled() const { return receiver_state.cancelled.load(); }
|
return receiver_state.done.load();
|
||||||
bool isReceiveWorkersIdle() const {
|
}
|
||||||
|
bool isReceiveCancelled() const {
|
||||||
|
return receiver_state.cancelled.load();
|
||||||
|
}
|
||||||
|
bool isReceiveWorkersIdle() const {
|
||||||
return !receiver_accept_active.load() && !receiver_broadcast_active.load();
|
return !receiver_accept_active.load() && !receiver_broadcast_active.load();
|
||||||
}
|
}
|
||||||
double receiveProgress() const { return receiver_state.progress(); }
|
double receiveProgress() const {
|
||||||
std::string receiveStatusText() const { return receiver_state.getStatus(); }
|
return receiver_state.progress();
|
||||||
bool restoreSucceeded() const { return restore_ok; }
|
}
|
||||||
std::string restoreError() const { return restore_error; }
|
std::string receiveStatusText() const {
|
||||||
|
return receiver_state.getStatus();
|
||||||
|
}
|
||||||
|
bool restoreSucceeded() const {
|
||||||
|
return restore_ok;
|
||||||
|
}
|
||||||
|
std::string restoreError() const {
|
||||||
|
return restore_error;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Sender
|
// Sender
|
||||||
TransferState sender_state;
|
TransferState sender_state;
|
||||||
std::atomic<int> sender_udp_sock{-1};
|
std::atomic<int> sender_udp_sock{-1};
|
||||||
std::atomic<int> sender_tcp_sock{-1};
|
std::atomic<int> sender_tcp_sock{-1};
|
||||||
std::atomic<bool> sender_active{false};
|
std::atomic<bool> sender_active{false};
|
||||||
|
|
||||||
// Receiver
|
// Receiver
|
||||||
TransferState receiver_state;
|
TransferState receiver_state;
|
||||||
std::atomic<int> receiver_client_sock{-1};
|
std::atomic<int> receiver_client_sock{-1};
|
||||||
std::atomic<int> receiver_listen_sock{-1};
|
std::atomic<int> receiver_listen_sock{-1};
|
||||||
std::atomic<int> receiver_bcast_sock{-1};
|
std::atomic<int> receiver_bcast_sock{-1};
|
||||||
std::atomic<bool> receiver_accept_active{false};
|
std::atomic<bool> receiver_accept_active{false};
|
||||||
std::atomic<bool> receiver_broadcast_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
|
// Stored at startReceive, read after network transfer completes
|
||||||
size_t restore_title_index{0};
|
size_t restore_title_index{0};
|
||||||
AccountUid restore_uid{};
|
AccountUid restore_uid{};
|
||||||
std::string restore_title_name;
|
std::string restore_title_name;
|
||||||
bool restore_ok{false};
|
bool restore_ok{false};
|
||||||
std::string restore_error;
|
std::string restore_error;
|
||||||
|
|
||||||
// Sender thread
|
// 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);
|
static void* senderEntry(void* arg);
|
||||||
void runSender(size_t title_index, AccountUid uid);
|
void runSender(size_t title_index, AccountUid uid);
|
||||||
void failSend(const std::string& reason);
|
void failSend(const std::string& reason);
|
||||||
int findServer(char* out_ip);
|
int findServer(char* out_ip);
|
||||||
|
|
||||||
// Receiver threads
|
// Receiver threads
|
||||||
struct AcceptArgs { TransferService* svc; int server_fd; };
|
struct AcceptArgs {
|
||||||
|
TransferService* svc;
|
||||||
|
int server_fd;
|
||||||
|
};
|
||||||
static void* broadcastEntry(void* arg);
|
static void* broadcastEntry(void* arg);
|
||||||
static void* acceptEntry(void* arg);
|
static void* acceptEntry(void* arg);
|
||||||
void runBroadcast();
|
void runBroadcast();
|
||||||
void runAccept(int server_fd);
|
void runAccept(int server_fd);
|
||||||
std::string replaceUsername(const std::string& file_path) const;
|
std::string replaceUsername(const std::string& file_path) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace nxst
|
} // namespace nxst
|
||||||
|
|||||||
+11
-11
@@ -1,18 +1,18 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <pu/Plutonium>
|
#include <pu/Plutonium>
|
||||||
|
|
||||||
#include <nxst/ui/theme.hpp>
|
#include <nxst/ui/theme.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
class Card {
|
class Card {
|
||||||
public:
|
public:
|
||||||
pu::ui::elm::Rectangle::Ref bg;
|
pu::ui::elm::Rectangle::Ref bg;
|
||||||
|
|
||||||
Card(pu::ui::Layout* parent, int x, int y, int w, int h,
|
Card(pu::ui::Layout* parent, int x, int y, int w, int h, pu::ui::Color color = theme::color::BgSurface,
|
||||||
pu::ui::Color color = theme::color::BgSurface,
|
int rad = theme::radius::lg) {
|
||||||
int rad = theme::radius::lg) {
|
bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad);
|
||||||
bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad);
|
parent->Add(bg);
|
||||||
parent->Add(bg);
|
}
|
||||||
}
|
};
|
||||||
};
|
} // namespace ui
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,86 +1,83 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <pu/Plutonium>
|
#include <pu/Plutonium>
|
||||||
|
|
||||||
|
#include <nxst/domain/account.hpp>
|
||||||
#include <nxst/ui/theme.hpp>
|
#include <nxst/ui/theme.hpp>
|
||||||
#include <nxst/ui/ui_context.hpp>
|
#include <nxst/ui/ui_context.hpp>
|
||||||
#include <nxst/domain/account.hpp>
|
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
class HeaderBar {
|
class HeaderBar {
|
||||||
private:
|
private:
|
||||||
pu::ui::elm::Rectangle::Ref bg;
|
pu::ui::elm::Rectangle::Ref bg;
|
||||||
pu::ui::elm::Rectangle::Ref divider;
|
pu::ui::elm::Rectangle::Ref divider;
|
||||||
pu::ui::elm::TextBlock::Ref appName;
|
pu::ui::elm::TextBlock::Ref appName;
|
||||||
pu::ui::elm::TextBlock::Ref subtitle;
|
pu::ui::elm::TextBlock::Ref subtitle;
|
||||||
pu::ui::elm::Rectangle::Ref chipBg;
|
pu::ui::elm::Rectangle::Ref chipBg;
|
||||||
pu::ui::elm::Image::Ref avatar;
|
pu::ui::elm::Image::Ref avatar;
|
||||||
pu::ui::elm::TextBlock::Ref userName;
|
pu::ui::elm::TextBlock::Ref userName;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") {
|
HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") {
|
||||||
using namespace theme;
|
using namespace theme;
|
||||||
|
|
||||||
bg = pu::ui::elm::Rectangle::New(
|
bg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface);
|
||||||
0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface);
|
divider = pu::ui::elm::Rectangle::New(0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider);
|
||||||
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 = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST");
|
||||||
appName->SetFont(type::font(type::Title));
|
appName->SetFont(type::font(type::Title));
|
||||||
appName->SetColor(color::TextPrimary);
|
appName->SetColor(color::TextPrimary);
|
||||||
|
|
||||||
subtitle = pu::ui::elm::TextBlock::New(space::lg, 46, sub);
|
subtitle = pu::ui::elm::TextBlock::New(space::lg, 46, sub);
|
||||||
subtitle->SetFont(type::font(type::Caption));
|
subtitle->SetFont(type::font(type::Caption));
|
||||||
subtitle->SetColor(color::TextMuted);
|
subtitle->SetColor(color::TextMuted);
|
||||||
|
|
||||||
const int chipW = 280;
|
const int chipW = 280;
|
||||||
const int chipX = layout::ScreenW - chipW - space::lg;
|
const int chipX = layout::ScreenW - chipW - space::lg;
|
||||||
chipBg = pu::ui::elm::Rectangle::New(
|
chipBg = pu::ui::elm::Rectangle::New(chipX, 16, chipW, 40, color::BgSurface2, radius::pill);
|
||||||
chipX, 16, chipW, 40,
|
chipBg->SetVisible(false);
|
||||||
color::BgSurface2, radius::pill);
|
|
||||||
chipBg->SetVisible(false);
|
|
||||||
|
|
||||||
avatar = pu::ui::elm::Image::New(chipX + 4, 20, "");
|
avatar = pu::ui::elm::Image::New(chipX + 4, 20, "");
|
||||||
avatar->SetWidth(32);
|
avatar->SetWidth(32);
|
||||||
avatar->SetHeight(32);
|
avatar->SetHeight(32);
|
||||||
avatar->SetVisible(false);
|
avatar->SetVisible(false);
|
||||||
|
|
||||||
userName = pu::ui::elm::TextBlock::New(chipX + 44, 24, "");
|
userName = pu::ui::elm::TextBlock::New(chipX + 44, 24, "");
|
||||||
userName->SetFont(type::font(type::Body));
|
userName->SetFont(type::font(type::Body));
|
||||||
userName->SetColor(color::TextPrimary);
|
userName->SetColor(color::TextPrimary);
|
||||||
userName->SetVisible(false);
|
userName->SetVisible(false);
|
||||||
|
|
||||||
parent->Add(bg);
|
parent->Add(bg);
|
||||||
parent->Add(divider);
|
parent->Add(divider);
|
||||||
parent->Add(appName);
|
parent->Add(appName);
|
||||||
parent->Add(subtitle);
|
parent->Add(subtitle);
|
||||||
parent->Add(chipBg);
|
parent->Add(chipBg);
|
||||||
parent->Add(avatar);
|
parent->Add(avatar);
|
||||||
parent->Add(userName);
|
parent->Add(userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetUser(const std::optional<AccountUid>& uid, const std::string& name) {
|
void SetUser(const std::optional<AccountUid>& uid, const std::string& name) {
|
||||||
const bool show = uid.has_value();
|
const bool show = uid.has_value();
|
||||||
chipBg->SetVisible(show);
|
chipBg->SetVisible(show);
|
||||||
userName->SetVisible(show);
|
userName->SetVisible(show);
|
||||||
if (show) {
|
if (show) {
|
||||||
userName->SetText(name);
|
userName->SetText(name);
|
||||||
std::string path = Account::iconPath(*uid);
|
std::string path = Account::iconPath(*uid);
|
||||||
if (!path.empty()) {
|
if (!path.empty()) {
|
||||||
avatar->SetImage(path);
|
avatar->SetImage(path);
|
||||||
avatar->SetWidth(32);
|
avatar->SetWidth(32);
|
||||||
avatar->SetHeight(32);
|
avatar->SetHeight(32);
|
||||||
avatar->SetVisible(avatar->IsImageValid());
|
avatar->SetVisible(avatar->IsImageValid());
|
||||||
} else {
|
|
||||||
avatar->SetVisible(false);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
avatar->SetVisible(false);
|
avatar->SetVisible(false);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
avatar->SetVisible(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SetSubtitle(const std::string& text) {
|
void SetSubtitle(const std::string& text) {
|
||||||
subtitle->SetText(text);
|
subtitle->SetText(text);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
} // namespace ui
|
||||||
|
|||||||
@@ -1,55 +1,56 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <pu/Plutonium>
|
|
||||||
#include <nxst/ui/theme.hpp>
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <pu/Plutonium>
|
||||||
|
|
||||||
|
#include <nxst/ui/theme.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
struct Hint {
|
struct Hint {
|
||||||
std::string glyph;
|
std::string glyph;
|
||||||
std::string label;
|
std::string label;
|
||||||
};
|
};
|
||||||
|
|
||||||
class HintBar {
|
class HintBar {
|
||||||
private:
|
private:
|
||||||
pu::ui::Layout* parent;
|
pu::ui::Layout* parent;
|
||||||
pu::ui::elm::Rectangle::Ref bg;
|
pu::ui::elm::Rectangle::Ref bg;
|
||||||
pu::ui::elm::Rectangle::Ref divider;
|
pu::ui::elm::Rectangle::Ref divider;
|
||||||
std::vector<pu::ui::elm::TextBlock::Ref> labels;
|
std::vector<pu::ui::elm::TextBlock::Ref> labels;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
HintBar(pu::ui::Layout* p) : parent(p) {
|
HintBar(pu::ui::Layout* p) : parent(p) {
|
||||||
using namespace theme;
|
using namespace theme;
|
||||||
bg = pu::ui::elm::Rectangle::New(
|
bg = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, layout::HintH,
|
||||||
0, layout::ScreenH - layout::HintH,
|
color::BgSurface);
|
||||||
layout::ScreenW, layout::HintH, color::BgSurface);
|
divider = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, 1,
|
||||||
divider = pu::ui::elm::Rectangle::New(
|
color::Divider);
|
||||||
0, layout::ScreenH - layout::HintH,
|
parent->Add(bg);
|
||||||
layout::ScreenW, 1, color::Divider);
|
parent->Add(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;
|
} // namespace ui
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
+64
-63
@@ -1,78 +1,79 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <pu/Plutonium>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <pu/Plutonium>
|
||||||
|
|
||||||
namespace theme {
|
namespace theme {
|
||||||
using pu::ui::Color;
|
using pu::ui::Color;
|
||||||
|
|
||||||
namespace color {
|
namespace color {
|
||||||
constexpr Color BgBase{0x10, 0x14, 0x1C, 0xFF};
|
constexpr Color BgBase{0x10, 0x14, 0x1C, 0xFF};
|
||||||
constexpr Color BgSurface{0x18, 0x1F, 0x2A, 0xFF};
|
constexpr Color BgSurface{0x18, 0x1F, 0x2A, 0xFF};
|
||||||
constexpr Color BgSurface2{0x22, 0x2B, 0x39, 0xFF};
|
constexpr Color BgSurface2{0x22, 0x2B, 0x39, 0xFF};
|
||||||
constexpr Color Scrim{0x00, 0x00, 0x00, 0xB8};
|
constexpr Color Scrim{0x00, 0x00, 0x00, 0xB8};
|
||||||
|
|
||||||
constexpr Color Primary{0xE2, 0x4B, 0x55, 0xFF};
|
constexpr Color Primary{0xE2, 0x4B, 0x55, 0xFF};
|
||||||
constexpr Color PrimaryDim{0x9C, 0x33, 0x3A, 0xFF};
|
constexpr Color PrimaryDim{0x9C, 0x33, 0x3A, 0xFF};
|
||||||
constexpr Color Accent{0x4A, 0xC2, 0xE0, 0xFF};
|
constexpr Color Accent{0x4A, 0xC2, 0xE0, 0xFF};
|
||||||
|
|
||||||
constexpr Color TextPrimary{0xF2, 0xF4, 0xF8, 0xFF};
|
constexpr Color TextPrimary{0xF2, 0xF4, 0xF8, 0xFF};
|
||||||
constexpr Color TextSecondary{0xB6, 0xBE, 0xCB, 0xFF};
|
constexpr Color TextSecondary{0xB6, 0xBE, 0xCB, 0xFF};
|
||||||
constexpr Color TextMuted{0x70, 0x7A, 0x8C, 0xFF};
|
constexpr Color TextMuted{0x70, 0x7A, 0x8C, 0xFF};
|
||||||
|
|
||||||
constexpr Color Success{0x55, 0xC8, 0x8A, 0xFF};
|
constexpr Color Success{0x55, 0xC8, 0x8A, 0xFF};
|
||||||
constexpr Color Error{0xE0, 0x6C, 0x6C, 0xFF};
|
constexpr Color Error{0xE0, 0x6C, 0x6C, 0xFF};
|
||||||
constexpr Color Warning{0xE6, 0xB4, 0x55, 0xFF};
|
constexpr Color Warning{0xE6, 0xB4, 0x55, 0xFF};
|
||||||
|
|
||||||
constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF};
|
constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF};
|
||||||
constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF};
|
constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF};
|
||||||
}
|
} // namespace color
|
||||||
|
|
||||||
namespace space {
|
namespace space {
|
||||||
constexpr int xs = 4;
|
constexpr int xs = 4;
|
||||||
constexpr int sm = 8;
|
constexpr int sm = 8;
|
||||||
constexpr int md = 16;
|
constexpr int md = 16;
|
||||||
constexpr int lg = 24;
|
constexpr int lg = 24;
|
||||||
constexpr int xl = 32;
|
constexpr int xl = 32;
|
||||||
constexpr int xxl = 48;
|
constexpr int xxl = 48;
|
||||||
}
|
} // namespace space
|
||||||
|
|
||||||
namespace radius {
|
namespace radius {
|
||||||
constexpr int sm = 6;
|
constexpr int sm = 6;
|
||||||
constexpr int md = 12;
|
constexpr int md = 12;
|
||||||
constexpr int lg = 20;
|
constexpr int lg = 20;
|
||||||
constexpr int pill = 9999;
|
constexpr int pill = 9999;
|
||||||
}
|
} // namespace radius
|
||||||
|
|
||||||
namespace type {
|
namespace type {
|
||||||
constexpr int Display = 38;
|
constexpr int Display = 38;
|
||||||
constexpr int Title = 30;
|
constexpr int Title = 30;
|
||||||
constexpr int Body = 25;
|
constexpr int Body = 25;
|
||||||
constexpr int Label = 20;
|
constexpr int Label = 20;
|
||||||
constexpr int Caption = 18;
|
constexpr int Caption = 18;
|
||||||
|
|
||||||
inline std::string font(int size) {
|
inline std::string font(int size) {
|
||||||
return "DefaultFont@" + std::to_string(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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} // 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
|
||||||
|
|||||||
@@ -1,59 +1,63 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <pu/Plutonium>
|
#include <memory>
|
||||||
#include <nxst/ui/const.h>
|
|
||||||
#include <nxst/domain/title.hpp>
|
|
||||||
#include <nxst/domain/account.hpp>
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#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/header_bar.hpp>
|
||||||
#include <nxst/ui/hint_bar.hpp>
|
#include <nxst/ui/hint_bar.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
enum class TitlesFocus { List, Actions };
|
enum class TitlesFocus { List, Actions };
|
||||||
enum class TitlesAction { Transfer, Receive };
|
enum class TitlesAction { Transfer, Receive };
|
||||||
|
|
||||||
class TitlesLayout : public pu::ui::Layout {
|
class TitlesLayout : public pu::ui::Layout {
|
||||||
private:
|
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;
|
pu::ui::elm::Rectangle::Ref panelBg;
|
||||||
std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache;
|
pu::ui::elm::TextBlock::Ref panelTitle;
|
||||||
bool m_inputLocked = false;
|
pu::ui::elm::TextBlock::Ref panelHint;
|
||||||
std::unique_ptr<HeaderBar> header;
|
pu::ui::elm::Rectangle::Ref btnTransferBg;
|
||||||
std::unique_ptr<HintBar> hints;
|
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;
|
AccountUid current_uid{};
|
||||||
pu::ui::elm::TextBlock::Ref panelTitle;
|
TitlesFocus focus = TitlesFocus::List;
|
||||||
pu::ui::elm::TextBlock::Ref panelHint;
|
TitlesAction action = TitlesAction::Transfer;
|
||||||
pu::ui::elm::Rectangle::Ref btnTransferBg;
|
int lockedListIndex = 0;
|
||||||
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{};
|
void refreshPanel();
|
||||||
TitlesFocus focus = TitlesFocus::List;
|
void refreshButtons();
|
||||||
TitlesAction action = TitlesAction::Transfer;
|
void updateHints();
|
||||||
int lockedListIndex = 0;
|
void runTransfer(int index, Title& title);
|
||||||
|
void runReceive(int index, Title& title);
|
||||||
|
|
||||||
void refreshPanel();
|
public:
|
||||||
void refreshButtons();
|
TitlesLayout();
|
||||||
void updateHints();
|
void InitTitles(AccountUid uid);
|
||||||
void runTransfer(int index, Title& title);
|
void LockInput() {
|
||||||
void runReceive(int index, Title& title);
|
m_inputLocked = true;
|
||||||
|
}
|
||||||
|
void UnlockInput() {
|
||||||
|
m_inputLocked = false;
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos);
|
||||||
|
|
||||||
TitlesLayout();
|
PU_SMART_CTOR(TitlesLayout)
|
||||||
void InitTitles(AccountUid uid);
|
};
|
||||||
void LockInput() { m_inputLocked = true; }
|
} // namespace ui
|
||||||
void UnlockInput() { m_inputLocked = false; }
|
|
||||||
|
|
||||||
void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos);
|
|
||||||
|
|
||||||
PU_SMART_CTOR(TitlesLayout)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,94 +1,84 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <pu/Plutonium>
|
#include <pu/Plutonium>
|
||||||
#include <nxst/ui/theme.hpp>
|
|
||||||
#include <nxst/domain/util.hpp>
|
#include <nxst/domain/util.hpp>
|
||||||
|
#include <nxst/ui/theme.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
class TransferOverlay : public pu::ui::Overlay {
|
class TransferOverlay : public pu::ui::Overlay {
|
||||||
private:
|
private:
|
||||||
pu::ui::elm::Rectangle::Ref card;
|
pu::ui::elm::Rectangle::Ref card;
|
||||||
pu::ui::elm::TextBlock::Ref titleText;
|
pu::ui::elm::TextBlock::Ref titleText;
|
||||||
pu::ui::elm::TextBlock::Ref statusText;
|
pu::ui::elm::TextBlock::Ref statusText;
|
||||||
pu::ui::elm::Rectangle::Ref progressTrack;
|
pu::ui::elm::Rectangle::Ref progressTrack;
|
||||||
pu::ui::elm::ProgressBar::Ref progressBar;
|
pu::ui::elm::ProgressBar::Ref progressBar;
|
||||||
pu::ui::elm::TextBlock::Ref indeterminateText;
|
pu::ui::elm::TextBlock::Ref indeterminateText;
|
||||||
pu::ui::elm::TextBlock::Ref hintText;
|
pu::ui::elm::TextBlock::Ref hintText;
|
||||||
|
|
||||||
static constexpr int CardW = 720;
|
static constexpr int CardW = 720;
|
||||||
static constexpr int CardH = 360;
|
static constexpr int CardH = 360;
|
||||||
static constexpr int CardX = (theme::layout::ScreenW - CardW) / 2;
|
static constexpr int CardX = (theme::layout::ScreenW - CardW) / 2;
|
||||||
static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2;
|
static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TransferOverlay(const std::string &title)
|
TransferOverlay(const std::string& title)
|
||||||
: Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim)
|
: Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) {
|
||||||
{
|
using namespace theme;
|
||||||
using namespace theme;
|
|
||||||
|
|
||||||
card = pu::ui::elm::Rectangle::New(
|
card = pu::ui::elm::Rectangle::New(CardX, CardY, CardW, CardH, color::BgSurface, radius::lg);
|
||||||
CardX, CardY, CardW, CardH, color::BgSurface, radius::lg);
|
|
||||||
|
|
||||||
titleText = pu::ui::elm::TextBlock::New(
|
titleText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg, title);
|
||||||
CardX + space::lg, CardY + space::lg, title);
|
titleText->SetFont(type::font(type::Title));
|
||||||
titleText->SetFont(type::font(type::Title));
|
titleText->SetColor(color::TextPrimary);
|
||||||
titleText->SetColor(color::TextPrimary);
|
|
||||||
|
|
||||||
statusText = pu::ui::elm::TextBlock::New(
|
statusText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg + 56, "");
|
||||||
CardX + space::lg,
|
statusText->SetFont(type::font(type::Body));
|
||||||
CardY + space::lg + 56,
|
statusText->SetColor(color::TextSecondary);
|
||||||
"");
|
|
||||||
statusText->SetFont(type::font(type::Body));
|
|
||||||
statusText->SetColor(color::TextSecondary);
|
|
||||||
|
|
||||||
int barX = CardX + space::lg;
|
int barX = CardX + space::lg;
|
||||||
int barY = CardY + space::lg + 56 + 56;
|
int barY = CardY + space::lg + 56 + 56;
|
||||||
int barW = CardW - 2 * space::lg;
|
int barW = CardW - 2 * space::lg;
|
||||||
|
|
||||||
progressTrack = pu::ui::elm::Rectangle::New(
|
progressTrack = pu::ui::elm::Rectangle::New(barX, barY, barW, 8, color::Divider, radius::sm);
|
||||||
barX, barY, barW, 8, color::Divider, radius::sm);
|
|
||||||
|
|
||||||
progressBar = pu::ui::elm::ProgressBar::New(
|
progressBar = pu::ui::elm::ProgressBar::New(barX, barY, barW, 8, 100.0);
|
||||||
barX, barY, barW, 8, 100.0);
|
progressBar->SetProgressColor(color::Primary);
|
||||||
progressBar->SetProgressColor(color::Primary);
|
progressBar->SetBackgroundColor(color::Divider);
|
||||||
progressBar->SetBackgroundColor(color::Divider);
|
|
||||||
|
|
||||||
indeterminateText = pu::ui::elm::TextBlock::New(
|
indeterminateText = pu::ui::elm::TextBlock::New(barX, barY - 4, "Preparing transfer...");
|
||||||
barX, barY - 4, "Preparing transfer...");
|
indeterminateText->SetFont(type::font(type::Body));
|
||||||
indeterminateText->SetFont(type::font(type::Body));
|
indeterminateText->SetColor(color::TextMuted);
|
||||||
indeterminateText->SetColor(color::TextMuted);
|
indeterminateText->SetVisible(false);
|
||||||
indeterminateText->SetVisible(false);
|
|
||||||
|
|
||||||
hintText = pu::ui::elm::TextBlock::New(
|
hintText =
|
||||||
CardX + space::lg,
|
pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + CardH - space::lg - 18, "B to cancel");
|
||||||
CardY + CardH - space::lg - 18,
|
hintText->SetFont(type::font(type::Caption));
|
||||||
"B to cancel");
|
hintText->SetColor(color::TextMuted);
|
||||||
hintText->SetFont(type::font(type::Caption));
|
|
||||||
hintText->SetColor(color::TextMuted);
|
|
||||||
|
|
||||||
this->Add(card);
|
this->Add(card);
|
||||||
this->Add(titleText);
|
this->Add(titleText);
|
||||||
this->Add(statusText);
|
this->Add(statusText);
|
||||||
this->Add(progressTrack);
|
this->Add(progressTrack);
|
||||||
this->Add(progressBar);
|
this->Add(progressBar);
|
||||||
this->Add(indeterminateText);
|
this->Add(indeterminateText);
|
||||||
this->Add(hintText);
|
this->Add(hintText);
|
||||||
}
|
}
|
||||||
PU_SMART_CTOR(TransferOverlay)
|
PU_SMART_CTOR(TransferOverlay)
|
||||||
|
|
||||||
void SetStatus(const std::string &status) {
|
void SetStatus(const std::string& status) {
|
||||||
statusText->SetText(StringUtils::elide(status, 56));
|
statusText->SetText(StringUtils::elide(status, 56));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetProgress(double val) {
|
void SetProgress(double val) {
|
||||||
progressBar->SetProgress(val);
|
progressBar->SetProgress(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetProgressVisible(bool visible) {
|
void SetProgressVisible(bool visible) {
|
||||||
progressTrack->SetVisible(visible);
|
progressTrack->SetVisible(visible);
|
||||||
progressBar->SetVisible(visible);
|
progressBar->SetVisible(visible);
|
||||||
indeterminateText->SetVisible(!visible);
|
indeterminateText->SetVisible(!visible);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
} // namespace ui
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
#include <nxst/domain/account.hpp>
|
#include <nxst/domain/account.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
struct UiContext {
|
struct UiContext {
|
||||||
std::optional<AccountUid> selectedUser;
|
std::optional<AccountUid> selectedUser;
|
||||||
std::string selectedUserName;
|
std::string selectedUserName;
|
||||||
};
|
};
|
||||||
}
|
} // namespace ui
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
|
#include <memory>
|
||||||
|
|
||||||
#include <pu/Plutonium>
|
#include <pu/Plutonium>
|
||||||
|
|
||||||
#include <nxst/ui/const.h>
|
#include <nxst/ui/const.h>
|
||||||
#include <nxst/ui/header_bar.hpp>
|
#include <nxst/ui/header_bar.hpp>
|
||||||
#include <nxst/ui/hint_bar.hpp>
|
#include <nxst/ui/hint_bar.hpp>
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
class UsersLayout : public pu::ui::Layout {
|
class UsersLayout : public pu::ui::Layout {
|
||||||
private:
|
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;
|
public:
|
||||||
pu::ui::elm::Rectangle::Ref loadingBg;
|
UsersLayout();
|
||||||
pu::ui::elm::TextBlock::Ref loadingText;
|
|
||||||
std::unique_ptr<HeaderBar> header;
|
|
||||||
std::unique_ptr<HintBar> hints;
|
|
||||||
|
|
||||||
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);
|
PU_SMART_CTOR(UsersLayout)
|
||||||
|
};
|
||||||
int32_t GetCurrentIndex();
|
} // namespace ui
|
||||||
|
|
||||||
PU_SMART_CTOR(UsersLayout)
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
+14
-11
@@ -1,9 +1,12 @@
|
|||||||
#include <nxst/app/main_application.hpp>
|
|
||||||
#include <nxst/domain/util.hpp>
|
|
||||||
#include <nxst/app/main.hpp>
|
|
||||||
#include <unistd.h>
|
#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;
|
static int nxlink_sock = -1;
|
||||||
|
|
||||||
@@ -25,9 +28,9 @@ extern "C" void userAppExit() {
|
|||||||
if (ui::mainApp) {
|
if (ui::mainApp) {
|
||||||
ui::mainApp->transfer.cancelReceive();
|
ui::mainApp->transfer.cancelReceive();
|
||||||
ui::mainApp->transfer.cancelSend();
|
ui::mainApp->transfer.cancelSend();
|
||||||
for (int i = 0; i < 150 &&
|
for (int i = 0; i < 150 && (!ui::mainApp->transfer.isReceiveWorkersIdle() ||
|
||||||
(!ui::mainApp->transfer.isReceiveWorkersIdle() ||
|
!ui::mainApp->transfer.isSendWorkersIdle());
|
||||||
!ui::mainApp->transfer.isSendWorkersIdle()); i++) {
|
i++) {
|
||||||
usleep(10000);
|
usleep(10000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,8 +60,8 @@ int main() {
|
|||||||
|
|
||||||
// First create our renderer, where one can customize SDL or other stuff's
|
// First create our renderer, where one can customize SDL or other stuff's
|
||||||
// initialization.
|
// initialization.
|
||||||
auto renderer_opts = pu::ui::render::RendererInitOptions(
|
auto renderer_opts =
|
||||||
SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags);
|
pu::ui::render::RendererInitOptions(SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags);
|
||||||
renderer_opts.UseImage(pu::ui::render::IMGAllFlags);
|
renderer_opts.UseImage(pu::ui::render::IMGAllFlags);
|
||||||
renderer_opts.UseAudio(pu::ui::render::MixerAllFlags);
|
renderer_opts.UseAudio(pu::ui::render::MixerAllFlags);
|
||||||
renderer_opts.UseTTF();
|
renderer_opts.UseTTF();
|
||||||
@@ -68,7 +71,7 @@ int main() {
|
|||||||
renderer_opts.SetExtraDefaultFontSize(theme::type::Title);
|
renderer_opts.SetExtraDefaultFontSize(theme::type::Title);
|
||||||
renderer_opts.SetExtraDefaultFontSize(theme::type::Display);
|
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
|
// Create our main application from the renderer
|
||||||
auto main = ui::MainApplication::New(renderer);
|
auto main = ui::MainApplication::New(renderer);
|
||||||
@@ -78,5 +81,5 @@ int main() {
|
|||||||
main->Show();
|
main->Show();
|
||||||
|
|
||||||
servicesExit();
|
servicesExit();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <switch.h>
|
|
||||||
#include <switch/services/hid.h>
|
#include <switch/services/hid.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
#include <nxst/app/main_application.hpp>
|
#include <nxst/app/main_application.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
MainApplication *mainApp;
|
MainApplication* mainApp;
|
||||||
|
|
||||||
void MainApplication::OnLoad() {
|
void MainApplication::OnLoad() {
|
||||||
mainApp = this;
|
mainApp = this;
|
||||||
this->users_layout = UsersLayout::New();
|
this->users_layout = UsersLayout::New();
|
||||||
this->titles_layout = TitlesLayout::New();
|
this->titles_layout = TitlesLayout::New();
|
||||||
this->users_layout->SetOnInput(
|
this->users_layout->SetOnInput(std::bind(&UsersLayout::onInput, this->users_layout, std::placeholders::_1,
|
||||||
std::bind(&UsersLayout::onInput, this->users_layout, std::placeholders::_1, std::placeholders::_2,
|
std::placeholders::_2, std::placeholders::_3,
|
||||||
std::placeholders::_3, std::placeholders::_4));
|
std::placeholders::_4));
|
||||||
this->titles_layout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titles_layout, std::placeholders::_1, std::placeholders::_2,
|
this->titles_layout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titles_layout,
|
||||||
std::placeholders::_3, std::placeholders::_4));
|
std::placeholders::_1, std::placeholders::_2,
|
||||||
this->LoadLayout(this->users_layout);
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
}
|
this->LoadLayout(this->users_layout);
|
||||||
}
|
}
|
||||||
|
} // namespace ui
|
||||||
+25
-26
@@ -24,16 +24,17 @@
|
|||||||
* reasonable ways as different from the original version.
|
* reasonable ways as different from the original version.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <nxst/domain/account.hpp>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include <nxst/domain/account.hpp>
|
||||||
|
|
||||||
static std::map<AccountUid, User> mUsers;
|
static std::map<AccountUid, User> mUsers;
|
||||||
|
|
||||||
Result Account::init(void)
|
Result Account::init(void) {
|
||||||
{
|
|
||||||
Result res = accountInitialize(AccountServiceType_Application);
|
Result res = accountInitialize(AccountServiceType_Application);
|
||||||
if (R_FAILED(res)) return res;
|
if (R_FAILED(res))
|
||||||
|
return res;
|
||||||
|
|
||||||
AccountUid uids[8];
|
AccountUid uids[8];
|
||||||
s32 count = 0;
|
s32 count = 0;
|
||||||
@@ -44,13 +45,11 @@ Result Account::init(void)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Account::exit(void)
|
void Account::exit(void) {
|
||||||
{
|
|
||||||
accountExit();
|
accountExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<AccountUid> Account::ids(void)
|
std::vector<AccountUid> Account::ids(void) {
|
||||||
{
|
|
||||||
std::vector<AccountUid> v;
|
std::vector<AccountUid> v;
|
||||||
for (auto& value : mUsers) {
|
for (auto& value : mUsers) {
|
||||||
v.push_back(value.second.id);
|
v.push_back(value.second.id);
|
||||||
@@ -58,8 +57,7 @@ std::vector<AccountUid> Account::ids(void)
|
|||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
static User getUser(AccountUid id)
|
static User getUser(AccountUid id) {
|
||||||
{
|
|
||||||
User user{id, ""};
|
User user{id, ""};
|
||||||
AccountProfile profile;
|
AccountProfile profile;
|
||||||
AccountProfileBase profilebase;
|
AccountProfileBase profilebase;
|
||||||
@@ -74,8 +72,7 @@ static User getUser(AccountUid id)
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Account::username(AccountUid id)
|
std::string Account::username(AccountUid id) {
|
||||||
{
|
|
||||||
std::map<AccountUid, User>::const_iterator got = mUsers.find(id);
|
std::map<AccountUid, User>::const_iterator got = mUsers.find(id);
|
||||||
if (got == mUsers.end()) {
|
if (got == mUsers.end()) {
|
||||||
User user = getUser(id);
|
User user = getUser(id);
|
||||||
@@ -86,21 +83,21 @@ std::string Account::username(AccountUid id)
|
|||||||
return got->second.name;
|
return got->second.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Account::iconPath(AccountUid id)
|
std::string Account::iconPath(AccountUid id) {
|
||||||
{
|
|
||||||
char path[128];
|
char path[128];
|
||||||
snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg",
|
snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg", id.uid[0], id.uid[1]);
|
||||||
id.uid[0], id.uid[1]);
|
|
||||||
|
|
||||||
struct stat st;
|
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", 0755);
|
||||||
mkdir("sdmc:/switch/NXST", 0755);
|
mkdir("sdmc:/switch/NXST", 0755);
|
||||||
mkdir("sdmc:/switch/NXST/cache", 0755);
|
mkdir("sdmc:/switch/NXST/cache", 0755);
|
||||||
|
|
||||||
AccountProfile profile;
|
AccountProfile profile;
|
||||||
if (R_FAILED(accountGetProfile(&profile, id))) return "";
|
if (R_FAILED(accountGetProfile(&profile, id)))
|
||||||
|
return "";
|
||||||
|
|
||||||
u32 imgSize = 0;
|
u32 imgSize = 0;
|
||||||
if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) {
|
if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) {
|
||||||
@@ -112,26 +109,28 @@ std::string Account::iconPath(AccountUid id)
|
|||||||
u32 outSize = 0;
|
u32 outSize = 0;
|
||||||
Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize);
|
Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize);
|
||||||
accountProfileClose(&profile);
|
accountProfileClose(&profile);
|
||||||
if (R_FAILED(r) || outSize == 0) return "";
|
if (R_FAILED(r) || outSize == 0)
|
||||||
|
return "";
|
||||||
|
|
||||||
FILE* f = fopen(path, "wb");
|
FILE* f = fopen(path, "wb");
|
||||||
if (!f) return "";
|
if (!f)
|
||||||
|
return "";
|
||||||
fwrite(buf.data(), 1, outSize, f);
|
fwrite(buf.data(), 1, outSize, f);
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return std::string(path);
|
return std::string(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountUid Account::selectAccount(void)
|
AccountUid Account::selectAccount(void) {
|
||||||
{
|
|
||||||
LibAppletArgs args;
|
LibAppletArgs args;
|
||||||
libappletArgsCreate(&args, 0x10000);
|
libappletArgsCreate(&args, 0x10000);
|
||||||
u8 st_in[0xA0] = {0};
|
u8 st_in[0xA0] = {0};
|
||||||
u8 st_out[0x18] = {0};
|
u8 st_out[0x18] = {0};
|
||||||
size_t repsz;
|
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)) {
|
if (R_SUCCEEDED(res)) {
|
||||||
u64 lres = *(u64*)st_out;
|
u64 lres = *(u64*)st_out;
|
||||||
AccountUid uid = *(AccountUid*)&st_out[8];
|
AccountUid uid = *(AccountUid*)&st_out[8];
|
||||||
if (lres == 0)
|
if (lres == 0)
|
||||||
return uid;
|
return uid;
|
||||||
|
|||||||
+25
-28
@@ -26,8 +26,7 @@
|
|||||||
|
|
||||||
#include <nxst/domain/common.hpp>
|
#include <nxst/domain/common.hpp>
|
||||||
|
|
||||||
std::string DateTime::timeStr(void)
|
std::string DateTime::timeStr(void) {
|
||||||
{
|
|
||||||
time_t unixTime;
|
time_t unixTime;
|
||||||
struct tm timeStruct;
|
struct tm timeStruct;
|
||||||
time(&unixTime);
|
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);
|
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;
|
time_t unixTime;
|
||||||
struct tm timeStruct;
|
struct tm timeStruct;
|
||||||
time(&unixTime);
|
time(&unixTime);
|
||||||
localtime_r(&unixTime, &timeStruct);
|
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,
|
return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1,
|
||||||
timeStruct.tm_min, timeStruct.tm_sec);
|
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;
|
time_t unixTime;
|
||||||
struct tm timeStruct;
|
struct tm timeStruct;
|
||||||
time(&unixTime);
|
time(&unixTime);
|
||||||
localtime_r(&unixTime, &timeStruct);
|
localtime_r(&unixTime, &timeStruct);
|
||||||
return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday,
|
return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900,
|
||||||
timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
|
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;
|
static std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
|
||||||
std::string dst = convert.to_bytes(src);
|
std::string dst = convert.to_bytes(src);
|
||||||
return dst;
|
return dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string StringUtils::removeForbiddenCharacters(std::string src)
|
std::string StringUtils::removeForbiddenCharacters(std::string src) {
|
||||||
{
|
|
||||||
static const std::string illegalChars = ".,!\\/:?*\"<>|";
|
static const std::string illegalChars = ".,!\\/:?*\"<>|";
|
||||||
for (size_t i = 0, sz = src.length(); i < sz; i++) {
|
for (size_t i = 0, sz = src.length(); i < sz; i++) {
|
||||||
if (illegalChars.find(src[i]) != std::string::npos) {
|
if (illegalChars.find(src[i]) != std::string::npos) {
|
||||||
@@ -79,8 +75,7 @@ std::string StringUtils::removeForbiddenCharacters(std::string src)
|
|||||||
return src;
|
return src;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string StringUtils::format(const std::string fmt_str, ...)
|
std::string StringUtils::format(const std::string fmt_str, ...) {
|
||||||
{
|
|
||||||
va_list ap;
|
va_list ap;
|
||||||
char* fp = NULL;
|
char* fp = NULL;
|
||||||
va_start(ap, fmt_str);
|
va_start(ap, fmt_str);
|
||||||
@@ -90,8 +85,7 @@ std::string StringUtils::format(const std::string fmt_str, ...)
|
|||||||
return std::string(formatted.get());
|
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++) {
|
for (size_t i = 0, sz = str.length(); i < sz; i++) {
|
||||||
if (!isascii(str[i])) {
|
if (!isascii(str[i])) {
|
||||||
return true;
|
return true;
|
||||||
@@ -100,24 +94,27 @@ bool StringUtils::containsInvalidChar(const std::string& str)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StringUtils::ltrim(std::string& s)
|
void StringUtils::ltrim(std::string& s) {
|
||||||
{
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) {
|
||||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void StringUtils::rtrim(std::string& s)
|
void StringUtils::rtrim(std::string& s) {
|
||||||
{
|
s.erase(std::find_if(s.rbegin(), s.rend(),
|
||||||
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
|
[](int ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
})
|
||||||
|
.base(),
|
||||||
|
s.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
void StringUtils::trim(std::string& s)
|
void StringUtils::trim(std::string& s) {
|
||||||
{
|
|
||||||
ltrim(s);
|
ltrim(s);
|
||||||
rtrim(s);
|
rtrim(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
char* getConsoleIP(void)
|
char* getConsoleIP(void) {
|
||||||
{
|
|
||||||
struct in_addr in;
|
struct in_addr in;
|
||||||
in.s_addr = gethostid();
|
in.s_addr = gethostid();
|
||||||
return inet_ntoa(in);
|
return inet_ntoa(in);
|
||||||
|
|||||||
+89
-112
@@ -24,40 +24,39 @@
|
|||||||
* reasonable ways as different from the original version.
|
* reasonable ways as different from the original version.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <nxst/domain/title.hpp>
|
|
||||||
#include <nxst/app/main.hpp>
|
#include <nxst/app/main.hpp>
|
||||||
|
#include <nxst/domain/title.hpp>
|
||||||
|
|
||||||
static std::unordered_map<AccountUid, std::vector<Title>> titles;
|
static std::unordered_map<AccountUid, std::vector<Title>> titles;
|
||||||
static bool s_titlesLoaded = false;
|
static bool s_titlesLoaded = false;
|
||||||
|
|
||||||
bool areTitlesLoaded(void)
|
bool areTitlesLoaded(void) {
|
||||||
{
|
|
||||||
return s_titlesLoaded;
|
return s_titlesLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name, const std::string& author)
|
void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name,
|
||||||
{
|
const std::string& author) {
|
||||||
mId = id;
|
mId = id;
|
||||||
mUserId = userID;
|
mUserId = userID;
|
||||||
mSaveDataType = saveDataType;
|
mSaveDataType = saveDataType;
|
||||||
mUserName = Account::username(userID);
|
mUserName = Account::username(userID);
|
||||||
mAuthor = author;
|
mAuthor = author;
|
||||||
mName = name;
|
mName = name;
|
||||||
mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId) : StringUtils::removeForbiddenCharacters(name);
|
mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId)
|
||||||
mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName;
|
: StringUtils::removeForbiddenCharacters(name);
|
||||||
|
mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName;
|
||||||
|
|
||||||
std::string aname = StringUtils::removeAccents(mName);
|
std::string aname = StringUtils::removeAccents(mName);
|
||||||
size_t pos = aname.rfind(":");
|
size_t pos = aname.rfind(":");
|
||||||
mDisplayName = std::make_pair(aname, "");
|
mDisplayName = std::make_pair(aname, "");
|
||||||
if (pos != std::string::npos) {
|
if (pos != std::string::npos) {
|
||||||
std::string name1 = aname.substr(0, pos);
|
std::string name1 = aname.substr(0, pos);
|
||||||
std::string name2 = aname.substr(pos + 1);
|
std::string name2 = aname.substr(pos + 1);
|
||||||
StringUtils::trim(name1);
|
StringUtils::trim(name1);
|
||||||
StringUtils::trim(name2);
|
StringUtils::trim(name2);
|
||||||
mDisplayName.first = name1;
|
mDisplayName.first = name1;
|
||||||
mDisplayName.second = name2;
|
mDisplayName.second = name2;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// check for parenthesis
|
// check for parenthesis
|
||||||
size_t pos1 = aname.rfind("(");
|
size_t pos1 = aname.rfind("(");
|
||||||
size_t pos2 = 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);
|
std::string name2 = aname.substr(pos1 + 1, pos2 - 1 - pos1);
|
||||||
StringUtils::trim(name1);
|
StringUtils::trim(name1);
|
||||||
StringUtils::trim(name2);
|
StringUtils::trim(name2);
|
||||||
mDisplayName.first = name1;
|
mDisplayName.first = name1;
|
||||||
mDisplayName.second = name2;
|
mDisplayName.second = name2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,94 +73,77 @@ void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string&
|
|||||||
refreshDirectories();
|
refreshDirectories();
|
||||||
}
|
}
|
||||||
|
|
||||||
u8 Title::saveDataType(void)
|
u8 Title::saveDataType(void) {
|
||||||
{
|
|
||||||
return mSaveDataType;
|
return mSaveDataType;
|
||||||
}
|
}
|
||||||
|
|
||||||
u64 Title::id(void)
|
u64 Title::id(void) {
|
||||||
{
|
|
||||||
return mId;
|
return mId;
|
||||||
}
|
}
|
||||||
|
|
||||||
u64 Title::saveId(void)
|
u64 Title::saveId(void) {
|
||||||
{
|
|
||||||
return mSaveId;
|
return mSaveId;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Title::saveId(u64 saveId)
|
void Title::saveId(u64 saveId) {
|
||||||
{
|
|
||||||
mSaveId = saveId;
|
mSaveId = saveId;
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountUid Title::userId(void)
|
AccountUid Title::userId(void) {
|
||||||
{
|
|
||||||
return mUserId;
|
return mUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Title::userName(void)
|
std::string Title::userName(void) {
|
||||||
{
|
|
||||||
return mUserName;
|
return mUserName;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Title::author(void)
|
std::string Title::author(void) {
|
||||||
{
|
|
||||||
return mAuthor;
|
return mAuthor;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Title::name(void)
|
std::string Title::name(void) {
|
||||||
{
|
|
||||||
return mName;
|
return mName;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::pair<std::string, std::string> Title::displayName(void)
|
std::pair<std::string, std::string> Title::displayName(void) {
|
||||||
{
|
|
||||||
return mDisplayName;
|
return mDisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Title::path(void)
|
std::string Title::path(void) {
|
||||||
{
|
|
||||||
return mPath;
|
return mPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Title::fullPath(size_t index)
|
std::string Title::fullPath(size_t index) {
|
||||||
{
|
|
||||||
return mFullSavePaths.at(index);
|
return mFullSavePaths.at(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> Title::saves()
|
std::vector<std::string> Title::saves() {
|
||||||
{
|
|
||||||
return mSaves;
|
return mSaves;
|
||||||
}
|
}
|
||||||
|
|
||||||
u64 Title::playTimeNanoseconds(void)
|
u64 Title::playTimeNanoseconds(void) {
|
||||||
{
|
|
||||||
return mPlayTimeNanoseconds;
|
return mPlayTimeNanoseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Title::playTime(void)
|
std::string Title::playTime(void) {
|
||||||
{
|
|
||||||
const u64 playTimeMinutes = mPlayTimeNanoseconds / 60000000000;
|
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;
|
mPlayTimeNanoseconds = playTimeNanoseconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 Title::lastPlayedTimestamp(void)
|
u32 Title::lastPlayedTimestamp(void) {
|
||||||
{
|
|
||||||
return mLastPlayedTimestamp;
|
return mLastPlayedTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp)
|
void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp) {
|
||||||
{
|
|
||||||
mLastPlayedTimestamp = lastPlayedTimestamp;
|
mLastPlayedTimestamp = lastPlayedTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Title::refreshDirectories(void)
|
void Title::refreshDirectories(void) {
|
||||||
{
|
|
||||||
mSaves.clear();
|
mSaves.clear();
|
||||||
mFullSavePaths.clear();
|
mFullSavePaths.clear();
|
||||||
|
|
||||||
@@ -178,15 +160,15 @@ void Title::refreshDirectories(void)
|
|||||||
std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend());
|
std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend());
|
||||||
mSaves.insert(mSaves.begin(), g_emptySave);
|
mSaves.insert(mSaves.begin(), g_emptySave);
|
||||||
mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave);
|
mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave);
|
||||||
}
|
} else {
|
||||||
else {
|
Logger::getInstance().log(Logger::ERROR,
|
||||||
Logger::getInstance().log(Logger::ERROR, "Couldn't retrieve the extdata directory list for the title " + name());
|
"Couldn't retrieve the extdata directory list for the title " + name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadTitles(void)
|
void loadTitles(void) {
|
||||||
{
|
if (s_titlesLoaded)
|
||||||
if (s_titlesLoaded) return;
|
return;
|
||||||
s_titlesLoaded = true;
|
s_titlesLoaded = true;
|
||||||
|
|
||||||
titles.clear();
|
titles.clear();
|
||||||
@@ -194,9 +176,9 @@ void loadTitles(void)
|
|||||||
FsSaveDataInfoReader reader;
|
FsSaveDataInfoReader reader;
|
||||||
FsSaveDataInfo info;
|
FsSaveDataInfo info;
|
||||||
s64 total_entries = 0;
|
s64 total_entries = 0;
|
||||||
size_t outsize = 0;
|
size_t outsize = 0;
|
||||||
|
|
||||||
NacpLanguageEntry* nle = NULL;
|
NacpLanguageEntry* nle = NULL;
|
||||||
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
|
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
|
||||||
if (nsacd == NULL) {
|
if (nsacd == NULL) {
|
||||||
return;
|
return;
|
||||||
@@ -216,43 +198,44 @@ void loadTitles(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (info.save_data_type == FsSaveDataType_Account) {
|
if (info.save_data_type == FsSaveDataType_Account) {
|
||||||
u64 tid = info.application_id;
|
u64 tid = info.application_id;
|
||||||
u64 sid = info.save_data_id;
|
u64 sid = info.save_data_id;
|
||||||
AccountUid uid = info.uid;
|
AccountUid uid = info.uid;
|
||||||
// if (mFilterIds.find(tid) == mFilterIds.end()) {
|
// if (mFilterIds.find(tid) == mFilterIds.end()) {
|
||||||
res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd, sizeof(NsApplicationControlData), &outsize);
|
res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd,
|
||||||
if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) {
|
sizeof(NsApplicationControlData), &outsize);
|
||||||
res = nacpGetLanguageEntry(&nsacd->nacp, &nle);
|
if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) {
|
||||||
if (R_SUCCEEDED(res) && nle != NULL) {
|
res = nacpGetLanguageEntry(&nsacd->nacp, &nle);
|
||||||
Title title;
|
if (R_SUCCEEDED(res) && nle != NULL) {
|
||||||
title.init(info.save_data_type, tid, uid, std::string(nle->name), std::string(nle->author));
|
Title title;
|
||||||
title.saveId(sid);
|
title.init(info.save_data_type, tid, uid, std::string(nle->name),
|
||||||
|
std::string(nle->author));
|
||||||
|
title.saveId(sid);
|
||||||
|
|
||||||
// load play statistics
|
// load play statistics
|
||||||
PdmPlayStatistics stats;
|
PdmPlayStatistics stats;
|
||||||
res = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats);
|
res = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats);
|
||||||
if (R_SUCCEEDED(res)) {
|
if (R_SUCCEEDED(res)) {
|
||||||
title.playTimeNanoseconds(stats.playtime);
|
title.playTimeNanoseconds(stats.playtime);
|
||||||
title.lastPlayedTimestamp(stats.last_timestamp_user);
|
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
|
// check if the vector is already created
|
||||||
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
|
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
|
||||||
if (it != titles.end()) {
|
if (it != titles.end()) {
|
||||||
// found
|
// found
|
||||||
it->second.push_back(title);
|
it->second.push_back(title);
|
||||||
}
|
} else {
|
||||||
else {
|
// not found, insert into map
|
||||||
// not found, insert into map
|
std::vector<Title> v;
|
||||||
std::vector<Title> v;
|
v.push_back(title);
|
||||||
v.push_back(title);
|
titles.emplace(uid, v);
|
||||||
titles.emplace(uid, v);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nle = NULL;
|
}
|
||||||
|
nle = NULL;
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,45 +246,40 @@ void loadTitles(void)
|
|||||||
sortTitles();
|
sortTitles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void sortTitles(void)
|
void sortTitles(void) {
|
||||||
{
|
|
||||||
for (auto& vect : titles) {
|
for (auto& vect : titles) {
|
||||||
std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) {
|
std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) {
|
||||||
switch (g_sortMode) {
|
switch (g_sortMode) {
|
||||||
case SORT_LAST_PLAYED:
|
case SORT_LAST_PLAYED:
|
||||||
return l.lastPlayedTimestamp() > r.lastPlayedTimestamp();
|
return l.lastPlayedTimestamp() > r.lastPlayedTimestamp();
|
||||||
case SORT_PLAY_TIME:
|
case SORT_PLAY_TIME:
|
||||||
return l.playTimeNanoseconds() > r.playTimeNanoseconds();
|
return l.playTimeNanoseconds() > r.playTimeNanoseconds();
|
||||||
case SORT_ALPHA:
|
case SORT_ALPHA:
|
||||||
default:
|
default:
|
||||||
return l.name() < r.name();
|
return l.name() < r.name();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void rotateSortMode(void)
|
void rotateSortMode(void) {
|
||||||
{
|
|
||||||
g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT);
|
g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT);
|
||||||
sortTitles();
|
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);
|
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
|
||||||
if (it != titles.end() && i < getTitleCount(uid)) {
|
if (it != titles.end() && i < getTitleCount(uid)) {
|
||||||
dst = it->second.at(i);
|
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);
|
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
|
||||||
return it != titles.end() ? it->second.size() : 0;
|
return it != titles.end() ? it->second.size() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void refreshDirectories(u64 id)
|
void refreshDirectories(u64 id) {
|
||||||
{
|
|
||||||
for (auto& pair : titles) {
|
for (auto& pair : titles) {
|
||||||
for (size_t i = 0; i < pair.second.size(); i++) {
|
for (size_t i = 0; i < pair.second.size(); i++) {
|
||||||
if (pair.second.at(i).id() == id) {
|
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;
|
std::unordered_map<std::string, std::string> map;
|
||||||
for (const auto& pair : titles) {
|
for (const auto& pair : titles) {
|
||||||
for (auto value : pair.second) {
|
for (auto value : pair.second) {
|
||||||
|
|||||||
+30
-36
@@ -24,21 +24,19 @@
|
|||||||
* reasonable ways as different from the original version.
|
* 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/domain/util.hpp>
|
||||||
#include <nxst/infra/sys/logger.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();
|
Logger::getInstance().flush();
|
||||||
Account::exit();
|
Account::exit();
|
||||||
plExit();
|
plExit();
|
||||||
romfsExit();
|
romfsExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result servicesInit(void)
|
Result servicesInit(void) {
|
||||||
{
|
|
||||||
io::createDirectory("sdmc:/switch");
|
io::createDirectory("sdmc:/switch");
|
||||||
io::createDirectory("sdmc:/switch/NXST");
|
io::createDirectory("sdmc:/switch/NXST");
|
||||||
io::createDirectory("sdmc:/switch/NXST/saves");
|
io::createDirectory("sdmc:/switch/NXST/saves");
|
||||||
@@ -71,30 +69,28 @@ Result servicesInit(void)
|
|||||||
|
|
||||||
if (R_SUCCEEDED(res = hidsysInitialize())) {
|
if (R_SUCCEEDED(res = hidsysInitialize())) {
|
||||||
g_notificationLedAvailable = true;
|
g_notificationLedAvailable = true;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res);
|
Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Logger::getInstance().log(Logger::INFO, "NXST loading completed!");
|
Logger::getInstance().log(Logger::INFO, "NXST loading completed!");
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::u16string StringUtils::UTF8toUTF16(const char* src)
|
std::u16string StringUtils::UTF8toUTF16(const char* src) {
|
||||||
{
|
|
||||||
char16_t tmp[256] = {0};
|
char16_t tmp[256] = {0};
|
||||||
utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256);
|
utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256);
|
||||||
return std::u16string(tmp);
|
return std::u16string(tmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/14094621/change-all-accented-letters-to-normal-letters-in-c
|
// 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());
|
std::u16string src = UTF8toUTF16(str.c_str());
|
||||||
const std::u16string illegal = UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ");
|
const std::u16string illegal =
|
||||||
const std::u16string fixed = UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy");
|
UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ");
|
||||||
|
const std::u16string fixed =
|
||||||
|
UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy");
|
||||||
|
|
||||||
for (size_t i = 0, sz = src.length(); i < sz; i++) {
|
for (size_t i = 0, sz = src.length(); i < sz; i++) {
|
||||||
size_t index = illegal.find(src[i]);
|
size_t index = illegal.find(src[i]);
|
||||||
@@ -106,8 +102,7 @@ std::string StringUtils::removeAccents(std::string str)
|
|||||||
return UTF16toUTF8(src);
|
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++) {
|
for (size_t i = 0, sz = str.length(); i < sz; i++) {
|
||||||
if (!isascii(str[i])) {
|
if (!isascii(str[i])) {
|
||||||
str[i] = ' ';
|
str[i] = ' ';
|
||||||
@@ -116,9 +111,9 @@ std::string StringUtils::removeNotAscii(std::string str)
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string StringUtils::elide(const std::string& s, size_t maxChars)
|
std::string StringUtils::elide(const std::string& s, size_t maxChars) {
|
||||||
{
|
if (s.size() <= maxChars || maxChars < 6)
|
||||||
if (s.size() <= maxChars || maxChars < 6) return s;
|
return s;
|
||||||
constexpr const char* dots = "...";
|
constexpr const char* dots = "...";
|
||||||
size_t budget = maxChars - 3;
|
size_t budget = maxChars - 3;
|
||||||
size_t head = (budget + 1) / 2;
|
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);
|
return s.substr(0, head) + dots + s.substr(s.size() - tail);
|
||||||
}
|
}
|
||||||
|
|
||||||
HidsysNotificationLedPattern blinkLedPattern(u8 times)
|
HidsysNotificationLedPattern blinkLedPattern(u8 times) {
|
||||||
{
|
|
||||||
HidsysNotificationLedPattern pattern;
|
HidsysNotificationLedPattern pattern;
|
||||||
memset(&pattern, 0, sizeof(pattern));
|
memset(&pattern, 0, sizeof(pattern));
|
||||||
|
|
||||||
pattern.baseMiniCycleDuration = 0x1; // 12.5ms.
|
pattern.baseMiniCycleDuration = 0x1; // 12.5ms.
|
||||||
pattern.totalMiniCycles = 0x2; // 2 mini cycles.
|
pattern.totalMiniCycles = 0x2; // 2 mini cycles.
|
||||||
pattern.totalFullCycles = times; // Repeat n times.
|
pattern.totalFullCycles = times; // Repeat n times.
|
||||||
pattern.startIntensity = 0x0; // 0%.
|
pattern.startIntensity = 0x0; // 0%.
|
||||||
|
|
||||||
pattern.miniCycles[0].ledIntensity = 0xF; // 100%.
|
pattern.miniCycles[0].ledIntensity = 0xF; // 100%.
|
||||||
pattern.miniCycles[0].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
|
pattern.miniCycles[0].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
|
||||||
pattern.miniCycles[0].finalStepDuration = 0x0; // Forced 12.5ms.
|
pattern.miniCycles[0].finalStepDuration = 0x0; // Forced 12.5ms.
|
||||||
pattern.miniCycles[1].ledIntensity = 0x0; // 0%.
|
pattern.miniCycles[1].ledIntensity = 0x0; // 0%.
|
||||||
pattern.miniCycles[1].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
|
pattern.miniCycles[1].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
|
||||||
pattern.miniCycles[1].finalStepDuration = 0x0; // Forced 12.5ms.
|
pattern.miniCycles[1].finalStepDuration = 0x0; // Forced 12.5ms.
|
||||||
|
|
||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
void blinkLed(u8 times)
|
void blinkLed(u8 times) {
|
||||||
{
|
|
||||||
if (g_notificationLedAvailable) {
|
if (g_notificationLedAvailable) {
|
||||||
PadState pad;
|
PadState pad;
|
||||||
padInitializeDefault(&pad);
|
padInitializeDefault(&pad);
|
||||||
s32 n;
|
s32 n;
|
||||||
HidsysUniquePadId uniquePadIds[2] = {0};
|
HidsysUniquePadId uniquePadIds[2] = {0};
|
||||||
HidsysNotificationLedPattern pattern = blinkLedPattern(times);
|
HidsysNotificationLedPattern pattern = blinkLedPattern(times);
|
||||||
memset(uniquePadIds, 0, sizeof(uniquePadIds));
|
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)) {
|
if (R_SUCCEEDED(res)) {
|
||||||
for (s32 i = 0; i < n; i++) {
|
for (s32 i = 0; i < n; i++) {
|
||||||
hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]);
|
hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]);
|
||||||
|
|||||||
+10
-17
@@ -26,9 +26,8 @@
|
|||||||
|
|
||||||
#include <nxst/infra/fs/directory.hpp>
|
#include <nxst/infra/fs/directory.hpp>
|
||||||
|
|
||||||
Directory::Directory(const std::string& root)
|
Directory::Directory(const std::string& root) {
|
||||||
{
|
mGood = false;
|
||||||
mGood = false;
|
|
||||||
mError = 0;
|
mError = 0;
|
||||||
mList.clear();
|
mList.clear();
|
||||||
|
|
||||||
@@ -37,11 +36,10 @@ Directory::Directory(const std::string& root)
|
|||||||
|
|
||||||
if (dir == NULL) {
|
if (dir == NULL) {
|
||||||
mError = (Result)errno;
|
mError = (Result)errno;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
while ((ent = readdir(dir))) {
|
while ((ent = readdir(dir))) {
|
||||||
std::string name = std::string(ent->d_name);
|
std::string name = std::string(ent->d_name);
|
||||||
bool directory = ent->d_type == DT_DIR;
|
bool directory = ent->d_type == DT_DIR;
|
||||||
struct DirectoryEntry de = {name, directory};
|
struct DirectoryEntry de = {name, directory};
|
||||||
mList.push_back(de);
|
mList.push_back(de);
|
||||||
}
|
}
|
||||||
@@ -50,27 +48,22 @@ Directory::Directory(const std::string& root)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Result Directory::error(void)
|
Result Directory::error(void) {
|
||||||
{
|
|
||||||
return mError;
|
return mError;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Directory::good(void)
|
bool Directory::good(void) {
|
||||||
{
|
|
||||||
return mGood;
|
return mGood;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Directory::entry(size_t index)
|
std::string Directory::entry(size_t index) {
|
||||||
{
|
|
||||||
return index < mList.size() ? mList.at(index).name : "";
|
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;
|
return index < mList.size() ? mList.at(index).directory : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t Directory::size(void)
|
size_t Directory::size(void) {
|
||||||
{
|
|
||||||
return mList.size();
|
return mList.size();
|
||||||
}
|
}
|
||||||
@@ -26,17 +26,14 @@
|
|||||||
|
|
||||||
#include <nxst/infra/fs/filesystem.hpp>
|
#include <nxst/infra/fs/filesystem.hpp>
|
||||||
|
|
||||||
Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID)
|
Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) {
|
||||||
{
|
|
||||||
return fsOpen_SaveData(fileSystem, titleID, userID);
|
return fsOpen_SaveData(fileSystem, titleID, userID);
|
||||||
}
|
}
|
||||||
|
|
||||||
int FileSystem::mount(FsFileSystem fs)
|
int FileSystem::mount(FsFileSystem fs) {
|
||||||
{
|
|
||||||
return fsdevMountDevice("save", fs);
|
return fsdevMountDevice("save", fs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystem::unmount(void)
|
void FileSystem::unmount(void) {
|
||||||
{
|
|
||||||
fsdevUnmountDevice("save");
|
fsdevUnmountDevice("save");
|
||||||
}
|
}
|
||||||
+34
-47
@@ -24,20 +24,19 @@
|
|||||||
* reasonable ways as different from the original version.
|
* 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>
|
#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;
|
struct stat buffer;
|
||||||
return (stat(path.c_str(), &buffer) == 0);
|
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;
|
g_isTransferringFile = true;
|
||||||
|
|
||||||
nxst::FileHandle src(fopen(srcPath.c_str(), "rb"));
|
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;
|
u64 offset = 0;
|
||||||
|
|
||||||
size_t slashpos = srcPath.rfind('/');
|
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) {
|
while (offset < sz) {
|
||||||
u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get());
|
u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get());
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.",
|
nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.",
|
||||||
srcPath.c_str(), (unsigned long long)offset,
|
srcPath.c_str(), (unsigned long long)offset, (unsigned long long)sz, errno);
|
||||||
(unsigned long long)sz, errno);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
fwrite(buf.data(), 1, count, dst.get());
|
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;
|
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;
|
Result res = 0;
|
||||||
bool quit = false;
|
bool quit = false;
|
||||||
Directory items(srcPath);
|
Directory items(srcPath);
|
||||||
|
|
||||||
if (!items.good()) {
|
if (!items.good()) {
|
||||||
@@ -104,12 +101,10 @@ Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
|
|||||||
newsrc += "/";
|
newsrc += "/";
|
||||||
newdst += "/";
|
newdst += "/";
|
||||||
res = io::copyDirectory(newsrc, newdst);
|
res = io::copyDirectory(newsrc, newdst);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
quit = true;
|
quit = true;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
io::copyFile(newsrc, newdst);
|
io::copyFile(newsrc, newdst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,20 +112,17 @@ Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result io::createDirectory(const std::string& path)
|
Result io::createDirectory(const std::string& path) {
|
||||||
{
|
|
||||||
mkdir(path.c_str(), 0777);
|
mkdir(path.c_str(), 0777);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool io::directoryExists(const std::string& path)
|
bool io::directoryExists(const std::string& path) {
|
||||||
{
|
|
||||||
struct stat sb;
|
struct stat sb;
|
||||||
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
|
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);
|
Directory dir(path);
|
||||||
if (!dir.good()) {
|
if (!dir.good()) {
|
||||||
return dir.error();
|
return dir.error();
|
||||||
@@ -142,8 +134,7 @@ Result io::deleteFolderRecursively(const std::string& path)
|
|||||||
deleteFolderRecursively(newpath);
|
deleteFolderRecursively(newpath);
|
||||||
newpath = path + dir.entry(i);
|
newpath = path + dir.entry(i);
|
||||||
rmdir(newpath.c_str());
|
rmdir(newpath.c_str());
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
std::string newpath = path + dir.entry(i);
|
std::string newpath = path + dir.entry(i);
|
||||||
std::remove(newpath.c_str());
|
std::remove(newpath.c_str());
|
||||||
}
|
}
|
||||||
@@ -153,14 +144,12 @@ Result io::deleteFolderRecursively(const std::string& path)
|
|||||||
return 0;
|
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;
|
Title title;
|
||||||
getTitle(title, uid, index);
|
getTitle(title, uid, index);
|
||||||
|
|
||||||
nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.",
|
nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(),
|
||||||
title.name().c_str(), title.id(),
|
title.id(), title.userId().uid[1], title.userId().uid[0]);
|
||||||
title.userId().uid[1], title.userId().uid[0]);
|
|
||||||
|
|
||||||
nxst::FsFileSystemHandle fsHandle;
|
nxst::FsFileSystemHandle fsHandle;
|
||||||
Result res = FileSystem::mount(fsHandle.get(), title.id(), title.userId());
|
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
|
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());
|
io::createDirectory(title.path());
|
||||||
std::string dst_path = title.path() + "/" + suggestion;
|
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.
|
// 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);
|
std::vector<u8> nsacd_buf(sizeof(NsApplicationControlData), 0);
|
||||||
auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nsacd_buf.data());
|
auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nsacd_buf.data());
|
||||||
|
|
||||||
size_t outsize = 0;
|
size_t outsize = 0;
|
||||||
if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage,
|
if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title_id, nsacd,
|
||||||
title_id, nsacd,
|
sizeof(NsApplicationControlData), &outsize))) {
|
||||||
sizeof(NsApplicationControlData), &outsize))) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,30 +219,29 @@ static void createSaveIfNeeded(u64 title_id, AccountUid uid)
|
|||||||
|
|
||||||
FsSaveDataAttribute attr = {};
|
FsSaveDataAttribute attr = {};
|
||||||
attr.application_id = title_id;
|
attr.application_id = title_id;
|
||||||
attr.uid = uid;
|
attr.uid = uid;
|
||||||
attr.save_data_type = FsSaveDataType_Account;
|
attr.save_data_type = FsSaveDataType_Account;
|
||||||
attr.save_data_rank = FsSaveDataRank_Primary;
|
attr.save_data_rank = FsSaveDataRank_Primary;
|
||||||
|
|
||||||
FsSaveDataCreationInfo create_info = {};
|
FsSaveDataCreationInfo create_info = {};
|
||||||
create_info.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
|
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.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
|
||||||
create_info.available_size = 0x4000;
|
create_info.available_size = 0x4000;
|
||||||
create_info.owner_id = nsacd->nacp.save_data_owner_id;
|
create_info.owner_id = nsacd->nacp.save_data_owner_id;
|
||||||
create_info.save_data_space_id = FsSaveDataSpaceId_User;
|
create_info.save_data_space_id = FsSaveDataSpaceId_User;
|
||||||
|
|
||||||
fsCreateSaveDataFileSystem(&attr, &create_info, &meta);
|
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;
|
(void)cellIndex;
|
||||||
|
|
||||||
Title title;
|
Title title;
|
||||||
getTitle(title, uid, index);
|
getTitle(title, uid, index);
|
||||||
|
|
||||||
nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.",
|
nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(),
|
||||||
title.name().c_str(), title.id(),
|
title.id(), title.userId().uid[1], title.userId().uid[0]);
|
||||||
title.userId().uid[1], title.userId().uid[0]);
|
|
||||||
|
|
||||||
createSaveIfNeeded(title.id(), uid);
|
createSaveIfNeeded(title.id(), uid);
|
||||||
|
|
||||||
|
|||||||
+42
-16
@@ -1,10 +1,10 @@
|
|||||||
#include <nxst/infra/sys/logger.hpp>
|
|
||||||
|
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <nxst/infra/sys/logger.hpp>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
std::mutex g_log_mutex;
|
std::mutex g_log_mutex;
|
||||||
@@ -15,8 +15,7 @@ constexpr const char* kLogPath = "/switch/NXST/log.log";
|
|||||||
constexpr const char* kLogPath = "nxst.log";
|
constexpr const char* kLogPath = "nxst.log";
|
||||||
#endif
|
#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];
|
char msg[2048];
|
||||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
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 {
|
namespace nxst::log {
|
||||||
|
|
||||||
void write(Level level, const char* fmt, ...)
|
void write(Level level, const char* fmt, ...) {
|
||||||
{
|
|
||||||
const char* tag = "[INFO] ";
|
const char* tag = "[INFO] ";
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case Level::Debug: tag = "[DEBUG]"; break;
|
case Level::Debug:
|
||||||
case Level::Info: tag = "[INFO] "; break;
|
tag = "[DEBUG]";
|
||||||
case Level::Warn: tag = "[WARN] "; break;
|
break;
|
||||||
case Level::Error: tag = "[ERROR]"; break;
|
case Level::Info:
|
||||||
|
tag = "[INFO] ";
|
||||||
|
break;
|
||||||
|
case Level::Warn:
|
||||||
|
tag = "[WARN] ";
|
||||||
|
break;
|
||||||
|
case Level::Error:
|
||||||
|
tag = "[ERROR]";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, fmt);
|
va_start(args, fmt);
|
||||||
@@ -56,9 +62,29 @@ void write(Level level, const char* fmt, ...)
|
|||||||
va_end(args);
|
va_end(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
void debug(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[DEBUG]", fmt, args); va_end(args); }
|
void debug(const char* fmt, ...) {
|
||||||
void info (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[INFO] ", fmt, args); va_end(args); }
|
va_list args;
|
||||||
void warn (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[WARN] ", fmt, args); va_end(args); }
|
va_start(args, fmt);
|
||||||
void error(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[ERROR]", fmt, args); va_end(args); }
|
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
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#include <nxst/service/transfer_service.hpp>
|
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -12,11 +10,14 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <nxst/service/transfer_service.hpp>
|
||||||
|
|
||||||
#ifdef __SWITCH__
|
#ifdef __SWITCH__
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
#include <nxst/infra/fs/io.hpp>
|
|
||||||
#include <nxst/domain/util.hpp>
|
|
||||||
#include <nxst/domain/account.hpp>
|
#include <nxst/domain/account.hpp>
|
||||||
|
#include <nxst/domain/util.hpp>
|
||||||
|
#include <nxst/infra/fs/io.hpp>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <nxst/domain/protocol.hpp>
|
#include <nxst/domain/protocol.hpp>
|
||||||
@@ -32,7 +33,8 @@ static bool sendAll(int sock, const void* buf, size_t len) {
|
|||||||
size_t sent = 0;
|
size_t sent = 0;
|
||||||
while (sent < len) {
|
while (sent < len) {
|
||||||
ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0);
|
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;
|
sent += n;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -42,7 +44,8 @@ static bool recvAll(int sock, void* buf, size_t len) {
|
|||||||
size_t got = 0;
|
size_t got = 0;
|
||||||
while (got < len) {
|
while (got < len) {
|
||||||
ssize_t n = read(sock, static_cast<char*>(buf) + got, len - got);
|
ssize_t n = read(sock, static_cast<char*>(buf) + got, len - got);
|
||||||
if (n <= 0) return false;
|
if (n <= 0)
|
||||||
|
return false;
|
||||||
got += n;
|
got += n;
|
||||||
}
|
}
|
||||||
return true;
|
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) {
|
static bool sendFile(int sock, const fs::path& filepath, TransferState& state) {
|
||||||
std::ifstream infile(filepath, std::ios::binary | std::ios::ate);
|
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();
|
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);
|
infile.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
if (!sendAll(sock, &filename_len, sizeof(filename_len))) return false;
|
if (!sendAll(sock, &filename_len, sizeof(filename_len)))
|
||||||
if (!sendAll(sock, filepath.c_str(), filename_len)) return false;
|
return false;
|
||||||
if (!sendAll(sock, &file_size, sizeof(file_size))) 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);
|
std::vector<char> buffer(proto::BUF_SIZE);
|
||||||
uint64_t remaining = file_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);
|
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
|
||||||
infile.read(buffer.data(), (std::streamsize)to_read);
|
infile.read(buffer.data(), (std::streamsize)to_read);
|
||||||
std::streamsize count = infile.gcount();
|
std::streamsize count = infile.gcount();
|
||||||
if (count <= 0) break;
|
if (count <= 0)
|
||||||
if (!sendAll(sock, buffer.data(), (size_t)count)) return false;
|
break;
|
||||||
|
if (!sendAll(sock, buffer.data(), (size_t)count))
|
||||||
|
return false;
|
||||||
state.bytes_done.fetch_add((uint64_t)count);
|
state.bytes_done.fetch_add((uint64_t)count);
|
||||||
remaining -= (uint64_t)count;
|
remaining -= (uint64_t)count;
|
||||||
}
|
}
|
||||||
@@ -84,12 +93,12 @@ static void mkdirs(const std::string& path) {
|
|||||||
mkdir(path.c_str(), 0777);
|
mkdir(path.c_str(), 0777);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size,
|
static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size, TransferState& state) {
|
||||||
TransferState& state) {
|
|
||||||
size_t last_slash = rel_path.rfind('/');
|
size_t last_slash = rel_path.rfind('/');
|
||||||
if (last_slash != std::string::npos) {
|
if (last_slash != std::string::npos) {
|
||||||
std::string dir = rel_path.substr(0, last_slash);
|
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");
|
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) {
|
while (remaining > 0) {
|
||||||
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
|
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
|
||||||
ssize_t n = read(sock, drain.data(), to_read);
|
ssize_t n = read(sock, drain.data(), to_read);
|
||||||
if (n <= 0) break;
|
if (n <= 0)
|
||||||
|
break;
|
||||||
remaining -= (uint64_t)n;
|
remaining -= (uint64_t)n;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -113,7 +123,8 @@ static void receiveFile(int sock, const std::string& rel_path, uint64_t file_siz
|
|||||||
while (total < file_size) {
|
while (total < file_size) {
|
||||||
size_t to_read = (size_t)std::min(file_size - total, (uint64_t)proto::BUF_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);
|
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);
|
fwrite(buffer.data(), 1, (size_t)n, outfile);
|
||||||
total += (uint64_t)n;
|
total += (uint64_t)n;
|
||||||
state.bytes_done.store(total);
|
state.bytes_done.store(total);
|
||||||
@@ -131,17 +142,19 @@ void TransferService::failSend(const std::string& reason) {
|
|||||||
|
|
||||||
int TransferService::findServer(char* out_ip) {
|
int TransferService::findServer(char* out_ip) {
|
||||||
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
|
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);
|
sender_udp_sock.store(udp_fd);
|
||||||
|
|
||||||
auto releaseUdp = [&]() {
|
auto releaseUdp = [&]() {
|
||||||
int owned = sender_udp_sock.exchange(-1);
|
int owned = sender_udp_sock.exchange(-1);
|
||||||
if (owned == udp_fd) close(udp_fd);
|
if (owned == udp_fd)
|
||||||
|
close(udp_fd);
|
||||||
};
|
};
|
||||||
|
|
||||||
sockaddr_in addr{};
|
sockaddr_in addr{};
|
||||||
addr.sin_family = AF_INET;
|
addr.sin_family = AF_INET;
|
||||||
addr.sin_port = htons(proto::MULTICAST_PORT);
|
addr.sin_port = htons(proto::MULTICAST_PORT);
|
||||||
addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP);
|
addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP);
|
||||||
|
|
||||||
if (sendto(udp_fd, "DISCOVER_SERVER", 15, 0, (sockaddr*)&addr, sizeof(addr)) < 0) {
|
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
|
// Poll in 100ms slices so cancel races within 100ms
|
||||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
||||||
while (std::chrono::steady_clock::now() < deadline) {
|
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};
|
struct timeval tv{0, 100000};
|
||||||
fd_set fds;
|
fd_set fds;
|
||||||
FD_ZERO(&fds);
|
FD_ZERO(&fds);
|
||||||
FD_SET(udp_fd, &fds);
|
FD_SET(udp_fd, &fds);
|
||||||
if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) {
|
if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) {
|
||||||
sockaddr_in from{};
|
sockaddr_in from{};
|
||||||
socklen_t fromlen = sizeof(from);
|
socklen_t fromlen = sizeof(from);
|
||||||
char buf[256];
|
char buf[256];
|
||||||
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
|
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
buf[n] = '\0';
|
buf[n] = '\0';
|
||||||
if (strcmp(buf, "SERVER_HERE") == 0) {
|
if (strcmp(buf, "SERVER_HERE") == 0) {
|
||||||
@@ -179,8 +195,8 @@ int TransferService::findServer(char* out_ip) {
|
|||||||
void* TransferService::senderEntry(void* arg) {
|
void* TransferService::senderEntry(void* arg) {
|
||||||
auto* a = static_cast<SenderArgs*>(arg);
|
auto* a = static_cast<SenderArgs*>(arg);
|
||||||
TransferService* svc = a->svc;
|
TransferService* svc = a->svc;
|
||||||
size_t idx = a->title_index;
|
size_t idx = a->title_index;
|
||||||
AccountUid uid = a->uid;
|
AccountUid uid = a->uid;
|
||||||
delete a;
|
delete a;
|
||||||
svc->runSender(idx, uid);
|
svc->runSender(idx, uid);
|
||||||
return nullptr;
|
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.");
|
failSend("No receiver found.\nMake sure the other Switch is in Receive mode.");
|
||||||
return finish();
|
return finish();
|
||||||
}
|
}
|
||||||
if (sender_state.cancelled.load()) return finish();
|
if (sender_state.cancelled.load())
|
||||||
|
return finish();
|
||||||
|
|
||||||
sender_state.setStatus("Creating backup...");
|
sender_state.setStatus("Creating backup...");
|
||||||
#ifdef __SWITCH__
|
#ifdef __SWITCH__
|
||||||
@@ -212,24 +229,30 @@ void TransferService::runSender(size_t title_index, AccountUid uid) {
|
|||||||
fs::path directory = backup_result.value();
|
fs::path directory = backup_result.value();
|
||||||
#else
|
#else
|
||||||
fs::path directory = ".";
|
fs::path directory = ".";
|
||||||
(void)title_index; (void)uid;
|
(void)title_index;
|
||||||
|
(void)uid;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (sender_state.cancelled.load()) return finish();
|
if (sender_state.cancelled.load())
|
||||||
|
return finish();
|
||||||
|
|
||||||
sender_state.setStatus("Connecting...");
|
sender_state.setStatus("Connecting...");
|
||||||
int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
|
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);
|
sender_tcp_sock.store(tcp_fd);
|
||||||
|
|
||||||
auto releaseTcp = [&]() {
|
auto releaseTcp = [&]() {
|
||||||
int owned = sender_tcp_sock.exchange(-1);
|
int owned = sender_tcp_sock.exchange(-1);
|
||||||
if (owned == tcp_fd) close(tcp_fd);
|
if (owned == tcp_fd)
|
||||||
|
close(tcp_fd);
|
||||||
};
|
};
|
||||||
|
|
||||||
sockaddr_in serv{};
|
sockaddr_in serv{};
|
||||||
serv.sin_family = AF_INET;
|
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 ||
|
if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0 ||
|
||||||
connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) {
|
connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) {
|
||||||
if (!sender_state.cancelled.load())
|
if (!sender_state.cancelled.load())
|
||||||
@@ -245,11 +268,13 @@ void TransferService::runSender(size_t title_index, AccountUid uid) {
|
|||||||
sender_state.bytes_total.store(total);
|
sender_state.bytes_total.store(total);
|
||||||
|
|
||||||
for (const auto& entry : fs::recursive_directory_iterator(directory)) {
|
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();
|
const fs::path& p = entry.path();
|
||||||
if (fs::is_regular_file(p)) {
|
if (fs::is_regular_file(p)) {
|
||||||
sender_state.setStatus(p.filename().string());
|
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() {
|
void TransferService::cancelSend() {
|
||||||
sender_state.cancelled.store(true);
|
sender_state.cancelled.store(true);
|
||||||
int udp = sender_udp_sock.exchange(-1);
|
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);
|
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 ────────────────────────────────────────────────────────────────
|
// ─── Receiver ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
std::string TransferService::replaceUsername(const std::string& file_path) const {
|
std::string TransferService::replaceUsername(const std::string& file_path) const {
|
||||||
#ifdef __SWITCH__
|
#ifdef __SWITCH__
|
||||||
std::string username = StringUtils::removeNotAscii(
|
std::string username =
|
||||||
StringUtils::removeAccents(Account::username(restore_uid)));
|
StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(restore_uid)));
|
||||||
size_t last_slash = file_path.rfind('/');
|
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);
|
size_t prev_slash = file_path.rfind('/', last_slash - 1);
|
||||||
if (prev_slash == std::string::npos)
|
if (prev_slash == std::string::npos)
|
||||||
return username + file_path.substr(last_slash);
|
return username + file_path.substr(last_slash);
|
||||||
@@ -311,21 +343,25 @@ void TransferService::runBroadcast() {
|
|||||||
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
|
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
|
||||||
|
|
||||||
int udp = socket(AF_INET, SOCK_DGRAM, 0);
|
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);
|
receiver_bcast_sock.store(udp);
|
||||||
|
|
||||||
auto releaseUdp = [&]() {
|
auto releaseUdp = [&]() {
|
||||||
int owned = receiver_bcast_sock.exchange(-1);
|
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
|
struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit
|
||||||
setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||||
|
|
||||||
sockaddr_in addr{};
|
sockaddr_in addr{};
|
||||||
addr.sin_family = AF_INET;
|
addr.sin_family = AF_INET;
|
||||||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
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) {
|
if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||||
releaseUdp();
|
releaseUdp();
|
||||||
@@ -348,7 +384,8 @@ void TransferService::runBroadcast() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
|
ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
|
||||||
if (n < 0) {
|
if (n < 0) {
|
||||||
if (receiver_state.cancelled.load()) break;
|
if (receiver_state.cancelled.load())
|
||||||
|
break;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
buf[n] = '\0';
|
buf[n] = '\0';
|
||||||
@@ -365,7 +402,7 @@ void TransferService::runBroadcast() {
|
|||||||
void* TransferService::acceptEntry(void* arg) {
|
void* TransferService::acceptEntry(void* arg) {
|
||||||
auto* a = static_cast<AcceptArgs*>(arg);
|
auto* a = static_cast<AcceptArgs*>(arg);
|
||||||
TransferService* svc = a->svc;
|
TransferService* svc = a->svc;
|
||||||
int server_fd = a->server_fd;
|
int server_fd = a->server_fd;
|
||||||
delete a;
|
delete a;
|
||||||
svc->runAccept(server_fd);
|
svc->runAccept(server_fd);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -380,41 +417,48 @@ void TransferService::runAccept(int server_fd) {
|
|||||||
int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len);
|
int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len);
|
||||||
|
|
||||||
int owned_listen = receiver_listen_sock.exchange(-1);
|
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) {
|
if (client_sock >= 0) {
|
||||||
receiver_client_sock.store(client_sock);
|
receiver_client_sock.store(client_sock);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
uint32_t filename_len = 0;
|
uint32_t filename_len = 0;
|
||||||
if (!recvAll(client_sock, &filename_len, sizeof(filename_len))) break;
|
if (!recvAll(client_sock, &filename_len, sizeof(filename_len)))
|
||||||
if (filename_len == proto::EOF_SENTINEL) break;
|
break;
|
||||||
if (filename_len > proto::MAX_FILENAME) break;
|
if (filename_len == proto::EOF_SENTINEL)
|
||||||
|
break;
|
||||||
|
if (filename_len > proto::MAX_FILENAME)
|
||||||
|
break;
|
||||||
|
|
||||||
std::vector<char> filename_buf(filename_len + 1, '\0');
|
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);
|
std::string filename_str(filename_buf.data(), filename_len);
|
||||||
filename_str = replaceUsername(filename_str);
|
filename_str = replaceUsername(filename_str);
|
||||||
|
|
||||||
{
|
{
|
||||||
size_t sl = filename_str.rfind('/');
|
size_t sl = filename_str.rfind('/');
|
||||||
receiver_state.setStatus(
|
receiver_state.setStatus(sl != std::string::npos ? filename_str.substr(sl + 1)
|
||||||
sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str);
|
: filename_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t file_size = 0;
|
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);
|
receiveFile(client_sock, filename_str, file_size, receiver_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
int owned = receiver_client_sock.exchange(-1);
|
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()) {
|
if (!receiver_state.cancelled.load()) {
|
||||||
#ifdef __SWITCH__
|
#ifdef __SWITCH__
|
||||||
receiver_state.setStatus("Restoring...");
|
receiver_state.setStatus("Restoring...");
|
||||||
auto result = io::restore(restore_title_index, restore_uid, 0, restore_title_name);
|
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();
|
restore_error = result.isOk() ? "" : result.error();
|
||||||
#else
|
#else
|
||||||
restore_ok = true;
|
restore_ok = true;
|
||||||
@@ -430,29 +474,32 @@ int TransferService::startReceive(size_t title_index, AccountUid uid, std::strin
|
|||||||
receiver_state.reset();
|
receiver_state.reset();
|
||||||
receiver_state.setStatus("Waiting for connection...");
|
receiver_state.setStatus("Waiting for connection...");
|
||||||
restore_title_index = title_index;
|
restore_title_index = title_index;
|
||||||
restore_uid = uid;
|
restore_uid = uid;
|
||||||
restore_title_name = std::move(title_name);
|
restore_title_name = std::move(title_name);
|
||||||
restore_ok = false;
|
restore_ok = false;
|
||||||
restore_error.clear();
|
restore_error.clear();
|
||||||
|
|
||||||
pthread_t bcast_thread;
|
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;
|
receiver_bcast_thread = bcast_thread;
|
||||||
pthread_detach(bcast_thread);
|
pthread_detach(bcast_thread);
|
||||||
|
|
||||||
Socket server(socket(AF_INET, SOCK_STREAM, 0));
|
Socket server(socket(AF_INET, SOCK_STREAM, 0));
|
||||||
if (!server.valid()) { cancelReceive(); return 1; }
|
if (!server.valid()) {
|
||||||
|
cancelReceive();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
int yes = 1;
|
int yes = 1;
|
||||||
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||||
|
|
||||||
sockaddr_in addr{};
|
sockaddr_in addr{};
|
||||||
addr.sin_family = AF_INET;
|
addr.sin_family = AF_INET;
|
||||||
addr.sin_addr.s_addr = INADDR_ANY;
|
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 ||
|
if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 || listen(server, 3) < 0) {
|
||||||
listen(server, 3) < 0) {
|
|
||||||
cancelReceive();
|
cancelReceive();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -472,12 +519,22 @@ int TransferService::startReceive(size_t title_index, AccountUid uid, std::strin
|
|||||||
void TransferService::cancelReceive() {
|
void TransferService::cancelReceive() {
|
||||||
receiver_state.cancelled.store(true);
|
receiver_state.cancelled.store(true);
|
||||||
int sock = receiver_client_sock.exchange(-1);
|
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);
|
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);
|
int bsock = receiver_bcast_sock.exchange(-1);
|
||||||
if (bsock >= 0) { shutdown(bsock, SHUT_RDWR); close(bsock); }
|
if (bsock >= 0) {
|
||||||
if (receiver_broadcast_active.load()) pthread_cancel(receiver_bcast_thread);
|
shutdown(bsock, SHUT_RDWR);
|
||||||
|
close(bsock);
|
||||||
|
}
|
||||||
|
if (receiver_broadcast_active.load())
|
||||||
|
pthread_cancel(receiver_bcast_thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace nxst
|
} // namespace nxst
|
||||||
|
|||||||
+281
-279
@@ -1,310 +1,312 @@
|
|||||||
#include <nxst/app/main_application.hpp>
|
#include <nxst/app/main_application.hpp>
|
||||||
#include <nxst/domain/util.hpp>
|
#include <nxst/domain/util.hpp>
|
||||||
#include <nxst/ui/transfer_overlay.hpp>
|
|
||||||
#include <nxst/ui/const.h>
|
#include <nxst/ui/const.h>
|
||||||
|
#include <nxst/ui/transfer_overlay.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
extern MainApplication *mainApp;
|
extern MainApplication* mainApp;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int ListX = theme::space::lg;
|
constexpr int ListX = theme::space::lg;
|
||||||
constexpr int ListW = 760;
|
constexpr int ListW = 760;
|
||||||
constexpr int PanelX = ListX + ListW + theme::space::xl;
|
constexpr int PanelX = ListX + ListW + theme::space::xl;
|
||||||
constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg;
|
constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg;
|
||||||
constexpr int ContentY = theme::layout::ContentTop + theme::space::md;
|
constexpr int ContentY = theme::layout::ContentTop + theme::space::md;
|
||||||
constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md;
|
constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md;
|
||||||
constexpr int BtnH = 56;
|
constexpr int BtnH = 56;
|
||||||
constexpr int BtnW = PanelW - 2 * theme::space::lg;
|
constexpr int BtnW = PanelW - 2 * theme::space::lg;
|
||||||
}
|
} // namespace
|
||||||
|
|
||||||
TitlesLayout::TitlesLayout() : Layout::Layout() {
|
TitlesLayout::TitlesLayout() : Layout::Layout() {
|
||||||
using namespace theme;
|
using namespace theme;
|
||||||
|
|
||||||
this->titlesMenu = pu::ui::elm::Menu::New(
|
this->titlesMenu =
|
||||||
ListX, ContentY, ListW,
|
pu::ui::elm::Menu::New(ListX, ContentY, ListW, color::BgBase, color::BgSurface2, 88, 6);
|
||||||
color::BgBase, color::BgSurface2,
|
this->titlesMenu->SetScrollbarColor(color::Primary);
|
||||||
88, 6);
|
this->titlesMenu->SetItemsFocusColor(color::BgSurface2);
|
||||||
this->titlesMenu->SetScrollbarColor(color::Primary);
|
this->titlesMenu->SetOnSelectionChanged([this]() {
|
||||||
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->refreshPanel();
|
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() {
|
this->titlesMenu->ClearItems();
|
||||||
if (this->titlesMenu->GetItems().empty()) return;
|
for (auto& item : *items) {
|
||||||
int idx = this->titlesMenu->GetSelectedIndex();
|
this->titlesMenu->AddItem(item);
|
||||||
Title title;
|
|
||||||
getTitle(title, this->current_uid, idx);
|
|
||||||
this->panelTitle->SetText(StringUtils::elide(title.name(), 24));
|
|
||||||
}
|
}
|
||||||
|
this->titlesMenu->SetSelectedIndex(0);
|
||||||
|
|
||||||
void TitlesLayout::refreshButtons() {
|
const bool empty = items->empty();
|
||||||
using namespace theme;
|
this->titlesMenu->SetVisible(!empty);
|
||||||
const bool active = (focus == TitlesFocus::Actions);
|
this->panelBg->SetVisible(!empty);
|
||||||
if (active && action == TitlesAction::Transfer) {
|
this->panelTitle->SetVisible(!empty);
|
||||||
this->btnTransferBg->SetColor(color::Primary);
|
this->panelHint->SetVisible(!empty);
|
||||||
this->btnTransferText->SetColor(color::TextPrimary);
|
this->btnTransferBg->SetVisible(!empty);
|
||||||
this->btnReceiveBg->SetColor(color::BgSurface2);
|
this->btnTransferText->SetVisible(!empty);
|
||||||
this->btnReceiveText->SetColor(color::TextSecondary);
|
this->btnReceiveBg->SetVisible(!empty);
|
||||||
} else if (active && action == TitlesAction::Receive) {
|
this->btnReceiveText->SetVisible(!empty);
|
||||||
this->btnTransferBg->SetColor(color::BgSurface2);
|
this->panelFooter->SetVisible(!empty);
|
||||||
this->btnTransferText->SetColor(color::TextSecondary);
|
this->emptyText->SetVisible(empty);
|
||||||
this->btnReceiveBg->SetColor(color::Accent);
|
this->emptySub->SetVisible(empty);
|
||||||
this->btnReceiveText->SetColor(color::BgBase);
|
|
||||||
} else {
|
this->focus = TitlesFocus::List;
|
||||||
this->btnTransferBg->SetColor(color::BgSurface2);
|
this->action = TitlesAction::Transfer;
|
||||||
this->btnTransferText->SetColor(color::TextSecondary);
|
this->refreshPanel();
|
||||||
this->btnReceiveBg->SetColor(color::BgSurface2);
|
this->refreshButtons();
|
||||||
this->btnReceiveText->SetColor(color::TextSecondary);
|
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() {
|
void TitlesLayout::updateHints() {
|
||||||
if (focus == TitlesFocus::List) {
|
if (focus == TitlesFocus::List) {
|
||||||
this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}});
|
this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}});
|
||||||
} else {
|
} else {
|
||||||
this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}});
|
this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void TitlesLayout::runTransfer(int index, Title& title) {
|
void TitlesLayout::runTransfer(int index, Title& title) {
|
||||||
(void)title;
|
(void)title;
|
||||||
auto ovl = TransferOverlay::New("Transferring save data...");
|
auto ovl = TransferOverlay::New("Transferring save data...");
|
||||||
this->titlesMenu->SetVisible(false);
|
this->titlesMenu->SetVisible(false);
|
||||||
mainApp->StartOverlay(ovl);
|
mainApp->StartOverlay(ovl);
|
||||||
this->LockInput();
|
this->LockInput();
|
||||||
if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) {
|
if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) {
|
||||||
mainApp->EndOverlay();
|
|
||||||
this->titlesMenu->SetVisible(true);
|
|
||||||
this->UnlockInput();
|
|
||||||
mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
while (!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();
|
mainApp->EndOverlay();
|
||||||
this->titlesMenu->SetVisible(true);
|
this->titlesMenu->SetVisible(true);
|
||||||
this->UnlockInput();
|
this->UnlockInput();
|
||||||
|
mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true);
|
||||||
if (mainApp->transfer.isSendConnectionFailed()) {
|
return;
|
||||||
mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true);
|
|
||||||
} else if (mainApp->transfer.isSendCancelled()) {
|
|
||||||
mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true);
|
|
||||||
} else {
|
|
||||||
mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
while (!mainApp->transfer.isSendDone()) {
|
||||||
void TitlesLayout::runReceive(int index, Title& title) {
|
ovl->SetStatus(mainApp->transfer.sendStatusText());
|
||||||
if (mainApp->transfer.startReceive((size_t)index, this->current_uid, title.name()) != 0) {
|
ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown());
|
||||||
mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true);
|
ovl->SetProgress(mainApp->transfer.sendProgress());
|
||||||
return;
|
mainApp->CallForRender();
|
||||||
}
|
if (mainApp->GetButtonsDown() & HidNpadButton_B) {
|
||||||
auto ovl = TransferOverlay::New("Receiving save data...");
|
|
||||||
this->titlesMenu->SetVisible(false);
|
|
||||||
mainApp->StartOverlay(ovl);
|
|
||||||
this->LockInput();
|
|
||||||
while (!mainApp->transfer.isReceiveDone()) {
|
|
||||||
ovl->SetStatus(mainApp->transfer.receiveStatusText());
|
|
||||||
ovl->SetProgress(mainApp->transfer.receiveProgress());
|
|
||||||
mainApp->CallForRender();
|
|
||||||
if (mainApp->GetButtonsDown() & HidNpadButton_B) {
|
|
||||||
mainApp->transfer.cancelReceive();
|
|
||||||
}
|
|
||||||
svcSleepThread(16666666LL);
|
|
||||||
}
|
|
||||||
mainApp->EndOverlay();
|
|
||||||
this->titlesMenu->SetVisible(true);
|
|
||||||
this->UnlockInput();
|
|
||||||
|
|
||||||
if (mainApp->transfer.isReceiveCancelled()) {
|
|
||||||
mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true);
|
|
||||||
} else if (mainApp->transfer.restoreSucceeded()) {
|
|
||||||
mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true);
|
|
||||||
} else {
|
|
||||||
mainApp->CreateShowDialog("Receive", "Restore failed:\n" + mainApp->transfer.restoreError(), {"OK"}, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
|
|
||||||
(void)Up; (void)Held; (void)Pos;
|
|
||||||
if (m_inputLocked) return;
|
|
||||||
|
|
||||||
if (Down & HidNpadButton_Plus) {
|
|
||||||
mainApp->transfer.cancelSend();
|
mainApp->transfer.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->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;
|
return;
|
||||||
}
|
}
|
||||||
|
if (Down & HidNpadButton_A) {
|
||||||
if (focus == TitlesFocus::List) {
|
if (this->titlesMenu->GetItems().empty())
|
||||||
if (Down & HidNpadButton_B) {
|
|
||||||
this->header->SetUser(std::nullopt, "");
|
|
||||||
mainApp->LoadLayout(mainApp->users_layout);
|
|
||||||
return;
|
return;
|
||||||
}
|
this->lockedListIndex = this->titlesMenu->GetSelectedIndex();
|
||||||
if (Down & HidNpadButton_A) {
|
this->focus = TitlesFocus::Actions;
|
||||||
if (this->titlesMenu->GetItems().empty()) return;
|
this->action = TitlesAction::Transfer;
|
||||||
this->lockedListIndex = this->titlesMenu->GetSelectedIndex();
|
this->refreshButtons();
|
||||||
this->focus = TitlesFocus::Actions;
|
this->updateHints();
|
||||||
this->action = TitlesAction::Transfer;
|
return;
|
||||||
this->refreshButtons();
|
}
|
||||||
this->updateHints();
|
} else {
|
||||||
return;
|
if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) {
|
||||||
}
|
this->titlesMenu->SetSelectedIndex(this->lockedListIndex);
|
||||||
} else {
|
}
|
||||||
if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) {
|
if (Down & HidNpadButton_B) {
|
||||||
this->titlesMenu->SetSelectedIndex(this->lockedListIndex);
|
this->focus = TitlesFocus::List;
|
||||||
}
|
this->refreshButtons();
|
||||||
if (Down & HidNpadButton_B) {
|
this->updateHints();
|
||||||
this->focus = TitlesFocus::List;
|
return;
|
||||||
this->refreshButtons();
|
}
|
||||||
this->updateHints();
|
if (Down &
|
||||||
return;
|
(HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_StickLUp | HidNpadButton_StickLDown)) {
|
||||||
}
|
this->action =
|
||||||
if (Down & (HidNpadButton_Up | HidNpadButton_Down |
|
(action == TitlesAction::Transfer) ? TitlesAction::Receive : TitlesAction::Transfer;
|
||||||
HidNpadButton_StickLUp | HidNpadButton_StickLDown)) {
|
this->refreshButtons();
|
||||||
this->action = (action == TitlesAction::Transfer)
|
return;
|
||||||
? TitlesAction::Receive : TitlesAction::Transfer;
|
}
|
||||||
this->refreshButtons();
|
if (Down & HidNpadButton_A) {
|
||||||
return;
|
int idx = this->titlesMenu->GetSelectedIndex();
|
||||||
}
|
Title title;
|
||||||
if (Down & HidNpadButton_A) {
|
getTitle(title, this->current_uid, idx);
|
||||||
int idx = this->titlesMenu->GetSelectedIndex();
|
TitlesAction chosen = action;
|
||||||
Title title;
|
this->focus = TitlesFocus::List;
|
||||||
getTitle(title, this->current_uid, idx);
|
this->refreshButtons();
|
||||||
TitlesAction chosen = action;
|
this->updateHints();
|
||||||
this->focus = TitlesFocus::List;
|
if (chosen == TitlesAction::Transfer) {
|
||||||
this->refreshButtons();
|
this->runTransfer(idx, title);
|
||||||
this->updateHints();
|
} else {
|
||||||
if (chosen == TitlesAction::Transfer) {
|
this->runReceive(idx, title);
|
||||||
this->runTransfer(idx, title);
|
|
||||||
} else {
|
|
||||||
this->runReceive(idx, title);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // namespace ui
|
||||||
|
|||||||
+55
-61
@@ -1,75 +1,69 @@
|
|||||||
#include <nxst/app/main_application.hpp>
|
#include <nxst/app/main_application.hpp>
|
||||||
|
|
||||||
namespace ui {
|
namespace ui {
|
||||||
extern MainApplication *mainApp;
|
extern MainApplication* mainApp;
|
||||||
|
|
||||||
UsersLayout::UsersLayout() : Layout::Layout() {
|
UsersLayout::UsersLayout() : Layout::Layout() {
|
||||||
using namespace theme;
|
using namespace theme;
|
||||||
|
|
||||||
this->usersMenu = pu::ui::elm::Menu::New(
|
this->usersMenu = pu::ui::elm::Menu::New(0, layout::ContentTop + space::md, layout::ScreenW,
|
||||||
0, layout::ContentTop + space::md,
|
color::BgBase, color::BgSurface2, 88, 6);
|
||||||
layout::ScreenW,
|
this->usersMenu->SetScrollbarColor(color::Primary);
|
||||||
color::BgBase, color::BgSurface2,
|
this->usersMenu->SetItemsFocusColor(color::BgSurface2);
|
||||||
88, 6);
|
|
||||||
this->usersMenu->SetScrollbarColor(color::Primary);
|
|
||||||
this->usersMenu->SetItemsFocusColor(color::BgSurface2);
|
|
||||||
|
|
||||||
for (AccountUid const& uid : Account::ids()) {
|
for (AccountUid const& uid : Account::ids()) {
|
||||||
auto item = pu::ui::elm::MenuItem::New(Account::username(uid));
|
auto item = pu::ui::elm::MenuItem::New(Account::username(uid));
|
||||||
item->SetColor(color::TextPrimary);
|
item->SetColor(color::TextPrimary);
|
||||||
this->usersMenu->AddItem(item);
|
this->usersMenu->AddItem(item);
|
||||||
}
|
|
||||||
|
|
||||||
this->loadingBg = pu::ui::elm::Rectangle::New(
|
|
||||||
0, 0, layout::ScreenW, layout::ScreenH, color::Scrim);
|
|
||||||
this->loadingBg->SetVisible(false);
|
|
||||||
|
|
||||||
this->loadingText = pu::ui::elm::TextBlock::New(
|
|
||||||
layout::ScreenW / 2 - 120,
|
|
||||||
layout::ScreenH / 2 - 12,
|
|
||||||
"Loading saves...");
|
|
||||||
this->loadingText->SetFont(type::font(type::Body));
|
|
||||||
this->loadingText->SetColor(color::TextSecondary);
|
|
||||||
this->loadingText->SetVisible(false);
|
|
||||||
|
|
||||||
this->SetBackgroundColor(color::BgBase);
|
|
||||||
this->Add(this->usersMenu);
|
|
||||||
this->Add(this->loadingBg);
|
|
||||||
this->Add(this->loadingText);
|
|
||||||
|
|
||||||
this->header = std::make_unique<HeaderBar>(this, "Select a user");
|
|
||||||
this->hints = std::make_unique<HintBar>(this);
|
|
||||||
this->hints->SetHints({{"A", "Select"}, {"+", "Quit"}});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t UsersLayout::GetCurrentIndex() {
|
this->loadingBg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::ScreenH, color::Scrim);
|
||||||
return this->usersMenu->GetSelectedIndex();
|
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_A) {
|
||||||
if (Down & HidNpadButton_Plus) {
|
AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex());
|
||||||
mainApp->Close();
|
|
||||||
return;
|
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) {
|
mainApp->titles_layout->InitTitles(uid);
|
||||||
AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex());
|
mainApp->LoadLayout(mainApp->titles_layout);
|
||||||
|
|
||||||
if (!areTitlesLoaded()) {
|
|
||||||
this->usersMenu->SetVisible(false);
|
|
||||||
this->loadingBg->SetVisible(true);
|
|
||||||
this->loadingText->SetVisible(true);
|
|
||||||
mainApp->CallForRender();
|
|
||||||
|
|
||||||
loadTitles();
|
|
||||||
|
|
||||||
this->loadingBg->SetVisible(false);
|
|
||||||
this->loadingText->SetVisible(false);
|
|
||||||
this->usersMenu->SetVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
mainApp->titles_layout->InitTitles(uid);
|
|
||||||
mainApp->LoadLayout(mainApp->titles_layout);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // namespace ui
|
||||||
|
|||||||
Reference in New Issue
Block a user