phase 5: transfer service
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
#include <nxst/app/main_application.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace ui { extern MainApplication* mainApp; }
|
||||
|
||||
static int nxlink_sock = -1;
|
||||
|
||||
extern "C" void userAppInit() {
|
||||
appletInitialize();
|
||||
hidInitialize();
|
||||
nsInitialize();
|
||||
setsysInitialize();
|
||||
setInitialize();
|
||||
accountInitialize(AccountServiceType_Administrator);
|
||||
pmshellInitialize();
|
||||
socketInitializeDefault();
|
||||
pdmqryInitialize();
|
||||
nxlink_sock = nxlinkStdio();
|
||||
printf("userAppInit\n");
|
||||
}
|
||||
|
||||
extern "C" void userAppExit() {
|
||||
if (ui::mainApp) {
|
||||
ui::mainApp->transfer.cancelReceive();
|
||||
ui::mainApp->transfer.cancelSend();
|
||||
for (int i = 0; i < 150 &&
|
||||
(!ui::mainApp->transfer.isReceiveWorkersIdle() ||
|
||||
!ui::mainApp->transfer.isSendWorkersIdle()); i++) {
|
||||
usleep(10000);
|
||||
}
|
||||
}
|
||||
if (nxlink_sock != -1) {
|
||||
close(nxlink_sock);
|
||||
}
|
||||
appletExit();
|
||||
hidExit();
|
||||
nsExit();
|
||||
setsysExit();
|
||||
setExit();
|
||||
accountExit();
|
||||
pmshellExit();
|
||||
socketExit();
|
||||
pdmqryExit();
|
||||
}
|
||||
|
||||
// Main entrypoint
|
||||
int main() {
|
||||
Result res = servicesInit();
|
||||
if (R_FAILED(res)) {
|
||||
servicesExit();
|
||||
exit(res);
|
||||
}
|
||||
|
||||
printf("main");
|
||||
|
||||
// First create our renderer, where one can customize SDL or other stuff's
|
||||
// initialization.
|
||||
auto renderer_opts = pu::ui::render::RendererInitOptions(
|
||||
SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags);
|
||||
renderer_opts.UseImage(pu::ui::render::IMGAllFlags);
|
||||
renderer_opts.UseAudio(pu::ui::render::MixerAllFlags);
|
||||
renderer_opts.UseTTF();
|
||||
renderer_opts.SetExtraDefaultFontSize(theme::type::Caption);
|
||||
renderer_opts.SetExtraDefaultFontSize(theme::type::Label);
|
||||
renderer_opts.SetExtraDefaultFontSize(theme::type::Body);
|
||||
renderer_opts.SetExtraDefaultFontSize(theme::type::Title);
|
||||
renderer_opts.SetExtraDefaultFontSize(theme::type::Display);
|
||||
|
||||
auto renderer = pu::ui::render::Renderer::New(renderer_opts);
|
||||
|
||||
// Create our main application from the renderer
|
||||
auto main = ui::MainApplication::New(renderer);
|
||||
|
||||
main->Prepare();
|
||||
|
||||
main->Show();
|
||||
|
||||
servicesExit();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <string>
|
||||
#include <switch.h>
|
||||
#include <switch/services/hid.h>
|
||||
#include <vector>
|
||||
#include <nxst/app/main_application.hpp>
|
||||
|
||||
namespace ui {
|
||||
MainApplication *mainApp;
|
||||
|
||||
void MainApplication::OnLoad() {
|
||||
mainApp = this;
|
||||
this->users_layout = UsersLayout::New();
|
||||
this->titles_layout = TitlesLayout::New();
|
||||
this->users_layout->SetOnInput(
|
||||
std::bind(&UsersLayout::onInput, this->users_layout, std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3, std::placeholders::_4));
|
||||
this->titles_layout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titles_layout, std::placeholders::_1, std::placeholders::_2,
|
||||
std::placeholders::_3, std::placeholders::_4));
|
||||
this->LoadLayout(this->users_layout);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/domain/account.hpp>
|
||||
#include <sys/stat.h>
|
||||
#include <cstdio>
|
||||
|
||||
static std::map<AccountUid, User> mUsers;
|
||||
|
||||
Result Account::init(void)
|
||||
{
|
||||
Result res = accountInitialize(AccountServiceType_Application);
|
||||
if (R_FAILED(res)) return res;
|
||||
|
||||
AccountUid uids[8];
|
||||
s32 count = 0;
|
||||
accountListAllUsers(uids, 8, &count);
|
||||
for (s32 i = 0; i < count; i++) {
|
||||
Account::username(uids[i]); // populates mUsers as side effect
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Account::exit(void)
|
||||
{
|
||||
accountExit();
|
||||
}
|
||||
|
||||
std::vector<AccountUid> Account::ids(void)
|
||||
{
|
||||
std::vector<AccountUid> v;
|
||||
for (auto& value : mUsers) {
|
||||
v.push_back(value.second.id);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
static User getUser(AccountUid id)
|
||||
{
|
||||
User user{id, ""};
|
||||
AccountProfile profile;
|
||||
AccountProfileBase profilebase;
|
||||
memset(&profilebase, 0, sizeof(profilebase));
|
||||
|
||||
if (R_SUCCEEDED(accountGetProfile(&profile, id))) {
|
||||
if (R_SUCCEEDED(accountProfileGet(&profile, NULL, &profilebase))) {
|
||||
user.name = std::string(profilebase.nickname);
|
||||
}
|
||||
accountProfileClose(&profile);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
std::string Account::username(AccountUid id)
|
||||
{
|
||||
std::map<AccountUid, User>::const_iterator got = mUsers.find(id);
|
||||
if (got == mUsers.end()) {
|
||||
User user = getUser(id);
|
||||
mUsers.insert({id, user});
|
||||
return user.name;
|
||||
}
|
||||
|
||||
return got->second.name;
|
||||
}
|
||||
|
||||
std::string Account::iconPath(AccountUid id)
|
||||
{
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg",
|
||||
id.uid[0], id.uid[1]);
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0 && st.st_size > 0) return std::string(path);
|
||||
|
||||
mkdir("sdmc:/switch", 0755);
|
||||
mkdir("sdmc:/switch/NXST", 0755);
|
||||
mkdir("sdmc:/switch/NXST/cache", 0755);
|
||||
|
||||
AccountProfile profile;
|
||||
if (R_FAILED(accountGetProfile(&profile, id))) return "";
|
||||
|
||||
u32 imgSize = 0;
|
||||
if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) {
|
||||
accountProfileClose(&profile);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<u8> buf(imgSize);
|
||||
u32 outSize = 0;
|
||||
Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize);
|
||||
accountProfileClose(&profile);
|
||||
if (R_FAILED(r) || outSize == 0) return "";
|
||||
|
||||
FILE* f = fopen(path, "wb");
|
||||
if (!f) return "";
|
||||
fwrite(buf.data(), 1, outSize, f);
|
||||
fclose(f);
|
||||
return std::string(path);
|
||||
}
|
||||
|
||||
AccountUid Account::selectAccount(void)
|
||||
{
|
||||
LibAppletArgs args;
|
||||
libappletArgsCreate(&args, 0x10000);
|
||||
u8 st_in[0xA0] = {0};
|
||||
u8 st_out[0x18] = {0};
|
||||
size_t repsz;
|
||||
|
||||
Result res = libappletLaunch(AppletId_LibraryAppletPlayerSelect, &args, st_in, 0xA0, st_out, 0x18, &repsz);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
u64 lres = *(u64*)st_out;
|
||||
AccountUid uid = *(AccountUid*)&st_out[8];
|
||||
if (lres == 0)
|
||||
return uid;
|
||||
}
|
||||
|
||||
return AccountUid{};
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/domain/common.hpp>
|
||||
|
||||
std::string DateTime::timeStr(void)
|
||||
{
|
||||
time_t unixTime;
|
||||
struct tm timeStruct;
|
||||
time(&unixTime);
|
||||
localtime_r(&unixTime, &timeStruct);
|
||||
return StringUtils::format("%02i:%02i:%02i", timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
|
||||
}
|
||||
|
||||
std::string DateTime::dateTimeStr(void)
|
||||
{
|
||||
time_t unixTime;
|
||||
struct tm timeStruct;
|
||||
time(&unixTime);
|
||||
localtime_r(&unixTime, &timeStruct);
|
||||
return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday, timeStruct.tm_hour,
|
||||
timeStruct.tm_min, timeStruct.tm_sec);
|
||||
}
|
||||
|
||||
std::string DateTime::logDateTime(void)
|
||||
{
|
||||
time_t unixTime;
|
||||
struct tm timeStruct;
|
||||
time(&unixTime);
|
||||
localtime_r(&unixTime, &timeStruct);
|
||||
return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday,
|
||||
timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
|
||||
}
|
||||
|
||||
std::string StringUtils::UTF16toUTF8(const std::u16string& src)
|
||||
{
|
||||
static std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
|
||||
std::string dst = convert.to_bytes(src);
|
||||
return dst;
|
||||
}
|
||||
|
||||
std::string StringUtils::removeForbiddenCharacters(std::string src)
|
||||
{
|
||||
static const std::string illegalChars = ".,!\\/:?*\"<>|";
|
||||
for (size_t i = 0, sz = src.length(); i < sz; i++) {
|
||||
if (illegalChars.find(src[i]) != std::string::npos) {
|
||||
src[i] = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
size_t i;
|
||||
for (i = src.length() - 1; i > 0 && src[i] == ' '; i--)
|
||||
;
|
||||
src.erase(i + 1, src.length() - i);
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
std::string StringUtils::format(const std::string fmt_str, ...)
|
||||
{
|
||||
va_list ap;
|
||||
char* fp = NULL;
|
||||
va_start(ap, fmt_str);
|
||||
vasprintf(&fp, fmt_str.c_str(), ap);
|
||||
va_end(ap);
|
||||
std::unique_ptr<char[]> formatted(fp);
|
||||
return std::string(formatted.get());
|
||||
}
|
||||
|
||||
bool StringUtils::containsInvalidChar(const std::string& str)
|
||||
{
|
||||
for (size_t i = 0, sz = str.length(); i < sz; i++) {
|
||||
if (!isascii(str[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void StringUtils::ltrim(std::string& s)
|
||||
{
|
||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); }));
|
||||
}
|
||||
|
||||
void StringUtils::rtrim(std::string& s)
|
||||
{
|
||||
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end());
|
||||
}
|
||||
|
||||
void StringUtils::trim(std::string& s)
|
||||
{
|
||||
ltrim(s);
|
||||
rtrim(s);
|
||||
}
|
||||
|
||||
char* getConsoleIP(void)
|
||||
{
|
||||
struct in_addr in;
|
||||
in.s_addr = gethostid();
|
||||
return inet_ntoa(in);
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/domain/title.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
|
||||
static std::unordered_map<AccountUid, std::vector<Title>> titles;
|
||||
static bool s_titlesLoaded = false;
|
||||
|
||||
bool areTitlesLoaded(void)
|
||||
{
|
||||
return s_titlesLoaded;
|
||||
}
|
||||
|
||||
void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name, const std::string& author)
|
||||
{
|
||||
mId = id;
|
||||
mUserId = userID;
|
||||
mSaveDataType = saveDataType;
|
||||
mUserName = Account::username(userID);
|
||||
mAuthor = author;
|
||||
mName = name;
|
||||
mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId) : StringUtils::removeForbiddenCharacters(name);
|
||||
mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName;
|
||||
|
||||
std::string aname = StringUtils::removeAccents(mName);
|
||||
size_t pos = aname.rfind(":");
|
||||
mDisplayName = std::make_pair(aname, "");
|
||||
if (pos != std::string::npos) {
|
||||
std::string name1 = aname.substr(0, pos);
|
||||
std::string name2 = aname.substr(pos + 1);
|
||||
StringUtils::trim(name1);
|
||||
StringUtils::trim(name2);
|
||||
mDisplayName.first = name1;
|
||||
mDisplayName.second = name2;
|
||||
}
|
||||
else {
|
||||
// check for parenthesis
|
||||
size_t pos1 = aname.rfind("(");
|
||||
size_t pos2 = aname.rfind(")");
|
||||
if (pos1 != std::string::npos && pos2 != std::string::npos) {
|
||||
std::string name1 = aname.substr(0, pos1);
|
||||
std::string name2 = aname.substr(pos1 + 1, pos2 - 1 - pos1);
|
||||
StringUtils::trim(name1);
|
||||
StringUtils::trim(name2);
|
||||
mDisplayName.first = name1;
|
||||
mDisplayName.second = name2;
|
||||
}
|
||||
}
|
||||
|
||||
refreshDirectories();
|
||||
}
|
||||
|
||||
u8 Title::saveDataType(void)
|
||||
{
|
||||
return mSaveDataType;
|
||||
}
|
||||
|
||||
u64 Title::id(void)
|
||||
{
|
||||
return mId;
|
||||
}
|
||||
|
||||
u64 Title::saveId(void)
|
||||
{
|
||||
return mSaveId;
|
||||
}
|
||||
|
||||
void Title::saveId(u64 saveId)
|
||||
{
|
||||
mSaveId = saveId;
|
||||
}
|
||||
|
||||
AccountUid Title::userId(void)
|
||||
{
|
||||
return mUserId;
|
||||
}
|
||||
|
||||
std::string Title::userName(void)
|
||||
{
|
||||
return mUserName;
|
||||
}
|
||||
|
||||
std::string Title::author(void)
|
||||
{
|
||||
return mAuthor;
|
||||
}
|
||||
|
||||
std::string Title::name(void)
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string> Title::displayName(void)
|
||||
{
|
||||
return mDisplayName;
|
||||
}
|
||||
|
||||
std::string Title::path(void)
|
||||
{
|
||||
return mPath;
|
||||
}
|
||||
|
||||
std::string Title::fullPath(size_t index)
|
||||
{
|
||||
return mFullSavePaths.at(index);
|
||||
}
|
||||
|
||||
std::vector<std::string> Title::saves()
|
||||
{
|
||||
return mSaves;
|
||||
}
|
||||
|
||||
u64 Title::playTimeNanoseconds(void)
|
||||
{
|
||||
return mPlayTimeNanoseconds;
|
||||
}
|
||||
|
||||
std::string Title::playTime(void)
|
||||
{
|
||||
const u64 playTimeMinutes = mPlayTimeNanoseconds / 60000000000;
|
||||
return StringUtils::format("%d", playTimeMinutes / 60) + ":" + StringUtils::format("%02d", playTimeMinutes % 60) + " hours";
|
||||
}
|
||||
|
||||
void Title::playTimeNanoseconds(u64 playTimeNanoseconds)
|
||||
{
|
||||
mPlayTimeNanoseconds = playTimeNanoseconds;
|
||||
}
|
||||
|
||||
u32 Title::lastPlayedTimestamp(void)
|
||||
{
|
||||
return mLastPlayedTimestamp;
|
||||
}
|
||||
|
||||
void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp)
|
||||
{
|
||||
mLastPlayedTimestamp = lastPlayedTimestamp;
|
||||
}
|
||||
|
||||
void Title::refreshDirectories(void)
|
||||
{
|
||||
mSaves.clear();
|
||||
mFullSavePaths.clear();
|
||||
|
||||
Directory savelist(mPath);
|
||||
if (savelist.good()) {
|
||||
for (size_t i = 0, sz = savelist.size(); i < sz; i++) {
|
||||
if (savelist.folder(i)) {
|
||||
mSaves.push_back(savelist.entry(i));
|
||||
mFullSavePaths.push_back(mPath + "/" + savelist.entry(i));
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(mSaves.rbegin(), mSaves.rend());
|
||||
std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend());
|
||||
mSaves.insert(mSaves.begin(), g_emptySave);
|
||||
mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave);
|
||||
}
|
||||
else {
|
||||
Logger::getInstance().log(Logger::ERROR, "Couldn't retrieve the extdata directory list for the title " + name());
|
||||
}
|
||||
}
|
||||
|
||||
void loadTitles(void)
|
||||
{
|
||||
if (s_titlesLoaded) return;
|
||||
s_titlesLoaded = true;
|
||||
|
||||
titles.clear();
|
||||
|
||||
FsSaveDataInfoReader reader;
|
||||
FsSaveDataInfo info;
|
||||
s64 total_entries = 0;
|
||||
size_t outsize = 0;
|
||||
|
||||
NacpLanguageEntry* nle = NULL;
|
||||
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
|
||||
if (nsacd == NULL) {
|
||||
return;
|
||||
}
|
||||
memset(nsacd, 0, sizeof(NsApplicationControlData));
|
||||
|
||||
Result res = fsOpenSaveDataInfoReader(&reader, FsSaveDataSpaceId_User);
|
||||
if (R_FAILED(res)) {
|
||||
free(nsacd);
|
||||
return;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
res = fsSaveDataInfoReaderRead(&reader, &info, 1, &total_entries);
|
||||
if (R_FAILED(res) || total_entries == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (info.save_data_type == FsSaveDataType_Account) {
|
||||
u64 tid = info.application_id;
|
||||
u64 sid = info.save_data_id;
|
||||
AccountUid uid = info.uid;
|
||||
// if (mFilterIds.find(tid) == mFilterIds.end()) {
|
||||
res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd, sizeof(NsApplicationControlData), &outsize);
|
||||
if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) {
|
||||
res = nacpGetLanguageEntry(&nsacd->nacp, &nle);
|
||||
if (R_SUCCEEDED(res) && nle != NULL) {
|
||||
Title title;
|
||||
title.init(info.save_data_type, tid, uid, std::string(nle->name), std::string(nle->author));
|
||||
title.saveId(sid);
|
||||
|
||||
// load play statistics
|
||||
PdmPlayStatistics stats;
|
||||
res = pdmqryQueryPlayStatisticsByApplicationIdAndUserAccountId(tid, uid, false, &stats);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
title.playTimeNanoseconds(stats.playtime);
|
||||
title.lastPlayedTimestamp(stats.last_timestamp_user);
|
||||
}
|
||||
|
||||
// loadIcon(tid, nsacd, outsize - sizeof(nsacd->nacp));
|
||||
|
||||
// check if the vector is already created
|
||||
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
|
||||
if (it != titles.end()) {
|
||||
// found
|
||||
it->second.push_back(title);
|
||||
}
|
||||
else {
|
||||
// not found, insert into map
|
||||
std::vector<Title> v;
|
||||
v.push_back(title);
|
||||
titles.emplace(uid, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
nle = NULL;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
free(nsacd);
|
||||
fsSaveDataInfoReaderClose(&reader);
|
||||
|
||||
sortTitles();
|
||||
}
|
||||
|
||||
void sortTitles(void)
|
||||
{
|
||||
for (auto& vect : titles) {
|
||||
std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) {
|
||||
switch (g_sortMode) {
|
||||
case SORT_LAST_PLAYED:
|
||||
return l.lastPlayedTimestamp() > r.lastPlayedTimestamp();
|
||||
case SORT_PLAY_TIME:
|
||||
return l.playTimeNanoseconds() > r.playTimeNanoseconds();
|
||||
case SORT_ALPHA:
|
||||
default:
|
||||
return l.name() < r.name();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void rotateSortMode(void)
|
||||
{
|
||||
g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT);
|
||||
sortTitles();
|
||||
}
|
||||
|
||||
void getTitle(Title& dst, AccountUid uid, size_t i)
|
||||
{
|
||||
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
|
||||
if (it != titles.end() && i < getTitleCount(uid)) {
|
||||
dst = it->second.at(i);
|
||||
}
|
||||
}
|
||||
|
||||
size_t getTitleCount(AccountUid uid)
|
||||
{
|
||||
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
|
||||
return it != titles.end() ? it->second.size() : 0;
|
||||
}
|
||||
|
||||
void refreshDirectories(u64 id)
|
||||
{
|
||||
for (auto& pair : titles) {
|
||||
for (size_t i = 0; i < pair.second.size(); i++) {
|
||||
if (pair.second.at(i).id() == id) {
|
||||
pair.second.at(i).refreshDirectories();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::string> getCompleteTitleList(void)
|
||||
{
|
||||
std::unordered_map<std::string, std::string> map;
|
||||
for (const auto& pair : titles) {
|
||||
for (auto value : pair.second) {
|
||||
map.insert({StringUtils::format("0x%016llX", value.id()), value.name()});
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <nxst/infra/sys/logger.hpp>
|
||||
#include <nxst/app/main_application.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
|
||||
void servicesExit(void)
|
||||
{
|
||||
Logger::getInstance().flush();
|
||||
Account::exit();
|
||||
plExit();
|
||||
romfsExit();
|
||||
}
|
||||
|
||||
Result servicesInit(void)
|
||||
{
|
||||
io::createDirectory("sdmc:/switch");
|
||||
io::createDirectory("sdmc:/switch/NXST");
|
||||
io::createDirectory("sdmc:/switch/NXST/saves");
|
||||
|
||||
if (appletGetAppletType() != AppletType_Application) {
|
||||
Logger::getInstance().log(Logger::WARN, "Please do not run NXST in applet mode.");
|
||||
}
|
||||
|
||||
Result res = 0;
|
||||
|
||||
romfsInit();
|
||||
|
||||
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
|
||||
hidInitializeTouchScreen();
|
||||
|
||||
if (R_FAILED(res = plInitialize(PlServiceType_User))) {
|
||||
Logger::getInstance().log(Logger::ERROR, "plInitialize failed. Result code 0x%08lX.", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (R_FAILED(res = Account::init())) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Account::init failed. Result code 0x%08lX.", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (R_FAILED(res = nsInitialize())) {
|
||||
Logger::getInstance().log(Logger::ERROR, "nsInitialize failed. Result code 0x{:08X}.", res);
|
||||
return res;
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(res = hidsysInitialize())) {
|
||||
g_notificationLedAvailable = true;
|
||||
}
|
||||
else {
|
||||
Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res);
|
||||
}
|
||||
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "NXST loading completed!");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::u16string StringUtils::UTF8toUTF16(const char* src)
|
||||
{
|
||||
char16_t tmp[256] = {0};
|
||||
utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256);
|
||||
return std::u16string(tmp);
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/14094621/change-all-accented-letters-to-normal-letters-in-c
|
||||
std::string StringUtils::removeAccents(std::string str)
|
||||
{
|
||||
std::u16string src = UTF8toUTF16(str.c_str());
|
||||
const std::u16string illegal = UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ");
|
||||
const std::u16string fixed = UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy");
|
||||
|
||||
for (size_t i = 0, sz = src.length(); i < sz; i++) {
|
||||
size_t index = illegal.find(src[i]);
|
||||
if (index != std::string::npos) {
|
||||
src[i] = fixed[index];
|
||||
}
|
||||
}
|
||||
|
||||
return UTF16toUTF8(src);
|
||||
}
|
||||
|
||||
std::string StringUtils::removeNotAscii(std::string str)
|
||||
{
|
||||
for (size_t i = 0, sz = str.length(); i < sz; i++) {
|
||||
if (!isascii(str[i])) {
|
||||
str[i] = ' ';
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string StringUtils::elide(const std::string& s, size_t maxChars)
|
||||
{
|
||||
if (s.size() <= maxChars || maxChars < 6) return s;
|
||||
constexpr const char* dots = "...";
|
||||
size_t budget = maxChars - 3;
|
||||
size_t head = (budget + 1) / 2;
|
||||
size_t tail = budget - head;
|
||||
return s.substr(0, head) + dots + s.substr(s.size() - tail);
|
||||
}
|
||||
|
||||
HidsysNotificationLedPattern blinkLedPattern(u8 times)
|
||||
{
|
||||
HidsysNotificationLedPattern pattern;
|
||||
memset(&pattern, 0, sizeof(pattern));
|
||||
|
||||
pattern.baseMiniCycleDuration = 0x1; // 12.5ms.
|
||||
pattern.totalMiniCycles = 0x2; // 2 mini cycles.
|
||||
pattern.totalFullCycles = times; // Repeat n times.
|
||||
pattern.startIntensity = 0x0; // 0%.
|
||||
|
||||
pattern.miniCycles[0].ledIntensity = 0xF; // 100%.
|
||||
pattern.miniCycles[0].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
|
||||
pattern.miniCycles[0].finalStepDuration = 0x0; // Forced 12.5ms.
|
||||
pattern.miniCycles[1].ledIntensity = 0x0; // 0%.
|
||||
pattern.miniCycles[1].transitionSteps = 0xF; // 15 steps. Total 187.5ms.
|
||||
pattern.miniCycles[1].finalStepDuration = 0x0; // Forced 12.5ms.
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
void blinkLed(u8 times)
|
||||
{
|
||||
if (g_notificationLedAvailable) {
|
||||
PadState pad;
|
||||
padInitializeDefault(&pad);
|
||||
s32 n;
|
||||
HidsysUniquePadId uniquePadIds[2] = {0};
|
||||
HidsysNotificationLedPattern pattern = blinkLedPattern(times);
|
||||
memset(uniquePadIds, 0, sizeof(uniquePadIds));
|
||||
Result res = hidsysGetUniquePadsFromNpad(padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, uniquePadIds, 2, &n);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
for (s32 i = 0; i < n; i++) {
|
||||
hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/infra/fs/directory.hpp>
|
||||
|
||||
Directory::Directory(const std::string& root)
|
||||
{
|
||||
mGood = false;
|
||||
mError = 0;
|
||||
mList.clear();
|
||||
|
||||
DIR* dir = opendir(root.c_str());
|
||||
struct dirent* ent;
|
||||
|
||||
if (dir == NULL) {
|
||||
mError = (Result)errno;
|
||||
}
|
||||
else {
|
||||
while ((ent = readdir(dir))) {
|
||||
std::string name = std::string(ent->d_name);
|
||||
bool directory = ent->d_type == DT_DIR;
|
||||
struct DirectoryEntry de = {name, directory};
|
||||
mList.push_back(de);
|
||||
}
|
||||
closedir(dir);
|
||||
mGood = true;
|
||||
}
|
||||
}
|
||||
|
||||
Result Directory::error(void)
|
||||
{
|
||||
return mError;
|
||||
}
|
||||
|
||||
bool Directory::good(void)
|
||||
{
|
||||
return mGood;
|
||||
}
|
||||
|
||||
std::string Directory::entry(size_t index)
|
||||
{
|
||||
return index < mList.size() ? mList.at(index).name : "";
|
||||
}
|
||||
|
||||
bool Directory::folder(size_t index)
|
||||
{
|
||||
return index < mList.size() ? mList.at(index).directory : false;
|
||||
}
|
||||
|
||||
size_t Directory::size(void)
|
||||
{
|
||||
return mList.size();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/infra/fs/filesystem.hpp>
|
||||
|
||||
Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID)
|
||||
{
|
||||
return fsOpen_SaveData(fileSystem, titleID, userID);
|
||||
}
|
||||
|
||||
int FileSystem::mount(FsFileSystem fs)
|
||||
{
|
||||
return fsdevMountDevice("save", fs);
|
||||
}
|
||||
|
||||
void FileSystem::unmount(void)
|
||||
{
|
||||
fsdevUnmountDevice("save");
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/infra/fs/io.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
#include <nxst/infra/sys/logger.hpp>
|
||||
|
||||
bool io::fileExists(const std::string& path)
|
||||
{
|
||||
struct stat buffer;
|
||||
return (stat(path.c_str(), &buffer) == 0);
|
||||
}
|
||||
|
||||
void io::copyFile(const std::string& srcPath, const std::string& dstPath)
|
||||
{
|
||||
g_isTransferringFile = true;
|
||||
|
||||
FILE* src = fopen(srcPath.c_str(), "rb");
|
||||
if (src == NULL) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to open source file " + srcPath + " during copy with errno %d. Skipping...", errno);
|
||||
return;
|
||||
}
|
||||
FILE* dst = fopen(dstPath.c_str(), "wb");
|
||||
if (dst == NULL) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to open destination file " + dstPath + " during copy with errno " + std::to_string(errno) + ". Skipping...");
|
||||
fclose(src);
|
||||
return;
|
||||
}
|
||||
|
||||
fseek(src, 0, SEEK_END);
|
||||
u64 sz = ftell(src);
|
||||
rewind(src);
|
||||
|
||||
u8* buf = new u8[BUFFER_SIZE];
|
||||
u64 offset = 0;
|
||||
|
||||
size_t slashpos = srcPath.rfind("/");
|
||||
g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1);
|
||||
|
||||
while (offset < sz) {
|
||||
u32 count = fread((char*)buf, 1, BUFFER_SIZE, src);
|
||||
if (count == 0) {
|
||||
Logger::getInstance().log(Logger::ERROR, "fread returned 0 for file {} at offset {}/{} with errno {}. Aborting copy.", srcPath, offset, sz, errno);
|
||||
break;
|
||||
}
|
||||
fwrite((char*)buf, 1, count, dst);
|
||||
offset += count;
|
||||
}
|
||||
|
||||
delete[] buf;
|
||||
fclose(src);
|
||||
fclose(dst);
|
||||
|
||||
if (dstPath.rfind("save:/", 0) == 0) {
|
||||
fsdevCommitDevice("save");
|
||||
}
|
||||
|
||||
g_isTransferringFile = false;
|
||||
}
|
||||
|
||||
Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
|
||||
{
|
||||
Result res = 0;
|
||||
bool quit = false;
|
||||
Directory items(srcPath);
|
||||
|
||||
if (!items.good()) {
|
||||
return items.error();
|
||||
}
|
||||
|
||||
for (size_t i = 0, sz = items.size(); i < sz && !quit; i++) {
|
||||
std::string newsrc = srcPath + items.entry(i);
|
||||
std::string newdst = dstPath + items.entry(i);
|
||||
|
||||
if (items.folder(i)) {
|
||||
res = io::createDirectory(newdst);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
newsrc += "/";
|
||||
newdst += "/";
|
||||
res = io::copyDirectory(newsrc, newdst);
|
||||
}
|
||||
else {
|
||||
quit = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
io::copyFile(newsrc, newdst);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Result io::createDirectory(const std::string& path)
|
||||
{
|
||||
mkdir(path.c_str(), 0777);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool io::directoryExists(const std::string& path)
|
||||
{
|
||||
struct stat sb;
|
||||
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
|
||||
}
|
||||
|
||||
Result io::deleteFolderRecursively(const std::string& path)
|
||||
{
|
||||
Directory dir(path);
|
||||
if (!dir.good()) {
|
||||
return dir.error();
|
||||
}
|
||||
|
||||
for (size_t i = 0, sz = dir.size(); i < sz; i++) {
|
||||
if (dir.folder(i)) {
|
||||
std::string newpath = path + "/" + dir.entry(i) + "/";
|
||||
deleteFolderRecursively(newpath);
|
||||
newpath = path + dir.entry(i);
|
||||
rmdir(newpath.c_str());
|
||||
}
|
||||
else {
|
||||
std::string newpath = path + dir.entry(i);
|
||||
std::remove(newpath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir(path.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::tuple<bool, Result, std::string> io::backup(size_t index, AccountUid uid)
|
||||
{
|
||||
Result res = 0;
|
||||
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
|
||||
Title title;
|
||||
getTitle(title, uid, index);
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
|
||||
FsFileSystem fileSystem;
|
||||
res = FileSystem::mount(&fileSystem, title.id(), title.userId());
|
||||
if (R_SUCCEEDED(res)) {
|
||||
int rc = FileSystem::mount(fileSystem);
|
||||
if (rc == -1) {
|
||||
fsFsClose(&fileSystem);
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during backup. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
return std::make_tuple(false, -2, "Failed to mount save.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger::getInstance().log(Logger::ERROR,
|
||||
"Failed to mount filesystem during backup with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
return std::make_tuple(false, res, "Failed to mount save.");
|
||||
}
|
||||
|
||||
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId())));
|
||||
|
||||
io::createDirectory(title.path());
|
||||
std::string dstPath = title.path() + "/" + suggestion;
|
||||
|
||||
// Write to a temp dir first; rename on success so the existing backup
|
||||
// is never destroyed if the copy is interrupted mid-way.
|
||||
std::string tmpPath = dstPath + ".tmp";
|
||||
if (io::directoryExists(tmpPath)) {
|
||||
io::deleteFolderRecursively((tmpPath + "/").c_str());
|
||||
}
|
||||
io::createDirectory(tmpPath);
|
||||
res = io::copyDirectory("save:/", tmpPath + "/");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
io::deleteFolderRecursively((tmpPath + "/").c_str());
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory to " + tmpPath + " with result 0x%08lX.", res);
|
||||
return std::make_tuple(false, res, "Failed to backup save.");
|
||||
}
|
||||
|
||||
// Swap: delete old backup only after new one is fully written.
|
||||
if (io::directoryExists(dstPath)) {
|
||||
io::deleteFolderRecursively((dstPath + "/").c_str());
|
||||
}
|
||||
if (rename(tmpPath.c_str(), dstPath.c_str()) != 0) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to rename temp backup to " + dstPath);
|
||||
return std::make_tuple(false, (Result)-1, "Failed to finalise backup.");
|
||||
}
|
||||
|
||||
refreshDirectories(title.id());
|
||||
|
||||
FileSystem::unmount();
|
||||
|
||||
ret = std::make_tuple(true, 0, dstPath);
|
||||
Logger::getInstance().log(Logger::INFO, "Backup succeeded.");
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::tuple<bool, Result, std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell)
|
||||
{
|
||||
Result res = 0;
|
||||
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
|
||||
Title title;
|
||||
getTitle(title, uid, index);
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
|
||||
// If save data does not yet exist (game was never launched), create it via NACP.
|
||||
// fsCreateSaveDataFileSystem returns an error if the save already exists — this is expected.
|
||||
{
|
||||
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
|
||||
if (nsacd != NULL) {
|
||||
memset(nsacd, 0, sizeof(NsApplicationControlData));
|
||||
size_t outsize = 0;
|
||||
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title.id(), nsacd, sizeof(NsApplicationControlData), &outsize))) {
|
||||
static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail};
|
||||
|
||||
FsSaveDataAttribute attr = {};
|
||||
attr.application_id = title.id();
|
||||
attr.uid = uid;
|
||||
attr.save_data_type = FsSaveDataType_Account;
|
||||
attr.save_data_rank = FsSaveDataRank_Primary;
|
||||
|
||||
FsSaveDataCreationInfo createInfo = {};
|
||||
createInfo.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
|
||||
createInfo.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
|
||||
createInfo.available_size = 0x4000;
|
||||
createInfo.owner_id = nsacd->nacp.save_data_owner_id;
|
||||
createInfo.save_data_space_id = FsSaveDataSpaceId_User;
|
||||
|
||||
fsCreateSaveDataFileSystem(&attr, &createInfo, &meta);
|
||||
}
|
||||
free(nsacd);
|
||||
}
|
||||
}
|
||||
|
||||
FsFileSystem fileSystem;
|
||||
res = FileSystem::mount(&fileSystem, title.id(), uid);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
int rc = FileSystem::mount(fileSystem);
|
||||
if (rc == -1) {
|
||||
fsFsClose(&fileSystem);
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
|
||||
uid.uid[1], uid.uid[0]);
|
||||
return std::make_tuple(false, -2, "Failed to mount save.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger::getInstance().log(Logger::ERROR,
|
||||
"Failed to mount filesystem during restore with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
|
||||
uid.uid[1], uid.uid[0]);
|
||||
return std::make_tuple(false, res, "Failed to mount save.");
|
||||
}
|
||||
|
||||
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid)));
|
||||
std::string srcPath = title.path() + "/" + suggestion + "/";
|
||||
std::string dstPath = "save:/";
|
||||
|
||||
// Validate source exists and is non-empty before touching live save data.
|
||||
{
|
||||
Directory srcCheck(srcPath);
|
||||
if (!srcCheck.good() || srcCheck.size() == 0) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Restore source is empty or missing: " + srcPath);
|
||||
return std::make_tuple(false, (Result)-1, "Restore source is empty or missing.");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Directory saveRoot(dstPath);
|
||||
for (size_t i = 0, sz = saveRoot.size(); i < sz; i++) {
|
||||
if (saveRoot.folder(i)) {
|
||||
io::deleteFolderRecursively((dstPath + saveRoot.entry(i) + "/").c_str());
|
||||
rmdir((dstPath + saveRoot.entry(i)).c_str());
|
||||
} else {
|
||||
std::remove((dstPath + saveRoot.entry(i)).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = fsdevCommitDevice("save");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to commit save after clearing with result 0x%08lX.", res);
|
||||
return std::make_tuple(false, res, "Failed to commit save after delete.");
|
||||
}
|
||||
|
||||
res = io::copyDirectory(srcPath, dstPath);
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory " + srcPath + " to " + dstPath + " with result 0x%08lX. Skipping...", res);
|
||||
return std::make_tuple(false, res, "Failed to restore save.");
|
||||
}
|
||||
|
||||
res = fsdevCommitDevice("save");
|
||||
if (R_FAILED(res)) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to commit save with result 0x%08lX.", res);
|
||||
return std::make_tuple(false, res, "Failed to commit to save device.");
|
||||
}
|
||||
else {
|
||||
blinkLed(4);
|
||||
ret = std::make_tuple(true, 0, nameFromCell + "\nhas been restored successfully.");
|
||||
}
|
||||
|
||||
FileSystem::unmount();
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Restore succeeded.");
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// Logic moved to src/service/transfer_service.cpp
|
||||
@@ -0,0 +1 @@
|
||||
// Logic moved to src/service/transfer_service.cpp
|
||||
@@ -0,0 +1,64 @@
|
||||
#include <nxst/infra/sys/logger.hpp>
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex g_log_mutex;
|
||||
|
||||
#if defined(__SWITCH__)
|
||||
constexpr const char* kLogPath = "/switch/NXST/log.log";
|
||||
#else
|
||||
constexpr const char* kLogPath = "nxst.log";
|
||||
#endif
|
||||
|
||||
void writeEntry(const char* tag, const char* fmt, va_list args)
|
||||
{
|
||||
char msg[2048];
|
||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||
|
||||
time_t now = time(nullptr);
|
||||
struct tm tm_buf;
|
||||
localtime_r(&now, &tm_buf);
|
||||
char time_str[16];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M:%S", &tm_buf);
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_log_mutex);
|
||||
|
||||
fprintf(stderr, "[%s]%s %s\n", time_str, tag, msg);
|
||||
|
||||
FILE* log_file = fopen(kLogPath, "a");
|
||||
if (log_file != nullptr) {
|
||||
fprintf(log_file, "[%s]%s %s\n", time_str, tag, msg);
|
||||
fclose(log_file);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace nxst::log {
|
||||
|
||||
void write(Level level, const char* fmt, ...)
|
||||
{
|
||||
const char* tag = "[INFO] ";
|
||||
switch (level) {
|
||||
case Level::Debug: tag = "[DEBUG]"; break;
|
||||
case Level::Info: tag = "[INFO] "; break;
|
||||
case Level::Warn: tag = "[WARN] "; break;
|
||||
case Level::Error: tag = "[ERROR]"; break;
|
||||
}
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
writeEntry(tag, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void debug(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[DEBUG]", fmt, args); va_end(args); }
|
||||
void info (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[INFO] ", fmt, args); va_end(args); }
|
||||
void warn (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[WARN] ", fmt, args); va_end(args); }
|
||||
void error(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[ERROR]", fmt, args); va_end(args); }
|
||||
|
||||
} // namespace nxst::log
|
||||
@@ -0,0 +1,483 @@
|
||||
#include <nxst/service/transfer_service.hpp>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
#include <nxst/infra/fs/io.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <nxst/domain/account.hpp>
|
||||
#endif
|
||||
|
||||
#include <nxst/domain/protocol.hpp>
|
||||
#include <nxst/infra/net/socket.hpp>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace nxst {
|
||||
|
||||
// ─── File-transfer helpers ────────────────────────────────────────────────────
|
||||
|
||||
static bool sendAll(int sock, const void* buf, size_t len) {
|
||||
size_t sent = 0;
|
||||
while (sent < len) {
|
||||
ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0);
|
||||
if (n <= 0) return false;
|
||||
sent += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool recvAll(int sock, void* buf, size_t len) {
|
||||
size_t got = 0;
|
||||
while (got < len) {
|
||||
ssize_t n = read(sock, static_cast<char*>(buf) + got, len - got);
|
||||
if (n <= 0) return false;
|
||||
got += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool sendFile(int sock, const fs::path& filepath, TransferState& state) {
|
||||
std::ifstream infile(filepath, std::ios::binary | std::ios::ate);
|
||||
if (!infile.is_open()) return false;
|
||||
|
||||
uint32_t filename_len = (uint32_t)filepath.string().size();
|
||||
uint64_t file_size = (uint64_t)infile.tellg();
|
||||
infile.seekg(0, std::ios::beg);
|
||||
|
||||
if (!sendAll(sock, &filename_len, sizeof(filename_len))) return false;
|
||||
if (!sendAll(sock, filepath.c_str(), filename_len)) return false;
|
||||
if (!sendAll(sock, &file_size, sizeof(file_size))) return false;
|
||||
|
||||
std::vector<char> buffer(proto::BUF_SIZE);
|
||||
uint64_t remaining = file_size;
|
||||
while (remaining > 0) {
|
||||
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
|
||||
infile.read(buffer.data(), (std::streamsize)to_read);
|
||||
std::streamsize count = infile.gcount();
|
||||
if (count <= 0) break;
|
||||
if (!sendAll(sock, buffer.data(), (size_t)count)) return false;
|
||||
state.bytes_done.fetch_add((uint64_t)count);
|
||||
remaining -= (uint64_t)count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void mkdirs(const std::string& path) {
|
||||
for (size_t i = 1; i < path.size(); i++) {
|
||||
if (path[i] == '/') {
|
||||
std::string component = path.substr(0, i);
|
||||
mkdir(component.c_str(), 0777);
|
||||
}
|
||||
}
|
||||
mkdir(path.c_str(), 0777);
|
||||
}
|
||||
|
||||
static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size,
|
||||
TransferState& state) {
|
||||
size_t last_slash = rel_path.rfind('/');
|
||||
if (last_slash != std::string::npos) {
|
||||
std::string dir = rel_path.substr(0, last_slash);
|
||||
if (!dir.empty()) mkdirs(dir);
|
||||
}
|
||||
|
||||
FILE* outfile = fopen(rel_path.c_str(), "wb");
|
||||
if (!outfile) {
|
||||
std::vector<char> drain(proto::BUF_SIZE);
|
||||
uint64_t remaining = file_size;
|
||||
while (remaining > 0) {
|
||||
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
|
||||
ssize_t n = read(sock, drain.data(), to_read);
|
||||
if (n <= 0) break;
|
||||
remaining -= (uint64_t)n;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.bytes_total.store(file_size);
|
||||
state.bytes_done.store(0);
|
||||
|
||||
std::vector<char> buffer(proto::BUF_SIZE);
|
||||
uint64_t total = 0;
|
||||
while (total < file_size) {
|
||||
size_t to_read = (size_t)std::min(file_size - total, (uint64_t)proto::BUF_SIZE);
|
||||
ssize_t n = read(sock, buffer.data(), to_read);
|
||||
if (n <= 0) break;
|
||||
fwrite(buffer.data(), 1, (size_t)n, outfile);
|
||||
total += (uint64_t)n;
|
||||
state.bytes_done.store(total);
|
||||
}
|
||||
fclose(outfile);
|
||||
}
|
||||
|
||||
// ─── Sender ──────────────────────────────────────────────────────────────────
|
||||
|
||||
void TransferService::failSend(const std::string& reason) {
|
||||
sender_state.fail_reason = reason;
|
||||
sender_state.connection_failed.store(true);
|
||||
sender_state.done.store(true);
|
||||
}
|
||||
|
||||
int TransferService::findServer(char* out_ip) {
|
||||
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (udp_fd < 0) return -1;
|
||||
sender_udp_sock.store(udp_fd);
|
||||
|
||||
auto releaseUdp = [&]() {
|
||||
int owned = sender_udp_sock.exchange(-1);
|
||||
if (owned == udp_fd) close(udp_fd);
|
||||
};
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(proto::MULTICAST_PORT);
|
||||
addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP);
|
||||
|
||||
if (sendto(udp_fd, "DISCOVER_SERVER", 15, 0, (sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
releaseUdp();
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Poll in 100ms slices so cancel races within 100ms
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
if (sender_state.cancelled.load()) { releaseUdp(); return -1; }
|
||||
struct timeval tv{0, 100000};
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(udp_fd, &fds);
|
||||
if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) {
|
||||
sockaddr_in from{};
|
||||
socklen_t fromlen = sizeof(from);
|
||||
char buf[256];
|
||||
ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
|
||||
if (n > 0) {
|
||||
buf[n] = '\0';
|
||||
if (strcmp(buf, "SERVER_HERE") == 0) {
|
||||
inet_ntop(AF_INET, &from.sin_addr, out_ip, INET_ADDRSTRLEN);
|
||||
releaseUdp();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
releaseUdp();
|
||||
return -1;
|
||||
}
|
||||
|
||||
void* TransferService::senderEntry(void* arg) {
|
||||
auto* a = static_cast<SenderArgs*>(arg);
|
||||
TransferService* svc = a->svc;
|
||||
size_t idx = a->title_index;
|
||||
AccountUid uid = a->uid;
|
||||
delete a;
|
||||
svc->runSender(idx, uid);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TransferService::runSender(size_t title_index, AccountUid uid) {
|
||||
sender_active.store(true);
|
||||
|
||||
auto finish = [this]() {
|
||||
sender_state.done.store(true);
|
||||
sender_active.store(false);
|
||||
};
|
||||
|
||||
char server_ip[INET_ADDRSTRLEN];
|
||||
if (findServer(server_ip) != 0) {
|
||||
if (!sender_state.cancelled.load())
|
||||
failSend("No receiver found.\nMake sure the other Switch is in Receive mode.");
|
||||
return finish();
|
||||
}
|
||||
if (sender_state.cancelled.load()) return finish();
|
||||
|
||||
sender_state.setStatus("Creating backup...");
|
||||
#ifdef __SWITCH__
|
||||
auto backup_result = io::backup(title_index, uid);
|
||||
if (!std::get<0>(backup_result)) {
|
||||
failSend("Failed to create backup:\n" + std::get<2>(backup_result));
|
||||
return finish();
|
||||
}
|
||||
fs::path directory = std::get<2>(backup_result);
|
||||
#else
|
||||
fs::path directory = ".";
|
||||
(void)title_index; (void)uid;
|
||||
#endif
|
||||
|
||||
if (sender_state.cancelled.load()) return finish();
|
||||
|
||||
sender_state.setStatus("Connecting...");
|
||||
int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (tcp_fd < 0) { failSend("Failed to open socket."); return finish(); }
|
||||
sender_tcp_sock.store(tcp_fd);
|
||||
|
||||
auto releaseTcp = [&]() {
|
||||
int owned = sender_tcp_sock.exchange(-1);
|
||||
if (owned == tcp_fd) close(tcp_fd);
|
||||
};
|
||||
|
||||
sockaddr_in serv{};
|
||||
serv.sin_family = AF_INET;
|
||||
serv.sin_port = htons(proto::TCP_PORT);
|
||||
if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0 ||
|
||||
connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) {
|
||||
if (!sender_state.cancelled.load())
|
||||
failSend("Failed to connect to receiver.");
|
||||
releaseTcp();
|
||||
return finish();
|
||||
}
|
||||
|
||||
uint64_t total = 0;
|
||||
for (const auto& entry : fs::recursive_directory_iterator(directory))
|
||||
if (fs::is_regular_file(entry.path()))
|
||||
total += fs::file_size(entry.path());
|
||||
sender_state.bytes_total.store(total);
|
||||
|
||||
for (const auto& entry : fs::recursive_directory_iterator(directory)) {
|
||||
if (sender_state.cancelled.load()) break;
|
||||
const fs::path& p = entry.path();
|
||||
if (fs::is_regular_file(p)) {
|
||||
sender_state.setStatus(p.filename().string());
|
||||
if (!sendFile(tcp_fd, p, sender_state)) break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t sentinel = proto::EOF_SENTINEL;
|
||||
sendAll(tcp_fd, &sentinel, sizeof(sentinel));
|
||||
|
||||
releaseTcp();
|
||||
sender_state.setStatus("");
|
||||
return finish();
|
||||
}
|
||||
|
||||
int TransferService::startSend(size_t title_index, AccountUid uid) {
|
||||
sender_state.reset();
|
||||
sender_state.setStatus("Searching for receiver...");
|
||||
|
||||
auto* arg = new SenderArgs{this, title_index, uid};
|
||||
pthread_t thread;
|
||||
if (pthread_create(&thread, nullptr, senderEntry, arg) != 0) {
|
||||
delete arg;
|
||||
return -1;
|
||||
}
|
||||
pthread_detach(thread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TransferService::cancelSend() {
|
||||
sender_state.cancelled.store(true);
|
||||
int udp = sender_udp_sock.exchange(-1);
|
||||
if (udp >= 0) { shutdown(udp, SHUT_RDWR); close(udp); }
|
||||
int tcp = sender_tcp_sock.exchange(-1);
|
||||
if (tcp >= 0) { shutdown(tcp, SHUT_RDWR); close(tcp); }
|
||||
}
|
||||
|
||||
// ─── Receiver ────────────────────────────────────────────────────────────────
|
||||
|
||||
std::string TransferService::replaceUsername(const std::string& file_path) const {
|
||||
#ifdef __SWITCH__
|
||||
std::string username = StringUtils::removeNotAscii(
|
||||
StringUtils::removeAccents(Account::username(restore_uid)));
|
||||
size_t last_slash = file_path.rfind('/');
|
||||
if (last_slash == std::string::npos) return file_path;
|
||||
size_t prev_slash = file_path.rfind('/', last_slash - 1);
|
||||
if (prev_slash == std::string::npos)
|
||||
return username + file_path.substr(last_slash);
|
||||
return file_path.substr(0, prev_slash + 1) + username + file_path.substr(last_slash);
|
||||
#else
|
||||
return file_path;
|
||||
#endif
|
||||
}
|
||||
|
||||
void* TransferService::broadcastEntry(void* arg) {
|
||||
static_cast<TransferService*>(arg)->runBroadcast();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TransferService::runBroadcast() {
|
||||
receiver_broadcast_active.store(true);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr);
|
||||
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
|
||||
|
||||
int udp = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (udp < 0) { receiver_broadcast_active.store(false); return; }
|
||||
receiver_bcast_sock.store(udp);
|
||||
|
||||
auto releaseUdp = [&]() {
|
||||
int owned = receiver_bcast_sock.exchange(-1);
|
||||
if (owned == udp) close(udp);
|
||||
};
|
||||
|
||||
struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit
|
||||
setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
addr.sin_port = htons(proto::MULTICAST_PORT);
|
||||
|
||||
if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
releaseUdp();
|
||||
receiver_broadcast_active.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ip_mreq group{};
|
||||
group.imr_multiaddr.s_addr = inet_addr(proto::MULTICAST_GROUP);
|
||||
group.imr_interface.s_addr = htonl(INADDR_ANY);
|
||||
if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) {
|
||||
releaseUdp();
|
||||
receiver_broadcast_active.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[256];
|
||||
sockaddr_in from{};
|
||||
socklen_t fromlen = sizeof(from);
|
||||
while (true) {
|
||||
ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
|
||||
if (n < 0) {
|
||||
if (receiver_state.cancelled.load()) break;
|
||||
continue;
|
||||
}
|
||||
buf[n] = '\0';
|
||||
if (strcmp(buf, "DISCOVER_SERVER") == 0) {
|
||||
sendto(udp, "SERVER_HERE", 11, 0, (sockaddr*)&from, fromlen);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
releaseUdp();
|
||||
receiver_broadcast_active.store(false);
|
||||
}
|
||||
|
||||
void* TransferService::acceptEntry(void* arg) {
|
||||
auto* a = static_cast<AcceptArgs*>(arg);
|
||||
TransferService* svc = a->svc;
|
||||
int server_fd = a->server_fd;
|
||||
delete a;
|
||||
svc->runAccept(server_fd);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TransferService::runAccept(int server_fd) {
|
||||
receiver_accept_active.store(true);
|
||||
receiver_listen_sock.store(server_fd);
|
||||
|
||||
sockaddr_in client_addr{};
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len);
|
||||
|
||||
int owned_listen = receiver_listen_sock.exchange(-1);
|
||||
if (owned_listen == server_fd) close(server_fd);
|
||||
|
||||
if (client_sock >= 0) {
|
||||
receiver_client_sock.store(client_sock);
|
||||
|
||||
while (true) {
|
||||
uint32_t filename_len = 0;
|
||||
if (!recvAll(client_sock, &filename_len, sizeof(filename_len))) break;
|
||||
if (filename_len == proto::EOF_SENTINEL) break;
|
||||
if (filename_len > proto::MAX_FILENAME) break;
|
||||
|
||||
std::vector<char> filename_buf(filename_len + 1, '\0');
|
||||
if (!recvAll(client_sock, filename_buf.data(), filename_len)) break;
|
||||
std::string filename_str(filename_buf.data(), filename_len);
|
||||
filename_str = replaceUsername(filename_str);
|
||||
|
||||
{
|
||||
size_t sl = filename_str.rfind('/');
|
||||
receiver_state.setStatus(
|
||||
sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str);
|
||||
}
|
||||
|
||||
uint64_t file_size = 0;
|
||||
if (!recvAll(client_sock, &file_size, sizeof(file_size))) break;
|
||||
receiveFile(client_sock, filename_str, file_size, receiver_state);
|
||||
}
|
||||
|
||||
int owned = receiver_client_sock.exchange(-1);
|
||||
if (owned == client_sock) close(client_sock);
|
||||
|
||||
if (!receiver_state.cancelled.load()) {
|
||||
#ifdef __SWITCH__
|
||||
receiver_state.setStatus("Restoring...");
|
||||
auto result = io::restore(restore_title_index, restore_uid, 0, restore_title_name);
|
||||
restore_ok = std::get<0>(result);
|
||||
restore_error = restore_ok ? "" : std::get<2>(result);
|
||||
#else
|
||||
restore_ok = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
receiver_state.done.store(true);
|
||||
receiver_accept_active.store(false);
|
||||
}
|
||||
|
||||
int TransferService::startReceive(size_t title_index, AccountUid uid, std::string title_name) {
|
||||
receiver_state.reset();
|
||||
receiver_state.setStatus("Waiting for connection...");
|
||||
restore_title_index = title_index;
|
||||
restore_uid = uid;
|
||||
restore_title_name = std::move(title_name);
|
||||
restore_ok = false;
|
||||
restore_error.clear();
|
||||
|
||||
pthread_t bcast_thread;
|
||||
if (pthread_create(&bcast_thread, nullptr, broadcastEntry, this) != 0) return 1;
|
||||
receiver_bcast_thread = bcast_thread;
|
||||
pthread_detach(bcast_thread);
|
||||
|
||||
Socket server(socket(AF_INET, SOCK_STREAM, 0));
|
||||
if (!server.valid()) { cancelReceive(); return 1; }
|
||||
|
||||
int yes = 1;
|
||||
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
addr.sin_port = htons(proto::TCP_PORT);
|
||||
|
||||
if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 ||
|
||||
listen(server, 3) < 0) {
|
||||
cancelReceive();
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto* acc_args = new AcceptArgs{this, server.fd};
|
||||
pthread_t accept_thread;
|
||||
if (pthread_create(&accept_thread, nullptr, acceptEntry, acc_args) != 0) {
|
||||
delete acc_args;
|
||||
cancelReceive();
|
||||
return 1;
|
||||
}
|
||||
pthread_detach(accept_thread);
|
||||
server.release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TransferService::cancelReceive() {
|
||||
receiver_state.cancelled.store(true);
|
||||
int sock = receiver_client_sock.exchange(-1);
|
||||
if (sock >= 0) { shutdown(sock, SHUT_RDWR); close(sock); }
|
||||
int lsock = receiver_listen_sock.exchange(-1);
|
||||
if (lsock >= 0) { shutdown(lsock, SHUT_RDWR); close(lsock); }
|
||||
int bsock = receiver_bcast_sock.exchange(-1);
|
||||
if (bsock >= 0) { shutdown(bsock, SHUT_RDWR); close(bsock); }
|
||||
if (receiver_broadcast_active.load()) pthread_cancel(receiver_bcast_thread);
|
||||
}
|
||||
|
||||
} // namespace nxst
|
||||
@@ -0,0 +1,310 @@
|
||||
#include <nxst/app/main_application.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <nxst/ui/transfer_overlay.hpp>
|
||||
#include <nxst/ui/const.h>
|
||||
|
||||
namespace ui {
|
||||
extern MainApplication *mainApp;
|
||||
|
||||
namespace {
|
||||
constexpr int ListX = theme::space::lg;
|
||||
constexpr int ListW = 760;
|
||||
constexpr int PanelX = ListX + ListW + theme::space::xl;
|
||||
constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg;
|
||||
constexpr int ContentY = theme::layout::ContentTop + theme::space::md;
|
||||
constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md;
|
||||
constexpr int BtnH = 56;
|
||||
constexpr int BtnW = PanelW - 2 * theme::space::lg;
|
||||
}
|
||||
|
||||
TitlesLayout::TitlesLayout() : Layout::Layout() {
|
||||
using namespace theme;
|
||||
|
||||
this->titlesMenu = pu::ui::elm::Menu::New(
|
||||
ListX, ContentY, ListW,
|
||||
color::BgBase, color::BgSurface2,
|
||||
88, 6);
|
||||
this->titlesMenu->SetScrollbarColor(color::Primary);
|
||||
this->titlesMenu->SetItemsFocusColor(color::BgSurface2);
|
||||
this->titlesMenu->SetOnSelectionChanged([this]() { this->refreshPanel(); });
|
||||
this->SetBackgroundColor(color::BgBase);
|
||||
this->Add(this->titlesMenu);
|
||||
|
||||
this->panelBg = pu::ui::elm::Rectangle::New(
|
||||
PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg);
|
||||
this->Add(this->panelBg);
|
||||
|
||||
this->panelTitle = pu::ui::elm::TextBlock::New(
|
||||
PanelX + space::lg, ContentY + space::lg, "");
|
||||
this->panelTitle->SetFont(type::font(type::Title));
|
||||
this->panelTitle->SetColor(color::TextPrimary);
|
||||
this->Add(this->panelTitle);
|
||||
|
||||
this->panelHint = pu::ui::elm::TextBlock::New(
|
||||
PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:");
|
||||
this->panelHint->SetFont(type::font(type::Body));
|
||||
this->panelHint->SetColor(color::TextSecondary);
|
||||
this->Add(this->panelHint);
|
||||
|
||||
int btnY = ContentY + 200;
|
||||
this->btnTransferBg = pu::ui::elm::Rectangle::New(
|
||||
PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md);
|
||||
this->Add(this->btnTransferBg);
|
||||
this->btnTransferText = pu::ui::elm::TextBlock::New(
|
||||
PanelX + space::lg + space::md, btnY + 14, "Transfer to another device");
|
||||
this->btnTransferText->SetFont(type::font(type::Body));
|
||||
this->btnTransferText->SetColor(color::TextSecondary);
|
||||
this->Add(this->btnTransferText);
|
||||
|
||||
int btnY2 = btnY + BtnH + space::md;
|
||||
this->btnReceiveBg = pu::ui::elm::Rectangle::New(
|
||||
PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md);
|
||||
this->Add(this->btnReceiveBg);
|
||||
this->btnReceiveText = pu::ui::elm::TextBlock::New(
|
||||
PanelX + space::lg + space::md, btnY2 + 14, "Receive from another device");
|
||||
this->btnReceiveText->SetFont(type::font(type::Body));
|
||||
this->btnReceiveText->SetColor(color::TextSecondary);
|
||||
this->Add(this->btnReceiveText);
|
||||
|
||||
this->panelFooter = pu::ui::elm::TextBlock::New(
|
||||
PanelX + space::lg,
|
||||
ContentY + ContentH - space::lg - 18,
|
||||
"Save data only");
|
||||
this->panelFooter->SetFont(type::font(type::Caption));
|
||||
this->panelFooter->SetColor(color::TextMuted);
|
||||
this->Add(this->panelFooter);
|
||||
|
||||
this->emptyText = pu::ui::elm::TextBlock::New(
|
||||
ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40,
|
||||
"No save data on this profile");
|
||||
this->emptyText->SetFont(type::font(type::Display));
|
||||
this->emptyText->SetColor(color::TextPrimary);
|
||||
this->emptyText->SetVisible(false);
|
||||
this->Add(this->emptyText);
|
||||
|
||||
this->emptySub = pu::ui::elm::TextBlock::New(
|
||||
ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16,
|
||||
"Play something first, then come back.");
|
||||
this->emptySub->SetFont(type::font(type::Body));
|
||||
this->emptySub->SetColor(color::TextMuted);
|
||||
this->emptySub->SetVisible(false);
|
||||
this->Add(this->emptySub);
|
||||
|
||||
this->header = std::make_unique<HeaderBar>(this, "Save Transfer");
|
||||
this->hints = std::make_unique<HintBar>(this);
|
||||
this->updateHints();
|
||||
}
|
||||
|
||||
void TitlesLayout::InitTitles(AccountUid uid) {
|
||||
using namespace theme;
|
||||
this->current_uid = uid;
|
||||
|
||||
auto it = this->menuCache.find(uid);
|
||||
std::vector<pu::ui::elm::MenuItem::Ref>* items;
|
||||
if (it != this->menuCache.end()) {
|
||||
items = &it->second;
|
||||
} else {
|
||||
std::vector<pu::ui::elm::MenuItem::Ref> built;
|
||||
for (size_t i = 0; i < getTitleCount(uid); i++) {
|
||||
Title title;
|
||||
getTitle(title, uid, i);
|
||||
auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str());
|
||||
titleItem->SetColor(color::TextPrimary);
|
||||
built.push_back(titleItem);
|
||||
}
|
||||
auto inserted = this->menuCache.emplace(uid, std::move(built));
|
||||
items = &inserted.first->second;
|
||||
}
|
||||
|
||||
this->titlesMenu->ClearItems();
|
||||
for (auto& item : *items) {
|
||||
this->titlesMenu->AddItem(item);
|
||||
}
|
||||
this->titlesMenu->SetSelectedIndex(0);
|
||||
|
||||
const bool empty = items->empty();
|
||||
this->titlesMenu->SetVisible(!empty);
|
||||
this->panelBg->SetVisible(!empty);
|
||||
this->panelTitle->SetVisible(!empty);
|
||||
this->panelHint->SetVisible(!empty);
|
||||
this->btnTransferBg->SetVisible(!empty);
|
||||
this->btnTransferText->SetVisible(!empty);
|
||||
this->btnReceiveBg->SetVisible(!empty);
|
||||
this->btnReceiveText->SetVisible(!empty);
|
||||
this->panelFooter->SetVisible(!empty);
|
||||
this->emptyText->SetVisible(empty);
|
||||
this->emptySub->SetVisible(empty);
|
||||
|
||||
this->focus = TitlesFocus::List;
|
||||
this->action = TitlesAction::Transfer;
|
||||
this->refreshPanel();
|
||||
this->refreshButtons();
|
||||
this->updateHints();
|
||||
|
||||
this->header->SetUser(uid, Account::username(uid));
|
||||
}
|
||||
|
||||
void TitlesLayout::refreshPanel() {
|
||||
if (this->titlesMenu->GetItems().empty()) return;
|
||||
int idx = this->titlesMenu->GetSelectedIndex();
|
||||
Title title;
|
||||
getTitle(title, this->current_uid, idx);
|
||||
this->panelTitle->SetText(StringUtils::elide(title.name(), 24));
|
||||
}
|
||||
|
||||
void TitlesLayout::refreshButtons() {
|
||||
using namespace theme;
|
||||
const bool active = (focus == TitlesFocus::Actions);
|
||||
if (active && action == TitlesAction::Transfer) {
|
||||
this->btnTransferBg->SetColor(color::Primary);
|
||||
this->btnTransferText->SetColor(color::TextPrimary);
|
||||
this->btnReceiveBg->SetColor(color::BgSurface2);
|
||||
this->btnReceiveText->SetColor(color::TextSecondary);
|
||||
} else if (active && action == TitlesAction::Receive) {
|
||||
this->btnTransferBg->SetColor(color::BgSurface2);
|
||||
this->btnTransferText->SetColor(color::TextSecondary);
|
||||
this->btnReceiveBg->SetColor(color::Accent);
|
||||
this->btnReceiveText->SetColor(color::BgBase);
|
||||
} else {
|
||||
this->btnTransferBg->SetColor(color::BgSurface2);
|
||||
this->btnTransferText->SetColor(color::TextSecondary);
|
||||
this->btnReceiveBg->SetColor(color::BgSurface2);
|
||||
this->btnReceiveText->SetColor(color::TextSecondary);
|
||||
}
|
||||
}
|
||||
|
||||
void TitlesLayout::updateHints() {
|
||||
if (focus == TitlesFocus::List) {
|
||||
this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}});
|
||||
} else {
|
||||
this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}});
|
||||
}
|
||||
}
|
||||
|
||||
void TitlesLayout::runTransfer(int index, Title& title) {
|
||||
(void)title;
|
||||
auto ovl = TransferOverlay::New("Transferring save data...");
|
||||
this->titlesMenu->SetVisible(false);
|
||||
mainApp->StartOverlay(ovl);
|
||||
this->LockInput();
|
||||
if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) {
|
||||
mainApp->EndOverlay();
|
||||
this->titlesMenu->SetVisible(true);
|
||||
this->UnlockInput();
|
||||
mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true);
|
||||
return;
|
||||
}
|
||||
while (!mainApp->transfer.isSendDone()) {
|
||||
ovl->SetStatus(mainApp->transfer.sendStatusText());
|
||||
ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown());
|
||||
ovl->SetProgress(mainApp->transfer.sendProgress());
|
||||
mainApp->CallForRender();
|
||||
if (mainApp->GetButtonsDown() & HidNpadButton_B) {
|
||||
mainApp->transfer.cancelSend();
|
||||
}
|
||||
svcSleepThread(16666666LL);
|
||||
}
|
||||
mainApp->EndOverlay();
|
||||
this->titlesMenu->SetVisible(true);
|
||||
this->UnlockInput();
|
||||
|
||||
if (mainApp->transfer.isSendConnectionFailed()) {
|
||||
mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true);
|
||||
} else if (mainApp->transfer.isSendCancelled()) {
|
||||
mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true);
|
||||
} else {
|
||||
mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true);
|
||||
}
|
||||
}
|
||||
|
||||
void TitlesLayout::runReceive(int index, Title& title) {
|
||||
if (mainApp->transfer.startReceive((size_t)index, this->current_uid, title.name()) != 0) {
|
||||
mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true);
|
||||
return;
|
||||
}
|
||||
auto ovl = TransferOverlay::New("Receiving save data...");
|
||||
this->titlesMenu->SetVisible(false);
|
||||
mainApp->StartOverlay(ovl);
|
||||
this->LockInput();
|
||||
while (!mainApp->transfer.isReceiveDone()) {
|
||||
ovl->SetStatus(mainApp->transfer.receiveStatusText());
|
||||
ovl->SetProgress(mainApp->transfer.receiveProgress());
|
||||
mainApp->CallForRender();
|
||||
if (mainApp->GetButtonsDown() & HidNpadButton_B) {
|
||||
mainApp->transfer.cancelReceive();
|
||||
}
|
||||
svcSleepThread(16666666LL);
|
||||
}
|
||||
mainApp->EndOverlay();
|
||||
this->titlesMenu->SetVisible(true);
|
||||
this->UnlockInput();
|
||||
|
||||
if (mainApp->transfer.isReceiveCancelled()) {
|
||||
mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true);
|
||||
} else if (mainApp->transfer.restoreSucceeded()) {
|
||||
mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true);
|
||||
} else {
|
||||
mainApp->CreateShowDialog("Receive", "Restore failed:\n" + mainApp->transfer.restoreError(), {"OK"}, true);
|
||||
}
|
||||
}
|
||||
|
||||
void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
|
||||
(void)Up; (void)Held; (void)Pos;
|
||||
if (m_inputLocked) return;
|
||||
|
||||
if (Down & HidNpadButton_Plus) {
|
||||
mainApp->transfer.cancelSend();
|
||||
mainApp->transfer.cancelReceive();
|
||||
mainApp->Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (focus == TitlesFocus::List) {
|
||||
if (Down & HidNpadButton_B) {
|
||||
this->header->SetUser(std::nullopt, "");
|
||||
mainApp->LoadLayout(mainApp->users_layout);
|
||||
return;
|
||||
}
|
||||
if (Down & HidNpadButton_A) {
|
||||
if (this->titlesMenu->GetItems().empty()) return;
|
||||
this->lockedListIndex = this->titlesMenu->GetSelectedIndex();
|
||||
this->focus = TitlesFocus::Actions;
|
||||
this->action = TitlesAction::Transfer;
|
||||
this->refreshButtons();
|
||||
this->updateHints();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) {
|
||||
this->titlesMenu->SetSelectedIndex(this->lockedListIndex);
|
||||
}
|
||||
if (Down & HidNpadButton_B) {
|
||||
this->focus = TitlesFocus::List;
|
||||
this->refreshButtons();
|
||||
this->updateHints();
|
||||
return;
|
||||
}
|
||||
if (Down & (HidNpadButton_Up | HidNpadButton_Down |
|
||||
HidNpadButton_StickLUp | HidNpadButton_StickLDown)) {
|
||||
this->action = (action == TitlesAction::Transfer)
|
||||
? TitlesAction::Receive : TitlesAction::Transfer;
|
||||
this->refreshButtons();
|
||||
return;
|
||||
}
|
||||
if (Down & HidNpadButton_A) {
|
||||
int idx = this->titlesMenu->GetSelectedIndex();
|
||||
Title title;
|
||||
getTitle(title, this->current_uid, idx);
|
||||
TitlesAction chosen = action;
|
||||
this->focus = TitlesFocus::List;
|
||||
this->refreshButtons();
|
||||
this->updateHints();
|
||||
if (chosen == TitlesAction::Transfer) {
|
||||
this->runTransfer(idx, title);
|
||||
} else {
|
||||
this->runReceive(idx, title);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#include <nxst/app/main_application.hpp>
|
||||
|
||||
namespace ui {
|
||||
extern MainApplication *mainApp;
|
||||
|
||||
UsersLayout::UsersLayout() : Layout::Layout() {
|
||||
using namespace theme;
|
||||
|
||||
this->usersMenu = pu::ui::elm::Menu::New(
|
||||
0, layout::ContentTop + space::md,
|
||||
layout::ScreenW,
|
||||
color::BgBase, color::BgSurface2,
|
||||
88, 6);
|
||||
this->usersMenu->SetScrollbarColor(color::Primary);
|
||||
this->usersMenu->SetItemsFocusColor(color::BgSurface2);
|
||||
|
||||
for (AccountUid const& uid : Account::ids()) {
|
||||
auto item = pu::ui::elm::MenuItem::New(Account::username(uid));
|
||||
item->SetColor(color::TextPrimary);
|
||||
this->usersMenu->AddItem(item);
|
||||
}
|
||||
|
||||
this->loadingBg = pu::ui::elm::Rectangle::New(
|
||||
0, 0, layout::ScreenW, layout::ScreenH, color::Scrim);
|
||||
this->loadingBg->SetVisible(false);
|
||||
|
||||
this->loadingText = pu::ui::elm::TextBlock::New(
|
||||
layout::ScreenW / 2 - 120,
|
||||
layout::ScreenH / 2 - 12,
|
||||
"Loading saves...");
|
||||
this->loadingText->SetFont(type::font(type::Body));
|
||||
this->loadingText->SetColor(color::TextSecondary);
|
||||
this->loadingText->SetVisible(false);
|
||||
|
||||
this->SetBackgroundColor(color::BgBase);
|
||||
this->Add(this->usersMenu);
|
||||
this->Add(this->loadingBg);
|
||||
this->Add(this->loadingText);
|
||||
|
||||
this->header = std::make_unique<HeaderBar>(this, "Select a user");
|
||||
this->hints = std::make_unique<HintBar>(this);
|
||||
this->hints->SetHints({{"A", "Select"}, {"+", "Quit"}});
|
||||
}
|
||||
|
||||
int32_t UsersLayout::GetCurrentIndex() {
|
||||
return this->usersMenu->GetSelectedIndex();
|
||||
}
|
||||
|
||||
void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
|
||||
if (Down & HidNpadButton_Plus) {
|
||||
mainApp->Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Down & HidNpadButton_A) {
|
||||
AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex());
|
||||
|
||||
if (!areTitlesLoaded()) {
|
||||
this->usersMenu->SetVisible(false);
|
||||
this->loadingBg->SetVisible(true);
|
||||
this->loadingText->SetVisible(true);
|
||||
mainApp->CallForRender();
|
||||
|
||||
loadTitles();
|
||||
|
||||
this->loadingBg->SetVisible(false);
|
||||
this->loadingText->SetVisible(false);
|
||||
this->usersMenu->SetVisible(true);
|
||||
}
|
||||
|
||||
mainApp->titles_layout->InitTitles(uid);
|
||||
mainApp->LoadLayout(mainApp->titles_layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user