From b9b3acf7f2d3481c9f4a9264a0475886dc73a0c7 Mon Sep 17 00:00:00 2001 From: J-D-K Date: Mon, 28 Jul 2025 18:28:11 -0400 Subject: [PATCH] Debugging everywhere! --- .clang-format | 2 +- Makefile | 2 +- include/appstates/BaseTask.hpp | 3 + include/appstates/ConfirmState.hpp | 14 +- include/appstates/ProgressState.hpp | 2 +- include/appstates/SaveCreateState.hpp | 3 +- include/appstates/SettingsState.hpp | 6 + include/appstates/TaskState.hpp | 2 +- include/appstates/TitleInfoState.hpp | 47 ++--- include/appstates/TitleSelectCommon.hpp | 5 +- include/appstates/UserOptionState.hpp | 23 ++- include/config.hpp | 43 +++-- include/curl/curl.hpp | 3 +- include/fs/io.hpp | 31 ++- include/logger.hpp | 1 - include/ui/Menu.hpp | 48 +++-- include/ui/PopMessageManager.hpp | 11 +- include/ui/TextScroll.hpp | 66 +++++-- include/ui/TitleView.hpp | 21 ++- romfs/Text/ENUS.json | 62 +++--- source/JKSV.cpp | 7 +- source/appstates/BackupMenuState.cpp | 101 ++++++---- source/appstates/BaseState.cpp | 2 +- source/appstates/BaseTask.cpp | 12 +- source/appstates/ExtrasMenuState.cpp | 6 +- source/appstates/FadeInState.cpp | 2 +- source/appstates/MainMenuState.cpp | 16 +- source/appstates/SaveCreateState.cpp | 26 ++- source/appstates/SettingsState.cpp | 75 ++++---- source/appstates/TextTitleSelectState.cpp | 10 +- source/appstates/TitleInfoState.cpp | 220 ++++++++++------------ source/appstates/TitleOptionState.cpp | 8 +- source/appstates/TitleSelectCommon.cpp | 17 +- source/appstates/TitleSelectState.cpp | 1 - source/appstates/UserOptionState.cpp | 203 ++++++++++---------- source/config.cpp | 94 +++++---- source/data/TitleInfo.cpp | 61 +++--- source/data/User.cpp | 8 +- source/data/data.cpp | 7 +- source/fs/SaveMetaData.cpp | 11 +- source/fs/io.cpp | 147 +++++++++++---- source/fs/save_data_functions.cpp | 10 +- source/logger.cpp | 16 +- source/remote/GoogleDrive.cpp | 72 +++---- source/remote/Item.cpp | 10 +- source/remote/Storage.cpp | 5 +- source/remote/WebDav.cpp | 163 +++++++--------- source/remote/remote.cpp | 71 +++---- source/system/Task.cpp | 4 +- source/system/Timer.cpp | 9 +- source/tasks/backup.cpp | 174 ++++++++++++----- source/ui/Menu.cpp | 86 +++++---- source/ui/PopMessageManager.cpp | 61 +++--- source/ui/TextScroll.cpp | 112 ++++++++--- source/ui/TitleTile.cpp | 24 ++- source/ui/TitleView.cpp | 95 ++++++---- 56 files changed, 1322 insertions(+), 1019 deletions(-) diff --git a/.clang-format b/.clang-format index 11fc93c..8a4e900 100644 --- a/.clang-format +++ b/.clang-format @@ -32,7 +32,7 @@ AlignConsecutiveMacros: AlignFunctionPointers: false PadOperators: false AlignConsecutiveShortCaseStatements: - Enabled: false + Enabled: true AcrossEmptyLines: false AcrossComments: false AlignCaseArrows: false diff --git a/Makefile b/Makefile index d4ef995..b416c94 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ INCLUDES := include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SD EXEFS_SRC := exefs_src APP_TITLE := JKSV APP_AUTHOR := JK -APP_VERSION := 06.13.2025 +APP_VERSION := 07.28.2025 ROMFS := romfs ICON := icon.jpg diff --git a/include/appstates/BaseTask.hpp b/include/appstates/BaseTask.hpp index 6f4d7bc..0eeaf5d 100644 --- a/include/appstates/BaseTask.hpp +++ b/include/appstates/BaseTask.hpp @@ -42,6 +42,9 @@ class BaseTask : public BaseState /// @brief This is used to give the animation its pulsing color. ui::ColorMod m_colorMod{}; + /// @brief This is a pointer to the string popped when plus is pressed. + const char *m_popUnableExit{}; + /// @brief This array holds the glyphs of the loading sequence. I think it's from the Wii? static inline std::array sm_glyphArray = {"\ue020", "\ue021", "\ue022", "\ue023", "\ue024", "\ue025", "\ue026", "\ue027"}; diff --git a/include/appstates/ConfirmState.hpp b/include/appstates/ConfirmState.hpp index faf3748..726206c 100644 --- a/include/appstates/ConfirmState.hpp +++ b/include/appstates/ConfirmState.hpp @@ -41,13 +41,13 @@ class ConfirmState final : public BaseState /// @param function Function executed on confirmation. /// @param dataStruct shared_ptr that is passed to function. I tried templating this and it was a nightmare. ConfirmState(std::string_view query, bool holdRequired, TaskFunction function, std::shared_ptr dataStruct) - : BaseState(false) - , m_queryString(query) - , m_yesText(strings::get_by_name(strings::names::YES_NO, 0)) - , m_noText(strings::get_by_name(strings::names::YES_NO, 1)) - , m_holdRequired(holdRequired) - , m_function(function) - , m_dataStruct(dataStruct) + : BaseState{false} + , m_queryString{query} + , m_yesText{strings::get_by_name(strings::names::YES_NO, 0)} + , m_noText{strings::get_by_name(strings::names::YES_NO, 1)} + , m_holdRequired{holdRequired} + , m_function{function} + , m_dataStruct{dataStruct} { const int noWidth = sdl::text::get_width(22, m_noText); diff --git a/include/appstates/ProgressState.hpp b/include/appstates/ProgressState.hpp index 09ede15..22d24d1 100644 --- a/include/appstates/ProgressState.hpp +++ b/include/appstates/ProgressState.hpp @@ -15,7 +15,7 @@ class ProgressState final : public BaseTask /// @note All functions passed to this must follow this signature: void function(sys::ProgressTask *, ) template ProgressState(void (*function)(sys::ProgressTask *, Args...), Args... args) - : BaseTask() + : BaseTask{} { m_task = std::make_unique(function, std::forward(args)...); } diff --git a/include/appstates/SaveCreateState.hpp b/include/appstates/SaveCreateState.hpp index b3a3282..5d99607 100644 --- a/include/appstates/SaveCreateState.hpp +++ b/include/appstates/SaveCreateState.hpp @@ -5,6 +5,7 @@ #include "ui/Menu.hpp" #include "ui/SlideOutPanel.hpp" +#include #include /// @brief This is the state that is spawned when CreateSaveData is selected from the user menu. @@ -42,7 +43,7 @@ class SaveCreateState final : public BaseState std::vector m_titleInfoVector{}; /// @brief Whether or not a refresh is required on the next update() call. - bool m_refreshRequired{}; + std::atomic m_refreshRequired{}; /// @brief Shared slide panel all instances use. There's no point in allocating a new one every time. static inline std::unique_ptr sm_slidePanel{}; diff --git a/include/appstates/SettingsState.hpp b/include/appstates/SettingsState.hpp index 501f3a8..fcbb0be 100644 --- a/include/appstates/SettingsState.hpp +++ b/include/appstates/SettingsState.hpp @@ -36,6 +36,12 @@ class SettingsState final : public BaseState /// @brief Render target to render to. sdl::SharedTexture m_renderTarget{}; + /// @brief Loads the settings menu strings. + void load_settings_menu(); + + /// @brief Loads the on/off and sort type strings. + void load_extra_strings(); + /// @brief Runs a routine to update the menu strings for the menu. void update_menu_options(); diff --git a/include/appstates/TaskState.hpp b/include/appstates/TaskState.hpp index 7ecd991..d939578 100644 --- a/include/appstates/TaskState.hpp +++ b/include/appstates/TaskState.hpp @@ -14,7 +14,7 @@ class TaskState final : public BaseTask /// @note All functions passed must follow this signature: void function(sys::Task *, ) template TaskState(void (*function)(sys::Task *, Args...), Args... args) - : BaseTask() + : BaseTask{} { m_task = std::make_unique(function, std::forward(args)...); } diff --git a/include/appstates/TitleInfoState.hpp b/include/appstates/TitleInfoState.hpp index 813615c..d28ff91 100644 --- a/include/appstates/TitleInfoState.hpp +++ b/include/appstates/TitleInfoState.hpp @@ -7,6 +7,7 @@ #include #include +#include class TitleInfoState final : public BaseState { @@ -35,42 +36,18 @@ class TitleInfoState final : public BaseState /// @brief This is a pointer to the title's icon. sdl::SharedTexture m_icon{}; - /// @brief This is the scrolling text for the title. - ui::TextScroll m_titleScroll{}; - - /// @brief This is the scrolling text for the publisher. - ui::TextScroll m_publisherScroll{}; - - /// @brief This string holds the application ID. - std::string m_applicationID{}; - - /// @brief This holds the hex save data id of the file on nand. - std::string m_saveDataID{}; - - /// @brief This holds the time the game was first played. - std::string m_firstPlayed{}; - - /// @brief This holds the last played timestamp. - std::string m_lastPlayed{}; - - /// @brief This holds the play time string. - std::string m_playTime{}; - - /// @brief This holds the total launches string. - std::string m_totalLaunches{}; - - /// @brief This holds the save data type string. - std::string m_saveDataType{}; - - /// @brief Bool to tell whether or not static members are initialized. - static inline bool sm_initialized{}; - - /// @brief This is the render target for the title text just in case it needs scrolling. - static inline sdl::SharedTexture sm_titleTarget{}; - - /// @brief This is the render target for the publisher string. - static inline sdl::SharedTexture sm_publisherTarget{}; + /// @brief This controls the clear color of the fields rendered. + bool m_fieldClear{}; /// @brief Slide panel. static inline std::unique_ptr sm_slidePanel{}; + + /// @brief Initializes the static members if they haven't been already. + void initialize_static_members(); + + /// @brief Creates the scrolling text/cheating fields. + void create_info_fields(); + + /// @brief Helper function for creating text fields. + std::shared_ptr create_new_field(std::string_view text, int y); }; diff --git a/include/appstates/TitleSelectCommon.hpp b/include/appstates/TitleSelectCommon.hpp index c09628b..0164ab8 100644 --- a/include/appstates/TitleSelectCommon.hpp +++ b/include/appstates/TitleSelectCommon.hpp @@ -25,6 +25,9 @@ class TitleSelectCommon : public BaseState void render_control_guide(); private: + /// @brief Pointer to the control guide string. + static inline const char *sm_controlGuide{}; + /// @brief X coordinate the control guide is rendered at. - static inline int m_titleControlsX{}; + static inline int sm_controlGuideX{}; }; diff --git a/include/appstates/UserOptionState.hpp b/include/appstates/UserOptionState.hpp index 0763a64..71b38b7 100644 --- a/include/appstates/UserOptionState.hpp +++ b/include/appstates/UserOptionState.hpp @@ -54,5 +54,26 @@ class UserOptionState final : public BaseState bool m_refreshRequired{}; /// @brief Slide panel all instances shared. - static inline std::unique_ptr m_menuPanel{}; + static inline std::unique_ptr sm_menuPanel{}; + + /// @brief Creates the panel if it hasn't been yet. + void create_menu_panel(); + + /// @brief Creates and pushes the strings needed for the menu + void load_menu_strings(); + + /// @brief Assigns the data within the struct to point where it needs to. + void initialize_data_struct(); + + /// @brief Starts the backup all operation. + void backup_all(); + + /// @brief Creates and pushes a new save creation menu. + void create_save_create(); + + /// @brief Launches the create all save data for use task. + void create_all_save_data(); + + /// @brief Deletes all save data for the user. + void delete_all_save_data(); }; diff --git a/include/config.hpp b/include/config.hpp index 138054f..45f63c8 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -1,5 +1,6 @@ #pragma once #include "fslib.hpp" + #include namespace config @@ -73,28 +74,30 @@ namespace config /// @param pathOutSize Size of the buffer to write the path to. void get_custom_path(uint64_t applicationID, char *pathOut, size_t pathOutSize); - // Names of keys. Note: Not all of these are retrievable with GetByKey. Some of these are purely for config reading and writing. + // Names of keys. Note: Not all of these are retrievable with GetByKey. Some of these are purely for config reading and + // writing. namespace keys { - static constexpr std::string_view WORKING_DIRECTORY = "WorkingDirectory"; - static constexpr std::string_view INCLUDE_DEVICE_SAVES = "IncludeDeviceSaves"; - static constexpr std::string_view AUTO_BACKUP_ON_RESTORE = "AutoBackupOnRestore"; - static constexpr std::string_view AUTO_NAME_BACKUPS = "AutoNameBackups"; - static constexpr std::string_view AUTO_UPLOAD = "AutoUploadToRemote"; - static constexpr std::string_view HOLD_FOR_DELETION = "HoldForDeletion"; - static constexpr std::string_view HOLD_FOR_RESTORATION = "HoldForRestoration"; - static constexpr std::string_view HOLD_FOR_OVERWRITE = "HoldForOverWrite"; - static constexpr std::string_view ONLY_LIST_MOUNTABLE = "OnlyListMountable"; - static constexpr std::string_view LIST_ACCOUNT_SYS_SAVES = "ListAccountSystemSaves"; + static constexpr std::string_view WORKING_DIRECTORY = "WorkingDirectory"; + static constexpr std::string_view INCLUDE_DEVICE_SAVES = "IncludeDeviceSaves"; + static constexpr std::string_view AUTO_BACKUP_ON_RESTORE = "AutoBackupOnRestore"; + static constexpr std::string_view AUTO_NAME_BACKUPS = "AutoNameBackups"; + static constexpr std::string_view AUTO_UPLOAD = "AutoUploadToRemote"; + static constexpr std::string_view USE_TITLE_IDS = "AlwaysUseTitleID"; + static constexpr std::string_view HOLD_FOR_DELETION = "HoldForDeletion"; + static constexpr std::string_view HOLD_FOR_RESTORATION = "HoldForRestoration"; + static constexpr std::string_view HOLD_FOR_OVERWRITE = "HoldForOverWrite"; + static constexpr std::string_view ONLY_LIST_MOUNTABLE = "OnlyListMountable"; + static constexpr std::string_view LIST_ACCOUNT_SYS_SAVES = "ListAccountSystemSaves"; static constexpr std::string_view ALLOW_WRITING_TO_SYSTEM = "AllowSystemSaveWriting"; - static constexpr std::string_view EXPORT_TO_ZIP = "ExportToZip"; - static constexpr std::string_view ZIP_COMPRESSION_LEVEL = "ZipCompressionLevel"; - static constexpr std::string_view TITLE_SORT_TYPE = "TitleSortType"; - static constexpr std::string_view JKSM_TEXT_MODE = "JKSMTextMode"; - static constexpr std::string_view FORCE_ENGLISH = "ForceEnglish"; - static constexpr std::string_view ENABLE_TRASH_BIN = "EnableTrash"; - static constexpr std::string_view UI_ANIMATION_SCALE = "UIAnimationScaling"; - static constexpr std::string_view FAVORITES = "Favorites"; - static constexpr std::string_view BLACKLIST = "BlackList"; + static constexpr std::string_view EXPORT_TO_ZIP = "ExportToZip"; + static constexpr std::string_view ZIP_COMPRESSION_LEVEL = "ZipCompressionLevel"; + static constexpr std::string_view TITLE_SORT_TYPE = "TitleSortType"; + static constexpr std::string_view JKSM_TEXT_MODE = "JKSMTextMode"; + static constexpr std::string_view FORCE_ENGLISH = "ForceEnglish"; + static constexpr std::string_view ENABLE_TRASH_BIN = "EnableTrash"; + static constexpr std::string_view UI_ANIMATION_SCALE = "UIAnimationScaling"; + static constexpr std::string_view FAVORITES = "Favorites"; + static constexpr std::string_view BLACKLIST = "BlackList"; } // namespace keys } // namespace config diff --git a/include/curl/curl.hpp b/include/curl/curl.hpp index febe79d..e84a209 100644 --- a/include/curl/curl.hpp +++ b/include/curl/curl.hpp @@ -12,7 +12,7 @@ namespace curl { /// @brief JKSV's user agent string. - static const char *STRING_USER_AGENT = "JKSV"; + static constexpr const char *STRING_USER_AGENT = "JKSV"; /// @brief Self cleaning curl handle. using Handle = std::unique_ptr; @@ -57,6 +57,7 @@ namespace curl { curl_easy_reset(curl.get()); curl::set_option(curl, CURLOPT_USERAGENT, curl::STRING_USER_AGENT); + curl::set_option(curl, CURLOPT_CONNECTTIMEOUT, 5L); } /// @brief Logged inline wrapper function for curl_easy_perform. diff --git a/include/fs/io.hpp b/include/fs/io.hpp index 08cddaf..e02c7c5 100644 --- a/include/fs/io.hpp +++ b/include/fs/io.hpp @@ -12,11 +12,16 @@ namespace fs /// @param journalSize Optional. The size of the journal if data needs to be commited. /// @param commitDevice Optional. The device to commit to if it's needed. /// @param Task Optional. Progress tracking task to display progress of operation if needed. - void copy_file(const fslib::Path &source, - const fslib::Path &destination, - sys::ProgressTask *Task = nullptr, - uint64_t journalSize = 0, - std::string_view commitDevice = {}); + void copy_file(const fslib::Path &source, const fslib::Path &destination, sys::ProgressTask *Task = nullptr); + + /// @brief Same as a above. Committing data to the device passed while copying. + /// @param device Device to commit data to. + /// @param journalSize Size of the journal area of the save data. + void copy_file_commit(const fslib::Path &source, + const fslib::Path &destination, + std::string_view device, + int64_t journalSize, + sys::ProgressTask *task = nullptr); /// @brief Recursively copies source to destination. /// @param source Source path. @@ -24,9 +29,15 @@ namespace fs /// @param journalSize Optional. Journal size to be passed to copyFile if data needs to be commited to device. /// @param commitDevice Optional. Device to commit data to if needed. /// @param Task Option. Progress tracking task to be passed to copyFile to show progress of operation. - void copy_directory(const fslib::Path &source, - const fslib::Path &destination, - sys::ProgressTask *Task = nullptr, - uint64_t journalSize = 0, - std::string_view commitDevice = {}); + void copy_directory(const fslib::Path &source, const fslib::Path &destination, sys::ProgressTask *Task = nullptr); + + /// @brief Same as above but committing data passed while copying. + /// @param device Device to commit data to. + /// @param journalSize Size of the journaling area of the save. + void copy_directory_commit(const fslib::Path &source, + const fslib::Path &destination, + std::string_view device, + int64_t journalSize, + sys::ProgressTask *task = nullptr); + } // namespace fs diff --git a/include/logger.hpp b/include/logger.hpp index 6eb9ad4..33ed971 100644 --- a/include/logger.hpp +++ b/include/logger.hpp @@ -11,5 +11,4 @@ namespace logger /// @param arguments Va arguments. void log(const char *format, ...); - void log_straight(std::string_view string); } // namespace logger diff --git a/include/ui/Menu.hpp b/include/ui/Menu.hpp index 4425f65..ec9cd67 100644 --- a/include/ui/Menu.hpp +++ b/include/ui/Menu.hpp @@ -2,6 +2,8 @@ #include "sdl.hpp" #include "ui/ColorMod.hpp" #include "ui/Element.hpp" +#include "ui/TextScroll.hpp" + #include #include @@ -56,34 +58,52 @@ namespace ui protected: /// @brief X coordinate menu is rendered to. - double m_x; + double m_x{}; + /// @brief Y coordinate menu is rendered to. - double m_y; + double m_y{}; + /// @brief Currently selected option. - int m_selected = 0; + int m_selected{}; + /// @brief Color mod for bounding box. - ui::ColorMod m_colorMod; + ui::ColorMod m_colorMod{}; + /// @brief Height of options in pixels. - int m_optionHeight; + int m_optionHeight{}; + /// @brief Target options are rendered to. - sdl::SharedTexture m_optionTarget = nullptr; + sdl::SharedTexture m_optionTarget{}; private: /// @brief This to preserve the original Y coordinate passed. - double m_originalY; + double m_originalY{}; + /// @brief The target Y coordinate the menu should be rendered at. - double m_targetY; + double m_targetY{}; + /// @brief How many options before scrolling happens. - int m_scrollLength; + int m_scrollLength{}; + /// @brief Width of the menu in pixels. - int m_width; + int m_width{}; + /// @brief Font size in pixels. - int m_fontSize; + int m_fontSize{}; + + /// @brief The Y coord text is rendered to on that target. + int m_textY{}; + /// @brief Vertical size of the destination render target in pixels. - int m_renderTargetHeight; + int m_renderTargetHeight{}; + /// @brief Maximum number of display options render target can show. - int m_maxDisplayOptions; + int m_maxDisplayOptions{}; + + /// @brief Text scroll for when the current option is too long to on screen. + ui::TextScroll m_optionScroll{}; + /// @brief Vector of options. - std::vector m_options; + std::vector m_options{}; }; } // namespace ui diff --git a/include/ui/PopMessageManager.hpp b/include/ui/PopMessageManager.hpp index f767e19..80c523f 100644 --- a/include/ui/PopMessageManager.hpp +++ b/include/ui/PopMessageManager.hpp @@ -46,6 +46,7 @@ namespace ui private: // Only one instance allowed. PopMessageManager() = default; + // Returns the only instance. static PopMessageManager &get_instance() { @@ -53,10 +54,14 @@ namespace ui return manager; } // The queue for processing. SDL can't handle things being rendered in multiple threads. - std::vector> m_messageQueue; + std::vector> m_messageQueue{}; + // Actual vector of messages - std::vector m_messages; + std::vector m_messages{}; + // Mutex to attempt to make this thread safe. - std::mutex m_messageMutex; + std::mutex m_messageMutex{}; + + std::mutex m_queueMutex{}; }; } // namespace ui diff --git a/include/ui/TextScroll.hpp b/include/ui/TextScroll.hpp index 9992a8b..5a321f7 100644 --- a/include/ui/TextScroll.hpp +++ b/include/ui/TextScroll.hpp @@ -1,6 +1,8 @@ #pragma once +#include "sdl.hpp" #include "system/Timer.hpp" #include "ui/Element.hpp" + #include namespace ui @@ -15,11 +17,20 @@ namespace ui /// @brief Constructor for TextScroll. /// @param text Text to create the textscroll with. /// @param fontSize Size of the font in pixels to use. - /// @param availableWidth Available width of the render target. + /// @param width Maximum width to use for rendering text. + /// @param x X coordinate to render to. /// @param y Y coordinate to render to. /// @param center Whether or not text should be centered if it's not wide enough for scrolling. /// @param color Color to use to render the text. - TextScroll(std::string_view text, int fontSize, int availableWidth, int y, bool center, sdl::Color color); + TextScroll(std::string_view text, + int x, + int y, + int width, + int height, + int fontSize, + sdl::Color textColor, + sdl::Color clearColor, + bool center = true); /// @brief Required destructor. ~TextScroll() {}; @@ -31,7 +42,21 @@ namespace ui /// @param y Y coordinate used to render text. /// @param center Whether or not text should be centered if it's not wide enough for scrolling. /// @param color Color to use to render text. - void create(std::string_view text, int fontSize, int availableWidth, int y, bool center, sdl::Color color); + void create(std::string_view text, + int x, + int y, + int width, + int height, + int fontSize, + sdl::Color textColor, + sdl::Color clearColor, + bool center = true); + + /// @brief Sets and allows changing the text scrolled. + void set_text(std::string_view text, bool center); + + /// @brief Allows setting of the X and Y render coordinates. + void set_xy(int x, int y); /// @brief Runs the update routine. /// @param hasFocus Whether or not the calling state has focus. @@ -44,28 +69,45 @@ namespace ui private: /// @brief Text to display. - std::string m_text; + std::string m_text{}; /// @brief X coordinate to render text at. - int m_x; + int m_textX{}; - /// @brief Y coordinate to render text at. - int m_y; + /// @brief Y coordinate to render text at. This is centered vertically. + int m_textY{}; + + /// @brief The X coordinate the render target is rendered to. + int m_renderX{}; + + /// @brief Y coordinate to render the target to. + int m_renderY{}; /// @brief Font size used to calculate and render text. - int m_fontSize = 0; + int m_fontSize{}; /// @brief Color used to render the text. - sdl::Color m_textColor; + sdl::Color m_textColor{}; + + /// @brief Color used to clear the render target. + sdl::Color m_clearColor{}; /// @brief Width of text in pixels. - int m_textWidth = 0; + int m_textWidth{}; + + /// @brief Width of the render target. + int m_targetWidth{}; + + /// @brief Height of the render target. + int m_targetHeight{}; /// @brief Whether or not text is too wide to fit into the availableWidth passed to the constructor. - bool m_textScrolling = false; + bool m_textScrolling{}; /// @brief Whether or not a scroll was triggered. - bool m_textScrollTriggered = false; + bool m_textScrollTriggered{}; + + sdl::SharedTexture m_renderTarget{}; /// @brief Timer for scrolling text. sys::Timer m_scrollTimer; diff --git a/include/ui/TitleView.hpp b/include/ui/TitleView.hpp index 72bbcfe..a7b4d13 100644 --- a/include/ui/TitleView.hpp +++ b/include/ui/TitleView.hpp @@ -4,6 +4,7 @@ #include "ui/ColorMod.hpp" #include "ui/Element.hpp" #include "ui/TitleTile.hpp" + #include namespace ui @@ -40,18 +41,24 @@ namespace ui private: /// @brief Pointer to user passed. - data::User *m_user = nullptr; + data::User *m_user{}; + /// @brief Y coordinate. - double m_y = 28.0f; + double m_y{32.0f}; + /// @brief Currently highlighted/selected title. - int m_selected = 0; + int m_selected{}; + /// @brief X coordinate of the currently selected tile so it can be rendered over top of the rest. - double m_selectedX; + int m_selectedX{}; + /// @brief Y coordinate. Same as above. - double m_selectedY; + int m_selectedY{}; + /// @brief Color mod for bounding/selection pulse. - ui::ColorMod m_colorMod; + ui::ColorMod m_colorMod{}; + /// @brief Vector of selection tiles. - std::vector m_titleTiles; + std::vector m_titleTiles{}; }; } // namespace ui diff --git a/romfs/Text/ENUS.json b/romfs/Text/ENUS.json index df529fc..c57e2b0 100644 --- a/romfs/Text/ENUS.json +++ b/romfs/Text/ENUS.json @@ -19,7 +19,9 @@ "8: Error writing save meta data file!", "9: Error downloading file!", "10: Error uploading file!", - "11: Error processing save data meta!" + "11: Error processing save data meta!", + "12: Error creating target directory!", + "13: Backup must be a zip to upload!" ], "BackupMenuStatus": [ "0: Processing save data meta file..." @@ -85,7 +87,7 @@ ], "SaveCreatePops": [ "0: Save data created for #%s#!", - "1: Error creating save data!", + "1: Error creating save data for #%s#!", "2: Error deleting save data!" ], "SaveDataTypes": [ @@ -103,20 +105,21 @@ "2: Includes device, or shared saves, with users.", "3: Creates a backup automatically when restoring another.", "4: Auto-names backups and skips the keyboard.", - "5: Automatically uploads backups to Google Drive or WebDav and deletes them locally.", - "6: Whether or not holding [A] for three seconds is required to delete backups.", - "7: Whether or not holding [A] for three seconds is required to restore backups.", - "8: Whether or not holding [A] for three seconds is required to overwrite backups.", - "9: Only shows save data JKSV can successfully open.", - "10: Shows system saves that have an account ID tied to them.", - "11: Enables restoring system saves and writing to NAND partitions.", - "12: Exports save data to ZIP archives instead of unpacked folders.", - "13: Compression or deflate level used when writing to ZIP. The default value is 6. Lower values are faster, but offer less compression and space savings. Zero is store, or no compression.", - "14: Controls the way titles are sorted and displayed.", - "15: Displays titles as text menus like the original JKSM on 3DS instead of icon grids.", - "16: Forces English to be used as the language instead of the detected system language.", - "17: Moves deleted backups to the _TRASH_ folder instead of permanently deleting them.", - "18: Sets the speed at which transitions and animations occur. Lower is faster." + "5: Automatically uploads backups to remote and deletes them locally.", + "6: Always uses Application IDs for export folder names.", + "7: Whether or not holding [A] for three seconds is required to delete backups.", + "8: Whether or not holding [A] for three seconds is required to restore backups.", + "9: Whether or not holding [A] for three seconds is required to overwrite backups.", + "10: Only shows save data JKSV can successfully open.", + "11: Shows system saves that have an account ID tied to them.", + "12: Enables restoring system saves and writing to NAND partitions.", + "13: Exports save data to ZIP archives instead of unpacked folders.", + "14: Compression or deflate level used when writing to ZIP. The default value is 6. Lower values are faster, but offer less compression and space savings. Zero is store, or no compression.", + "15: Controls the way titles are sorted and displayed.", + "16: Displays titles as text menus like the original JKSM on 3DS instead of icon grids.", + "17: Forces English to be used as the language instead of the detected system language.", + "18: Moves deleted backups to the _TRASH_ folder instead of permanently deleting them.", + "19: Sets the speed at which transitions and animations occur. Lower is faster." ], "SettingsMenu": [ "0: Set JKSV output folder.", @@ -125,19 +128,20 @@ "3: Auto-backup on restore: %s", "4: Auto-name backups: %s", "5: Auto-upload backups to remote storage: %s", - "6: Hold to delete backups: %s", - "7: Hold to restore backups: %s", - "8: Hold to overwrite backups: %s", - "9: Only list mountable titles: %s", - "10: Show account system saves: %s", - "11: Enable writing to system saves and NAND: %s", - "12: Export saves to ZIP: %s", - "13: Zip compression level: %u", - "14: Title sort type: %s", - "15: Text menu (JKSM) mode: %s", - "16: Force English: %s", - "17: Enable trash bin: %s", - "18: Animation scaling: %.02f" + "6: Always use Application IDs: %s", + "7: Hold to delete backups: %s", + "8: Hold to restore backups: %s", + "9: Hold to overwrite backups: %s", + "10: Only list mountable titles: %s", + "11: Show account system saves: %s", + "12: Enable writing to system saves and NAND: %s", + "13: Export saves to ZIP: %s", + "14: Zip compression level: %u", + "15: Title sort type: %s", + "16: Text menu (JKSM) mode: %s", + "17: Force English: %s", + "18: Enable trash bin: %s", + "19: Animation scaling: %.02f" ], "SortTypes": [ "0: Alphabetically", diff --git a/source/JKSV.cpp b/source/JKSV.cpp index becddac..9ff7836 100644 --- a/source/JKSV.cpp +++ b/source/JKSV.cpp @@ -3,6 +3,7 @@ #include "StateManager.hpp" #include "appstates/FadeInState.hpp" #include "appstates/MainMenuState.hpp" +#include "appstates/TaskState.hpp" #include "colors.hpp" #include "config.hpp" #include "curl/curl.hpp" @@ -15,7 +16,9 @@ #include "strings.hpp" #include "ui/PopMessageManager.hpp" +#include #include +#include // Normally I try to avoid C macros in C++, but this cleans stuff up nicely. #define ABORT_ON_FAILURE(x) \ @@ -24,9 +27,9 @@ namespace { /// @brief Build month. - constexpr uint8_t BUILD_MON = 6; + constexpr uint8_t BUILD_MON = 7; /// @brief Build day. - constexpr uint8_t BUILD_DAY = 13; + constexpr uint8_t BUILD_DAY = 28; /// @brief Year. constexpr uint16_t BUILD_YEAR = 2025; } // namespace diff --git a/source/appstates/BackupMenuState.cpp b/source/appstates/BackupMenuState.cpp index 97e27c5..0cdcc12 100644 --- a/source/appstates/BackupMenuState.cpp +++ b/source/appstates/BackupMenuState.cpp @@ -22,25 +22,18 @@ namespace { - /// @brief This is the length allotted for naming backups. - constexpr size_t SIZE_NAME_LENGTH = 0x80; - - /// @brief This is just so there isn't random .zip comparisons everywhere. - constexpr const char *STRING_ZIP_EXT = ".zip"; - // These make some things cleaner and easier to type. using TaskConfirm = ConfirmState; using ProgressConfirm = ConfirmState; } // namespace BackupMenuState::BackupMenuState(data::User *user, data::TitleInfo *titleInfo) - : m_user(user) - , m_titleInfo(titleInfo) - , m_saveType(m_user->get_account_save_type()) - , m_directoryPath(config::get_working_directory() / m_titleInfo->get_path_safe_title()) - , m_directoryListing(m_directoryPath) - , m_dataStruct(std::make_shared()) - , m_controlGuide(strings::get_by_name(strings::names::CONTROL_GUIDES, 2)) + : m_user{user} + , m_titleInfo{titleInfo} + , m_saveType{m_user->get_account_save_type()} + , m_directoryPath{config::get_working_directory() / m_titleInfo->get_path_safe_title()} + , m_dataStruct{std::make_shared()} + , m_controlGuide{strings::get_by_name(strings::names::CONTROL_GUIDES, 2)} { BackupMenuState::initialize_static_members(); BackupMenuState::ensure_target_directory(); @@ -120,11 +113,12 @@ void BackupMenuState::refresh() remote::Storage *remote = remote::get_remote_storage(); m_directoryListing.open(m_directoryPath); if (!autoUpload && !m_directoryListing) { return; } + const char *optionNew = strings::get_by_name(strings::names::BACKUPMENU_MENU, 0); sm_backupMenu->reset(); m_menuEntries.clear(); - sm_backupMenu->add_option(strings::get_by_name(strings::names::BACKUPMENU_MENU, 0)); + sm_backupMenu->add_option(optionNew); m_menuEntries.push_back({MenuEntryType::Null, 0}); if (remote && remote->is_initialized()) @@ -136,6 +130,7 @@ void BackupMenuState::refresh() { const std::string_view name = item->get_name(); const std::string option = stringutil::get_formatted_string("%s %s", prefix.data(), name.data()); + sm_backupMenu->add_option(option); m_menuEntries.push_back({MenuEntryType::Remote, index++}); } @@ -167,14 +162,19 @@ void BackupMenuState::initialize_static_members() void BackupMenuState::ensure_target_directory() { + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popFailed = strings::get_by_name(strings::names::BACKUPMENU_POPS, 12); + // If this is enabled, don't bother. const bool autoUpload = config::get_by_key(config::keys::AUTO_UPLOAD); const bool directoryNeeded = !autoUpload && !fslib::directory_exists(m_directoryPath); - const bool directoryCreate = directoryNeeded && fslib::create_directory(m_directoryPath); + const bool directoryFailed = directoryNeeded && error::fslib(fslib::create_directory(m_directoryPath)); + if (!autoUpload && directoryFailed) { ui::PopMessageManager::push_message(popTicks, popFailed); } } void BackupMenuState::initialize_task_data() { + // The other members are set upon actions being taken. m_dataStruct->user = m_user; m_dataStruct->titleInfo = m_titleInfo; m_dataStruct->spawningState = this; @@ -185,19 +185,21 @@ void BackupMenuState::initialize_info_string() const char *nickname = m_user->get_nickname(); const char *title = m_titleInfo->get_title(); const std::string infoString = stringutil::get_formatted_string("`%s` - %s", nickname, title); - m_titleScroll.create(infoString, 22, sm_panelWidth, 8, true, colors::WHITE); + + m_titleScroll.create(infoString, 8, 8, sm_panelWidth - 16, 30, 22, colors::WHITE, colors::TRANSPARENT); + // m_titleScroll.create(infoString, 22, sm_panelWidth - 16, 8, 8, true, colors::WHITE, colors::TRANSPARENT); } void BackupMenuState::save_data_check() { fslib::Directory saveRoot{fs::DEFAULT_SAVE_ROOT}; - m_saveHasData = saveRoot.get_count() > 0; + m_saveHasData = saveRoot.is_open() && saveRoot.get_count() > 0; } void BackupMenuState::initialize_remote_storage() { remote::Storage *remote = remote::get_remote_storage(); - if (error::is_null(remote) || !remote->is_initialized()) { return; } + if (error::is_null(remote)) { return; } const bool supportsUtf8 = remote->supports_utf8(); const std::string_view remoteTitle = supportsUtf8 ? m_titleInfo->get_title() : m_titleInfo->get_path_safe_title(); @@ -213,22 +215,32 @@ void BackupMenuState::initialize_remote_storage() void BackupMenuState::name_and_create_backup() { - const bool autoName = config::get_by_key(config::keys::AUTO_NAME_BACKUPS); - const bool autoUpload = config::get_by_key(config::keys::AUTO_UPLOAD); - const bool exportZip = config::get_by_key(config::keys::EXPORT_TO_ZIP); - const bool zrHeld = input::button_held(HidNpadButton_ZR); - const char *keyboardHeader = strings::get_by_name(strings::names::KEYBOARD, 0); - const bool autoNamed = (autoName || zrHeld); // This can be eval'd here. + static constexpr size_t SIZE_NAME_LENGTH = 0x80; + static constexpr const char *STRING_ZIP_EXT = ".zip"; + + remote::Storage *remote = remote::get_remote_storage(); + const bool autoName = config::get_by_key(config::keys::AUTO_NAME_BACKUPS); + const bool autoUpload = config::get_by_key(config::keys::AUTO_UPLOAD); + const bool exportZip = autoUpload || config::get_by_key(config::keys::EXPORT_TO_ZIP); + const bool zrHeld = input::button_held(HidNpadButton_ZR); + const char *keyboardHeader = strings::get_by_name(strings::names::KEYBOARD, 0); + const bool autoNamed = (autoName || zrHeld); // This can be eval'd here. + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popErrorCreating = strings::get_by_name(strings::names::BACKUPMENU_POPS, 5); + char name[SIZE_NAME_LENGTH + 1] = {0}; - std::snprintf(name, SIZE_NAME_LENGTH, "%s - %s", m_user->get_path_safe_nickname(), stringutil::get_date_string().c_str()); - + { + const char *nickname = m_user->get_path_safe_nickname(); + const std::string date = stringutil::get_date_string(); + std::snprintf(name, SIZE_NAME_LENGTH, "%s - %s", nickname, date.c_str()); + } const bool named = autoNamed || keyboard::get_input(SwkbdType_QWERTY, name, keyboardHeader, name, SIZE_NAME_LENGTH); if (!named) { return; } const bool hasZipExt = std::strstr(name, STRING_ZIP_EXT); // This might not be the best check. std::shared_ptr backupTask{}; - if (autoUpload) + if (autoUpload && remote) { if (!hasZipExt) { std::strncat(name, STRING_ZIP_EXT, SIZE_NAME_LENGTH); } backupTask = std::make_shared(tasks::backup::create_new_backup_remote, @@ -241,11 +253,14 @@ void BackupMenuState::name_and_create_backup() else { fslib::Path target{m_directoryPath / name}; + const bool dirNeeded = !exportZip && !fslib::directory_exists(target); + const bool dirError = dirNeeded && error::fslib(fslib::create_directory(target)); + if (exportZip && !hasZipExt) { target += STRING_ZIP_EXT; } - else if (!exportZip && !hasZipExt) + else if (!exportZip && dirNeeded && dirError) { - const bool targetExists = fslib::directory_exists(target); - const bool targetCreated = !targetExists && fslib::create_directory(target); + ui::PopMessageManager::push_message(popTicks, popErrorCreating); + return; } backupTask = @@ -256,24 +271,24 @@ void BackupMenuState::name_and_create_backup() void BackupMenuState::confirm_overwrite() { - remote::Storage *remote = remote::get_remote_storage(); const int selected = sm_backupMenu->get_selected(); const MenuEntry &entry = m_menuEntries.at(selected); const bool holdRequired = config::get_by_key(config::keys::HOLD_FOR_OVERWRITE); const char *confirmTemplate = strings::get_by_name(strings::names::BACKUPMENU_CONFS, 0); std::shared_ptr confirm{}; - if (entry.type == MenuEntryType::Remote && remote && remote->is_initialized()) + if (entry.type == MenuEntryType::Remote) { m_dataStruct->remoteItem = m_remoteListing.at(entry.index); - const std::string query = - stringutil::get_formatted_string(confirmTemplate, m_dataStruct->remoteItem->get_name().data()); + const char *itemName = m_dataStruct->remoteItem->get_name().data(); + const std::string query = stringutil::get_formatted_string(confirmTemplate, itemName); confirm = std::make_shared(query, holdRequired, tasks::backup::overwrite_backup_remote, m_dataStruct); } else if (entry.type == MenuEntryType::Local) { - const std::string query = stringutil::get_formatted_string(confirmTemplate, m_directoryListing[entry.index]); m_dataStruct->path = m_directoryPath / m_directoryListing[entry.index]; + const char *targetName = m_directoryListing[entry.index]; + const std::string query = stringutil::get_formatted_string(confirmTemplate, targetName); confirm = std::make_shared(query, holdRequired, tasks::backup::overwrite_backup_local, m_dataStruct); } @@ -352,13 +367,25 @@ void BackupMenuState::confirm_delete() void BackupMenuState::upload_backup() { + remote::Storage *remote = remote::get_remote_storage(); + if (error::is_null(remote)) { return; } + const int selected = sm_backupMenu->get_selected(); const MenuEntry &entry = m_menuEntries.at(selected); + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popNotZip = strings::get_by_name(strings::names::BACKUPMENU_POPS, 13); if (entry.type != BackupMenuState::MenuEntryType::Local) { return; } - m_dataStruct->path = m_directoryPath / m_directoryListing[entry.index]; + fslib::Path target = m_directoryPath / m_directoryListing[entry.index]; + const bool isDir = fslib::directory_exists(target); + if (isDir) + { + ui::PopMessageManager::push_message(popTicks, popNotZip); + return; + } - auto upload = std::make_shared(tasks::backup::upload_backup, m_dataStruct); + m_dataStruct->path = std::move(target); + auto upload = std::make_shared(tasks::backup::upload_backup, m_dataStruct); StateManager::push_state(upload); } diff --git a/source/appstates/BaseState.cpp b/source/appstates/BaseState.cpp index f2ead48..594ad7a 100644 --- a/source/appstates/BaseState.cpp +++ b/source/appstates/BaseState.cpp @@ -3,7 +3,7 @@ #include BaseState::BaseState(bool isClosable) - : m_isClosable(isClosable) + : m_isClosable{isClosable} { if (m_isClosable) { return; } appletBeginBlockingHomeButton(0); diff --git a/source/appstates/BaseTask.cpp b/source/appstates/BaseTask.cpp index 04cde67..9d443a2 100644 --- a/source/appstates/BaseTask.cpp +++ b/source/appstates/BaseTask.cpp @@ -1,6 +1,9 @@ #include "appstates/BaseTask.hpp" #include "colors.hpp" +#include "input.hpp" +#include "strings.hpp" +#include "ui/PopMessageManager.hpp" namespace { @@ -9,23 +12,28 @@ namespace } // namespace BaseTask::BaseTask() - : BaseState(false) + : BaseState{false} + , m_popUnableExit{strings::get_by_name(strings::names::GENERAL_POPS, 0)} { m_frameTimer.start(TICKS_GLYPH_TRIGGER); } void BaseTask::update() { + const bool plusPressed = input::button_pressed(HidNpadButton_Plus); + if (!m_task->is_running()) { BaseState::deactivate(); return; } + else if (plusPressed) { ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_TICKS, m_popUnableExit); } + + m_colorMod.update(); // Just bail if the timer wasn't triggered yet. if (!m_frameTimer.is_triggered()) { return; } if (++m_currentFrame >= 8) { m_currentFrame = 0; } - m_colorMod.update(); } void BaseTask::render_loading_glyph() diff --git a/source/appstates/ExtrasMenuState.cpp b/source/appstates/ExtrasMenuState.cpp index ab1b8f7..cbadf4d 100644 --- a/source/appstates/ExtrasMenuState.cpp +++ b/source/appstates/ExtrasMenuState.cpp @@ -29,11 +29,11 @@ namespace } // namespace ExtrasMenuState::ExtrasMenuState() - : m_extrasMenu(32, 8, 1000, 24, 555) - , m_renderTarget(sdl::TextureManager::create_load_texture(SECONDARY_TARGET, + : m_extrasMenu{32, 8, 1000, 24, 555} + , m_renderTarget{sdl::TextureManager::create_load_texture(SECONDARY_TARGET, 1080, 555, - SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)) + SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)} { ExtrasMenuState::initialize_menu(); } diff --git a/source/appstates/FadeInState.cpp b/source/appstates/FadeInState.cpp index 28c98ba..f94461a 100644 --- a/source/appstates/FadeInState.cpp +++ b/source/appstates/FadeInState.cpp @@ -4,7 +4,7 @@ #include "sdl.hpp" FadeInState::FadeInState(std::shared_ptr nextState) - : m_nextState(nextState) + : m_nextState{nextState} { m_fadeTimer.start(1); } diff --git a/source/appstates/MainMenuState.cpp b/source/appstates/MainMenuState.cpp index 795840b..2812b8c 100644 --- a/source/appstates/MainMenuState.cpp +++ b/source/appstates/MainMenuState.cpp @@ -15,16 +15,16 @@ #include "strings.hpp" MainMenuState::MainMenuState() - : m_renderTarget(sdl::TextureManager::create_load_texture("mainMenuTarget", + : m_renderTarget{sdl::TextureManager::create_load_texture("mainMenuTarget", 200, 555, - SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)) - , m_background(sdl::TextureManager::create_load_texture("mainBackground", "romfs:/Textures/MenuBackground.png")) - , m_settingsIcon(sdl::TextureManager::create_load_texture("settingsIcon", "romfs:/Textures/SettingsIcon.png")) - , m_extrasIcon(sdl::TextureManager::create_load_texture("extrasIcon", "romfs:/Textures/ExtrasIcon.png")) - , m_mainMenu(50, 15, 555) - , m_controlGuide(strings::get_by_name(strings::names::CONTROL_GUIDES, 0)) - , m_controlGuideX(1220 - sdl::text::get_width(22, m_controlGuide)) + SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)} + , m_background{sdl::TextureManager::create_load_texture("mainBackground", "romfs:/Textures/MenuBackground.png")} + , m_settingsIcon{sdl::TextureManager::create_load_texture("settingsIcon", "romfs:/Textures/SettingsIcon.png")} + , m_extrasIcon{sdl::TextureManager::create_load_texture("extrasIcon", "romfs:/Textures/ExtrasIcon.png")} + , m_mainMenu{50, 15, 555} + , m_controlGuide{strings::get_by_name(strings::names::CONTROL_GUIDES, 0)} + , m_controlGuideX{static_cast(1220 - sdl::text::get_width(22, m_controlGuide))} { MainMenuState::initialize_settings_extras(); MainMenuState::initialize_menu(); diff --git a/source/appstates/SaveCreateState.cpp b/source/appstates/SaveCreateState.cpp index 41ed25c..14cb4ee 100644 --- a/source/appstates/SaveCreateState.cpp +++ b/source/appstates/SaveCreateState.cpp @@ -27,9 +27,9 @@ static void create_save_data(sys::Task *task, static bool compare_info(data::TitleInfo *infoA, data::TitleInfo *infoB); SaveCreateState::SaveCreateState(data::User *user, TitleSelectCommon *titleSelect) - : m_user(user) - , m_titleSelect(titleSelect) - , m_saveMenu(8, 8, 624, 22, 720) + : m_user{user} + , m_titleSelect{titleSelect} + , m_saveMenu{8, 8, 624, 22, 720} { // If the panel is null, create it. if (!sm_slidePanel) @@ -51,14 +51,11 @@ void SaveCreateState::update() { const bool hasFocus = BaseState::has_focus(); - if (m_refreshRequired) + if (m_refreshRequired.load()) { - // There's no other way to get the save info so... m_user->load_user_data(); - // Refresh the view. m_titleSelect->refresh(); - // No more refresh needed. - m_refreshRequired = false; + m_refreshRequired.store(false); } m_saveMenu.update(hasFocus); @@ -87,7 +84,7 @@ void SaveCreateState::render() sm_slidePanel->render(NULL, hasFocus); } -void SaveCreateState::data_and_view_refresh_required() { m_refreshRequired = true; } +void SaveCreateState::data_and_view_refresh_required() { m_refreshRequired.store(true); } static void create_save_data(sys::Task *task, data::User *targetUser, @@ -98,23 +95,24 @@ static void create_save_data(sys::Task *task, const char *statusTemplate = strings::get_by_name(strings::names::USEROPTION_STATUS, 0); const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; - const char *popSuccess = strings::get_by_name(strings::names::SAVECREATE_POPS, 8); - const char *popFailed = strings::get_by_name(strings::names::SAVECREATE_POPS, 9); + const char *popSuccess = strings::get_by_name(strings::names::SAVECREATE_POPS, 0); + const char *popFailed = strings::get_by_name(strings::names::SAVECREATE_POPS, 1); { const std::string status = stringutil::get_formatted_string(statusTemplate, titleInfo->get_title()); task->set_status(status); } - if (fs::create_save_data_for(targetUser, titleInfo)) + const bool created = fs::create_save_data_for(targetUser, titleInfo); + if (created) { - const std::string popMessage = stringutil::get_formatted_string(popSuccess, titleInfo->get_title()); + const char *title = titleInfo->get_title(); + std::string popMessage = stringutil::get_formatted_string(popSuccess, title); ui::PopMessageManager::push_message(popTicks, popMessage); } else { ui::PopMessageManager::push_message(popTicks, popFailed); } spawningState->data_and_view_refresh_required(); - task->finished(); } diff --git a/source/appstates/SettingsState.cpp b/source/appstates/SettingsState.cpp index ae993d5..cc7f4a1 100644 --- a/source/appstates/SettingsState.cpp +++ b/source/appstates/SettingsState.cpp @@ -18,13 +18,16 @@ namespace // All of these states share the same render target. constexpr std::string_view SECONDARY_TARGET = "SecondaryTarget"; + constexpr std::string_view CONFIG_KEY_NULL = "NULL"; + // This is needed to be able to get and set keys by index. Anything "NULL" isn't a key that can be easily toggled. - constexpr std::array CONFIG_KEY_ARRAY = {"NULL", - "NULL", + constexpr std::array CONFIG_KEY_ARRAY = {CONFIG_KEY_NULL, + CONFIG_KEY_NULL, config::keys::INCLUDE_DEVICE_SAVES, config::keys::AUTO_BACKUP_ON_RESTORE, config::keys::AUTO_NAME_BACKUPS, config::keys::AUTO_UPLOAD, + config::keys::USE_TITLE_IDS, config::keys::HOLD_FOR_DELETION, config::keys::HOLD_FOR_RESTORATION, config::keys::HOLD_FOR_OVERWRITE, @@ -37,29 +40,20 @@ namespace config::keys::JKSM_TEXT_MODE, config::keys::FORCE_ENGLISH, config::keys::ENABLE_TRASH_BIN, - "NULL"}; + CONFIG_KEY_NULL}; } // namespace SettingsState::SettingsState() - : m_settingsMenu(32, 8, 1000, 24, 555) - , m_controlGuide(strings::get_by_name(strings::names::CONTROL_GUIDES, 3)) - , m_controlGuideX(1220 - sdl::text::get_width(22, m_controlGuide)) - , m_renderTarget(sdl::TextureManager::create_load_texture(SECONDARY_TARGET, + : m_settingsMenu{32, 8, 1000, 24, 555} + , m_controlGuide{strings::get_by_name(strings::names::CONTROL_GUIDES, 3)} + , m_controlGuideX{static_cast(1220 - sdl::text::get_width(22, m_controlGuide))} + , m_renderTarget{sdl::TextureManager::create_load_texture(SECONDARY_TARGET, 1080, 555, - SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)) + SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)} { - - for (int i = 0; const char *setting = strings::get_by_name(strings::names::SETTINGS_MENU, i); i++) - { - m_settingsMenu.add_option(setting); - } - - for (int i = 0; const char *onOff = strings::get_by_name(strings::names::ON_OFF, i); i++) { m_onOff[i] = onOff; } - for (int i = 0; const char *sortType = strings::get_by_name(strings::names::SORT_TYPES, i); i++) - { - m_sortTypes[i] = sortType; - } + SettingsState::load_settings_menu(); + SettingsState::load_extra_strings(); SettingsState::update_menu_options(); } @@ -85,9 +79,26 @@ void SettingsState::render() if (hasFocus) { sdl::text::render(NULL, m_controlGuideX, 673, 22, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_controlGuide); } } +void SettingsState::load_settings_menu() +{ + for (int i = 0; const char *option = strings::get_by_name(strings::names::SETTINGS_MENU, i); i++) + { + m_settingsMenu.add_option(option); + } +} + +void SettingsState::load_extra_strings() +{ + for (int i = 0; const char *onOff = strings::get_by_name(strings::names::ON_OFF, i); i++) { m_onOff[i] = onOff; } + for (int i = 0; const char *sortType = strings::get_by_name(strings::names::SORT_TYPES, i); i++) + { + m_sortTypes[i] = sortType; + } +} + void SettingsState::update_menu_options() { - for (int i : {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16, 17}) + for (int i : {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 16, 17, 18}) { const char *optionTemplate = strings::get_by_name(strings::names::SETTINGS_MENU, i); const uint8_t value = config::get_by_key(CONFIG_KEY_ARRAY[i]); @@ -97,25 +108,25 @@ void SettingsState::update_menu_options() } { - const char *zipCompTemplate = strings::get_by_name(strings::names::SETTINGS_MENU, 13); - const uint8_t zipLevel = config::get_by_key(CONFIG_KEY_ARRAY[13]); + const char *zipCompTemplate = strings::get_by_name(strings::names::SETTINGS_MENU, 14); + const uint8_t zipLevel = config::get_by_key(CONFIG_KEY_ARRAY[14]); const std::string zipOption = stringutil::get_formatted_string(zipCompTemplate, zipLevel); - m_settingsMenu.edit_option(13, zipOption); + m_settingsMenu.edit_option(14, zipOption); } { - const char *titleSortTemplate = strings::get_by_name(strings::names::SETTINGS_MENU, 14); - const uint8_t sortType = config::get_by_key(CONFIG_KEY_ARRAY[14]); + const char *titleSortTemplate = strings::get_by_name(strings::names::SETTINGS_MENU, 15); + const uint8_t sortType = config::get_by_key(CONFIG_KEY_ARRAY[15]); const char *typeText = SettingsState::get_sort_type_text(sortType); const std::string sortTypeOption = stringutil::get_formatted_string(titleSortTemplate, typeText); - m_settingsMenu.edit_option(14, sortTypeOption); + m_settingsMenu.edit_option(15, sortTypeOption); } { - const char *scalingTemplate = strings::get_by_name(strings::names::SETTINGS_MENU, 18); + const char *scalingTemplate = strings::get_by_name(strings::names::SETTINGS_MENU, 19); const double scaling = config::get_animation_scaling(); const std::string scalingOption = stringutil::get_formatted_string(scalingTemplate, scaling); - m_settingsMenu.edit_option(18, scalingOption); + m_settingsMenu.edit_option(19, scalingOption); } } @@ -124,10 +135,10 @@ void SettingsState::toggle_options() const int selected = m_settingsMenu.get_selected(); switch (selected) { - case 13: SettingsState::cycle_zip_level(); break; - case 14: SettingsState::cycle_sort_type(); break; - case 15: SettingsState::toggle_jksm_mode(); break; - case 18: SettingsState::cycle_anim_scaling(); break; + case 14: SettingsState::cycle_zip_level(); break; + case 15: SettingsState::cycle_sort_type(); break; + case 16: SettingsState::toggle_jksm_mode(); break; + case 19: SettingsState::cycle_anim_scaling(); break; default: config::toggle_by_key(CONFIG_KEY_ARRAY[selected]); } config::save(); diff --git a/source/appstates/TextTitleSelectState.cpp b/source/appstates/TextTitleSelectState.cpp index b2c12f3..1f93647 100644 --- a/source/appstates/TextTitleSelectState.cpp +++ b/source/appstates/TextTitleSelectState.cpp @@ -21,13 +21,13 @@ namespace } // namespace TextTitleSelectState::TextTitleSelectState(data::User *user) - : TitleSelectCommon() - , m_user(user) - , m_titleSelectMenu(32, 8, 1000, 20, 555) - , m_renderTarget(sdl::TextureManager::create_load_texture(SECONDARY_TARGET, + : TitleSelectCommon{} + , m_user{user} + , m_titleSelectMenu{32, 8, 1000, 20, 555} + , m_renderTarget{sdl::TextureManager::create_load_texture(SECONDARY_TARGET, 1080, 555, - SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)) + SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET)} { TextTitleSelectState::refresh(); } diff --git a/source/appstates/TitleInfoState.cpp b/source/appstates/TitleInfoState.cpp index 2d756e6..e675a1d 100644 --- a/source/appstates/TitleInfoState.cpp +++ b/source/appstates/TitleInfoState.cpp @@ -1,6 +1,7 @@ #include "appstates/TitleInfoState.hpp" #include "colors.hpp" +#include "error.hpp" #include "input.hpp" #include "sdl.hpp" #include "strings.hpp" @@ -24,71 +25,12 @@ namespace } // namespace TitleInfoState::TitleInfoState(data::User *user, data::TitleInfo *titleInfo) - : m_user(user) - , m_titleInfo(titleInfo) - , m_icon(m_titleInfo->get_icon()) + : m_user{user} + , m_titleInfo{titleInfo} + , m_icon{m_titleInfo->get_icon()} { - // This needs to be checked. All title information panels share these members. - if (!sm_initialized) - { - // This is the slide panel everything is rendered to. - sm_slidePanel = std::make_unique(SIZE_PANEL_WIDTH, ui::SlideOutPanel::Side::Right); - - // This is the render target for the title. - sm_titleTarget = sdl::TextureManager::create_load_texture("infoTitleTarget", - SIZE_PANEL_WIDTH - SIZE_PANEL_SUB, - SIZE_TEXT_TARGET_HEIGHT, - SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); - - // This is the render target for the publisher. - sm_publisherTarget = sdl::TextureManager::create_load_texture("infoPublisherTarget", - SIZE_PANEL_WIDTH - SIZE_PANEL_SUB, - SIZE_TEXT_TARGET_HEIGHT, - SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); - sm_initialized = true; - } - - // Do this instead of calling the function everytime. - uint64_t applicationID = m_titleInfo->get_application_id(); - - // These is needed at some point. - FsSaveDataInfo *saveInfo = m_user->get_save_info_by_id(applicationID); - PdmPlayStatistics *playStats = m_user->get_play_stats_by_id(applicationID); - - // Title text. - m_titleScroll.create(m_titleInfo->get_title(), SIZE_FONT, SIZE_PANEL_WIDTH - SIZE_PANEL_SUB, 6, false, colors::WHITE); - // Publisher. - m_publisherScroll - .create(m_titleInfo->get_publisher(), SIZE_FONT, SIZE_PANEL_WIDTH - SIZE_PANEL_SUB, 6, false, colors::WHITE); - - // Grab the application ID string. - m_applicationID = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLEINFO, 0), applicationID); - // Same here. I think these use lower case characters on the NAND? - m_saveDataID = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLEINFO, 1), saveInfo->save_data_id); - // This is simple. - m_totalLaunches = - stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLEINFO, 5), playStats->total_launches); - // This should be semi-simple. - m_saveDataType = - stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLEINFO, 6), - strings::get_by_name(strings::names::SAVE_DATA_TYPES, saveInfo->save_data_type)); - - // Going to "cheat" with the next two. This works, so screw it. - char playBuffer[0x40] = {0}; - std::tm *firstPlayed = std::localtime(reinterpret_cast(&playStats->first_timestamp_user)); - std::strftime(playBuffer, 0x40, strings::get_by_name(strings::names::TITLEINFO, 2), firstPlayed); - m_firstPlayed.assign(playBuffer); - - std::tm *lastPlayed = std::localtime(reinterpret_cast(&playStats->last_timestamp_user)); - std::strftime(playBuffer, 0x40, strings::get_by_name(strings::names::TITLEINFO, 3), lastPlayed); - m_lastPlayed.assign(playBuffer); - - // Calculate play time. We're going to use non-floating point to truncate the remainders. - int64_t seconds = playStats->playtime / static_cast(1e+9); - int64_t hours = seconds / 3600; - int64_t minutes = (seconds % 3600) / 60; - seconds %= 60; - m_playTime = stringutil::get_formatted_string(strings::get_by_name(strings::names::TITLEINFO, 4), hours, minutes, seconds); + TitleInfoState::initialize_static_members(); + TitleInfoState::create_info_fields(); } TitleInfoState::~TitleInfoState() @@ -105,10 +47,6 @@ void TitleInfoState::update() // Update slide panel. sm_slidePanel->update(hasFocus); - // Update the scrolling text. - m_titleScroll.update(hasFocus); - m_publisherScroll.update(hasFocus); - if (input::button_pressed(HidNpadButton_B)) { sm_slidePanel->close(); } else if (sm_slidePanel->is_closed()) { @@ -119,67 +57,99 @@ void TitleInfoState::update() void TitleInfoState::render() { - // This is the vertical gap for rendering text. - static constexpr int SIZE_VERT_GAP = SIZE_TEXT_TARGET_HEIGHT + 4; - // This is the size to render the rectangles at. - static constexpr int SIZE_RECT_WIDTH = SIZE_PANEL_WIDTH - SIZE_PANEL_SUB; - - // This is whether or not the state currently has focus. - bool hasFocus = BaseState::has_focus(); - - // Clear the title and publisher targets. - sm_titleTarget->clear(colors::DIALOG_BOX); - sm_publisherTarget->clear(colors::CLEAR_COLOR); - - // Grab the panel's target and clear it. To do: This how I originally intended to. + const bool hasFocus = BaseState::has_focus(); sm_slidePanel->clear_target(); - - // Grab the panel target for rendering to. SDL_Texture *panelTarget = sm_slidePanel->get_target(); - // This is our Y rendering coordinate. It's easier to adjust this as needed than change everything. - // This starts under the icon. - int y = 280; - - // Start by rendering the icon. m_icon->render_stretched(panelTarget, 88, 8, 304, 304); - // Render the textscroll to the target, then target to the panel target. - m_titleScroll.render(sm_titleTarget->get(), hasFocus); - sm_titleTarget->render(panelTarget, 8, (y += SIZE_VERT_GAP)); - - // Publisher. - m_publisherScroll.render(sm_publisherTarget->get(), hasFocus); - sm_publisherTarget->render(panelTarget, 8, (y += SIZE_VERT_GAP)); - - // Application ID. - sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::DIALOG_BOX); - sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_applicationID.c_str()); - - // Save data ID. - sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::CLEAR_COLOR); - // Text needs to be aligned like the scrolling text. - sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_saveDataID.c_str()); - - // First played. - sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::DIALOG_BOX); - sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_firstPlayed.c_str()); - - // Last played. - sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::CLEAR_COLOR); - sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_lastPlayed.c_str()); - - // Play time. - sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::DIALOG_BOX); - sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_playTime.c_str()); - - // Total launches. - sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::CLEAR_COLOR); - sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_totalLaunches.c_str()); - - // Save data type. - sdl::render_rect_fill(panelTarget, 8, (y += SIZE_VERT_GAP), SIZE_RECT_WIDTH, SIZE_TEXT_TARGET_HEIGHT, colors::DIALOG_BOX); - sdl::text::render(panelTarget, 16, y + 6, SIZE_FONT, sdl::text::NO_TEXT_WRAP, colors::WHITE, m_saveDataType.c_str()); - sm_slidePanel->render(NULL, hasFocus); } + +void TitleInfoState::initialize_static_members() +{ + if (!sm_slidePanel) + { + sm_slidePanel = std::make_unique(SIZE_PANEL_WIDTH, ui::SlideOutPanel::Side::Right); + } +} + +void TitleInfoState::create_info_fields() +{ + static constexpr int SIZE_VERT_GAP = SIZE_TEXT_TARGET_HEIGHT + 4; + static constexpr int COORD_INIT_Y = 278; + + const uint64_t applicationID = m_titleInfo->get_application_id(); + const FsSaveDataInfo *saveInfo = m_user->get_save_info_by_id(applicationID); + const PdmPlayStatistics *playStats = m_user->get_play_stats_by_id(applicationID); + if (error::is_null(saveInfo) || error::is_null(playStats)) { return; } + + const char *appIDFormat = strings::get_by_name(strings::names::TITLEINFO, 0); + const char *saveIDFormat = strings::get_by_name(strings::names::TITLEINFO, 1); + const char *firstPlayedFormat = strings::get_by_name(strings::names::TITLEINFO, 2); + const char *lastPlayedFormat = strings::get_by_name(strings::names::TITLEINFO, 3); + const char *playTimeFormat = strings::get_by_name(strings::names::TITLEINFO, 4); + const char *launchesFormat = strings::get_by_name(strings::names::TITLEINFO, 5); + const char *saveTypeFormat = strings::get_by_name(strings::names::TITLEINFO, 6); + + // Gonna push this all here and loop it later to make this "easier." + std::vector textVector{}; + textVector.push_back(m_titleInfo->get_title()); + textVector.push_back(m_titleInfo->get_publisher()); + + const std::string appIDString = stringutil::get_formatted_string(appIDFormat, applicationID); + textVector.push_back(appIDString); + + const std::string saveIDString = stringutil::get_formatted_string(saveIDFormat, saveInfo->save_data_id); + textVector.push_back(saveIDString); + + std::array firstPlayed = {0}; + const std::time_t firstTime = static_cast(playStats->first_timestamp_user); + const std::tm *firstLocal = std::localtime(&firstTime); + std::strftime(firstPlayed.data(), 0x40, firstPlayedFormat, firstLocal); + textVector.push_back(firstPlayed.data()); + + std::array lastPlayed = {0}; + const std::time_t lastTime = static_cast(playStats->last_timestamp_user); + const std::tm *lastLocal = std::localtime(&lastTime); + std::strftime(lastPlayed.data(), 0x40, lastPlayedFormat, lastLocal); + textVector.push_back(lastPlayed.data()); + + int64_t seconds = playStats->playtime / 1000000000; + int64_t hours = seconds / 3600; + int64_t minutes = (seconds % 3600) / 60; + seconds %= 60; + const std::string playTimeString = stringutil::get_formatted_string(playTimeFormat, hours, minutes, seconds); + textVector.push_back(playTimeString); + + const std::string launchesString = stringutil::get_formatted_string(launchesFormat, playStats->total_launches); + textVector.push_back(launchesString); + + const char *saveType = strings::get_by_name(strings::names::SAVE_DATA_TYPES, saveInfo->save_data_type); + const std::string saveTypeString = stringutil::get_formatted_string(saveTypeFormat, saveType); + textVector.push_back(saveTypeString); + + // Just loop, I guess. + int y = COORD_INIT_Y; + for (const std::string_view &string : textVector) + { + auto newField = TitleInfoState::create_new_field(string, (y += SIZE_VERT_GAP)); + sm_slidePanel->push_new_element(newField); + } +} + +std::shared_ptr TitleInfoState::create_new_field(std::string_view text, int y) +{ + static constexpr int SIZE_FIELD_WIDTH = SIZE_PANEL_WIDTH - SIZE_PANEL_SUB; + auto textFieldScroll = std::make_shared(text, + 8, + y, + SIZE_FIELD_WIDTH, + SIZE_TEXT_TARGET_HEIGHT, + SIZE_FONT, + colors::WHITE, + m_fieldClear ? colors::CLEAR_COLOR : colors::DIALOG_BOX, + false); + m_fieldClear = m_fieldClear ? false : true; + return textFieldScroll; +} diff --git a/source/appstates/TitleOptionState.cpp b/source/appstates/TitleOptionState.cpp index 154c90c..2e0cccb 100644 --- a/source/appstates/TitleOptionState.cpp +++ b/source/appstates/TitleOptionState.cpp @@ -46,10 +46,10 @@ static void extend_save_data(sys::Task *task, std::shared_ptr()) + : m_user{user} + , m_titleInfo{titleInfo} + , m_titleSelect{titleSelect} + , m_dataStruct{std::make_shared()} { // Create panel if needed. if (!sm_initialized) diff --git a/source/appstates/TitleSelectCommon.cpp b/source/appstates/TitleSelectCommon.cpp index 6c1bf55..1493c55 100644 --- a/source/appstates/TitleSelectCommon.cpp +++ b/source/appstates/TitleSelectCommon.cpp @@ -1,14 +1,15 @@ #include "appstates/TitleSelectCommon.hpp" + #include "colors.hpp" #include "sdl.hpp" #include "strings.hpp" TitleSelectCommon::TitleSelectCommon() { - if (m_titleControlsX == 0) - { - m_titleControlsX = 1220 - sdl::text::get_width(22, strings::get_by_name(strings::names::CONTROL_GUIDES, 1)); - } + if (sm_controlGuide && sm_controlGuideX != 0) { return; } + + sm_controlGuide = strings::get_by_name(strings::names::CONTROL_GUIDES, 1); + sm_controlGuideX = 1220 - sdl::text::get_width(22, sm_controlGuide); } void TitleSelectCommon::render_control_guide() @@ -17,12 +18,6 @@ void TitleSelectCommon::render_control_guide() if (hasFocus) { - sdl::text::render(NULL, - m_titleControlsX, - 673, - 22, - sdl::text::NO_TEXT_WRAP, - colors::WHITE, - strings::get_by_name(strings::names::CONTROL_GUIDES, 1)); + sdl::text::render(NULL, sm_controlGuideX, 673, 22, sdl::text::NO_TEXT_WRAP, colors::WHITE, sm_controlGuide); } } diff --git a/source/appstates/TitleSelectState.cpp b/source/appstates/TitleSelectState.cpp index c5e4487..56b0ac1 100644 --- a/source/appstates/TitleSelectState.cpp +++ b/source/appstates/TitleSelectState.cpp @@ -78,7 +78,6 @@ void TitleSelectState::create_backup_menu() const uint64_t applicationID = m_user->get_application_id_at(selected); const FsSaveDataInfo *saveInfo = m_user->get_save_info_at(selected); data::TitleInfo *titleInfo = data::get_title_info_by_id(applicationID); - const FsSaveDataType saveType = static_cast(saveInfo->save_data_type); const bool saveMounted = fslib::open_save_data_with_save_info(fs::DEFAULT_SAVE_MOUNT, *saveInfo); if (!saveMounted) { return; } diff --git a/source/appstates/UserOptionState.cpp b/source/appstates/UserOptionState.cpp index 858b8e6..5e0abe5 100644 --- a/source/appstates/UserOptionState.cpp +++ b/source/appstates/UserOptionState.cpp @@ -28,6 +28,10 @@ namespace CREATE_ALL_SAVE, DELETE_ALL_SAVE }; + + using TaskConfirm = ConfirmState; + using ProgressConfirm = ConfirmState; + } // namespace // Declarations here. Defintions after class. @@ -39,30 +43,23 @@ static void create_all_save_data_for_user(sys::Task *task, std::shared_ptr dataStruct); UserOptionState::UserOptionState(data::User *user, TitleSelectCommon *titleSelect) - : m_user(user) - , m_titleSelect(titleSelect) - , m_userOptionMenu(8, 8, 460, 22, 720) - , m_dataStruct(std::make_shared()) + : m_user{user} + , m_titleSelect{titleSelect} + , m_userOptionMenu{8, 8, 460, 22, 720} + , m_dataStruct{std::make_shared()} { - // Check if panel needs to be created. It's shared by all instances. - if (!m_menuPanel) { m_menuPanel = std::make_unique(480, ui::SlideOutPanel::Side::Right); } - - int currentStringIndex = 0; - const char *currentString = nullptr; - while ((currentString = strings::get_by_name(strings::names::USEROPTION_MENU, currentStringIndex++)) != nullptr) - { - m_userOptionMenu.add_option(stringutil::get_formatted_string(currentString, m_user->get_nickname())); - } - - // Fill this is. - m_dataStruct->user = m_user; - m_dataStruct->spawningState = this; + UserOptionState::create_menu_panel(); + UserOptionState::load_menu_strings(); + UserOptionState::initialize_data_struct(); } void UserOptionState::update() { - // Update the main panel. - m_menuPanel->update(BaseState::has_focus()); + const bool hasFocus = BaseState::has_focus(); + const bool aPressed = input::button_pressed(HidNpadButton_A); + const bool bPressed = input::button_pressed(HidNpadButton_B); + + sm_menuPanel->update(hasFocus); // See if this needs to be done. if (m_refreshRequired) @@ -72,77 +69,21 @@ void UserOptionState::update() m_refreshRequired = false; } - if (input::button_pressed(HidNpadButton_A) && m_user->get_account_save_type() != FsSaveDataType_System) + if (aPressed) { switch (m_userOptionMenu.get_selected()) { - case BACKUP_ALL: - { - // This is broken down to make it easier to read. - std::string queryString = - stringutil::get_formatted_string(strings::get_by_name(strings::names::USEROPTION_CONFS, 0), - m_user->get_nickname()); - - // State to push - auto confirmBackupAll = - std::make_shared>( - queryString, - false, - backup_all_for_user, - m_dataStruct); - - StateManager::push_state(confirmBackupAll); - } - break; - - case CREATE_SAVE: - { - auto saveCreateState = std::make_shared(m_user, m_titleSelect); - - // This just pushes the state with the menu to select. - StateManager::push_state(std::make_shared(m_user, m_titleSelect)); - } - break; - - case CREATE_ALL_SAVE: - { - std::string queryString = - stringutil::get_formatted_string(strings::get_by_name(strings::names::USEROPTION_CONFS, 1), - m_user->get_nickname()); - - auto confirmCreateAll = std::make_shared>( - queryString, - true, - create_all_save_data_for_user, - m_dataStruct); - - // Done? - StateManager::push_state(confirmCreateAll); - } - break; - - case DELETE_ALL_SAVE: - { - std::string queryString = - stringutil::get_formatted_string(strings::get_by_name(strings::names::USEROPTION_CONFS, 2), - m_user->get_nickname()); - - auto confirmDeleteAll = std::make_shared>( - queryString, - true, - delete_all_save_data_for_user, - m_dataStruct); - - StateManager::push_state(confirmDeleteAll); - } - break; + case BACKUP_ALL: UserOptionState::backup_all(); break; + case CREATE_SAVE: UserOptionState::create_save_create(); break; + case CREATE_ALL_SAVE: UserOptionState::create_all_save_data(); break; + case DELETE_ALL_SAVE: UserOptionState::delete_all_save_data(); break; } } - else if (input::button_pressed(HidNpadButton_B)) { m_menuPanel->close(); } - else if (m_menuPanel->is_closed()) + else if (bPressed) { sm_menuPanel->close(); } + else if (sm_menuPanel->is_closed()) { BaseState::deactivate(); - m_menuPanel->reset(); + sm_menuPanel->reset(); } m_userOptionMenu.update(BaseState::has_focus()); @@ -154,19 +95,83 @@ void UserOptionState::render() m_titleSelect->render(); // Render panel. - m_menuPanel->clear_target(); - m_userOptionMenu.render(m_menuPanel->get_target(), BaseState::has_focus()); - m_menuPanel->render(NULL, BaseState::has_focus()); + sm_menuPanel->clear_target(); + m_userOptionMenu.render(sm_menuPanel->get_target(), BaseState::has_focus()); + sm_menuPanel->render(NULL, BaseState::has_focus()); } void UserOptionState::data_and_view_refresh_required() { m_refreshRequired = true; } +void UserOptionState::create_menu_panel() +{ + static constexpr int SIZE_PANEL_WIDTH = 480; + if (!sm_menuPanel) { sm_menuPanel = std::make_unique(SIZE_PANEL_WIDTH, ui::SlideOutPanel::Side::Right); } +} + +void UserOptionState::load_menu_strings() +{ + const char *nickname = m_user->get_nickname(); + + for (int i = 0; const char *format = strings::get_by_name(strings::names::USEROPTION_MENU, i); i++) + { + const std::string option = stringutil::get_formatted_string(format, nickname); + m_userOptionMenu.add_option(option); + } +} + +void UserOptionState::initialize_data_struct() +{ + m_dataStruct->user = m_user; + m_dataStruct->spawningState = this; +} + +void UserOptionState::backup_all() +{ + const char *confirmFormat = strings::get_by_name(strings::names::USEROPTION_CONFS, 0); + const char *nickname = m_user->get_nickname(); + + const std::string queryString = stringutil::get_formatted_string(confirmFormat, nickname); + + auto confirm = std::make_shared(queryString, true, backup_all_for_user, m_dataStruct); + StateManager::push_state(confirm); +} + +void UserOptionState::create_save_create() +{ + auto saveCreate = std::make_shared(m_user, m_titleSelect); + StateManager::push_state(saveCreate); +} + +void UserOptionState::create_all_save_data() +{ + const char *confirmFormat = strings::get_by_name(strings::names::USEROPTION_CONFS, 1); + const char *nickname = m_user->get_nickname(); + + const std::string queryString = stringutil::get_formatted_string(confirmFormat, nickname); + + auto confirm = std::make_shared(queryString, true, create_all_save_data_for_user, m_dataStruct); + StateManager::push_state(confirm); +} + +void UserOptionState::delete_all_save_data() +{ + const uint8_t saveType = m_user->get_account_save_type(); + if (saveType == FsSaveDataType_System || saveType == FsSaveDataType_SystemBcat) { return; } + + const char *confirmFormat = strings::get_by_name(strings::names::USEROPTION_CONFS, 2); + const char *nickname = m_user->get_nickname(); + + const std::string queryString = stringutil::get_formatted_string(confirmFormat, nickname); + + auto confirm = std::make_shared(queryString, true, delete_all_save_data_for_user, m_dataStruct); + StateManager::push_state(confirm); +} + static void backup_all_for_user(sys::ProgressTask *task, std::shared_ptr dataStruct) { if (error::is_null(task)) { return; } - data::User *user = dataStruct->user; - UserOptionState *spawningState = dataStruct->spawningState; + data::User *user = dataStruct->user; const bool exportZip = config::get_by_key(config::keys::EXPORT_TO_ZIP) || config::get_by_key(config::keys::AUTO_UPLOAD); const bool autoUpload = config::get_by_key(config::keys::AUTO_UPLOAD); @@ -230,16 +235,17 @@ static void create_all_save_data_for_user(sys::Task *task, std::shared_ptruser; UserOptionState *spawningState = dataStruct->spawningState; - auto &titleInfoMap = data::get_title_info_map(); - const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; - const char *statusTemplate = strings::get_by_name(strings::names::USEROPTION_STATUS, 0); - const char *popFailure = strings::get_by_name(strings::names::SAVECREATE_POPS, 0); - const FsSaveDataType saveType = user->get_account_save_type(); + auto &titleInfoMap = data::get_title_info_map(); + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *statusTemplate = strings::get_by_name(strings::names::USEROPTION_STATUS, 0); + const char *popFailure = strings::get_by_name(strings::names::SAVECREATE_POPS, 1); + const uint8_t saveType = user->get_account_save_type(); for (auto &[applicationID, titleInfo] : titleInfoMap) { + const bool hasType = titleInfo.has_save_data_type(saveType); - if (!hasType) { return; } + if (!hasType) { continue; } { const std::string status = stringutil::get_formatted_string(statusTemplate, titleInfo.get_title()); @@ -247,7 +253,12 @@ static void create_all_save_data_for_user(sys::Task *task, std::shared_ptrdata_and_view_refresh_required(); task->finished(); @@ -275,7 +286,7 @@ static void delete_all_save_data_for_user(sys::Task *task, std::shared_ptrget_save_info_at(i); - if (error::is_null(saveInfo)) { continue; } + if (error::is_null(saveInfo) || saveInfo->save_data_type == FsSaveDataType_System) { continue; } { const uint64_t applicationID = user->get_application_id_at(i); diff --git a/source/config.cpp b/source/config.cpp index 32ec34a..4ac6e4c 100644 --- a/source/config.cpp +++ b/source/config.cpp @@ -39,19 +39,9 @@ namespace std::map s_pathMap; } // namespace -static void read_array_to_vector(std::vector &vector, json_object *array) -{ - // Just in case. Shouldn't happen though. - vector.clear(); - - const size_t arrayLength = json_object_array_length(array); - for (size_t i = 0; i < arrayLength; i++) - { - json_object *arrayEntry = json_object_array_get_idx(array, i); - if (!arrayEntry) { continue; } - vector.push_back(std::strtoull(json_object_get_string(arrayEntry), NULL, 16)); - } -} +// Definitions at bottom. +static void read_array_to_vector(std::vector &vector, json_object *array); +static void save_custom_paths(); void config::initialize() { @@ -128,6 +118,7 @@ void config::reset_to_default() s_configMap[config::keys::AUTO_BACKUP_ON_RESTORE.data()] = 1; s_configMap[config::keys::AUTO_NAME_BACKUPS.data()] = 0; s_configMap[config::keys::AUTO_UPLOAD.data()] = 0; + s_configMap[config::keys::USE_TITLE_IDS.data()] = 0; s_configMap[config::keys::HOLD_FOR_DELETION.data()] = 1; s_configMap[config::keys::HOLD_FOR_RESTORATION.data()] = 1; s_configMap[config::keys::HOLD_FOR_OVERWRITE.data()] = 1; @@ -153,9 +144,10 @@ void config::save() json::add_object(configJSON, config::keys::WORKING_DIRECTORY.data(), workingDirectory); // Loop through map and add it. - for (auto &[key, value] : s_configMap) + for (const auto &[key, value] : s_configMap) { json_object *jsonValue = json_object_new_uint64(value); + json::add_object(configJSON, key.c_str(), jsonValue); } @@ -165,45 +157,31 @@ void config::save() // Favorites json_object *favoritesArray = json_object_new_array(); - for (uint64_t &titleID : s_favorites) + for (const uint64_t &titleID : s_favorites) { - // Need to do it like this or json-c does decimal instead of hex. - json_object *newFavorite = json_object_new_string(stringutil::get_formatted_string("%016lX", titleID).c_str()); + const std::string idHex = stringutil::get_formatted_string("%016llX", titleID); + json_object *newFavorite = json_object_new_string(idHex.c_str()); + json_object_array_add(favoritesArray, newFavorite); } json::add_object(configJSON, config::keys::FAVORITES.data(), favoritesArray); // Same but blacklist json_object *blacklistArray = json_object_new_array(); - for (uint64_t &titleID : s_blacklist) + for (const uint64_t &titleID : s_blacklist) { - json_object *newBlacklist = json_object_new_string(stringutil::get_formatted_string("%016lX", titleID).c_str()); + const std::string idHex = stringutil::get_formatted_string("%016llX", titleID); + json_object *newBlacklist = json_object_new_string(idHex.c_str()); + json_object_array_add(blacklistArray, newBlacklist); } json::add_object(configJSON, config::keys::BLACKLIST.data(), blacklistArray); // Write config file - fslib::File configFile{PATH_CONFIG_FILE, - FsOpenMode_Create | FsOpenMode_Write, - std::strlen(json_object_get_string(configJSON.get()))}; - if (configFile) { configFile << json_object_get_string(configJSON.get()); } - } - - if (!s_pathMap.empty()) - { - json::Object pathsJSON = json::new_object(json_object_new_object); - for (const auto &[applicationID, path] : s_pathMap) - { - const std::string idHex = stringutil::get_formatted_string("%016llX", applicationID); - - json_object *pathObject = json_object_new_string(path.c_str()); - json::add_object(pathsJSON, idHex.c_str(), pathObject); - } - // Write it. - fslib::File pathsFile(PATH_PATHS_PATH, - FsOpenMode_Create | FsOpenMode_Write, - std::strlen(json_object_get_string(pathsJSON.get()))); - if (pathsFile.is_open()) { pathsFile << json_object_get_string(pathsJSON.get()); } + const char *jsonString = json_object_get_string(configJSON.get()); + const int64_t configLength = std::char_traits::length(jsonString); + fslib::File configFile{PATH_CONFIG_FILE, FsOpenMode_Create | FsOpenMode_Write, configLength}; + if (configFile) { configFile << jsonString; } } } @@ -266,6 +244,7 @@ bool config::is_blacklisted(uint64_t applicationID) void config::add_custom_path(uint64_t applicationID, std::string_view customPath) { s_pathMap[applicationID] = customPath.data(); + save_custom_paths(); } bool config::has_custom_path(uint64_t applicationID) @@ -280,3 +259,38 @@ void config::get_custom_path(uint64_t applicationID, char *pathOut, size_t pathO if (s_pathMap.find(applicationID) == s_pathMap.end()) { return; } std::memcpy(pathOut, s_pathMap[applicationID].c_str(), s_pathMap[applicationID].length()); } + +static void read_array_to_vector(std::vector &vector, json_object *array) +{ + // Just in case. Shouldn't happen though. + vector.clear(); + + const size_t arrayLength = json_object_array_length(array); + for (size_t i = 0; i < arrayLength; i++) + { + json_object *arrayEntry = json_object_array_get_idx(array, i); + if (!arrayEntry) { continue; } + vector.push_back(std::strtoull(json_object_get_string(arrayEntry), NULL, 16)); + } +} + +static void save_custom_paths() +{ + json::Object pathsJson = json::new_object(json_object_new_object); + if (!pathsJson) { return; } + + for (const auto &[applicationId, path] : s_pathMap) + { + const std::string titleIdHex = stringutil::get_formatted_string("%016llX", applicationId); + json_object *jsonPath = json_object_new_string(path.c_str()); + + json::add_object(pathsJson, titleIdHex, jsonPath); + } + + const char *jsonString = json_object_get_string(pathsJson.get()); + const int64_t jsonLength = std::char_traits::length(jsonString); + fslib::File pathsFile{PATH_PATHS_PATH, FsOpenMode_Create | FsOpenMode_Write, jsonLength}; + if (error::fslib(pathsFile)) { return; } + + pathsFile << jsonString; +} diff --git a/source/data/TitleInfo.cpp b/source/data/TitleInfo.cpp index 774e270..2d09279 100644 --- a/source/data/TitleInfo.cpp +++ b/source/data/TitleInfo.cpp @@ -10,8 +10,8 @@ #include data::TitleInfo::TitleInfo(uint64_t applicationID) - : m_applicationID(applicationID) - , m_data(std::make_unique()) + : m_applicationID{applicationID} + , m_data{std::make_unique()} { static constexpr size_t SIZE_CTRL_DATA = sizeof(NsApplicationControlData); static constexpr size_t SIZE_NACP = sizeof(NacpStruct); @@ -50,8 +50,8 @@ data::TitleInfo::TitleInfo(uint64_t applicationID) // To do: Make this safer... data::TitleInfo::TitleInfo(uint64_t applicationID, NsApplicationControlData &controlData) - : m_applicationID(applicationID) - , m_data(std::make_unique()) + : m_applicationID{applicationID} + , m_data{std::make_unique()} { NsApplicationControlData *data = m_data.get(); @@ -60,6 +60,7 @@ data::TitleInfo::TitleInfo(uint64_t applicationID, NsApplicationControlData &con NacpLanguageEntry *entry{}; const bool entryError = error::libnx(nacpGetLanguageEntry(&data->nacp, &entry)); if (entryError) { std::snprintf(entry->name, TitleInfo::SIZE_PATH_SAFE, "%016lX", m_applicationID); } + TitleInfo::get_create_path_safe_title(); m_icon = sdl::TextureManager::create_load_texture(entry->name, m_data->icon, sizeof(m_data->icon)); } @@ -114,11 +115,11 @@ int64_t data::TitleInfo::get_save_data_size(uint8_t saveType) const const NacpStruct &nacp = m_data->nacp; switch (saveType) { - case FsSaveDataType_Account: return nacp.user_account_save_data_size; - case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size; - case FsSaveDataType_Device: return nacp.device_save_data_size; + case FsSaveDataType_Account: return nacp.user_account_save_data_size; + case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size; + case FsSaveDataType_Device: return nacp.device_save_data_size; case FsSaveDataType_Temporary: return nacp.temporary_storage_size; - case FsSaveDataType_Cache: return nacp.cache_storage_size; + case FsSaveDataType_Cache: return nacp.cache_storage_size; } return 0; } @@ -128,11 +129,11 @@ int64_t data::TitleInfo::get_save_data_size_max(uint8_t saveType) const const NacpStruct &nacp = m_data->nacp; switch (saveType) { - case FsSaveDataType_Account: return std::max(nacp.user_account_save_data_size, nacp.user_account_save_data_size_max); - case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size; - case FsSaveDataType_Device: return std::max(nacp.device_save_data_size, nacp.device_save_data_size_max); + case FsSaveDataType_Account: return std::max(nacp.user_account_save_data_size, nacp.user_account_save_data_size_max); + case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size; + case FsSaveDataType_Device: return std::max(nacp.device_save_data_size, nacp.device_save_data_size_max); case FsSaveDataType_Temporary: return nacp.temporary_storage_size; - case FsSaveDataType_Cache: return std::max(nacp.cache_storage_size, nacp.cache_storage_data_and_journal_size_max); + case FsSaveDataType_Cache: return std::max(nacp.cache_storage_size, nacp.cache_storage_data_and_journal_size_max); } return 0; } @@ -142,11 +143,11 @@ int64_t data::TitleInfo::get_journal_size(uint8_t saveType) const const NacpStruct &nacp = m_data->nacp; switch (saveType) { - case FsSaveDataType_Account: return nacp.user_account_save_data_journal_size; - case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size; - case FsSaveDataType_Device: return nacp.device_save_data_journal_size; + case FsSaveDataType_Account: return nacp.user_account_save_data_journal_size; + case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size; + case FsSaveDataType_Device: return nacp.device_save_data_journal_size; case FsSaveDataType_Temporary: return nacp.temporary_storage_size; - case FsSaveDataType_Cache: return nacp.cache_storage_journal_size; + case FsSaveDataType_Cache: return nacp.cache_storage_journal_size; } return 0; } @@ -158,8 +159,8 @@ int64_t data::TitleInfo::get_journal_size_max(uint8_t saveType) const { case FsSaveDataType_Account: return std::max(nacp.user_account_save_data_journal_size, nacp.user_account_save_data_journal_size_max); - case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size; - case FsSaveDataType_Device: return std::max(nacp.device_save_data_journal_size, nacp.device_save_data_journal_size_max); + case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size; + case FsSaveDataType_Device: return std::max(nacp.device_save_data_journal_size, nacp.device_save_data_journal_size_max); case FsSaveDataType_Temporary: return nacp.temporary_storage_size; case FsSaveDataType_Cache: return std::max(nacp.cache_storage_journal_size, nacp.cache_storage_data_and_journal_size_max); @@ -173,9 +174,9 @@ bool data::TitleInfo::has_save_data_type(uint8_t saveType) const switch (saveType) { case FsSaveDataType_Account: return nacp.user_account_save_data_size > 0 || nacp.user_account_save_data_size_max > 0; - case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size > 0; - case FsSaveDataType_Device: return nacp.device_save_data_size > 0 || nacp.device_save_data_size_max > 0; - case FsSaveDataType_Cache: return nacp.cache_storage_size > 0 || nacp.cache_storage_data_and_journal_size_max > 0; + case FsSaveDataType_Bcat: return nacp.bcat_delivery_cache_storage_size > 0; + case FsSaveDataType_Device: return nacp.device_save_data_size > 0 || nacp.device_save_data_size_max > 0; + case FsSaveDataType_Cache: return nacp.cache_storage_size > 0 || nacp.cache_storage_data_and_journal_size_max > 0; } return false; } @@ -194,12 +195,18 @@ void data::TitleInfo::get_create_path_safe_title() const uint64_t applicationID = TitleInfo::get_application_id(); NacpLanguageEntry *entry{}; - const bool hasCustomPath = config::has_custom_path(applicationID); - const bool entryError = !hasCustomPath && error::libnx(nacpGetLanguageEntry(&m_data->nacp, &entry)); - const bool sanitizeError = !hasCustomPath && !entryError && - !stringutil::sanitize_string_for_path(entry->name, m_pathSafeTitle, TitleInfo::SIZE_PATH_SAFE); - if (hasCustomPath) { config::get_custom_path(applicationID, m_pathSafeTitle, TitleInfo::SIZE_PATH_SAFE); } - else if (entryError || sanitizeError) + const bool hasCustom = config::has_custom_path(applicationID); + if (hasCustom) + { + config::get_custom_path(applicationID, m_pathSafeTitle, TitleInfo::SIZE_PATH_SAFE); + return; + } + + const bool useTitleId = config::get_by_key(config::keys::USE_TITLE_IDS); + const bool entryError = !useTitleId && error::libnx(nacpGetLanguageEntry(&m_data->nacp, &entry)); + const bool sanitized = + !useTitleId && !entryError && stringutil::sanitize_string_for_path(entry->name, m_pathSafeTitle, SIZE_PATH_SAFE); + if (useTitleId || entryError || !sanitized) { std::snprintf(m_pathSafeTitle, TitleInfo::SIZE_PATH_SAFE, "%016lX", m_applicationID); } diff --git a/source/data/User.cpp b/source/data/User.cpp index dfd9478..8f90ddc 100644 --- a/source/data/User.cpp +++ b/source/data/User.cpp @@ -34,8 +34,8 @@ namespace static bool sort_user_data(const data::UserDataEntry &entryA, const data::UserDataEntry &entryB); data::User::User(AccountUid accountID, FsSaveDataType saveType) - : m_accountID(accountID) - , m_saveType(saveType) + : m_accountID{accountID} + , m_saveType{saveType} { AccountProfile profile{}; AccountProfileBase profileBase{}; @@ -48,8 +48,8 @@ data::User::User(AccountUid accountID, FsSaveDataType saveType) } data::User::User(AccountUid accountID, std::string_view nickname, std::string_view pathSafeNickname, FsSaveDataType saveType) - : m_accountID(accountID) - , m_saveType(saveType) + : m_accountID{accountID} + , m_saveType{saveType} { m_icon = gfxutil::create_generic_icon(nickname, 48, colors::DIALOG_BOX, colors::WHITE); std::memcpy(m_nickname, nickname.data(), nickname.length()); diff --git a/source/data/data.cpp b/source/data/data.cpp index db6b705..c58826b 100644 --- a/source/data/data.cpp +++ b/source/data/data.cpp @@ -201,9 +201,6 @@ static bool read_cache_file() constexpr size_t SIZE_UNSIGNED = sizeof(unsigned int); constexpr size_t SIZE_CACHE_ENTRY = sizeof(CacheEntry); - const fslib::Path cachePath{PATH_CACHE_PATH}; - const bool cacheExists = fslib::file_exists(cachePath); - fslib::File cache{PATH_CACHE_PATH, FsOpenMode_Read}; if (error::fslib(cache.is_open())) { return false; } @@ -212,7 +209,8 @@ static bool read_cache_file() if (!countRead) { return false; } auto entryBuffer = std::make_unique(titleCount); // I've read there might not be any point in error checking. - const bool cacheRead = cache.read(entryBuffer.get(), SIZE_CACHE_ENTRY * titleCount) == SIZE_CACHE_ENTRY * titleCount; + const bool cacheRead = + cache.read(entryBuffer.get(), SIZE_CACHE_ENTRY * titleCount) == static_cast(SIZE_CACHE_ENTRY * titleCount); if (!cacheRead) { return false; } // Loop through the cache entries and emplace them to the map. @@ -248,6 +246,7 @@ static void create_cache_file() const bool idWrite = cache.write(&applicationID, SIZE_UINT64) == SIZE_UINT64; const bool dataWrite = cache.write(data, SIZE_CTRL_DATA) == SIZE_CTRL_DATA; if (!idWrite || !dataWrite) { return; } + ++titleCount; } diff --git a/source/fs/SaveMetaData.cpp b/source/fs/SaveMetaData.cpp index 85f69c4..3aa71b8 100644 --- a/source/fs/SaveMetaData.cpp +++ b/source/fs/SaveMetaData.cpp @@ -41,6 +41,9 @@ bool fs::fill_save_meta_data(const FsSaveDataInfo *saveInfo, fs::SaveMetaData &m bool fs::process_save_meta_data(const FsSaveDataInfo *saveInfo, const SaveMetaData &meta) { + const bool closeError = error::fslib(fslib::close_file_system(fs::DEFAULT_SAVE_MOUNT)); + if (closeError) { return false; } + const FsSaveDataSpaceId spaceID = static_cast(saveInfo->save_data_space_id); const uint64_t saveID = saveInfo->save_data_id; @@ -50,14 +53,12 @@ bool fs::process_save_meta_data(const FsSaveDataInfo *saveInfo, const SaveMetaDa if (readError) { return false; } // We need to close this temporarily. To do: Look for a way to make this not needed? - const bool closeError = error::fslib(fslib::close_file_system(fs::DEFAULT_SAVE_MOUNT)); const bool needsExtend = extraData.data_size < meta.saveDataSize; - const bool extended = !closeError && needsExtend && fs::extend_save_data(saveInfo, meta.saveDataSize, meta.journalSize); + const bool extended = needsExtend && fs::extend_save_data(saveInfo, meta.saveDataSize, meta.journalSize); if (needsExtend && !extended) { return false; } const bool reopenError = error::fslib(fslib::open_save_data_with_save_info(fs::DEFAULT_SAVE_MOUNT, *saveInfo)); + if (reopenError) { return false; } - // Maybe more later. - - return reopenError; + return true; } diff --git a/source/fs/io.cpp b/source/fs/io.cpp index fe32c7b..4c7c603 100644 --- a/source/fs/io.cpp +++ b/source/fs/io.cpp @@ -59,43 +59,34 @@ static void readThreadFunction(fslib::File &sourceFile, std::shared_ptr 0 && !commitDevice.empty(); - const int64_t fileSize = sourceFile.get_size(); + const int64_t sourceSize = sourceFile.get_size(); if (task) { const std::string status = stringutil::get_formatted_string(statusTemplate, source.full_path()); task->set_status(status); - task->reset(static_cast(fileSize)); + task->reset(static_cast(sourceSize)); } auto sharedData = std::make_shared(); sharedData->sharedBuffer = std::make_unique(SIZE_FILE_BUFFER); auto localBuffer = std::make_unique(SIZE_FILE_BUFFER); - std::mutex &lock = sharedData->lock; - std::condition_variable &condition = sharedData->condition; - bool &bufferReady = sharedData->bufferReady; - ssize_t &readSize = sharedData->readSize; - std::unique_ptr &sharedBuffer = sharedData->sharedBuffer; + std::mutex &lock = sharedData->lock; + std::condition_variable &condition = sharedData->condition; + bool &bufferReady = sharedData->bufferReady; + ssize_t &readSize = sharedData->readSize; + auto &sharedBuffer = sharedData->sharedBuffer; std::thread readThread(readThreadFunction, std::ref(sourceFile), sharedData); - - int64_t journalCount{}; - for (int64_t i = 0; i < fileSize; i++) + for (int64_t i = 0; i < sourceSize; i++) { ssize_t localRead{}; { @@ -110,42 +101,90 @@ void fs::copy_file(const fslib::Path &source, bufferReady = false; condition.notify_one(); } + // This should be checked. Not sure how yet... + destFile.write(localBuffer.get(), localRead); + i += localRead; + if (task) { task->update_current(static_cast(i)); } + } + readThread.join(); +} - const bool commitNeeded = needsCommits && (journalCount + localRead) >= static_cast(journalSize); - if (commitNeeded) +void fs::copy_file_commit(const fslib::Path &source, + const fslib::Path &destination, + std::string_view device, + int64_t journalSize, + sys::ProgressTask *task) +{ + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const std::string_view popCommitFailed = strings::get_by_name(strings::names::IO_POPS, 0); + const char *copyingStatus = strings::get_by_name(strings::names::IO_STATUSES, 0); + + fslib::File sourceFile{source, FsOpenMode_Read}; + fslib::File destFile{destination, FsOpenMode_Create | FsOpenMode_Write, sourceFile.get_size()}; + if (error::fslib(sourceFile.is_open()) || error::fslib(destFile.is_open())) { return; } + + const int64_t sourceSize = sourceFile.get_size(); + if (task) + { + const std::string status = stringutil::get_formatted_string(copyingStatus, source.full_path()); + task->set_status(status); + task->reset(static_cast(sourceSize)); + } + + auto sharedData = std::make_shared(); + auto localBuffer = std::make_unique(SIZE_FILE_BUFFER); + sharedData->sharedBuffer = std::make_unique(SIZE_FILE_BUFFER); + + std::mutex &lock = sharedData->lock; + std::condition_variable &condition = sharedData->condition; + bool &bufferReady = sharedData->bufferReady; + ssize_t &readSize = sharedData->readSize; + auto &sharedBuffer = sharedData->sharedBuffer; + + std::thread readThread{readThreadFunction, std::ref(sourceFile), sharedData}; + int64_t journalCount{}; + for (int64_t i = 0; i < sourceSize;) + { + ssize_t localRead{}; + { + std::unique_lock bufferlock{lock}; + condition.wait(bufferlock, [&]() { return bufferReady == true; }); + + localRead = readSize; + std::memcpy(localBuffer.get(), sharedBuffer.get(), localRead); + + bufferReady = false; + condition.notify_one(); + } + + if (localRead == -1) { break; } + + const bool needsCommit = journalCount + localRead >= journalSize; + if (needsCommit) { destFile.close(); - const bool commitError = error::fslib(fslib::commit_data_to_file_system(commitDevice)); - if (commitError) - { - ui::PopMessageManager::push_message(popticks, popErrorCommitting); - break; - } + const bool commitError = error::fslib(fslib::commit_data_to_file_system(device)); + // To do: Handle this better. Threads current have no way of communicating errors. + if (commitError) { ui::PopMessageManager::push_message(popTicks, popCommitFailed); } destFile.open(destination, FsOpenMode_Write); destFile.seek(i, destFile.BEGINNING); - - journalCount = 0; } - // This should be checked. Not sure how yet... + destFile.write(localBuffer.get(), localRead); + i += localRead; journalCount += localRead; if (task) { task->update_current(static_cast(i)); } } - readThread.join(); destFile.close(); - const bool commitError = needsCommits && error::fslib(fslib::commit_data_to_file_system(commitDevice)); - if (commitError) { ui::PopMessageManager::push_message(popticks, popErrorCommitting); } + const bool commitError = error::fslib(fslib::commit_data_to_file_system(device)); + if (commitError) { ui::PopMessageManager::push_message(popTicks, popCommitFailed); } } -void fs::copy_directory(const fslib::Path &source, - const fslib::Path &destination, - sys::ProgressTask *task, - uint64_t journalSize, - std::string_view commitDevice) +void fs::copy_directory(const fslib::Path &source, const fslib::Path &destination, sys::ProgressTask *task) { fslib::Directory sourceDir{source}; if (error::fslib(sourceDir.is_open())) { return; } @@ -162,8 +201,36 @@ void fs::copy_directory(const fslib::Path &source, const bool destExists = fslib::directory_exists(fullDest); const bool createError = !destExists && fslib::create_directory(fullDest); if (!destExists && createError) { continue; } - fs::copy_directory(fullSource, fullDest, task, journalSize, commitDevice); + fs::copy_directory(fullSource, fullDest, task); } - else { fs::copy_file(fullSource, fullDest, task, journalSize, commitDevice); } + else { fs::copy_file(fullSource, fullDest, task); } + } +} + +void fs::copy_directory_commit(const fslib::Path &source, + const fslib::Path &destination, + std::string_view device, + int64_t journalSize, + sys::ProgressTask *task) +{ + fslib::Directory sourceDir{source}; + if (error::fslib(sourceDir.is_open())) { return; } + + const int64_t dirCount = sourceDir.get_count(); + for (int64_t i = 0; i < dirCount; i++) + { + if (sourceDir[i] == fs::NAME_SAVE_META) { continue; } // We don't want or need to copy this to the save. + + const fslib::Path fullSource{source / sourceDir[i]}; + const fslib::Path fullDest{destination / sourceDir[i]}; + if (sourceDir.is_directory(i)) + { + const bool destExists = fslib::directory_exists(fullDest); + const bool createError = !destExists && error::fslib(fslib::create_directory(fullDest)); + if (!destExists && createError) { continue; } + + fs::copy_directory_commit(fullSource, fullDest, device, journalSize, task); + } + else { fs::copy_file_commit(fullSource, fullDest, device, journalSize, task); } } } diff --git a/source/fs/save_data_functions.cpp b/source/fs/save_data_functions.cpp index 25ac0df..ef1b840 100644 --- a/source/fs/save_data_functions.cpp +++ b/source/fs/save_data_functions.cpp @@ -5,6 +5,8 @@ bool fs::create_save_data_for(data::User *targetUser, data::TitleInfo *titleInfo) { + static constexpr FsSaveDataMetaInfo saveMeta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; + const uint8_t saveType = targetUser->get_account_save_type(); const uint64_t applicationID = titleInfo->get_application_id(); const AccountUid accountID = saveType == FsSaveDataType_Account ? targetUser->get_account_id() : data::BLANK_ACCOUNT_ID; @@ -26,11 +28,9 @@ bool fs::create_save_data_for(data::User *targetUser, data::TitleInfo *titleInfo .flags = 0, .save_data_space_id = FsSaveDataSpaceId_User}; - const FsSaveDataMetaInfo saveMeta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; - // I want this recorded. const bool createError = error::libnx(fsCreateSaveDataFileSystem(&saveAttributes, &saveCreation, &saveMeta)); - return createError; + return createError == false; } bool fs::delete_save_data(const FsSaveDataInfo *saveInfo) @@ -49,7 +49,7 @@ bool fs::delete_save_data(const FsSaveDataInfo *saveInfo) .save_data_index = saveInfo->save_data_index}; const bool deleteError = error::libnx(fsDeleteSaveDataFileSystemBySaveDataAttribute(spaceID, &saveAttributes)); - return deleteError; + return deleteError == false; } bool fs::extend_save_data(const FsSaveDataInfo *saveInfo, int64_t size, int64_t journalSize) @@ -58,7 +58,7 @@ bool fs::extend_save_data(const FsSaveDataInfo *saveInfo, int64_t size, int64_t const uint64_t saveID = saveInfo->save_data_id; const bool extendError = error::libnx(fsExtendSaveDataFileSystem(spaceID, saveID, size, journalSize)); - return extendError; + return extendError == false; } bool fs::is_system_save_data(const FsSaveDataInfo *saveInfo) diff --git a/source/logger.cpp b/source/logger.cpp index 0a61087..f57574c 100644 --- a/source/logger.cpp +++ b/source/logger.cpp @@ -4,15 +4,15 @@ #include "fslib.hpp" #include +#include #include +#include namespace { /// @brief This is the path to the log file. fslib::Path s_logFilePath{}; - std::mutex s_logLock{}; - /// @brief This is the buffer size for log strings. constexpr size_t VA_BUFFER_SIZE = 0x1000; } // namespace @@ -28,6 +28,8 @@ void logger::initialize() void logger::log(const char *format, ...) { + static std::mutex logLock{}; + char vaBuffer[VA_BUFFER_SIZE] = {0}; std::va_list vaList; @@ -35,16 +37,8 @@ void logger::log(const char *format, ...) vsnprintf(vaBuffer, VA_BUFFER_SIZE, format, vaList); va_end(vaList); - std::scoped_lock logLock(s_logLock); + std::lock_guard logGuard(logLock); fslib::File logFile(s_logFilePath, FsOpenMode_Append); logFile << vaBuffer << "\n"; logFile.flush(); } - -void logger::log_straight(std::string_view string) -{ - std::scoped_lock logLock(s_logLock); - fslib::File logFile{s_logFilePath, FsOpenMode_Append}; - logFile << string.data() << "\n"; - logFile.flush(); -} diff --git a/source/remote/GoogleDrive.cpp b/source/remote/GoogleDrive.cpp index f142122..0a82904 100644 --- a/source/remote/GoogleDrive.cpp +++ b/source/remote/GoogleDrive.cpp @@ -48,7 +48,7 @@ namespace remote::GoogleDrive::GoogleDrive() : Storage("[GD]", true) { - static const char *STRING_ERROR_READING_CONFIG = "Error reading Google Drive config: %s"; + static constexpr const char *STRING_ERROR_READING_CONFIG = "Error reading Google Drive config: %s"; // Load the json file. json::Object clientJson = json::new_object(json_object_from_file, remote::PATH_GOOGLE_DRIVE_CONFIG.data()); @@ -146,7 +146,6 @@ bool remote::GoogleDrive::create_directory(std::string_view name) bool remote::GoogleDrive::upload_file(const fslib::Path &source, std::string_view name, sys::ProgressTask *task) { if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; } - const char *statusTemplate = strings::get_by_name(strings::names::IO_STATUSES, 5); fslib::File sourceFile(source, FsOpenMode_Read); if (!sourceFile) @@ -193,12 +192,7 @@ bool remote::GoogleDrive::upload_file(const fslib::Path &source, std::string_vie return false; } - if (task) - { - std::string status = stringutil::get_formatted_string(statusTemplate, source.full_path()); - task->set_status(status); - task->reset(static_cast(sourceFile.get_size())); - } + if (task) { task->reset(static_cast(sourceFile.get_size())); } std::string response; curl::UploadStruct uploadData = {.source = &sourceFile, .task = task}; @@ -242,10 +236,9 @@ bool remote::GoogleDrive::upload_file(const fslib::Path &source, std::string_vie bool remote::GoogleDrive::patch_file(remote::Item *file, const fslib::Path &source, sys::ProgressTask *task) { - static const char *STRING_PATCH_ERROR = "Error patching file: %s"; + static constexpr const char *STRING_PATCH_ERROR = "Error patching file: %s"; if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; } - const char *statusTemplate = strings::get_by_name(strings::names::BACKUPMENU_STATUS, 2); fslib::File sourceFile(source, FsOpenMode_Read); if (!sourceFile) @@ -281,12 +274,7 @@ bool remote::GoogleDrive::patch_file(remote::Item *file, const fslib::Path &sour return false; } - if (task) - { - const std::string status = stringutil::get_formatted_string(statusTemplate, source.full_path()); - task->set_status(status); - task->reset(static_cast(sourceFile.get_size())); - } + if (task) { task->reset(static_cast(sourceFile.get_size())); } // For some reason, this doesn't need the auth header. curl::UploadStruct uploadData = {.source = &sourceFile, .task = task}; @@ -307,7 +295,6 @@ bool remote::GoogleDrive::patch_file(remote::Item *file, const fslib::Path &sour bool remote::GoogleDrive::download_file(const remote::Item *file, const fslib::Path &destination, sys::ProgressTask *task) { if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; } - const char *statusTemplate = strings::get_by_name(strings::names::IO_STATUSES, 4); fslib::File destFile{destination, FsOpenMode_Create | FsOpenMode_Write, file->get_size()}; if (!destFile) @@ -316,12 +303,7 @@ bool remote::GoogleDrive::download_file(const remote::Item *file, const fslib::P return false; } - if (task) - { - const std::string status = stringutil::get_formatted_string(statusTemplate, file->get_name().data()); - task->set_status(status); - task->reset(static_cast(file->get_size())); - } + if (task) { task->reset(static_cast(file->get_size())); } curl::HeaderList header = curl::new_header_list(); curl::append_header(header, m_authHeader); @@ -393,11 +375,13 @@ bool remote::GoogleDrive::sign_in_required() const { return !m_isInitialized || bool remote::GoogleDrive::get_sign_in_data(std::string &message, std::string &code, std::time_t &expiration, int &wait) { - static const char *STRING_SIGN_IN_ERROR = "Error requesting sign-in data: %s"; + static constexpr const char *STRING_SIGN_IN_ERROR = "Error requesting sign-in data: %s"; // This is the the URL for device codes and limited input. - static const char *API_URL_DEVICE_CODE = "https://oauth2.googleapis.com/device/code"; + static constexpr const char *API_URL_DEVICE_CODE = "https://oauth2.googleapis.com/device/code"; // This is a gimped version of what I used to use. - static const char *STRING_DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file"; + static constexpr const char *STRING_DRIVE_FILE_SCOPE = "https://www.googleapis.com/auth/drive.file"; + + const char *messageTemplate = strings::get_by_name(strings::names::GOOGLE_DRIVE, 0); curl::HeaderList header = curl::new_header_list(); curl::append_header(header, HEADER_CONTENT_TYPE_FORM); @@ -426,31 +410,25 @@ bool remote::GoogleDrive::get_sign_in_data(std::string &message, std::string &co // These are required and fatal. if (!deviceCode || !userCode || !verificationUrl || !expiresIn || !interval) { - logger::log(STRING_SIGN_IN_ERROR, "Malformed or bad response."); + logger::log(STRING_SIGN_IN_ERROR, "Malformed response."); return false; } - // I hate how this looks, but whatever. - message = stringutil::get_formatted_string(strings::get_by_name(strings::names::GOOGLE_DRIVE, 0), + message = stringutil::get_formatted_string(messageTemplate, json_object_get_string(verificationUrl), json_object_get_string(userCode)); - - code = json_object_get_string(deviceCode); - - // This is a quick, dirty way to get the expiration time. + code = json_object_get_string(deviceCode); expiration = std::time(NULL) + json_object_get_uint64(expiresIn); - - // This should be the time in seconds to wait between verification checks. - wait = json_object_get_int64(interval); + wait = json_object_get_int64(interval); return true; } bool remote::GoogleDrive::poll_sign_in(std::string_view code) { - static const char *STRING_ERROR_POLLING = "Error polling Google OAuth2: %s"; + static constexpr const char *STRING_ERROR_POLLING = "Error polling Google OAuth2: %s"; // I'm just gonna save this pre-escaped instead of wasting time escaping it. - static const char *STRING_DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"; + static constexpr const char *STRING_DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"; // I'm not sure how else to make this really work with JKSV's task threading system? remote::Form post{}; @@ -489,8 +467,7 @@ bool remote::GoogleDrive::poll_sign_in(std::string_view code) m_token = json_object_get_string(accessToken); m_refreshToken = json_object_get_string(refreshToken); m_tokenExpires = std::time(NULL) + json_object_get_uint64(expiresIn); - - m_authHeader = std::string(HEADER_AUTH_BEARER) + m_token; + m_authHeader = std::string(HEADER_AUTH_BEARER) + m_token; json::Object config = json::new_object(json_object_from_file, remote::PATH_GOOGLE_DRIVE_CONFIG.data()); if (config) @@ -505,7 +482,6 @@ bool remote::GoogleDrive::poll_sign_in(std::string_view code) if (configFile) { configFile << json_object_get_string(config.get()); } } - // Not sure where else to really put this. if (!GoogleDrive::get_root_id()) { return false; } m_isInitialized = true; @@ -516,7 +492,7 @@ bool remote::GoogleDrive::poll_sign_in(std::string_view code) bool remote::GoogleDrive::get_root_id() { // This is the only place this is used. V3 of the API doesn't allow you to retrieve this for some reason? - static const char *API_URL_ABOUT_ROOT_ID = "https://www.googleapis.com/drive/v2/about?fields=rootFolderId"; + static constexpr const char *API_URL_ABOUT_ROOT_ID = "https://www.googleapis.com/drive/v2/about?fields=rootFolderId"; if (!GoogleDrive::token_is_valid() && !GoogleDrive::refresh_token()) { return false; } @@ -584,12 +560,9 @@ bool remote::GoogleDrive::refresh_token() json_object *expiresIn = json::get_object(parser, JSON_KEY_EXPIRES_IN); if (!accessToken || !expiresIn) { return false; } - // Got our new access token. - m_token = json_object_get_string(accessToken); - // This is better than pinging Google's servers every single time. + m_token = json_object_get_string(accessToken); m_tokenExpires = std::time(NULL) + json_object_get_uint64(expiresIn); - - m_authHeader = std::string(HEADER_AUTH_BEARER) + m_token; + m_authHeader = std::string(HEADER_AUTH_BEARER) + m_token; return true; } @@ -617,7 +590,6 @@ bool remote::GoogleDrive::request_listing() // This is used as the loop condition. json_object *nextPageToken = nullptr; do { - // This needs to be cleared for every request. response.clear(); if (!curl::perform(m_curl)) { return false; } @@ -643,7 +615,7 @@ bool remote::GoogleDrive::request_listing() bool remote::GoogleDrive::process_listing(json::Object &json) { - static const char *STRING_ERROR_PROCESSING = "Error processing Google Drive listing: %s"; + static constexpr const char *STRING_ERROR_PROCESSING = "Error processing Google Drive listing: %s"; json_object *files = json::get_object(json, "files"); if (!files) { return false; } @@ -659,7 +631,6 @@ bool remote::GoogleDrive::process_listing(json::Object &json) json_object *id = json_object_object_get(currentFile, JSON_KEY_ID); json_object *name = json_object_object_get(currentFile, JSON_KEY_NAME); json_object *size = json_object_object_get(currentFile, "size"); - // All of these are REQUIRED! Size doesn't exist for folders. if (!mimeType || !parents || !id || !name) { logger::log(STRING_ERROR_PROCESSING, "Malformed or missing data!"); @@ -680,6 +651,7 @@ bool remote::GoogleDrive::process_listing(json::Object &json) size ? json_object_get_uint64(size) : 0, std::strcmp(MIME_TYPE_DIRECTORY, json_object_get_string(mimeType)) == 0); } + return true; } diff --git a/source/remote/Item.cpp b/source/remote/Item.cpp index b2c8348..216d548 100644 --- a/source/remote/Item.cpp +++ b/source/remote/Item.cpp @@ -3,11 +3,11 @@ #include "logger.hpp" remote::Item::Item(std::string_view name, std::string_view id, std::string_view parent, size_t size, bool directory) - : m_name(name) - , m_id(id) - , m_parent(parent) - , m_size(size) - , m_isDirectory(directory) {}; + : m_name{name} + , m_id{id} + , m_parent{parent} + , m_size{size} + , m_isDirectory{directory} {}; std::string_view remote::Item::get_name() const { return m_name; } diff --git a/source/remote/Storage.cpp b/source/remote/Storage.cpp index fb64b08..29296dd 100644 --- a/source/remote/Storage.cpp +++ b/source/remote/Storage.cpp @@ -27,9 +27,9 @@ remote::Item *remote::Storage::get_directory_by_name(std::string_view name) remote::Storage::DirectoryListing remote::Storage::get_directory_listing() { - remote::Storage::DirectoryListing listing; + remote::Storage::DirectoryListing listing{}; - Storage::List::iterator current = m_list.begin(); + auto current = m_list.begin(); while ((current = std::find_if(current, m_list.end(), [&](const Item &item) { return item.get_parent_id() == this->m_parent; })) != m_list.end()) @@ -37,7 +37,6 @@ remote::Storage::DirectoryListing remote::Storage::get_directory_listing() listing.push_back(&(*current)); ++current; } - return listing; } diff --git a/source/remote/WebDav.cpp b/source/remote/WebDav.cpp index 943c21f..70fe624 100644 --- a/source/remote/WebDav.cpp +++ b/source/remote/WebDav.cpp @@ -2,18 +2,15 @@ #include "JSON.hpp" #include "curl/curl.hpp" +#include "error.hpp" #include "logger.hpp" #include "remote/remote.hpp" #include "strings.hpp" #include "stringutil.hpp" +#include "ui/PopMessageManager.hpp" #include -namespace -{ - const char *TAG_XML_HREF = "href"; -} // namespace - // Declarations here. Definitions at bottom. /// @brief Gets a XML element by name. This is namespace agnostic. /// @param parent Parent XML element. @@ -33,7 +30,7 @@ static std::string slice_name_from_href(curl::Handle &handle, std::string_view h remote::WebDav::WebDav() : Storage("[WD]") { - static const char *STRING_CONFIG_READ_ERROR = "Error initializing WebDav: %s"; + static constexpr const char *STRING_CONFIG_READ_ERROR = "Error initializing WebDav: %s"; json::Object config = json::new_object(json_object_from_file, remote::PATH_WEBDAV_CONFIG.data()); if (!config) @@ -47,8 +44,6 @@ remote::WebDav::WebDav() json_object *basepath = json::get_object(config, "basepath"); json_object *username = json::get_object(config, "username"); json_object *password = json::get_object(config, "password"); - - // This is the bare minimum required to continue. if (!origin) { logger::log(STRING_CONFIG_READ_ERROR, "Config is missing origin!"); @@ -73,20 +68,20 @@ remote::WebDav::WebDav() // This'll recursively get the full listing of the basepath for the WebDav server. std::string xml{}; - if (!WebDav::prop_find(url, xml) || !WebDav::process_listing(xml)) - { - logger::log(STRING_CONFIG_READ_ERROR, "Error retrieving listing from WebDav server!"); - return; - } + const bool propFind = WebDav::prop_find(url, xml); + const bool xmlProcessed = propFind && WebDav::process_listing(xml); + if (!propFind || !xmlProcessed) { return; } + m_isInitialized = true; } bool remote::WebDav::create_directory(std::string_view name) { - static const char *STRING_CREATE_DIR_ERROR = "Error creating WebDav directory: %s"; + static constexpr const char *STRING_CREATE_DIR_ERROR = "Error creating WebDav directory: %s"; - std::string escapedName; - if (!curl::escape_string(m_curl, name, escapedName)) + std::string escapedName{}; + const bool nameEscaped = curl::escape_string(m_curl, name, escapedName); + if (!nameEscaped) { logger::log(STRING_CREATE_DIR_ERROR, "Error escaping directory name!"); return false; @@ -118,34 +113,24 @@ bool remote::WebDav::create_directory(std::string_view name) bool remote::WebDav::upload_file(const fslib::Path &source, std::string_view remoteName, sys::ProgressTask *task) { static constexpr const char *STRING_ERROR_UPLOADING = "Error uploading to WebDav: %s"; - const char *statusTemplate = strings::get_by_name(strings::names::IO_STATUSES, 5); fslib::File sourceFile{source, FsOpenMode_Read}; - if (!sourceFile.is_open()) - { - logger::log(STRING_ERROR_UPLOADING, fslib::error::get_string()); - return false; - } + if (error::fslib(sourceFile.is_open())) { return false; } - std::string escapedName; - if (!curl::escape_string(m_curl, remoteName, escapedName)) + std::string escapedName{}; + const bool nameEscaped = curl::escape_string(m_curl, remoteName, escapedName); + if (!nameEscaped) { logger::log(STRING_ERROR_UPLOADING, "Failed to escape filename!"); return false; } - if (task) - { - const std::string status = stringutil::get_formatted_string(statusTemplate, remoteName.data()); - task->set_status(status); - task->reset(static_cast(sourceFile.get_size())); - } + if (task) { task->reset(static_cast(sourceFile.get_size())); } remote::URL url{m_origin}; url.append_path(m_parent).append_path(escapedName); - curl::UploadStruct uploadData = {.source = &sourceFile, .task = task}; - + curl::UploadStruct uploadData{.source = &sourceFile, .task = task}; curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_URL, url.get()); @@ -164,52 +149,49 @@ bool remote::WebDav::upload_file(const fslib::Path &source, std::string_view rem bool remote::WebDav::patch_file(remote::Item *item, const fslib::Path &source, sys::ProgressTask *task) { - static const char *STRING_ERROR_PATCHING = "Error patching file: %s"; + static constexpr const char *STRING_ERROR_PATCHING = "Error patching file: %s"; - fslib::File file{source, FsOpenMode_Read}; - if (!file) + fslib::File sourceFile{source, FsOpenMode_Read}; + if (!sourceFile) { logger::log(STRING_ERROR_PATCHING, fslib::error::get_string()); return false; } + if (task) { task->reset(static_cast(sourceFile.get_size())); } + remote::URL url{m_origin}; url.append_path(item->get_id()); + curl::UploadStruct uploadData{.source = &sourceFile, .task = task}; curl::reset_handle(m_curl); WebDav::append_credentials(); curl::set_option(m_curl, CURLOPT_URL, url.get()); curl::set_option(m_curl, CURLOPT_UPLOAD, 1L); curl::set_option(m_curl, CURLOPT_UPLOAD_BUFFERSIZE, Storage::SIZE_UPLOAD_BUFFER); curl::set_option(m_curl, CURLOPT_READFUNCTION, curl::read_data_from_file); - curl::set_option(m_curl, CURLOPT_READDATA, &file); + curl::set_option(m_curl, CURLOPT_READDATA, &uploadData); if (!curl::perform(m_curl)) { return false; } // Just update the size this time. - item->set_size(file.get_size()); + item->set_size(sourceFile.get_size()); return true; } bool remote::WebDav::download_file(const remote::Item *item, const fslib::Path &destination, sys::ProgressTask *task) { - static const char *STRING_ERROR_DOWNLOADING = "Error downloading file: %s"; - const char *statusTemplate = strings::get_by_name(strings::names::IO_STATUSES, 4); + static constexpr const char *STRING_ERROR_DOWNLOADING = "Error downloading file: %s"; - fslib::File destFile{destination, FsOpenMode_Create | FsOpenMode_Write, item->get_size()}; + fslib::File destFile{destination, FsOpenMode_Create | FsOpenMode_Write, static_cast(item->get_size())}; if (!destFile) { logger::log(STRING_ERROR_DOWNLOADING, fslib::error::get_string()); return false; } - if (task) - { - const std::string status = stringutil::get_formatted_string(statusTemplate, item->get_name().data()); - task->set_status(status); - task->reset(static_cast(item->get_size())); - } + if (task) { task->reset(static_cast(item->get_size())); } remote::URL url{m_origin}; url.append_path(item->get_id()); @@ -224,6 +206,10 @@ bool remote::WebDav::download_file(const remote::Item *item, const fslib::Path & std::thread writeThread{curl::download_write_thread_function, std::ref(download)}; if (!curl::perform(m_curl)) { return false; } + + // Copied from gd.cpp implementation. + // TODO: Not sure how a thread helps if this parent waits here. + // TODO: Read and understand what's actually happening before making comments on other's choices. writeThread.join(); return true; @@ -231,7 +217,7 @@ bool remote::WebDav::download_file(const remote::Item *item, const fslib::Path & bool remote::WebDav::delete_item(const remote::Item *item) { - static const char *STRING_ERROR_DELETING = "Error deleting item: %s"; + static constexpr const char *STRING_ERROR_DELETING = "Error deleting item: %s"; remote::URL url{m_origin}; url.append_path(item->get_id()); @@ -284,7 +270,14 @@ bool remote::WebDav::prop_find(const remote::URL &url, std::string &xml) bool remote::WebDav::process_listing(std::string_view xml) { - static const char *STRING_ERROR_PROCESSING_XML = "Error processing XML: %s"; + static constexpr const char *STRING_ERROR_PROCESSING_XML = "Error processing XML: %s"; + // These are so string_views aren't constructed every loop. + static constexpr std::string_view tagHref{"href"}; + static constexpr std::string_view tagPropStat{"propstat"}; + static constexpr std::string_view tagProp{"prop"}; + static constexpr std::string_view tagResourceType{"resourcetype"}; + static constexpr std::string_view tagCollection{"collection"}; + static constexpr std::string_view tagGetContentLength{"getcontentlength"}; tinyxml2::XMLDocument listing{}; if (listing.Parse(xml.data(), xml.length())) @@ -293,70 +286,54 @@ bool remote::WebDav::process_listing(std::string_view xml) return false; } - tinyxml2::XMLElement *root = listing.RootElement(); - - // The first element is what we're using as the parent. - tinyxml2::XMLElement *parent = root->FirstChildElement(); - - tinyxml2::XMLElement *parentLocation = get_element_by_name(parent, TAG_XML_HREF); + tinyxml2::XMLElement *root = listing.RootElement(); + tinyxml2::XMLElement *parent = root->FirstChildElement(); + tinyxml2::XMLElement *parentLocation = get_element_by_name(parent, tagHref); if (!parentLocation) { logger::log(STRING_ERROR_PROCESSING_XML, "Error finding list parent location!"); return false; } - // There's no point in continuing if this fails. Just return true. - tinyxml2::XMLElement *current = parent->NextSiblingElement(); - if (!current) { return true; } - - do { + const char *parentLocationText = parentLocation->GetText(); // Possibly going to need this a lot. + tinyxml2::XMLElement *current{}; + for (current = parent->NextSiblingElement(); current; current = current->NextSiblingElement()) + { // Parsing XML is actually annoying. Even with tinyxml2. - tinyxml2::XMLElement *href = get_element_by_name(current, TAG_XML_HREF); - tinyxml2::XMLElement *propstat = get_element_by_name(current, "propstat"); - tinyxml2::XMLElement *prop = get_element_by_name(propstat, "prop"); - tinyxml2::XMLElement *resourceType = get_element_by_name(prop, "resourcetype"); - if (!href || !propstat || !prop || !resourceType) - { - logger::log(STRING_ERROR_PROCESSING_XML, "Element is missing data!"); - continue; - } + tinyxml2::XMLElement *href = get_element_by_name(current, tagHref); + tinyxml2::XMLElement *propstat = get_element_by_name(current, tagPropStat); + tinyxml2::XMLElement *prop = get_element_by_name(propstat, tagProp); + tinyxml2::XMLElement *resourceType = get_element_by_name(prop, tagResourceType); + if (!href || !propstat || !prop || !resourceType) { continue; } - // This is the best way to detect a directory. - tinyxml2::XMLElement *collection = get_element_by_name(resourceType, "collection"); + // Avoid redundant calls the GetText later. + const char *hrefText = href->GetText(); + const std::string name = slice_name_from_href(m_curl, hrefText); + tinyxml2::XMLElement *collection = get_element_by_name(resourceType, tagCollection); if (collection) { - m_list.emplace_back(slice_name_from_href(m_curl, href->GetText()), - href->GetText(), - parentLocation->GetText(), - 0, - true); + logger::log("%s, %s, %s", name.c_str(), hrefText, parentLocationText); + + m_list.emplace_back(name, hrefText, parentLocationText, 0, true); remote::URL nextUrl{m_origin}; - nextUrl.append_path(href->GetText()); + nextUrl.append_path(hrefText); std::string xml{}; - if (!WebDav::prop_find(nextUrl, xml) || !WebDav::process_listing(xml)) - { - logger::log(STRING_ERROR_PROCESSING_XML, href->GetText()); - } + const bool propFind = WebDav::prop_find(nextUrl, xml); + const bool processXml = propFind && WebDav::process_listing(xml); + if (!propFind || !processXml) { logger::log(STRING_ERROR_PROCESSING_XML, hrefText); } } else { - tinyxml2::XMLElement *getContentLength = get_element_by_name(prop, "getcontentlength"); - if (!getContentLength) - { - logger::log(STRING_ERROR_PROCESSING_XML, "Missing needed data for file!"); - continue; - } + tinyxml2::XMLElement *getContentLength = get_element_by_name(prop, tagGetContentLength); + if (!getContentLength) { continue; } - m_list.emplace_back(slice_name_from_href(m_curl, href->GetText()), - href->GetText(), - parentLocation->GetText(), - std::strtoll(getContentLength->GetText(), NULL, 10), - false); + const char *lengthString = getContentLength->GetText(); + const int64_t contentLength = std::strtoll(lengthString, nullptr, 10); + m_list.emplace_back(name, hrefText, parentLocationText, contentLength, false); } - } while ((current = current->NextSiblingElement())); - + } return true; } diff --git a/source/remote/remote.cpp b/source/remote/remote.cpp index f24ffcf..5a64b1d 100644 --- a/source/remote/remote.cpp +++ b/source/remote/remote.cpp @@ -32,11 +32,10 @@ static void drive_set_jksv_root(remote::GoogleDrive *drive); void remote::initialize_google_drive() { - // Create drive instance. - s_storage = std::make_unique(); - - // Need to cast this for it to work right. - remote::GoogleDrive *drive = static_cast(s_storage.get()); + s_storage = std::make_unique(); + remote::GoogleDrive *drive = static_cast(s_storage.get()); + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popDriveSuccess = strings::get_by_name(strings::names::GOOGLE_DRIVE, 1); if (drive->sign_in_required()) { @@ -48,26 +47,20 @@ void remote::initialize_google_drive() // To do: Handle this better. Maybe retry somehow? if (!drive->is_initialized()) { return; } - // Can't forget this. + drive_set_jksv_root(drive); - ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_TICKS, - strings::get_by_name(strings::names::GOOGLE_DRIVE, 1)); + ui::PopMessageManager::push_message(popTicks, popDriveSuccess); } void remote::initialize_webdav() { - s_storage = std::make_unique(); + s_storage = std::make_unique(); + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popDavSuccess = strings::get_by_name(strings::names::WEBDAV, 0); + const char *popDavFailed = strings::get_by_name(strings::names::WEBDAV, 1); - if (s_storage->is_initialized()) - { - ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_TICKS, - strings::get_by_name(strings::names::WEBDAV, 0)); - } - else - { - ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_TICKS, - strings::get_by_name(strings::names::WEBDAV, 1)); - } + if (s_storage->is_initialized()) { ui::PopMessageManager::push_message(popTicks, popDavSuccess); } + else { ui::PopMessageManager::push_message(popTicks, popDavFailed); } } remote::Storage *remote::get_remote_storage() @@ -78,12 +71,14 @@ remote::Storage *remote::get_remote_storage() static void drive_sign_in(sys::Task *task, remote::GoogleDrive *drive) { - static const char *STRING_ERROR_SIGNING_IN = "Error signing into Google Drive: %s"; + static constexpr const char *STRING_ERROR_SIGNING_IN = "Error signing into Google Drive: %s"; + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popDriveSuccess = strings::get_by_name(strings::names::GOOGLE_DRIVE, 1); + const char *popDriveFailed = strings::get_by_name(strings::names::GOOGLE_DRIVE, 2); std::string message{}, deviceCode{}; - std::time_t expiration = 0; - int pollingInterval = 0; - + std::time_t expiration{}; + int pollingInterval{}; if (!drive->get_sign_in_data(message, deviceCode, expiration, pollingInterval)) { logger::log(STRING_ERROR_SIGNING_IN, "Getting sign in data failed!"); @@ -100,37 +95,25 @@ static void drive_sign_in(sys::Task *task, remote::GoogleDrive *drive) if (drive->is_initialized()) { - // Run this quick so the root is set correctly. drive_set_jksv_root(drive); - // Show everyone I did it! - ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_TICKS, - strings::get_by_name(strings::names::GOOGLE_DRIVE, 1)); - } - else - { - ui::PopMessageManager::push_message(ui::PopMessageManager::DEFAULT_TICKS, - strings::get_by_name(strings::names::GOOGLE_DRIVE, 2)); + ui::PopMessageManager::push_message(popTicks, popDriveSuccess); } + else { ui::PopMessageManager::push_message(popTicks, popDriveFailed); } task->finished(); } static void drive_set_jksv_root(remote::GoogleDrive *drive) { - static const char *STRING_ERROR_SETTING_DIR = "Error creating/setting JKSV directory on Drive: %s"; + static constexpr const char *STRING_ERROR_SETTING_DIR = "Error creating/setting JKSV directory on Drive: %s"; - if (!drive->directory_exists(STRING_JKSV_DIR) && !drive->create_directory(STRING_JKSV_DIR)) - { - logger::log(STRING_ERROR_SETTING_DIR, "Error finding and/or creating directory!"); - return; - } + const bool jksvExists = drive->directory_exists(STRING_JKSV_DIR); + const bool jksvCreated = !jksvExists && drive->create_directory(STRING_JKSV_DIR); + if (!jksvExists && !jksvCreated) { return; } + + const remote::Item *jksvDir = drive->get_directory_by_name(STRING_JKSV_DIR); + if (!jksvDir) { return; } - remote::Item *jksvDir = drive->get_directory_by_name(STRING_JKSV_DIR); - if (!jksvDir) - { - logger::log(STRING_ERROR_SETTING_DIR, "Error locating directory in list! This shouldn't be able to happen!"); - return; - } drive->set_root_directory(jksvDir); drive->change_directory(jksvDir); } diff --git a/source/system/Task.cpp b/source/system/Task.cpp index 5cb17ab..de69ee7 100644 --- a/source/system/Task.cpp +++ b/source/system/Task.cpp @@ -16,12 +16,12 @@ void sys::Task::finished() { m_isRunning = false; } void sys::Task::set_status(std::string_view status) { - std::scoped_lock statusLock(m_statusLock); + std::lock_guard statusGuard{m_statusLock}; m_status = status; } std::string sys::Task::get_status() { - std::scoped_lock StatusLock(m_statusLock); + std::lock_guard statusGuard{m_statusLock}; return m_status; } diff --git a/source/system/Timer.cpp b/source/system/Timer.cpp index 488bd80..2342095 100644 --- a/source/system/Timer.cpp +++ b/source/system/Timer.cpp @@ -8,20 +8,17 @@ sys::Timer::Timer(uint64_t triggerTicks) { Timer::start(triggerTicks); } void sys::Timer::start(uint64_t triggerTicks) { - // Start by recording trigger ticks. - m_triggerTicks = triggerTicks; - - // Get the current tick count. + m_triggerTicks = triggerTicks; m_startingTicks = SDL_GetTicks64(); } bool sys::Timer::is_triggered() { const uint64_t currentTicks = SDL_GetTicks64(); - if (currentTicks - m_startingTicks < m_triggerTicks) { return false; } + const bool triggered = (currentTicks - m_startingTicks) >= m_triggerTicks; + if (!triggered) { return false; } m_startingTicks = currentTicks; - // Trigger me timbers~ return true; } diff --git a/source/tasks/backup.cpp b/source/tasks/backup.cpp index 0b10f02..c50b4ad 100644 --- a/source/tasks/backup.cpp +++ b/source/tasks/backup.cpp @@ -13,10 +13,15 @@ namespace { - constexpr const char *STRING_ZIP_EXT = ".zip"; - constexpr size_t SIZE_SAVE_META = sizeof(fs::SaveMetaData); + constexpr const char *STRING_ZIP_EXT = ".zip"; + constexpr const char *STRING_JKSV_TEMP = "sdmc:/jksvTemp.zip"; // This is name this so if something fails, people know. + constexpr size_t SIZE_SAVE_META = sizeof(fs::SaveMetaData); } +// Definitions at bottom. +static bool read_and_process_meta(const fslib::Path &targetDir, BackupMenuState::TaskData taskData, sys::ProgressTask *task); +static bool read_and_process_meta(fs::MiniUnzip &unzip, BackupMenuState::TaskData taskData, sys::ProgressTask *task); + void tasks::backup::create_new_backup_local(sys::ProgressTask *task, data::User *user, data::TitleInfo *titleInfo, @@ -26,7 +31,6 @@ void tasks::backup::create_new_backup_local(sys::ProgressTask *task, { if (error::is_null(task)) { return; } - const bool autoUpload = config::get_by_key(config::keys::AUTO_UPLOAD); const bool hasZipExt = std::strstr(target.full_path(), STRING_ZIP_EXT); const uint64_t applicationID = titleInfo->get_application_id(); const FsSaveDataInfo *saveInfo = user->get_save_info_by_id(applicationID); @@ -87,10 +91,12 @@ void tasks::backup::create_new_backup_remote(sys::ProgressTask *task, const uint64_t applicationID = titleInfo->get_application_id(); const FsSaveDataInfo *saveInfo = user->get_save_info_by_id(applicationID); const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; - const char *uploadTemplate = strings::get_by_name(strings::names::BACKUPMENU_STATUS, 1); + const char *uploadTemplate = strings::get_by_name(strings::names::IO_STATUSES, 5); const char *popErrorCreating = strings::get_by_name(strings::names::BACKUPMENU_POPS, 5); const char *popErrorDeleting = strings::get_by_name(strings::names::BACKUPMENU_POPS, 4); const char *popMetaFailed = strings::get_by_name(strings::names::BACKUPMENU_POPS, 8); + const char *popErrorUploading = strings::get_by_name(strings::names::BACKUPMENU_POPS, 10); + if (error::is_null(saveInfo)) { task->finished(); @@ -98,7 +104,7 @@ void tasks::backup::create_new_backup_remote(sys::ProgressTask *task, } // Since we're uploading this right away, no point in it being in the "right" place. - const fslib::Path tempPath{"sdmc:/temp.zip"}; + const fslib::Path tempPath{STRING_JKSV_TEMP}; fs::MiniZip zip{tempPath}; if (!zip.is_open()) { @@ -124,14 +130,11 @@ void tasks::backup::create_new_backup_remote(sys::ProgressTask *task, const std::string status = stringutil::get_formatted_string(uploadTemplate, remoteName.data()); task->set_status(status); } - remote->upload_file(tempPath, remoteName, task); - - // Finally nuke the local one. + const bool uploaded = remote->upload_file(tempPath, remoteName, task); const bool deleteError = error::fslib(fslib::delete_file(tempPath)); - if (deleteError) { ui::PopMessageManager::push_message(popTicks, popErrorDeleting); } + if (!uploaded || deleteError) { ui::PopMessageManager::push_message(popTicks, popErrorUploading); } spawningState->refresh(); - if (killTask) { task->finished(); } } @@ -168,16 +171,18 @@ void tasks::backup::overwrite_backup_remote(sys::ProgressTask *task, BackupMenuS const uint64_t applicationID = titleInfo->get_application_id(); const FsSaveDataInfo *saveInfo = user->get_save_info_by_id(applicationID); remote::Item *target = taskData->remoteItem; - const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; - const char *popErrorDeleting = strings::get_by_name(strings::names::BACKUPMENU_POPS, 4); - const char *popErrorMeta = strings::get_by_name(strings::names::BACKUPMENU_POPS, 8); - if (error::is_null(remote) || error::is_null(saveInfo) || error::is_null(target) || !remote->is_initialized()) + if (error::is_null(remote) || error::is_null(saveInfo) || error::is_null(target)) { task->finished(); return; } - const fslib::Path tempPath{"sdmc:/temp.zip"}; + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popErrorDeleting = strings::get_by_name(strings::names::BACKUPMENU_POPS, 4); + const char *popErrorMeta = strings::get_by_name(strings::names::BACKUPMENU_POPS, 8); + const char *statusUploading = strings::get_by_name(strings::names::IO_STATUSES, 5); + + const fslib::Path tempPath{STRING_JKSV_TEMP}; fs::MiniZip zip{tempPath}; if (!zip.is_open()) { @@ -199,6 +204,11 @@ void tasks::backup::overwrite_backup_remote(sys::ProgressTask *task, BackupMenuS fs::copy_directory_to_zip(fs::DEFAULT_SAVE_ROOT, zip, task); zip.close(); + { + const char *targetName = target->get_name().data(); + const std::string status = stringutil::get_formatted_string(statusUploading, targetName); + task->set_status(status); + } remote->patch_file(target, tempPath, task); const bool deleteError = error::fslib(fslib::delete_file(tempPath)); @@ -213,6 +223,7 @@ void tasks::backup::restore_backup_local(sys::ProgressTask *task, BackupMenuStat if (error::is_null(task)) { return; } + remote::Storage *remote = remote::get_remote_storage(); data::User *user = taskData->user; data::TitleInfo *titleInfo = taskData->titleInfo; const fslib::Path &target = taskData->path; @@ -224,7 +235,8 @@ void tasks::backup::restore_backup_local(sys::ProgressTask *task, BackupMenuStat const uint64_t journalSize = titleInfo->get_journal_size(saveType); const bool autoBackup = config::get_by_key(config::keys::AUTO_BACKUP_ON_RESTORE); - const bool exportZip = config::get_by_key(config::keys::EXPORT_TO_ZIP) || config::get_by_key(config::keys::AUTO_UPLOAD); + const bool autoUpload = config::get_by_key(config::keys::AUTO_UPLOAD); + const bool exportZip = autoUpload || config::get_by_key(config::keys::EXPORT_TO_ZIP); const bool isDir = fslib::directory_exists(target); const bool hasZipExt = std::strstr(target.full_path(), STRING_ZIP_EXT); @@ -232,6 +244,7 @@ void tasks::backup::restore_backup_local(sys::ProgressTask *task, BackupMenuStat const char *popErrorResetting = strings::get_by_name(strings::names::BACKUPMENU_POPS, 2); const char *popErrorCreating = strings::get_by_name(strings::names::BACKUPMENU_POPS, 5); const char *popErrorOpenZip = strings::get_by_name(strings::names::EXTRASMENU_POPS, 7); + const char *statusUploading = strings::get_by_name(strings::names::IO_STATUSES, 5); if (autoBackup) { @@ -249,6 +262,15 @@ void tasks::backup::restore_backup_local(sys::ProgressTask *task, BackupMenuStat if (exportZip) { autoTarget += ".zip"; }; tasks::backup::create_new_backup_local(task, user, titleInfo, autoTarget, spawningState, false); + + // Not sure if this is really needed here, but I'm sure someone will point it out. + if (autoUpload && remote) + { + const std::string status = stringutil::get_formatted_string(statusUploading, autoTarget.get_filename()); + task->set_status(status); + remote->upload_file(autoTarget, autoTarget.get_filename(), task); + fslib::delete_file(autoTarget); // I don't care if this fails, + } } const bool resetError = error::fslib(fslib::delete_directory_recursively(fs::DEFAULT_SAVE_ROOT)); @@ -270,29 +292,16 @@ void tasks::backup::restore_backup_local(sys::ProgressTask *task, BackupMenuStat return; } - { - fs::SaveMetaData metaData{}; - const bool hasMeta = unzip.locate_file(fs::NAME_SAVE_META); - const bool readMeta = hasMeta && unzip.read(&metaData, SIZE_META) == SIZE_META; - if (readMeta) { fs::process_save_meta_data(saveInfo, metaData); }; - } - // To do: Maybe use the meta instead of what the NACP has? + read_and_process_meta(unzip, taskData, task); fs::copy_zip_to_directory(unzip, fs::DEFAULT_SAVE_ROOT, journalSize, fs::DEFAULT_SAVE_MOUNT, task); } else if (isDir) { - { - fs::SaveMetaData metaData{}; - const fslib::Path metaPath{target / fs::NAME_SAVE_META}; - fslib::File metaFile{metaPath, FsOpenMode_Read}; - const bool metaRead = metaFile.is_open() && metaFile.read(&metaData, SIZE_META) == SIZE_META; - if (metaRead) { fs::process_save_meta_data(saveInfo, metaData); } - } - // To do: Same here? - fs::copy_directory(target, fs::DEFAULT_SAVE_ROOT, task, journalSize, fs::DEFAULT_SAVE_MOUNT); + read_and_process_meta(target, taskData, task); + fs::copy_directory_commit(target, fs::DEFAULT_SAVE_ROOT, fs::DEFAULT_SAVE_MOUNT, journalSize, task); } - else { fs::copy_file(target, fs::DEFAULT_SAVE_ROOT, task, journalSize, fs::DEFAULT_SAVE_MOUNT); } - + else { fs::copy_file_commit(target, fs::DEFAULT_SAVE_ROOT, fs::DEFAULT_SAVE_MOUNT, journalSize, task); } + spawningState->refresh(); task->finished(); } @@ -309,9 +318,10 @@ void tasks::backup::restore_backup_remote(sys::ProgressTask *task, BackupMenuSta const char *popErrorDeleting = strings::get_by_name(strings::names::BACKUPMENU_POPS, 4); const char *popErrorDownloading = strings::get_by_name(strings::names::BACKUPMENU_POPS, 9); const char *popErrorProcessingMeta = strings::get_by_name(strings::names::BACKUPMENU_POPS, 11); + const char *downloadingFormat = strings::get_by_name(strings::names::IO_STATUSES, 4); remote::Storage *remote = remote::get_remote_storage(); - if (!remote || !remote->is_initialized()) + if (!remote) { task->finished(); return; @@ -321,6 +331,12 @@ void tasks::backup::restore_backup_remote(sys::ProgressTask *task, BackupMenuSta const char *statusDownloading = strings::get_by_name(strings::names::IO_STATUSES, 4); const fslib::Path tempPath{"sdmc:/temp.zip"}; + { + const char *name = target->get_name().data(); + const std::string status = stringutil::get_formatted_string(downloadingFormat, name); + task->set_status(status); + } + const bool downloaded = remote->download_file(target, tempPath, task); if (!downloaded) { @@ -337,13 +353,11 @@ void tasks::backup::restore_backup_remote(sys::ProgressTask *task, BackupMenuSta return; } - { - fs::SaveMetaData saveMeta{}; - const bool hasMeta = backup.locate_file(fs::NAME_SAVE_META); - const bool loadMeta = hasMeta && backup.read(&saveMeta, SIZE_SAVE_META) == SIZE_SAVE_META; - const bool processMeta = loadMeta && fs::process_save_meta_data(saveInfo, saveMeta); - if (hasMeta && (!loadMeta || !processMeta)) { ui::PopMessageManager::push_message(popTicks, popErrorProcessingMeta); } - } + const uint8_t saveType = user->get_account_save_type(); + const uint64_t journalSize = titleInfo->get_journal_size(saveType); + read_and_process_meta(backup, taskData, task); + fs::copy_zip_to_directory(backup, fs::DEFAULT_SAVE_ROOT, journalSize, fs::DEFAULT_SAVE_MOUNT, task); + backup.close(); const bool deleteError = error::fslib(fslib::delete_file(tempPath)); if (deleteError) { ui::PopMessageManager::push_message(popTicks, popErrorDeleting); } @@ -376,7 +390,7 @@ void tasks::backup::delete_backup_local(sys::Task *task, BackupMenuState::TaskDa void tasks::backup::delete_backup_remote(sys::Task *task, BackupMenuState::TaskData taskData) { remote::Storage *remote = remote::get_remote_storage(); - if (error::is_null(task) || error::is_null(remote) || !remote->is_initialized()) { return; } + if (error::is_null(task) || error::is_null(remote)) { return; } remote::Item *target = taskData->remoteItem; BackupMenuState *spawningState = taskData->spawningState; @@ -400,13 +414,83 @@ void tasks::backup::upload_backup(sys::ProgressTask *task, BackupMenuState::Task { remote::Storage *remote = remote::get_remote_storage(); if (error::is_null(task) || error::is_null(remote)) { return; } + + const fslib::Path &path = taskData->path; BackupMenuState *spawningState = taskData->spawningState; + const char *statusTemplate = strings::get_by_name(strings::names::IO_STATUSES, 5); + { + const char *filename = path.get_filename(); + const std::string status = stringutil::get_formatted_string(statusTemplate, filename); + task->set_status(status); + } // The backup menu should've made sure the remote is pointing to the correct location. - const fslib::Path &path = taskData->path; - remote->upload_file(path, path.get_filename(), task); spawningState->refresh(); task->finished(); } + +static bool read_and_process_meta(const fslib::Path &targetDir, BackupMenuState::TaskData taskData, sys::ProgressTask *task) +{ + if (error::is_null(task)) { return false; } + + data::User *user = taskData->user; + data::TitleInfo *titleInfo = taskData->titleInfo; + const uint64_t applicationID = titleInfo->get_application_id(); + const FsSaveDataInfo *saveInfo = user->get_save_info_by_id(applicationID); + if (error::is_null(saveInfo)) { return false; } + + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popErrorProcessing = strings::get_by_name(strings::names::BACKUPMENU_POPS, 11); + const char *statusProcessing = strings::get_by_name(strings::names::BACKUPMENU_STATUS, 0); + + task->set_status(statusProcessing); + + const fslib::Path metaPath{targetDir / fs::NAME_SAVE_META}; + fslib::File metaFile{metaPath, FsOpenMode_Read}; + if (!metaFile.is_open()) + { + ui::PopMessageManager::push_message(popTicks, popErrorProcessing); + return false; + } + + fs::SaveMetaData metaData{}; + const bool metaRead = metaFile.read(&metaData, SIZE_SAVE_META) == SIZE_SAVE_META; + const bool processed = metaRead && fs::process_save_meta_data(saveInfo, metaData); + if (!metaRead || !processed) + { + ui::PopMessageManager::push_message(popTicks, popErrorProcessing); + return false; + } + + return true; +} + +static bool read_and_process_meta(fs::MiniUnzip &unzip, BackupMenuState::TaskData taskData, sys::ProgressTask *task) +{ + if (error::is_null(task)) { return false; } + + data::User *user = taskData->user; + data::TitleInfo *titleInfo = taskData->titleInfo; + const uint64_t applicationID = titleInfo->get_application_id(); + const FsSaveDataInfo *saveInfo = user->get_save_info_by_id(applicationID); + if (error::is_null(saveInfo)) { return false; } + + const int popTicks = ui::PopMessageManager::DEFAULT_TICKS; + const char *popErrorProcessing = strings::get_by_name(strings::names::BACKUPMENU_POPS, 11); + const char *statusProcessing = strings::get_by_name(strings::names::BACKUPMENU_STATUS, 0); + + task->set_status(statusProcessing); + + fs::SaveMetaData saveMeta{}; + const bool metaFound = unzip.locate_file(fs::NAME_SAVE_META); + const bool metaRead = metaFound && unzip.read(&saveMeta, SIZE_SAVE_META) == SIZE_SAVE_META; + const bool metaProcessed = metaRead && fs::process_save_meta_data(saveInfo, saveMeta); + if (!metaFound || !metaRead || !metaProcessed) + { + ui::PopMessageManager::push_message(popTicks, popErrorProcessing); + return false; + } + return true; +} diff --git a/source/ui/Menu.cpp b/source/ui/Menu.cpp index 7e85b18..791997a 100644 --- a/source/ui/Menu.cpp +++ b/source/ui/Menu.cpp @@ -15,7 +15,9 @@ ui::Menu::Menu(int x, int y, int width, int fontSize, int renderTargetHeight) , m_targetY(y) , m_width(width) , m_fontSize(fontSize) + , m_textY((m_optionHeight / 2) - (m_fontSize / 2) - 1) // This seems to be the best alignment. , m_renderTargetHeight(renderTargetHeight) + , m_optionScroll("temp", 16, 0, m_width - 6, m_optionHeight, m_fontSize, colors::BLUE_GREEN, colors::TRANSPARENT) { // Create render target for options static int MENU_ID = 0; @@ -34,6 +36,8 @@ void ui::Menu::update(bool hasFocus) { if (m_options.empty()) { return; } + m_optionScroll.update(hasFocus); + const bool upPressed = input::button_pressed(HidNpadButton_AnyUp); const bool downPressed = input::button_pressed(HidNpadButton_AnyDown); const bool leftPressed = input::button_pressed(HidNpadButton_AnyLeft); @@ -41,79 +45,79 @@ void ui::Menu::update(bool hasFocus) const bool lShoulderPressed = input::button_pressed(HidNpadButton_L); const bool rShoulderPressed = input::button_pressed(HidNpadButton_R); const int optionsSize = m_options.size(); + const int prevSelected = m_selected; - const bool wrapSelectedUp = upPressed && --m_selected < 0; - const bool wrapSelectedDown = downPressed && ++m_selected >= optionsSize; - const bool stopSelectedLeft = leftPressed && (m_selected -= m_scrollLength) < 0; - const bool stopSelectedRight = rightPressed && (m_selected += m_scrollLength) >= optionsSize; - const bool stopShoulderLeft = lShoulderPressed && (m_selected -= m_scrollLength * 3) < 0; - const bool stopShoulderRight = rShoulderPressed && (m_selected += m_scrollLength * 3) >= optionsSize; + if (upPressed) { --m_selected; } + else if (downPressed) { ++m_selected; } + else if (leftPressed) { m_selected -= m_scrollLength; } + else if (rightPressed) { m_selected += m_scrollLength; } + else if (lShoulderPressed) { m_selected -= m_scrollLength * 3; } + else if (rShoulderPressed) { m_selected += m_scrollLength * 3; } - if (wrapSelectedUp || stopSelectedRight || stopShoulderRight) { m_selected = optionsSize - 1; } - else if (wrapSelectedDown || stopSelectedLeft || stopShoulderLeft) { m_selected = 0; } + if (m_selected < 0) { m_selected = 0; } + else if (m_selected >= optionsSize) { m_selected = optionsSize - 1; } + + // This needs updating. + if (m_selected != prevSelected) { m_optionScroll.set_text(m_options[m_selected], false); } // Don't bother continuing further if there's no reason to scroll. if (optionsSize <= m_maxDisplayOptions) { return; } - if (m_selected < m_scrollLength) { m_targetY = m_originalY; } - else if (m_selected >= static_cast(m_options.size()) - (m_maxDisplayOptions - m_scrollLength)) - { - m_targetY = m_originalY - ((m_options.size() - m_maxDisplayOptions) * m_optionHeight); - } - else if (m_selected >= m_scrollLength) { m_targetY = m_originalY - ((m_selected - m_scrollLength) * m_optionHeight); } + // These are to make things slightly easier to read down here. + const int endScrollPoint = optionsSize - (m_maxDisplayOptions - m_scrollLength); + const int scrolledItems = m_selected - m_scrollLength; + const double scaling = config::get_animation_scaling(); - if (m_y != m_targetY) { m_y += std::ceil((m_targetY - m_y) / config::get_animation_scaling()); } + if (m_selected < m_scrollLength) { m_targetY = m_originalY; } // Don't bother. There's no point. + else if (m_selected >= endScrollPoint) { m_targetY = m_originalY - (optionsSize - m_maxDisplayOptions) * m_optionHeight; } + else if (m_selected >= m_scrollLength) { m_targetY = m_originalY - (scrolledItems * m_optionHeight); } + + if (m_y != m_targetY) { m_y += std::ceil((m_targetY - m_y) / scaling); } } void ui::Menu::render(SDL_Texture *target, bool hasFocus) { if (m_options.empty()) { return; } + SDL_Texture *optionTarget = m_optionTarget->get(); m_colorMod.update(); // I hate doing this. - int targetHeight = 0; - SDL_QueryTexture(target, NULL, NULL, NULL, &targetHeight); - for (int i = 0, tempY = m_y; i < static_cast(m_options.size()); i++, tempY += m_optionHeight) + const int optionSize = m_options.size(); + for (int i = 0, tempY = m_y; i < optionSize; i++, tempY += m_optionHeight) { if (tempY < -m_fontSize) { continue; } - else if (tempY > m_renderTargetHeight) - { - // This is safe to break the loop for. - break; - } + else if (tempY > m_renderTargetHeight) { break; } - // Clear target texture. m_optionTarget->clear(colors::TRANSPARENT); if (i == m_selected) { - if (hasFocus) - { - // render the bounding box - ui::render_bounding_box(target, m_x - 4, tempY - 4, m_width + 8, m_optionHeight + 8, m_colorMod); - } - // render the little rectangle. - sdl::render_rect_fill(m_optionTarget->get(), 8, 8, 4, m_optionHeight - 16, colors::BLUE_GREEN); + if (hasFocus) { ui::render_bounding_box(target, m_x - 4, tempY - 4, m_width + 8, m_optionHeight + 8, m_colorMod); } + sdl::render_rect_fill(optionTarget, 8, 8, 4, m_optionHeight - 16, colors::BLUE_GREEN); + m_optionScroll.render(optionTarget, hasFocus); + } + else + { + const char *option = m_options[i].c_str(); + sdl::text::render(optionTarget, 24, m_textY, m_fontSize, sdl::text::NO_TEXT_WRAP, colors::WHITE, option); } - // render text to target. - sdl::text::render(m_optionTarget->get(), - 24, - ((m_optionHeight / 2) - (m_fontSize / 2)) - 1, - m_fontSize, - sdl::text::NO_TEXT_WRAP, - i == m_selected && hasFocus ? colors::BLUE_GREEN : colors::WHITE, - m_options.at(i).c_str()); // render target to target m_optionTarget->render(target, m_x, tempY); } } -void ui::Menu::add_option(std::string_view newOption) { m_options.push_back(newOption.data()); } +void ui::Menu::add_option(std::string_view newOption) +{ + // This is needed. The initialization is just a temporary one. + if (m_options.empty()) { m_optionScroll.set_text(newOption, false); } + m_options.push_back(newOption.data()); +} void ui::Menu::edit_option(int index, std::string_view newOption) { - if (index < 0 || index >= static_cast(m_options.size())) { return; } + const int optionSize = m_options.size(); + if (index < 0 || index >= optionSize) { return; } m_options[index] = newOption.data(); } diff --git a/source/ui/PopMessageManager.cpp b/source/ui/PopMessageManager.cpp index 676d739..49244ab 100644 --- a/source/ui/PopMessageManager.cpp +++ b/source/ui/PopMessageManager.cpp @@ -17,47 +17,53 @@ namespace void ui::PopMessageManager::update() { + static constexpr double COORD_INIT_Y = 594.0f; + // Grab instance. PopMessageManager &manager = PopMessageManager::get_instance(); - auto &messageQueue = manager.m_messageQueue; - auto &messages = manager.m_messages; + auto &messageQueue = manager.m_messageQueue; + auto &messages = manager.m_messages; + std::mutex &messageMutex = manager.m_messageMutex; + std::mutex &queueMutex = manager.m_queueMutex; - // Bail if the queue is empty. - if (!messageQueue.empty()) { - // Loop through the queue and process it so we don't wind up with black characters. - for (auto &[displayTicks, currentMessage] : messageQueue) + std::lock_guard queueGuard{queueMutex}; + if (!messageQueue.empty()) { - const size_t messageWidth = sdl::text::get_width(32, currentMessage.c_str()) + 32; - const ui::PopMessage newMessage = {.y = 720, - .targetY = 720, - .width = messageWidth, - .message = std::move(currentMessage), - .timer = sys::Timer{displayTicks}}; + // Loop through the queue and process it so we don't wind up with black characters. + for (auto &[displayTicks, currentMessage] : messageQueue) + { + const size_t messageWidth = sdl::text::get_width(32, currentMessage.c_str()) + 32; + const ui::PopMessage newMessage = {.y = 720, + .targetY = 720, + .width = messageWidth, + .message = std::move(currentMessage), + .timer = sys::Timer{static_cast(displayTicks)}}; - messages.push_back(std::move(newMessage)); + messages.push_back(std::move(newMessage)); + } + messageQueue.clear(); } - messageQueue.clear(); } // Update all the messages. // This is the first Y position a message should be displayed at.; - double currentY = 594.0f; + double currentY = COORD_INIT_Y; const double animationScaling = config::get_animation_scaling(); - const int messageCount = messages.size(); - for (int i = 0; i < messageCount; i++) + std::lock_guard messageGuard{messageMutex}; + for (auto message = messages.begin(); message != messages.end();) { - ui::PopMessage &message = messages.at(i); - if (message.timer.is_triggered()) + if (message->timer.is_triggered()) { - messages.erase(messages.begin() + i); + message = messages.erase(message); continue; } - if (message.targetY != currentY) { message.targetY = currentY; } - if (message.y != message.targetY) { message.y += (message.targetY - message.y) / animationScaling; } + if (message->targetY != currentY) { message->targetY = currentY; } + else if (message->y != message->targetY) { message->y += (message->targetY - message->y) / animationScaling; } currentY -= 56; + ++message; } } @@ -65,7 +71,9 @@ void ui::PopMessageManager::render() { PopMessageManager &manager = PopMessageManager::get_instance(); auto &messages = manager.m_messages; + std::mutex &messageMutex = manager.m_messageMutex; + std::lock_guard messageGuard{messageMutex}; for (const auto &message : messages) { ui::render_dialog_box(nullptr, 20, message.y - 6, message.width, 52); @@ -76,15 +84,10 @@ void ui::PopMessageManager::render() void ui::PopMessageManager::push_message(int displayTicks, std::string_view message) { PopMessageManager &manager = PopMessageManager::get_instance(); - std::mutex &messageMutex = manager.m_messageMutex; + std::mutex &queueMutex = manager.m_queueMutex; auto &messageQueue = manager.m_messageQueue; - auto &messages = manager.m_messages; - - const bool empty = messages.empty(); - const bool backMatches = !empty && messages.back().message == message; - if (backMatches) { return; } + std::lock_guard queueGuard(queueMutex); auto queuePair = std::make_pair(displayTicks, std::string{message}); - std::scoped_lock messageLock(messageMutex); messageQueue.push_back(std::move(queuePair)); } diff --git a/source/ui/TextScroll.cpp b/source/ui/TextScroll.cpp index ebd6827..4d935b3 100644 --- a/source/ui/TextScroll.cpp +++ b/source/ui/TextScroll.cpp @@ -11,69 +11,125 @@ namespace constexpr int SIZE_TEXT_GAP = 0; } // namespace -ui::TextScroll::TextScroll(std::string_view text, int fontSize, int availableWidth, int y, bool center, sdl::Color color) +ui::TextScroll::TextScroll(std::string_view text, + int x, + int y, + int width, + int height, + int fontSize, + sdl::Color textColor, + sdl::Color clearColor, + bool center) { - TextScroll::create(text, fontSize, availableWidth, y, center, color); + TextScroll::create(text, x, y, width, height, fontSize, textColor, clearColor, center); } -void ui::TextScroll::create(std::string_view text, int fontSize, int availableWidth, int y, bool center, sdl::Color color) +void ui::TextScroll::create(std::string_view text, + int x, + int y, + int width, + int height, + int fontSize, + sdl::Color textColor, + sdl::Color clearColor, + bool center) { - // Copy the text and stuff. - m_text = text; - m_y = y; - m_fontSize = fontSize; - m_textColor = color; + static int TARGET_ID{}; + + m_renderX = x; + m_renderY = y; + m_fontSize = fontSize; + m_textColor = textColor; + m_clearColor = clearColor; + m_targetWidth = width; + m_targetHeight = height; + m_textY = (m_targetHeight / 2) - (m_fontSize / 2) - 1; // This gets the job done, but could be better. m_scrollTimer.start(TICKS_SCROLL_TRIGGER); - // Get the width and calculate X. - m_textWidth = sdl::text::get_width(m_fontSize, m_text.c_str()); - if (m_textWidth > availableWidth) { - // Set the X coordinate to 8 and make sure this knows it needs to scroll. - m_x = 8; + const std::string targetName = "textScroll_" + std::to_string(TARGET_ID++); + m_renderTarget = sdl::TextureManager::create_load_texture(targetName, + m_targetWidth, + m_targetHeight, + SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET); + } + + TextScroll::set_text(text, center); +} + +void ui::TextScroll::set_text(std::string_view text, bool center) +{ + m_text = text; + m_textWidth = sdl::text::get_width(m_fontSize, m_text.c_str()); + m_textScrollTriggered = false; + + if (m_textWidth > m_targetWidth - 16) + { + m_textX = 8; m_textScrolling = true; + m_scrollTimer.restart(); } else if (center) { - // Just center it. - m_x = (availableWidth / 2) - (m_textWidth / 2); + m_textX = (m_targetWidth / 2) - (m_textWidth / 2); + m_textScrolling = false; } else { - // Just set this to 8. To do: Figure out how to make this cleaner. - m_x = 8; + m_textX = 8; + m_textScrolling = false; } } void ui::TextScroll::update(bool hasFocus) { - // I don't think needs to care about having focus. - if (m_textScrolling && m_scrollTimer.is_triggered()) + // I don't think this needs to care about having focus. + const int invertedWidth = -(m_textWidth + SIZE_TEXT_GAP); + const bool scrollTriggered = hasFocus && m_textScrolling && m_scrollTimer.is_triggered(); + const bool keepScrolling = m_textScrollTriggered && m_textX > invertedWidth; + const bool scrollFinished = m_textScrollTriggered && m_textX <= invertedWidth; + + if (scrollTriggered) { - m_x -= 2; + m_textX -= 2; m_textScrollTriggered = true; } - else if (m_textScrollTriggered && m_x > -(m_textWidth + SIZE_TEXT_GAP)) { m_x -= 2; } - else if (m_textScrollTriggered && m_x <= -(m_textWidth + SIZE_TEXT_GAP)) + else if (keepScrolling) { m_textX -= 2; } + else if (scrollFinished) { - // This will snap the text back to where it was, but the user won't even notice it. It just looks like it's scrolling. - m_x = 8; + // This will snap the text back. You can't see it though. + m_textX = 8; m_textScrollTriggered = false; m_scrollTimer.restart(); } } +void ui::TextScroll::set_xy(int x, int y) +{ + m_renderX = x; + m_renderY = y; +} + void ui::TextScroll::render(SDL_Texture *target, bool hasFocus) { - // If we don't need to scroll the text, just render it as-is centered according to the width passed earlier. + m_renderTarget->clear(m_clearColor); + SDL_Texture *renderTarget = m_renderTarget->get(); + if (!m_textScrolling) { - sdl::text::render(target, m_x, m_y, m_fontSize, sdl::text::NO_TEXT_WRAP, m_textColor, m_text.c_str()); + sdl::text::render(renderTarget, m_textX, m_textY, m_fontSize, sdl::text::NO_TEXT_WRAP, m_textColor, m_text.c_str()); } else { // We're going to render text twice so it looks like it's scrolling and doesn't end. Ever. - sdl::text::render(target, m_x, m_y, m_fontSize, sdl::text::NO_TEXT_WRAP, m_textColor, m_text.c_str()); - sdl::text::render(target, m_x + m_textWidth + 8, m_y, m_fontSize, sdl::text::NO_TEXT_WRAP, m_textColor, m_text.c_str()); + sdl::text::render(renderTarget, m_textX, m_textY, m_fontSize, sdl::text::NO_TEXT_WRAP, m_textColor, m_text.c_str()); + sdl::text::render(renderTarget, + m_textX + m_textWidth + 8, + m_textY, + m_fontSize, + sdl::text::NO_TEXT_WRAP, + m_textColor, + m_text.c_str()); } + m_renderTarget->render(target, m_renderX, m_renderY); } diff --git a/source/ui/TitleTile.cpp b/source/ui/TitleTile.cpp index 7bd494b..d1e6d13 100644 --- a/source/ui/TitleTile.cpp +++ b/source/ui/TitleTile.cpp @@ -1,6 +1,7 @@ #include "ui/TitleTile.hpp" #include "colors.hpp" +#include "logger.hpp" ui::TitleTile::TitleTile(bool isFavorite, sdl::SharedTexture icon) : m_isFavorite(isFavorite) @@ -8,28 +9,33 @@ ui::TitleTile::TitleTile(bool isFavorite, sdl::SharedTexture icon) void ui::TitleTile::update(bool isSelected) { - if (isSelected && m_renderWidth < 164) + static constexpr int BASE_WIDTH = 128; + static constexpr int EXPAND_WIDTH = 176; + static constexpr int INCREASE = 16; + static constexpr int DECREASE = 8; + + if (isSelected && m_renderWidth != EXPAND_WIDTH) { // I think it's safe to assume both are too small. - m_renderWidth += 18; - m_renderHeight += 18; + m_renderWidth += INCREASE; + m_renderHeight += INCREASE; } - else if (!isSelected && m_renderWidth > 128) + else if (!isSelected && m_renderWidth != BASE_WIDTH) { - m_renderWidth -= 9; - m_renderHeight -= 9; + m_renderWidth -= DECREASE; + m_renderHeight -= DECREASE; } } void ui::TitleTile::render(SDL_Texture *target, int x, int y) { - int renderX = x - ((m_renderWidth - 128) / 2); - int renderY = y - ((m_renderHeight - 128) / 2); + const int renderX = x - ((m_renderWidth - 128) / 2); + const int renderY = y - ((m_renderHeight - 128) / 2); m_icon->render_stretched(target, renderX, renderY, m_renderWidth, m_renderHeight); if (m_isFavorite) { - sdl::text::render(target, renderX + 4, renderY + 2, 28, sdl::text::NO_TEXT_WRAP, colors::PINK, "\uE017"); + sdl::text::render(target, renderX + 2, renderY + 2, 28, sdl::text::NO_TEXT_WRAP, colors::PINK, "\uE017"); } } diff --git a/source/ui/TitleView.cpp b/source/ui/TitleView.cpp index e6a4f1b..b4df5d2 100644 --- a/source/ui/TitleView.cpp +++ b/source/ui/TitleView.cpp @@ -21,87 +21,102 @@ ui::TitleView::TitleView(data::User *user) void ui::TitleView::update(bool hasFocus) { + // These are named like this because of where they sit on the screen. + static constexpr double UPPER_THRESHOLD = 32.0f; + static constexpr double LOWER_THRESHOLD = 388.0f; + if (m_titleTiles.empty()) { return; } // Update pulse if (hasFocus) { m_colorMod.update(); } - // Input. - int totalTiles = m_titleTiles.size() - 1; - if (input::button_pressed(HidNpadButton_AnyUp) && (m_selected -= ICON_ROW_SIZE) < 0) { m_selected = 0; } - else if (input::button_pressed(HidNpadButton_AnyDown) && (m_selected += ICON_ROW_SIZE) > totalTiles) - { - m_selected = totalTiles; - } - else if (input::button_pressed(HidNpadButton_AnyLeft) && m_selected > 0) { --m_selected; } - else if (input::button_pressed(HidNpadButton_AnyRight) && m_selected < totalTiles) { ++m_selected; } - else if (input::button_pressed(HidNpadButton_L) && (m_selected -= 21) < 0) { m_selected = 0; } - else if (input::button_pressed(HidNpadButton_R) && (m_selected += 21) > totalTiles) { m_selected = totalTiles; } + const bool upPressed = input::button_pressed(HidNpadButton_AnyUp); + const bool downPressed = input::button_pressed(HidNpadButton_AnyDown); + const bool leftPressed = input::button_pressed(HidNpadButton_AnyLeft); + const bool rightPressed = input::button_pressed(HidNpadButton_AnyRight); + const bool lShoulderPressed = input::button_pressed(HidNpadButton_L); + const bool rShoulderPressed = input::button_pressed(HidNpadButton_R); + const int totalTiles = m_titleTiles.size() - 1; - double scaling = config::get_animation_scaling(); - if (m_selectedY > 388.0f) { m_y += std::ceil((388.0f - m_selectedY) / scaling); } - else if (m_selectedY < 28.0f) { m_y += std::ceil((28.0f - m_selectedY) / scaling); } + if (upPressed) { m_selected -= ICON_ROW_SIZE; } + else if (leftPressed) { --m_selected; } + else if (lShoulderPressed) { m_selected -= ICON_ROW_SIZE * 3; } + else if (downPressed) { m_selected += ICON_ROW_SIZE; } + else if (rightPressed) { ++m_selected; } + else if (rShoulderPressed) { m_selected += ICON_ROW_SIZE * 3; } - for (size_t i = 0; i < m_titleTiles.size(); i++) + if (m_selected < 0) { m_selected = 0; } + else if (m_selected > totalTiles) { m_selected = totalTiles; } + + const double scaling = config::get_animation_scaling(); + if (m_selectedY < UPPER_THRESHOLD) { m_y += std::ceil((UPPER_THRESHOLD - m_selectedY) / scaling); } + else if (m_selectedY > LOWER_THRESHOLD) { m_y += std::ceil((LOWER_THRESHOLD - m_selectedY) / scaling); } + + const int tileCount = m_titleTiles.size(); + for (int i = 0; i < tileCount; i++) { - m_titleTiles.at(i).update(m_selected == static_cast(i) ? true : false); + const bool isSelected = m_selected == i; + ui::TitleTile &tile = m_titleTiles[i]; + + tile.update(isSelected); } } void ui::TitleView::render(SDL_Texture *target, bool hasFocus) { + static constexpr int TILE_SPACE_VERT = 144; + static constexpr int TILE_SPACE_HOR = 144; + if (m_titleTiles.empty()) { return; } - for (int i = 0, tempY = m_y; i < static_cast(m_titleTiles.size()); tempY += 144) + const int tileCount = m_titleTiles.size(); + for (int i = 0, tempY = m_y; i < tileCount; i += ICON_ROW_SIZE, tempY += TILE_SPACE_VERT) { - int endRow = i + 7; - for (int j = i, tempX = 32; j < endRow; j++, i++, tempX += 144) + const int endRow = i + ICON_ROW_SIZE; + for (int j = i, tempX = 32; j < endRow && j < tileCount; j++, tempX += TILE_SPACE_HOR) { - if (i >= static_cast(m_titleTiles.size())) { break; } - - // Save the X and Y to render the selected tile over the rest. - if (i == m_selected) + if (j == m_selected) { m_selectedX = tempX; m_selectedY = tempY; continue; } - // Just render - m_titleTiles.at(i).render(target, tempX, tempY); + + ui::TitleTile &tile = m_titleTiles[j]; + tile.render(target, tempX, tempY); } } - // Now render the selected title. if (hasFocus) { - sdl::render_rect_fill(target, m_selectedX - 23, m_selectedY - 23, 174, 174, colors::CLEAR_COLOR); - ui::render_bounding_box(target, m_selectedX - 24, m_selectedY - 24, 176, 176, m_colorMod); + sdl::render_rect_fill(target, m_selectedX - 29, m_selectedY - 29, 187, 187, colors::CLEAR_COLOR); + ui::render_bounding_box(target, m_selectedX - 30, m_selectedY - 30, 188, 188, m_colorMod); } - m_titleTiles.at(m_selected).render(target, m_selectedX, m_selectedY); + ui::TitleTile &selectedTile = m_titleTiles.at(m_selected); + selectedTile.render(target, m_selectedX, m_selectedY); } int ui::TitleView::get_selected() const { return m_selected; } void ui::TitleView::refresh() { - // Clear the current tiles. m_titleTiles.clear(); + const int entryCount = m_user->get_total_data_entries(); - // Loop through the user's data entries. - int userEntryCount = m_user->get_total_data_entries(); - - for (int i = 0; i < userEntryCount; i++) + for (int i = 0; i < entryCount; i++) { - // Get pointer to data from user save index I. - data::TitleInfo *currentTitleInfo = data::get_title_info_by_id(m_user->get_application_id_at(i)); + const uint64_t applicationID = m_user->get_application_id_at(i); + const bool isFavorite = config::is_favorite(applicationID); + data::TitleInfo *titleInfo = data::get_title_info_by_id(applicationID); + sdl::SharedTexture icon = titleInfo->get_icon(); // I don't like this but w/e. - // Emplace is faster than push - m_titleTiles.emplace_back(config::is_favorite(m_user->get_application_id_at(i)), currentTitleInfo->get_icon()); + m_titleTiles.emplace_back(isFavorite, icon); } - // Just to be sure. - if (m_selected > 0 && m_selected >= static_cast(m_titleTiles.size())) { m_selected = m_titleTiles.size() - 1; } + const int tileCount = m_titleTiles.size() - 1; + if (tileCount <= 0) { m_selected = 0; } + else if (m_selected > tileCount) { m_selected = tileCount; } } void ui::TitleView::reset()