diff --git a/Makefile b/Makefile index 9ab25d4..356daa7 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ INCLUDES := include include/net lib/Plutonium/include EXEFS_SRC := exefs_src APP_TITLE := NXST APP_AUTHOR := DragonSpirit -APP_VERSION := 04.24.2026 +APP_VERSION := 04.26.2026 ICON := icon.png #--------------------------------------------------------------------------------- @@ -45,8 +45,7 @@ ICON := icon.png #--------------------------------------------------------------------------------- ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE -CFLAGS += -g -O2 -ffunction-sections \ - $(ARCH) $(DEFINES) +CFLAGS += -g -O2 -ffunction-sections $(ARCH) $(DEFINES) CFLAGS += $(INCLUDE) -D__SWITCH__ -D_GNU_SOURCE=1 @@ -55,7 +54,9 @@ 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) -LIBS := -lpu -lz -lminizip -lfreetype -lSDL2_mixer -lopusfile -lopus -lmodplug -lmpg123 -lvorbisidec -logg -lSDL2_ttf -lSDL2_gfx -lSDL2_image -lSDL2 -lEGL -lGLESv2 -lglapi -ldrm_nouveau -lwebp -lpng -ljpeg `sdl2-config --libs` `freetype-config --libs` -lnx +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 diff --git a/include/Theme.hpp b/include/Theme.hpp new file mode 100644 index 0000000..f32f31a --- /dev/null +++ b/include/Theme.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +namespace theme { + using pu::ui::Color; + + namespace color { + constexpr Color BgBase{0x10, 0x14, 0x1C, 0xFF}; + constexpr Color BgSurface{0x18, 0x1F, 0x2A, 0xFF}; + constexpr Color BgSurface2{0x22, 0x2B, 0x39, 0xFF}; + constexpr Color Scrim{0x00, 0x00, 0x00, 0xB8}; + + constexpr Color Primary{0xE2, 0x4B, 0x55, 0xFF}; + constexpr Color PrimaryDim{0x9C, 0x33, 0x3A, 0xFF}; + constexpr Color Accent{0x4A, 0xC2, 0xE0, 0xFF}; + + constexpr Color TextPrimary{0xF2, 0xF4, 0xF8, 0xFF}; + constexpr Color TextSecondary{0xB6, 0xBE, 0xCB, 0xFF}; + constexpr Color TextMuted{0x70, 0x7A, 0x8C, 0xFF}; + + constexpr Color Success{0x55, 0xC8, 0x8A, 0xFF}; + constexpr Color Error{0xE0, 0x6C, 0x6C, 0xFF}; + constexpr Color Warning{0xE6, 0xB4, 0x55, 0xFF}; + + constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF}; + constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF}; + } + + namespace space { + constexpr int xs = 4; + constexpr int sm = 8; + constexpr int md = 16; + constexpr int lg = 24; + constexpr int xl = 32; + constexpr int xxl = 48; + } + + namespace radius { + constexpr int sm = 6; + constexpr int md = 12; + constexpr int lg = 20; + constexpr int pill = 9999; + } + + namespace type { + constexpr int Display = 38; + constexpr int Title = 30; + constexpr int Body = 25; + constexpr int Label = 20; + constexpr int Caption = 18; + + inline std::string font(int size) { + return "DefaultFont@" + std::to_string(size); + } + } + + namespace layout { + constexpr int ScreenW = 1280; + constexpr int ScreenH = 720; + constexpr int HeaderH = 72; + constexpr int HintH = 56; + constexpr int ContentTop = HeaderH; + constexpr int ContentH = ScreenH - HeaderH - HintH; + } + + namespace motion { + constexpr int FadeFrames = 20; + constexpr int SlideFrames = 14; + constexpr int SpinnerFrames = 72; + } + + namespace font { + constexpr const char* Default = "Inter"; + constexpr const char* Medium = "InterMedium"; + } +} diff --git a/include/TitlesLayout.hpp b/include/TitlesLayout.hpp index 17dd0de..b8b6c39 100644 --- a/include/TitlesLayout.hpp +++ b/include/TitlesLayout.hpp @@ -3,17 +3,49 @@ #include #include #include +#include +#include +#include +#include namespace ui { + + enum class TitlesFocus { List, Actions }; + enum class TitlesAction { Transfer, Receive }; + class TitlesLayout : public pu::ui::Layout { private: pu::ui::elm::Menu::Ref titlesMenu; - std::unordered_map menuCache; + std::unordered_map> menuCache; bool m_inputLocked = false; + std::unique_ptr header; + std::unique_ptr hints; + + pu::ui::elm::Rectangle::Ref panelBg; + pu::ui::elm::TextBlock::Ref panelTitle; + pu::ui::elm::TextBlock::Ref panelHint; + pu::ui::elm::Rectangle::Ref btnTransferBg; + pu::ui::elm::TextBlock::Ref btnTransferText; + pu::ui::elm::Rectangle::Ref btnReceiveBg; + pu::ui::elm::TextBlock::Ref btnReceiveText; + pu::ui::elm::TextBlock::Ref panelFooter; + pu::ui::elm::TextBlock::Ref emptyText; + pu::ui::elm::TextBlock::Ref emptySub; + + TitlesFocus focus = TitlesFocus::List; + TitlesAction action = TitlesAction::Transfer; + int lockedListIndex = 0; + + void refreshPanel(); + void refreshButtons(); + void updateHints(); + void runTransfer(int index, Title& title); + void runReceive(int index, Title& title); public: + TitlesLayout(); void InitTitles(); void LockInput() { m_inputLocked = true; } void UnlockInput() { m_inputLocked = false; } @@ -22,4 +54,4 @@ namespace ui { PU_SMART_CTOR(TitlesLayout) }; -} \ No newline at end of file +} diff --git a/include/TransferOverlay.hpp b/include/TransferOverlay.hpp index 9530224..ab9d5a6 100644 --- a/include/TransferOverlay.hpp +++ b/include/TransferOverlay.hpp @@ -1,51 +1,83 @@ #pragma once #include +#include +#include namespace ui { class TransferOverlay : public pu::ui::Overlay { private: + pu::ui::elm::Rectangle::Ref card; pu::ui::elm::TextBlock::Ref titleText; pu::ui::elm::TextBlock::Ref statusText; + pu::ui::elm::Rectangle::Ref progressTrack; pu::ui::elm::ProgressBar::Ref progressBar; + pu::ui::elm::TextBlock::Ref indeterminateText; pu::ui::elm::TextBlock::Ref hintText; + static constexpr int CardW = 720; + static constexpr int CardH = 360; + static constexpr int CardX = (theme::layout::ScreenW - CardW) / 2; + static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2; + public: - static constexpr int OvlX = 200; - static constexpr int OvlY = 240; - static constexpr int OvlW = 880; - static constexpr int OvlH = 240; - TransferOverlay(const std::string &title) - : Overlay(OvlX, OvlY, OvlW, OvlH, pu::ui::Color(30, 30, 30, 220)) + : Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) { - titleText = pu::ui::elm::TextBlock::New(40, 30, title); - titleText->SetColor(pu::ui::Color(255, 255, 255, 255)); + using namespace theme; - statusText = pu::ui::elm::TextBlock::New(40, 90, ""); - statusText->SetColor(pu::ui::Color(180, 180, 180, 255)); + card = pu::ui::elm::Rectangle::New( + CardX, CardY, CardW, CardH, color::BgSurface, radius::lg); - progressBar = pu::ui::elm::ProgressBar::New(40, 140, OvlW - 80, 20, 100.0); - progressBar->SetProgressColor(pu::ui::Color(100, 180, 255, 255)); - progressBar->SetBackgroundColor(pu::ui::Color(70, 70, 70, 255)); + titleText = pu::ui::elm::TextBlock::New( + CardX + space::lg, CardY + space::lg, title); + titleText->SetFont(type::font(type::Title)); + titleText->SetColor(color::TextPrimary); - hintText = pu::ui::elm::TextBlock::New(40, 195, "Press B to cancel"); - hintText->SetColor(pu::ui::Color(130, 130, 130, 255)); + statusText = pu::ui::elm::TextBlock::New( + CardX + space::lg, + CardY + space::lg + 56, + ""); + statusText->SetFont(type::font(type::Body)); + statusText->SetColor(color::TextSecondary); + int barX = CardX + space::lg; + int barY = CardY + space::lg + 56 + 56; + int barW = CardW - 2 * space::lg; + + progressTrack = pu::ui::elm::Rectangle::New( + barX, barY, barW, 8, color::Divider, radius::sm); + + progressBar = pu::ui::elm::ProgressBar::New( + barX, barY, barW, 8, 100.0); + progressBar->SetProgressColor(color::Primary); + progressBar->SetBackgroundColor(color::Divider); + + indeterminateText = pu::ui::elm::TextBlock::New( + barX, barY - 4, "Preparing transfer..."); + indeterminateText->SetFont(type::font(type::Body)); + indeterminateText->SetColor(color::TextMuted); + indeterminateText->SetVisible(false); + + hintText = pu::ui::elm::TextBlock::New( + CardX + space::lg, + CardY + CardH - space::lg - 18, + "B to cancel"); + hintText->SetFont(type::font(type::Caption)); + hintText->SetColor(color::TextMuted); + + this->Add(card); this->Add(titleText); this->Add(statusText); + this->Add(progressTrack); this->Add(progressBar); + this->Add(indeterminateText); this->Add(hintText); } PU_SMART_CTOR(TransferOverlay) void SetStatus(const std::string &status) { - static constexpr size_t MaxChars = 48; - if (status.size() > MaxChars) { - statusText->SetText(status.substr(0, MaxChars - 3) + "..."); - } else { - statusText->SetText(status); - } + statusText->SetText(StringUtils::elide(status, 56)); } void SetProgress(double val) { @@ -53,17 +85,9 @@ namespace ui { } void SetProgressVisible(bool visible) { + progressTrack->SetVisible(visible); progressBar->SetVisible(visible); - if (visible) { - this->SetHeight(OvlH); - this->SetY(OvlY); - hintText->SetY(195); - } else { - static constexpr int SmallH = 160; - this->SetHeight(SmallH); - this->SetY((720 - SmallH) / 2); - hintText->SetY(120); - } + indeterminateText->SetVisible(!visible); } }; diff --git a/include/UsersLayout.hpp b/include/UsersLayout.hpp index 25a9168..e61f191 100644 --- a/include/UsersLayout.hpp +++ b/include/UsersLayout.hpp @@ -1,5 +1,8 @@ #include #include +#include +#include +#include namespace ui { @@ -9,6 +12,8 @@ namespace ui { pu::ui::elm::Menu::Ref usersMenu; pu::ui::elm::Rectangle::Ref loadingBg; pu::ui::elm::TextBlock::Ref loadingText; + std::unique_ptr header; + std::unique_ptr hints; public: diff --git a/include/account.hpp b/include/account.hpp index 71f359c..ec43644 100644 --- a/include/account.hpp +++ b/include/account.hpp @@ -70,6 +70,7 @@ namespace Account { std::vector ids(void); AccountUid selectAccount(void); std::string username(AccountUid id); + std::string iconPath(AccountUid id); } #endif \ No newline at end of file diff --git a/include/const.h b/include/const.h index 8dda9ed..0cf33f4 100644 --- a/include/const.h +++ b/include/const.h @@ -1,2 +1,6 @@ -#define BACKGROUND_COLOR COLOR("00FFFFFF") -#define COLOR(hex) pu::ui::Color::FromHex(hex) \ No newline at end of file +#pragma once + +#include + +#define COLOR(hex) pu::ui::Color::FromHex(hex) +#define BACKGROUND_COLOR theme::color::BgBase diff --git a/include/logger.hpp b/include/logger.hpp index aee29c6..177694e 100644 --- a/include/logger.hpp +++ b/include/logger.hpp @@ -40,10 +40,10 @@ public: return mLogger; } - inline static const std::string INFO = "[ INFO]"; + inline static const std::string INFO = "[INFO]"; inline static const std::string DEBUG = "[DEBUG]"; inline static const std::string ERROR = "[ERROR]"; - inline static const std::string WARN = "[ WARN]"; + inline static const std::string WARN = "[WARN]"; template void log(const std::string& level, const std::string& format = {}, Args... args) diff --git a/include/server.hpp b/include/server.hpp index 96e2ee3..5e917e0 100644 --- a/include/server.hpp +++ b/include/server.hpp @@ -2,6 +2,7 @@ int startSendingThread(); bool isServerTransferDone(); bool isServerTransferCancelled(); +bool isServerWorkersIdle(); void cancelServerTransfer(); double getServerProgress(); std::string getServerStatusText(); \ No newline at end of file diff --git a/include/ui/Card.hpp b/include/ui/Card.hpp new file mode 100644 index 0000000..c923f4d --- /dev/null +++ b/include/ui/Card.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +namespace ui { + + class Card { + public: + pu::ui::elm::Rectangle::Ref bg; + + Card(pu::ui::Layout* parent, int x, int y, int w, int h, + pu::ui::Color color = theme::color::BgSurface, + int rad = theme::radius::lg) { + bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad); + parent->Add(bg); + } + }; +} diff --git a/include/ui/HeaderBar.hpp b/include/ui/HeaderBar.hpp new file mode 100644 index 0000000..59b1a20 --- /dev/null +++ b/include/ui/HeaderBar.hpp @@ -0,0 +1,86 @@ +#pragma once +#include +#include +#include +#include + +namespace ui { + + class HeaderBar { + private: + pu::ui::elm::Rectangle::Ref bg; + pu::ui::elm::Rectangle::Ref divider; + pu::ui::elm::TextBlock::Ref appName; + pu::ui::elm::TextBlock::Ref subtitle; + pu::ui::elm::Rectangle::Ref chipBg; + pu::ui::elm::Image::Ref avatar; + pu::ui::elm::TextBlock::Ref userName; + + public: + HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") { + using namespace theme; + + bg = pu::ui::elm::Rectangle::New( + 0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface); + divider = pu::ui::elm::Rectangle::New( + 0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider); + + appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST"); + appName->SetFont(type::font(type::Title)); + appName->SetColor(color::TextPrimary); + + subtitle = pu::ui::elm::TextBlock::New(space::lg, 46, sub); + subtitle->SetFont(type::font(type::Caption)); + subtitle->SetColor(color::TextMuted); + + const int chipW = 280; + const int chipX = layout::ScreenW - chipW - space::lg; + chipBg = pu::ui::elm::Rectangle::New( + chipX, 16, chipW, 40, + color::BgSurface2, radius::pill); + chipBg->SetVisible(false); + + avatar = pu::ui::elm::Image::New(chipX + 4, 20, ""); + avatar->SetWidth(32); + avatar->SetHeight(32); + avatar->SetVisible(false); + + userName = pu::ui::elm::TextBlock::New(chipX + 44, 24, ""); + userName->SetFont(type::font(type::Body)); + userName->SetColor(color::TextPrimary); + userName->SetVisible(false); + + parent->Add(bg); + parent->Add(divider); + parent->Add(appName); + parent->Add(subtitle); + parent->Add(chipBg); + parent->Add(avatar); + parent->Add(userName); + } + + void SetUser(const std::optional& uid, const std::string& name) { + const bool show = uid.has_value(); + chipBg->SetVisible(show); + userName->SetVisible(show); + if (show) { + userName->SetText(name); + std::string path = Account::iconPath(*uid); + if (!path.empty()) { + avatar->SetImage(path); + avatar->SetWidth(32); + avatar->SetHeight(32); + avatar->SetVisible(avatar->IsImageValid()); + } else { + avatar->SetVisible(false); + } + } else { + avatar->SetVisible(false); + } + } + + void SetSubtitle(const std::string& text) { + subtitle->SetText(text); + } + }; +} diff --git a/include/ui/HintBar.hpp b/include/ui/HintBar.hpp new file mode 100644 index 0000000..76107ed --- /dev/null +++ b/include/ui/HintBar.hpp @@ -0,0 +1,55 @@ +#pragma once +#include +#include +#include +#include + +namespace ui { + + struct Hint { + std::string glyph; + std::string label; + }; + + class HintBar { + private: + pu::ui::Layout* parent; + pu::ui::elm::Rectangle::Ref bg; + pu::ui::elm::Rectangle::Ref divider; + std::vector labels; + + public: + HintBar(pu::ui::Layout* p) : parent(p) { + using namespace theme; + bg = pu::ui::elm::Rectangle::New( + 0, layout::ScreenH - layout::HintH, + layout::ScreenW, layout::HintH, color::BgSurface); + divider = pu::ui::elm::Rectangle::New( + 0, layout::ScreenH - layout::HintH, + layout::ScreenW, 1, color::Divider); + parent->Add(bg); + parent->Add(divider); + } + + void SetHints(const std::vector& 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); + } + } + }; +} diff --git a/include/ui/UiContext.hpp b/include/ui/UiContext.hpp new file mode 100644 index 0000000..3d444e8 --- /dev/null +++ b/include/ui/UiContext.hpp @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include +#include + +namespace ui { + struct UiContext { + std::optional selectedUser; + std::string selectedUserName; + }; +} diff --git a/include/util.hpp b/include/util.hpp index 1a53a76..5fafd97 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -47,6 +47,7 @@ namespace StringUtils { std::string removeAccents(std::string str); std::string removeNotAscii(std::string str); std::u16string UTF8toUTF16(const char* src); + std::string elide(const std::string& s, size_t maxChars); } #endif diff --git a/source/Main.cpp b/source/Main.cpp index ce2b9f9..3e2a78e 100644 --- a/source/Main.cpp +++ b/source/Main.cpp @@ -1,6 +1,8 @@ #include #include "util.hpp" #include "main.hpp" +#include +#include static int nxlink_sock = -1; @@ -19,6 +21,10 @@ extern "C" void userAppInit() { } extern "C" void userAppExit() { + cancelServerTransfer(); + for (int i = 0; i < 150 && !isServerWorkersIdle(); i++) { + usleep(10000); + } if (nxlink_sock != -1) { close(nxlink_sock); } @@ -50,6 +56,11 @@ int main() { 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); diff --git a/source/TitlesLayout.cpp b/source/TitlesLayout.cpp index 94f251d..8431cd0 100644 --- a/source/TitlesLayout.cpp +++ b/source/TitlesLayout.cpp @@ -6,43 +6,256 @@ #include #include -static std::vector accSids, devSids, bcatSids, cacheSids; - 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 PC"); + 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 PC"); + 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(this, "Save Transfer"); + this->hints = std::make_unique(this); + this->updateHints(); + } + void TitlesLayout::InitTitles() { + using namespace theme; Logger::getInstance().log(Logger::INFO, "InitTitles"); auto it = this->menuCache.find(g_currentUId); + std::vector* items; if (it != this->menuCache.end()) { - this->titlesMenu = it->second; + items = &it->second; } else { - auto menu = pu::ui::elm::Menu::New(0, 0, 1280, COLOR("#67000000"), COLOR("#170909FF"), 94, 7); + std::vector built; for (size_t i = 0; i < getTitleCount(g_currentUId); i++) { Title title; getTitle(title, g_currentUId, i); auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); - titleItem->SetColor(COLOR("#FFFFFFFF")); - menu->AddItem(titleItem); + titleItem->SetColor(color::TextPrimary); + built.push_back(titleItem); } - this->menuCache.emplace(g_currentUId, menu); - this->titlesMenu = menu; + auto inserted = this->menuCache.emplace(g_currentUId, std::move(built)); + items = &inserted.first->second; } - this->Clear(); - this->Add(this->titlesMenu); + this->titlesMenu->ClearItems(); + for (auto& item : *items) { + this->titlesMenu->AddItem(item); + } + this->titlesMenu->SetSelectedIndex(0); - this->SetBackgroundColor(BACKGROUND_COLOR); + 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(g_currentUId, Account::username(g_currentUId)); + } + + void TitlesLayout::refreshPanel() { + if (this->titlesMenu->GetItems().empty()) return; + int idx = this->titlesMenu->GetSelectedIndex(); + Title title; + getTitle(title, g_currentUId, 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) { + auto ovl = TransferOverlay::New("Transferring save data..."); + this->titlesMenu->SetVisible(false); + mainApp->StartOverlay(ovl); + this->LockInput(); + if (transfer_files(index, g_currentUId) != 0) { + mainApp->EndOverlay(); + this->titlesMenu->SetVisible(true); + this->UnlockInput(); + mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); + return; + } + while (!isClientTransferDone()) { + ovl->SetStatus(getClientStatusText()); + ovl->SetProgressVisible(isClientProgressKnown()); + ovl->SetProgress(getClientProgress()); + mainApp->CallForRender(); + if (mainApp->GetButtonsDown() & HidNpadButton_B) { + cancelClientTransfer(); + } + svcSleepThread(16666666LL); + } + mainApp->EndOverlay(); + this->titlesMenu->SetVisible(true); + this->UnlockInput(); + + if (isClientConnectionFailed()) { + mainApp->CreateShowDialog("Transfer", getClientFailReason(), {"OK"}, true); + } else if (isClientTransferCancelled()) { + 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 (startSendingThread() != 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 (!isServerTransferDone()) { + ovl->SetStatus(getServerStatusText()); + ovl->SetProgress(getServerProgress()); + mainApp->CallForRender(); + if (mainApp->GetButtonsDown() & HidNpadButton_B) { + cancelServerTransfer(); + } + svcSleepThread(16666666LL); + } + mainApp->EndOverlay(); + this->titlesMenu->SetVisible(true); + this->UnlockInput(); + + if (isServerTransferCancelled()) { + mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); + return; + } + auto restoreResult = io::restore(index, g_currentUId, 0, title.name()); + if (std::get<0>(restoreResult)) { + mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); + } else { + mainApp->CreateShowDialog("Receive", "Restore failed:\n" + std::get<2>(restoreResult), {"OK"}, true); + } } void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { if (m_inputLocked) return; - if (Down & HidNpadButton_B) { - mainApp->LoadLayout(mainApp->usersLayout); - return; - } - if (Down & HidNpadButton_Plus) { cancelClientTransfer(); cancelServerTransfer(); @@ -50,87 +263,50 @@ namespace ui { return; } - if (Down & HidNpadButton_A) { - auto index = this->titlesMenu->GetSelectedIndex(); - Title title; - getTitle(title, g_currentUId, index); - printf("userid is 0x%lX%lX\n", title.userId().uid[1], title.userId().uid[0]); - // printf("current game index is %i\n", index); - int opt = mainApp->CreateShowDialog(title.name().c_str(), "What do you want?", { "Transfer", "Receive" }, false); - printf("opt is %i\n", opt); - switch (opt) { - case 0: { - // Transfer selected - { - auto ovl = TransferOverlay::New("Transferring save data..."); - this->titlesMenu->SetVisible(false); - mainApp->StartOverlay(ovl); - this->LockInput(); - if (transfer_files(index, g_currentUId) != 0) { - mainApp->EndOverlay(); - this->titlesMenu->SetVisible(true); - this->UnlockInput(); - mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); - break; - } - while (!isClientTransferDone()) { - ovl->SetStatus(getClientStatusText()); - ovl->SetProgressVisible(isClientProgressKnown()); - ovl->SetProgress(getClientProgress()); - mainApp->CallForRender(); - if (mainApp->GetButtonsDown() & HidNpadButton_B) { - cancelClientTransfer(); - } - svcSleepThread(16666666LL); - } - mainApp->EndOverlay(); - this->titlesMenu->SetVisible(true); - this->UnlockInput(); - } - if (isClientConnectionFailed()) { - mainApp->CreateShowDialog("Transfer", getClientFailReason(), {"OK"}, true); - } else if (isClientTransferCancelled()) { - mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); - } else { - mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); - } - break; - } - case 1: { - // Receive selected - if (startSendingThread() != 0) { - mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true); - break; - } - { - auto ovl = TransferOverlay::New("Receiving save data..."); - this->titlesMenu->SetVisible(false); - mainApp->StartOverlay(ovl); - this->LockInput(); - while (!isServerTransferDone()) { - ovl->SetStatus(getServerStatusText()); - ovl->SetProgress(getServerProgress()); - mainApp->CallForRender(); - if (mainApp->GetButtonsDown() & HidNpadButton_B) { - cancelServerTransfer(); - } - svcSleepThread(16666666LL); - } - mainApp->EndOverlay(); - this->titlesMenu->SetVisible(true); - this->UnlockInput(); - } - if (isServerTransferCancelled()) { - mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); - break; - } - auto restoreResult = io::restore(index, g_currentUId, 0, title.name()); - if (std::get<0>(restoreResult)) { - mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); - } else { - mainApp->CreateShowDialog("Receive", "Restore failed:\n" + std::get<2>(restoreResult), {"OK"}, true); - } - break; + if (focus == TitlesFocus::List) { + if (Down & HidNpadButton_B) { + this->header->SetUser(std::nullopt, ""); + mainApp->LoadLayout(mainApp->usersLayout); + 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, g_currentUId, 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); } } } diff --git a/source/UsersLayout.cpp b/source/UsersLayout.cpp index 5ce5d61..6231d70 100644 --- a/source/UsersLayout.cpp +++ b/source/UsersLayout.cpp @@ -6,26 +6,42 @@ namespace ui { extern MainApplication *mainApp; UsersLayout::UsersLayout() : Layout::Layout() { - this->usersMenu = pu::ui::elm::Menu::New(0, 0, 1280, COLOR("#67000000"), COLOR("#170909FF"), 94, 7); - this->usersMenu->SetScrollbarColor(COLOR("#170909FF")); + 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("#FFFFFFFF")); + item->SetColor(color::TextPrimary); this->usersMenu->AddItem(item); } - this->loadingBg = pu::ui::elm::Rectangle::New(0, 0, 1280, 720, pu::ui::Color(30, 30, 30, 220)); + 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(480, 340, "Reading game list..."); - this->loadingText->SetColor(pu::ui::Color(255, 255, 255, 255)); + 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(BACKGROUND_COLOR); + this->SetBackgroundColor(color::BgBase); this->Add(this->usersMenu); this->Add(this->loadingBg); this->Add(this->loadingText); + + this->header = std::make_unique(this, "Select a user"); + this->hints = std::make_unique(this); + this->hints->SetHints({{"A", "Select"}, {"+", "Quit"}}); } int32_t UsersLayout::GetCurrentIndex() { @@ -34,8 +50,7 @@ namespace ui { void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { if (Down & HidNpadButton_Plus) { - mainApp->Close(); - return; + svcExitProcess(); } if (Down & HidNpadButton_A) { diff --git a/source/account.cpp b/source/account.cpp index 5bf72e7..12d3967 100644 --- a/source/account.cpp +++ b/source/account.cpp @@ -26,6 +26,8 @@ #include "account.hpp" #include +#include +#include static std::map mUsers; @@ -85,6 +87,41 @@ std::string Account::username(AccountUid id) 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 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; diff --git a/source/server.cpp b/source/server.cpp index 3e6de02..a864df3 100644 --- a/source/server.cpp +++ b/source/server.cpp @@ -26,20 +26,36 @@ static TransferState g_server_state; static std::atomic g_server_client_sock{-1}; static std::atomic g_server_listen_sock{-1}; static std::atomic g_broadcast_sock{-1}; +static std::atomic g_accept_thread_active{false}; +static std::atomic g_broadcast_thread_active{false}; +static pthread_t g_broadcast_thread{}; bool isServerTransferDone() { return g_server_state.done.load(); } bool isServerTransferCancelled() { return g_server_state.cancelled.load(); } +bool isServerWorkersIdle() { return !g_accept_thread_active.load() && !g_broadcast_thread_active.load(); } double getServerProgress() { return g_server_state.progress(); } std::string getServerStatusText() { return g_server_state.getStatus(); } void cancelServerTransfer() { g_server_state.cancelled.store(true); - int sock = g_server_client_sock.load(); - if (sock >= 0) shutdown(sock, SHUT_RDWR); - int lsock = g_server_listen_sock.load(); - if (lsock >= 0) shutdown(lsock, SHUT_RDWR); - int bsock = g_broadcast_sock.load(); - if (bsock >= 0) shutdown(bsock, SHUT_RDWR); + int sock = g_server_client_sock.exchange(-1); + if (sock >= 0) { + shutdown(sock, SHUT_RDWR); + close(sock); + } + int lsock = g_server_listen_sock.exchange(-1); + if (lsock >= 0) { + shutdown(lsock, SHUT_RDWR); + close(lsock); + } + int bsock = g_broadcast_sock.exchange(-1); + if (bsock >= 0) { + shutdown(bsock, SHUT_RDWR); + close(bsock); + } + if (g_broadcast_thread_active.load()) { + pthread_cancel(g_broadcast_thread); + } } #ifdef __SWITCH__ @@ -166,13 +182,17 @@ static void* handle_client(void* socket_desc) { receive_file(client_socket, filename_str, file_size); } - close(client_socket); + int owned_client = g_server_client_sock.exchange(-1); + if (owned_client == client_socket) { + close(client_socket); + } return nullptr; } struct AcceptArgs { int server_fd; }; static void* accept_and_handle(void* arg) { + g_accept_thread_active.store(true); int server_fd = static_cast(arg)->server_fd; delete static_cast(arg); @@ -180,8 +200,10 @@ static void* accept_and_handle(void* arg) { sockaddr_in client_addr{}; socklen_t client_len = sizeof(client_addr); int client_socket = accept(server_fd, (sockaddr*)&client_addr, &client_len); - g_server_listen_sock.store(-1); - close(server_fd); + int owned_listen = g_server_listen_sock.exchange(-1); + if (owned_listen == server_fd) { + close(server_fd); + } if (client_socket >= 0) { g_server_client_sock.store(client_socket); @@ -191,23 +213,27 @@ static void* accept_and_handle(void* arg) { } else { close(client_socket); } - g_server_client_sock.store(-1); } g_server_state.done.store(true); + g_accept_thread_active.store(false); return nullptr; } static void* broadcast_listener(void* arg) { - Socket udp(socket(AF_INET, SOCK_DGRAM, 0)); - if (!udp.valid()) { + g_broadcast_thread_active.store(true); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr); + pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr); + int udp = socket(AF_INET, SOCK_DGRAM, 0); + if (udp < 0) { perror("broadcast_listener: socket"); + g_broadcast_thread_active.store(false); return nullptr; } - g_broadcast_sock.store(udp.fd); + g_broadcast_sock.store(udp); - struct timeval tv{0, 100000}; // 100ms poll so cancel is detected quickly + 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{}; @@ -217,7 +243,9 @@ static void* broadcast_listener(void* arg) { if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("broadcast_listener: bind"); - g_broadcast_sock.store(-1); + int owned = g_broadcast_sock.exchange(-1); + if (owned == udp) close(udp); + g_broadcast_thread_active.store(false); return nullptr; } @@ -226,7 +254,9 @@ static void* broadcast_listener(void* arg) { group.imr_interface.s_addr = htonl(INADDR_ANY); if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) { perror("broadcast_listener: setsockopt"); - g_broadcast_sock.store(-1); + int owned = g_broadcast_sock.exchange(-1); + if (owned == udp) close(udp); + g_broadcast_thread_active.store(false); return nullptr; } @@ -250,21 +280,28 @@ static void* broadcast_listener(void* arg) { } } - g_broadcast_sock.store(-1); + int owned = g_broadcast_sock.exchange(-1); + if (owned == udp) close(udp); + g_broadcast_thread_active.store(false); return nullptr; } int startSendingThread() { + g_server_state.reset(); + g_server_state.setStatus("Waiting for connection..."); + pthread_t broadcast_thread; if (pthread_create(&broadcast_thread, nullptr, broadcast_listener, nullptr) != 0) { perror("startSendingThread: broadcast thread"); return 1; } + g_broadcast_thread = broadcast_thread; pthread_detach(broadcast_thread); Socket server(socket(AF_INET, SOCK_STREAM, 0)); if (!server.valid()) { perror("startSendingThread: socket"); + cancelServerTransfer(); return 1; } @@ -278,20 +315,20 @@ int startSendingThread() { if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("startSendingThread: bind"); + cancelServerTransfer(); return 1; } if (listen(server, 3) < 0) { perror("startSendingThread: listen"); + cancelServerTransfer(); return 1; } - g_server_state.reset(); - g_server_state.setStatus("Waiting for connection..."); - AcceptArgs* acc_args = new AcceptArgs{server.fd}; pthread_t accept_thread; if (pthread_create(&accept_thread, nullptr, accept_and_handle, acc_args) != 0) { delete acc_args; + cancelServerTransfer(); return 1; } pthread_detach(accept_thread); diff --git a/source/util.cpp b/source/util.cpp index d27abe2..81f4219 100644 --- a/source/util.cpp +++ b/source/util.cpp @@ -126,6 +126,16 @@ std::string StringUtils::removeNotAscii(std::string str) return str; } +std::string StringUtils::elide(const std::string& s, size_t maxChars) +{ + if (s.size() <= maxChars || maxChars < 6) return s; + 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;