diff --git a/Source/hoc-clk/overlay/Makefile b/Source/hoc-clk/overlay/Makefile
index 2fe755ba..67ed2c13 100644
--- a/Source/hoc-clk/overlay/Makefile
+++ b/Source/hoc-clk/overlay/Makefile
@@ -69,7 +69,7 @@ CXXFLAGS := $(CFLAGS) -fno-exceptions -std=gnu++23
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
-LIBS := -lcurl -lz -lzzip -lmbedtls -lmbedx509 -lmbedcrypto -ljansson -lnx
+LIBS := -lcurl -lz -lminizip -lmbedtls -lmbedx509 -lmbedcrypto -ljansson -lnx
#---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing
diff --git a/Source/hoc-clk/overlay/src/ui/gui/app_profile_gui.cpp b/Source/hoc-clk/overlay/src/ui/gui/app_profile_gui.cpp
index 8d1965e1..cb1e15c0 100644
--- a/Source/hoc-clk/overlay/src/ui/gui/app_profile_gui.cpp
+++ b/Source/hoc-clk/overlay/src/ui/gui/app_profile_gui.cpp
@@ -12,9 +12,9 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
- *
+ *
*/
-
+
/* --------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* , ,
@@ -121,7 +121,7 @@ void AppProfileGui::addModuleListItem(HocClkProfile profile, HocClkModule module
// Reset to "Default" (0 MHz)
this->profileList->mhzMap[profile][module] = 0;
listItem->setValue(module == HocClkModule_MEM ? formatListFreqMem(0, memUnit) : formatListFreqMHz(0));
-
+
Result rc = hocclkIpcSetProfiles(this->applicationId, this->profileList);
if(R_FAILED(rc))
{
@@ -139,19 +139,19 @@ void AppProfileGui::addModuleListItemToggle(HocClkProfile profile, HocClkModule
{
const char* moduleName = hocclkFormatModule(module, true);
std::uint32_t currentValue = this->profileList->mhzMap[profile][module];
-
+
tsl::elm::ToggleListItem* toggle = new tsl::elm::ToggleListItem(moduleName, currentValue != 0);
-
+
toggle->setStateChangedListener([this, profile, module](bool state) {
this->profileList->mhzMap[profile][module] = state ? 1 : 0;
-
+
Result rc = hocclkIpcSetProfiles(this->applicationId, this->profileList);
if(R_FAILED(rc))
{
FatalGui::openWithResultCode("hocclkIpcSetProfiles", rc);
}
});
-
+
this->listElement->addItem(toggle);
}
@@ -166,7 +166,7 @@ std::string AppProfileGui::formatValueDisplay(
if (value == 0) {
return FREQ_DEFAULT_TEXT;
}
-
+
if (!namedValues.empty()) {
for (const auto& namedValue : namedValues) {
if (namedValue.value == value) {
@@ -174,7 +174,7 @@ std::string AppProfileGui::formatValueDisplay(
}
}
}
-
+
char buf[32];
if (decimalPlaces > 0) {
double displayValue = (double)value / divisor;
@@ -203,9 +203,9 @@ void AppProfileGui::addModuleListItemValue(
tsl::elm::ListItem* listItem =
new tsl::elm::ListItem(hocclkFormatModule(module, true));
std::uint32_t storedValue = this->profileList->mhzMap[profile][module];
-
+
listItem->setValue(this->formatValueDisplay(storedValue, namedValues, suffix, divisor, decimalPlaces));
-
+
listItem->setClickListener(
[this,
listItem,
@@ -243,7 +243,7 @@ void AppProfileGui::addModuleListItemValue(
{
this->profileList->mhzMap[profile][module] = value / divisor;
listItem->setValue(this->formatValueDisplay(value / divisor, namedValues, suffix, divisor, decimalPlaces));
-
+
Result rc =
hocclkIpcSetProfiles(this->applicationId,
this->profileList);
@@ -341,7 +341,7 @@ void AppProfileGui::addGovernorSection(HocClkProfile profile) {
}
void AppProfileGui::addProfileUI(HocClkProfile profile)
-{
+{
BaseMenuGui::refresh();
if(!this->context)
return;
@@ -395,7 +395,7 @@ void AppProfileGui::addProfileUI(HocClkProfile profile)
NamedValue("230 Hz", 230),
NamedValue("240 Hz", 240)
};
-
+
this->addModuleListItemValue(profile, HocClkModule_Display, "Display", 40, 240, 1, " Hz", 1, 0, DThresholdsOLED, dockedFreqs);
} else if (IsAula() && !this->context->isSysDockInstalled) {
std::vector dockedFreqsLimited = {
@@ -407,7 +407,7 @@ void AppProfileGui::addProfileUI(HocClkProfile profile)
NamedValue("72 Hz", 72),
NamedValue("75 Hz", 75)
};
-
+
this->addModuleListItemValue(profile, HocClkModule_Display, "Display", 50, 75, 1, " Hz", 1, 0, DThresholdsOLED, dockedFreqsLimited);
} else {
std::vector dockedFreqsStandard = {
diff --git a/Source/hoc-clk/overlay/src/ui/gui/base_menu_gui.cpp b/Source/hoc-clk/overlay/src/ui/gui/base_menu_gui.cpp
index 548a2bc1..2f00d4e5 100644
--- a/Source/hoc-clk/overlay/src/ui/gui/base_menu_gui.cpp
+++ b/Source/hoc-clk/overlay/src/ui/gui/base_menu_gui.cpp
@@ -361,4 +361,4 @@ tsl::elm::Element *BaseMenuGui::baseUI() {
this->listUI();
return list;
-}
\ No newline at end of file
+}
diff --git a/Source/hoc-clk/overlay/src/ui/gui/main_gui.cpp b/Source/hoc-clk/overlay/src/ui/gui/main_gui.cpp
index 4b2021e8..027204f7 100644
--- a/Source/hoc-clk/overlay/src/ui/gui/main_gui.cpp
+++ b/Source/hoc-clk/overlay/src/ui/gui/main_gui.cpp
@@ -31,7 +31,7 @@
#include "main_gui.h"
#include "misc_gui.h"
#include "ult_ext.h"
-
+#include "update_gui.h"
tsl::elm::Element *MainGui::baseUI() {
auto *list = new BoxClippedList();
@@ -110,6 +110,18 @@ void MainGui::listUI() {
return false;
});
this->listElement->addItem(aboutItem);
+
+ tsl::elm::ListItem *updateItem = new tsl::elm::ListItem("Updates");
+ updateItem->setClickListener([this](u64 keys) {
+ if ((keys & HidNpadButton_A) == HidNpadButton_A && this->context) {
+ tsl::changeTo();
+ return true;
+ }
+
+ return false;
+ });
+ this->listElement->addItem(updateItem);
+
}
void MainGui::refresh() {
diff --git a/Source/hoc-clk/overlay/src/ui/gui/update_gui.cpp b/Source/hoc-clk/overlay/src/ui/gui/update_gui.cpp
new file mode 100644
index 00000000..40b7f413
--- /dev/null
+++ b/Source/hoc-clk/overlay/src/ui/gui/update_gui.cpp
@@ -0,0 +1,269 @@
+/*
+ * Copyright (c) Souldbminer, Lightos_ and Horizon OC Contributors
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU General Public License,
+ * version 2, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope 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 .
+ */
+
+#include "update_gui.h"
+#include "ult_ext.h"
+#include "base_menu_gui.h"
+#include "../format.h"
+#include "../style.h"
+
+#include
+#include
+#include
+#include
+
+
+static constexpr u16 PANEL_H = 80;
+
+class UpdateStatusPanel : public tsl::elm::Element {
+public:
+ void setState(int stage, int progress,
+ const std::string &pkg, const std::string &msg) {
+ m_stage = stage;
+ m_progress = progress;
+ m_pkg = pkg;
+ m_msg = msg;
+ }
+
+ void draw(tsl::gfx::Renderer *renderer) override {
+ if (this->getHeight() == 0) return;
+
+ const s32 PAD = 20;
+ const s32 x = this->getX() + PAD;
+ const s32 y = this->getY();
+ const s32 w = this->getWidth() - PAD * 2;
+
+ if (m_stage == 0) {
+ return;
+ }
+
+ if (m_stage == 1 || m_stage == 2) {
+ const char *opLabel = (m_stage == 1) ? "Downloading" : "Extracting";
+ tsl::Color accent = (m_stage == 1)
+ ? tsl::Color{0x4, 0x8, 0xF, 0xF} // blue
+ : tsl::Color{0x3, 0xA, 0x8, 0xF}; // teal
+
+ renderer->drawString(opLabel, false, x, y + 20, 15, accent);
+ renderer->drawString(m_pkg.c_str(), false, x + 104, y + 20, 15,
+ tsl::sectionTextColor);
+
+ const s32 barY = y + 48;
+ renderer->drawRect(x, barY, w, 7, tsl::Color{0x3, 0x3, 0x3, 0xF});
+
+ int pct = (m_progress < 0) ? 0 : (m_progress > 100 ? 100 : m_progress);
+ s32 fillW = (s32)((long)w * pct / 100);
+ if (fillW > 0)
+ renderer->drawRect(x, barY, fillW, 7, accent);
+
+ char buf[8];
+ if (m_progress < 0)
+ std::snprintf(buf, sizeof(buf), "...");
+ else
+ std::snprintf(buf, sizeof(buf), "%d%%", pct);
+
+ u32 tw = renderer->getTextDimensions(buf, false, 13).first;
+ renderer->drawString(buf, false,
+ x + w - (s32)tw, barY - 14, 13, tsl::infoTextColor);
+
+ } else {
+ tsl::Color col;
+ switch (m_stage) {
+ case 3: col = {0x3, 0xB, 0x5, 0xF}; break; // Done → green
+ case 4: col = {0xE, 0x3, 0x3, 0xF}; break; // Failed → red
+ default: col = {0x7, 0x7, 0x7, 0xF}; break; // Cancel → grey
+ }
+ renderer->drawString(m_msg.c_str(), false, x, y + 30, 14, col);
+ }
+ }
+
+ void layout(u16, u16, u16, u16) override {
+ this->setBoundaries(this->getX(), this->getY(),
+ this->getWidth(), PANEL_H);
+ }
+
+ Element *requestFocus(Element *, tsl::FocusDirection) override {
+ return nullptr;
+ }
+
+private:
+ int m_stage = 0;
+ int m_progress = 0;
+ std::string m_pkg;
+ std::string m_msg;
+};
+
+UpdateGui::UpdateGui() = default;
+UpdateGui::~UpdateGui() { reapThread(); }
+
+void UpdateGui::startJob(int packageIndex, bool extractOnly) {
+ if (isBusy()) return;
+
+ m_activePackage = packageIndex;
+ m_extractOnly = extractOnly;
+ m_resultMessage.clear();
+
+ ult::abortDownload.store(false, std::memory_order_release);
+ ult::abortUnzip.store(false, std::memory_order_release);
+
+ m_stage.store(UpdateStage::Downloading, std::memory_order_release);
+
+ if (R_SUCCEEDED(threadCreate(&m_thread, jobEntry, this,
+ nullptr, 0x8000, 0x20, -2))) {
+ m_threadActive = true;
+ threadStart(&m_thread);
+ } else {
+ m_resultMessage = "Failed to create thread";
+ m_stage.store(UpdateStage::Failed, std::memory_order_release);
+ m_threadActive = false;
+ }
+}
+
+void UpdateGui::requestCancel() {
+ ult::abortDownload.store(true, std::memory_order_release);
+ ult::abortUnzip.store(true, std::memory_order_release);
+}
+
+void UpdateGui::reapThread() {
+ if (m_threadActive) {
+ threadWaitForExit(&m_thread);
+ threadClose(&m_thread);
+ m_threadActive = false;
+ }
+}
+
+/* static */ void UpdateGui::jobEntry(void *arg) {
+ static_cast(arg)->jobBody();
+}
+
+void UpdateGui::jobBody() {
+ const PackageInfo &pkg = kPackages[m_activePackage];
+
+ if (!m_extractOnly) {
+ m_stage.store(UpdateStage::Downloading, std::memory_order_release);
+ ult::createDirectory("sdmc:/config/horizon-oc/");
+
+ bool ok = ult::downloadFile(pkg.url, pkg.zipPath, false, false);
+ if (!ok || ult::abortDownload.load(std::memory_order_acquire)) {
+ m_resultMessage = ult::abortDownload.load(std::memory_order_acquire)
+ ? "Download cancelled"
+ : "Download failed \u2014 check network";
+ m_stage.store(
+ ult::abortDownload.load() ? UpdateStage::Cancelled : UpdateStage::Failed,
+ std::memory_order_release);
+ return;
+ }
+ }
+
+ m_stage.store(UpdateStage::Extracting, std::memory_order_release);
+
+ bool ok = ult::unzipFile(pkg.zipPath, "sdmc:/");
+ if (!ok || ult::abortUnzip.load(std::memory_order_acquire)) {
+ m_resultMessage = ult::abortUnzip.load(std::memory_order_acquire)
+ ? "Extraction cancelled"
+ : "Extraction failed \u2014 archive may be corrupt";
+ m_stage.store(
+ ult::abortUnzip.load() ? UpdateStage::Cancelled : UpdateStage::Failed,
+ std::memory_order_release);
+ return;
+ }
+
+ m_resultMessage = "Update complete! Restart required.";
+ m_stage.store(UpdateStage::Done, std::memory_order_release);
+}
+
+
+void UpdateGui::listUI() {
+ BaseMenuGui::refresh();
+ if (!this->context) return;
+
+ this->listElement->addItem(new tsl::elm::CategoryHeader("Packages"));
+
+ for (int i = 0; i < 2; ++i) {
+ auto *item = new tsl::elm::ListItem(kPackages[i].displayName);
+ item->setValue("\uE0E0");
+
+ const int idx = i;
+ item->setClickListener([this, idx](u64 keys) -> bool {
+ if (isBusy()) return false;
+ if (keys & HidNpadButton_A) {
+ startJob(idx, false);
+ return true;
+ }
+ return false;
+ });
+
+ m_items[i] = item;
+ this->listElement->addItem(item);
+ }
+
+ this->listElement->addItem(new tsl::elm::CategoryHeader("Status"));
+
+ auto *panel = new UpdateStatusPanel();
+ panel->setState(0, 0, "", "");
+ m_status = panel;
+ this->listElement->addItem(panel);
+}
+
+void UpdateGui::pollJob() {
+ auto stage = m_stage.load(std::memory_order_acquire);
+ if (stage == UpdateStage::Idle || !m_status) return;
+
+ const char *pkgName = (m_activePackage >= 0 && m_activePackage < 2)
+ ? kPackages[m_activePackage].displayName : "";
+
+ if (stage == UpdateStage::Downloading) {
+ int pct = ult::downloadPercentage.load(std::memory_order_relaxed);
+ m_status->setState(1, pct, pkgName, "");
+ if (m_items[m_activePackage])
+ m_items[m_activePackage]->setValue("...");
+ return;
+ }
+
+ if (stage == UpdateStage::Extracting) {
+ int pct = ult::unzipPercentage.load(std::memory_order_relaxed);
+ m_status->setState(2, pct, pkgName, "");
+ if (m_items[m_activePackage])
+ m_items[m_activePackage]->setValue("...");
+ return;
+ }
+
+ int panelStage = (stage == UpdateStage::Done) ? 3
+ : (stage == UpdateStage::Failed) ? 4 : 5;
+
+ m_status->setState(panelStage, 100, pkgName, m_resultMessage);
+
+ for (int i = 0; i < 2; ++i)
+ if (m_items[i]) m_items[i]->setValue("\uE0E0");
+
+ reapThread();
+ m_stage.store(UpdateStage::Idle, std::memory_order_release);
+}
+
+void UpdateGui::update() {
+ BaseMenuGui::update();
+ pollJob();
+}
+
+bool UpdateGui::handleInput(u64 keysDown, u64 keysHeld,
+ const HidTouchState &touchPos,
+ HidAnalogStickState leftJoy,
+ HidAnalogStickState rightJoy) {
+ if ((keysDown & HidNpadButton_R) && isBusy()) {
+ requestCancel();
+ return true;
+ }
+ return false;
+}
diff --git a/Source/hoc-clk/overlay/src/ui/gui/update_gui.h b/Source/hoc-clk/overlay/src/ui/gui/update_gui.h
new file mode 100644
index 00000000..0bb3f699
--- /dev/null
+++ b/Source/hoc-clk/overlay/src/ui/gui/update_gui.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) Souldbminer, Lightos_ and Horizon OC Contributors
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU General Public License,
+ * version 2, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope 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 .
+ */
+
+#pragma once
+#include "../../ipc.h"
+
+#include
+#include
+
+#include "base_menu_gui.h"
+
+class UpdateStatusPanel;
+
+class UpdateGui : public BaseMenuGui {
+
+public:
+ UpdateGui();
+ ~UpdateGui();
+
+ void listUI() override;
+ void update() override;
+ bool handleInput(u64 keysDown, u64 keysHeld, const HidTouchState &touchPos,
+ HidAnalogStickState leftJoy, HidAnalogStickState rightJoy) override;
+
+private:
+ enum class UpdateStage {
+ Idle,
+ Downloading,
+ Extracting,
+ Done,
+ Failed,
+ Cancelled,
+ };
+
+ struct PackageInfo {
+ const char *displayName;
+ const char *url;
+ const char *zipPath;
+ };
+
+ static constexpr PackageInfo kPackages[2] = {
+ {"HOC", "https://github.com/Horizon-OC/Horizon-OC/releases/latest/download/dist.zip", "sdmc:/config/horizon-oc/dist.zip"},
+ {"HOC-EXT", "https://github.com/Horizon-OC/Horizon-OC/releases/latest/download/dist_ext.zip", "sdmc:/config/horizon-oc/dist_ext.zip"},
+ };
+
+ bool isBusy() const {
+ return m_stage.load(std::memory_order_acquire) != UpdateStage::Idle;
+ }
+
+ void startJob(int packageIndex, bool extractOnly);
+ void requestCancel();
+ void reapThread();
+ void pollJob();
+
+ static void jobEntry(void *arg);
+ void jobBody();
+
+ std::atomic m_stage{UpdateStage::Idle};
+ int m_activePackage = -1;
+ bool m_extractOnly = false;
+ bool m_threadActive = false;
+ Thread m_thread{};
+ std::string m_resultMessage;
+
+ tsl::elm::ListItem *m_items[2] = {nullptr, nullptr};
+ UpdateStatusPanel *m_status = nullptr;
+};