Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ include $(DEVKITPRO)/libnx/switch_rules

APP_TITLE=micro NX Save Sync
APP_AUTHOR=prodeveloper0
APP_VERSION=1.0.0
APP_VERSION=1.0.1
APP_TITLEID=uNSS

#---------------------------------------------------------------------------------
Expand Down
24 changes: 6 additions & 18 deletions client/source/gui/Gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,21 @@ namespace gui

bool Renderer::loadSystemFont()
{
PlFontData koFont;
PlFontData sysFont;
Result rc = plInitialize(PlServiceType_User);
if (R_FAILED(rc))
return false;

// 한글 폰트 로드 (영문/숫자도 포함)
rc = plGetSharedFontByType(&koFont, PlSharedFontType_KO);
rc = plGetSharedFontByType(&sysFont, PlSharedFontType_Standard);
if (R_FAILED(rc))
{
// fallback: Standard 폰트
PlFontData stdFont;
rc = plGetSharedFontByType(&stdFont, PlSharedFontType_Standard);
if (R_FAILED(rc))
{
plExit();
return false;
}
this->fontDataSize = stdFont.size;
this->fontData = malloc(stdFont.size);
memcpy(this->fontData, stdFont.address, stdFont.size);
plExit();
return true;
return false;
}

this->fontDataSize = koFont.size;
this->fontData = malloc(koFont.size);
memcpy(this->fontData, koFont.address, koFont.size);
this->fontDataSize = sysFont.size;
this->fontData = malloc(sysFont.size);
memcpy(this->fontData, sysFont.address, sysFont.size);

plExit();
return true;
Expand Down
3 changes: 2 additions & 1 deletion client/source/gui/MainScreen.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "TitleSelectionScreen.hpp"
#include "MainScreen.hpp"
#include "ProgressScreen.hpp"
#include "AccountScreen.hpp"
Expand Down Expand Up @@ -87,10 +88,10 @@ void MainScreen::rebuildMenu()

menuItems.push_back({"Push to Server", [this]() { startPush(); }, remoteEnabled});
menuItems.push_back({"Pull from Server", [this]() { startPull(); }, true});
menuItems.push_back({"Select Games", [this]() { App::instance().pushScreen(new TitleSelectionScreen(config, account.uid)); }, true});
}
}


void MainScreen::update(u64 kDown)
{
// 첫 프레임: 계정 해석
Expand Down
173 changes: 173 additions & 0 deletions client/source/gui/TitleSelectionScreen.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#include "TitleSelectionScreen.hpp"
#include "../utils.hpp"
#include "../account.hpp"
#include <set>
#include <algorithm> // Required for std::sort

namespace gui
{

TitleSelectionScreen::TitleSelectionScreen(Config& config, AccountUid uid) : config(config), uid(uid)
{
loadTitles();
}

void TitleSelectionScreen::loadTitles()
{
std::set<u64> excludedIds;
std::string currentExclusions = config["title"]["excludedTitleIds"].value;

// Parse current exclusions
size_t start = 0;
while (start < currentExclusions.size()) {
size_t pos = currentExclusions.find(',', start);
std::string hex = (pos == std::string::npos) ? currentExclusions.substr(start) : currentExclusions.substr(start, pos - start);

hex.erase(0, hex.find_first_not_of(" \t\r\n"));
hex.erase(hex.find_last_not_of(" \t\r\n") + 1);

if (!hex.empty()) {
excludedIds.insert(fromHex<u64>(hex));
}
if (pos == std::string::npos) break;
start = pos + 1;
}

// FIX: Gather ALL installed titles AND titles with save data to match Push/Pull logic
std::vector<u64> allInstalledIds;
std::vector<u64> createdSaveIds;
probeAllTitles(uid, allInstalledIds); // <-- Change here
probeSaveDataCreatedTitles(uid, createdSaveIds);

// Combine and deduplicate
std::set<u64> combinedIds(allInstalledIds.begin(), allInstalledIds.end());
combinedIds.insert(createdSaveIds.begin(), createdSaveIds.end());

for (u64 id : combinedIds) {
TitleItem item;
item.id = id;

// Invert logic: If it is IN the exclusion list, it is NOT selected.
item.selected = (excludedIds.count(id) == 0);

if (getTitleName(id, item.name) != 0) {
item.name = "Unknown Title (" + toHex(id) + ")";
}
titles.push_back(item);
}

// FIX: Sort alphabetically to make finding games easier
std::sort(titles.begin(), titles.end(), [](const TitleItem& a, const TitleItem& b) {
return a.name < b.name;
});
}

void TitleSelectionScreen::saveSelection()
{
std::set<u64> finalExcludedIds;

// 1. Keep previously excluded IDs that might not be installed anymore
// (prevents them from being wiped when you uninstall a game)
std::string currentExclusions = config["title"]["excludedTitleIds"].value;
size_t start = 0;
while (start < currentExclusions.size()) {
size_t pos = currentExclusions.find(',', start);
std::string hex = (pos == std::string::npos) ? currentExclusions.substr(start) : currentExclusions.substr(start, pos - start);
hex.erase(0, hex.find_first_not_of(" \t\r\n"));
hex.erase(hex.find_last_not_of(" \t\r\n") + 1);
if (!hex.empty()) finalExcludedIds.insert(fromHex<u64>(hex));
if (pos == std::string::npos) break;
start = pos + 1;
}

// 2. Add unselected items to exclusion, remove selected items from exclusion
for (const auto& item : titles) {
if (!item.selected) {
finalExcludedIds.insert(item.id);
} else {
finalExcludedIds.erase(item.id);
}
}

// 3. Build string
std::string newExclusions = "";
for (u64 id : finalExcludedIds) {
if (!newExclusions.empty()) newExclusions += ", ";
newExclusions += toHex(id);
}

config["title"]["excludedTitleIds"] = newExclusions;
config.save("sdmc:/uNSS/config.ini");
}

void TitleSelectionScreen::update(u64 kDown)
{
if (titles.empty()) {
if (kDown & HidNpadButton_B) App::instance().popScreen();
return;
}

int maxVisible = 10;

if (kDown & HidNpadButton_AnyUp) {
selectedIndex--;
if (selectedIndex < 0) selectedIndex = titles.size() - 1;
}
if (kDown & HidNpadButton_AnyDown) {
selectedIndex++;
if (selectedIndex >= (int)titles.size()) selectedIndex = 0;
}

if (selectedIndex < scrollOffset) scrollOffset = selectedIndex;
if (selectedIndex >= scrollOffset + maxVisible) scrollOffset = selectedIndex - maxVisible + 1;

if (kDown & HidNpadButton_A) {
titles[selectedIndex].selected = !titles[selectedIndex].selected;
}

if (kDown & HidNpadButton_X) {
for (auto& item : titles) item.selected = true;
}

if (kDown & HidNpadButton_Y) {
for (auto& item : titles) item.selected = false;
}

if (kDown & HidNpadButton_B) {
saveSelection();
App::instance().popScreen();
}
}

void TitleSelectionScreen::render(Renderer& r)
{
int x = 80;
int y = 60;

r.drawText("Select Games to Sync", x, y, 32, COLOR_ACCENT);
y += 50;
r.drawRect(x, y, r.screenWidth() - x * 2, 2, COLOR_ACCENT);
y += 20;

if (titles.empty()) {
r.drawText("No save data found.", x, y, 24, COLOR_DIM);
} else {
int maxVisible = 10;
int lineHeight = 40;

for (int i = scrollOffset; i < (int)titles.size() && i < scrollOffset + maxVisible; i++) {
Color textColor = (i == selectedIndex) ? COLOR_HIGHLIGHT : COLOR_TEXT;

std::string checkbox = titles[i].selected ? "[X] " : "[ ] ";
r.drawText(checkbox + titles[i].name, x, y, 24, textColor);

y += lineHeight;
}
}

int fy = r.screenHeight() - 50;
r.drawRect(x, fy - 10, r.screenWidth() - x * 2, 2, {80, 80, 80, 255});
r.drawText("A: Toggle X: Select All Y: Deselect All B: Save & Back", x, fy, 18, COLOR_DIM);
}

} // namespace gui
35 changes: 35 additions & 0 deletions client/source/gui/TitleSelectionScreen.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once
#include "Gui.hpp"
#include "../title.hpp"
#include "../ini.hpp"
#include <vector>
#include <string>

namespace gui
{

struct TitleItem {
u64 id;
std::string name;
bool selected; // UI uses "selected" (whitelist)
};

class TitleSelectionScreen : public Screen
{
public:
TitleSelectionScreen(Config& config, AccountUid uid);
void update(u64 kDown) override;
void render(Renderer& r) override;

private:
Config& config;
AccountUid uid;
std::vector<TitleItem> titles;
int selectedIndex = 0;
int scrollOffset = 0;

void loadTitles();
void saveSelection();
};

} // namespace gui
10 changes: 8 additions & 2 deletions client/source/http.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ size_t HTTPClient::readCallback(void* ptr, size_t size, size_t nmemb, void* user
HTTPClient* client = static_cast<HTTPClient*>(userp);
if (!client || !client->onSend)
{
return size * nmemb;
return 0;
}

size_t actualSize = 0;
Expand Down Expand Up @@ -112,7 +112,9 @@ int HTTPClient::perform()

curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);


curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "uNSS/1.0");
// HTTP 메서드 설정
if (method == "GET")
{
Expand All @@ -121,6 +123,10 @@ int HTTPClient::perform()
else if (method == "POST")
{
curl_easy_setopt(curl, CURLOPT_POST, 1L);
if (!onSend)
{
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0L);
}
}

// 비동기 모드 설정
Expand Down
20 changes: 20 additions & 0 deletions client/source/ini.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,26 @@ class Config_
return result;
}

public:
bool save(const std::string& filename)
{
std::ofstream ofs(filename);
if(!ofs.is_open()) return false;

for(auto const& section : config)
{
ofs << "[" << section.first << "]\n";
for(auto const& prop : section.second.properties)
{
// Quote the value if it has spaces or special chars, otherwise write direct
ofs << prop.first << "=\"" << prop.second.value << "\"\n";
}
ofs << "\n";
}
ofs.close();
return true;
}

public:
Property& operator [] (const std::string& s)
{
Expand Down
Loading