Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
365ae2d0cb | ||
|
|
5b6e09b926 | ||
|
|
7072647611 | ||
|
|
30cf4826f8 | ||
|
|
ca47fc1f89 | ||
|
|
16a2c84edd | ||
|
|
df5e27dd06 | ||
|
|
d95226f8c0 | ||
|
|
164fec5b73 | ||
|
|
8dad96f39f | ||
|
|
2244e73c53 | ||
|
|
456cb02d2a | ||
|
|
f310704472 | ||
|
|
96e5a7081b | ||
|
|
1c93e18822 | ||
|
|
ac152454f0 | ||
|
|
7851f7f400 | ||
|
|
2b561dd438 | ||
|
|
3545f557fc | ||
|
|
8dfb9b9ba6 | ||
|
|
7cf36cd25f | ||
|
|
c53692022b | ||
|
|
0f3b7da0b2 | ||
|
|
e22daefb08 | ||
|
|
6fb5319da3 | ||
|
|
6970fec554 | ||
|
|
36be56647f | ||
|
|
cca6326314 | ||
|
|
9176c6780a | ||
|
|
b1a6b12cf3 | ||
|
|
c7cc11cc98 | ||
|
|
ec4b96b95d | ||
|
|
a2e343daa7 | ||
|
|
b811c9e3cd | ||
|
|
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 | ||
|
|
5aca92a2cc | ||
|
|
7471885119 | ||
|
|
5038fb0c28 | ||
|
|
ff9f493460 | ||
|
|
89e82927ee | ||
|
|
651d9fa495 | ||
|
|
3141100457 | ||
|
|
6b4e81c935 | ||
|
|
e243d5b64e | ||
|
|
252cd0cee6 | ||
|
|
14abcc50b5 | ||
|
|
134aadad5a | ||
|
|
a56bc9e4fa | ||
|
|
5bd466a9b6 | ||
|
|
16c58512ec | ||
|
|
b1b0b13f2a | ||
|
|
03e77faf06 | ||
|
|
7e381924ab | ||
|
|
5763610e54 | ||
|
|
49956a3f84 | ||
|
|
b2915a8142 | ||
|
|
e002aa9ec2 | ||
|
|
0aaf460dbf | ||
|
|
76c8b806d0 | ||
|
|
61783bc530 | ||
|
|
a3a2a04991 | ||
|
|
b6304fca75 | ||
|
|
5612ae5691 | ||
|
|
657c160599 | ||
|
|
f66494aeb5 | ||
|
|
650e7812e5 | ||
|
|
cca54340a2 | ||
|
|
8161b52e7b | ||
|
|
9390bd3865 | ||
|
|
483be133a5 | ||
|
|
e2022eac4c | ||
|
|
977331c3b2 | ||
|
|
64a40ae672 |
2
.gitignore
vendored
@@ -22,3 +22,5 @@ libs/tweeny
|
|||||||
|
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
out
|
out
|
||||||
|
|
||||||
|
usb_test/
|
||||||
|
|||||||
78
README.md
@@ -1,10 +1,12 @@
|
|||||||
# sphaira
|
# Sphaira
|
||||||
|
|
||||||
A homebrew menu for the switch.
|
A homebrew menu for the Nintendo Switch.
|
||||||
|
|
||||||
[See the gbatemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
|
[See the GBATemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
|
||||||
|
|
||||||
## showcase
|
[We have now have a Discord server!](https://discord.gg/8vZBsrprEc). Please use the issues tab to report bugs, as it is much easier for me to track.
|
||||||
|
|
||||||
|
## Showcase
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
:-------------------------:|:-------------------------:
|
:-------------------------:|:-------------------------:
|
||||||
@@ -13,29 +15,29 @@ A homebrew menu for the switch.
|
|||||||
 | 
|
 | 
|
||||||
 | 
|
 | 
|
||||||
|
|
||||||
## bug reports
|
## Bug reports
|
||||||
|
|
||||||
for any bug reports, please use the issues tab and explain in as much detail as possible!
|
For any bug reports, please use the issues tab and explain in as much detail as possible!
|
||||||
|
|
||||||
please include:
|
Please include:
|
||||||
|
|
||||||
- CFW type (i assume Atmosphere, but someone out there is still using Rajnx)
|
- CFW type (i assume Atmosphere, but someone out there is still using Rajnx);
|
||||||
- CFW version
|
- CFW version;
|
||||||
- FW version
|
- FW version;
|
||||||
- The bug itself and how to reproduce it
|
- The bug itself and how to reproduce it.
|
||||||
|
|
||||||
## ftp
|
## FTP
|
||||||
|
|
||||||
ftp can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
|
FTP can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
|
||||||
of all configs available](https://github.com/ITotalJustice/ftpsrv/blob/master/assets/config.ini.template).
|
of all configs available](https://github.com/ITotalJustice/ftpsrv/blob/master/assets/config.ini.template).
|
||||||
|
|
||||||
## mtp
|
## MTP
|
||||||
|
|
||||||
mtp can be enabled via the network menu.
|
MTP can be enabled via the Network menu.
|
||||||
|
|
||||||
## file assoc
|
## File association
|
||||||
|
|
||||||
sphaira has file assoc support. lets say your app supports loading .png files, then you could write an assoc file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as retroarch, melonds, mgba etc.
|
Sphaira has file association support. Let's say your app supports loading .png files, then you could write an association file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as RetroArch, MelonDS, mGBA etc.
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[config]
|
[config]
|
||||||
@@ -43,9 +45,43 @@ path=/switch/your_app.nro
|
|||||||
supported_extensions=jpg|png|mp4|mp3
|
supported_extensions=jpg|png|mp4|mp3
|
||||||
```
|
```
|
||||||
|
|
||||||
the `path` field is optional. if left out, it will use the name of the ini to find the nro. For example, if the ini is called mgba.ini, it will try to find the nro in /switch/mgba.nro and /switch/folder/mgba.nro.
|
The `path` field is optional. If left out, it will use the name of the ini to find the nro. For example, if the ini is called mgba.ini, it will try to find the nro in /switch/mgba.nro and /switch/folder/mgba.nro.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -57,7 +93,7 @@ see `assets/romfs/assoc/` for more examples of file assoc entries
|
|||||||
- deko3d-nanovg
|
- deko3d-nanovg
|
||||||
- libpulsar
|
- libpulsar
|
||||||
- minIni
|
- minIni
|
||||||
- gbatemp
|
- GBATemp
|
||||||
- hb-appstore
|
- hb-appstore
|
||||||
- haze
|
- haze
|
||||||
- everyone who has contributed to this project!
|
- Everyone who has contributed to this project!
|
||||||
|
|||||||
@@ -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,80 +1,84 @@
|
|||||||
{
|
{
|
||||||
"[Applet Mode]": "[Applet-Modus]",
|
"[Applet Mode]": " | Applet Modus |",
|
||||||
"No Internet": "Keine Internetverbindung",
|
"No Internet": "Kein Internet",
|
||||||
"Files": "Dateien",
|
"Files": "Dateien",
|
||||||
"Apps": "Apps",
|
"Apps": "hb-Apps",
|
||||||
"Store": "Store",
|
"Store": "hb-Store",
|
||||||
"Menu": "Menü",
|
"Menu": "Menü",
|
||||||
"Options": "Optionen",
|
"Options": "Optionen",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
"Back": "Zurück",
|
"Back": "Zurück",
|
||||||
"Select": "Auswählen",
|
"Select": "Auswählen",
|
||||||
"Open": "Öffnen",
|
"Open": "Öffne",
|
||||||
"Launch": "Starten",
|
"Launch": "Starte",
|
||||||
"Info": "Info",
|
"Info": "Info",
|
||||||
"Install": "Installieren",
|
"Install": "Installieren",
|
||||||
"Delete": "Löschen",
|
"Delete": "Löschen",
|
||||||
"Restart": "Neustart",
|
"Restart": "Neustart",
|
||||||
"Changelog": "Changelog",
|
"Changelog": "Neuerungen",
|
||||||
"Details": "Details",
|
"Details": "Details",
|
||||||
"Update": "Update",
|
"Update": "Update",
|
||||||
"Remove": "Entfernen",
|
"Remove": "Entfernen",
|
||||||
"Restore": "",
|
"Restore": "Wiederherstellen",
|
||||||
"Download": "Download",
|
"Download": "Download",
|
||||||
"Next Page": "Nächste Seite",
|
"Next Page": "Nächste Seite",
|
||||||
"Prev Page": "Vorherige Seite",
|
"Prev Page": "Vorherige Seite",
|
||||||
"Unstar": "Favorit entfernen",
|
"Unstar": "Kein Favorit",
|
||||||
"Star": "Favorit",
|
"Star": "Favorit",
|
||||||
"System memory": "System-Speicher",
|
"System memory": "NAND Systemspeicher",
|
||||||
"microSD card": "microSD-Karte",
|
"microSD card": "SD-Karte",
|
||||||
"Sd": "",
|
"Sd": "SD-Karte | Root-Verzeichnis",
|
||||||
"Image System memory": "",
|
"Image System memory": "Album | NAND Systemspeicher",
|
||||||
"Image microSD card": "",
|
"Image microSD card": "Album | SD-Karte",
|
||||||
|
"Slow": "Niedrig",
|
||||||
|
"Normal": "Mittel",
|
||||||
|
"Fast": "Hoch",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"No": "Nein",
|
"No": "Nein",
|
||||||
"Enabled": "Aktiviert",
|
"Enabled": "An",
|
||||||
"Disabled": "Deaktiviert",
|
"Disabled": "Aus",
|
||||||
|
|
||||||
"Sort By": "Sortieren nach",
|
"Sort By": "Sortierung",
|
||||||
"Sort Options": "Sortieroptionen",
|
"Sort Options": " Sortierung | Optionen",
|
||||||
"Filter": "Filter",
|
"Filter": "Rubrik",
|
||||||
"Sort": "Sortieren",
|
"Sort": "Sortiert nach",
|
||||||
"Order": "Reihenfolge",
|
"Order": "Anordnung",
|
||||||
"Search": "Suchen",
|
"Search": "Suchen",
|
||||||
"Updated": "Aktualisiert",
|
"Updated": "zuletzt aktualisiert",
|
||||||
"Updated (Star)": "Aktualisiert (Favoriten)",
|
"Updated (Star)": "Favorit | zuletzt aktualisiert",
|
||||||
"Downloads": "Downloads",
|
"Downloads": "Downloads",
|
||||||
"Size": "Größe",
|
"Size": "Größe",
|
||||||
"Size (Star)": "Größe (Favoriten)",
|
"Size (Star)": "Favorit | Größe",
|
||||||
"Alphabetical": "Alphabetisch",
|
"Alphabetical": "Name",
|
||||||
"Alphabetical (Star)": "Alphabetisch (Favoriten)",
|
"Alphabetical (Star)": "Favorit | Name",
|
||||||
"Likes": "Likes",
|
"Likes": "Beliebtheit",
|
||||||
"ID": "ID",
|
"ID": "Theme | Paket ID",
|
||||||
"Decending": "Absteigend",
|
"Descending": "Absteigend ↓",
|
||||||
"Descending (down)": "Absteigend",
|
"Descending (down)": "Absteigend ↓",
|
||||||
"Desc": "Abst.",
|
"Desc": " ↓",
|
||||||
"Ascending": "Aufsteigend",
|
"Ascending": "Aufsteigend ↑",
|
||||||
"Ascending (Up)": "Aufsteigend",
|
"Ascending (Up)": "Aufsteigend ↑",
|
||||||
"Asc": "Aufst.",
|
"Asc": " ↑",
|
||||||
|
|
||||||
"Menu Options": "Menü-Optionen",
|
"Menu Options": " Menü | Optionen",
|
||||||
"Theme": "Theme",
|
"Theme": "Themes",
|
||||||
"Theme Options": "Theme-Optionen",
|
"Theme Options": " Themes | Optionen",
|
||||||
"Select Theme": "Theme auswählen",
|
"Select Theme": "Theme wählen",
|
||||||
"Shuffle": "Zufällig",
|
"Shuffle": "Zufällig",
|
||||||
"Music": "Musik",
|
"Music": "Musik",
|
||||||
"Network": "Netzwerk",
|
"12 Hour Time": "12-Std Zeitformat",
|
||||||
"Network Options": "Netzwerk-Optionen",
|
"Network": "Konnektivität",
|
||||||
|
"Network Options": "Konnektivität | Optionen",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
"Mtp": "MTP",
|
"Mtp": "MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "NXLink",
|
||||||
"Nxlink Connected": "Nxlink verbunden",
|
"Nxlink Connected": "NXLink | Verbunden",
|
||||||
"Nxlink Upload": "Nxlink Upload",
|
"Nxlink Upload": "NXLink | wird hochgeladen...",
|
||||||
"Nxlink Finished": "Nxlink abgeschlossen",
|
"Nxlink Finished": "NXLink | Hochladen beendet",
|
||||||
"Switch-Handheld!": "Switch-Handheld!",
|
"Switch-Handheld!": "Handheld!",
|
||||||
"Switch-Docked!": "Switch-Dock-Modus!",
|
"Switch-Docked!": "Angedockt!",
|
||||||
"Language": "Sprache",
|
"Language": "Sprache",
|
||||||
"Auto": "Auto",
|
"Auto": "Systemsprache",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
"Japanese": "日本語",
|
"Japanese": "日本語",
|
||||||
"French": "Français",
|
"French": "Français",
|
||||||
@@ -83,53 +87,55 @@
|
|||||||
"Spanish": "Español",
|
"Spanish": "Español",
|
||||||
"Chinese": "中文",
|
"Chinese": "中文",
|
||||||
"Korean": "한국어",
|
"Korean": "한국어",
|
||||||
"Dutch": "Dutch",
|
"Dutch": "Nederlands",
|
||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
"Logging": "Logging",
|
"Vietnamese": "tiếng Việt",
|
||||||
"Replace hbmenu on exit": "hbmenu beim Beenden ersetzen",
|
"Logging": "Protokollieren",
|
||||||
"Misc": "Sonstiges",
|
"Replace hbmenu on exit": "hbmenu durch sphaira ersetzen",
|
||||||
"Misc Options": "Weitere Optionen",
|
"Misc": "Extras",
|
||||||
"Web": "Web",
|
"Misc Options": " Extras | Optionen",
|
||||||
|
"Web": "WEB Browser",
|
||||||
"Install forwarders": "Forwarder installieren",
|
"Install forwarders": "Forwarder installieren",
|
||||||
"Install location": "Installationsort",
|
"Install location": "Einhängepunkt",
|
||||||
"Show install warning": "Installationswarnung anzeigen",
|
"Show install warning": "Warnungen anzeigen",
|
||||||
|
"Text scroll speed": "Laufschrift Tempo",
|
||||||
|
|
||||||
"FileBrowser": "Datei-Browser",
|
"FileBrowser": "Datei-Manager",
|
||||||
"%zd files": "%zd Dateien",
|
"%zd files": "%zd Dateien",
|
||||||
"%zd dirs": "%zd Ordner",
|
"%zd dirs": "%zd Ordner",
|
||||||
"File Options": "Datei-Optionen",
|
"File Options": "Datei - Ordner | Optionen",
|
||||||
"Show Hidden": "Versteckte anzeigen",
|
"Show Hidden": "Versteckte zeigen",
|
||||||
"Folders First": "Ordner zuerst",
|
"Folders First": "Ordner zuerst",
|
||||||
"Hidden Last": "Versteckte zuletzt",
|
"Hidden Last": "Versteckte zuletzt",
|
||||||
"Cut": "Ausschneiden",
|
"Cut": "Ausschneiden",
|
||||||
"Copy": "Kopieren",
|
"Copy": "Kopieren",
|
||||||
"Paste": "Einfügen",
|
"Paste": "Einfügen",
|
||||||
"Paste ": "Einfügen ",
|
"Paste ": "Einfügen von: ",
|
||||||
" file(s)?": " Datei(en)?",
|
" file(s)?": " Datei/en?",
|
||||||
"Rename": "Umbenennen",
|
"Rename": "Umbenennen",
|
||||||
"Set New File Name": "Neuen Dateinamen eingeben",
|
"Set New File Name": "Neuen Dateinamen festlegen",
|
||||||
"Advanced": "Erweitert",
|
"Advanced": "Erweitert...",
|
||||||
"Advanced Options": "Erweiterte Optionen",
|
"Advanced Options": " Erweitert | Optionen",
|
||||||
"Create File": "Datei erstellen",
|
"Create File": "Neue Datei",
|
||||||
"Set File Name": "Dateinamen eingeben",
|
"Set File Name": "Dateiname festlegen",
|
||||||
"Create Folder": "Ordner erstellen",
|
"Create Folder": "Neuer Ordner",
|
||||||
"Set Folder Name": "Ordnernamen eingeben",
|
"Set Folder Name": "Ordner umbenennen",
|
||||||
"View as text (unfinished)": "Als Text anzeigen (Beta)",
|
"View as text (unfinished)": "Als Text anzeigen",
|
||||||
"Ignore read only": "",
|
"Ignore read only": "Schreibschutz umgehen?",
|
||||||
"Mount": "",
|
"Mount": "Einhängen",
|
||||||
"Empty...": "Leer...",
|
"Empty...": "Keine Daten...",
|
||||||
"Open with DayBreak?": "Mit DayBreak öffnen?",
|
"Open with DayBreak?": "Mit Daybreak öffnen?",
|
||||||
"Launch ": "Starten ",
|
"Launch ": "Starte ",
|
||||||
"Launch option for: ": "Startoption für: ",
|
"Launch option for: ": "Start Option für: ",
|
||||||
"Select launcher for: ": "Launcher auswählen für: ",
|
"Select launcher for: ": "Wähle Launcher für: ",
|
||||||
|
|
||||||
"Homebrew": "Homebrew",
|
"Homebrew": "hbmenu",
|
||||||
"Homebrew Options": "Homebrew-Optionen",
|
"Homebrew Options": " hbmenu | Optionen",
|
||||||
"Hide Sphaira": "Sphaira ausblenden",
|
"Hide Sphaira": "Verstecke sphaira",
|
||||||
"Install Forwarder": "Forwarder installieren",
|
"Install Forwarder": "Forwarder installieren",
|
||||||
"WARNING: Installing forwarders will lead to a ban!": "WARNUNG: Installation von Forwardern führt zum Ban!",
|
"WARNING: Installing forwarders will lead to a ban!": "Installiere Forwarder-NSP´s mit VORSICHT.\nEs erhöht das Risiko eines Konsolen-Banns!",
|
||||||
"Installing Forwarder": "Installiere Forwarder",
|
"Installing Forwarder": "Installiere Forwarder",
|
||||||
"Creating Program": "Erstelle Programm",
|
"Creating Program": "Erstelle Programm",
|
||||||
"Creating Control": "Erstelle Control",
|
"Creating Control": "Erstelle Control",
|
||||||
@@ -138,26 +144,26 @@
|
|||||||
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
|
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
|
||||||
"Pushing application record": "Übertrage Anwendungsdaten",
|
"Pushing application record": "Übertrage Anwendungsdaten",
|
||||||
"Installed!": "Installiert!",
|
"Installed!": "Installiert!",
|
||||||
"Failed to install forwarder": "Forwarder-Installation fehlgeschlagen",
|
"Failed to install forwarder": "Fehler beim installieren des Forwarders",
|
||||||
"Unstarred ": "Favorit entfernt ",
|
"Unstarred ": "Favorit entfernt ",
|
||||||
"Starred ": "Favorit hinzugefügt ",
|
"Starred ": "Favorit ",
|
||||||
|
|
||||||
"AppStore": "AppStore",
|
"AppStore": "hb-AppStore",
|
||||||
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortierung: %s | Reihenfolge: %s",
|
"Filter: %s | Sort: %s | Order: %s": "Rubrik: %s | Sort.nach.: %s | Ordnung: %s",
|
||||||
"AppStore Options": "AppStore-Optionen",
|
"AppStore Options": " hb-AppStore | Optionen",
|
||||||
"All": "Alle",
|
"All": "Alles anzeigen",
|
||||||
"Games": "Spiele",
|
"Games": "Spiele",
|
||||||
"Emulators": "Emulatoren",
|
"Emulators": "Emulatoren",
|
||||||
"Tools": "Tools",
|
"Tools": "Tools",
|
||||||
"Themes": "Themes",
|
"Themes": "Themes",
|
||||||
"Legacy": "Legacy",
|
"Legacy": "Älteres",
|
||||||
"version: %s": "Version: %s",
|
"version: %s": "Version: %s",
|
||||||
"updated: %s": "Aktualisiert: %s",
|
"updated: %s": "Letztes Update am: %s",
|
||||||
"category: %s": "Kategorie: %s",
|
"category: %s": "Rubrik: %s",
|
||||||
"extracted: %.2f MiB": "Entpackt: %.2f MiB",
|
"extracted: %.2f MiB": "Größe: %.2f MiB",
|
||||||
"app_dls: %s": "Downloads: %s",
|
"app_dls: %s": "Anzahl Downloads: %s",
|
||||||
"More by Author": "Mehr vom Entwickler",
|
"More by Author": "Weitere Apps des Entwicklers",
|
||||||
"Leave Feedback": "Feedback geben",
|
"Leave Feedback": "Feedback hinterlassen",
|
||||||
|
|
||||||
"Irs": "IR-Sensor",
|
"Irs": "IR-Sensor",
|
||||||
"Ambient Noise Level: ": "Umgebungsrauschen: ",
|
"Ambient Noise Level: ": "Umgebungsrauschen: ",
|
||||||
@@ -197,53 +203,55 @@
|
|||||||
"External Light Filter": "Externes Lichtfilter",
|
"External Light Filter": "Externes Lichtfilter",
|
||||||
"Load Default": "Standard laden",
|
"Load Default": "Standard laden",
|
||||||
|
|
||||||
"Themezer": "Themezer",
|
"Themezer": "Themezer | NX Themes",
|
||||||
"Themezer Options": "Themezer-Optionen",
|
"Themezer Options": " Themezer | Optionen",
|
||||||
"Nsfw": "NSFW",
|
"Nsfw": "NSFW",
|
||||||
"Page": "Seite",
|
"Page": "Seiten Nr. wählen ",
|
||||||
"Page %zu / %zu": "Seite %zu / %zu",
|
"Page %zu / %zu": " %zu / %zu",
|
||||||
"Enter Page Number": "Seitenzahl eingeben",
|
"Enter Page Number": "Zu Seite Nr.: ___",
|
||||||
"Bad Page": "Ungültige Seite",
|
"Bad Page": "Seite nicht gefunden",
|
||||||
"Download theme?": "Theme herunterladen?",
|
"Download theme?": "Theme herunterladen?",
|
||||||
|
|
||||||
"GitHub": "",
|
"GitHub": "GitHub",
|
||||||
"Downloading json": "",
|
"Downloading json": "Lade JSON-File",
|
||||||
"Select asset to download for ": "",
|
"Select asset to download for ": "Wähle Asset für den Download von ",
|
||||||
|
|
||||||
"Installing ": "Installiere ",
|
"Installing ": "Installiert wird: ",
|
||||||
"Uninstalling ": "Deinstalliere ",
|
"Uninstalling ": "Deinstalliert wird: ",
|
||||||
"Deleting ": "Lösche ",
|
"Deleting ": "Gelöscht wird: ",
|
||||||
"Deleting": "Lösche",
|
"Deleting": "Gelöscht wurde:",
|
||||||
"Pasting ": "Füge ein ",
|
"Pasting ": "Eingefügt wird: ",
|
||||||
"Pasting": "Füge ein",
|
"Pasting": "Eingefügt wurde:",
|
||||||
"Removing ": "Entferne ",
|
"Removing ": "Entfernt wird: ",
|
||||||
"Scanning ": "Scanne ",
|
"Scanning ": "Gescannt wird: ",
|
||||||
"Creating ": "Erstelle ",
|
"Creating ": "Erstellt wird: ",
|
||||||
"Copying ": "Kopiere ",
|
"Copying ": "Kopiert wird: ",
|
||||||
"Trying to load ": "Lade ",
|
"Trying to load ": "Versucht zu laden wird: ",
|
||||||
"Downloading ": "Lade herunter ",
|
"Downloading ": "Heruntergeladen wird: ",
|
||||||
"Downloaded ": "",
|
"Downloaded ": "Heruntergeladen wurde: ",
|
||||||
"Removed ": "",
|
"Removed ": "Entfernt wurde: ",
|
||||||
"Checking MD5": "Prüfe MD5",
|
"Checking MD5": "Checke MD5 Prüfsumme",
|
||||||
"Loading...": "Lade...",
|
"Loading...": "Wird geladen...",
|
||||||
"Loading": "Lade",
|
"Loading": "Wird geladen",
|
||||||
"Empty!": "Leer!",
|
"Empty!": "Keine Daten!",
|
||||||
"Not Ready...": "Nicht bereit...",
|
"Not Ready...": "Nicht bereit...",
|
||||||
"Error loading page!": "Fehler beim Laden!",
|
"Error loading page!": "Ladefehler!",
|
||||||
"Update avaliable: ": "Update verfügbar: ",
|
"Update avaliable: ": "Update verfügbar: ",
|
||||||
"Download update: ": "Update herunterladen: ",
|
"Download update: ": " Herunterladen des Updates: ",
|
||||||
"Updated to ": "Aktualisiert auf ",
|
"Updated to ": "Aktualisiert auf: ",
|
||||||
"Restart Sphaira?": "Sphaira neustarten?",
|
"Press OK to restart Sphaira": "Drücke OK um sphaira erneut zustarten",
|
||||||
"Failed to download update": "Update-Download fehlgeschlagen",
|
"Restart Sphaira?": "sphaira erneut starten?",
|
||||||
"Restore hbmenu?": "",
|
"Failed to download update": "Herunterladen des Updates fehlgeschlagen!",
|
||||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
|
"Restore hbmenu?": "hbmenu wiederherstellen?",
|
||||||
"Failed to restore hbmenu, please re-download hbmenu": "",
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Die Datei /switch/hbmenu.nro fehlt.\nInstalliere hbmenu über den hb-AppStore.",
|
||||||
"Failed to restore hbmenu, using sphaira instead": "",
|
"Failed to restore hbmenu, please re-download hbmenu": "Fehler, hbmenu nicht wiederhergrstellt!\nInstalliere hbmenu über den hb-AppStore.",
|
||||||
"Restored hbmenu, closing sphaira": "",
|
"Failed to restore hbmenu, using sphaira instead": "Fehler, hbmenu nicht wiederhergrstellt!\nVerwende weiter sphaira",
|
||||||
"Restored hbmenu": "",
|
"Restored hbmenu, closing sphaira": "hbmenu wurde wiederhergestellt, schließe sphaira",
|
||||||
"Delete Selected files?": "Ausgewählte Dateien löschen?",
|
"Restored hbmenu": "hbmenu wurde wiederhergestellt",
|
||||||
"Completely remove ": "Vollständig entfernen ",
|
"Delete Selected files?": "Ausgewähle Dateien löschen?",
|
||||||
"Are you sure you want to delete ": "Wirklich löschen ",
|
"Completely remove ": "Komplett gelöscht wird: ",
|
||||||
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
|
"Are you sure you want to delete ": "Bist du sicher zu löschen? Bestätige Löschung von: ",
|
||||||
|
"Are you sure you wish to cancel?": "Bist du sicher dass du abbrechen willst?",
|
||||||
|
"Audio disabled due to suspended game": "Audio deaktivert wegen Spielabbruch",
|
||||||
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen."
|
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen."
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "Sd",
|
"Sd": "Sd",
|
||||||
"Image System memory": "Image System memory",
|
"Image System memory": "Image System memory",
|
||||||
"Image microSD card": "Image microSD card",
|
"Image microSD card": "Image microSD card",
|
||||||
|
"Slow": "Slow",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Fast",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Enabled": "Enabled",
|
"Enabled": "Enabled",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "Alphabetical (Star)",
|
"Alphabetical (Star)": "Alphabetical (Star)",
|
||||||
"Likes": "Likes",
|
"Likes": "Likes",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Decending",
|
"Descending": "Descending",
|
||||||
"Descending (down)": "Descending (down)",
|
"Descending (down)": "Descending (down)",
|
||||||
"Desc": "Desc",
|
"Desc": "Desc",
|
||||||
"Ascending": "Ascending",
|
"Ascending": "Ascending",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "Select Theme",
|
"Select Theme": "Select Theme",
|
||||||
"Shuffle": "Shuffle",
|
"Shuffle": "Shuffle",
|
||||||
"Music": "Music",
|
"Music": "Music",
|
||||||
|
"12 Hour Time": "12 Hour Time",
|
||||||
"Network": "Network",
|
"Network": "Network",
|
||||||
"Network Options": "Network Options",
|
"Network Options": "Network Options",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Logging",
|
"Logging": "Logging",
|
||||||
"Replace hbmenu on exit": "Replace hbmenu on exit",
|
"Replace hbmenu on exit": "Replace hbmenu on exit",
|
||||||
"Misc": "Misc",
|
"Misc": "Misc",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "Install forwarders",
|
"Install forwarders": "Install forwarders",
|
||||||
"Install location": "Install location",
|
"Install location": "Install location",
|
||||||
"Show install warning": "Show install warning",
|
"Show install warning": "Show install warning",
|
||||||
|
"Text scroll speed": "Text scroll speed",
|
||||||
|
|
||||||
"FileBrowser": "FileBrowser",
|
"FileBrowser": "FileBrowser",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd files",
|
||||||
@@ -233,6 +239,7 @@
|
|||||||
"Update avaliable: ": "Update avaliable: ",
|
"Update avaliable: ": "Update avaliable: ",
|
||||||
"Download update: ": "Download update: ",
|
"Download update: ": "Download update: ",
|
||||||
"Updated to ": "Updated to ",
|
"Updated to ": "Updated to ",
|
||||||
|
"Press OK to restart Sphaira": "Press OK to restart Sphaira",
|
||||||
"Restart Sphaira?": "Restart Sphaira?",
|
"Restart Sphaira?": "Restart Sphaira?",
|
||||||
"Failed to download update": "Failed to download update",
|
"Failed to download update": "Failed to download update",
|
||||||
"Restore hbmenu?": "Restore hbmenu?",
|
"Restore hbmenu?": "Restore hbmenu?",
|
||||||
@@ -245,5 +252,6 @@
|
|||||||
"Completely remove ": "Completely remove ",
|
"Completely remove ": "Completely remove ",
|
||||||
"Are you sure you want to delete ": "Are you sure you want to delete ",
|
"Are you sure you want to delete ": "Are you sure you want to delete ",
|
||||||
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
|
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
|
||||||
|
"Audio disabled due to suspended game": "Audio disabled due to suspended game",
|
||||||
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue."
|
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue."
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"Details": "Detalles",
|
"Details": "Detalles",
|
||||||
"Update": "Actualizar",
|
"Update": "Actualizar",
|
||||||
"Remove": "Borrar",
|
"Remove": "Borrar",
|
||||||
"Restore": "",
|
"Restore": "Restaurar",
|
||||||
"Download": "Descargar",
|
"Download": "Descargar",
|
||||||
"Next Page": "Página siguiente",
|
"Next Page": "Página siguiente",
|
||||||
"Prev Page": "Página anterior",
|
"Prev Page": "Página anterior",
|
||||||
@@ -27,9 +27,12 @@
|
|||||||
"Star": "Favorito",
|
"Star": "Favorito",
|
||||||
"System memory": "Memoria de sistema",
|
"System memory": "Memoria de sistema",
|
||||||
"microSD card": "microSD",
|
"microSD card": "microSD",
|
||||||
"Sd": "",
|
"Sd": "SD",
|
||||||
"Image System memory": "",
|
"Image System memory": "Imagen memoria interna",
|
||||||
"Image microSD card": "",
|
"Image microSD card": "Imagen tarjeta microSD",
|
||||||
|
"Slow": "Lento",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Rápido",
|
||||||
"Yes": "Sí",
|
"Yes": "Sí",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Enabled": "Activado",
|
"Enabled": "Activado",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "Alfabético (favorito)",
|
"Alphabetical (Star)": "Alfabético (favorito)",
|
||||||
"Likes": "Me Gusta",
|
"Likes": "Me Gusta",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Descendente",
|
"Descending": "Descendente",
|
||||||
"Descending (down)": "Descendente (abajo)",
|
"Descending (down)": "Descendente (abajo)",
|
||||||
"Desc": "Descendente",
|
"Desc": "Descendente",
|
||||||
"Ascending": "Ascendente",
|
"Ascending": "Ascendente",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "Seleccionar tema",
|
"Select Theme": "Seleccionar tema",
|
||||||
"Shuffle": "Barajar",
|
"Shuffle": "Barajar",
|
||||||
"Music": "Música",
|
"Music": "Música",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Red",
|
"Network": "Red",
|
||||||
"Network Options": "Opciones de red",
|
"Network Options": "Opciones de red",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,18 +91,20 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
"Logging": "Registros",
|
"Vietnamese": "Vietnamese",
|
||||||
"Replace hbmenu on exit": "Reemplazar hbmenu al salir",
|
"Logging": "Registro",
|
||||||
|
"Replace hbmenu on exit": "Reemplazar hbmenu",
|
||||||
"Misc": "Varios",
|
"Misc": "Varios",
|
||||||
"Misc Options": "Opciones varias",
|
"Misc Options": "Opciones varias",
|
||||||
"Web": "Web",
|
"Web": "Web",
|
||||||
"Install forwarders": "Instalar forwarders",
|
"Install forwarders": "Instalar forwarders",
|
||||||
"Install location": "Ruta de instalación ",
|
"Install location": "Dispositivo de instalación",
|
||||||
"Show install warning": "Mostrar precaución de instalación",
|
"Show install warning": "Precaución de instalación",
|
||||||
|
"Text scroll speed": "Velocidad de scroll",
|
||||||
|
|
||||||
"FileBrowser": "Explorador de archivos",
|
"FileBrowser": "Explorador de archivos",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd archivos",
|
||||||
"%zd dirs": "%zd dirs",
|
"%zd dirs": "%zd carpetas",
|
||||||
"File Options": "Opciones de archivo",
|
"File Options": "Opciones de archivo",
|
||||||
"Show Hidden": "Mostrar archivos ocultos",
|
"Show Hidden": "Mostrar archivos ocultos",
|
||||||
"Folders First": "Carpetas primero",
|
"Folders First": "Carpetas primero",
|
||||||
@@ -117,15 +123,15 @@
|
|||||||
"Create Folder": "Crear carpeta",
|
"Create Folder": "Crear carpeta",
|
||||||
"Set Folder Name": "Establecer nombre de carpeta",
|
"Set Folder Name": "Establecer nombre de carpeta",
|
||||||
"View as text (unfinished)": "Ver como texto (sin terminar)",
|
"View as text (unfinished)": "Ver como texto (sin terminar)",
|
||||||
"Ignore read only": "",
|
"Ignore read only": "Ignorar sólo lectura",
|
||||||
"Mount": "",
|
"Mount": "Montar",
|
||||||
"Empty...": "Vacío...",
|
"Empty...": "Vacío...",
|
||||||
"Open with DayBreak?": "¿Abrir con DayBreak?",
|
"Open with DayBreak?": "¿Abrir con DayBreak?",
|
||||||
"Launch ": "Abrir ",
|
"Launch ": "Abrir ",
|
||||||
"Launch option for: ": "Opción de abrir con: ",
|
"Launch option for: ": "Opción de abrir con: ",
|
||||||
"Select launcher for: ": "Seleccionar abrir con: ",
|
"Select launcher for: ": "Seleccionar abrir con: ",
|
||||||
|
|
||||||
"Homebrew": "Honebrew",
|
"Homebrew": "Homebrew",
|
||||||
"Homebrew Options": "Opciones de Homebrew",
|
"Homebrew Options": "Opciones de Homebrew",
|
||||||
"Hide Sphaira": "Ocultar Sphaira",
|
"Hide Sphaira": "Ocultar Sphaira",
|
||||||
"Install Forwarder": "Instalar Forwarder",
|
"Install Forwarder": "Instalar Forwarder",
|
||||||
@@ -162,16 +168,16 @@
|
|||||||
"Irs": "IRS",
|
"Irs": "IRS",
|
||||||
"Ambient Noise Level: ": "Nivel de Ruido Ambiente",
|
"Ambient Noise Level: ": "Nivel de Ruido Ambiente",
|
||||||
"Controller": "Control",
|
"Controller": "Control",
|
||||||
"Pad ": "Almohadilla ",
|
"Pad ": "GamePad ",
|
||||||
" (Available)": " (Disponible)",
|
" (Available)": " (Disponible)",
|
||||||
" (Unsupported)": "(No Compatible)",
|
" (Unsupported)": "(No Compatible)",
|
||||||
" (Unconnected)": " (Desconectado)",
|
" (Unconnected)": " (Desconectado)",
|
||||||
"HandHeld": "Portátil",
|
"HandHeld": "Portátil",
|
||||||
"Rotation": "Rotación",
|
"Rotation": "Rotación",
|
||||||
"0 (Sideways)": "0 (De lado)",
|
"0 (Sideways)": "0° (De lado)",
|
||||||
"90 (Flat)": "90 (Plano)",
|
"90 (Flat)": "90° (Plano)",
|
||||||
"180 (-Sideways)": "180 (-De lado)",
|
"180 (-Sideways)": "180° (De lado)",
|
||||||
"270 (Upside down)": "270 (Al revés)",
|
"270 (Upside down)": "270° (Al revés)",
|
||||||
"Colour": "Color",
|
"Colour": "Color",
|
||||||
"Grey": "Gris",
|
"Grey": "Gris",
|
||||||
"Ironbow": "Paleta térmica",
|
"Ironbow": "Paleta térmica",
|
||||||
@@ -206,9 +212,9 @@
|
|||||||
"Bad Page": "Página Errónea",
|
"Bad Page": "Página Errónea",
|
||||||
"Download theme?": "¿Descargar Tema?",
|
"Download theme?": "¿Descargar Tema?",
|
||||||
|
|
||||||
"GitHub": "",
|
"GitHub": "GitHub",
|
||||||
"Downloading json": "",
|
"Downloading json": "Descargando json",
|
||||||
"Select asset to download for ": "",
|
"Select asset to download for ": "Seleccionar recurso a descargar para ",
|
||||||
|
|
||||||
"Installing ": "Instalando ",
|
"Installing ": "Instalando ",
|
||||||
"Uninstalling ": "Desinstalando ",
|
"Uninstalling ": "Desinstalando ",
|
||||||
@@ -220,10 +226,10 @@
|
|||||||
"Scanning ": "Escaneando ",
|
"Scanning ": "Escaneando ",
|
||||||
"Creating ": "Creando ",
|
"Creating ": "Creando ",
|
||||||
"Copying ": "Copiando ",
|
"Copying ": "Copiando ",
|
||||||
"Trying to load ": "Intentando cargar",
|
"Trying to load ": "Intentando cargar ",
|
||||||
"Downloading ": "Descargando ",
|
"Downloading ": "Descargando ",
|
||||||
"Downloaded ": "",
|
"Downloaded ": "Descargado ",
|
||||||
"Removed ": "",
|
"Removed ": "Removido ",
|
||||||
"Checking MD5": "Chequeando MD5",
|
"Checking MD5": "Chequeando MD5",
|
||||||
"Loading...": "Cargando...",
|
"Loading...": "Cargando...",
|
||||||
"Loading": "Cargando",
|
"Loading": "Cargando",
|
||||||
@@ -233,17 +239,19 @@
|
|||||||
"Update avaliable: ": "Actualización disponible: ",
|
"Update avaliable: ": "Actualización disponible: ",
|
||||||
"Download update: ": "Descargar actualización: ",
|
"Download update: ": "Descargar actualización: ",
|
||||||
"Updated to ": "Actualizado a ",
|
"Updated to ": "Actualizado a ",
|
||||||
"Restart Sphaira?": "¿Reiniciar Sphaira?",
|
"Press OK to restart Sphaira": "Presiona OK para reiniciar sphaira",
|
||||||
|
"Restart Sphaira?": "¿Reiniciar sphaira?",
|
||||||
"Failed to download update": "Fallo al descargar actualización",
|
"Failed to download update": "Fallo al descargar actualización",
|
||||||
"Restore hbmenu?": "",
|
"Restore hbmenu?": "¿Restaurar hbmenu?",
|
||||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Fallo al encontrar /switch/hbmenu.nro\nUsar la Tienda para reinstalar hbmenu",
|
||||||
"Failed to restore hbmenu, please re-download hbmenu": "",
|
"Failed to restore hbmenu, please re-download hbmenu": "Fallo al restaurar hbmenu, por favor volver a descargar hbmenu",
|
||||||
"Failed to restore hbmenu, using sphaira instead": "",
|
"Failed to restore hbmenu, using sphaira instead": "Fallo al restaurar hbmenu, se usará sphaira",
|
||||||
"Restored hbmenu, closing sphaira": "",
|
"Restored hbmenu, closing sphaira": "hbmenu restaurado, cerrando sphaira",
|
||||||
"Restored hbmenu": "",
|
"Restored hbmenu": "hbmenu restaurado",
|
||||||
"Delete Selected files?": "¿Eliminar archivos seleccionados?",
|
"Delete Selected files?": "¿Eliminar archivos seleccionados?",
|
||||||
"Completely remove ": "Eliminar completamente",
|
"Completely remove ": "Eliminar completamente",
|
||||||
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
|
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
|
||||||
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
|
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
|
||||||
|
"Audio disabled due to suspended game": "",
|
||||||
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
|
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "Sd",
|
"Sd": "Sd",
|
||||||
"Image System memory": "Image de la mémoire System",
|
"Image System memory": "Image de la mémoire System",
|
||||||
"Image microSD card": "Image de la Carte microSD",
|
"Image microSD card": "Image de la Carte microSD",
|
||||||
|
"Slow": "Lent",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Rapide",
|
||||||
"Yes": "Oui",
|
"Yes": "Oui",
|
||||||
"No": "Non",
|
"No": "Non",
|
||||||
"Enabled": "Activé(e)",
|
"Enabled": "Activé(e)",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "Alphabétique (Favories)",
|
"Alphabetical (Star)": "Alphabétique (Favories)",
|
||||||
"Likes": "Likes",
|
"Likes": "Likes",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Décroissant",
|
"Descending": "Décroissant",
|
||||||
"Descending (down)": "Décroissant",
|
"Descending (down)": "Décroissant",
|
||||||
"Desc": "Décroissant",
|
"Desc": "Décroissant",
|
||||||
"Ascending": "Croissant",
|
"Ascending": "Croissant",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "Choisir un Thème",
|
"Select Theme": "Choisir un Thème",
|
||||||
"Shuffle": "Aléatoire",
|
"Shuffle": "Aléatoire",
|
||||||
"Music": "Musique",
|
"Music": "Musique",
|
||||||
|
"12 Hour Time": "Temps sur 12 heures",
|
||||||
"Network": "Réseau",
|
"Network": "Réseau",
|
||||||
"Network Options": "Options Réseau",
|
"Network Options": "Options Réseau",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Journalisation",
|
"Logging": "Journalisation",
|
||||||
"Replace hbmenu on exit": "Remplacer hbmenu quand quitté",
|
"Replace hbmenu on exit": "Remplacer hbmenu quand quitté",
|
||||||
"Misc": "Divers",
|
"Misc": "Divers",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "Installer les Forwarders",
|
"Install forwarders": "Installer les Forwarders",
|
||||||
"Install location": "Emplacement d'installation",
|
"Install location": "Emplacement d'installation",
|
||||||
"Show install warning": "Afficher l'avertissement d'installation",
|
"Show install warning": "Afficher l'avertissement d'installation",
|
||||||
|
"Text scroll speed": "Vitesse de défilement du texte",
|
||||||
|
|
||||||
"FileBrowser": "Explorateur de Fichiers",
|
"FileBrowser": "Explorateur de Fichiers",
|
||||||
"%zd files": "%zd fichiers",
|
"%zd files": "%zd fichiers",
|
||||||
@@ -233,6 +239,7 @@
|
|||||||
"Update avaliable: ": "Mise à jour disponible: ",
|
"Update avaliable: ": "Mise à jour disponible: ",
|
||||||
"Download update: ": "Télécharger la mise à jour: ",
|
"Download update: ": "Télécharger la mise à jour: ",
|
||||||
"Updated to ": "Mis à jour vers ",
|
"Updated to ": "Mis à jour vers ",
|
||||||
|
"Press OK to restart Sphaira": "Appuyez sur OK pour redémarrer Sphaira",
|
||||||
"Restart Sphaira?": "Redémarrer Sphaira?",
|
"Restart Sphaira?": "Redémarrer Sphaira?",
|
||||||
"Failed to download update": "Echec du téléchargement de la mise à jour",
|
"Failed to download update": "Echec du téléchargement de la mise à jour",
|
||||||
"Restore hbmenu?": "Restaurer hbmenu?",
|
"Restore hbmenu?": "Restaurer hbmenu?",
|
||||||
@@ -245,5 +252,6 @@
|
|||||||
"Completely remove ": "Supprimer totalement ",
|
"Completely remove ": "Supprimer totalement ",
|
||||||
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
|
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
|
||||||
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
|
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
|
||||||
|
"Audio disabled due to suspended game": "Audio désactivé à cause d'un jeu suspendu",
|
||||||
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
|
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "SD",
|
"Sd": "SD",
|
||||||
"Image System memory": "Immagine memoria di sistema",
|
"Image System memory": "Immagine memoria di sistema",
|
||||||
"Image microSD card": "Immagine scheda microSD",
|
"Image microSD card": "Immagine scheda microSD",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Sì",
|
"Yes": "Sì",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
"Enabled": "Abilitato",
|
"Enabled": "Abilitato",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "Alfabetico (Preferiti)",
|
"Alphabetical (Star)": "Alfabetico (Preferiti)",
|
||||||
"Likes": "Mi Piace",
|
"Likes": "Mi Piace",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Decrescente",
|
"Descending": "Decrescente",
|
||||||
"Descending (down)": "Decrescente",
|
"Descending (down)": "Decrescente",
|
||||||
"Desc": "Decrescente",
|
"Desc": "Decrescente",
|
||||||
"Ascending": "Crescente",
|
"Ascending": "Crescente",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "Seleziona tema",
|
"Select Theme": "Seleziona tema",
|
||||||
"Shuffle": "Mescola",
|
"Shuffle": "Mescola",
|
||||||
"Music": "Musica",
|
"Music": "Musica",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Rete",
|
"Network": "Rete",
|
||||||
"Network Options": "Opzioni di rete",
|
"Network Options": "Opzioni di rete",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Logging",
|
"Logging": "Logging",
|
||||||
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
|
"Replace hbmenu on exit": "Sostituisci hbmenu all'uscita",
|
||||||
"Misc": "Varie",
|
"Misc": "Varie",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "Installa forwarder",
|
"Install forwarders": "Installa forwarder",
|
||||||
"Install location": "Installa posizione",
|
"Install location": "Installa posizione",
|
||||||
"Show install warning": "Mostra avvertimento installazione",
|
"Show install warning": "Mostra avvertimento installazione",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "FileBrowser",
|
"FileBrowser": "FileBrowser",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd files",
|
||||||
@@ -198,52 +204,54 @@
|
|||||||
"Load Default": "Carica predefinito",
|
"Load Default": "Carica predefinito",
|
||||||
|
|
||||||
"Themezer": "Themezer",
|
"Themezer": "Themezer",
|
||||||
"Themezer Options": "",
|
"Themezer Options": "Impostazioni Themezer",
|
||||||
"Nsfw": "",
|
"Nsfw": "NSFW",
|
||||||
"Page": "",
|
"Page": "Pagina",
|
||||||
"Page %zu / %zu": "Page %zu / %zu",
|
"Page %zu / %zu": "Pagina %zu / %zu",
|
||||||
"Enter Page Number": "",
|
"Enter Page Number": "Inserisci il numero della pagina",
|
||||||
"Bad Page": "",
|
"Bad Page": "Pagina invalida",
|
||||||
"Download theme?": "",
|
"Download theme?": "Vuoi scaricare il tema?",
|
||||||
|
|
||||||
"GitHub": "",
|
"GitHub": "GitHub",
|
||||||
"Downloading json": "",
|
"Downloading json": "Scaricamento json",
|
||||||
"Select asset to download for ": "",
|
"Select asset to download for": "Scegli l'asset da scaricare per",
|
||||||
|
|
||||||
"Installing ": "",
|
"Installing ": "Installazione",
|
||||||
"Uninstalling ": "",
|
"Uninstalling ": "Disinstallazione",
|
||||||
"Deleting ": "",
|
"Deleting ": "Eliminazione",
|
||||||
"Deleting": "",
|
"Deleting": "Eliminazione",
|
||||||
"Pasting ": "",
|
"Pasting ": "Incollo",
|
||||||
"Pasting": "",
|
"Pasting": "Incollo",
|
||||||
"Removing ": "",
|
"Removing ": "Rimozione",
|
||||||
"Scanning ": "",
|
"Scanning ": "Scan",
|
||||||
"Creating ": "",
|
"Creating ": "Creazione",
|
||||||
"Copying ": "",
|
"Copying ": "Copio",
|
||||||
"Trying to load ": "",
|
"Trying to load ": "Cercando di caricare",
|
||||||
"Downloading ": "",
|
"Downloading ": "Scaricando",
|
||||||
"Downloaded ": "",
|
"Downloaded ": "Scaricato",
|
||||||
"Removed ": "",
|
"Removed ": ""Rimosso,
|
||||||
"Checking MD5": "",
|
"Checking MD5": "Controllo MD5",
|
||||||
"Loading...": "",
|
"Loading...": "Caricamento...",
|
||||||
"Loading": "",
|
"Loading": "Caricamento",
|
||||||
"Empty!": "",
|
"Empty!": "Vuoto!",
|
||||||
"Not Ready...": "",
|
"Not Ready...": "Non pronto...",
|
||||||
"Error loading page!": "",
|
"Error loading page!": "Errore nel caricare la pagina!",
|
||||||
"Update avaliable: ": "",
|
"Update avaliable: ": "Aggiornamento disponibile",
|
||||||
"Download update: ": "",
|
"Download update: ": "Scarica aggiornamento",
|
||||||
"Updated to ": "",
|
"Updated to ": "Aggiornato a",
|
||||||
"Restart Sphaira?": "",
|
"Press OK to restart Sphaira": "Premi OK per riavviare Sphaira",
|
||||||
"Failed to download update": "",
|
"Restart Sphaira?": "Vuoi riavviare Sphaira?",
|
||||||
"Restore hbmenu?": "",
|
"Failed to download update": "Download aggiornamento fallito",
|
||||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
|
"Restore hbmenu?": "Vuoi ripristinare hbmenu?",
|
||||||
"Failed to restore hbmenu, please re-download hbmenu": "",
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Impossibile trovare /switch/hbmenu.nro\nUsa l'Appstore per reinstallare hbmenu",
|
||||||
"Failed to restore hbmenu, using sphaira instead": "",
|
"Failed to restore hbmenu, please re-download hbmenu": "Impossibile ripristinare hbmenu, per favore riscaricalo",
|
||||||
"Restored hbmenu, closing sphaira": "",
|
"Failed to restore hbmenu, using sphaira instead": "Impossibile ripristinare hbmenu, uso Sphaira invece",
|
||||||
"Restored hbmenu": "",
|
"Restored hbmenu, closing sphaira": "hbmenu ripristinato, chiudo Sphaira",
|
||||||
"Delete Selected files?": "",
|
"Restored hbmenu": "hbmenu ripristinato",
|
||||||
"Completely remove ": "",
|
"Delete Selected files?": "Vuoi rimuovere i file selezionati?",
|
||||||
|
"Completely remove ": "Elimina definitivamente",
|
||||||
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
|
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
|
||||||
"Are you sure you wish to cancel?": "",
|
"Are you sure you wish to cancel?": "Sei sicuro di voler annullare?",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"Audio disabled due to suspended game": "Audio disabilitato poichè un app è in pausa",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "Se questo messaggio appare frequentemente, segnala il bug."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "SDメモリーカード",
|
"Sd": "SDメモリーカード",
|
||||||
"Image System memory": "システムメモリイメージ",
|
"Image System memory": "システムメモリイメージ",
|
||||||
"Image microSD card": "SDイメージ",
|
"Image microSD card": "SDイメージ",
|
||||||
|
"Slow": "遅い",
|
||||||
|
"Normal": "普通",
|
||||||
|
"Fast": "速い",
|
||||||
"Yes": "はい",
|
"Yes": "はい",
|
||||||
"No": "いいえ",
|
"No": "いいえ",
|
||||||
"Enabled": "",
|
"Enabled": "",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "アルファベット順(お気に入り)",
|
"Alphabetical (Star)": "アルファベット順(お気に入り)",
|
||||||
"Likes": "いいね順",
|
"Likes": "いいね順",
|
||||||
"ID": "デベロッパー順",
|
"ID": "デベロッパー順",
|
||||||
"Decending": "降順",
|
"Descending": "降順",
|
||||||
"Descending (down)": "降順",
|
"Descending (down)": "降順",
|
||||||
"Desc": "降順",
|
"Desc": "降順",
|
||||||
"Ascending": "上昇",
|
"Ascending": "上昇",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "テーマを選ぶ",
|
"Select Theme": "テーマを選ぶ",
|
||||||
"Shuffle": "シャッフル",
|
"Shuffle": "シャッフル",
|
||||||
"Music": "BGM",
|
"Music": "BGM",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "ネットワーク",
|
"Network": "ネットワーク",
|
||||||
"Network Options": "ネットワーク設定",
|
"Network Options": "ネットワーク設定",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "ログの取得",
|
"Logging": "ログの取得",
|
||||||
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
|
"Replace hbmenu on exit": "終了時に hbmenu を置き換える",
|
||||||
"Misc": "その他",
|
"Misc": "その他",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "Forwarderのインストール機能",
|
"Install forwarders": "Forwarderのインストール機能",
|
||||||
"Install location": "インストール経路",
|
"Install location": "インストール経路",
|
||||||
"Show install warning": "警告文を示す",
|
"Show install warning": "警告文を示す",
|
||||||
|
"Text scroll speed": "流れる文字の速さ",
|
||||||
|
|
||||||
"FileBrowser": "ファイルブラウザ",
|
"FileBrowser": "ファイルブラウザ",
|
||||||
"%zd files": "%zd個のファイル",
|
"%zd files": "%zd個のファイル",
|
||||||
@@ -233,6 +239,7 @@
|
|||||||
"Update avaliable: ": "アップデート可能: ",
|
"Update avaliable: ": "アップデート可能: ",
|
||||||
"Download update: ": "アップデートをダウンロード: ",
|
"Download update: ": "アップデートをダウンロード: ",
|
||||||
"Updated to ": "アップデート: ",
|
"Updated to ": "アップデート: ",
|
||||||
|
"Press OK to restart Sphaira": "確認ボタンを押してSphairaを再起動",
|
||||||
"Restart Sphaira?": "Sphairaを再起動しますか?",
|
"Restart Sphaira?": "Sphairaを再起動しますか?",
|
||||||
"Failed to download update": "アップデートのダウンロード失敗",
|
"Failed to download update": "アップデートのダウンロード失敗",
|
||||||
"Restore hbmenu?": "hbmenuに戻しますか?",
|
"Restore hbmenu?": "hbmenuに戻しますか?",
|
||||||
@@ -245,5 +252,6 @@
|
|||||||
"Completely remove ": "除去しますか ",
|
"Completely remove ": "除去しますか ",
|
||||||
"Are you sure you want to delete ": "消去してもよろしいですか ",
|
"Are you sure you want to delete ": "消去してもよろしいですか ",
|
||||||
"Are you sure you wish to cancel?": "本当に取り消しますか?",
|
"Are you sure you wish to cancel?": "本当に取り消しますか?",
|
||||||
|
"Audio disabled due to suspended game": "ゲームが一時停止状態の場合、オーディオは無効になります",
|
||||||
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
|
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "SD 카드",
|
"Sd": "SD 카드",
|
||||||
"Image System memory": "낸드 이미지",
|
"Image System memory": "낸드 이미지",
|
||||||
"Image microSD card": "SD 이미지",
|
"Image microSD card": "SD 이미지",
|
||||||
|
"Slow": "느림",
|
||||||
|
"Normal": "보통",
|
||||||
|
"Fast": "빠름",
|
||||||
"Yes": "예",
|
"Yes": "예",
|
||||||
"No": "아니요",
|
"No": "아니요",
|
||||||
"Enabled": "",
|
"Enabled": "",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
|
"Alphabetical (Star)": "알파벳순 (즐겨찾기)",
|
||||||
"Likes": "좋아요순",
|
"Likes": "좋아요순",
|
||||||
"ID": "ID순",
|
"ID": "ID순",
|
||||||
"Decending": "내림차순",
|
"Descending": "내림차순",
|
||||||
"Descending (down)": "내림차순",
|
"Descending (down)": "내림차순",
|
||||||
"Desc": "내림차순",
|
"Desc": "내림차순",
|
||||||
"Ascending": "오름차순",
|
"Ascending": "오름차순",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "테마 선택",
|
"Select Theme": "테마 선택",
|
||||||
"Shuffle": "셔플",
|
"Shuffle": "셔플",
|
||||||
"Music": "BGM",
|
"Music": "BGM",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "네트워크",
|
"Network": "네트워크",
|
||||||
"Network Options": "네트워크 옵션",
|
"Network Options": "네트워크 옵션",
|
||||||
"Ftp": "FTP (무선)",
|
"Ftp": "FTP (무선)",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "로깅",
|
"Logging": "로깅",
|
||||||
"Replace hbmenu on exit": "종료 시 hbmenu 교체",
|
"Replace hbmenu on exit": "종료 시 hbmenu 교체",
|
||||||
"Misc": "기타",
|
"Misc": "기타",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "바로가기 설치",
|
"Install forwarders": "바로가기 설치",
|
||||||
"Install location": "설치 위치",
|
"Install location": "설치 위치",
|
||||||
"Show install warning": "경고 메시지",
|
"Show install warning": "경고 메시지",
|
||||||
|
"Text scroll speed": "흐르는 텍스트 속도",
|
||||||
|
|
||||||
"FileBrowser": "파일 탐색기",
|
"FileBrowser": "파일 탐색기",
|
||||||
"%zd files": "%zd 개 파일",
|
"%zd files": "%zd 개 파일",
|
||||||
@@ -233,6 +239,7 @@
|
|||||||
"Update avaliable: ": "업데이트 가능: ",
|
"Update avaliable: ": "업데이트 가능: ",
|
||||||
"Download update: ": "업데이트 다운로드: ",
|
"Download update: ": "업데이트 다운로드: ",
|
||||||
"Updated to ": "업데이트: ",
|
"Updated to ": "업데이트: ",
|
||||||
|
"Press OK to restart Sphaira": "확인 버튼 입력하여 Sphaira 재시작",
|
||||||
"Restart Sphaira?": "Sphaira를 재시작할까요?",
|
"Restart Sphaira?": "Sphaira를 재시작할까요?",
|
||||||
"Failed to download update": "업데이트 다운로드 실패함",
|
"Failed to download update": "업데이트 다운로드 실패함",
|
||||||
"Restore hbmenu?": "hbmenu로 교체할까요?",
|
"Restore hbmenu?": "hbmenu로 교체할까요?",
|
||||||
@@ -245,5 +252,6 @@
|
|||||||
"Completely remove ": "정말 삭제할까요 ",
|
"Completely remove ": "정말 삭제할까요 ",
|
||||||
"Are you sure you want to delete ": "정말 삭제할까요 ",
|
"Are you sure you want to delete ": "정말 삭제할까요 ",
|
||||||
"Are you sure you wish to cancel?": "정말 취소할까요?",
|
"Are you sure you wish to cancel?": "정말 취소할까요?",
|
||||||
|
"Audio disabled due to suspended game": "게임 실행 중에는 BGM이 비활성화 됩니다",
|
||||||
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
|
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "",
|
"Sd": "",
|
||||||
"Image System memory": "",
|
"Image System memory": "",
|
||||||
"Image microSD card": "",
|
"Image microSD card": "",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"No": "Nee",
|
"No": "Nee",
|
||||||
"Enabled": "Ingeschakeld",
|
"Enabled": "Ingeschakeld",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "",
|
"Alphabetical (Star)": "",
|
||||||
"Likes": "",
|
"Likes": "",
|
||||||
"ID": "",
|
"ID": "",
|
||||||
"Decending": "Aflopend",
|
"Descending": "Aflopend",
|
||||||
"Descending (down)": "Aflopend",
|
"Descending (down)": "Aflopend",
|
||||||
"Desc": "Aflopend",
|
"Desc": "Aflopend",
|
||||||
"Ascending": "Oplopend",
|
"Ascending": "Oplopend",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "Selecteer Thema",
|
"Select Theme": "Selecteer Thema",
|
||||||
"Shuffle": "Schudden",
|
"Shuffle": "Schudden",
|
||||||
"Music": "Muziek",
|
"Music": "Muziek",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Netwerk",
|
"Network": "Netwerk",
|
||||||
"Network Options": "Netwerkopties",
|
"Network Options": "Netwerkopties",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Loggen",
|
"Logging": "Loggen",
|
||||||
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
|
"Replace hbmenu on exit": "Vervang hbmenu bij afsluiten",
|
||||||
"Misc": "Diversen",
|
"Misc": "Diversen",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "",
|
"Install forwarders": "",
|
||||||
"Install location": "",
|
"Install location": "",
|
||||||
"Show install warning": "",
|
"Show install warning": "",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "Bestandsbrowser",
|
"FileBrowser": "Bestandsbrowser",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd files",
|
||||||
@@ -233,6 +239,7 @@
|
|||||||
"Update avaliable: ": "",
|
"Update avaliable: ": "",
|
||||||
"Download update: ": "",
|
"Download update: ": "",
|
||||||
"Updated to ": "",
|
"Updated to ": "",
|
||||||
|
"Press OK to restart Sphaira": "",
|
||||||
"Restart Sphaira?": "",
|
"Restart Sphaira?": "",
|
||||||
"Failed to download update": "",
|
"Failed to download update": "",
|
||||||
"Restore hbmenu?": "",
|
"Restore hbmenu?": "",
|
||||||
@@ -245,5 +252,6 @@
|
|||||||
"Completely remove ": "",
|
"Completely remove ": "",
|
||||||
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
|
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
|
||||||
"Are you sure you wish to cancel?": "",
|
"Are you sure you wish to cancel?": "",
|
||||||
|
"Audio disabled due to suspended game": "",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"If this message appears repeatedly, please open an issue.": ""
|
||||||
}
|
}
|
||||||
@@ -28,45 +28,49 @@
|
|||||||
"System memory": "Memória do console",
|
"System memory": "Memória do console",
|
||||||
"microSD card": "Cartão microSD",
|
"microSD card": "Cartão microSD",
|
||||||
"Sd": "SD",
|
"Sd": "SD",
|
||||||
"Image System memory": "Imagem (Memória do console)",
|
"Image System memory": "Imagem (memória do console)",
|
||||||
"Image microSD card": "Imagem (Cartão microSD)",
|
"Image microSD card": "Imagem (cartão microSD)",
|
||||||
|
"Slow": "Lenta",
|
||||||
|
"Normal": "Normal",
|
||||||
|
"Fast": "Rápida",
|
||||||
"Yes": "Sim",
|
"Yes": "Sim",
|
||||||
"No": "Não",
|
"No": "Não",
|
||||||
"Enabled": "Habilitado",
|
"Enabled": "Sim",
|
||||||
"Disabled": "Desabilitado",
|
"Disabled": "Não",
|
||||||
|
|
||||||
"Sort By": "Ordenar por",
|
"Sort By": "Ordernar/Organizar",
|
||||||
"Sort Options": "Opções de classificação",
|
"Sort Options": "Ordernar/Organizar",
|
||||||
"Filter": "Filtro",
|
"Filter": "Filtro",
|
||||||
"Sort": "Organizar",
|
"Sort": "Organizar por",
|
||||||
"Order": "Ordem",
|
"Order": "Ordem",
|
||||||
"Search": "Procurar",
|
"Search": "Buscar",
|
||||||
"Updated": "Atualizado",
|
"Updated": "Atualizado",
|
||||||
"Updated (Star)": "Atualizado (Favoritos)",
|
"Updated (Star)": "Atualizado (favoritos)",
|
||||||
"Downloads": "Downloads",
|
"Downloads": "Nº de downloads",
|
||||||
"Size": "Tamanho",
|
"Size": "Tamanho",
|
||||||
"Size (Star)": "Tamanho (Favoritos)",
|
"Size (Star)": "Tamanho (favoritos)",
|
||||||
"Alphabetical": "Alfabético",
|
"Alphabetical": "Ordem alfabética",
|
||||||
"Alphabetical (Star)": "Alfabético (Favoritos)",
|
"Alphabetical (Star)": "Ordem alfabética (favoritos)",
|
||||||
"Likes": "Curtidas",
|
"Likes": "Nº de curtidas",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Decrescente",
|
"Descending": "Decrescente",
|
||||||
"Descending (down)": "Decrescente (Baixo)",
|
"Descending (down)": "Decrescente (baixo)",
|
||||||
"Desc": "Decr.",
|
"Desc": "Decr.",
|
||||||
"Ascending": "Ascendente",
|
"Ascending": "Ascendente",
|
||||||
"Ascending (Up)": "Ascendente (Cima)",
|
"Ascending (Up)": "Ascendente (cima)",
|
||||||
"Asc": "Asc.",
|
"Asc": "Asc.",
|
||||||
|
|
||||||
"Menu Options": "Opções do menu",
|
"Menu Options": "Opções do menu",
|
||||||
"Theme": "Tema",
|
"Theme": "Tema",
|
||||||
"Theme Options": "Opções de tema",
|
"Theme Options": "Opções de tema",
|
||||||
"Select Theme": "Selecionar tema",
|
"Select Theme": "Tema atual",
|
||||||
"Shuffle": "Embaralhar",
|
"Shuffle": "Embaralhar temas",
|
||||||
"Music": "Música",
|
"Music": "Música",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Rede",
|
"Network": "Rede",
|
||||||
"Network Options": "Opções de rede",
|
"Network Options": "Opções de rede",
|
||||||
"Ftp": "FTP",
|
"Ftp": "Servidor FTP",
|
||||||
"Mtp": "MTP",
|
"Mtp": "Escuta MTP",
|
||||||
"Nxlink": "Nxlink",
|
"Nxlink": "Nxlink",
|
||||||
"Nxlink Connected": "Nxlink conectado",
|
"Nxlink Connected": "Nxlink conectado",
|
||||||
"Nxlink Upload": "Envio Nxlink",
|
"Nxlink Upload": "Envio Nxlink",
|
||||||
@@ -87,49 +91,51 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Registro de depuração",
|
"Logging": "Registro de depuração",
|
||||||
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
|
"Replace hbmenu on exit": "Substituir hbmenu ao sair",
|
||||||
"Misc": "Diversos",
|
"Misc": "Diversos",
|
||||||
"Misc Options": "Opções diversas",
|
"Misc Options": "Opções diversas",
|
||||||
"Web": "Navegador web",
|
"Web": "Navegador de internet",
|
||||||
"Install forwarders": "Instalar forwarder",
|
"Install forwarders": "Instalar atalhos (forwarders)",
|
||||||
"Install location": "Local de instalação",
|
"Install location": "Local de instalação",
|
||||||
"Show install warning": "Mostrar aviso de instalação",
|
"Show install warning": "Mostrar aviso de instalação",
|
||||||
|
"Text scroll speed": "Rolagem do texto",
|
||||||
|
|
||||||
"FileBrowser": "Navegador de arquivos",
|
"FileBrowser": "Arquivos",
|
||||||
"%zd files": "%zd arquivo(s)",
|
"%zd files": "%zd arquivo(s)",
|
||||||
"%zd dirs": "%zd diretório(s)",
|
"%zd dirs": "%zd diretório(s)",
|
||||||
"File Options": "Opções de arquivo",
|
"File Options": "Opções de arquivo",
|
||||||
"Show Hidden": "Mostrar ocultos",
|
"Show Hidden": "Mostrar ocultos",
|
||||||
"Folders First": "Pastas primeiro",
|
"Folders First": "Pastas primeiro",
|
||||||
"Hidden Last": "Ocultos por último",
|
"Hidden Last": "Ocultos por último",
|
||||||
"Cut": "Cortar",
|
"Cut": "Recortar",
|
||||||
"Copy": "Copiar",
|
"Copy": "Copiar",
|
||||||
"Paste": "Colar",
|
"Paste": "Colar",
|
||||||
"Paste ": "Colar",
|
"Paste ": "Colar ",
|
||||||
" file(s)?": " arquivo(s)?",
|
" file(s)?": " arquivo(s)?",
|
||||||
"Rename": "Renomear",
|
"Rename": "Renomear",
|
||||||
"Set New File Name": "Definir novo nome do arquivo",
|
"Set New File Name": "Defina o nome do novo arquivo",
|
||||||
"Advanced": "Avançado",
|
"Advanced": "Avançado",
|
||||||
"Advanced Options": "Opções avançadas",
|
"Advanced Options": "Opções avançadas",
|
||||||
"Create File": "Criar arquivo",
|
"Create File": "Criar arquivo",
|
||||||
"Set File Name": "Definir nome do arquivo",
|
"Set File Name": "Defina o nome do arquivo",
|
||||||
"Create Folder": "Criar pasta",
|
"Create Folder": "Criar pasta",
|
||||||
"Set Folder Name": "Definir novo nome da pasta",
|
"Set Folder Name": "Defina o nome da pasta",
|
||||||
"View as text (unfinished)": "Ver como texto (inacabado)",
|
"View as text (unfinished)": "Ver como texto (inacabado)",
|
||||||
"Ignore read only": "Ignorar somente leitura",
|
"Ignore read only": "Ignorar somente leitura",
|
||||||
"Mount": "Montar",
|
"Mount": "Montar",
|
||||||
"Empty...": "Vazio...",
|
"Empty...": "Vazio...",
|
||||||
"Open with DayBreak?": "Abrir com DayBreak?",
|
"Open with DayBreak?": "Abrir com DayBreak?",
|
||||||
"Launch ": "Iniciar",
|
"Launch ": "Iniciar ",
|
||||||
"Launch option for: ": "Opções de inicialização para: ",
|
"Launch option for: ": "Opções de inicialização para: ",
|
||||||
"Select launcher for: ": "Selecionar launcher para: ",
|
"Select launcher for: ": "Selecionar launcher para: ",
|
||||||
|
|
||||||
"Homebrew": "Homebrew",
|
"Homebrew": "Aplicativos",
|
||||||
"Homebrew Options": "Opções do Homebrew",
|
"Homebrew Options": "Opções do aplicativo",
|
||||||
"Hide Sphaira": "Esconder Sphaira",
|
"Hide Sphaira": "Esconder sphaira",
|
||||||
"Install Forwarder": "Instalar forwarder",
|
"Install Forwarder": "Instalar atalho (forwarder)",
|
||||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar forwarders pode\nresultar em um banimento!",
|
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode\nresultar em um banimento!",
|
||||||
"Installing Forwarder": "Instalando forwarder",
|
"Installing Forwarder": "Instalando forwarder",
|
||||||
"Creating Program": "Criando Program",
|
"Creating Program": "Criando Program",
|
||||||
"Creating Control": "Criando Control",
|
"Creating Control": "Criando Control",
|
||||||
@@ -142,9 +148,9 @@
|
|||||||
"Unstarred ": "Desfavoritado ",
|
"Unstarred ": "Desfavoritado ",
|
||||||
"Starred ": "Favoritado ",
|
"Starred ": "Favoritado ",
|
||||||
|
|
||||||
"AppStore": "AppStore",
|
"AppStore": "Loja",
|
||||||
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Organizar: %s | Ordem: %s",
|
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Por: %s | Ordem: %s",
|
||||||
"AppStore Options": "Opções da AppStore",
|
"AppStore Options": "Opções da loja",
|
||||||
"All": "Todos",
|
"All": "Todos",
|
||||||
"Games": "Jogos",
|
"Games": "Jogos",
|
||||||
"Emulators": "Emuladores",
|
"Emulators": "Emuladores",
|
||||||
@@ -156,29 +162,29 @@
|
|||||||
"category: %s": "categoria: %s",
|
"category: %s": "categoria: %s",
|
||||||
"extracted: %.2f MiB": "tam. extraído: %.2f MiB",
|
"extracted: %.2f MiB": "tam. extraído: %.2f MiB",
|
||||||
"app_dls: %s": "downloads: %s",
|
"app_dls: %s": "downloads: %s",
|
||||||
"More by Author": "Mais do autor",
|
"More by Author": "Mais deste autor",
|
||||||
"Leave Feedback": "Deixar um feedback",
|
"Leave Feedback": "Deixar um feedback",
|
||||||
|
|
||||||
"Irs": "Irs",
|
"Irs": "Sensor infravermelho",
|
||||||
"Ambient Noise Level: ": "Nível de ruído ambiente",
|
"Ambient Noise Level: ": "Nível de ruído ambiente: ",
|
||||||
"Controller": "Controle",
|
"Controller": "Controle",
|
||||||
"Pad ": "Pad ",
|
"Pad ": "Pad ",
|
||||||
" (Available)": " (Disponível)",
|
" (Available)": " (disponível)",
|
||||||
" (Unsupported)": "(Não suportado)",
|
" (Unsupported)": "(não suportado)",
|
||||||
" (Unconnected)": " (Desconectado)",
|
" (Unconnected)": " (desconectado)",
|
||||||
"HandHeld": "Portátil",
|
"HandHeld": "Portátil",
|
||||||
"Rotation": "Rotação",
|
"Rotation": "Rotação",
|
||||||
"0 (Sideways)": "0 (Lateralmente)",
|
"0 (Sideways)": "0 (lateralmente)",
|
||||||
"90 (Flat)": "90 (plano)",
|
"90 (Flat)": "90 (plano)",
|
||||||
"180 (-Sideways)": "180 (-Lateralmente)",
|
"180 (-Sideways)": "180 (-lateralmente)",
|
||||||
"270 (Upside down)": "270 (De cabeça para baixo)",
|
"270 (Upside down)": "270 (de cabeça para baixo)",
|
||||||
"Colour": "Cor",
|
"Colour": "Cor",
|
||||||
"Grey": "Cinza",
|
"Grey": "Cinza",
|
||||||
"Ironbow": "Arco de ferro",
|
"Ironbow": "Ferro",
|
||||||
"Green": "Verde",
|
"Green": "Verde",
|
||||||
"Red": "Vermelho",
|
"Red": "Vermelho",
|
||||||
"Blue": "Azul",
|
"Blue": "Azul",
|
||||||
"Light Target": "Alvo leve",
|
"Light Target": "Alvo de luz",
|
||||||
"All leds": "Todos os LEDs",
|
"All leds": "Todos os LEDs",
|
||||||
"Bright group": "Grupo claro",
|
"Bright group": "Grupo claro",
|
||||||
"Dim group": "Grupo escuro",
|
"Dim group": "Grupo escuro",
|
||||||
@@ -193,27 +199,27 @@
|
|||||||
"80x60": "80×60",
|
"80x60": "80×60",
|
||||||
"40x30": "40×30",
|
"40x30": "40×30",
|
||||||
"20x15": "20×15",
|
"20x15": "20×15",
|
||||||
"Trimming Format": "Formato de corte",
|
"Trimming Format": "Formato do recorte",
|
||||||
"External Light Filter": "Filtro de luz externa",
|
"External Light Filter": "Filtro de luz externa",
|
||||||
"Load Default": "Carregar padrão",
|
"Load Default": "Restaurar padrão",
|
||||||
|
|
||||||
"Themezer": "Themezer",
|
"Themezer": "Themezer",
|
||||||
"Themezer Options": "Opções do Themezer",
|
"Themezer Options": "Opções do Themezer",
|
||||||
"Nsfw": "NSFW",
|
"Nsfw": "Temas 18+ (NSFW)",
|
||||||
"Page": "Página",
|
"Page": "Ir para página",
|
||||||
"Page %zu / %zu": "Page %zu / %zu",
|
"Page %zu / %zu": "Página %zu / %zu",
|
||||||
"Enter Page Number": "Digite o número da página",
|
"Enter Page Number": "Número da página",
|
||||||
"Bad Page": "Página inválida",
|
"Bad Page": "Página inválida",
|
||||||
"Download theme?": "Baixar tema?",
|
"Download theme?": "Baixar tema?",
|
||||||
|
|
||||||
"GitHub": "GitHub",
|
"GitHub": "GitHub",
|
||||||
"Downloading json": "Baixando JSON",
|
"Downloading json": "Baixando JSON",
|
||||||
"Select asset to download for ": "Selecione o recurso para baixar em ",
|
"Select asset to download for ": "Selecione o recurso para baixar de ",
|
||||||
|
|
||||||
"Installing ": "Instalando ",
|
"Installing ": "Instalando ",
|
||||||
"Uninstalling ": "Desinstalando ",
|
"Uninstalling ": "Desinstalando ",
|
||||||
"Deleting ": "Deletando ",
|
"Deleting ": "Excluindo ",
|
||||||
"Deleting": "Deletando ",
|
"Deleting": "Excluindo",
|
||||||
"Pasting ": "Colando ",
|
"Pasting ": "Colando ",
|
||||||
"Pasting": "Colando ",
|
"Pasting": "Colando ",
|
||||||
"Removing ": "Removendo ",
|
"Removing ": "Removendo ",
|
||||||
@@ -222,18 +228,19 @@
|
|||||||
"Copying ": "Copiando ",
|
"Copying ": "Copiando ",
|
||||||
"Trying to load ": "Tentando carregar ",
|
"Trying to load ": "Tentando carregar ",
|
||||||
"Downloading ": "Baixando ",
|
"Downloading ": "Baixando ",
|
||||||
"Downloaded ": "Baixado",
|
"Downloaded ": "Baixado ",
|
||||||
"Removed ": "Removido",
|
"Removed ": "Removido ",
|
||||||
"Checking MD5": "Checando MD5",
|
"Checking MD5": "Checando MD5",
|
||||||
"Loading...": "Carregando...",
|
"Loading...": "Carregando...",
|
||||||
"Loading": "Carregando",
|
"Loading": "Carregando",
|
||||||
"Empty!": "Vazio!",
|
"Empty!": "Vazio",
|
||||||
"Not Ready...": "Não está pronto...",
|
"Not Ready...": "Não está pronto...",
|
||||||
"Error loading page!": "Erro ao carregar página!",
|
"Error loading page!": "Erro ao carregar página!",
|
||||||
"Update avaliable: ": "Atualização disponível: ",
|
"Update avaliable: ": "Atualização disponível: ",
|
||||||
"Download update: ": "Baixar autalização: ",
|
"Download update: ": "Baixar autalização: ",
|
||||||
"Updated to ": "Atualizado para ",
|
"Updated to ": "Atualizado para ",
|
||||||
"Restart Sphaira?": "Reiniciar Sphaira?",
|
"Press OK to restart Sphaira": "Selecione OK para reiniciar o sphaira",
|
||||||
|
"Restart Sphaira?": "Reiniciar sphaira?",
|
||||||
"Failed to download update": "Falha ao baixar a atualização",
|
"Failed to download update": "Falha ao baixar a atualização",
|
||||||
"Restore hbmenu?": "Restaurar hbmenu?",
|
"Restore hbmenu?": "Restaurar hbmenu?",
|
||||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a AppStore para reinstalar o hbmenu",
|
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a AppStore para reinstalar o hbmenu",
|
||||||
@@ -241,9 +248,10 @@
|
|||||||
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira",
|
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira",
|
||||||
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira",
|
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira",
|
||||||
"Restored hbmenu": "hbmenu restaurado",
|
"Restored hbmenu": "hbmenu restaurado",
|
||||||
"Delete Selected files?": "Deletar arquivos selecionados?",
|
"Delete Selected files?": "Excluir os arquivos selecionados?",
|
||||||
"Completely remove ": "Remover completamente ",
|
"Completely remove ": "Remover completamente ",
|
||||||
"Are you sure you want to delete ": "Você tem certeza que quer deletar ",
|
"Are you sure you want to delete ": "Você tem certeza que quer excluir ",
|
||||||
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
|
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
|
||||||
|
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso",
|
||||||
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
|
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "",
|
"Sd": "",
|
||||||
"Image System memory": "",
|
"Image System memory": "",
|
||||||
"Image microSD card": "",
|
"Image microSD card": "",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Да",
|
"Yes": "Да",
|
||||||
"No": "Нет",
|
"No": "Нет",
|
||||||
"Enabled": "Включено",
|
"Enabled": "Включено",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "",
|
"Alphabetical (Star)": "",
|
||||||
"Likes": "",
|
"Likes": "",
|
||||||
"ID": "",
|
"ID": "",
|
||||||
"Decending": "По убыванию",
|
"Descending": "По убыванию",
|
||||||
"Descending (down)": "По убыванию",
|
"Descending (down)": "По убыванию",
|
||||||
"Desc": "По убыванию",
|
"Desc": "По убыванию",
|
||||||
"Ascending": "По возрастанию",
|
"Ascending": "По возрастанию",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "Выберите тему",
|
"Select Theme": "Выберите тему",
|
||||||
"Shuffle": "Перетасовать",
|
"Shuffle": "Перетасовать",
|
||||||
"Music": "Музыка",
|
"Music": "Музыка",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Сеть",
|
"Network": "Сеть",
|
||||||
"Network Options": "Параметры сети",
|
"Network Options": "Параметры сети",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Журналирование",
|
"Logging": "Журналирование",
|
||||||
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
|
"Replace hbmenu on exit": "Заменить hbmenu при выходе",
|
||||||
"Misc": "Прочее",
|
"Misc": "Прочее",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "",
|
"Install forwarders": "",
|
||||||
"Install location": "",
|
"Install location": "",
|
||||||
"Show install warning": "",
|
"Show install warning": "",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "Файловый менеджер",
|
"FileBrowser": "Файловый менеджер",
|
||||||
"%zd files": "%zd files",
|
"%zd files": "%zd files",
|
||||||
@@ -233,6 +239,7 @@
|
|||||||
"Update avaliable: ": "",
|
"Update avaliable: ": "",
|
||||||
"Download update: ": "",
|
"Download update: ": "",
|
||||||
"Updated to ": "",
|
"Updated to ": "",
|
||||||
|
"Press OK to restart Sphaira": "",
|
||||||
"Restart Sphaira?": "",
|
"Restart Sphaira?": "",
|
||||||
"Failed to download update": "",
|
"Failed to download update": "",
|
||||||
"Restore hbmenu?": "",
|
"Restore hbmenu?": "",
|
||||||
@@ -245,5 +252,6 @@
|
|||||||
"Completely remove ": "",
|
"Completely remove ": "",
|
||||||
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
|
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
|
||||||
"Are you sure you wish to cancel?": "",
|
"Are you sure you wish to cancel?": "",
|
||||||
|
"Audio disabled due to suspended game": "",
|
||||||
"If this message appears repeatedly, please open an issue.": ""
|
"If this message appears repeatedly, please open an issue.": ""
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "Sd",
|
"Sd": "Sd",
|
||||||
"Image System memory": "Avbild Systemminne",
|
"Image System memory": "Avbild Systemminne",
|
||||||
"Image microSD card": "Avbild microSD-kort",
|
"Image microSD card": "Avbild microSD-kort",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"No": "Nej",
|
"No": "Nej",
|
||||||
"Enabled": "Aktiverad",
|
"Enabled": "Aktiverad",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "Alfabetisk (Stjärna)",
|
"Alphabetical (Star)": "Alfabetisk (Stjärna)",
|
||||||
"Likes": "Gillar",
|
"Likes": "Gillar",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Fallande",
|
"Descending": "Fallande",
|
||||||
"Descending (down)": "Fallande (nedåt)",
|
"Descending (down)": "Fallande (nedåt)",
|
||||||
"Desc": "Fall",
|
"Desc": "Fall",
|
||||||
"Ascending": "Stigande",
|
"Ascending": "Stigande",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "Välj tema",
|
"Select Theme": "Välj tema",
|
||||||
"Shuffle": "Blanda",
|
"Shuffle": "Blanda",
|
||||||
"Music": "Musik",
|
"Music": "Musik",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Nätverk",
|
"Network": "Nätverk",
|
||||||
"Network Options": "Nätverksalternativ",
|
"Network Options": "Nätverksalternativ",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Portugisiska",
|
"Portuguese": "Portugisiska",
|
||||||
"Russian": "Ryska",
|
"Russian": "Ryska",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "Loggning",
|
"Logging": "Loggning",
|
||||||
"Replace hbmenu on exit": "Ersätt hbmenu vid avslut",
|
"Replace hbmenu on exit": "Ersätt hbmenu vid avslut",
|
||||||
"Misc": "Övrigt",
|
"Misc": "Övrigt",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "Installera genvägar",
|
"Install forwarders": "Installera genvägar",
|
||||||
"Install location": "Installationsplats",
|
"Install location": "Installationsplats",
|
||||||
"Show install warning": "Visa installationsvarning",
|
"Show install warning": "Visa installationsvarning",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "Filbläddrare",
|
"FileBrowser": "Filbläddrare",
|
||||||
"%zd files": "%zd filer",
|
"%zd files": "%zd filer",
|
||||||
@@ -233,6 +239,7 @@
|
|||||||
"Update avaliable: ": "Uppdatering tillgänglig: ",
|
"Update avaliable: ": "Uppdatering tillgänglig: ",
|
||||||
"Download update: ": "Ladda ner uppdatering: ",
|
"Download update: ": "Ladda ner uppdatering: ",
|
||||||
"Updated to ": "Uppdaterad till ",
|
"Updated to ": "Uppdaterad till ",
|
||||||
|
"Press OK to restart Sphaira": "",
|
||||||
"Restart Sphaira?": "Starta om Sphaira?",
|
"Restart Sphaira?": "Starta om Sphaira?",
|
||||||
"Failed to download update": "Misslyckades att ladda ner uppdatering",
|
"Failed to download update": "Misslyckades att ladda ner uppdatering",
|
||||||
"Restore hbmenu?": "Återställ hbmenu?",
|
"Restore hbmenu?": "Återställ hbmenu?",
|
||||||
@@ -245,5 +252,6 @@
|
|||||||
"Completely remove ": "Ta bort helt ",
|
"Completely remove ": "Ta bort helt ",
|
||||||
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
|
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
|
||||||
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
|
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
|
||||||
|
"Audio disabled due to suspended game": "Ljud är avstängt på grund av bakgrundsprogram",
|
||||||
"If this message appears repeatedly, please open an issue.": "Om detta meddelande visas upprepade gånger, vänligen öppna en felanmälan.",
|
"If this message appears repeatedly, please open an issue.": "Om detta meddelande visas upprepade gånger, vänligen öppna en felanmälan.",
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "Sd",
|
"Sd": "Sd",
|
||||||
"Image System memory": "Bộ nhớ hệ thống hình ảnh",
|
"Image System memory": "Bộ nhớ hệ thống hình ảnh",
|
||||||
"Image microSD card": "Thẻ nhớ hệ thống hình ảnh",
|
"Image microSD card": "Thẻ nhớ hệ thống hình ảnh",
|
||||||
|
"Slow": "",
|
||||||
|
"Normal": "",
|
||||||
|
"Fast": "",
|
||||||
"Yes": "Có",
|
"Yes": "Có",
|
||||||
"No": "Không",
|
"No": "Không",
|
||||||
"Enabled": "Bật",
|
"Enabled": "Bật",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "A-Z (Yêu thích)",
|
"Alphabetical (Star)": "A-Z (Yêu thích)",
|
||||||
"Likes": "Thích",
|
"Likes": "Thích",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "Giảm dần",
|
"Descending": "Giảm dần",
|
||||||
"Descending (down)": "Giảm dần (xuống)",
|
"Descending (down)": "Giảm dần (xuống)",
|
||||||
"Desc": "Giảm",
|
"Desc": "Giảm",
|
||||||
"Ascending": "Tăng dần",
|
"Ascending": "Tăng dần",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "Chọn Theme",
|
"Select Theme": "Chọn Theme",
|
||||||
"Shuffle": "Trộn",
|
"Shuffle": "Trộn",
|
||||||
"Music": "Âm nhạc",
|
"Music": "Âm nhạc",
|
||||||
|
"12 Hour Time": "",
|
||||||
"Network": "Mạng",
|
"Network": "Mạng",
|
||||||
"Network Options": "Tuỳ chọn mạng",
|
"Network Options": "Tuỳ chọn mạng",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -76,7 +80,6 @@
|
|||||||
"Language": "Ngôn ngữ",
|
"Language": "Ngôn ngữ",
|
||||||
"Auto": "Tự động",
|
"Auto": "Tự động",
|
||||||
"English": "English",
|
"English": "English",
|
||||||
"Vietnamese": "Việt Nam",
|
|
||||||
"Japanese": "日本語",
|
"Japanese": "日本語",
|
||||||
"French": "Français",
|
"French": "Français",
|
||||||
"German": "Deutsch",
|
"German": "Deutsch",
|
||||||
@@ -88,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Việt Nam",
|
||||||
"Logging": "Logging",
|
"Logging": "Logging",
|
||||||
"Replace hbmenu on exit": "Thay thế hbmenu khi thoát",
|
"Replace hbmenu on exit": "Thay thế hbmenu khi thoát",
|
||||||
"Misc": "Tiện ích",
|
"Misc": "Tiện ích",
|
||||||
@@ -96,6 +100,7 @@
|
|||||||
"Install forwarders": "Cài ra màn hình",
|
"Install forwarders": "Cài ra màn hình",
|
||||||
"Install location": "Vị trí cài đặt",
|
"Install location": "Vị trí cài đặt",
|
||||||
"Show install warning": "Hiển thị cảnh báo cài đặt",
|
"Show install warning": "Hiển thị cảnh báo cài đặt",
|
||||||
|
"Text scroll speed": "",
|
||||||
|
|
||||||
"FileBrowser": "Duyệt tập tin",
|
"FileBrowser": "Duyệt tập tin",
|
||||||
"%zd files": "%zd tập tin",
|
"%zd files": "%zd tập tin",
|
||||||
@@ -234,6 +239,7 @@
|
|||||||
"Update avaliable: ": "Cập nhậc có sẵn: ",
|
"Update avaliable: ": "Cập nhậc có sẵn: ",
|
||||||
"Download update: ": "Tải cập nhật: ",
|
"Download update: ": "Tải cập nhật: ",
|
||||||
"Updated to ": "Đã cập nhật ",
|
"Updated to ": "Đã cập nhật ",
|
||||||
|
"Press OK to restart Sphaira": "",
|
||||||
"Restart Sphaira?": "Khởi động lại Sphaira?",
|
"Restart Sphaira?": "Khởi động lại Sphaira?",
|
||||||
"Failed to download update": "Cập nhật thất bại",
|
"Failed to download update": "Cập nhật thất bại",
|
||||||
"Restore hbmenu?": "Khôi phục hbmenu?",
|
"Restore hbmenu?": "Khôi phục hbmenu?",
|
||||||
@@ -246,5 +252,6 @@
|
|||||||
"Completely remove ": "Đã gỡ thành công ",
|
"Completely remove ": "Đã gỡ thành công ",
|
||||||
"Are you sure you want to delete ": "Bạn có muốn xoá ",
|
"Are you sure you want to delete ": "Bạn có muốn xoá ",
|
||||||
"Are you sure you wish to cancel?": "Bạn có chắn muốn huỷ không?",
|
"Are you sure you wish to cancel?": "Bạn có chắn muốn huỷ không?",
|
||||||
|
"Audio disabled due to suspended game": "",
|
||||||
"If this message appears repeatedly, please open an issue.": "Nếu thấy tin nhắn này, hãy báo lỗi."
|
"If this message appears repeatedly, please open an issue.": "Nếu thấy tin nhắn này, hãy báo lỗi."
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
"Sd": "SD卡",
|
"Sd": "SD卡",
|
||||||
"Image System memory": "主机内存图像",
|
"Image System memory": "主机内存图像",
|
||||||
"Image microSD card": "SD卡图像",
|
"Image microSD card": "SD卡图像",
|
||||||
|
"Slow": "慢",
|
||||||
|
"Normal": "正常",
|
||||||
|
"Fast": "快",
|
||||||
"Yes": "是",
|
"Yes": "是",
|
||||||
"No": "否",
|
"No": "否",
|
||||||
"Enabled": "启用",
|
"Enabled": "启用",
|
||||||
@@ -50,7 +53,7 @@
|
|||||||
"Alphabetical (Star)": "按字母顺序(星标优先)",
|
"Alphabetical (Star)": "按字母顺序(星标优先)",
|
||||||
"Likes": "点赞量",
|
"Likes": "点赞量",
|
||||||
"ID": "ID",
|
"ID": "ID",
|
||||||
"Decending": "降序",
|
"Descending": "降序",
|
||||||
"Descending (down)": "降序",
|
"Descending (down)": "降序",
|
||||||
"Desc": "降序",
|
"Desc": "降序",
|
||||||
"Ascending": "升序",
|
"Ascending": "升序",
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"Select Theme": "选择主题",
|
"Select Theme": "选择主题",
|
||||||
"Shuffle": "随机播放",
|
"Shuffle": "随机播放",
|
||||||
"Music": "音乐",
|
"Music": "音乐",
|
||||||
|
"12 Hour Time": "12小时制时间",
|
||||||
"Network": "网络",
|
"Network": "网络",
|
||||||
"Network Options": "网络选项",
|
"Network Options": "网络选项",
|
||||||
"Ftp": "FTP",
|
"Ftp": "FTP",
|
||||||
@@ -87,6 +91,7 @@
|
|||||||
"Portuguese": "Português",
|
"Portuguese": "Português",
|
||||||
"Russian": "Русский",
|
"Russian": "Русский",
|
||||||
"Swedish": "Svenska",
|
"Swedish": "Svenska",
|
||||||
|
"Vietnamese": "Vietnamese",
|
||||||
"Logging": "日志",
|
"Logging": "日志",
|
||||||
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
|
"Replace hbmenu on exit": "退出后用Sphaira替换hbmenu",
|
||||||
"Misc": "拓展",
|
"Misc": "拓展",
|
||||||
@@ -95,6 +100,7 @@
|
|||||||
"Install forwarders": "允许安装前端应用",
|
"Install forwarders": "允许安装前端应用",
|
||||||
"Install location": "安装位置",
|
"Install location": "安装位置",
|
||||||
"Show install warning": "显示安装警告",
|
"Show install warning": "显示安装警告",
|
||||||
|
"Text scroll speed": "文本滚动速度",
|
||||||
|
|
||||||
"FileBrowser": "文件浏览",
|
"FileBrowser": "文件浏览",
|
||||||
"%zd files": "%zd 个文件",
|
"%zd files": "%zd 个文件",
|
||||||
@@ -164,7 +170,7 @@
|
|||||||
"Controller": "控制器",
|
"Controller": "控制器",
|
||||||
"Pad ": "手柄 ",
|
"Pad ": "手柄 ",
|
||||||
" (Available)": " (可用的)",
|
" (Available)": " (可用的)",
|
||||||
" (Unsupported)": "",
|
" (Unsupported)": " (不支持的)",
|
||||||
" (Unconnected)": " (未连接)",
|
" (Unconnected)": " (未连接)",
|
||||||
"HandHeld": "掌机模式",
|
"HandHeld": "掌机模式",
|
||||||
"Rotation": "旋转",
|
"Rotation": "旋转",
|
||||||
@@ -233,6 +239,7 @@
|
|||||||
"Update avaliable: ": "有可用更新!",
|
"Update avaliable: ": "有可用更新!",
|
||||||
"Download update: ": "下载更新:",
|
"Download update: ": "下载更新:",
|
||||||
"Updated to ": "更新至 ",
|
"Updated to ": "更新至 ",
|
||||||
|
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
|
||||||
"Restart Sphaira?": "重启 Sphaira?",
|
"Restart Sphaira?": "重启 Sphaira?",
|
||||||
"Failed to download update": "更新下载失败",
|
"Failed to download update": "更新下载失败",
|
||||||
"Restore hbmenu?": "恢复 hbmenu?",
|
"Restore hbmenu?": "恢复 hbmenu?",
|
||||||
@@ -245,5 +252,6 @@
|
|||||||
"Completely remove ": "彻底删除 ",
|
"Completely remove ": "彻底删除 ",
|
||||||
"Are you sure you want to delete ": "您确定要删除吗 ",
|
"Are you sure you want to delete ": "您确定要删除吗 ",
|
||||||
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
||||||
"If this message appears repeatedly, please open an issue.": "如果此消息反复出现,请提交一个 issue。"
|
"Audio disabled due to suspended game": "由于游戏暂停,音频已禁用",
|
||||||
|
"If this message appears repeatedly, please open an issue.": "若此消息反复出现,请提交问题报告。"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
assets/romfs/theme/icons-sp/icon_SP_audio.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_file.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_folder.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_image.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_nro.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_video.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_zip.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
15
assets/romfs/themes/black_theme_sp_icons.ini
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[meta]
|
||||||
|
name=Black alt-icons-SP
|
||||||
|
author=spkatsi
|
||||||
|
version=1.0.0
|
||||||
|
inherit=romfs:/themes/base_black_theme.ini
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
icon_audio = romfs:/theme/icons-sp/icon_SP_audio.png
|
||||||
|
icon_video = romfs:/theme/icons-sp/icon_SP_video.png
|
||||||
|
icon_image = romfs:/theme/icons-sp/icon_SP_image.png
|
||||||
|
icon_file = romfs:/theme/icons-sp/icon_SP_file.png
|
||||||
|
icon_folder = romfs:/theme/icons-sp/icon_SP_folder.png
|
||||||
|
icon_zip = romfs:/theme/icons-sp/icon_SP_zip.png
|
||||||
|
icon_nro = romfs:/theme/icons-sp/icon_SP_nro.png
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.13)
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
|
||||||
set(sphaira_VERSION 0.6.0)
|
set(sphaira_VERSION 0.9.1)
|
||||||
|
|
||||||
project(sphaira
|
project(sphaira
|
||||||
VERSION ${sphaira_VERSION}
|
VERSION ${sphaira_VERSION}
|
||||||
@@ -46,6 +46,10 @@ add_executable(sphaira
|
|||||||
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/ftp_menu.cpp
|
||||||
|
source/ui/menus/gc_menu.cpp
|
||||||
|
source/ui/menus/game_menu.cpp
|
||||||
|
|
||||||
source/ui/error_box.cpp
|
source/ui/error_box.cpp
|
||||||
source/ui/notification.cpp
|
source/ui/notification.cpp
|
||||||
@@ -58,6 +62,7 @@ add_executable(sphaira
|
|||||||
source/ui/widget.cpp
|
source/ui/widget.cpp
|
||||||
source/ui/list.cpp
|
source/ui/list.cpp
|
||||||
source/ui/bubbles.cpp
|
source/ui/bubbles.cpp
|
||||||
|
source/ui/scrolling_text.cpp
|
||||||
|
|
||||||
source/app.cpp
|
source/app.cpp
|
||||||
source/download.cpp
|
source/download.cpp
|
||||||
@@ -71,9 +76,24 @@ 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
|
||||||
|
|
||||||
|
source/yati/yati.cpp
|
||||||
|
source/yati/container/nsp.cpp
|
||||||
|
source/yati/container/xci.cpp
|
||||||
|
source/yati/source/file.cpp
|
||||||
|
source/yati/source/stdio.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/keys.cpp
|
||||||
|
source/yati/nx/nca.cpp
|
||||||
|
source/yati/nx/ncm.cpp
|
||||||
|
source/yati/nx/ns.cpp
|
||||||
|
source/yati/nx/nxdumptool_rsa.c
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(sphaira PRIVATE
|
target_compile_definitions(sphaira PRIVATE
|
||||||
@@ -81,12 +101,55 @@ target_compile_definitions(sphaira PRIVATE
|
|||||||
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
|
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
target_compile_options(sphaira PRIVATE
|
||||||
|
-Wall
|
||||||
|
-Wextra
|
||||||
|
|
||||||
|
# unsure if it's a good idea to enable these by default as
|
||||||
|
# it may cause breakage upon compiler updates.
|
||||||
|
# -Werror
|
||||||
|
# -Wfatal-errors
|
||||||
|
|
||||||
|
# disabled as nx uses s64 for size and offset, however stl uses size_t instead, thus
|
||||||
|
# there being a lot of warnings.
|
||||||
|
-Wno-sign-compare
|
||||||
|
# disabled as many overriden methods don't use the params.
|
||||||
|
-Wno-unused-parameter
|
||||||
|
# pedantic warning, missing fields are set to 0.
|
||||||
|
-Wno-missing-field-initializers
|
||||||
|
# disabled as it warns for strcat 2 paths together, but it will never
|
||||||
|
# overflow due to fs enforcing a max path len anyway.
|
||||||
|
-Wno-format-truncation
|
||||||
|
|
||||||
|
# the below are taken from my gba emulator, they've served me well ;)
|
||||||
|
-Wformat-overflow=2
|
||||||
|
-Wundef
|
||||||
|
-Wmissing-include-dirs
|
||||||
|
-fstrict-aliasing
|
||||||
|
-Wstrict-overflow=2
|
||||||
|
-Walloca
|
||||||
|
-Wduplicated-cond
|
||||||
|
-Wwrite-strings
|
||||||
|
-Wdate-time
|
||||||
|
-Wlogical-op
|
||||||
|
-Wpacked
|
||||||
|
-Wcast-qual
|
||||||
|
-Wcast-align
|
||||||
|
-Wimplicit-fallthrough=5
|
||||||
|
-Wsuggest-final-types
|
||||||
|
-Wuninitialized
|
||||||
|
-fimplicit-constexpr
|
||||||
|
-Wmissing-requires
|
||||||
|
)
|
||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
set(FETCHCONTENT_QUIET FALSE)
|
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.1
|
# GIT_TAG 1.2.2
|
||||||
|
GIT_TAG f8a30fd
|
||||||
|
SOURCE_SUBDIR NONE
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_Declare(libhaze
|
FetchContent_Declare(libhaze
|
||||||
@@ -101,7 +164,7 @@ FetchContent_Declare(libpulsar
|
|||||||
|
|
||||||
FetchContent_Declare(nanovg
|
FetchContent_Declare(nanovg
|
||||||
GIT_REPOSITORY https://github.com/ITotalJustice/nanovg-deko3d.git
|
GIT_REPOSITORY https://github.com/ITotalJustice/nanovg-deko3d.git
|
||||||
GIT_TAG 1902b38
|
GIT_TAG 845c9fc
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_Declare(stb
|
FetchContent_Declare(stb
|
||||||
@@ -119,6 +182,24 @@ FetchContent_Declare(minIni
|
|||||||
GIT_TAG 11cac8b
|
GIT_TAG 11cac8b
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FetchContent_Declare(zstd
|
||||||
|
GIT_REPOSITORY https://github.com/facebook/zstd.git
|
||||||
|
GIT_TAG v1.5.7
|
||||||
|
SOURCE_SUBDIR build/cmake
|
||||||
|
)
|
||||||
|
|
||||||
|
set(USE_NEW_ZSTD ON)
|
||||||
|
|
||||||
|
set(ZSTD_BUILD_STATIC ON)
|
||||||
|
set(ZSTD_BUILD_SHARED OFF)
|
||||||
|
set(ZSTD_BUILD_COMPRESSION OFF)
|
||||||
|
set(ZSTD_BUILD_DECOMPRESSION ON)
|
||||||
|
set(ZSTD_BUILD_DICTBUILDER OFF)
|
||||||
|
set(ZSTD_LEGACY_SUPPORT OFF)
|
||||||
|
set(ZSTD_MULTITHREAD_SUPPORT OFF)
|
||||||
|
set(ZSTD_BUILD_PROGRAMS OFF)
|
||||||
|
set(ZSTD_BUILD_TESTS OFF)
|
||||||
|
|
||||||
set(MININI_LIB_NAME minIni)
|
set(MININI_LIB_NAME minIni)
|
||||||
set(MININI_USE_STDIO ON)
|
set(MININI_USE_STDIO ON)
|
||||||
set(MININI_USE_NX ON)
|
set(MININI_USE_NX ON)
|
||||||
@@ -136,8 +217,6 @@ set(NANOVG_NO_GIF ON)
|
|||||||
set(NANOVG_NO_HDR ON)
|
set(NANOVG_NO_HDR ON)
|
||||||
set(NANOVG_NO_PIC ON)
|
set(NANOVG_NO_PIC ON)
|
||||||
set(NANOVG_NO_PNM ON)
|
set(NANOVG_NO_PNM ON)
|
||||||
set(NANOVG_STBI_STATIC OFF)
|
|
||||||
set(NANOVG_STBTT_STATIC ON)
|
|
||||||
|
|
||||||
set(YYJSON_DISABLE_READER OFF)
|
set(YYJSON_DISABLE_READER OFF)
|
||||||
set(YYJSON_DISABLE_WRITER OFF)
|
set(YYJSON_DISABLE_WRITER OFF)
|
||||||
@@ -148,20 +227,16 @@ set(YYJSON_DISABLE_UTF8_VALIDATION ON)
|
|||||||
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
|
set(YYJSON_DISABLE_UNALIGNED_MEMORY_ACCESS OFF)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(
|
FetchContent_MakeAvailable(
|
||||||
# ftpsrv
|
ftpsrv
|
||||||
libhaze
|
libhaze
|
||||||
libpulsar
|
libpulsar
|
||||||
nanovg
|
nanovg
|
||||||
stb
|
stb
|
||||||
minIni
|
minIni
|
||||||
yyjson
|
yyjson
|
||||||
|
zstd
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_GetProperties(ftpsrv)
|
|
||||||
if (NOT ftpsrv_POPULATED)
|
|
||||||
FetchContent_Populate(ftpsrv)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(FTPSRV_LIB_BUILD TRUE)
|
set(FTPSRV_LIB_BUILD TRUE)
|
||||||
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
|
set(FTPSRV_LIB_SOCK_UNISTD TRUE)
|
||||||
set(FTPSRV_LIB_VFS_CUSTOM ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.h)
|
set(FTPSRV_LIB_VFS_CUSTOM ${ftpsrv_SOURCE_DIR}/src/platform/nx/vfs_nx.h)
|
||||||
@@ -178,6 +253,7 @@ set(FTPSRV_LIB_CUSTOM_DEFINES
|
|||||||
USE_VFS_SAVE=$<BOOL:TRUE>
|
USE_VFS_SAVE=$<BOOL:TRUE>
|
||||||
USE_VFS_STORAGE=$<BOOL:TRUE>
|
USE_VFS_STORAGE=$<BOOL:TRUE>
|
||||||
USE_VFS_GC=$<BOOL:${USE_VFS_GC}>
|
USE_VFS_GC=$<BOOL:${USE_VFS_GC}>
|
||||||
|
USE_VFS_USBHSFS=$<BOOL:FALSE>
|
||||||
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
|
VFS_NX_BUFFER_IO=$<BOOL:TRUE>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -238,6 +314,11 @@ find_package(CURL REQUIRED)
|
|||||||
find_path(mbedtls_inc mbedtls REQUIRED)
|
find_path(mbedtls_inc mbedtls REQUIRED)
|
||||||
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
|
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
|
||||||
|
|
||||||
|
if (NOT USE_NEW_ZSTD)
|
||||||
|
find_path(zstd_inc zstd.h REQUIRED)
|
||||||
|
find_library(zstd_lib zstd REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
set_target_properties(sphaira PROPERTIES
|
set_target_properties(sphaira PROPERTIES
|
||||||
C_STANDARD 11
|
C_STANDARD 11
|
||||||
C_EXTENSIONS ON
|
C_EXTENSIONS ON
|
||||||
@@ -260,6 +341,15 @@ target_link_libraries(sphaira PRIVATE
|
|||||||
${mbedcrypto_lib}
|
${mbedcrypto_lib}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (USE_NEW_ZSTD)
|
||||||
|
message(STATUS "USING UPSTREAM ZSTD")
|
||||||
|
target_link_libraries(sphaira PRIVATE libzstd_static)
|
||||||
|
else()
|
||||||
|
message(STATUS "USING LOCAL ZSTD")
|
||||||
|
target_link_libraries(sphaira PRIVATE ${zstd_lib})
|
||||||
|
target_include_directories(sphaira PRIVATE ${zstd_inc})
|
||||||
|
endif()
|
||||||
|
|
||||||
target_include_directories(sphaira PRIVATE
|
target_include_directories(sphaira PRIVATE
|
||||||
include
|
include
|
||||||
${minizip_inc}
|
${minizip_inc}
|
||||||
@@ -294,7 +384,7 @@ nx_generate_nacp(
|
|||||||
OUTPUT sphaira.nacp
|
OUTPUT sphaira.nacp
|
||||||
NAME ${CMAKE_PROJECT_NAME}
|
NAME ${CMAKE_PROJECT_NAME}
|
||||||
AUTHOR TotalJustice
|
AUTHOR TotalJustice
|
||||||
VERSION ${CMAKE_PROJECT_VERSION}
|
VERSION ${sphaira_VERSION}
|
||||||
)
|
)
|
||||||
|
|
||||||
# create nro
|
# create nro
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ public:
|
|||||||
~App();
|
~App();
|
||||||
void Loop();
|
void Loop();
|
||||||
|
|
||||||
|
static App* GetApp();
|
||||||
|
|
||||||
static void Exit();
|
static void Exit();
|
||||||
static void ExitRestart();
|
static void ExitRestart();
|
||||||
static auto GetVg() -> NVGcontext*;
|
static auto GetVg() -> NVGcontext*;
|
||||||
@@ -62,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;
|
||||||
@@ -77,8 +79,8 @@ public:
|
|||||||
static auto GetInstallEnable() -> bool;
|
static auto GetInstallEnable() -> bool;
|
||||||
static auto GetInstallSdEnable() -> bool;
|
static auto GetInstallSdEnable() -> bool;
|
||||||
static auto GetInstallPrompt() -> bool;
|
static auto GetInstallPrompt() -> bool;
|
||||||
static auto GetThemeShuffleEnable() -> bool;
|
|
||||||
static auto GetThemeMusicEnable() -> bool;
|
static auto GetThemeMusicEnable() -> bool;
|
||||||
|
static auto Get12HourTimeEnable() -> bool;
|
||||||
static auto GetLanguage() -> long;
|
static auto GetLanguage() -> long;
|
||||||
static auto GetTextScrollSpeed() -> long;
|
static auto GetTextScrollSpeed() -> long;
|
||||||
|
|
||||||
@@ -90,8 +92,8 @@ public:
|
|||||||
static void SetInstallEnable(bool enable);
|
static void SetInstallEnable(bool enable);
|
||||||
static void SetInstallSdEnable(bool enable);
|
static void SetInstallSdEnable(bool enable);
|
||||||
static void SetInstallPrompt(bool enable);
|
static void SetInstallPrompt(bool enable);
|
||||||
static void SetThemeShuffleEnable(bool enable);
|
|
||||||
static void SetThemeMusicEnable(bool enable);
|
static void SetThemeMusicEnable(bool enable);
|
||||||
|
static void Set12HourTimeEnable(bool enable);
|
||||||
static void SetLanguage(long index);
|
static void SetLanguage(long index);
|
||||||
static void SetTextScrollSpeed(long index);
|
static void SetTextScrollSpeed(long index);
|
||||||
|
|
||||||
@@ -100,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();
|
||||||
@@ -119,6 +128,21 @@ public:
|
|||||||
return type == AppletType_Application || type == AppletType_SystemApplication;
|
return type == AppletType_Application || type == AppletType_SystemApplication;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static auto IsApplet() -> bool {
|
||||||
|
return !IsApplication();
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if launched in applet mode with a title suspended in the background.
|
||||||
|
static auto IsAppletWithSuspendedApp() -> bool {
|
||||||
|
R_UNLESS(IsApplet(), false);
|
||||||
|
R_TRY_RESULT(pmdmntInitialize(), false);
|
||||||
|
ON_SCOPE_EXIT(pmdmntExit());
|
||||||
|
|
||||||
|
u64 pid;
|
||||||
|
return R_SUCCEEDED(pmdmntGetApplicationProcessId(&pid));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// private:
|
// private:
|
||||||
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
|
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
|
||||||
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
|
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
|
||||||
@@ -157,16 +181,35 @@ public:
|
|||||||
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
|
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
|
||||||
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
|
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
|
||||||
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
|
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
|
||||||
|
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||||
|
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
|
||||||
|
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
||||||
|
option::OptionString m_right_side_menu{INI_SECTION, "right_side_menu", "Appstore"};
|
||||||
|
|
||||||
|
// install options
|
||||||
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::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
|
option::OptionLong m_boost_mode{INI_SECTION, "boost_mode", false};
|
||||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
|
||||||
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
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_skip_base{INI_SECTION, "skip_base", 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_data_patch{INI_SECTION, "skip_data_patch", false};
|
||||||
|
option::OptionBool m_skip_ticket{INI_SECTION, "skip_ticket", false};
|
||||||
|
option::OptionBool m_skip_nca_hash_verify{INI_SECTION, "skip_nca_hash_verify", false};
|
||||||
|
option::OptionBool m_skip_rsa_header_fixed_key_verify{INI_SECTION, "skip_rsa_header_fixed_key_verify", false};
|
||||||
|
option::OptionBool m_skip_rsa_npdm_fixed_key_verify{INI_SECTION, "skip_rsa_npdm_fixed_key_verify", false};
|
||||||
|
option::OptionBool m_ignore_distribution_bit{INI_SECTION, "ignore_distribution_bit", false};
|
||||||
|
option::OptionBool m_convert_to_standard_crypto{INI_SECTION, "convert_to_standard_crypto", false};
|
||||||
|
option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false};
|
||||||
|
option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false};
|
||||||
|
|
||||||
// todo: move this into it's own menu
|
// todo: move this into it's own menu
|
||||||
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||||
|
|
||||||
PLSR_BFSAR m_qlaunch_bfsar{};
|
|
||||||
PLSR_PlayerSoundId m_sound_ids[SoundEffect_MAX]{};
|
PLSR_PlayerSoundId m_sound_ids[SoundEffect_MAX]{};
|
||||||
|
|
||||||
private: // from nanovg decko3d example by adubbz
|
private: // from nanovg decko3d example by adubbz
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <stop_token>
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
namespace sphaira::curl {
|
namespace sphaira::curl {
|
||||||
@@ -29,6 +30,7 @@ struct ApiResult;
|
|||||||
using Path = fs::FsPath;
|
using Path = fs::FsPath;
|
||||||
using OnComplete = std::function<void(ApiResult& result)>;
|
using OnComplete = std::function<void(ApiResult& result)>;
|
||||||
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
|
using OnProgress = std::function<bool(u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow)>;
|
||||||
|
using StopToken = std::stop_token;
|
||||||
|
|
||||||
struct Url {
|
struct Url {
|
||||||
Url() = default;
|
Url() = default;
|
||||||
@@ -71,6 +73,7 @@ struct ApiResult {
|
|||||||
struct DownloadEventData {
|
struct DownloadEventData {
|
||||||
OnComplete callback;
|
OnComplete callback;
|
||||||
ApiResult result;
|
ApiResult result;
|
||||||
|
StopToken stoken;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto Init() -> bool;
|
auto Init() -> bool;
|
||||||
@@ -84,6 +87,9 @@ auto ToFile(const Api& e) -> ApiResult;
|
|||||||
auto ToMemoryAsync(const Api& e) -> bool;
|
auto ToMemoryAsync(const Api& e) -> bool;
|
||||||
auto ToFileAsync(const Api& e) -> bool;
|
auto ToFileAsync(const Api& e) -> bool;
|
||||||
|
|
||||||
|
// uses curl to convert string to their %XX
|
||||||
|
auto EscapeString(const std::string& str) -> std::string;
|
||||||
|
|
||||||
struct Api {
|
struct Api {
|
||||||
Api() = default;
|
Api() = default;
|
||||||
|
|
||||||
@@ -114,6 +120,7 @@ struct Api {
|
|||||||
auto ToMemory(Ts&&... ts) {
|
auto ToMemory(Ts&&... ts) {
|
||||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||||
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
|
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
|
||||||
|
static_assert(!std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must not be specified");
|
||||||
Api::set_option(std::forward<Ts>(ts)...);
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
return curl::ToMemory(*this);
|
return curl::ToMemory(*this);
|
||||||
}
|
}
|
||||||
@@ -122,6 +129,7 @@ struct Api {
|
|||||||
auto ToFile(Ts&&... ts) {
|
auto ToFile(Ts&&... ts) {
|
||||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||||
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
|
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
|
||||||
|
static_assert(!std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must not be specified");
|
||||||
Api::set_option(std::forward<Ts>(ts)...);
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
return curl::ToFile(*this);
|
return curl::ToFile(*this);
|
||||||
}
|
}
|
||||||
@@ -131,6 +139,7 @@ struct Api {
|
|||||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||||
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
|
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
|
||||||
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
|
static_assert(!std::disjunction_v<std::is_same<Path, Ts>...>, "Path must not valid for memory");
|
||||||
|
static_assert(std::disjunction_v<std::is_same<StopToken, Ts>...>, "StopToken must be specified");
|
||||||
Api::set_option(std::forward<Ts>(ts)...);
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
return curl::ToMemoryAsync(*this);
|
return curl::ToMemoryAsync(*this);
|
||||||
}
|
}
|
||||||
@@ -140,18 +149,38 @@ struct Api {
|
|||||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||||
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
|
static_assert(std::disjunction_v<std::is_same<Path, Ts>...>, "Path must be specified");
|
||||||
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
|
static_assert(std::disjunction_v<std::is_same<OnComplete, Ts>...>, "OnComplete must be specified");
|
||||||
|
static_assert(std::disjunction_v<std::is_same<StopToken, Ts>...>, "StopToken must be specified");
|
||||||
Api::set_option(std::forward<Ts>(ts)...);
|
Api::set_option(std::forward<Ts>(ts)...);
|
||||||
return curl::ToFileAsync(*this);
|
return curl::ToFileAsync(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
Url m_url;
|
auto& GetUrl() const {
|
||||||
Fields m_fields{};
|
return m_url.m_str;
|
||||||
Header m_header{};
|
}
|
||||||
Flags m_flags{};
|
auto& GetFields() const {
|
||||||
Path m_path{};
|
return m_fields.m_str;
|
||||||
OnComplete m_on_complete = nullptr;
|
}
|
||||||
OnProgress m_on_progress = nullptr;
|
auto& GetHeader() const {
|
||||||
Priority m_prio = Priority::High;
|
return m_header;
|
||||||
|
}
|
||||||
|
auto& GetFlags() const {
|
||||||
|
return m_flags.m_flags;
|
||||||
|
}
|
||||||
|
auto& GetPath() const {
|
||||||
|
return m_path;
|
||||||
|
}
|
||||||
|
auto& GetOnComplete() const {
|
||||||
|
return m_on_complete;
|
||||||
|
}
|
||||||
|
auto& GetOnProgress() const {
|
||||||
|
return m_on_progress;
|
||||||
|
}
|
||||||
|
auto& GetPriority() const {
|
||||||
|
return m_prio;
|
||||||
|
}
|
||||||
|
auto& GetToken() const {
|
||||||
|
return m_stoken;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SetOption(Url&& v) {
|
void SetOption(Url&& v) {
|
||||||
@@ -178,6 +207,9 @@ private:
|
|||||||
void SetOption(Priority&& v) {
|
void SetOption(Priority&& v) {
|
||||||
m_prio = v;
|
m_prio = v;
|
||||||
}
|
}
|
||||||
|
void SetOption(StopToken&& v) {
|
||||||
|
m_stoken = v;
|
||||||
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void set_option(T&& t) {
|
void set_option(T&& t) {
|
||||||
@@ -189,6 +221,18 @@ private:
|
|||||||
set_option(std::forward<T>(t));
|
set_option(std::forward<T>(t));
|
||||||
set_option(std::forward<Ts>(ts)...);
|
set_option(std::forward<Ts>(ts)...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Url m_url;
|
||||||
|
Fields m_fields{};
|
||||||
|
Header m_header{};
|
||||||
|
Flags m_flags{};
|
||||||
|
Path m_path{};
|
||||||
|
OnComplete m_on_complete{nullptr};
|
||||||
|
OnProgress m_on_progress{nullptr};
|
||||||
|
Priority m_prio{Priority::High};
|
||||||
|
std::stop_source m_stop_source{};
|
||||||
|
StopToken m_stoken{m_stop_source.get_token()};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::curl
|
} // namespace sphaira::curl
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace sphaira::i18n {
|
namespace sphaira::i18n {
|
||||||
|
|
||||||
bool init(long index);
|
bool init(long index);
|
||||||
void exit();
|
void exit();
|
||||||
|
|
||||||
std::string get(const char* str);
|
std::string get(std::string_view str);
|
||||||
|
|
||||||
} // namespace sphaira::i18n
|
} // namespace sphaira::i18n
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
#define sphaira_USE_LOG 1
|
#define sphaira_USE_LOG 1
|
||||||
|
|
||||||
#include <cstdarg>
|
#include <stdarg.h>
|
||||||
|
|
||||||
#if sphaira_USE_LOG
|
#if sphaira_USE_LOG
|
||||||
auto log_file_init() -> bool;
|
bool log_file_init();
|
||||||
auto log_nxlink_init() -> bool;
|
bool log_nxlink_init();
|
||||||
void log_file_exit();
|
void log_file_exit();
|
||||||
void log_nxlink_exit();
|
void log_nxlink_exit();
|
||||||
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
||||||
void log_write_arg(const char* s, std::va_list& v);
|
void log_write_arg(const char* s, va_list* v);
|
||||||
#else
|
#else
|
||||||
inline auto log_file_init() -> bool {
|
inline bool log_file_init() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
inline auto log_nxlink_init() -> bool {
|
inline bool log_nxlink_init() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#define log_file_exit()
|
#define log_file_exit()
|
||||||
#define log_nxlink_exit()
|
#define log_nxlink_exit()
|
||||||
#define log_write(...)
|
#define log_write(...)
|
||||||
|
#define log_write_arg(...)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ private:
|
|||||||
const s64 m_row;
|
const s64 m_row;
|
||||||
const s64 m_page;
|
const s64 m_page;
|
||||||
|
|
||||||
Vec4 m_v;
|
Vec4 m_v{};
|
||||||
Vec2 m_pad;
|
Vec2 m_pad{};
|
||||||
|
|
||||||
Vec4 m_scrollbar{};
|
Vec4 m_scrollbar{};
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
#include "ui/menus/menu_base.hpp"
|
#include "ui/menus/menu_base.hpp"
|
||||||
#include "ui/scrollable_text.hpp"
|
#include "ui/scrollable_text.hpp"
|
||||||
|
#include "ui/scrolling_text.hpp"
|
||||||
#include "ui/list.hpp"
|
#include "ui/list.hpp"
|
||||||
#include "nro.hpp"
|
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
@@ -42,26 +42,26 @@ enum class EntryStatus {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
std::string category; // todo: lable
|
std::string category{}; // todo: lable
|
||||||
std::string binary; // optional, only valid for .nro
|
std::string binary{}; // optional, only valid for .nro
|
||||||
std::string updated; // date of update
|
std::string updated{}; // date of update
|
||||||
std::string name;
|
std::string name{};
|
||||||
std::string license; // optional
|
std::string license{}; // optional
|
||||||
std::string title; // same as name but with spaces
|
std::string title{}; // same as name but with spaces
|
||||||
std::string url; // url of repo (optional?)
|
std::string url{}; // url of repo (optional?)
|
||||||
std::string description;
|
std::string description{};
|
||||||
std::string author;
|
std::string author{};
|
||||||
std::string changelog; // optional
|
std::string changelog{}; // optional
|
||||||
u64 screens; // number of screenshots
|
u64 screens{}; // number of screenshots
|
||||||
u64 extracted; // extracted size in KiB
|
u64 extracted{}; // extracted size in KiB
|
||||||
std::string version;
|
std::string version{};
|
||||||
u64 filesize; // compressed size in KiB
|
u64 filesize{}; // compressed size in KiB
|
||||||
std::string details;
|
std::string details{};
|
||||||
u64 app_dls;
|
u64 app_dls{};
|
||||||
std::string md5; // md5 of the zip
|
std::string md5{}; // md5 of the zip
|
||||||
|
|
||||||
LazyImage image;
|
LazyImage image{};
|
||||||
u32 updated_num;
|
u32 updated_num{};
|
||||||
EntryStatus status{EntryStatus::Get};
|
EntryStatus status{EntryStatus::Get};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +73,7 @@ struct EntryMenu final : MenuBase {
|
|||||||
EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu);
|
EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu);
|
||||||
~EntryMenu();
|
~EntryMenu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "Entry"; };
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
// void OnFocusGained() override;
|
// void OnFocusGained() override;
|
||||||
@@ -99,13 +100,13 @@ private:
|
|||||||
Menu& m_menu;
|
Menu& m_menu;
|
||||||
|
|
||||||
s64 m_index{}; // where i am in the array
|
s64 m_index{}; // where i am in the array
|
||||||
std::vector<Option> m_options;
|
std::vector<Option> m_options{};
|
||||||
LazyImage m_banner;
|
LazyImage m_banner{};
|
||||||
std::unique_ptr<List> m_list;
|
std::unique_ptr<List> m_list{};
|
||||||
|
|
||||||
std::shared_ptr<ScrollableText> m_details;
|
std::shared_ptr<ScrollableText> m_details{};
|
||||||
std::shared_ptr<ScrollableText> m_changelog;
|
std::shared_ptr<ScrollableText> m_changelog{};
|
||||||
std::shared_ptr<ScrollableText> m_detail_changelog;
|
std::shared_ptr<ScrollableText> m_detail_changelog{};
|
||||||
|
|
||||||
bool m_show_changlog{};
|
bool m_show_changlog{};
|
||||||
};
|
};
|
||||||
@@ -130,42 +131,15 @@ enum SortType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum OrderType {
|
enum OrderType {
|
||||||
OrderType_Decending,
|
OrderType_Descending,
|
||||||
OrderType_Ascending,
|
OrderType_Ascending,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FeedbackEntry {
|
|
||||||
u32 id;
|
|
||||||
u64 time;
|
|
||||||
std::string package; // name of package
|
|
||||||
std::string content; // the feedback message that was sent
|
|
||||||
std::string reply; // the reply, "" if no reply yet :)
|
|
||||||
};
|
|
||||||
|
|
||||||
struct FeedbackMenu final : MenuBase {
|
|
||||||
FeedbackMenu(const std::vector<Entry>& package_entries, LazyImage& default_image);
|
|
||||||
~FeedbackMenu();
|
|
||||||
|
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
|
||||||
void OnFocusGained() override;
|
|
||||||
|
|
||||||
void SetIndex(s64 index);
|
|
||||||
void ScanHomebrew();
|
|
||||||
void Sort();
|
|
||||||
|
|
||||||
private:
|
|
||||||
const std::vector<Entry>& m_package_entries;
|
|
||||||
LazyImage& m_default_image;
|
|
||||||
std::vector<FeedbackEntry> m_entries;
|
|
||||||
s64 m_index{}; // where i am in the array
|
|
||||||
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Menu final : MenuBase {
|
struct Menu final : MenuBase {
|
||||||
Menu(const std::vector<NroEntry>& nro_entries);
|
Menu();
|
||||||
~Menu();
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "Store"; };
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
@@ -190,28 +164,31 @@ struct Menu final : MenuBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::vector<NroEntry>& m_nro_entries;
|
std::vector<Entry> m_entries{};
|
||||||
std::vector<Entry> m_entries;
|
std::vector<EntryMini> m_entries_index[Filter_MAX]{};
|
||||||
std::vector<EntryMini> m_entries_index[Filter_MAX];
|
std::vector<EntryMini> m_entries_index_author{};
|
||||||
std::vector<EntryMini> m_entries_index_author;
|
std::vector<EntryMini> m_entries_index_search{};
|
||||||
std::vector<EntryMini> m_entries_index_search;
|
std::span<EntryMini> m_entries_current{};
|
||||||
std::span<EntryMini> m_entries_current;
|
|
||||||
|
ScrollingText m_scroll_name{};
|
||||||
|
ScrollingText m_scroll_author{};
|
||||||
|
ScrollingText m_scroll_version{};
|
||||||
|
|
||||||
Filter m_filter{Filter::Filter_All};
|
Filter m_filter{Filter::Filter_All};
|
||||||
SortType m_sort{SortType::SortType_Updated};
|
SortType m_sort{SortType::SortType_Updated};
|
||||||
OrderType m_order{OrderType::OrderType_Decending};
|
OrderType m_order{OrderType::OrderType_Descending};
|
||||||
|
|
||||||
s64 m_index{}; // where i am in the array
|
s64 m_index{}; // where i am in the array
|
||||||
LazyImage m_default_image;
|
LazyImage m_default_image{};
|
||||||
LazyImage m_update;
|
LazyImage m_update{};
|
||||||
LazyImage m_get;
|
LazyImage m_get{};
|
||||||
LazyImage m_local;
|
LazyImage m_local{};
|
||||||
LazyImage m_installed;
|
LazyImage m_installed{};
|
||||||
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
ImageDownloadState m_repo_download_state{ImageDownloadState::None};
|
||||||
std::unique_ptr<List> m_list;
|
std::unique_ptr<List> m_list{};
|
||||||
|
|
||||||
std::string m_search_term;
|
std::string m_search_term{};
|
||||||
std::string m_author_term;
|
std::string m_author_term{};
|
||||||
s64 m_entry_search_jump_back{};
|
s64 m_entry_search_jump_back{};
|
||||||
s64 m_entry_author_jump_back{};
|
s64 m_entry_author_jump_back{};
|
||||||
bool m_is_search{};
|
bool m_is_search{};
|
||||||
|
|||||||
@@ -10,18 +10,19 @@ struct Menu final : MenuBase {
|
|||||||
Menu(const fs::FsPath& path);
|
Menu(const fs::FsPath& path);
|
||||||
~Menu();
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "File"; };
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const fs::FsPath m_path;
|
const fs::FsPath m_path;
|
||||||
fs::FsNativeSd m_fs;
|
fs::FsNativeSd m_fs{};
|
||||||
FsFile m_file;
|
FsFile m_file{};
|
||||||
s64 m_file_size{};
|
s64 m_file_size{};
|
||||||
s64 m_file_offset{};
|
s64 m_file_offset{};
|
||||||
|
|
||||||
std::unique_ptr<ScrollableText> m_scroll_text;
|
std::unique_ptr<ScrollableText> m_scroll_text{};
|
||||||
|
|
||||||
s64 m_start{};
|
s64 m_start{};
|
||||||
s64 m_index{}; // where i am in the array
|
s64 m_index{}; // where i am in the array
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ enum SortType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum OrderType {
|
enum OrderType {
|
||||||
OrderType_Decending,
|
OrderType_Descending,
|
||||||
OrderType_Ascending,
|
OrderType_Ascending,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,23 +86,36 @@ struct FileEntry : FsDirectoryEntry {
|
|||||||
|
|
||||||
struct FileAssocEntry {
|
struct FileAssocEntry {
|
||||||
fs::FsPath path{}; // ini name
|
fs::FsPath path{}; // ini name
|
||||||
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 {
|
||||||
fs::FsPath name;
|
fs::FsPath name{};
|
||||||
s64 index;
|
s64 index{};
|
||||||
float offset;
|
float offset{};
|
||||||
s64 entries_count;
|
s64 entries_count{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FsDirCollection {
|
struct FsDirCollection {
|
||||||
fs::FsPath path;
|
fs::FsPath path{};
|
||||||
fs::FsPath parent_name;
|
fs::FsPath parent_name{};
|
||||||
std::vector<FsDirectoryEntry> files;
|
std::vector<FsDirectoryEntry> files{};
|
||||||
std::vector<FsDirectoryEntry> dirs;
|
std::vector<FsDirectoryEntry> dirs{};
|
||||||
};
|
};
|
||||||
|
|
||||||
using FsDirCollections = std::vector<FsDirCollection>;
|
using FsDirCollections = std::vector<FsDirCollection>;
|
||||||
@@ -111,6 +124,7 @@ struct Menu final : MenuBase {
|
|||||||
Menu(const std::vector<NroEntry>& nro_entries);
|
Menu(const std::vector<NroEntry>& nro_entries);
|
||||||
~Menu();
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "Files"; };
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
@@ -122,6 +136,15 @@ struct Menu final : MenuBase {
|
|||||||
private:
|
private:
|
||||||
void SetIndex(s64 index);
|
void SetIndex(s64 index);
|
||||||
void InstallForwarder();
|
void InstallForwarder();
|
||||||
|
void InstallFile(const FileEntry& target);
|
||||||
|
void InstallFiles(const std::vector<FileEntry>& targets);
|
||||||
|
|
||||||
|
void UnzipFile(const fs::FsPath& folder, const FileEntry& target);
|
||||||
|
void UnzipFiles(fs::FsPath folder, const std::vector<FileEntry>& targets);
|
||||||
|
|
||||||
|
void ZipFile(const fs::FsPath& zip_path, const FileEntry& target);
|
||||||
|
void ZipFiles(fs::FsPath zip_path, const std::vector<FileEntry>& targets);
|
||||||
|
|
||||||
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
||||||
|
|
||||||
void LoadAssocEntriesPath(const fs::FsPath& path);
|
void LoadAssocEntriesPath(const fs::FsPath& path);
|
||||||
@@ -231,38 +254,38 @@ private:
|
|||||||
static constexpr inline const char* INI_SECTION = "filebrowser";
|
static constexpr inline const char* INI_SECTION = "filebrowser";
|
||||||
|
|
||||||
const std::vector<NroEntry>& m_nro_entries;
|
const std::vector<NroEntry>& m_nro_entries;
|
||||||
std::unique_ptr<fs::FsNative> m_fs;
|
std::unique_ptr<fs::FsNative> m_fs{};
|
||||||
FsType m_fs_type;
|
FsType m_fs_type{};
|
||||||
fs::FsPath m_path;
|
fs::FsPath m_path{};
|
||||||
std::vector<FileEntry> m_entries;
|
std::vector<FileEntry> m_entries{};
|
||||||
std::vector<u32> m_entries_index; // files not including hidden
|
std::vector<u32> m_entries_index{}; // files not including hidden
|
||||||
std::vector<u32> m_entries_index_hidden; // includes hidden files
|
std::vector<u32> m_entries_index_hidden{}; // includes hidden files
|
||||||
std::vector<u32> m_entries_index_search; // files found via search
|
std::vector<u32> m_entries_index_search{}; // files found via search
|
||||||
std::span<u32> m_entries_current;
|
std::span<u32> m_entries_current{};
|
||||||
|
|
||||||
std::unique_ptr<List> m_list;
|
std::unique_ptr<List> m_list{};
|
||||||
std::optional<fs::FsPath> m_daybreak_path;
|
std::optional<fs::FsPath> m_daybreak_path{};
|
||||||
|
|
||||||
// search options
|
// search options
|
||||||
// show files [X]
|
// show files [X]
|
||||||
// show folders [X]
|
// show folders [X]
|
||||||
// recursive (slow) [ ]
|
// recursive (slow) [ ]
|
||||||
|
|
||||||
std::vector<FileAssocEntry> m_assoc_entries;
|
std::vector<FileAssocEntry> m_assoc_entries{};
|
||||||
std::vector<FileEntry> m_selected_files;
|
std::vector<FileEntry> m_selected_files{};
|
||||||
|
|
||||||
// this keeps track of the highlighted file before opening a folder
|
// this keeps track of the highlighted file before opening a folder
|
||||||
// if the user presses B to go back to the previous dir
|
// if the user presses B to go back to the previous dir
|
||||||
// this vector is popped, then, that entry is checked if it still exists
|
// this vector is popped, then, that entry is checked if it still exists
|
||||||
// if it does, the index becomes that file.
|
// if it does, the index becomes that file.
|
||||||
std::vector<LastFile> m_previous_highlighted_file;
|
std::vector<LastFile> m_previous_highlighted_file{};
|
||||||
fs::FsPath m_selected_path;
|
fs::FsPath m_selected_path{};
|
||||||
s64 m_index{};
|
s64 m_index{};
|
||||||
s64 m_selected_count{};
|
s64 m_selected_count{};
|
||||||
SelectedType m_selected_type{SelectedType::None};
|
SelectedType m_selected_type{SelectedType::None};
|
||||||
|
|
||||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
|
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical};
|
||||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};
|
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||||
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};
|
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false};
|
||||||
option::OptionBool m_folders_first{INI_SECTION, "folders_first", true};
|
option::OptionBool m_folders_first{INI_SECTION, "folders_first", true};
|
||||||
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false};
|
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false};
|
||||||
|
|||||||
60
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#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();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "FTP"; };
|
||||||
|
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
|
||||||
88
sphaira/include/ui/menus/game_menu.hpp
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "ui/scrolling_text.hpp"
|
||||||
|
#include "ui/list.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "option.hpp"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::game {
|
||||||
|
|
||||||
|
enum class NacpLoadStatus {
|
||||||
|
// not yet attempted to be loaded.
|
||||||
|
None,
|
||||||
|
// loaded, ready to parse.
|
||||||
|
Loaded,
|
||||||
|
// failed to load, do not attempt to load again!
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
u64 app_id{};
|
||||||
|
s64 size{};
|
||||||
|
char display_version[0x10]{};
|
||||||
|
NacpLanguageEntry lang{};
|
||||||
|
int image{};
|
||||||
|
|
||||||
|
std::unique_ptr<NsApplicationControlData> control{};
|
||||||
|
u64 control_size{};
|
||||||
|
NacpLoadStatus status{NacpLoadStatus::None};
|
||||||
|
|
||||||
|
auto GetName() const -> const char* {
|
||||||
|
return lang.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetAuthor() const -> const char* {
|
||||||
|
return lang.author;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetDisplayVersion() const -> const char* {
|
||||||
|
return display_version;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
enum SortType {
|
||||||
|
SortType_Updated,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum OrderType {
|
||||||
|
OrderType_Descending,
|
||||||
|
OrderType_Ascending,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Menu final : MenuBase {
|
||||||
|
Menu();
|
||||||
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "Games"; };
|
||||||
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
void OnFocusGained() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetIndex(s64 index);
|
||||||
|
void ScanHomebrew();
|
||||||
|
void Sort();
|
||||||
|
void SortAndFindLastFile();
|
||||||
|
void FreeEntries();
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr inline const char* INI_SECTION = "games";
|
||||||
|
|
||||||
|
std::vector<Entry> m_entries{};
|
||||||
|
s64 m_index{}; // where i am in the array
|
||||||
|
std::unique_ptr<List> m_list{};
|
||||||
|
bool m_is_reversed{};
|
||||||
|
bool m_dirty{};
|
||||||
|
|
||||||
|
ScrollingText m_scroll_name{};
|
||||||
|
ScrollingText m_scroll_author{};
|
||||||
|
ScrollingText m_scroll_version{};
|
||||||
|
|
||||||
|
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
|
||||||
|
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||||
|
option::OptionBool m_hide_forwarders{INI_SECTION, "hide_forwarders", false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::game
|
||||||
82
sphaira/include/ui/menus/gc_menu.hpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#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();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "GC"; };
|
||||||
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
void OnFocusGained() 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
|
||||||
@@ -10,41 +10,42 @@
|
|||||||
namespace sphaira::ui::menu::gh {
|
namespace sphaira::ui::menu::gh {
|
||||||
|
|
||||||
struct AssetEntry {
|
struct AssetEntry {
|
||||||
std::string name;
|
std::string name{};
|
||||||
std::string path;
|
std::string path{};
|
||||||
std::string pre_install_message;
|
std::string pre_install_message{};
|
||||||
std::string post_install_message;
|
std::string post_install_message{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
fs::FsPath json_path;
|
fs::FsPath json_path{};
|
||||||
std::string url;
|
std::string url{};
|
||||||
std::string owner;
|
std::string owner{};
|
||||||
std::string repo;
|
std::string repo{};
|
||||||
std::string tag;
|
std::string tag{};
|
||||||
std::string pre_install_message;
|
std::string pre_install_message{};
|
||||||
std::string post_install_message;
|
std::string post_install_message{};
|
||||||
std::vector<AssetEntry> assets;
|
std::vector<AssetEntry> assets{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GhApiAsset {
|
struct GhApiAsset {
|
||||||
std::string name;
|
std::string name{};
|
||||||
std::string content_type;
|
std::string content_type{};
|
||||||
u64 size;
|
u64 size{};
|
||||||
u64 download_count;
|
u64 download_count{};
|
||||||
std::string browser_download_url;
|
std::string browser_download_url{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GhApiEntry {
|
struct GhApiEntry {
|
||||||
std::string tag_name;
|
std::string tag_name{};
|
||||||
std::string name;
|
std::string name{};
|
||||||
std::vector<GhApiAsset> assets;
|
std::vector<GhApiAsset> assets{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Menu final : MenuBase {
|
struct Menu final : MenuBase {
|
||||||
Menu();
|
Menu();
|
||||||
~Menu();
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "GitHub"; };
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
@@ -66,9 +67,9 @@ private:
|
|||||||
void UpdateSubheading();
|
void UpdateSubheading();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::vector<Entry> m_entries;
|
std::vector<Entry> m_entries{};
|
||||||
s64 m_index{};
|
s64 m_index{};
|
||||||
std::unique_ptr<List> m_list;
|
std::unique_ptr<List> m_list{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui::menu::gh
|
} // namespace sphaira::ui::menu::gh
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/menus/menu_base.hpp"
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "ui/scrolling_text.hpp"
|
||||||
#include "ui/list.hpp"
|
#include "ui/list.hpp"
|
||||||
#include "nro.hpp"
|
#include "nro.hpp"
|
||||||
#include "fs.hpp"
|
#include "fs.hpp"
|
||||||
@@ -18,7 +19,7 @@ enum SortType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum OrderType {
|
enum OrderType {
|
||||||
OrderType_Decending,
|
OrderType_Descending,
|
||||||
OrderType_Ascending,
|
OrderType_Ascending,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,36 +27,43 @@ struct Menu final : MenuBase {
|
|||||||
Menu();
|
Menu();
|
||||||
~Menu();
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "Apps"; };
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
|
|
||||||
void SetIndex(s64 index);
|
|
||||||
void InstallHomebrew();
|
|
||||||
void ScanHomebrew();
|
|
||||||
void Sort();
|
|
||||||
void SortAndFindLastFile();
|
|
||||||
|
|
||||||
auto GetHomebrewList() const -> const std::vector<NroEntry>& {
|
auto GetHomebrewList() const -> const std::vector<NroEntry>& {
|
||||||
return m_entries;
|
return m_entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto IsStarEnabled() -> bool {
|
|
||||||
return m_sort.Get() >= SortType_UpdatedStar;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
|
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
|
||||||
static Result InstallHomebrewFromPath(const fs::FsPath& path);
|
static Result InstallHomebrewFromPath(const fs::FsPath& path);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void SetIndex(s64 index);
|
||||||
|
void InstallHomebrew();
|
||||||
|
void ScanHomebrew();
|
||||||
|
void Sort();
|
||||||
|
void SortAndFindLastFile();
|
||||||
|
void FreeEntries();
|
||||||
|
|
||||||
|
auto IsStarEnabled() -> bool {
|
||||||
|
return m_sort.Get() >= SortType_UpdatedStar;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr inline const char* INI_SECTION = "homebrew";
|
static constexpr inline const char* INI_SECTION = "homebrew";
|
||||||
|
|
||||||
std::vector<NroEntry> m_entries;
|
std::vector<NroEntry> m_entries{};
|
||||||
s64 m_index{}; // where i am in the array
|
s64 m_index{}; // where i am in the array
|
||||||
std::unique_ptr<List> m_list;
|
std::unique_ptr<List> m_list{};
|
||||||
|
|
||||||
|
ScrollingText m_scroll_name{};
|
||||||
|
ScrollingText m_scroll_author{};
|
||||||
|
ScrollingText m_scroll_version{};
|
||||||
|
|
||||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
|
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
|
||||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Decending};
|
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||||
option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false};
|
option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,16 +30,19 @@ struct Menu final : MenuBase {
|
|||||||
Menu();
|
Menu();
|
||||||
~Menu();
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "IRS"; };
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
|
|
||||||
|
private:
|
||||||
void PollCameraStatus(bool statup = false);
|
void PollCameraStatus(bool statup = false);
|
||||||
void LoadDefaultConfig();
|
void LoadDefaultConfig();
|
||||||
void UpdateConfig(const IrsImageTransferProcessorExConfig* config);
|
void UpdateConfig(const IrsImageTransferProcessorExConfig* config);
|
||||||
void ResetImage();
|
void ResetImage();
|
||||||
void UpdateImage();
|
void UpdateImage();
|
||||||
void updateColourArray();
|
void updateColourArray();
|
||||||
|
auto GetEntryName(s64 i) -> std::string;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Result m_init_rc{};
|
Result m_init_rc{};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "ui/widget.hpp"
|
#include "ui/widget.hpp"
|
||||||
#include "ui/menus/homebrew.hpp"
|
#include "ui/menus/homebrew.hpp"
|
||||||
#include "ui/menus/filebrowser.hpp"
|
#include "ui/menus/filebrowser.hpp"
|
||||||
#include "ui/menus/appstore.hpp"
|
|
||||||
|
|
||||||
namespace sphaira::ui::menu::main {
|
namespace sphaira::ui::menu::main {
|
||||||
|
|
||||||
@@ -18,6 +17,32 @@ enum class UpdateState {
|
|||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using MiscMenuFunction = std::function<std::shared_ptr<ui::menu::MenuBase>(void)>;
|
||||||
|
|
||||||
|
enum MiscMenuFlag : u8 {
|
||||||
|
// can be set as the rightside menu.
|
||||||
|
MiscMenuFlag_Shortcut = 1 << 0,
|
||||||
|
// needs install option to be enabled.
|
||||||
|
MiscMenuFlag_Install = 1 << 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MiscMenuEntry {
|
||||||
|
const char* name;
|
||||||
|
const char* title;
|
||||||
|
MiscMenuFunction func;
|
||||||
|
u8 flag;
|
||||||
|
|
||||||
|
auto IsShortcut() const -> bool {
|
||||||
|
return flag & MiscMenuFlag_Shortcut;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto IsInstall() const -> bool {
|
||||||
|
return flag & MiscMenuFlag_Install;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry>;
|
||||||
|
|
||||||
// this holds 2 menus and allows for switching between them
|
// this holds 2 menus and allows for switching between them
|
||||||
struct MainMenu final : Widget {
|
struct MainMenu final : Widget {
|
||||||
MainMenu();
|
MainMenu();
|
||||||
@@ -39,7 +64,7 @@ private:
|
|||||||
private:
|
private:
|
||||||
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
|
std::shared_ptr<homebrew::Menu> m_homebrew_menu{};
|
||||||
std::shared_ptr<filebrowser::Menu> m_filebrowser_menu{};
|
std::shared_ptr<filebrowser::Menu> m_filebrowser_menu{};
|
||||||
std::shared_ptr<appstore::Menu> m_app_store_menu{};
|
std::shared_ptr<MenuBase> m_right_side_menu{};
|
||||||
std::shared_ptr<MenuBase> m_current_menu{};
|
std::shared_ptr<MenuBase> m_current_menu{};
|
||||||
|
|
||||||
std::string m_update_url{};
|
std::string m_update_url{};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ struct MenuBase : Widget {
|
|||||||
MenuBase(std::string title);
|
MenuBase(std::string title);
|
||||||
virtual ~MenuBase();
|
virtual ~MenuBase();
|
||||||
|
|
||||||
|
virtual auto GetShortTitle() const -> const char* = 0;
|
||||||
virtual void Update(Controller* controller, TouchInfo* touch);
|
virtual void Update(Controller* controller, TouchInfo* touch);
|
||||||
virtual void Draw(NVGcontext* vg, Theme* theme);
|
virtual void Draw(NVGcontext* vg, Theme* theme);
|
||||||
|
|
||||||
@@ -21,14 +22,19 @@ struct MenuBase : Widget {
|
|||||||
void SetTitleSubHeading(std::string sub_heading);
|
void SetTitleSubHeading(std::string sub_heading);
|
||||||
void SetSubHeading(std::string sub_heading);
|
void SetSubHeading(std::string sub_heading);
|
||||||
|
|
||||||
|
auto GetTitle() const {
|
||||||
|
return m_title;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void UpdateVars();
|
void UpdateVars();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_title;
|
std::string m_title{};
|
||||||
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{};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "ui/menus/menu_base.hpp"
|
#include "ui/menus/menu_base.hpp"
|
||||||
#include "ui/scrollable_text.hpp"
|
#include "ui/scrollable_text.hpp"
|
||||||
|
#include "ui/scrolling_text.hpp"
|
||||||
#include "ui/list.hpp"
|
#include "ui/list.hpp"
|
||||||
#include "option.hpp"
|
#include "option.hpp"
|
||||||
#include <span>
|
#include <span>
|
||||||
@@ -42,79 +43,50 @@ enum class PageLoadState {
|
|||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
// all commented out entries are those that we don't query for.
|
|
||||||
// this saves time not only processing the json, but also the download
|
|
||||||
// of said json.
|
|
||||||
// by reducing the fields to only what we need, the size is 4-5x smaller.
|
|
||||||
|
|
||||||
struct Creator {
|
struct Creator {
|
||||||
std::string id;
|
std::string id{};
|
||||||
std::string display_name;
|
std::string display_name{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Details {
|
struct Details {
|
||||||
std::string name;
|
std::string name{};
|
||||||
// std::string description;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Preview {
|
struct Preview {
|
||||||
// std::string original;
|
std::string thumb{};
|
||||||
std::string thumb;
|
LazyImage lazy_image{};
|
||||||
LazyImage lazy_image;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DownloadPack {
|
struct DownloadPack {
|
||||||
std::string filename;
|
std::string filename{};
|
||||||
std::string url;
|
std::string url{};
|
||||||
std::string mimetype;
|
std::string mimetype{};
|
||||||
};
|
};
|
||||||
|
|
||||||
using DownloadTheme = DownloadPack;
|
using DownloadTheme = DownloadPack;
|
||||||
|
|
||||||
struct ThemeEntry {
|
struct ThemeEntry {
|
||||||
std::string id;
|
std::string id{};
|
||||||
// Creator creator;
|
Preview preview{};
|
||||||
// Details details;
|
|
||||||
// std::string last_updated;
|
|
||||||
// u64 dl_count;
|
|
||||||
// u64 like_count;
|
|
||||||
// std::vector<std::string> categories;
|
|
||||||
// std::string target;
|
|
||||||
Preview preview;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// struct Pack {
|
|
||||||
// std::string id;
|
|
||||||
// Creator creator;
|
|
||||||
// Details details;
|
|
||||||
// std::string last_updated;
|
|
||||||
// std::vector<std::string> categories;
|
|
||||||
// u64 dl_count;
|
|
||||||
// u64 like_count;
|
|
||||||
// std::vector<ThemeEntry> themes;
|
|
||||||
// };
|
|
||||||
|
|
||||||
struct PackListEntry {
|
struct PackListEntry {
|
||||||
std::string id;
|
std::string id{};
|
||||||
Creator creator;
|
Creator creator{};
|
||||||
Details details;
|
Details details{};
|
||||||
// std::string last_updated;
|
std::vector<ThemeEntry> themes{};
|
||||||
// std::vector<std::string> categories;
|
|
||||||
// u64 dl_count;
|
|
||||||
// u64 like_count;
|
|
||||||
std::vector<ThemeEntry> themes;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Pagination {
|
struct Pagination {
|
||||||
u64 page;
|
u64 page{};
|
||||||
u64 limit;
|
u64 limit{};
|
||||||
u64 page_count;
|
u64 page_count{};
|
||||||
u64 item_count;
|
u64 item_count{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PackList {
|
struct PackList {
|
||||||
std::vector<PackListEntry> packList;
|
std::vector<PackListEntry> packList{};
|
||||||
Pagination pagination;
|
Pagination pagination{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Config {
|
struct Config {
|
||||||
@@ -123,10 +95,10 @@ struct Config {
|
|||||||
u32 sort_index{};
|
u32 sort_index{};
|
||||||
u32 order_index{};
|
u32 order_index{};
|
||||||
// search query, if empty, its not used
|
// search query, if empty, its not used
|
||||||
std::string query;
|
std::string query{};
|
||||||
// this is actually an array of creator ids, but we don't support that feature
|
// this is actually an array of creator ids, but we don't support that feature
|
||||||
// if empty, its not used
|
// if empty, its not used
|
||||||
std::string creator;
|
std::string creator{};
|
||||||
// defaults
|
// defaults
|
||||||
u32 page{1};
|
u32 page{1};
|
||||||
u32 limit{18};
|
u32 limit{18};
|
||||||
@@ -152,7 +124,7 @@ struct Config {
|
|||||||
struct Menu; // fwd
|
struct Menu; // fwd
|
||||||
|
|
||||||
struct PageEntry {
|
struct PageEntry {
|
||||||
std::vector<PackListEntry> m_packList;
|
std::vector<PackListEntry> m_packList{};
|
||||||
Pagination m_pagination{};
|
Pagination m_pagination{};
|
||||||
PageLoadState m_ready{PageLoadState::None};
|
PageLoadState m_ready{PageLoadState::None};
|
||||||
};
|
};
|
||||||
@@ -161,6 +133,7 @@ struct Menu final : MenuBase {
|
|||||||
Menu();
|
Menu();
|
||||||
~Menu();
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "Themezer"; };
|
||||||
void Update(Controller* controller, TouchInfo* touch) override;
|
void Update(Controller* controller, TouchInfo* touch) override;
|
||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
void OnFocusGained() override;
|
void OnFocusGained() override;
|
||||||
@@ -172,9 +145,6 @@ struct Menu final : MenuBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// void SetSearch(const std::string& term);
|
|
||||||
// void SetAuthor();
|
|
||||||
|
|
||||||
void InvalidateAllPages();
|
void InvalidateAllPages();
|
||||||
void PackListDownload();
|
void PackListDownload();
|
||||||
void OnPackListDownload();
|
void OnPackListDownload();
|
||||||
@@ -183,14 +153,17 @@ private:
|
|||||||
static constexpr inline const char* INI_SECTION = "themezer";
|
static constexpr inline const char* INI_SECTION = "themezer";
|
||||||
static constexpr inline u32 MAX_ON_PAGE = 16; // same as website
|
static constexpr inline u32 MAX_ON_PAGE = 16; // same as website
|
||||||
|
|
||||||
std::vector<PageEntry> m_pages;
|
std::vector<PageEntry> m_pages{};
|
||||||
s64 m_page_index{};
|
s64 m_page_index{};
|
||||||
s64 m_page_index_max{1};
|
s64 m_page_index_max{1};
|
||||||
|
|
||||||
std::string m_search{};
|
std::string m_search{};
|
||||||
|
|
||||||
s64 m_index{}; // where i am in the array
|
s64 m_index{}; // where i am in the array
|
||||||
std::unique_ptr<List> m_list;
|
std::unique_ptr<List> m_list{};
|
||||||
|
|
||||||
|
ScrollingText m_scroll_name{};
|
||||||
|
ScrollingText m_scroll_author{};
|
||||||
|
|
||||||
// options
|
// options
|
||||||
option::OptionLong m_sort{INI_SECTION, "sort", 0};
|
option::OptionLong m_sort{INI_SECTION, "sort", 0};
|
||||||
|
|||||||
45
sphaira/include/ui/menus/usb_menu.hpp
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/menus/menu_base.hpp"
|
||||||
|
#include "yati/source/usb.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::usb {
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
// not connected.
|
||||||
|
None,
|
||||||
|
// just connected, waiting for file list.
|
||||||
|
Connected_WaitForFileList,
|
||||||
|
// just connected, starts the transfer.
|
||||||
|
Connected_StartingTransfer,
|
||||||
|
// set whilst transfer is in progress.
|
||||||
|
Progress,
|
||||||
|
// set when the transfer is finished.
|
||||||
|
Done,
|
||||||
|
// failed to connect.
|
||||||
|
Failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Menu final : MenuBase {
|
||||||
|
Menu();
|
||||||
|
~Menu();
|
||||||
|
|
||||||
|
auto GetShortTitle() const -> const char* override { return "USB"; };
|
||||||
|
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<yati::source::Usb> m_usb_source{};
|
||||||
|
bool m_was_mtp_enabled{};
|
||||||
|
|
||||||
|
Thread m_thread{};
|
||||||
|
Mutex m_mutex{};
|
||||||
|
// the below are shared across threads, lock with the above mutex!
|
||||||
|
State m_state{State::None};
|
||||||
|
std::vector<std::string> m_names{};
|
||||||
|
bool m_usb_has_connection{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::usb
|
||||||
@@ -22,9 +22,9 @@ private:
|
|||||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_text;
|
std::string m_text{};
|
||||||
std::size_t m_count{180}; // count down to zero
|
std::size_t m_count{180}; // count down to zero
|
||||||
Side m_side;
|
Side m_side{};
|
||||||
bool m_bounds_measured{};
|
bool m_bounds_measured{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ private:
|
|||||||
void Draw(NVGcontext* vg, Theme* theme, Entries& entries);
|
void Draw(NVGcontext* vg, Theme* theme, Entries& entries);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Entries m_entries_left;
|
Entries m_entries_left{};
|
||||||
Entries m_entries_right;
|
Entries m_entries_right{};
|
||||||
Mutex m_mutex{};
|
Mutex m_mutex{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "types.hpp"
|
#include "types.hpp"
|
||||||
|
#include <stop_token>
|
||||||
|
|
||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
|
|
||||||
class Object {
|
class Object {
|
||||||
public:
|
public:
|
||||||
Object() = default;
|
Object() = default;
|
||||||
virtual ~Object() = default;
|
virtual ~Object() {
|
||||||
|
m_stop_source.request_stop();
|
||||||
|
}
|
||||||
|
|
||||||
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void = 0;
|
virtual auto Draw(NVGcontext* vg, Theme* theme) -> void = 0;
|
||||||
|
|
||||||
@@ -71,8 +74,14 @@ public:
|
|||||||
m_hidden = value;
|
m_hidden = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto GetToken() const {
|
||||||
|
return m_stop_source.get_token();
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Vec4 m_pos{};
|
Vec4 m_pos{};
|
||||||
|
// used for lifetime management across threads.
|
||||||
|
std::stop_source m_stop_source{};
|
||||||
bool m_hidden{false};
|
bool m_hidden{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_text;
|
std::string m_text{};
|
||||||
Vec2 m_text_pos{};
|
Vec2 m_text_pos{};
|
||||||
bool m_selected{false};
|
bool m_selected{false};
|
||||||
};
|
};
|
||||||
@@ -32,11 +32,9 @@ public:
|
|||||||
using Options = std::vector<Option>;
|
using Options = std::vector<Option>;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}); // confirm
|
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}, int image = 0); // confirm
|
||||||
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb); // yesno
|
OptionBox(const std::string& message, const Option& a, const Option& b, Callback cb, int image = 0); // yesno
|
||||||
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb); // yesno
|
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb, int image = 0); // yesno
|
||||||
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, Callback cb); // tri
|
|
||||||
OptionBox(const std::string& message, const Option& a, const Option& b, const Option& c, s64 index, Callback cb); // tri
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -48,13 +46,14 @@ private:
|
|||||||
void SetIndex(s64 index);
|
void SetIndex(s64 index);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_message;
|
std::string m_message{};
|
||||||
Callback m_callback;
|
Callback m_callback{};
|
||||||
|
int m_image{};
|
||||||
|
|
||||||
Vec4 m_spacer_line{};
|
Vec4 m_spacer_line{};
|
||||||
|
|
||||||
s64 m_index{};
|
s64 m_index{};
|
||||||
std::vector<OptionBoxEntry> m_entries;
|
std::vector<OptionBoxEntry> m_entries{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/widget.hpp"
|
#include "ui/widget.hpp"
|
||||||
|
#include "ui/scrolling_text.hpp"
|
||||||
#include "ui/list.hpp"
|
#include "ui/list.hpp"
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
@@ -32,12 +33,14 @@ private:
|
|||||||
static constexpr float m_text_xoffset{15.f};
|
static constexpr float m_text_xoffset{15.f};
|
||||||
static constexpr float m_line_width{1220.f};
|
static constexpr float m_line_width{1220.f};
|
||||||
|
|
||||||
std::string m_title;
|
std::string m_title{};
|
||||||
Items m_items;
|
Items m_items{};
|
||||||
Callback m_callback;
|
Callback m_callback{};
|
||||||
s64 m_index; // index in list array
|
s64 m_index{}; // index in list array
|
||||||
|
s64 m_starting_index{};
|
||||||
|
|
||||||
std::unique_ptr<List> m_list;
|
std::unique_ptr<List> m_list{};
|
||||||
|
ScrollingText m_scroll_text{};
|
||||||
|
|
||||||
float m_yoff{};
|
float m_yoff{};
|
||||||
float m_line_top{};
|
float m_line_top{};
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -21,8 +24,12 @@ struct ProgressBox final : Widget {
|
|||||||
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -40,24 +47,36 @@ struct ProgressBox final : Widget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void FreeImage();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
struct ThreadData {
|
struct ThreadData {
|
||||||
ProgressBox* pbox;
|
ProgressBox* pbox{};
|
||||||
ProgressBoxCallback callback;
|
ProgressBoxCallback callback{};
|
||||||
bool result;
|
bool result{};
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
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{};
|
||||||
bool m_exit_requested{};
|
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.
|
||||||
|
|||||||
@@ -14,15 +14,15 @@ struct ScrollableText final : Widget {
|
|||||||
// float m_y_off = m_y_off_base;
|
// float m_y_off = m_y_off_base;
|
||||||
// static constexpr float m_clip_y = 250.0F;
|
// static constexpr float m_clip_y = 250.0F;
|
||||||
|
|
||||||
|
static constexpr inline float m_step = 30;
|
||||||
|
const float m_font_size;
|
||||||
const float m_y_off_base;
|
const float m_y_off_base;
|
||||||
float m_y_off;
|
|
||||||
const float m_clip_y;
|
const float m_clip_y;
|
||||||
const float m_end_w;
|
const float m_end_w;
|
||||||
static constexpr float m_step = 30;
|
|
||||||
|
|
||||||
int m_index = 0;
|
float m_y_off{};
|
||||||
const float m_font_size;
|
int m_index{};
|
||||||
float m_bounds[4];
|
float m_bounds[4]{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sphaira::ui
|
} // namespace sphaira::ui
|
||||||
|
|||||||
19
sphaira/include/ui/scrolling_text.hpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ui/widget.hpp"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace sphaira::ui {
|
||||||
|
|
||||||
|
struct ScrollingText final {
|
||||||
|
public:
|
||||||
|
void Draw(NVGcontext*, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const std::string& text_entry);
|
||||||
|
void DrawArgs(NVGcontext*, bool focus, float x, float y, float w, float size, int align, const NVGcolor& colour, const char* s, ...) __attribute__ ((format (printf, 10, 11)));
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string m_str;
|
||||||
|
s64 m_tick;
|
||||||
|
float m_text_xoff;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ui
|
||||||
@@ -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 {
|
||||||
@@ -323,37 +322,36 @@ inline ActionType operator|(ActionType a, ActionType b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Action final {
|
struct Action final {
|
||||||
|
using CallbackEmpty = std::function<void()>;
|
||||||
|
using CallbackWithBool = std::function<void(bool)>;
|
||||||
using Callback = std::variant<
|
using Callback = std::variant<
|
||||||
std::function<void()>,
|
CallbackEmpty,
|
||||||
std::function<void(bool)>
|
CallbackWithBool
|
||||||
>;
|
>;
|
||||||
|
|
||||||
Action(Callback cb) : m_type{ActionType::DOWN}, m_hint{""}, m_callback{cb}, m_hidden{true} {}
|
Action(Callback cb) : Action{ActionType::DOWN, "", cb} {}
|
||||||
Action(std::string hint, Callback cb) : m_type{ActionType::DOWN}, m_hint{hint}, m_callback{cb} {}
|
Action(std::string hint, Callback cb) : Action{ActionType::DOWN, hint, cb} {}
|
||||||
Action(u8 type, Callback cb) : m_type{type}, m_hint{""}, m_callback{cb}, m_hidden{true} {}
|
Action(u8 type, Callback cb) : Action{type, "", cb} {}
|
||||||
Action(u8 type, std::string hint, Callback cb) : m_type{type}, m_hint{hint}, m_callback{cb} {}
|
Action(u8 type, std::string hint, Callback cb) : m_type{type}, m_callback{cb}, m_hint{hint} {}
|
||||||
|
|
||||||
auto IsHidden() const noexcept { return m_hidden; }
|
auto IsHidden() const noexcept { return m_hint.empty(); }
|
||||||
|
|
||||||
auto Invoke(bool down) const {
|
auto Invoke(bool down) const {
|
||||||
// todo: make this a visit
|
std::visit([down](auto&& arg){
|
||||||
switch (m_callback.index()) {
|
using T = std::decay_t<decltype(arg)>;
|
||||||
case 0:
|
if constexpr(std::is_same_v<T, CallbackEmpty>) {
|
||||||
std::get<0>(m_callback)();
|
arg();
|
||||||
break;
|
} else if constexpr(std::is_same_v<T, CallbackWithBool>) {
|
||||||
case 1:
|
arg(down);
|
||||||
std::get<1>(m_callback)(down);
|
} else {
|
||||||
break;
|
static_assert(false, "non-exhaustive visitor!");
|
||||||
}
|
}
|
||||||
// std::visit([down, this](auto& cb){
|
}, m_callback);
|
||||||
// cb(down);
|
|
||||||
// }), m_callback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u8 m_type;
|
u8 m_type{};
|
||||||
std::string m_hint; // todo: make optional
|
Callback m_callback{};
|
||||||
Callback m_callback;
|
std::string m_hint{};
|
||||||
bool m_hidden{false}; // replace this optional text
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Controller {
|
struct Controller {
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ struct Widget : public Object {
|
|||||||
|
|
||||||
auto GetUiButtons() const -> uiButtons;
|
auto GetUiButtons() const -> uiButtons;
|
||||||
|
|
||||||
Actions m_actions;
|
Actions m_actions{};
|
||||||
Vec2 m_button_pos{1220, 675};
|
Vec2 m_button_pos{1220, 675};
|
||||||
bool m_focus{false};
|
bool m_focus{false};
|
||||||
bool m_pop{false};
|
bool m_pop{false};
|
||||||
|
|||||||
@@ -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
|
|
||||||
43
sphaira/include/yati/container/base.hpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "yati/source/base.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <memory>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::container {
|
||||||
|
|
||||||
|
enum class CollectionType {
|
||||||
|
CollectionType_NCA,
|
||||||
|
CollectionType_NCZ,
|
||||||
|
CollectionType_TIK,
|
||||||
|
CollectionType_CERT,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CollectionEntry {
|
||||||
|
// collection name within file.
|
||||||
|
std::string name{};
|
||||||
|
// collection offset within file.
|
||||||
|
s64 offset{};
|
||||||
|
// collection size within file, may be compressed size.
|
||||||
|
s64 size{};
|
||||||
|
};
|
||||||
|
|
||||||
|
using Collections = std::vector<CollectionEntry>;
|
||||||
|
|
||||||
|
struct Base {
|
||||||
|
using Source = source::Base;
|
||||||
|
|
||||||
|
Base(std::shared_ptr<Source> source) : m_source{source} { }
|
||||||
|
virtual ~Base() = default;
|
||||||
|
virtual Result GetCollections(Collections& out) = 0;
|
||||||
|
auto GetSource() const {
|
||||||
|
return m_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<Source> m_source;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::container
|
||||||
13
sphaira/include/yati/container/nsp.hpp
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::container {
|
||||||
|
|
||||||
|
struct Nsp final : Base {
|
||||||
|
using Base::Base;
|
||||||
|
Result GetCollections(Collections& out) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::container
|
||||||
15
sphaira/include/yati/container/xci.hpp
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::container {
|
||||||
|
|
||||||
|
struct Xci final : Base {
|
||||||
|
using Base::Base;
|
||||||
|
Result GetCollections(Collections& out) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::container
|
||||||
57
sphaira/include/yati/nx/crypto.hpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::crypto {
|
||||||
|
|
||||||
|
struct Aes128 {
|
||||||
|
Aes128(const void *key, bool is_encryptor) {
|
||||||
|
m_is_encryptor = is_encryptor;
|
||||||
|
aes128ContextCreate(&m_ctx, key, is_encryptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Run(void *dst, const void *src) {
|
||||||
|
if (m_is_encryptor) {
|
||||||
|
aes128EncryptBlock(&m_ctx, dst, src);
|
||||||
|
} else {
|
||||||
|
aes128DecryptBlock(&m_ctx, dst, src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Aes128Context m_ctx;
|
||||||
|
bool m_is_encryptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Aes128Xts {
|
||||||
|
Aes128Xts(const u8 *key, bool is_encryptor) : Aes128Xts{key, key + 0x10, is_encryptor} { }
|
||||||
|
Aes128Xts(const void *key0, const void *key1, bool is_encryptor) {
|
||||||
|
m_is_encryptor = is_encryptor;
|
||||||
|
aes128XtsContextCreate(&m_ctx, key0, key1, is_encryptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Run(void *dst, const void *src, u64 sector, u64 sector_size, u64 data_size) {
|
||||||
|
for (u64 pos = 0; pos < data_size; pos += sector_size) {
|
||||||
|
aes128XtsContextResetSector(&m_ctx, sector++, true);
|
||||||
|
if (m_is_encryptor) {
|
||||||
|
aes128XtsEncrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
|
||||||
|
} else {
|
||||||
|
aes128XtsDecrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Aes128XtsContext m_ctx;
|
||||||
|
bool m_is_encryptor;
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline void cryptoAes128(const void *in, void *out, const void* key, bool is_encryptor) {
|
||||||
|
Aes128(key, is_encryptor).Run(out, in);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void cryptoAes128Xts(const void* in, void* out, const u8* key, u64 sector, u64 sector_size, u64 data_size, bool is_encryptor) {
|
||||||
|
Aes128Xts(key, is_encryptor).Run(out, in, sector, sector_size, data_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::crypto
|
||||||
83
sphaira/include/yati/nx/es.hpp
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include <span>
|
||||||
|
#include "ncm.hpp"
|
||||||
|
#include "keys.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::es {
|
||||||
|
|
||||||
|
enum { TicketModule = 522 };
|
||||||
|
|
||||||
|
enum : Result {
|
||||||
|
// found ticket has missmatching rights_id from it's name.
|
||||||
|
Result_InvalidTicketBadRightsId = MAKERESULT(TicketModule, 71),
|
||||||
|
Result_InvalidTicketVersion = MAKERESULT(TicketModule, 72),
|
||||||
|
Result_InvalidTicketKeyType = MAKERESULT(TicketModule, 73),
|
||||||
|
Result_InvalidTicketKeyRevision = MAKERESULT(TicketModule, 74),
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TicketSigantureType {
|
||||||
|
TicketSigantureType_RSA_4096_SHA1 = 0x010000,
|
||||||
|
TicketSigantureType_RSA_2048_SHA1 = 0x010001,
|
||||||
|
TicketSigantureType_ECDSA_SHA1 = 0x010002,
|
||||||
|
TicketSigantureType_RSA_4096_SHA256 = 0x010003,
|
||||||
|
TicketSigantureType_RSA_2048_SHA256 = 0x010004,
|
||||||
|
TicketSigantureType_ECDSA_SHA256 = 0x010005,
|
||||||
|
TicketSigantureType_HMAC_SHA1_160 = 0x010006,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TicketTitleKeyType {
|
||||||
|
TicketTitleKeyType_Common = 0,
|
||||||
|
TicketTitleKeyType_Personalized = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TicketPropertiesBitfield {
|
||||||
|
TicketPropertiesBitfield_None = 0,
|
||||||
|
// temporary ticket, removed on restart
|
||||||
|
TicketPropertiesBitfield_Temporary = 1 << 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TicketData {
|
||||||
|
u8 issuer[0x40];
|
||||||
|
u8 title_key_block[0x100];
|
||||||
|
u8 ticket_version1;
|
||||||
|
u8 title_key_type;
|
||||||
|
u16 ticket_version2;
|
||||||
|
u8 license_type;
|
||||||
|
u8 master_key_revision;
|
||||||
|
u16 properties_bitfield;
|
||||||
|
u8 _0x148[0x8];
|
||||||
|
u64 ticket_id;
|
||||||
|
u64 device_id;
|
||||||
|
FsRightsId rights_id;
|
||||||
|
u32 account_id;
|
||||||
|
u8 _0x174[0xC];
|
||||||
|
u8 _0x180[0x140];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(TicketData) == 0x2C0);
|
||||||
|
|
||||||
|
struct EticketRsaDeviceKey {
|
||||||
|
u8 ctr[AES_128_KEY_SIZE];
|
||||||
|
u8 private_exponent[0x100];
|
||||||
|
u8 modulus[0x100];
|
||||||
|
u32 public_exponent; ///< Stored using big endian byte order. Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT.
|
||||||
|
u8 padding[0x14];
|
||||||
|
u64 device_id;
|
||||||
|
u8 ghash[0x10];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(EticketRsaDeviceKey) == 0x240);
|
||||||
|
|
||||||
|
// es functions.
|
||||||
|
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
|
||||||
|
|
||||||
|
// ticket functions.
|
||||||
|
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out);
|
||||||
|
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out);
|
||||||
|
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
|
||||||
|
|
||||||
|
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 PatchTicket(std::span<u8> ticket, const keys::Keys& keys);
|
||||||
|
|
||||||
|
} // namespace sphaira::es
|
||||||
70
sphaira/include/yati/nx/keys.hpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
#include "defines.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::keys {
|
||||||
|
|
||||||
|
struct KeyEntry {
|
||||||
|
u8 key[AES_128_KEY_SIZE]{};
|
||||||
|
|
||||||
|
auto IsValid() const -> bool {
|
||||||
|
const KeyEntry empty{};
|
||||||
|
return std::memcmp(key, &empty, sizeof(key));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
using KeySection = std::array<KeyEntry, 0x20>;
|
||||||
|
struct Keys {
|
||||||
|
u8 header_key[0x20]{};
|
||||||
|
// the below are only found if read_from_file=true
|
||||||
|
KeySection key_area_key[0x3]{}; // index
|
||||||
|
KeySection titlekek{};
|
||||||
|
KeySection master_key{};
|
||||||
|
KeyEntry eticket_rsa_kek{};
|
||||||
|
SetCalRsa2048DeviceKey eticket_device_key{};
|
||||||
|
|
||||||
|
static auto FixKey(u8 key) -> u8 {
|
||||||
|
if (key) {
|
||||||
|
return key - 1;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HasNcaKeyArea(u8 key, u8 index) const -> bool {
|
||||||
|
return key_area_key[index][FixKey(key)].IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HasTitleKek(u8 key) const -> bool {
|
||||||
|
return titlekek[FixKey(key)].IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HasMasterKey(u8 key) const -> bool {
|
||||||
|
return master_key[FixKey(key)].IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetNcaKeyArea(KeyEntry* out, u8 key, u8 index) const -> Result {
|
||||||
|
R_UNLESS(HasNcaKeyArea(key, index), 0x1);
|
||||||
|
*out = key_area_key[index][FixKey(key)];
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetTitleKek(KeyEntry* out, u8 key) const -> Result {
|
||||||
|
R_UNLESS(HasTitleKek(key), 0x1);
|
||||||
|
*out = titlekek[FixKey(key)];
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetMasterKey(KeyEntry* out, u8 key) const -> Result {
|
||||||
|
R_UNLESS(HasMasterKey(key), 0x1);
|
||||||
|
*out = master_key[FixKey(key)];
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void parse_hex_key(void* key, const char* hex);
|
||||||
|
Result parse_keys(Keys& out, bool read_from_file);
|
||||||
|
|
||||||
|
} // namespace sphaira::keys
|
||||||
221
sphaira/include/yati/nx/nca.hpp
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include "keys.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::nca {
|
||||||
|
|
||||||
|
#define NCA0_MAGIC 0x3041434E
|
||||||
|
#define NCA2_MAGIC 0x3241434E
|
||||||
|
#define NCA3_MAGIC 0x3341434E
|
||||||
|
|
||||||
|
#define NCA_SECTOR_SIZE 0x200
|
||||||
|
#define NCA_XTS_SECTION_SIZE 0xC00
|
||||||
|
#define NCA_SECTION_TOTAL 0x4
|
||||||
|
#define NCA_MEDIA_REAL(x)((x * 0x200))
|
||||||
|
|
||||||
|
#define NCA_PROGRAM_LOGO_OFFSET 0x8000
|
||||||
|
#define NCA_META_CNMT_OFFSET 0xC20
|
||||||
|
|
||||||
|
enum KeyGenerationOld {
|
||||||
|
KeyGenerationOld_100 = 0x0,
|
||||||
|
KeyGenerationOld_Unused = 0x1,
|
||||||
|
KeyGenerationOld_300 = 0x2,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum KeyGeneration {
|
||||||
|
KeyGeneration_301 = 0x3,
|
||||||
|
KeyGeneration_400 = 0x4,
|
||||||
|
KeyGeneration_500 = 0x5,
|
||||||
|
KeyGeneration_600 = 0x6,
|
||||||
|
KeyGeneration_620 = 0x7,
|
||||||
|
KeyGeneration_700 = 0x8,
|
||||||
|
KeyGeneration_810 = 0x9,
|
||||||
|
KeyGeneration_900 = 0x0A,
|
||||||
|
KeyGeneration_910 = 0x0B,
|
||||||
|
KeyGeneration_1210 = 0x0C,
|
||||||
|
KeyGeneration_1300 = 0x0D,
|
||||||
|
KeyGeneration_1400 = 0x0E,
|
||||||
|
KeyGeneration_1500 = 0x0F,
|
||||||
|
KeyGeneration_1600 = 0x10,
|
||||||
|
KeyGeneration_1700 = 0x11,
|
||||||
|
KeyGeneration_1800 = 0x12,
|
||||||
|
KeyGeneration_1900 = 0x13,
|
||||||
|
KeyGeneration_2000 = 0x14,
|
||||||
|
KeyGeneration_Invalid = 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum KeyAreaEncryptionKeyIndex {
|
||||||
|
KeyAreaEncryptionKeyIndex_Application = 0x0,
|
||||||
|
KeyAreaEncryptionKeyIndex_Ocean = 0x1,
|
||||||
|
KeyAreaEncryptionKeyIndex_System = 0x2
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DistributionType {
|
||||||
|
DistributionType_System = 0x0,
|
||||||
|
DistributionType_GameCard = 0x1
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ContentType {
|
||||||
|
ContentType_Program = 0x0,
|
||||||
|
ContentType_Meta = 0x1,
|
||||||
|
ContentType_Control = 0x2,
|
||||||
|
ContentType_Manual = 0x3,
|
||||||
|
ContentType_Data = 0x4,
|
||||||
|
ContentType_PublicData = 0x5,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum FileSystemType {
|
||||||
|
FileSystemType_RomFS = 0x0,
|
||||||
|
FileSystemType_PFS0 = 0x1
|
||||||
|
};
|
||||||
|
|
||||||
|
enum HashType {
|
||||||
|
HashType_Auto = 0x0,
|
||||||
|
HashType_HierarchicalSha256 = 0x2,
|
||||||
|
HashType_HierarchicalIntegrity = 0x3
|
||||||
|
};
|
||||||
|
|
||||||
|
enum EncryptionType {
|
||||||
|
EncryptionType_Auto = 0x0,
|
||||||
|
EncryptionType_None = 0x1,
|
||||||
|
EncryptionType_AesXts = 0x2,
|
||||||
|
EncryptionType_AesCtr = 0x3,
|
||||||
|
EncryptionType_AesCtrEx = 0x4,
|
||||||
|
EncryptionType_AesCtrSkipLayerHash = 0x5, // [14.0.0+]
|
||||||
|
EncryptionType_AesCtrExSkipLayerHash = 0x6, // [14.0.0+]
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SectionTableEntry {
|
||||||
|
u32 media_start_offset; // divided by 0x200.
|
||||||
|
u32 media_end_offset; // divided by 0x200.
|
||||||
|
u8 _0x8[0x4]; // unknown.
|
||||||
|
u8 _0xC[0x4]; // unknown.
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LayerRegion {
|
||||||
|
u64 offset;
|
||||||
|
u64 size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HierarchicalSha256Data {
|
||||||
|
u8 master_hash[0x20];
|
||||||
|
u32 block_size;
|
||||||
|
u32 layer_count;
|
||||||
|
LayerRegion hash_layer;
|
||||||
|
LayerRegion pfs0_layer;
|
||||||
|
LayerRegion unused_layers[3];
|
||||||
|
u8 _0x78[0x80];
|
||||||
|
};
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||||
|
u64 logical_offset;
|
||||||
|
u64 hash_data_size;
|
||||||
|
u32 block_size; // log2
|
||||||
|
u32 _0x14; // reserved
|
||||||
|
};
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
struct InfoLevelHash {
|
||||||
|
u32 max_layers;
|
||||||
|
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
||||||
|
u8 signature_salt[0x20];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IntegrityMetaInfo {
|
||||||
|
u32 magic; // IVFC
|
||||||
|
u32 version;
|
||||||
|
u32 master_hash_size;
|
||||||
|
InfoLevelHash info_level_hash;
|
||||||
|
u8 master_hash[0x20];
|
||||||
|
u8 _0xE0[0x18];
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
||||||
|
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
||||||
|
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
||||||
|
|
||||||
|
struct FsHeader {
|
||||||
|
u16 version; // always 2.
|
||||||
|
u8 fs_type; // see FileSystemType.
|
||||||
|
u8 hash_type; // see HashType.
|
||||||
|
u8 encryption_type; // see EncryptionType.
|
||||||
|
u8 metadata_hash_type;
|
||||||
|
u8 _0x6[0x2]; // empty.
|
||||||
|
|
||||||
|
union {
|
||||||
|
HierarchicalSha256Data hierarchical_sha256_data;
|
||||||
|
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
||||||
|
} hash_data;
|
||||||
|
|
||||||
|
u8 patch_info[0x40];
|
||||||
|
u64 section_ctr;
|
||||||
|
u8 spares_info[0x30];
|
||||||
|
u8 compression_info[0x28];
|
||||||
|
u8 meta_data_hash_data_info[0x30];
|
||||||
|
u8 reserved[0x30];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(FsHeader) == 0x200);
|
||||||
|
static_assert(sizeof(FsHeader::hash_data) == 0xF8);
|
||||||
|
|
||||||
|
struct SectionHeaderHash {
|
||||||
|
u8 sha256[0x20];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct KeyArea {
|
||||||
|
u8 area[0x10];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
u8 rsa_fixed_key[0x100];
|
||||||
|
u8 rsa_npdm[0x100]; // key from npdm.
|
||||||
|
u32 magic;
|
||||||
|
u8 distribution_type; // see DistributionType.
|
||||||
|
u8 content_type; // see ContentType.
|
||||||
|
u8 old_key_gen; // see KeyGenerationOld.
|
||||||
|
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
|
||||||
|
u64 size;
|
||||||
|
u64 program_id;
|
||||||
|
u32 context_id;
|
||||||
|
u32 sdk_version;
|
||||||
|
u8 key_gen; // see KeyGeneration.
|
||||||
|
u8 sig_key_gen;
|
||||||
|
u8 _0x222[0xE]; // empty.
|
||||||
|
FsRightsId rights_id;
|
||||||
|
|
||||||
|
SectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
||||||
|
SectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
||||||
|
KeyArea key_area[NCA_SECTION_TOTAL];
|
||||||
|
|
||||||
|
u8 _0x340[0xC0]; // empty.
|
||||||
|
|
||||||
|
FsHeader fs_header[NCA_SECTION_TOTAL];
|
||||||
|
|
||||||
|
auto GetKeyGeneration() const -> u8 {
|
||||||
|
if (old_key_gen < key_gen) {
|
||||||
|
return key_gen;
|
||||||
|
} else {
|
||||||
|
return old_key_gen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetKeyGeneration(u8 key_generation) {
|
||||||
|
if (key_generation <= 0x2) {
|
||||||
|
old_key_gen = key_generation;
|
||||||
|
key_gen = 0;
|
||||||
|
} else {
|
||||||
|
old_key_gen = 0x2;
|
||||||
|
key_gen = key_generation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
static_assert(sizeof(Header) == 0xC00);
|
||||||
|
|
||||||
|
Result DecryptKeak(const keys::Keys& keys, Header& header);
|
||||||
|
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
|
||||||
|
Result VerifyFixedKey(const Header& header);
|
||||||
|
|
||||||
|
auto GetKeyGenStr(u8 key_gen) -> const char*;
|
||||||
|
|
||||||
|
} // namespace sphaira::nca
|
||||||
44
sphaira/include/yati/nx/ncm.hpp
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::ncm {
|
||||||
|
|
||||||
|
struct PackagedContentMeta {
|
||||||
|
u64 title_id;
|
||||||
|
u32 title_version;
|
||||||
|
u8 meta_type; // NcmContentMetaType
|
||||||
|
u8 content_meta_platform; // [17.0.0+]
|
||||||
|
NcmContentMetaHeader meta_header;
|
||||||
|
u8 install_type; // NcmContentInstallType
|
||||||
|
u8 _0x17;
|
||||||
|
u32 required_sys_version;
|
||||||
|
u8 _0x1C[0x4];
|
||||||
|
};
|
||||||
|
static_assert(sizeof(PackagedContentMeta) == 0x20);
|
||||||
|
|
||||||
|
struct ContentStorageRecord {
|
||||||
|
NcmContentMetaKey key;
|
||||||
|
u8 storage_id; // NcmStorageId
|
||||||
|
u8 padding[0x7];
|
||||||
|
};
|
||||||
|
|
||||||
|
union ExtendedHeader {
|
||||||
|
NcmApplicationMetaExtendedHeader application;
|
||||||
|
NcmPatchMetaExtendedHeader patch;
|
||||||
|
NcmAddOnContentMetaExtendedHeader addon;
|
||||||
|
NcmLegacyAddOnContentMetaExtendedHeader addon_legacy;
|
||||||
|
NcmDataPatchMetaExtendedHeader data_patch;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto GetMetaTypeStr(u8 meta_type) -> const char*;
|
||||||
|
auto GetStorageIdStr(u8 storage_id) -> const char*;
|
||||||
|
|
||||||
|
auto GetAppId(u8 meta_type, u64 id) -> u64;
|
||||||
|
auto GetAppId(const NcmContentMetaKey& key) -> u64;
|
||||||
|
auto GetAppId(const PackagedContentMeta& meta) -> u64;
|
||||||
|
|
||||||
|
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
|
||||||
|
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);
|
||||||
|
|
||||||
|
} // namespace sphaira::ncm
|
||||||
54
sphaira/include/yati/nx/ncz.hpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::ncz {
|
||||||
|
|
||||||
|
#define NCZ_SECTION_MAGIC 0x4E544345535A434EUL
|
||||||
|
// todo: byteswap this
|
||||||
|
#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL)
|
||||||
|
|
||||||
|
#define NCZ_SECTION_OFFSET (0x4000 + sizeof(ncz::Header))
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
u64 magic; // NCZ_SECTION_MAGIC
|
||||||
|
u64 total_sections;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BlockHeader {
|
||||||
|
u64 magic; // NCZ_BLOCK_MAGIC
|
||||||
|
u8 version;
|
||||||
|
u8 type;
|
||||||
|
u8 padding;
|
||||||
|
u8 block_size_exponent;
|
||||||
|
u32 total_blocks;
|
||||||
|
u64 decompressed_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Block {
|
||||||
|
u32 size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BlockInfo {
|
||||||
|
u64 offset; // compressed offset.
|
||||||
|
u64 size; // compressed size.
|
||||||
|
|
||||||
|
auto InRange(u64 off) const -> bool {
|
||||||
|
return off < offset + size && off >= offset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Section {
|
||||||
|
u64 offset;
|
||||||
|
u64 size;
|
||||||
|
u64 crypto_type;
|
||||||
|
u64 padding;
|
||||||
|
u8 key[0x10];
|
||||||
|
u8 counter[0x10];
|
||||||
|
|
||||||
|
auto InRange(u64 off) const -> bool {
|
||||||
|
return off < offset + size && off >= offset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::ncz
|
||||||
62
sphaira/include/yati/nx/npdm.hpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::npdm {
|
||||||
|
|
||||||
|
struct Meta {
|
||||||
|
u32 magic; // "META"
|
||||||
|
u32 signature_key_generation; // +9.0.0
|
||||||
|
u32 _0x8;
|
||||||
|
u8 flags;
|
||||||
|
u8 _0xD;
|
||||||
|
u8 main_thread_priority;
|
||||||
|
u8 main_thread_core_num;
|
||||||
|
u32 _0x10;
|
||||||
|
u32 sys_resource_size; // +3.0.0
|
||||||
|
u32 version;
|
||||||
|
u32 main_thread_stack_size;
|
||||||
|
char title_name[0x10];
|
||||||
|
char product_code[0x10];
|
||||||
|
u8 _0x40[0x30];
|
||||||
|
u32 aci0_offset;
|
||||||
|
u32 aci0_size;
|
||||||
|
u32 acid_offset;
|
||||||
|
u32 acid_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Acid {
|
||||||
|
u8 rsa_sig[0x100];
|
||||||
|
u8 rsa_pub[0x100];
|
||||||
|
u32 magic; // "ACID"
|
||||||
|
u32 size;
|
||||||
|
u8 version;
|
||||||
|
u8 _0x209[0x1];
|
||||||
|
u8 _0x20A[0x2];
|
||||||
|
u32 flags;
|
||||||
|
u64 program_id_min;
|
||||||
|
u64 program_id_max;
|
||||||
|
u32 fac_offset;
|
||||||
|
u32 fac_size;
|
||||||
|
u32 sac_offset;
|
||||||
|
u32 sac_size;
|
||||||
|
u32 kac_offset;
|
||||||
|
u32 kac_size;
|
||||||
|
u8 _0x238[0x8];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Aci0 {
|
||||||
|
u32 magic; // "ACI0"
|
||||||
|
u8 _0x4[0xC];
|
||||||
|
u64 program_id;
|
||||||
|
u8 _0x18[0x8];
|
||||||
|
u32 fac_offset;
|
||||||
|
u32 fac_size;
|
||||||
|
u32 sac_offset;
|
||||||
|
u32 sac_size;
|
||||||
|
u32 kac_offset;
|
||||||
|
u32 kac_size;
|
||||||
|
u8 _0x38[0x8];
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::npdm
|
||||||
22
sphaira/include/yati/nx/ns.hpp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <switch.h>
|
||||||
|
#include "ncm.hpp"
|
||||||
|
|
||||||
|
namespace sphaira::ns {
|
||||||
|
|
||||||
|
enum ApplicationRecordType {
|
||||||
|
// installed
|
||||||
|
ApplicationRecordType_Installed = 0x3,
|
||||||
|
// application is gamecard, but gamecard isn't insterted
|
||||||
|
ApplicationRecordType_GamecardMissing = 0x5,
|
||||||
|
// archived
|
||||||
|
ApplicationRecordType_Archived = 0xB,
|
||||||
|
};
|
||||||
|
|
||||||
|
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count);
|
||||||
|
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read);
|
||||||
|
Result DeleteApplicationRecord(Service* srv, u64 tid);
|
||||||
|
Result InvalidateApplicationControlCache(Service* srv, u64 tid);
|
||||||
|
|
||||||
|
} // namespace sphaira::ns
|
||||||
62
sphaira/include/yati/nx/nxdumptool_rsa.h
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* rsa.c
|
||||||
|
*
|
||||||
|
* Copyright (c) 2018-2019, SciresM.
|
||||||
|
* 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 __RSA_H__
|
||||||
|
#define __RSA_H__
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#define RSA2048_BYTES 0x100
|
||||||
|
#define RSA2048_BITS (RSA2048_BYTES * 8)
|
||||||
|
|
||||||
|
#define RSA2048_SIG_SIZE RSA2048_BYTES
|
||||||
|
#define RSA2048_PUBKEY_SIZE RSA2048_BYTES
|
||||||
|
|
||||||
|
/// Verifies a RSA-2048-PSS with SHA-256 signature.
|
||||||
|
/// Suitable for NCA and NPDM signatures.
|
||||||
|
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||||
|
bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
|
||||||
|
|
||||||
|
/// Verifies a RSA-2048-PKCS#1 v1.5 with SHA-256 signature.
|
||||||
|
/// Suitable for ticket and certificate chain signatures.
|
||||||
|
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||||
|
bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
|
||||||
|
|
||||||
|
/// Performs RSA-2048-OAEP decryption.
|
||||||
|
/// Suitable to decrypt the titlekey block from personalized tickets.
|
||||||
|
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||||
|
/// 'label' and 'label_size' arguments are optional -- if not needed, these may be set to NULL and 0, respectively.
|
||||||
|
bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \
|
||||||
|
size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* __RSA_H__ */
|
||||||
29
sphaira/include/yati/source/base.hpp
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
struct Base {
|
||||||
|
virtual ~Base() = default;
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void SignalCancel() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetOpenResult() const {
|
||||||
|
return m_open_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Result m_open_result{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
19
sphaira/include/yati/source/file.hpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
struct File final : Base {
|
||||||
|
File(FsFileSystem* fs, const fs::FsPath& path);
|
||||||
|
~File();
|
||||||
|
|
||||||
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FsFile m_file{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
20
sphaira/include/yati/source/stdio.hpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
struct Stdio final : Base {
|
||||||
|
Stdio(const fs::FsPath& path);
|
||||||
|
~Stdio();
|
||||||
|
|
||||||
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::FILE* m_file{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sphaira::yati::source
|
||||||
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
@@ -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
|
||||||
99
sphaira/include/yati/source/usb.hpp
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "base.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <new>
|
||||||
|
#include <switch.h>
|
||||||
|
|
||||||
|
namespace sphaira::yati::source {
|
||||||
|
|
||||||
|
struct Usb final : Base {
|
||||||
|
enum { USBModule = 523 };
|
||||||
|
|
||||||
|
enum : Result {
|
||||||
|
Result_BadMagic = MAKERESULT(USBModule, 0),
|
||||||
|
Result_BadVersion = MAKERESULT(USBModule, 1),
|
||||||
|
Result_BadCount = MAKERESULT(USBModule, 2),
|
||||||
|
Result_BadTransferSize = MAKERESULT(USBModule, 3),
|
||||||
|
Result_BadTotalSize = MAKERESULT(USBModule, 4),
|
||||||
|
Result_Cancelled = MAKERESULT(USBModule, 11),
|
||||||
|
};
|
||||||
|
|
||||||
|
Usb(u64 transfer_timeout);
|
||||||
|
~Usb();
|
||||||
|
|
||||||
|
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||||
|
Result Finished();
|
||||||
|
|
||||||
|
Result Init();
|
||||||
|
Result IsUsbConnected(u64 timeout);
|
||||||
|
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
|
||||||
|
void SetFileNameForTranfser(const std::string& name);
|
||||||
|
|
||||||
|
auto GetCancelEvent() {
|
||||||
|
return &m_uevent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalCancel() override {
|
||||||
|
ueventSignal(GetCancelEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
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:
|
||||||
|
enum UsbSessionEndpoint {
|
||||||
|
UsbSessionEndpoint_In = 0,
|
||||||
|
UsbSessionEndpoint_Out = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
Result SendCmdHeader(u32 cmdId, size_t dataSize);
|
||||||
|
Result SendFileRangeCmd(u64 offset, u64 size);
|
||||||
|
|
||||||
|
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
||||||
|
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout);
|
||||||
|
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 TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
|
||||||
|
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
|
||||||
|
|
||||||
|
private:
|
||||||
|
UsbDsInterface* m_interface{};
|
||||||
|
UsbDsEndpoint* m_endpoints[2]{};
|
||||||
|
u64 m_transfer_timeout{};
|
||||||
|
UEvent m_uevent{};
|
||||||
|
// std::vector<UEvent*> m_cancel_events{};
|
||||||
|
// 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
|
||||||
142
sphaira/include/yati/yati.hpp
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* Notes:
|
||||||
|
* - nca's that use title key encryption are decrypted using Tegra SE, whereas
|
||||||
|
* standard crypto uses software decryption.
|
||||||
|
* The latter is almost always (slightly) faster, and removed the need for es patch.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "source/base.hpp"
|
||||||
|
#include "container/base.hpp"
|
||||||
|
#include "nx/ncm.hpp"
|
||||||
|
#include "ui/progress_box.hpp"
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
namespace sphaira::yati {
|
||||||
|
|
||||||
|
enum { YatiModule = 521 };
|
||||||
|
|
||||||
|
enum : Result {
|
||||||
|
// unkown container for the source provided.
|
||||||
|
Result_ContainerNotFound = MAKERESULT(YatiModule, 10),
|
||||||
|
Result_Cancelled = MAKERESULT(YatiModule, 11),
|
||||||
|
|
||||||
|
// nca required by the cnmt but not found in collection.
|
||||||
|
Result_NcaNotFound = MAKERESULT(YatiModule, 30),
|
||||||
|
Result_InvalidNcaReadSize = MAKERESULT(YatiModule, 31),
|
||||||
|
Result_InvalidNcaSigKeyGen = MAKERESULT(YatiModule, 32),
|
||||||
|
Result_InvalidNcaMagic = MAKERESULT(YatiModule, 33),
|
||||||
|
Result_InvalidNcaSignature0 = MAKERESULT(YatiModule, 34),
|
||||||
|
Result_InvalidNcaSignature1 = MAKERESULT(YatiModule, 35),
|
||||||
|
// invalid sha256 over the entire nca.
|
||||||
|
Result_InvalidNcaSha256 = MAKERESULT(YatiModule, 36),
|
||||||
|
|
||||||
|
// section could not be found.
|
||||||
|
Result_NczSectionNotFound = MAKERESULT(YatiModule, 50),
|
||||||
|
// section count == 0.
|
||||||
|
Result_InvalidNczSectionCount = MAKERESULT(YatiModule, 51),
|
||||||
|
// block could not be found.
|
||||||
|
Result_NczBlockNotFound = MAKERESULT(YatiModule, 52),
|
||||||
|
// block version != 2.
|
||||||
|
Result_InvalidNczBlockVersion = MAKERESULT(YatiModule, 53),
|
||||||
|
// block type != 1.
|
||||||
|
Result_InvalidNczBlockType = MAKERESULT(YatiModule, 54),
|
||||||
|
// block count == 0.
|
||||||
|
Result_InvalidNczBlockTotal = MAKERESULT(YatiModule, 55),
|
||||||
|
// block size exponent < 14 || > 32.
|
||||||
|
Result_InvalidNczBlockSizeExponent = MAKERESULT(YatiModule, 56),
|
||||||
|
// zstd error while decompressing ncz.
|
||||||
|
Result_InvalidNczZstdError = MAKERESULT(YatiModule, 57),
|
||||||
|
|
||||||
|
// nca has rights_id but matching ticket wasn't found.
|
||||||
|
Result_TicketNotFound = MAKERESULT(YatiModule, 70),
|
||||||
|
// found ticket has missmatching rights_id from it's name.
|
||||||
|
Result_InvalidTicketBadRightsId = MAKERESULT(YatiModule, 71),
|
||||||
|
Result_InvalidTicketVersion = MAKERESULT(YatiModule, 72),
|
||||||
|
Result_InvalidTicketKeyType = MAKERESULT(YatiModule, 73),
|
||||||
|
Result_InvalidTicketKeyRevision = MAKERESULT(YatiModule, 74),
|
||||||
|
|
||||||
|
// cert not found for the ticket.
|
||||||
|
Result_CertNotFound = MAKERESULT(YatiModule, 90),
|
||||||
|
|
||||||
|
// unable to fetch header from ncm database.
|
||||||
|
Result_NcmDbCorruptHeader = MAKERESULT(YatiModule, 110),
|
||||||
|
// unable to total infos from ncm database.
|
||||||
|
Result_NcmDbCorruptInfos = MAKERESULT(YatiModule, 111),
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
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.
|
||||||
|
bool allow_downgrade{};
|
||||||
|
|
||||||
|
// ignores the install if already installed.
|
||||||
|
// checks that every nca is available.
|
||||||
|
bool skip_if_already_installed{};
|
||||||
|
|
||||||
|
// installs tickets only.
|
||||||
|
bool ticket_only{};
|
||||||
|
|
||||||
|
// flags to enable / disable install of specific types.
|
||||||
|
bool skip_base{};
|
||||||
|
bool skip_patch{};
|
||||||
|
bool skip_addon{};
|
||||||
|
bool skip_data_patch{};
|
||||||
|
bool skip_ticket{};
|
||||||
|
|
||||||
|
// enables the option to skip sha256 verification.
|
||||||
|
bool skip_nca_hash_verify{};
|
||||||
|
|
||||||
|
// enables the option to skip rsa nca fixed key verification.
|
||||||
|
bool skip_rsa_header_fixed_key_verify{};
|
||||||
|
|
||||||
|
// enables the option to skip rsa npdm fixed key verification.
|
||||||
|
bool skip_rsa_npdm_fixed_key_verify{};
|
||||||
|
|
||||||
|
// if set, it will ignore the distribution bit in the nca header.
|
||||||
|
bool ignore_distribution_bit{};
|
||||||
|
|
||||||
|
// converts titlekey to standard crypto, also known as "ticketless".
|
||||||
|
// this will not work with addon (dlc), so, addon tickets will be installed.
|
||||||
|
bool convert_to_standard_crypto{};
|
||||||
|
|
||||||
|
// encrypts the keak with master key 0, this allows the game to be launched on every fw.
|
||||||
|
// implicitly performs standard crypto.
|
||||||
|
bool lower_master_key{};
|
||||||
|
|
||||||
|
// sets the system_firmware field in the cnmt extended header.
|
||||||
|
// if mkey is higher than fw version, the game still won't launch
|
||||||
|
// as the fw won't have the key to decrypt keak.
|
||||||
|
bool lower_system_version{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// overridable options, set to avoid
|
||||||
|
struct ConfigOverride {
|
||||||
|
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, const ConfigOverride& override = {});
|
||||||
|
Result InstallFromStdioFile(ui::ProgressBox* pbox, const fs::FsPath& path, const ConfigOverride& override = {});
|
||||||
|
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
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
#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 "app.hpp"
|
#include "app.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
@@ -34,8 +39,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]{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,6 +75,11 @@ struct ThemeIdPair {
|
|||||||
ElementType type{ElementType::None};
|
ElementType type{ElementType::None};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FrameBufferSize {
|
||||||
|
Vec2 size;
|
||||||
|
Vec2 scale;
|
||||||
|
};
|
||||||
|
|
||||||
constexpr ThemeIdPair THEME_ENTRIES[] = {
|
constexpr ThemeIdPair THEME_ENTRIES[] = {
|
||||||
{ "background", ThemeEntryID_BACKGROUND },
|
{ "background", ThemeEntryID_BACKGROUND },
|
||||||
{ "grid", ThemeEntryID_GRID },
|
{ "grid", ThemeEntryID_GRID },
|
||||||
@@ -223,6 +258,26 @@ void appplet_hook_calback(AppletHookType type, void *param) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto GetFrameBufferSize() -> FrameBufferSize {
|
||||||
|
FrameBufferSize fb{};
|
||||||
|
|
||||||
|
switch (appletGetOperationMode()) {
|
||||||
|
case AppletOperationMode_Handheld:
|
||||||
|
fb.size.x = 1280;
|
||||||
|
fb.size.y = 720;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AppletOperationMode_Console:
|
||||||
|
fb.size.x = 1920;
|
||||||
|
fb.size.y = 1080;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
fb.scale.x = fb.size.x / SCREEN_WIDTH;
|
||||||
|
fb.scale.y = fb.size.y / SCREEN_HEIGHT;
|
||||||
|
return fb;
|
||||||
|
}
|
||||||
|
|
||||||
// this will try to decompress the icon and then re-convert it to jpg
|
// this will try to decompress the icon and then re-convert it to jpg
|
||||||
// in order to strip exif data.
|
// in order to strip exif data.
|
||||||
// this doesn't take long at all, but it's very overkill.
|
// this doesn't take long at all, but it's very overkill.
|
||||||
@@ -327,7 +382,7 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level
|
|||||||
if (!ini_browse(cb, &theme_data, meta.ini_path)) {
|
if (!ini_browse(cb, &theme_data, meta.ini_path)) {
|
||||||
log_write("failed to open ini: %s\n", meta.ini_path.s);
|
log_write("failed to open ini: %s\n", meta.ini_path.s);
|
||||||
} else {
|
} else {
|
||||||
log_write("opened ini: %s\n", meta.ini_path);
|
log_write("opened ini: %s\n", meta.ini_path.s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,31 +476,21 @@ void App::Loop() {
|
|||||||
}
|
}
|
||||||
} else if constexpr(std::is_same_v<T, curl::DownloadEventData>) {
|
} else if constexpr(std::is_same_v<T, curl::DownloadEventData>) {
|
||||||
log_write("[DownloadEventData] got event\n");
|
log_write("[DownloadEventData] got event\n");
|
||||||
arg.callback(arg.result);
|
if (arg.callback && !arg.stoken.stop_requested()) {
|
||||||
|
arg.callback(arg.result);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
static_assert(false, "non-exhaustive visitor!");
|
static_assert(false, "non-exhaustive visitor!");
|
||||||
}
|
}
|
||||||
}, event.value());
|
}, event.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 w{},h{};
|
const auto fb = GetFrameBufferSize();
|
||||||
switch (appletGetOperationMode()) {
|
if (fb.size.x != s_width || fb.size.y != s_height) {
|
||||||
case AppletOperationMode_Handheld:
|
s_width = fb.size.x;
|
||||||
w = 1280;
|
s_height = fb.size.y;
|
||||||
h = 720;
|
m_scale = fb.scale;
|
||||||
break;
|
this->destroyFramebufferResources();
|
||||||
|
|
||||||
case AppletOperationMode_Console:
|
|
||||||
w = 1920;
|
|
||||||
h = 1080;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (w != s_width || h != s_height) {
|
|
||||||
s_width = w;
|
|
||||||
s_height = h;
|
|
||||||
m_scale.x = (float)s_width / SCREEN_WIDTH;
|
|
||||||
m_scale.y = (float)s_height / SCREEN_HEIGHT;
|
|
||||||
this->createFramebufferResources();
|
this->createFramebufferResources();
|
||||||
renderer->UpdateViewSize(s_width, s_height);
|
renderer->UpdateViewSize(s_width, s_height);
|
||||||
}
|
}
|
||||||
@@ -529,7 +574,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,10 +610,6 @@ auto App::GetInstallPrompt() -> bool {
|
|||||||
return g_app->m_install_prompt.Get();
|
return g_app->m_install_prompt.Get();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto App::GetThemeShuffleEnable() -> bool {
|
|
||||||
return g_app->m_theme_shuffle.Get();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto App::GetThemeMusicEnable() -> bool {
|
auto App::GetThemeMusicEnable() -> bool {
|
||||||
return g_app->m_theme_music.Get();
|
return g_app->m_theme_music.Get();
|
||||||
}
|
}
|
||||||
@@ -589,6 +630,10 @@ auto App::GetTextScrollSpeed() -> long {
|
|||||||
return g_app->m_text_scroll_speed.Get();
|
return g_app->m_text_scroll_speed.Get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto App::Get12HourTimeEnable() -> bool {
|
||||||
|
return g_app->m_12hour_time.Get();
|
||||||
|
}
|
||||||
|
|
||||||
void App::SetNxlinkEnable(bool enable) {
|
void App::SetNxlinkEnable(bool enable) {
|
||||||
if (App::GetNxlinkEnable() != enable) {
|
if (App::GetNxlinkEnable() != enable) {
|
||||||
g_app->m_nxlink_enabled.Set(enable);
|
g_app->m_nxlink_enabled.Set(enable);
|
||||||
@@ -616,13 +661,11 @@ void App::SetReplaceHbmenuEnable(bool enable) {
|
|||||||
g_app->m_replace_hbmenu.Set(enable);
|
g_app->m_replace_hbmenu.Set(enable);
|
||||||
if (!enable) {
|
if (!enable) {
|
||||||
// check we have already replaced hbmenu with sphaira
|
// check we have already replaced hbmenu with sphaira
|
||||||
NacpStruct hbmenu_nacp;
|
NacpStruct hbmenu_nacp{};
|
||||||
if (R_FAILED(nro_get_nacp("/hbmenu.nro", hbmenu_nacp))) {
|
if (R_SUCCEEDED(nro_get_nacp("/hbmenu.nro", hbmenu_nacp))) {
|
||||||
return;
|
if (std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
if (std::strcmp(hbmenu_nacp.lang[0].name, "sphaira")) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ask user if they want to restore hbmenu
|
// ask user if they want to restore hbmenu
|
||||||
@@ -664,7 +707,7 @@ void App::SetReplaceHbmenuEnable(bool enable) {
|
|||||||
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
|
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
|
||||||
if (std::strcmp(sphaira_nacp.display_version, hbmenu_nacp.display_version) < 0) {
|
if (std::strcmp(sphaira_nacp.display_version, hbmenu_nacp.display_version) < 0) {
|
||||||
if (R_FAILED(rc = fs.copy_entire_file(sphaira_path, "/hbmenu.nro"))) {
|
if (R_FAILED(rc = fs.copy_entire_file(sphaira_path, "/hbmenu.nro"))) {
|
||||||
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
||||||
} else {
|
} else {
|
||||||
log_write("success with updating hbmenu!\n");
|
log_write("success with updating hbmenu!\n");
|
||||||
}
|
}
|
||||||
@@ -724,15 +767,15 @@ void App::SetInstallPrompt(bool enable) {
|
|||||||
g_app->m_install_prompt.Set(enable);
|
g_app->m_install_prompt.Set(enable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::SetThemeShuffleEnable(bool enable) {
|
|
||||||
g_app->m_theme_shuffle.Set(enable);
|
|
||||||
}
|
|
||||||
|
|
||||||
void App::SetThemeMusicEnable(bool enable) {
|
void App::SetThemeMusicEnable(bool enable) {
|
||||||
g_app->m_theme_music.Set(enable);
|
g_app->m_theme_music.Set(enable);
|
||||||
PlaySoundEffect(SoundEffect::SoundEffect_Music);
|
PlaySoundEffect(SoundEffect::SoundEffect_Music);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::Set12HourTimeEnable(bool enable) {
|
||||||
|
g_app->m_12hour_time.Set(enable);
|
||||||
|
}
|
||||||
|
|
||||||
void App::SetMtpEnable(bool enable) {
|
void App::SetMtpEnable(bool enable) {
|
||||||
if (App::GetMtpEnable() != enable) {
|
if (App::GetMtpEnable() != enable) {
|
||||||
g_app->m_mtp_enabled.Set(enable);
|
g_app->m_mtp_enabled.Set(enable);
|
||||||
@@ -870,23 +913,26 @@ void App::Poll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto gesture = gestures[i];
|
auto gesture = gestures[i];
|
||||||
if (gesture_count && gesture.type == HidGestureType_Swipe) {
|
if (gesture_count && gesture.type == HidGestureType_Touch) {
|
||||||
log_write("[SWIPE] got gesture type: %d direction: %d sampling_number: %d context_number: %d\n", gesture.type, gesture.direction, gesture.sampling_number, gesture.context_number);
|
log_write("[TOUCH] got gesture attr: %u direction: %u sampling_number: %zu context_number: %zu\n", gesture.attributes, gesture.direction, gesture.sampling_number, gesture.context_number);
|
||||||
|
}
|
||||||
|
else if (gesture_count && gesture.type == HidGestureType_Swipe) {
|
||||||
|
log_write("[SWIPE] got gesture direction: %u sampling_number: %zu context_number: %zu\n", gesture.direction, gesture.sampling_number, gesture.context_number);
|
||||||
}
|
}
|
||||||
else if (gesture_count && gesture.type == HidGestureType_Tap) {
|
else if (gesture_count && gesture.type == HidGestureType_Tap) {
|
||||||
log_write("[TAP] got gesture type: %d direction: %d sampling_number: %d context_number: %d\n", gesture.type, gesture.direction, gesture.sampling_number, gesture.context_number);
|
log_write("[TAP] got gesture direction: %u sampling_number: %zu context_number: %zu\n", gesture.direction, gesture.sampling_number, gesture.context_number);
|
||||||
}
|
}
|
||||||
else if (gesture_count && gesture.type == HidGestureType_Press) {
|
else if (gesture_count && gesture.type == HidGestureType_Press) {
|
||||||
log_write("[PRESS] got gesture type: %d direction: %d sampling_number: %d context_number: %d\n", gesture.type, gesture.direction, gesture.sampling_number, gesture.context_number);
|
log_write("[PRESS] got gesture direction: %u sampling_number: %zu context_number: %zu\n", gesture.direction, gesture.sampling_number, gesture.context_number);
|
||||||
}
|
}
|
||||||
else if (gesture_count && gesture.type == HidGestureType_Cancel) {
|
else if (gesture_count && gesture.type == HidGestureType_Cancel) {
|
||||||
log_write("[CANCEL] got gesture type: %d direction: %d sampling_number: %d context_number: %d\n", gesture.type, gesture.direction, gesture.sampling_number, gesture.context_number);
|
log_write("[CANCEL] got gesture direction: %u sampling_number: %zu context_number: %zu\n", gesture.direction, gesture.sampling_number, gesture.context_number);
|
||||||
}
|
}
|
||||||
else if (gesture_count && gesture.type == HidGestureType_Complete) {
|
else if (gesture_count && gesture.type == HidGestureType_Complete) {
|
||||||
log_write("[COMPLETE] got gesture type: %d direction: %d sampling_number: %d context_number: %d\n", gesture.type, gesture.direction, gesture.sampling_number, gesture.context_number);
|
log_write("[COMPLETE] got gesture direction: %u sampling_number: %zu context_number: %zu\n", gesture.direction, gesture.sampling_number, gesture.context_number);
|
||||||
}
|
}
|
||||||
else if (gesture_count && gesture.type == HidGestureType_Pan) {
|
else if (gesture_count && gesture.type == HidGestureType_Pan) {
|
||||||
log_write("[PAN] got gesture sampling_number: %d context_number: %d x: %d y: %d dx: %d dy: %d vx: %.2f vy: %.2f count: %d\n", gesture.sampling_number, gesture.context_number, gesture.x, gesture.y, gesture.delta_x, gesture.delta_y, gesture.velocity_x, gesture.velocity_y, gesture.point_count);
|
log_write("[PAN] got gesture direction: %u sampling_number: %zu context_number: %zu x: %d y: %d dx: %d dy: %d vx: %.2f vy: %.2f count: %d\n", gesture.direction, gesture.sampling_number, gesture.context_number, gesture.x, gesture.y, gesture.delta_x, gesture.delta_y, gesture.velocity_x, gesture.velocity_y, gesture.point_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -992,6 +1038,10 @@ void App::Draw() {
|
|||||||
this->queue.presentImage(this->swapchain, slot);
|
this->queue.presentImage(this->swapchain, slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto App::GetApp() -> App* {
|
||||||
|
return g_app;
|
||||||
|
}
|
||||||
|
|
||||||
auto App::GetVg() -> NVGcontext* {
|
auto App::GetVg() -> NVGcontext* {
|
||||||
return g_app->vg;
|
return g_app->vg;
|
||||||
}
|
}
|
||||||
@@ -1084,7 +1134,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) {
|
||||||
@@ -1114,10 +1163,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1205,6 +1256,12 @@ App::App(const char* argv0) {
|
|||||||
|
|
||||||
curl::Init();
|
curl::Init();
|
||||||
|
|
||||||
|
// get current size of the framebuffer
|
||||||
|
const auto fb = GetFrameBufferSize();
|
||||||
|
s_width = fb.size.x;
|
||||||
|
s_height = fb.size.y;
|
||||||
|
m_scale = fb.scale;
|
||||||
|
|
||||||
// Create the deko3d device
|
// Create the deko3d device
|
||||||
this->device = dk::DeviceMaker{}
|
this->device = dk::DeviceMaker{}
|
||||||
.setCbDebug(deko3d_error_cb)
|
.setCbDebug(deko3d_error_cb)
|
||||||
@@ -1228,7 +1285,7 @@ App::App(const char* argv0) {
|
|||||||
// Create the framebuffer resources
|
// Create the framebuffer resources
|
||||||
this->createFramebufferResources();
|
this->createFramebufferResources();
|
||||||
|
|
||||||
this->renderer.emplace(SCREEN_WIDTH, SCREEN_HEIGHT, this->device, this->queue, *this->pool_images, *this->pool_code, *this->pool_data);
|
this->renderer.emplace(s_width, s_height, this->device, this->queue, *this->pool_images, *this->pool_code, *this->pool_data);
|
||||||
this->vg = nvgCreateDk(&*this->renderer, NVG_ANTIALIAS | NVG_STENCIL_STROKES);
|
this->vg = nvgCreateDk(&*this->renderer, NVG_ANTIALIAS | NVG_STENCIL_STROKES);
|
||||||
|
|
||||||
i18n::init(GetLanguage());
|
i18n::init(GetLanguage());
|
||||||
@@ -1260,22 +1317,36 @@ App::App(const char* argv0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disable audio in applet mode with a suspended application due to audren fatal.
|
||||||
|
// see: https://github.com/ITotalJustice/sphaira/issues/92
|
||||||
|
if (IsAppletWithSuspendedApp()) {
|
||||||
|
App::Notify("Audio disabled due to suspended game"_i18n);
|
||||||
|
} else {
|
||||||
|
plsrPlayerInit();
|
||||||
|
}
|
||||||
|
|
||||||
if (R_SUCCEEDED(romfsMountDataStorageFromProgram(0x0100000000001000, "qlaunch"))) {
|
if (R_SUCCEEDED(romfsMountDataStorageFromProgram(0x0100000000001000, "qlaunch"))) {
|
||||||
ON_SCOPE_EXIT(romfsUnmount("qlaunch"));
|
ON_SCOPE_EXIT(romfsUnmount("qlaunch"));
|
||||||
plsrPlayerInit();
|
PLSR_BFSAR qlaunch_bfsar;
|
||||||
plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &m_qlaunch_bfsar);
|
if (R_SUCCEEDED(plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &qlaunch_bfsar))) {
|
||||||
ON_SCOPE_EXIT(plsrBFSARClose(&m_qlaunch_bfsar));
|
ON_SCOPE_EXIT(plsrBFSARClose(&qlaunch_bfsar));
|
||||||
|
|
||||||
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeGameIconFocus", &m_sound_ids[SoundEffect_Focus]);
|
const auto load_sound = [&](const char* name, u32 id) {
|
||||||
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeGameIconScroll", &m_sound_ids[SoundEffect_Scroll]);
|
if (R_FAILED(plsrPlayerLoadSoundByName(&qlaunch_bfsar, name, &m_sound_ids[id]))) {
|
||||||
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeGameIconLimit", &m_sound_ids[SoundEffect_Limit]);
|
log_write("[PLSR] failed to load sound effect: %s\n", name);
|
||||||
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeStartupMenu_game", &m_sound_ids[SoundEffect_Startup]);
|
}
|
||||||
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeGameIconAdd", &m_sound_ids[SoundEffect_Install]);
|
};
|
||||||
plsrPlayerLoadSoundByName(&m_qlaunch_bfsar, "SeInsertError", &m_sound_ids[SoundEffect_Error]);
|
|
||||||
|
|
||||||
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Limit], 2.0f);
|
load_sound("SeGameIconFocus", SoundEffect_Focus);
|
||||||
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Focus], 0.5f);
|
load_sound("SeGameIconScroll", SoundEffect_Scroll);
|
||||||
PlaySoundEffect(SoundEffect_Startup);
|
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_Focus], 0.5f);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log_write("failed to mount romfs 0x0100000000001000\n");
|
log_write("failed to mount romfs 0x0100000000001000\n");
|
||||||
}
|
}
|
||||||
@@ -1284,11 +1355,7 @@ App::App(const char* argv0) {
|
|||||||
|
|
||||||
fs::FsPath theme_path{};
|
fs::FsPath theme_path{};
|
||||||
constexpr fs::FsPath default_theme_path{"romfs:/themes/abyss_theme.ini"};
|
constexpr fs::FsPath default_theme_path{"romfs:/themes/abyss_theme.ini"};
|
||||||
if (App::GetThemeShuffleEnable() && m_theme_meta_entries.size()) {
|
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
|
||||||
theme_path = m_theme_meta_entries[randomGet64() % m_theme_meta_entries.size()].ini_path;
|
|
||||||
} else {
|
|
||||||
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
// try and load previous theme, default to previous version otherwise.
|
// try and load previous theme, default to previous version otherwise.
|
||||||
ThemeMeta theme_meta;
|
ThemeMeta theme_meta;
|
||||||
@@ -1333,7 +1400,7 @@ App::App(const char* argv0) {
|
|||||||
log_write("launching from sphaira created forwarder\n");
|
log_write("launching from sphaira created forwarder\n");
|
||||||
m_is_launched_via_sphaira_forwader = true;
|
m_is_launched_via_sphaira_forwader = true;
|
||||||
} else {
|
} else {
|
||||||
log_write("launching from unknown forwader: %.*s size: %zu\n", loader_info_size, envGetLoaderInfo(), loader_info_size);
|
log_write("launching from unknown forwader: %.*s size: %zu\n", (int)loader_info_size, envGetLoaderInfo(), loader_info_size);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log_write("not launching from forwarder\n");
|
log_write("not launching from forwarder\n");
|
||||||
@@ -1343,15 +1410,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());
|
||||||
@@ -1361,8 +1419,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");
|
||||||
@@ -1381,6 +1464,215 @@ 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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [](bool& enable){
|
||||||
|
App::Set12HourTimeEnable(enable);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
|
||||||
|
if (e.name == g_app->m_right_side_menu.Get()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.IsInstall() && !App::GetInstallEnable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryCallback>(i18n::get(e.title), [e](){
|
||||||
|
App::Push(e.func());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
std::vector<const char*> menu_names;
|
||||||
|
for (auto& e : ui::menu::main::GetMiscMenuEntries()) {
|
||||||
|
if (!e.IsShortcut()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.IsInstall() && !App::GetInstallEnable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_names.emplace_back(e.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::SidebarEntryArray::Items right_side_menu_items;
|
||||||
|
for (auto& str : menu_names) {
|
||||||
|
right_side_menu_items.push_back(i18n::get(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto it = std::find(menu_names.cbegin(), menu_names.cend(), g_app->m_right_side_menu.Get());
|
||||||
|
if (it == menu_names.cend()) {
|
||||||
|
g_app->m_right_side_menu.Set(menu_names[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [](bool& enable){
|
||||||
|
App::SetLogEnable(enable);
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){
|
||||||
|
App::SetReplaceHbmenuEnable(enable);
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){
|
||||||
|
App::SetTextScrollSpeed(index_out);
|
||||||
|
}, App::GetTextScrollSpeed()));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<ui::SidebarEntryArray>("Set right-side menu"_i18n, right_side_menu_items, [menu_names](s64& index_out){
|
||||||
|
const auto e = menu_names[index_out];
|
||||||
|
if (g_app->m_right_side_menu.Get() != e) {
|
||||||
|
g_app->m_right_side_menu.Set(e);
|
||||||
|
App::Push(std::make_shared<ui::OptionBox>(
|
||||||
|
"Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){
|
||||||
|
App::ExitRestart();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}, i18n::get(g_app->m_right_side_menu.Get())));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
App::~App() {
|
App::~App() {
|
||||||
log_write("starting to exit\n");
|
log_write("starting to exit\n");
|
||||||
|
|
||||||
@@ -1407,11 +1699,8 @@ App::~App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the archive
|
|
||||||
plsrBFSARClose(&m_qlaunch_bfsar);
|
|
||||||
|
|
||||||
// De-initialize our player
|
// De-initialize our player
|
||||||
plsrPlayerExit();
|
plsrPlayerExit();
|
||||||
|
|
||||||
this->destroyFramebufferResources();
|
this->destroyFramebufferResources();
|
||||||
nvgDeleteDk(this->vg);
|
nvgDeleteDk(this->vg);
|
||||||
@@ -1433,7 +1722,7 @@ App::~App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath()))) {
|
if (R_FAILED(rc = fs.copy_entire_file("/hbmenu.nro", GetExePath()))) {
|
||||||
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath(), rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", GetExePath().s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
||||||
} else {
|
} else {
|
||||||
log_write("success with copying over root file!\n");
|
log_write("success with copying over root file!\n");
|
||||||
}
|
}
|
||||||
@@ -1458,7 +1747,7 @@ App::~App() {
|
|||||||
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
|
if (R_SUCCEEDED(rc) && !std::strcmp(sphaira_nacp.lang[0].name, "sphaira")) {
|
||||||
if (std::strcmp(hbmenu_nacp.display_version, sphaira_nacp.display_version) < 0) {
|
if (std::strcmp(hbmenu_nacp.display_version, sphaira_nacp.display_version) < 0) {
|
||||||
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) {
|
if (R_FAILED(rc = fs.copy_entire_file(GetExePath(), sphaira_path))) {
|
||||||
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path, rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
log_write("failed to copy entire file: %s 0x%X module: %u desc: %u\n", sphaira_path.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
||||||
} else {
|
} else {
|
||||||
log_write("success with updating hbmenu!\n");
|
log_write("success with updating hbmenu!\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ namespace {
|
|||||||
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
|
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
|
||||||
} \
|
} \
|
||||||
|
|
||||||
#define USE_THREAD_QUEUE 1
|
|
||||||
constexpr auto API_AGENT = "ITotalJustice";
|
constexpr auto API_AGENT = "ITotalJustice";
|
||||||
constexpr u64 CHUNK_SIZE = 1024*1024;
|
constexpr u64 CHUNK_SIZE = 1024*1024;
|
||||||
constexpr auto MAX_THREADS = 4;
|
constexpr auto MAX_THREADS = 4;
|
||||||
@@ -196,6 +195,8 @@ private:
|
|||||||
} else {
|
} else {
|
||||||
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
|
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
|
||||||
if (value.empty()) {
|
if (value.empty()) {
|
||||||
|
// workaround for appstore accepting etags but not returning them.
|
||||||
|
yyjson_mut_obj_remove_str(hash_key, tag);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
auto key = yyjson_mut_obj_get(hash_key, tag);
|
auto key = yyjson_mut_obj_get(hash_key, tag);
|
||||||
@@ -302,18 +303,19 @@ struct ThreadQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto Add(const Api& api) -> bool {
|
auto Add(const Api& api) -> bool {
|
||||||
|
if (api.GetUrl().empty() || api.GetPath().empty() || !api.GetOnComplete()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
mutexLock(&m_mutex);
|
mutexLock(&m_mutex);
|
||||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||||
|
|
||||||
ThreadQueueEntry entry{};
|
switch (api.GetPriority()) {
|
||||||
entry.api = api;
|
|
||||||
|
|
||||||
switch (api.m_prio) {
|
|
||||||
case Priority::Normal:
|
case Priority::Normal:
|
||||||
m_entries.emplace_back(entry);
|
m_entries.emplace_back(api);
|
||||||
break;
|
break;
|
||||||
case Priority::High:
|
case Priority::High:
|
||||||
m_entries.emplace_front(entry);
|
m_entries.emplace_front(api);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,13 +352,13 @@ auto ProgressCallbackFunc1(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -> size_t {
|
auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) -> size_t {
|
||||||
if (!g_running) {
|
auto api = static_cast<Api*>(clientp);
|
||||||
|
if (!g_running || api->GetToken().stop_requested()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow);
|
// log_write("pcall called %u %u %u %u\n", dltotal, dlnow, ultotal, ulnow);
|
||||||
auto callback = *static_cast<OnProgress*>(clientp);
|
if (!api->GetOnProgress()(dltotal, dlnow, ultotal, ulnow)) {
|
||||||
if (!callback(dltotal, dlnow, ultotal, ulnow)) {
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,12 +445,17 @@ auto header_callback(char* b, size_t size, size_t nitems, void* userdata) -> siz
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||||
|
// check if stop has been requested before starting download
|
||||||
|
if (e.GetToken().stop_requested()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
fs::FsPath tmp_buf;
|
fs::FsPath tmp_buf;
|
||||||
const bool has_file = !e.m_path.empty() && e.m_path != "";
|
const bool has_file = !e.GetPath().empty() && e.GetPath() != "";
|
||||||
const bool has_post = !e.m_fields.m_str.empty() && e.m_fields.m_str != "";
|
const bool has_post = !e.GetFields().empty() && e.GetFields() != "";
|
||||||
|
|
||||||
DataStruct chunk;
|
DataStruct chunk;
|
||||||
Header header_in = e.m_header;
|
Header header_in = e.GetHeader();
|
||||||
Header header_out;
|
Header header_out;
|
||||||
fs::FsNativeSd fs;
|
fs::FsNativeSd fs;
|
||||||
|
|
||||||
@@ -457,17 +464,18 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
|||||||
fs.CreateDirectoryRecursivelyWithPath(tmp_buf);
|
fs.CreateDirectoryRecursivelyWithPath(tmp_buf);
|
||||||
|
|
||||||
if (auto rc = fs.CreateFile(tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
if (auto rc = fs.CreateFile(tmp_buf, 0, 0); R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create file: %s\n", tmp_buf);
|
log_write("failed to create file: %s\n", tmp_buf.s);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(fs.OpenFile(tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
|
if (R_FAILED(fs.OpenFile(tmp_buf, FsOpenMode_Write|FsOpenMode_Append, &chunk.f))) {
|
||||||
log_write("failed to open file: %s\n", tmp_buf);
|
log_write("failed to open file: %s\n", tmp_buf.s);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.m_flags.m_flags & Flag_Cache) {
|
// only add etag if the dst file still exists.
|
||||||
g_cache.get(e.m_path, header_in);
|
if ((e.GetFlags() & Flag_Cache) && fs::FileExists(&fs.m_fs, e.GetPath())) {
|
||||||
|
g_cache.get(e.GetPath(), header_in);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +483,7 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
|||||||
chunk.data.reserve(CHUNK_SIZE);
|
chunk.data.reserve(CHUNK_SIZE);
|
||||||
|
|
||||||
curl_easy_reset(curl);
|
curl_easy_reset(curl);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.m_url.m_str.c_str());
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, e.GetUrl().c_str());
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, "TotalJustice");
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||||
@@ -485,10 +493,12 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
|||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
|
||||||
|
// enable all forms of compression supported by libcurl.
|
||||||
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||||
|
|
||||||
if (has_post) {
|
if (has_post) {
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.m_fields.m_str.c_str());
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_POSTFIELDS, e.GetFields().c_str());
|
||||||
log_write("setting post field: %s\n", e.m_fields.m_str.c_str());
|
log_write("setting post field: %s\n", e.GetFields().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
struct curl_slist* list = NULL;
|
struct curl_slist* list = NULL;
|
||||||
@@ -517,8 +527,8 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// progress calls.
|
// progress calls.
|
||||||
if (e.m_on_progress) {
|
if (e.GetOnProgress()) {
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e.m_on_progress);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
|
||||||
} else {
|
} else {
|
||||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
|
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
|
||||||
@@ -546,16 +556,25 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
|||||||
|
|
||||||
if (res == CURLE_OK) {
|
if (res == CURLE_OK) {
|
||||||
if (http_code == 304) {
|
if (http_code == 304) {
|
||||||
log_write("cached download: %s\n", e.m_url.m_str.c_str());
|
log_write("cached download: %s\n", e.GetUrl().c_str());
|
||||||
} else {
|
} else {
|
||||||
log_write("un-cached download: %s code: %u\n", e.m_url.m_str.c_str(), http_code);
|
log_write("un-cached download: %s code: %lu\n", e.GetUrl().c_str(), http_code);
|
||||||
if (e.m_flags.m_flags & Flag_Cache) {
|
if (e.GetFlags() & Flag_Cache) {
|
||||||
g_cache.set(e.m_path, header_out);
|
g_cache.set(e.GetPath(), header_out);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.DeleteFile(e.m_path);
|
// enable to log received headers.
|
||||||
fs.CreateDirectoryRecursivelyWithPath(e.m_path);
|
#if 0
|
||||||
if (R_FAILED(fs.RenameFile(tmp_buf, e.m_path))) {
|
log_write("\n\nLOGGING HEADER\n");
|
||||||
|
for (auto [a, b] : header_out.m_map) {
|
||||||
|
log_write("\t%s: %s\n", a.c_str(), b.c_str());
|
||||||
|
}
|
||||||
|
log_write("\n\n");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
fs.DeleteFile(e.GetPath());
|
||||||
|
fs.CreateDirectoryRecursivelyWithPath(e.GetPath());
|
||||||
|
if (R_FAILED(fs.RenameFile(tmp_buf, e.GetPath()))) {
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,8 +587,8 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log_write("Downloaded %s %s\n", e.m_url.m_str.c_str(), curl_easy_strerror(res));
|
log_write("Downloaded %s %s\n", e.GetUrl().c_str(), curl_easy_strerror(res));
|
||||||
return {success, http_code, header_out, chunk.data, e.m_path};
|
return {success, http_code, header_out, chunk.data, e.GetPath()};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DownloadInternal(const Api& e) -> ApiResult {
|
auto DownloadInternal(const Api& e) -> ApiResult {
|
||||||
@@ -596,23 +615,18 @@ void ThreadEntry::ThreadFunc(void* p) {
|
|||||||
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
auto rc = waitSingle(waiterForUEvent(&data->m_uevent), UINT64_MAX);
|
||||||
// log_write("woke up\n");
|
// log_write("woke up\n");
|
||||||
if (!g_running) {
|
if (!g_running) {
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc)) {
|
if (R_FAILED(rc)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 1
|
|
||||||
const auto result = DownloadInternal(data->m_curl, data->m_api);
|
const auto result = DownloadInternal(data->m_curl, data->m_api);
|
||||||
if (g_running) {
|
if (g_running && data->m_api.GetOnComplete() && !data->m_api.GetToken().stop_requested()) {
|
||||||
const DownloadEventData event_data{data->m_api.m_on_complete, result};
|
const DownloadEventData event_data{data->m_api.GetOnComplete(), result, data->m_api.GetToken()};
|
||||||
evman::push(std::move(event_data), false);
|
evman::push(std::move(event_data), false);
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
// mutexLock(&data->m_mutex);
|
|
||||||
// ON_SCOPE_EXIT(mutexUnlock(&data->m_mutex));
|
|
||||||
|
|
||||||
data->m_in_progress = false;
|
data->m_in_progress = false;
|
||||||
// notify the queue that there's a space free
|
// notify the queue that there's a space free
|
||||||
@@ -736,53 +750,35 @@ void Exit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto ToMemory(const Api& e) -> ApiResult {
|
auto ToMemory(const Api& e) -> ApiResult {
|
||||||
if (!e.m_path.empty()) {
|
if (!e.GetPath().empty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return DownloadInternal(e);
|
return DownloadInternal(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ToFile(const Api& e) -> ApiResult {
|
auto ToFile(const Api& e) -> ApiResult {
|
||||||
if (e.m_path.empty()) {
|
if (e.GetPath().empty()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return DownloadInternal(e);
|
return DownloadInternal(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ToMemoryAsync(const Api& api) -> bool {
|
auto ToMemoryAsync(const Api& api) -> bool {
|
||||||
#if USE_THREAD_QUEUE
|
|
||||||
return g_thread_queue.Add(api);
|
return g_thread_queue.Add(api);
|
||||||
#else
|
|
||||||
// mutexLock(&g_thread_queue.m_mutex);
|
|
||||||
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
|
|
||||||
|
|
||||||
for (auto& entry : g_threads) {
|
|
||||||
if (!entry.InProgress()) {
|
|
||||||
return entry.Setup(callback, url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log_write("failed to start download, no avaliable threads\n");
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ToFileAsync(const Api& e) -> bool {
|
auto ToFileAsync(const Api& e) -> bool {
|
||||||
#if USE_THREAD_QUEUE
|
|
||||||
return g_thread_queue.Add(e);
|
return g_thread_queue.Add(e);
|
||||||
#else
|
}
|
||||||
// mutexLock(&g_thread_queue.m_mutex);
|
|
||||||
// ON_SCOPE_EXIT(mutexUnlock(&g_thread_queue.m_mutex));
|
|
||||||
|
|
||||||
for (auto& entry : g_threads) {
|
auto EscapeString(const std::string& str) -> std::string {
|
||||||
if (!entry.InProgress()) {
|
std::string result;
|
||||||
return entry.Setup(callback, url, out);
|
const auto s = curl_escape(str.data(), str.length());
|
||||||
}
|
if (s) {
|
||||||
|
result = s;
|
||||||
|
curl_free(s);
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
log_write("failed to start download, no avaliable threads\n");
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sphaira::curl
|
} // namespace sphaira::curl
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ Result CreateDirectoryRecursively(FsFileSystem* fs, const FsPath& _path, bool ig
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create folder: %s\n", path);
|
log_write("failed to create folder: %s\n", path.s);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ Result CreateDirectoryRecursivelyWithPath(FsFileSystem* fs, const FsPath& _path,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create folder recursively: %s\n", path);
|
log_write("failed to create folder recursively: %s\n", path.s);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" {
|
||||||
@@ -132,9 +418,9 @@ void log_file_write(const char* msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void log_file_fwrite(const char* fmt, ...) {
|
void log_file_fwrite(const char* fmt, ...) {
|
||||||
std::va_list v{};
|
va_list v{};
|
||||||
va_start(v, fmt);
|
va_start(v, fmt);
|
||||||
log_write_arg(fmt, v);
|
log_write_arg(fmt, &v);
|
||||||
va_end(v);
|
va_end(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ yyjson_doc* json;
|
|||||||
yyjson_val* root;
|
yyjson_val* root;
|
||||||
std::unordered_map<std::string, std::string> g_tr_cache;
|
std::unordered_map<std::string, std::string> g_tr_cache;
|
||||||
|
|
||||||
std::string get_internal(const char* str, size_t len) {
|
std::string get_internal(std::string_view str) {
|
||||||
const std::string kkey = {str, len};
|
const std::string kkey = {str.data(), str.length()};
|
||||||
|
|
||||||
if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) {
|
if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) {
|
||||||
return it->second;
|
return it->second;
|
||||||
@@ -28,7 +28,7 @@ std::string get_internal(const char* str, size_t len) {
|
|||||||
return kkey;
|
return kkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto key = yyjson_obj_getn(root, str, len);
|
auto key = yyjson_obj_getn(root, str.data(), str.length());
|
||||||
if (!key) {
|
if (!key) {
|
||||||
log_write("\tfailed to find key: [%s]\n", kkey.c_str());
|
log_write("\tfailed to find key: [%s]\n", kkey.c_str());
|
||||||
return kkey;
|
return kkey;
|
||||||
@@ -92,6 +92,7 @@ bool init(long index) {
|
|||||||
case SetLanguage_PT: lang_name = "pt"; break;
|
case SetLanguage_PT: lang_name = "pt"; break;
|
||||||
case SetLanguage_RU: lang_name = "ru"; break;
|
case SetLanguage_RU: lang_name = "ru"; break;
|
||||||
case SetLanguage_ZHTW: lang_name = "zh"; break;
|
case SetLanguage_ZHTW: lang_name = "zh"; break;
|
||||||
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json";
|
const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json";
|
||||||
@@ -133,8 +134,8 @@ void exit() {
|
|||||||
g_i18n_data.clear();
|
g_i18n_data.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string get(const char* str) {
|
std::string get(std::string_view str) {
|
||||||
return get_internal(str, std::strlen(str));
|
return get_internal(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sphaira::i18n
|
} // namespace sphaira::i18n
|
||||||
@@ -142,7 +143,7 @@ std::string get(const char* str) {
|
|||||||
namespace literals {
|
namespace literals {
|
||||||
|
|
||||||
std::string operator"" _i18n(const char* str, size_t len) {
|
std::string operator"" _i18n(const char* str, size_t len) {
|
||||||
return sphaira::i18n::get_internal(str, len);
|
return sphaira::i18n::get_internal({str, len});
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace literals
|
} // namespace literals
|
||||||
|
|||||||
@@ -4,14 +4,19 @@
|
|||||||
#pragma GCC diagnostic push
|
#pragma GCC diagnostic push
|
||||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||||
#pragma GCC diagnostic ignored "-Warray-bounds="
|
#pragma GCC diagnostic ignored "-Warray-bounds="
|
||||||
#include "nanovg/stb_image.h"
|
#pragma GCC diagnostic ignored "-Wcast-qual"
|
||||||
|
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include <stb_image.h>
|
||||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||||
#define STB_IMAGE_WRITE_STATIC
|
#define STB_IMAGE_WRITE_STATIC
|
||||||
#define STBI_WRITE_NO_STDIO
|
#define STBI_WRITE_NO_STDIO
|
||||||
#include "stb_image_write.h"
|
#include <stb_image_write.h>
|
||||||
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
#define STB_IMAGE_RESIZE_IMPLEMENTATION
|
||||||
#define STB_IMAGE_RESIZE_STATIC
|
#define STB_IMAGE_RESIZE_STATIC
|
||||||
#include "stb_image_resize2.h"
|
#include <stb_image_resize2.h>
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
|
|
||||||
|
|||||||
@@ -14,18 +14,20 @@ std::FILE* file{};
|
|||||||
int nxlink_socket{};
|
int nxlink_socket{};
|
||||||
std::mutex mutex{};
|
std::mutex mutex{};
|
||||||
|
|
||||||
void log_write_arg_internal(const char* s, std::va_list& v) {
|
void log_write_arg_internal(const char* s, std::va_list* v) {
|
||||||
if (file) {
|
if (file) {
|
||||||
std::vfprintf(file, s, v);
|
std::vfprintf(file, s, *v);
|
||||||
std::fflush(file);
|
std::fflush(file);
|
||||||
}
|
}
|
||||||
if (nxlink_socket) {
|
if (nxlink_socket) {
|
||||||
std::vprintf(s, v);
|
std::vprintf(s, *v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
auto log_file_init() -> bool {
|
auto log_file_init() -> bool {
|
||||||
std::scoped_lock lock{mutex};
|
std::scoped_lock lock{mutex};
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -70,11 +72,11 @@ void log_write(const char* s, ...) {
|
|||||||
|
|
||||||
std::va_list v{};
|
std::va_list v{};
|
||||||
va_start(v, s);
|
va_start(v, s);
|
||||||
log_write_arg_internal(s, v);
|
log_write_arg_internal(s, &v);
|
||||||
va_end(v);
|
va_end(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
void log_write_arg(const char* s, std::va_list& v) {
|
void log_write_arg(const char* s, va_list* v) {
|
||||||
std::scoped_lock lock{mutex};
|
std::scoped_lock lock{mutex};
|
||||||
if (!file && !nxlink_socket) {
|
if (!file && !nxlink_socket) {
|
||||||
return;
|
return;
|
||||||
@@ -83,4 +85,6 @@ void log_write_arg(const char* s, std::va_list& v) {
|
|||||||
log_write_arg_internal(s, v);
|
log_write_arg_internal(s, v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
|||||||
if (e.type == FsDirEntryType_Dir) {
|
if (e.type == FsDirEntryType_Dir) {
|
||||||
// assert(!root && "dir should only be scanned on non-root!");
|
// assert(!root && "dir should only be scanned on non-root!");
|
||||||
fs::FsPath fullpath;
|
fs::FsPath fullpath;
|
||||||
std::snprintf(fullpath, sizeof(fullpath), "%s/%s/%s.nro", path, e.name, e.name);
|
std::snprintf(fullpath, sizeof(fullpath), "%s/%s/%s.nro", path.s, e.name, e.name);
|
||||||
|
|
||||||
// fast path for detecting an nro in a folder
|
// fast path for detecting an nro in a folder
|
||||||
NroEntry entry;
|
NroEntry entry;
|
||||||
@@ -133,12 +133,12 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
|||||||
nros.emplace_back(entry);
|
nros.emplace_back(entry);
|
||||||
} else {
|
} else {
|
||||||
// slow path...
|
// slow path...
|
||||||
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path, e.name);
|
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path.s, e.name);
|
||||||
nro_scan_internal(fullpath, nros, hide_sphaira, nested, scan_all_dir, false);
|
nro_scan_internal(fullpath, nros, hide_sphaira, nested, scan_all_dir, false);
|
||||||
}
|
}
|
||||||
} else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) {
|
} else if (e.type == FsDirEntryType_File && std::string_view{e.name}.ends_with(".nro")) {
|
||||||
fs::FsPath fullpath;
|
fs::FsPath fullpath;
|
||||||
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path, e.name);
|
std::snprintf(fullpath, sizeof(fullpath), "%s/%s", path.s, e.name);
|
||||||
|
|
||||||
NroEntry entry;
|
NroEntry entry;
|
||||||
if (R_SUCCEEDED(nro_parse_internal(fs, fullpath, entry))) {
|
if (R_SUCCEEDED(nro_parse_internal(fs, fullpath, entry))) {
|
||||||
@@ -148,7 +148,7 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
|||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log_write("error when trying to parse %s\n", fullpath);
|
log_write("error when trying to parse %s\n", fullpath.s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ void loop(void* args) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_write("got name: %s\n", name);
|
log_write("got name: %s\n", name.s);
|
||||||
|
|
||||||
u32 filesize{};
|
u32 filesize{};
|
||||||
if (!recvall(connfd, &filesize, sizeof(filesize))) {
|
if (!recvall(connfd, &filesize, sizeof(filesize))) {
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
|
#include "yati/nx/nca.hpp"
|
||||||
|
#include "yati/nx/ncm.hpp"
|
||||||
|
#include "yati/nx/npdm.hpp"
|
||||||
|
#include "yati/nx/ns.hpp"
|
||||||
|
#include "yati/nx/es.hpp"
|
||||||
|
#include "yati/nx/keys.hpp"
|
||||||
|
#include "yati/nx/crypto.hpp"
|
||||||
|
|
||||||
#include "owo.hpp"
|
#include "owo.hpp"
|
||||||
#include "defines.hpp"
|
#include "defines.hpp"
|
||||||
#include "app.hpp"
|
#include "app.hpp"
|
||||||
@@ -23,52 +31,9 @@ constexpr u32 PFS0_EXEFS_HASH_BLOCK_SIZE = 0x10000;
|
|||||||
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
|
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
|
||||||
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
|
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
|
||||||
constexpr u32 PFS0_PADDING_SIZE = 0x200;
|
constexpr u32 PFS0_PADDING_SIZE = 0x200;
|
||||||
constexpr u32 NCA_SECTION_TOTAL = 0x4;
|
|
||||||
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
||||||
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
|
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
|
||||||
|
|
||||||
enum NcaDistributionType {
|
|
||||||
NcaDistributionType_System = 0x0,
|
|
||||||
NcaDistributionType_GameCard = 0x1
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NcaContentType {
|
|
||||||
NcaContentType_Program = 0x0,
|
|
||||||
NcaContentType_Meta = 0x1,
|
|
||||||
NcaContentType_Control = 0x2,
|
|
||||||
NcaContentType_Manual = 0x3,
|
|
||||||
NcaContentType_Data = 0x4,
|
|
||||||
NcaContentType_PublicData = 0x5,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NcaFileSystemType {
|
|
||||||
NcaFileSystemType_RomFS = 0x0,
|
|
||||||
NcaFileSystemType_PFS0 = 0x1
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NcaHashType {
|
|
||||||
NcaHashType_Auto = 0x0,
|
|
||||||
NcaHashType_HierarchicalSha256 = 0x2,
|
|
||||||
NcaHashType_HierarchicalIntegrity = 0x3
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NcaEncryptionType {
|
|
||||||
NcaEncryptionType_Auto = 0x0,
|
|
||||||
NcaEncryptionType_None = 0x1,
|
|
||||||
NcaEncryptionType_AesCtrOld = 0x2,
|
|
||||||
NcaEncryptionType_AesCtr = 0x3,
|
|
||||||
NcaEncryptionType_AesCtrEx = 0x4
|
|
||||||
};
|
|
||||||
|
|
||||||
enum NsApplicationRecordType {
|
|
||||||
// installed
|
|
||||||
NsApplicationRecordType_Installed = 0x3,
|
|
||||||
// application is gamecard, but gamecard isn't insterted
|
|
||||||
NsApplicationRecordType_GamecardMissing = 0x5,
|
|
||||||
// archived
|
|
||||||
NsApplicationRecordType_Archived = 0xB,
|
|
||||||
};
|
|
||||||
|
|
||||||
// stdio-like wrapper for std::vector
|
// stdio-like wrapper for std::vector
|
||||||
struct BufHelper {
|
struct BufHelper {
|
||||||
BufHelper() = default;
|
BufHelper() = default;
|
||||||
@@ -124,12 +89,6 @@ struct CnmtHeader {
|
|||||||
};
|
};
|
||||||
static_assert(sizeof(CnmtHeader) == 0x20);
|
static_assert(sizeof(CnmtHeader) == 0x20);
|
||||||
|
|
||||||
struct NcmContentStorageRecord {
|
|
||||||
NcmContentMetaKey key;
|
|
||||||
u8 storage_id; //
|
|
||||||
u8 padding[0x7];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NcmContentMetaData {
|
struct NcmContentMetaData {
|
||||||
NcmContentMetaHeader header;
|
NcmContentMetaHeader header;
|
||||||
NcmApplicationMetaExtendedHeader extended;
|
NcmApplicationMetaExtendedHeader extended;
|
||||||
@@ -142,7 +101,7 @@ struct NcaMetaEntry {
|
|||||||
NcaEntry nca_entry;
|
NcaEntry nca_entry;
|
||||||
NcmContentMetaHeader content_meta_header{};
|
NcmContentMetaHeader content_meta_header{};
|
||||||
NcmContentMetaKey content_meta_key{};
|
NcmContentMetaKey content_meta_key{};
|
||||||
NcmContentStorageRecord content_storage_record{};
|
ncm::ContentStorageRecord content_storage_record{};
|
||||||
NcmContentMetaData content_meta_data{};
|
NcmContentMetaData content_meta_data{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,61 +130,6 @@ struct FileEntry {
|
|||||||
|
|
||||||
using FileEntries = std::vector<FileEntry>;
|
using FileEntries = std::vector<FileEntry>;
|
||||||
|
|
||||||
struct NpdmMeta {
|
|
||||||
u32 magic; // "META"
|
|
||||||
u32 signature_key_generation; // +9.0.0
|
|
||||||
u32 _0x8;
|
|
||||||
u8 flags;
|
|
||||||
u8 _0xD;
|
|
||||||
u8 main_thread_priority;
|
|
||||||
u8 main_thread_core_num;
|
|
||||||
u32 _0x10;
|
|
||||||
u32 sys_resource_size; // +3.0.0
|
|
||||||
u32 version;
|
|
||||||
u32 main_thread_stack_size;
|
|
||||||
char title_name[0x10];
|
|
||||||
char product_code[0x10];
|
|
||||||
u8 _0x40[0x30];
|
|
||||||
u32 aci0_offset;
|
|
||||||
u32 aci0_size;
|
|
||||||
u32 acid_offset;
|
|
||||||
u32 acid_size;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NpdmAcid {
|
|
||||||
u8 rsa_sig[0x100];
|
|
||||||
u8 rsa_pub[0x100];
|
|
||||||
u32 magic; // "ACID"
|
|
||||||
u32 size;
|
|
||||||
u8 version;
|
|
||||||
u8 _0x209[0x1];
|
|
||||||
u8 _0x20A[0x2];
|
|
||||||
u32 flags;
|
|
||||||
u64 program_id_min;
|
|
||||||
u64 program_id_max;
|
|
||||||
u32 fac_offset;
|
|
||||||
u32 fac_size;
|
|
||||||
u32 sac_offset;
|
|
||||||
u32 sac_size;
|
|
||||||
u32 kac_offset;
|
|
||||||
u32 kac_size;
|
|
||||||
u8 _0x238[0x8];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NpdmAci0 {
|
|
||||||
u32 magic; // "ACI0"
|
|
||||||
u8 _0x4[0xC];
|
|
||||||
u64 program_id;
|
|
||||||
u8 _0x18[0x8];
|
|
||||||
u32 fac_offset;
|
|
||||||
u32 fac_size;
|
|
||||||
u32 sac_offset;
|
|
||||||
u32 sac_size;
|
|
||||||
u32 kac_offset;
|
|
||||||
u32 kac_size;
|
|
||||||
u8 _0x38[0x8];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NpdmPatch {
|
struct NpdmPatch {
|
||||||
char title_name[0x10]{"Application"};
|
char title_name[0x10]{"Application"};
|
||||||
char product_code[0x10]{};
|
char product_code[0x10]{};
|
||||||
@@ -238,56 +142,6 @@ struct NcapPatch {
|
|||||||
u64 tid;
|
u64 tid;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct NcaSectionTableEntry {
|
|
||||||
u32 media_start_offset; // divided by 0x200.
|
|
||||||
u32 media_end_offset; // divided by 0x200.
|
|
||||||
u8 _0x8[0x4]; // unknown.
|
|
||||||
u8 _0xC[0x4]; // unknown.
|
|
||||||
};
|
|
||||||
|
|
||||||
struct LayerRegion {
|
|
||||||
u64 offset;
|
|
||||||
u64 size;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct HierarchicalSha256Data {
|
|
||||||
u8 master_hash[0x20];
|
|
||||||
u32 block_size;
|
|
||||||
u32 layer_count;
|
|
||||||
LayerRegion hash_layer;
|
|
||||||
LayerRegion pfs0_layer;
|
|
||||||
LayerRegion unused_layers[3];
|
|
||||||
u8 _0x78[0x80];
|
|
||||||
};
|
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
|
||||||
struct HierarchicalIntegrityVerificationLevelInformation {
|
|
||||||
u64 logical_offset;
|
|
||||||
u64 hash_data_size;
|
|
||||||
u32 block_size; // log2
|
|
||||||
u32 _0x14; // reserved
|
|
||||||
};
|
|
||||||
#pragma pack(pop)
|
|
||||||
|
|
||||||
struct InfoLevelHash {
|
|
||||||
u32 max_layers;
|
|
||||||
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
|
||||||
u8 signature_salt[0x20];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct IntegrityMetaInfo {
|
|
||||||
u32 magic; // IVFC
|
|
||||||
u32 version;
|
|
||||||
u32 master_hash_size;
|
|
||||||
InfoLevelHash info_level_hash;
|
|
||||||
u8 master_hash[0x20];
|
|
||||||
u8 _0xE0[0x18];
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
|
||||||
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
|
||||||
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
|
||||||
|
|
||||||
typedef struct romfs_dirent_ctx {
|
typedef struct romfs_dirent_ctx {
|
||||||
u32 entry_offset;
|
u32 entry_offset;
|
||||||
struct romfs_dirent_ctx *parent; /* Parent node */
|
struct romfs_dirent_ctx *parent; /* Parent node */
|
||||||
@@ -317,70 +171,6 @@ typedef struct {
|
|||||||
u64 file_partition_size;
|
u64 file_partition_size;
|
||||||
} romfs_ctx_t;
|
} romfs_ctx_t;
|
||||||
|
|
||||||
struct NcaFsHeader {
|
|
||||||
u16 version; // always 2.
|
|
||||||
u8 fs_type; // see NcaFileSystemType.
|
|
||||||
u8 hash_type; // see NcaHashType.
|
|
||||||
u8 encryption_type; // see NcaEncryptionType.
|
|
||||||
u8 metadata_hash_type;
|
|
||||||
u8 _0x6[0x2]; // empty.
|
|
||||||
|
|
||||||
union {
|
|
||||||
HierarchicalSha256Data hierarchical_sha256_data;
|
|
||||||
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
|
||||||
} hash_data;
|
|
||||||
|
|
||||||
u8 patch_info[0x40];
|
|
||||||
u64 section_ctr;
|
|
||||||
u8 spares_info[0x30];
|
|
||||||
u8 compression_info[0x28];
|
|
||||||
u8 meta_data_hash_data_info[0x30];
|
|
||||||
u8 reserved[0x30];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NcaSectionHeaderHash {
|
|
||||||
u8 sha256[0x20];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NcaKeyArea {
|
|
||||||
u8 area[0x10];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct NcaHeader {
|
|
||||||
u8 rsa_fixed_key[0x100];
|
|
||||||
u8 rsa_npdm[0x100]; // key from npdm.
|
|
||||||
u32 magic;
|
|
||||||
u8 distribution_type; // see NcaDistributionType.
|
|
||||||
u8 content_type; // see NcaContentType.
|
|
||||||
u8 old_key_gen; // see NcaOldKeyGeneration.
|
|
||||||
u8 kaek_index; // see NcaKeyAreaEncryptionKeyIndex.
|
|
||||||
u64 size;
|
|
||||||
u64 title_id;
|
|
||||||
u32 context_id;
|
|
||||||
u32 sdk_version;
|
|
||||||
u8 key_gen; // see NcaKeyGeneration.
|
|
||||||
u8 header_1_sig_key_gen;
|
|
||||||
u8 _0x222[0xE]; // empty.
|
|
||||||
FsRightsId rights_id;
|
|
||||||
|
|
||||||
NcaSectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
|
||||||
NcaSectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
|
||||||
NcaKeyArea key_area[NCA_SECTION_TOTAL];
|
|
||||||
|
|
||||||
u8 _0x340[0xC0]; // empty.
|
|
||||||
|
|
||||||
NcaFsHeader fs_header[NCA_SECTION_TOTAL];
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr u8 HEADER_KEK_SRC[0x10] = {
|
|
||||||
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr u8 HEADER_KEY_SRC[0x20] = {
|
|
||||||
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
|
|
||||||
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
|
|
||||||
};
|
|
||||||
|
|
||||||
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
|
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
|
||||||
const u64 size = block - (off % block);
|
const u64 size = block - (off % block);
|
||||||
if (size) {
|
if (size) {
|
||||||
@@ -632,9 +422,9 @@ auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 va
|
|||||||
|
|
||||||
// todo: manually build npdm
|
// todo: manually build npdm
|
||||||
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||||
NpdmMeta meta{};
|
npdm::Meta meta{};
|
||||||
NpdmAci0 aci0{};
|
npdm::Aci0 aci0{};
|
||||||
NpdmAcid acid{};
|
npdm::Acid acid{};
|
||||||
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
||||||
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
||||||
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
||||||
@@ -655,7 +445,7 @@ void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
|||||||
splGetConfig(SplConfigItem_ExosphereVersion, &ver);
|
splGetConfig(SplConfigItem_ExosphereVersion, &ver);
|
||||||
ver >>= 40;
|
ver >>= 40;
|
||||||
|
|
||||||
if (ver >= MAKEHOSVERSION(1,7,1)) {
|
if (ver >= MAKEHOSVERSION(1,8,0)) {
|
||||||
npdm_patch_kc(npdm, meta.aci0_offset + aci0.kac_offset, aci0.kac_size, 16, BIT(19));
|
npdm_patch_kc(npdm, meta.aci0_offset + aci0.kac_offset, aci0.kac_size, 16, BIT(19));
|
||||||
npdm_patch_kc(npdm, meta.acid_offset + acid.kac_offset, acid.kac_size, 16, BIT(19));
|
npdm_patch_kc(npdm, meta.acid_offset + acid.kac_offset, acid.kac_size, 16, BIT(19));
|
||||||
}
|
}
|
||||||
@@ -805,7 +595,7 @@ void write_nca_padding(BufHelper& buf) {
|
|||||||
write_padding(buf, buf.tell(), 0x200);
|
write_padding(buf, buf.tell(), 0x200);
|
||||||
}
|
}
|
||||||
|
|
||||||
void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
void nca_encrypt_header(nca::Header* header, std::span<const u8> key) {
|
||||||
Aes128XtsContext ctx{};
|
Aes128XtsContext ctx{};
|
||||||
aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true);
|
aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true);
|
||||||
|
|
||||||
@@ -816,41 +606,41 @@ void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_section(NcaHeader& nca_header, u8 index, u64 start, u64 end) {
|
void write_nca_section(nca::Header& nca_header, u8 index, u64 start, u64 end) {
|
||||||
auto& section = nca_header.fs_table[index];
|
auto& section = nca_header.fs_table[index];
|
||||||
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
|
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
|
||||||
section.media_end_offset = end / 0x200; // Section end offset / 200
|
section.media_end_offset = end / 0x200; // Section end offset / 200
|
||||||
section._0x8[0] = 0x1; // Always 1
|
section._0x8[0] = 0x1; // Always 1
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_fs_header_pfs0(NcaHeader& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
void write_nca_fs_header_pfs0(nca::Header& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
||||||
auto& fs_header = nca_header.fs_header[index];
|
auto& fs_header = nca_header.fs_header[index];
|
||||||
fs_header.hash_type = NcaHashType_HierarchicalSha256;
|
fs_header.hash_type = nca::HashType_HierarchicalSha256;
|
||||||
fs_header.fs_type = NcaFileSystemType_PFS0;
|
fs_header.fs_type = nca::FileSystemType_PFS0;
|
||||||
fs_header.version = 0x2; // Always 2
|
fs_header.version = 0x2; // Always 2
|
||||||
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
|
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
|
||||||
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
|
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
|
||||||
fs_header.encryption_type = NcaEncryptionType_None;
|
fs_header.encryption_type = nca::EncryptionType_None;
|
||||||
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
|
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
|
||||||
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
|
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
|
||||||
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_fs_header_romfs(NcaHeader& nca_header, u8 index) {
|
void write_nca_fs_header_romfs(nca::Header& nca_header, u8 index) {
|
||||||
auto& fs_header = nca_header.fs_header[index];
|
auto& fs_header = nca_header.fs_header[index];
|
||||||
fs_header.hash_type = NcaHashType_HierarchicalIntegrity;
|
fs_header.hash_type = nca::HashType_HierarchicalIntegrity;
|
||||||
fs_header.fs_type = NcaFileSystemType_RomFS;
|
fs_header.fs_type = nca::FileSystemType_RomFS;
|
||||||
fs_header.version = 0x2; // Always 2
|
fs_header.version = 0x2; // Always 2
|
||||||
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
|
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
|
||||||
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
|
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
|
||||||
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
|
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
|
||||||
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
|
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
|
||||||
fs_header.encryption_type = NcaEncryptionType_None;
|
fs_header.encryption_type = nca::EncryptionType_None;
|
||||||
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
|
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
|
||||||
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_pfs0(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
void write_nca_pfs0(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||||
const auto pfs0 = build_pfs0(entries);
|
const auto pfs0 = build_pfs0(entries);
|
||||||
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
|
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
|
||||||
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
|
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
|
||||||
@@ -887,7 +677,7 @@ auto ivfc_create_level(const std::vector<u8>& src) -> std::vector<u8> {
|
|||||||
return buf.buf;
|
return buf.buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
void write_nca_romfs(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||||
auto& fs_header = nca_header.fs_header[index];
|
auto& fs_header = nca_header.fs_header[index];
|
||||||
auto& meta_info = fs_header.hash_data.integrity_meta_info;
|
auto& meta_info = fs_header.hash_data.integrity_meta_info;
|
||||||
auto& info_level_hash = meta_info.info_level_hash;
|
auto& info_level_hash = meta_info.info_level_hash;
|
||||||
@@ -921,22 +711,22 @@ void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries
|
|||||||
write_nca_fs_header_romfs(nca_header, index);
|
write_nca_fs_header_romfs(nca_header, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span<const u8> key, NcaContentType type, BufHelper& buf) {
|
void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Keys& keys, nca::ContentType type, BufHelper& buf) {
|
||||||
nca_header.magic = 0x3341434E;
|
nca_header.magic = NCA3_MAGIC;
|
||||||
nca_header.distribution_type = NcaDistributionType_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();
|
||||||
|
|
||||||
nca_encrypt_header(&nca_header, key);
|
nca_encrypt_header(&nca_header, keys.header_key);
|
||||||
buf.seek(0);
|
buf.seek(0);
|
||||||
buf.write(&nca_header, sizeof(nca_header));
|
buf.write(&nca_header, sizeof(nca_header));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
auto create_program_nca(u64 tid, const keys::Keys& keys, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
||||||
BufHelper buf;
|
BufHelper buf;
|
||||||
NcaHeader nca_header{};
|
nca::Header nca_header{};
|
||||||
buf.write(&nca_header, sizeof(nca_header));
|
buf.write(&nca_header, sizeof(nca_header));
|
||||||
|
|
||||||
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
|
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
|
||||||
@@ -945,23 +735,23 @@ auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exe
|
|||||||
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
|
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
|
||||||
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
|
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
|
||||||
}
|
}
|
||||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Program, buf);
|
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Program, buf);
|
||||||
|
|
||||||
return {buf, NcmContentType_Program};
|
return {buf, NcmContentType_Program};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto create_control_nca(u64 tid, std::span<const u8> key, const FileEntries& romfs) -> NcaEntry{
|
auto create_control_nca(u64 tid, const keys::Keys& keys, const FileEntries& romfs) -> NcaEntry{
|
||||||
NcaHeader nca_header{};
|
nca::Header nca_header{};
|
||||||
BufHelper buf;
|
BufHelper buf;
|
||||||
buf.write(&nca_header, sizeof(nca_header));
|
buf.write(&nca_header, sizeof(nca_header));
|
||||||
|
|
||||||
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
|
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
|
||||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Control, buf);
|
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Control, buf);
|
||||||
|
|
||||||
return {buf, NcmContentType_Control};
|
return {buf, NcmContentType_Control};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
auto create_meta_nca(u64 tid, const keys::Keys& keys, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
||||||
CnmtHeader cnmt_header{};
|
CnmtHeader cnmt_header{};
|
||||||
NcmApplicationMetaExtendedHeader cnmt_extended{};
|
NcmApplicationMetaExtendedHeader cnmt_extended{};
|
||||||
NcmPackagedContentInfo packaged_content_info[2]{};
|
NcmPackagedContentInfo packaged_content_info[2]{};
|
||||||
@@ -997,10 +787,10 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
|
|||||||
std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid);
|
std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid);
|
||||||
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
|
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
|
||||||
|
|
||||||
NcaHeader nca_header{};
|
nca::Header nca_header{};
|
||||||
buf.write(&nca_header, sizeof(nca_header));
|
buf.write(&nca_header, sizeof(nca_header));
|
||||||
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
|
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
|
||||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Meta, buf);
|
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Meta, buf);
|
||||||
|
|
||||||
// entry
|
// entry
|
||||||
NcaMetaEntry entry{buf, NcmContentType_Meta};
|
NcaMetaEntry entry{buf, NcmContentType_Meta};
|
||||||
@@ -1040,26 +830,6 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result nsDeleteApplicationRecord(Service* srv, u64 tid) {
|
|
||||||
return serviceDispatchIn(srv, 27, tid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result nsPushApplicationRecord(Service* srv, u64 tid, const NcmContentStorageRecord* records, u32 count) {
|
|
||||||
const struct {
|
|
||||||
u8 last_modified_event;
|
|
||||||
u8 padding[0x7];
|
|
||||||
u64 tid;
|
|
||||||
} in = { NsApplicationRecordType_Installed, {0}, tid };
|
|
||||||
|
|
||||||
return serviceDispatchIn(srv, 16, in,
|
|
||||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
|
||||||
.buffers = { { records, sizeof(NcmContentStorageRecord) * count } });
|
|
||||||
}
|
|
||||||
|
|
||||||
Result nsInvalidateApplicationControlCache(Service* srv, u64 tid) {
|
|
||||||
return serviceDispatchIn(srv, 404, tid);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
|
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
|
||||||
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
|
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
|
||||||
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
|
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
|
||||||
@@ -1073,14 +843,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
R_TRY(nsInitialize());
|
R_TRY(nsInitialize());
|
||||||
ON_SCOPE_EXIT(nsExit());
|
ON_SCOPE_EXIT(nsExit());
|
||||||
|
|
||||||
// generate header kek
|
keys::Keys keys;
|
||||||
u8 header_kek[0x20];
|
R_TRY(keys::parse_keys(keys, false));
|
||||||
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
|
|
||||||
// gen header key 0
|
|
||||||
u8 key[0x20];
|
|
||||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, key));
|
|
||||||
// gen header key 1
|
|
||||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, key + 0x10));
|
|
||||||
|
|
||||||
// fix args to include nro path
|
// fix args to include nro path
|
||||||
if (config.args.empty()) {
|
if (config.args.empty()) {
|
||||||
@@ -1093,7 +857,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
u64 hash_data[SHA256_HASH_SIZE / sizeof(u64)];
|
u64 hash_data[SHA256_HASH_SIZE / sizeof(u64)];
|
||||||
const auto hash_path = config.nro_path + config.args;
|
const auto hash_path = config.nro_path + config.args;
|
||||||
sha256CalculateHash(hash_data, hash_path.data(), hash_path.length());
|
sha256CalculateHash(hash_data, hash_path.data(), hash_path.length());
|
||||||
const u64 tid = 0x0100000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
|
const u64 old_tid = 0x0100000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
|
||||||
|
const u64 tid = 0x0500000000000000 | (hash_data[0] & 0x00FFFFFFFFFFF000);
|
||||||
|
|
||||||
std::vector<NcaEntry> nca_entries;
|
std::vector<NcaEntry> nca_entries;
|
||||||
|
|
||||||
@@ -1124,7 +889,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
patch_npdm(exefs[1].data, npdm_patch);
|
patch_npdm(exefs[1].data, npdm_patch);
|
||||||
|
|
||||||
nca_entries.emplace_back(
|
nca_entries.emplace_back(
|
||||||
create_program_nca(tid, key, exefs, romfs, logo)
|
create_program_nca(tid, keys, exefs, romfs, logo)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
nca_entries.emplace_back(
|
nca_entries.emplace_back(
|
||||||
@@ -1147,18 +912,18 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
|
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
|
||||||
|
|
||||||
nca_entries.emplace_back(
|
nca_entries.emplace_back(
|
||||||
create_control_nca(tid, key, romfs)
|
create_control_nca(tid, keys, romfs)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create meta
|
// create meta
|
||||||
NcmContentMetaHeader content_meta_header;
|
NcmContentMetaHeader content_meta_header;
|
||||||
NcmContentMetaKey content_meta_key;
|
NcmContentMetaKey content_meta_key;
|
||||||
NcmContentStorageRecord content_storage_record;
|
ncm::ContentStorageRecord content_storage_record;
|
||||||
NcmContentMetaData content_meta_data;
|
NcmContentMetaData content_meta_data;
|
||||||
{
|
{
|
||||||
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
|
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
|
||||||
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
|
const auto meta_entry = create_meta_nca(tid, keys, storage_id, nca_entries);
|
||||||
|
|
||||||
nca_entries.emplace_back(meta_entry.nca_entry);
|
nca_entries.emplace_back(meta_entry.nca_entry);
|
||||||
content_meta_header = meta_entry.content_meta_header;
|
content_meta_header = meta_entry.content_meta_header;
|
||||||
@@ -1216,17 +981,23 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
|||||||
R_TRY(nsIsAnyApplicationEntityInstalled(tid, &already_installed));
|
R_TRY(nsIsAnyApplicationEntityInstalled(tid, &already_installed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove old id for forwarders.
|
||||||
|
const auto rc = nsDeleteApplicationCompletely(old_tid);
|
||||||
|
if (R_FAILED(rc) && rc != 0x410) { // not found
|
||||||
|
App::Notify("Failed to remove old forwarder, please manually remove it!");
|
||||||
|
}
|
||||||
|
|
||||||
// remove previous application record
|
// remove previous application record
|
||||||
if (already_installed || hosversionBefore(2,0,0)) {
|
if (already_installed || hosversionBefore(2,0,0)) {
|
||||||
const auto rc = nsDeleteApplicationRecord(srv_ptr, tid);
|
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);
|
||||||
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
||||||
}
|
}
|
||||||
|
|
||||||
R_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
||||||
|
|
||||||
// force flush
|
// force flush
|
||||||
if (already_installed || hosversionBefore(2,0,0)) {
|
if (already_installed || hosversionBefore(2,0,0)) {
|
||||||
const auto rc = nsInvalidateApplicationControlCache(srv_ptr, tid);
|
const auto rc = ns::InvalidateApplicationControlCache(srv_ptr, tid);
|
||||||
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1241,7 +1012,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();
|
||||||
|
|||||||
@@ -42,14 +42,15 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Result ShowText(std::string& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
Result ShowText(std::string& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||||
Config cfg;
|
Config cfg{};
|
||||||
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
||||||
out = cfg.out_text;
|
out = cfg.out_text;
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result ShowNumPad(s64& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
Result ShowNumPad(s64& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||||
Config cfg;
|
Config cfg{};
|
||||||
|
cfg.numpad = true;
|
||||||
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
||||||
out = std::atoll(cfg.out_text);
|
out = std::atoll(cfg.out_text);
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <yyjson.h>
|
#include <yyjson.h>
|
||||||
#include <nanovg/stb_image.h>
|
#include <stb_image.h>
|
||||||
#include <minizip/unzip.h>
|
#include <minizip/unzip.h>
|
||||||
#include <mbedtls/md5.h>
|
#include <mbedtls/md5.h>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
@@ -65,60 +65,27 @@ auto BuildIconUrl(const Entry& e) -> std::string {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
|
||||||
auto BuildInfoUrl(const Entry& e) -> std::string {
|
|
||||||
char out[0x100];
|
|
||||||
std::snprintf(out, sizeof(out), "%s/packages/%s/info.json", URL_BASE, e.name.c_str());
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
auto BuildBannerUrl(const Entry& e) -> std::string {
|
auto BuildBannerUrl(const Entry& e) -> std::string {
|
||||||
char out[0x100];
|
char out[0x100];
|
||||||
std::snprintf(out, sizeof(out), "%s/packages/%s/screen.png", URL_BASE, e.name.c_str());
|
std::snprintf(out, sizeof(out), "%s/packages/%s/screen.png", URL_BASE, e.name.c_str());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
|
||||||
auto BuildScreensUrl(const Entry& e, u8 num) -> std::string {
|
|
||||||
char out[0x100];
|
|
||||||
std::snprintf(out, sizeof(out), "%s/packages/%s/screen%u.png", URL_BASE, e.name.c_str(), num+1);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
auto BuildMainifestUrl(const Entry& e) -> std::string {
|
|
||||||
char out[0x100];
|
|
||||||
std::snprintf(out, sizeof(out), "%s/packages/%s/manifest.install", URL_BASE, e.name.c_str());
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto BuildZipUrl(const Entry& e) -> std::string {
|
auto BuildZipUrl(const Entry& e) -> std::string {
|
||||||
char out[0x100];
|
char out[0x100];
|
||||||
std::snprintf(out, sizeof(out), "%s/zips/%s.zip", URL_BASE, e.name.c_str());
|
std::snprintf(out, sizeof(out), "%s/zips/%s.zip", URL_BASE, e.name.c_str());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto BuildFeedbackUrl(std::span<u32> ids) -> std::string {
|
|
||||||
std::string out{"https://wiiubru.com/feedback/messages?ids="};
|
|
||||||
for (u32 i = 0; i < ids.size(); i++) {
|
|
||||||
if (i != 0) {
|
|
||||||
out.push_back(',');
|
|
||||||
}
|
|
||||||
out += std::to_string(ids[i]);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto BuildIconCachePath(const Entry& e) -> fs::FsPath {
|
auto BuildIconCachePath(const Entry& e) -> fs::FsPath {
|
||||||
fs::FsPath out;
|
fs::FsPath out;
|
||||||
std::snprintf(out, sizeof(out), "%s/icons/%s.png", CACHE_PATH, e.name.c_str());
|
std::snprintf(out, sizeof(out), "%s/icons/%s.png", CACHE_PATH.s, e.name.c_str());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto BuildBannerCachePath(const Entry& e) -> fs::FsPath {
|
auto BuildBannerCachePath(const Entry& e) -> fs::FsPath {
|
||||||
fs::FsPath out;
|
fs::FsPath out;
|
||||||
std::snprintf(out, sizeof(out), "%s/banners/%s.png", CACHE_PATH, e.name.c_str());
|
std::snprintf(out, sizeof(out), "%s/banners/%s.png", CACHE_PATH.s, e.name.c_str());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,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 ? 5 : 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 ? 5 : 0);
|
||||||
} else {
|
|
||||||
gfx::drawImage(vg, ix, iy, iw, ih, i.image);
|
|
||||||
}
|
|
||||||
if (crop) {
|
if (crop) {
|
||||||
nvgRestore(vg);
|
nvgRestore(vg);
|
||||||
}
|
}
|
||||||
@@ -302,7 +266,6 @@ auto AppDlToStr(u32 value) -> std::string {
|
|||||||
|
|
||||||
void ReadFromInfoJson(Entry& e) {
|
void ReadFromInfoJson(Entry& e) {
|
||||||
const auto info_path = BuildInfoCachePath(e);
|
const auto info_path = BuildInfoCachePath(e);
|
||||||
const auto manifest_path = BuildManifestCachePath(e);
|
|
||||||
|
|
||||||
yyjson_read_err err;
|
yyjson_read_err err;
|
||||||
auto doc = yyjson_read_file(info_path, YYJSON_READ_NOFLAG, nullptr, &err);
|
auto doc = yyjson_read_file(info_path, YYJSON_READ_NOFLAG, nullptr, &err);
|
||||||
@@ -340,9 +303,9 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
const auto safe_buf = fs::AppendPath("/", e.path);
|
const auto safe_buf = fs::AppendPath("/", e.path);
|
||||||
// this will handle read only files, ie, hbmenu.nro
|
// this will handle read only files, ie, hbmenu.nro
|
||||||
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
||||||
log_write("failed to delete file: %s\n", safe_buf);
|
log_write("failed to delete file: %s\n", safe_buf.s);
|
||||||
} else {
|
} else {
|
||||||
log_write("deleted file: %s\n", safe_buf);
|
log_write("deleted file: %s\n", safe_buf.s);
|
||||||
// todo: delete empty directories!
|
// todo: delete empty directories!
|
||||||
// fs::delete_directory(safe_buf);
|
// fs::delete_directory(safe_buf);
|
||||||
}
|
}
|
||||||
@@ -353,9 +316,9 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
const auto dir = BuildPackageCachePath(entry);
|
const auto dir = BuildPackageCachePath(entry);
|
||||||
pbox->NewTransfer("Removing "_i18n + dir);
|
pbox->NewTransfer("Removing "_i18n + dir);
|
||||||
if (R_FAILED(fs.DeleteDirectoryRecursively(dir))) {
|
if (R_FAILED(fs.DeleteDirectoryRecursively(dir))) {
|
||||||
log_write("failed to delete folder: %s\n", dir);
|
log_write("failed to delete folder: %s\n", dir.s);
|
||||||
} else {
|
} else {
|
||||||
log_write("deleted: %s\n", dir);
|
log_write("deleted: %s\n", dir.s);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -458,7 +421,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
if (!pbox->ShouldExit()) {
|
if (!pbox->ShouldExit()) {
|
||||||
auto zfile = unzOpen64(zip_out);
|
auto zfile = unzOpen64(zip_out);
|
||||||
if (!zfile) {
|
if (!zfile) {
|
||||||
log_write("failed to open zip: %s\n", zip_out);
|
log_write("failed to open zip: %s\n", zip_out.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(unzClose(zfile));
|
ON_SCOPE_EXIT(unzClose(zfile));
|
||||||
@@ -501,7 +464,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
pbox->NewTransfer(inzip);
|
pbox->NewTransfer(inzip);
|
||||||
|
|
||||||
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, inzip, 0)) {
|
if (UNZ_END_OF_LIST_OF_FILE == unzLocateFile(zfile, inzip, 0)) {
|
||||||
log_write("failed to find %s\n", inzip);
|
log_write("failed to find %s\n", inzip.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,19 +489,19 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
|
|
||||||
Result rc;
|
Result rc;
|
||||||
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc = fs.CreateFile(output, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create file: %s 0x%04X\n", output, rc);
|
log_write("failed to create file: %s 0x%04X\n", output.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (R_FAILED(rc = fs.OpenFile(output, FsOpenMode_Write, &f))) {
|
if (R_FAILED(rc = fs.OpenFile(output, FsOpenMode_Write, &f))) {
|
||||||
log_write("failed to open file: %s 0x%04X\n", output, rc);
|
log_write("failed to open file: %s 0x%04X\n", output.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(fsFileClose(&f));
|
ON_SCOPE_EXIT(fsFileClose(&f));
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
||||||
log_write("failed to set file size: %s 0x%04X\n", output, rc);
|
log_write("failed to set file size: %s 0x%04X\n", output.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,12 +514,12 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
|
|
||||||
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
const auto bytes_read = unzReadCurrentFile(zfile, buf.data(), buf.size());
|
||||||
if (bytes_read <= 0) {
|
if (bytes_read <= 0) {
|
||||||
log_write("failed to read zip file: %s\n", inzip);
|
log_write("failed to read zip file: %s\n", inzip.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
|
if (R_FAILED(rc = fsFileWrite(&f, offset, buf.data(), bytes_read, FsWriteOption_None))) {
|
||||||
log_write("failed to write file: %s 0x%04X\n", output, rc);
|
log_write("failed to write file: %s 0x%04X\n", output.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,9 +578,9 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
|||||||
const auto safe_buf = fs::AppendPath("/", old_entry.path);
|
const auto safe_buf = fs::AppendPath("/", old_entry.path);
|
||||||
// std::strcat(safe_buf, old_entry.path);
|
// std::strcat(safe_buf, old_entry.path);
|
||||||
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
if (R_FAILED(fs.DeleteFile(safe_buf))) {
|
||||||
log_write("failed to delete: %s\n", safe_buf);
|
log_write("failed to delete: %s\n", safe_buf.s);
|
||||||
} else {
|
} else {
|
||||||
log_write("deleted file: %s\n", safe_buf);
|
log_write("deleted file: %s\n", safe_buf.s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -627,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)
|
||||||
@@ -663,6 +634,7 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
|||||||
curl::Url{URL_POST_FEEDBACK},
|
curl::Url{URL_POST_FEEDBACK},
|
||||||
curl::Path{file},
|
curl::Path{file},
|
||||||
curl::Fields{post},
|
curl::Fields{post},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
curl::OnComplete{[](auto& result){
|
curl::OnComplete{[](auto& result){
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
log_write("got feedback!\n");
|
log_write("got feedback!\n");
|
||||||
@@ -697,6 +669,7 @@ EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
|||||||
curl::Url{url},
|
curl::Url{url},
|
||||||
curl::Path{path},
|
curl::Path{path},
|
||||||
curl::Flags{curl::Flag_Cache},
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
curl::OnComplete{[this, path](auto& result){
|
curl::OnComplete{[this, path](auto& result){
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (result.code == 304) {
|
if (result.code == 304) {
|
||||||
@@ -759,13 +732,9 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str());
|
gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "app_dls: %s"_i18n.c_str(), AppDlToStr(m_entry.app_dls).c_str());
|
||||||
text_start_y += text_inc_y;
|
text_start_y += text_inc_y;
|
||||||
|
|
||||||
// for (const auto& option : m_options) {
|
|
||||||
const auto& text_col = theme->GetColour(ThemeEntryID_TEXT);
|
|
||||||
|
|
||||||
// todo: rewrite this mess and use list
|
// todo: rewrite this mess and use list
|
||||||
constexpr float mm = 0;//20;
|
constexpr float mm = 0;//20;
|
||||||
constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f};
|
constexpr Vec4 block{968.f + mm, 110.f, 256.f - mm*2, 60.f};
|
||||||
constexpr float text_xoffset{15.f};
|
|
||||||
const float x = block.x;
|
const float x = block.x;
|
||||||
float y = 1.f + text_start_y + (text_inc_y * 3) ;
|
float y = 1.f + text_start_y + (text_inc_y * 3) ;
|
||||||
const float h = block.h;
|
const float h = block.h;
|
||||||
@@ -779,7 +748,7 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
gfx::drawRectOutline(vg, theme, 4.f, Vec4{x, y, w, h});
|
gfx::drawRectOutline(vg, theme, 4.f, Vec4{x, y, w, h});
|
||||||
}
|
}
|
||||||
|
|
||||||
gfx::drawTextArgs(vg, x + w / 2, y + h / 2, 22, NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER, theme->GetColour(ThemeEntryID_TEXT), option.display_text.c_str());
|
gfx::drawTextArgs(vg, x + w / 2, y + h / 2, 22, NVG_ALIGN_MIDDLE | NVG_ALIGN_CENTER, theme->GetColour(text_id), option.display_text.c_str());
|
||||||
y -= block.h + 18;
|
y -= block.h + 18;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,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) {
|
||||||
@@ -818,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) {
|
||||||
@@ -877,59 +846,40 @@ void EntryMenu::SetIndex(s64 index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto toLower(const std::string& str) -> std::string {
|
Menu::Menu() : MenuBase{"AppStore"_i18n} {
|
||||||
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} {
|
|
||||||
fs::FsNativeSd fs;
|
fs::FsNativeSd fs;
|
||||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
|
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
|
||||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
|
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
|
||||||
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](){
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
if (m_entries_current.empty()) {
|
if (m_is_author) {
|
||||||
return;
|
m_is_author = false;
|
||||||
}
|
if (m_is_search) {
|
||||||
|
SetSearch(m_search_term);
|
||||||
|
} else {
|
||||||
|
SetFilter(m_filter);
|
||||||
|
}
|
||||||
|
|
||||||
if (m_index < (m_entries_current.size() - 1) && (m_index + 1) % 3 != 0) {
|
SetIndex(m_entry_author_jump_back);
|
||||||
SetIndex(m_index + 1);
|
if (m_entry_author_jump_back >= 9) {
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
|
||||||
log_write("moved right\n");
|
} else {
|
||||||
}
|
m_list->SetYoff(0);
|
||||||
}}),
|
}
|
||||||
std::make_pair(Button::LEFT, Action{[this](){
|
} else if (m_is_search) {
|
||||||
if (m_entries_current.empty()) {
|
m_is_search = false;
|
||||||
return;
|
SetFilter(m_filter);
|
||||||
}
|
SetIndex(m_entry_search_jump_back);
|
||||||
|
if (m_entry_search_jump_back >= 9) {
|
||||||
if (m_index != 0 && (m_index % 3) != 0) {
|
m_list->SetYoff(0);
|
||||||
SetIndex(m_index - 1);
|
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
|
||||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
} else {
|
||||||
log_write("moved left\n");
|
m_list->SetYoff(0);
|
||||||
}
|
}
|
||||||
}}),
|
} else {
|
||||||
std::make_pair(Button::DOWN, Action{[this](){
|
SetPop();
|
||||||
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](){
|
||||||
@@ -960,7 +910,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
|||||||
sort_items.push_back("Alphabetical"_i18n);
|
sort_items.push_back("Alphabetical"_i18n);
|
||||||
|
|
||||||
SidebarEntryArray::Items order_items;
|
SidebarEntryArray::Items order_items;
|
||||||
order_items.push_back("Decending"_i18n);
|
order_items.push_back("Descending"_i18n);
|
||||||
order_items.push_back("Ascending"_i18n);
|
order_items.push_back("Ascending"_i18n);
|
||||||
|
|
||||||
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](s64& index_out){
|
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](s64& index_out){
|
||||||
@@ -990,6 +940,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
|||||||
curl::Url{URL_JSON},
|
curl::Url{URL_JSON},
|
||||||
curl::Path{REPO_PATH},
|
curl::Path{REPO_PATH},
|
||||||
curl::Flags{curl::Flag_Cache},
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
curl::OnComplete{[this](auto& result){
|
curl::OnComplete{[this](auto& result){
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
m_repo_download_state = ImageDownloadState::Done;
|
m_repo_download_state = ImageDownloadState::Done;
|
||||||
@@ -1018,8 +969,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);
|
||||||
@@ -1071,6 +1022,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
curl::Url{url},
|
curl::Url{url},
|
||||||
curl::Path{path},
|
curl::Path{path},
|
||||||
curl::Flags{curl::Flag_Cache},
|
curl::Flags{curl::Flag_Cache},
|
||||||
|
curl::StopToken{this->GetToken()},
|
||||||
curl::OnComplete{[this, &image](auto& result) {
|
curl::OnComplete{[this, &image](auto& result) {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
image.state = ImageDownloadState::Done;
|
image.state = ImageDownloadState::Done;
|
||||||
@@ -1104,7 +1056,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto text_id = ThemeEntryID_TEXT;
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
if (pos == m_index) {
|
const auto selected = pos == m_index;
|
||||||
|
if (selected) {
|
||||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
} else {
|
} else {
|
||||||
@@ -1117,29 +1070,27 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
|||||||
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale);
|
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale);
|
||||||
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image);
|
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image);
|
||||||
|
|
||||||
nvgSave(vg);
|
const auto text_off = 148;
|
||||||
nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip
|
const auto text_x = x + text_off;
|
||||||
{
|
const auto text_clip_w = w - 30.f - text_off;
|
||||||
const float font_size = 18;
|
const float font_size = 18;
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.title.c_str());
|
m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.title.c_str());
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str());
|
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.author.c_str());
|
||||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str());
|
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.version.c_str());
|
||||||
}
|
|
||||||
nvgRestore(vg);
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1294,7 +1245,7 @@ void Menu::Sort() {
|
|||||||
case SortType_Updated: {
|
case SortType_Updated: {
|
||||||
if (lhs.updated_num == rhs.updated_num) {
|
if (lhs.updated_num == rhs.updated_num) {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||||
} else if (m_order == OrderType_Decending) {
|
} else if (m_order == OrderType_Descending) {
|
||||||
return lhs.updated_num > rhs.updated_num;
|
return lhs.updated_num > rhs.updated_num;
|
||||||
} else {
|
} else {
|
||||||
return lhs.updated_num < rhs.updated_num;
|
return lhs.updated_num < rhs.updated_num;
|
||||||
@@ -1303,7 +1254,7 @@ void Menu::Sort() {
|
|||||||
case SortType_Downloads: {
|
case SortType_Downloads: {
|
||||||
if (lhs.app_dls == rhs.app_dls) {
|
if (lhs.app_dls == rhs.app_dls) {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||||
} else if (m_order == OrderType_Decending) {
|
} else if (m_order == OrderType_Descending) {
|
||||||
return lhs.app_dls > rhs.app_dls;
|
return lhs.app_dls > rhs.app_dls;
|
||||||
} else {
|
} else {
|
||||||
return lhs.app_dls < rhs.app_dls;
|
return lhs.app_dls < rhs.app_dls;
|
||||||
@@ -1312,14 +1263,14 @@ void Menu::Sort() {
|
|||||||
case SortType_Size: {
|
case SortType_Size: {
|
||||||
if (lhs.extracted == rhs.extracted) {
|
if (lhs.extracted == rhs.extracted) {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||||
} else if (m_order == OrderType_Decending) {
|
} else if (m_order == OrderType_Descending) {
|
||||||
return lhs.extracted > rhs.extracted;
|
return lhs.extracted > rhs.extracted;
|
||||||
} else {
|
} else {
|
||||||
return lhs.extracted < rhs.extracted;
|
return lhs.extracted < rhs.extracted;
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case SortType_Alphabetical: {
|
case SortType_Alphabetical: {
|
||||||
if (m_order == OrderType_Decending) {
|
if (m_order == OrderType_Descending) {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||||
} else {
|
} else {
|
||||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) > 0;
|
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) > 0;
|
||||||
@@ -1342,7 +1293,6 @@ void Menu::Sort() {
|
|||||||
void Menu::SetFilter(Filter filter) {
|
void Menu::SetFilter(Filter filter) {
|
||||||
m_is_search = false;
|
m_is_search = false;
|
||||||
m_is_author = false;
|
m_is_author = false;
|
||||||
RemoveAction(Button::B);
|
|
||||||
|
|
||||||
m_filter = filter;
|
m_filter = filter;
|
||||||
m_entries_current = m_entries_index[m_filter];
|
m_entries_current = m_entries_index[m_filter];
|
||||||
@@ -1372,27 +1322,15 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
|
||||||
SetFilter(m_filter);
|
|
||||||
SetIndex(m_entry_search_jump_back);
|
|
||||||
if (m_entry_search_jump_back >= 9) {
|
|
||||||
m_list->SetYoff(0);
|
|
||||||
m_list->SetYoff((((m_entry_search_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
|
|
||||||
} else {
|
|
||||||
m_list->SetYoff(0);
|
|
||||||
}
|
|
||||||
}});
|
|
||||||
|
|
||||||
m_is_search = true;
|
m_is_search = true;
|
||||||
m_entries_current = m_entries_index_search;
|
m_entries_current = m_entries_index_search;
|
||||||
SetIndex(0);
|
SetIndex(0);
|
||||||
@@ -1406,29 +1344,15 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
|
||||||
if (m_is_search) {
|
|
||||||
SetSearch(m_search_term);
|
|
||||||
} else {
|
|
||||||
SetFilter(m_filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
SetIndex(m_entry_author_jump_back);
|
|
||||||
if (m_entry_author_jump_back >= 9) {
|
|
||||||
m_list->SetYoff((((m_entry_author_jump_back - 9) + 3) / 3) * m_list->GetMaxY());
|
|
||||||
} else {
|
|
||||||
m_list->SetYoff(0);
|
|
||||||
}
|
|
||||||
}});
|
|
||||||
|
|
||||||
m_is_author = true;
|
m_is_author = true;
|
||||||
m_entries_current = m_entries_index_author;
|
m_entries_current = m_entries_index_author;
|
||||||
SetIndex(0);
|
SetIndex(0);
|
||||||
|
|||||||
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
|
||||||
452
sphaira/source/ui/menus/game_menu.cpp
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
#include "app.hpp"
|
||||||
|
#include "log.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "ui/menus/game_menu.hpp"
|
||||||
|
#include "ui/sidebar.hpp"
|
||||||
|
#include "ui/error_box.hpp"
|
||||||
|
#include "ui/option_box.hpp"
|
||||||
|
#include "ui/progress_box.hpp"
|
||||||
|
#include "ui/popup_list.hpp"
|
||||||
|
#include "ui/nvg_util.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "i18n.hpp"
|
||||||
|
#include "yati/nx/ncm.hpp"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::game {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// thank you Shchmue ^^
|
||||||
|
struct ApplicationOccupiedSizeEntry {
|
||||||
|
u8 storageId;
|
||||||
|
u8 padding[0x7];
|
||||||
|
u64 sizeApplication;
|
||||||
|
u64 sizePatch;
|
||||||
|
u64 sizeAddOnContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ApplicationOccupiedSize {
|
||||||
|
ApplicationOccupiedSizeEntry entry[4];
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(ApplicationOccupiedSize) == sizeof(NsApplicationOccupiedSize));
|
||||||
|
|
||||||
|
using MetaEntries = std::vector<NsApplicationContentMetaStatus>;
|
||||||
|
|
||||||
|
Result Notify(Result rc, const std::string& error_message) {
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
App::Push(std::make_shared<ui::ErrorBox>(rc,
|
||||||
|
i18n::get(error_message)
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
App::Notify("Success");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result GetMetaEntries(const Entry& e, MetaEntries& out) {
|
||||||
|
s32 count;
|
||||||
|
R_TRY(nsCountApplicationContentMeta(e.app_id, &count));
|
||||||
|
|
||||||
|
out.resize(count);
|
||||||
|
R_TRY(nsListApplicationContentMetaStatus(e.app_id, 0, out.data(), out.size(), &count));
|
||||||
|
|
||||||
|
out.resize(count);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
// also sets the status to error.
|
||||||
|
void FakeNacpEntry(Entry& e) {
|
||||||
|
e.status = NacpLoadStatus::Error;
|
||||||
|
// fake the nacp entry
|
||||||
|
std::strcpy(e.lang.name, "Corrupted");
|
||||||
|
std::strcpy(e.lang.author, "Corrupted");
|
||||||
|
std::strcpy(e.display_version, "0.0.0");
|
||||||
|
e.control.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadControlImage(Entry& e) {
|
||||||
|
if (!e.image && e.control) {
|
||||||
|
const auto jpeg_size = e.control_size - sizeof(NacpStruct);
|
||||||
|
e.image = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, jpeg_size);
|
||||||
|
e.control.reset();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadControlEntry(Entry& e, bool force_image_load = false) {
|
||||||
|
if (e.status == NacpLoadStatus::None) {
|
||||||
|
e.control = std::make_unique<NsApplicationControlData>();
|
||||||
|
if (R_FAILED(nsGetApplicationControlData(NsApplicationControlSource_Storage, e.app_id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) {
|
||||||
|
FakeNacpEntry(e);
|
||||||
|
} else {
|
||||||
|
NacpLanguageEntry* lang{};
|
||||||
|
if (R_FAILED(nsGetApplicationDesiredLanguage(&e.control->nacp, &lang)) || !lang) {
|
||||||
|
FakeNacpEntry(e);
|
||||||
|
} else {
|
||||||
|
e.lang = *lang;
|
||||||
|
std::memcpy(e.display_version, e.control->nacp.display_version, sizeof(e.display_version));
|
||||||
|
e.status = NacpLoadStatus::Loaded;
|
||||||
|
|
||||||
|
if (force_image_load) {
|
||||||
|
LoadControlImage(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FreeEntry(NVGcontext* vg, Entry& e) {
|
||||||
|
nvgDeleteImage(vg, e.image);
|
||||||
|
e.image = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LaunchEntry(const Entry& e) {
|
||||||
|
const auto rc = appletRequestLaunchApplication(e.app_id, nullptr);
|
||||||
|
Notify(rc, "Failed to launch application");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Menu::Menu() : MenuBase{"Games"_i18n} {
|
||||||
|
this->SetActions(
|
||||||
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
|
SetPop();
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||||
|
if (m_entries.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LaunchEntry(m_entries[m_index]);
|
||||||
|
}}),
|
||||||
|
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||||
|
auto options = std::make_shared<Sidebar>("Game Options"_i18n, Sidebar::Side::RIGHT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
if (m_entries.size()) {
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Sort By"_i18n, [this](){
|
||||||
|
auto options = std::make_shared<Sidebar>("Sort Options"_i18n, Sidebar::Side::RIGHT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(options));
|
||||||
|
|
||||||
|
SidebarEntryArray::Items sort_items;
|
||||||
|
sort_items.push_back("Updated"_i18n);
|
||||||
|
|
||||||
|
SidebarEntryArray::Items order_items;
|
||||||
|
order_items.push_back("Descending"_i18n);
|
||||||
|
order_items.push_back("Ascending"_i18n);
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||||
|
m_sort.Set(index_out);
|
||||||
|
SortAndFindLastFile();
|
||||||
|
}, m_sort.Get()));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
|
||||||
|
m_order.Set(index_out);
|
||||||
|
SortAndFindLastFile();
|
||||||
|
}, m_order.Get()));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryBool>("Hide forwarders"_i18n, m_hide_forwarders.Get(), [this](bool& v_out){
|
||||||
|
m_hide_forwarders.Set(v_out);
|
||||||
|
m_dirty = true;
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Info"_i18n, [this](){
|
||||||
|
|
||||||
|
}));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Launch random game"_i18n, [this](){
|
||||||
|
const auto random_index = randomGet64() % std::size(m_entries);
|
||||||
|
auto& e = m_entries[random_index];
|
||||||
|
LoadControlEntry(e, true);
|
||||||
|
|
||||||
|
App::Push(std::make_shared<OptionBox>(
|
||||||
|
"Launch "_i18n + e.GetName(),
|
||||||
|
"Back"_i18n, "Launch"_i18n, 1, [this, &e](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
LaunchEntry(e);
|
||||||
|
}
|
||||||
|
}, e.image
|
||||||
|
));
|
||||||
|
}));
|
||||||
|
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("List meta records"_i18n, [this](){
|
||||||
|
MetaEntries meta_entries;
|
||||||
|
const auto rc = GetMetaEntries(m_entries[m_index], meta_entries);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
App::Push(std::make_shared<ui::ErrorBox>(rc,
|
||||||
|
i18n::get("Failed to list application meta entries")
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta_entries.empty()) {
|
||||||
|
App::Notify("No meta entries found...\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PopupList::Items items;
|
||||||
|
for (auto& e : meta_entries) {
|
||||||
|
char buf[256];
|
||||||
|
std::snprintf(buf, sizeof(buf), "Type: %s Storage: %s [%016lX][v%u]", ncm::GetMetaTypeStr(e.meta_type), ncm::GetStorageIdStr(e.storageID), e.application_id, e.version);
|
||||||
|
items.emplace_back(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
App::Push(std::make_shared<PopupList>(
|
||||||
|
"Entries", items, [this, meta_entries](auto op_index){
|
||||||
|
#if 0
|
||||||
|
if (op_index) {
|
||||||
|
const auto& e = meta_entries[*op_index];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}));
|
||||||
|
|
||||||
|
// completely deletes the application record and all data.
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Delete"_i18n, [this](){
|
||||||
|
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
|
||||||
|
App::Push(std::make_shared<OptionBox>(
|
||||||
|
buf,
|
||||||
|
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
const auto rc = nsDeleteApplicationCompletely(m_entries[m_index].app_id);
|
||||||
|
if (R_SUCCEEDED(Notify(rc, "Failed to delete application"))) {
|
||||||
|
FreeEntry(App::GetVg(), m_entries[m_index]);
|
||||||
|
m_entries.erase(m_entries.begin() + m_index);
|
||||||
|
SetIndex(m_index ? m_index - 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, m_entries[m_index].image
|
||||||
|
));
|
||||||
|
}, true));
|
||||||
|
|
||||||
|
// removes installed data but keeps the record, basically archiving.
|
||||||
|
options->Add(std::make_shared<SidebarEntryCallback>("Delete entity"_i18n, [this](){
|
||||||
|
const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?";
|
||||||
|
App::Push(std::make_shared<OptionBox>(
|
||||||
|
buf,
|
||||||
|
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
const auto rc = nsDeleteApplicationEntity(m_entries[m_index].app_id);
|
||||||
|
Notify(rc, "Failed to delete application");
|
||||||
|
}
|
||||||
|
}, m_entries[m_index].image
|
||||||
|
));
|
||||||
|
}, true));
|
||||||
|
}
|
||||||
|
}})
|
||||||
|
);
|
||||||
|
|
||||||
|
const Vec4 v{75, 110, 370, 155};
|
||||||
|
const Vec2 pad{10, 10};
|
||||||
|
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||||
|
nsInitialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::~Menu() {
|
||||||
|
FreeEntries();
|
||||||
|
nsExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
|
MenuBase::Update(controller, touch);
|
||||||
|
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||||
|
if (touch && m_index == i) {
|
||||||
|
FireAction(Button::A);
|
||||||
|
} else {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
SetIndex(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
|
// max images per frame, in order to not hit io / gpu too hard.
|
||||||
|
const int image_load_max = 2;
|
||||||
|
int image_load_count = 0;
|
||||||
|
|
||||||
|
m_list->Draw(vg, theme, m_entries.size(), [this, &image_load_count](auto* vg, auto* theme, auto v, auto pos) {
|
||||||
|
const auto& [x, y, w, h] = v;
|
||||||
|
auto& e = m_entries[pos];
|
||||||
|
|
||||||
|
if (e.status == NacpLoadStatus::None) {
|
||||||
|
LoadControlEntry(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// lazy load image
|
||||||
|
if (image_load_count < image_load_max) {
|
||||||
|
if (LoadControlImage(e)) {
|
||||||
|
image_load_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
|
const auto selected = pos == m_index;
|
||||||
|
if (selected) {
|
||||||
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
|
} else {
|
||||||
|
DrawElement(v, ThemeEntryID_GRID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float image_size = 115;
|
||||||
|
gfx::drawImage(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage(), 5);
|
||||||
|
|
||||||
|
const auto text_off = 148;
|
||||||
|
const auto text_x = x + text_off;
|
||||||
|
const auto text_clip_w = w - 30.f - text_off;
|
||||||
|
const float font_size = 18;
|
||||||
|
m_scroll_name.Draw(vg, selected, text_x, y + 45, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetName());
|
||||||
|
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor());
|
||||||
|
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::OnFocusGained() {
|
||||||
|
MenuBase::OnFocusGained();
|
||||||
|
if (m_dirty || m_entries.empty()) {
|
||||||
|
ScanHomebrew();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::SetIndex(s64 index) {
|
||||||
|
m_index = index;
|
||||||
|
if (!m_index) {
|
||||||
|
m_list->SetYoff(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
char title_id[33];
|
||||||
|
std::snprintf(title_id, sizeof(title_id), "%016lX", m_entries[m_index].app_id);
|
||||||
|
SetTitleSubHeading(title_id);
|
||||||
|
this->SetSubHeading(std::to_string(m_index + 1) + " / " + std::to_string(m_entries.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::ScanHomebrew() {
|
||||||
|
constexpr auto ENTRY_CHUNK_COUNT = 1000;
|
||||||
|
const auto hide_forwarders = m_hide_forwarders.Get();
|
||||||
|
TimeStamp ts;
|
||||||
|
|
||||||
|
FreeEntries();
|
||||||
|
m_entries.reserve(ENTRY_CHUNK_COUNT);
|
||||||
|
|
||||||
|
std::vector<NsApplicationRecord> record_list(ENTRY_CHUNK_COUNT);
|
||||||
|
s32 offset{};
|
||||||
|
while (true) {
|
||||||
|
s32 record_count{};
|
||||||
|
if (R_FAILED(nsListApplicationRecord(record_list.data(), record_list.size(), offset, &record_count))) {
|
||||||
|
log_write("failed to list application records at offset: %d\n", offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// finished parsing all entries.
|
||||||
|
if (!record_count) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (s32 i = 0; i < record_count; i++) {
|
||||||
|
const auto& e = record_list[i];
|
||||||
|
#if 0
|
||||||
|
u8 unk_x09 = e.unk_x09;
|
||||||
|
u64 unk_x0a;// = e.unk_x0a;
|
||||||
|
u8 unk_x10 = e.unk_x10;
|
||||||
|
u64 unk_x11;// = e.unk_x11;
|
||||||
|
memcpy(&unk_x0a, e.unk_x0a, sizeof(e.unk_x0a));
|
||||||
|
memcpy(&unk_x11, e.unk_x11, sizeof(e.unk_x11));
|
||||||
|
log_write("ID: %016lx got type: %u unk_x09: %u unk_x0a: %zu unk_x10: %u unk_x11: %zu\n", e.application_id, e.type,
|
||||||
|
unk_x09,
|
||||||
|
unk_x0a,
|
||||||
|
unk_x10,
|
||||||
|
unk_x11
|
||||||
|
);
|
||||||
|
#endif
|
||||||
|
if (hide_forwarders && (e.application_id & 0x0500000000000000) == 0x0500000000000000) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
s64 size{};
|
||||||
|
// code for sorting by size, it's too slow however...
|
||||||
|
#if 0
|
||||||
|
ApplicationOccupiedSize occupied_size;
|
||||||
|
if (R_SUCCEEDED(nsCalculateApplicationOccupiedSize(e.application_id, (NsApplicationOccupiedSize*)&occupied_size))) {
|
||||||
|
for (auto& s : occupied_size.entry) {
|
||||||
|
size += s.sizeApplication;
|
||||||
|
size += s.sizePatch;
|
||||||
|
size += s.sizeAddOnContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
m_entries.emplace_back(e.application_id, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += record_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_is_reversed = false;
|
||||||
|
m_dirty = false;
|
||||||
|
log_write("games found: %zu time_taken: %.2f seconds %zu ms %zu ns\n", m_entries.size(), ts.GetSecondsD(), ts.GetMs(), ts.GetNs());
|
||||||
|
this->Sort();
|
||||||
|
SetIndex(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Sort() {
|
||||||
|
// const auto sort = m_sort.Get();
|
||||||
|
const auto order = m_order.Get();
|
||||||
|
|
||||||
|
if (order == OrderType_Ascending) {
|
||||||
|
if (!m_is_reversed) {
|
||||||
|
std::reverse(m_entries.begin(), m_entries.end());
|
||||||
|
m_is_reversed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (m_is_reversed) {
|
||||||
|
std::reverse(m_entries.begin(), m_entries.end());
|
||||||
|
m_is_reversed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::SortAndFindLastFile() {
|
||||||
|
const auto app_id = m_entries[m_index].app_id;
|
||||||
|
Sort();
|
||||||
|
SetIndex(0);
|
||||||
|
|
||||||
|
s64 index = -1;
|
||||||
|
for (u64 i = 0; i < m_entries.size(); i++) {
|
||||||
|
if (app_id == m_entries[i].app_id) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
// guesstimate where the position is
|
||||||
|
if (index >= 9) {
|
||||||
|
m_list->SetYoff((((index - 9) + 3) / 3) * m_list->GetMaxY());
|
||||||
|
} else {
|
||||||
|
m_list->SetYoff(0);
|
||||||
|
}
|
||||||
|
SetIndex(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::FreeEntries() {
|
||||||
|
auto vg = App::GetVg();
|
||||||
|
|
||||||
|
for (auto&p : m_entries) {
|
||||||
|
FreeEntry(vg, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::game
|
||||||
534
sphaira/source/ui/menus/gc_menu.cpp
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::OnFocusGained() {
|
||||||
|
MenuBase::OnFocusGained();
|
||||||
|
|
||||||
|
GcOnEvent();
|
||||||
|
UpdateStorageSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (m_entries.size() > 1) {
|
||||||
|
SetAction(Button::L2, Action{"Prev"_i18n, [this](){
|
||||||
|
if (m_entry_index != 0) {
|
||||||
|
OnChangeIndex(m_entry_index - 1);
|
||||||
|
}
|
||||||
|
}});
|
||||||
|
SetAction(Button::R2, 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::L2);
|
||||||
|
RemoveAction(Button::R2);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -119,7 +119,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
|
|||||||
log_write("found zip\n");
|
log_write("found zip\n");
|
||||||
auto zfile = unzOpen64(temp_file);
|
auto zfile = unzOpen64(temp_file);
|
||||||
if (!zfile) {
|
if (!zfile) {
|
||||||
log_write("failed to open zip: %s\n", temp_file);
|
log_write("failed to open zip: %s\n", temp_file.s);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(unzClose(zfile));
|
ON_SCOPE_EXIT(unzClose(zfile));
|
||||||
@@ -155,29 +155,29 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry
|
|||||||
Result rc;
|
Result rc;
|
||||||
if (file_path[strlen(file_path) -1] == '/') {
|
if (file_path[strlen(file_path) -1] == '/') {
|
||||||
if (R_FAILED(rc = fs.CreateDirectoryRecursively(file_path)) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc = fs.CreateDirectoryRecursively(file_path)) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create folder: %s 0x%04X\n", file_path, rc);
|
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(file_path)) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc = fs.CreateDirectoryRecursivelyWithPath(file_path)) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create folder: %s 0x%04X\n", file_path, rc);
|
log_write("failed to create folder: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||||
log_write("failed to create file: %s 0x%04X\n", file_path, rc);
|
log_write("failed to create file: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
|
if (R_FAILED(rc = fs.OpenFile(file_path, FsOpenMode_Write, &f))) {
|
||||||
log_write("failed to open file: %s 0x%04X\n", file_path, rc);
|
log_write("failed to open file: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ON_SCOPE_EXIT(fsFileClose(&f));
|
ON_SCOPE_EXIT(fsFileClose(&f));
|
||||||
|
|
||||||
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
if (R_FAILED(rc = fsFileSetSize(&f, info.uncompressed_size))) {
|
||||||
log_write("failed to set file size: %s 0x%04X\n", file_path, rc);
|
log_write("failed to set file size: %s 0x%04X\n", file_path.s, rc);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|||||||