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; +};