├── .gitattributes ├── .gitignore ├── .gitmodules ├── Application ├── Makefile ├── icon.jpg ├── include │ ├── Application.ExportJob.hpp │ ├── Application.ImportJob.hpp │ ├── Application.hpp │ ├── Config.hpp │ ├── Types.hpp │ ├── nx │ │ ├── PlayData.hpp │ │ ├── Title.hpp │ │ └── User.hpp │ ├── ui │ │ ├── Theme.hpp │ │ ├── ThemePresets.hpp │ │ ├── element │ │ │ ├── Graph.hpp │ │ │ ├── ListActivity.hpp │ │ │ ├── ListAdjust.hpp │ │ │ ├── ListColour.hpp │ │ │ ├── ListHide.hpp │ │ │ ├── ListSession.hpp │ │ │ ├── ListUser.hpp │ │ │ └── SortedList.hpp │ │ ├── overlay │ │ │ ├── ColourPicker.hpp │ │ │ ├── PlaySession.hpp │ │ │ ├── PlaytimePicker.hpp │ │ │ └── ProgressBox.hpp │ │ └── screen │ │ │ ├── AdjustPlaytime.hpp │ │ │ ├── AllActivity.hpp │ │ │ ├── CustomTheme.hpp │ │ │ ├── Details.hpp │ │ │ ├── HideTitles.hpp │ │ │ ├── RecentActivity.hpp │ │ │ ├── Settings.hpp │ │ │ ├── Update.hpp │ │ │ └── UserSelect.hpp │ └── utils │ │ ├── Curl.hpp │ │ ├── Forwarder.hpp │ │ ├── Lang.hpp │ │ ├── NX.hpp │ │ ├── ThemeUtils.hpp │ │ ├── Time.hpp │ │ ├── UpdateUtils.hpp │ │ └── Utils.hpp ├── romfs │ └── icon │ │ ├── download.png │ │ └── no_icon.jpg └── source │ ├── Application.ExportJob.cpp │ ├── Application.ImportJob.cpp │ ├── Application.cpp │ ├── Config.cpp │ ├── Types.cpp │ ├── main.cpp │ ├── nx │ ├── PlayData.cpp │ ├── Title.cpp │ └── User.cpp │ ├── ui │ ├── Theme.cpp │ ├── element │ │ ├── Graph.cpp │ │ ├── ListActivity.cpp │ │ ├── ListAdjust.cpp │ │ ├── ListColour.cpp │ │ ├── ListHide.cpp │ │ ├── ListSession.cpp │ │ ├── ListUser.cpp │ │ └── SortedList.cpp │ ├── overlay │ │ ├── ColourPicker.cpp │ │ ├── PlaySession.cpp │ │ ├── PlaytimePicker.cpp │ │ └── ProgressBox.cpp │ └── screen │ │ ├── AdjustPlaytime.cpp │ │ ├── AllActivity.cpp │ │ ├── CustomTheme.cpp │ │ ├── Details.cpp │ │ ├── HideTitles.cpp │ │ ├── RecentActivity.cpp │ │ ├── Settings.cpp │ │ ├── Update.cpp │ │ └── UserSelect.cpp │ └── utils │ ├── Curl.cpp │ ├── Forwarder.cpp │ ├── Lang.cpp │ ├── NX.cpp │ ├── ThemeUtils.cpp │ ├── Time.cpp │ ├── UpdateUtils.cpp │ └── Utils.cpp ├── Forwarder ├── LICENSE.md ├── Makefile ├── README.md ├── exefs.json └── source │ ├── main.c │ └── trampoline.s ├── Images ├── sc_activity.jpg ├── sc_detailed.jpg ├── sc_recent.jpg └── shop.png ├── LICENSE ├── Makefile └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | include/utils/JSON.hpp linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .VSCodeCounter 3 | build/ 4 | sdcard/ 5 | *.elf 6 | *.nro 7 | *.nacp 8 | *.psd 9 | *.nsp 10 | *.nso 11 | *.npdm -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Translations"] 2 | path = Application/romfs/lang 3 | url = https://github.com/tallbl0nde/NX-Activity-Log-Translations.git 4 | [submodule "libs/json"] 5 | path = Application/libs/json 6 | url = https://github.com/nlohmann/json.git 7 | [submodule "Application/libs/SimpleIniParser"] 8 | path = Application/libs/SimpleIniParser 9 | url = https://git.nicholemattera.com/NicholeMattera/Simple-INI-Parser 10 | [submodule "Application/libs/Aether"] 11 | path = Application/libs/Aether 12 | url = https://github.com/tallbl0nde/Aether 13 | -------------------------------------------------------------------------------- /Application/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tallbl0nde/NX-Activity-Log/b3aab30c5f574c78144ca1ebe4875ef9ccbd95e6/Application/icon.jpg -------------------------------------------------------------------------------- /Application/include/Application.ExportJob.hpp: -------------------------------------------------------------------------------- 1 | #ifndef APPLICATION_EXPORTJOB_HPP 2 | #define APPLICATION_EXPORTJOB_HPP 3 | 4 | #include "Application.hpp" 5 | #include "Aether/ThreadPool.Job.hpp" 6 | 7 | namespace Main { 8 | // Implements an Aether thread pool job to export the current play data 9 | // read within the application to a JSON file on disk. 10 | class Application::ExportJob : public Aether::ThreadPool::Job { 11 | private: 12 | // Pointer to application object 13 | Application * app; 14 | 15 | // Atomic to update with status 16 | std::atomic & percent; 17 | 18 | protected: 19 | // Implements to perform the export 20 | void work(); 21 | 22 | public: 23 | // Constructor takes application object 24 | ExportJob(Application *, std::atomic &); 25 | }; 26 | }; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /Application/include/Application.ImportJob.hpp: -------------------------------------------------------------------------------- 1 | #ifndef APPLICATION_IMPORTJOB_HPP 2 | #define APPLICATION_IMPORTJOB_HPP 3 | 4 | #include "Application.hpp" 5 | #include "Aether/ThreadPool.Job.hpp" 6 | 7 | namespace Main { 8 | // Implements an Aether thread pool job to import play data from a JSON 9 | // to another JSON used internally. 10 | class Application::ImportJob : public Aether::ThreadPool::Job { 11 | private: 12 | // Pointer to application object 13 | Application * app; 14 | 15 | // Atomic to update with status 16 | std::atomic & percent; 17 | 18 | protected: 19 | // Implements to perform the import 20 | void work(); 21 | 22 | public: 23 | // Constructor takes application object 24 | ImportJob(Application *, std::atomic &); 25 | }; 26 | }; 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /Application/include/Application.hpp: -------------------------------------------------------------------------------- 1 | #ifndef APPLICATION_HPP 2 | #define APPLICATION_HPP 3 | 4 | #include 5 | #include "Config.hpp" 6 | #include 7 | #include "utils/NX.hpp" 8 | #include "nx/PlayData.hpp" 9 | #include 10 | #include "ui/Theme.hpp" 11 | #include "utils/Time.hpp" 12 | 13 | namespace Screen { 14 | class AdjustPlaytime; 15 | class AllActivity; 16 | class CustomTheme; 17 | class Details; 18 | class HideTitles; 19 | class RecentActivity; 20 | class Settings; 21 | class Update; 22 | class UserSelect; 23 | }; 24 | 25 | enum class ReinitState { 26 | False, 27 | True, 28 | Wait 29 | }; 30 | 31 | namespace Main { 32 | // The Application class represents the "root" object of the app. It stores/handles all states 33 | // and objects used through the app 34 | class Application { 35 | private: 36 | // Forward declare job classes 37 | class ExportJob; 38 | class ImportJob; 39 | 40 | // Main aether object used for rendering 41 | Aether::Window * window; 42 | 43 | // Screens of the app 44 | Screen::AdjustPlaytime * scAdjustPlaytime; 45 | Screen::AllActivity * scAllActivity; 46 | Screen::CustomTheme * scCustomTheme; 47 | Screen::Details * scDetails; 48 | Screen::HideTitles * scHideTitles; 49 | Screen::RecentActivity * scRecentActivity; 50 | Screen::Settings * scSettings; 51 | Screen::Update * scUpdate; 52 | Screen::UserSelect * scUserSelect; 53 | 54 | // ID of current screen 55 | ScreenID screen; 56 | // Reason for screen creation (set to Normal after reinit) 57 | ScreenCreate createReason; 58 | // Set true by reinitScreens() in order to recreate screens 59 | // before next loop 60 | ReinitState reinitScreens_; 61 | // Create screens 62 | void createScreens(); 63 | // Delete screens 64 | void deleteScreens(); 65 | // Stack of ScreenIDs matching pushed screens 66 | std::stack screenStack; 67 | 68 | // Config object allows interfacing with config file 69 | Config * config_; 70 | // PlayData object used for all play stats 71 | NX::PlayData * playdata_; 72 | // Stores current theme colours 73 | Theme * theme_; 74 | 75 | // Set true when an update is available 76 | std::atomic hasUpdate_; 77 | // Future for update check thread 78 | std::future updateThread; 79 | // Function to check for update 80 | void checkForUpdate(); 81 | 82 | // Time to view recent activity, etc... 83 | struct tm tm; 84 | // Copy of above struct used to determine if changed 85 | struct tm tmCopy; 86 | // Period of time to view 87 | ViewPeriod viewType; 88 | // Copy of view period to determine if changed 89 | ViewPeriod viewTypeCopy; 90 | // Set true when either have been changed 91 | bool timeChanged_; 92 | 93 | // Set true if launched via user page 94 | bool isUserPage_; 95 | // Vector of users 96 | std::vector users; 97 | // Index of selected user 98 | unsigned short userIdx; 99 | 100 | // Vector of titles 101 | std::vector titles; 102 | // Index of "active" title (used for specific screens) 103 | unsigned int titleIdx; 104 | 105 | // Date picker overlay 106 | Aether::DateTime * dtpicker; 107 | // ViewPeriod picker 108 | Aether::PopupList * periodpicker; 109 | 110 | public: 111 | // Constructor inits Aether, screens + other objects 112 | Application(); 113 | 114 | // Destroys and recreates screens (effectively a restart without restarting in terms of UI?) 115 | void reinitScreens(ScreenCreate); 116 | 117 | // Wrapper for window function 118 | void setHoldDelay(int); 119 | 120 | // Pass an overlay element in order to render 121 | // Element is not deleted when closed! 122 | void addOverlay(Aether::Overlay *); 123 | 124 | // Pass screen enum to change to it 125 | void setScreen(ScreenID); 126 | // Push current screen on stack (i.e. keep in memory) 127 | void pushScreen(); 128 | // Pop screen from stack 129 | void popScreen(); 130 | 131 | // Decrease/increase date tm by one d/m/y 132 | void decreaseDate(); 133 | void increaseDate(); 134 | // Create date picker overlay 135 | void createDatePicker(); 136 | // Create view period picker overlay 137 | void createPeriodPicker(); 138 | 139 | // Returns Config object 140 | Config * config(); 141 | // Returns PlayData object 142 | NX::PlayData * playdata(); 143 | // Returns theme object 144 | Theme * theme(); 145 | // (Re)set the display theme variables for highlight/select/etc... using values in theme object 146 | void setDisplayTheme(); 147 | 148 | // True if an update is available 149 | bool hasUpdate(); 150 | 151 | // Returns copy of tm struct (can only be set by this class) 152 | struct tm time(); 153 | // Return current viewperiod (also only set by this class) 154 | ViewPeriod viewPeriod(); 155 | // Has the time struct or period been changed? 156 | bool timeChanged(); 157 | 158 | // Returns isUserPage_ 159 | bool isUserPage(); 160 | 161 | // Returns user object of active user 162 | NX::User * activeUser(); 163 | // Sets active user given index 164 | void setActiveUser(unsigned short); 165 | 166 | // Returns reference to titles vector 167 | std::vector titleVector(); 168 | // Returns currently selected title 169 | NX::Title * activeTitle(); 170 | // Set active title given index 171 | void setActiveTitle(unsigned int); 172 | 173 | // Imports play data from a JSON (by matching usernames). 174 | // This operation is performed an it's own thread, updating the provided atomic. 175 | void importFromJSON(std::atomic & percent); 176 | // Exports all stored play data as a JSON. 177 | // This operation is performed an it's own thread, updating the provided atomic. 178 | void exportToJSON(std::atomic & percent); 179 | 180 | // Handles window loop 181 | void run(); 182 | // Call to stop window loop 183 | void exit(); 184 | 185 | // Destructor frees memory and quits Aether 186 | ~Application(); 187 | }; 188 | }; 189 | 190 | #endif 191 | -------------------------------------------------------------------------------- /Application/include/Config.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_HPP 2 | #define CONFIG_HPP 3 | 4 | #include 5 | #include "SimpleIniParser.hpp" 6 | #include "Types.hpp" 7 | 8 | struct AdjustmentValue { 9 | uint64_t titleID; 10 | AccountUid userID; 11 | int value; 12 | }; 13 | 14 | namespace Main { 15 | // Reads/writes and stores config of application 16 | class Config { 17 | private: 18 | // Ini object to read/write to/from file 19 | simpleIniParser::Ini * ini; 20 | 21 | // Vector of adjustments for each title ID 22 | std::vector adjustments; 23 | 24 | // Vector of hidden title IDs 25 | std::vector hidden; 26 | 27 | bool gGraph_; 28 | bool gIs24H_; 29 | Language gLang_; 30 | ThemeType gTheme_; 31 | ScreenID lScreen_; 32 | SortType lSort_; 33 | ViewPeriod lView_; 34 | bool tImage_; 35 | 36 | public: 37 | // Sets ini to nullptr 38 | Config(); 39 | // Deletes ini 40 | ~Config(); 41 | 42 | // Reads in config from file (call first otherwise expect crashes!) 43 | void readConfig(); 44 | // Writes config to file 45 | void writeConfig(); 46 | 47 | // Getters + setters for all settings' 48 | std::vector adjustmentValues(); 49 | std::vector hiddenTitles(); 50 | bool gGraph(); 51 | bool gIs24H(); 52 | Language gLang(); 53 | ThemeType gTheme(); 54 | ScreenID lScreen(); 55 | SortType lSort(); 56 | ViewPeriod lView(); 57 | bool tImage(); 58 | 59 | bool setAdjustmentValues(const std::vector &); 60 | bool setHiddenTitles(const std::vector &); 61 | void setGGraph(bool); 62 | void setGIs24H(bool); 63 | void setGLang(Language); 64 | void setGTheme(ThemeType); 65 | void setLScreen(ScreenID); 66 | void setLSort(SortType); 67 | void setLView(ViewPeriod); 68 | void setTImage(bool); 69 | }; 70 | }; 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /Application/include/Types.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_HPP 2 | #define TYPES_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | #include 6 | #include 7 | 8 | // App language 9 | enum Language { 10 | Default, // Match system language 11 | English, 12 | French, 13 | German, 14 | Spanish, 15 | Italian, 16 | Portugese, 17 | Russian, 18 | Turkish, 19 | ChineseTraditional, 20 | Chinese, 21 | Korean, 22 | TotalLanguages // Total number of languages (only used for iterating) 23 | }; 24 | // Return string matching language 25 | std::string toString(Language); 26 | 27 | // Type of screen creation 28 | enum class ScreenCreate { 29 | Normal, // No specific reason 30 | Language, // Language was changed 31 | Theme, // Theme was changed 32 | ThemeEdit // Theme was customized 33 | }; 34 | 35 | // Enumeration for screens (allows for easy switching) 36 | enum ScreenID { 37 | UserSelect, 38 | AllActivity, 39 | RecentActivity, 40 | Settings, 41 | AdjustPlaytime, 42 | HideTitles, 43 | Details, 44 | Update, 45 | CustomTheme 46 | }; 47 | 48 | // Enumerations for sorting methods 49 | enum SortType { 50 | AlphaAsc, // Alphabetically (A-Z) 51 | FirstPlayed, // First played (earliest first) 52 | LastPlayed, // Last played (most recent first) 53 | HoursAsc, // Playtime (ascending) 54 | HoursDec, // Playtime (descending) 55 | LaunchAsc, // Launches (ascending) 56 | LaunchDec, // Launches (descending) 57 | TotalSorts // Total number of sort methods (only used for iterating) 58 | }; 59 | // Return string matching sort type 60 | std::string toString(SortType); 61 | 62 | enum ThemeType { 63 | Auto, // Note this isn't actually set in the object 64 | Light, // Basic White 65 | Dark, // Basic Black 66 | Custom, // Custom set by user 67 | TotalThemes // Total number of themes (only used for iterating) 68 | }; 69 | // Return string matching theme type 70 | std::string toString(ThemeType); 71 | 72 | // Variables (colours) forming a theme 73 | typedef struct { 74 | Aether::Colour accent; 75 | Aether::Colour altBG; 76 | Aether::Colour bg; 77 | Aether::Colour fg; 78 | Aether::Colour highlight1; 79 | Aether::Colour highlight2; 80 | Aether::Colour highlightBG; 81 | Aether::Colour mutedLine; 82 | Aether::Colour mutedText; 83 | Aether::Colour selected; 84 | Aether::Colour text; 85 | } ThemeSet; 86 | 87 | typedef struct { 88 | std::string changelog; // Changelog (with markdown syntax) 89 | bool success; // Set true if the query was successful 90 | std::string url; // URL of .nro file to download 91 | std::string version; // Version of latest release (vX.X.X) 92 | } UpdateData; 93 | 94 | // Period to view recent stats for 95 | enum ViewPeriod { 96 | Day, 97 | Month, 98 | Year, 99 | TotalViews // Total number of view types (only used for iterating) 100 | }; 101 | // Return string matching theme type 102 | std::string toString(ViewPeriod); 103 | 104 | // Just so function decs. are easier to read 105 | typedef u64 TitleID; 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /Application/include/nx/PlayData.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLAYDATA_HPP 2 | #define PLAYDATA_HPP 3 | 4 | #include "nx/Title.hpp" 5 | #include "Types.hpp" 6 | #include 7 | 8 | namespace NX { 9 | // Enumeration for event type in PlayEvent struct 10 | enum PlayEventType { 11 | PlayEvent_Applet, // PlayEvent contains applet event 12 | PlayEvent_Account // PlayEvent contains account event 13 | }; 14 | 15 | // Enumeration for applet/account event type in PlayEvent struct 16 | enum EventType { 17 | Applet_Launch, // Applet launched 18 | Applet_Exit, // Applet quit 19 | Applet_InFocus, // Applet gained focus 20 | Applet_OutFocus, // Applet lost focus 21 | Account_Active, // Account selected 22 | Account_Inactive // Account closed? 23 | }; 24 | 25 | // PlayEvents are parsed PdmPlayEvents containing only necessary information 26 | struct PlayEvent { 27 | PlayEventType type; // Type of PlayEvent 28 | AccountUid userID; // UserID 29 | TitleID titleID; // TitleID 30 | EventType eventType; // See EventType enum 31 | u64 clockTimestamp; // Time of event 32 | u64 steadyTimestamp; // Steady timestamp (used for calculating duration) 33 | }; 34 | 35 | // A PlaySession represents a session of play for a game. It contains the start 36 | // and end timestamps and playtime for convenience. Note that (end-start) != playtime 37 | // due to time when the game may not have been focussed. 38 | struct PlaySession { 39 | u32 playtime; // Total playtime in seconds 40 | u64 startTimestamp; // Time of launch 41 | u64 endTimestamp; // Time of exit 42 | }; 43 | 44 | // PdmPlayStatistics but only the necessary things 45 | struct PlayStatistics { 46 | TitleID titleID; // TitleID of these stats 47 | u32 firstPlayed; // Timestamp of first launch 48 | u32 lastPlayed; // Timestamp of last play (exit) 49 | u32 playtime; // Total playtime in seconds 50 | u32 launches; // Total launches 51 | }; 52 | 53 | // RecentPlayStatistics struct is similar to PdmPlayStatistics but 54 | // only contains recent values 55 | struct RecentPlayStatistics { 56 | TitleID titleID; // TitleID of these statistics 57 | u32 playtime; // Total playtime in seconds 58 | u32 launches; // Total launches 59 | }; 60 | 61 | // Struct used for analyzing/splitting up play sessions 62 | struct PD_Session { 63 | size_t index; // Index of first (launch) event 64 | size_t num; // Number of events for this session 65 | }; 66 | 67 | // Pair of play events and summaries 68 | typedef std::pair, std::vector> PlayEventsAndSummaries; 69 | 70 | // PlayData stores PlayEvents which are created from processing PlayEvent.dat using pdm. 71 | // The data can then be queried across a period of time, with the summation of these 72 | // statistics being returned. It also offers wrappers for some pdm functions. 73 | class PlayData { 74 | private: 75 | // Vector containing created PlayEvents 76 | // Should be in chronological order (ie. as read from pdm) 77 | std::vector events; 78 | 79 | // Vector containing all read summaries 80 | // Not in any specific order 81 | std::vector summaries; 82 | 83 | // Vector of titles in imported data 84 | std::vector> titles; 85 | 86 | // Timestamp indicating when summaries were imported 87 | // Used to "merge" with system stats 88 | uint64_t importTimestamp; 89 | 90 | // Return vector of PD_Sessions for given title/user IDs + time range 91 | // Give a titleID of zero to include all titles 92 | std::vector getPDSessions(TitleID, AccountUid, u64, u64); 93 | 94 | // Counts playtime and launches for the given sessions 95 | // TitleID in returned struct is zero 96 | RecentPlayStatistics * countPlaytime(std::vector, u64, u64); 97 | 98 | // Reads and parses data using pdm 99 | PlayEventsAndSummaries readPlayDataFromPdm(); 100 | 101 | // Reads and parses data from imported file 102 | PlayEventsAndSummaries readPlayDataFromImport(); 103 | 104 | // Merges the data within the two passed vectors by only keeping unique PlayEvents 105 | std::vector mergePlayEvents(std::vector &, std::vector &); 106 | 107 | public: 108 | // The constructor prepares + creates PlayEvents 109 | PlayData(); 110 | 111 | // Returns title objects that are not present in the given vector 112 | std::vector getMissingTitles(std::vector<Title *>); 113 | 114 | // Returns vector containing PlayEvents between the given times 115 | // Start time, end time, titleID, userID 116 | std::vector<PlayEvent> getPlayEvents(u64, u64, TitleID, AccountUid); 117 | 118 | // Returns all play sessions for the given title ID and user ID 119 | std::vector<PlaySession> getPlaySessionsForUser(TitleID, AccountUid); 120 | 121 | // Returns a RecentPlayStatistics for the given time range for all users 122 | RecentPlayStatistics * getRecentStatisticsForUser(u64, u64, AccountUid); 123 | 124 | // Returns a RecentPlayStatistics for the given time range and user ID 125 | RecentPlayStatistics * getRecentStatisticsForTitleAndUser(TitleID, u64, u64, AccountUid); 126 | 127 | // Returns a PlayStatistics for the given titleID and userID 128 | PlayStatistics * getStatisticsForUser(TitleID, AccountUid); 129 | 130 | // The destructor deletes PlayEvents (frees memory) 131 | ~PlayData(); 132 | }; 133 | }; 134 | 135 | #endif 136 | -------------------------------------------------------------------------------- /Application/include/nx/Title.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TITLE_HPP 2 | #define TITLE_HPP 3 | 4 | #include "Types.hpp" 5 | 6 | namespace NX { 7 | class Title { 8 | private: 9 | // Game's titleID 10 | TitleID titleID_; 11 | // Is the game installed? 12 | bool is_installed; 13 | // Game's name 14 | std::string name_; 15 | // Pointer to and size of image 16 | u8 * ptr; 17 | u32 size; 18 | 19 | public: 20 | // The constructor derives all relevant info from given titleID 21 | Title(TitleID, bool); 22 | 23 | // Creates title with no icon as it is not installed 24 | Title(const TitleID, const std::string &); 25 | 26 | // These functions return Title's data 27 | TitleID titleID(); 28 | bool isInstalled(); 29 | std::string name(); 30 | u8 * imgPtr(); 31 | u32 imgSize(); 32 | 33 | // Frees memory icon is stored in 34 | ~Title(); 35 | }; 36 | }; 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /Application/include/nx/User.hpp: -------------------------------------------------------------------------------- 1 | #ifndef USER_HPP 2 | #define USER_HPP 3 | 4 | #include <string> 5 | #include <switch.h> 6 | 7 | namespace NX { 8 | // The User class stores all relevant/useful information about a user 9 | // on the console. All this is read using the provided userID 10 | class User { 11 | private: 12 | // The selected user's ID (set to zero when an error occurs) 13 | AccountUid ID_; 14 | // The user's username 15 | std::string username_; 16 | // The user's profile image (pointer to and size) 17 | u8 * ptr; 18 | u32 size; 19 | 20 | public: 21 | // Constructor takes ID and derives username and icon texture 22 | User(AccountUid); 23 | 24 | // Returns private members 25 | AccountUid ID(); 26 | std::string username(); 27 | u8 * imgPtr(); 28 | u32 imgSize(); 29 | 30 | // Destructor frees memory containing image 31 | ~User(); 32 | }; 33 | }; 34 | 35 | #endif -------------------------------------------------------------------------------- /Application/include/ui/Theme.hpp: -------------------------------------------------------------------------------- 1 | #ifndef THEME_HPP 2 | #define THEME_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | #include "Types.hpp" 6 | 7 | class Theme { 8 | private: 9 | // Type of theme 10 | ThemeType type; 11 | 12 | // Theme colours 13 | ThemeSet theme; 14 | 15 | // Functions to set colours based on type 16 | void setThemeAuto(); 17 | void setThemeCustom(); 18 | void setThemeDark(); 19 | void setThemeLight(); 20 | 21 | public: 22 | // Constructor takes type and sets colours 23 | Theme(ThemeType); 24 | 25 | // Return colours 26 | Aether::Colour accent(); 27 | Aether::Colour altBG(); 28 | Aether::Colour bg(); 29 | Aether::Colour fg(); 30 | std::function<Aether::Colour(uint32_t)> highlightFunc(); // Actually constructs function when called 31 | Aether::Colour highlight1(); 32 | Aether::Colour highlight2(); 33 | Aether::Colour highlightBG(); 34 | Aether::Colour mutedLine(); 35 | Aether::Colour mutedText(); 36 | Aether::Colour selected(); 37 | Aether::Colour text(); 38 | 39 | // Set private variables (only works if type is custom) 40 | void setAccent(Aether::Colour); 41 | void setAltBG(Aether::Colour); 42 | void setBg(Aether::Colour); 43 | void setFg(Aether::Colour); 44 | void setHighlight1(Aether::Colour); 45 | void setHighlight2(Aether::Colour); 46 | void setHighlightBG(Aether::Colour); 47 | void setMutedLine(Aether::Colour); 48 | void setMutedText(Aether::Colour); 49 | void setSelected(Aether::Colour); 50 | void setText(Aether::Colour); 51 | 52 | // Set theme to type 53 | void setTheme(ThemeType); 54 | 55 | // Ensure changes to custom colours are saved 56 | void saveCustom(); 57 | 58 | // Destructor saves ini if theme type is custom 59 | ~Theme(); 60 | }; 61 | 62 | #endif -------------------------------------------------------------------------------- /Application/include/ui/ThemePresets.hpp: -------------------------------------------------------------------------------- 1 | #ifndef THEME_PRESETS_HPP 2 | #define THEME_PRESETS_HPP 3 | 4 | #include "Types.hpp" 5 | 6 | namespace ThemePreset { 7 | // Colours for Basic Black 8 | const ThemeSet Dark { 9 | Aether::Theme::Dark.accent, 10 | Aether::Theme::Dark.altBG, 11 | Aether::Theme::Dark.bg, 12 | Aether::Theme::Dark.fg, 13 | Aether::Colour{0, 150, 190, 255}, // highlight1 14 | Aether::Colour{0, 250, 250, 255}, // highlight2 15 | Aether::Theme::Dark.highlightBG, 16 | Aether::Theme::Dark.mutedLine, 17 | Aether::Theme::Dark.mutedText, 18 | Aether::Theme::Dark.selected, 19 | Aether::Theme::Dark.text 20 | }; 21 | 22 | // Colours for Basic White 23 | const ThemeSet Light { 24 | Aether::Theme::Light.accent, 25 | Aether::Theme::Light.altBG, 26 | Aether::Theme::Light.bg, 27 | Aether::Theme::Light.fg, 28 | Aether::Colour{0, 200, 190, 255}, // highlight1 29 | Aether::Colour{0, 250, 216, 255}, // highlight2 30 | Aether::Theme::Light.highlightBG, 31 | Aether::Theme::Light.mutedLine, 32 | Aether::Theme::Light.mutedText, 33 | Aether::Theme::Light.selected, 34 | Aether::Theme::Light.text 35 | }; 36 | }; 37 | 38 | #endif -------------------------------------------------------------------------------- /Application/include/ui/element/Graph.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELEMENT_GRAPH_HPP 2 | #define ELEMENT_GRAPH_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | // Stores info required for each "bar"/entry (within elements) 7 | struct Entry { 8 | Aether::Rectangle * bar; 9 | Aether::Text * label; 10 | Aether::Text * value; 11 | }; 12 | 13 | namespace CustomElm { 14 | // A Graph only handles the actual graph and not any titles/headings. 15 | // Draws a simple bar graph. 16 | class Graph : public Aether::Element { 17 | private: 18 | // Vector of Entry struct 19 | std::vector<Entry> column; 20 | // Vector of x-axis separating lines 21 | std::vector<Aether::Rectangle *> sep; 22 | // Vector of y-axis steps 23 | std::vector<Aether::Rectangle *> steps; 24 | // Vector of y-axis labels 25 | std::vector<Aether::Text *> yLabel; 26 | 27 | // Main axis lines 28 | Aether::Rectangle * xAxis; 29 | Aether::Rectangle * yAxis; 30 | 31 | // Colours (all set white initially) 32 | Aether::Colour barC; 33 | Aether::Colour labelC; 34 | Aether::Colour lineC; 35 | 36 | // Bar width in percent of column 37 | float barWidth; 38 | // Label font size 39 | unsigned int labelFont; 40 | // Whether to show values 41 | bool showValues; 42 | 43 | // Precisions 44 | unsigned int labelPrecision; 45 | unsigned int valuePrecision; 46 | 47 | // Y-axis properties 48 | double yMax; 49 | 50 | // Set to true to call below function (avoid a lot of redraws) 51 | bool needsUpdate; 52 | // Repositions + resizes ALL elements 53 | void updateElements(); 54 | 55 | public: 56 | // x, y, w, h + number of elements to initialise (can be changed later) 57 | Graph(int, int, int, int, unsigned int); 58 | 59 | // Returns number of entries/columns 60 | unsigned int entries(); 61 | // Change number of entries 62 | // Increasing will set new entries "empty" 63 | // Decreasing will delete excess entries! 64 | void setNumberOfEntries(unsigned int); 65 | 66 | // Set the characteristics of each entry (pass index starting from 0 as first parameter) 67 | // Does nothing if outside range 68 | void setLabel(unsigned int, std::string); 69 | void setValue(unsigned int, double); 70 | 71 | // Set the colour of all bars (+ values) 72 | void setBarColour(Aether::Colour); 73 | // Set the colour of all labels 74 | void setLabelColour(Aether::Colour); 75 | // Set the colour of all lines 76 | void setLineColour(Aether::Colour); 77 | 78 | // Set the width of each bar (0 - 1) 79 | void setBarWidth(float); 80 | // Set font size of all labels 81 | void setFontSize(unsigned int); 82 | // Whether to show values above bars 83 | void setValueVisibility(bool); 84 | 85 | // Set number of decimal points for label 86 | void setLabelPrecision(unsigned int); 87 | // Set number of decimal points for value 88 | // Doesn't update current values!! 89 | void setValuePrecision(unsigned int); 90 | // Set maximum y-axis value 91 | void setMaximumValue(double); 92 | // Set number of steps on y-axis 93 | void setYSteps(unsigned int); 94 | 95 | // Changing dimensions must reposition elements 96 | void setW(int); 97 | void setH(int); 98 | 99 | // Elements will be repositioned on update if need be 100 | void update(uint32_t); 101 | }; 102 | }; 103 | 104 | #endif -------------------------------------------------------------------------------- /Application/include/ui/element/ListActivity.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELEMENT_LISTACTIVITY_HPP 2 | #define ELEMENT_LISTACTIVITY_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomElm { 7 | // List item containing game icon and a few left/right aligned strings 8 | class ListActivity : public Aether::AsyncItem { 9 | private: 10 | // Height of item in pixels 11 | static const int height = 120; 12 | 13 | // Pointers to elements 14 | Aether::Rectangle * topR; 15 | Aether::Rectangle * bottomR; 16 | Aether::Image * icon; 17 | Aether::Text * title; 18 | Aether::Text * playtime; 19 | Aether::Text * mutedLeft; 20 | Aether::Text * rank; 21 | Aether::Text * mutedRight; 22 | 23 | // Sets width of applicable elements 24 | void positionElements(); 25 | 26 | // Helper to prepare new element 27 | void processText(Aether::Text * &, std::function<Aether::Text * ()>); 28 | 29 | public: 30 | ListActivity(); 31 | 32 | // Override update() to also change the alpha of lines 33 | // (they are not monitored by AsyncItem) 34 | void update(uint32_t); 35 | 36 | // Set icon using raw data 37 | void setImage(uint8_t *, uint32_t); 38 | 39 | // Set strings 40 | void setTitle(const std::string &); 41 | void setPlaytime(const std::string &); 42 | void setLeftMuted(const std::string &); 43 | void setRightMuted(const std::string &); 44 | void setRank(const std::string &); 45 | 46 | // Set colours 47 | void setTitleColour(const Aether::Colour &); 48 | void setPlaytimeColour(const Aether::Colour &); 49 | void setMutedColour(const Aether::Colour &); 50 | void setLineColour(const Aether::Colour &); 51 | }; 52 | }; 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Application/include/ui/element/ListAdjust.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELEMENT_LISTADJUST_HPP 2 | #define ELEMENT_LISTADJUST_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomElm { 7 | // A list element storing an icon, name and two playtime strings 8 | class ListAdjust : public Aether::AsyncItem { 9 | private: 10 | // Height of item 11 | static const int height = 100; 12 | 13 | // Bounding lines 14 | Aether::Rectangle * topR; 15 | Aether::Rectangle * bottomR; 16 | 17 | // Title elements 18 | Aether::Image * icon; 19 | Aether::Text * title; 20 | Aether::Text * recordTime; 21 | Aether::Text * adjustTime; 22 | 23 | // Positions element 24 | void positionElements(); 25 | 26 | public: 27 | // Contructs a new item using the passed title and playtime strings 28 | ListAdjust(const std::string &, const std::string &, const std::string &); 29 | 30 | // Override update to fade in/out lines 31 | void update(uint32_t); 32 | 33 | // Update the adjusted play time 34 | void setAdjustedTime(const std::string &); 35 | 36 | // Set the icon 37 | void setImage(uint8_t *, uint32_t); 38 | 39 | // Set colours 40 | void setAdjustColour(const Aether::Colour &); 41 | void setLineColour(const Aether::Colour &); 42 | void setRecordColour(const Aether::Colour &); 43 | void setTitleColour(const Aether::Colour &); 44 | }; 45 | }; 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /Application/include/ui/element/ListColour.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELEMENT_LISTCOLOUR_HPP 2 | #define ELEMENT_LISTCOLOUR_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomElm { 7 | // A list element storing a string, colour hex value and colour 'box' 8 | class ListColour : public Aether::Element { 9 | private: 10 | Aether::Rectangle * topR; 11 | Aether::Rectangle * bottomR; 12 | Aether::Text * text; 13 | Aether::Text * hex; 14 | Aether::Rectangle * colour; 15 | 16 | public: 17 | // Pass text and callback 18 | ListColour(std::string, std::function<void()>); 19 | 20 | // Set colour of box and hex value 21 | void setColour(Aether::Colour); 22 | 23 | // Setter for colours 24 | void setLineColour(Aether::Colour); 25 | void setTextColour(Aether::Colour); 26 | 27 | // Adjusting width (when added to list) 28 | // must adjust line width and reposition elements 29 | void setW(int); 30 | }; 31 | }; 32 | 33 | #endif -------------------------------------------------------------------------------- /Application/include/ui/element/ListHide.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELEMENT_LISTHIDE_HPP 2 | #define ELEMENT_LISTHIDE_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomElm { 7 | // List item containing game icon, name, id + tick 8 | class ListHide : public Aether::AsyncItem { 9 | private: 10 | // Height of item in pixels 11 | static const int height = 80; 12 | 13 | // Pointers to elements (async) 14 | Aether::Image * icon; 15 | Aether::Text * title; 16 | Aether::Text * titleID; 17 | 18 | // Pointers to sync elements 19 | Aether::Rectangle * topR; 20 | Aether::Rectangle * bottomR; 21 | Aether::Box * circle; 22 | Aether::Tick * tick; 23 | 24 | // Position elements once rendering is complete 25 | void positionElements(); 26 | 27 | public: 28 | // Constructor accepts title and ID 29 | ListHide(const std::string &, const std::string &); 30 | 31 | // Override update to fade in sync items 32 | void update(uint32_t); 33 | 34 | // Set icon using raw data 35 | void setImage(uint8_t *, uint32_t); 36 | 37 | // Set colours 38 | void setIDColour(const Aether::Colour &); 39 | void setLineColour(const Aether::Colour &); 40 | void setTitleColour(const Aether::Colour &); 41 | void setTickBackgroundColour(const Aether::Colour &); 42 | void setTickForegroundColour(const Aether::Colour &); 43 | 44 | // Toggle hidden state 45 | bool isTicked(); 46 | void setTicked(const bool); 47 | }; 48 | }; 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /Application/include/ui/element/ListSession.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELEMENT_LISTSESSION_HPP 2 | #define ELEMENT_LISTSESSION_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomElm { 7 | class ListSession : public Aether::AsyncItem { 8 | private: 9 | // Height of item 10 | static const int height = 85; 11 | 12 | // Font size of all text 13 | static const unsigned int fontSize = 20; 14 | 15 | // Pointers to elements 16 | Aether::Rectangle * top; 17 | Aether::Rectangle * bottom; 18 | Aether::Text * time; 19 | Aether::Text * playtime; 20 | Aether::Text * percentage; 21 | 22 | // Sets width of text 23 | void positionElements(); 24 | 25 | // Helper for updating text 26 | void processText(Aether::Text * &, std::function<Aether::Text * ()>); 27 | 28 | public: 29 | // Constructor sets up elements 30 | ListSession(); 31 | 32 | // Override update() to also change the alpha of lines 33 | // (they are not monitored by AsyncItem) 34 | void update(uint32_t); 35 | 36 | // Set colours 37 | void setLineColour(Aether::Colour); 38 | void setPercentageColour(Aether::Colour); 39 | void setPlaytimeColour(Aether::Colour); 40 | void setTimeColour(Aether::Colour); 41 | 42 | // Set strings 43 | void setPercentageString(std::string); 44 | void setPlaytimeString(std::string); 45 | void setTimeString(std::string); 46 | }; 47 | }; 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /Application/include/ui/element/ListUser.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELEMENT_LISTUSER_HPP 2 | #define ELEMENT_LISTUSER_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomElm { 7 | // A list element storing a user image and name 8 | class ListUser : public Aether::Element { 9 | private: 10 | Aether::Rectangle * topR; 11 | Aether::Rectangle * bottomR; 12 | Aether::Text * name; 13 | 14 | public: 15 | // Pass username and pointer to / size of image 16 | ListUser(std::string, uint8_t *, uint32_t); 17 | 18 | // Setter for colours 19 | void setLineColour(Aether::Colour); 20 | void setTextColour(Aether::Colour); 21 | 22 | // Adjusting width (when added to list) 23 | // must adjust line width 24 | void setW(int); 25 | }; 26 | }; 27 | 28 | #endif -------------------------------------------------------------------------------- /Application/include/ui/element/SortedList.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ELEMENT_SORTEDLIST_HPP 2 | #define ELEMENT_SORTEDLIST_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | #include "ui/element/ListActivity.hpp" 6 | #include "Types.hpp" 7 | 8 | // Struct storing information about entry used for sorting 9 | struct SortInfo { 10 | std::string name; // Name of title 11 | unsigned long long int titleID; // Title's ID 12 | unsigned int firstPlayed; // Timestamp of first launch 13 | unsigned int lastPlayed; // Timestamp of last play 14 | unsigned int playtime; // Total playtime in seconds 15 | unsigned int launches; // Total launches 16 | }; 17 | 18 | namespace CustomElm { 19 | // A SortedList extends Aether::List by storing a SortType, 20 | // and reorders items without the need for recreation. 21 | class SortedList : public Aether::List { 22 | private: 23 | // Pointer to heading to change text 24 | Aether::Text * heading; 25 | 26 | // Vector of pointers to sortinfo (aligns with element position) 27 | std::vector<SortInfo *> sortinfo; 28 | 29 | // Method items are currently sorted with 30 | SortType sorting; 31 | 32 | public: 33 | // X, Y, W, H 34 | SortedList(int, int, int, int); 35 | 36 | // An added element needs to have relevant information stored in order to sort 37 | // Only accepts ListActivity due to information requirements 38 | void addElement(ListActivity *, SortInfo *); 39 | // Also frees memory used by SortInfos 40 | void removeAllElements(); 41 | // Returns all list items and SortInfos 42 | void returnAllElements(); 43 | 44 | // Sets sort method and sorts items 45 | void setSort(SortType); 46 | // Returns SortType 47 | SortType sort(); 48 | 49 | // Set colour of heading text 50 | void setHeadingColour(Aether::Colour); 51 | }; 52 | }; 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Application/include/ui/overlay/ColourPicker.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OVERLAY_COLOURPICKER_HPP 2 | #define OVERLAY_COLOURPICKER_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomOvl { 7 | // Select a colour using a spinner for r, g, b, a. Shows preview, hex value 8 | // and requires a callback for when OK is pressed 9 | class ColourPicker : public Aether::Overlay { 10 | private: 11 | // Pointers to elements 12 | Aether::ControlBar * ctrlBar; 13 | Aether::Rectangle * rect, * top, * bottom; 14 | Aether::Text * title; 15 | Aether::BorderButton * button; 16 | Aether::Text * tip; 17 | 18 | // Keep pointers to each item 19 | Aether::Spinner * red; 20 | Aether::Spinner * green; 21 | Aether::Spinner * blue; 22 | Aether::Spinner * alpha; 23 | 24 | // Preview elements 25 | Aether::Rectangle * colourRect; 26 | Aether::Text * colourHex; 27 | 28 | // Callback func 29 | std::function<void(Aether::Colour)> func; 30 | // Wrapper for callback that passes colour 31 | void callFunc(); 32 | 33 | public: 34 | // Pass title, colour and callback which is given colour (not called if closed by cancelling!) 35 | ColourPicker(std::string, Aether::Colour, std::function<void(Aether::Colour)>); 36 | 37 | // Update colour every frame 38 | void update(uint32_t); 39 | 40 | // Set button labels 41 | void setBackLabel(std::string); 42 | void setOKLabel(std::string); 43 | void setTipText(std::string); 44 | 45 | // Set text for each spinner's hint 46 | void setRedHint(std::string); 47 | void setGreenHint(std::string); 48 | void setBlueHint(std::string); 49 | void setAlphaHint(std::string); 50 | 51 | // Set colours 52 | void setBackgroundColour(Aether::Colour); 53 | void setHighlightColour(Aether::Colour); 54 | void setInactiveColour(Aether::Colour); 55 | void setTextColour(Aether::Colour); 56 | }; 57 | }; 58 | 59 | #endif -------------------------------------------------------------------------------- /Application/include/ui/overlay/PlaySession.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OVERLAY_PLAYSESSION_HPP 2 | #define OVERLAY_PLAYSESSION_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomOvl { 7 | // Overlay to show a breakdown of a PlaySession 8 | class PlaySession : public Aether::Overlay { 9 | private: 10 | // Static elements 11 | Aether::Text * title; 12 | Aether::Rectangle * rect; 13 | Aether::Rectangle * top; 14 | Aether::Rectangle * bottom; 15 | Aether::Rectangle * sep; 16 | Aether::ControlBar * ctrlBar; 17 | Aether::List * list; 18 | 19 | // Summary at bottom 20 | Aether::Text * length; 21 | Aether::Text * lengthSub; 22 | Aether::Text * playtime; 23 | Aether::Text * playtimeSub; 24 | 25 | // Scroll bar colour 26 | Aether::Colour col; 27 | 28 | public: 29 | PlaySession(); 30 | 31 | // Add element to list 32 | void addListItem(Aether::Element *); 33 | 34 | // Set bottom strings 35 | void setLength(std::string); 36 | void setPlaytime(std::string); 37 | 38 | // Set colours 39 | void setAccentColour(Aether::Colour); 40 | void setBackgroundColour(Aether::Colour); 41 | void setLineColour(Aether::Colour); 42 | void setMutedLineColour(Aether::Colour); 43 | void setTextColour(Aether::Colour); 44 | }; 45 | }; 46 | 47 | #endif -------------------------------------------------------------------------------- /Application/include/ui/overlay/PlaytimePicker.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OVERLAY_PLAYTIMEPICKER_HPP 2 | #define OVERLAY_PLAYTIMEPICKER_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomOvl { 7 | // Picks a playtime adjustment value using hour, minute and seconds spinners. 8 | class PlaytimePicker : public Aether::Overlay { 9 | private: 10 | // Pointers to elements 11 | Aether::ControlBar * ctrlBar; 12 | Aether::Rectangle * rect, * top, * bottom; 13 | Aether::Text * title; 14 | Aether::BorderButton * button; 15 | Aether::Text * tip; 16 | 17 | // Keep pointers to each item 18 | Aether::Spinner * hour; 19 | Aether::Spinner * minute; 20 | Aether::Spinner * second; 21 | 22 | // Callback function 23 | std::function<void(int)> func; 24 | 25 | // Helper to invoke callback 26 | void callFunc(); 27 | 28 | public: 29 | // Pass title and callback which is invoked when confirmed 30 | PlaytimePicker(const std::string &, const int, std::function<void(int)>); 31 | 32 | // Set button labels 33 | void setBackLabel(const std::string &); 34 | void setOKLabel(const std::string &); 35 | void setTipText(const std::string &); 36 | 37 | // Set text for each spinner's hint 38 | void setHourHint(const std::string &); 39 | void setMinuteHint(const std::string &); 40 | void setSecondHint(const std::string &); 41 | 42 | // Set colours 43 | void setBackgroundColour(const Aether::Colour &); 44 | void setHighlightColour(const Aether::Colour &); 45 | void setInactiveColour(const Aether::Colour &); 46 | void setTextColour(const Aether::Colour &); 47 | }; 48 | }; 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /Application/include/ui/overlay/ProgressBox.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OVERLAY_PROGRESSBOX_HPP 2 | #define OVERLAY_PROGRESSBOX_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace CustomOvl { 7 | // Overlay which shows a brief message along with a progress bar 8 | class ProgressBox : public Aether::Overlay { 9 | private: 10 | // Background rectangle 11 | Aether::Rectangle * rect; 12 | 13 | // Message 14 | Aether::TextBlock * heading; 15 | 16 | // Progress bar 17 | Aether::RoundProgressBar * bar; 18 | 19 | // Progress bar value 20 | Aether::Text * value; 21 | 22 | public: 23 | // Constructs a new (blank) ProgressBox 24 | ProgressBox(); 25 | 26 | void setHeading(const std::string &); 27 | void setValue(const double); 28 | 29 | void setBackgroundColour(const Aether::Colour); 30 | void setBarBackgroundColour(const Aether::Colour); 31 | void setBarForegroundColour(const Aether::Colour); 32 | void setTextColour(const Aether::Colour); 33 | }; 34 | }; 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /Application/include/ui/screen/AdjustPlaytime.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_ADJUSTPLAYTIME_HPP 2 | #define SCREEN_ADJUSTPLAYTIME_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace Main { 7 | class Application; 8 | }; 9 | 10 | namespace CustomElm { 11 | class ListAdjust; 12 | }; 13 | 14 | namespace CustomOvl { 15 | class PlaytimePicker; 16 | }; 17 | 18 | namespace Screen { 19 | // Screen allowing user to adjust the playtime displayed in All Activity. 20 | class AdjustPlaytime : public Aether::Screen { 21 | private: 22 | // Pointer to application object 23 | Main::Application * app; 24 | 25 | // List object 26 | Aether::List * list; 27 | 28 | // User 29 | Aether::Image * userimage; 30 | Aether::Text * username; 31 | 32 | // Adjustment picker 33 | CustomOvl::PlaytimePicker * picker; 34 | 35 | // Helper to get value string 36 | std::string getValueString(int); 37 | 38 | // Create and show the adjustment overlay 39 | void setupPlaytimePicker(const std::string &, size_t, CustomElm::ListAdjust *); 40 | 41 | // Vector of title IDs and their adjustment value 42 | std::vector<AdjustmentValue> adjustments; 43 | 44 | public: 45 | // Constructs the screen 46 | AdjustPlaytime(Main::Application *); 47 | 48 | // Create relevant elements on load 49 | void onLoad(); 50 | 51 | // Undo onLoad(); 52 | void onUnload(); 53 | }; 54 | }; 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /Application/include/ui/screen/AllActivity.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_ALLACTIVITY_HPP 2 | #define SCREEN_ALLACTIVITY_HPP 3 | 4 | #include "ui/element/SortedList.hpp" 5 | 6 | // Forward declaration due to circular dependency 7 | namespace Main { 8 | class Application; 9 | }; 10 | 11 | namespace Screen { 12 | // "All Activity" page 13 | class AllActivity : public Aether::Screen { 14 | private: 15 | // Pointer to main app object for user + titles 16 | Main::Application * app; 17 | 18 | // Pointers to elements 19 | Aether::Text * heading; 20 | Aether::Text * hours; 21 | Aether::Image * image; 22 | CustomElm::SortedList * list; 23 | Aether::Menu * menu; 24 | Aether::Image * updateElm; 25 | 26 | // Choose sort overlay 27 | Aether::PopupList * sortOverlay; 28 | 29 | // Set elements and highlight one in overlay 30 | void setupOverlay(); 31 | 32 | public: 33 | // Passed main application object 34 | AllActivity(Main::Application *); 35 | 36 | // Prepare user-specific elements 37 | void onLoad(); 38 | // Delete elements created in onLoad() 39 | void onUnload(); 40 | 41 | // Deletes overlay 42 | ~AllActivity(); 43 | }; 44 | }; 45 | 46 | #endif -------------------------------------------------------------------------------- /Application/include/ui/screen/CustomTheme.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_CUSTOMTHEME_HPP 2 | #define SCREEN_CUSTOMTHEME_HPP 3 | 4 | #include "ui/overlay/ColourPicker.hpp" 5 | #include "ui/element/ListColour.hpp" 6 | 7 | // Forward declaration due to circular dependency 8 | namespace Main { 9 | class Application; 10 | }; 11 | 12 | namespace Screen { 13 | // "Update" page 14 | class CustomTheme : public Aether::Screen { 15 | private: 16 | // Pointer to main app object for user + titles 17 | Main::Application * app; 18 | Aether::Image * updateElm; 19 | 20 | // Need pointers to every element in order to update colours 21 | Aether::Text * heading; 22 | Aether::Rectangle * topR; 23 | Aether::Rectangle * bottomR; 24 | Aether::ControlBar * controls; 25 | Aether::List * list; 26 | 27 | Aether::ListButton * optionPreset; 28 | 29 | Aether::ListOption * optionImage; 30 | Aether::ListComment * imageHint; 31 | CustomElm::ListColour * colourAccent; 32 | 33 | CustomElm::ListColour * colourAltBG; 34 | CustomElm::ListColour * colourBackground; 35 | 36 | CustomElm::ListColour * colourHighlight1; 37 | CustomElm::ListColour * colourHighlight2; 38 | CustomElm::ListColour * colourHighlighted; 39 | CustomElm::ListColour * colourSelected; 40 | 41 | CustomElm::ListColour * colourLine; 42 | CustomElm::ListColour * colourMutedLine; 43 | 44 | CustomElm::ListColour * colourText; 45 | CustomElm::ListColour * colourMutedText; 46 | 47 | // Copy theme on load (highlight1/2 aren't in Theme_T) 48 | bool oldImage; 49 | Aether::Theme_T oldTheme; 50 | Aether::Colour oldHighlight1; 51 | Aether::Colour oldHighlight2; 52 | 53 | // Error popup 54 | Aether::MessageBox * msgbox; 55 | // Create popup 56 | void showErrorMsg(); 57 | 58 | // Colour picker 59 | CustomOvl::ColourPicker * picker; 60 | // Setup picker (title, colour, callback) 61 | void setupPicker(std::string, Aether::Colour, std::function<void(Aether::Colour)>); 62 | 63 | // Popup of presets 64 | Aether::PopupList * presetList; 65 | // Fill list with presets and callbacks 66 | void createPresetList(); 67 | 68 | // Recolour everything 69 | void recolourElements(); 70 | 71 | public: 72 | // Passed main application object 73 | CustomTheme(Main::Application *); 74 | 75 | void onLoad(); 76 | // Delete elements created in onLoad() 77 | void onUnload(); 78 | }; 79 | }; 80 | 81 | 82 | #endif -------------------------------------------------------------------------------- /Application/include/ui/screen/Details.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_DETAILS_HPP 2 | #define SCREEN_DETAILS_HPP 3 | 4 | #include "ui/element/Graph.hpp" 5 | #include "ui/overlay/PlaySession.hpp" 6 | 7 | // Forward declaration due to circular dependency 8 | namespace Main { 9 | class Application; 10 | }; 11 | 12 | namespace Screen { 13 | // Detailed activity screen 14 | class Details : public Aether::Screen { 15 | private: 16 | // Pointer to app for theme 17 | Main::Application * app; 18 | // Used to udpate prev. screen 19 | bool popped; 20 | 21 | // Updates graph if data matching current time range 22 | void updateGraph(); 23 | // Updates the list of play sessions matching current time range 24 | void updateSessions(); 25 | // Element which marks top of sessions 26 | Aether::Element * topElm; 27 | 28 | // Container holding heading and L/R 29 | Aether::Container * header; 30 | 31 | // Pointers to elements 32 | CustomElm::Graph * graph; 33 | Aether::Text * graphHeading; 34 | Aether::Text * graphSubheading; 35 | Aether::Element * graphTotal; 36 | Aether::Text * graphTotalHead; 37 | Aether::Text * graphTotalSub; 38 | 39 | Aether::Text * playtime; 40 | Aether::Text * avgplaytime; 41 | Aether::Text * timeplayed; 42 | Aether::Text * firstplayed; 43 | Aether::Text * lastplayed; 44 | 45 | Aether::Image * icon; 46 | Aether::List * list; 47 | Aether::ListHeadingHelp * playHeading; 48 | Aether::Text * noStats; 49 | Aether::Text * title; 50 | Aether::Image * updateElm; 51 | Aether::Image * userimage; 52 | Aether::Text * username; 53 | 54 | // MessageBox overlay (used for multiple things) 55 | Aether::MessageBox * msgbox; 56 | // Popup for session breakdown 57 | CustomOvl::PlaySession * panel; 58 | 59 | // Prepare msgbox for session help 60 | void setupSessionHelp(); 61 | 62 | // Prepare panel for session breakdown 63 | // Takes PlaySession 64 | void setupSessionBreakdown(NX::PlaySession); 65 | 66 | // Updates the "recent activity" part of the screen 67 | void updateActivity(); 68 | 69 | public: 70 | // Sets up elements 71 | Details(Main::Application *); 72 | 73 | // Update checks for change in application's time and regenerates graph 74 | void update(uint32_t); 75 | 76 | // Create user + title reliant elements 77 | void onLoad(); 78 | // Free elements created in onLoad() 79 | void onUnload(); 80 | }; 81 | }; 82 | 83 | #endif -------------------------------------------------------------------------------- /Application/include/ui/screen/HideTitles.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_HIDETITLES_HPP 2 | #define SCREEN_HIDETITLES_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | // Forward declare due to circular dependency 7 | namespace Main { 8 | class Application; 9 | }; 10 | 11 | namespace Screen { 12 | class HideTitles : public Aether::Screen { 13 | private: 14 | // Pointer to application object 15 | Main::Application * app; 16 | 17 | // Main list element 18 | Aether::List * list; 19 | 20 | // Titles hidden in sidebar 21 | Aether::Text * hiddenCountText; 22 | Aether::TextBlock * hiddenSubText; 23 | std::vector<uint64_t> hiddenIDs; 24 | 25 | // Update the titles hidden counter 26 | void updateHiddenCounter(); 27 | 28 | public: 29 | // Constructor takes application object 30 | HideTitles(Main::Application *); 31 | 32 | // Prepare and show list 33 | void onLoad(); 34 | 35 | // Undo onLoad(); 36 | void onUnload(); 37 | }; 38 | }; 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /Application/include/ui/screen/RecentActivity.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_RECENTACTIVITY_HPP 2 | #define SCREEN_RECENTACTIVITY_HPP 3 | 4 | #include "ui/element/Graph.hpp" 5 | #include "ui/element/SortedList.hpp" 6 | 7 | // Forward declaration due to circular dependency 8 | namespace Main { 9 | class Application; 10 | }; 11 | 12 | namespace Screen { 13 | class RecentActivity : public Aether::Screen { 14 | private: 15 | // Pointer to app for theme 16 | Main::Application * app; 17 | 18 | // Updates the "recent activity" part of the screen 19 | void updateActivity(); 20 | // Updates graph if data matching current time range 21 | void updateGraph(); 22 | // Updates the list of games played underneath 23 | void updateTitles(); 24 | 25 | // Element which marks top of sessions 26 | Aether::Element * topElm; 27 | 28 | // Container holding heading and L/R 29 | Aether::Container * header; 30 | 31 | // Pointers to elements 32 | Aether::ListHeading * gameHeading; 33 | CustomElm::Graph * graph; 34 | Aether::Text * graphHeading; 35 | Aether::Text * graphSubheading; 36 | Aether::Text * heading; 37 | Aether::Image * image; 38 | Aether::Text * hours; 39 | Aether::List * list; 40 | Aether::Menu * menu; 41 | Aether::Text * noStats; 42 | Aether::Image * updateElm; 43 | 44 | // Copy of tm on push 45 | struct tm tmCopy; 46 | // Copy of viewPeriod on push 47 | ViewPeriod viewCopy; 48 | 49 | public: 50 | // Passed main application object 51 | RecentActivity(Main::Application *); 52 | 53 | // Updates list when time is changed 54 | void update(uint32_t); 55 | 56 | // Prepare user-specific elements + list 57 | void onLoad(); 58 | // Delete elements created in onLoad() 59 | void onUnload(); 60 | 61 | void onPush(); 62 | void onPop(); 63 | }; 64 | }; 65 | 66 | #endif -------------------------------------------------------------------------------- /Application/include/ui/screen/Settings.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_SETTINGS_HPP 2 | #define SCREEN_SETTINGS_HPP 3 | 4 | #include "ui/overlay/ProgressBox.hpp" 5 | 6 | // Forward declaration due to circular dependency 7 | namespace Main { 8 | class Application; 9 | }; 10 | 11 | namespace Screen { 12 | // Application Settings screen 13 | class Settings : public Aether::Screen { 14 | private: 15 | // Types of running jobs 16 | enum class Job { 17 | Import, 18 | Export, 19 | None 20 | }; 21 | 22 | // Current job running 23 | Job job; 24 | 25 | // Pointer to main app object for config 26 | Main::Application * app; 27 | // Type of creation 28 | ScreenCreate createReason; 29 | 30 | // Pointers to elements 31 | Aether::Text * heading; 32 | Aether::Image * image; 33 | Aether::List * list; 34 | Aether::Menu * menu; 35 | Aether::Image * updateElm; 36 | 37 | // Pointers to options so the value can be updated 38 | Aether::ListOption * optionLang; 39 | Aether::ListOption * optionScreen; 40 | Aether::ListOption * optionSort; 41 | Aether::ListOption * optionTheme; 42 | Aether::ListButton * optionThemeEdit; 43 | Aether::ListOption * optionView; 44 | 45 | Aether::ListOption * option24H; 46 | Aether::ListOption * optionGraph; 47 | Aether::ListButton * optionAdjust; 48 | Aether::ListButton * optionHide; 49 | Aether::ListOption * optionPage; 50 | 51 | Aether::ListButton * optionDeleteImport; 52 | 53 | // MessageBox for forwarder/setting info 54 | Aether::MessageBox * msgbox; 55 | // PopupList for list-related options 56 | Aether::PopupList * popuplist; 57 | 58 | // MessageBox containing progress bar 59 | CustomOvl::ProgressBox * progressbox; 60 | std::atomic<double> progressValue; 61 | 62 | void installForwarder(); 63 | 64 | // Set elements and highlight one in overlay 65 | void prepareMessageBox(); 66 | void preparePopupList(std::string); 67 | void setupGenericMessageOverlay(const std::string &); 68 | void setupLangOverlay(); 69 | void setupScreenOverlay(); 70 | void setupSortOverlay(); 71 | void setupThemeOverlay(); 72 | void setupViewOverlay(); 73 | 74 | // Show overlays 75 | void showExportOverlay(); 76 | void showImportOverlay(); 77 | void showDeleteImportedOverlay(); 78 | 79 | public: 80 | // Passed main application object 81 | Settings(Main::Application *, ScreenCreate); 82 | 83 | void update(uint32_t); 84 | 85 | // Prepare user-specific elements 86 | void onLoad(); 87 | // Delete elements created in onLoad() 88 | void onUnload(); 89 | }; 90 | }; 91 | 92 | #endif 93 | -------------------------------------------------------------------------------- /Application/include/ui/screen/Update.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_UPDATE_HPP 2 | #define SCREEN_UPDATE_HPP 3 | 4 | // Forward declaration due to circular dependency 5 | namespace Main { 6 | class Application; 7 | }; 8 | 9 | namespace Screen { 10 | // "Update" page 11 | class Update : public Aether::Screen { 12 | private: 13 | // Pointer to main app object for user + titles 14 | Main::Application * app; 15 | 16 | // Controls so the colours can be changed 17 | Aether::ControlBar * controls; 18 | // Element containing other elements created based on outcome 19 | Aether::Element * el; 20 | // Progress elements 21 | Aether::RoundProgressBar * pbar; 22 | Aether::Text * ptext; 23 | 24 | // "Checking" overlay 25 | Aether::MessageBox * msgbox; 26 | 27 | // Future for check thread 28 | std::future<UpdateData> data; 29 | // Set true once thread is complete 30 | bool threadDone; 31 | 32 | // Percentage of download finished 33 | std::atomic<double> downloaded; 34 | // Is the update being downloaded (eg update UI?) 35 | bool isDownloading; 36 | // URL to download from 37 | std::string nroURL; 38 | // Future for update thread 39 | std::future<bool> updateThread; 40 | 41 | // Create a 'blank' message box without OK button 42 | void createMsgbox(); 43 | // Callback for download to update progress 44 | void progressCallback(long long int, long long int); 45 | // Show download box 46 | void showDownloadProgress(); 47 | // Create elements based on if there's an update or not 48 | void showElements(); 49 | // Show error message for download 50 | void showError(); 51 | // Download succeeded 52 | void showSuccess(); 53 | 54 | public: 55 | // Passed main application object 56 | Update(Main::Application *); 57 | 58 | // Update handles hiding overlay + updating status 59 | void update(uint32_t); 60 | 61 | void onLoad(); 62 | // Delete elements created in onLoad() 63 | void onUnload(); 64 | }; 65 | }; 66 | 67 | 68 | #endif -------------------------------------------------------------------------------- /Application/include/ui/screen/UserSelect.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCREEN_USERSELECT_HPP 2 | #define SCREEN_USERSELECT_HPP 3 | 4 | #include "nx/User.hpp" 5 | 6 | // Forward declaration due to circular dependency 7 | namespace Main { 8 | class Application; 9 | }; 10 | 11 | namespace Screen { 12 | // User select screen shown on launch (if not user page) 13 | class UserSelect : public Aether::Screen { 14 | private: 15 | Main::Application * app; 16 | 17 | // Need pointer to list in order to update 18 | Aether::List * list; 19 | Aether::Image * updateElm; 20 | 21 | // Stores copy of user vector 22 | std::vector<NX::User *> users; 23 | 24 | public: 25 | // Constructor takes window element and vector of User objects and sets up elements 26 | UserSelect(Main::Application *, std::vector<NX::User *>); 27 | 28 | // User images + names are handled + elements created on load 29 | void onLoad(); 30 | // Undoes everything done on onLoad to free memory 31 | void onUnload(); 32 | }; 33 | }; 34 | 35 | #endif -------------------------------------------------------------------------------- /Application/include/utils/Curl.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_CURL_HPP 2 | #define UTILS_CURL_HPP 3 | 4 | #include <functional> 5 | #include <string> 6 | 7 | namespace Utils::Curl { 8 | void init(); 9 | void exit(); 10 | 11 | // Download file from URL and write to file (returns true on success) 12 | // (URL, path, progress function(bytes received, total bytes)) 13 | bool downloadToFile(std::string, std::string, std::function<void(long long int, long long int)>); 14 | 15 | // Return response from URL as string (empty on error) 16 | std::string downloadToString(std::string); 17 | }; 18 | 19 | #endif -------------------------------------------------------------------------------- /Application/include/utils/Forwarder.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_FORWARDER_HPP 2 | #define UTILS_FORWARDER_HPP 3 | 4 | // These are a bunch of helper functions used to detect if the forwarder 5 | // is installed, etc... 6 | namespace Utils::Forwarder { 7 | // Inits variables (don't need to cleanup - just reinit!) 8 | void initVars(); 9 | 10 | // Is it installed? 11 | bool installed(); 12 | 13 | // Uninstall (delete file from relevant folders) 14 | void uninstall(); 15 | 16 | // Install (copy file to relevant folders) 17 | void install(); 18 | 19 | // Is the .nro in the right location? 20 | bool prepared(); 21 | 22 | // Returns true if installed for CFW 23 | bool atmosphere(); 24 | bool reinx(); 25 | bool sxos(); 26 | }; 27 | 28 | #endif -------------------------------------------------------------------------------- /Application/include/utils/Lang.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_LANG_HPP 2 | #define UTILS_LANG_HPP 3 | 4 | #include "Types.hpp" 5 | 6 | namespace Utils::Lang { 7 | // Set the .json file used for reading strings 8 | // Returns true on success 9 | bool setFile(std::string); 10 | 11 | // Set the language 12 | bool setLanguage(Language); 13 | 14 | // Returns the string matching the given key 15 | // Returns "?" if no matching key was found 16 | std::string string(std::string); 17 | }; 18 | 19 | // Operator for string() 20 | inline std::string operator ""_lang (const char * key, size_t size) { 21 | return Utils::Lang::string(std::string(key, size)); 22 | } 23 | 24 | #endif -------------------------------------------------------------------------------- /Application/include/utils/NX.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_NX_HPP 2 | #define UTILS_NX_HPP 3 | 4 | #include "nx/Title.hpp" 5 | #include "Types.hpp" 6 | #include "nx/User.hpp" 7 | #include <vector> 8 | 9 | // Comparison of AccountUid 10 | bool operator ==(const AccountUid &a, const AccountUid &b); 11 | 12 | // A whole heap of utility functions for Switch-related objects/things 13 | namespace Utils::NX { 14 | // Returns ThemeType matching Horizon's 15 | ThemeType getHorizonTheme(); 16 | 17 | // Returns NX::User* if launched via User Page, nullptr otherwise 18 | ::NX::User * getUserPageUser(); 19 | 20 | // Returns Language matching system language, or default if no language found 21 | Language getSystemLanguage(); 22 | 23 | // Returns vector containing users on the console 24 | std::vector<::NX::User *> getUserObjects(); 25 | 26 | // Returns vector containing ALL played titles 27 | std::vector<::NX::Title *> getTitleObjects(std::vector<::NX::User *>); 28 | 29 | void startServices(); 30 | void stopServices(); 31 | }; 32 | 33 | #endif -------------------------------------------------------------------------------- /Application/include/utils/ThemeUtils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_THEME_HPP 2 | #define UTILS_THEME_HPP 3 | 4 | #include "Aether/Aether.hpp" 5 | 6 | namespace Utils::Theme { 7 | // Reads theme.ini 8 | void readIni(); 9 | 10 | // Fill variable with values under section 11 | void readValues(std::string, Aether::Colour &); 12 | 13 | // Write variable to ini section 14 | void writeValues(std::string, Aether::Colour); 15 | 16 | // Destroys and saves ini object 17 | void deleteIni(); 18 | }; 19 | 20 | #endif -------------------------------------------------------------------------------- /Application/include/utils/Time.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_TIME_HPP 2 | #define UTILS_TIME_HPP 3 | 4 | #include <ctime> 5 | #include "Types.hpp" 6 | 7 | // Bunch of helper functions for time_t and tm structs 8 | namespace Utils::Time { 9 | // TIME_T FUNCTIONS 10 | // Get time_t equivalent of given tm 11 | time_t getTimeT(struct tm); 12 | 13 | // Returns a string containing time since timestamp 14 | std::string timestampToString(time_t); 15 | 16 | // TM STRUCT FUNCTIONS 17 | // Return true if two structs have different dates 18 | bool areDifferentDates(struct tm, struct tm); 19 | 20 | // Dec/Inc tm by one D/M/Y 21 | struct tm decreaseTm(struct tm, char); 22 | struct tm increaseTm(struct tm, char); 23 | 24 | // Get time struct for current time 25 | struct tm getTmForCurrentTime(); 26 | 27 | // Get time struct for given value 28 | struct tm getTm(time_t); 29 | 30 | // Converts a POSIX timestamp to Pdm format 31 | uint32_t posixTimestampToPdm(uint64_t); 32 | 33 | // Return number of days in tm's month 34 | int tmGetDaysInMonth(struct tm); 35 | 36 | // Return a string with the date matching the tm struct in the given format 37 | // Max string length is optional and defaults to 50 38 | std::string tmToString(struct tm, std::string, unsigned short = 50); 39 | 40 | // Returns string in format "m d<suffix>, y" 41 | // Year can be omitted by passing false 42 | std::string tmToDate(struct tm, bool = true); 43 | 44 | // MISCELLANEOUS FUNCTIONS 45 | // Format given date to 'activity for' string 46 | std::string dateToActivityForString(struct tm, ViewPeriod); 47 | 48 | // Returns am/pm for given 24-hour int 49 | // Set true for upper case 50 | std::string getAMPM(int, bool = false); 51 | 52 | // Returns suffix for given date (within 1 - 31) 53 | std::string getDateSuffix(int); 54 | 55 | // Return full month string 56 | std::string getMonthString(int); 57 | 58 | // Return three character version of month 59 | std::string getShortMonthString(int); 60 | }; 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /Application/include/utils/UpdateUtils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_UPDATE_HPP 2 | #define UTILS_UPDATE_HPP 3 | 4 | #include <functional> 5 | #include "Types.hpp" 6 | 7 | namespace Utils::Update { 8 | // Returns true if an update available 9 | bool available(); 10 | 11 | // Query GitHub for latest release 12 | UpdateData check(); 13 | 14 | // Download latest release to /switch/NX-Activity-Log/download.nro 15 | // Accepts url to release, callback function for progress and return whether successful 16 | bool download(std::string, std::function<void(long long int, long long int)>); 17 | 18 | // Install (move) update nro (does nothing if .nro not present) 19 | void install(); 20 | 21 | // Returns true if it is time for an update (i.e. not checked today) 22 | bool needsCheck(); 23 | }; 24 | 25 | #endif -------------------------------------------------------------------------------- /Application/include/utils/Utils.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_HPP 2 | #define UTILS_HPP 3 | 4 | #include <string> 5 | #include <vector> 6 | 7 | namespace Utils { 8 | // Copy file from source path to dest path 9 | void copyFile(std::string, std::string); 10 | 11 | // Format given hour (0 - 23) in 12 hour format 12 | std::string format12H(unsigned short); 13 | 14 | // Format heading (takes username) 15 | std::string formatHeading(std::string); 16 | 17 | // Add commas to provided number 18 | std::string formatNumberComma(unsigned int); 19 | 20 | // Converts passed number to a hexadecimal string 21 | std::string formatHexString(const uint64_t); 22 | 23 | // Converts the provided hex string to a 64bit number 24 | uint64_t stringToU64(const std::string &); 25 | 26 | // Insert given version into string (replacing $[v]) (string, ver) 27 | std::string insertVersionInString(std::string, std::string); 28 | 29 | // Format the given timestamp as 'last played' string 30 | std::string lastPlayedToString(unsigned int); 31 | 32 | // Format the given number of launches into a string 33 | std::string launchesToString(unsigned int); 34 | std::string launchesToPlayedString(unsigned int); 35 | 36 | // Format the given playtime (in seconds) into hours and minutes 37 | std::string playtimeToString(unsigned int); 38 | 39 | // Format the given playtime (in seconds) into 'played for' string 40 | std::string playtimeToPlayedForString(unsigned int); 41 | 42 | // Format the given playtime (in seconds) into 'total playtime' string 43 | std::string playtimeToTotalPlaytimeString(unsigned int); 44 | 45 | // Merges two vectors into one (for sorting) 46 | // Vector to merge into, two vectors to merge 47 | template <typename A, typename B> 48 | void mergeVectors(std::vector<std::pair<A, B> >& m, const std::vector<A>& a, const std::vector<B>& b) { 49 | for (size_t i = 0; i < a.size(); i++) { 50 | m.push_back(std::make_pair(a[i], b[i])); 51 | } 52 | } 53 | 54 | // Splits a vector of pairs into two vectors 55 | // Vector with pairs, vectors to split into 56 | template <typename A, typename B> 57 | void splitVectors(const std::vector<std::pair<A, B> >& m, std::vector<A>& a, std::vector<B>& b) { 58 | for (size_t i = 0; i < m.size(); i++) { 59 | a[i] = m[i].first; 60 | b[i] = m[i].second; 61 | } 62 | } 63 | 64 | // Round the given double to the specified number of decimal places 65 | double roundToDecimalPlace(double, unsigned int); 66 | 67 | // Truncate string to given decimal places (don't use on strings without a decimal!) 68 | // Does nothing if outside of range or no decimal place 69 | std::string truncateToDecimalPlace(std::string, unsigned int); 70 | }; 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /Application/romfs/icon/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tallbl0nde/NX-Activity-Log/b3aab30c5f574c78144ca1ebe4875ef9ccbd95e6/Application/romfs/icon/download.png -------------------------------------------------------------------------------- /Application/romfs/icon/no_icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tallbl0nde/NX-Activity-Log/b3aab30c5f574c78144ca1ebe4875ef9ccbd95e6/Application/romfs/icon/no_icon.jpg -------------------------------------------------------------------------------- /Application/source/Application.ExportJob.cpp: -------------------------------------------------------------------------------- 1 | #include "Application.ExportJob.hpp" 2 | #include <fstream> 3 | #include "nlohmann/json.hpp" 4 | #include "utils/Utils.hpp" 5 | 6 | namespace Main { 7 | Application::ExportJob::ExportJob(Application * app, std::atomic<double> & percent) : Aether::ThreadPool::Job(), percent(percent) { 8 | this->app = app; 9 | } 10 | 11 | void Application::ExportJob::work() { 12 | // Reset percentage 13 | this->percent = 0; 14 | 15 | // Create JSON object and fill with metadata 16 | nlohmann::json json; 17 | struct tm time = Utils::Time::getTmForCurrentTime(); 18 | json["exportString"] = Utils::Time::tmToString(time, "%B %d %Y, %T"); 19 | json["exportTimestamp"] = Utils::Time::getTimeT(time); 20 | json["exportVersion"] = std::string(VER_STRING); 21 | 22 | // Iterate over each user 23 | json["users"] = nlohmann::json::array(); 24 | for (size_t i = 0; i < this->app->users.size(); i++) { 25 | NX::User * user = this->app->users[i]; 26 | nlohmann::json uJson; 27 | 28 | // Add user metadata 29 | uJson["name"] = user->username(); 30 | uJson["id"] = Utils::formatHexString(user->ID().uid[0]) + Utils::formatHexString(user->ID().uid[1]); 31 | uJson["titles"] = nlohmann::json::array(); 32 | 33 | // Iterate over user's played titles 34 | for (size_t j = 0; j < this->app->titles.size(); j++) { 35 | NX::Title * title = this->app->titles[j]; 36 | nlohmann::json tJson; 37 | 38 | // Check if played, and if not move onto next 39 | NX::RecentPlayStatistics * stats = this->app->playdata_->getRecentStatisticsForTitleAndUser(title->titleID(), std::numeric_limits<u64>::min(), std::numeric_limits<u64>::max(), user->ID()); 40 | bool recentLaunched = (stats->launches != 0); 41 | delete stats; 42 | 43 | // Add title metadata 44 | tJson["name"] = title->name(); 45 | tJson["id"] = Utils::formatHexString(title->titleID()); 46 | 47 | // Get all title events 48 | std::vector<NX::PlayEvent> events = this->app->playdata_->getPlayEvents(std::numeric_limits<u64>::min(), std::numeric_limits<u64>::max(), title->titleID(), user->ID()); 49 | tJson["events"] = nlohmann::json::array(); 50 | if (!events.empty()) { 51 | // Iterate over all events 52 | nlohmann::json eJson; 53 | for (size_t k = 0; k < events.size(); k++) { 54 | std::string str; 55 | switch (events[k].eventType) { 56 | case NX::EventType::Applet_Launch: 57 | str = "Launched"; 58 | break; 59 | 60 | case NX::EventType::Applet_Exit: 61 | str = "Closed"; 62 | break; 63 | 64 | case NX::EventType::Applet_InFocus: 65 | str = "Gained Focus"; 66 | break; 67 | 68 | case NX::EventType::Applet_OutFocus: 69 | str = "Lost Focus"; 70 | break; 71 | 72 | case NX::EventType::Account_Active: 73 | str = "Account Login"; 74 | break; 75 | 76 | case NX::EventType::Account_Inactive: 77 | str = "Account Logout"; 78 | break; 79 | 80 | default: 81 | str = "Unknown"; 82 | break; 83 | } 84 | 85 | eJson["clockTimestamp"] = events[k].clockTimestamp; 86 | eJson["steadyTimestamp"] = events[k].steadyTimestamp; 87 | eJson["type"] = str; 88 | tJson["events"].push_back(eJson); 89 | } 90 | } 91 | 92 | // Get all summary stats 93 | NX::PlayStatistics * stats2 = this->app->playdata_->getStatisticsForUser(title->titleID(), user->ID()); 94 | bool allLaunched = (stats2->launches != 0); 95 | tJson["summary"]["firstPlayed"] = pdmPlayTimestampToPosix(stats2->firstPlayed); 96 | tJson["summary"]["lastPlayed"] = pdmPlayTimestampToPosix(stats2->lastPlayed); 97 | tJson["summary"]["playtime"] = stats2->playtime; 98 | tJson["summary"]["launches"] = stats2->launches; 99 | delete stats2; 100 | 101 | // Append title if played at least once 102 | if (recentLaunched || allLaunched) { 103 | uJson["titles"].push_back(tJson); 104 | } 105 | 106 | // Update percentage 107 | size_t current = (i * this->app->titles.size()) + j; 108 | size_t total = this->app->users.size() * this->app->titles.size(); 109 | this->percent = 99 * (current/static_cast<double>(total)); 110 | } 111 | 112 | // Append user 113 | json["users"].push_back(uJson); 114 | } 115 | 116 | // Write to file 117 | std::ofstream file("/switch/NX-Activity-Log/export.json"); 118 | file << json.dump(4) << std::endl; 119 | 120 | // Pause so those with little data can still see the process completed successfully 121 | this->percent = 99.9; 122 | std::this_thread::sleep_for(std::chrono::milliseconds(250)); 123 | this->percent = 100; 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /Application/source/Application.ImportJob.cpp: -------------------------------------------------------------------------------- 1 | #include "Application.ImportJob.hpp" 2 | #include <filesystem> 3 | #include <fstream> 4 | #include <iomanip> 5 | #include "nlohmann/json.hpp" 6 | #include "utils/Utils.hpp" 7 | #include "utils/Time.hpp" 8 | 9 | namespace Main { 10 | Application::ImportJob::ImportJob(Application * app, std::atomic<double> & percent) : Aether::ThreadPool::Job(), percent(percent) { 11 | this->app = app; 12 | } 13 | 14 | void Application::ImportJob::work() { 15 | // Reset percentage 16 | this->percent = 0; 17 | 18 | // Read in import JSON 19 | if (!std::filesystem::exists("/switch/NX-Activity-Log/import.json")) { 20 | this->percent = 100; 21 | return; 22 | } 23 | std::ifstream importFile("/switch/NX-Activity-Log/import.json"); 24 | nlohmann::json importJson = nlohmann::json::parse(importFile); 25 | 26 | // Create imported JSON 27 | nlohmann::json json; 28 | struct tm time = Utils::Time::getTmForCurrentTime(); 29 | json["aWarning"] = "This file was generated automatically - editing it may cause crashes!"; 30 | json["importString"] = Utils::Time::tmToString(time, "%B %d %Y, %T"); 31 | json["importTimestamp"] = Utils::Time::getTimeT(time); 32 | json["importVersion"] = std::string(VER_STRING); 33 | 34 | // Abort if no users array in JSON 35 | if (importJson["users"] == nullptr) { 36 | this->percent = 100; 37 | return; 38 | } 39 | 40 | // Count number of "iterations" that need to be performed in order to 41 | // monitor the progress 42 | size_t current = 0; 43 | size_t total = 0; 44 | for (nlohmann::json user : importJson["users"]) { 45 | if (user["titles"] != nullptr) { 46 | total += user["titles"].size(); 47 | } 48 | } 49 | 50 | // Iterate over each user in the import JSON, looking for 51 | // matching usernames 52 | for (nlohmann::json user : importJson["users"]) { 53 | // Skip over users with no name, id or titles 54 | if (user["name"] == nullptr || user["id"] == nullptr || user["titles"] == nullptr) { 55 | continue; 56 | } 57 | 58 | // Check if we have a user with matching name 59 | std::vector<NX::User *>::iterator it = std::find_if(this->app->users.begin(), this->app->users.end(), [user](NX::User * u) { 60 | return (user["name"].get<std::string>() == u->username()); 61 | }); 62 | if (it == this->app->users.end()) { 63 | continue; 64 | } 65 | 66 | // We have a matching user, so now create an entry for them 67 | NX::User * u = (*it); 68 | nlohmann::json uJson; 69 | std::string id = user["id"].get<std::string>(); 70 | uJson["id"] = nlohmann::json::array(); 71 | uJson["id"].push_back(u->ID().uid[0]); 72 | uJson["id"].push_back(u->ID().uid[1]); 73 | uJson["titles"] = nlohmann::json::array(); 74 | 75 | // Iterate over each title, copying all metadata + events 76 | // (but reformat to an easier to use format) 77 | for (nlohmann::json title : user["titles"]) { 78 | current++; 79 | 80 | // Skip title if no name or id present 81 | if (title["id"] == nullptr || title["name"] == nullptr) { 82 | continue; 83 | } 84 | 85 | nlohmann::json tJson; 86 | tJson["id"] = Utils::stringToU64(title["id"].get<std::string>()); 87 | tJson["name"] = title["name"]; 88 | tJson["events"] = nlohmann::json::array(); 89 | 90 | // Iterate over each event 91 | nlohmann::json eJson; 92 | for (nlohmann::json event : title["events"]) { 93 | // Skip event if no timestamp or type prsent 94 | if (event["type"] == nullptr || event["clockTimestamp"] == nullptr || event["steadyTimestamp"] == nullptr) { 95 | continue; 96 | } 97 | 98 | // Convert event type to enum 99 | std::string str = event["type"].get<std::string>(); 100 | int val; 101 | if (str == "Launched") { 102 | val = static_cast<int>(NX::EventType::Applet_Launch); 103 | 104 | } else if (str == "Closed") { 105 | val = static_cast<int>(NX::EventType::Applet_Exit); 106 | 107 | } else if (str == "Gained Focus") { 108 | val = static_cast<int>(NX::EventType::Applet_InFocus); 109 | 110 | } else if (str == "Lost Focus") { 111 | val = static_cast<int>(NX::EventType::Applet_OutFocus); 112 | 113 | } else if (str == "Account Login") { 114 | val = static_cast<int>(NX::EventType::Account_Active); 115 | 116 | } else if (str == "Account Logout") { 117 | val = static_cast<int>(NX::EventType::Account_Inactive); 118 | 119 | } else { 120 | // Skip event if invalid type 121 | continue; 122 | } 123 | 124 | eJson["clockTimestamp"] = event["clockTimestamp"]; 125 | eJson["steadyTimestamp"] = event["steadyTimestamp"]; 126 | eJson["type"] = val; 127 | tJson["events"].push_back(eJson); 128 | } 129 | 130 | // Copy over summary stats 131 | if (title["summary"] != nullptr) { 132 | nlohmann::json s = title["summary"]; 133 | if (s["firstPlayed"] != nullptr && s["lastPlayed"] != nullptr && s["playtime"] != nullptr && s["launches"] != nullptr) { 134 | tJson["summary"] = nlohmann::json(); 135 | tJson["summary"]["firstPlayed"] = Utils::Time::posixTimestampToPdm(s["firstPlayed"].get<uint64_t>()); 136 | tJson["summary"]["lastPlayed"] = Utils::Time::posixTimestampToPdm(s["lastPlayed"].get<uint64_t>()); 137 | tJson["summary"]["playtime"] = s["playtime"]; 138 | tJson["summary"]["launches"] = s["launches"]; 139 | } 140 | } 141 | 142 | // Append title if at least one event or a summary is present 143 | if (tJson["events"].size() > 0 || tJson["summary"] != nullptr) { 144 | uJson["titles"].push_back(tJson); 145 | } 146 | 147 | // Update percentage 148 | this->percent = 99 * (current/static_cast<double>(total)); 149 | } 150 | 151 | // Append user if they have some titles 152 | if (uJson["titles"].size() > 0) { 153 | json["users"].push_back(uJson); 154 | } 155 | } 156 | 157 | // Write imported data to file 158 | std::ofstream importedFile("/switch/NX-Activity-Log/importedData.json"); 159 | importedFile << json.dump(4) << std::endl; 160 | 161 | // Pause so those with little data can still see the process completed successfully 162 | this->percent = 99.9; 163 | std::this_thread::sleep_for(std::chrono::milliseconds(250)); 164 | this->percent = 100; 165 | } 166 | }; 167 | -------------------------------------------------------------------------------- /Application/source/Types.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/Lang.hpp" 2 | #include "Types.hpp" 3 | 4 | std::string toString(Language l) { 5 | std::string str; 6 | 7 | switch (l) { 8 | case Language::Default: 9 | str = "common.default"_lang; 10 | break; 11 | 12 | case Language::English: 13 | str = "English"; 14 | break; 15 | 16 | case Language::French: 17 | str = "Français"; 18 | break; 19 | 20 | case Language::German: 21 | str = "Deutsch"; 22 | break; 23 | 24 | case Language::Spanish: 25 | str = "Español"; 26 | break; 27 | 28 | case Language::Italian: 29 | str = "Italiano"; 30 | break; 31 | 32 | case Language::Portugese: 33 | str = "Português"; 34 | break; 35 | 36 | case Language::Russian: 37 | str = "Pусский"; 38 | break; 39 | 40 | case Language::Turkish: 41 | str = "Türkçe"; 42 | break; 43 | 44 | case Language::ChineseTraditional: 45 | str = "繁体中文"; 46 | break; 47 | 48 | case Language::Chinese: 49 | str = "简体中文"; 50 | break; 51 | 52 | case Language::Korean: 53 | str = "한국어"; 54 | break; 55 | 56 | default: 57 | break; 58 | } 59 | 60 | return str; 61 | } 62 | 63 | std::string toString(SortType s) { 64 | std::string str; 65 | 66 | switch (s) { 67 | case SortType::AlphaAsc: 68 | str = "common.sort.name"_lang; 69 | break; 70 | case SortType::FirstPlayed: 71 | str = "common.sort.firstPlayed"_lang; 72 | break; 73 | case SortType::LastPlayed: 74 | str = "common.sort.recentlyPlayed"_lang; 75 | break; 76 | case SortType::HoursAsc: 77 | str = "common.sort.mostPlaytime"_lang; 78 | break; 79 | case SortType::HoursDec: 80 | str = "common.sort.leastPlaytime"_lang; 81 | break; 82 | case SortType::LaunchAsc: 83 | str = "common.sort.mostLaunched"_lang; 84 | break; 85 | case SortType::LaunchDec: 86 | str = "common.sort.leastLaunched"_lang; 87 | break; 88 | 89 | default: 90 | break; 91 | } 92 | 93 | return str; 94 | } 95 | 96 | std::string toString(ThemeType t) { 97 | std::string str; 98 | 99 | switch (t) { 100 | case Auto: 101 | str = "settings.appearance.theme.auto"_lang; 102 | break; 103 | 104 | case Custom: 105 | str = "settings.appearance.theme.custom"_lang; 106 | break; 107 | 108 | case Dark: 109 | str = "settings.appearance.theme.dark"_lang; 110 | break; 111 | 112 | case Light: 113 | str = "settings.appearance.theme.light"_lang; 114 | break; 115 | 116 | default: 117 | break; 118 | } 119 | 120 | return str; 121 | } 122 | 123 | std::string toString(ViewPeriod v) { 124 | std::string str; 125 | 126 | switch (v) { 127 | case ViewPeriod::Day: 128 | str = "common.view.day"_lang; 129 | break; 130 | 131 | case ViewPeriod::Month: 132 | str = "common.view.month"_lang; 133 | break; 134 | 135 | case ViewPeriod::Year: 136 | str = "common.view.year"_lang; 137 | break; 138 | 139 | default: 140 | break; 141 | } 142 | 143 | return str; 144 | } -------------------------------------------------------------------------------- /Application/source/main.cpp: -------------------------------------------------------------------------------- 1 | #include "Application.hpp" 2 | 3 | int main() { 4 | Main::Application * app = new Main::Application(); 5 | app->run(); 6 | delete app; 7 | return 0; 8 | } -------------------------------------------------------------------------------- /Application/source/nx/Title.cpp: -------------------------------------------------------------------------------- 1 | #include <cstring> 2 | #include <fstream> 3 | #include "nx/Title.hpp" 4 | 5 | namespace NX { 6 | Title::Title(TitleID titleID, bool installed) { 7 | this->titleID_ = titleID; 8 | this->is_installed = installed; 9 | this->name_ = ""; 10 | this->ptr = nullptr; 11 | 12 | // Get name and icon 13 | NsApplicationControlData data; 14 | NacpLanguageEntry * lang = nullptr; 15 | size_t nacp_size; 16 | Result rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, this->titleID_, &data, sizeof(NsApplicationControlData), &nacp_size); 17 | if (R_SUCCEEDED(rc)){ 18 | // Get name 19 | rc = nacpGetLanguageEntry(&data.nacp, &lang); 20 | if (R_SUCCEEDED(rc)){ 21 | this->name_ = std::string(lang->name); 22 | } 23 | 24 | // Get icon 25 | this->size = nacp_size - sizeof(data.nacp); 26 | this->ptr = (u8 *) malloc(this->size); 27 | memcpy(this->ptr, data.icon, this->size); 28 | } 29 | } 30 | 31 | Title::Title(const TitleID titleID, const std::string & name) { 32 | this->titleID_ = titleID; 33 | this->is_installed = false; 34 | this->name_ = name; 35 | 36 | // Load in image 37 | std::ifstream file("romfs:/icon/no_icon.jpg", std::ios::binary); 38 | file.unsetf(std::ios::skipws); 39 | file.seekg(0, std::ios::end); 40 | this->size = file.tellg(); 41 | file.seekg(0, std::ios::beg); 42 | 43 | this->ptr = static_cast<u8 *>(malloc(this->size)); 44 | u8 byte; 45 | size_t pos = 0; 46 | while (file >> byte && pos < this->size) { 47 | this->ptr[pos] = byte; 48 | pos++; 49 | } 50 | } 51 | 52 | TitleID Title::titleID() { 53 | return this->titleID_; 54 | } 55 | 56 | bool Title::isInstalled() { 57 | return this->is_installed; 58 | } 59 | 60 | std::string Title::name() { 61 | return this->name_; 62 | } 63 | 64 | u8 * Title::imgPtr() { 65 | return this->ptr; 66 | } 67 | 68 | u32 Title::imgSize() { 69 | return this->size; 70 | } 71 | 72 | Title::~Title() { 73 | if (this->ptr != nullptr) { 74 | free(this->ptr); 75 | } 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /Application/source/nx/User.cpp: -------------------------------------------------------------------------------- 1 | #include "nx/User.hpp" 2 | 3 | namespace NX { 4 | User::User(AccountUid ID){ 5 | this->ID_ = ID; 6 | 7 | // Set values in case of an error 8 | this->username_ = ""; 9 | this->ptr = nullptr; 10 | this->size = 0; 11 | 12 | // Get name and icon 13 | AccountProfile profile; 14 | AccountProfileBase profile_base; 15 | 16 | Result rc = accountGetProfile(&profile, this->ID_); 17 | if (R_SUCCEEDED(rc)){ 18 | // Get username 19 | rc = accountProfileGet(&profile, NULL, &profile_base); 20 | if (R_SUCCEEDED(rc)){ 21 | this->username_ = profile_base.nickname; 22 | } 23 | 24 | u32 tmp; 25 | // Get image size and allocate memory 26 | rc = accountProfileGetImageSize(&profile, &this->size); 27 | if (R_SUCCEEDED(rc)) { 28 | this->ptr = (u8 *) malloc(this->size); 29 | 30 | // Get image 31 | rc = accountProfileLoadImage(&profile, this->ptr, this->size, &tmp); 32 | } 33 | 34 | // Close profile 35 | accountProfileClose(&profile); 36 | } 37 | } 38 | 39 | AccountUid User::ID() { 40 | return this->ID_; 41 | } 42 | 43 | std::string User::username() { 44 | return this->username_; 45 | } 46 | 47 | u8 * User::imgPtr() { 48 | return this->ptr; 49 | } 50 | 51 | u32 User::imgSize() { 52 | return this->size; 53 | } 54 | 55 | User::~User() { 56 | // Free memory used by image texture 57 | if (this->ptr != nullptr) { 58 | free(this->ptr); 59 | } 60 | } 61 | }; -------------------------------------------------------------------------------- /Application/source/ui/Theme.cpp: -------------------------------------------------------------------------------- 1 | #include <numbers> 2 | #include "utils/NX.hpp" 3 | #include "ui/Theme.hpp" 4 | #include "ui/ThemePresets.hpp" 5 | #include "utils/ThemeUtils.hpp" 6 | 7 | Theme::Theme(ThemeType t) { 8 | Utils::Theme::readIni(); 9 | this->setTheme(t); 10 | } 11 | 12 | Aether::Colour Theme::accent() { 13 | return this->theme.accent; 14 | } 15 | 16 | Aether::Colour Theme::altBG() { 17 | return this->theme.altBG; 18 | } 19 | 20 | Aether::Colour Theme::bg() { 21 | return this->theme.bg; 22 | } 23 | 24 | Aether::Colour Theme::fg() { 25 | return this->theme.fg; 26 | } 27 | 28 | std::function<Aether::Colour(uint32_t)> Theme::highlightFunc() { 29 | // Copy colours in case object is destroyed 30 | Aether::Colour h1 = this->theme.highlight1; 31 | Aether::Colour h2 = this->theme.highlight2; 32 | return [h1, h2](uint32_t t) { 33 | Aether::Colour col; 34 | col.setR(h1.r() + ((h2.r() - h1.r()) * (0.5 * sin(1.8 * std::numbers::pi * (t/1000.0)) + 0.5))); 35 | col.setG(h1.g() + ((h2.g() - h1.g()) * (0.5 * sin(1.8 * std::numbers::pi * (t/1000.0)) + 0.5))); 36 | col.setB(h1.b() + ((h2.b() - h1.b()) * (0.5 * sin(1.8 * std::numbers::pi * (t/1000.0)) + 0.5))); 37 | col.setA(h1.a() + ((h2.a() - h1.a()) * (0.5 * sin(1.8 * std::numbers::pi * (t/1000.0)) + 0.5))); 38 | return col; 39 | }; 40 | } 41 | 42 | Aether::Colour Theme::highlight1() { 43 | return this->theme.highlight1; 44 | } 45 | 46 | Aether::Colour Theme::highlight2() { 47 | return this->theme.highlight2; 48 | } 49 | 50 | Aether::Colour Theme::highlightBG() { 51 | return this->theme.highlightBG; 52 | } 53 | 54 | Aether::Colour Theme::mutedLine() { 55 | return this->theme.mutedLine; 56 | } 57 | 58 | Aether::Colour Theme::mutedText() { 59 | return this->theme.mutedText; 60 | } 61 | 62 | Aether::Colour Theme::selected() { 63 | return this->theme.selected; 64 | } 65 | 66 | Aether::Colour Theme::text() { 67 | return this->theme.text; 68 | } 69 | 70 | void Theme::setAccent(Aether::Colour c) { 71 | if (this->type != ThemeType::Custom) { 72 | return; 73 | } 74 | 75 | this->theme.accent = c; 76 | } 77 | 78 | void Theme::setAltBG(Aether::Colour c) { 79 | if (this->type != ThemeType::Custom) { 80 | return; 81 | } 82 | 83 | this->theme.altBG = c; 84 | } 85 | 86 | void Theme::setBg(Aether::Colour c) { 87 | if (this->type != ThemeType::Custom) { 88 | return; 89 | } 90 | 91 | this->theme.bg = c; 92 | } 93 | 94 | void Theme::setFg(Aether::Colour c) { 95 | if (this->type != ThemeType::Custom) { 96 | return; 97 | } 98 | 99 | this->theme.fg = c; 100 | } 101 | 102 | void Theme::setHighlight1(Aether::Colour c) { 103 | if (this->type != ThemeType::Custom) { 104 | return; 105 | } 106 | 107 | this->theme.highlight1 = c; 108 | } 109 | 110 | void Theme::setHighlight2(Aether::Colour c) { 111 | if (this->type != ThemeType::Custom) { 112 | return; 113 | } 114 | 115 | this->theme.highlight2 = c; 116 | } 117 | 118 | void Theme::setHighlightBG(Aether::Colour c) { 119 | if (this->type != ThemeType::Custom) { 120 | return; 121 | } 122 | 123 | this->theme.highlightBG = c; 124 | } 125 | 126 | void Theme::setMutedLine(Aether::Colour c) { 127 | if (this->type != ThemeType::Custom) { 128 | return; 129 | } 130 | 131 | this->theme.mutedLine = c; 132 | } 133 | 134 | void Theme::setMutedText(Aether::Colour c) { 135 | if (this->type != ThemeType::Custom) { 136 | return; 137 | } 138 | 139 | this->theme.mutedText = c; 140 | } 141 | 142 | void Theme::setSelected(Aether::Colour c) { 143 | if (this->type != ThemeType::Custom) { 144 | return; 145 | } 146 | 147 | this->theme.selected = c; 148 | } 149 | 150 | void Theme::setText(Aether::Colour c) { 151 | if (this->type != ThemeType::Custom) { 152 | return; 153 | } 154 | 155 | this->theme.text = c; 156 | } 157 | 158 | 159 | void Theme::setTheme(ThemeType t) { 160 | switch (t) { 161 | case ThemeType::Auto: 162 | this->setThemeAuto(); 163 | break; 164 | 165 | case ThemeType::Custom: 166 | this->setThemeCustom(); 167 | break; 168 | 169 | case ThemeType::Dark: 170 | this->setThemeDark(); 171 | break; 172 | 173 | case ThemeType::Light: 174 | this->setThemeLight(); 175 | break; 176 | 177 | // Never called but I don't like compiler warnings 178 | default: 179 | break; 180 | } 181 | } 182 | 183 | void Theme::setThemeAuto() { 184 | switch (Utils::NX::getHorizonTheme()) { 185 | case ThemeType::Dark: 186 | this->setThemeDark(); 187 | break; 188 | 189 | case ThemeType::Light: 190 | this->setThemeLight(); 191 | break; 192 | 193 | // Never called but I don't like compiler warnings 194 | default: 195 | break; 196 | } 197 | } 198 | 199 | void Theme::setThemeCustom() { 200 | // Set values to default in case values don't exist 201 | this->setThemeAuto(); 202 | // Get values 203 | Utils::Theme::readValues("accent", this->theme.accent); 204 | Utils::Theme::readValues("altBG", this->theme.altBG); 205 | Utils::Theme::readValues("bg", this->theme.bg); 206 | Utils::Theme::readValues("fg", this->theme.fg); 207 | Utils::Theme::readValues("highlight1", this->theme.highlight1); 208 | Utils::Theme::readValues("highlight2", this->theme.highlight2); 209 | Utils::Theme::readValues("highlightBG", this->theme.highlightBG); 210 | Utils::Theme::readValues("mutedLine", this->theme.mutedLine); 211 | Utils::Theme::readValues("mutedText", this->theme.mutedText); 212 | Utils::Theme::readValues("selected", this->theme.selected); 213 | Utils::Theme::readValues("text", this->theme.text); 214 | this->type = ThemeType::Custom; 215 | } 216 | 217 | void Theme::setThemeDark() { 218 | this->theme = ThemePreset::Dark; 219 | this->type = ThemeType::Dark; 220 | } 221 | 222 | void Theme::setThemeLight() { 223 | this->theme = ThemePreset::Light; 224 | this->type = ThemeType::Light; 225 | } 226 | 227 | void Theme::saveCustom() { 228 | if (this->type == ThemeType::Custom) { 229 | // Write custom values 230 | Utils::Theme::writeValues("accent", this->theme.accent); 231 | Utils::Theme::writeValues("altBG", this->theme.altBG); 232 | Utils::Theme::writeValues("bg", this->theme.bg); 233 | Utils::Theme::writeValues("fg", this->theme.fg); 234 | Utils::Theme::writeValues("highlight1", this->theme.highlight1); 235 | Utils::Theme::writeValues("highlight2", this->theme.highlight2); 236 | Utils::Theme::writeValues("highlightBG", this->theme.highlightBG); 237 | Utils::Theme::writeValues("mutedLine", this->theme.mutedLine); 238 | Utils::Theme::writeValues("mutedText", this->theme.mutedText); 239 | Utils::Theme::writeValues("selected", this->theme.selected); 240 | Utils::Theme::writeValues("text", this->theme.text); 241 | Utils::Theme::deleteIni(); 242 | Utils::Theme::readIni(); 243 | } 244 | } 245 | 246 | Theme::~Theme() { 247 | this->saveCustom(); 248 | Utils::Theme::deleteIni(); 249 | } -------------------------------------------------------------------------------- /Application/source/ui/element/ListActivity.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/element/ListActivity.hpp" 2 | 3 | namespace CustomElm { 4 | ListActivity::ListActivity() : AsyncItem() { 5 | Element::setH(ListActivity::height); 6 | 7 | // Create our 'bounding' rectangles 8 | this->topR = new Aether::Rectangle(this->x(), this->y(), this->w(), 1); 9 | this->addElement(this->topR); 10 | this->bottomR = new Aether::Rectangle(this->x(), this->y() + this->h(), this->w(), 1); 11 | this->addElement(this->bottomR); 12 | 13 | // Start with blank elements 14 | this->icon = new Aether::Image(0, 0, "", Aether::Render::Wait); 15 | this->title = new Aether::Text(0, 0, "", 20, Aether::Render::Wait); 16 | this->playtime = new Aether::Text(0, 0, "", 20, Aether::Render::Wait); 17 | this->mutedLeft = new Aether::Text(0, 0, "", 20, Aether::Render::Wait); 18 | this->mutedRight = new Aether::Text(0, 0, "", 20, Aether::Render::Wait); 19 | this->rank = new Aether::Text(0, 0, "", 20, Aether::Render::Wait); 20 | } 21 | 22 | void ListActivity::positionElements() { 23 | this->topR->setRectSize(this->w(), 1); 24 | this->bottomR->setRectSize(this->w(), 1); 25 | 26 | this->mutedRight->setX(this->x() + this->w() - this->mutedRight->w() - 15); 27 | this->rank->setX(this->x() + this->w() - this->rank->w() - 15); 28 | 29 | if (this->title->textureWidth() > this->rank->x() - this->title->x() - 25) { 30 | this->title->setW(this->rank->x() - this->title->x() - 25); 31 | } 32 | } 33 | 34 | void ListActivity::processText(Aether::Text * & text, std::function<Aether::Text * ()> getNew) { 35 | // Remove original 36 | this->removeTexture(text); 37 | this->removeElement(text); 38 | 39 | // Get (and assign) new text object 40 | text = getNew(); 41 | 42 | // Don't add if empty string 43 | if (!text->string().empty()) { 44 | this->addElement(text); 45 | this->addTexture(text); 46 | } 47 | } 48 | 49 | void ListActivity::update(uint32_t dt) { 50 | // Update normally first 51 | AsyncItem::update(dt); 52 | 53 | // Match rectangle alphas with the icon's 54 | Aether::Colour col = this->topR->colour(); 55 | col.setA(this->icon->colour().a()); 56 | this->topR->setColour(col); 57 | this->bottomR->setColour(col); 58 | 59 | bool hide = (this->icon->hidden()); 60 | this->topR->setHidden(hide); 61 | this->bottomR->setHidden(hide); 62 | } 63 | 64 | void ListActivity::setImage(uint8_t * ptr, uint32_t size) { 65 | // Remove original 66 | this->removeTexture(this->icon); 67 | this->removeElement(this->icon); 68 | 69 | // Add new icon and render it asynchronously 70 | this->icon = new Aether::Image(0, 0, ptr, size, Aether::Render::Wait); 71 | this->icon->setXYWH(this->x() + 10, this->y() + 10, this->h() - 20, this->h() - 20); 72 | this->icon->setScaleDimensions(this->icon->w(), this->icon->h()); 73 | this->addElement(this->icon); 74 | this->addTexture(this->icon); 75 | } 76 | 77 | void ListActivity::setTitle(const std::string & title) { 78 | this->processText(this->title, [this, title]() { 79 | Aether::Text * t = new Aether::Text(this->x() + 130, this->y() + 15, title, 22, Aether::Render::Wait); 80 | t->setCanScroll(true); 81 | t->setScrollSpeed(75); 82 | return t; 83 | }); 84 | } 85 | 86 | void ListActivity::setPlaytime(const std::string & playtime) { 87 | this->processText(this->playtime, [this, playtime]() { 88 | return new Aether::Text(this->x() + 130, this->y() + 51, playtime, 18, Aether::Render::Wait); 89 | }); 90 | } 91 | 92 | void ListActivity::setLeftMuted(const std::string & text) { 93 | this->processText(this->mutedLeft, [this, text]() { 94 | return new Aether::Text(this->x() + 130, this->y() + 82, text, 18, Aether::Render::Wait); 95 | }); 96 | } 97 | 98 | void ListActivity::setRightMuted(const std::string & text) { 99 | this->processText(this->mutedRight, [this, text]() { 100 | return new Aether::Text(this->x() + this->w(), this->y() + 84, text, 16, Aether::Render::Wait); 101 | }); 102 | } 103 | 104 | void ListActivity::setRank(const std::string & text) { 105 | this->processText(this->rank, [this, text]() { 106 | Aether::Text * t = new Aether::Text(this->x() + this->w(), this->y() + 15, text, 18, Aether::Render::Wait); 107 | t->setColour(this->mutedRight->colour()); 108 | return t; 109 | }); 110 | } 111 | 112 | void ListActivity::setTitleColour(const Aether::Colour & c) { 113 | this->title->setColour(c); 114 | } 115 | 116 | void ListActivity::setPlaytimeColour(const Aether::Colour & c) { 117 | this->playtime->setColour(c); 118 | } 119 | 120 | void ListActivity::setMutedColour(const Aether::Colour & c) { 121 | this->mutedLeft->setColour(c); 122 | this->mutedRight->setColour(c); 123 | this->rank->setColour(c); 124 | } 125 | 126 | void ListActivity::setLineColour(const Aether::Colour & c) { 127 | this->topR->setColour(c); 128 | this->bottomR->setColour(c); 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /Application/source/ui/element/ListAdjust.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/element/ListAdjust.hpp" 2 | 3 | namespace CustomElm { 4 | ListAdjust::ListAdjust(const std::string & title, const std::string & record, const std::string & adjust) { 5 | this->setH(ListAdjust::height); 6 | 7 | this->topR = new Aether::Rectangle(this->x(), this->y(), this->w(), 1); 8 | this->addElement(this->topR); 9 | this->bottomR = new Aether::Rectangle(this->x(), this->y() + this->h(), this->w(), 1); 10 | this->addElement(this->bottomR); 11 | 12 | // Create elements 13 | this->icon = new Aether::Image(0, 0, "", Aether::Render::Wait); 14 | this->title = new Aether::Text(0, 0, title, 20, Aether::Render::Wait); 15 | this->title->setCanScroll(true); 16 | this->title->setScrollSpeed(75); 17 | this->recordTime = new Aether::Text(0, 0, record, 16, Aether::Render::Wait); 18 | this->adjustTime = new Aether::Text(0, 0, adjust, 16, Aether::Render::Wait); 19 | 20 | this->addElement(this->title); 21 | this->addTexture(this->title); 22 | this->addElement(this->recordTime); 23 | this->addTexture(this->recordTime); 24 | this->addElement(this->adjustTime); 25 | this->addTexture(this->adjustTime); 26 | } 27 | 28 | void ListAdjust::positionElements() { 29 | this->topR->setRectSize(this->w(), 1); 30 | this->bottomR->setRectSize(this->w(), 1); 31 | 32 | this->icon->setXY(this->x() + 10, this->y() + 10); 33 | this->title->setXY(this->icon->x() + this->icon->w() + 15, this->icon->y() + 3); 34 | this->recordTime->setXY(this->title->x(), this->title->y() + this->title->h() + 10); 35 | this->adjustTime->setXY(this->recordTime->x(), this->recordTime->y() + this->recordTime->h() + 6); 36 | 37 | int sz = this->w() - this->icon->w() - 40; 38 | if (this->title->textureWidth() > sz) { 39 | this->title->setW(sz); 40 | } 41 | } 42 | 43 | void ListAdjust::update(uint32_t dt) { 44 | // Update normally first 45 | AsyncItem::update(dt); 46 | 47 | // Match rectangle alphas with the icon's 48 | Aether::Colour col = this->topR->colour(); 49 | col.setA(this->icon->colour().a()); 50 | this->topR->setColour(col); 51 | this->bottomR->setColour(col); 52 | 53 | bool hide = (this->icon->hidden()); 54 | this->topR->setHidden(hide); 55 | this->bottomR->setHidden(hide); 56 | } 57 | 58 | void ListAdjust::setAdjustedTime(const std::string & str) { 59 | this->adjustTime->setString(str); 60 | this->positionElements(); 61 | } 62 | 63 | void ListAdjust::setImage(uint8_t * ptr, uint32_t size) { 64 | // Remove original 65 | this->removeTexture(this->icon); 66 | this->removeElement(this->icon); 67 | 68 | // Add new icon and render it asynchronously 69 | this->icon = new Aether::Image(0, 0, ptr, size, Aether::Render::Wait); 70 | this->icon->setWH(this->h() - 20, this->h() - 20); 71 | this->icon->setScaleDimensions(this->icon->w(), this->icon->h()); 72 | this->icon->setColour(Aether::Colour(255, 255, 255, 0)); 73 | this->addElement(this->icon); 74 | this->addTexture(this->icon); 75 | } 76 | 77 | void ListAdjust::setAdjustColour(const Aether::Colour & c) { 78 | this->adjustTime->setColour(c); 79 | } 80 | 81 | void ListAdjust::setLineColour(const Aether::Colour & c) { 82 | this->topR->setColour(c); 83 | this->bottomR->setColour(c); 84 | } 85 | 86 | void ListAdjust::setRecordColour(const Aether::Colour & c) { 87 | this->recordTime->setColour(c); 88 | } 89 | 90 | void ListAdjust::setTitleColour(const Aether::Colour & c) { 91 | this->title->setColour(c); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /Application/source/ui/element/ListColour.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/element/ListColour.hpp" 2 | #include "utils/Utils.hpp" 3 | 4 | // Height of element 5 | #define HEIGHT 70 6 | // Padding on left/right of text 7 | #define TEXT_PADDING 16 8 | 9 | // Formats colour as hex string 10 | static std::string RGBtoHex(Aether::Colour c) { 11 | char str[10]; 12 | snprintf(str, 10, "#%02x%02x%02x%02x", c.r(), c.g(), c.b(), c.a()); 13 | return std::string(str); 14 | } 15 | 16 | namespace CustomElm { 17 | ListColour::ListColour(std::string s, std::function<void()> f) : Aether::Element() { 18 | this->setH(HEIGHT); 19 | this->topR = new Aether::Rectangle(this->x(), this->y(), this->w(), 1); 20 | this->addElement(topR); 21 | this->bottomR = new Aether::Rectangle(this->x(), this->y() + this->h(), this->w(), 1); 22 | this->addElement(bottomR); 23 | this->text = new Aether::Text(this->x() + TEXT_PADDING, this->y() + this->h()/2, s, 22); 24 | this->text->setY(this->text->y() - this->text->h()/2); 25 | this->addElement(this->text); 26 | this->colour = new Aether::Rectangle(this->x() + this->w() - TEXT_PADDING - 100, this->y() + this->h()/2 - 20, 100, 40); 27 | this->addElement(this->colour); 28 | this->hex = new Aether::Text(this->x(), this->y(), "#", 18); 29 | this->hex->setY(this->y() + (this->h() - this->hex->h())/2); 30 | this->addElement(this->hex); 31 | this->onPress(f); 32 | } 33 | 34 | void ListColour::setColour(Aether::Colour c) { 35 | this->colour->setColour(c); 36 | this->hex->setString(RGBtoHex(c)); 37 | this->hex->setX(this->colour->x() - (1.5 * TEXT_PADDING) - this->hex->w()); 38 | } 39 | 40 | void ListColour::setLineColour(Aether::Colour c) { 41 | this->topR->setColour(c); 42 | this->bottomR->setColour(c); 43 | } 44 | 45 | void ListColour::setTextColour(Aether::Colour c) { 46 | this->hex->setColour(c); 47 | this->text->setColour(c); 48 | } 49 | 50 | void ListColour::setW(int w) { 51 | Element::setW(w); 52 | this->topR->setRectSize(w, 1); 53 | this->bottomR->setRectSize(w, 1); 54 | this->colour->setX(this->x() + this->w() - TEXT_PADDING - 100); 55 | this->hex->setX(this->colour->x() - (1.5 * TEXT_PADDING) - this->hex->w()); 56 | } 57 | }; -------------------------------------------------------------------------------- /Application/source/ui/element/ListHide.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/element/ListHide.hpp" 2 | 3 | namespace CustomElm { 4 | ListHide::ListHide(const std::string & title, const std::string & id) : Aether::AsyncItem() { 5 | Element::setH(ListHide::height); 6 | 7 | // Create our bounding rectangles 8 | this->topR = new Aether::Rectangle(this->x(), this->y(), this->w(), 1); 9 | this->addElement(this->topR); 10 | this->bottomR = new Aether::Rectangle(this->x(), this->y() + this->h(), this->w(), 1); 11 | this->addElement(this->bottomR); 12 | 13 | // Create tick 14 | this->circle = new Aether::Box(0, 0, this->h() - 50, this->h() - 50, 2, (this->h() - 48)/2); 15 | this->tick = new Aether::Tick(0, 0, this->h() - 50); 16 | 17 | this->addElement(this->circle); 18 | this->addElement(this->tick); 19 | 20 | // Create elements 21 | this->icon = new Aether::Image(0, 0, "", Aether::Render::Wait); 22 | this->title = new Aether::Text(0, 0, title, 22, Aether::Render::Wait); 23 | this->title->setCanScroll(true); 24 | this->title->setScrollSpeed(75); 25 | this->titleID = new Aether::Text(0, 0, id, 16, Aether::Render::Wait); 26 | 27 | this->addElement(this->title); 28 | this->addTexture(this->title); 29 | this->addElement(this->titleID); 30 | this->addTexture(this->titleID); 31 | } 32 | 33 | void ListHide::positionElements() { 34 | this->topR->setRectSize(this->w(), 1); 35 | this->bottomR->setRectSize(this->w(), 1); 36 | 37 | this->icon->setXY(this->x() + 10, this->y() + 10); 38 | this->title->setXY(this->icon->x() + this->icon->w() + 15, this->icon->y() + 5); 39 | this->titleID->setXY(this->title->x(), this->title->y() + this->title->h() + 10); 40 | this->circle->setXY(this->x() + this->w() - 20 - this->circle->w(), this->y() + 25); 41 | this->tick->setXY(this->circle->x(), this->circle->y()); 42 | 43 | int sz = this->w() - this->icon->w() - this->tick->w() - 60; 44 | if (this->title->textureWidth() > sz) { 45 | this->title->setW(sz); 46 | } 47 | } 48 | 49 | void ListHide::update(uint32_t dt) { 50 | // Update normally first 51 | AsyncItem::update(dt); 52 | 53 | // Match rectangle alphas with the icon's 54 | Aether::Colour col = this->topR->colour(); 55 | col.setA(this->icon->colour().a()); 56 | this->topR->setColour(col); 57 | this->bottomR->setColour(col); 58 | 59 | bool hide = (this->icon->hidden()); 60 | this->topR->setHidden(hide); 61 | this->bottomR->setHidden(hide); 62 | 63 | // Same with tick + circle 64 | if (!this->tick->hidden()) { 65 | col = this->tick->getCircleColour(); 66 | col.setA(this->icon->colour().a()); 67 | this->tick->setCircleColour(col); 68 | 69 | col = this->tick->getTickColour(); 70 | col.setA(this->icon->colour().a()); 71 | this->tick->setTickColour(col); 72 | } 73 | 74 | if (!this->circle->hidden()) { 75 | col = this->circle->colour(); 76 | col.setA(this->icon->colour().a()); 77 | this->circle->setColour(col); 78 | } 79 | } 80 | 81 | void ListHide::setImage(uint8_t * ptr, uint32_t size) { 82 | // Remove original 83 | this->removeTexture(this->icon); 84 | this->removeElement(this->icon); 85 | 86 | // Add new icon and render it asynchronously 87 | this->icon = new Aether::Image(0, 0, ptr, size, Aether::Render::Wait); 88 | this->icon->setWH(this->h() - 20, this->h() - 20); 89 | this->icon->setScaleDimensions(this->icon->w(), this->icon->h()); 90 | this->icon->setColour(Aether::Colour(255, 255, 255, 0)); 91 | this->addElement(this->icon); 92 | this->addTexture(this->icon); 93 | } 94 | 95 | void ListHide::setIDColour(const Aether::Colour & c) { 96 | this->titleID->setColour(c); 97 | } 98 | 99 | void ListHide::setLineColour(const Aether::Colour & c) { 100 | this->topR->setColour(c); 101 | this->bottomR->setColour(c); 102 | } 103 | 104 | void ListHide::setTitleColour(const Aether::Colour & c) { 105 | this->title->setColour(c); 106 | } 107 | 108 | void ListHide::setTickBackgroundColour(const Aether::Colour & c) { 109 | Aether::Colour col = c; 110 | col.setA(0); 111 | this->tick->setCircleColour(col); 112 | } 113 | 114 | void ListHide::setTickForegroundColour(const Aether::Colour & c) { 115 | Aether::Colour col = c; 116 | col.setA(0); 117 | this->tick->setTickColour(col); 118 | this->circle->setColour(col); 119 | } 120 | 121 | bool ListHide::isTicked() { 122 | return (!this->tick->hidden()); 123 | } 124 | 125 | void ListHide::setTicked(const bool b) { 126 | this->tick->setHidden(!b); 127 | this->circle->setHidden(b); 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /Application/source/ui/element/ListSession.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/element/ListSession.hpp" 2 | 3 | namespace CustomElm { 4 | ListSession::ListSession() : Aether::AsyncItem() { 5 | this->setH(ListSession::height); 6 | 7 | // Bounding rectangles 8 | this->top = new Aether::Rectangle(0, 0, this->w(), 1); 9 | this->addElement(this->top); 10 | this->bottom = new Aether::Rectangle(0, this->h(), this->w(), 1); 11 | this->addElement(this->bottom); 12 | 13 | // 'Dummy' elements 14 | this->time = new Aether::Text(0, 0, "", 1, Aether::Render::Wait); 15 | this->playtime = new Aether::Text(0, 0, "", 1, Aether::Render::Wait); 16 | this->percentage = new Aether::Text(0, 0, "", 1, Aether::Render::Wait); 17 | } 18 | 19 | void ListSession::positionElements() { 20 | this->top->setRectSize(this->w(), 1); 21 | this->bottom->setRectSize(this->w(), 1); 22 | this->percentage->setX(this->x() + this->w() - 16 - this->percentage->w()); 23 | } 24 | 25 | void ListSession::processText(Aether::Text * & text, std::function<Aether::Text * ()> getNew) { 26 | // Remove original 27 | this->removeTexture(text); 28 | this->removeElement(text); 29 | 30 | // Get (and assign) new text object 31 | text = getNew(); 32 | 33 | // Don't add if empty string 34 | if (!text->string().empty()) { 35 | this->addElement(text); 36 | this->addTexture(text); 37 | } 38 | } 39 | 40 | void ListSession::update(uint32_t dt) { 41 | // Update normally first 42 | AsyncItem::update(dt); 43 | 44 | // Match rectangle alphas with the playtime's 45 | Aether::Colour col = this->top->colour(); 46 | col.setA(this->playtime->colour().a()); 47 | this->top->setColour(col); 48 | this->bottom->setColour(col); 49 | } 50 | 51 | void ListSession::setLineColour(Aether::Colour c) { 52 | this->top->setColour(c); 53 | this->bottom->setColour(c); 54 | } 55 | 56 | void ListSession::setPercentageColour(Aether::Colour c) { 57 | this->percentage->setColour(c); 58 | } 59 | 60 | void ListSession::setPlaytimeColour(Aether::Colour c) { 61 | this->playtime->setColour(c); 62 | } 63 | 64 | void ListSession::setTimeColour(Aether::Colour c) { 65 | this->time->setColour(c); 66 | } 67 | 68 | void ListSession::setPercentageString(std::string s) { 69 | this->processText(this->percentage, [this, s]() { 70 | return new Aether::Text(this->x() + 16, this->y() + 50, s, ListSession::fontSize, Aether::Render::Wait); 71 | }); 72 | } 73 | 74 | void ListSession::setPlaytimeString(std::string s) { 75 | this->processText(this->playtime, [this, s]() { 76 | return new Aether::Text(this->x() + 16, this->y() + 50, s, ListSession::fontSize, Aether::Render::Wait); 77 | }); 78 | } 79 | 80 | void ListSession::setTimeString(std::string s) { 81 | this->processText(this->time, [this, s]() { 82 | return new Aether::Text(this->x() + 16, this->y() + 15, s, ListSession::fontSize, Aether::Render::Wait); 83 | }); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /Application/source/ui/element/ListUser.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/element/ListUser.hpp" 2 | 3 | #define HEIGHT 90 4 | 5 | namespace CustomElm { 6 | ListUser::ListUser(std::string n, uint8_t * p, uint32_t s) : Aether::Element() { 7 | this->setH(HEIGHT); 8 | this->topR = new Aether::Rectangle(this->x(), this->y(), this->w(), 1); 9 | this->addElement(topR); 10 | this->bottomR = new Aether::Rectangle(this->x(), this->y() + this->h(), this->w(), 1); 11 | this->addElement(bottomR); 12 | Aether::Image * im = new Aether::Image(this->x() + 10, this->y() + 10, p, s, Aether::Render::Wait); 13 | im->setWH(70, 70); 14 | im->setScaleDimensions(70, 70); 15 | im->renderSync(); 16 | this->addElement(im); 17 | this->name = new Aether::Text(this->x() + 105, this->y() + this->h()/2, n, 24); 18 | this->name->setY(this->name->y() - this->name->h()/2); 19 | this->addElement(this->name); 20 | } 21 | 22 | void ListUser::setLineColour(Aether::Colour c) { 23 | this->topR->setColour(c); 24 | this->bottomR->setColour(c); 25 | } 26 | 27 | void ListUser::setTextColour(Aether::Colour c) { 28 | this->name->setColour(c); 29 | } 30 | 31 | void ListUser::setW(int w) { 32 | Element::setW(w); 33 | this->topR->setRectSize(w, 1); 34 | this->bottomR->setRectSize(w, 1); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /Application/source/ui/element/SortedList.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/Lang.hpp" 2 | #include "ui/element/SortedList.hpp" 3 | #include "utils/Utils.hpp" 4 | 5 | namespace CustomElm { 6 | SortedList::SortedList(int x, int y, int w, int h) : Aether::List(x, y, w, h) { 7 | this->heading = new Aether::Text(0, 0, "I", 18); 8 | List::addElement(this->heading); 9 | this->heading->setY(this->heading->y() - 15); 10 | List::addElement(new Aether::ListSeparator(20)); 11 | } 12 | 13 | void SortedList::addElement(ListActivity * e, SortInfo * i) { 14 | List::addElement(e); 15 | this->sortinfo.push_back(i); 16 | } 17 | 18 | void SortedList::removeAllElements() { 19 | // Remove everything but heading + separator 20 | while (this->children.size() > 2) { 21 | delete this->children[2]; 22 | this->children.erase(this->children.begin() + 2); 23 | } 24 | while (this->sortinfo.size() > 0) { 25 | delete this->sortinfo[0]; 26 | this->sortinfo.erase(this->sortinfo.begin()); 27 | } 28 | } 29 | 30 | void SortedList::returnAllElements() { 31 | // Return everything but heading + separator 32 | for (size_t i = 2; i < this->children.size(); i++) { 33 | this->children[i]->setParent(nullptr); 34 | } 35 | this->children.erase(this->children.begin() + 2, this->children.end()); 36 | this->sortinfo.clear(); 37 | } 38 | 39 | void SortedList::setSort(SortType t) { 40 | this->sorting = t; 41 | 42 | // Merge vectors 43 | std::vector<Element *> items(this->children.begin() + 2, this->children.end()); 44 | std::vector<std::pair<Element *, SortInfo *> > merged; 45 | Utils::mergeVectors(merged, items, this->sortinfo); 46 | 47 | // Remove (without deleting) each item from the list 48 | this->returnAllElements(); 49 | 50 | // Change heading text + reorder items 51 | switch (t) { 52 | case AlphaAsc: 53 | this->heading->setString("allActivity.sort.name"_lang); 54 | std::sort(merged.begin(), merged.end(), [](std::pair<Element *, SortInfo *> lhs, std::pair<Element *, SortInfo *> rhs){ 55 | return lhs.second->name < rhs.second->name; 56 | }); 57 | break; 58 | case HoursAsc: 59 | this->heading->setString("allActivity.sort.mostPlaytime"_lang); 60 | std::sort(merged.begin(), merged.end(), [](std::pair<Element *, SortInfo *> lhs, std::pair<Element *, SortInfo *> rhs){ 61 | return lhs.second->playtime > rhs.second->playtime; 62 | }); 63 | break; 64 | case HoursDec: 65 | this->heading->setString("allActivity.sort.leastPlaytime"_lang); 66 | std::sort(merged.begin(), merged.end(), [](std::pair<Element *, SortInfo *> lhs, std::pair<Element *, SortInfo *> rhs){ 67 | return lhs.second->playtime < rhs.second->playtime; 68 | }); 69 | break; 70 | case LaunchAsc: 71 | this->heading->setString("allActivity.sort.mostLaunched"_lang); 72 | std::sort(merged.begin(), merged.end(), [](std::pair<Element *, SortInfo *> lhs, std::pair<Element *, SortInfo *> rhs){ 73 | return lhs.second->launches > rhs.second->launches; 74 | }); 75 | break; 76 | case LaunchDec: 77 | this->heading->setString("allActivity.sort.leastLaunched"_lang); 78 | std::sort(merged.begin(), merged.end(), [](std::pair<Element *, SortInfo *> lhs, std::pair<Element *, SortInfo *> rhs){ 79 | return lhs.second->launches < rhs.second->launches; 80 | }); 81 | break; 82 | case FirstPlayed: 83 | this->heading->setString("allActivity.sort.firstPlayed"_lang); 84 | std::sort(merged.begin(), merged.end(), [](std::pair<Element *, SortInfo *> lhs, std::pair<Element *, SortInfo *> rhs){ 85 | return lhs.second->firstPlayed < rhs.second->firstPlayed; 86 | }); 87 | // Push never played to bottom 88 | while (merged[0].second->firstPlayed == 0) { 89 | std::rotate(merged.begin(), merged.begin()+1, merged.end()); 90 | } 91 | break; 92 | case LastPlayed: 93 | this->heading->setString("allActivity.sort.recentlyPlayed"_lang); 94 | std::sort(merged.begin(), merged.end(), [](std::pair<Element *, SortInfo *> lhs, std::pair<Element *, SortInfo *> rhs){ 95 | return lhs.second->lastPlayed > rhs.second->lastPlayed; 96 | }); 97 | break; 98 | 99 | default: 100 | break; 101 | } 102 | this->heading->setX(this->x() + (this->w() - this->heading->w())/2); 103 | 104 | // Split vectors and re-add to list 105 | std::vector<SortInfo *> tmp(merged.size(), nullptr); 106 | Utils::splitVectors(merged, items, tmp); 107 | 108 | for (size_t i = 0; i < items.size(); i++) { 109 | ListActivity * item = static_cast<ListActivity *>(items[i]); 110 | this->addElement(item, tmp[i]); 111 | 112 | // Update ranks 113 | if (this->sorting != AlphaAsc) { 114 | item->setRank("#" + std::to_string(i+1)); 115 | } else { 116 | item->setRank(""); 117 | } 118 | } 119 | 120 | // Finally reset scroll pos 121 | this->setScrollPos(0); 122 | if (this->children.size() > 2) { 123 | this->setFocussed(this->children[2]); 124 | } 125 | } 126 | 127 | SortType SortedList::sort() { 128 | return this->sorting; 129 | } 130 | 131 | void SortedList::setHeadingColour(Aether::Colour c) { 132 | this->heading->setColour(c); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /Application/source/ui/overlay/ColourPicker.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/overlay/ColourPicker.hpp" 2 | 3 | // Defaults 4 | #define HEIGHT 470 5 | #define TITLE_FONT_SIZE 26 6 | 7 | // Formats colour as hex string 8 | static std::string RGBtoHex(Aether::Colour c) { 9 | char str[10]; 10 | snprintf(str, 10, "#%02x%02x%02x%02x", c.r(), c.g(), c.b(), c.a()); 11 | return std::string(str); 12 | } 13 | 14 | namespace CustomOvl { 15 | ColourPicker::ColourPicker(std::string s, Aether::Colour c, std::function<void(Aether::Colour)> f) : Aether::Overlay() { 16 | this->func = f; 17 | 18 | // Create elements 19 | this->rect = new Aether::Rectangle(this->x(), this->y() + this->h() - HEIGHT, this->w(), HEIGHT); 20 | this->addElement(this->rect); 21 | this->top = new Aether::Rectangle(this->x() + 30, this->rect->y() + 72, this->w() - 60, 1); 22 | this->addElement(this->top); 23 | this->bottom = new Aether::Rectangle(this->x() + 30, this->rect->y() + this->rect->h() - 72, this->w() - 60, 1); 24 | this->addElement(this->bottom); 25 | 26 | this->title = new Aether::Text(this->x() + 72, this->rect->y() + 40, s, TITLE_FONT_SIZE); 27 | this->title->setY(this->title->y() - this->title->h()/2); 28 | this->addElement(this->title); 29 | 30 | this->ctrlBar = new Aether::ControlBar(); 31 | this->ctrlBar->addControl(Aether::Button::A, "OK"); 32 | this->ctrlBar->addControl(Aether::Button::B, "Back"); 33 | this->addElement(this->ctrlBar); 34 | 35 | this->tip = new Aether::Text(this->x() + 72, this->bottom->y() + 36, "Tip:", 20); 36 | this->tip->setY(this->tip->y() - this->tip->h()/2); 37 | this->addElement(this->tip); 38 | 39 | // Spinners 40 | int y = this->top->y() + (this->bottom->y() - this->top->y())/2; 41 | this->red = new Aether::Spinner(this->rect->x() + 220, 0, 120); 42 | this->red->setMin(0); 43 | this->red->setMax(255); 44 | this->red->setLabel("Red"); 45 | this->red->setValue(c.r()); 46 | this->red->setY(y - this->red->h()/2); 47 | this->addElement(this->red); 48 | 49 | this->green = new Aether::Spinner(this->red->x() + 150, 0, 120); 50 | this->green->setMin(0); 51 | this->green->setMax(255); 52 | this->green->setLabel("Green"); 53 | this->green->setValue(c.g()); 54 | this->green->setY(y - this->green->h()/2); 55 | this->addElement(this->green); 56 | 57 | this->blue = new Aether::Spinner(this->green->x() + 150, 0, 120); 58 | this->blue->setMin(0); 59 | this->blue->setMax(255); 60 | this->blue->setLabel("Blue"); 61 | this->blue->setValue(c.b()); 62 | this->blue->setY(y - this->blue->h()/2); 63 | this->addElement(this->blue); 64 | 65 | this->alpha = new Aether::Spinner(this->blue->x() + 150, 0, 120); 66 | this->alpha->setMin(0); 67 | this->alpha->setMax(255); 68 | this->alpha->setLabel("Alpha"); 69 | this->alpha->setValue(c.a()); 70 | this->alpha->setY(y - this->alpha->h()/2); 71 | this->addElement(this->alpha); 72 | 73 | // OK Button 74 | this->button = new Aether::BorderButton(this->alpha->x() + 250, 0, 160, 60, 3, "OK", 22, [this](){ 75 | this->callFunc(); 76 | this->close(); 77 | }); 78 | this->button->setY(y - this->button->h()/2 - 26); 79 | this->addElement(this->button); 80 | 81 | // Preview 82 | this->colourRect = new Aether::Rectangle(this->rect->x() + this->rect->w() - 192, this->rect->y() + 17, 120, 40); 83 | this->colourRect->setColour(c); 84 | this->addElement(this->colourRect); 85 | this->colourHex = new Aether::Text(0, this->colourRect->y() + this->colourRect->h()/2, RGBtoHex(c), 20); 86 | this->colourHex->setX(this->colourRect->x() - 24 - this->colourHex->w()); 87 | this->colourHex->setY(this->colourRect->y() + (this->colourRect->h() - this->colourHex->h())/2); 88 | this->addElement(this->colourHex); 89 | 90 | // Close without updating 91 | this->onButtonPress(Aether::Button::B, [this](){ 92 | this->close(); 93 | }); 94 | // Close and update 95 | this->onButtonPress(Aether::Button::A, [this](){ 96 | this->callFunc(); 97 | this->close(); 98 | }); 99 | 100 | // Change speed when ZL/ZR pressed 101 | this->onButtonPress(Aether::Button::ZR, [this]() { 102 | this->red->setChangeAmount(10); 103 | this->green->setChangeAmount(10); 104 | this->blue->setChangeAmount(10); 105 | this->alpha->setChangeAmount(10); 106 | }); 107 | this->onButtonRelease(Aether::Button::ZR, [this]() { 108 | this->red->setChangeAmount(1); 109 | this->green->setChangeAmount(1); 110 | this->blue->setChangeAmount(1); 111 | this->alpha->setChangeAmount(1); 112 | }); 113 | this->onButtonPress(Aether::Button::ZL, [this]() { 114 | this->red->setChangeAmount(10); 115 | this->green->setChangeAmount(10); 116 | this->blue->setChangeAmount(10); 117 | this->alpha->setChangeAmount(10); 118 | }); 119 | this->onButtonRelease(Aether::Button::ZL, [this]() { 120 | this->red->setChangeAmount(1); 121 | this->green->setChangeAmount(1); 122 | this->blue->setChangeAmount(1); 123 | this->alpha->setChangeAmount(1); 124 | }); 125 | } 126 | 127 | void ColourPicker::callFunc() { 128 | this->func(this->colourRect->colour()); 129 | } 130 | 131 | void ColourPicker::update(uint32_t dt) { 132 | Aether::Overlay::update(dt); 133 | 134 | // Adjust colour text and rectangle 135 | Aether::Colour c = Aether::Colour(this->red->value(), this->green->value(), this->blue->value(), this->alpha->value()); 136 | this->colourRect->setColour(c); 137 | this->colourHex->setString(RGBtoHex(c)); 138 | this->colourHex->setX(this->colourRect->x() - 24 - this->colourHex->w()); 139 | } 140 | 141 | void ColourPicker::setBackLabel(std::string s) { 142 | this->ctrlBar->updateControl(Aether::Button::B, s); 143 | } 144 | 145 | void ColourPicker::setOKLabel(std::string s) { 146 | this->ctrlBar->updateControl(Aether::Button::A, s); 147 | } 148 | 149 | void ColourPicker::setTipText(std::string s) { 150 | this->tip->setString(s); 151 | } 152 | 153 | void ColourPicker::setRedHint(std::string s) { 154 | this->red->setLabel(s); 155 | } 156 | 157 | void ColourPicker::setGreenHint(std::string s) { 158 | this->green->setLabel(s); 159 | } 160 | 161 | void ColourPicker::setBlueHint(std::string s) { 162 | this->blue->setLabel(s); 163 | } 164 | 165 | void ColourPicker::setAlphaHint(std::string s) { 166 | this->alpha->setLabel(s); 167 | } 168 | 169 | void ColourPicker::setBackgroundColour(Aether::Colour c) { 170 | this->rect->setColour(c); 171 | } 172 | 173 | void ColourPicker::setInactiveColour(Aether::Colour c) { 174 | this->red->setArrowColour(c); 175 | this->green->setArrowColour(c); 176 | this->blue->setArrowColour(c); 177 | this->alpha->setArrowColour(c); 178 | this->tip->setColour(c); 179 | } 180 | 181 | void ColourPicker::setHighlightColour(Aether::Colour c) { 182 | this->red->setHighlightColour(c); 183 | this->green->setHighlightColour(c); 184 | this->blue->setHighlightColour(c); 185 | this->alpha->setHighlightColour(c); 186 | } 187 | 188 | void ColourPicker::setTextColour(Aether::Colour c) { 189 | this->red->setTextColour(c); 190 | this->green->setTextColour(c); 191 | this->blue->setTextColour(c); 192 | this->alpha->setTextColour(c); 193 | this->button->setBorderColour(c); 194 | this->button->setTextColour(c); 195 | this->title->setColour(c); 196 | this->top->setColour(c); 197 | this->bottom->setColour(c); 198 | this->ctrlBar->setDisabledColour(c); 199 | this->ctrlBar->setEnabledColour(c); 200 | this->colourHex->setColour(c); 201 | } 202 | }; -------------------------------------------------------------------------------- /Application/source/ui/overlay/PlaySession.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/Lang.hpp" 2 | #include "ui/overlay/PlaySession.hpp" 3 | 4 | #define WIDTH 800 5 | 6 | namespace CustomOvl { 7 | PlaySession::PlaySession() : Overlay() { 8 | // Create elements 9 | this->rect = new Aether::Rectangle(this->x() + (this->w() - WIDTH)/2, this->y(), WIDTH, this->h()); 10 | this->addElement(this->rect); 11 | this->title = new Aether::Text(this->rect->x() + 50, this->rect->y() + 45, "details.break.heading"_lang, 28); 12 | this->title->setY(this->title->y() - this->title->h()/2); 13 | this->addElement(this->title); 14 | this->top = new Aether::Rectangle(this->rect->x() + 30, this->rect->y() + 87, this->rect->w() - 60, 1); 15 | this->addElement(this->top); 16 | this->bottom = new Aether::Rectangle(this->rect->x() + 30, this->rect->y() + 647, this->rect->w() - 60, 1); 17 | this->addElement(this->bottom); 18 | this->sep = new Aether::Rectangle(this->rect->x() + 30, this->bottom->y() - 110, this->bottom->w(), 1); 19 | this->addElement(this->sep); 20 | this->ctrlBar = new Aether::ControlBar(); 21 | this->ctrlBar->setXYWH(this->rect->x() + 45, 647, this->rect->w() - 90, this->ctrlBar->h()); 22 | this->ctrlBar->addControl(Aether::Button::B, "common.close"_lang); 23 | this->addElement(this->ctrlBar); 24 | 25 | this->list = nullptr; 26 | this->col = Aether::Colour{255, 255, 255, 255}; 27 | 28 | this->length = new Aether::Text(this->rect->x() + 3*this->rect->w()/4, this->bottom->y() - 80, "details.break.length"_lang, 20); 29 | this->length->setX(this->length->x() - this->length->w()/2); 30 | this->addElement(this->length); 31 | this->lengthSub = new Aether::Text(this->rect->x() + 3*this->rect->w()/4, this->bottom->y() - 50, "", 18); 32 | this->addElement(this->lengthSub); 33 | this->playtime = new Aether::Text(this->rect->x() + this->rect->w()/4, this->bottom->y() - 80, "details.break.playtime"_lang, 20); 34 | this->playtime->setX(this->playtime->x() - this->playtime->w()/2); 35 | this->addElement(this->playtime); 36 | this->playtimeSub = new Aether::Text(this->rect->x() + this->rect->w()/4, this->bottom->y() - 50, "", 18); 37 | this->addElement(this->playtimeSub); 38 | 39 | // Close on B 40 | this->onButtonPress(Aether::Button::B, [this](){ 41 | this->close(); 42 | this->removeElement(this->list); 43 | this->list = nullptr; 44 | }); 45 | } 46 | 47 | void PlaySession::addListItem(Aether::Element * e) { 48 | if (this->list == nullptr) { 49 | this->list = new Aether::List(this->rect->x(), this->top->y() + 1, this->rect->w() - 50, this->sep->y() - this->top->y() - 1); 50 | this->list->setScrollBarColour(this->col); 51 | this->addElement(this->list); 52 | this->setFocussed(this->list); 53 | } 54 | this->list->addElement(e); 55 | } 56 | 57 | void PlaySession::setLength(std::string s) { 58 | this->lengthSub->setString(s); 59 | this->lengthSub->setX(this->rect->x() + 3*this->rect->w()/4 - this->lengthSub->w()/2); 60 | } 61 | 62 | void PlaySession::setPlaytime(std::string s) { 63 | this->playtimeSub->setString(s); 64 | this->playtimeSub->setX(this->rect->x() + this->rect->w()/4 - this->playtimeSub->w()/2); 65 | } 66 | 67 | void PlaySession::setAccentColour(Aether::Colour c) { 68 | this->lengthSub->setColour(c); 69 | this->playtimeSub->setColour(c); 70 | } 71 | 72 | void PlaySession::setBackgroundColour(Aether::Colour c) { 73 | this->rect->setColour(c); 74 | } 75 | 76 | void PlaySession::setLineColour(Aether::Colour c) { 77 | this->top->setColour(c); 78 | this->bottom->setColour(c); 79 | this->sep->setColour(c); 80 | } 81 | 82 | void PlaySession::setMutedLineColour(Aether::Colour c) { 83 | this->col = c; 84 | } 85 | 86 | void PlaySession::setTextColour(Aether::Colour c) { 87 | this->length->setColour(c); 88 | this->playtime->setColour(c); 89 | this->title->setColour(c); 90 | this->ctrlBar->setDisabledColour(c); 91 | this->ctrlBar->setEnabledColour(c); 92 | } 93 | }; -------------------------------------------------------------------------------- /Application/source/ui/overlay/PlaytimePicker.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/overlay/PlaytimePicker.hpp" 2 | 3 | // Defaults 4 | #define HEIGHT 470 5 | #define TITLE_FONT_SIZE 26 6 | 7 | namespace CustomOvl { 8 | PlaytimePicker::PlaytimePicker(const std::string & s, const int val, std::function<void(int)> f) : Aether::Overlay() { 9 | this->func = f; 10 | 11 | // Create elements 12 | this->rect = new Aether::Rectangle(this->x(), this->y() + this->h() - HEIGHT, this->w(), HEIGHT); 13 | this->addElement(this->rect); 14 | this->top = new Aether::Rectangle(this->x() + 30, this->rect->y() + 72, this->w() - 60, 1); 15 | this->addElement(this->top); 16 | this->bottom = new Aether::Rectangle(this->x() + 30, this->rect->y() + this->rect->h() - 72, this->w() - 60, 1); 17 | this->addElement(this->bottom); 18 | 19 | this->title = new Aether::Text(this->x() + 72, this->rect->y() + 40, s, TITLE_FONT_SIZE); 20 | this->title->setY(this->title->y() - this->title->h()/2); 21 | this->addElement(this->title); 22 | 23 | this->ctrlBar = new Aether::ControlBar(); 24 | this->ctrlBar->addControl(Aether::Button::A, "OK"); 25 | this->ctrlBar->addControl(Aether::Button::B, "Back"); 26 | this->addElement(this->ctrlBar); 27 | 28 | this->tip = new Aether::Text(this->x() + 72, this->bottom->y() + 36, "Tip:", 20); 29 | this->tip->setY(this->tip->y() - this->tip->h()/2); 30 | this->addElement(this->tip); 31 | 32 | // Spinners 33 | int y = this->top->y() + (this->bottom->y() - this->top->y())/2; 34 | this->hour = new Aether::Spinner(this->rect->x() + 280, 0, 120); 35 | this->hour->setMin(-999); 36 | this->hour->setMax(999); 37 | this->hour->setLabel("Hour"); 38 | this->hour->setValue(val / 3600); 39 | this->hour->setY(y - this->hour->h()/2); 40 | this->addElement(this->hour); 41 | 42 | this->minute = new Aether::Spinner(this->hour->x() + 150, 0, 120); 43 | this->minute->setDigits(2); 44 | this->minute->setMin(-59); 45 | this->minute->setMax(59); 46 | this->minute->setLabel("Minute"); 47 | this->minute->setValue((val / 60) % 60); 48 | this->minute->setY(y - this->minute->h()/2); 49 | this->addElement(this->minute); 50 | 51 | this->second = new Aether::Spinner(this->minute->x() + 150, 0, 120); 52 | this->second->setDigits(2); 53 | this->second->setMin(-59); 54 | this->second->setMax(59); 55 | this->second->setLabel("Second"); 56 | this->second->setValue(val % 60); 57 | this->second->setY(y - this->second->h()/2); 58 | this->addElement(this->second); 59 | 60 | // OK Button 61 | this->button = new Aether::BorderButton(this->second->x() + 250, 0, 160, 60, 3, "OK", 22, [this](){ 62 | this->callFunc(); 63 | this->close(); 64 | }); 65 | this->button->setY(y - this->button->h()/2 - 26); 66 | this->addElement(this->button); 67 | 68 | // Close without updating 69 | this->onButtonPress(Aether::Button::B, [this](){ 70 | this->close(); 71 | }); 72 | // Close and update 73 | this->onButtonPress(Aether::Button::A, [this](){ 74 | this->callFunc(); 75 | this->close(); 76 | }); 77 | 78 | // Change speed when ZL/ZR pressed 79 | this->onButtonPress(Aether::Button::ZR, [this]() { 80 | this->hour->setChangeAmount(10); 81 | this->minute->setChangeAmount(10); 82 | this->second->setChangeAmount(10); 83 | }); 84 | this->onButtonRelease(Aether::Button::ZR, [this]() { 85 | this->hour->setChangeAmount(1); 86 | this->minute->setChangeAmount(1); 87 | this->second->setChangeAmount(1); 88 | }); 89 | this->onButtonPress(Aether::Button::ZL, [this]() { 90 | this->hour->setChangeAmount(10); 91 | this->minute->setChangeAmount(10); 92 | this->second->setChangeAmount(10); 93 | }); 94 | this->onButtonRelease(Aether::Button::ZL, [this]() { 95 | this->hour->setChangeAmount(1); 96 | this->minute->setChangeAmount(1); 97 | this->second->setChangeAmount(1); 98 | }); 99 | } 100 | 101 | void PlaytimePicker::callFunc() { 102 | this->func((this->hour->value() * 3600) + (this->minute->value() * 60) + this->second->value()); 103 | } 104 | 105 | void PlaytimePicker::setBackLabel(const std::string & s) { 106 | this->ctrlBar->updateControl(Aether::Button::B, s); 107 | } 108 | 109 | void PlaytimePicker::setOKLabel(const std::string & s) { 110 | this->ctrlBar->updateControl(Aether::Button::A, s); 111 | } 112 | 113 | void PlaytimePicker::setTipText(const std::string & s) { 114 | this->tip->setString(s); 115 | } 116 | 117 | void PlaytimePicker::setHourHint(const std::string & s) { 118 | this->hour->setLabel(s); 119 | } 120 | 121 | void PlaytimePicker::setMinuteHint(const std::string & s) { 122 | this->minute->setLabel(s); 123 | } 124 | 125 | void PlaytimePicker::setSecondHint(const std::string & s) { 126 | this->second->setLabel(s); 127 | } 128 | 129 | void PlaytimePicker::setBackgroundColour(const Aether::Colour & c) { 130 | this->rect->setColour(c); 131 | } 132 | 133 | void PlaytimePicker::setInactiveColour(const Aether::Colour & c) { 134 | this->hour->setArrowColour(c); 135 | this->minute->setArrowColour(c); 136 | this->second->setArrowColour(c); 137 | this->tip->setColour(c); 138 | } 139 | 140 | void PlaytimePicker::setHighlightColour(const Aether::Colour & c) { 141 | this->hour->setHighlightColour(c); 142 | this->minute->setHighlightColour(c); 143 | this->second->setHighlightColour(c); 144 | } 145 | 146 | void PlaytimePicker::setTextColour(const Aether::Colour & c) { 147 | this->hour->setTextColour(c); 148 | this->minute->setTextColour(c); 149 | this->second->setTextColour(c); 150 | this->button->setBorderColour(c); 151 | this->button->setTextColour(c); 152 | this->title->setColour(c); 153 | this->top->setColour(c); 154 | this->bottom->setColour(c); 155 | this->ctrlBar->setDisabledColour(c); 156 | this->ctrlBar->setEnabledColour(c); 157 | } 158 | }; 159 | -------------------------------------------------------------------------------- /Application/source/ui/overlay/ProgressBox.cpp: -------------------------------------------------------------------------------- 1 | #include "ui/overlay/ProgressBox.hpp" 2 | 3 | #define WIDTH 560 4 | 5 | namespace CustomOvl { 6 | ProgressBox::ProgressBox() : Aether::Overlay() { 7 | // Create background rectangle 8 | this->rect = new Aether::Rectangle((this->w() - WIDTH)/2, this->h()/2, WIDTH, 1, 8); 9 | this->addElement(this->rect); 10 | 11 | // Create heading 12 | this->heading = new Aether::TextBlock(this->rect->x() + 50, this->rect->y() + 35, "|", 24, this->rect->w() - 100); 13 | this->addElement(this->heading); 14 | 15 | // Create progress bar + value 16 | this->bar = new Aether::RoundProgressBar(this->heading->x(), this->heading->y() + this->heading->h() + 25, this->rect->w() - 160); 17 | this->bar->setValue(0); 18 | this->addElement(this->bar); 19 | 20 | this->value = new Aether::Text(this->bar->x() + this->bar->w() + 25, 0, "0%", 18); 21 | this->value->setY(this->bar->y() + (this->bar->h() - this->value->h())/2); 22 | this->addElement(this->value); 23 | 24 | // Resize rectangle + position elements 25 | this->rect->setRectSize(this->rect->w(), (this->bar->y() + this->bar->h()) - this->heading->y() + 75); 26 | this->rect->setY((this->h() - this->rect->h())/2); 27 | this->heading->setY(this->heading->y() - this->rect->h()/2); 28 | this->bar->setY(this->bar->y() - this->rect->h()/2); 29 | this->value->setY(this->value->y() - this->rect->h()/2); 30 | 31 | // Disable closing by button press 32 | this->onButtonPress(Aether::Button::B, nullptr); 33 | } 34 | 35 | void ProgressBox::setHeading(const std::string & str) { 36 | this->heading->setString(str); 37 | } 38 | 39 | void ProgressBox::setValue(const double value) { 40 | this->bar->setValue(value); 41 | this->value->setString(std::to_string(static_cast<int>(value)) + "%"); 42 | } 43 | 44 | void ProgressBox::setBackgroundColour(const Aether::Colour c) { 45 | this->rect->setColour(c); 46 | } 47 | 48 | void ProgressBox::setBarBackgroundColour(const Aether::Colour c) { 49 | this->bar->setBackgroundColour(c); 50 | } 51 | 52 | void ProgressBox::setBarForegroundColour(const Aether::Colour c) { 53 | this->bar->setForegroundColour(c); 54 | } 55 | 56 | void ProgressBox::setTextColour(const Aether::Colour c) { 57 | this->heading->setColour(c); 58 | this->value->setColour(c); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /Application/source/ui/screen/AdjustPlaytime.cpp: -------------------------------------------------------------------------------- 1 | #include "Application.hpp" 2 | #include "utils/Lang.hpp" 3 | #include "ui/element/ListAdjust.hpp" 4 | #include "ui/overlay/PlaytimePicker.hpp" 5 | #include "ui/screen/AdjustPlaytime.hpp" 6 | #include "utils/Utils.hpp" 7 | 8 | namespace Screen { 9 | AdjustPlaytime::AdjustPlaytime(Main::Application * app) : Aether::Screen() { 10 | this->app = app; 11 | 12 | // Create "static" elements 13 | Aether::Rectangle * r = new Aether::Rectangle(30, 87, 1220, 1); 14 | r->setColour(this->app->theme()->fg()); 15 | this->addElement(r); 16 | r = new Aether::Rectangle(30, 647, 1220, 1); 17 | r->setColour(this->app->theme()->fg()); 18 | this->addElement(r); 19 | Aether::Text * t = new Aether::Text(65, 44, "adjustPlaytime.heading"_lang, 28); 20 | t->setY(t->y() - t->h()/2); 21 | t->setColour(this->app->theme()->text()); 22 | this->addElement(t); 23 | Aether::ControlBar * c = new Aether::ControlBar(); 24 | c->addControl(Aether::Button::A, "common.buttonHint.ok"_lang); 25 | c->addControl(Aether::Button::B, "common.buttonHint.back"_lang); 26 | c->addControl(Aether::Button::X, "adjustPlaytime.save"_lang); 27 | c->setDisabledColour(this->app->theme()->text()); 28 | c->setEnabledColour(this->app->theme()->text()); 29 | this->addElement(c); 30 | 31 | // Save on X 32 | this->onButtonPress(Aether::Button::X, [this]() { 33 | this->app->config()->setAdjustmentValues(this->adjustments); 34 | this->app->popScreen(); 35 | }); 36 | // Quit without saving on B 37 | this->onButtonPress(Aether::Button::B, [this](){ 38 | this->app->popScreen(); 39 | }); 40 | 41 | // Scroll faster with ZL/ZR 42 | this->onButtonPress(Aether::Button::ZR, [this](){ 43 | this->app->setHoldDelay(30); 44 | }); 45 | this->onButtonRelease(Aether::Button::ZR, [this](){ 46 | this->app->setHoldDelay(100); 47 | }); 48 | this->onButtonPress(Aether::Button::ZL, [this](){ 49 | this->app->setHoldDelay(30); 50 | }); 51 | this->onButtonRelease(Aether::Button::ZL, [this](){ 52 | this->app->setHoldDelay(100); 53 | }); 54 | } 55 | 56 | std::string AdjustPlaytime::getValueString(int val) { 57 | return (val < 0 ? "- " : "+ ") + Utils::playtimeToString(std::abs(val)); 58 | } 59 | 60 | void AdjustPlaytime::setupPlaytimePicker(const std::string & title, size_t idx, CustomElm::ListAdjust * l) { 61 | delete this->picker; 62 | this->picker = new CustomOvl::PlaytimePicker(title, this->adjustments[idx].value, [this, idx, l](int val) { 63 | this->adjustments[idx].value = val; 64 | l->setAdjustedTime(this->getValueString(val)); 65 | }); 66 | this->picker->setBackLabel("common.buttonHint.back"_lang); 67 | this->picker->setOKLabel("common.buttonHint.ok"_lang); 68 | this->picker->setTipText("customTheme.picker.tip"_lang); 69 | this->picker->setHourHint("adjustPlaytime.picker.hour"_lang); 70 | this->picker->setMinuteHint("adjustPlaytime.picker.minute"_lang); 71 | this->picker->setSecondHint("adjustPlaytime.picker.second"_lang); 72 | this->picker->setBackgroundColour(this->app->theme()->altBG()); 73 | this->picker->setHighlightColour(this->app->theme()->accent()); 74 | this->picker->setInactiveColour(this->app->theme()->mutedText()); 75 | this->picker->setTextColour(this->app->theme()->text()); 76 | this->app->addOverlay(this->picker); 77 | } 78 | 79 | void AdjustPlaytime::onLoad() { 80 | // Render user's image 81 | this->userimage = new Aether::Image(1155, 14, this->app->activeUser()->imgPtr(), this->app->activeUser()->imgSize(), Aether::Render::Wait); 82 | this->userimage->setScaleDimensions(60, 60); 83 | this->userimage->renderSync(); 84 | this->addElement(this->userimage); 85 | 86 | // Render user's name 87 | this->username = new Aether::Text(1135, 45, this->app->activeUser()->username(), 20); 88 | this->username->setXY(this->username->x() - this->username->w(), this->username->y() - this->username->h()/2); 89 | this->username->setColour(this->app->theme()->mutedText()); 90 | this->addElement(this->username); 91 | 92 | // Create list 93 | this->list = new Aether::List(200, 88, 880, 559); 94 | this->list->setScrollBarColour(this->app->theme()->mutedLine()); 95 | 96 | // Populate list with all titles 97 | std::vector<NX::Title *> titles = this->app->titleVector(); 98 | std::sort(titles.begin(), titles.end(), [](NX::Title * lhs, NX::Title * rhs) { 99 | return (lhs->name() < rhs->name()); 100 | }); 101 | 102 | this->adjustments = this->app->config()->adjustmentValues(); 103 | std::vector<uint64_t> hidden = this->app->config()->hiddenTitles(); 104 | for (NX::Title * title : titles) { 105 | // Skip over hidden games 106 | if (std::find(hidden.begin(), hidden.end(), title->titleID()) != hidden.end()) { 107 | continue; 108 | } 109 | 110 | // Find adjustment value or create if one doesn't exist 111 | std::vector<AdjustmentValue>::iterator it = std::find_if(this->adjustments.begin(), this->adjustments.end(), [this, title](AdjustmentValue val) { 112 | return (val.titleID == title->titleID() && val.userID == this->app->activeUser()->ID()); 113 | }); 114 | if (it == this->adjustments.end()) { 115 | this->adjustments.push_back(AdjustmentValue{title->titleID(), this->app->activeUser()->ID(), 0}); 116 | it = (this->adjustments.end() - 1); 117 | } 118 | 119 | size_t idx = std::distance(this->adjustments.begin(), it); 120 | NX::PlayStatistics * stats = this->app->playdata()->getStatisticsForUser(title->titleID(), this->app->activeUser()->ID()); 121 | CustomElm::ListAdjust * l = new CustomElm::ListAdjust(title->name(), Utils::playtimeToPlayedForString(stats->playtime), this->getValueString(this->adjustments[idx].value)); 122 | delete stats; 123 | 124 | l->setImage(title->imgPtr(), title->imgSize()); 125 | l->setAdjustColour(this->app->theme()->accent()); 126 | l->setLineColour(this->app->theme()->mutedLine()); 127 | l->setRecordColour(this->app->theme()->mutedText()); 128 | l->setTitleColour(this->app->theme()->text()); 129 | l->onPress([this, title, idx, l]() { 130 | this->setupPlaytimePicker(title->name(), idx, l); 131 | }); 132 | 133 | this->list->addElement(l); 134 | } 135 | 136 | this->addElement(this->list); 137 | this->picker = nullptr; 138 | } 139 | 140 | void AdjustPlaytime::onUnload() { 141 | delete this->picker; 142 | this->removeElement(this->userimage); 143 | this->removeElement(this->username); 144 | this->removeElement(this->list); 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /Application/source/ui/screen/HideTitles.cpp: -------------------------------------------------------------------------------- 1 | #include <algorithm> 2 | #include "Application.hpp" 3 | #include "utils/Lang.hpp" 4 | #include "ui/element/ListHide.hpp" 5 | #include "ui/screen/HideTitles.hpp" 6 | #include "utils/Utils.hpp" 7 | 8 | namespace Screen { 9 | HideTitles::HideTitles(Main::Application * a) : Aether::Screen() { 10 | this->app = a; 11 | 12 | // Create "static" elements 13 | Aether::Rectangle * r = new Aether::Rectangle(30, 87, 1220, 1); 14 | r->setColour(this->app->theme()->fg()); 15 | this->addElement(r); 16 | r = new Aether::Rectangle(30, 647, 1220, 1); 17 | r->setColour(this->app->theme()->fg()); 18 | this->addElement(r); 19 | Aether::Text * t = new Aether::Text(65, 44, "hideTitles.heading"_lang, 28); 20 | t->setY(t->y() - t->h()/2); 21 | t->setColour(this->app->theme()->text()); 22 | this->addElement(t); 23 | Aether::ControlBar * c = new Aether::ControlBar(); 24 | c->addControl(Aether::Button::A, "common.buttonHint.ok"_lang); 25 | c->addControl(Aether::Button::B, "common.buttonHint.back"_lang); 26 | c->setDisabledColour(this->app->theme()->text()); 27 | c->setEnabledColour(this->app->theme()->text()); 28 | this->addElement(c); 29 | 30 | // Create side panel 31 | if (!this->app->config()->tImage() || this->app->config()->gTheme() != ThemeType::Custom) { 32 | r = new Aether::Rectangle(890, 88, 360, 559); 33 | r->setColour(this->app->theme()->altBG()); 34 | this->addElement(r); 35 | } 36 | 37 | // Titles hidden and save button 38 | this->hiddenCountText = new Aether::Text(0, 190, "0", 30); 39 | this->hiddenCountText->setColour(this->app->theme()->text()); 40 | this->addElement(this->hiddenCountText); 41 | this->hiddenSubText = new Aether::TextBlock(0, this->hiddenCountText->y() + this->hiddenCountText->h() + 10, "hideTitles.titlesHidden"_lang, 20, 300); 42 | this->hiddenSubText->setColour(this->app->theme()->mutedText()); 43 | this->addElement(this->hiddenSubText); 44 | 45 | Aether::FilledButton * b = new Aether::FilledButton(980, 500, 180, 60, "hideTitles.save"_lang, 22, [this]() { 46 | this->app->config()->setHiddenTitles(this->hiddenIDs); 47 | this->app->popScreen(); 48 | }); 49 | b->setFillColour(this->app->theme()->accent()); 50 | b->setTextColour(this->app->theme()->altBG()); 51 | this->addElement(b); 52 | 53 | // Quit without saving on B 54 | this->onButtonPress(Aether::Button::B, [this](){ 55 | this->app->popScreen(); 56 | }); 57 | 58 | // Scroll faster with ZL/ZR 59 | this->onButtonPress(Aether::Button::ZR, [this](){ 60 | this->app->setHoldDelay(30); 61 | }); 62 | this->onButtonRelease(Aether::Button::ZR, [this](){ 63 | this->app->setHoldDelay(100); 64 | }); 65 | this->onButtonPress(Aether::Button::ZL, [this](){ 66 | this->app->setHoldDelay(30); 67 | }); 68 | this->onButtonRelease(Aether::Button::ZL, [this](){ 69 | this->app->setHoldDelay(100); 70 | }); 71 | } 72 | 73 | void HideTitles::updateHiddenCounter() { 74 | this->hiddenCountText->setString(std::to_string(this->hiddenIDs.size())); 75 | this->hiddenCountText->setX(1070 - this->hiddenCountText->w()/2); 76 | this->hiddenSubText->setString(this->hiddenIDs.size() == 1 ? "hideTitles.titleHidden"_lang : "hideTitles.titlesHidden"_lang); 77 | this->hiddenSubText->setX(1070 - this->hiddenSubText->w()/2); 78 | } 79 | 80 | void HideTitles::onLoad() { 81 | // Create list 82 | this->list = new Aether::List(30, 88, 830, 559); 83 | this->list->setScrollBarColour(this->app->theme()->mutedLine()); 84 | 85 | // Populate list with all titles 86 | this->hiddenIDs = this->app->config()->hiddenTitles(); 87 | std::vector<NX::Title *> titles = this->app->titleVector(); 88 | std::sort(titles.begin(), titles.end(), [](NX::Title * lhs, NX::Title * rhs) { 89 | return (lhs->name() < rhs->name()); 90 | }); 91 | 92 | for (NX::Title * title : titles) { 93 | CustomElm::ListHide * l = new CustomElm::ListHide(title->name(), Utils::formatHexString(title->titleID())); 94 | l->setImage(title->imgPtr(), title->imgSize()); 95 | l->setIDColour(this->app->theme()->mutedText()); 96 | l->setLineColour(this->app->theme()->mutedLine()); 97 | l->setTitleColour(this->app->theme()->text()); 98 | l->setTickBackgroundColour(this->app->theme()->accent()); 99 | l->setTickForegroundColour(this->app->theme()->mutedLine()); 100 | l->onPress([this, title, l]() { 101 | if (l->isTicked()) { 102 | l->setTicked(false); 103 | this->hiddenIDs.erase(std::remove(this->hiddenIDs.begin(), this->hiddenIDs.end(), title->titleID()), this->hiddenIDs.end()); 104 | 105 | } else { 106 | l->setTicked(true); 107 | this->hiddenIDs.push_back(title->titleID()); 108 | } 109 | this->updateHiddenCounter(); 110 | }); 111 | bool hidden = std::find(this->hiddenIDs.begin(), this->hiddenIDs.end(), title->titleID()) != this->hiddenIDs.end(); 112 | l->setTicked(hidden); 113 | 114 | this->list->addElement(l); 115 | } 116 | 117 | this->addElement(this->list); 118 | this->setFocused(this->list); 119 | this->updateHiddenCounter(); 120 | } 121 | 122 | void HideTitles::onUnload() { 123 | this->removeElement(this->list); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /Application/source/ui/screen/UserSelect.cpp: -------------------------------------------------------------------------------- 1 | #include "Application.hpp" 2 | #include "utils/Lang.hpp" 3 | #include "ui/element/ListUser.hpp" 4 | #include "ui/screen/UserSelect.hpp" 5 | 6 | namespace Screen { 7 | UserSelect::UserSelect(Main::Application * a, std::vector<NX::User *> u) { 8 | this->app = a; 9 | this->users = u; 10 | 11 | // Create "static" elements 12 | Aether::Rectangle * r = new Aether::Rectangle(30, 87, 1220, 1); 13 | r->setColour(this->app->theme()->fg()); 14 | this->addElement(r); 15 | r = new Aether::Rectangle(30, 647, 1220, 1); 16 | r->setColour(this->app->theme()->fg()); 17 | this->addElement(r); 18 | Aether::Text * t = new Aether::Text(65, 44, "userSelect.heading"_lang, 28); 19 | t->setY(t->y() - t->h()/2); 20 | t->setColour(this->app->theme()->text()); 21 | this->addElement(t); 22 | Aether::ControlBar * c = new Aether::ControlBar(); 23 | c->addControl(Aether::Button::A, "common.buttonHint.ok"_lang); 24 | c->addControl(Aether::Button::PLUS, "common.buttonHint.exit"_lang); 25 | c->setDisabledColour(this->app->theme()->text()); 26 | c->setEnabledColour(this->app->theme()->text()); 27 | this->addElement(c); 28 | 29 | // Add handling of plus/B to exit 30 | this->onButtonPress(Aether::Button::PLUS, [this](){ 31 | this->app->exit(); 32 | }); 33 | this->onButtonPress(Aether::Button::B, [this](){ 34 | this->app->exit(); 35 | }); 36 | } 37 | 38 | void UserSelect::onLoad() { 39 | // Create list 40 | this->list = new Aether::List(340, 88, 600, 558); 41 | this->list->setScrollBarColour(this->app->theme()->mutedLine()); 42 | this->addElement(this->list); 43 | 44 | // Create list items for each user in the vector 45 | for (size_t i = 0; i < this->users.size(); i++) { 46 | CustomElm::ListUser * l = new CustomElm::ListUser(this->users[i]->username(), this->users[i]->imgPtr(), this->users[i]->imgSize()); 47 | l->setLineColour(this->app->theme()->mutedLine()); 48 | l->setTextColour(this->app->theme()->text()); 49 | l->onPress([this, i](){ 50 | this->app->setActiveUser(i); 51 | this->app->setScreen(this->app->config()->lScreen()); 52 | }); 53 | this->list->addElement(l); 54 | } 55 | this->setFocussed(this->list); 56 | 57 | // Show update icon if needbe 58 | this->updateElm =nullptr; 59 | if (this->app->hasUpdate()) { 60 | this->updateElm = new Aether::Image(50, 669, "romfs:/icon/download.png"); 61 | this->updateElm->setColour(this->app->theme()->text()); 62 | this->addElement(this->updateElm); 63 | } 64 | } 65 | 66 | void UserSelect::onUnload() { 67 | // Remove + delete elements within list 68 | this->removeElement(this->updateElm); 69 | this->removeElement(this->list); 70 | } 71 | }; -------------------------------------------------------------------------------- /Application/source/utils/Curl.cpp: -------------------------------------------------------------------------------- 1 | #include <cstdio> 2 | #include <curl/curl.h> 3 | #include "utils/Curl.hpp" 4 | #include <fstream> 5 | #include <sstream> 6 | 7 | // Timeout for connecting 8 | #define TIMEOUT 3000L 9 | // User Agent string 10 | #define USER_AGENT "NX-Activity-Log" 11 | 12 | namespace Utils::Curl { 13 | void init() { 14 | curl_global_init(CURL_GLOBAL_ALL); 15 | } 16 | 17 | void exit() { 18 | curl_global_cleanup(); 19 | } 20 | 21 | // Setup curl 22 | CURL * initCurlHandle() { 23 | CURL * c = curl_easy_init(); 24 | curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT_MS, TIMEOUT); 25 | curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); 26 | curl_easy_setopt(c, CURLOPT_SSL_VERIFYPEER, 0L); 27 | curl_easy_setopt(c, CURLOPT_SSL_VERIFYHOST, 0L); 28 | curl_easy_setopt(c, CURLOPT_USERAGENT, USER_AGENT); 29 | return c; 30 | } 31 | 32 | // Progress callback (simply passes download params to std::function) 33 | int progFunc(void * f, curl_off_t dltotal, curl_off_t dlnow, curl_off_t uptotal, curl_off_t upnow) { 34 | std::function<void(long long int, long long int)> * func = reinterpret_cast< std::function<void(long long int, long long int)> *>(f); 35 | (*func)(dlnow, dltotal); 36 | return 0; 37 | } 38 | 39 | // Write data to stringstream 40 | static size_t writeData(char *ptr, size_t size, size_t nmemb, void *userdata) { 41 | std::ostringstream * ss = (std::ostringstream *)userdata; 42 | size_t bytes = size * nmemb; 43 | ss->write(ptr, bytes); 44 | return bytes; 45 | } 46 | 47 | // Write data to file 48 | static size_t writeFile(char *ptr, size_t size, size_t nmemb, void *stream) { 49 | std::ofstream * file = (std::ofstream *)stream; 50 | auto before = file->tellp(); 51 | file->write(ptr, nmemb); 52 | return file->tellp() - before; 53 | } 54 | 55 | bool downloadToFile(std::string url, std::string path, std::function<void(long long int, long long int)> prog) { 56 | // Open file 57 | std::ofstream file(path); 58 | 59 | // Setup request 60 | CURL * c = initCurlHandle(); 61 | curl_easy_setopt(c, CURLOPT_URL, url.c_str()); 62 | curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, writeFile); 63 | curl_easy_setopt(c, CURLOPT_WRITEDATA, &file); 64 | 65 | if (prog != nullptr) { 66 | curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0L); 67 | curl_easy_setopt(c, CURLOPT_XFERINFODATA, reinterpret_cast<void *>(&prog)); 68 | curl_easy_setopt(c, CURLOPT_XFERINFOFUNCTION, progFunc); 69 | } 70 | 71 | // Send request and download 72 | CURLcode res = curl_easy_perform(c); 73 | curl_easy_cleanup(c); 74 | 75 | return (res == CURLE_OK); 76 | } 77 | 78 | std::string downloadToString(std::string url) { 79 | std::ostringstream ss; 80 | 81 | // Setup request 82 | CURL * c = initCurlHandle(); 83 | curl_easy_setopt(c, CURLOPT_URL, url.c_str()); 84 | curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, writeData); 85 | curl_easy_setopt(c, CURLOPT_WRITEDATA, &ss); 86 | 87 | // Send request and wait for response 88 | CURLcode res = curl_easy_perform(c); 89 | curl_easy_cleanup(c); 90 | 91 | if (res == CURLE_OK) { 92 | return ss.str(); 93 | } 94 | return ""; 95 | } 96 | }; -------------------------------------------------------------------------------- /Application/source/utils/Forwarder.cpp: -------------------------------------------------------------------------------- 1 | #include <filesystem> 2 | #include "utils/Forwarder.hpp" 3 | #include "utils/Utils.hpp" 4 | 5 | // Variables used for installation/detection 6 | // Realistically I could do without them but this saves repeat 'exists' calls 7 | static bool atms; 8 | static bool rei; 9 | static bool sx; 10 | static bool hasAtms; 11 | static bool hasRei; 12 | static bool hasSx; 13 | 14 | namespace Utils::Forwarder { 15 | void initVars() { 16 | atms = false; 17 | rei = false; 18 | sx = false; 19 | hasAtms = false; 20 | hasRei = false; 21 | hasSx = false; 22 | 23 | // Check for files 24 | if (std::filesystem::exists("/atmosphere/contents")) { 25 | hasAtms = true; 26 | if (std::filesystem::exists("/atmosphere/contents/0100000000001013/exefs.nsp")) { 27 | atms = true; 28 | } 29 | } 30 | if (std::filesystem::exists("/ReiNX/contents")) { 31 | hasRei = true; 32 | if (std::filesystem::exists("/ReiNX/contents/0100000000001013/exefs.nsp")) { 33 | rei = true; 34 | } 35 | } 36 | if (std::filesystem::exists("/sxos/titles")) { 37 | hasSx = true; 38 | if (std::filesystem::exists("/sxos/titles/0100000000001013/exefs.nsp")) { 39 | sx = true; 40 | } 41 | } 42 | } 43 | 44 | bool installed() { 45 | return (atmosphere() || reinx() || sxos()); 46 | } 47 | 48 | void uninstall() { 49 | if (atms) { 50 | std::filesystem::remove("/atmosphere/contents/0100000000001013/exefs.nsp"); 51 | atms = false; 52 | } 53 | if (rei) { 54 | std::filesystem::remove("/ReiNX/contents/0100000000001013/exefs.nsp"); 55 | rei = false; 56 | } 57 | if (sx) { 58 | std::filesystem::remove("/sxos/titles/0100000000001013/exefs.nsp"); 59 | sx = false; 60 | } 61 | } 62 | 63 | void install() { 64 | if (hasAtms) { 65 | std::filesystem::create_directory("/atmosphere/contents/0100000000001013"); 66 | Utils::copyFile("romfs:/exefs.nsp", "/atmosphere/contents/0100000000001013/exefs.nsp"); 67 | atms = true; 68 | } 69 | 70 | if (hasRei) { 71 | std::filesystem::create_directory("/ReiNX/contents/0100000000001013"); 72 | Utils::copyFile("romfs:/exefs.nsp", "/ReiNX/contents/0100000000001013/exefs.nsp"); 73 | rei = true; 74 | } 75 | 76 | if (hasSx) { 77 | std::filesystem::create_directory("/sxos/titles/0100000000001013"); 78 | Utils::copyFile("romfs:/exefs.nsp", "/sxos/titles/0100000000001013/exefs.nsp"); 79 | sx = true; 80 | } 81 | } 82 | 83 | bool prepared() { 84 | return std::filesystem::exists("/switch/NX-Activity-Log/NX-Activity-Log.nro"); 85 | } 86 | 87 | bool atmosphere() { 88 | return atms; 89 | } 90 | 91 | bool reinx() { 92 | return rei; 93 | } 94 | 95 | bool sxos() { 96 | return sx; 97 | } 98 | }; -------------------------------------------------------------------------------- /Application/source/utils/Lang.cpp: -------------------------------------------------------------------------------- 1 | #include <filesystem> 2 | #include <fstream> 3 | #include "nlohmann/json.hpp" 4 | #include "utils/Lang.hpp" 5 | #include "utils/NX.hpp" 6 | #include <sstream> 7 | 8 | namespace Utils::Lang { 9 | // json object which stores strings 10 | static nlohmann::json en = nullptr; 11 | static nlohmann::json j = nullptr; 12 | 13 | bool setFile(std::string path) { 14 | // Check it exists 15 | if (!std::filesystem::exists(path)) { 16 | return false; 17 | } 18 | 19 | // Open and copy into object 20 | std::ifstream in(path); 21 | j = nlohmann::json::parse(in); 22 | 23 | return true; 24 | } 25 | 26 | bool setLanguage(Language l) { 27 | // Also set the fallback language to English here on first set 28 | if (en == nullptr) { 29 | std::ifstream in("romfs:/lang/en.json"); 30 | en = nlohmann::json::parse(in); 31 | } 32 | 33 | std::string path = ""; 34 | if (l == Default) { 35 | l = Utils::NX::getSystemLanguage(); 36 | } 37 | 38 | switch (l) { 39 | case Default: 40 | case English: 41 | path = "romfs:/lang/en.json"; 42 | break; 43 | 44 | case French: 45 | path = "romfs:/lang/fr.json"; 46 | break; 47 | 48 | case German: 49 | path = "romfs:/lang/de.json"; 50 | break; 51 | 52 | case Italian: 53 | path = "romfs:/lang/it.json"; 54 | break; 55 | 56 | case Portugese: 57 | path = "romfs:/lang/pt-BR.json"; 58 | break; 59 | 60 | case Russian: 61 | path = "romfs:/lang/ru.json"; 62 | break; 63 | 64 | case Spanish: 65 | path = "romfs:/lang/es.json"; 66 | break; 67 | 68 | case Turkish: 69 | path = "romfs:/lang/tr.json"; 70 | break; 71 | 72 | case ChineseTraditional: 73 | path = "romfs:/lang/zh-HANT.json"; 74 | break; 75 | 76 | case Chinese: 77 | path = "romfs:/lang/zh-HANS.json"; 78 | break; 79 | 80 | case Korean: 81 | path = "romfs:/lang/ko.json"; 82 | break; 83 | 84 | default: 85 | break; 86 | } 87 | 88 | return setFile(path); 89 | } 90 | 91 | std::string string(std::string key) { 92 | bool haveString = false; 93 | bool isFallback = false; 94 | 95 | // First 'navigate' to nested object 96 | nlohmann::json t = j; 97 | while (!haveString) { 98 | std::istringstream ss(key); 99 | std::string k; 100 | while (std::getline(ss, k, '.') && t != nullptr) { 101 | t = t[k]; 102 | } 103 | 104 | // If the string is not present return key 105 | if (t == nullptr || !t.is_string()) { 106 | // Check fallback json, otherwise return key 107 | if (!isFallback) { 108 | t = en; 109 | isFallback = true; 110 | } else { 111 | return key; 112 | } 113 | 114 | } else { 115 | haveString = true; 116 | } 117 | } 118 | 119 | return t.get<std::string>(); 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /Application/source/utils/NX.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/NX.hpp" 2 | 3 | // Maximum number of titles to read using pdm 4 | #define MAX_TITLES 2000 5 | 6 | // Comparison of AccountUids 7 | bool operator == (const AccountUid &a, const AccountUid &b) { 8 | if (a.uid[0] == b.uid[0] && a.uid[1] == b.uid[1]) { 9 | return true; 10 | } 11 | return false; 12 | } 13 | 14 | namespace Utils::NX { 15 | ThemeType getHorizonTheme() { 16 | ColorSetId thm; 17 | Result rc = setsysGetColorSetId(&thm); 18 | if (R_SUCCEEDED(rc)) { 19 | switch (thm) { 20 | case ColorSetId_Light: 21 | return ThemeType::Light; 22 | break; 23 | 24 | case ColorSetId_Dark: 25 | return ThemeType::Dark; 26 | break; 27 | } 28 | } 29 | 30 | // If it fails return dark 31 | return ThemeType::Dark; 32 | } 33 | 34 | Language getSystemLanguage() { 35 | SetLanguage sl; 36 | u64 l; 37 | setInitialize(); 38 | setGetSystemLanguage(&l); 39 | setMakeLanguage(l, &sl); 40 | setExit(); 41 | 42 | Language lang; 43 | switch (sl) { 44 | case SetLanguage_ENGB: 45 | case SetLanguage_ENUS: 46 | lang = English; 47 | break; 48 | 49 | case SetLanguage_FR: 50 | lang = French; 51 | break; 52 | 53 | case SetLanguage_DE: 54 | lang = German; 55 | break; 56 | 57 | case SetLanguage_IT: 58 | lang = Italian; 59 | break; 60 | 61 | case SetLanguage_PT: 62 | lang = Portugese; 63 | break; 64 | 65 | case SetLanguage_RU: 66 | lang = Russian; 67 | break; 68 | 69 | case SetLanguage_ES: 70 | lang = Spanish; 71 | break; 72 | 73 | case SetLanguage_ZHHANT: 74 | lang = ChineseTraditional; 75 | break; 76 | 77 | case SetLanguage_ZHCN: 78 | case SetLanguage_ZHHANS: 79 | lang = Chinese; 80 | break; 81 | 82 | case SetLanguage_KO: 83 | lang = Korean; 84 | break; 85 | 86 | default: 87 | lang = Default; 88 | break; 89 | } 90 | 91 | return lang; 92 | } 93 | 94 | ::NX::User * getUserPageUser() { 95 | ::NX::User * u = nullptr; 96 | 97 | AppletType t = appletGetAppletType(); 98 | if (t == AppletType_LibraryApplet) { 99 | // Attempt to get user id from IStorage 100 | AppletStorage * s = (AppletStorage *)malloc(sizeof(AppletStorage)); 101 | // Pop common args IStorage 102 | if (R_SUCCEEDED(appletPopInData(s))) { 103 | // Pop MyPage-specific args IStorage 104 | if (R_SUCCEEDED(appletPopInData(s))) { 105 | // Get user id 106 | AccountUid uid; 107 | appletStorageRead(s, 0x8, &uid, 0x10); 108 | 109 | // Check if valid 110 | AccountUid userIDs[ACC_USER_LIST_SIZE]; 111 | s32 num = 0; 112 | accountListAllUsers(userIDs, ACC_USER_LIST_SIZE, &num); 113 | for (s32 i = 0; i < num; i++) { 114 | if (uid == userIDs[i]) { 115 | u = new ::NX::User(uid); 116 | break; 117 | } 118 | } 119 | } 120 | } 121 | free(s); 122 | } 123 | 124 | return u; 125 | } 126 | 127 | std::vector<::NX::User *> getUserObjects() { 128 | // Get IDs 129 | std::vector<::NX::User *> users; 130 | AccountUid userIDs[ACC_USER_LIST_SIZE]; 131 | s32 num = 0; 132 | Result rc = accountListAllUsers(userIDs, ACC_USER_LIST_SIZE, &num); 133 | 134 | if (R_SUCCEEDED(rc)) { 135 | // Create objects and insert into vector 136 | for (s32 i = 0; i < num; i++) { 137 | users.push_back(new ::NX::User(userIDs[i])); 138 | } 139 | } 140 | 141 | // Returns an empty vector if an error occurred 142 | return users; 143 | } 144 | 145 | std::vector<::NX::Title *> getTitleObjects(std::vector<::NX::User *> u) { 146 | Result rc; 147 | 148 | // Get ALL played titles for ALL users 149 | // (this doesn't include installed games that haven't been played) 150 | std::vector<TitleID> playedIDs; 151 | for (unsigned short i = 0; i < u.size(); i++) { 152 | s32 playedTotal = 0; 153 | TitleID tmpID = 0; 154 | PdmAccountPlayEvent *userPlayEvents = new PdmAccountPlayEvent[MAX_TITLES]; 155 | rc = pdmqryQueryAccountPlayEvent(0, u[i]->ID(), userPlayEvents, MAX_TITLES, &playedTotal); 156 | if (R_FAILED(rc) || playedTotal == 0) { 157 | delete[] userPlayEvents; 158 | continue; 159 | } 160 | 161 | // Push back ID if not already in the vector 162 | for (s32 j = 0; j < playedTotal; j++) { 163 | bool found = false; 164 | tmpID = (static_cast<TitleID>(userPlayEvents[j].application_id[0]) << 32) | userPlayEvents[j].application_id[1]; 165 | for (size_t k = 0; k < playedIDs.size(); k++) { 166 | if (playedIDs[k] == tmpID) { 167 | found = true; 168 | break; 169 | } 170 | } 171 | 172 | if (!found) { 173 | playedIDs.push_back(tmpID); 174 | } 175 | } 176 | delete[] userPlayEvents; 177 | } 178 | 179 | // Get IDs of all installed titles 180 | std::vector<TitleID> installedIDs; 181 | NsApplicationRecord * records = new NsApplicationRecord[MAX_TITLES]; 182 | s32 count = 0; 183 | s32 out = 0; 184 | while (true) { 185 | rc = nsListApplicationRecord(records, MAX_TITLES, count, &out); 186 | // Break if at the end or no titles 187 | if (R_FAILED(rc) || out == 0){ 188 | break; 189 | } 190 | for (s32 i = 0; i < out; i++) { 191 | installedIDs.push_back((records + i)->application_id); 192 | } 193 | count += out; 194 | } 195 | delete[] records; 196 | 197 | // Create Title objects from IDs 198 | std::vector<::NX::Title *> titles; 199 | for (size_t i = 0; i < playedIDs.size(); i++) { 200 | // Loop over installed titles to determine if installed or not 201 | bool installed = false; 202 | for (size_t j = 0; j < installedIDs.size(); j++) { 203 | if (installedIDs[j] == playedIDs[i]) { 204 | installed = true; 205 | break; 206 | } 207 | } 208 | 209 | titles.push_back(new ::NX::Title(playedIDs[i], installed)); 210 | } 211 | 212 | return titles; 213 | } 214 | 215 | void startServices() { 216 | accountInitialize(AccountServiceType_System); 217 | nsInitialize(); 218 | pdmqryInitialize(); 219 | romfsInit(); 220 | setsysInitialize(); 221 | socketInitializeDefault(); 222 | 223 | #if _NXLINK_ 224 | nxlinkStdio(); 225 | #endif 226 | } 227 | 228 | void stopServices() { 229 | accountExit(); 230 | nsExit(); 231 | pdmqryExit(); 232 | romfsExit(); 233 | setsysExit(); 234 | socketExit(); 235 | } 236 | }; 237 | -------------------------------------------------------------------------------- /Application/source/utils/ThemeUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "SimpleIniParser.hpp" 2 | #include "utils/ThemeUtils.hpp" 3 | 4 | // Ini file path 5 | #define INI_PATH "/config/NX-Activity-Log/theme.ini" 6 | // Ini var 7 | static simpleIniParser::Ini * ini = nullptr; 8 | // Set true when a value has been changed and thus should write to file 9 | static bool changed; 10 | 11 | namespace Utils::Theme { 12 | void readIni() { 13 | ini = simpleIniParser::Ini::parseOrCreateFile(INI_PATH); 14 | changed = false; 15 | } 16 | 17 | void readValues(std::string s, Aether::Colour & col) { 18 | if (ini == nullptr) { 19 | return; 20 | } 21 | 22 | simpleIniParser::IniSection * sec = ini->findOrCreateSection(s); 23 | simpleIniParser::IniOption * option = sec->findOrCreateFirstOption("r", std::to_string(col.r())); 24 | col.setR(std::stoi(option->value)); 25 | option = sec->findOrCreateFirstOption("g", std::to_string(col.g())); 26 | col.setG(std::stoi(option->value)); 27 | option = sec->findOrCreateFirstOption("b", std::to_string(col.b())); 28 | col.setB(std::stoi(option->value)); 29 | option = sec->findOrCreateFirstOption("a", std::to_string(col.a())); 30 | col.setA(std::stoi(option->value)); 31 | } 32 | 33 | void writeValues(std::string s, Aether::Colour col) { 34 | if (ini == nullptr) { 35 | return; 36 | } 37 | 38 | simpleIniParser::IniSection * sec = ini->findOrCreateSection(s); 39 | simpleIniParser::IniOption * option = sec->findOrCreateFirstOption("r", std::to_string(col.r())); 40 | option->value = std::to_string(col.r()); 41 | option = sec->findOrCreateFirstOption("g", std::to_string(col.g())); 42 | option->value = std::to_string(col.g()); 43 | option = sec->findOrCreateFirstOption("b", std::to_string(col.b())); 44 | option->value = std::to_string(col.b()); 45 | option = sec->findOrCreateFirstOption("a", std::to_string(col.a())); 46 | option->value = std::to_string(col.a()); 47 | changed = true; 48 | } 49 | 50 | void deleteIni() { 51 | if (changed) { 52 | ini->writeToFile(INI_PATH); 53 | } 54 | delete ini; 55 | ini = nullptr; 56 | } 57 | }; -------------------------------------------------------------------------------- /Application/source/utils/UpdateUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/Curl.hpp" 2 | #include <filesystem> 3 | #include <fstream> 4 | #include "nlohmann/json.hpp" 5 | #include "utils/UpdateUtils.hpp" 6 | #include "utils/Time.hpp" 7 | 8 | // Flag indicating an update is available 9 | #define AVAILABLE_FILE "/config/NX-Activity-Log/update.flag" 10 | // URL to query release data 11 | #define GITHUB_API_URL "https://api.github.com/repos/tallbl0nde/NX-Activity-Log/releases/latest" 12 | // File storing timestamp of last check 13 | #define TIMESTAMP_FILE "/config/NX-Activity-Log/update.time" 14 | // Path of downloaded .nro 15 | #define TMP_DONE_PATH "/switch/NX-Activity-Log/update.nro" 16 | // Path of downloading .nro (not finished) 17 | #define TMP_PATH "/switch/NX-Activity-Log/tmp.nro" 18 | 19 | namespace Utils::Update { 20 | bool available() { 21 | return std::filesystem::exists(AVAILABLE_FILE); 22 | } 23 | 24 | UpdateData check() { 25 | UpdateData d; 26 | d.success = false; 27 | std::string data = Utils::Curl::downloadToString(GITHUB_API_URL); 28 | if (data.length() > 0) { 29 | nlohmann::json j = nlohmann::json::parse(data); 30 | if (j["tag_name"] != nullptr && j["body"] != nullptr) { 31 | d.version = j["tag_name"].get<std::string>(); 32 | d.changelog = j["body"].get<std::string>(); 33 | if (j["assets"] != nullptr) { 34 | if (j["assets"][0] != nullptr) { 35 | if (j["assets"][0]["browser_download_url"] != nullptr) { 36 | d.url = j["assets"][0]["browser_download_url"].get<std::string>(); 37 | d.success = true; 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | if (!available()) { 45 | if (d.success) { 46 | if (d.version != "v" VER_STRING) { 47 | std::ofstream file(AVAILABLE_FILE); 48 | file << d.version; 49 | } 50 | } 51 | } 52 | 53 | // Get timestamp and write to file 54 | if (d.success) { 55 | std::ofstream file(TIMESTAMP_FILE, std::ios::trunc); 56 | file << Utils::Time::getTimeT(Utils::Time::getTmForCurrentTime()); 57 | } 58 | 59 | return d; 60 | } 61 | 62 | bool download(std::string url, std::function<void(long long int, long long int)> func) { 63 | // Create folders if necessary 64 | if (!std::filesystem::exists("/switch/NX-Activity-Log/")) { 65 | std::filesystem::create_directory("/switch"); 66 | std::filesystem::create_directory("/switch/NX-Activity-Log"); 67 | } 68 | 69 | bool b = Utils::Curl::downloadToFile(url, TMP_PATH, func); 70 | if (!b) { 71 | // If failed remove file 72 | std::filesystem::remove(TMP_PATH); 73 | } else { 74 | // If it succeeded rename 75 | std::filesystem::rename(TMP_PATH, TMP_DONE_PATH); 76 | } 77 | 78 | return b; 79 | } 80 | 81 | void install() { 82 | if (std::filesystem::exists(TMP_DONE_PATH)) { 83 | std::filesystem::remove("/switch/NX-Activity-Log/NX-Activity-Log.nro"); 84 | std::filesystem::rename(TMP_DONE_PATH, "/switch/NX-Activity-Log/NX-Activity-Log.nro"); 85 | if (std::filesystem::exists(AVAILABLE_FILE)) { 86 | std::filesystem::remove(AVAILABLE_FILE); 87 | } 88 | } 89 | } 90 | 91 | bool needsCheck() { 92 | // Don't check if an update is available 93 | if (std::filesystem::exists(AVAILABLE_FILE)) { 94 | return false; 95 | } 96 | 97 | // If no timestamp file check 98 | if (!std::filesystem::exists(TIMESTAMP_FILE)) { 99 | return true; 100 | } 101 | 102 | // Read timestamp from file 103 | std::ifstream file(TIMESTAMP_FILE); 104 | unsigned int ts; 105 | file >> ts; 106 | return Utils::Time::areDifferentDates(Utils::Time::getTm(ts), Utils::Time::getTmForCurrentTime()); 107 | } 108 | }; -------------------------------------------------------------------------------- /Forwarder/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017-2018 nx-hbloader Authors 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /Forwarder/Makefile: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------- 2 | .SUFFIXES: 3 | #--------------------------------------------------------------------------------- 4 | 5 | ifeq ($(strip $(DEVKITPRO)),) 6 | $(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/devkitpro") 7 | endif 8 | 9 | TOPDIR ?= $(CURDIR) 10 | include $(DEVKITPRO)/libnx/switch_rules 11 | 12 | #--------------------------------------------------------------------------------- 13 | # TARGET is the name of the output 14 | # BUILD is the directory where object files & intermediate files will be placed 15 | # SOURCES is a list of directories containing source code 16 | # DATA is a list of directories containing data files 17 | # INCLUDES is a list of directories containing header files 18 | # ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) 19 | # 20 | # NO_ICON: if set to anything, do not use icon. 21 | # NO_NACP: if set to anything, no .nacp file is generated. 22 | # APP_TITLE is the name of the app stored in the .nacp file (Optional) 23 | # APP_AUTHOR is the author of the app stored in the .nacp file (Optional) 24 | # APP_VERSION is the version of the app stored in the .nacp file (Optional) 25 | # APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) 26 | # ICON is the filename of the icon (.jpg), relative to the project folder. 27 | # If not set, it attempts to use one of the following (in this order): 28 | # - <Project name>.jpg 29 | # - icon.jpg 30 | # - <libnx folder>/default_icon.jpg 31 | # 32 | # CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. 33 | # If not set, it attempts to use one of the following (in this order): 34 | # - <Project name>.json 35 | # - config.json 36 | # If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead 37 | # of a homebrew executable (.nro). This is intended to be used for sysmodules. 38 | # NACP building is skipped as well. 39 | #--------------------------------------------------------------------------------- 40 | TARGET := exefs 41 | BUILD := build 42 | SOURCES := source 43 | DATA := data 44 | INCLUDES := include 45 | #ROMFS := romfs 46 | APP_VERSION := 2.2.0 47 | 48 | #--------------------------------------------------------------------------------- 49 | # options for code generation 50 | #--------------------------------------------------------------------------------- 51 | ARCH := -march=armv8-a -mtune=cortex-a57 -mtp=soft -fPIE 52 | 53 | CFLAGS := -g -Wall -O2 -ffunction-sections \ 54 | $(ARCH) $(DEFINES) 55 | 56 | CFLAGS += $(INCLUDE) -D__SWITCH__ -DVERSION=\"v$(APP_VERSION)\" 57 | 58 | CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions 59 | 60 | ASFLAGS := -g $(ARCH) 61 | LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-wrap,exit -Wl,-Map,$(notdir $*.map) 62 | 63 | LIBS := -lnx 64 | 65 | #--------------------------------------------------------------------------------- 66 | # list of directories containing libraries, this must be the top level containing 67 | # include and lib 68 | #--------------------------------------------------------------------------------- 69 | LIBDIRS := $(PORTLIBS) $(LIBNX) 70 | 71 | 72 | #--------------------------------------------------------------------------------- 73 | # no real need to edit anything past this point unless you need to add additional 74 | # rules for different file extensions 75 | #--------------------------------------------------------------------------------- 76 | ifneq ($(BUILD),$(notdir $(CURDIR))) 77 | #--------------------------------------------------------------------------------- 78 | 79 | export OUTPUT := $(CURDIR)/$(TARGET) 80 | export TOPDIR := $(CURDIR) 81 | 82 | export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ 83 | $(foreach dir,$(DATA),$(CURDIR)/$(dir)) 84 | 85 | export DEPSDIR := $(CURDIR)/$(BUILD) 86 | 87 | CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) 88 | CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) 89 | SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) 90 | BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) 91 | 92 | #--------------------------------------------------------------------------------- 93 | # use CXX for linking C++ projects, CC for standard C 94 | #--------------------------------------------------------------------------------- 95 | ifeq ($(strip $(CPPFILES)),) 96 | #--------------------------------------------------------------------------------- 97 | export LD := $(CC) 98 | #--------------------------------------------------------------------------------- 99 | else 100 | #--------------------------------------------------------------------------------- 101 | export LD := $(CXX) 102 | #--------------------------------------------------------------------------------- 103 | endif 104 | #--------------------------------------------------------------------------------- 105 | 106 | export OFILES_BIN := $(addsuffix .o,$(BINFILES)) 107 | export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) 108 | export OFILES := $(OFILES_BIN) $(OFILES_SRC) 109 | export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) 110 | 111 | export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ 112 | $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ 113 | -I$(CURDIR)/$(BUILD) 114 | 115 | export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) 116 | 117 | ifeq ($(strip $(CONFIG_JSON)),) 118 | jsons := $(wildcard *.json) 119 | ifneq (,$(findstring $(TARGET).json,$(jsons))) 120 | export APP_JSON := $(TOPDIR)/$(TARGET).json 121 | else 122 | ifneq (,$(findstring config.json,$(jsons))) 123 | export APP_JSON := $(TOPDIR)/config.json 124 | endif 125 | endif 126 | else 127 | export APP_JSON := $(TOPDIR)/$(CONFIG_JSON) 128 | endif 129 | 130 | ifeq ($(strip $(ICON)),) 131 | icons := $(wildcard *.jpg) 132 | ifneq (,$(findstring $(TARGET).jpg,$(icons))) 133 | export APP_ICON := $(TOPDIR)/$(TARGET).jpg 134 | else 135 | ifneq (,$(findstring icon.jpg,$(icons))) 136 | export APP_ICON := $(TOPDIR)/icon.jpg 137 | endif 138 | endif 139 | else 140 | export APP_ICON := $(TOPDIR)/$(ICON) 141 | endif 142 | 143 | ifeq ($(strip $(NO_ICON)),) 144 | export NROFLAGS += --icon=$(APP_ICON) 145 | endif 146 | 147 | ifeq ($(strip $(NO_NACP)),) 148 | export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp 149 | endif 150 | 151 | ifneq ($(APP_TITLEID),) 152 | export NACPFLAGS += --titleid=$(APP_TITLEID) 153 | endif 154 | 155 | ifneq ($(ROMFS),) 156 | export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) 157 | endif 158 | 159 | .PHONY: $(BUILD) clean all 160 | 161 | #--------------------------------------------------------------------------------- 162 | all: $(BUILD) 163 | 164 | $(BUILD): 165 | @[ -d $@ ] || mkdir -p $@ 166 | @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile 167 | 168 | #--------------------------------------------------------------------------------- 169 | clean: 170 | @echo clean ... 171 | ifeq ($(strip $(APP_JSON)),) 172 | @rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf 173 | else 174 | @rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf 175 | endif 176 | 177 | 178 | #--------------------------------------------------------------------------------- 179 | else 180 | .PHONY: all 181 | 182 | DEPENDS := $(OFILES:.o=.d) 183 | 184 | #--------------------------------------------------------------------------------- 185 | # main targets 186 | #--------------------------------------------------------------------------------- 187 | ifeq ($(strip $(APP_JSON)),) 188 | 189 | all : $(OUTPUT).nro 190 | 191 | ifeq ($(strip $(NO_NACP)),) 192 | $(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp 193 | else 194 | $(OUTPUT).nro : $(OUTPUT).elf 195 | endif 196 | 197 | else 198 | 199 | all : $(OUTPUT).nsp 200 | 201 | $(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm 202 | 203 | $(OUTPUT).nso : $(OUTPUT).elf 204 | 205 | endif 206 | 207 | $(OUTPUT).elf : $(OFILES) 208 | 209 | $(OFILES_SRC) : $(HFILES_BIN) 210 | 211 | #--------------------------------------------------------------------------------- 212 | # you need a rule like this for each extension you use as binary data 213 | #--------------------------------------------------------------------------------- 214 | %.bin.o %_bin.h : %.bin 215 | #--------------------------------------------------------------------------------- 216 | @echo $(notdir $<) 217 | @$(bin2o) 218 | 219 | -include $(DEPENDS) 220 | 221 | #--------------------------------------------------------------------------------------- 222 | endif 223 | #--------------------------------------------------------------------------------------- 224 | -------------------------------------------------------------------------------- /Forwarder/README.md: -------------------------------------------------------------------------------- 1 | # NX Activity Log - Forwarder 2 | 3 | This is simply a modified version of nx-hbloader that overrides the user page with my app. It was originally developed by [switchbrew](https://github.com/switchbrew/)! -------------------------------------------------------------------------------- /Forwarder/exefs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hbloader", 3 | "title_id": "0x01000000000010013", 4 | "title_id_range_min": "0x01000000000010013", 5 | "title_id_range_max": "0x01000000000010013", 6 | "main_thread_stack_size": "0x100000", 7 | "main_thread_priority": 44, 8 | "default_cpu_id": 0, 9 | "process_category": 0, 10 | "pool_partition": 0, 11 | "is_64_bit": true, 12 | "address_space_type": 1, 13 | "is_retail": true, 14 | "filesystem_access": { 15 | "permissions": "0xFFFFFFFFFFFFFFFF" 16 | }, 17 | "service_host": [ 18 | "*" 19 | ], 20 | "service_access": [ 21 | "*" 22 | ], 23 | "kernel_capabilities": [ 24 | { 25 | "type": "kernel_flags", 26 | "value": { 27 | "highest_thread_priority": 59, 28 | "lowest_thread_priority": 28, 29 | "highest_cpu_id": 2, 30 | "lowest_cpu_id": 0 31 | } 32 | }, 33 | { 34 | "type": "syscalls", 35 | "value": { 36 | "svcUnknown00": "0x00", 37 | "svcSetHeapSize": "0x01", 38 | "svcSetMemoryPermission": "0x02", 39 | "svcSetMemoryAttribute": "0x03", 40 | "svcMapMemory": "0x04", 41 | "svcUnmapMemory": "0x05", 42 | "svcQueryMemory": "0x06", 43 | "svcExitProcess": "0x07", 44 | "svcCreateThread": "0x08", 45 | "svcStartThread": "0x09", 46 | "svcExitThread": "0x0A", 47 | "svcSleepThread": "0x0B", 48 | "svcGetThreadPriority": "0x0C", 49 | "svcSetThreadPriority": "0x0D", 50 | "svcGetThreadCoreMask": "0x0E", 51 | "svcSetThreadCoreMask": "0x0F", 52 | "svcGetCurrentProcessorNumber": "0x10", 53 | "svcSignalEvent": "0x11", 54 | "svcClearEvent": "0x12", 55 | "svcMapSharedMemory": "0x13", 56 | "svcUnmapSharedMemory": "0x14", 57 | "svcCreateTransferMemory": "0x15", 58 | "svcCloseHandle": "0x16", 59 | "svcResetSignal": "0x17", 60 | "svcWaitSynchronization": "0x18", 61 | "svcCancelSynchronization": "0x19", 62 | "svcArbitrateLock": "0x1A", 63 | "svcArbitrateUnlock": "0x1B", 64 | "svcWaitProcessWideKeyAtomic": "0x1C", 65 | "svcSignalProcessWideKey": "0x1D", 66 | "svcGetSystemTick": "0x1E", 67 | "svcConnectToNamedPort": "0x1F", 68 | "svcSendSyncRequestLight": "0x20", 69 | "svcSendSyncRequest": "0x21", 70 | "svcSendSyncRequestWithUserBuffer": "0x22", 71 | "svcSendAsyncRequestWithUserBuffer": "0x23", 72 | "svcGetProcessId": "0x24", 73 | "svcGetThreadId": "0x25", 74 | "svcBreak": "0x26", 75 | "svcOutputDebugString": "0x27", 76 | "svcReturnFromException": "0x28", 77 | "svcGetInfo": "0x29", 78 | "svcFlushEntireDataCache": "0x2A", 79 | "svcFlushDataCache": "0x2B", 80 | "svcMapPhysicalMemory": "0x2C", 81 | "svcUnmapPhysicalMemory": "0x2D", 82 | "svcGetFutureThreadInfo": "0x2E", 83 | "svcGetLastThreadInfo": "0x2F", 84 | "svcGetResourceLimitLimitValue": "0x30", 85 | "svcGetResourceLimitCurrentValue": "0x31", 86 | "svcSetThreadActivity": "0x32", 87 | "svcGetThreadContext3": "0x33", 88 | "svcWaitForAddress": "0x34", 89 | "svcSignalToAddress": "0x35", 90 | "svcUnknown36": "0x36", 91 | "svcUnknown37": "0x37", 92 | "svcUnknown38": "0x38", 93 | "svcUnknown39": "0x39", 94 | "svcUnknown3a": "0x3A", 95 | "svcUnknown3b": "0x3B", 96 | "svcDumpInfo": "0x3C", 97 | "svcDumpInfoNew": "0x3D", 98 | "svcUnknown3e": "0x3E", 99 | "svcUnknown3f": "0x3F", 100 | "svcCreateSession": "0x40", 101 | "svcAcceptSession": "0x41", 102 | "svcReplyAndReceiveLight": "0x42", 103 | "svcReplyAndReceive": "0x43", 104 | "svcReplyAndReceiveWithUserBuffer": "0x44", 105 | "svcCreateEvent": "0x45", 106 | "svcUnknown46": "0x46", 107 | "svcUnknown47": "0x47", 108 | "svcMapPhysicalMemoryUnsafe": "0x48", 109 | "svcUnmapPhysicalMemoryUnsafe": "0x49", 110 | "svcSetUnsafeLimit": "0x4A", 111 | "svcCreateCodeMemory": "0x4B", 112 | "svcControlCodeMemory": "0x4C", 113 | "svcSleepSystem": "0x4D", 114 | "svcReadWriteRegister": "0x4E", 115 | "svcSetProcessActivity": "0x4F", 116 | "svcCreateSharedMemory": "0x50", 117 | "svcMapTransferMemory": "0x51", 118 | "svcUnmapTransferMemory": "0x52", 119 | "svcDebugActiveProcess": "0x60", 120 | "svcBreakDebugProcess": "0x61", 121 | "svcTerminateDebugProcess": "0x62", 122 | "svcGetDebugEvent": "0x63", 123 | "svcContinueDebugEvent": "0x64", 124 | "svcGetProcessList": "0x65", 125 | "svcGetThreadList": "0x66", 126 | "svcGetDebugThreadContext": "0x67", 127 | "svcSetDebugThreadContext": "0x68", 128 | "svcQueryDebugProcessMemory": "0x69", 129 | "svcReadDebugProcessMemory": "0x6A", 130 | "svcWriteDebugProcessMemory": "0x6B", 131 | "svcSetHardwareBreakPoint": "0x6C", 132 | "svcGetDebugThreadParam": "0x6D", 133 | "svcConnectToPort": "0x72", 134 | "svcSetProcessMemoryPermission": "0x73", 135 | "svcMapProcessMemory": "0x74", 136 | "svcUnmapProcessMemory": "0x75", 137 | "svcQueryProcessMemory": "0x76", 138 | "svcMapProcessCodeMemory": "0x77", 139 | "svcUnmapProcessCodeMemory": "0x78", 140 | "svcCallSecureMonitor": "0x7F" 141 | } 142 | }, 143 | { 144 | "type": "application_type", 145 | "value": 2 146 | }, 147 | { 148 | "type": "min_kernel_version", 149 | "value": "0x30" 150 | }, 151 | { 152 | "type": "handle_table_size", 153 | "value": 512 154 | }, 155 | { 156 | "type": "debug_flags", 157 | "value": { 158 | "allow_debug": true, 159 | "force_debug": true 160 | } 161 | } 162 | ] 163 | } 164 | -------------------------------------------------------------------------------- /Forwarder/source/trampoline.s: -------------------------------------------------------------------------------- 1 | .section .text.nroEntrypointTrampoline, "ax", %progbits 2 | 3 | .global nroEntrypointTrampoline 4 | .type nroEntrypointTrampoline, %function 5 | .align 2 6 | 7 | .global __libnx_exception_entry 8 | .type __libnx_exception_entry, %function 9 | 10 | .cfi_startproc 11 | 12 | nroEntrypointTrampoline: 13 | 14 | // Reset stack pointer. 15 | adrp x8, __stack_top //Defined in libnx. 16 | ldr x8, [x8, #:lo12:__stack_top] 17 | mov sp, x8 18 | 19 | // Call NRO. 20 | blr x2 21 | 22 | // Save retval 23 | adrp x1, g_lastRet 24 | str w0, [x1, #:lo12:g_lastRet] 25 | 26 | // Reset stack pointer and load next NRO. 27 | adrp x8, __stack_top 28 | ldr x8, [x8, #:lo12:__stack_top] 29 | mov sp, x8 30 | 31 | b loadNro 32 | 33 | .cfi_endproc 34 | 35 | .section .text.__libnx_exception_entry, "ax", %progbits 36 | .align 2 37 | 38 | .cfi_startproc 39 | 40 | __libnx_exception_entry: 41 | adrp x7, g_nroAddr 42 | ldr x7, [x7, #:lo12:g_nroAddr] 43 | cbz x7, __libnx_exception_entry_fail 44 | br x7 45 | 46 | __libnx_exception_entry_fail: 47 | mov w0, #0xf801 48 | bl svcReturnFromException 49 | b . 50 | 51 | .cfi_endproc 52 | -------------------------------------------------------------------------------- /Images/sc_activity.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tallbl0nde/NX-Activity-Log/b3aab30c5f574c78144ca1ebe4875ef9ccbd95e6/Images/sc_activity.jpg -------------------------------------------------------------------------------- /Images/sc_detailed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tallbl0nde/NX-Activity-Log/b3aab30c5f574c78144ca1ebe4875ef9ccbd95e6/Images/sc_detailed.jpg -------------------------------------------------------------------------------- /Images/sc_recent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tallbl0nde/NX-Activity-Log/b3aab30c5f574c78144ca1ebe4875ef9ccbd95e6/Images/sc_recent.jpg -------------------------------------------------------------------------------- /Images/shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tallbl0nde/NX-Activity-Log/b3aab30c5f574c78144ca1ebe4875ef9ccbd95e6/Images/shop.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 tallbl0nde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This makefile compiles each component of the overall program 2 | # and then copies compiled files to the output directory 3 | 4 | # Codes used to change output colour 5 | COLOUR_BOLD = \033[1m 6 | COLOUR_DEFAULT = \033[0m 7 | 8 | # Path to copy forwarder to 9 | FORWARDER_DEST = $(CURDIR)/Application/romfs/exefs.nsp 10 | # Path to copy forwarder from 11 | FORWARDER_SRC = $(CURDIR)/Forwarder/exefs.nsp 12 | 13 | # All rules are virtual 14 | .PHONY: all application clean clean-application clean-forwarder 15 | 16 | # Compile everything and copy compiled files 17 | all: application 18 | @mkdir -p $(CURDIR)/sdcard/switch/NX-Activity-Log 19 | @cp $(CURDIR)/Application/NX-Activity-Log.nro $(CURDIR)/sdcard/switch/NX-Activity-Log/ 20 | @echo -e '${COLOUR_BOLD}>> Done! Copy the contents of ./sdcard to your SD Card!${COLOUR_DEFAULT}' 21 | 22 | # Compile the application (which depends on the forwarder) 23 | application: $(FORWARDER_DEST) 24 | @echo -e '${COLOUR_BOLD}>> Application${COLOUR_DEFAULT}' 25 | @$(MAKE) -s -C $(CURDIR)/Application/ 26 | 27 | # Compile the forwarder and copy to romfs 28 | $(FORWARDER_DEST): $(FORWARDER_SRC) 29 | @echo Copying Forwarder to Application romfs... 30 | @cp $(FORWARDER_SRC) $(FORWARDER_DEST) 31 | 32 | $(FORWARDER_SRC): FORCE 33 | @echo -e '${COLOUR_BOLD}>> Forwarder${COLOUR_DEFAULT}' 34 | @$(MAKE) -s -C $(CURDIR)/Forwarder/ 35 | 36 | # Clean all build files 37 | clean: clean-application clean-forwarder 38 | @rm -rf $(CURDIR)/sdcard 39 | @echo -e '${COLOUR_BOLD}>> All build files removed!${COLOUR_DEFAULT}' 40 | 41 | clean-application: 42 | @echo -e '${COLOUR_BOLD}>> Application${COLOUR_DEFAULT}' 43 | @$(MAKE) -s -C $(CURDIR)/Application/ clean 44 | 45 | clean-forwarder: 46 | @echo -e '${COLOUR_BOLD}>> Forwarder${COLOUR_DEFAULT}' 47 | @$(MAKE) -s -C $(CURDIR)/Forwarder/ clean 48 | @rm -rf $(FORWARDER_DEST) 49 | 50 | FORCE: -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NX Activity Log 2 | 3 | **NX Activity Log** is a homebrew application for the Nintendo Switch which displays more precise information about your play activity. 4 | 5 | [Download](https://github.com/tallbl0nde/NX-Activity-Log/releases) 6 | 7 | Curious about what's next? See my to-do list on [Trello](https://trello.com/b/HaJ1THGq/nx-activity-log) 8 | 9 | ## Contents 10 | 11 | 1. [Features](#features) 12 | 2. [Screenshots](#screenshots) 13 | 3. [Reporting Incorrect Data](#reporting-incorrect-data) 14 | 4. [Known Issues](#known-issues) 15 | 5. [Translations](#translations) 16 | 6. [Credits](#credits) 17 | 7. [Support](#support-3) 18 | 19 | ## Features 20 | 21 | This application currently has the following features: 22 | 23 | * Viewing Play Activity (per user) 24 | * All Time Activity 25 | * Total Playtime in minutes 26 | * Number of times a game has been launched 27 | * Date of first launch 28 | * Date/time of most recent launch 29 | * Average time spent in a game 30 | * Sorting games by time played, recently played, etc. 31 | * Recent Activity (see note below) 32 | * Graph visualizing play time 33 | * View by day, month or year 34 | * Total playtime in seconds 35 | * Number of launches 36 | * Viewing each 'Play Session' 37 | * Event-by-event breakdown of your activity 38 | * Multi-language Support 39 | * English, Chinese (Simplified and Traditional), French, German, Italian, Korean, Portugese, Russian, Spanish and Turkish 40 | * Theme Support 41 | * Automatic theme detection to match the Switch's colour scheme 42 | * Custom Themes 43 | * All colours used in the app can be adjusted 44 | * Set a custom background image 45 | * **Requires a PNG saved at /config/NX-Activity-Log/background.png** 46 | * Update in-app 47 | * Checks for updates automatically and displays an icon if one is available 48 | * Update completely within the app 49 | * User Page Replacement 50 | * Requires LayeredFS and either Atmosphere 0.10.0+, ReiNX or SXOS (or build and copy the forwarder to your CFW-specific titles folder) 51 | * **Requires .nro to be at /switch/NX-Activity-Log/NX-Activity-Log.nro** 52 | 53 | _Note: The data shown in Recent Activity and Details may be slightly inaccurate over larger periods of time (ie. off by a few minutes) but I will try to improve this over time. If activity is not being shown for earlier periods of time it is likely your switch has been reset at some point, which wipes the data used to calculate playtime in this way._ 54 | 55 | ## Screenshots 56 | 57 | ![Recent View](/Images/sc_recent.jpg) 58 | ![All Activity View](/Images/sc_activity.jpg) 59 | ![Detailed View](/Images/sc_detailed.jpg) 60 | 61 | ## Reporting Incorrect Data 62 | 63 | If you are seeing wildly incorrect values/believe the playtime shown is incorrect within Recent Activity or the Details screen, please do the following: 64 | 65 | 1. Download and run the .nro from [here](https://github.com/tallbl0nde/PlayEventParser/releases) 66 | 2. Leave it run; if it appears to be frozen leave it for up to a minute! 67 | 3. Once it is done there should be a playlog.txt at the root of your SD card. 68 | 4. Create an issue with the following: 69 | * Screenshots of the incorrect data (make sure the date/time and playtime are visible!) 70 | * The playlog.txt acquired from the last steps 71 | 72 | _Note: If the data shown under All Activity is incorrect there is nothing I can do! These values are read directly from the Switch and aren't the result of any calculations._ 73 | 74 | ## Known Issues 75 | 76 | * The main issue at the moment is that a few users' playtime is incorrect. This is due to the Switch being factory reset at some point and/or some games not requiring a user to be selected to play it. _I am looking into how to fix the former!_ 77 | * Having a lot of games logged can cause too much memory usage when launched via User Page/Album, leading to out of memory crashes and/or missing images/text. 78 | * I think I know a way to fix this but if it impacts you you'll have to launch this app in title mode for now. 79 | * Having a lot of games logged causes a long pause/freeze when loading the 'All Activity' screen 80 | * Once I learn how to handle threads this won't be a problem :) 81 | 82 | ## Translations 83 | 84 | If you'd like to translate the app or fix an issue with a translation, please make a pull request to [this repo](https://github.com/tallbl0nde/NX-Activity-Log-Translations)! I'll add all the relevant code here (if need be) once I see the request :) 85 | 86 | ## Credits 87 | 88 | I'd like to thank: 89 | 90 | * AtlasNX for [SimpleIniParser](https://github.com/AtlasNX/SimpleIniParser) 91 | * Used to read/write the config file 92 | * nlohmann for his [JSON library](https://github.com/nlohmann/json) 93 | * Switchbrew for [nx-hbloader](https://github.com/switchbrew/nx-hbloader) 94 | * Adapted to override User Page with this app 95 | * Anyone else involved with the development of homebrew tools and reverse engineering of the Switch! 96 | 97 | ## Support <3 98 | 99 | There is absolutely no obligation, however if you have found this software useful you can support me on Ko-fi! 100 | 101 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/J3J718RRQ) 102 | 103 | Knowing my software is being used is enough to motivate me to continue work on it! 104 | --------------------------------------------------------------------------------