From 2c2f602d146feb1e19009ec71fbc5e4e3a0235c2 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Sun, 27 Apr 2025 20:01:13 +0100 Subject: [PATCH] add gc_menu, add progress, icon, time remaining to progress bar (see full commit message). - fix ignore distribution bit doing nothing. - fix yati failing to parse control nca causing the transfer to abort. - yati now uses ncm rather than ns to get the latest app version. - improve ui::list input handling (it handles directional buttons now). - progress bar displays speed and time remaining. - added gc menu (taken from my gc installer nx and gci). --- sphaira/include/app.hpp | 7 + sphaira/include/ui/list.hpp | 4 +- sphaira/include/ui/menus/gc_menu.hpp | 67 +++- sphaira/include/ui/nvg_util.hpp | 14 +- sphaira/include/ui/progress_box.hpp | 21 +- sphaira/include/yati/nx/nca.hpp | 2 + sphaira/include/yati/nx/ncm.hpp | 4 +- sphaira/include/yati/nx/tik.h | 265 ------------- sphaira/include/yati/yati.hpp | 27 +- sphaira/source/app.cpp | 198 +++++++++- sphaira/source/owo.cpp | 2 +- sphaira/source/ui/list.cpp | 33 +- sphaira/source/ui/menus/appstore.cpp | 67 +--- sphaira/source/ui/menus/filebrowser.cpp | 33 +- sphaira/source/ui/menus/ftp_menu.cpp | 6 +- sphaira/source/ui/menus/gc_menu.cpp | 474 ++++++++++++++++++++---- sphaira/source/ui/menus/ghdl.cpp | 29 +- sphaira/source/ui/menus/homebrew.cpp | 40 +- sphaira/source/ui/menus/main_menu.cpp | 182 +-------- sphaira/source/ui/menus/themezer.cpp | 47 +-- sphaira/source/ui/menus/usb_menu.cpp | 6 +- sphaira/source/ui/nvg_util.cpp | 44 +-- sphaira/source/ui/popup_list.cpp | 16 +- sphaira/source/ui/progress_box.cpp | 107 +++++- sphaira/source/ui/sidebar.cpp | 18 +- sphaira/source/yati/nx/nca.cpp | 26 ++ sphaira/source/yati/nx/ncm.cpp | 20 +- sphaira/source/yati/yati.cpp | 230 +++++++----- 28 files changed, 1059 insertions(+), 930 deletions(-) delete mode 100644 sphaira/include/yati/nx/tik.h diff --git a/sphaira/include/app.hpp b/sphaira/include/app.hpp index 29fcbf5..36e2985 100644 --- a/sphaira/include/app.hpp +++ b/sphaira/include/app.hpp @@ -102,6 +102,13 @@ public: static void PlaySoundEffect(SoundEffect effect); + static void DisplayThemeOptions(bool left_side = true); + // todo: + static void DisplayNetworkOptions(bool left_side = true); + static void DisplayMiscOptions(bool left_side = true); + static void DisplayAdvancedOptions(bool left_side = true); + static void DisplayInstallOptions(bool left_side = true); + void Draw(); void Update(); void Poll(); diff --git a/sphaira/include/ui/list.hpp b/sphaira/include/ui/list.hpp index 7d5a97d..088f9e5 100644 --- a/sphaira/include/ui/list.hpp +++ b/sphaira/include/ui/list.hpp @@ -6,11 +6,11 @@ namespace sphaira::ui { struct List final : Object { using Callback = std::function; - using TouchCallback = std::function; + using TouchCallback = std::function; List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {}); - void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback); + void OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback); void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const; diff --git a/sphaira/include/ui/menus/gc_menu.hpp b/sphaira/include/ui/menus/gc_menu.hpp index 49af9a6..279dd00 100644 --- a/sphaira/include/ui/menus/gc_menu.hpp +++ b/sphaira/include/ui/menus/gc_menu.hpp @@ -3,20 +3,38 @@ #include "ui/menus/menu_base.hpp" #include "yati/container/base.hpp" #include "yati/source/base.hpp" +#include "ui/list.hpp" +#include +#include namespace sphaira::ui::menu::gc { -enum class State { - // no gamecard inserted. - None, - // set whilst transfer is in progress. - Progress, - // set when the transfer is finished. - Done, - // set when no gamecard is inserted. - NotFound, - // failed to parse gamecard. - Failed, +struct GcCollection : yati::container::CollectionEntry { + GcCollection(const char* _name, s64 _size, u8 _type) { + name = _name; + size = _size; + type = _type; + } + + // NcmContentType + u8 type{}; +}; + +using GcCollections = std::vector; + +struct ApplicationEntry { + u64 app_id{}; + u32 version{}; + u8 key_gen{}; + + std::vector application{}; + std::vector patch{}; + std::vector add_on{}; + std::vector data_patch{}; + yati::container::Collections tickets{}; + + auto GetSize() const -> s64; + auto GetSize(const std::vector& entries) const -> s64; }; struct Menu final : MenuBase { @@ -26,13 +44,32 @@ struct Menu final : MenuBase { void Update(Controller* controller, TouchInfo* touch) override; void Draw(NVGcontext* vg, Theme* theme) override; - Result ScanGamecard(); +private: + Result GcMount(); + void GcUnmount(); + Result GcPoll(bool* inserted); + Result UpdateStorageSize(); + + void FreeImage(); + void OnChangeIndex(s64 new_index); private: - std::unique_ptr m_fs{}; FsDeviceOperator m_dev_op{}; - yati::container::Collections m_collections{}; - State m_state{State::None}; + FsGameCardHandle m_handle{}; + std::unique_ptr m_fs{}; + + std::vector m_entries{}; + std::unique_ptr m_list{}; + s64 m_entry_index{}; + s64 m_option_index{}; + + s64 m_size_free_sd{}; + s64 m_size_total_sd{}; + s64 m_size_free_nand{}; + s64 m_size_total_nand{}; + NacpLanguageEntry m_lang_entry{}; + int m_icon{}; + bool m_mounted{}; }; } // namespace sphaira::ui::menu::gc diff --git a/sphaira/include/ui/nvg_util.hpp b/sphaira/include/ui/nvg_util.hpp index 139aadf..0be7260 100644 --- a/sphaira/include/ui/nvg_util.hpp +++ b/sphaira/include/ui/nvg_util.hpp @@ -5,17 +5,15 @@ namespace sphaira::ui::gfx { -void drawImage(NVGcontext*, float x, float y, float w, float h, int texture); -void drawImage(NVGcontext*, const Vec4& v, int texture); -void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture); -void drawImageRounded(NVGcontext*, const Vec4& v, int texture); +void drawImage(NVGcontext*, float x, float y, float w, float h, int texture, float rounded = 0.F); +void drawImage(NVGcontext*, const Vec4& v, int texture, float rounded = 0.F); void dimBackground(NVGcontext*); -void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false); -void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false); -void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false); -void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, bool rounded = false); +void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, float rounding = 0.F); +void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, float rounding = 0.F); +void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, float rounding = 0.F); +void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.F); void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h); void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v); diff --git a/sphaira/include/ui/progress_box.hpp b/sphaira/include/ui/progress_box.hpp index 3e8c7b7..bc8fc8a 100644 --- a/sphaira/include/ui/progress_box.hpp +++ b/sphaira/include/ui/progress_box.hpp @@ -3,6 +3,7 @@ #include "widget.hpp" #include "fs.hpp" #include +#include namespace sphaira::ui { @@ -12,6 +13,8 @@ using ProgressBoxDoneCallback = std::function; struct ProgressBox final : Widget { ProgressBox( + int image, + const std::string& action, const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){}, int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024 @@ -24,6 +27,9 @@ struct ProgressBox final : Widget { auto SetTitle(const std::string& title) -> ProgressBox&; auto NewTransfer(const std::string& transfer) -> ProgressBox&; auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&; + // not const in order to avoid copy by using std::swap + auto SetImageData(std::vector& data) -> ProgressBox&; + auto SetImageDataConst(std::span data) -> ProgressBox&; void RequestExit(); auto ShouldExit() -> bool; @@ -41,6 +47,9 @@ struct ProgressBox final : Widget { }; } +private: + void FreeImage(); + public: struct ThreadData { ProgressBox* pbox{}; @@ -52,12 +61,22 @@ private: Mutex m_mutex{}; Thread m_thread{}; ThreadData m_thread_data{}; - ProgressBoxDoneCallback m_done{}; + + // shared data start. + std::string m_action{}; std::string m_title{}; std::string m_transfer{}; s64 m_size{}; s64 m_offset{}; + s64 m_last_offset{}; + s64 m_speed{}; + TimeStamp m_timestamp{}; + std::vector m_image_data{}; + // shared data end. + + int m_image{}; + bool m_own_image{}; }; // this is a helper function that does many things. diff --git a/sphaira/include/yati/nx/nca.hpp b/sphaira/include/yati/nx/nca.hpp index 7a403ad..cc4e086 100644 --- a/sphaira/include/yati/nx/nca.hpp +++ b/sphaira/include/yati/nx/nca.hpp @@ -215,4 +215,6 @@ Result DecryptKeak(const keys::Keys& keys, Header& header); Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation); Result VerifyFixedKey(const Header& header); +auto GetKeyGenStr(u8 key_gen) -> const char*; + } // namespace sphaira::nca diff --git a/sphaira/include/yati/nx/ncm.hpp b/sphaira/include/yati/nx/ncm.hpp index 95d3729..5efcd0e 100644 --- a/sphaira/include/yati/nx/ncm.hpp +++ b/sphaira/include/yati/nx/ncm.hpp @@ -19,7 +19,7 @@ static_assert(sizeof(PackagedContentMeta) == 0x20); struct ContentStorageRecord { NcmContentMetaKey key; - u8 storage_id; + u8 storage_id; // NcmStorageId u8 padding[0x7]; }; @@ -31,7 +31,9 @@ union ExtendedHeader { NcmDataPatchMetaExtendedHeader data_patch; }; +auto GetAppId(u8 meta_type, u64 id) -> u64; auto GetAppId(const NcmContentMetaKey& key) -> u64; +auto GetAppId(const PackagedContentMeta& meta) -> u64; Result Delete(NcmContentStorage* cs, const NcmContentId *content_id); Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id); diff --git a/sphaira/include/yati/nx/tik.h b/sphaira/include/yati/nx/tik.h deleted file mode 100644 index c949936..0000000 --- a/sphaira/include/yati/nx/tik.h +++ /dev/null @@ -1,265 +0,0 @@ -/* - * tik.h - * - * Copyright (c) 2020-2024, DarkMatterCore . - * - * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). - * - * nxdumptool is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * nxdumptool is distributed in the hope that 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 - -#ifndef __TIK_H__ -#define __TIK_H__ - -#include "signature.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define SIGNED_TIK_MIN_SIZE sizeof(TikSigHmac160) /* Assuming no ESV1/ESV2 records are available. */ -#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */ - -#define TIK_FORMAT_VERSION 2 - -#define GENERATE_TIK_STRUCT(sigtype, tiksize) \ -typedef struct { \ - SignatureBlock##sigtype sig_block; \ - TikCommonBlock tik_common_block; \ - u8 es_section_record_data[]; \ -} TikSig##sigtype; \ -NXDT_ASSERT(TikSig##sigtype, tiksize); - -typedef enum { - TikTitleKeyType_Common = 0, - TikTitleKeyType_Personalized = 1, - TikTitleKeyType_Count = 2 ///< Total values supported by this enum. -} TikTitleKeyType; - -typedef enum { - TikLicenseType_Permanent = 0, - TikLicenseType_Demo = 1, - TikLicenseType_Trial = 2, - TikLicenseType_Rental = 3, - TikLicenseType_Subscription = 4, - TikLicenseType_Service = 5, - TikLicenseType_Count = 6 ///< Total values supported by this enum. -} TikLicenseType; - -typedef enum { - TikPropertyMask_None = 0, - TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats. - TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats. - TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats. - TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console. - TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted. - TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification. - TikPropertyMask_Count = 6 ///< Total values supported by this enum. -} TikPropertyMask; - -/// Placed after the ticket signature block. -typedef struct { - char issuer[0x40]; - u8 titlekey_block[0x100]; - u8 format_version; ///< Always matches TIK_FORMAT_VERSION. - u8 titlekey_type; ///< TikTitleKeyType. - u16 ticket_version; - u8 license_type; ///< TikLicenseType. - u8 key_generation; ///< NcaKeyGeneration. - u16 property_mask; ///< TikPropertyMask. - u8 reserved[0x8]; - u64 ticket_id; - u64 device_id; - FsRightsId rights_id; - u32 account_id; - u32 sect_total_size; - u32 sect_hdr_offset; - u16 sect_hdr_count; - u16 sect_hdr_entry_size; -} TikCommonBlock; - -NXDT_ASSERT(TikCommonBlock, 0x180); - -/// ESV1/ESV2 section records are placed right after the ticket data. These aren't available in TikTitleKeyType_Common tickets. -/// These are only used if the sect_* fields from the common block are non-zero (other than 'sect_hdr_offset'). -/// Each ESV2 section record is followed by a 'record_count' number of ESV1 records, each one of 'record_size' size. - -typedef enum { - TikSectionType_None = 0, - TikSectionType_Permanent = 1, - TikSectionType_Subscription = 2, - TikSectionType_Content = 3, - TikSectionType_ContentConsumption = 4, - TikSectionType_AccessTitle = 5, - TikSectionType_LimitedResource = 6, - TikSectionType_Count = 7 ///< Total values supported by this enum. -} TikSectionType; - -typedef struct { - u32 sect_offset; - u32 record_size; - u32 section_size; - u16 record_count; - u16 section_type; ///< TikSectionType. -} TikESV2SectionRecord; - -/// Used with TikSectionType_Permanent. -typedef struct { - u8 ref_id[0x10]; - u32 ref_id_attr; -} TikESV1PermanentRecord; - -/// Used with TikSectionType_Subscription. -typedef struct { - u32 limit; - u8 ref_id[0x10]; - u32 ref_id_attr; -} TikESV1SubscriptionRecord; - -/// Used with TikSectionType_Content. -typedef struct { - u32 offset; - u8 access_mask[0x80]; -} TikESV1ContentRecord; - -/// Used with TikSectionType_ContentConsumption. -typedef struct { - u16 index; - u16 code; - u32 limit; -} TikESV1ContentConsumptionRecord; - -/// Used with TikSectionType_AccessTitle. -typedef struct { - u64 access_title_id; - u64 access_title_mask; -} TikESV1AccessTitleRecord; - -/// Used with TikSectionType_LimitedResource. -typedef struct { - u32 limit; - u8 ref_id[0x10]; - u32 ref_id_attr; -} TikESV1LimitedResourceRecord; - -/// All tickets generated below use a little endian sig_type field. -GENERATE_TIK_STRUCT(Rsa4096, 0x3C0); /// RSA-4096 signature. -GENERATE_TIK_STRUCT(Rsa2048, 0x2C0); /// RSA-2048 signature. -GENERATE_TIK_STRUCT(Ecc480, 0x200); /// ECC signature. -GENERATE_TIK_STRUCT(Hmac160, 0x1C0); /// HMAC signature. - -/// Ticket type. -typedef enum { - TikType_None = 0, - TikType_SigRsa4096 = 1, - TikType_SigRsa2048 = 2, - TikType_SigEcc480 = 3, - TikType_SigHmac160 = 4, - TikType_Count = 5 ///< Total values supported by this enum. -} TikType; - -/// Used to store ticket type, size and raw data, as well as titlekey data. -typedef struct { - u8 type; ///< TikType. - u64 size; ///< Raw ticket size. - u8 data[SIGNED_TIK_MAX_SIZE]; ///< Raw ticket data. - u8 key_generation; ///< NcaKeyGeneration. - u8 enc_titlekey[0x10]; ///< Titlekey with titlekek crypto (RSA-OAEP unwrapped if dealing with a TikTitleKeyType_Personalized ticket). - char enc_titlekey_str[0x21]; ///< Character string representation of enc_titlekey. - u8 dec_titlekey[0x10]; ///< Titlekey without titlekek crypto. Ready to use for NCA FS section decryption. - char dec_titlekey_str[0x21]; ///< Character string representation of dec_titlekey. - char rights_id_str[0x21]; ///< Character string representation of the rights ID from the ticket. -} Ticket; - -/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure Hash FS partition from an inserted gamecard. -/// Both the input rights ID and key generation values must have been retrieved from a NCA that depends on the desired ticket. -/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek-decrypted right away. -bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard); - -/// Converts a TikTitleKeyType_Personalized ticket into a TikTitleKeyType_Common ticket and optionally generates a raw certificate chain for the new signature issuer. -/// Bear in mind the 'size' member from the Ticket parameter will be updated by this function to remove any possible references to ESV1/ESV2 records. -/// If both 'out_raw_cert_chain' and 'out_raw_cert_chain_size' pointers are provided, raw certificate chain data will be saved to them. -/// certGenerateRawCertificateChainBySignatureIssuer() is used internally, so the output buffer must be freed by the user. -bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size); - -/// Helper inline functions for signed ticket blobs. - -NX_INLINE TikCommonBlock *tikGetCommonBlockFromSignedTicketBlob(void *buf) -{ - return (TikCommonBlock*)signatureGetPayloadFromSignedBlob(buf, false); -} - -NX_INLINE u64 tikGetSectionRecordsSizeFromSignedTicketBlob(void *buf) -{ - TikCommonBlock *tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf); - if (!tik_common_block) return 0; - - u64 offset = sizeof(TikCommonBlock), out_size = 0; - - for(u32 i = 0; i < tik_common_block->sect_hdr_count; i++) - { - TikESV2SectionRecord *rec = (TikESV2SectionRecord*)((u8*)tik_common_block + offset); - offset += (sizeof(TikESV2SectionRecord) + ((u64)rec->record_count * (u64)rec->record_size)); - out_size += offset; - } - - return out_size; -} - -NX_INLINE bool tikIsValidSignedTicketBlob(void *buf) -{ - u64 ticket_size = (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock)); - return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) <= SIGNED_TIK_MAX_SIZE); -} - -NX_INLINE u64 tikGetSignedTicketBlobSize(void *buf) -{ - return (tikIsValidSignedTicketBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0); -} - -NX_INLINE u64 tikGetSignedTicketBlobHashAreaSize(void *buf) -{ - return (tikIsValidSignedTicketBlob(buf) ? (sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0); -} - -/// Helper inline functions for Ticket elements. - -NX_INLINE bool tikIsValidTicket(Ticket *tik) -{ - return (tik && tik->type > TikType_None && tik->type < TikType_Count && tik->size >= SIGNED_TIK_MIN_SIZE && tik->size <= SIGNED_TIK_MAX_SIZE && tikIsValidSignedTicketBlob(tik->data)); -} - -NX_INLINE TikCommonBlock *tikGetCommonBlockFromTicket(Ticket *tik) -{ - return (tikIsValidTicket(tik) ? tikGetCommonBlockFromSignedTicketBlob(tik->data) : NULL); -} - -NX_INLINE bool tikIsPersonalizedTicket(Ticket *tik) -{ - TikCommonBlock *tik_common_block = tikGetCommonBlockFromTicket(tik); - return (tik_common_block ? (tik_common_block->titlekey_type == TikTitleKeyType_Personalized) : false); -} - -NX_INLINE u64 tikGetHashAreaSizeFromTicket(Ticket *tik) -{ - return (tikIsValidTicket(tik) ? tikGetSignedTicketBlobHashAreaSize(tik->data) : 0); -} - -#ifdef __cplusplus -} -#endif - -#endif /* __TIK_H__ */ diff --git a/sphaira/include/yati/yati.hpp b/sphaira/include/yati/yati.hpp index d7fdbc9..d448fb4 100644 --- a/sphaira/include/yati/yati.hpp +++ b/sphaira/include/yati/yati.hpp @@ -10,8 +10,10 @@ #include "fs.hpp" #include "source/base.hpp" #include "container/base.hpp" +#include "nx/ncm.hpp" #include "ui/progress_box.hpp" #include +#include namespace sphaira::yati { @@ -112,10 +114,25 @@ struct Config { bool lower_system_version{}; }; -Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path); -Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path); -Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr source, const fs::FsPath& path); -Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr container); -Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections); +// overridable options, set to avoid +struct ConfigOverride { + std::optional sd_card_install{}; + std::optional skip_nca_hash_verify{}; + std::optional skip_rsa_header_fixed_key_verify{}; + std::optional skip_rsa_npdm_fixed_key_verify{}; + std::optional ignore_distribution_bit{}; + std::optional convert_to_standard_crypto{}; + std::optional lower_master_key{}; + std::optional lower_system_version{}; +}; + +Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override = {}); +Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override = {}); +Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr source, const fs::FsPath& path, const ConfigOverride& override = {}); +Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr container, const ConfigOverride& override = {}); +Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections, const ConfigOverride& override = {}); + +Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector& extended_header, std::vector& infos); +Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector* icon_out = nullptr); } // namespace sphaira::yati diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 6bde6e5..0916588 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -1,7 +1,18 @@ -#include "ui/menus/main_menu.hpp" -#include "ui/error_box.hpp" #include "ui/option_box.hpp" #include "ui/bubbles.hpp" +#include "ui/sidebar.hpp" +#include "ui/popup_list.hpp" +#include "ui/option_box.hpp" +#include "ui/progress_box.hpp" +#include "ui/error_box.hpp" + +#include "ui/menus/main_menu.hpp" +#include "ui/menus/irs_menu.hpp" +#include "ui/menus/themezer.hpp" +#include "ui/menus/ghdl.hpp" +#include "ui/menus/usb_menu.hpp" +#include "ui/menus/ftp_menu.hpp" +#include "ui/menus/gc_menu.hpp" #include "app.hpp" #include "log.hpp" @@ -15,6 +26,7 @@ #include "defines.hpp" #include "i18n.hpp" #include "ftpsrv_helper.hpp" +#include "web.hpp" #include #include @@ -1374,15 +1386,6 @@ App::App(const char* argv0) { const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH); ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH); - s64 sd_free_space; - if (R_SUCCEEDED(fs.GetFreeSpace("/", &sd_free_space))) { - log_write("sd_free_space: %zd\n", sd_free_space); - } - s64 sd_total_space; - if (R_SUCCEEDED(fs.GetTotalSpace("/", &sd_total_space))) { - log_write("sd_total_space: %zd\n", sd_total_space); - } - // load default image if (R_SUCCEEDED(romfsInit())) { ON_SCOPE_EXIT(romfsExit()); @@ -1437,6 +1440,179 @@ void App::PlaySoundEffect(SoundEffect effect) { plsrPlayerPlay(id); } +void App::DisplayThemeOptions(bool left_side) { + ui::SidebarEntryArray::Items theme_items{}; + const auto theme_meta = App::GetThemeMetaList(); + for (auto& p : theme_meta) { + theme_items.emplace_back(p.name); + } + + auto options = std::make_shared("Theme Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(options)); + + options->Add(std::make_shared("Select Theme"_i18n, theme_items, [theme_items](s64& index_out){ + App::SetTheme(index_out); + }, App::GetThemeIndex())); + + options->Add(std::make_shared("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){ + App::SetThemeMusicEnable(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){ + App::Set12HourTimeEnable(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); +} + +void App::DisplayNetworkOptions(bool left_side) { + +} + +void App::DisplayMiscOptions(bool left_side) { + auto options = std::make_shared("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(options)); + + options->Add(std::make_shared("Themezer"_i18n, [](){ + App::Push(std::make_shared()); + })); + + options->Add(std::make_shared("GitHub"_i18n, [](){ + App::Push(std::make_shared()); + })); + + options->Add(std::make_shared("Irs"_i18n, [](){ + App::Push(std::make_shared()); + })); + + if (App::IsApplication()) { + options->Add(std::make_shared("Web"_i18n, [](){ + WebShow("https://lite.duckduckgo.com/lite"); + })); + } + + if (App::GetApp()->m_install.Get()) { + if (App::GetFtpEnable()) { + options->Add(std::make_shared("Ftp Install"_i18n, [](){ + App::Push(std::make_shared()); + })); + } + + options->Add(std::make_shared("Usb Install"_i18n, [](){ + App::Push(std::make_shared()); + })); + + options->Add(std::make_shared("GameCard Install"_i18n, [](){ + App::Push(std::make_shared()); + })); + } +} + +void App::DisplayAdvancedOptions(bool left_side) { + auto options = std::make_shared("Advanced Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(options)); + + ui::SidebarEntryArray::Items text_scroll_speed_items; + text_scroll_speed_items.push_back("Slow"_i18n); + text_scroll_speed_items.push_back("Normal"_i18n); + text_scroll_speed_items.push_back("Fast"_i18n); + + options->Add(std::make_shared("Logging"_i18n, App::GetLogEnable(), [](bool& enable){ + App::SetLogEnable(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){ + App::SetReplaceHbmenuEnable(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){ + App::SetTextScrollSpeed(index_out); + }, (s64)App::GetTextScrollSpeed())); + + options->Add(std::make_shared("Install options"_i18n, [left_side](){ + App::DisplayInstallOptions(left_side); + })); +} + +void App::DisplayInstallOptions(bool left_side) { + auto options = std::make_shared("Install Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(options)); + + ui::SidebarEntryArray::Items install_items; + install_items.push_back("System memory"_i18n); + install_items.push_back("microSD card"_i18n); + + options->Add(std::make_shared("Enable"_i18n, App::GetInstallEnable(), [](bool& enable){ + App::SetInstallEnable(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Show install warning"_i18n, App::GetInstallPrompt(), [](bool& enable){ + App::SetInstallPrompt(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Install location"_i18n, install_items, [](s64& index_out){ + App::SetInstallSdEnable(index_out); + }, (s64)App::GetInstallSdEnable())); + + options->Add(std::make_shared("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){ + App::GetApp()->m_allow_downgrade.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){ + App::GetApp()->m_skip_if_already_installed.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){ + App::GetApp()->m_ticket_only.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){ + App::GetApp()->m_skip_base.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){ + App::GetApp()->m_skip_patch.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){ + App::GetApp()->m_skip_addon.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){ + App::GetApp()->m_skip_data_patch.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){ + App::GetApp()->m_skip_ticket.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){ + App::GetApp()->m_skip_nca_hash_verify.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){ + App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){ + App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){ + App::GetApp()->m_ignore_distribution_bit.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){ + App::GetApp()->m_convert_to_standard_crypto.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){ + App::GetApp()->m_lower_master_key.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); + + options->Add(std::make_shared("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){ + App::GetApp()->m_lower_system_version.Set(enable); + }, "Enabled"_i18n, "Disabled"_i18n)); +} + App::~App() { log_write("starting to exit\n"); diff --git a/sphaira/source/owo.cpp b/sphaira/source/owo.cpp index 7efaf9d..1a4c012 100644 --- a/sphaira/source/owo.cpp +++ b/sphaira/source/owo.cpp @@ -1005,7 +1005,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st } auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result { - App::Push(std::make_shared("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool { + App::Push(std::make_shared(0, "Installing Forwarder"_i18n, config.name, [config, storage_id](auto pbox) mutable -> bool { return R_SUCCEEDED(install_forwarder(pbox, config, storage_id)); })); R_SUCCEED(); diff --git a/sphaira/source/ui/list.cpp b/sphaira/source/ui/list.cpp index a619c4f..3bd480f 100644 --- a/sphaira/source/ui/list.cpp +++ b/sphaira/source/ui/list.cpp @@ -34,8 +34,35 @@ auto List::ClampY(float y, s64 count) const -> float { return y; } -void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) { - if (touch->is_clicked && touch->in_range(GetPos())) { +void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) { + const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2; + const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2; + + if (controller->GotDown(Button::DOWN)) { + if (ScrollDown(index, m_row, count)) { + callback(false, index); + } + } else if (controller->GotDown(Button::UP)) { + if (ScrollUp(index, m_row, count)) { + callback(false, index); + } + } else if (controller->GotDown(page_down_button)) { + if (ScrollDown(index, m_page, count)) { + callback(false, index); + } + } else if (controller->GotDown(page_up_button)) { + if (ScrollUp(index, m_page, count)) { + callback(false, index); + } + } else if (m_row > 1 && controller->GotDown(Button::RIGHT)) { + if (count && index < (count - 1) && (index + 1) % m_row != 0) { + callback(false, index + 1); + } + } else if (m_row > 1 && controller->GotDown(Button::LEFT)) { + if (count && index != 0 && (index % m_row) != 0) { + callback(false, index - 1); + } + } else if (touch->is_clicked && touch->in_range(GetPos())) { auto v = m_v; v.y -= ClampY(m_yoff + m_y_prog, count); @@ -63,7 +90,7 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCa vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y; if (touch->in_range(vv)) { - callback(i); + callback(true, i); return; } } diff --git a/sphaira/source/ui/menus/appstore.cpp b/sphaira/source/ui/menus/appstore.cpp index 952de59..52dda1e 100644 --- a/sphaira/source/ui/menus/appstore.cpp +++ b/sphaira/source/ui/menus/appstore.cpp @@ -236,18 +236,15 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f bool crop = false; if (iw < w || ih < h) { rounded_image = false; - gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded); + gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 15 : 0); } if (iw > w || ih > h) { crop = true; nvgSave(vg); nvgIntersectScissor(vg, x, y, w, h); } - if (rounded_image) { - gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image); - } else { - gfx::drawImage(vg, ix, iy, iw, ih, i.image); - } + + gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 15 : 0); if (crop) { nvgRestore(vg); } @@ -769,7 +766,7 @@ void EntryMenu::UpdateOptions() { }; const auto install = [this](){ - App::Push(std::make_shared("Installing "_i18n + m_entry.title, [this](auto pbox){ + App::Push(std::make_shared(m_entry.image.image, "Downloading "_i18n, m_entry.title, [this](auto pbox){ return InstallApp(pbox, m_entry); }, [this](bool success){ if (success) { @@ -782,7 +779,7 @@ void EntryMenu::UpdateOptions() { }; const auto uninstall = [this](){ - App::Push(std::make_shared("Uninstalling "_i18n + m_entry.title, [this](auto pbox){ + App::Push(std::make_shared(m_entry.image.image, "Uninstalling "_i18n, m_entry.title, [this](auto pbox){ return UninstallApp(pbox, m_entry); }, [this](bool success){ if (success) { @@ -854,48 +851,6 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"AppStore"_i18n} fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens"); this->SetActions( - std::make_pair(Button::RIGHT, Action{[this](){ - if (m_entries_current.empty()) { - return; - } - - if (m_index < (m_entries_current.size() - 1) && (m_index + 1) % 3 != 0) { - SetIndex(m_index + 1); - App::PlaySoundEffect(SoundEffect_Scroll); - log_write("moved right\n"); - } - }}), - std::make_pair(Button::LEFT, Action{[this](){ - if (m_entries_current.empty()) { - return; - } - - if (m_index != 0 && (m_index % 3) != 0) { - SetIndex(m_index - 1); - App::PlaySoundEffect(SoundEffect_Scroll); - log_write("moved left\n"); - } - }}), - std::make_pair(Button::DOWN, Action{[this](){ - if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::UP, Action{[this](){ - if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::R2, Action{[this](){ - if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::L2, Action{[this](){ - if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) { - SetIndex(m_index); - } - }}), std::make_pair(Button::A, Action{"Info"_i18n, [this](){ if (m_entries_current.empty()) { // log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size()); @@ -983,8 +938,8 @@ Menu::~Menu() { void Menu::Update(Controller* controller, TouchInfo* touch) { MenuBase::Update(controller, touch); - m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) { - if (m_index == i) { + m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) { + if (touch && m_index == i) { FireAction(Button::A); } else { App::PlaySoundEffect(SoundEffect_Focus); @@ -1096,16 +1051,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { float i_size = 22; switch (e.status) { case EntryStatus::Get: - gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image); + gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 15); break; case EntryStatus::Installed: - gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image); + gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 15); break; case EntryStatus::Local: - gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image); + gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 15); break; case EntryStatus::Update: - gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image); + gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 15); break; } }); diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index 5c9b1d2..9240052 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -279,26 +279,6 @@ Menu::Menu(const std::vector& nro_entries) : MenuBase{"FileBrowser"_i1 m_selected_count--; } }}), - std::make_pair(Button::DOWN, Action{[this](){ - if (m_list->ScrollDown(m_index, 1, m_entries_current.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::UP, Action{[this](){ - if (m_list->ScrollUp(m_index, 1, m_entries_current.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::DPAD_RIGHT, Action{[this](){ - if (m_list->ScrollDown(m_index, 8, m_entries_current.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::DPAD_LEFT, Action{[this](){ - if (m_list->ScrollUp(m_index, 8, m_entries_current.size())) { - SetIndex(m_index); - } - }}), std::make_pair(Button::A, Action{"Open"_i18n, [this](){ if (m_entries_current.empty()) { return; @@ -626,8 +606,8 @@ Menu::~Menu() { void Menu::Update(Controller* controller, TouchInfo* touch) { MenuBase::Update(controller, touch); - m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) { - if (m_index == i) { + m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) { + if (touch && m_index == i) { FireAction(Button::A); } else { App::PlaySoundEffect(SoundEffect_Focus); @@ -802,7 +782,7 @@ void Menu::InstallForwarder() { if (op_index) { const auto assoc = assoc_list[*op_index]; log_write("pushing it\n"); - App::Push(std::make_shared("Installing Forwarder"_i18n, [assoc, this](auto pbox) -> bool { + App::Push(std::make_shared(0, "Installing Forwarder"_i18n, GetEntry().name, [assoc, this](auto pbox) -> bool { log_write("inside callback\n"); NroEntry nro{}; @@ -829,6 +809,7 @@ void Menu::InstallForwarder() { // config.name = file_name; config.nacp = nro.nacp; config.icon = GetRomIcon(m_fs.get(), pbox, file_name, db_indexs, nro); + pbox->SetImageDataConst(config.icon); return R_SUCCEEDED(App::Install(pbox, config)); })); @@ -849,7 +830,7 @@ void Menu::InstallFiles(const std::vector& targets) { if (op_index && *op_index) { App::PopToMenu(); - App::Push(std::make_shared("Installing App"_i18n, [this, targets](auto pbox) mutable -> bool { + App::Push(std::make_shared(0, "Installing "_i18n, "", [this, targets](auto pbox) mutable -> bool { for (auto& e : targets) { const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e)); if (rc == yati::Result_Cancelled) { @@ -1226,7 +1207,7 @@ void Menu::OnDeleteCallback() { Scan(m_path); log_write("did delete\n"); } else { - App::Push(std::make_shared("Deleting"_i18n, [this](auto pbox){ + App::Push(std::make_shared(0, "Deleting"_i18n, "", [this](auto pbox){ FsDirCollections collections; // build list of dirs / files @@ -1319,7 +1300,7 @@ void Menu::OnPasteCallback() { Scan(m_path); log_write("did paste\n"); } else { - App::Push(std::make_shared("Pasting"_i18n, [this](auto pbox){ + App::Push(std::make_shared(0, "Pasting"_i18n, "", [this](auto pbox){ if (m_selected_type == SelectedType::Cut) { for (const auto& p : m_selected_files) { diff --git a/sphaira/source/ui/menus/ftp_menu.cpp b/sphaira/source/ui/menus/ftp_menu.cpp index e96df89..53745fe 100644 --- a/sphaira/source/ui/menus/ftp_menu.cpp +++ b/sphaira/source/ui/menus/ftp_menu.cpp @@ -146,6 +146,10 @@ Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} { SetPop(); }}); + SetAction(Button::X, Action{"Options"_i18n, [this](){ + App::DisplayInstallOptions(false); + }}); + mutexInit(&m_mutex); ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose); @@ -183,7 +187,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { log_write("set to progress\n"); m_state = State::Progress; log_write("got connection\n"); - App::Push(std::make_shared("Installing App"_i18n, [this](auto pbox) mutable -> bool { + App::Push(std::make_shared(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool { log_write("inside progress box\n"); const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path); if (R_FAILED(rc)) { diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index bd6afb6..53dc2ea 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -1,5 +1,6 @@ #include "ui/menus/gc_menu.hpp" #include "yati/yati.hpp" +#include "yati/nx/nca.hpp" #include "app.hpp" #include "defines.hpp" #include "log.hpp" @@ -10,26 +11,103 @@ namespace sphaira::ui::menu::gc { namespace { +const char *g_option_list[] = { + "Nand Install", + "SD Card Install", + "Exit", +}; + +struct HashStr { + char str[0x21]; +}; + +HashStr hexIdToStr(auto id) { + HashStr str{}; + const auto id_lower = std::byteswap(*(u64*)id.c); + const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8)); + std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper); + return str; +} + +// @Gc is the mount point, S is for secure partion, the remaining is the +// the gamecard handle value in lower-case hex. +auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPartition partiton = FsGameCardPartition_Secure) -> fs::FsPath { + static const char mount_parition[] = { + [FsGameCardPartition_Update] = 'U', + [FsGameCardPartition_Normal] = 'N', + [FsGameCardPartition_Secure] = 'S', + [FsGameCardPartition_Logo] = 'L', + }; + + fs::FsPath path; + std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_parition[partiton], handle->value, name); + return path; +} + auto InRange(u64 off, u64 offset, u64 size) -> bool { return off < offset + size && off >= offset; } struct GcSource final : yati::source::Base { - GcSource(const yati::container::Collections& collections, fs::FsNativeGameCard* fs); + GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install); ~GcSource(); Result Read(void* buf, s64 off, s64 size, u64* bytes_read); - const yati::container::Collections& m_collections; + yati::container::Collections m_collections{}; + yati::ConfigOverride m_config{}; fs::FsNativeGameCard* m_fs{}; FsFile m_file{}; s64 m_offset{}; s64 m_size{}; }; -GcSource::GcSource(const yati::container::Collections& collections, fs::FsNativeGameCard* fs) -: m_collections{collections} -, m_fs{fs} { +GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install) +: m_fs{fs} { m_offset = -1; + + s64 offset{}; + const auto add_collections = [&](const auto& collections) { + for (auto collection : collections) { + collection.offset = offset; + m_collections.emplace_back(collection); + offset += collection.size; + } + }; + + const auto add_entries = [&](const auto& entries) { + for (auto& e : entries) { + add_collections(e); + } + }; + + // yati can handle all of this for use, however, yati lacks information + // for ncas until it installs the cnmt and parses it. + // as we already have this info, we can only send yati what we want to install. + if (App::GetApp()->m_ticket_only.Get()) { + add_collections(entry.tickets); + } else { + if (!App::GetApp()->m_skip_base.Get()) { + add_entries(entry.application); + } + if (!App::GetApp()->m_skip_patch.Get()) { + add_entries(entry.patch); + } + if (!App::GetApp()->m_skip_addon.Get()) { + add_entries(entry.add_on); + } + if (!App::GetApp()->m_skip_data_patch.Get()) { + add_entries(entry.data_patch); + } + if (!App::GetApp()->m_skip_ticket.Get()) { + add_collections(entry.tickets); + } + } + + // we don't need to verify the nca's, this speeds up installs. + m_config.sd_card_install = sd_install; + m_config.skip_nca_hash_verify = true; + m_config.skip_rsa_header_fixed_key_verify = true; + m_config.skip_rsa_npdm_fixed_key_verify = true; } GcSource::~GcSource() { @@ -63,86 +141,138 @@ Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) { } // namespace -Menu::Menu() : MenuBase{"GameCard"_i18n} { - SetAction(Button::B, Action{"Back"_i18n, [this](){ - SetPop(); - }}); +auto ApplicationEntry::GetSize(const std::vector& entries) const -> s64 { + s64 size{}; + for (auto& e : entries) { + for (auto& collection : e) { + size += collection.size; + } + } + return size; +} - SetAction(Button::X, Action{"Refresh"_i18n, [this](){ - m_state = State::None; - }}); +auto ApplicationEntry::GetSize() const -> s64 { + s64 size{}; + size += GetSize(application); + size += GetSize(patch); + size += GetSize(add_on); + size += GetSize(data_patch); + return size; +} + +Menu::Menu() : MenuBase{"GameCard"_i18n} { + this->SetActions( + std::make_pair(Button::B, Action{"Back"_i18n, [this](){ + SetPop(); + }}), + std::make_pair(Button::X, Action{"Options"_i18n, [this](){ + App::DisplayInstallOptions(false); + }}) + ); + + const Vec4 v{485, 275, 720, 70}; + const Vec2 pad{0, 125 - v.h}; + m_list = std::make_unique(1, 3, m_pos, v, pad); fsOpenDeviceOperator(std::addressof(m_dev_op)); + UpdateStorageSize(); } Menu::~Menu() { - // manually close this as it needs(?) to be closed before dev_op. - m_fs.reset(); + GcUnmount(); fsDeviceOperatorClose(std::addressof(m_dev_op)); } void Menu::Update(Controller* controller, TouchInfo* touch) { - MenuBase::Update(controller, touch); - - switch (m_state) { - case State::None: { - bool gc_inserted; - if (R_FAILED(fsDeviceOperatorIsGameCardInserted(std::addressof(m_dev_op), std::addressof(gc_inserted)))) { - m_state = State::Failed; - } else { - if (!gc_inserted) { - m_state = State::NotFound; - } else { - if (R_FAILED(ScanGamecard())) { - m_state = State::Failed; - } - } - } - } break; - - case State::Progress: - case State::Done: - case State::NotFound: - case State::Failed: - break; + // poll for the gamecard first before handling inputs as the gamecard + // may have been removed, thus pressing A would fail. + bool inserted{}; + GcPoll(&inserted); + if (m_mounted != inserted) { + log_write("gc state changed\n"); + m_mounted = inserted; + if (m_mounted) { + log_write("trying to mount\n"); + m_mounted = R_SUCCEEDED(GcMount()); + } else { + log_write("trying to unmount\n"); + GcUnmount(); + } } + + MenuBase::Update(controller, touch); + m_list->OnUpdate(controller, touch, m_option_index, std::size(g_option_list), [this](bool touch, auto i) { + if (touch && m_option_index == i) { + FireAction(Button::A); + } else { + App::PlaySoundEffect(SoundEffect_Focus); + m_option_index = i; + } + }); } void Menu::Draw(NVGcontext* vg, Theme* theme) { MenuBase::Draw(vg, theme); - switch (m_state) { - case State::None: - gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str()); - break; + #define STORAGE_BAR_W 325 + #define STORAGE_BAR_H 14 - case State::Progress: - gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str()); - break; + const auto size_sd_gb = (double)m_size_free_sd / 0x40000000; + const auto size_nand_gb = (double)m_size_free_nand / 0x40000000; - case State::NotFound: - gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "No GameCard inserted, press X to refresh"_i18n.c_str()); - break; + gfx::drawTextArgs(vg, 490, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System memory %.1f GB", size_nand_gb); + gfx::drawRect(vg, 480, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT)); + gfx::drawRect(vg, 480 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND)); + gfx::drawRect(vg, 480 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_nand / (double)m_size_total_nand) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT)); - case State::Done: - gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Installed GameCard, press B to exit..."_i18n.c_str()); - break; + gfx::drawTextArgs(vg, 870, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD card %.1f GB", size_sd_gb); + gfx::drawRect(vg, 860, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT)); + gfx::drawRect(vg, 860 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND)); + gfx::drawRect(vg, 860 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_sd / (double)m_size_total_sd) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT)); - case State::Failed: - gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to scan GameCard..."_i18n.c_str()); - break; + gfx::drawRect(vg, 30, 90, 375, 555, theme->GetColour(ThemeEntryID_GRID)); + + if (!m_entries.empty()) { + const auto& e = m_entries[m_entry_index]; + const auto size = e.GetSize(); + gfx::drawImage(vg, 90, 130, 256, 256, m_icon ? m_icon : App::GetDefaultImage()); + + nvgSave(vg); + nvgIntersectScissor(vg, 50, 90, 325, 555); + gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.name); + gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.author); + gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id); + gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen)); + gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000); + gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu", e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size()); + nvgRestore(vg); } + + m_list->Draw(vg, theme, std::size(g_option_list), [this](auto* vg, auto* theme, auto v, auto i) { + const auto& [x, y, w, h] = v; + const auto text_y = y + (h / 2.f); + auto colour = ThemeEntryID_TEXT; + if (i == m_option_index) { + gfx::drawRectOutline(vg, theme, 4.f, v); + // g_background.selected_bar = create_shape(Colour_Nintendo_Cyan, 90, 230, 4, 45, true); + // draw_shape_position(&g_background.selected_bar, 485, g_options[i].text->rect.y - 10); + gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED)); + colour = ThemeEntryID_TEXT_SELECTED; + } + if (i != 2 && !m_mounted) { + colour = ThemeEntryID_TEXT_INFO; + } + + gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", g_option_list[i]); + }); } -Result Menu::ScanGamecard() { - m_state = State::None; - m_fs.reset(); - m_collections.clear(); +Result Menu::GcMount() { + GcUnmount(); - FsGameCardHandle gc_handle; - R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(gc_handle))); + R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle))); - m_fs = std::make_unique(std::addressof(gc_handle), FsGameCardPartition_Secure, false); + m_fs = std::make_unique(std::addressof(m_handle), FsGameCardPartition_Secure, false); R_TRY(m_fs->GetFsOpenResult()); FsDir dir; @@ -155,33 +285,215 @@ Result Menu::ScanGamecard() { std::vector buf(count); s64 total_entries; R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data())); - m_collections.reserve(total_entries); + R_UNLESS(buf.size() == total_entries, 0x1); - s64 offset{}; - for (s64 i = 0; i < total_entries; i++) { - yati::container::CollectionEntry entry{}; - entry.name = buf[i].name; - entry.offset = offset; - entry.size = buf[i].file_size; - m_collections.emplace_back(entry); - offset += buf[i].file_size; + yati::container::Collections ticket_collections; + for (const auto& e : buf) { + if (!std::string_view(e.name).ends_with(".tik") && !std::string_view(e.name).ends_with(".cert")) { + continue; + } + + ticket_collections.emplace_back(e.name, 0, e.file_size); } - m_state = State::Progress; - App::Push(std::make_shared("Installing App"_i18n, [this](auto pbox) mutable -> bool { - auto source = std::make_shared(m_collections, m_fs.get()); - return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, m_collections)); - }, [this](bool result){ - if (result) { - App::Notify("Gc install success!"_i18n); - m_state = State::Done; - } else { - App::Notify("Gc install failed!"_i18n); - m_state = State::Failed; + for (const auto& e : buf) { + // we could use ncm to handle finding all the ncas for us + // however, we can parse faster than ncm. + // not only that, the first few calls trying to mount ncm db for + // the gamecard will fail as it has not yet been parsed (or it's locked?). + // we could, of course, just wait until ncm is ready, which is about + // 32ms, but i already have code for manually parsing cnmt so lets re-use it. + if (!std::string_view(e.name).ends_with(".cnmt.nca")) { + continue; } - })); + + // we don't yet use the header or extended header. + ncm::PackagedContentMeta header; + std::vector extended_header; + std::vector infos; + const auto path = BuildGcPath(e.name, &m_handle); + R_TRY(yati::ParseCnmtNca(path, header, extended_header, infos)); + + u8 key_gen; + FsRightsId rights_id; + R_TRY(fsGetRightsIdAndKeyGenerationByPath(path, FsContentAttributes_All, &key_gen, &rights_id)); + + // always add tickets, yati will ignore them if not needed. + GcCollections collections; + // add cnmt file. + collections.emplace_back(e.name, e.file_size, NcmContentType_Meta); + + for (const auto& info : infos) { + // these don't exist for gamecards, however i may copy/paste this code + // somewhere so i'm future proofing against myself. + if (info.info.content_type == NcmContentType_DeltaFragment) { + continue; + } + + // find the nca file, this will never fail for gamecards, see above comment. + const auto str = hexIdToStr(info.info.content_id); + const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){ + return !std::strncmp(str.str, e.name, std::strlen(str.str)); + }); + + R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound); + collections.emplace_back(it->name, it->file_size, info.info.content_type); + } + + const auto app_id = ncm::GetAppId(header); + ApplicationEntry* app_entry{}; + for (auto& app : m_entries) { + if (app.app_id == app_id) { + app_entry = &app; + break; + } + } + + if (!app_entry) { + app_entry = &m_entries.emplace_back(app_id, header.title_version); + } + + app_entry->version = std::max(app_entry->version, header.title_version); + app_entry->key_gen = std::max(app_entry->key_gen, key_gen); + + if (header.meta_type == NcmContentMetaType_Application) { + app_entry->application.emplace_back(collections); + } else if (header.meta_type == NcmContentMetaType_Patch) { + app_entry->patch.emplace_back(collections); + } else if (header.meta_type == NcmContentMetaType_AddOnContent) { + app_entry->add_on.emplace_back(collections); + } else if (header.meta_type == NcmContentMetaType_DataPatch) { + app_entry->data_patch.emplace_back(collections); + } + } + + R_UNLESS(m_entries.size(), 0x1); + + // append tickets to every application, yati will ignore if undeeded. + for (auto& e : m_entries) { + e.tickets = ticket_collections; + } + + SetAction(Button::A, Action{"OK"_i18n, [this](){ + if (m_option_index == 2) { + SetPop(); + } else { + if (m_mounted) { + App::Push(std::make_shared(m_icon, "Installing "_i18n, m_lang_entry.name, [this](auto pbox) mutable -> bool { + auto source = std::make_shared(m_entries[m_entry_index], m_fs.get(), m_option_index == 1); + return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config)); + }, [this](bool result){ + if (result) { + App::Notify("Gc install success!"_i18n); + } else { + App::Notify("Gc install failed!"_i18n); + } + + UpdateStorageSize(); + })); + } + } + }}); + + if (m_entries.size() > 1) { + SetAction(Button::L, Action{"Prev"_i18n, [this](){ + if (m_entry_index != 0) { + OnChangeIndex(m_entry_index - 1); + } + }}); + SetAction(Button::R, Action{"Next"_i18n, [this](){ + if (m_entry_index < m_entries.size()) { + OnChangeIndex(m_entry_index + 1); + } + }}); + } + + OnChangeIndex(0); + R_SUCCEED(); +} + +void Menu::GcUnmount() { + m_fs.reset(); + m_entries.clear(); + m_entry_index = 0; + m_mounted = false; + m_lang_entry = {}; + FreeImage(); + + RemoveAction(Button::L); + RemoveAction(Button::R); +} + +Result Menu::GcPoll(bool* inserted) { + R_TRY(fsDeviceOperatorIsGameCardInserted(&m_dev_op, inserted)); + + // if the handle changed, re-mount the game card. + if (*inserted && m_mounted) { + FsGameCardHandle handle; + R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(handle))); + if (handle.value != m_handle.value) { + R_TRY(GcMount()); + } + } R_SUCCEED(); } +Result Menu::UpdateStorageSize() { + fs::FsNativeContentStorage fs_nand{FsContentStorageId_User}; + fs::FsNativeContentStorage fs_sd{FsContentStorageId_SdCard}; + + R_TRY(fs_sd.GetFreeSpace("/", &m_size_free_sd)); + R_TRY(fs_sd.GetTotalSpace("/", &m_size_total_sd)); + R_TRY(fs_nand.GetFreeSpace("/", &m_size_free_nand)); + R_TRY(fs_nand.GetTotalSpace("/", &m_size_total_nand)); + R_SUCCEED(); +} + +void Menu::FreeImage() { + if (m_icon) { + nvgDeleteImage(App::GetVg(), m_icon); + m_icon = 0; + } +} + +void Menu::OnChangeIndex(s64 new_index) { + FreeImage(); + m_entry_index = new_index; + + const auto index = m_entries.empty() ? 0 : m_entry_index + 1; + this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size())); + + // nsGetApplicationControlData() will fail if it's the first time + // mounting a gamecard if the image is not already cached. + // waiting 1-2s after mount, then calling seems to work. + // however, we can just manually parse the nca to get the data we need, + // which always works and *is* faster too ;) + for (auto& e : m_entries[m_entry_index].application) { + for (auto& collection : e) { + if (collection.type == NcmContentType_Control) { + NacpStruct nacp; + std::vector icon; + const auto path = BuildGcPath(collection.name.c_str(), &m_handle); + if (R_SUCCEEDED(yati::ParseControlNca(path, m_entries[m_entry_index].app_id, &nacp, sizeof(nacp), &icon))) { + log_write("managed to parse control nca %s\n", path.s); + NacpLanguageEntry* lang_entry{}; + nacpGetLanguageEntry(&nacp, &lang_entry); + + if (lang_entry) { + m_lang_entry = *lang_entry; + } + + m_icon = nvgCreateImageMem(App::GetVg(), 0, icon.data(), icon.size()); + if (m_icon > 0) { + return; + } + } else { + log_write("\tFAILED to parse control nca %s\n", path.s); + } + } + } + } +} + } // namespace sphaira::ui::menu::gc diff --git a/sphaira/source/ui/menus/ghdl.cpp b/sphaira/source/ui/menus/ghdl.cpp index 86a4d2f..983b766 100644 --- a/sphaira/source/ui/menus/ghdl.cpp +++ b/sphaira/source/ui/menus/ghdl.cpp @@ -247,27 +247,6 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} { fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH); this->SetActions( - std::make_pair(Button::DOWN, Action{[this](){ - if (m_list->ScrollDown(m_index, 1, m_entries.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::UP, Action{[this](){ - if (m_list->ScrollUp(m_index, 1, m_entries.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::DPAD_RIGHT, Action{[this](){ - if (m_list->ScrollDown(m_index, 8, m_entries.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::DPAD_LEFT, Action{[this](){ - if (m_list->ScrollUp(m_index, 8, m_entries.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::A, Action{"Download"_i18n, [this](){ if (m_entries.empty()) { return; @@ -277,7 +256,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} { static GhApiEntry gh_entry; gh_entry = {}; - App::Push(std::make_shared("Downloading "_i18n + GetEntry().repo, [this](auto pbox){ + App::Push(std::make_shared(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox){ return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry); }, [this](bool success){ if (success) { @@ -325,7 +304,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} { } const auto func = [this, &asset_entry, ptr](){ - App::Push(std::make_shared("Downloading "_i18n + GetEntry().repo, [this, &asset_entry, ptr](auto pbox){ + App::Push(std::make_shared(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox){ return DownloadApp(pbox, asset_entry, ptr); }, [this, ptr](bool success){ if (success) { @@ -373,8 +352,8 @@ Menu::~Menu() { void Menu::Update(Controller* controller, TouchInfo* touch) { MenuBase::Update(controller, touch); - m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) { - if (m_index == i) { + m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) { + if (touch && m_index == i) { FireAction(Button::A); } else { App::PlaySoundEffect(SoundEffect_Focus); diff --git a/sphaira/source/ui/menus/homebrew.cpp b/sphaira/source/ui/menus/homebrew.cpp index db00280..d0e71af 100644 --- a/sphaira/source/ui/menus/homebrew.cpp +++ b/sphaira/source/ui/menus/homebrew.cpp @@ -28,40 +28,6 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath { Menu::Menu() : MenuBase{"Homebrew"_i18n} { this->SetActions( - std::make_pair(Button::RIGHT, Action{[this](){ - if (m_index < (m_entries.size() - 1) && (m_index + 1) % 3 != 0) { - SetIndex(m_index + 1); - App::PlaySoundEffect(SoundEffect_Scroll); - log_write("moved right\n"); - } - }}), - std::make_pair(Button::LEFT, Action{[this](){ - if (m_index != 0 && (m_index % 3) != 0) { - SetIndex(m_index - 1); - App::PlaySoundEffect(SoundEffect_Scroll); - log_write("moved left\n"); - } - }}), - std::make_pair(Button::DOWN, Action{[this](){ - if (m_list->ScrollDown(m_index, 3, m_entries.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::UP, Action{[this](){ - if (m_list->ScrollUp(m_index, 3, m_entries.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::R2, Action{[this](){ - if (m_list->ScrollDown(m_index, 9, m_entries.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::L2, Action{[this](){ - if (m_list->ScrollUp(m_index, 9, m_entries.size())) { - SetIndex(m_index); - } - }}), std::make_pair(Button::A, Action{"Launch"_i18n, [this](){ nro_launch(m_entries[m_index].path); }}), @@ -157,8 +123,8 @@ Menu::~Menu() { void Menu::Update(Controller* controller, TouchInfo* touch) { MenuBase::Update(controller, touch); - m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) { - if (m_index == i) { + m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) { + if (touch && m_index == i) { FireAction(Button::A); } else { App::PlaySoundEffect(SoundEffect_Focus); @@ -202,7 +168,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } const float image_size = 115; - gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage()); + gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 15); nvgSave(vg); nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index 20cebc1..f5a4afe 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -1,10 +1,4 @@ #include "ui/menus/main_menu.hpp" -#include "ui/menus/irs_menu.hpp" -#include "ui/menus/themezer.hpp" -#include "ui/menus/ghdl.hpp" -#include "ui/menus/usb_menu.hpp" -#include "ui/menus/ftp_menu.hpp" -#include "ui/menus/gc_menu.hpp" #include "ui/sidebar.hpp" #include "ui/popup_list.hpp" @@ -16,7 +10,6 @@ #include "log.hpp" #include "download.hpp" #include "defines.hpp" -#include "web.hpp" #include "i18n.hpp" #include @@ -235,48 +228,29 @@ MainMenu::MainMenu() { language_items.push_back("Swedish"_i18n); language_items.push_back("Vietnamese"_i18n); - options->Add(std::make_shared("Theme"_i18n, [this](){ - SidebarEntryArray::Items theme_items{}; - const auto theme_meta = App::GetThemeMetaList(); - for (auto& p : theme_meta) { - theme_items.emplace_back(p.name); - } - - auto options = std::make_shared("Theme Options"_i18n, Sidebar::Side::LEFT); - ON_SCOPE_EXIT(App::Push(options)); - - options->Add(std::make_shared("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){ - App::SetTheme(index_out); - }, App::GetThemeIndex())); - - options->Add(std::make_shared("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){ - App::SetThemeMusicEnable(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [this](bool& enable){ - App::Set12HourTimeEnable(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); + options->Add(std::make_shared("Theme"_i18n, [](){ + App::DisplayThemeOptions(); })); options->Add(std::make_shared("Network"_i18n, [this](){ auto options = std::make_shared("Network Options"_i18n, Sidebar::Side::LEFT); ON_SCOPE_EXIT(App::Push(options)); - options->Add(std::make_shared("Ftp"_i18n, App::GetFtpEnable(), [this](bool& enable){ + options->Add(std::make_shared("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){ App::SetFtpEnable(enable); }, "Enabled"_i18n, "Disabled"_i18n)); - options->Add(std::make_shared("Mtp"_i18n, App::GetMtpEnable(), [this](bool& enable){ + options->Add(std::make_shared("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){ App::SetMtpEnable(enable); }, "Enabled"_i18n, "Disabled"_i18n)); - options->Add(std::make_shared("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){ + options->Add(std::make_shared("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){ App::SetNxlinkEnable(enable); }, "Enabled"_i18n, "Disabled"_i18n)); if (m_update_state == UpdateState::Update) { options->Add(std::make_shared("Download update: "_i18n + m_update_version, [this](){ - App::Push(std::make_shared("Downloading "_i18n + m_update_version, [this](auto pbox){ + App::Push(std::make_shared(0, "Downloading "_i18n, "Sphaira v" + m_update_version, [this](auto pbox){ return InstallUpdate(pbox, m_update_url, m_update_version); }, [this](bool success){ if (success) { @@ -295,150 +269,16 @@ MainMenu::MainMenu() { } })); - options->Add(std::make_shared("Language"_i18n, language_items, [this](s64& index_out){ + options->Add(std::make_shared("Language"_i18n, language_items, [](s64& index_out){ App::SetLanguage(index_out); }, (s64)App::GetLanguage())); - options->Add(std::make_shared("Misc"_i18n, [this](){ - auto options = std::make_shared("Misc Options"_i18n, Sidebar::Side::LEFT); - ON_SCOPE_EXIT(App::Push(options)); - - options->Add(std::make_shared("Themezer"_i18n, [](){ - App::Push(std::make_shared()); - })); - - options->Add(std::make_shared("GitHub"_i18n, [](){ - App::Push(std::make_shared()); - })); - - options->Add(std::make_shared("Irs"_i18n, [](){ - App::Push(std::make_shared()); - })); - - if (App::IsApplication()) { - options->Add(std::make_shared("Web"_i18n, [](){ - WebShow("https://lite.duckduckgo.com/lite"); - })); - } - - if (App::GetApp()->m_install.Get()) { - if (App::GetFtpEnable()) { - options->Add(std::make_shared("Ftp Install"_i18n, [](){ - App::Push(std::make_shared()); - })); - } - - options->Add(std::make_shared("Usb Install"_i18n, [](){ - App::Push(std::make_shared()); - })); - - options->Add(std::make_shared("GameCard Install"_i18n, [](){ - App::Push(std::make_shared()); - })); - } + options->Add(std::make_shared("Misc"_i18n, [](){ + App::DisplayMiscOptions(); })); - options->Add(std::make_shared("Advanced"_i18n, [this](){ - auto options = std::make_shared("Advanced Options"_i18n, Sidebar::Side::LEFT); - ON_SCOPE_EXIT(App::Push(options)); - - SidebarEntryArray::Items text_scroll_speed_items; - text_scroll_speed_items.push_back("Slow"_i18n); - text_scroll_speed_items.push_back("Normal"_i18n); - text_scroll_speed_items.push_back("Fast"_i18n); - - options->Add(std::make_shared("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){ - App::SetLogEnable(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [this](bool& enable){ - App::SetReplaceHbmenuEnable(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){ - App::SetTextScrollSpeed(index_out); - }, (s64)App::GetTextScrollSpeed())); - - options->Add(std::make_shared("Install options"_i18n, [this](){ - auto options = std::make_shared("Install Options"_i18n, Sidebar::Side::LEFT); - ON_SCOPE_EXIT(App::Push(options)); - - SidebarEntryArray::Items install_items; - install_items.push_back("System memory"_i18n); - install_items.push_back("microSD card"_i18n); - - options->Add(std::make_shared("Enable"_i18n, App::GetInstallEnable(), [this](bool& enable){ - App::SetInstallEnable(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){ - App::SetInstallPrompt(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Install location"_i18n, install_items, [this](s64& index_out){ - App::SetInstallSdEnable(index_out); - }, (s64)App::GetInstallSdEnable())); - - options->Add(std::make_shared("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [this](bool& enable){ - App::GetApp()->m_allow_downgrade.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [this](bool& enable){ - App::GetApp()->m_skip_if_already_installed.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [this](bool& enable){ - App::GetApp()->m_ticket_only.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){ - App::GetApp()->m_skip_base.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [this](bool& enable){ - App::GetApp()->m_skip_patch.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [this](bool& enable){ - App::GetApp()->m_skip_addon.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [this](bool& enable){ - App::GetApp()->m_skip_data_patch.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [this](bool& enable){ - App::GetApp()->m_skip_ticket.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [this](bool& enable){ - App::GetApp()->m_skip_nca_hash_verify.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [this](bool& enable){ - App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [this](bool& enable){ - App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [this](bool& enable){ - App::GetApp()->m_ignore_distribution_bit.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [this](bool& enable){ - App::GetApp()->m_convert_to_standard_crypto.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [this](bool& enable){ - App::GetApp()->m_lower_master_key.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - - options->Add(std::make_shared("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [this](bool& enable){ - App::GetApp()->m_lower_system_version.Set(enable); - }, "Enabled"_i18n, "Disabled"_i18n)); - })); + options->Add(std::make_shared("Advanced"_i18n, [](){ + App::DisplayAdvancedOptions(); })); }}) ); diff --git a/sphaira/source/ui/menus/themezer.cpp b/sphaira/source/ui/menus/themezer.cpp index 7973c1c..070e30b 100644 --- a/sphaira/source/ui/menus/themezer.cpp +++ b/sphaira/source/ui/menus/themezer.cpp @@ -375,45 +375,6 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} { }}); this->SetActions( - std::make_pair(Button::RIGHT, Action{[this](){ - const auto& page = m_pages[m_page_index]; - if (m_index < (page.m_packList.size() - 1) && (m_index + 1) % 3 != 0) { - SetIndex(m_index + 1); - App::PlaySoundEffect(SoundEffect_Scroll); - log_write("moved right\n"); - } - }}), - std::make_pair(Button::LEFT, Action{[this](){ - if (m_index != 0 && (m_index % 3) != 0) { - SetIndex(m_index - 1); - App::PlaySoundEffect(SoundEffect_Scroll); - log_write("moved left\n"); - } - }}), - std::make_pair(Button::DOWN, Action{[this](){ - const auto& page = m_pages[m_page_index]; - if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::UP, Action{[this](){ - const auto& page = m_pages[m_page_index]; - if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::R2, Action{[this](){ - const auto& page = m_pages[m_page_index]; - if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::L2, Action{[this](){ - const auto& page = m_pages[m_page_index]; - if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) { - SetIndex(m_index); - } - }}), std::make_pair(Button::A, Action{"Download"_i18n, [this](){ App::Push(std::make_shared( "Download theme?"_i18n, @@ -424,7 +385,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} { const auto& entry = page.m_packList[m_index]; const auto url = apiBuildUrlDownloadPack(entry); - App::Push(std::make_shared("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){ + App::Push(std::make_shared(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox){ return InstallTheme(pbox, entry); }, [this, &entry](bool success){ if (success) { @@ -532,8 +493,8 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { return; } - m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) { - if (m_index == i) { + m_list->OnUpdate(controller, touch, m_index, page.m_packList.size(), [this](bool touch, auto i) { + if (touch && m_index == i) { FireAction(Button::A); } else { App::PlaySoundEffect(SoundEffect_Focus); @@ -642,7 +603,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } } - gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage()); + gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 15); } nvgSave(vg); diff --git a/sphaira/source/ui/menus/usb_menu.cpp b/sphaira/source/ui/menus/usb_menu.cpp index de6a20d..f84bf59 100644 --- a/sphaira/source/ui/menus/usb_menu.cpp +++ b/sphaira/source/ui/menus/usb_menu.cpp @@ -43,6 +43,10 @@ Menu::Menu() : MenuBase{"USB"_i18n} { SetPop(); }}); + SetAction(Button::X, Action{"Options"_i18n, [this](){ + App::DisplayInstallOptions(false); + }}); + // if mtp is enabled, disable it for now. m_was_mtp_enabled = App::GetMtpEnable(); if (m_was_mtp_enabled) { @@ -99,7 +103,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { log_write("set to progress\n"); m_state = State::Progress; log_write("got connection\n"); - App::Push(std::make_shared("Installing App"_i18n, [this](auto pbox) mutable -> bool { + App::Push(std::make_shared(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool { log_write("inside progress box\n"); for (u32 i = 0; i < m_usb_count; i++) { std::string file_name; diff --git a/sphaira/source/ui/nvg_util.cpp b/sphaira/source/ui/nvg_util.cpp index 9cadf9e..03a0169 100644 --- a/sphaira/source/ui/nvg_util.cpp +++ b/sphaira/source/ui/nvg_util.cpp @@ -34,24 +34,16 @@ constexpr std::array buttons = { }; // NEW --------------------- -void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) { +void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) { nvgBeginPath(vg); - if (rounded) { - nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15); - } else { - nvgRect(vg, v.x, v.y, v.w, v.h); - } + nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded); nvgFillColor(vg, c); nvgFill(vg); } -void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) { +void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) { nvgBeginPath(vg); - if (rounded) { - nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15); - } else { - nvgRect(vg, v.x, v.y, v.w, v.h); - } + nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded); nvgFillPaint(vg, p); nvgFill(vg); } @@ -164,25 +156,13 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const drawText(vg, x, y, size, buffer, nullptr, align, c); } -void drawImage(NVGcontext* vg, const Vec4& v, int texture) { +void drawImage(NVGcontext* vg, const Vec4& v, int texture, float rounded) { const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f); - drawRect(vg, v, paint, false); + drawRect(vg, v, paint, rounded); } -void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture) { - drawImage(vg, Vec4(x, y, w, h), texture); -} - -void drawImageRounded(NVGcontext* vg, const Vec4& v, int texture) { - const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f); - nvgBeginPath(vg); - nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15); - nvgFillPaint(vg, paint); - nvgFill(vg); -} - -void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int texture) { - drawImageRounded(vg, Vec4(x, y, w, h), texture); +void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture, float rounded) { + drawImage(vg, Vec4(x, y, w, h), texture, rounded); } void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) { @@ -208,19 +188,19 @@ void dimBackground(NVGcontext* vg) { drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false); } -void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) { +void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, float rounded) { drawRectIntenal(vg, {x,y,w,h}, c, rounded); } -void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) { +void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) { drawRectIntenal(vg, v, c, rounded); } -void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) { +void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, float rounded) { drawRectIntenal(vg, {x,y,w,h}, p, rounded); } -void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) { +void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) { drawRectIntenal(vg, v, p, rounded); } diff --git a/sphaira/source/ui/popup_list.cpp b/sphaira/source/ui/popup_list.cpp index 48bf462..69b2950 100644 --- a/sphaira/source/ui/popup_list.cpp +++ b/sphaira/source/ui/popup_list.cpp @@ -62,16 +62,6 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index) , m_callback{cb} , m_index{index} { this->SetActions( - std::make_pair(Button::DOWN, Action{[this](){ - if (m_list->ScrollDown(m_index, 1, m_items.size())) { - SetIndex(m_index); - } - }}), - std::make_pair(Button::UP, Action{[this](){ - if (m_list->ScrollUp(m_index, 1, m_items.size())) { - SetIndex(m_index); - } - }}), std::make_pair(Button::A, Action{"Select"_i18n, [this](){ if (m_callback) { m_callback(m_index); @@ -103,9 +93,11 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index) auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void { Widget::Update(controller, touch); - m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) { + m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) { SetIndex(i); - FireAction(Button::A); + if (touch) { + FireAction(Button::A); + } }); } diff --git a/sphaira/source/ui/progress_box.cpp b/sphaira/source/ui/progress_box.cpp index 8d44709..5a886c5 100644 --- a/sphaira/source/ui/progress_box.cpp +++ b/sphaira/source/ui/progress_box.cpp @@ -5,6 +5,7 @@ #include "defines.hpp" #include "log.hpp" #include "i18n.hpp" +#include namespace sphaira::ui { namespace { @@ -17,7 +18,7 @@ void threadFunc(void* arg) { } // namespace -ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) { +ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) { SetAction(Button::B, Action{"Back"_i18n, [this](){ App::Push(std::make_shared("Are you sure you wish to cancel?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){ if (op_index && *op_index) { @@ -27,11 +28,6 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, })); }}); - m_pos.w = 770.f; - m_pos.h = 430.f; - m_pos.x = 255; - m_pos.y = 145; - m_pos.w = 770.f; m_pos.h = 295.f; m_pos.x = (SCREEN_WIDTH / 2.f) - (m_pos.w / 2.f); @@ -39,6 +35,8 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, m_done = done; m_title = title; + m_action = action; + m_image = image; m_thread_data.pbox = this; m_thread_data.callback = callback; @@ -60,6 +58,7 @@ ProgressBox::~ProgressBox() { log_write("failed to close thread\n"); } + FreeImage(); m_done(m_thread_data.result); } @@ -73,12 +72,28 @@ auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void { auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void { mutexLock(&m_mutex); + std::vector image_data{}; + std::swap(m_image_data, image_data); + if (m_timestamp.GetSeconds()) { + m_timestamp.Update(); + m_speed = m_offset - m_last_offset; + m_last_offset = m_offset; + } + const auto title = m_title; const auto transfer = m_transfer; const auto size = m_size; const auto offset = m_offset; + const auto speed = m_speed; + const auto last_offset = m_last_offset; mutexUnlock(&m_mutex); + if (!image_data.empty()) { + FreeImage(); + m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size()); + m_own_image = true; + } + gfx::dimBackground(vg); gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP)); @@ -86,20 +101,62 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void { // const Vec4 box = { 255, 145, 770, 430 }; const auto center_x = m_pos.x + m_pos.w/2; const auto end_y = m_pos.y + m_pos.h; - const Vec4 prog_bar = { 400, end_y - 80, 480, 12 }; + const auto progress_bar_w = m_pos.w - 230; + const Vec4 prog_bar = { center_x - progress_bar_w / 2, end_y - 100, progress_bar_w, 12 }; + + nvgSave(vg); + nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH()); + + if (m_image) { + gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 10); + } // shapes. if (offset && size) { - gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND)); + const auto font_size = 18.F; + const auto pad = 15.F; + const float rounding = 5; + + gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding); const u32 percentage = ((double)offset / (double)size) * 100.0; - gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR)); - gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage); + gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding); + gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage); + + const double speed_mb = (double)speed / (1024.0 * 1024.0); + const double speed_kb = (double)speed / (1024.0); + + char speed_str[32]; + if (speed_mb >= 0.01) { + std::snprintf(speed_str, sizeof(speed_str), "%.2f MiB/s", speed_mb); + } else { + std::snprintf(speed_str, sizeof(speed_str), "%.2f KiB/s", speed_kb); + } + + const auto left = size - last_offset; + const auto left_seconds = left / speed; + const auto hours = left_seconds / (60 * 60); + const auto minutes = left_seconds % (60 * 60) / 60; + const auto seconds = left_seconds % 60; + + char time_str[64]; + if (hours) { + std::snprintf(time_str, sizeof(time_str), "%zu hours %zu minutes remaining", hours, minutes); + } else if (minutes) { + std::snprintf(time_str, sizeof(time_str), "%zu minutes %zu seconds remaining", minutes, seconds); + } else { + std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining", seconds); + } + + gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, speed_str); } - gfx::drawTextArgs(vg, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str()); + gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_action.c_str()); + gfx::drawTextArgs(vg, center_x, m_pos.y + 100, 22, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str()); if (!transfer.empty()) { - gfx::drawTextArgs(vg, center_x, prog_bar.y - 15 - 20 * 1.5F, 20, NVG_ALIGN_CENTER, theme->GetColour(ThemeEntryID_TEXT), "%s", transfer.c_str()); + gfx::drawTextArgs(vg, center_x, m_pos.y + 150, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", transfer.c_str()); } + + nvgRestore(vg); } auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& { @@ -115,6 +172,8 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& { m_transfer = transfer; m_size = 0; m_offset = 0; + m_last_offset = 0; + m_timestamp.Update(); mutexUnlock(&m_mutex); Yield(); return *this; @@ -129,6 +188,21 @@ auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& { return *this; } +auto ProgressBox::SetImageData(std::vector& data) -> ProgressBox& { + mutexLock(&m_mutex); + std::swap(m_image_data, data); + mutexUnlock(&m_mutex); + return *this; +} + +auto ProgressBox::SetImageDataConst(std::span data) -> ProgressBox& { + mutexLock(&m_mutex); + m_image_data.resize(data.size()); + std::memcpy(m_image_data.data(), data.data(), m_image_data.size()); + mutexUnlock(&m_mutex); + return *this; +} + void ProgressBox::RequestExit() { m_stop_source.request_stop(); } @@ -184,4 +258,13 @@ void ProgressBox::Yield() { svcSleepThread(YieldType_WithoutCoreMigration); } +void ProgressBox::FreeImage() { + if (m_image && m_own_image) { + nvgDeleteImage(App::GetVg(), m_image); + } + + m_image = 0; + m_own_image = false; +} + } // namespace sphaira::ui diff --git a/sphaira/source/ui/sidebar.cpp b/sphaira/source/ui/sidebar.cpp index 4340200..4997409 100644 --- a/sphaira/source/ui/sidebar.cpp +++ b/sphaira/source/ui/sidebar.cpp @@ -259,9 +259,11 @@ auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void { if (touch->is_clicked && !touch->in_range(GetPos())) { App::PopToMenu(); } else { - m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) { + m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) { SetIndex(i); - FireAction(Button::A); + if (touch) { + FireAction(Button::A); + } }); } @@ -334,18 +336,6 @@ void Sidebar::SetupButtons() { // add default actions, overriding if needed. this->SetActions( - std::make_pair(Button::DOWN, Action{[this](){ - auto index = m_index; - if (m_list->ScrollDown(index, 1, m_items.size())) { - SetIndex(index); - } - }}), - std::make_pair(Button::UP, Action{[this](){ - auto index = m_index; - if (m_list->ScrollUp(index, 1, m_items.size())) { - SetIndex(index); - } - }}), // each item has it's own Action, but we take over B std::make_pair(Button::B, Action{"Back"_i18n, [this](){ SetPop(); diff --git a/sphaira/source/yati/nx/nca.cpp b/sphaira/source/yati/nx/nca.cpp index 9ba80e6..a8ae260 100644 --- a/sphaira/source/yati/nx/nca.cpp +++ b/sphaira/source/yati/nx/nca.cpp @@ -151,4 +151,30 @@ Result VerifyFixedKey(const Header& header) { R_SUCCEED(); } +auto GetKeyGenStr(u8 key_gen) -> const char* { + switch (key_gen) { + case KeyGenerationOld_100: return "1.0.0"; + case KeyGenerationOld_300: return "3.0.0"; + case KeyGeneration_301: return "3.0.1"; + case KeyGeneration_400: return "4.0.0"; + case KeyGeneration_500: return "5.0.0"; + case KeyGeneration_600: return "6.0.0"; + case KeyGeneration_620: return "6.2.0"; + case KeyGeneration_700: return "7.0.0"; + case KeyGeneration_810: return "8.1.0"; + case KeyGeneration_900: return "9.0.0"; + case KeyGeneration_910: return "9.1.0"; + case KeyGeneration_1210: return "12.1.0"; + case KeyGeneration_1300: return "13.0.0"; + case KeyGeneration_1400: return "14.0.0"; + case KeyGeneration_1500: return "15.0.0"; + case KeyGeneration_1600: return "16.0.0"; + case KeyGeneration_1700: return "17.0.0"; + case KeyGeneration_1800: return "18.0.0"; + case KeyGeneration_1900: return "19.0.0"; + } + + return "Unknown"; +} + } // namespace sphaira::nca diff --git a/sphaira/source/yati/nx/ncm.cpp b/sphaira/source/yati/nx/ncm.cpp index a338816..ba6ecf9 100644 --- a/sphaira/source/yati/nx/ncm.cpp +++ b/sphaira/source/yati/nx/ncm.cpp @@ -7,16 +7,24 @@ namespace { } // namespace -auto GetAppId(const NcmContentMetaKey& key) -> u64 { - if (key.type == NcmContentMetaType_Patch) { - return key.id ^ 0x800; - } else if (key.type == NcmContentMetaType_AddOnContent) { - return (key.id ^ 0x1000) & ~0xFFF; +auto GetAppId(u8 meta_type, u64 id) -> u64 { + if (meta_type == NcmContentMetaType_Patch) { + return id ^ 0x800; + } else if (meta_type == NcmContentMetaType_AddOnContent) { + return (id ^ 0x1000) & ~0xFFF; } else { - return key.id; + return id; } } +auto GetAppId(const NcmContentMetaKey& key) -> u64 { + return GetAppId(key.type, key.id); +} + +auto GetAppId(const PackagedContentMeta& meta) -> u64 { + return GetAppId(meta.meta_type, meta.title_id); +} + Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) { bool has; R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id)); diff --git a/sphaira/source/yati/yati.cpp b/sphaira/source/yati/yati.cpp index 1e9a85c..4ea3dba 100644 --- a/sphaira/source/yati/yati.cpp +++ b/sphaira/source/yati/yati.cpp @@ -274,7 +274,7 @@ struct Yati { Yati(ui::ProgressBox*, std::shared_ptr); ~Yati(); - Result Setup(); + Result Setup(const ConfigOverride& override); Result InstallNca(std::span tickets, NcaCollection& nca); Result InstallCnmtNca(std::span tickets, CnmtCollection& cnmt, const container::Collections& collections); Result InstallControlNca(std::span tickets, const CnmtCollection& cnmt, NcaCollection& nca); @@ -549,7 +549,7 @@ Result Yati::decompressFuncInternal(ThreadData* t) { t->write_size = header.size; R_TRY(ncmContentStorageSetPlaceHolderSize(std::addressof(cs), std::addressof(t->nca->placeholder_id), header.size)); - if (header.distribution_type == nca::DistributionType_GameCard) { + if (!config.ignore_distribution_bit && header.distribution_type == nca::DistributionType_GameCard) { header.distribution_type = nca::DistributionType_System; t->nca->modified = true; } @@ -792,8 +792,8 @@ Yati::~Yati() { appletSetMediaPlaybackState(false); } -Result Yati::Setup() { - config.sd_card_install = App::GetApp()->m_install_sd.Get(); +Result Yati::Setup(const ConfigOverride& override) { + config.sd_card_install = override.sd_card_install.value_or(App::GetApp()->m_install_sd.Get()); config.allow_downgrade = App::GetApp()->m_allow_downgrade.Get(); config.skip_if_already_installed = App::GetApp()->m_skip_if_already_installed.Get(); config.ticket_only = App::GetApp()->m_ticket_only.Get(); @@ -802,13 +802,13 @@ Result Yati::Setup() { config.skip_addon = App::GetApp()->m_skip_addon.Get(); config.skip_data_patch = App::GetApp()->m_skip_data_patch.Get(); config.skip_ticket = App::GetApp()->m_skip_ticket.Get(); - config.skip_nca_hash_verify = App::GetApp()->m_skip_nca_hash_verify.Get(); - config.skip_rsa_header_fixed_key_verify = App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(); - config.skip_rsa_npdm_fixed_key_verify = App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(); - config.ignore_distribution_bit = App::GetApp()->m_ignore_distribution_bit.Get(); - config.convert_to_standard_crypto = App::GetApp()->m_convert_to_standard_crypto.Get(); - config.lower_master_key = App::GetApp()->m_lower_master_key.Get(); - config.lower_system_version = App::GetApp()->m_lower_system_version.Get(); + config.skip_nca_hash_verify = override.skip_nca_hash_verify.value_or(App::GetApp()->m_skip_nca_hash_verify.Get()); + config.skip_rsa_header_fixed_key_verify = override.skip_rsa_header_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get()); + config.skip_rsa_npdm_fixed_key_verify = override.skip_rsa_npdm_fixed_key_verify.value_or(App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get()); + config.ignore_distribution_bit = override.ignore_distribution_bit.value_or(App::GetApp()->m_ignore_distribution_bit.Get()); + config.convert_to_standard_crypto = override.convert_to_standard_crypto.value_or(App::GetApp()->m_convert_to_standard_crypto.Get()); + config.lower_master_key = override.lower_master_key.value_or(App::GetApp()->m_lower_master_key.Get()); + config.lower_system_version = override.lower_system_version.value_or(App::GetApp()->m_lower_system_version.Get()); storage_id = config.sd_card_install ? NcmStorageId_SdCard : NcmStorageId_BuiltInUser; R_TRY(source->GetOpenResult()); @@ -925,37 +925,9 @@ Result Yati::InstallCnmtNca(std::span tickets, CnmtCollection& cn R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs))); R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(cnmt.placeholder_id))); - FsFileSystem fs; - R_TRY(fsOpenFileSystem(std::addressof(fs), FsFileSystemType_ContentMeta, path)); - ON_SCOPE_EXIT(fsFsClose(std::addressof(fs))); - - FsDir dir; - R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir))); - ON_SCOPE_EXIT(fsDirClose(std::addressof(dir))); - - s64 total_entries; - FsDirectoryEntry buf; - R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf))); - - FsFile file; - R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file))); - ON_SCOPE_EXIT(fsFileClose(std::addressof(file))); - - s64 offset{}; - u64 bytes_read; ncm::PackagedContentMeta header; - R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read))); - offset += bytes_read; - - // read extended header - cnmt.extended_header.resize(header.meta_header.extended_header_size); - R_TRY(fsFileRead(std::addressof(file), offset, cnmt.extended_header.data(), cnmt.extended_header.size(), 0, std::addressof(bytes_read))); - offset += bytes_read; - - // read infos. - std::vector infos(header.meta_header.content_count); - R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read))); - offset += bytes_read; + std::vector infos; + R_TRY(ParseCnmtNca(path, header, cnmt.extended_header, infos)); for (const auto& info : infos) { if (info.info.content_type == NcmContentType_DeltaFragment) { @@ -1020,21 +992,15 @@ Result Yati::InstallControlNca(std::span tickets, const CnmtColle R_TRY(ncmContentStorageFlushPlaceHolder(std::addressof(cs))); R_TRY(ncmContentStorageGetPlaceHolderPath(std::addressof(cs), path, sizeof(path), std::addressof(nca.placeholder_id))); - log_write("got control path: %s\n", path.s); - FsFileSystem fs; - R_TRY(fsOpenFileSystemWithId(std::addressof(fs), ncm::GetAppId(cnmt.key), FsFileSystemType_ContentControl, path, FsContentAttributes_All)); - ON_SCOPE_EXIT(fsFsClose(std::addressof(fs))); - log_write("opened control path fs: %s\n", path.s); - - FsFile file; - R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file))); - ON_SCOPE_EXIT(fsFileClose(std::addressof(file))); - log_write("got control path file: %s\n", path.s); - + // this can fail if it's not a valid control nca, examples are mario 3d all stars. + // there are 4 control ncas, only 1 is valid (InvalidNcaId 0x235E02). NacpLanguageEntry entry; - u64 bytes_read; - R_TRY(fsFileRead(&file, 0, &entry, sizeof(entry), 0, &bytes_read)); - pbox->SetTitle("Installing "_i18n + entry.name); + std::vector icon; + if (R_SUCCEEDED(yati::ParseControlNca(path, ncm::GetAppId(cnmt.key), &entry, sizeof(entry), &icon))) { + pbox->SetTitle(entry.name).SetImageData(icon); + } else { + log_write("\tWARNING: failed to parse control nca!\n"); + } R_SUCCEED(); } @@ -1070,41 +1036,37 @@ Result Yati::ParseTicketsIntoCollection(std::vector& tickets, con Result Yati::GetLatestVersion(const CnmtCollection& cnmt, u32& version_out, bool& skip) { const auto app_id = ncm::GetAppId(cnmt.key); - - bool has_records; - R_TRY(nsIsAnyApplicationEntityInstalled(app_id, &has_records)); - - // TODO: fix this when gamecard is inserted as it will only return records - // for the gamecard... - // may have to use ncm directly to get the keys, then parse that. version_out = cnmt.key.version; - if (has_records) { - s32 meta_count{}; - R_TRY(nsCountApplicationContentMeta(app_id, &meta_count)); - R_UNLESS(meta_count > 0, 0x1); - std::vector records(meta_count); - s32 count; - R_TRY(ns::ListApplicationRecordContentMeta(std::addressof(ns_app), 0, app_id, records.data(), records.size(), &count)); - R_UNLESS(count == records.size(), 0x1); - - for (auto& record : records) { - log_write("found record: 0x%016lX type: %u version: %u\n", record.key.id, record.key.type, record.key.version); - log_write("cnmt record: 0x%016lX type: %u version: %u\n", cnmt.key.id, cnmt.key.type, cnmt.key.version); - - if (record.key.id == cnmt.key.id && cnmt.key.version == record.key.version && config.skip_if_already_installed) { - log_write("skipping as already installed\n"); - skip = true; + for (auto& db : ncm_db) { + s32 db_list_total; + s32 db_list_count; + std::vector keys(1); + if (R_SUCCEEDED(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), NcmContentMetaType_Unknown, app_id, 0, UINT64_MAX, NcmContentInstallType_Full))) { + if (db_list_total != keys.size()) { + keys.resize(db_list_total); + if (keys.size()) { + R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(db_list_total), std::addressof(db_list_count), keys.data(), keys.size(), NcmContentMetaType_Unknown, app_id, 0, UINT64_MAX, NcmContentInstallType_Full)); + } } - // check if we are downgrading - if (cnmt.key.type == NcmContentMetaType_Patch) { - if (cnmt.key.type == record.key.type && cnmt.key.version < record.key.version && !config.allow_downgrade) { - log_write("skipping due to it being lower\n"); + for (auto& key : keys) { + log_write("found record: %016lX type: %u version: %u\n", key.id, key.type, key.version); + + if (key.id == cnmt.key.id && cnmt.key.version == key.version && config.skip_if_already_installed) { + log_write("skipping as already installed\n"); skip = true; } - } else { - version_out = std::max(version_out, record.key.version); + + // check if we are downgrading + if (cnmt.key.type == NcmContentMetaType_Patch) { + if (cnmt.key.type == key.type && cnmt.key.version < key.version && !config.allow_downgrade) { + log_write("skipping due to it being lower\n"); + skip = true; + } + } else { + version_out = std::max(version_out, key.version); + } } } } @@ -1160,7 +1122,6 @@ Result Yati::RemoveInstalledNcas(const CnmtCollection& cnmt) { s32 db_list_count; u64 id_min = cnmt.key.id; u64 id_max = cnmt.key.id; - std::vector keys(1); // if installing a patch, remove all previously installed patches. if (cnmt.key.type == NcmContentMetaType_Patch) { @@ -1263,9 +1224,9 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve R_SUCCEED(); } -Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections) { +Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections, const ConfigOverride& override) { auto yati = std::make_unique(pbox, source); - R_TRY(yati->Setup()); + R_TRY(yati->Setup(override)); std::vector tickets{}; R_TRY(yati->ParseTicketsIntoCollection(tickets, collections, true)); @@ -1318,9 +1279,9 @@ Result InstallInternal(ui::ProgressBox* pbox, std::shared_ptr sour R_SUCCEED(); } -Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr source, container::Collections collections) { +Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr source, container::Collections collections, const ConfigOverride& override) { auto yati = std::make_unique(pbox, source); - R_TRY(yati->Setup()); + R_TRY(yati->Setup(override)); // not supported with stream installs (yet). yati->config.convert_to_standard_crypto = false; @@ -1415,40 +1376,107 @@ Result InstallInternalStream(ui::ProgressBox* pbox, std::shared_ptr(fs, path), path); - // return InstallFromSource(pbox, std::make_shared(fs, path), path); +Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override) { + return InstallFromSource(pbox, std::make_shared(fs, path), path, override); + // return InstallFromSource(pbox, std::make_shared(fs, path), path, override); } -Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path) { - return InstallFromSource(pbox, std::make_shared(path), path); +Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override) { + return InstallFromSource(pbox, std::make_shared(path), path, override); } -Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr source, const fs::FsPath& path) { +Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr source, const fs::FsPath& path, const ConfigOverride& override) { const auto ext = std::strrchr(path.s, '.'); R_UNLESS(ext, Result_ContainerNotFound); if (!strcasecmp(ext, ".nsp") || !strcasecmp(ext, ".nsz")) { - return InstallFromContainer(pbox, std::make_unique(source)); + return InstallFromContainer(pbox, std::make_unique(source), override); } else if (!strcasecmp(ext, ".xci") || !strcasecmp(ext, ".xcz")) { - return InstallFromContainer(pbox, std::make_unique(source)); + return InstallFromContainer(pbox, std::make_unique(source), override); } R_THROW(Result_ContainerNotFound); } -Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr container) { +Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr container, const ConfigOverride& override) { container::Collections collections; R_TRY(container->GetCollections(collections)); return InstallFromCollections(pbox, container->GetSource(), collections); } -Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections) { +Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr source, const container::Collections& collections, const ConfigOverride& override) { if (source->IsStream()) { - return InstallInternalStream(pbox, source, collections); + return InstallInternalStream(pbox, source, collections, override); } else { - return InstallInternal(pbox, source, collections); + return InstallInternal(pbox, source, collections, override); } } +Result ParseCnmtNca(const fs::FsPath& path, ncm::PackagedContentMeta& header, std::vector& extended_header, std::vector& infos) { + FsFileSystem fs; + R_TRY(fsOpenFileSystem(std::addressof(fs), FsFileSystemType_ContentMeta, path)); + ON_SCOPE_EXIT(fsFsClose(std::addressof(fs))); + + FsDir dir; + R_TRY(fsFsOpenDirectory(std::addressof(fs), fs::FsPath{"/"}, FsDirOpenMode_ReadFiles, std::addressof(dir))); + ON_SCOPE_EXIT(fsDirClose(std::addressof(dir))); + + s64 total_entries; + FsDirectoryEntry buf; + R_TRY(fsDirRead(std::addressof(dir), std::addressof(total_entries), 1, std::addressof(buf))); + + FsFile file; + R_TRY(fsFsOpenFile(std::addressof(fs), fs::AppendPath("/", buf.name), FsOpenMode_Read, std::addressof(file))); + ON_SCOPE_EXIT(fsFileClose(std::addressof(file))); + + s64 offset{}; + u64 bytes_read; + R_TRY(fsFileRead(std::addressof(file), offset, std::addressof(header), sizeof(header), 0, std::addressof(bytes_read))); + offset += bytes_read; + + // read extended header + extended_header.resize(header.meta_header.extended_header_size); + R_TRY(fsFileRead(std::addressof(file), offset, extended_header.data(), extended_header.size(), 0, std::addressof(bytes_read))); + offset += bytes_read; + + // read infos. + infos.resize(header.meta_header.content_count); + R_TRY(fsFileRead(std::addressof(file), offset, infos.data(), infos.size() * sizeof(NcmPackagedContentInfo), 0, std::addressof(bytes_read))); + offset += bytes_read; + + R_SUCCEED(); +} + +Result ParseControlNca(const fs::FsPath& path, u64 id, void* nacp_out, s64 nacp_size, std::vector* icon_out) { + FsFileSystem fs; + R_TRY(fsOpenFileSystemWithId(std::addressof(fs), id, FsFileSystemType_ContentControl, path, FsContentAttributes_All)); + ON_SCOPE_EXIT(fsFsClose(std::addressof(fs))); + + // read nacp. + if (nacp_out) { + FsFile file; + R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/control.nacp"}, FsOpenMode_Read, std::addressof(file))); + ON_SCOPE_EXIT(fsFileClose(std::addressof(file))); + + u64 bytes_read; + R_TRY(fsFileRead(&file, 0, nacp_out, nacp_size, 0, &bytes_read)); + } + + // read icon. + if (icon_out) { + FsFile file; + R_TRY(fsFsOpenFile(std::addressof(fs), fs::FsPath{"/icon_AmericanEnglish.dat"}, FsOpenMode_Read, std::addressof(file))); + ON_SCOPE_EXIT(fsFileClose(std::addressof(file))); + + s64 size; + R_TRY(fsFileGetSize(std::addressof(file), std::addressof(size))); + icon_out->resize(size); + + u64 bytes_read; + R_TRY(fsFileRead(&file, 0, icon_out->data(), icon_out->size(), 0, &bytes_read)); + } + + R_SUCCEED(); +} + } // namespace sphaira::yati