Add Borealis GUI for patch extraction on Switch.
All checks were successful
Build NRO / build (push) Successful in 1m48s

Replace the console UI with a Borealis-based flow, bundle ROMFS assets and
borealis as a submodule, and apply small upstream patches at build time.
Self-delete runs after romfsExit on quit so the NRO can be removed like the
old console build.
This commit is contained in:
2026-05-28 22:22:44 +02:00
parent 828edf6ad2
commit 2cf3db2097
19 changed files with 956 additions and 214 deletions

View File

@@ -18,9 +18,14 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
submodules: recursive
- name: Build - name: Build
run: make run: |
patch -d library/borealis -p1 -N < patches/borealis-swkbd-libnx.patch || true
patch -d library/borealis -p1 -N < patches/borealis-applet-frame-hints.patch || true
make
- name: Upload NRO - name: Upload NRO
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
build/ build/
resources/shaders/
*.nro *.nro
*.nacp *.nacp
*.elf *.elf

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "library/borealis"]
path = library/borealis
url = https://github.com/natinusala/borealis.git

View File

@@ -21,6 +21,10 @@ SOURCES := source
DATA := data DATA := data
INCLUDES := include INCLUDES := include
ROMFS := resources
BOREALIS_PATH := library/borealis
OUT_SHADERS := shaders
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
# options for code generation # options for code generation
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
@@ -31,7 +35,7 @@ CFLAGS := -g -Wall -O2 -ffunction-sections \
CFLAGS += $(INCLUDE) -D__SWITCH__ CFLAGS += $(INCLUDE) -D__SWITCH__
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions CXXFLAGS := $(CFLAGS) -std=c++1z -O2 -Wno-volatile -include optional
ASFLAGS := -g $(ARCH) ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
@@ -41,10 +45,14 @@ LIBS := -lminizip -lz -lnx
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
LIBDIRS := $(PORTLIBS) $(LIBNX) LIBDIRS := $(PORTLIBS) $(LIBNX)
include $(TOPDIR)/$(BOREALIS_PATH)/library/borealis.mk
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR))) ifneq ($(BUILD),$(notdir $(CURDIR)))
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
.DEFAULT_GOAL := all
export OUTPUT := $(CURDIR)/$(TARGET) export OUTPUT := $(CURDIR)/$(TARGET)
export TOPDIR := $(CURDIR) export TOPDIR := $(CURDIR)
@@ -56,6 +64,7 @@ export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
GLSLFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.glsl)))
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
@@ -75,6 +84,18 @@ export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SRC) export OFILES := $(OFILES_BIN) $(OFILES_SRC)
export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES)))
ifneq ($(strip $(ROMFS)),)
ROMFS_TARGETS :=
ROMFS_FOLDERS :=
ifneq ($(strip $(OUT_SHADERS)),)
ROMFS_SHADERS := $(ROMFS)/$(OUT_SHADERS)
ROMFS_TARGETS += $(patsubst %.glsl, $(ROMFS_SHADERS)/%.dksh, $(GLSLFILES))
ROMFS_FOLDERS += $(ROMFS_SHADERS)
endif
export ROMFS_DEPS := $(foreach file,$(ROMFS_TARGETS),$(CURDIR)/$(file))
endif
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD) -I$(CURDIR)/$(BUILD)
@@ -123,22 +144,63 @@ ifneq ($(ROMFS),)
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
endif endif
.PHONY: $(BUILD) clean all .PHONY: $(BUILD) clean all patch-borealis
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
all: $(BUILD) patch-borealis:
@if ! grep -q 'SWKBD_CONFIG_SET_STRING_LEN_MAX_EXT' $(BOREALIS_PATH)/library/lib/platforms/switch/swkbd.cpp 2>/dev/null; then \
patch -d $(BOREALIS_PATH) -p1 -N -i $(CURDIR)/patches/borealis-swkbd-libnx.patch >/dev/null 2>&1 || true; \
fi
@if ! grep -q 'footer_hint_right' $(BOREALIS_PATH)/library/lib/views/applet_frame.cpp 2>/dev/null; then \
patch -d $(BOREALIS_PATH) -p1 -N -i $(CURDIR)/patches/borealis-applet-frame-hints.patch >/dev/null 2>&1 || true; \
fi
$(BUILD): all: $(ROMFS_TARGETS) | $(BUILD)
$(BUILD): patch-borealis
@[ -d $@ ] || mkdir -p $@ @[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile @MSYS2_ARG_CONV_EXCL="-D;$(MSYS2_ARG_CONV_EXCL)" $(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
ifneq ($(strip $(ROMFS_TARGETS)),)
$(ROMFS_TARGETS): | $(ROMFS_FOLDERS)
$(ROMFS_FOLDERS):
@mkdir -p $@
$(ROMFS_SHADERS)/%_vsh.dksh: %_vsh.glsl
@echo {vert} $(notdir $<)
@uam -s vert -o $@ $<
$(ROMFS_SHADERS)/%_tcsh.dksh: %_tcsh.glsl
@echo {tess_ctrl} $(notdir $<)
@uam -s tess_ctrl -o $@ $<
$(ROMFS_SHADERS)/%_tesh.dksh: %_tesh.glsl
@echo {tess_eval} $(notdir $<)
@uam -s tess_eval -o $@ $<
$(ROMFS_SHADERS)/%_gsh.dksh: %_gsh.glsl
@echo {geom} $(notdir $<)
@uam -s geom -o $@ $<
$(ROMFS_SHADERS)/%_fsh.dksh: %_fsh.glsl
@echo {frag} $(notdir $<)
@uam -s frag -o $@ $<
$(ROMFS_SHADERS)/%.dksh: %.glsl
@echo {comp} $(notdir $<)
@uam -s comp -o $@ $<
endif
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
clean: clean:
@echo clean ... @echo clean ...
ifeq ($(strip $(APP_JSON)),) ifeq ($(strip $(APP_JSON)),)
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf @rm -fr $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nro $(TARGET).nacp $(TARGET).elf
else else
@rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf @rm -fr $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf
endif endif
@@ -154,9 +216,9 @@ ifeq ($(strip $(APP_JSON)),)
all : $(OUTPUT).nro all : $(OUTPUT).nro
ifeq ($(strip $(NO_NACP)),) ifeq ($(strip $(NO_NACP)),)
$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp $(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp $(ROMFS_DEPS)
else else
$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nro : $(OUTPUT).elf $(ROMFS_DEPS)
endif endif
else else

View File

@@ -7,7 +7,7 @@ Nintendo Switch Homebrew App für [OmniNX OC](https://git.niklascfw.de/OmniNX/Om
1. Sucht nach `sd:/SaltySD/plugins/FPSLocker/patches.zip` 1. Sucht nach `sd:/SaltySD/plugins/FPSLocker/patches.zip`
2. Entpackt alle Patches in das gleiche Verzeichnis (überschreibt existierende Dateien) 2. Entpackt alle Patches in das gleiche Verzeichnis (überschreibt existierende Dateien)
3. Löscht `patches.zip` nach erfolgreichem Entpacken 3. Löscht `patches.zip` nach erfolgreichem Entpacken
4. Löscht sich selbst (`sd:/switch/PatchExtractor.nro`) 4. Löscht `sd:/switch/PatchExtractor.nro` beim Beenden (nach `romfsExit`, da eingebettetes ROMFS die Datei sonst offen hält)
## Nutzung ## Nutzung
@@ -15,14 +15,17 @@ Die App ist bereits in OmniNX OC enthalten und kann direkt aus Sphaira (hbmenu)
## Selber bauen ## Selber bauen
Benötigt [devkitPro](https://devkitpro.org/) mit libnx, zlib und minizip. Benötigt [devkitPro](https://devkitpro.org/) mit libnx, zlib, minizip und [borealis](https://github.com/natinusala/borealis) (als Git-Submodul).
```bash ```bash
git submodule update --init --recursive
export DEVKITPRO=/opt/devkitpro export DEVKITPRO=/opt/devkitpro
export PATH=$DEVKITPRO/devkitA64/bin:$DEVKITPRO/tools/bin:$PATH export PATH=$DEVKITPRO/devkitA64/bin:$DEVKITPRO/tools/bin:$PATH
make make
``` ```
Die Oberfläche nutzt die Borealis UI-Bibliothek (Switch-Systemdesign).
## Lizenz ## Lizenz
MIT MIT

1
library/borealis Submodule

Submodule library/borealis added at 20e2d33b6c

View File

@@ -0,0 +1,383 @@
diff --git a/library/include/borealis/views/applet_frame.hpp b/library/include/borealis/views/applet_frame.hpp
index 1402928..beacd1a 100644
--- a/library/include/borealis/views/applet_frame.hpp
+++ b/library/include/borealis/views/applet_frame.hpp
@@ -19,6 +19,7 @@
#include <borealis/core/bind.hpp>
#include <borealis/core/box.hpp>
+#include <borealis/core/event.hpp>
#include <borealis/views/image.hpp>
#include <borealis/views/label.hpp>
@@ -30,6 +31,7 @@ class AppletFrame : public Box
{
public:
AppletFrame();
+ ~AppletFrame() override;
void handleXMLElement(tinyxml2::XMLElement* element) override;
@@ -48,8 +50,20 @@ class AppletFrame : public Box
static View* create();
private:
+ void updateFooterHints();
+ void clearHintBox(Box* box);
+ void addHintItem(Box* box, const Action& action, float spacingAfter);
+
BRLS_BIND(Label, title, "brls/applet_frame/title_label");
BRLS_BIND(Image, icon, "brls/applet_frame/title_icon");
+ BRLS_BIND(Box, footerHintRight, "brls/applet_frame/footer_hint_right");
+ BRLS_BIND(Box, footerHintLeft, "brls/applet_frame/footer_hint_left");
+
+ GenericEvent::Subscription focusSubscription;
+ VoidEvent::Subscription hintsSubscription;
+
+ int switchFont = FONT_INVALID;
+ int regularFont = FONT_INVALID;
protected:
View* contentView = nullptr;
diff --git a/library/include/borealis/views/label.hpp b/library/include/borealis/views/label.hpp
index 3e1ccef..797d328 100644
--- a/library/include/borealis/views/label.hpp
+++ b/library/include/borealis/views/label.hpp
@@ -85,6 +85,7 @@ class Label : public View
void setVerticalAlign(VerticalAlign align);
void setFontSize(float value);
+ void setFontFace(int fontFace);
void setLineHeight(float value);
void setTextColor(NVGcolor color);
diff --git a/library/lib/views/applet_frame.cpp b/library/lib/views/applet_frame.cpp
index f9f47dc..11b8cd6 100644
--- a/library/lib/views/applet_frame.cpp
+++ b/library/lib/views/applet_frame.cpp
@@ -15,6 +15,11 @@
limitations under the License.
*/
+#include <algorithm>
+#include <set>
+
+#include <borealis/core/application.hpp>
+#include <borealis/core/font.hpp>
#include <borealis/core/logger.hpp>
#include <borealis/core/util.hpp>
#include <borealis/views/applet_frame.hpp>
@@ -22,6 +27,73 @@
namespace brls
{
+namespace
+{
+
+constexpr float HINT_FONT_SIZE = 22.0f;
+constexpr float HINT_SPACING = 30.0f;
+constexpr float HINT_ICON_GAP = 4.0f;
+
+const char* getButtonIcon(ControllerButton button)
+{
+ switch (button)
+ {
+ case BUTTON_A:
+ return "\uE0E0";
+ case BUTTON_B:
+ return "\uE0E1";
+ case BUTTON_X:
+ return "\uE0E2";
+ case BUTTON_Y:
+ return "\uE0E3";
+ case BUTTON_START:
+ return "\uE0EF";
+ case BUTTON_BACK:
+ return "\uE0F0";
+ case BUTTON_UP:
+ return "\uE0EB";
+ case BUTTON_DOWN:
+ return "\uE0EC";
+ case BUTTON_LEFT:
+ return "\uE0ED";
+ case BUTTON_RIGHT:
+ return "\uE0EE";
+ default:
+ return "\uE152";
+ }
+}
+
+std::string getButtonHintText(const Action& action)
+{
+ if (!action.hintText.empty())
+ return action.hintText;
+
+ switch (action.button)
+ {
+ case BUTTON_A:
+ return "Ok";
+ case BUTTON_B:
+ return "Zurück";
+ case BUTTON_START:
+ return "Beenden";
+ default:
+ return "";
+ }
+}
+
+bool actionsSort(const Action& a, const Action& b)
+{
+ if (a.button == BUTTON_START)
+ return true;
+ if (b.button == BUTTON_A)
+ return true;
+ if (b.button == BUTTON_B && a.button != BUTTON_A)
+ return true;
+ return false;
+}
+
+} // namespace
+
const std::string appletFrameXML = R"xml(
<brls:Box
width="auto"
@@ -36,9 +108,9 @@ const std::string appletFrameXML = R"xml(
axis="row"
paddingTop="@style/brls/applet_frame/header_padding_top_bottom"
paddingBottom="@style/brls/applet_frame/header_padding_top_bottom"
- paddingLeft="@style/brls/applet_frame/header_padding_sides"
+ paddingLeft="26px"
paddingRight="@style/brls/applet_frame/header_padding_sides"
- marginLeft="@style/brls/applet_frame/padding_sides"
+ marginLeft="22px"
marginRight="@style/brls/applet_frame/padding_sides"
lineColor="@theme/brls/applet_frame/separator"
lineBottom="1px">
@@ -61,16 +133,12 @@ const std::string appletFrameXML = R"xml(
<!-- Content will be injected here with grow="1.0" -->
- <!--
- Footer
- Direction inverted so that the bottom left text can be
- set to visibility="gone" without affecting the hint
- -->
+ <!-- Footer -->
<brls:Box
width="auto"
height="@style/brls/applet_frame/footer_height"
axis="row"
- direction="rightToLeft"
+ direction="leftToRight"
paddingLeft="@style/brls/applet_frame/footer_padding_sides"
paddingRight="@style/brls/applet_frame/footer_padding_sides"
paddingTop="@style/brls/applet_frame/footer_padding_top_bottom"
@@ -81,15 +149,25 @@ const std::string appletFrameXML = R"xml(
lineTop="1px"
justifyContent="spaceBetween" >
- <brls:Rectangle
- width="272px"
+ <brls:Box
+ id="brls/applet_frame/footer_hint_left"
+ width="auto"
height="auto"
- color="#FF0000" />
+ axis="row"
+ direction="leftToRight"
+ justifyContent="flexStart"
+ alignItems="center"
+ visibility="gone" />
- <brls:Rectangle
- width="75px"
+ <brls:Box
+ id="brls/applet_frame/footer_hint_right"
+ width="auto"
height="auto"
- color="#FF00FF" />
+ grow="1.0"
+ axis="row"
+ direction="leftToRight"
+ justifyContent="flexEnd"
+ alignItems="center" />
</brls:Box>
@@ -109,6 +187,141 @@ AppletFrame::AppletFrame()
});
this->forwardXMLAttribute("iconInterpolation", this->icon, "interpolation");
+
+ this->switchFont = Application::getFont(FONT_SWITCH_ICONS);
+ this->regularFont = Application::getFont(FONT_REGULAR);
+
+ this->focusSubscription = Application::getGlobalFocusChangeEvent()->subscribe([this](View*) {
+ this->updateFooterHints();
+ });
+
+ this->hintsSubscription = Application::getGlobalHintsUpdateEvent()->subscribe([this]() {
+ this->updateFooterHints();
+ });
+
+ this->updateFooterHints();
+}
+
+AppletFrame::~AppletFrame()
+{
+ Application::getGlobalFocusChangeEvent()->unsubscribe(this->focusSubscription);
+ Application::getGlobalHintsUpdateEvent()->unsubscribe(this->hintsSubscription);
+}
+
+void AppletFrame::clearHintBox(Box* box)
+{
+ if (!box)
+ return;
+
+ std::vector<View*> children = box->getChildren();
+ while (!children.empty())
+ {
+ View* child = children.back();
+ box->removeView(child);
+ children.pop_back();
+ }
+}
+
+void AppletFrame::addHintItem(Box* box, const Action& action, float spacingAfter)
+{
+ const std::string hintText = getButtonHintText(action);
+ if (hintText.empty())
+ return;
+
+ Box* item = new Box(Axis::ROW);
+ item->setDirection(Direction::LEFT_TO_RIGHT);
+ item->setAlignItems(AlignItems::CENTER);
+ if (spacingAfter > 0.0f)
+ item->setMarginRight(spacingAfter);
+
+ Label* icon = new Label();
+ icon->setText(getButtonIcon(action.button));
+ icon->setFontSize(HINT_FONT_SIZE);
+ icon->setSingleLine(true);
+ if (this->switchFont != FONT_INVALID)
+ icon->setFontFace(this->switchFont);
+
+ Label* text = new Label();
+ text->setText(hintText);
+ text->setFontSize(HINT_FONT_SIZE);
+ text->setSingleLine(true);
+ text->setMarginLeft(HINT_ICON_GAP);
+ if (this->regularFont != FONT_INVALID)
+ text->setFontFace(this->regularFont);
+
+ item->addView(icon);
+ item->addView(text);
+ box->addView(item);
+}
+
+void AppletFrame::updateFooterHints()
+{
+ this->clearHintBox(this->footerHintRight);
+ this->clearHintBox(this->footerHintLeft);
+
+ std::set<ControllerButton> addedKeys;
+ std::vector<Action> rightActions;
+ std::vector<Action> leftActions;
+
+ View* focusParent = Application::getCurrentFocus();
+ if (!focusParent)
+ focusParent = this->contentView;
+
+ while (focusParent)
+ {
+ for (const Action& action : focusParent->getActions())
+ {
+ if (action.hidden || !action.available)
+ continue;
+
+ if (addedKeys.find(action.button) != addedKeys.end())
+ continue;
+
+ addedKeys.insert(action.button);
+
+ if (action.button == BUTTON_START)
+ leftActions.push_back(action);
+ else
+ rightActions.push_back(action);
+ }
+
+ focusParent = focusParent->getParent();
+ }
+
+ std::stable_sort(rightActions.begin(), rightActions.end(), actionsSort);
+ std::stable_sort(leftActions.begin(), leftActions.end(), actionsSort);
+
+ for (size_t i = 0; i < rightActions.size(); i++)
+ {
+ float spacing = (i + 1 < rightActions.size()) ? HINT_SPACING : 0.0f;
+ this->addHintItem(this->footerHintRight, rightActions[i], spacing);
+ }
+
+ for (size_t i = 0; i < leftActions.size(); i++)
+ {
+ float spacing = (i + 1 < leftActions.size()) ? HINT_SPACING : 0.0f;
+ this->addHintItem(this->footerHintLeft, leftActions[i], spacing);
+ }
+
+ std::string* commonFooter = Application::getCommonFooter();
+ if (commonFooter && !commonFooter->empty())
+ {
+ Label* text = new Label();
+ text->setText(*commonFooter);
+ text->setFontSize(HINT_FONT_SIZE);
+ text->setSingleLine(true);
+ if (!leftActions.empty())
+ text->setMarginLeft(HINT_SPACING);
+ if (this->regularFont != FONT_INVALID)
+ text->setFontFace(this->regularFont);
+ this->footerHintLeft->addView(text);
+ }
+
+ const bool hasRight = !this->footerHintRight->getChildren().empty();
+ const bool hasLeft = !this->footerHintLeft->getChildren().empty();
+
+ this->footerHintRight->setVisibility(hasRight ? Visibility::VISIBLE : Visibility::GONE);
+ this->footerHintLeft->setVisibility(hasLeft ? Visibility::VISIBLE : Visibility::GONE);
}
void AppletFrame::setIconFromRes(std::string name)
@@ -132,7 +345,6 @@ void AppletFrame::setContentView(View* view)
{
if (this->contentView)
{
- // Remove the node
this->removeView(this->contentView);
this->contentView = nullptr;
}
@@ -146,6 +358,7 @@ void AppletFrame::setContentView(View* view)
this->contentView->setGrow(1.0f);
this->addView(this->contentView, 1);
+ this->updateFooterHints();
}
void AppletFrame::handleXMLElement(tinyxml2::XMLElement* element)
diff --git a/library/lib/views/label.cpp b/library/lib/views/label.cpp
index c31d904..25e4bdf 100644
--- a/library/lib/views/label.cpp
+++ b/library/lib/views/label.cpp
@@ -316,6 +316,14 @@ void Label::setFontSize(float value)
this->invalidate();
}
+void Label::setFontFace(int fontFace)
+{
+ if (fontFace != FONT_INVALID)
+ this->font = fontFace;
+
+ this->invalidate();
+}
+
void Label::setLineHeight(float value)
{
this->lineHeight = value;

View File

@@ -0,0 +1,12 @@
--- a/library/lib/platforms/switch/swkbd.cpp
+++ b/library/lib/platforms/switch/swkbd.cpp
@@ -39,7 +39,9 @@ static SwkbdConfig createSwkbdBaseConfig(std::string headerText, std::string sub
swkbdConfigSetSubText(&config, subText.c_str());
swkbdConfigSetStringLenMax(&config, maxStringLength);
swkbdConfigSetInitialText(&config, initialText.c_str());
+#if defined(SWKBD_CONFIG_SET_STRING_LEN_MAX_EXT)
swkbdConfigSetStringLenMaxExt(&config, 1);
+#endif
swkbdConfigSetBlurBackground(&config, true);
return config;

View File

@@ -0,0 +1,15 @@
{
"hints": {
"ok": "Ok",
"back": "Zurück",
"exit": "Beenden"
},
"crash_frame": {
"button": "Ok"
},
"thumbnail_sidebar": {
"save": "Speichern"
}
}

View File

@@ -0,0 +1,15 @@
{
"hints": {
"ok": "Ok",
"back": "Back",
"exit": "Exit"
},
"crash_frame": {
"button": "Ok"
},
"thumbnail_sidebar": {
"save": "Save"
}
}

BIN
resources/img/omninx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

1
resources/inter Symbolic link
View File

@@ -0,0 +1 @@
../library/borealis/resources/inter

1
resources/material Symbolic link
View File

@@ -0,0 +1 @@
../library/borealis/resources/material

134
source/extractor.cpp Normal file
View File

@@ -0,0 +1,134 @@
#include "extractor.hpp"
#include <cstdio>
#include <cstring>
#include <sys/stat.h>
#ifdef __SWITCH__
#include <switch.h>
#endif
static constexpr size_t READ_BUF_SIZE = 8192;
int PatchExtractor::mkdirs(const char* path) {
char tmp[512];
snprintf(tmp, sizeof(tmp), "%s", path);
size_t len = strlen(tmp);
if (len == 0) return 0;
if (tmp[len - 1] == '/') tmp[len - 1] = '\0';
for (char* p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
mkdir(tmp, 0755);
*p = '/';
}
}
return mkdir(tmp, 0755);
}
bool PatchExtractor::open() {
zip = unzOpen(PATCHES_ZIP);
if (!zip) return false;
unz_global_info gi{};
if (unzGetGlobalInfo(zip, &gi) != UNZ_OK) {
close();
return false;
}
total = gi.number_entry;
extracted = 0;
skipped = 0;
finished = false;
err = unzGoToFirstFile(zip);
return true;
}
void PatchExtractor::close() {
if (zip) {
unzClose(zip);
zip = nullptr;
}
}
int PatchExtractor::getProgressPercent() const {
if (total == 0) return 100;
return static_cast<int>((extracted * 100) / total);
}
bool PatchExtractor::step() {
if (!zip || finished) return false;
if (err != UNZ_OK) {
finished = true;
return false;
}
char filename[512];
char fullpath[1024];
unz_file_info fi{};
unzGetCurrentFileInfo(zip, &fi, filename, sizeof(filename), nullptr, 0, nullptr, 0);
snprintf(fullpath, sizeof(fullpath), "%s%s", EXTRACT_DIR, filename);
currentFile = filename;
size_t flen = strlen(filename);
if (flen > 0 && filename[flen - 1] == '/') {
mkdirs(fullpath);
} else {
char dirpart[1024];
snprintf(dirpart, sizeof(dirpart), "%s", fullpath);
char* last_slash = strrchr(dirpart, '/');
if (last_slash) {
*last_slash = '\0';
mkdirs(dirpart);
}
if (unzOpenCurrentFile(zip) == UNZ_OK) {
FILE* out = fopen(fullpath, "wb");
if (out) {
unsigned char buf[READ_BUF_SIZE];
int bytes;
while ((bytes = unzReadCurrentFile(zip, buf, READ_BUF_SIZE)) > 0) {
fwrite(buf, 1, static_cast<size_t>(bytes), out);
}
fclose(out);
} else {
skipped++;
}
unzCloseCurrentFile(zip);
} else {
skipped++;
}
}
extracted++;
err = unzGoToNextFile(zip);
if (err != UNZ_OK) finished = true;
return !finished;
}
bool PatchExtractor::cleanup() {
cleanupOk_ = true;
if (remove(PATCHES_ZIP) != 0)
cleanupOk_ = false;
// Cannot delete our own NRO while the app is still running; try anyway for edge cases.
remove(SELF_NRO);
remove("sdmc:/switch/.PatchExtractor.nro.star");
remove("sdmc:/switch/.packages/boot_package.ini");
return cleanupOk_;
}
void PatchExtractor::tryDeleteSelfOnExit() {
#ifdef __SWITCH__
// Embedded ROMFS keeps the .nro open on SD; unmount before unlink (console build had no ROMFS).
romfsExit();
#endif
remove(SELF_NRO);
}

43
source/extractor.hpp Normal file
View File

@@ -0,0 +1,43 @@
#pragma once
#include <minizip/unzip.h>
#include <string>
class PatchExtractor {
public:
static constexpr const char* PATCHES_ZIP = "sdmc:/SaltySD/plugins/FPSLocker/patches.zip";
static constexpr const char* EXTRACT_DIR = "sdmc:/SaltySD/plugins/FPSLocker/";
static constexpr const char* SELF_NRO = "sdmc:/switch/PatchExtractor.nro";
bool open();
void close();
/** Process one zip entry. Returns false when finished or on fatal error. */
bool step();
bool isOpen() const { return zip != nullptr; }
unsigned long getTotal() const { return total; }
unsigned long getExtracted() const { return extracted; }
unsigned long getSkipped() const { return skipped; }
int getProgressPercent() const;
const std::string& getCurrentFile() const { return currentFile; }
bool isFinished() const { return finished; }
bool cleanup();
bool cleanupOk() const { return cleanupOk_; }
/** Call after the UI has shut down; releases romfs so the NRO file can be unlinked. */
static void tryDeleteSelfOnExit();
private:
static int mkdirs(const char* path);
unzFile zip = nullptr;
unsigned long total = 0;
unsigned long extracted = 0;
unsigned long skipped = 0;
int err = UNZ_OK;
bool finished = false;
bool cleanupOk_ = true;
std::string currentFile;
};

View File

@@ -1,202 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>
#include <switch.h>
#include <minizip/unzip.h>
#define PATCHES_ZIP "sdmc:/SaltySD/plugins/FPSLocker/patches.zip"
#define EXTRACT_DIR "sdmc:/SaltySD/plugins/FPSLocker/"
#define SELF_NRO "sdmc:/switch/PatchExtractor.nro"
#define READ_BUF_SIZE 8192
#define STATUS_ROW 11
static PrintConsole *con;
static void status(const char *fmt, ...) {
printf("\x1b[%d;0H\x1b[2K", STATUS_ROW);
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
}
static int mkdirs(const char *path) {
char tmp[512];
snprintf(tmp, sizeof(tmp), "%s", path);
size_t len = strlen(tmp);
if (len == 0) return 0;
if (tmp[len - 1] == '/') tmp[len - 1] = '\0';
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
mkdir(tmp, 0755);
*p = '/';
}
}
return mkdir(tmp, 0755);
}
int main(int argc, char *argv[]) {
con = consoleInit(NULL);
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
PadState pad;
padInitializeDefault(&pad);
printf("\n");
printf(" ========================================\n");
printf(" PatchExtractor - OmniNX OC\n");
printf(" SaltyNX / FPSLocker Patch Installer\n");
printf(" ========================================\n\n");
unzFile zip = unzOpen(PATCHES_ZIP);
if (!zip) {
printf(" \x1b[31mFEHLER:\x1b[0m patches.zip nicht gefunden!\n\n");
printf(" Pfad: %s\n\n", PATCHES_ZIP);
printf(" Bist du sicher, dass du OmniNX OC installiert hast?\n\n");
printf(" Die Datei patches.zip muss unter\n");
printf(" sd:/SaltySD/plugins/FPSLocker/ liegen.\n\n");
printf(" ----------------------------------------\n");
printf(" Druecke + zum Beenden.\n");
while (appletMainLoop()) {
padUpdate(&pad);
if (padGetButtonsDown(&pad) & HidNpadButton_Plus) break;
consoleUpdate(NULL);
}
consoleExit(NULL);
return 1;
}
unz_global_info gi;
unzGetGlobalInfo(zip, &gi);
unsigned long total = gi.number_entry;
printf(" patches.zip gefunden! (%lu Eintraege)\n", total);
printf(" Druecke A zum Entpacken, + zum Abbrechen.\n\n");
consoleUpdate(NULL);
while (appletMainLoop()) {
padUpdate(&pad);
u64 kDown = padGetButtonsDown(&pad);
if (kDown & HidNpadButton_A) break;
if (kDown & HidNpadButton_Plus) {
unzClose(zip);
consoleExit(NULL);
return 0;
}
consoleUpdate(NULL);
}
printf("\x1b[10;0H \x1b[93mBitte nicht beenden, bis der Vorgang abgeschlossen ist!\x1b[0m\n");
consoleUpdate(NULL);
char filename[512];
char fullpath[1024];
unsigned char buf[READ_BUF_SIZE];
unsigned long extracted = 0;
unsigned long skipped = 0;
int err = unzGoToFirstFile(zip);
while (err == UNZ_OK) {
unz_file_info fi;
unzGetCurrentFileInfo(zip, &fi, filename, sizeof(filename), NULL, 0, NULL, 0);
snprintf(fullpath, sizeof(fullpath), "%s%s", EXTRACT_DIR, filename);
unsigned long progress = (unsigned long)((extracted * 100) / total);
size_t flen = strlen(filename);
if (flen > 0 && filename[flen - 1] == '/') {
mkdirs(fullpath);
status(" [%3lu%%] DIR %s", progress, filename);
} else {
char dirpart[1024];
snprintf(dirpart, sizeof(dirpart), "%s", fullpath);
char *last_slash = strrchr(dirpart, '/');
if (last_slash) {
*last_slash = '\0';
mkdirs(dirpart);
}
if (unzOpenCurrentFile(zip) != UNZ_OK) {
status(" [%3lu%%] \x1b[31mERR\x1b[0m %s", progress, filename);
skipped++;
extracted++;
err = unzGoToNextFile(zip);
continue;
}
FILE *out = fopen(fullpath, "wb");
if (!out) {
status(" [%3lu%%] \x1b[31mERR\x1b[0m %s", progress, filename);
unzCloseCurrentFile(zip);
skipped++;
extracted++;
err = unzGoToNextFile(zip);
continue;
}
int bytes;
while ((bytes = unzReadCurrentFile(zip, buf, READ_BUF_SIZE)) > 0) {
fwrite(buf, 1, bytes, out);
}
fclose(out);
unzCloseCurrentFile(zip);
status(" [%3lu%%] FILE %s", progress, filename);
}
extracted++;
consoleUpdate(NULL);
err = unzGoToNextFile(zip);
}
unzClose(zip);
status(" [100%%] Fertig! %lu / %lu entpackt.", extracted - skipped, total);
consoleUpdate(NULL);
int cleanup_ok = 1;
printf("\x1b[%d;0H", STATUS_ROW + 2);
if (remove(PATCHES_ZIP) == 0) {
printf(" patches.zip geloescht.\n");
} else {
printf(" \x1b[31mpatches.zip konnte nicht geloescht werden.\x1b[0m\n");
cleanup_ok = 0;
}
if (remove(SELF_NRO) == 0) {
printf(" PatchExtractor.nro geloescht (Selbstreinigung).\n");
} else {
printf(" \x1b[31mPatchExtractor.nro konnte nicht geloescht werden.\x1b[0m\n");
cleanup_ok = 0;
}
remove("sdmc:/switch/.PatchExtractor.nro.star");
remove("sdmc:/switch/.packages/boot_package.ini");
printf("\n ========================================\n");
if (skipped == 0 && cleanup_ok) {
printf(" \x1b[32mAlles erledigt!\x1b[0m\n");
} else {
printf(" \x1b[32mEntpacken abgeschlossen.\x1b[0m\n");
if (skipped > 0)
printf(" \x1b[31m%lu Eintraege fehlgeschlagen.\x1b[0m\n", skipped);
}
printf(" ========================================\n\n");
printf(" Druecke + zum Beenden.\n");
while (appletMainLoop()) {
padUpdate(&pad);
if (padGetButtonsDown(&pad) & HidNpadButton_Plus) break;
consoleUpdate(NULL);
}
consoleExit(NULL);
return 0;
}

25
source/main.cpp Normal file
View File

@@ -0,0 +1,25 @@
#include <borealis.hpp>
#include "extractor.hpp"
#include "patch_activity.hpp"
int main(int argc, char* argv[]) {
(void)argc;
(void)argv;
brls::Logger::setLogLevel(brls::LogLevel::WARNING);
if (!brls::Application::init()) {
brls::Logger::error("Borealis init failed");
return 1;
}
brls::Application::createWindow("PatchExtractor");
brls::Application::setGlobalQuit(true);
brls::Application::pushActivity(new PatchActivity());
while (brls::Application::mainLoop()) {}
PatchExtractor::tryDeleteSelfOnExit();
return 0;
}

201
source/patch_activity.cpp Normal file
View File

@@ -0,0 +1,201 @@
#include "patch_activity.hpp"
#include <cstdio>
PatchActivity::PatchActivity() {
extractor = std::make_unique<PatchExtractor>();
extractTimer.setCallback([this]() { onExtractTick(); });
extractTimer.setPeriod(0);
}
PatchActivity::~PatchActivity() {
extractTimer.stop();
if (extractor) extractor->close();
}
brls::View* PatchActivity::createContentView() {
frame = new brls::AppletFrame();
frame->setTitle("PatchExtractor");
frame->setIconFromRes("img/omninx.png");
contentBox = new brls::Box(brls::Axis::COLUMN);
contentBox->setAlignItems(brls::AlignItems::CENTER);
contentBox->setJustifyContent(brls::JustifyContent::CENTER);
contentBox->setMargins(40, 40, 40, 40);
contentBox->setGrow(1.0f);
titleLabel = new brls::Label();
titleLabel->setText("PatchExtractor");
titleLabel->setFontSize(28);
titleLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER);
titleLabel->setMarginBottom(16);
messageLabel = new brls::Label();
messageLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER);
messageLabel->setMarginBottom(12);
detailLabel = new brls::Label();
detailLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER);
detailLabel->setFontSize(14);
detailLabel->setMarginBottom(24);
detailLabel->setVisibility(brls::Visibility::GONE);
progressLabel = new brls::Label();
progressLabel->setHorizontalAlign(brls::HorizontalAlign::CENTER);
progressLabel->setFontSize(16);
progressLabel->setMarginBottom(8);
progressLabel->setVisibility(brls::Visibility::GONE);
progressTrack = new brls::Box(brls::Axis::ROW);
progressTrack->setWidth(600);
progressTrack->setHeight(12);
progressTrack->setMarginBottom(24);
progressTrack->setVisibility(brls::Visibility::GONE);
progressFill = new brls::Rectangle(nvgRGB(0, 190, 80));
progressFill->setHeight(12);
progressFill->setWidth(0);
progressTrack->addView(progressFill);
actionButton = new brls::Button();
actionButton->setStyle(&brls::BUTTONSTYLE_PRIMARY);
actionButton->setWidth(320);
actionButton->registerClickAction([this](brls::View* view) { return onExtractClicked(view); });
contentBox->addView(titleLabel);
contentBox->addView(messageLabel);
contentBox->addView(detailLabel);
contentBox->addView(progressLabel);
contentBox->addView(progressTrack);
contentBox->addView(actionButton);
frame->setContentView(contentBox);
return frame;
}
void PatchActivity::onContentAvailable() {
registerExitAction(brls::BUTTON_START);
if (extractor->open()) {
char detail[128];
snprintf(detail, sizeof(detail), "%lu Eintraege in patches.zip", extractor->getTotal());
detailLabel->setText(detail);
showScreen(Screen::Ready);
} else {
detailLabel->setText(PatchExtractor::PATCHES_ZIP);
detailLabel->setVisibility(brls::Visibility::VISIBLE);
showScreen(Screen::Error);
}
}
void PatchActivity::showScreen(Screen screen) {
currentScreen = screen;
progressLabel->setVisibility(brls::Visibility::GONE);
progressTrack->setVisibility(brls::Visibility::GONE);
actionButton->setVisibility(brls::Visibility::VISIBLE);
switch (screen) {
case Screen::Error:
titleLabel->setText("Fehler");
messageLabel->setText("patches.zip nicht gefunden!");
detailLabel->setVisibility(brls::Visibility::VISIBLE);
actionButton->setText("Beenden");
actionButton->registerClickAction([](brls::View*) {
brls::Application::quit();
return true;
});
brls::Application::giveFocus(actionButton);
break;
case Screen::Ready:
titleLabel->setText("OmniNX OC");
messageLabel->setText("SaltyNX / FPSLocker Patch Installer");
detailLabel->setVisibility(brls::Visibility::VISIBLE);
actionButton->setText("Entpacken");
actionButton->registerClickAction([this](brls::View* view) { return onExtractClicked(view); });
brls::Application::giveFocus(actionButton);
break;
case Screen::Extracting:
titleLabel->setText("Entpacken...");
messageLabel->setText("Bitte nicht beenden, bis der Vorgang abgeschlossen ist!");
detailLabel->setVisibility(brls::Visibility::VISIBLE);
progressLabel->setVisibility(brls::Visibility::VISIBLE);
progressTrack->setVisibility(brls::Visibility::VISIBLE);
actionButton->setVisibility(brls::Visibility::GONE);
break;
case Screen::Done: {
titleLabel->setText("Fertig");
const unsigned long ok = extractor->getExtracted() - extractor->getSkipped();
char msg[128];
if (extractor->getSkipped() == 0 && extractor->cleanupOk())
snprintf(msg, sizeof(msg), "Alles erledigt! %lu / %lu entpackt.", ok, extractor->getTotal());
else
snprintf(msg, sizeof(msg), "Entpacken abgeschlossen. %lu / %lu entpackt.", ok, extractor->getTotal());
messageLabel->setText(msg);
if (extractor->getSkipped() > 0) {
char err[64];
snprintf(err, sizeof(err), "%lu Eintraege fehlgeschlagen.", extractor->getSkipped());
detailLabel->setText(err);
detailLabel->setVisibility(brls::Visibility::VISIBLE);
} else if (!extractor->cleanupOk()) {
detailLabel->setText("Aufräumen teilweise fehlgeschlagen.");
detailLabel->setVisibility(brls::Visibility::VISIBLE);
} else {
detailLabel->setVisibility(brls::Visibility::GONE);
}
actionButton->setText("Beenden");
actionButton->setVisibility(brls::Visibility::VISIBLE);
actionButton->registerClickAction([](brls::View*) {
brls::Application::quit();
return true;
});
brls::Application::unblockInputs();
brls::Application::giveFocus(actionButton);
break;
}
}
}
bool PatchActivity::onExtractClicked(brls::View* view) {
(void)view;
if (currentScreen != Screen::Ready) return false;
startExtraction();
return true;
}
void PatchActivity::startExtraction() {
showScreen(Screen::Extracting);
brls::Application::blockInputs();
updateProgressUi();
extractTimer.start();
}
void PatchActivity::onExtractTick() {
if (!extractor->step()) {
extractTimer.stop();
extractor->close();
extractor->cleanup();
updateProgressUi();
showScreen(Screen::Done);
return;
}
updateProgressUi();
}
void PatchActivity::updateProgressUi() {
const int pct = extractor->getProgressPercent();
progressLabel->setText(std::to_string(pct) + "%");
constexpr float trackWidth = 600.0f;
progressFill->setWidth(trackWidth * static_cast<float>(pct) / 100.0f);
const std::string& file = extractor->getCurrentFile();
if (!file.empty())
detailLabel->setText(file);
}

39
source/patch_activity.hpp Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include <borealis.hpp>
#include <memory>
#include "extractor.hpp"
class PatchActivity : public brls::Activity {
public:
PatchActivity();
~PatchActivity() override;
brls::View* createContentView() override;
void onContentAvailable() override;
private:
enum class Screen { Error, Ready, Extracting, Done };
void showScreen(Screen screen);
void startExtraction();
void onExtractTick();
void updateProgressUi();
bool onExtractClicked(brls::View* view);
brls::AppletFrame* frame = nullptr;
brls::Box* contentBox = nullptr;
brls::Label* titleLabel = nullptr;
brls::Label* messageLabel = nullptr;
brls::Label* detailLabel = nullptr;
brls::Label* progressLabel = nullptr;
brls::Box* progressTrack = nullptr;
brls::Rectangle* progressFill = nullptr;
brls::Button* actionButton = nullptr;
std::unique_ptr<PatchExtractor> extractor;
brls::RepeatingTimer extractTimer;
Screen currentScreen = Screen::Ready;
};