mirror of
https://github.com/J-D-K/JKSV.git
synced 2026-04-26 01:59:55 -05:00
Add update check and download on boot.
This commit is contained in:
parent
74678357bb
commit
22b9734c37
2
Makefile
2
Makefile
|
|
@ -39,7 +39,7 @@ INCLUDES := include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SD
|
|||
EXEFS_SRC := exefs_src
|
||||
APP_TITLE := JKSV
|
||||
APP_AUTHOR := JK
|
||||
APP_VERSION := 09.25.2025
|
||||
APP_VERSION := 09.26.2025
|
||||
ROMFS := romfs
|
||||
ICON := icon.jpg
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class ConfirmState final : public BaseState
|
|||
, m_yesText(strings::get_by_name(strings::names::YES_NO_OK, 0))
|
||||
, m_noText(strings::get_by_name(strings::names::YES_NO_OK, 1))
|
||||
, m_holdRequired(holdRequired)
|
||||
, m_transition(280, 720, 280, 262, 4)
|
||||
, m_transition(280, 720, 280, 229, 4)
|
||||
, m_function(function)
|
||||
, m_taskData(taskData)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ class MainMenuState final : public BaseState
|
|||
/// @brief Renders menu to screen.
|
||||
void render() override;
|
||||
|
||||
/// @brief Allows the update task to signal it found an update.
|
||||
void signal_update_found();
|
||||
|
||||
/// @brief Signals to
|
||||
static void initialize_view_states();
|
||||
|
||||
|
|
@ -45,6 +48,7 @@ class MainMenuState final : public BaseState
|
|||
struct DataStruct : sys::Task::DataStruct
|
||||
{
|
||||
data::UserList userList;
|
||||
MainMenuState *spawningState{};
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
|
@ -70,6 +74,9 @@ class MainMenuState final : public BaseState
|
|||
/// @brief This is the data struct passed to tasks.
|
||||
std::shared_ptr<MainMenuState::DataStruct> m_dataStruct{};
|
||||
|
||||
/// @brief Allows the check_for_update task to signal that an update was found (to prevent corrupted textures!)
|
||||
std::atomic_bool m_updateFound{};
|
||||
|
||||
/// @brief Records the size of the sm_users vector.
|
||||
static inline int sm_userCount{};
|
||||
|
||||
|
|
@ -94,6 +101,9 @@ class MainMenuState final : public BaseState
|
|||
/// @brief Initializes the data struct.
|
||||
void initialize_data_struct();
|
||||
|
||||
/// @brief Silently checks for an update in the background.
|
||||
void check_for_update();
|
||||
|
||||
/// @brief Pushes the target state to the vector.
|
||||
void push_target_state();
|
||||
|
||||
|
|
@ -102,4 +112,6 @@ class MainMenuState final : public BaseState
|
|||
|
||||
/// @brief Backups up all save data for all users.
|
||||
void backup_all_for_all();
|
||||
|
||||
void confirm_update();
|
||||
};
|
||||
|
|
|
|||
8
include/builddate.hpp
Normal file
8
include/builddate.hpp
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
namespace builddate
|
||||
{
|
||||
inline constexpr int MONTH = 9;
|
||||
inline constexpr int DAY = 26;
|
||||
inline constexpr int YEAR = 2025;
|
||||
}
|
||||
|
|
@ -56,6 +56,8 @@ namespace curl
|
|||
static inline void reset_handle(curl::Handle &curl)
|
||||
{
|
||||
curl_easy_reset(curl.get());
|
||||
curl::set_option(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
curl::set_option(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
curl::set_option(curl, CURLOPT_USERAGENT, curl::STRING_USER_AGENT);
|
||||
curl::set_option(curl, CURLOPT_CONNECTTIMEOUT, 5L);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ namespace strings::names
|
|||
inline constexpr std::string_view TITLEOPTION_STATUS = "TitleOptionStatus";
|
||||
inline constexpr std::string_view TITLEOPTION = "TitleOptions";
|
||||
inline constexpr std::string_view TRANSLATION = "TranslationInfo";
|
||||
inline constexpr std::string_view UPDATE_CONFIRMATION = "UpdateConfirmation";
|
||||
inline constexpr std::string_view USEROPTION_CONFS = "UserOptionConfirmations";
|
||||
inline constexpr std::string_view USEROPTION_STATUS = "UserOptionStatus";
|
||||
inline constexpr std::string_view USEROPTION_MENU = "UserOptions";
|
||||
|
|
|
|||
11
include/tasks/update.hpp
Normal file
11
include/tasks/update.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
#include "sys/threadpool.hpp"
|
||||
|
||||
namespace tasks::update
|
||||
{
|
||||
/// @brief Checks for updates. If one is found, prompts to download it.
|
||||
void check_for_update(sys::threadpool::JobData jobData);
|
||||
|
||||
/// @brief Downloads the new JKSV NRO.
|
||||
void download_update(sys::threadpool::JobData jobData);
|
||||
}
|
||||
|
|
@ -267,6 +267,9 @@
|
|||
"0: Translated by: %s",
|
||||
"1: NULL"
|
||||
],
|
||||
"UpdateConfirmation": [
|
||||
"A new update has been found for JKSV. Would you like to download it now?"
|
||||
],
|
||||
"UserOptionConfirmations": [
|
||||
"0: Are you sure you want to backup the save data for every title found for `%s`? This can take a while.",
|
||||
"1: Are you sure you want to create save data for all titles found on your system for `%s`? This can take a while.",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include "appstates/FileModeState.hpp"
|
||||
#include "appstates/MainMenuState.hpp"
|
||||
#include "appstates/TaskState.hpp"
|
||||
#include "builddate.hpp"
|
||||
#include "config/config.hpp"
|
||||
#include "curl/curl.hpp"
|
||||
#include "data/data.hpp"
|
||||
|
|
@ -29,13 +30,6 @@
|
|||
|
||||
namespace
|
||||
{
|
||||
/// @brief Build month.
|
||||
constexpr uint8_t BUILD_MON = 9;
|
||||
/// @brief Build day.
|
||||
constexpr uint8_t BUILD_DAY = 25;
|
||||
/// @brief Year.
|
||||
constexpr uint16_t BUILD_YEAR = 2025;
|
||||
|
||||
/// @brief Config for socket.
|
||||
constexpr SocketInitConfig SOCKET_INIT_CONFIG = {.tcp_tx_buf_size = 0x20000,
|
||||
.tcp_rx_buf_size = 0x20000,
|
||||
|
|
@ -83,7 +77,7 @@ JKSV::JKSV()
|
|||
const char *author = strings::get_by_name(strings::names::TRANSLATION, 1);
|
||||
m_showTranslationInfo = std::char_traits<char>::compare(author, "NULL", 4) != 0; // This is whether or not to show.
|
||||
m_translationInfo = stringutil::get_formatted_string(translationFormat, author);
|
||||
m_buildString = stringutil::get_formatted_string("v. %02d.%02d.%04d", BUILD_MON, BUILD_DAY, BUILD_YEAR);
|
||||
m_buildString = stringutil::get_formatted_string("v. %02d.%02d.%04d", builddate::MONTH, builddate::DAY, builddate::YEAR);
|
||||
|
||||
// This needs the config init'd or read to work.
|
||||
JKSV::create_directories();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
#include "sdl.hpp"
|
||||
#include "strings/strings.hpp"
|
||||
#include "stringutil.hpp"
|
||||
#include "sys/sys.hpp"
|
||||
#include "tasks/mainmenu.hpp"
|
||||
#include "tasks/update.hpp"
|
||||
#include "ui/PopMessageManager.hpp"
|
||||
|
||||
MainMenuState::MainMenuState()
|
||||
|
|
@ -32,6 +34,7 @@ MainMenuState::MainMenuState()
|
|||
MainMenuState::initialize_menu();
|
||||
MainMenuState::initialize_view_states();
|
||||
MainMenuState::initialize_data_struct();
|
||||
MainMenuState::check_for_update();
|
||||
}
|
||||
|
||||
void MainMenuState::update()
|
||||
|
|
@ -47,6 +50,11 @@ void MainMenuState::update()
|
|||
if (aPressed) { MainMenuState::push_target_state(); }
|
||||
else if (toUserOptions) { MainMenuState::create_user_options(); }
|
||||
else if (yPressed) { MainMenuState::backup_all_for_all(); }
|
||||
else if (m_updateFound.load())
|
||||
{
|
||||
m_updateFound.store(false);
|
||||
MainMenuState::confirm_update();
|
||||
}
|
||||
|
||||
m_mainMenu->update(hasFocus);
|
||||
m_controlGuide->update(hasFocus);
|
||||
|
|
@ -73,6 +81,8 @@ void MainMenuState::render()
|
|||
}
|
||||
}
|
||||
|
||||
void MainMenuState::signal_update_found() { m_updateFound.store(true); }
|
||||
|
||||
void MainMenuState::initialize_view_states()
|
||||
{
|
||||
const bool jksmMode = config::get_by_key(config::keys::JKSM_TEXT_MODE);
|
||||
|
|
@ -118,7 +128,13 @@ void MainMenuState::initialize_menu()
|
|||
m_mainMenu->add_option(m_extrasIcon);
|
||||
}
|
||||
|
||||
void MainMenuState::initialize_data_struct() { m_dataStruct->userList = sm_users; }
|
||||
void MainMenuState::initialize_data_struct()
|
||||
{
|
||||
m_dataStruct->userList = sm_users;
|
||||
m_dataStruct->spawningState = this;
|
||||
}
|
||||
|
||||
void MainMenuState::check_for_update() { sys::threadpool::push_job(tasks::update::check_for_update, m_dataStruct); }
|
||||
|
||||
void MainMenuState::push_target_state()
|
||||
{
|
||||
|
|
@ -165,3 +181,11 @@ void MainMenuState::backup_all_for_all()
|
|||
}
|
||||
else { ConfirmProgress::create_push_fade(query, true, tasks::mainmenu::backup_all_for_all_local, m_dataStruct); }
|
||||
}
|
||||
|
||||
void MainMenuState::confirm_update()
|
||||
{
|
||||
const char *confirmUpdate = strings::get_by_name(strings::names::UPDATE_CONFIRMATION, 0);
|
||||
auto taskData = std::make_shared<sys::Task::DataStruct>();
|
||||
|
||||
ConfirmProgress::create_push_fade(confirmUpdate, false, tasks::update::download_update, taskData);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
MessageState::MessageState(std::string_view message)
|
||||
: m_message(message)
|
||||
, m_transition(280, 720, 280, 262, 4)
|
||||
, m_transition(280, 720, 280, 229, 4)
|
||||
{
|
||||
MessageState::initialize_static_members();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@
|
|||
namespace
|
||||
{
|
||||
constexpr int COORD_BAR_X = 312;
|
||||
constexpr int COORD_BAR_Y = 470;
|
||||
constexpr int COORD_BAR_Y = 437;
|
||||
|
||||
constexpr int COORD_TEXT_Y = 475;
|
||||
constexpr int COORD_TEXT_Y = 442;
|
||||
|
||||
constexpr int COORD_DISPLAY_CENTER = 640;
|
||||
constexpr double SIZE_BAR_WIDTH = 656.0f;
|
||||
|
|
@ -54,9 +54,9 @@ void ProgressState::render()
|
|||
|
||||
sdl::render_rect_fill(sdl::Texture::Null, 0, 0, 1280, 720, colors::DIM_BACKGROUND);
|
||||
sm_dialog->render(sdl::Texture::Null, hasFocus);
|
||||
sdl::text::render(sdl::Texture::Null, 312, 288, BaseTask::FONT_SIZE, 656, colors::WHITE, status);
|
||||
sdl::text::render(sdl::Texture::Null, 312, 255, BaseTask::FONT_SIZE, 656, colors::WHITE, status);
|
||||
|
||||
sdl::render_line(sdl::Texture::Null, 280, 454, 999, 454, colors::DIV_COLOR);
|
||||
sdl::render_line(sdl::Texture::Null, 280, 421, 999, 421, colors::DIV_COLOR);
|
||||
sdl::render_rect_fill(sdl::Texture::Null, COORD_BAR_X, COORD_BAR_Y, barWidth, 32, colors::BLACK);
|
||||
sdl::render_rect_fill(sdl::Texture::Null, COORD_BAR_X, COORD_BAR_Y, m_progressBarWidth, 32, colors::BAR_GREEN);
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ void ProgressState::initialize_static_members()
|
|||
|
||||
if (sm_dialog && sm_barEdges) { return; }
|
||||
|
||||
sm_dialog = ui::DialogBox::create(280, 262, 720, 256);
|
||||
sm_dialog = ui::DialogBox::create(280, 229, 720, 256);
|
||||
sm_barEdges = sdl::TextureManager::load(BAR_EDGE_NAME, "romfs:/Textures/BarEdges.png");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ void curl::prepare_get(curl::Handle &curl)
|
|||
|
||||
// Setup basic request.
|
||||
curl::set_option(curl, CURLOPT_HTTPGET, 1L);
|
||||
// curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
}
|
||||
|
||||
void curl::prepare_post(curl::Handle &curl)
|
||||
|
|
@ -189,7 +189,7 @@ void curl::prepare_post(curl::Handle &curl)
|
|||
curl::reset_handle(curl);
|
||||
|
||||
curl::set_option(curl, CURLOPT_POST, 1L);
|
||||
// curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
}
|
||||
|
||||
void curl::prepare_upload(curl::Handle &curl)
|
||||
|
|
@ -197,5 +197,5 @@ void curl::prepare_upload(curl::Handle &curl)
|
|||
curl::reset_handle(curl);
|
||||
|
||||
curl::set_option(curl, CURLOPT_UPLOAD, 1L);
|
||||
// curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
int main(int argc, const char *argv[])
|
||||
{
|
||||
JKSV jksv{};
|
||||
while (appletMainLoop() && jksv.is_running())
|
||||
while (jksv.is_running())
|
||||
{
|
||||
jksv.update();
|
||||
jksv.render();
|
||||
|
|
|
|||
128
source/tasks/update.cpp
Normal file
128
source/tasks/update.cpp
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
#include "tasks/update.hpp"
|
||||
|
||||
#include "JKSV.hpp"
|
||||
#include "appstates/ConfirmState.hpp"
|
||||
#include "appstates/MainMenuState.hpp"
|
||||
#include "builddate.hpp"
|
||||
#include "curl/curl.hpp"
|
||||
#include "error.hpp"
|
||||
#include "json.hpp"
|
||||
#include "logging/logger.hpp"
|
||||
#include "remote/remote.hpp"
|
||||
#include "strings/strings.hpp"
|
||||
#include "stringutil.hpp"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
// Defined at bottom.
|
||||
static std::string get_git_json(curl::Handle &handle);
|
||||
static bool get_date_as_ints(std::string_view dateString, int &month, int &day, int &year);
|
||||
|
||||
void tasks::update::check_for_update(sys::threadpool::JobData jobData)
|
||||
{
|
||||
auto castData = std::static_pointer_cast<MainMenuState::DataStruct>(jobData);
|
||||
MainMenuState *spawningState = castData->spawningState;
|
||||
|
||||
// Just borrow this.
|
||||
if (!remote::has_internet_connection()) { return; }
|
||||
|
||||
curl::Handle curlHandle = curl::new_handle();
|
||||
const std::string gitJson = get_git_json(curlHandle);
|
||||
json::Object parser = json::new_object(json_tokener_parse, gitJson.c_str());
|
||||
if (!parser) { return; }
|
||||
|
||||
json_object *tagName = json::get_object(parser, "tag_name");
|
||||
if (!tagName) { return; }
|
||||
|
||||
int month{}, day{}, year{};
|
||||
const char *dateString = json_object_get_string(tagName);
|
||||
get_date_as_ints(dateString, month, day, year);
|
||||
|
||||
const bool greaterYear = year > builddate::YEAR;
|
||||
const bool greaterMonth = year == builddate::YEAR && month > builddate::MONTH;
|
||||
const bool greaterDay = year == builddate::YEAR && month == builddate::MONTH && day > builddate::DAY;
|
||||
const bool updateAvailable = greaterYear || greaterMonth || greaterDay;
|
||||
if (updateAvailable) { spawningState->signal_update_found(); }
|
||||
}
|
||||
|
||||
void tasks::update::download_update(sys::threadpool::JobData jobData)
|
||||
{
|
||||
auto castData = std::static_pointer_cast<sys::Task::DataStruct>(jobData);
|
||||
sys::ProgressTask *task = static_cast<sys::ProgressTask *>(castData->task);
|
||||
if (error::is_null(task)) { return; }
|
||||
|
||||
curl::Handle downloadCurl = curl::new_handle();
|
||||
const std::string gitJson = get_git_json(downloadCurl);
|
||||
json::Object parser = json::new_object(json_tokener_parse, gitJson.c_str());
|
||||
if (!parser) { TASK_FINISH_RETURN(task); }
|
||||
|
||||
const char *downloadingFormat = strings::get_by_name(strings::names::IO_STATUSES, 4);
|
||||
std::string status = stringutil::get_formatted_string(downloadingFormat, "JKSV.nro");
|
||||
task->set_status(status);
|
||||
|
||||
json_object *assets = json::get_object(parser, "assets");
|
||||
if (!assets) { TASK_FINISH_RETURN(task); }
|
||||
|
||||
json_object *assetZero = json_object_array_get_idx(assets, 0);
|
||||
if (!assetZero) { TASK_FINISH_RETURN(task); }
|
||||
|
||||
json_object *downloadUrl = json_object_object_get(assetZero, "browser_download_url");
|
||||
json_object *downloadSize = json_object_object_get(assetZero, "size");
|
||||
if (!downloadUrl || !downloadSize) { TASK_FINISH_RETURN(task); }
|
||||
|
||||
const char *nroUrl = json_object_get_string(downloadUrl);
|
||||
const uint64_t nroSize = json_object_get_uint64(downloadSize);
|
||||
task->reset(static_cast<double>(nroSize));
|
||||
|
||||
// To do: Figure out how to get the argument from main here.
|
||||
error::libnx(romfsExit()); // This is needed so I can overwrite the NRO.
|
||||
fslib::File jksv{"sdmc:/switch/JKSV.nro", FsOpenMode_Create | FsOpenMode_Write, static_cast<int64_t>(nroSize)};
|
||||
if (error::fslib(jksv.is_open())) { TASK_FINISH_RETURN(task); }
|
||||
|
||||
auto download = curl::create_download_struct(jksv, task, nroSize);
|
||||
curl::set_option(downloadCurl, CURLOPT_URL, nroUrl);
|
||||
curl::set_option(downloadCurl, CURLOPT_WRITEFUNCTION, curl::download_file_threaded);
|
||||
curl::set_option(downloadCurl, CURLOPT_WRITEDATA, download.get());
|
||||
curl::set_option(downloadCurl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
sys::threadpool::push_job(curl::download_write_thread_function, download);
|
||||
if (!curl::perform(downloadCurl)) { TASK_FINISH_RETURN(task); }
|
||||
download->writeComplete.acquire();
|
||||
|
||||
task->complete();
|
||||
}
|
||||
|
||||
static std::string get_git_json(curl::Handle &handle)
|
||||
{
|
||||
static constexpr const char *URL_GIT_API = "https://api.github.com/repos/J-D-K/JKSV/releases/latest";
|
||||
|
||||
std::string response{};
|
||||
curl::prepare_get(handle);
|
||||
curl::set_option(handle, CURLOPT_URL, URL_GIT_API);
|
||||
curl::set_option(handle, CURLOPT_WRITEFUNCTION, curl::write_response_string);
|
||||
curl::set_option(handle, CURLOPT_WRITEDATA, &response);
|
||||
curl::set_option(handle, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl::perform(handle);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static bool get_date_as_ints(std::string_view dateString, int &month, int &day, int &year)
|
||||
{
|
||||
size_t monthSlash = dateString.find_first_of('/');
|
||||
if (monthSlash == dateString.npos) { return false; }
|
||||
|
||||
const std::string monthString{dateString.substr(0, monthSlash)};
|
||||
|
||||
size_t daySlash = dateString.find_first_of('/', ++monthSlash);
|
||||
if (daySlash == dateString.npos) { return false; }
|
||||
|
||||
const std::string dayString{dateString.substr(monthSlash, daySlash - monthSlash)};
|
||||
const std::string yearString{dateString.substr(++daySlash, dateString.npos)};
|
||||
|
||||
month = std::strtol(monthString.c_str(), nullptr, 10);
|
||||
day = std::strtol(dayString.c_str(), nullptr, 10);
|
||||
year = std::strtol(yearString.c_str(), nullptr, 10);
|
||||
|
||||
return true;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user