Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ffaa56bc3 | ||
|
|
eca3358e57 | ||
|
|
757e380e08 | ||
|
|
6c1b5de932 | ||
|
|
d79ac126f7 | ||
|
|
2d7763444e | ||
|
|
1dafa2748c | ||
|
|
9f7bf9581c | ||
|
|
8f39acbaa2 | ||
|
|
81469d0ac9 | ||
|
|
1eae35f072 | ||
|
|
5b82e07b1c | ||
|
|
73886c28ae | ||
|
|
eea09f6e57 | ||
|
|
282c6e5493 | ||
|
|
2c2f602d14 | ||
|
|
f7f1254699 | ||
|
|
90f8a62823 | ||
|
|
e2a1c8b5e3 | ||
|
|
21f6f4b74d | ||
|
|
75d3b3ee0d | ||
|
|
0dde379932 | ||
|
|
9800bbecdf | ||
|
|
60e915c255 | ||
|
|
786f8a42fa | ||
|
|
5a4a0f75f2 |
34
README.md
34
README.md
@@ -49,6 +49,40 @@ The `path` field is optional. If left out, it will use the name of the ini to fi
|
|||||||
|
|
||||||
See `assets/romfs/assoc/` for more examples of file assoc entries.
|
See `assets/romfs/assoc/` for more examples of file assoc entries.
|
||||||
|
|
||||||
|
## Installing (applications)
|
||||||
|
|
||||||
|
Sphaira can install applications (nsp, xci, nsz, xcz) from various sources (sd card, gamecard, ftp, usb).
|
||||||
|
|
||||||
|
For informantion about the install options, [see the wiki](https://github.com/ITotalJustice/sphaira/wiki/Install).
|
||||||
|
|
||||||
|
### Usb (install)
|
||||||
|
|
||||||
|
The USB protocol is the same as tinfoil, so tools such as [ns-usbloader](https://github.com/developersu/ns-usbloader) and [fluffy](https://github.com/fourminute/Fluffy) should work with sphaira. You may also use the provided python script found [here](tools/usb_install_pc.py).
|
||||||
|
|
||||||
|
### Ftp (install)
|
||||||
|
|
||||||
|
Once you have connected your ftp client to your switch, you can upload files to install into the `install` folder.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
You will first need to install [devkitPro](https://devkitpro.org/wiki/Getting_Started).
|
||||||
|
|
||||||
|
Next you will need to install the dependencies:
|
||||||
|
```sh
|
||||||
|
sudo pacman -S switch-dev deko3d switch-cmake switch-curl switch-glm switch-zlib
|
||||||
|
```
|
||||||
|
|
||||||
|
Once devkitPro and all dependencies are installed, you can now build sphaira.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/ITotalJustice/sphaira.git
|
||||||
|
cd sphaira
|
||||||
|
cmake --preset MinSizeRel
|
||||||
|
cmake --build --preset MinSizeRel
|
||||||
|
```
|
||||||
|
|
||||||
|
The output will be found in `build/MinSizeRel/sphaira.nro`
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
- borealis
|
- borealis
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
path=/retroarch/cores/fbneo_libretro_libnx.nro
|
path=/retroarch/cores/fbneo_libretro_libnx.nro
|
||||||
supported_extensions=zip|7z|cue|ccd
|
supported_extensions=zip|7z|cue|ccd
|
||||||
database=FBNeo - Arcade Games
|
database=FBNeo - Arcade Games
|
||||||
|
use_base_name=true
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
path=/retroarch/cores/mame2000_libretro_libnx.nro
|
path=/retroarch/cores/mame2000_libretro_libnx.nro
|
||||||
supported_extensions=zip|7z
|
supported_extensions=zip|7z
|
||||||
database=MAME 2000
|
database=MAME 2000
|
||||||
|
use_base_name=true
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
path=/retroarch/cores/mame2003_libretro_libnx.nro
|
path=/retroarch/cores/mame2003_libretro_libnx.nro
|
||||||
supported_extensions=zip
|
supported_extensions=zip
|
||||||
database=MAME 2003
|
database=MAME 2003
|
||||||
|
use_base_name=true
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
|
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
|
||||||
supported_extensions=zip
|
supported_extensions=zip
|
||||||
database=MAME 2003-Plus
|
database=MAME 2003-Plus
|
||||||
|
use_base_name=true
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
path=/retroarch/cores/xrick_libretro_libnx.nro
|
path=/retroarch/cores/xrick_libretro_libnx.nro
|
||||||
supported_extensions=zip
|
supported_extensions=zip
|
||||||
database=Rick Dangerous
|
database=Rick Dangerous
|
||||||
|
use_base_name=true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.13)
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
|
||||||
set(sphaira_VERSION 0.7.0)
|
set(sphaira_VERSION 0.8.1)
|
||||||
|
|
||||||
project(sphaira
|
project(sphaira
|
||||||
VERSION ${sphaira_VERSION}
|
VERSION ${sphaira_VERSION}
|
||||||
@@ -41,12 +41,13 @@ add_executable(sphaira
|
|||||||
source/ui/menus/file_viewer.cpp
|
source/ui/menus/file_viewer.cpp
|
||||||
source/ui/menus/filebrowser.cpp
|
source/ui/menus/filebrowser.cpp
|
||||||
source/ui/menus/homebrew.cpp
|
source/ui/menus/homebrew.cpp
|
||||||
source/ui/menus/irs_menu.cpp
|
|
||||||
source/ui/menus/main_menu.cpp
|
source/ui/menus/main_menu.cpp
|
||||||
source/ui/menus/menu_base.cpp
|
source/ui/menus/menu_base.cpp
|
||||||
source/ui/menus/themezer.cpp
|
source/ui/menus/themezer.cpp
|
||||||
source/ui/menus/ghdl.cpp
|
source/ui/menus/ghdl.cpp
|
||||||
source/ui/menus/usb_menu.cpp
|
source/ui/menus/usb_menu.cpp
|
||||||
|
source/ui/menus/ftp_menu.cpp
|
||||||
|
source/ui/menus/gc_menu.cpp
|
||||||
|
|
||||||
source/ui/error_box.cpp
|
source/ui/error_box.cpp
|
||||||
source/ui/notification.cpp
|
source/ui/notification.cpp
|
||||||
@@ -72,7 +73,6 @@ add_executable(sphaira
|
|||||||
source/nxlink.cpp
|
source/nxlink.cpp
|
||||||
source/owo.cpp
|
source/owo.cpp
|
||||||
source/swkbd.cpp
|
source/swkbd.cpp
|
||||||
source/web.cpp
|
|
||||||
source/i18n.cpp
|
source/i18n.cpp
|
||||||
source/ftpsrv_helper.cpp
|
source/ftpsrv_helper.cpp
|
||||||
|
|
||||||
@@ -82,6 +82,8 @@ add_executable(sphaira
|
|||||||
source/yati/source/file.cpp
|
source/yati/source/file.cpp
|
||||||
source/yati/source/stdio.cpp
|
source/yati/source/stdio.cpp
|
||||||
source/yati/source/usb.cpp
|
source/yati/source/usb.cpp
|
||||||
|
source/yati/source/stream.cpp
|
||||||
|
source/yati/source/stream_file.cpp
|
||||||
|
|
||||||
source/yati/nx/es.cpp
|
source/yati/nx/es.cpp
|
||||||
source/yati/nx/keys.cpp
|
source/yati/nx/keys.cpp
|
||||||
@@ -142,7 +144,8 @@ set(FETCHCONTENT_QUIET FALSE)
|
|||||||
|
|
||||||
FetchContent_Declare(ftpsrv
|
FetchContent_Declare(ftpsrv
|
||||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||||
GIT_TAG 1.2.2
|
# GIT_TAG 1.2.2
|
||||||
|
GIT_TAG f8a30fd
|
||||||
SOURCE_SUBDIR NONE
|
SOURCE_SUBDIR NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public:
|
|||||||
static void SetTheme(s64 theme_index);
|
static void SetTheme(s64 theme_index);
|
||||||
static auto GetThemeIndex() -> s64;
|
static auto GetThemeIndex() -> s64;
|
||||||
|
|
||||||
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
|
static auto GetDefaultImage() -> int;
|
||||||
|
|
||||||
// returns argv[0]
|
// returns argv[0]
|
||||||
static auto GetExePath() -> fs::FsPath;
|
static auto GetExePath() -> fs::FsPath;
|
||||||
@@ -102,6 +102,13 @@ public:
|
|||||||
|
|
||||||
static void PlaySoundEffect(SoundEffect effect);
|
static void PlaySoundEffect(SoundEffect effect);
|
||||||
|
|
||||||
|
static void DisplayThemeOptions(bool left_side = true);
|
||||||
|
// todo:
|
||||||
|
static void DisplayNetworkOptions(bool left_side = true);
|
||||||
|
static void DisplayMiscOptions(bool left_side = true);
|
||||||
|
static void DisplayAdvancedOptions(bool left_side = true);
|
||||||
|
static void DisplayInstallOptions(bool left_side = true);
|
||||||
|
|
||||||
void Draw();
|
void Draw();
|
||||||
void Update();
|
void Update();
|
||||||
void Poll();
|
void Poll();
|
||||||
@@ -182,10 +189,10 @@ public:
|
|||||||
option::OptionBool m_install{INI_SECTION, "install", false};
|
option::OptionBool m_install{INI_SECTION, "install", false};
|
||||||
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
||||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||||
|
option::OptionLong m_boost_mode{INI_SECTION, "boost_mode", false};
|
||||||
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
|
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
|
||||||
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
|
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
|
||||||
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
|
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
|
||||||
option::OptionBool m_patch_ticket{INI_SECTION, "patch_ticket", true};
|
|
||||||
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
|
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
|
||||||
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
|
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
|
||||||
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};
|
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};
|
||||||
|
|||||||
@@ -454,4 +454,10 @@ struct FsNativeContentStorage final : FsNative {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FsNativeGameCard final : FsNative {
|
||||||
|
FsNativeGameCard(const FsGameCardHandle* handle, FsGameCardPartition partition, bool ignore_read_only = true) : FsNative{ignore_read_only} {
|
||||||
|
m_open_result = fsOpenGameCardFileSystem(&m_fs, handle, partition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace fs
|
} // namespace fs
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
namespace sphaira::ftpsrv {
|
namespace sphaira::ftpsrv {
|
||||||
|
|
||||||
bool Init();
|
bool Init();
|
||||||
void Exit();
|
void Exit();
|
||||||
|
|
||||||
|
using OnInstallStart = std::function<bool(void* user, const char* path)>;
|
||||||
|
using OnInstallWrite = std::function<bool(void* user, const void* buf, size_t size)>;
|
||||||
|
using OnInstallClose = std::function<void(void* user)>;
|
||||||
|
|
||||||
|
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close);
|
||||||
|
void DisableInstallMode();
|
||||||
|
|
||||||
|
unsigned GetPort();
|
||||||
|
bool IsAnon();
|
||||||
|
const char* GetUser();
|
||||||
|
const char* GetPass();
|
||||||
|
|
||||||
} // namespace sphaira::ftpsrv
|
} // namespace sphaira::ftpsrv
|
||||||
|
|||||||
@@ -8,15 +8,14 @@ namespace sphaira::ui {
|
|||||||
class ErrorBox final : public Widget {
|
class ErrorBox final : public Widget {
|
||||||
public:
|
public:
|
||||||
ErrorBox(Result code, const std::string& message);
|
ErrorBox(Result code, const std::string& message);
|
||||||
|
ErrorBox(const std::string& message);
|
||||||
|
|
||||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Result m_code{};
|
std::optional<Result> m_code{};
|
||||||
std::string m_message{};
|
std::string m_message{};
|
||||||
std::string m_module_str{};
|
|
||||||
std::string m_description_str{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ namespace sphaira::ui {
|
|||||||
|
|
||||||
struct List final : Object {
|
struct List final : Object {
|
||||||
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
|
using Callback = std::function<void(NVGcontext* vg, Theme* theme, Vec4 v, s64 index)>;
|
||||||
using TouchCallback = std::function<void(s64 index)>;
|
using TouchCallback = std::function<void(bool touch, s64 index)>;
|
||||||
|
|
||||||
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
|
List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad = {});
|
||||||
|
|
||||||
void OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback);
|
void OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
|
||||||
|
|
||||||
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
|
void Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,19 @@ struct FileAssocEntry {
|
|||||||
std::string name{}; // ini name
|
std::string name{}; // ini name
|
||||||
std::vector<std::string> ext{}; // list of ext
|
std::vector<std::string> ext{}; // list of ext
|
||||||
std::vector<std::string> database{}; // list of systems
|
std::vector<std::string> database{}; // list of systems
|
||||||
|
bool use_base_name{}; // if set, uses base name (rom.zip) otherwise uses internal name (rom.gba)
|
||||||
|
|
||||||
|
auto IsExtension(std::string_view extension, std::string_view internal_extension) const -> bool {
|
||||||
|
for (const auto& assoc_ext : ext) {
|
||||||
|
if (extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), extension.data(), assoc_ext.length())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (internal_extension.length() == assoc_ext.length() && !strncasecmp(assoc_ext.data(), internal_extension.data(), assoc_ext.length())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LastFile {
|
struct LastFile {
|
||||||
|
|||||||
59
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
59
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "yati/source/stream.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::ftp {
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
// not connected.
|
||||||
|
None,
|
||||||
|
// just connected, starts the transfer.
|
||||||
|
Connected,
|
||||||
|
// set whilst transfer is in progress.
|
||||||
|
Progress,
|
||||||
|
// set when the transfer is finished.
|
||||||
|
Done,
|
||||||
|
// failed to connect.
|
||||||
|
Failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StreamFtp final : yati::source::Stream {
|
||||||
|
StreamFtp(const fs::FsPath& path, std::stop_token token);
|
||||||
|
|
||||||
|
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||||
|
bool Push(const void* buf, s64 size);
|
||||||
|
void Disable();
|
||||||
|
|
||||||
|
// private:
|
||||||
|
fs::FsPath m_path{};
|
||||||
|
std::stop_token m_token{};
|
||||||
|
std::vector<u8> m_buffer{};
|
||||||
|
Mutex m_mutex{};
|
||||||
|
bool m_active{};
|
||||||
|
// bool m_push_exit{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Menu final : MenuBase {
|
||||||
|
Menu();
|
||||||
|
~Menu();
|
||||||
|
|
||||||
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
void OnFocusGained() override;
|
||||||
|
|
||||||
|
// this should be private
|
||||||
|
// private:
|
||||||
|
std::shared_ptr<StreamFtp> m_source;
|
||||||
|
Thread m_thread{};
|
||||||
|
Mutex m_mutex{};
|
||||||
|
// the below are shared across threads, lock with the above mutex!
|
||||||
|
State m_state{State::None};
|
||||||
|
|
||||||
|
const char* m_user{};
|
||||||
|
const char* m_pass{};
|
||||||
|
unsigned m_port{};
|
||||||
|
bool m_anon{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::ftp
|
||||||
80
sphaira/include/ui/menus/gc_menu.hpp
Normal file
80
sphaira/include/ui/menus/gc_menu.hpp
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "yati/container/base.hpp"
|
||||||
|
#include "yati/source/base.hpp"
|
||||||
|
#include "ui/list.hpp"
|
||||||
|
#include <span>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::gc {
|
||||||
|
|
||||||
|
struct GcCollection : yati::container::CollectionEntry {
|
||||||
|
GcCollection(const char* _name, s64 _size, u8 _type, u8 _id_offset) {
|
||||||
|
name = _name;
|
||||||
|
size = _size;
|
||||||
|
type = _type;
|
||||||
|
id_offset = _id_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NcmContentType
|
||||||
|
u8 type{};
|
||||||
|
u8 id_offset{};
|
||||||
|
};
|
||||||
|
|
||||||
|
using GcCollections = std::vector<GcCollection>;
|
||||||
|
|
||||||
|
struct ApplicationEntry {
|
||||||
|
u64 app_id{};
|
||||||
|
u32 version{};
|
||||||
|
u8 key_gen{};
|
||||||
|
|
||||||
|
std::vector<GcCollections> application{};
|
||||||
|
std::vector<GcCollections> patch{};
|
||||||
|
std::vector<GcCollections> add_on{};
|
||||||
|
std::vector<GcCollections> data_patch{};
|
||||||
|
yati::container::Collections tickets{};
|
||||||
|
|
||||||
|
auto GetSize() const -> s64;
|
||||||
|
auto GetSize(const std::vector<GcCollections>& entries) const -> s64;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Menu final : MenuBase {
|
||||||
|
Menu();
|
||||||
|
~Menu();
|
||||||
|
|
||||||
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Result GcMount();
|
||||||
|
void GcUnmount();
|
||||||
|
Result GcPoll(bool* inserted);
|
||||||
|
Result GcOnEvent();
|
||||||
|
Result UpdateStorageSize();
|
||||||
|
|
||||||
|
void FreeImage();
|
||||||
|
void OnChangeIndex(s64 new_index);
|
||||||
|
|
||||||
|
private:
|
||||||
|
FsDeviceOperator m_dev_op{};
|
||||||
|
FsGameCardHandle m_handle{};
|
||||||
|
std::unique_ptr<fs::FsNativeGameCard> m_fs{};
|
||||||
|
FsEventNotifier m_event_notifier{};
|
||||||
|
Event m_event{};
|
||||||
|
|
||||||
|
std::vector<ApplicationEntry> m_entries{};
|
||||||
|
std::unique_ptr<List> m_list{};
|
||||||
|
s64 m_entry_index{};
|
||||||
|
s64 m_option_index{};
|
||||||
|
|
||||||
|
s64 m_size_free_sd{};
|
||||||
|
s64 m_size_total_sd{};
|
||||||
|
s64 m_size_free_nand{};
|
||||||
|
s64 m_size_total_nand{};
|
||||||
|
NacpLanguageEntry m_lang_entry{};
|
||||||
|
int m_icon{};
|
||||||
|
bool m_mounted{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::gc
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "ui/menus/menu_base.hpp"
|
|
||||||
#include <span>
|
|
||||||
|
|
||||||
namespace sphaira::ui::menu::irs {
|
|
||||||
|
|
||||||
enum Rotation {
|
|
||||||
Rotation_0,
|
|
||||||
Rotation_90,
|
|
||||||
Rotation_180,
|
|
||||||
Rotation_270,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum Colour {
|
|
||||||
Colour_Grey,
|
|
||||||
Colour_Ironbow,
|
|
||||||
Colour_Green,
|
|
||||||
Colour_Red,
|
|
||||||
Colour_Blue,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Entry {
|
|
||||||
IrsIrCameraHandle m_handle{};
|
|
||||||
IrsIrCameraStatus status{};
|
|
||||||
bool m_update_needed{};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Menu final : MenuBase {
|
|
||||||
Menu();
|
|
||||||
~Menu();
|
|
||||||
|
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
|
||||||
void OnFocusGained() override;
|
|
||||||
|
|
||||||
void PollCameraStatus(bool statup = false);
|
|
||||||
void LoadDefaultConfig();
|
|
||||||
void UpdateConfig(const IrsImageTransferProcessorExConfig* config);
|
|
||||||
void ResetImage();
|
|
||||||
void UpdateImage();
|
|
||||||
void updateColourArray();
|
|
||||||
|
|
||||||
private:
|
|
||||||
Result m_init_rc{};
|
|
||||||
|
|
||||||
IrsImageTransferProcessorExConfig m_config{};
|
|
||||||
IrsMomentProcessorConfig m_moment_config{};
|
|
||||||
IrsClusteringProcessorConfig m_clustering_config{};
|
|
||||||
IrsTeraPluginProcessorConfig m_tera_config{};
|
|
||||||
IrsIrLedProcessorConfig m_led_config{};
|
|
||||||
IrsAdaptiveClusteringProcessorConfig m_adaptive_config{};
|
|
||||||
IrsHandAnalysisConfig m_hand_config{};
|
|
||||||
|
|
||||||
Entry m_entries[IRS_MAX_CAMERAS]{};
|
|
||||||
u32 m_irs_width{};
|
|
||||||
u32 m_irs_height{};
|
|
||||||
std::vector<u32> m_rgba{};
|
|
||||||
std::vector<u8> m_irs_buffer{};
|
|
||||||
IrsImageTransferProcessorState m_prev_state{};
|
|
||||||
Rotation m_rotation{Rotation_90};
|
|
||||||
Colour m_colour{Colour_Grey};
|
|
||||||
int m_image{};
|
|
||||||
s64 m_index{};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::irs
|
|
||||||
@@ -29,6 +29,7 @@ private:
|
|||||||
std::string m_title_sub_heading{};
|
std::string m_title_sub_heading{};
|
||||||
std::string m_sub_heading{};
|
std::string m_sub_heading{};
|
||||||
|
|
||||||
|
protected:
|
||||||
struct tm m_tm{};
|
struct tm m_tm{};
|
||||||
TimeStamp m_poll_timestamp{};
|
TimeStamp m_poll_timestamp{};
|
||||||
u32 m_battery_percetange{};
|
u32 m_battery_percetange{};
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ namespace sphaira::ui::menu::usb {
|
|||||||
enum class State {
|
enum class State {
|
||||||
// not connected.
|
// not connected.
|
||||||
None,
|
None,
|
||||||
|
// just connected, waiting for file list.
|
||||||
|
Connected_WaitForFileList,
|
||||||
// just connected, starts the transfer.
|
// just connected, starts the transfer.
|
||||||
Connected,
|
Connected_StartingTransfer,
|
||||||
// set whilst transfer is in progress.
|
// set whilst transfer is in progress.
|
||||||
Progress,
|
Progress,
|
||||||
// set when the transfer is finished.
|
// set when the transfer is finished.
|
||||||
@@ -35,9 +37,8 @@ struct Menu final : MenuBase {
|
|||||||
Mutex m_mutex{};
|
Mutex m_mutex{};
|
||||||
// the below are shared across threads, lock with the above mutex!
|
// the below are shared across threads, lock with the above mutex!
|
||||||
State m_state{State::None};
|
State m_state{State::None};
|
||||||
|
std::vector<std::string> m_names{};
|
||||||
bool m_usb_has_connection{};
|
bool m_usb_has_connection{};
|
||||||
u32 m_usb_speed{};
|
|
||||||
u32 m_usb_count{};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::usb
|
} // namespace sphaira::ui::menu::usb
|
||||||
|
|||||||
@@ -5,17 +5,15 @@
|
|||||||
|
|
||||||
namespace sphaira::ui::gfx {
|
namespace sphaira::ui::gfx {
|
||||||
|
|
||||||
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture);
|
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture, float rounded = 0.F);
|
||||||
void drawImage(NVGcontext*, const Vec4& v, int texture);
|
void drawImage(NVGcontext*, const Vec4& v, int texture, float rounded = 0.F);
|
||||||
void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture);
|
|
||||||
void drawImageRounded(NVGcontext*, const Vec4& v, int texture);
|
|
||||||
|
|
||||||
void dimBackground(NVGcontext*);
|
void dimBackground(NVGcontext*);
|
||||||
|
|
||||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false);
|
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, float rounding = 0.F);
|
||||||
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false);
|
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, float rounding = 0.F);
|
||||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
|
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, float rounding = 0.F);
|
||||||
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, bool rounded = false);
|
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.F);
|
||||||
|
|
||||||
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
|
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
|
||||||
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);
|
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "widget.hpp"
|
#include "widget.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
|
|
||||||
@@ -12,6 +13,8 @@ using ProgressBoxDoneCallback = std::function<void(bool success)>;
|
|||||||
|
|
||||||
struct ProgressBox final : Widget {
|
struct ProgressBox final : Widget {
|
||||||
ProgressBox(
|
ProgressBox(
|
||||||
|
int image,
|
||||||
|
const std::string& action,
|
||||||
const std::string& title,
|
const std::string& title,
|
||||||
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
|
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
|
||||||
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024
|
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024
|
||||||
@@ -24,6 +27,9 @@ struct ProgressBox final : Widget {
|
|||||||
auto SetTitle(const std::string& title) -> ProgressBox&;
|
auto SetTitle(const std::string& title) -> ProgressBox&;
|
||||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||||
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||||
|
// not const in order to avoid copy by using std::swap
|
||||||
|
auto SetImageData(std::vector<u8>& data) -> ProgressBox&;
|
||||||
|
auto SetImageDataConst(std::span<const u8> data) -> ProgressBox&;
|
||||||
void RequestExit();
|
void RequestExit();
|
||||||
auto ShouldExit() -> bool;
|
auto ShouldExit() -> bool;
|
||||||
|
|
||||||
@@ -41,6 +47,9 @@ struct ProgressBox final : Widget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void FreeImage();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
struct ThreadData {
|
struct ThreadData {
|
||||||
ProgressBox* pbox{};
|
ProgressBox* pbox{};
|
||||||
@@ -52,12 +61,22 @@ private:
|
|||||||
Mutex m_mutex{};
|
Mutex m_mutex{};
|
||||||
Thread m_thread{};
|
Thread m_thread{};
|
||||||
ThreadData m_thread_data{};
|
ThreadData m_thread_data{};
|
||||||
|
|
||||||
ProgressBoxDoneCallback m_done{};
|
ProgressBoxDoneCallback m_done{};
|
||||||
|
|
||||||
|
// shared data start.
|
||||||
|
std::string m_action{};
|
||||||
std::string m_title{};
|
std::string m_title{};
|
||||||
std::string m_transfer{};
|
std::string m_transfer{};
|
||||||
s64 m_size{};
|
s64 m_size{};
|
||||||
s64 m_offset{};
|
s64 m_offset{};
|
||||||
|
s64 m_last_offset{};
|
||||||
|
s64 m_speed{};
|
||||||
|
TimeStamp m_timestamp{};
|
||||||
|
std::vector<u8> m_image_data{};
|
||||||
|
// shared data end.
|
||||||
|
|
||||||
|
int m_image{};
|
||||||
|
bool m_own_image{};
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is a helper function that does many things.
|
// this is a helper function that does many things.
|
||||||
|
|||||||
@@ -226,7 +226,6 @@ struct ThemeMeta {
|
|||||||
|
|
||||||
struct Theme {
|
struct Theme {
|
||||||
ThemeMeta meta;
|
ThemeMeta meta;
|
||||||
PLSR_BFSTM music;
|
|
||||||
ElementEntry elements[ThemeEntryID_MAX];
|
ElementEntry elements[ThemeEntryID_MAX];
|
||||||
|
|
||||||
auto GetColour(ThemeEntryID id) const {
|
auto GetColour(ThemeEntryID id) const {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <switch.h>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace sphaira {
|
|
||||||
|
|
||||||
// if show_error = true, it will display popup error box on
|
|
||||||
// faliure. set this to false if you want to handle errors
|
|
||||||
// from the caller.
|
|
||||||
auto WebShow(const std::string& url, bool show_error = true) -> Result;
|
|
||||||
|
|
||||||
} // namespace sphaira
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "yati/source/base.hpp"
|
#include "yati/source/base.hpp"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
namespace sphaira::yati::container {
|
namespace sphaira::yati::container {
|
||||||
@@ -28,12 +29,15 @@ using Collections = std::vector<CollectionEntry>;
|
|||||||
struct Base {
|
struct Base {
|
||||||
using Source = source::Base;
|
using Source = source::Base;
|
||||||
|
|
||||||
Base(Source* source) : m_source{source} { }
|
Base(std::shared_ptr<Source> source) : m_source{source} { }
|
||||||
virtual ~Base() = default;
|
virtual ~Base() = default;
|
||||||
virtual Result GetCollections(Collections& out) = 0;
|
virtual Result GetCollections(Collections& out) = 0;
|
||||||
|
auto GetSource() const {
|
||||||
|
return m_source;
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Source* m_source;
|
std::shared_ptr<Source> m_source;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::yati::container
|
} // namespace sphaira::yati::container
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ namespace sphaira::yati::container {
|
|||||||
struct Nsp final : Base {
|
struct Nsp final : Base {
|
||||||
using Base::Base;
|
using Base::Base;
|
||||||
Result GetCollections(Collections& out) override;
|
Result GetCollections(Collections& out) override;
|
||||||
static Result Validate(source::Base* source);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::yati::container
|
} // namespace sphaira::yati::container
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ namespace sphaira::yati::container {
|
|||||||
struct Xci final : Base {
|
struct Xci final : Base {
|
||||||
using Base::Base;
|
using Base::Base;
|
||||||
Result GetCollections(Collections& out) override;
|
Result GetCollections(Collections& out) override;
|
||||||
static Result Validate(source::Base* source);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::yati::container
|
} // namespace sphaira::yati::container
|
||||||
|
|||||||
@@ -78,6 +78,6 @@ Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
|
|||||||
|
|
||||||
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
|
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
|
||||||
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
||||||
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised);
|
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys);
|
||||||
|
|
||||||
} // namespace sphaira::es
|
} // namespace sphaira::es
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ struct Header {
|
|||||||
u8 old_key_gen; // see KeyGenerationOld.
|
u8 old_key_gen; // see KeyGenerationOld.
|
||||||
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
|
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
|
||||||
u64 size;
|
u64 size;
|
||||||
u64 title_id;
|
u64 program_id;
|
||||||
u32 context_id;
|
u32 context_id;
|
||||||
u32 sdk_version;
|
u32 sdk_version;
|
||||||
u8 key_gen; // see KeyGeneration.
|
u8 key_gen; // see KeyGeneration.
|
||||||
@@ -215,4 +215,6 @@ Result DecryptKeak(const keys::Keys& keys, Header& header);
|
|||||||
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
|
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
|
||||||
Result VerifyFixedKey(const Header& header);
|
Result VerifyFixedKey(const Header& header);
|
||||||
|
|
||||||
|
auto GetKeyGenStr(u8 key_gen) -> const char*;
|
||||||
|
|
||||||
} // namespace sphaira::nca
|
} // namespace sphaira::nca
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ static_assert(sizeof(PackagedContentMeta) == 0x20);
|
|||||||
|
|
||||||
struct ContentStorageRecord {
|
struct ContentStorageRecord {
|
||||||
NcmContentMetaKey key;
|
NcmContentMetaKey key;
|
||||||
u8 storage_id;
|
u8 storage_id; // NcmStorageId
|
||||||
u8 padding[0x7];
|
u8 padding[0x7];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,7 +31,9 @@ union ExtendedHeader {
|
|||||||
NcmDataPatchMetaExtendedHeader data_patch;
|
NcmDataPatchMetaExtendedHeader data_patch;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto GetAppId(u8 meta_type, u64 id) -> u64;
|
||||||
auto GetAppId(const NcmContentMetaKey& key) -> u64;
|
auto GetAppId(const NcmContentMetaKey& key) -> u64;
|
||||||
|
auto GetAppId(const PackagedContentMeta& meta) -> u64;
|
||||||
|
|
||||||
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
|
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
|
||||||
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);
|
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);
|
||||||
|
|||||||
@@ -1,265 +0,0 @@
|
|||||||
/*
|
|
||||||
* tik.h
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
|
||||||
*
|
|
||||||
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
|
||||||
*
|
|
||||||
* nxdumptool is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* nxdumptool is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#ifndef __TIK_H__
|
|
||||||
#define __TIK_H__
|
|
||||||
|
|
||||||
#include "signature.h"
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define SIGNED_TIK_MIN_SIZE sizeof(TikSigHmac160) /* Assuming no ESV1/ESV2 records are available. */
|
|
||||||
#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */
|
|
||||||
|
|
||||||
#define TIK_FORMAT_VERSION 2
|
|
||||||
|
|
||||||
#define GENERATE_TIK_STRUCT(sigtype, tiksize) \
|
|
||||||
typedef struct { \
|
|
||||||
SignatureBlock##sigtype sig_block; \
|
|
||||||
TikCommonBlock tik_common_block; \
|
|
||||||
u8 es_section_record_data[]; \
|
|
||||||
} TikSig##sigtype; \
|
|
||||||
NXDT_ASSERT(TikSig##sigtype, tiksize);
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
TikTitleKeyType_Common = 0,
|
|
||||||
TikTitleKeyType_Personalized = 1,
|
|
||||||
TikTitleKeyType_Count = 2 ///< Total values supported by this enum.
|
|
||||||
} TikTitleKeyType;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
TikLicenseType_Permanent = 0,
|
|
||||||
TikLicenseType_Demo = 1,
|
|
||||||
TikLicenseType_Trial = 2,
|
|
||||||
TikLicenseType_Rental = 3,
|
|
||||||
TikLicenseType_Subscription = 4,
|
|
||||||
TikLicenseType_Service = 5,
|
|
||||||
TikLicenseType_Count = 6 ///< Total values supported by this enum.
|
|
||||||
} TikLicenseType;
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
TikPropertyMask_None = 0,
|
|
||||||
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
|
|
||||||
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
|
|
||||||
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
|
|
||||||
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
|
|
||||||
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
|
|
||||||
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
|
|
||||||
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
|
|
||||||
} TikPropertyMask;
|
|
||||||
|
|
||||||
/// Placed after the ticket signature block.
|
|
||||||
typedef struct {
|
|
||||||
char issuer[0x40];
|
|
||||||
u8 titlekey_block[0x100];
|
|
||||||
u8 format_version; ///< Always matches TIK_FORMAT_VERSION.
|
|
||||||
u8 titlekey_type; ///< TikTitleKeyType.
|
|
||||||
u16 ticket_version;
|
|
||||||
u8 license_type; ///< TikLicenseType.
|
|
||||||
u8 key_generation; ///< NcaKeyGeneration.
|
|
||||||
u16 property_mask; ///< TikPropertyMask.
|
|
||||||
u8 reserved[0x8];
|
|
||||||
u64 ticket_id;
|
|
||||||
u64 device_id;
|
|
||||||
FsRightsId rights_id;
|
|
||||||
u32 account_id;
|
|
||||||
u32 sect_total_size;
|
|
||||||
u32 sect_hdr_offset;
|
|
||||||
u16 sect_hdr_count;
|
|
||||||
u16 sect_hdr_entry_size;
|
|
||||||
} TikCommonBlock;
|
|
||||||
|
|
||||||
NXDT_ASSERT(TikCommonBlock, 0x180);
|
|
||||||
|
|
||||||
/// ESV1/ESV2 section records are placed right after the ticket data. These aren't available in TikTitleKeyType_Common tickets.
|
|
||||||
/// These are only used if the sect_* fields from the common block are non-zero (other than 'sect_hdr_offset').
|
|
||||||
/// Each ESV2 section record is followed by a 'record_count' number of ESV1 records, each one of 'record_size' size.
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
TikSectionType_None = 0,
|
|
||||||
TikSectionType_Permanent = 1,
|
|
||||||
TikSectionType_Subscription = 2,
|
|
||||||
TikSectionType_Content = 3,
|
|
||||||
TikSectionType_ContentConsumption = 4,
|
|
||||||
TikSectionType_AccessTitle = 5,
|
|
||||||
TikSectionType_LimitedResource = 6,
|
|
||||||
TikSectionType_Count = 7 ///< Total values supported by this enum.
|
|
||||||
} TikSectionType;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
u32 sect_offset;
|
|
||||||
u32 record_size;
|
|
||||||
u32 section_size;
|
|
||||||
u16 record_count;
|
|
||||||
u16 section_type; ///< TikSectionType.
|
|
||||||
} TikESV2SectionRecord;
|
|
||||||
|
|
||||||
/// Used with TikSectionType_Permanent.
|
|
||||||
typedef struct {
|
|
||||||
u8 ref_id[0x10];
|
|
||||||
u32 ref_id_attr;
|
|
||||||
} TikESV1PermanentRecord;
|
|
||||||
|
|
||||||
/// Used with TikSectionType_Subscription.
|
|
||||||
typedef struct {
|
|
||||||
u32 limit;
|
|
||||||
u8 ref_id[0x10];
|
|
||||||
u32 ref_id_attr;
|
|
||||||
} TikESV1SubscriptionRecord;
|
|
||||||
|
|
||||||
/// Used with TikSectionType_Content.
|
|
||||||
typedef struct {
|
|
||||||
u32 offset;
|
|
||||||
u8 access_mask[0x80];
|
|
||||||
} TikESV1ContentRecord;
|
|
||||||
|
|
||||||
/// Used with TikSectionType_ContentConsumption.
|
|
||||||
typedef struct {
|
|
||||||
u16 index;
|
|
||||||
u16 code;
|
|
||||||
u32 limit;
|
|
||||||
} TikESV1ContentConsumptionRecord;
|
|
||||||
|
|
||||||
/// Used with TikSectionType_AccessTitle.
|
|
||||||
typedef struct {
|
|
||||||
u64 access_title_id;
|
|
||||||
u64 access_title_mask;
|
|
||||||
} TikESV1AccessTitleRecord;
|
|
||||||
|
|
||||||
/// Used with TikSectionType_LimitedResource.
|
|
||||||
typedef struct {
|
|
||||||
u32 limit;
|
|
||||||
u8 ref_id[0x10];
|
|
||||||
u32 ref_id_attr;
|
|
||||||
} TikESV1LimitedResourceRecord;
|
|
||||||
|
|
||||||
/// All tickets generated below use a little endian sig_type field.
|
|
||||||
GENERATE_TIK_STRUCT(Rsa4096, 0x3C0); /// RSA-4096 signature.
|
|
||||||
GENERATE_TIK_STRUCT(Rsa2048, 0x2C0); /// RSA-2048 signature.
|
|
||||||
GENERATE_TIK_STRUCT(Ecc480, 0x200); /// ECC signature.
|
|
||||||
GENERATE_TIK_STRUCT(Hmac160, 0x1C0); /// HMAC signature.
|
|
||||||
|
|
||||||
/// Ticket type.
|
|
||||||
typedef enum {
|
|
||||||
TikType_None = 0,
|
|
||||||
TikType_SigRsa4096 = 1,
|
|
||||||
TikType_SigRsa2048 = 2,
|
|
||||||
TikType_SigEcc480 = 3,
|
|
||||||
TikType_SigHmac160 = 4,
|
|
||||||
TikType_Count = 5 ///< Total values supported by this enum.
|
|
||||||
} TikType;
|
|
||||||
|
|
||||||
/// Used to store ticket type, size and raw data, as well as titlekey data.
|
|
||||||
typedef struct {
|
|
||||||
u8 type; ///< TikType.
|
|
||||||
u64 size; ///< Raw ticket size.
|
|
||||||
u8 data[SIGNED_TIK_MAX_SIZE]; ///< Raw ticket data.
|
|
||||||
u8 key_generation; ///< NcaKeyGeneration.
|
|
||||||
u8 enc_titlekey[0x10]; ///< Titlekey with titlekek crypto (RSA-OAEP unwrapped if dealing with a TikTitleKeyType_Personalized ticket).
|
|
||||||
char enc_titlekey_str[0x21]; ///< Character string representation of enc_titlekey.
|
|
||||||
u8 dec_titlekey[0x10]; ///< Titlekey without titlekek crypto. Ready to use for NCA FS section decryption.
|
|
||||||
char dec_titlekey_str[0x21]; ///< Character string representation of dec_titlekey.
|
|
||||||
char rights_id_str[0x21]; ///< Character string representation of the rights ID from the ticket.
|
|
||||||
} Ticket;
|
|
||||||
|
|
||||||
/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure Hash FS partition from an inserted gamecard.
|
|
||||||
/// Both the input rights ID and key generation values must have been retrieved from a NCA that depends on the desired ticket.
|
|
||||||
/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek-decrypted right away.
|
|
||||||
bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard);
|
|
||||||
|
|
||||||
/// Converts a TikTitleKeyType_Personalized ticket into a TikTitleKeyType_Common ticket and optionally generates a raw certificate chain for the new signature issuer.
|
|
||||||
/// Bear in mind the 'size' member from the Ticket parameter will be updated by this function to remove any possible references to ESV1/ESV2 records.
|
|
||||||
/// If both 'out_raw_cert_chain' and 'out_raw_cert_chain_size' pointers are provided, raw certificate chain data will be saved to them.
|
|
||||||
/// certGenerateRawCertificateChainBySignatureIssuer() is used internally, so the output buffer must be freed by the user.
|
|
||||||
bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size);
|
|
||||||
|
|
||||||
/// Helper inline functions for signed ticket blobs.
|
|
||||||
|
|
||||||
NX_INLINE TikCommonBlock *tikGetCommonBlockFromSignedTicketBlob(void *buf)
|
|
||||||
{
|
|
||||||
return (TikCommonBlock*)signatureGetPayloadFromSignedBlob(buf, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
NX_INLINE u64 tikGetSectionRecordsSizeFromSignedTicketBlob(void *buf)
|
|
||||||
{
|
|
||||||
TikCommonBlock *tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf);
|
|
||||||
if (!tik_common_block) return 0;
|
|
||||||
|
|
||||||
u64 offset = sizeof(TikCommonBlock), out_size = 0;
|
|
||||||
|
|
||||||
for(u32 i = 0; i < tik_common_block->sect_hdr_count; i++)
|
|
||||||
{
|
|
||||||
TikESV2SectionRecord *rec = (TikESV2SectionRecord*)((u8*)tik_common_block + offset);
|
|
||||||
offset += (sizeof(TikESV2SectionRecord) + ((u64)rec->record_count * (u64)rec->record_size));
|
|
||||||
out_size += offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
return out_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
NX_INLINE bool tikIsValidSignedTicketBlob(void *buf)
|
|
||||||
{
|
|
||||||
u64 ticket_size = (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock));
|
|
||||||
return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) <= SIGNED_TIK_MAX_SIZE);
|
|
||||||
}
|
|
||||||
|
|
||||||
NX_INLINE u64 tikGetSignedTicketBlobSize(void *buf)
|
|
||||||
{
|
|
||||||
return (tikIsValidSignedTicketBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
NX_INLINE u64 tikGetSignedTicketBlobHashAreaSize(void *buf)
|
|
||||||
{
|
|
||||||
return (tikIsValidSignedTicketBlob(buf) ? (sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper inline functions for Ticket elements.
|
|
||||||
|
|
||||||
NX_INLINE bool tikIsValidTicket(Ticket *tik)
|
|
||||||
{
|
|
||||||
return (tik && tik->type > TikType_None && tik->type < TikType_Count && tik->size >= SIGNED_TIK_MIN_SIZE && tik->size <= SIGNED_TIK_MAX_SIZE && tikIsValidSignedTicketBlob(tik->data));
|
|
||||||
}
|
|
||||||
|
|
||||||
NX_INLINE TikCommonBlock *tikGetCommonBlockFromTicket(Ticket *tik)
|
|
||||||
{
|
|
||||||
return (tikIsValidTicket(tik) ? tikGetCommonBlockFromSignedTicketBlob(tik->data) : NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
NX_INLINE bool tikIsPersonalizedTicket(Ticket *tik)
|
|
||||||
{
|
|
||||||
TikCommonBlock *tik_common_block = tikGetCommonBlockFromTicket(tik);
|
|
||||||
return (tik_common_block ? (tik_common_block->titlekey_type == TikTitleKeyType_Personalized) : false);
|
|
||||||
}
|
|
||||||
|
|
||||||
NX_INLINE u64 tikGetHashAreaSizeFromTicket(Ticket *tik)
|
|
||||||
{
|
|
||||||
return (tikIsValidTicket(tik) ? tikGetSignedTicketBlobHashAreaSize(tik->data) : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* __TIK_H__ */
|
|
||||||
@@ -10,6 +10,10 @@ struct Base {
|
|||||||
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||||
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||||
|
|
||||||
|
virtual bool IsStream() const {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Result GetOpenResult() const {
|
Result GetOpenResult() const {
|
||||||
return m_open_result;
|
return m_open_result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
namespace sphaira::yati::source {
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
struct Stdio final : Base {
|
struct Stdio final : Base {
|
||||||
Stdio(const char* path);
|
Stdio(const fs::FsPath& path);
|
||||||
~Stdio();
|
~Stdio();
|
||||||
|
|
||||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||||
|
|||||||
28
sphaira/include/yati/source/stream.hpp
Normal file
28
sphaira/include/yati/source/stream.hpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
// streams are for data that do not allow for random access,
|
||||||
|
// such as FTP or MTP.
|
||||||
|
struct Stream : Base {
|
||||||
|
virtual ~Stream() = default;
|
||||||
|
virtual Result ReadChunk(void* buf, s64 size, u64* bytes_read) = 0;
|
||||||
|
|
||||||
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||||
|
|
||||||
|
bool IsStream() const override {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Result m_open_result{};
|
||||||
|
|
||||||
|
private:
|
||||||
|
s64 m_offset{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
22
sphaira/include/yati/source/stream_file.hpp
Normal file
22
sphaira/include/yati/source/stream_file.hpp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// this is used for testing that streams work, this code isn't used in normal
|
||||||
|
// release builds as it is slower / less feature complete than normal.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "stream.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
struct StreamFile final : Stream {
|
||||||
|
StreamFile(FsFileSystem* fs, const fs::FsPath& path);
|
||||||
|
~StreamFile();
|
||||||
|
|
||||||
|
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FsFile m_file{};
|
||||||
|
s64 m_offset{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
#include "base.hpp"
|
#include "base.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <new>
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
namespace sphaira::yati::source {
|
namespace sphaira::yati::source {
|
||||||
@@ -21,10 +25,39 @@ struct Usb final : Base {
|
|||||||
~Usb();
|
~Usb();
|
||||||
|
|
||||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||||
Result Finished() const;
|
Result Finished();
|
||||||
|
|
||||||
Result Init();
|
Result Init();
|
||||||
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
|
Result IsUsbConnected(u64 timeout) const;
|
||||||
|
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
|
||||||
|
void SetFileNameForTranfser(const std::string& name);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// custom allocator for std::vector that respects alignment.
|
||||||
|
// https://en.cppreference.com/w/cpp/named_req/Allocator
|
||||||
|
template <typename T, std::size_t Align>
|
||||||
|
struct CustomVectorAllocator {
|
||||||
|
public:
|
||||||
|
// https://en.cppreference.com/w/cpp/memory/new/operator_new
|
||||||
|
auto allocate(std::size_t n) -> T* {
|
||||||
|
return new(align) T[n];
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://en.cppreference.com/w/cpp/memory/new/operator_delete
|
||||||
|
auto deallocate(T* p, std::size_t n) noexcept -> void {
|
||||||
|
::operator delete[] (p, n, align);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr inline std::align_val_t align{Align};
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct PageAllocator : CustomVectorAllocator<T, 0x1000> {
|
||||||
|
using value_type = T; // used by std::vector
|
||||||
|
};
|
||||||
|
|
||||||
|
using PageAlignedVector = std::vector<u8, PageAllocator<u8>>;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum UsbSessionEndpoint {
|
enum UsbSessionEndpoint {
|
||||||
@@ -32,20 +65,24 @@ private:
|
|||||||
UsbSessionEndpoint_Out = 1,
|
UsbSessionEndpoint_Out = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
Result SendCommand(s64 off, s64 size) const;
|
Result SendCmdHeader(u32 cmdId, size_t dataSize);
|
||||||
Result InternalRead(void* buf, s64 off, s64 size) const;
|
Result SendFileRangeCmd(u64 offset, u64 size);
|
||||||
|
|
||||||
bool GetConfigured() const;
|
|
||||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
||||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
|
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
|
||||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
|
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
|
||||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
|
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
|
||||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const;
|
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const;
|
||||||
|
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
UsbDsInterface* m_interface{};
|
UsbDsInterface* m_interface{};
|
||||||
UsbDsEndpoint* m_endpoints[2]{};
|
UsbDsEndpoint* m_endpoints[2]{};
|
||||||
u64 m_transfer_timeout{};
|
u64 m_transfer_timeout{};
|
||||||
|
// aligned buffer that transfer data is copied to and from.
|
||||||
|
// a vector is used to avoid multiple alloc within the transfer loop.
|
||||||
|
PageAlignedVector m_aligned{};
|
||||||
|
std::string m_transfer_file_name{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::yati::source
|
} // namespace sphaira::yati::source
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
|
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include "source/base.hpp"
|
#include "source/base.hpp"
|
||||||
|
#include "container/base.hpp"
|
||||||
|
#include "nx/ncm.hpp"
|
||||||
#include "ui/progress_box.hpp"
|
#include "ui/progress_box.hpp"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
namespace sphaira::yati {
|
namespace sphaira::yati {
|
||||||
|
|
||||||
@@ -68,6 +71,10 @@ enum : Result {
|
|||||||
struct Config {
|
struct Config {
|
||||||
bool sd_card_install{};
|
bool sd_card_install{};
|
||||||
|
|
||||||
|
// sets the performance mode to FastLoad which boosts the CPU clock
|
||||||
|
// and lowers the GPU clock.
|
||||||
|
bool boost_mode{};
|
||||||
|
|
||||||
// enables downgrading patch / data patch (dlc) version.
|
// enables downgrading patch / data patch (dlc) version.
|
||||||
bool allow_downgrade{};
|
bool allow_downgrade{};
|
||||||
|
|
||||||
@@ -78,11 +85,6 @@ struct Config {
|
|||||||
// installs tickets only.
|
// installs tickets only.
|
||||||
bool ticket_only{};
|
bool ticket_only{};
|
||||||
|
|
||||||
// converts personalised tickets to common tickets, allows for offline play.
|
|
||||||
// this breaks ticket signature so es needs to be patched.
|
|
||||||
// modified common tickets are patched regardless of this setting.
|
|
||||||
bool patch_ticket{};
|
|
||||||
|
|
||||||
// flags to enable / disable install of specific types.
|
// flags to enable / disable install of specific types.
|
||||||
bool skip_base{};
|
bool skip_base{};
|
||||||
bool skip_patch{};
|
bool skip_patch{};
|
||||||
@@ -116,12 +118,25 @@ struct Config {
|
|||||||
bool lower_system_version{};
|
bool lower_system_version{};
|
||||||
};
|
};
|
||||||
|
|
||||||
Result InstallFromFile(FsFileSystem* fs, const fs::FsPath& path);
|
// overridable options, set to avoid
|
||||||
Result InstallFromStdioFile(const char* path);
|
struct ConfigOverride {
|
||||||
Result InstallFromSource(std::shared_ptr<source::Base> source);
|
std::optional<bool> sd_card_install{};
|
||||||
|
std::optional<bool> skip_nca_hash_verify{};
|
||||||
|
std::optional<bool> skip_rsa_header_fixed_key_verify{};
|
||||||
|
std::optional<bool> skip_rsa_npdm_fixed_key_verify{};
|
||||||
|
std::optional<bool> ignore_distribution_bit{};
|
||||||
|
std::optional<bool> convert_to_standard_crypto{};
|
||||||
|
std::optional<bool> lower_master_key{};
|
||||||
|
std::optional<bool> lower_system_version{};
|
||||||
|
};
|
||||||
|
|
||||||
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path);
|
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path, const ConfigOverride& override = {});
|
||||||
Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path);
|
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override = {});
|
||||||
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source);
|
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const fs::FsPath& path, const ConfigOverride& override = {});
|
||||||
|
Result InstallFromContainer(ui::ProgressBox* pbox, std::shared_ptr<container::Base> container, const ConfigOverride& override = {});
|
||||||
|
Result InstallFromCollections(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source, const container::Collections& collections, const ConfigOverride& override = {});
|
||||||
|
|
||||||
|
Result ParseCnmtNca(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
|
||||||
|
Result ParseControlNca(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
|
||||||
|
|
||||||
} // namespace sphaira::yati
|
} // namespace sphaira::yati
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
#include "ui/menus/main_menu.hpp"
|
|
||||||
#include "ui/error_box.hpp"
|
|
||||||
#include "ui/option_box.hpp"
|
#include "ui/option_box.hpp"
|
||||||
#include "ui/bubbles.hpp"
|
#include "ui/bubbles.hpp"
|
||||||
|
#include "ui/sidebar.hpp"
|
||||||
|
#include "ui/popup_list.hpp"
|
||||||
|
#include "ui/option_box.hpp"
|
||||||
|
#include "ui/progress_box.hpp"
|
||||||
|
#include "ui/error_box.hpp"
|
||||||
|
|
||||||
|
#include "ui/menus/main_menu.hpp"
|
||||||
|
#include "ui/menus/themezer.hpp"
|
||||||
|
#include "ui/menus/ghdl.hpp"
|
||||||
|
#include "ui/menus/usb_menu.hpp"
|
||||||
|
#include "ui/menus/ftp_menu.hpp"
|
||||||
|
#include "ui/menus/gc_menu.hpp"
|
||||||
|
|
||||||
#include "app.hpp"
|
#include "app.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
@@ -34,8 +44,33 @@ extern "C" {
|
|||||||
namespace sphaira {
|
namespace sphaira {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr fs::FsPath DEFAULT_MUSIC_PATH = "/config/sphaira/themes/default_music.bfstm";
|
||||||
|
constexpr const char* DEFAULT_MUSIC_URL = "https://files.catbox.moe/1ovji1.bfstm";
|
||||||
|
// constexpr const char* DEFAULT_MUSIC_URL = "https://raw.githubusercontent.com/ITotalJustice/sphaira/refs/heads/master/assets/default_music.bfstm";
|
||||||
|
|
||||||
|
void download_default_music() {
|
||||||
|
App::Push(std::make_shared<ui::ProgressBox>(0, "Downloading "_i18n, "default_music.bfstm", [](auto pbox){
|
||||||
|
const auto result = curl::Api().ToFile(
|
||||||
|
curl::Url{DEFAULT_MUSIC_URL},
|
||||||
|
curl::Path{DEFAULT_MUSIC_PATH},
|
||||||
|
curl::OnProgress{pbox->OnDownloadProgressCallback()}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.success;
|
||||||
|
}, [](bool success){
|
||||||
|
if (success) {
|
||||||
|
App::Notify("Downloaded "_i18n + "default_music.bfstm");
|
||||||
|
App::SetTheme(App::GetThemeIndex());
|
||||||
|
} else {
|
||||||
|
App::Push(std::make_shared<ui::ErrorBox>(
|
||||||
|
"Failed to download default_music.bfstm, please try again"_i18n
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
struct ThemeData {
|
struct ThemeData {
|
||||||
fs::FsPath music_path{"/config/sphaira/themes/default_music.bfstm"};
|
fs::FsPath music_path{DEFAULT_MUSIC_PATH};
|
||||||
std::string elements[ThemeEntryID_MAX]{};
|
std::string elements[ThemeEntryID_MAX]{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -544,7 +579,7 @@ auto App::GetThemeIndex() -> s64 {
|
|||||||
return g_app->m_theme_index;
|
return g_app->m_theme_index;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto App::GetDefaultImage(int* w, int* h) -> int {
|
auto App::GetDefaultImage() -> int {
|
||||||
return g_app->m_default_image;
|
return g_app->m_default_image;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1104,7 +1139,6 @@ void App::CloseTheme() {
|
|||||||
if (m_sound_ids[SoundEffect_Music]) {
|
if (m_sound_ids[SoundEffect_Music]) {
|
||||||
plsrPlayerFree(m_sound_ids[SoundEffect_Music]);
|
plsrPlayerFree(m_sound_ids[SoundEffect_Music]);
|
||||||
m_sound_ids[SoundEffect_Music] = nullptr;
|
m_sound_ids[SoundEffect_Music] = nullptr;
|
||||||
plsrBFSTMClose(&m_theme.music);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto& e : m_theme.elements) {
|
for (auto& e : m_theme.elements) {
|
||||||
@@ -1134,10 +1168,12 @@ void App::LoadTheme(const ThemeMeta& meta) {
|
|||||||
|
|
||||||
// load music
|
// load music
|
||||||
if (!theme_data.music_path.empty()) {
|
if (!theme_data.music_path.empty()) {
|
||||||
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &m_theme.music))) {
|
PLSR_BFSTM music_stream;
|
||||||
if (R_SUCCEEDED(plsrPlayerLoadStream(&m_theme.music, &m_sound_ids[SoundEffect_Music]))) {
|
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &music_stream))) {
|
||||||
|
if (R_SUCCEEDED(plsrPlayerLoadStream(&music_stream, &m_sound_ids[SoundEffect_Music]))) {
|
||||||
PlaySoundEffect(SoundEffect_Music);
|
PlaySoundEffect(SoundEffect_Music);
|
||||||
}
|
}
|
||||||
|
plsrBFSTMClose(&music_stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1300,16 +1336,21 @@ App::App(const char* argv0) {
|
|||||||
if (R_SUCCEEDED(plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &qlaunch_bfsar))) {
|
if (R_SUCCEEDED(plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &qlaunch_bfsar))) {
|
||||||
ON_SCOPE_EXIT(plsrBFSARClose(&qlaunch_bfsar));
|
ON_SCOPE_EXIT(plsrBFSARClose(&qlaunch_bfsar));
|
||||||
|
|
||||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconFocus", &m_sound_ids[SoundEffect_Focus]);
|
const auto load_sound = [&](const char* name, u32 id) {
|
||||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconScroll", &m_sound_ids[SoundEffect_Scroll]);
|
if (R_FAILED(plsrPlayerLoadSoundByName(&qlaunch_bfsar, name, &m_sound_ids[id]))) {
|
||||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconLimit", &m_sound_ids[SoundEffect_Limit]);
|
log_write("[PLSR] failed to load sound effect: %s\n", name);
|
||||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeStartupMenu_game", &m_sound_ids[SoundEffect_Startup]);
|
}
|
||||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconAdd", &m_sound_ids[SoundEffect_Install]);
|
};
|
||||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeInsertError", &m_sound_ids[SoundEffect_Error]);
|
|
||||||
|
load_sound("SeGameIconFocus", SoundEffect_Focus);
|
||||||
|
load_sound("SeGameIconScroll", SoundEffect_Scroll);
|
||||||
|
load_sound("SeGameIconLimit", SoundEffect_Limit);
|
||||||
|
load_sound("StartupMenu_Game", SoundEffect_Startup);
|
||||||
|
load_sound("SeGameIconAdd", SoundEffect_Install);
|
||||||
|
load_sound("SeInsertError", SoundEffect_Error);
|
||||||
|
|
||||||
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Limit], 2.0f);
|
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Limit], 2.0f);
|
||||||
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Focus], 0.5f);
|
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Focus], 0.5f);
|
||||||
PlaySoundEffect(SoundEffect_Startup);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log_write("failed to mount romfs 0x0100000000001000\n");
|
log_write("failed to mount romfs 0x0100000000001000\n");
|
||||||
@@ -1374,15 +1415,6 @@ App::App(const char* argv0) {
|
|||||||
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
|
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
|
||||||
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
|
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
|
||||||
|
|
||||||
s64 sd_free_space;
|
|
||||||
if (R_SUCCEEDED(fs.GetFreeSpace("/", &sd_free_space))) {
|
|
||||||
log_write("sd_free_space: %zd\n", sd_free_space);
|
|
||||||
}
|
|
||||||
s64 sd_total_space;
|
|
||||||
if (R_SUCCEEDED(fs.GetTotalSpace("/", &sd_total_space))) {
|
|
||||||
log_write("sd_total_space: %zd\n", sd_total_space);
|
|
||||||
}
|
|
||||||
|
|
||||||
// load default image
|
// load default image
|
||||||
if (R_SUCCEEDED(romfsInit())) {
|
if (R_SUCCEEDED(romfsInit())) {
|
||||||
ON_SCOPE_EXIT(romfsExit());
|
ON_SCOPE_EXIT(romfsExit());
|
||||||
@@ -1392,8 +1424,33 @@ App::App(const char* argv0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// soon (tm)
|
struct EventDay {
|
||||||
// ui::bubble::Init();
|
u8 day;
|
||||||
|
u8 month;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr EventDay event_days[] = {
|
||||||
|
{ .day = 1, .month = 1 }, // New years
|
||||||
|
|
||||||
|
{ .day = 3, .month = 3 }, // March 3 (switch 1)
|
||||||
|
{ .day = 10, .month = 5 }, // June 10 (switch 2)
|
||||||
|
{ .day = 15, .month = 5 }, // June 15
|
||||||
|
|
||||||
|
{ .day = 25, .month = 12 }, // Christmas
|
||||||
|
{ .day = 26, .month = 12 },
|
||||||
|
{ .day = 27, .month = 12 },
|
||||||
|
{ .day = 28, .month = 12 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto time = std::time(nullptr);
|
||||||
|
const auto tm = std::localtime(&time);
|
||||||
|
|
||||||
|
for (auto e : event_days) {
|
||||||
|
if (e.day == tm->tm_mday && e.month == (tm->tm_mon + 1)) {
|
||||||
|
ui::bubble::Init();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
App::Push(std::make_shared<ui::menu::main::MainMenu>());
|
App::Push(std::make_shared<ui::menu::main::MainMenu>());
|
||||||
log_write("finished app constructor\n");
|
log_write("finished app constructor\n");
|
||||||
@@ -1412,6 +1469,190 @@ void App::PlaySoundEffect(SoundEffect effect) {
|
|||||||
plsrPlayerPlay(id);
|
plsrPlayerPlay(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::DisplayThemeOptions(bool left_side) {
|
||||||
|
ui::SidebarEntryArray::Items theme_items{};
|
||||||
|
const auto theme_meta = App::GetThemeMetaList();
|
||||||
|
for (auto& p : theme_meta) {
|
||||||
|
theme_items.emplace_back(p.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto options = std::make_shared<ui::Sidebar>("Theme Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryArray>("Select Theme"_i18n, theme_items, [](s64& index_out){
|
||||||
|
App::SetTheme(index_out);
|
||||||
|
}, App::GetThemeIndex()));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){
|
||||||
|
App::SetThemeMusicEnable(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
|
||||||
|
App::Set12HourTimeEnable(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("Download Default Music"_i18n, [](){
|
||||||
|
// check if we already have music
|
||||||
|
if (fs::FileExists(DEFAULT_MUSIC_PATH)) {
|
||||||
|
App::Push(std::make_shared<ui::OptionBox>(
|
||||||
|
"Overwrite current default music?"_i18n,
|
||||||
|
"No"_i18n, "Yes"_i18n, 0, [](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
download_default_music();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
download_default_music();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::DisplayNetworkOptions(bool left_side) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::DisplayMiscOptions(bool left_side) {
|
||||||
|
auto options = std::make_shared<ui::Sidebar>("Misc Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("Themezer"_i18n, [](){
|
||||||
|
App::Push(std::make_shared<ui::menu::themezer::Menu>());
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("GitHub"_i18n, [](){
|
||||||
|
App::Push(std::make_shared<ui::menu::gh::Menu>());
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (App::GetApp()->m_install.Get()) {
|
||||||
|
if (App::GetFtpEnable()) {
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("Ftp Install"_i18n, [](){
|
||||||
|
App::Push(std::make_shared<ui::menu::ftp::Menu>());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("Usb Install"_i18n, [](){
|
||||||
|
App::Push(std::make_shared<ui::menu::usb::Menu>());
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("GameCard Install"_i18n, [](){
|
||||||
|
App::Push(std::make_shared<ui::menu::gc::Menu>());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::DisplayAdvancedOptions(bool left_side) {
|
||||||
|
auto options = std::make_shared<ui::Sidebar>("Advanced Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
ui::SidebarEntryArray::Items text_scroll_speed_items;
|
||||||
|
text_scroll_speed_items.push_back("Slow"_i18n);
|
||||||
|
text_scroll_speed_items.push_back("Normal"_i18n);
|
||||||
|
text_scroll_speed_items.push_back("Fast"_i18n);
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
|
||||||
|
App::SetLogEnable(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
|
||||||
|
App::SetReplaceHbmenuEnable(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
|
||||||
|
App::SetTextScrollSpeed(index_out);
|
||||||
|
}, (s64)App::GetTextScrollSpeed()));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>("Install options"_i18n, [left_side](){
|
||||||
|
App::DisplayInstallOptions(left_side);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::DisplayInstallOptions(bool left_side) {
|
||||||
|
auto options = std::make_shared<ui::Sidebar>("Install Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
ui::SidebarEntryArray::Items install_items;
|
||||||
|
install_items.push_back("System memory"_i18n);
|
||||||
|
install_items.push_back("microSD card"_i18n);
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable"_i18n, App::GetApp()->m_install.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_install.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Show install warning"_i18n, App::GetApp()->m_install_prompt.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_install_prompt.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryArray>("Install location"_i18n, install_items, [](s64& index_out){
|
||||||
|
App::SetInstallSdEnable(index_out);
|
||||||
|
}, (s64)App::GetInstallSdEnable()));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Boost CPU clock"_i18n, App::GetApp()->m_boost_mode.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_boost_mode.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_allow_downgrade.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_if_already_installed.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_ticket_only.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_base.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip patch"_i18n, App::GetApp()->m_skip_patch.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_patch.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip dlc"_i18n, App::GetApp()->m_skip_addon.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_addon.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_data_patch.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_ticket.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_ignore_distribution_bit.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_lower_master_key.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [](bool& enable){
|
||||||
|
App::GetApp()->m_lower_system_version.Set(enable);
|
||||||
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
}
|
||||||
|
|
||||||
App::~App() {
|
App::~App() {
|
||||||
log_write("starting to exit\n");
|
log_write("starting to exit\n");
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,30 @@
|
|||||||
#include <nx/vfs_nx.h>
|
#include <nx/vfs_nx.h>
|
||||||
#include <nx/utils.h>
|
#include <nx/utils.h>
|
||||||
|
|
||||||
|
namespace sphaira::ftpsrv {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
struct InstallSharedData {
|
||||||
|
std::mutex mutex;
|
||||||
|
|
||||||
|
std::deque<std::string> queued_files;
|
||||||
|
|
||||||
|
void* user;
|
||||||
|
OnInstallStart on_start;
|
||||||
|
OnInstallWrite on_write;
|
||||||
|
OnInstallClose on_close;
|
||||||
|
|
||||||
|
bool in_progress;
|
||||||
|
bool enabled;
|
||||||
|
};
|
||||||
|
|
||||||
const char* INI_PATH = "/config/ftpsrv/config.ini";
|
const char* INI_PATH = "/config/ftpsrv/config.ini";
|
||||||
FtpSrvConfig g_ftpsrv_config = {0};
|
FtpSrvConfig g_ftpsrv_config = {0};
|
||||||
volatile bool g_should_exit = false;
|
volatile bool g_should_exit = false;
|
||||||
bool g_is_running{false};
|
bool g_is_running{false};
|
||||||
Thread g_thread;
|
Thread g_thread;
|
||||||
std::mutex g_mutex{};
|
std::mutex g_mutex{};
|
||||||
|
InstallSharedData g_shared_data{};
|
||||||
|
|
||||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||||
sphaira::App::NotifyFlashLed();
|
sphaira::App::NotifyFlashLed();
|
||||||
@@ -29,6 +45,235 @@ void ftp_progress_callback(void) {
|
|||||||
sphaira::App::NotifyFlashLed();
|
sphaira::App::NotifyFlashLed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* SUPPORTED_EXT[] = {
|
||||||
|
".nsp", ".xci", ".nsz", ".xcz",
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VfsUserData {
|
||||||
|
char* path;
|
||||||
|
int valid;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ive given up with good names.
|
||||||
|
void on_thing() {
|
||||||
|
log_write("[FTP] doing on_thing\n");
|
||||||
|
std::scoped_lock lock{g_shared_data.mutex};
|
||||||
|
log_write("[FTP] locked on_thing\n");
|
||||||
|
|
||||||
|
if (!g_shared_data.in_progress) {
|
||||||
|
if (!g_shared_data.queued_files.empty()) {
|
||||||
|
log_write("[FTP] pushing new file data\n");
|
||||||
|
if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) {
|
||||||
|
g_shared_data.queued_files.clear();
|
||||||
|
} else {
|
||||||
|
log_write("[FTP] success on new file push\n");
|
||||||
|
g_shared_data.in_progress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_open(void* user, const char* path, enum FtpVfsOpenMode mode) {
|
||||||
|
{
|
||||||
|
std::scoped_lock lock{g_shared_data.mutex};
|
||||||
|
auto data = static_cast<VfsUserData*>(user);
|
||||||
|
data->valid = 0;
|
||||||
|
|
||||||
|
if (mode != FtpVfsOpenMode_WRITE) {
|
||||||
|
errno = EACCES;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_shared_data.enabled) {
|
||||||
|
errno = EACCES;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ext = strrchr(path, '.');
|
||||||
|
if (!ext) {
|
||||||
|
errno = EACCES;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) {
|
||||||
|
if (!strcasecmp(ext, SUPPORTED_EXT[i])) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we already have this file queued.
|
||||||
|
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), path);
|
||||||
|
if (it != g_shared_data.queued_files.cend()) {
|
||||||
|
errno = EEXIST;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_shared_data.queued_files.push_back(path);
|
||||||
|
data->path = strdup(path);
|
||||||
|
data->valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
on_thing();
|
||||||
|
log_write("[FTP] got file: %s\n", path);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_read(void* user, void* buf, size_t size) {
|
||||||
|
errno = EACCES;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_write(void* user, const void* buf, size_t size) {
|
||||||
|
std::scoped_lock lock{g_shared_data.mutex};
|
||||||
|
if (!g_shared_data.enabled) {
|
||||||
|
errno = EACCES;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = static_cast<VfsUserData*>(user);
|
||||||
|
if (!data->valid) {
|
||||||
|
errno = EACCES;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, size)) {
|
||||||
|
errno = EIO;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_seek(void* user, const void* buf, size_t size, size_t off) {
|
||||||
|
errno = ESPIPE;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_isfile_open(void* user) {
|
||||||
|
std::scoped_lock lock{g_shared_data.mutex};
|
||||||
|
auto data = static_cast<VfsUserData*>(user);
|
||||||
|
return data->valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_isfile_ready(void* user) {
|
||||||
|
std::scoped_lock lock{g_shared_data.mutex};
|
||||||
|
auto data = static_cast<VfsUserData*>(user);
|
||||||
|
const auto ready = !g_shared_data.queued_files.empty() && data->path == g_shared_data.queued_files[0];
|
||||||
|
return ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_close(void* user) {
|
||||||
|
{
|
||||||
|
log_write("[FTP] closing file\n");
|
||||||
|
std::scoped_lock lock{g_shared_data.mutex};
|
||||||
|
auto data = static_cast<VfsUserData*>(user);
|
||||||
|
if (data->valid) {
|
||||||
|
log_write("[FTP] closing valid file\n");
|
||||||
|
|
||||||
|
auto it = std::find(g_shared_data.queued_files.cbegin(), g_shared_data.queued_files.cend(), data->path);
|
||||||
|
if (it != g_shared_data.queued_files.cend()) {
|
||||||
|
if (it == g_shared_data.queued_files.cbegin()) {
|
||||||
|
log_write("[FTP] closing current file\n");
|
||||||
|
if (g_shared_data.on_close) {
|
||||||
|
g_shared_data.on_close(g_shared_data.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_shared_data.in_progress = false;
|
||||||
|
} else {
|
||||||
|
log_write("[FTP] closing other file...\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
g_shared_data.queued_files.erase(it);
|
||||||
|
} else {
|
||||||
|
log_write("[FTP] could not find file in queue...\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data->path) {
|
||||||
|
free(data->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
data->valid = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(data, 0, sizeof(*data));
|
||||||
|
}
|
||||||
|
|
||||||
|
on_thing();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_opendir(void* user, const char* path) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* vfs_install_readdir(void* user, void* user_entry) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_dirlstat(void* user, const void* user_entry, const char* path, struct stat* st) {
|
||||||
|
st->st_nlink = 1;
|
||||||
|
st->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_isdir_open(void* user) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_closedir(void* user) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_stat(const char* path, struct stat* st) {
|
||||||
|
st->st_nlink = 1;
|
||||||
|
st->st_mode = S_IFDIR | S_IWUSR | S_IWGRP | S_IWOTH;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_mkdir(const char* path) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_unlink(const char* path) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_rmdir(const char* path) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int vfs_install_rename(const char* src, const char* dst) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FtpVfs g_vfs_install = {
|
||||||
|
.open = vfs_install_open,
|
||||||
|
.read = vfs_install_read,
|
||||||
|
.write = vfs_install_write,
|
||||||
|
.seek = vfs_install_seek,
|
||||||
|
.close = vfs_install_close,
|
||||||
|
.isfile_open = vfs_install_isfile_open,
|
||||||
|
.isfile_ready = vfs_install_isfile_ready,
|
||||||
|
.opendir = vfs_install_opendir,
|
||||||
|
.readdir = vfs_install_readdir,
|
||||||
|
.dirlstat = vfs_install_dirlstat,
|
||||||
|
.closedir = vfs_install_closedir,
|
||||||
|
.isdir_open = vfs_install_isdir_open,
|
||||||
|
.stat = vfs_install_stat,
|
||||||
|
.lstat = vfs_install_stat,
|
||||||
|
.mkdir = vfs_install_mkdir,
|
||||||
|
.unlink = vfs_install_unlink,
|
||||||
|
.rmdir = vfs_install_rmdir,
|
||||||
|
.rename = vfs_install_rename,
|
||||||
|
};
|
||||||
|
|
||||||
void loop(void* arg) {
|
void loop(void* arg) {
|
||||||
while (!g_should_exit) {
|
while (!g_should_exit) {
|
||||||
ftpsrv_init(&g_ftpsrv_config);
|
ftpsrv_init(&g_ftpsrv_config);
|
||||||
@@ -44,8 +289,6 @@ void loop(void* arg) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace sphaira::ftpsrv {
|
|
||||||
|
|
||||||
bool Init() {
|
bool Init() {
|
||||||
std::scoped_lock lock{g_mutex};
|
std::scoped_lock lock{g_mutex};
|
||||||
if (g_is_running) {
|
if (g_is_running) {
|
||||||
@@ -84,6 +327,9 @@ bool Init() {
|
|||||||
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
|
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, INI_PATH);
|
||||||
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
|
save_writable = ini_getbool("Nx-App", "save_writable", save_writable, INI_PATH);
|
||||||
|
|
||||||
|
mount_devices = true;
|
||||||
|
g_ftpsrv_config.timeout = 0;
|
||||||
|
|
||||||
if (!g_ftpsrv_config.port) {
|
if (!g_ftpsrv_config.port) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -93,7 +339,13 @@ bool Init() {
|
|||||||
g_ftpsrv_config.anon = true;
|
g_ftpsrv_config.anon = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
vfs_nx_init(mount_devices, save_writable, mount_bis);
|
const VfsNxCustomPath custom = {
|
||||||
|
.name = "install",
|
||||||
|
.user = NULL,
|
||||||
|
.func = &g_vfs_install,
|
||||||
|
};
|
||||||
|
|
||||||
|
vfs_nx_init(&custom, mount_devices, save_writable, mount_bis);
|
||||||
|
|
||||||
Result rc;
|
Result rc;
|
||||||
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
|
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
|
||||||
@@ -123,6 +375,40 @@ void Exit() {
|
|||||||
fsdev_wrapUnmountAll();
|
fsdev_wrapUnmountAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) {
|
||||||
|
std::scoped_lock lock{g_shared_data.mutex};
|
||||||
|
g_shared_data.user = user;
|
||||||
|
g_shared_data.on_start = on_start;
|
||||||
|
g_shared_data.on_write = on_write;
|
||||||
|
g_shared_data.on_close = on_close;
|
||||||
|
g_shared_data.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisableInstallMode() {
|
||||||
|
std::scoped_lock lock{g_shared_data.mutex};
|
||||||
|
g_shared_data.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned GetPort() {
|
||||||
|
std::scoped_lock lock{g_mutex};
|
||||||
|
return g_ftpsrv_config.port;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsAnon() {
|
||||||
|
std::scoped_lock lock{g_mutex};
|
||||||
|
return g_ftpsrv_config.anon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* GetUser() {
|
||||||
|
std::scoped_lock lock{g_mutex};
|
||||||
|
return g_ftpsrv_config.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* GetPass() {
|
||||||
|
std::scoped_lock lock{g_mutex};
|
||||||
|
return g_ftpsrv_config.pass;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ftpsrv
|
} // namespace sphaira::ftpsrv
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ void userAppInit(void) {
|
|||||||
if (R_FAILED(rc = ncmInitialize()))
|
if (R_FAILED(rc = ncmInitialize()))
|
||||||
diagAbortWithResult(rc);
|
diagAbortWithResult(rc);
|
||||||
|
|
||||||
|
// it doesn't matter if this fails.
|
||||||
|
appletSetScreenShotPermission(AppletScreenShotPermission_Enable);
|
||||||
|
|
||||||
log_nxlink_init();
|
log_nxlink_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -715,7 +715,7 @@ void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Key
|
|||||||
nca_header.magic = NCA3_MAGIC;
|
nca_header.magic = NCA3_MAGIC;
|
||||||
nca_header.distribution_type = nca::DistributionType_System;
|
nca_header.distribution_type = nca::DistributionType_System;
|
||||||
nca_header.content_type = type;
|
nca_header.content_type = type;
|
||||||
nca_header.title_id = tid;
|
nca_header.program_id = tid;
|
||||||
nca_header.sdk_version = 0x000C1100;
|
nca_header.sdk_version = 0x000C1100;
|
||||||
nca_header.size = buf.tell();
|
nca_header.size = buf.tell();
|
||||||
|
|
||||||
@@ -1005,7 +1005,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
|
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
|
||||||
App::Push(std::make_shared<ui::ProgressBox>("Installing Forwarder"_i18n, [config, storage_id](auto pbox) mutable -> bool {
|
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing Forwarder"_i18n, config.name, [config, storage_id](auto pbox) mutable -> bool {
|
||||||
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
|
return R_SUCCEEDED(install_forwarder(pbox, config, storage_id));
|
||||||
}));
|
}));
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -34,8 +34,35 @@ auto List::ClampY(float y, s64 count) const -> float {
|
|||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) {
|
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
|
||||||
if (touch->is_clicked && touch->in_range(GetPos())) {
|
const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2;
|
||||||
|
const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2;
|
||||||
|
|
||||||
|
if (controller->GotDown(Button::DOWN)) {
|
||||||
|
if (ScrollDown(index, m_row, count)) {
|
||||||
|
callback(false, index);
|
||||||
|
}
|
||||||
|
} else if (controller->GotDown(Button::UP)) {
|
||||||
|
if (ScrollUp(index, m_row, count)) {
|
||||||
|
callback(false, index);
|
||||||
|
}
|
||||||
|
} else if (controller->GotDown(page_down_button)) {
|
||||||
|
if (ScrollDown(index, m_page, count)) {
|
||||||
|
callback(false, index);
|
||||||
|
}
|
||||||
|
} else if (controller->GotDown(page_up_button)) {
|
||||||
|
if (ScrollUp(index, m_page, count)) {
|
||||||
|
callback(false, index);
|
||||||
|
}
|
||||||
|
} else if (m_row > 1 && controller->GotDown(Button::RIGHT)) {
|
||||||
|
if (count && index < (count - 1) && (index + 1) % m_row != 0) {
|
||||||
|
callback(false, index + 1);
|
||||||
|
}
|
||||||
|
} else if (m_row > 1 && controller->GotDown(Button::LEFT)) {
|
||||||
|
if (count && index != 0 && (index % m_row) != 0) {
|
||||||
|
callback(false, index - 1);
|
||||||
|
}
|
||||||
|
} else if (touch->is_clicked && touch->in_range(GetPos())) {
|
||||||
auto v = m_v;
|
auto v = m_v;
|
||||||
v.y -= ClampY(m_yoff + m_y_prog, count);
|
v.y -= ClampY(m_yoff + m_y_prog, count);
|
||||||
|
|
||||||
@@ -63,7 +90,7 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCa
|
|||||||
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
|
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
|
||||||
|
|
||||||
if (touch->in_range(vv)) {
|
if (touch->in_range(vv)) {
|
||||||
callback(i);
|
callback(true, i);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,18 +236,15 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
|
|||||||
bool crop = false;
|
bool crop = false;
|
||||||
if (iw < w || ih < h) {
|
if (iw < w || ih < h) {
|
||||||
rounded_image = false;
|
rounded_image = false;
|
||||||
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded);
|
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 15 : 0);
|
||||||
}
|
}
|
||||||
if (iw > w || ih > h) {
|
if (iw > w || ih > h) {
|
||||||
crop = true;
|
crop = true;
|
||||||
nvgSave(vg);
|
nvgSave(vg);
|
||||||
nvgIntersectScissor(vg, x, y, w, h);
|
nvgIntersectScissor(vg, x, y, w, h);
|
||||||
}
|
}
|
||||||
if (rounded_image) {
|
|
||||||
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
|
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 15 : 0);
|
||||||
} else {
|
|
||||||
gfx::drawImage(vg, ix, iy, iw, ih, i.image);
|
|
||||||
}
|
|
||||||
if (crop) {
|
if (crop) {
|
||||||
nvgRestore(vg);
|
nvgRestore(vg);
|
||||||
}
|
}
|
||||||
@@ -593,6 +590,14 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// case-insensitive version of str.find()
|
||||||
|
auto FindCaseInsensitive(std::string_view base, std::string_view term) -> bool {
|
||||||
|
const auto it = std::search(base.cbegin(), base.cend(), term.cbegin(), term.cend(), [](char a, char b){
|
||||||
|
return std::toupper(a) == std::toupper(b);
|
||||||
|
});
|
||||||
|
return it != base.cend();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
||||||
@@ -769,7 +774,7 @@ void EntryMenu::UpdateOptions() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const auto install = [this](){
|
const auto install = [this](){
|
||||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + m_entry.title, [this](auto pbox){
|
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Downloading "_i18n, m_entry.title, [this](auto pbox){
|
||||||
return InstallApp(pbox, m_entry);
|
return InstallApp(pbox, m_entry);
|
||||||
}, [this](bool success){
|
}, [this](bool success){
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -782,7 +787,7 @@ void EntryMenu::UpdateOptions() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const auto uninstall = [this](){
|
const auto uninstall = [this](){
|
||||||
App::Push(std::make_shared<ProgressBox>("Uninstalling "_i18n + m_entry.title, [this](auto pbox){
|
App::Push(std::make_shared<ProgressBox>(m_entry.image.image, "Uninstalling "_i18n, m_entry.title, [this](auto pbox){
|
||||||
return UninstallApp(pbox, m_entry);
|
return UninstallApp(pbox, m_entry);
|
||||||
}, [this](bool success){
|
}, [this](bool success){
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -841,12 +846,6 @@ void EntryMenu::SetIndex(s64 index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto toLower(const std::string& str) -> std::string {
|
|
||||||
std::string lower;
|
|
||||||
std::transform(str.cbegin(), str.cend(), std::back_inserter(lower), tolower);
|
|
||||||
return lower;
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}, m_nro_entries{nro_entries} {
|
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}, m_nro_entries{nro_entries} {
|
||||||
fs::FsNativeSd fs;
|
fs::FsNativeSd fs;
|
||||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
|
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
|
||||||
@@ -854,48 +853,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
|||||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
|
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
|
||||||
|
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::RIGHT, Action{[this](){
|
|
||||||
if (m_entries_current.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_index < (m_entries_current.size() - 1) && (m_index + 1) % 3 != 0) {
|
|
||||||
SetIndex(m_index + 1);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
log_write("moved right\n");
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::LEFT, Action{[this](){
|
|
||||||
if (m_entries_current.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_index != 0 && (m_index % 3) != 0) {
|
|
||||||
SetIndex(m_index - 1);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
log_write("moved left\n");
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::R2, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::L2, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
|
||||||
if (m_entries_current.empty()) {
|
if (m_entries_current.empty()) {
|
||||||
// log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size());
|
// log_write("pushing A when empty: size: %zu count: %zu\n", repo_json.size(), m_entries_current.size());
|
||||||
@@ -983,8 +940,8 @@ Menu::~Menu() {
|
|||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
MenuBase::Update(controller, touch);
|
MenuBase::Update(controller, touch);
|
||||||
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||||
if (m_index == i) {
|
if (touch && m_index == i) {
|
||||||
FireAction(Button::A);
|
FireAction(Button::A);
|
||||||
} else {
|
} else {
|
||||||
App::PlaySoundEffect(SoundEffect_Focus);
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
@@ -1096,16 +1053,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
float i_size = 22;
|
float i_size = 22;
|
||||||
switch (e.status) {
|
switch (e.status) {
|
||||||
case EntryStatus::Get:
|
case EntryStatus::Get:
|
||||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
|
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 15);
|
||||||
break;
|
break;
|
||||||
case EntryStatus::Installed:
|
case EntryStatus::Installed:
|
||||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
|
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 15);
|
||||||
break;
|
break;
|
||||||
case EntryStatus::Local:
|
case EntryStatus::Local:
|
||||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
|
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 15);
|
||||||
break;
|
break;
|
||||||
case EntryStatus::Update:
|
case EntryStatus::Update:
|
||||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
|
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 15);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1338,12 +1295,11 @@ void Menu::SetSearch(const std::string& term) {
|
|||||||
|
|
||||||
m_search_term = term;
|
m_search_term = term;
|
||||||
m_entries_index_search.clear();
|
m_entries_index_search.clear();
|
||||||
const auto query = toLower(m_search_term);
|
const auto query = m_search_term;
|
||||||
const auto npos = std::string::npos;
|
|
||||||
|
|
||||||
for (u64 i = 0; i < m_entries.size(); i++) {
|
for (u64 i = 0; i < m_entries.size(); i++) {
|
||||||
const auto& e = m_entries[i];
|
const auto& e = m_entries[i];
|
||||||
if (toLower(e.title).find(query) != npos || toLower(e.author).find(query) != npos || toLower(e.details).find(query) != npos || toLower(e.description).find(query) != npos) {
|
if (FindCaseInsensitive(e.title, query) || FindCaseInsensitive(e.author, query) || FindCaseInsensitive(e.description, query)) {
|
||||||
m_entries_index_search.emplace_back(i);
|
m_entries_index_search.emplace_back(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1372,10 +1328,11 @@ void Menu::SetAuthor() {
|
|||||||
|
|
||||||
m_author_term = m_entries[m_entries_current[m_index]].author;
|
m_author_term = m_entries[m_entries_current[m_index]].author;
|
||||||
m_entries_index_author.clear();
|
m_entries_index_author.clear();
|
||||||
|
const auto query = m_author_term;
|
||||||
|
|
||||||
for (u64 i = 0; i < m_entries.size(); i++) {
|
for (u64 i = 0; i < m_entries.size(); i++) {
|
||||||
const auto& e = m_entries[i];
|
const auto& e = m_entries[i];
|
||||||
if (e.author.find(m_author_term) != std::string::npos) {
|
if (FindCaseInsensitive(e.author, query)) {
|
||||||
m_entries_index_author.emplace_back(i);
|
m_entries_index_author.emplace_back(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,29 @@ constexpr std::string_view INSTALL_EXTENSIONS[] = {
|
|||||||
|
|
||||||
|
|
||||||
struct RomDatabaseEntry {
|
struct RomDatabaseEntry {
|
||||||
std::string_view folder;
|
// uses the naming scheme from retropie.
|
||||||
std::string_view database;
|
std::string_view folder{};
|
||||||
|
// uses the naming scheme from Retroarch.
|
||||||
|
std::string_view database{};
|
||||||
|
// custom alias, to make everyone else happy.
|
||||||
|
std::array<std::string_view, 4> alias{};
|
||||||
|
|
||||||
|
// compares against all of the above strings.
|
||||||
|
auto IsDatabase(std::string_view name) const {
|
||||||
|
if (name == folder || name == database) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& str : alias) {
|
||||||
|
if (!str.empty() && name == str) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// using PathPair = std::pair<std::string_view, std::string_view>;
|
|
||||||
constexpr RomDatabaseEntry PATHS[]{
|
constexpr RomDatabaseEntry PATHS[]{
|
||||||
{ "3do", "The 3DO Company - 3DO"},
|
{ "3do", "The 3DO Company - 3DO"},
|
||||||
{ "atari800", "Atari - 8-bit"},
|
{ "atari800", "Atari - 8-bit"},
|
||||||
@@ -100,6 +118,14 @@ constexpr RomDatabaseEntry PATHS[]{
|
|||||||
{ "pico8", "Sega - PICO"},
|
{ "pico8", "Sega - PICO"},
|
||||||
{ "wonderswan", "Bandai - WonderSwan"},
|
{ "wonderswan", "Bandai - WonderSwan"},
|
||||||
{ "wonderswancolor", "Bandai - WonderSwan Color"},
|
{ "wonderswancolor", "Bandai - WonderSwan Color"},
|
||||||
|
|
||||||
|
{ "mame", "MAME 2000", { "MAME", "mame-libretro", } },
|
||||||
|
{ "mame", "MAME 2003", { "MAME", "mame-libretro", } },
|
||||||
|
{ "mame", "MAME 2003-Plus", { "MAME", "mame-libretro", } },
|
||||||
|
|
||||||
|
{ "neogeo", "SNK - Neo Geo Pocket" },
|
||||||
|
{ "neogeo", "SNK - Neo Geo Pocket Color" },
|
||||||
|
{ "neogeo", "SNK - Neo Geo CD" },
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"};
|
constexpr fs::FsPath DAYBREAK_PATH{"/switch/daybreak.nro"};
|
||||||
@@ -120,49 +146,49 @@ auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool {
|
|||||||
// tries to find database path using folder name
|
// tries to find database path using folder name
|
||||||
// names are taken from retropie
|
// names are taken from retropie
|
||||||
// retroarch database names can also be used
|
// retroarch database names can also be used
|
||||||
auto GetRomDatabaseFromPath(std::string_view path) -> int {
|
using RomDatabaseIndexs = std::vector<size_t>;
|
||||||
|
auto GetRomDatabaseFromPath(std::string_view path) -> RomDatabaseIndexs {
|
||||||
if (path.length() <= 1) {
|
if (path.length() <= 1) {
|
||||||
return -1;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// this won't fail :)
|
// this won't fail :)
|
||||||
|
RomDatabaseIndexs indexs;
|
||||||
const auto db_name = path.substr(path.find_last_of('/') + 1);
|
const auto db_name = path.substr(path.find_last_of('/') + 1);
|
||||||
// log_write("new path: %s\n", db_name.c_str());
|
// log_write("new path: %s\n", db_name.c_str());
|
||||||
|
|
||||||
for (int i = 0; i < std::size(PATHS); i++) {
|
for (int i = 0; i < std::size(PATHS); i++) {
|
||||||
auto& p = PATHS[i];
|
const auto& p = PATHS[i];
|
||||||
if ((
|
if (p.IsDatabase(db_name)) {
|
||||||
p.folder.length() == db_name.length() && !strncasecmp(p.folder.data(), db_name.data(), p.folder.length())) ||
|
|
||||||
(p.database.length() == db_name.length() && !strncasecmp(p.database.data(), db_name.data(), p.database.length()))) {
|
|
||||||
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
|
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
|
||||||
return i;
|
indexs.emplace_back(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we failed, try again but with the folder about
|
// if we failed, try again but with the folder about
|
||||||
// "/roms/psx/scooby-doo/scooby-doo.bin", this will check psx
|
// "/roms/psx/scooby-doo/scooby-doo.bin", this will check psx
|
||||||
|
if (indexs.empty()) {
|
||||||
const auto last_off = path.substr(0, path.find_last_of('/'));
|
const auto last_off = path.substr(0, path.find_last_of('/'));
|
||||||
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
|
if (const auto off = last_off.find_last_of('/'); off != std::string_view::npos) {
|
||||||
const auto db_name2 = last_off.substr(off + 1);
|
const auto db_name2 = last_off.substr(off + 1);
|
||||||
// printf("got db: %s\n", db_name2.c_str());
|
// printf("got db: %s\n", db_name2.c_str());
|
||||||
for (int i = 0; i < std::size(PATHS); i++) {
|
for (int i = 0; i < std::size(PATHS); i++) {
|
||||||
auto& p = PATHS[i];
|
const auto& p = PATHS[i];
|
||||||
if ((
|
if (p.IsDatabase(db_name2)) {
|
||||||
p.folder.length() == db_name2.length() && !strcasecmp(p.folder.data(), db_name2.data())) ||
|
|
||||||
(p.database.length() == db_name2.length() && !strcasecmp(p.database.data(), db_name2.data()))) {
|
|
||||||
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
|
log_write("found it :) %.*s\n", (int)p.database.length(), p.database.data());
|
||||||
return i;
|
indexs.emplace_back(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return indexs;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::string extension, int db_idx, const NroEntry& nro) {
|
auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, const RomDatabaseIndexs& db_indexs, const NroEntry& nro) {
|
||||||
// if no db entries, use nro icon
|
// if no db entries, use nro icon
|
||||||
if (db_idx < 0) {
|
if (db_indexs.empty()) {
|
||||||
log_write("using nro image\n");
|
log_write("using nro image\n");
|
||||||
return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset);
|
return nro_get_icon(nro.path, nro.icon_size, nro.icon_offset);
|
||||||
}
|
}
|
||||||
@@ -184,6 +210,7 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::
|
|||||||
#define RA_THUMBNAIL_PATH "/retroarch/thumbnails/"
|
#define RA_THUMBNAIL_PATH "/retroarch/thumbnails/"
|
||||||
#define RA_BOXART_EXT ".png"
|
#define RA_BOXART_EXT ".png"
|
||||||
|
|
||||||
|
for (auto db_idx : db_indexs) {
|
||||||
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
|
const auto system_name = std::string{PATHS[db_idx].database.data(), PATHS[db_idx].database.length()};//GetDatabaseFromExt(database, extension);
|
||||||
auto system_name_gh = system_name + "/master";
|
auto system_name_gh = system_name + "/master";
|
||||||
for (auto& c : system_name_gh) {
|
for (auto& c : system_name_gh) {
|
||||||
@@ -229,6 +256,7 @@ auto GetRomIcon(fs::FsNative* fs, ProgressBox* pbox, std::string filename, std::
|
|||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// use nro icon
|
// use nro icon
|
||||||
log_write("using nro image\n");
|
log_write("using nro image\n");
|
||||||
@@ -251,26 +279,6 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
|||||||
m_selected_count--;
|
m_selected_count--;
|
||||||
}
|
}
|
||||||
}}),
|
}}),
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 1, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 1, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 8, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::DPAD_LEFT, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 8, m_entries_current.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::A, Action{"Open"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Open"_i18n, [this](){
|
||||||
if (m_entries_current.empty()) {
|
if (m_entries_current.empty()) {
|
||||||
return;
|
return;
|
||||||
@@ -598,8 +606,8 @@ Menu::~Menu() {
|
|||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
MenuBase::Update(controller, touch);
|
MenuBase::Update(controller, touch);
|
||||||
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||||
if (m_index == i) {
|
if (touch && m_index == i) {
|
||||||
FireAction(Button::A);
|
FireAction(Button::A);
|
||||||
} else {
|
} else {
|
||||||
App::PlaySoundEffect(SoundEffect_Focus);
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
@@ -774,7 +782,7 @@ void Menu::InstallForwarder() {
|
|||||||
if (op_index) {
|
if (op_index) {
|
||||||
const auto assoc = assoc_list[*op_index];
|
const auto assoc = assoc_list[*op_index];
|
||||||
log_write("pushing it\n");
|
log_write("pushing it\n");
|
||||||
App::Push(std::make_shared<ProgressBox>("Installing Forwarder"_i18n, [assoc, this](auto pbox) -> bool {
|
App::Push(std::make_shared<ProgressBox>(0, "Installing Forwarder"_i18n, GetEntry().name, [assoc, this](auto pbox) -> bool {
|
||||||
log_write("inside callback\n");
|
log_write("inside callback\n");
|
||||||
|
|
||||||
NroEntry nro{};
|
NroEntry nro{};
|
||||||
@@ -784,8 +792,7 @@ void Menu::InstallForwarder() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
log_write("got nro data\n");
|
log_write("got nro data\n");
|
||||||
std::string file_name = GetEntry().GetInternalName();
|
auto file_name = assoc.use_base_name ? GetEntry().GetName() : GetEntry().GetInternalName();
|
||||||
std::string extension = GetEntry().GetInternalExtension();
|
|
||||||
|
|
||||||
if (auto pos = file_name.find_last_of('.'); pos != std::string::npos) {
|
if (auto pos = file_name.find_last_of('.'); pos != std::string::npos) {
|
||||||
log_write("got filename\n");
|
log_write("got filename\n");
|
||||||
@@ -793,7 +800,7 @@ void Menu::InstallForwarder() {
|
|||||||
log_write("got filename2: %s\n\n", file_name.c_str());
|
log_write("got filename2: %s\n\n", file_name.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto db_idx = GetRomDatabaseFromPath(m_path);
|
const auto db_indexs = GetRomDatabaseFromPath(m_path);
|
||||||
|
|
||||||
OwoConfig config{};
|
OwoConfig config{};
|
||||||
config.nro_path = assoc.path.toString();
|
config.nro_path = assoc.path.toString();
|
||||||
@@ -801,7 +808,8 @@ void Menu::InstallForwarder() {
|
|||||||
config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name;
|
config.name = nro.nacp.lang[0].name + std::string{" | "} + file_name;
|
||||||
// config.name = file_name;
|
// config.name = file_name;
|
||||||
config.nacp = nro.nacp;
|
config.nacp = nro.nacp;
|
||||||
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, extension, db_idx, nro);
|
config.icon = GetRomIcon(m_fs.get(), pbox, file_name, db_indexs, nro);
|
||||||
|
pbox->SetImageDataConst(config.icon);
|
||||||
|
|
||||||
return R_SUCCEEDED(App::Install(pbox, config));
|
return R_SUCCEEDED(App::Install(pbox, config));
|
||||||
}));
|
}));
|
||||||
@@ -822,7 +830,7 @@ void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
|
|||||||
if (op_index && *op_index) {
|
if (op_index && *op_index) {
|
||||||
App::PopToMenu();
|
App::PopToMenu();
|
||||||
|
|
||||||
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this, targets](auto pbox) mutable -> bool {
|
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this, targets](auto pbox) mutable -> bool {
|
||||||
for (auto& e : targets) {
|
for (auto& e : targets) {
|
||||||
const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e));
|
const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e));
|
||||||
if (rc == yati::Result_Cancelled) {
|
if (rc == yati::Result_Cancelled) {
|
||||||
@@ -912,32 +920,32 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
|||||||
|
|
||||||
auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
|
auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
|
||||||
// only support roms in correctly named folders, sorry!
|
// only support roms in correctly named folders, sorry!
|
||||||
const auto db_idx = GetRomDatabaseFromPath(m_path);
|
const auto db_indexs = GetRomDatabaseFromPath(m_path);
|
||||||
const auto& entry = GetEntry();
|
const auto& entry = GetEntry();
|
||||||
const auto extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
|
const auto extension = entry.extension;
|
||||||
if (extension.empty()) {
|
const auto internal_extension = entry.internal_extension.empty() ? entry.extension : entry.internal_extension;
|
||||||
|
if (extension.empty() && internal_extension.empty()) {
|
||||||
// log_write("failed to get extension for db: %s path: %s\n", database_entry.c_str(), m_path);
|
// log_write("failed to get extension for db: %s path: %s\n", database_entry.c_str(), m_path);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// log_write("got extension for db: %s path: %s\n", database_entry.c_str(), m_path);
|
|
||||||
|
|
||||||
|
|
||||||
std::vector<FileAssocEntry> out_entries;
|
std::vector<FileAssocEntry> out_entries;
|
||||||
if (db_idx >= 0) {
|
if (!db_indexs.empty()) {
|
||||||
// if database isn't empty, then we are in a valid folder
|
// if database isn't empty, then we are in a valid folder
|
||||||
// search for an entry that matches the db and ext
|
// search for an entry that matches the db and ext
|
||||||
for (const auto& assoc : m_assoc_entries) {
|
for (const auto& assoc : m_assoc_entries) {
|
||||||
for (const auto& assoc_db : assoc.database) {
|
for (const auto& assoc_db : assoc.database) {
|
||||||
if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
|
// if (assoc_db == PATHS[db_idx].folder || assoc_db == PATHS[db_idx].database) {
|
||||||
for (const auto& assoc_ext : assoc.ext) {
|
for (auto db_idx : db_indexs) {
|
||||||
if (assoc_ext == extension) {
|
if (PATHS[db_idx].IsDatabase(assoc_db)) {
|
||||||
log_write("found ext: %s assoc_ext: %s assoc.ext: %s\n", assoc.path.s, assoc_ext.c_str(), extension.c_str());
|
if (assoc.IsExtension(extension, internal_extension)) {
|
||||||
out_entries.emplace_back(assoc);
|
out_entries.emplace_back(assoc);
|
||||||
|
goto jump;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
jump:
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise, if not in a valid folder, find an entry that doesn't
|
// otherwise, if not in a valid folder, find an entry that doesn't
|
||||||
@@ -948,15 +956,13 @@ auto Menu::FindFileAssocFor() -> std::vector<FileAssocEntry> {
|
|||||||
// to be in the correct folder, ie psx, to know what system that .iso is for.
|
// to be in the correct folder, ie psx, to know what system that .iso is for.
|
||||||
for (const auto& assoc : m_assoc_entries) {
|
for (const auto& assoc : m_assoc_entries) {
|
||||||
if (assoc.database.empty()) {
|
if (assoc.database.empty()) {
|
||||||
for (const auto& assoc_ext : assoc.ext) {
|
if (assoc.IsExtension(extension, internal_extension)) {
|
||||||
if (assoc_ext == extension) {
|
|
||||||
log_write("found ext: %s\n", assoc.path.s);
|
log_write("found ext: %s\n", assoc.path.s);
|
||||||
out_entries.emplace_back(assoc);
|
out_entries.emplace_back(assoc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return out_entries;
|
return out_entries;
|
||||||
}
|
}
|
||||||
@@ -1009,6 +1015,10 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (!strcmp(Key, "use_base_name")) {
|
||||||
|
if (!strcmp(Value, "true") || !strcmp(Value, "1")) {
|
||||||
|
assoc->use_base_name = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
}, &assoc, full_path);
|
}, &assoc, full_path);
|
||||||
@@ -1044,7 +1054,7 @@ void Menu::LoadAssocEntriesPath(const fs::FsPath& path) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// log_write("\tpath: %s\n", assoc.path.c_str());
|
// log_write("\tpath: %s\n", assoc.path.s);
|
||||||
// log_write("\tname: %s\n", assoc.name.c_str());
|
// log_write("\tname: %s\n", assoc.name.c_str());
|
||||||
// for (const auto& ext : assoc.ext) {
|
// for (const auto& ext : assoc.ext) {
|
||||||
// log_write("\t\text: %s\n", ext.c_str());
|
// log_write("\t\text: %s\n", ext.c_str());
|
||||||
@@ -1197,7 +1207,7 @@ void Menu::OnDeleteCallback() {
|
|||||||
Scan(m_path);
|
Scan(m_path);
|
||||||
log_write("did delete\n");
|
log_write("did delete\n");
|
||||||
} else {
|
} else {
|
||||||
App::Push(std::make_shared<ProgressBox>("Deleting"_i18n, [this](auto pbox){
|
App::Push(std::make_shared<ProgressBox>(0, "Deleting"_i18n, "", [this](auto pbox){
|
||||||
FsDirCollections collections;
|
FsDirCollections collections;
|
||||||
|
|
||||||
// build list of dirs / files
|
// build list of dirs / files
|
||||||
@@ -1290,7 +1300,7 @@ void Menu::OnPasteCallback() {
|
|||||||
Scan(m_path);
|
Scan(m_path);
|
||||||
log_write("did paste\n");
|
log_write("did paste\n");
|
||||||
} else {
|
} else {
|
||||||
App::Push(std::make_shared<ProgressBox>("Pasting"_i18n, [this](auto pbox){
|
App::Push(std::make_shared<ProgressBox>(0, "Pasting"_i18n, "", [this](auto pbox){
|
||||||
|
|
||||||
if (m_selected_type == SelectedType::Cut) {
|
if (m_selected_type == SelectedType::Cut) {
|
||||||
for (const auto& p : m_selected_files) {
|
for (const auto& p : m_selected_files) {
|
||||||
|
|||||||
300
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
300
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
#include "ui/menus/ftp_menu.hpp"
|
||||||
|
#include "yati/yati.hpp"
|
||||||
|
#include "app.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include "ui/nvg_util.hpp"
|
||||||
|
#include "i18n.hpp"
|
||||||
|
#include "ftpsrv_helper.hpp"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::ftp {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32;
|
||||||
|
constexpr u64 SLEEPNS = 1000;
|
||||||
|
volatile bool IN_PUSH_THREAD{};
|
||||||
|
|
||||||
|
bool OnInstallStart(void* user, const char* path) {
|
||||||
|
auto menu = (Menu*)user;
|
||||||
|
log_write("[INSTALL] inside OnInstallStart()\n");
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
mutexLock(&menu->m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
|
||||||
|
|
||||||
|
if (menu->m_state != State::Progress) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu->GetToken().stop_requested()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
svcSleepThread(1e+6);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state);
|
||||||
|
|
||||||
|
if (menu->m_source) {
|
||||||
|
log_write("[INSTALL] OnInstallStart() we have source\n");
|
||||||
|
for (;;) {
|
||||||
|
mutexLock(&menu->m_source->m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex));
|
||||||
|
|
||||||
|
if (!IN_PUSH_THREAD) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu->GetToken().stop_requested()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
svcSleepThread(1e+6);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write("[INSTALL] OnInstallStart() stopped polling source\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write("[INSTALL] OnInstallStart() doing make_shared\n");
|
||||||
|
menu->m_source = std::make_shared<StreamFtp>(path, menu->GetToken());
|
||||||
|
|
||||||
|
mutexLock(&menu->m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex));
|
||||||
|
menu->m_state = State::Connected;
|
||||||
|
log_write("[INSTALL] OnInstallStart() done make shared\n");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OnInstallWrite(void* user, const void* buf, size_t size) {
|
||||||
|
auto menu = (Menu*)user;
|
||||||
|
|
||||||
|
return menu->m_source->Push(buf, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnInstallClose(void* user) {
|
||||||
|
auto menu = (Menu*)user;
|
||||||
|
menu->m_source->Disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) {
|
||||||
|
m_path = path;
|
||||||
|
m_token = token;
|
||||||
|
m_buffer.reserve(MAX_BUFFER_SIZE);
|
||||||
|
m_active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||||
|
while (!m_token.stop_requested()) {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
|
if (m_buffer.empty()) {
|
||||||
|
if (!m_active) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
svcSleepThread(SLEEPNS);
|
||||||
|
} else {
|
||||||
|
size = std::min<s64>(size, m_buffer.size());
|
||||||
|
std::memcpy(buf, m_buffer.data(), size);
|
||||||
|
m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size);
|
||||||
|
*bytes_read = size;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StreamFtp::Push(const void* buf, s64 size) {
|
||||||
|
IN_PUSH_THREAD = true;
|
||||||
|
ON_SCOPE_EXIT(IN_PUSH_THREAD = false);
|
||||||
|
|
||||||
|
while (!m_token.stop_requested()) {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
|
if (!m_active) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_buffer.size() + size >= MAX_BUFFER_SIZE) {
|
||||||
|
svcSleepThread(SLEEPNS);
|
||||||
|
} else {
|
||||||
|
const auto offset = m_buffer.size();
|
||||||
|
m_buffer.resize(offset + size);
|
||||||
|
std::memcpy(m_buffer.data() + offset, buf, size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StreamFtp::Disable() {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
m_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::Menu() : MenuBase{"FTP Install (EXPERIMENTAL)"_i18n} {
|
||||||
|
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||||
|
SetPop();
|
||||||
|
}});
|
||||||
|
|
||||||
|
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||||
|
App::DisplayInstallOptions(false);
|
||||||
|
}});
|
||||||
|
|
||||||
|
mutexInit(&m_mutex);
|
||||||
|
ftpsrv::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose);
|
||||||
|
|
||||||
|
m_port = ftpsrv::GetPort();
|
||||||
|
m_anon = ftpsrv::IsAnon();
|
||||||
|
if (!m_anon) {
|
||||||
|
m_user = ftpsrv::GetUser();
|
||||||
|
m_pass = ftpsrv::GetPass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::~Menu() {
|
||||||
|
// signal for thread to exit and wait.
|
||||||
|
ftpsrv::DisableInstallMode();
|
||||||
|
m_stop_source.request_stop();
|
||||||
|
|
||||||
|
if (m_source) {
|
||||||
|
m_source->Disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write("closing data!!!!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
|
MenuBase::Update(controller, touch);
|
||||||
|
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
|
switch (m_state) {
|
||||||
|
case State::None:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Connected:
|
||||||
|
log_write("set to progress\n");
|
||||||
|
m_state = State::Progress;
|
||||||
|
log_write("got connection\n");
|
||||||
|
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
|
||||||
|
log_write("inside progress box\n");
|
||||||
|
const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
m_source->Disable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write("progress box is done\n");
|
||||||
|
return true;
|
||||||
|
}, [this](bool result){
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
App::Notify("Ftp install success!"_i18n);
|
||||||
|
m_state = State::Done;
|
||||||
|
} else {
|
||||||
|
App::Notify("Ftp install failed!"_i18n);
|
||||||
|
m_state = State::Failed;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Progress:
|
||||||
|
case State::Done:
|
||||||
|
case State::Failed:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
|
if (m_ip) {
|
||||||
|
if (m_type == NifmInternetConnectionType_WiFi) {
|
||||||
|
SetSubHeading("Connection Type: WiFi | Strength: "_i18n + std::to_string(m_strength));
|
||||||
|
} else {
|
||||||
|
SetSubHeading("Connection Type: Ethernet"_i18n);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SetSubHeading("Connection Type: None"_i18n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float start_x = 80;
|
||||||
|
const float font_size = 22;
|
||||||
|
const float spacing = 33;
|
||||||
|
float start_y = 125;
|
||||||
|
float bounds[4];
|
||||||
|
|
||||||
|
nvgFontSize(vg, font_size);
|
||||||
|
|
||||||
|
// note: textbounds strips spaces...todo: use nvgTextGlyphPositions() instead.
|
||||||
|
#define draw(key, ...) \
|
||||||
|
gfx::textBounds(vg, start_x, start_y, bounds, key.c_str()); \
|
||||||
|
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT), key.c_str()); \
|
||||||
|
gfx::drawTextArgs(vg, bounds[2], start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), __VA_ARGS__); \
|
||||||
|
start_y += spacing;
|
||||||
|
|
||||||
|
if (m_ip) {
|
||||||
|
draw("Host:"_i18n, " %u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
|
||||||
|
draw("Port:"_i18n, " %u", m_port);
|
||||||
|
if (!m_anon) {
|
||||||
|
draw("Username:"_i18n, " %s", m_user);
|
||||||
|
draw("Password:"_i18n, " %s", m_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_type == NifmInternetConnectionType_WiFi) {
|
||||||
|
NifmNetworkProfileData profile{};
|
||||||
|
if (R_SUCCEEDED(nifmGetCurrentNetworkProfile(&profile))) {
|
||||||
|
const auto& settings = profile.wireless_setting_data;
|
||||||
|
std::string passphrase;
|
||||||
|
std::transform(std::cbegin(settings.passphrase), std::cend(settings.passphrase), passphrase.begin(), toascii);
|
||||||
|
draw("SSID:"_i18n, " %.*s", settings.ssid_len, settings.ssid);
|
||||||
|
draw("Passphrase:"_i18n, " %s", passphrase.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef draw
|
||||||
|
|
||||||
|
switch (m_state) {
|
||||||
|
case State::None:
|
||||||
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Connected:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Progress:
|
||||||
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Done:
|
||||||
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Failed:
|
||||||
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via FTP, press B to exit..."_i18n.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::OnFocusGained() {
|
||||||
|
MenuBase::OnFocusGained();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::ftp
|
||||||
531
sphaira/source/ui/menus/gc_menu.cpp
Normal file
531
sphaira/source/ui/menus/gc_menu.cpp
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
#include "ui/menus/gc_menu.hpp"
|
||||||
|
#include "yati/yati.hpp"
|
||||||
|
#include "yati/nx/nca.hpp"
|
||||||
|
#include "app.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include "ui/nvg_util.hpp"
|
||||||
|
#include "i18n.hpp"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::gc {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const char *g_option_list[] = {
|
||||||
|
"Nand Install",
|
||||||
|
"SD Card Install",
|
||||||
|
"Exit",
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HashStr {
|
||||||
|
char str[0x21];
|
||||||
|
};
|
||||||
|
|
||||||
|
HashStr hexIdToStr(auto id) {
|
||||||
|
HashStr str{};
|
||||||
|
const auto id_lower = std::byteswap(*(u64*)id.c);
|
||||||
|
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
|
||||||
|
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Gc is the mount point, S is for secure partion, the remaining is the
|
||||||
|
// the gamecard handle value in lower-case hex.
|
||||||
|
auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPartition partiton = FsGameCardPartition_Secure) -> fs::FsPath {
|
||||||
|
static const char mount_parition[] = {
|
||||||
|
[FsGameCardPartition_Update] = 'U',
|
||||||
|
[FsGameCardPartition_Normal] = 'N',
|
||||||
|
[FsGameCardPartition_Secure] = 'S',
|
||||||
|
[FsGameCardPartition_Logo] = 'L',
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::FsPath path;
|
||||||
|
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_parition[partiton], handle->value, name);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
|
||||||
|
return serviceDispatch(fsGetServiceSession(), 501,
|
||||||
|
.out_num_objects = 1,
|
||||||
|
.out_objects = &out->s
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto InRange(u64 off, u64 offset, u64 size) -> bool {
|
||||||
|
return off < offset + size && off >= offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GcSource final : yati::source::Base {
|
||||||
|
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install);
|
||||||
|
~GcSource();
|
||||||
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
|
||||||
|
|
||||||
|
yati::container::Collections m_collections{};
|
||||||
|
yati::ConfigOverride m_config{};
|
||||||
|
fs::FsNativeGameCard* m_fs{};
|
||||||
|
FsFile m_file{};
|
||||||
|
s64 m_offset{};
|
||||||
|
s64 m_size{};
|
||||||
|
};
|
||||||
|
|
||||||
|
GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs, bool sd_install)
|
||||||
|
: m_fs{fs} {
|
||||||
|
m_offset = -1;
|
||||||
|
|
||||||
|
s64 offset{};
|
||||||
|
const auto add_collections = [&](const auto& collections) {
|
||||||
|
for (auto collection : collections) {
|
||||||
|
collection.offset = offset;
|
||||||
|
m_collections.emplace_back(collection);
|
||||||
|
offset += collection.size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto add_entries = [&](const auto& entries) {
|
||||||
|
for (auto& e : entries) {
|
||||||
|
add_collections(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// yati can handle all of this for use, however, yati lacks information
|
||||||
|
// for ncas until it installs the cnmt and parses it.
|
||||||
|
// as we already have this info, we can only send yati what we want to install.
|
||||||
|
if (App::GetApp()->m_ticket_only.Get()) {
|
||||||
|
add_collections(entry.tickets);
|
||||||
|
} else {
|
||||||
|
if (!App::GetApp()->m_skip_base.Get()) {
|
||||||
|
add_entries(entry.application);
|
||||||
|
}
|
||||||
|
if (!App::GetApp()->m_skip_patch.Get()) {
|
||||||
|
add_entries(entry.patch);
|
||||||
|
}
|
||||||
|
if (!App::GetApp()->m_skip_addon.Get()) {
|
||||||
|
add_entries(entry.add_on);
|
||||||
|
}
|
||||||
|
if (!App::GetApp()->m_skip_data_patch.Get()) {
|
||||||
|
add_entries(entry.data_patch);
|
||||||
|
}
|
||||||
|
if (!App::GetApp()->m_skip_ticket.Get()) {
|
||||||
|
add_collections(entry.tickets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't need to verify the nca's, this speeds up installs.
|
||||||
|
m_config.sd_card_install = sd_install;
|
||||||
|
m_config.skip_nca_hash_verify = true;
|
||||||
|
m_config.skip_rsa_header_fixed_key_verify = true;
|
||||||
|
m_config.skip_rsa_npdm_fixed_key_verify = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GcSource::~GcSource() {
|
||||||
|
fsFileClose(&m_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||||
|
// check is we need to open a new file.
|
||||||
|
if (!InRange(off, m_offset, m_size)) {
|
||||||
|
fsFileClose(&m_file);
|
||||||
|
m_file = {};
|
||||||
|
|
||||||
|
// find new file based on the offset.
|
||||||
|
bool found = false;
|
||||||
|
for (auto& collection : m_collections) {
|
||||||
|
if (InRange(off, collection.offset, collection.size)) {
|
||||||
|
found = true;
|
||||||
|
m_offset = collection.offset;
|
||||||
|
m_size = collection.size;
|
||||||
|
R_TRY(m_fs->OpenFile(fs::AppendPath("/", collection.name), FsOpenMode_Read, &m_file));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will never fail, unless i break something in yati.
|
||||||
|
R_UNLESS(found, 0x1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fsFileRead(&m_file, off - m_offset, buf, size, 0, bytes_read);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto ApplicationEntry::GetSize(const std::vector<GcCollections>& entries) const -> s64 {
|
||||||
|
s64 size{};
|
||||||
|
for (auto& e : entries) {
|
||||||
|
for (auto& collection : e) {
|
||||||
|
size += collection.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ApplicationEntry::GetSize() const -> s64 {
|
||||||
|
s64 size{};
|
||||||
|
size += GetSize(application);
|
||||||
|
size += GetSize(patch);
|
||||||
|
size += GetSize(add_on);
|
||||||
|
size += GetSize(data_patch);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::Menu() : MenuBase{"GameCard"_i18n} {
|
||||||
|
this->SetActions(
|
||||||
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
|
SetPop();
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||||
|
App::DisplayInstallOptions(false);
|
||||||
|
}})
|
||||||
|
);
|
||||||
|
|
||||||
|
const Vec4 v{485, 275, 720, 70};
|
||||||
|
const Vec2 pad{0, 125 - v.h};
|
||||||
|
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
|
||||||
|
|
||||||
|
fsOpenDeviceOperator(std::addressof(m_dev_op));
|
||||||
|
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
|
||||||
|
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
|
||||||
|
GcOnEvent();
|
||||||
|
UpdateStorageSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::~Menu() {
|
||||||
|
GcUnmount();
|
||||||
|
eventClose(std::addressof(m_event));
|
||||||
|
fsEventNotifierClose(std::addressof(m_event_notifier));
|
||||||
|
fsDeviceOperatorClose(std::addressof(m_dev_op));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
|
// poll for the gamecard first before handling inputs as the gamecard
|
||||||
|
// may have been removed, thus pressing A would fail.
|
||||||
|
if (R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) {
|
||||||
|
GcOnEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuBase::Update(controller, touch);
|
||||||
|
m_list->OnUpdate(controller, touch, m_option_index, std::size(g_option_list), [this](bool touch, auto i) {
|
||||||
|
if (touch && m_option_index == i) {
|
||||||
|
FireAction(Button::A);
|
||||||
|
} else {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
m_option_index = i;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
|
#define STORAGE_BAR_W 325
|
||||||
|
#define STORAGE_BAR_H 14
|
||||||
|
|
||||||
|
const auto size_sd_gb = (double)m_size_free_sd / 0x40000000;
|
||||||
|
const auto size_nand_gb = (double)m_size_free_nand / 0x40000000;
|
||||||
|
|
||||||
|
gfx::drawTextArgs(vg, 490, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System memory %.1f GB", size_nand_gb);
|
||||||
|
gfx::drawRect(vg, 480, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
|
||||||
|
gfx::drawRect(vg, 480 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
|
||||||
|
gfx::drawRect(vg, 480 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_nand / (double)m_size_total_nand) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
|
||||||
|
|
||||||
|
gfx::drawTextArgs(vg, 870, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD card %.1f GB", size_sd_gb);
|
||||||
|
gfx::drawRect(vg, 860, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
|
||||||
|
gfx::drawRect(vg, 860 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
|
||||||
|
gfx::drawRect(vg, 860 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_sd / (double)m_size_total_sd) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
|
||||||
|
|
||||||
|
gfx::drawRect(vg, 30, 90, 375, 555, theme->GetColour(ThemeEntryID_GRID));
|
||||||
|
|
||||||
|
if (!m_entries.empty()) {
|
||||||
|
const auto& e = m_entries[m_entry_index];
|
||||||
|
const auto size = e.GetSize();
|
||||||
|
gfx::drawImage(vg, 90, 130, 256, 256, m_icon ? m_icon : App::GetDefaultImage());
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
nvgIntersectScissor(vg, 50, 90, 325, 555);
|
||||||
|
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.name);
|
||||||
|
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", m_lang_entry.author);
|
||||||
|
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id);
|
||||||
|
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
|
||||||
|
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000);
|
||||||
|
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu", e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size());
|
||||||
|
nvgRestore(vg);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_list->Draw(vg, theme, std::size(g_option_list), [this](auto* vg, auto* theme, auto v, auto i) {
|
||||||
|
const auto& [x, y, w, h] = v;
|
||||||
|
const auto text_y = y + (h / 2.f);
|
||||||
|
auto colour = ThemeEntryID_TEXT;
|
||||||
|
if (i == m_option_index) {
|
||||||
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
|
// g_background.selected_bar = create_shape(Colour_Nintendo_Cyan, 90, 230, 4, 45, true);
|
||||||
|
// draw_shape_position(&g_background.selected_bar, 485, g_options[i].text->rect.y - 10);
|
||||||
|
gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
|
||||||
|
colour = ThemeEntryID_TEXT_SELECTED;
|
||||||
|
}
|
||||||
|
if (i != 2 && !m_mounted) {
|
||||||
|
colour = ThemeEntryID_TEXT_INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", g_option_list[i]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Menu::GcMount() {
|
||||||
|
GcUnmount();
|
||||||
|
|
||||||
|
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle)));
|
||||||
|
|
||||||
|
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(m_handle), FsGameCardPartition_Secure, false);
|
||||||
|
R_TRY(m_fs->GetFsOpenResult());
|
||||||
|
|
||||||
|
FsDir dir;
|
||||||
|
R_TRY(m_fs->OpenDirectory("/", FsDirOpenMode_ReadFiles, std::addressof(dir)));
|
||||||
|
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
|
||||||
|
|
||||||
|
s64 count;
|
||||||
|
R_TRY(m_fs->DirGetEntryCount(std::addressof(dir), std::addressof(count)));
|
||||||
|
|
||||||
|
std::vector<FsDirectoryEntry> buf(count);
|
||||||
|
s64 total_entries;
|
||||||
|
R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data()));
|
||||||
|
R_UNLESS(buf.size() == total_entries, 0x1);
|
||||||
|
|
||||||
|
yati::container::Collections ticket_collections;
|
||||||
|
for (const auto& e : buf) {
|
||||||
|
if (!std::string_view(e.name).ends_with(".tik") && !std::string_view(e.name).ends_with(".cert")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket_collections.emplace_back(e.name, 0, e.file_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& e : buf) {
|
||||||
|
// we could use ncm to handle finding all the ncas for us
|
||||||
|
// however, we can parse faster than ncm.
|
||||||
|
// not only that, the first few calls trying to mount ncm db for
|
||||||
|
// the gamecard will fail as it has not yet been parsed (or it's locked?).
|
||||||
|
// we could, of course, just wait until ncm is ready, which is about
|
||||||
|
// 32ms, but i already have code for manually parsing cnmt so lets re-use it.
|
||||||
|
if (!std::string_view(e.name).ends_with(".cnmt.nca")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't yet use the header or extended header.
|
||||||
|
ncm::PackagedContentMeta header;
|
||||||
|
std::vector<u8> extended_header;
|
||||||
|
std::vector<NcmPackagedContentInfo> infos;
|
||||||
|
const auto path = BuildGcPath(e.name, &m_handle);
|
||||||
|
R_TRY(yati::ParseCnmtNca(path, 0, header, extended_header, infos));
|
||||||
|
|
||||||
|
u8 key_gen;
|
||||||
|
FsRightsId rights_id;
|
||||||
|
R_TRY(fsGetRightsIdAndKeyGenerationByPath(path, FsContentAttributes_All, &key_gen, &rights_id));
|
||||||
|
|
||||||
|
// always add tickets, yati will ignore them if not needed.
|
||||||
|
GcCollections collections;
|
||||||
|
// add cnmt file.
|
||||||
|
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta, 0);
|
||||||
|
|
||||||
|
for (const auto& packed_info : infos) {
|
||||||
|
const auto& info = packed_info.info;
|
||||||
|
// these don't exist for gamecards, however i may copy/paste this code
|
||||||
|
// somewhere so i'm future proofing against myself.
|
||||||
|
if (info.content_type == NcmContentType_DeltaFragment) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the nca file, this will never fail for gamecards, see above comment.
|
||||||
|
const auto str = hexIdToStr(info.content_id);
|
||||||
|
const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
|
||||||
|
return !std::strncmp(str.str, e.name, std::strlen(str.str));
|
||||||
|
});
|
||||||
|
|
||||||
|
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
|
||||||
|
collections.emplace_back(it->name, it->file_size, info.content_type, info.id_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto app_id = ncm::GetAppId(header);
|
||||||
|
ApplicationEntry* app_entry{};
|
||||||
|
for (auto& app : m_entries) {
|
||||||
|
if (app.app_id == app_id) {
|
||||||
|
app_entry = &app;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app_entry) {
|
||||||
|
app_entry = &m_entries.emplace_back(app_id, header.title_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
app_entry->version = std::max(app_entry->version, header.title_version);
|
||||||
|
app_entry->key_gen = std::max(app_entry->key_gen, key_gen);
|
||||||
|
|
||||||
|
if (header.meta_type == NcmContentMetaType_Application) {
|
||||||
|
app_entry->application.emplace_back(collections);
|
||||||
|
} else if (header.meta_type == NcmContentMetaType_Patch) {
|
||||||
|
app_entry->patch.emplace_back(collections);
|
||||||
|
} else if (header.meta_type == NcmContentMetaType_AddOnContent) {
|
||||||
|
app_entry->add_on.emplace_back(collections);
|
||||||
|
} else if (header.meta_type == NcmContentMetaType_DataPatch) {
|
||||||
|
app_entry->data_patch.emplace_back(collections);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R_UNLESS(m_entries.size(), 0x1);
|
||||||
|
|
||||||
|
// append tickets to every application, yati will ignore if undeeded.
|
||||||
|
for (auto& e : m_entries) {
|
||||||
|
e.tickets = ticket_collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetAction(Button::A, Action{"OK"_i18n, [this](){
|
||||||
|
if (m_option_index == 2) {
|
||||||
|
SetPop();
|
||||||
|
} else {
|
||||||
|
if (m_mounted) {
|
||||||
|
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_lang_entry.name, [this](auto pbox) mutable -> bool {
|
||||||
|
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get(), m_option_index == 1);
|
||||||
|
return R_SUCCEEDED(yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config));
|
||||||
|
}, [this](bool result){
|
||||||
|
if (result) {
|
||||||
|
App::Notify("Gc install success!"_i18n);
|
||||||
|
} else {
|
||||||
|
App::Notify("Gc install failed!"_i18n);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateStorageSize();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (m_entries.size() > 1) {
|
||||||
|
SetAction(Button::L, Action{"Prev"_i18n, [this](){
|
||||||
|
if (m_entry_index != 0) {
|
||||||
|
OnChangeIndex(m_entry_index - 1);
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
SetAction(Button::R, Action{"Next"_i18n, [this](){
|
||||||
|
if (m_entry_index < m_entries.size()) {
|
||||||
|
OnChangeIndex(m_entry_index + 1);
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
OnChangeIndex(0);
|
||||||
|
m_mounted = true;
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::GcUnmount() {
|
||||||
|
m_fs.reset();
|
||||||
|
m_entries.clear();
|
||||||
|
m_entry_index = 0;
|
||||||
|
m_mounted = false;
|
||||||
|
m_lang_entry = {};
|
||||||
|
FreeImage();
|
||||||
|
|
||||||
|
RemoveAction(Button::L);
|
||||||
|
RemoveAction(Button::R);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Menu::GcPoll(bool* inserted) {
|
||||||
|
R_TRY(fsDeviceOperatorIsGameCardInserted(&m_dev_op, inserted));
|
||||||
|
|
||||||
|
// if the handle changed, re-mount the game card.
|
||||||
|
if (*inserted && m_mounted) {
|
||||||
|
FsGameCardHandle handle;
|
||||||
|
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(handle)));
|
||||||
|
if (handle.value != m_handle.value) {
|
||||||
|
R_TRY(GcMount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Menu::GcOnEvent() {
|
||||||
|
bool inserted{};
|
||||||
|
R_TRY(GcPoll(&inserted));
|
||||||
|
|
||||||
|
if (m_mounted != inserted) {
|
||||||
|
log_write("gc state changed\n");
|
||||||
|
m_mounted = inserted;
|
||||||
|
if (m_mounted) {
|
||||||
|
log_write("trying to mount\n");
|
||||||
|
m_mounted = R_SUCCEEDED(GcMount());
|
||||||
|
if (m_mounted) {
|
||||||
|
App::PlaySoundEffect(SoundEffect::SoundEffect_Startup);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_write("trying to unmount\n");
|
||||||
|
GcUnmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Menu::UpdateStorageSize() {
|
||||||
|
fs::FsNativeContentStorage fs_nand{FsContentStorageId_User};
|
||||||
|
fs::FsNativeContentStorage fs_sd{FsContentStorageId_SdCard};
|
||||||
|
|
||||||
|
R_TRY(fs_sd.GetFreeSpace("/", &m_size_free_sd));
|
||||||
|
R_TRY(fs_sd.GetTotalSpace("/", &m_size_total_sd));
|
||||||
|
R_TRY(fs_nand.GetFreeSpace("/", &m_size_free_nand));
|
||||||
|
R_TRY(fs_nand.GetTotalSpace("/", &m_size_total_nand));
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::FreeImage() {
|
||||||
|
if (m_icon) {
|
||||||
|
nvgDeleteImage(App::GetVg(), m_icon);
|
||||||
|
m_icon = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::OnChangeIndex(s64 new_index) {
|
||||||
|
FreeImage();
|
||||||
|
m_entry_index = new_index;
|
||||||
|
|
||||||
|
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
|
||||||
|
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
|
||||||
|
|
||||||
|
// nsGetApplicationControlData() will fail if it's the first time
|
||||||
|
// mounting a gamecard if the image is not already cached.
|
||||||
|
// waiting 1-2s after mount, then calling seems to work.
|
||||||
|
// however, we can just manually parse the nca to get the data we need,
|
||||||
|
// which always works and *is* faster too ;)
|
||||||
|
for (auto& e : m_entries[m_entry_index].application) {
|
||||||
|
for (auto& collection : e) {
|
||||||
|
if (collection.type == NcmContentType_Control) {
|
||||||
|
NacpStruct nacp;
|
||||||
|
std::vector<u8> icon;
|
||||||
|
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
|
||||||
|
|
||||||
|
u64 program_id = m_entries[m_entry_index].app_id | collection.id_offset;
|
||||||
|
if (hosversionAtLeast(17, 0, 0)) {
|
||||||
|
fsGetProgramId(&program_id, path, FsContentAttributes_All);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (R_SUCCEEDED(yati::ParseControlNca(path, program_id, &nacp, sizeof(nacp), &icon))) {
|
||||||
|
log_write("managed to parse control nca %s\n", path.s);
|
||||||
|
NacpLanguageEntry* lang_entry{};
|
||||||
|
nacpGetLanguageEntry(&nacp, &lang_entry);
|
||||||
|
|
||||||
|
if (lang_entry) {
|
||||||
|
m_lang_entry = *lang_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_icon = nvgCreateImageMem(App::GetVg(), 0, icon.data(), icon.size());
|
||||||
|
if (m_icon > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_write("\tFAILED to parse control nca %s\n", path.s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::gc
|
||||||
@@ -247,27 +247,6 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
|||||||
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||||
|
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 1, m_entries.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 1, m_entries.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 8, m_entries.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::DPAD_LEFT, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 8, m_entries.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
|
|
||||||
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
||||||
if (m_entries.empty()) {
|
if (m_entries.empty()) {
|
||||||
return;
|
return;
|
||||||
@@ -277,7 +256,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
|||||||
static GhApiEntry gh_entry;
|
static GhApiEntry gh_entry;
|
||||||
gh_entry = {};
|
gh_entry = {};
|
||||||
|
|
||||||
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this](auto pbox){
|
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this](auto pbox){
|
||||||
return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry);
|
return DownloadAssetJson(pbox, GenerateApiUrl(GetEntry()), gh_entry);
|
||||||
}, [this](bool success){
|
}, [this](bool success){
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -325,7 +304,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto func = [this, &asset_entry, ptr](){
|
const auto func = [this, &asset_entry, ptr](){
|
||||||
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
|
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, GetEntry().repo, [this, &asset_entry, ptr](auto pbox){
|
||||||
return DownloadApp(pbox, asset_entry, ptr);
|
return DownloadApp(pbox, asset_entry, ptr);
|
||||||
}, [this, ptr](bool success){
|
}, [this, ptr](bool success){
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -373,8 +352,8 @@ Menu::~Menu() {
|
|||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
MenuBase::Update(controller, touch);
|
MenuBase::Update(controller, touch);
|
||||||
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||||
if (m_index == i) {
|
if (touch && m_index == i) {
|
||||||
FireAction(Button::A);
|
FireAction(Button::A);
|
||||||
} else {
|
} else {
|
||||||
App::PlaySoundEffect(SoundEffect_Focus);
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
|||||||
@@ -28,40 +28,6 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
|
|||||||
|
|
||||||
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::RIGHT, Action{[this](){
|
|
||||||
if (m_index < (m_entries.size() - 1) && (m_index + 1) % 3 != 0) {
|
|
||||||
SetIndex(m_index + 1);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
log_write("moved right\n");
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::LEFT, Action{[this](){
|
|
||||||
if (m_index != 0 && (m_index % 3) != 0) {
|
|
||||||
SetIndex(m_index - 1);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
log_write("moved left\n");
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::R2, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 9, m_entries.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::L2, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 9, m_entries.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||||
nro_launch(m_entries[m_index].path);
|
nro_launch(m_entries[m_index].path);
|
||||||
}}),
|
}}),
|
||||||
@@ -157,8 +123,8 @@ Menu::~Menu() {
|
|||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
MenuBase::Update(controller, touch);
|
MenuBase::Update(controller, touch);
|
||||||
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||||
if (m_index == i) {
|
if (touch && m_index == i) {
|
||||||
FireAction(Button::A);
|
FireAction(Button::A);
|
||||||
} else {
|
} else {
|
||||||
App::PlaySoundEffect(SoundEffect_Focus);
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
@@ -202,7 +168,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const float image_size = 115;
|
const float image_size = 115;
|
||||||
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
|
gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 15);
|
||||||
|
|
||||||
nvgSave(vg);
|
nvgSave(vg);
|
||||||
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||||
|
|||||||
@@ -1,533 +0,0 @@
|
|||||||
#include "ui/menus/irs_menu.hpp"
|
|
||||||
#include "ui/sidebar.hpp"
|
|
||||||
#include "ui/popup_list.hpp"
|
|
||||||
#include "app.hpp"
|
|
||||||
#include "defines.hpp"
|
|
||||||
#include "log.hpp"
|
|
||||||
#include "ui/nvg_util.hpp"
|
|
||||||
#include "i18n.hpp"
|
|
||||||
#include <cstring>
|
|
||||||
#include <array>
|
|
||||||
|
|
||||||
namespace sphaira::ui::menu::irs {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// from trial and error
|
|
||||||
constexpr u32 GAIN_MIN = 1;
|
|
||||||
constexpr u32 GAIN_MAX = 16;
|
|
||||||
|
|
||||||
consteval auto generte_iron_palette_table() {
|
|
||||||
std::array<u32, 256> array{};
|
|
||||||
|
|
||||||
const u32 iron_palette[] = {
|
|
||||||
0xff000014, 0xff000025, 0xff00002a, 0xff000032, 0xff000036, 0xff00003e, 0xff000042, 0xff00004f,
|
|
||||||
0xff010055, 0xff010057, 0xff02005c, 0xff03005e, 0xff040063, 0xff050065, 0xff070069, 0xff0a0070,
|
|
||||||
0xff0b0073, 0xff0d0075, 0xff0d0076, 0xff100078, 0xff120079, 0xff15007c, 0xff17007d, 0xff1c0081,
|
|
||||||
0xff200084, 0xff220085, 0xff260087, 0xff280089, 0xff2c008a, 0xff2e008b, 0xff32008d, 0xff38008f,
|
|
||||||
0xff390090, 0xff3c0092, 0xff3e0093, 0xff410094, 0xff420095, 0xff450096, 0xff470096, 0xff4c0097,
|
|
||||||
0xff4f0097, 0xff510097, 0xff540098, 0xff560098, 0xff5a0099, 0xff5c0099, 0xff5f009a, 0xff64009b,
|
|
||||||
0xff66009b, 0xff6a009b, 0xff6c009c, 0xff6f009c, 0xff70009c, 0xff73009d, 0xff75009d, 0xff7a009d,
|
|
||||||
0xff7e009d, 0xff7f009d, 0xff83009d, 0xff84009d, 0xff87009d, 0xff89009d, 0xff8b009d, 0xff91009c,
|
|
||||||
0xff93009c, 0xff96009b, 0xff98009b, 0xff9b009b, 0xff9c009b, 0xff9f009b, 0xffa0009b, 0xffa4009b,
|
|
||||||
0xffa7009a, 0xffa8009a, 0xffaa0099, 0xffab0099, 0xffae0198, 0xffaf0198, 0xffb00198, 0xffb30196,
|
|
||||||
0xffb40296, 0xffb60295, 0xffb70395, 0xffb90495, 0xffba0495, 0xffbb0593, 0xffbc0593, 0xffbf0692,
|
|
||||||
0xffc00791, 0xffc00791, 0xffc10990, 0xffc20a8f, 0xffc30b8e, 0xffc40c8d, 0xffc60d8b, 0xffc81088,
|
|
||||||
0xffc91187, 0xffca1385, 0xffcb1385, 0xffcc1582, 0xffcd1681, 0xffce187e, 0xffcf187c, 0xffd11b78,
|
|
||||||
0xffd21c75, 0xffd21d74, 0xffd32071, 0xffd4216f, 0xffd5236b, 0xffd52469, 0xffd72665, 0xffd92a60,
|
|
||||||
0xffda2b5e, 0xffdb2e5a, 0xffdb2f57, 0xffdd3051, 0xffdd314e, 0xffde3347, 0xffdf3444, 0xffe0373a,
|
|
||||||
0xffe03933, 0xffe13a30, 0xffe23c2a, 0xffe33d26, 0xffe43f20, 0xffe4411d, 0xffe5431b, 0xffe64616,
|
|
||||||
0xffe74715, 0xffe74913, 0xffe84a12, 0xffe84c0f, 0xffe94d0e, 0xffea4e0c, 0xffea4f0c, 0xffeb520a,
|
|
||||||
0xffec5409, 0xffec5608, 0xffec5808, 0xffed5907, 0xffed5b06, 0xffee5c06, 0xffee5d05, 0xffef6004,
|
|
||||||
0xffef6104, 0xfff06303, 0xfff06403, 0xfff16603, 0xfff16603, 0xfff16803, 0xfff16902, 0xfff16b02,
|
|
||||||
0xfff26d01, 0xfff26e01, 0xfff37001, 0xfff37101, 0xfff47300, 0xfff47400, 0xfff47600, 0xfff47a00,
|
|
||||||
0xfff57b00, 0xfff57e00, 0xfff57f00, 0xfff68100, 0xfff68200, 0xfff78400, 0xfff78500, 0xfff88800,
|
|
||||||
0xfff88900, 0xfff88a00, 0xfff88c00, 0xfff98d00, 0xfff98e00, 0xfff98f00, 0xfff99100, 0xfffa9400,
|
|
||||||
0xfffa9500, 0xfffb9800, 0xfffb9900, 0xfffb9c00, 0xfffc9d00, 0xfffca000, 0xfffca100, 0xfffda400,
|
|
||||||
0xfffda700, 0xfffda800, 0xfffdab00, 0xfffdac00, 0xfffdae00, 0xfffeaf00, 0xfffeb100, 0xfffeb400,
|
|
||||||
0xfffeb500, 0xfffeb800, 0xfffeb900, 0xfffeba00, 0xfffebb00, 0xfffebd00, 0xfffebe00, 0xfffec200,
|
|
||||||
0xfffec400, 0xfffec500, 0xfffec700, 0xfffec800, 0xfffeca01, 0xfffeca01, 0xfffecc02, 0xfffecf04,
|
|
||||||
0xfffecf04, 0xfffed106, 0xfffed308, 0xfffed50a, 0xfffed60a, 0xfffed80c, 0xfffed90d, 0xffffdb10,
|
|
||||||
0xffffdc14, 0xffffdd16, 0xffffde1b, 0xffffdf1e, 0xffffe122, 0xffffe224, 0xffffe328, 0xffffe531,
|
|
||||||
0xffffe635, 0xffffe73c, 0xffffe83f, 0xffffea46, 0xffffeb49, 0xffffec50, 0xffffed54, 0xffffee5f,
|
|
||||||
0xffffef67, 0xfffff06a, 0xfffff172, 0xfffff177, 0xfffff280, 0xfffff285, 0xfffff38e, 0xfffff49a,
|
|
||||||
0xfffff59e, 0xfffff5a6, 0xfffff6aa, 0xfffff7b3, 0xfffff7b6, 0xfffff8bd, 0xfffff8c1, 0xfffff9ca,
|
|
||||||
0xfffffad1, 0xfffffad4, 0xfffffcdb, 0xfffffcdf, 0xfffffde5, 0xfffffde8, 0xfffffeee, 0xfffffff6
|
|
||||||
};
|
|
||||||
|
|
||||||
for (u32 i = 0; i < 256; i++) {
|
|
||||||
const auto c = iron_palette[i];
|
|
||||||
array[i] = RGBA8_MAXALPHA((c >> 16) & 0xFF, (c >> 8) & 0xFF, (c >> 0) & 0xFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ARGB Ironbow palette
|
|
||||||
constexpr auto iron_palette = generte_iron_palette_table();
|
|
||||||
|
|
||||||
void irsConvertConfigExToNormal(const IrsImageTransferProcessorExConfig* ex, IrsImageTransferProcessorConfig* nor) {
|
|
||||||
std::memcpy(nor, ex, sizeof(*nor));
|
|
||||||
}
|
|
||||||
|
|
||||||
void irsConvertConfigNormalToEx(const IrsImageTransferProcessorConfig* nor, IrsImageTransferProcessorExConfig* ex) {
|
|
||||||
std::memcpy(ex, nor, sizeof(*nor));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Menu::Menu() : MenuBase{"Irs"_i18n} {
|
|
||||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
|
||||||
SetPop();
|
|
||||||
}});
|
|
||||||
|
|
||||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
|
||||||
auto options = std::make_shared<Sidebar>("Options"_i18n, Sidebar::Side::RIGHT);
|
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
|
||||||
|
|
||||||
SidebarEntryArray::Items controller_str;
|
|
||||||
for (u32 i = 0; i < IRS_MAX_CAMERAS; i++) {
|
|
||||||
const auto& e = m_entries[i];
|
|
||||||
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
|
|
||||||
switch (e.status) {
|
|
||||||
case IrsIrCameraStatus_Available:
|
|
||||||
text += " (Available)"_i18n;
|
|
||||||
break;
|
|
||||||
case IrsIrCameraStatus_Unsupported:
|
|
||||||
text += " (Unsupported)"_i18n;
|
|
||||||
break;
|
|
||||||
case IrsIrCameraStatus_Unconnected:
|
|
||||||
text += " (Unconnected)"_i18n;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
controller_str.emplace_back(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
SidebarEntryArray::Items rotation_str;
|
|
||||||
rotation_str.emplace_back("0 (Sideways)"_i18n);
|
|
||||||
rotation_str.emplace_back("90 (Flat)"_i18n);
|
|
||||||
rotation_str.emplace_back("180 (-Sideways)"_i18n);
|
|
||||||
rotation_str.emplace_back("270 (Upside down)"_i18n);
|
|
||||||
|
|
||||||
SidebarEntryArray::Items colour_str;
|
|
||||||
colour_str.emplace_back("Grey"_i18n);
|
|
||||||
colour_str.emplace_back("Ironbow"_i18n);
|
|
||||||
colour_str.emplace_back("Green"_i18n);
|
|
||||||
colour_str.emplace_back("Red"_i18n);
|
|
||||||
colour_str.emplace_back("Blue"_i18n);
|
|
||||||
|
|
||||||
SidebarEntryArray::Items light_target_str;
|
|
||||||
light_target_str.emplace_back("All leds"_i18n);
|
|
||||||
light_target_str.emplace_back("Bright group"_i18n);
|
|
||||||
light_target_str.emplace_back("Dim group"_i18n);
|
|
||||||
light_target_str.emplace_back("None"_i18n);
|
|
||||||
|
|
||||||
SidebarEntryArray::Items gain_str;
|
|
||||||
for (u32 i = GAIN_MIN; i <= GAIN_MAX; i++) {
|
|
||||||
gain_str.emplace_back(std::to_string(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
SidebarEntryArray::Items is_negative_image_used_str;
|
|
||||||
is_negative_image_used_str.emplace_back("Normal image"_i18n);
|
|
||||||
is_negative_image_used_str.emplace_back("Negative image"_i18n);
|
|
||||||
|
|
||||||
SidebarEntryArray::Items format_str;
|
|
||||||
format_str.emplace_back("320x240"_i18n);
|
|
||||||
format_str.emplace_back("160x120"_i18n);
|
|
||||||
format_str.emplace_back("80x60"_i18n);
|
|
||||||
if (hosversionAtLeast(4,0,0)) {
|
|
||||||
format_str.emplace_back("40x30"_i18n);
|
|
||||||
format_str.emplace_back("20x15"_i18n);
|
|
||||||
}
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Controller"_i18n, controller_str, [this](s64& index){
|
|
||||||
irsStopImageProcessor(m_entries[m_index].m_handle);
|
|
||||||
m_index = index;
|
|
||||||
UpdateConfig(&m_config);
|
|
||||||
}, m_index));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Rotation"_i18n, rotation_str, [this](s64& index){
|
|
||||||
m_rotation = (Rotation)index;
|
|
||||||
}, m_rotation));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Colour"_i18n, colour_str, [this](s64& index){
|
|
||||||
m_colour = (Colour)index;
|
|
||||||
updateColourArray();
|
|
||||||
}, m_colour));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Light Target"_i18n, light_target_str, [this](s64& index){
|
|
||||||
m_config.light_target = index;
|
|
||||||
UpdateConfig(&m_config);
|
|
||||||
}, m_config.light_target));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Gain"_i18n, gain_str, [this](s64& index){
|
|
||||||
m_config.gain = GAIN_MIN + index;
|
|
||||||
UpdateConfig(&m_config);
|
|
||||||
}, m_config.gain - GAIN_MIN));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Negative Image"_i18n, is_negative_image_used_str, [this](s64& index){
|
|
||||||
m_config.is_negative_image_used = index;
|
|
||||||
UpdateConfig(&m_config);
|
|
||||||
}, m_config.is_negative_image_used));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Format"_i18n, format_str, [this](s64& index){
|
|
||||||
m_config.orig_format = index;
|
|
||||||
m_config.trimming_format = index;
|
|
||||||
UpdateConfig(&m_config);
|
|
||||||
}, m_config.orig_format));
|
|
||||||
|
|
||||||
if (hosversionAtLeast(4,0,0)) {
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Trimming Format"_i18n, format_str, [this](s64& index){
|
|
||||||
// you cannot set trim a larger region than the source
|
|
||||||
if (index < m_config.orig_format) {
|
|
||||||
index = m_config.orig_format;
|
|
||||||
} else {
|
|
||||||
m_config.trimming_format = index;
|
|
||||||
UpdateConfig(&m_config);
|
|
||||||
}
|
|
||||||
}, m_config.orig_format));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("External Light Filter"_i18n, m_config.is_external_light_filter_enabled, [this](bool& enable){
|
|
||||||
m_config.is_external_light_filter_enabled = enable;
|
|
||||||
UpdateConfig(&m_config);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
}
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Load Default"_i18n, [this](){
|
|
||||||
LoadDefaultConfig();
|
|
||||||
}, true));
|
|
||||||
}});
|
|
||||||
|
|
||||||
if (R_FAILED(m_init_rc = irsInitialize())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
static_assert(IRS_MAX_CAMERAS >= 9, "max camaeras has gotten smaller!");
|
|
||||||
|
|
||||||
// open all handles
|
|
||||||
irsGetIrCameraHandle(&m_entries[0].m_handle, HidNpadIdType_No1);
|
|
||||||
irsGetIrCameraHandle(&m_entries[1].m_handle, HidNpadIdType_No2);
|
|
||||||
irsGetIrCameraHandle(&m_entries[2].m_handle, HidNpadIdType_No3);
|
|
||||||
irsGetIrCameraHandle(&m_entries[3].m_handle, HidNpadIdType_No4);
|
|
||||||
irsGetIrCameraHandle(&m_entries[4].m_handle, HidNpadIdType_No5);
|
|
||||||
irsGetIrCameraHandle(&m_entries[5].m_handle, HidNpadIdType_No6);
|
|
||||||
irsGetIrCameraHandle(&m_entries[6].m_handle, HidNpadIdType_No7);
|
|
||||||
irsGetIrCameraHandle(&m_entries[7].m_handle, HidNpadIdType_No8);
|
|
||||||
irsGetIrCameraHandle(&m_entries[8].m_handle, HidNpadIdType_Handheld);
|
|
||||||
// get status of all handles
|
|
||||||
PollCameraStatus(true);
|
|
||||||
// load default config
|
|
||||||
LoadDefaultConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
Menu::~Menu() {
|
|
||||||
ResetImage();
|
|
||||||
|
|
||||||
for (auto& e : m_entries) {
|
|
||||||
irsStopImageProcessor(e.m_handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
// this closes all handles
|
|
||||||
irsExit();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
|
||||||
MenuBase::Update(controller, touch);
|
|
||||||
PollCameraStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|
||||||
MenuBase::Draw(vg, theme);
|
|
||||||
|
|
||||||
IrsImageTransferProcessorState state;
|
|
||||||
const auto rc = irsGetImageTransferProcessorState(m_entries[m_index].m_handle, m_irs_buffer.data(), m_irs_buffer.size(), &state);
|
|
||||||
if (R_SUCCEEDED(rc) && state.sampling_number != m_prev_state.sampling_number) {
|
|
||||||
m_prev_state = state;
|
|
||||||
SetSubHeading("Ambient Noise Level: "_i18n + std::to_string(m_prev_state.ambient_noise_level));
|
|
||||||
updateColourArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_image) {
|
|
||||||
float cx{}, cy{};
|
|
||||||
float w{}, h{};
|
|
||||||
float angle{};
|
|
||||||
|
|
||||||
switch (m_rotation) {
|
|
||||||
case Rotation_0: {
|
|
||||||
const auto scale_x = m_pos.w / float(m_irs_width);
|
|
||||||
const auto scale_y = m_pos.h / float(m_irs_height);
|
|
||||||
const auto scale = std::min(scale_x, scale_y);
|
|
||||||
w = m_irs_width * scale;
|
|
||||||
h = m_irs_height * scale;
|
|
||||||
cx = (m_pos.x + m_pos.w / 2.F) - w / 2.F;
|
|
||||||
cy = (m_pos.y + m_pos.h / 2.F) - h / 2.F;
|
|
||||||
angle = 0;
|
|
||||||
} break;
|
|
||||||
case Rotation_90: {
|
|
||||||
const auto scale_x = m_pos.w / float(m_irs_height);
|
|
||||||
const auto scale_y = m_pos.h / float(m_irs_width);
|
|
||||||
const auto scale = std::min(scale_x, scale_y);
|
|
||||||
w = m_irs_width * scale;
|
|
||||||
h = m_irs_height * scale;
|
|
||||||
cx = (m_pos.x + m_pos.w / 2.F) + h / 2.F;
|
|
||||||
cy = (m_pos.y + m_pos.h / 2.F) - w / 2.F;
|
|
||||||
angle = 90;
|
|
||||||
} break;
|
|
||||||
case Rotation_180: {
|
|
||||||
const auto scale_x = m_pos.w / float(m_irs_width);
|
|
||||||
const auto scale_y = m_pos.h / float(m_irs_height);
|
|
||||||
const auto scale = std::min(scale_x, scale_y);
|
|
||||||
w = m_irs_width * scale;
|
|
||||||
h = m_irs_height * scale;
|
|
||||||
cx = (m_pos.x + m_pos.w / 2.F) + w / 2.F;
|
|
||||||
cy = (m_pos.y + m_pos.h / 2.F) + h / 2.F;
|
|
||||||
angle = 180;
|
|
||||||
} break;
|
|
||||||
case Rotation_270: {
|
|
||||||
const auto scale_x = m_pos.w / float(m_irs_height);
|
|
||||||
const auto scale_y = m_pos.h / float(m_irs_width);
|
|
||||||
const auto scale = std::min(scale_x, scale_y);
|
|
||||||
w = m_irs_width * scale;
|
|
||||||
h = m_irs_height * scale;
|
|
||||||
cx = (m_pos.x + m_pos.w / 2.F) - h / 2.F;
|
|
||||||
cy = (m_pos.y + m_pos.h / 2.F) + w / 2.F;
|
|
||||||
angle = 270;
|
|
||||||
} break;
|
|
||||||
}
|
|
||||||
|
|
||||||
nvgSave(vg);
|
|
||||||
nvgTranslate(vg, cx, cy);
|
|
||||||
const auto paint = nvgImagePattern(vg, 0, 0, w, h, 0, m_image, 1.f);
|
|
||||||
nvgRotate(vg, nvgDegToRad(angle));
|
|
||||||
nvgBeginPath(vg);
|
|
||||||
nvgRect(vg, 0, 0, w, h);
|
|
||||||
nvgFillPaint(vg, paint);
|
|
||||||
nvgFill(vg);
|
|
||||||
nvgRestore(vg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::OnFocusGained() {
|
|
||||||
MenuBase::OnFocusGained();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::PollCameraStatus(bool statup) {
|
|
||||||
int index = 0;
|
|
||||||
for (auto& e : m_entries) {
|
|
||||||
IrsIrCameraStatus status;
|
|
||||||
if (R_FAILED(irsGetIrCameraStatus(e.m_handle, &status))) {
|
|
||||||
log_write("failed to get ir status\n");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.status != status || statup) {
|
|
||||||
e.status = status;
|
|
||||||
e.m_update_needed = false;
|
|
||||||
|
|
||||||
log_write("status changed\n");
|
|
||||||
switch (e.status) {
|
|
||||||
case IrsIrCameraStatus_Available:
|
|
||||||
if (hosversionAtLeast(4,0,0)) {
|
|
||||||
// calling this breaks the handle, kinda
|
|
||||||
#if 0
|
|
||||||
if (R_FAILED(irsCheckFirmwareUpdateNecessity(e.m_handle, &e.m_update_needed))) {
|
|
||||||
log_write("failed to check if update needed: %u\n", e.m_update_needed);
|
|
||||||
} else {
|
|
||||||
if (e.m_update_needed) {
|
|
||||||
log_write("update needed\n");
|
|
||||||
} else {
|
|
||||||
log_write("no update needed\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
log_write("irs index: %d status: IrsIrCameraStatus_Available\n", index);
|
|
||||||
break;
|
|
||||||
case IrsIrCameraStatus_Unsupported:
|
|
||||||
log_write("irs index: %d status: IrsIrCameraStatus_Unsupported\n", index);
|
|
||||||
break;
|
|
||||||
case IrsIrCameraStatus_Unconnected:
|
|
||||||
log_write("irs index: %d status: IrsIrCameraStatus_Unconnected\n", index);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::ResetImage() {
|
|
||||||
if (m_image) {
|
|
||||||
nvgDeleteImage(App::GetVg(), m_image);
|
|
||||||
m_image = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::UpdateImage() {
|
|
||||||
ResetImage();
|
|
||||||
m_image = nvgCreateImageRGBA(App::GetVg(), m_irs_width, m_irs_height, NVG_IMAGE_NEAREST, (const unsigned char*)m_rgba.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::LoadDefaultConfig() {
|
|
||||||
IrsImageTransferProcessorExConfig ex_config;
|
|
||||||
|
|
||||||
if (hosversionAtLeast(4,0,0)) {
|
|
||||||
irsGetDefaultImageTransferProcessorExConfig(&ex_config);
|
|
||||||
} else {
|
|
||||||
IrsImageTransferProcessorConfig nor_config;
|
|
||||||
irsGetDefaultImageTransferProcessorConfig(&nor_config);
|
|
||||||
irsConvertConfigNormalToEx(&nor_config, &ex_config);
|
|
||||||
}
|
|
||||||
|
|
||||||
irsGetMomentProcessorDefaultConfig(&m_moment_config);
|
|
||||||
irsGetClusteringProcessorDefaultConfig(&m_clustering_config);
|
|
||||||
irsGetIrLedProcessorDefaultConfig(&m_led_config);
|
|
||||||
|
|
||||||
m_tera_config = {};
|
|
||||||
m_adaptive_config = {};
|
|
||||||
m_hand_config = {};
|
|
||||||
|
|
||||||
UpdateConfig(&ex_config);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::UpdateConfig(const IrsImageTransferProcessorExConfig* config) {
|
|
||||||
m_config = *config;
|
|
||||||
irsStopImageProcessor(m_entries[m_index].m_handle);
|
|
||||||
|
|
||||||
if (R_FAILED(irsRunMomentProcessor(m_entries[m_index].m_handle, &m_moment_config))) {
|
|
||||||
log_write("failed to irsRunMomentProcessor\n");
|
|
||||||
} else {
|
|
||||||
log_write("did irsRunMomentProcessor\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (R_FAILED(irsRunClusteringProcessor(m_entries[m_index].m_handle, &m_clustering_config))) {
|
|
||||||
log_write("failed to irsRunClusteringProcessor\n");
|
|
||||||
} else {
|
|
||||||
log_write("did irsRunClusteringProcessor\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (R_FAILED(irsRunPointingProcessor(m_entries[m_index].m_handle))) {
|
|
||||||
log_write("failed to irsRunPointingProcessor\n");
|
|
||||||
} else {
|
|
||||||
log_write("did irsRunPointingProcessor\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (R_FAILED(irsRunTeraPluginProcessor(m_entries[m_index].m_handle, &m_tera_config))) {
|
|
||||||
log_write("failed to irsRunTeraPluginProcessor\n");
|
|
||||||
} else {
|
|
||||||
log_write("did irsRunTeraPluginProcessor\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (R_FAILED(irsRunIrLedProcessor(m_entries[m_index].m_handle, &m_led_config))) {
|
|
||||||
log_write("failed to irsRunIrLedProcessor\n");
|
|
||||||
} else {
|
|
||||||
log_write("did irsRunIrLedProcessor\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (R_FAILED(irsRunAdaptiveClusteringProcessor(m_entries[m_index].m_handle, &m_adaptive_config))) {
|
|
||||||
log_write("failed to irsRunAdaptiveClusteringProcessor\n");
|
|
||||||
} else {
|
|
||||||
log_write("did irsRunAdaptiveClusteringProcessor\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (R_FAILED(irsRunHandAnalysis(m_entries[m_index].m_handle, &m_hand_config))) {
|
|
||||||
log_write("failed to irsRunHandAnalysis\n");
|
|
||||||
} else {
|
|
||||||
log_write("did irsRunHandAnalysis\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hosversionAtLeast(4,0,0)) {
|
|
||||||
m_init_rc = irsRunImageTransferExProcessor(m_entries[m_index].m_handle, &m_config, 0x10000000);
|
|
||||||
} else {
|
|
||||||
IrsImageTransferProcessorConfig nor;
|
|
||||||
irsConvertConfigExToNormal(&m_config, &nor);
|
|
||||||
m_init_rc = irsRunImageTransferProcessor(m_entries[m_index].m_handle, &nor, 0x10000000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (R_FAILED(m_init_rc)) {
|
|
||||||
log_write("irs failed to set config!\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto format = m_config.orig_format;
|
|
||||||
log_write("IRS CONFIG\n");
|
|
||||||
log_write("\texposure_time: %lu\n", m_config.exposure_time);
|
|
||||||
log_write("\tlight_target: %u\n", m_config.light_target);
|
|
||||||
log_write("\tgain: %u\n", m_config.gain);
|
|
||||||
log_write("\tis_negative_image_used: %u\n", m_config.is_negative_image_used);
|
|
||||||
log_write("\tlight_target: %u\n", m_config.light_target);
|
|
||||||
if (hosversionAtLeast(4,0,0)) {
|
|
||||||
format = m_config.trimming_format;
|
|
||||||
log_write("\ttrimming_format: %u\n", m_config.trimming_format);
|
|
||||||
log_write("\ttrimming_start_x: %u\n", m_config.trimming_start_x);
|
|
||||||
log_write("\ttrimming_start_y: %u\n", m_config.trimming_start_y);
|
|
||||||
log_write("\tis_external_light_filter_enabled: %u\n", m_config.is_external_light_filter_enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (format) {
|
|
||||||
case IrsImageTransferProcessorFormat_320x240:
|
|
||||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_320x240");
|
|
||||||
m_irs_width = 320;
|
|
||||||
m_irs_height = 240;
|
|
||||||
break;
|
|
||||||
case IrsImageTransferProcessorFormat_160x120:
|
|
||||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_160x120");
|
|
||||||
m_irs_width = 160;
|
|
||||||
m_irs_height = 120;
|
|
||||||
break;
|
|
||||||
case IrsImageTransferProcessorFormat_80x60:
|
|
||||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_80x60");
|
|
||||||
m_irs_width = 80;
|
|
||||||
m_irs_height = 60;
|
|
||||||
break;
|
|
||||||
case IrsImageTransferProcessorFormat_40x30:
|
|
||||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_40x30");
|
|
||||||
m_irs_width = 40;
|
|
||||||
m_irs_height = 30;
|
|
||||||
break;
|
|
||||||
case IrsImageTransferProcessorFormat_20x15:
|
|
||||||
log_write("\tsetting format: %s\n", "IrsImageTransferProcessorFormat_20x15");
|
|
||||||
m_irs_width = 20;
|
|
||||||
m_irs_height = 15;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_rgba.resize(m_irs_width * m_irs_height);
|
|
||||||
m_irs_buffer.resize(m_irs_width * m_irs_height);
|
|
||||||
m_prev_state.sampling_number = UINT64_MAX;
|
|
||||||
std::fill(m_irs_buffer.begin(), m_irs_buffer.end(), 0);
|
|
||||||
updateColourArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Menu::updateColourArray() {
|
|
||||||
const auto ir_width = m_irs_width;
|
|
||||||
const auto ir_height = m_irs_height;
|
|
||||||
const auto colour = m_colour;
|
|
||||||
|
|
||||||
for (u32 y = 0; y < ir_height; y++) {
|
|
||||||
for (u32 x = 0; x < ir_width; x++) {
|
|
||||||
const u32 pos = y * ir_width + x;
|
|
||||||
const u32 pos2 = y * ir_width + x;
|
|
||||||
|
|
||||||
switch (colour) {
|
|
||||||
case Colour_Grey:
|
|
||||||
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], m_irs_buffer[pos2], m_irs_buffer[pos2]);
|
|
||||||
break;
|
|
||||||
case Colour_Ironbow:
|
|
||||||
m_rgba[pos] = iron_palette[m_irs_buffer[pos2]];
|
|
||||||
break;
|
|
||||||
case Colour_Green:
|
|
||||||
m_rgba[pos] = RGBA8_MAXALPHA(0, m_irs_buffer[pos2], 0);
|
|
||||||
break;
|
|
||||||
case Colour_Red:
|
|
||||||
m_rgba[pos] = RGBA8_MAXALPHA(m_irs_buffer[pos2], 0, 0);
|
|
||||||
break;
|
|
||||||
case Colour_Blue:
|
|
||||||
m_rgba[pos] = RGBA8_MAXALPHA(0, 0, m_irs_buffer[pos2]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateImage();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::irs
|
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
#include "ui/menus/main_menu.hpp"
|
#include "ui/menus/main_menu.hpp"
|
||||||
#include "ui/menus/irs_menu.hpp"
|
|
||||||
#include "ui/menus/themezer.hpp"
|
|
||||||
#include "ui/menus/ghdl.hpp"
|
|
||||||
#include "ui/menus/usb_menu.hpp"
|
|
||||||
|
|
||||||
#include "ui/sidebar.hpp"
|
#include "ui/sidebar.hpp"
|
||||||
#include "ui/popup_list.hpp"
|
#include "ui/popup_list.hpp"
|
||||||
@@ -14,7 +10,6 @@
|
|||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
#include "download.hpp"
|
#include "download.hpp"
|
||||||
#include "defines.hpp"
|
#include "defines.hpp"
|
||||||
#include "web.hpp"
|
|
||||||
#include "i18n.hpp"
|
#include "i18n.hpp"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -213,6 +208,9 @@ MainMenu::MainMenu() {
|
|||||||
|
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::START, Action{App::Exit}),
|
std::make_pair(Button::START, Action{App::Exit}),
|
||||||
|
std::make_pair(Button::SELECT, Action{"Misc"_i18n, [this](){
|
||||||
|
App::DisplayMiscOptions();
|
||||||
|
}}),
|
||||||
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
||||||
auto options = std::make_shared<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
|
auto options = std::make_shared<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
@@ -233,48 +231,29 @@ MainMenu::MainMenu() {
|
|||||||
language_items.push_back("Swedish"_i18n);
|
language_items.push_back("Swedish"_i18n);
|
||||||
language_items.push_back("Vietnamese"_i18n);
|
language_items.push_back("Vietnamese"_i18n);
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [](){
|
||||||
SidebarEntryArray::Items theme_items{};
|
App::DisplayThemeOptions();
|
||||||
const auto theme_meta = App::GetThemeMetaList();
|
|
||||||
for (auto& p : theme_meta) {
|
|
||||||
theme_items.emplace_back(p.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
|
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){
|
|
||||||
App::SetTheme(index_out);
|
|
||||||
}, App::GetThemeIndex()));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
|
|
||||||
App::SetThemeMusicEnable(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [this](bool& enable){
|
|
||||||
App::Set12HourTimeEnable(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
|
||||||
auto options = std::make_shared<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
|
auto options = std::make_shared<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [this](bool& enable){
|
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
|
||||||
App::SetFtpEnable(enable);
|
App::SetFtpEnable(enable);
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [this](bool& enable){
|
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
|
||||||
App::SetMtpEnable(enable);
|
App::SetMtpEnable(enable);
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){
|
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
|
||||||
App::SetNxlinkEnable(enable);
|
App::SetNxlinkEnable(enable);
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||||
|
|
||||||
if (m_update_state == UpdateState::Update) {
|
if (m_update_state == UpdateState::Update) {
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
|
||||||
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + m_update_version, [this](auto pbox){
|
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, "Sphaira v" + m_update_version, [this](auto pbox){
|
||||||
return InstallUpdate(pbox, m_update_url, m_update_version);
|
return InstallUpdate(pbox, m_update_url, m_update_version);
|
||||||
}, [this](bool success){
|
}, [this](bool success){
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -293,144 +272,16 @@ MainMenu::MainMenu() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](s64& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [](s64& index_out){
|
||||||
App::SetLanguage(index_out);
|
App::SetLanguage(index_out);
|
||||||
}, (s64)App::GetLanguage()));
|
}, (s64)App::GetLanguage()));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [](){
|
||||||
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
|
App::DisplayMiscOptions();
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Themezer"_i18n, [](){
|
|
||||||
App::Push(std::make_shared<menu::themezer::Menu>());
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
|
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [](){
|
||||||
App::Push(std::make_shared<menu::gh::Menu>());
|
App::DisplayAdvancedOptions();
|
||||||
}));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
|
|
||||||
App::Push(std::make_shared<menu::irs::Menu>());
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (App::IsApplication()) {
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Web"_i18n, [](){
|
|
||||||
WebShow("https://lite.duckduckgo.com/lite");
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (App::GetApp()->m_install.Get()) {
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Usb Install"_i18n, [](){
|
|
||||||
App::Push(std::make_shared<menu::usb::Menu>());
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
|
||||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
|
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
|
||||||
|
|
||||||
SidebarEntryArray::Items text_scroll_speed_items;
|
|
||||||
text_scroll_speed_items.push_back("Slow"_i18n);
|
|
||||||
text_scroll_speed_items.push_back("Normal"_i18n);
|
|
||||||
text_scroll_speed_items.push_back("Fast"_i18n);
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
|
|
||||||
App::SetLogEnable(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [this](bool& enable){
|
|
||||||
App::SetReplaceHbmenuEnable(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
|
|
||||||
App::SetTextScrollSpeed(index_out);
|
|
||||||
}, (s64)App::GetTextScrollSpeed()));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryCallback>("Install options"_i18n, [this](){
|
|
||||||
auto options = std::make_shared<Sidebar>("Install Options"_i18n, Sidebar::Side::LEFT);
|
|
||||||
ON_SCOPE_EXIT(App::Push(options));
|
|
||||||
|
|
||||||
SidebarEntryArray::Items install_items;
|
|
||||||
install_items.push_back("System memory"_i18n);
|
|
||||||
install_items.push_back("microSD card"_i18n);
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [this](bool& enable){
|
|
||||||
App::SetInstallEnable(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
|
||||||
App::SetInstallPrompt(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
|
||||||
App::SetInstallSdEnable(index_out);
|
|
||||||
}, (s64)App::GetInstallSdEnable()));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_allow_downgrade.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_if_already_installed.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_ticket_only.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Patch ticket"_i18n, App::GetApp()->m_patch_ticket.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_patch_ticket.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_base.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_patch.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_addon.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_data_patch.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_ticket.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_ignore_distribution_bit.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_lower_master_key.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [this](bool& enable){
|
|
||||||
App::GetApp()->m_lower_system_version.Set(enable);
|
|
||||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
|
||||||
}));
|
|
||||||
}));
|
}));
|
||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,17 +18,18 @@ MenuBase::~MenuBase() {
|
|||||||
|
|
||||||
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
||||||
Widget::Update(controller, touch);
|
Widget::Update(controller, touch);
|
||||||
|
|
||||||
// update every second.
|
|
||||||
if (m_poll_timestamp.GetSeconds() >= 1) {
|
|
||||||
UpdateVars();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
||||||
Widget::Draw(vg, theme);
|
Widget::Draw(vg, theme);
|
||||||
|
|
||||||
|
// update every second, do this in Draw because Update() isn't called if it
|
||||||
|
// doesn't have focus.
|
||||||
|
if (m_poll_timestamp.GetSeconds() >= 1) {
|
||||||
|
UpdateVars();
|
||||||
|
}
|
||||||
|
|
||||||
const float start_y = 70;
|
const float start_y = 70;
|
||||||
const float font_size = 22;
|
const float font_size = 22;
|
||||||
const float spacing = 30;
|
const float spacing = 30;
|
||||||
|
|||||||
@@ -375,45 +375,6 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
|||||||
}});
|
}});
|
||||||
|
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::RIGHT, Action{[this](){
|
|
||||||
const auto& page = m_pages[m_page_index];
|
|
||||||
if (m_index < (page.m_packList.size() - 1) && (m_index + 1) % 3 != 0) {
|
|
||||||
SetIndex(m_index + 1);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
log_write("moved right\n");
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::LEFT, Action{[this](){
|
|
||||||
if (m_index != 0 && (m_index % 3) != 0) {
|
|
||||||
SetIndex(m_index - 1);
|
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
|
||||||
log_write("moved left\n");
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
|
||||||
const auto& page = m_pages[m_page_index];
|
|
||||||
if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
|
||||||
const auto& page = m_pages[m_page_index];
|
|
||||||
if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::R2, Action{[this](){
|
|
||||||
const auto& page = m_pages[m_page_index];
|
|
||||||
if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::L2, Action{[this](){
|
|
||||||
const auto& page = m_pages[m_page_index];
|
|
||||||
if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
||||||
App::Push(std::make_shared<OptionBox>(
|
App::Push(std::make_shared<OptionBox>(
|
||||||
"Download theme?"_i18n,
|
"Download theme?"_i18n,
|
||||||
@@ -424,7 +385,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
|||||||
const auto& entry = page.m_packList[m_index];
|
const auto& entry = page.m_packList[m_index];
|
||||||
const auto url = apiBuildUrlDownloadPack(entry);
|
const auto url = apiBuildUrlDownloadPack(entry);
|
||||||
|
|
||||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
|
App::Push(std::make_shared<ProgressBox>(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox){
|
||||||
return InstallTheme(pbox, entry);
|
return InstallTheme(pbox, entry);
|
||||||
}, [this, &entry](bool success){
|
}, [this, &entry](bool success){
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -532,8 +493,8 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) {
|
m_list->OnUpdate(controller, touch, m_index, page.m_packList.size(), [this](bool touch, auto i) {
|
||||||
if (m_index == i) {
|
if (touch && m_index == i) {
|
||||||
FireAction(Button::A);
|
FireAction(Button::A);
|
||||||
} else {
|
} else {
|
||||||
App::PlaySoundEffect(SoundEffect_Focus);
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
@@ -642,7 +603,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
|
gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
nvgSave(vg);
|
nvgSave(vg);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
namespace sphaira::ui::menu::usb {
|
namespace sphaira::ui::menu::usb {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3;
|
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 1; // 1 second
|
||||||
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5;
|
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5; // 5 seconds
|
||||||
|
|
||||||
void thread_func(void* user) {
|
void thread_func(void* user) {
|
||||||
auto app = static_cast<Menu*>(user);
|
auto app = static_cast<Menu*>(user);
|
||||||
@@ -21,19 +21,28 @@ void thread_func(void* user) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto rc = app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, app->m_usb_speed, app->m_usb_count);
|
const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT);
|
||||||
|
|
||||||
|
// set connected status
|
||||||
mutexLock(&app->m_mutex);
|
mutexLock(&app->m_mutex);
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
if (R_SUCCEEDED(rc)) {
|
||||||
|
app->m_state = State::Connected_WaitForFileList;
|
||||||
|
} else {
|
||||||
|
app->m_state = State::None;
|
||||||
|
}
|
||||||
|
mutexUnlock(&app->m_mutex);
|
||||||
|
|
||||||
if (R_SUCCEEDED(rc)) {
|
if (R_SUCCEEDED(rc)) {
|
||||||
app->m_state = State::Connected;
|
std::vector<std::string> names;
|
||||||
break;
|
if (R_SUCCEEDED(app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, names))) {
|
||||||
} else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) {
|
mutexLock(&app->m_mutex);
|
||||||
log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc));
|
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
||||||
app->m_state = State::Failed;
|
app->m_state = State::Connected_StartingTransfer;
|
||||||
|
app->m_names = names;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -43,6 +52,10 @@ Menu::Menu() : MenuBase{"USB"_i18n} {
|
|||||||
SetPop();
|
SetPop();
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||||
|
App::DisplayInstallOptions(false);
|
||||||
|
}});
|
||||||
|
|
||||||
// if mtp is enabled, disable it for now.
|
// if mtp is enabled, disable it for now.
|
||||||
m_was_mtp_enabled = App::GetMtpEnable();
|
m_was_mtp_enabled = App::GetMtpEnable();
|
||||||
if (m_was_mtp_enabled) {
|
if (m_was_mtp_enabled) {
|
||||||
@@ -91,46 +104,36 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
|||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
switch (m_state) {
|
if (m_state == State::Connected_StartingTransfer) {
|
||||||
case State::None:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case State::Connected:
|
|
||||||
log_write("set to progress\n");
|
log_write("set to progress\n");
|
||||||
m_state = State::Progress;
|
m_state = State::Progress;
|
||||||
log_write("got connection\n");
|
log_write("got connection\n");
|
||||||
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this](auto pbox) mutable -> bool {
|
App::Push(std::make_shared<ui::ProgressBox>(0, "Installing "_i18n, "", [this](auto pbox) mutable -> bool {
|
||||||
|
ON_SCOPE_EXIT(m_usb_source->Finished());
|
||||||
|
|
||||||
log_write("inside progress box\n");
|
log_write("inside progress box\n");
|
||||||
for (u32 i = 0; i < m_usb_count; i++) {
|
for (const auto& file_name : m_names) {
|
||||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source);
|
m_usb_source->SetFileNameForTranfser(file_name);
|
||||||
|
|
||||||
|
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
|
||||||
if (R_FAILED(rc)) {
|
if (R_FAILED(rc)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
App::Notify("Installed via usb"_i18n);
|
App::Notify("Installed via usb"_i18n);
|
||||||
m_usb_source->Finished();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}, [this](bool result){
|
}, [this](bool result){
|
||||||
if (result) {
|
if (result) {
|
||||||
App::Notify("Usb install success!"_i18n);
|
App::Notify("Usb install success!"_i18n);
|
||||||
|
m_state = State::Done;
|
||||||
|
SetPop();
|
||||||
} else {
|
} else {
|
||||||
App::Notify("Usb install failed!"_i18n);
|
App::Notify("Usb install failed!"_i18n);
|
||||||
|
m_state = State::Failed;
|
||||||
}
|
}
|
||||||
m_state = State::Done;
|
|
||||||
this->SetPop();
|
|
||||||
}));
|
}));
|
||||||
break;
|
|
||||||
|
|
||||||
case State::Progress:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case State::Done:
|
|
||||||
break;
|
|
||||||
|
|
||||||
case State::Failed:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +148,12 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case State::Connected:
|
case State::Connected_WaitForFileList:
|
||||||
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, waiting for file list..."_i18n.c_str());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Connected_StartingTransfer:
|
||||||
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, starting transfer..."_i18n.c_str());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case State::Progress:
|
case State::Progress:
|
||||||
@@ -153,12 +161,11 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case State::Done:
|
case State::Done:
|
||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to Exit..."_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to exit..."_i18n.c_str());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case State::Failed:
|
case State::Failed:
|
||||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb..."_i18n.c_str());
|
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb, press B to exit..."_i18n.c_str());
|
||||||
this->SetPop();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,24 +34,16 @@ constexpr std::array buttons = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// NEW ---------------------
|
// NEW ---------------------
|
||||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
|
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
|
||||||
nvgBeginPath(vg);
|
nvgBeginPath(vg);
|
||||||
if (rounded) {
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
||||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
|
||||||
} else {
|
|
||||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
|
||||||
}
|
|
||||||
nvgFillColor(vg, c);
|
nvgFillColor(vg, c);
|
||||||
nvgFill(vg);
|
nvgFill(vg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
|
||||||
nvgBeginPath(vg);
|
nvgBeginPath(vg);
|
||||||
if (rounded) {
|
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
||||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
|
||||||
} else {
|
|
||||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
|
||||||
}
|
|
||||||
nvgFillPaint(vg, p);
|
nvgFillPaint(vg, p);
|
||||||
nvgFill(vg);
|
nvgFill(vg);
|
||||||
}
|
}
|
||||||
@@ -164,25 +156,13 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const
|
|||||||
drawText(vg, x, y, size, buffer, nullptr, align, c);
|
drawText(vg, x, y, size, buffer, nullptr, align, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawImage(NVGcontext* vg, const Vec4& v, int texture) {
|
void drawImage(NVGcontext* vg, const Vec4& v, int texture, float rounded) {
|
||||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||||
drawRect(vg, v, paint, false);
|
drawRect(vg, v, paint, rounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture) {
|
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture, float rounded) {
|
||||||
drawImage(vg, Vec4(x, y, w, h), texture);
|
drawImage(vg, Vec4(x, y, w, h), texture, rounded);
|
||||||
}
|
|
||||||
|
|
||||||
void drawImageRounded(NVGcontext* vg, const Vec4& v, int texture) {
|
|
||||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
|
||||||
nvgBeginPath(vg);
|
|
||||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
|
||||||
nvgFillPaint(vg, paint);
|
|
||||||
nvgFill(vg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int texture) {
|
|
||||||
drawImageRounded(vg, Vec4(x, y, w, h), texture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
|
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
|
||||||
@@ -208,19 +188,19 @@ void dimBackground(NVGcontext* vg) {
|
|||||||
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
|
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
|
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, float rounded) {
|
||||||
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
|
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
|
||||||
drawRectIntenal(vg, v, c, rounded);
|
drawRectIntenal(vg, v, c, rounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
|
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, float rounded) {
|
||||||
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
|
||||||
drawRectIntenal(vg, v, p, rounded);
|
drawRectIntenal(vg, v, p, rounded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,16 +62,6 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
|
|||||||
, m_callback{cb}
|
, m_callback{cb}
|
||||||
, m_index{index} {
|
, m_index{index} {
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
|
||||||
if (m_list->ScrollDown(m_index, 1, m_items.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
|
||||||
if (m_list->ScrollUp(m_index, 1, m_items.size())) {
|
|
||||||
SetIndex(m_index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
|
std::make_pair(Button::A, Action{"Select"_i18n, [this](){
|
||||||
if (m_callback) {
|
if (m_callback) {
|
||||||
m_callback(m_index);
|
m_callback(m_index);
|
||||||
@@ -103,9 +93,11 @@ PopupList::PopupList(std::string title, Items items, Callback cb, s64 index)
|
|||||||
|
|
||||||
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
|
auto PopupList::Update(Controller* controller, TouchInfo* touch) -> void {
|
||||||
Widget::Update(controller, touch);
|
Widget::Update(controller, touch);
|
||||||
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) {
|
||||||
SetIndex(i);
|
SetIndex(i);
|
||||||
|
if (touch) {
|
||||||
FireAction(Button::A);
|
FireAction(Button::A);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "defines.hpp"
|
#include "defines.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
#include "i18n.hpp"
|
#include "i18n.hpp"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -17,7 +18,7 @@ void threadFunc(void* arg) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
|
ProgressBox::ProgressBox(int image, const std::string& action, const std::string& title, ProgressBoxCallback callback, ProgressBoxDoneCallback done, int cpuid, int prio, int stack_size) {
|
||||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||||
App::Push(std::make_shared<OptionBox>("Are you sure you wish to cancel?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
App::Push(std::make_shared<OptionBox>("Are you sure you wish to cancel?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||||
if (op_index && *op_index) {
|
if (op_index && *op_index) {
|
||||||
@@ -27,11 +28,6 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
|
|||||||
}));
|
}));
|
||||||
}});
|
}});
|
||||||
|
|
||||||
m_pos.w = 770.f;
|
|
||||||
m_pos.h = 430.f;
|
|
||||||
m_pos.x = 255;
|
|
||||||
m_pos.y = 145;
|
|
||||||
|
|
||||||
m_pos.w = 770.f;
|
m_pos.w = 770.f;
|
||||||
m_pos.h = 295.f;
|
m_pos.h = 295.f;
|
||||||
m_pos.x = (SCREEN_WIDTH / 2.f) - (m_pos.w / 2.f);
|
m_pos.x = (SCREEN_WIDTH / 2.f) - (m_pos.w / 2.f);
|
||||||
@@ -39,6 +35,8 @@ ProgressBox::ProgressBox(const std::string& title, ProgressBoxCallback callback,
|
|||||||
|
|
||||||
m_done = done;
|
m_done = done;
|
||||||
m_title = title;
|
m_title = title;
|
||||||
|
m_action = action;
|
||||||
|
m_image = image;
|
||||||
|
|
||||||
m_thread_data.pbox = this;
|
m_thread_data.pbox = this;
|
||||||
m_thread_data.callback = callback;
|
m_thread_data.callback = callback;
|
||||||
@@ -60,6 +58,7 @@ ProgressBox::~ProgressBox() {
|
|||||||
log_write("failed to close thread\n");
|
log_write("failed to close thread\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FreeImage();
|
||||||
m_done(m_thread_data.result);
|
m_done(m_thread_data.result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,12 +72,28 @@ auto ProgressBox::Update(Controller* controller, TouchInfo* touch) -> void {
|
|||||||
|
|
||||||
auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
|
std::vector<u8> image_data{};
|
||||||
|
std::swap(m_image_data, image_data);
|
||||||
|
if (m_timestamp.GetSeconds()) {
|
||||||
|
m_timestamp.Update();
|
||||||
|
m_speed = m_offset - m_last_offset;
|
||||||
|
m_last_offset = m_offset;
|
||||||
|
}
|
||||||
|
|
||||||
const auto title = m_title;
|
const auto title = m_title;
|
||||||
const auto transfer = m_transfer;
|
const auto transfer = m_transfer;
|
||||||
const auto size = m_size;
|
const auto size = m_size;
|
||||||
const auto offset = m_offset;
|
const auto offset = m_offset;
|
||||||
|
const auto speed = m_speed;
|
||||||
|
const auto last_offset = m_last_offset;
|
||||||
mutexUnlock(&m_mutex);
|
mutexUnlock(&m_mutex);
|
||||||
|
|
||||||
|
if (!image_data.empty()) {
|
||||||
|
FreeImage();
|
||||||
|
m_image = nvgCreateImageMem(vg, 0, image_data.data(), image_data.size());
|
||||||
|
m_own_image = true;
|
||||||
|
}
|
||||||
|
|
||||||
gfx::dimBackground(vg);
|
gfx::dimBackground(vg);
|
||||||
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
gfx::drawRect(vg, m_pos, theme->GetColour(ThemeEntryID_POPUP));
|
||||||
|
|
||||||
@@ -86,20 +101,62 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
|||||||
// const Vec4 box = { 255, 145, 770, 430 };
|
// const Vec4 box = { 255, 145, 770, 430 };
|
||||||
const auto center_x = m_pos.x + m_pos.w/2;
|
const auto center_x = m_pos.x + m_pos.w/2;
|
||||||
const auto end_y = m_pos.y + m_pos.h;
|
const auto end_y = m_pos.y + m_pos.h;
|
||||||
const Vec4 prog_bar = { 400, end_y - 80, 480, 12 };
|
const auto progress_bar_w = m_pos.w - 230;
|
||||||
|
const Vec4 prog_bar = { center_x - progress_bar_w / 2, end_y - 100, progress_bar_w, 12 };
|
||||||
|
|
||||||
|
nvgSave(vg);
|
||||||
|
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||||
|
|
||||||
|
if (m_image) {
|
||||||
|
gfx::drawImage(vg, GetX() + 30, GetY() + 30, 128, 128, m_image, 10);
|
||||||
|
}
|
||||||
|
|
||||||
// shapes.
|
// shapes.
|
||||||
if (offset && size) {
|
if (offset && size) {
|
||||||
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND));
|
const auto font_size = 18.F;
|
||||||
|
const auto pad = 15.F;
|
||||||
|
const float rounding = 5;
|
||||||
|
|
||||||
|
gfx::drawRect(vg, prog_bar, theme->GetColour(ThemeEntryID_PROGRESSBAR_BACKGROUND), rounding);
|
||||||
const u32 percentage = ((double)offset / (double)size) * 100.0;
|
const u32 percentage = ((double)offset / (double)size) * 100.0;
|
||||||
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR));
|
gfx::drawRect(vg, prog_bar.x, prog_bar.y, ((float)offset / (float)size) * prog_bar.w, prog_bar.h, theme->GetColour(ThemeEntryID_PROGRESSBAR), rounding);
|
||||||
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + 10, prog_bar.y, 20, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
|
gfx::drawTextArgs(vg, prog_bar.x + prog_bar.w + pad, prog_bar.y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%u%%", percentage);
|
||||||
|
|
||||||
|
const double speed_mb = (double)speed / (1024.0 * 1024.0);
|
||||||
|
const double speed_kb = (double)speed / (1024.0);
|
||||||
|
|
||||||
|
char speed_str[32];
|
||||||
|
if (speed_mb >= 0.01) {
|
||||||
|
std::snprintf(speed_str, sizeof(speed_str), "%.2f MiB/s", speed_mb);
|
||||||
|
} else {
|
||||||
|
std::snprintf(speed_str, sizeof(speed_str), "%.2f KiB/s", speed_kb);
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::drawTextArgs(vg, center_x, m_pos.y + 60, 25, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
|
const auto left = size - last_offset;
|
||||||
if (!transfer.empty()) {
|
const auto left_seconds = left / speed;
|
||||||
gfx::drawTextArgs(vg, center_x, prog_bar.y - 15 - 20 * 1.5F, 20, NVG_ALIGN_CENTER, theme->GetColour(ThemeEntryID_TEXT), "%s", transfer.c_str());
|
const auto hours = left_seconds / (60 * 60);
|
||||||
|
const auto minutes = left_seconds % (60 * 60) / 60;
|
||||||
|
const auto seconds = left_seconds % 60;
|
||||||
|
|
||||||
|
char time_str[64];
|
||||||
|
if (hours) {
|
||||||
|
std::snprintf(time_str, sizeof(time_str), "%zu hours %zu minutes remaining", hours, minutes);
|
||||||
|
} else if (minutes) {
|
||||||
|
std::snprintf(time_str, sizeof(time_str), "%zu minutes %zu seconds remaining", minutes, seconds);
|
||||||
|
} else {
|
||||||
|
std::snprintf(time_str, sizeof(time_str), "%zu seconds remaining", seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gfx::drawTextArgs(vg, center_x, prog_bar.y + prog_bar.h + 30, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s (%s)", time_str, speed_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
gfx::drawTextArgs(vg, center_x, m_pos.y + 40, 24, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), m_action.c_str());
|
||||||
|
gfx::drawTextArgs(vg, center_x, m_pos.y + 100, 22, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), title.c_str());
|
||||||
|
if (!transfer.empty()) {
|
||||||
|
gfx::drawTextArgs(vg, center_x, m_pos.y + 150, 18, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", transfer.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
nvgRestore(vg);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
|
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
|
||||||
@@ -115,6 +172,8 @@ auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
|||||||
m_transfer = transfer;
|
m_transfer = transfer;
|
||||||
m_size = 0;
|
m_size = 0;
|
||||||
m_offset = 0;
|
m_offset = 0;
|
||||||
|
m_last_offset = 0;
|
||||||
|
m_timestamp.Update();
|
||||||
mutexUnlock(&m_mutex);
|
mutexUnlock(&m_mutex);
|
||||||
Yield();
|
Yield();
|
||||||
return *this;
|
return *this;
|
||||||
@@ -129,6 +188,21 @@ auto ProgressBox::UpdateTransfer(s64 offset, s64 size) -> ProgressBox& {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto ProgressBox::SetImageData(std::vector<u8>& data) -> ProgressBox& {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
std::swap(m_image_data, data);
|
||||||
|
mutexUnlock(&m_mutex);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ProgressBox::SetImageDataConst(std::span<const u8> data) -> ProgressBox& {
|
||||||
|
mutexLock(&m_mutex);
|
||||||
|
m_image_data.resize(data.size());
|
||||||
|
std::memcpy(m_image_data.data(), data.data(), m_image_data.size());
|
||||||
|
mutexUnlock(&m_mutex);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
void ProgressBox::RequestExit() {
|
void ProgressBox::RequestExit() {
|
||||||
m_stop_source.request_stop();
|
m_stop_source.request_stop();
|
||||||
}
|
}
|
||||||
@@ -184,4 +258,13 @@ void ProgressBox::Yield() {
|
|||||||
svcSleepThread(YieldType_WithoutCoreMigration);
|
svcSleepThread(YieldType_WithoutCoreMigration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProgressBox::FreeImage() {
|
||||||
|
if (m_image && m_own_image) {
|
||||||
|
nvgDeleteImage(App::GetVg(), m_image);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_image = 0;
|
||||||
|
m_own_image = false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
@@ -259,9 +259,11 @@ auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
|
|||||||
if (touch->is_clicked && !touch->in_range(GetPos())) {
|
if (touch->is_clicked && !touch->in_range(GetPos())) {
|
||||||
App::PopToMenu();
|
App::PopToMenu();
|
||||||
} else {
|
} else {
|
||||||
m_list->OnUpdate(controller, touch, m_items.size(), [this](auto i) {
|
m_list->OnUpdate(controller, touch, m_index, m_items.size(), [this](bool touch, auto i) {
|
||||||
SetIndex(i);
|
SetIndex(i);
|
||||||
|
if (touch) {
|
||||||
FireAction(Button::A);
|
FireAction(Button::A);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,18 +336,6 @@ void Sidebar::SetupButtons() {
|
|||||||
|
|
||||||
// add default actions, overriding if needed.
|
// add default actions, overriding if needed.
|
||||||
this->SetActions(
|
this->SetActions(
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
|
||||||
auto index = m_index;
|
|
||||||
if (m_list->ScrollDown(index, 1, m_items.size())) {
|
|
||||||
SetIndex(index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
std::make_pair(Button::UP, Action{[this](){
|
|
||||||
auto index = m_index;
|
|
||||||
if (m_list->ScrollUp(index, 1, m_items.size())) {
|
|
||||||
SetIndex(index);
|
|
||||||
}
|
|
||||||
}}),
|
|
||||||
// each item has it's own Action, but we take over B
|
// each item has it's own Action, but we take over B
|
||||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
SetPop();
|
SetPop();
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
#include "web.hpp"
|
|
||||||
#include "log.hpp"
|
|
||||||
#include "defines.hpp"
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
namespace sphaira {
|
|
||||||
|
|
||||||
auto WebShow(const std::string& url, bool show_error) -> Result {
|
|
||||||
// showError("Running in applet mode\nPlease launch hbmenu by holding R on an APP (e.g. a game) NOT an applet (e.g. Gallery)", "", 0);
|
|
||||||
// showError("Error: Nag active, check more details", "Browser won't launch if supernag is active\n\nUse gagorder or switch-sys-tweak (the latter is bundled with BrowseNX) to disable supernag.", 0);
|
|
||||||
// log_write("web show with url: %s\n", url.c_str());
|
|
||||||
// return 0;
|
|
||||||
WebCommonConfig config{};
|
|
||||||
WebCommonReply reply{};
|
|
||||||
WebExitReason reason{};
|
|
||||||
AccountUid account_uid{};
|
|
||||||
char last_url[FS_MAX_PATH]{};
|
|
||||||
size_t last_url_len{};
|
|
||||||
|
|
||||||
// WebBackgroundKind_Unknown1 = shows background
|
|
||||||
// WebBackgroundKind_Unknown2 = shows background faded
|
|
||||||
if (R_FAILED(accountTrySelectUserWithoutInteraction(&account_uid, false))) { log_write("failed: accountTrySelectUserWithoutInteraction\n"); }
|
|
||||||
if (R_FAILED(webPageCreate(&config, url.c_str()))) { log_write("failed: webPageCreate\n"); }
|
|
||||||
if (R_FAILED(webConfigSetWhitelist(&config, "^http"))) { log_write("failed: webConfigSetWhitelist\n"); }
|
|
||||||
if (R_FAILED(webConfigSetEcClientCert(&config, true))) { log_write("failed: webConfigSetEcClientCert\n"); }
|
|
||||||
if (R_FAILED(webConfigSetScreenShot(&config, true))) { log_write("failed: webConfigSetScreenShot\n"); }
|
|
||||||
if (R_FAILED(webConfigSetBootDisplayKind(&config, WebBootDisplayKind_Black))) { log_write("failed: webConfigSetBootDisplayKind\n"); }
|
|
||||||
if (R_FAILED(webConfigSetBackgroundKind(&config, WebBackgroundKind_Default))) { log_write("failed: webConfigSetBackgroundKind\n"); }
|
|
||||||
if (R_FAILED(webConfigSetPointer(&config, true))) { log_write("failed: webConfigSetPointer\n"); }
|
|
||||||
if (R_FAILED(webConfigSetLeftStickMode(&config, WebLeftStickMode_Pointer))) { log_write("failed: webConfigSetLeftStickMode\n"); }
|
|
||||||
// if (R_FAILED(webConfigSetBootAsMediaPlayer(&config, true))) { log_write("failed: webConfigSetBootAsMediaPlayer\n"); }
|
|
||||||
if (R_FAILED(webConfigSetJsExtension(&config, true))) { log_write("failed: webConfigSetJsExtension\n"); }
|
|
||||||
if (R_FAILED(webConfigSetMediaPlayerAutoClose(&config, true))) { log_write("failed: webConfigSetMediaPlayerAutoClose\n"); }
|
|
||||||
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
|
|
||||||
if (R_FAILED(webConfigSetFooterFixedKind(&config, WebFooterFixedKind_Hidden))) { log_write("failed: webConfigSetFooterFixedKind\n"); }
|
|
||||||
if (R_FAILED(webConfigSetPageFade(&config, true))) { log_write("failed: webConfigSetPageFade\n"); }
|
|
||||||
if (R_FAILED(webConfigSetPageScrollIndicator(&config, true))) { log_write("failed: webConfigSetPageScrollIndicator\n"); }
|
|
||||||
// if (R_FAILED(webConfigSetMediaPlayerSpeedControl(&config, true))) { log_write("failed: webConfigSetMediaPlayerSpeedControl\n"); }
|
|
||||||
if (R_FAILED(webConfigSetBootMode(&config, WebSessionBootMode_AllForeground))) { log_write("failed: webConfigSetBootMode\n"); }
|
|
||||||
if (R_FAILED(webConfigSetTransferMemory(&config, true))) { log_write("failed: webConfigSetTransferMemory\n"); }
|
|
||||||
if (R_FAILED(webConfigSetTouchEnabledOnContents(&config, true))) { log_write("failed: webConfigSetTouchEnabledOnContents\n"); }
|
|
||||||
// if (R_FAILED(webConfigSetMediaPlayerUi(&config, true))) { log_write("failed: webConfigSetMediaPlayerUi\n"); }
|
|
||||||
// if (R_FAILED(webConfigSetWebAudio(&config, true))) { log_write("failed: webConfigSetWebAudio\n"); }
|
|
||||||
if (R_FAILED(webConfigSetPageCache(&config, true))) { log_write("failed: webConfigSetPageCache\n"); }
|
|
||||||
if (R_FAILED(webConfigSetBootLoadingIcon(&config, true))) { log_write("failed: webConfigSetBootLoadingIcon\n"); }
|
|
||||||
if (R_FAILED(webConfigSetUid(&config, account_uid))) { log_write("failed: webConfigSetUid\n"); }
|
|
||||||
if (R_FAILED(webConfigShow(&config, &reply))) { log_write("failed: webConfigShow\n"); }
|
|
||||||
if (R_FAILED(webReplyGetExitReason(&reply, &reason))) { log_write("failed: webReplyGetExitReason\n"); }
|
|
||||||
if (R_FAILED(webReplyGetLastUrl(&reply, last_url, sizeof(last_url), &last_url_len))) { log_write("failed: webReplyGetLastUrl\n"); }
|
|
||||||
log_write("last url: %s\n", last_url);
|
|
||||||
R_SUCCEED();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace sphaira
|
|
||||||
@@ -24,14 +24,6 @@ struct Pfs0FileTableEntry {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Result Nsp::Validate(source::Base* source) {
|
|
||||||
u32 magic;
|
|
||||||
u64 bytes_read;
|
|
||||||
R_TRY(source->Read(std::addressof(magic), 0, sizeof(magic), std::addressof(bytes_read)));
|
|
||||||
R_UNLESS(magic == PFS0_MAGIC, 0x1);
|
|
||||||
R_SUCCEED();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result Nsp::GetCollections(Collections& out) {
|
Result Nsp::GetCollections(Collections& out) {
|
||||||
u64 bytes_read;
|
u64 bytes_read;
|
||||||
s64 off = 0;
|
s64 off = 0;
|
||||||
|
|||||||
@@ -60,22 +60,14 @@ Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Result Xci::Validate(source::Base* source) {
|
|
||||||
u32 magic;
|
|
||||||
u64 bytes_read;
|
|
||||||
R_TRY(source->Read(std::addressof(magic), 0x100, sizeof(magic), std::addressof(bytes_read)));
|
|
||||||
R_UNLESS(magic == XCI_MAGIC, 0x1);
|
|
||||||
R_SUCCEED();
|
|
||||||
}
|
|
||||||
|
|
||||||
Result Xci::GetCollections(Collections& out) {
|
Result Xci::GetCollections(Collections& out) {
|
||||||
Hfs0 root{};
|
Hfs0 root{};
|
||||||
R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root));
|
R_TRY(Hfs0GetPartition(m_source.get(), HFS0_HEADER_OFFSET, root));
|
||||||
|
|
||||||
for (u32 i = 0; i < root.header.total_files; i++) {
|
for (u32 i = 0; i < root.header.total_files; i++) {
|
||||||
if (root.string_table[i] == "secure") {
|
if (root.string_table[i] == "secure") {
|
||||||
Hfs0 secure{};
|
Hfs0 secure{};
|
||||||
R_TRY(Hfs0GetPartition(m_source, root.data_offset + root.file_table[i].data_offset, secure));
|
R_TRY(Hfs0GetPartition(m_source.get(), root.data_offset + root.file_table[i].data_offset, secure));
|
||||||
|
|
||||||
for (u32 i = 0; i < secure.header.total_files; i++) {
|
for (u32 i = 0; i < secure.header.total_files; i++) {
|
||||||
CollectionEntry entry;
|
CollectionEntry entry;
|
||||||
|
|||||||
@@ -106,13 +106,13 @@ Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys)
|
|||||||
|
|
||||||
// todo: i thought i already wrote the code for this??
|
// todo: i thought i already wrote the code for this??
|
||||||
// todo: patch the ticket.
|
// todo: patch the ticket.
|
||||||
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised) {
|
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys) {
|
||||||
TicketData data;
|
TicketData data;
|
||||||
R_TRY(GetTicketData(ticket, &data));
|
R_TRY(GetTicketData(ticket, &data));
|
||||||
|
|
||||||
if (data.title_key_type == es::TicketTitleKeyType_Common) {
|
if (data.title_key_type == es::TicketTitleKeyType_Common) {
|
||||||
// todo: verify common signature
|
// todo: verify common signature
|
||||||
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized && convert_personalised) {
|
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,4 +151,30 @@ Result VerifyFixedKey(const Header& header) {
|
|||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto GetKeyGenStr(u8 key_gen) -> const char* {
|
||||||
|
switch (key_gen) {
|
||||||
|
case KeyGenerationOld_100: return "1.0.0";
|
||||||
|
case KeyGenerationOld_300: return "3.0.0";
|
||||||
|
case KeyGeneration_301: return "3.0.1";
|
||||||
|
case KeyGeneration_400: return "4.0.0";
|
||||||
|
case KeyGeneration_500: return "5.0.0";
|
||||||
|
case KeyGeneration_600: return "6.0.0";
|
||||||
|
case KeyGeneration_620: return "6.2.0";
|
||||||
|
case KeyGeneration_700: return "7.0.0";
|
||||||
|
case KeyGeneration_810: return "8.1.0";
|
||||||
|
case KeyGeneration_900: return "9.0.0";
|
||||||
|
case KeyGeneration_910: return "9.1.0";
|
||||||
|
case KeyGeneration_1210: return "12.1.0";
|
||||||
|
case KeyGeneration_1300: return "13.0.0";
|
||||||
|
case KeyGeneration_1400: return "14.0.0";
|
||||||
|
case KeyGeneration_1500: return "15.0.0";
|
||||||
|
case KeyGeneration_1600: return "16.0.0";
|
||||||
|
case KeyGeneration_1700: return "17.0.0";
|
||||||
|
case KeyGeneration_1800: return "18.0.0";
|
||||||
|
case KeyGeneration_1900: return "19.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sphaira::nca
|
} // namespace sphaira::nca
|
||||||
|
|||||||
@@ -7,16 +7,24 @@ namespace {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
|
auto GetAppId(u8 meta_type, u64 id) -> u64 {
|
||||||
if (key.type == NcmContentMetaType_Patch) {
|
if (meta_type == NcmContentMetaType_Patch) {
|
||||||
return key.id ^ 0x800;
|
return id ^ 0x800;
|
||||||
} else if (key.type == NcmContentMetaType_AddOnContent) {
|
} else if (meta_type == NcmContentMetaType_AddOnContent) {
|
||||||
return (key.id ^ 0x1000) & ~0xFFF;
|
return (id ^ 0x1000) & ~0xFFF;
|
||||||
} else {
|
} else {
|
||||||
return key.id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
|
||||||
|
return GetAppId(key.type, key.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetAppId(const PackagedContentMeta& meta) -> u64 {
|
||||||
|
return GetAppId(meta.meta_type, meta.title_id);
|
||||||
|
}
|
||||||
|
|
||||||
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
|
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
|
||||||
bool has;
|
bool has;
|
||||||
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));
|
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace sphaira::yati::source {
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
Stdio::Stdio(const char* path) {
|
Stdio::Stdio(const fs::FsPath& path) {
|
||||||
m_file = std::fopen(path, "rb");
|
m_file = std::fopen(path, "rb");
|
||||||
if (!m_file) {
|
if (!m_file) {
|
||||||
m_open_result = fsdevGetLastResult();
|
m_open_result = fsdevGetLastResult();
|
||||||
|
|||||||
41
sphaira/source/yati/source/stream.cpp
Normal file
41
sphaira/source/yati/source/stream.cpp
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#include "yati/source/stream.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
Result Stream::Read(void* _buf, s64 off, s64 size, u64* bytes_read_out) {
|
||||||
|
// streams don't allow for random access (seeking backwards).
|
||||||
|
R_UNLESS(off >= m_offset, 0x1);
|
||||||
|
|
||||||
|
auto buf = static_cast<u8*>(_buf);
|
||||||
|
*bytes_read_out = 0;
|
||||||
|
|
||||||
|
// check if we already have some data in the buffer.
|
||||||
|
while (size) {
|
||||||
|
// while it is invalid to seek backwards, it is valid to seek forwards.
|
||||||
|
// this can be done to skip padding, skip undeeded files etc.
|
||||||
|
// to handle this, simply read the data into a buffer and discard it.
|
||||||
|
if (off > m_offset) {
|
||||||
|
const auto skip_size = off - m_offset;
|
||||||
|
std::vector<u8> temp_buf(skip_size);
|
||||||
|
u64 bytes_read;
|
||||||
|
R_TRY(ReadChunk(temp_buf.data(), temp_buf.size(), &bytes_read));
|
||||||
|
|
||||||
|
m_offset += bytes_read;
|
||||||
|
} else {
|
||||||
|
u64 bytes_read;
|
||||||
|
R_TRY(ReadChunk(buf, size, &bytes_read));
|
||||||
|
|
||||||
|
*bytes_read_out += bytes_read;
|
||||||
|
buf += bytes_read;
|
||||||
|
off += bytes_read;
|
||||||
|
m_offset += bytes_read;
|
||||||
|
size -= bytes_read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
23
sphaira/source/yati/source/stream_file.cpp
Normal file
23
sphaira/source/yati/source/stream_file.cpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#include "yati/source/stream_file.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
StreamFile::StreamFile(FsFileSystem* fs, const fs::FsPath& path) {
|
||||||
|
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamFile::~StreamFile() {
|
||||||
|
if (R_SUCCEEDED(GetOpenResult())) {
|
||||||
|
fsFileClose(std::addressof(m_file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result StreamFile::ReadChunk(void* buf, s64 size, u64* bytes_read) {
|
||||||
|
R_TRY(GetOpenResult());
|
||||||
|
const auto rc = fsFileRead(std::addressof(m_file), m_offset, buf, size, 0, bytes_read);
|
||||||
|
m_offset += *bytes_read;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
@@ -14,33 +14,58 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Most of the usb transfer code was taken from Haze.
|
// The USB transfer code was taken from Haze (part of Atmosphere).
|
||||||
|
// The USB protocol was taken from Tinfoil, by Adubbz.
|
||||||
|
|
||||||
#include "yati/source/usb.hpp"
|
#include "yati/source/usb.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
namespace sphaira::yati::source {
|
namespace sphaira::yati::source {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr u32 MAGIC = 0x53504841;
|
enum USBCmdType : u8 {
|
||||||
constexpr u32 VERSION = 1;
|
REQUEST = 0,
|
||||||
|
RESPONSE = 1
|
||||||
struct SendHeader {
|
|
||||||
u32 magic;
|
|
||||||
u32 version;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RecvHeader {
|
enum USBCmdId : u32 {
|
||||||
u32 magic;
|
EXIT = 0,
|
||||||
u32 version;
|
FILE_RANGE = 1
|
||||||
u32 bcdUSB;
|
|
||||||
u32 count;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct NX_PACKED USBCmdHeader {
|
||||||
|
u32 magic;
|
||||||
|
USBCmdType type;
|
||||||
|
u8 padding[0x3] = {0};
|
||||||
|
u32 cmdId;
|
||||||
|
u64 dataSize;
|
||||||
|
u8 reserved[0xC] = {0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileRangeCmdHeader {
|
||||||
|
u64 size;
|
||||||
|
u64 offset;
|
||||||
|
u64 nspNameLen;
|
||||||
|
u64 padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TUSHeader {
|
||||||
|
u32 magic; // TUL0 (Tinfoil Usb List 0)
|
||||||
|
u32 nspListSize;
|
||||||
|
u64 padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
|
||||||
|
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Usb::Usb(u64 transfer_timeout) {
|
Usb::Usb(u64 transfer_timeout) {
|
||||||
m_open_result = usbDsInitialize();
|
m_open_result = usbDsInitialize();
|
||||||
m_transfer_timeout = transfer_timeout;
|
m_transfer_timeout = transfer_timeout;
|
||||||
|
// this avoids allocations during transfers.
|
||||||
|
m_aligned.reserve(1024 * 1024 * 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
Usb::~Usb() {
|
Usb::~Usb() {
|
||||||
@@ -53,6 +78,11 @@ Result Usb::Init() {
|
|||||||
log_write("doing USB init\n");
|
log_write("doing USB init\n");
|
||||||
R_TRY(m_open_result);
|
R_TRY(m_open_result);
|
||||||
|
|
||||||
|
SetSysSerialNumber serial_number;
|
||||||
|
R_TRY(setsysInitialize());
|
||||||
|
ON_SCOPE_EXIT(setsysExit());
|
||||||
|
R_TRY(setsysGetSerialNumber(&serial_number));
|
||||||
|
|
||||||
u8 iManufacturer, iProduct, iSerialNumber;
|
u8 iManufacturer, iProduct, iSerialNumber;
|
||||||
static const u16 supported_langs[1] = {0x0409};
|
static const u16 supported_langs[1] = {0x0409};
|
||||||
// Send language descriptor
|
// Send language descriptor
|
||||||
@@ -62,7 +92,7 @@ Result Usb::Init() {
|
|||||||
// Send product
|
// Send product
|
||||||
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
||||||
// Send serial number
|
// Send serial number
|
||||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
|
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, serial_number.number));
|
||||||
|
|
||||||
// Send device descriptors
|
// Send device descriptors
|
||||||
struct usb_device_descriptor device_descriptor = {
|
struct usb_device_descriptor device_descriptor = {
|
||||||
@@ -131,13 +161,11 @@ Result Usb::Init() {
|
|||||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||||
.bDescriptorType = USB_DT_ENDPOINT,
|
.bDescriptorType = USB_DT_ENDPOINT,
|
||||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||||
.wMaxPacketSize = 0x40,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||||
@@ -145,7 +173,6 @@ Result Usb::Init() {
|
|||||||
.bDescriptorType = USB_DT_ENDPOINT,
|
.bDescriptorType = USB_DT_ENDPOINT,
|
||||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||||
.wMaxPacketSize = 0x40,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||||
@@ -163,6 +190,8 @@ Result Usb::Init() {
|
|||||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||||
|
|
||||||
// Full Speed Config
|
// Full Speed Config
|
||||||
|
endpoint_descriptor_in.wMaxPacketSize = 0x40;
|
||||||
|
endpoint_descriptor_out.wMaxPacketSize = 0x40;
|
||||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||||
@@ -194,40 +223,38 @@ Result Usb::Init() {
|
|||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
|
Result Usb::IsUsbConnected(u64 timeout) const {
|
||||||
const SendHeader send_header{
|
return usbDsWaitReady(timeout);
|
||||||
.magic = MAGIC,
|
}
|
||||||
.version = VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
alignas(0x1000) u8 aligned[0x1000]{};
|
Result Usb::WaitForConnection(u64 timeout, std::vector<std::string>& out_names) {
|
||||||
std::memcpy(aligned, std::addressof(send_header), sizeof(send_header));
|
TUSHeader header;
|
||||||
|
R_TRY(TransferAll(true, &header, sizeof(header), timeout));
|
||||||
|
R_UNLESS(header.magic == 0x304C5554, Result_BadMagic);
|
||||||
|
R_UNLESS(header.nspListSize > 0, Result_BadCount);
|
||||||
|
log_write("USB got header\n");
|
||||||
|
|
||||||
// send header.
|
std::vector<char> names(header.nspListSize);
|
||||||
u32 transferredSize;
|
R_TRY(TransferAll(true, names.data(), names.size(), timeout));
|
||||||
R_TRY(TransferPacketImpl(false, aligned, sizeof(send_header), &transferredSize, timeout));
|
|
||||||
|
|
||||||
// receive header.
|
out_names.clear();
|
||||||
struct RecvHeader recv_header{};
|
for (const auto& name : std::views::split(names, '\n')) {
|
||||||
R_TRY(TransferPacketImpl(true, aligned, sizeof(recv_header), &transferredSize, timeout));
|
if (!name.empty()) {
|
||||||
|
out_names.emplace_back(name.data(), name.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// copy data into header struct.
|
for (auto& name : out_names) {
|
||||||
std::memcpy(&recv_header, aligned, sizeof(recv_header));
|
log_write("got name: %s\n", name.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// validate received header.
|
R_UNLESS(!out_names.empty(), Result_BadCount);
|
||||||
R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic);
|
log_write("USB SUCCESS\n");
|
||||||
R_UNLESS(recv_header.version == VERSION, Result_BadVersion);
|
|
||||||
R_UNLESS(recv_header.count > 0, Result_BadCount);
|
|
||||||
|
|
||||||
count = recv_header.count;
|
|
||||||
speed = recv_header.bcdUSB;
|
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Usb::GetConfigured() const {
|
void Usb::SetFileNameForTranfser(const std::string& name) {
|
||||||
UsbState usb_state;
|
m_transfer_file_name = name;
|
||||||
usbDsGetState(std::addressof(usb_state));
|
|
||||||
return usb_state == UsbState_Configured;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
|
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
|
||||||
@@ -264,12 +291,7 @@ Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_tr
|
|||||||
u32 urb_id;
|
u32 urb_id;
|
||||||
|
|
||||||
/* If we're not configured yet, wait to become configured first. */
|
/* If we're not configured yet, wait to become configured first. */
|
||||||
// R_TRY(usbDsWaitReady(timeout));
|
R_TRY(IsUsbConnected(timeout));
|
||||||
if (!GetConfigured()) {
|
|
||||||
R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout));
|
|
||||||
R_TRY(eventClear(usbDsGetStateChangeEvent()));
|
|
||||||
R_THROW(0xEA01);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Select the appropriate endpoint and begin a transfer. */
|
/* Select the appropriate endpoint and begin a transfer. */
|
||||||
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
||||||
@@ -282,78 +304,73 @@ Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_tr
|
|||||||
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
|
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result Usb::SendCommand(s64 off, s64 size) const {
|
// while it may seem like a bad idea to transfer data to a buffer and copy it
|
||||||
struct {
|
// in practice, this has no impact on performance.
|
||||||
u32 hash;
|
// the switch is *massively* bottlenecked by slow io (nand and sd).
|
||||||
u32 magic;
|
// so making usb transfers zero-copy provides no benefit other than increased
|
||||||
s64 off;
|
// code complexity and the increase of future bugs if/when sphaira is forked
|
||||||
s64 size;
|
// an changes are made.
|
||||||
} meta{0, 0, off, size};
|
// yati already goes to great lengths to be zero-copy during installing
|
||||||
|
// by swapping buffers and inflating in-place.
|
||||||
alignas(0x1000) static u8 aligned[0x1000]{};
|
Result Usb::TransferAll(bool read, void *data, u32 size, u64 timeout) {
|
||||||
std::memcpy(aligned, std::addressof(meta), sizeof(meta));
|
auto buf = static_cast<u8*>(data);
|
||||||
|
m_aligned.resize((size + 0xFFF) & ~0xFFF);
|
||||||
u32 transferredSize;
|
|
||||||
return TransferPacketImpl(false, aligned, sizeof(meta), &transferredSize, m_transfer_timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result Usb::Finished() const {
|
|
||||||
return SendCommand(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result Usb::InternalRead(void* _buf, s64 off, s64 size) const {
|
|
||||||
u8* buf = (u8*)_buf;
|
|
||||||
alignas(0x1000) u8 aligned[0x1000]{};
|
|
||||||
const auto stored_size = size;
|
|
||||||
s64 total = 0;
|
|
||||||
|
|
||||||
while (size) {
|
while (size) {
|
||||||
auto read_size = size;
|
if (!read) {
|
||||||
auto read_buf = buf;
|
std::memcpy(m_aligned.data(), buf, size);
|
||||||
|
|
||||||
if (u64(buf) & 0xFFF) {
|
|
||||||
read_size = std::min<u64>(size, sizeof(aligned) - (u64(buf) & 0xFFF));
|
|
||||||
read_buf = aligned;
|
|
||||||
log_write("unaligned read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
|
||||||
} else if (read_size & 0xFFF) {
|
|
||||||
if (read_size <= 0xFFF) {
|
|
||||||
log_write("unaligned small read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
|
||||||
read_buf = aligned;
|
|
||||||
} else {
|
|
||||||
log_write("unaligned big read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
|
||||||
// read as much as possible into buffer, the rest will
|
|
||||||
// be handled in a second read which will be aligned size aligned.
|
|
||||||
read_size = read_size & ~0xFFF;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
R_TRY(SendCommand(off, read_size));
|
u32 out_size_transferred;
|
||||||
|
R_TRY(TransferPacketImpl(read, m_aligned.data(), size, &out_size_transferred, timeout));
|
||||||
|
|
||||||
u32 transferredSize{};
|
if (read) {
|
||||||
R_TRY(TransferPacketImpl(true, read_buf, read_size, &transferredSize, m_transfer_timeout));
|
std::memcpy(buf, m_aligned.data(), out_size_transferred);
|
||||||
R_UNLESS(transferredSize <= read_size, Result_BadTransferSize);
|
|
||||||
|
|
||||||
if (read_buf == aligned) {
|
|
||||||
std::memcpy(buf, aligned, transferredSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transferredSize < read_size) {
|
buf += out_size_transferred;
|
||||||
log_write("reading less than expected! %u vs %zd stored: %zd\n", transferredSize, read_size, stored_size);
|
size -= out_size_transferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
off += transferredSize;
|
|
||||||
buf += transferredSize;
|
|
||||||
size -= transferredSize;
|
|
||||||
total += transferredSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
R_UNLESS(total == stored_size, Result_BadTotalSize);
|
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result Usb::SendCmdHeader(u32 cmdId, size_t dataSize) {
|
||||||
|
USBCmdHeader header{
|
||||||
|
.magic = 0x30435554, // TUC0 (Tinfoil USB Command 0)
|
||||||
|
.type = USBCmdType::REQUEST,
|
||||||
|
.cmdId = cmdId,
|
||||||
|
.dataSize = dataSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
return TransferAll(false, &header, sizeof(header), m_transfer_timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Usb::SendFileRangeCmd(u64 off, u64 size) {
|
||||||
|
FileRangeCmdHeader fRangeHeader;
|
||||||
|
fRangeHeader.size = size;
|
||||||
|
fRangeHeader.offset = off;
|
||||||
|
fRangeHeader.nspNameLen = m_transfer_file_name.size();
|
||||||
|
fRangeHeader.padding = 0;
|
||||||
|
|
||||||
|
R_TRY(SendCmdHeader(USBCmdId::FILE_RANGE, sizeof(fRangeHeader) + fRangeHeader.nspNameLen));
|
||||||
|
R_TRY(TransferAll(false, &fRangeHeader, sizeof(fRangeHeader), m_transfer_timeout));
|
||||||
|
R_TRY(TransferAll(false, m_transfer_file_name.data(), m_transfer_file_name.size(), m_transfer_timeout));
|
||||||
|
|
||||||
|
USBCmdHeader responseHeader;
|
||||||
|
R_TRY(TransferAll(true, &responseHeader, sizeof(responseHeader), m_transfer_timeout));
|
||||||
|
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Usb::Finished() {
|
||||||
|
return SendCmdHeader(USBCmdId::EXIT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||||
R_TRY(GetOpenResult());
|
R_TRY(GetOpenResult());
|
||||||
R_TRY(InternalRead(buf, off, size));
|
R_TRY(SendFileRangeCmd(off, size));
|
||||||
|
R_TRY(TransferAll(true, buf, size, m_transfer_timeout));
|
||||||
*bytes_read = size;
|
*bytes_read = size;
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1
tools/requirements.txt
Normal file
1
tools/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pyusb
|
||||||
171
tools/usb_install_pc.py
Normal file
171
tools/usb_install_pc.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# This script depends on PyUSB. You can get it with pip install pyusb.
|
||||||
|
# You will also need libusb installed
|
||||||
|
|
||||||
|
# My sincere apologies for this process being overly complicated. Apparently Python and Windows
|
||||||
|
# aren't very friendly :(
|
||||||
|
# Windows Instructions:
|
||||||
|
# 1. Download Zadig from https://zadig.akeo.ie/.
|
||||||
|
# 2. With your switch plugged in and on the Tinfoil USB install menu,
|
||||||
|
# choose "List All Devices" under the options menu in Zadig, and select libnx USB comms.
|
||||||
|
# 3. Choose libusbK from the driver list and click the "Replace Driver" button.
|
||||||
|
# 4. Run this script
|
||||||
|
|
||||||
|
# macOS Instructions:
|
||||||
|
# 1. Install Homebrew https://brew.sh
|
||||||
|
# 2. Install Python 3
|
||||||
|
# sudo mkdir /usr/local/Frameworks
|
||||||
|
# sudo chown $(whoami) /usr/local/Frameworks
|
||||||
|
# brew install python
|
||||||
|
# 3. Install PyUSB
|
||||||
|
# pip3 install pyusb
|
||||||
|
# 4. Install libusb
|
||||||
|
# brew install libusb
|
||||||
|
# 5. Plug in your Switch and go to Tinfoil > Title Management > USB Install NSP
|
||||||
|
# 6. Run this script
|
||||||
|
# python3 usb_install_pc.py <path/to/nsp_folder>
|
||||||
|
|
||||||
|
import usb.core
|
||||||
|
import usb.util
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
import time
|
||||||
|
|
||||||
|
CMD_ID_EXIT = 0
|
||||||
|
CMD_ID_FILE_RANGE = 1
|
||||||
|
|
||||||
|
CMD_TYPE_RESPONSE = 1
|
||||||
|
|
||||||
|
# list of supported extensions.
|
||||||
|
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
|
||||||
|
|
||||||
|
def send_response_header(out_ep, cmd_id, data_size):
|
||||||
|
out_ep.write(b'TUC0') # Tinfoil USB Command 0
|
||||||
|
out_ep.write(struct.pack('<B', CMD_TYPE_RESPONSE))
|
||||||
|
out_ep.write(b'\x00' * 3)
|
||||||
|
out_ep.write(struct.pack('<I', cmd_id))
|
||||||
|
out_ep.write(struct.pack('<Q', data_size))
|
||||||
|
out_ep.write(b'\x00' * 0xC)
|
||||||
|
|
||||||
|
def file_range_cmd(nsp_dir, in_ep, out_ep, data_size):
|
||||||
|
file_range_header = in_ep.read(0x20)
|
||||||
|
|
||||||
|
range_size = struct.unpack('<Q', file_range_header[:8])[0]
|
||||||
|
range_offset = struct.unpack('<Q', file_range_header[8:16])[0]
|
||||||
|
nsp_name_len = struct.unpack('<Q', file_range_header[16:24])[0]
|
||||||
|
#in_ep.read(0x8) # Reserved
|
||||||
|
nsp_name = bytes(in_ep.read(nsp_name_len)).decode('utf-8')
|
||||||
|
|
||||||
|
print('Range Size: {}, Range Offset: {}, Name len: {}, Name: {}'.format(range_size, range_offset, nsp_name_len, nsp_name))
|
||||||
|
send_response_header(out_ep, CMD_ID_FILE_RANGE, range_size)
|
||||||
|
|
||||||
|
with open(nsp_name, 'rb') as f:
|
||||||
|
f.seek(range_offset)
|
||||||
|
|
||||||
|
curr_off = 0x0
|
||||||
|
end_off = range_size
|
||||||
|
read_size = 0x800000
|
||||||
|
|
||||||
|
while curr_off < end_off:
|
||||||
|
if curr_off + read_size >= end_off:
|
||||||
|
read_size = end_off - curr_off
|
||||||
|
|
||||||
|
buf = f.read(read_size)
|
||||||
|
out_ep.write(data=buf, timeout=0)
|
||||||
|
curr_off += read_size
|
||||||
|
|
||||||
|
def poll_commands(nsp_dir, in_ep, out_ep):
|
||||||
|
while True:
|
||||||
|
cmd_header = bytes(in_ep.read(0x20, timeout=0))
|
||||||
|
magic = cmd_header[:4]
|
||||||
|
print('Magic: {}'.format(magic), flush=True)
|
||||||
|
|
||||||
|
if magic != b'TUC0': # Tinfoil USB Command 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
cmd_type = struct.unpack('<B', cmd_header[4:5])[0]
|
||||||
|
cmd_id = struct.unpack('<I', cmd_header[8:12])[0]
|
||||||
|
data_size = struct.unpack('<Q', cmd_header[12:20])[0]
|
||||||
|
|
||||||
|
print('Cmd Type: {}, Command id: {}, Data size: {}'.format(cmd_type, cmd_id, data_size), flush=True)
|
||||||
|
|
||||||
|
if cmd_id == CMD_ID_EXIT:
|
||||||
|
print('Exiting...')
|
||||||
|
break
|
||||||
|
elif cmd_id == CMD_ID_FILE_RANGE:
|
||||||
|
file_range_cmd(nsp_dir, in_ep, out_ep, data_size)
|
||||||
|
|
||||||
|
def send_nsp_list(nsp_dir, out_ep):
|
||||||
|
nsp_path_list = list()
|
||||||
|
nsp_path_list_len = 0
|
||||||
|
|
||||||
|
# Add all files with the extension .nsp in the provided dir
|
||||||
|
for nsp_path in [f for f in nsp_dir.iterdir() if f.is_file() and (f.suffix in EXTS)]:
|
||||||
|
nsp_path_list.append(nsp_path.__str__() + '\n')
|
||||||
|
nsp_path_list_len += len(nsp_path.__str__()) + 1
|
||||||
|
|
||||||
|
print('Sending header...')
|
||||||
|
|
||||||
|
out_ep.write(b'TUL0') # Tinfoil USB List 0
|
||||||
|
out_ep.write(struct.pack('<I', nsp_path_list_len))
|
||||||
|
out_ep.write(b'\x00' * 0x8) # Padding
|
||||||
|
|
||||||
|
print('Sending NSP list: {}'.format(nsp_path_list))
|
||||||
|
|
||||||
|
for nsp_path in nsp_path_list:
|
||||||
|
out_ep.write(nsp_path)
|
||||||
|
|
||||||
|
def print_usage():
|
||||||
|
print("""\
|
||||||
|
usb_install_pc.py
|
||||||
|
|
||||||
|
Used for the installation of NSPs over USB.
|
||||||
|
|
||||||
|
Usage: usb_install_pc.py <nsp folder>""")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print_usage()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
nsp_dir = Path(sys.argv[1])
|
||||||
|
|
||||||
|
if not nsp_dir.is_dir():
|
||||||
|
raise ValueError('1st argument must be a directory')
|
||||||
|
|
||||||
|
print("waiting for switch...\n")
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
while (dev is None):
|
||||||
|
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
print("found the switch!\n")
|
||||||
|
|
||||||
|
cfg = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
cfg = dev.get_active_configuration()
|
||||||
|
print("found active config")
|
||||||
|
except usb.core.USBError:
|
||||||
|
print("no currently active config")
|
||||||
|
cfg = None
|
||||||
|
|
||||||
|
if cfg is None:
|
||||||
|
dev.reset()
|
||||||
|
dev.set_configuration()
|
||||||
|
cfg = dev.get_active_configuration()
|
||||||
|
|
||||||
|
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||||
|
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||||
|
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
|
||||||
|
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
|
||||||
|
|
||||||
|
assert out_ep is not None
|
||||||
|
assert in_ep is not None
|
||||||
|
|
||||||
|
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
|
||||||
|
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
|
||||||
|
|
||||||
|
send_nsp_list(nsp_dir, out_ep)
|
||||||
|
poll_commands(nsp_dir, in_ep, out_ep)
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
# based on usb.py from Tinfoil, by Adubbz.
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import usb.core
|
|
||||||
import usb.util
|
|
||||||
import time
|
|
||||||
import glob
|
|
||||||
|
|
||||||
# magic number (SPHA) for the script and switch.
|
|
||||||
MAGIC = 0x53504841
|
|
||||||
# version of the usb script.
|
|
||||||
VERSION = 1
|
|
||||||
# list of supported extensions.
|
|
||||||
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
|
|
||||||
|
|
||||||
def verify_switch(bcdUSB, count, in_ep, out_ep):
|
|
||||||
header = in_ep.read(8, timeout=0)
|
|
||||||
switch_magic = struct.unpack('<I', header[0:4])[0]
|
|
||||||
switch_version = struct.unpack('<I', header[4:8])[0]
|
|
||||||
|
|
||||||
if switch_magic != MAGIC:
|
|
||||||
raise Exception("Unexpected magic {}".format(switch_magic))
|
|
||||||
|
|
||||||
if switch_version != VERSION:
|
|
||||||
raise Exception("Unexpected version {}".format(switch_version))
|
|
||||||
|
|
||||||
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
|
|
||||||
out_ep.write(data=send_data, timeout=0)
|
|
||||||
|
|
||||||
def wait_for_input(path, in_ep, out_ep):
|
|
||||||
buf = None
|
|
||||||
predicted_off = 0
|
|
||||||
print("now waiting for intput\n")
|
|
||||||
|
|
||||||
with open(path, "rb") as file:
|
|
||||||
while True:
|
|
||||||
header = in_ep.read(24, timeout=0)
|
|
||||||
|
|
||||||
range_offset = struct.unpack('<Q', header[8:16])[0]
|
|
||||||
range_size = struct.unpack('<Q', header[16:24])[0]
|
|
||||||
|
|
||||||
if (range_offset == 0 and range_size == 0):
|
|
||||||
break
|
|
||||||
|
|
||||||
if (buf != None and range_offset == predicted_off and range_size == len(buf)):
|
|
||||||
# print("predicted the read off {} size {}".format(predicted_off, len(buf)))
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
file.seek(range_offset)
|
|
||||||
buf = file.read(range_size)
|
|
||||||
|
|
||||||
if (len(buf) != range_size):
|
|
||||||
# print("off: {} size: {}".format(range_offset, range_size))
|
|
||||||
raise ValueError('bad buf size!!!!!')
|
|
||||||
|
|
||||||
result = out_ep.write(data=buf, timeout=0)
|
|
||||||
if (len(buf) != result):
|
|
||||||
print("off: {} size: {}".format(range_offset, range_size))
|
|
||||||
raise ValueError('bad result!!!!!')
|
|
||||||
|
|
||||||
predicted_off = range_offset + range_size
|
|
||||||
buf = file.read(range_size)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("hello world")
|
|
||||||
|
|
||||||
# check which mode the user has selected.
|
|
||||||
args = len(sys.argv)
|
|
||||||
if (args != 2):
|
|
||||||
print("either run python usb_total.py game.nsp OR drag and drop the game onto the python file (if python is in your path)")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
path = sys.argv[1]
|
|
||||||
files = []
|
|
||||||
|
|
||||||
if os.path.isfile(path) and path.endswith(EXTS):
|
|
||||||
files.append(path)
|
|
||||||
elif os.path.isdir(path):
|
|
||||||
for f in glob.glob(path + "/**/*.*", recursive=True):
|
|
||||||
if os.path.isfile(f) and f.endswith(EXTS):
|
|
||||||
files.append(f)
|
|
||||||
else:
|
|
||||||
raise ValueError('must be a file!')
|
|
||||||
|
|
||||||
# for file in files:
|
|
||||||
# print("found file: {}".format(file))
|
|
||||||
|
|
||||||
# Find the switch
|
|
||||||
print("waiting for switch...\n")
|
|
||||||
dev = None
|
|
||||||
|
|
||||||
while (dev is None):
|
|
||||||
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
print("found the switch!\n")
|
|
||||||
|
|
||||||
cfg = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
cfg = dev.get_active_configuration()
|
|
||||||
print("found active config")
|
|
||||||
except usb.core.USBError:
|
|
||||||
print("no currently active config")
|
|
||||||
cfg = None
|
|
||||||
|
|
||||||
if cfg is None:
|
|
||||||
dev.set_configuration()
|
|
||||||
cfg = dev.get_active_configuration()
|
|
||||||
|
|
||||||
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
|
||||||
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
|
|
||||||
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
|
|
||||||
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
|
|
||||||
assert out_ep is not None
|
|
||||||
assert in_ep is not None
|
|
||||||
|
|
||||||
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
|
|
||||||
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
|
|
||||||
|
|
||||||
try:
|
|
||||||
verify_switch(dev.bcdUSB, len(files), in_ep, out_ep)
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
print("installing file: {}".format(file))
|
|
||||||
wait_for_input(file, in_ep, out_ep)
|
|
||||||
dev.reset()
|
|
||||||
except Exception as inst:
|
|
||||||
print("An exception occurred " + str(inst))
|
|
||||||
Reference in New Issue
Block a user