Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d50bcb650f | ||
|
|
4244be9592 | ||
|
|
a7fc19e28a | ||
|
|
cf192fca85 | ||
|
|
041bb2bbe5 | ||
|
|
df558d5dcc | ||
|
|
33de03a923 | ||
|
|
1000b9c8ec | ||
|
|
74fddecebc | ||
|
|
a64d4dce7a | ||
|
|
5daca4354c | ||
|
|
da9235f58e | ||
|
|
bd6566524c | ||
|
|
eadc46b0e4 | ||
|
|
71df5317be | ||
|
|
bd7eadc6a0 | ||
|
|
544272925d | ||
|
|
70a31be134 | ||
|
|
55ae2a63d9 | ||
|
|
5a53947a3e | ||
|
|
3bbb5ccb3c | ||
|
|
83472f1020 | ||
|
|
0167bf034c | ||
|
|
35abe363a6 | ||
|
|
97d3fd396e | ||
|
|
b98ccb927e | ||
|
|
db23f072a2 | ||
|
|
4d3d7e81d4 | ||
|
|
441807bc53 | ||
|
|
20e2d85843 | ||
|
|
e279a70606 | ||
|
|
5d9e24af31 | ||
|
|
078627e07b | ||
|
|
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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,6 +11,7 @@ old_code
|
||||
created_ncas
|
||||
assets/romfs/shaders
|
||||
.vscode/settings.json
|
||||
.idea
|
||||
info/
|
||||
romfs/shaders
|
||||
assets/unused
|
||||
@@ -22,3 +23,5 @@ libs/tweeny
|
||||
|
||||
compile_commands.json
|
||||
out
|
||||
|
||||
usb_test/
|
||||
|
||||
34
README.md
34
README.md
@@ -49,6 +49,40 @@ The `path` field is optional. If left out, it will use the name of the ini to fi
|
||||
|
||||
See `assets/romfs/assoc/` for more examples of file assoc entries.
|
||||
|
||||
## 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
|
||||
|
||||
- borealis
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/fbneo_libretro_libnx.nro
|
||||
supported_extensions=zip|7z|cue|ccd
|
||||
database=FBNeo - Arcade Games
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2000_libretro_libnx.nro
|
||||
supported_extensions=zip|7z
|
||||
database=MAME 2000
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2003_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=MAME 2003
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/mame2003_plus_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=MAME 2003-Plus
|
||||
use_base_name=true
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
path=/retroarch/cores/xrick_libretro_libnx.nro
|
||||
supported_extensions=zip
|
||||
database=Rick Dangerous
|
||||
use_base_name=true
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"[Applet Mode]": "[Applet-Modus]",
|
||||
"No Internet": "Keine Internetverbindung",
|
||||
"[Applet Mode]": " | Applet Modus |",
|
||||
"No Internet": "Kein Internet",
|
||||
"Files": "Dateien",
|
||||
"Apps": "Apps",
|
||||
"Store": "Store",
|
||||
"Apps": "hb-Apps",
|
||||
"Store": "hb-Store",
|
||||
"Menu": "Menü",
|
||||
"Options": "Optionen",
|
||||
"OK": "OK",
|
||||
"Back": "Zurück",
|
||||
"Select": "Auswählen",
|
||||
"Open": "Öffnen",
|
||||
"Launch": "Starten",
|
||||
"Open": "Öffne",
|
||||
"Launch": "Starte",
|
||||
"Info": "Info",
|
||||
"Install": "Installieren",
|
||||
"Delete": "Löschen",
|
||||
"Restart": "Neustart",
|
||||
"Changelog": "Changelog",
|
||||
"Changelog": "Neuerungen",
|
||||
"Details": "Details",
|
||||
"Update": "Update",
|
||||
"Remove": "Entfernen",
|
||||
@@ -23,62 +23,62 @@
|
||||
"Download": "Download",
|
||||
"Next Page": "Nächste Seite",
|
||||
"Prev Page": "Vorherige Seite",
|
||||
"Unstar": "Favorit entfernen",
|
||||
"Unstar": "Kein Favorit",
|
||||
"Star": "Favorit",
|
||||
"System memory": "System-Speicher",
|
||||
"microSD card": "microSD-Karte",
|
||||
"Sd": "SD",
|
||||
"Image System memory": "System-Speicher Bild",
|
||||
"Image microSD card": "microSD-Karten Bild",
|
||||
"Slow": "Langsam",
|
||||
"Normal": "Normal",
|
||||
"Fast": "Schnell",
|
||||
"System memory": "NAND Systemspeicher",
|
||||
"microSD card": "SD-Karte",
|
||||
"Sd": "SD-Karte | Root-Verzeichnis",
|
||||
"Image System memory": "Album | NAND Systemspeicher",
|
||||
"Image microSD card": "Album | SD-Karte",
|
||||
"Slow": "Niedrig",
|
||||
"Normal": "Mittel",
|
||||
"Fast": "Hoch",
|
||||
"Yes": "Ja",
|
||||
"No": "Nein",
|
||||
"Enabled": "Aktiviert",
|
||||
"Disabled": "Deaktiviert",
|
||||
"Enabled": "An",
|
||||
"Disabled": "Aus",
|
||||
|
||||
"Sort By": "Sortieren nach",
|
||||
"Sort Options": "Sortieroptionen",
|
||||
"Filter": "Filter",
|
||||
"Sort": "Sortieren",
|
||||
"Order": "Reihenfolge",
|
||||
"Sort By": "Sortierung",
|
||||
"Sort Options": " Sortierung | Optionen",
|
||||
"Filter": "Rubrik",
|
||||
"Sort": "Sortiert nach",
|
||||
"Order": "Anordnung",
|
||||
"Search": "Suchen",
|
||||
"Updated": "Aktualisiert",
|
||||
"Updated (Star)": "Aktualisiert (Favoriten)",
|
||||
"Updated": "zuletzt aktualisiert",
|
||||
"Updated (Star)": "Favorit | zuletzt aktualisiert",
|
||||
"Downloads": "Downloads",
|
||||
"Size": "Größe",
|
||||
"Size (Star)": "Größe (Favoriten)",
|
||||
"Alphabetical": "Alphabetisch",
|
||||
"Alphabetical (Star)": "Alphabetisch (Favoriten)",
|
||||
"Likes": "Likes",
|
||||
"ID": "ID",
|
||||
"Descending": "Absteigend",
|
||||
"Descending (down)": "Absteigend",
|
||||
"Desc": "Abst.",
|
||||
"Ascending": "Aufsteigend",
|
||||
"Ascending (Up)": "Aufsteigend",
|
||||
"Asc": "Aufst.",
|
||||
"Size (Star)": "Favorit | Größe",
|
||||
"Alphabetical": "Name",
|
||||
"Alphabetical (Star)": "Favorit | Name",
|
||||
"Likes": "Beliebtheit",
|
||||
"ID": "Theme | Paket ID",
|
||||
"Descending": "Absteigend ↓",
|
||||
"Descending (down)": "Absteigend ↓",
|
||||
"Desc": " ↓",
|
||||
"Ascending": "Aufsteigend ↑",
|
||||
"Ascending (Up)": "Aufsteigend ↑",
|
||||
"Asc": " ↑",
|
||||
|
||||
"Menu Options": "Menü-Optionen",
|
||||
"Theme": "Theme",
|
||||
"Theme Options": "Theme-Optionen",
|
||||
"Select Theme": "Theme auswählen",
|
||||
"Menu Options": " Menü | Optionen",
|
||||
"Theme": "Themes",
|
||||
"Theme Options": " Themes | Optionen",
|
||||
"Select Theme": "Theme wählen",
|
||||
"Shuffle": "Zufällig",
|
||||
"Music": "Musik",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Netzwerk",
|
||||
"Network Options": "Netzwerk-Optionen",
|
||||
"12 Hour Time": "12-Std Zeitformat",
|
||||
"Network": "Konnektivität",
|
||||
"Network Options": "Konnektivität | Optionen",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink verbunden",
|
||||
"Nxlink Upload": "Nxlink Upload",
|
||||
"Nxlink Finished": "Nxlink abgeschlossen",
|
||||
"Switch-Handheld!": "Switch-Handheld!",
|
||||
"Switch-Docked!": "Switch-Dock-Modus!",
|
||||
"Nxlink": "NXLink",
|
||||
"Nxlink Connected": "NXLink | Verbunden",
|
||||
"Nxlink Upload": "NXLink | wird hochgeladen...",
|
||||
"Nxlink Finished": "NXLink | Hochladen beendet",
|
||||
"Switch-Handheld!": "Handheld!",
|
||||
"Switch-Docked!": "Angedockt!",
|
||||
"Language": "Sprache",
|
||||
"Auto": "Auto",
|
||||
"Auto": "Systemsprache",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
@@ -87,55 +87,55 @@
|
||||
"Spanish": "Español",
|
||||
"Chinese": "中文",
|
||||
"Korean": "한국어",
|
||||
"Dutch": "Dutch",
|
||||
"Dutch": "Nederlands",
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Vietnamese",
|
||||
"Logging": "Logging",
|
||||
"Replace hbmenu on exit": "hbmenu beim Beenden ersetzen",
|
||||
"Misc": "Sonstiges",
|
||||
"Misc Options": "Weitere Optionen",
|
||||
"Web": "Web",
|
||||
"Vietnamese": "tiếng Việt",
|
||||
"Logging": "Protokollieren",
|
||||
"Replace hbmenu on exit": "hbmenu durch sphaira ersetzen",
|
||||
"Misc": "Extras",
|
||||
"Misc Options": " Extras | Optionen",
|
||||
"Web": "WEB Browser",
|
||||
"Install forwarders": "Forwarder installieren",
|
||||
"Install location": "Installationsort",
|
||||
"Show install warning": "Installationswarnung anzeigen",
|
||||
"Text scroll speed": "Textlaufgeschwindigkeit",
|
||||
"Install location": "Einhängepunkt",
|
||||
"Show install warning": "Warnungen anzeigen",
|
||||
"Text scroll speed": "Laufschrift Tempo",
|
||||
|
||||
"FileBrowser": "Datei-Browser",
|
||||
"FileBrowser": "Datei-Manager",
|
||||
"%zd files": "%zd Dateien",
|
||||
"%zd dirs": "%zd Ordner",
|
||||
"File Options": "Datei-Optionen",
|
||||
"Show Hidden": "Versteckte anzeigen",
|
||||
"File Options": "Datei - Ordner | Optionen",
|
||||
"Show Hidden": "Versteckte zeigen",
|
||||
"Folders First": "Ordner zuerst",
|
||||
"Hidden Last": "Versteckte zuletzt",
|
||||
"Cut": "Ausschneiden",
|
||||
"Copy": "Kopieren",
|
||||
"Paste": "Einfügen",
|
||||
"Paste ": "Einfügen ",
|
||||
" file(s)?": " Datei(en)?",
|
||||
"Paste ": "Einfügen von: ",
|
||||
" file(s)?": " Datei/en?",
|
||||
"Rename": "Umbenennen",
|
||||
"Set New File Name": "Neuen Dateinamen eingeben",
|
||||
"Advanced": "Erweitert",
|
||||
"Advanced Options": "Erweiterte Optionen",
|
||||
"Create File": "Datei erstellen",
|
||||
"Set File Name": "Dateinamen eingeben",
|
||||
"Create Folder": "Ordner erstellen",
|
||||
"Set Folder Name": "Ordnernamen eingeben",
|
||||
"View as text (unfinished)": "Als Text anzeigen (Beta)",
|
||||
"Ignore read only": "Schreibschutz ignorieren",
|
||||
"Mount": "Einbinden",
|
||||
"Empty...": "Leer...",
|
||||
"Open with DayBreak?": "Mit DayBreak öffnen?",
|
||||
"Launch ": "Starten ",
|
||||
"Launch option for: ": "Startoption für: ",
|
||||
"Select launcher for: ": "Launcher auswählen für: ",
|
||||
"Set New File Name": "Neuen Dateinamen festlegen",
|
||||
"Advanced": "Erweitert...",
|
||||
"Advanced Options": " Erweitert | Optionen",
|
||||
"Create File": "Neue Datei",
|
||||
"Set File Name": "Dateiname festlegen",
|
||||
"Create Folder": "Neuer Ordner",
|
||||
"Set Folder Name": "Ordner umbenennen",
|
||||
"View as text (unfinished)": "Als Text anzeigen",
|
||||
"Ignore read only": "Schreibschutz umgehen?",
|
||||
"Mount": "Einhängen",
|
||||
"Empty...": "Keine Daten...",
|
||||
"Open with DayBreak?": "Mit Daybreak öffnen?",
|
||||
"Launch ": "Starte ",
|
||||
"Launch option for: ": "Start Option für: ",
|
||||
"Select launcher for: ": "Wähle Launcher für: ",
|
||||
|
||||
"Homebrew": "Homebrew",
|
||||
"Homebrew Options": "Homebrew-Optionen",
|
||||
"Hide Sphaira": "Sphaira ausblenden",
|
||||
"Homebrew": "hbmenu",
|
||||
"Homebrew Options": " hbmenu | Optionen",
|
||||
"Hide Sphaira": "Verstecke sphaira",
|
||||
"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",
|
||||
"Creating Program": "Erstelle Programm",
|
||||
"Creating Control": "Erstelle Control",
|
||||
@@ -144,26 +144,26 @@
|
||||
"Updating ncm databse": "Aktualisiere NCM-Datenbank",
|
||||
"Pushing application record": "Übertrage Anwendungsdaten",
|
||||
"Installed!": "Installiert!",
|
||||
"Failed to install forwarder": "Forwarder-Installation fehlgeschlagen",
|
||||
"Failed to install forwarder": "Fehler beim installieren des Forwarders",
|
||||
"Unstarred ": "Favorit entfernt ",
|
||||
"Starred ": "Favorit hinzugefügt ",
|
||||
"Starred ": "Favorit ",
|
||||
|
||||
"AppStore": "AppStore",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filter: %s | Sortierung: %s | Reihenfolge: %s",
|
||||
"AppStore Options": "AppStore-Optionen",
|
||||
"All": "Alle",
|
||||
"AppStore": "hb-AppStore",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Rubrik: %s | Sort.nach.: %s | Ordnung: %s",
|
||||
"AppStore Options": " hb-AppStore | Optionen",
|
||||
"All": "Alles anzeigen",
|
||||
"Games": "Spiele",
|
||||
"Emulators": "Emulatoren",
|
||||
"Tools": "Tools",
|
||||
"Themes": "Themes",
|
||||
"Legacy": "Legacy",
|
||||
"Legacy": "Älteres",
|
||||
"version: %s": "Version: %s",
|
||||
"updated: %s": "Aktualisiert: %s",
|
||||
"category: %s": "Kategorie: %s",
|
||||
"extracted: %.2f MiB": "Entpackt: %.2f MiB",
|
||||
"app_dls: %s": "Downloads: %s",
|
||||
"More by Author": "Mehr vom Entwickler",
|
||||
"Leave Feedback": "Feedback geben",
|
||||
"updated: %s": "Letztes Update am: %s",
|
||||
"category: %s": "Rubrik: %s",
|
||||
"extracted: %.2f MiB": "Größe: %.2f MiB",
|
||||
"app_dls: %s": "Anzahl Downloads: %s",
|
||||
"More by Author": "Weitere Apps des Entwicklers",
|
||||
"Leave Feedback": "Feedback hinterlassen",
|
||||
|
||||
"Irs": "IR-Sensor",
|
||||
"Ambient Noise Level: ": "Umgebungsrauschen: ",
|
||||
@@ -203,55 +203,55 @@
|
||||
"External Light Filter": "Externes Lichtfilter",
|
||||
"Load Default": "Standard laden",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Themezer-Optionen",
|
||||
"Themezer": "Themezer | NX Themes",
|
||||
"Themezer Options": " Themezer | Optionen",
|
||||
"Nsfw": "NSFW",
|
||||
"Page": "Seite",
|
||||
"Page %zu / %zu": "Seite %zu / %zu",
|
||||
"Enter Page Number": "Seitenzahl eingeben",
|
||||
"Bad Page": "Ungültige Seite",
|
||||
"Page": "Seiten Nr. wählen ",
|
||||
"Page %zu / %zu": " %zu / %zu",
|
||||
"Enter Page Number": "Zu Seite Nr.: ___",
|
||||
"Bad Page": "Seite nicht gefunden",
|
||||
"Download theme?": "Theme herunterladen?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Lade JSON herunter",
|
||||
"Select asset to download for ": "Wähle Asset zum Download für ",
|
||||
"Downloading json": "Lade JSON-File",
|
||||
"Select asset to download for ": "Wähle Asset für den Download von ",
|
||||
|
||||
"Installing ": "Installiere ",
|
||||
"Uninstalling ": "Deinstalliere ",
|
||||
"Deleting ": "Lösche ",
|
||||
"Deleting": "Lösche",
|
||||
"Pasting ": "Füge ein ",
|
||||
"Pasting": "Füge ein",
|
||||
"Removing ": "Entferne ",
|
||||
"Scanning ": "Scanne ",
|
||||
"Creating ": "Erstelle ",
|
||||
"Copying ": "Kopiere ",
|
||||
"Trying to load ": "Lade ",
|
||||
"Downloading ": "Lade herunter ",
|
||||
"Downloaded ": "Heruntergeladen ",
|
||||
"Removed ": "Entfernt ",
|
||||
"Checking MD5": "Prüfe MD5",
|
||||
"Loading...": "Lade...",
|
||||
"Loading": "Lade",
|
||||
"Empty!": "Leer!",
|
||||
"Installing ": "Installiert wird: ",
|
||||
"Uninstalling ": "Deinstalliert wird: ",
|
||||
"Deleting ": "Gelöscht wird: ",
|
||||
"Deleting": "Gelöscht wurde:",
|
||||
"Pasting ": "Eingefügt wird: ",
|
||||
"Pasting": "Eingefügt wurde:",
|
||||
"Removing ": "Entfernt wird: ",
|
||||
"Scanning ": "Gescannt wird: ",
|
||||
"Creating ": "Erstellt wird: ",
|
||||
"Copying ": "Kopiert wird: ",
|
||||
"Trying to load ": "Versucht zu laden wird: ",
|
||||
"Downloading ": "Heruntergeladen wird: ",
|
||||
"Downloaded ": "Heruntergeladen wurde: ",
|
||||
"Removed ": "Entfernt wurde: ",
|
||||
"Checking MD5": "Checke MD5 Prüfsumme",
|
||||
"Loading...": "Wird geladen...",
|
||||
"Loading": "Wird geladen",
|
||||
"Empty!": "Keine Daten!",
|
||||
"Not Ready...": "Nicht bereit...",
|
||||
"Error loading page!": "Fehler beim Laden!",
|
||||
"Error loading page!": "Ladefehler!",
|
||||
"Update avaliable: ": "Update verfügbar: ",
|
||||
"Download update: ": "Update herunterladen: ",
|
||||
"Updated to ": "Aktualisiert auf ",
|
||||
"Press OK to restart Sphaira": "OK drücken um Sphaira neuzustarten",
|
||||
"Restart Sphaira?": "Sphaira neustarten?",
|
||||
"Failed to download update": "Update-Download fehlgeschlagen",
|
||||
"Download update: ": " Herunterladen des Updates: ",
|
||||
"Updated to ": "Aktualisiert auf: ",
|
||||
"Press OK to restart Sphaira": "Drücke OK um sphaira erneut zustarten",
|
||||
"Restart Sphaira?": "sphaira erneut starten?",
|
||||
"Failed to download update": "Herunterladen des Updates fehlgeschlagen!",
|
||||
"Restore hbmenu?": "hbmenu wiederherstellen?",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Konnte /switch/hbmenu.nro nicht finden\nBitte hbmenu über den AppStore neu installieren",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Wiederherstellung fehlgeschlagen, bitte hbmenu neu herunterladen",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Wiederherstellung fehlgeschlagen, verwende stattdessen Sphaira",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu wiederhergestellt, Sphaira wird beendet",
|
||||
"Restored hbmenu": "hbmenu wiederhergestellt",
|
||||
"Delete Selected files?": "Ausgewählte Dateien löschen?",
|
||||
"Completely remove ": "Vollständig entfernen ",
|
||||
"Are you sure you want to delete ": "Wirklich löschen ",
|
||||
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"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, please re-download hbmenu": "Fehler, hbmenu nicht wiederhergrstellt!\nInstalliere hbmenu über den hb-AppStore.",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Fehler, hbmenu nicht wiederhergrstellt!\nVerwende weiter sphaira",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu wurde wiederhergestellt, schließe sphaira",
|
||||
"Restored hbmenu": "hbmenu wurde wiederhergestellt",
|
||||
"Delete Selected files?": "Ausgewähle Dateien löschen?",
|
||||
"Completely remove ": "Komplett gelöscht wird: ",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"[Applet Mode]": "[Modo Applet]",
|
||||
"No Internet": "Sem Internet",
|
||||
"[Applet Mode]": "[Modo applet]",
|
||||
"No Internet": "Sem internet",
|
||||
"Files": "Arquivos",
|
||||
"Apps": "Aplicativos",
|
||||
"Store": "Loja",
|
||||
@@ -21,6 +21,8 @@
|
||||
"Remove": "Remover",
|
||||
"Restore": "Restaurar",
|
||||
"Download": "Baixar",
|
||||
"Next": "Prómixo",
|
||||
"Prev": "Anterior",
|
||||
"Next Page": "Próxima página",
|
||||
"Prev Page": "Página anterior",
|
||||
"Unstar": "Desfavoritar",
|
||||
@@ -35,6 +37,9 @@
|
||||
"Fast": "Rápida",
|
||||
"Yes": "Sim",
|
||||
"No": "Não",
|
||||
"On": "Sim",
|
||||
"Off": "Não",
|
||||
"Enable": "Habilitar",
|
||||
"Enabled": "Sim",
|
||||
"Disabled": "Não",
|
||||
|
||||
@@ -43,6 +48,7 @@
|
||||
"Filter": "Filtro",
|
||||
"Sort": "Organizar por",
|
||||
"Order": "Ordem",
|
||||
"Layout": "Exibição",
|
||||
"Search": "Buscar",
|
||||
"Updated": "Atualizado",
|
||||
"Updated (Star)": "Atualizado (favoritos)",
|
||||
@@ -59,6 +65,9 @@
|
||||
"Ascending": "Ascendente",
|
||||
"Ascending (Up)": "Ascendente (cima)",
|
||||
"Asc": "Asc.",
|
||||
"List": "Lista",
|
||||
"Icon": "Ícones",
|
||||
"Grid": "Grade",
|
||||
|
||||
"Menu Options": "Opções do menu",
|
||||
"Theme": "Tema",
|
||||
@@ -66,7 +75,8 @@
|
||||
"Select Theme": "Tema atual",
|
||||
"Shuffle": "Embaralhar temas",
|
||||
"Music": "Música",
|
||||
"12 Hour Time": "",
|
||||
"12 Hour Time": "Relógio de 12 horas",
|
||||
"Download Default Music": "Baixar música padrão",
|
||||
"Network": "Rede",
|
||||
"Network Options": "Opções de rede",
|
||||
"Ftp": "Servidor FTP",
|
||||
@@ -101,6 +111,7 @@
|
||||
"Install location": "Local de instalação",
|
||||
"Show install warning": "Mostrar aviso de instalação",
|
||||
"Text scroll speed": "Rolagem do texto",
|
||||
"Set right-side menu": "Menu do botão R",
|
||||
|
||||
"FileBrowser": "Arquivos",
|
||||
"%zd files": "%zd arquivo(s)",
|
||||
@@ -115,6 +126,7 @@
|
||||
"Paste ": "Colar ",
|
||||
" file(s)?": " arquivo(s)?",
|
||||
"Rename": "Renomear",
|
||||
"Compress to zip": "Comprimir em zip",
|
||||
"Set New File Name": "Defina o nome do novo arquivo",
|
||||
"Advanced": "Avançado",
|
||||
"Advanced Options": "Opções avançadas",
|
||||
@@ -125,18 +137,18 @@
|
||||
"View as text (unfinished)": "Ver como texto (inacabado)",
|
||||
"Ignore read only": "Ignorar somente leitura",
|
||||
"Mount": "Montar",
|
||||
"Empty...": "Vazio...",
|
||||
"Empty...": "Vazio",
|
||||
"Open with DayBreak?": "Abrir com DayBreak?",
|
||||
"Launch ": "Iniciar ",
|
||||
"Launch option for: ": "Opções de inicialização para: ",
|
||||
"Select launcher for: ": "Selecionar launcher para: ",
|
||||
|
||||
"Homebrew": "Aplicativos",
|
||||
"Homebrew Options": "Opções do aplicativo",
|
||||
"Homebrew Options": "Opções de aplicativo",
|
||||
"Hide Sphaira": "Esconder sphaira",
|
||||
"Install Forwarder": "Instalar atalho (forwarder)",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode\nresultar em um banimento!",
|
||||
"Installing Forwarder": "Instalando forwarder",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode resultar em um banimento!",
|
||||
"Installing Forwarder": "Instalando forwarder...",
|
||||
"Creating Program": "Criando Program",
|
||||
"Creating Control": "Criando Control",
|
||||
"Creating Meta": "Criando Meta",
|
||||
@@ -149,10 +161,11 @@
|
||||
"Starred ": "Favoritado ",
|
||||
|
||||
"AppStore": "Loja",
|
||||
"Appstore": "Loja",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Filtro: %s | Por: %s | Ordem: %s",
|
||||
"AppStore Options": "Opções da loja",
|
||||
"All": "Todos",
|
||||
"Games": "Jogos",
|
||||
"Games": "Softwares",
|
||||
"Emulators": "Emuladores",
|
||||
"Tools": "Ferramentas",
|
||||
"Themes": "Temas",
|
||||
@@ -164,8 +177,31 @@
|
||||
"app_dls: %s": "downloads: %s",
|
||||
"More by Author": "Mais deste autor",
|
||||
"Leave Feedback": "Deixar um feedback",
|
||||
|
||||
"Game Options": "Opções de software",
|
||||
"Launch random game": "Iniciar um software aleatório",
|
||||
"List meta records": "Registro de conteúdos",
|
||||
"Entries": "Entradas",
|
||||
"Hide forwarders": "Ocultar atalhos (forwarders)",
|
||||
|
||||
"Dump": "Exportar",
|
||||
"Select content to dump": "Exportação de conteúdo",
|
||||
"Dump All": "Exportar tudo",
|
||||
"Dump Application": "Exportar software base",
|
||||
"Dump Patch": "Exportar atualização",
|
||||
"Dump AddOnContent": "Exportar DLCs",
|
||||
"Dump DataPatch": "Exportar patch de dados",
|
||||
"Select dump location": "Selecione o local de exportação",
|
||||
"microSD card (/dumps/NSP/)": "Cartão microSD (/dump/NSP/)",
|
||||
"USB transfer (Switch 2 Switch)": "Transferência via USB (Switch 2 Switch)",
|
||||
"/dev/null (Speed Test)": "Teste de velocidade (/dev/null)",
|
||||
"Dumping": "Extraindo...",
|
||||
"Dump successfull!": "Extração foi concluída com sucesso.",
|
||||
"Dump failed!": "Extração falhou.",
|
||||
|
||||
"Irs": "Sensor infravermelho",
|
||||
"Irs": "Câmera de movimento IR",
|
||||
"IRS": "Câmera de movimento IR",
|
||||
"IRS (Infrared Joycon Camera)": "Câmera de movimento IR",
|
||||
"Ambient Noise Level: ": "Nível de ruído ambiente: ",
|
||||
"Controller": "Controle",
|
||||
"Pad ": "Pad ",
|
||||
@@ -216,10 +252,64 @@
|
||||
"Downloading json": "Baixando JSON",
|
||||
"Select asset to download for ": "Selecione o recurso para baixar de ",
|
||||
|
||||
"Install Options": "Opções de instalação",
|
||||
"Install options": "Opções de instalação",
|
||||
"Enable sysmmc": "Habilitar sysMMC",
|
||||
"Enable emummc": "Habilitar emuMMC",
|
||||
"Boost CPU clock": "Aumentar clock da CPU",
|
||||
"Allow downgrade": "Permitir downgrade",
|
||||
"Skip if already installed": "Pular se já instalado",
|
||||
"Ticket only": "Instalar apenas ticket",
|
||||
"Patch ticket": "Fazer patch de ticket",
|
||||
"Skip base": "Pular software base",
|
||||
"Skip patch": "Pular atualizações",
|
||||
"Skip dlc": "Pular DLCs",
|
||||
"Skip data patch": "Pular patch de dados",
|
||||
"Skip ticket": "Pular ticket",
|
||||
"skip NCA hash verify": "Pular checagem de hash NCA",
|
||||
"Skip RSA header verify": "Pular checagem de header RSA",
|
||||
"Skip RSA NPDM verify": "Pular checagem de NPDM RSA",
|
||||
"Ignore distribution bit": "Ignorar bit de distribuição",
|
||||
"Convert to standard crypto": "Convertr para crypto padrão",
|
||||
"Lower master key": "Reduzir a master key",
|
||||
"Lower system version": "Reduzir versão do sistema",
|
||||
"Install Selected files?": "Instalar os arquivos selecionados?",
|
||||
"Installed ": "Instalado ",
|
||||
"FTP Install": "Instalação via FTP",
|
||||
"USB Install": "Instalação via USB",
|
||||
"GameCard Install": "Instalação de cartão de jogo",
|
||||
|
||||
"FTP Install (EXPERIMENTAL)": "Instalação via FTP (EXPERIMENTAL)",
|
||||
"USB": "USB",
|
||||
"GameCard": "Cartão de jogo",
|
||||
"Disable MTP for usb install": "Escuta MTP desabilitada temporáriamente.",
|
||||
"Re-enabled MTP": "Escuta MTP reabilitada.",
|
||||
"Waiting for connection...": "Aguardando conexão...",
|
||||
"Transferring data...": "Transferindo dados...",
|
||||
"Ftp install success!": "Instalação via FTP concluída com sucesso.",
|
||||
"Ftp install failed!": "Instalação via FTP falhou.",
|
||||
"Usb install success!": "Instalação via USB concluída com sucesso.",
|
||||
"Usb install failed!": "Instalação via USB falhou.",
|
||||
"Gc install success!": "Instalação de cartão de jogo concluída com sucesso.",
|
||||
"Gc install failed!": "Instalação de cartão de jogo falhou.",
|
||||
"Installed via usb": "Instalado via USB",
|
||||
"Failed to install via FTP, press B to exit...": "Falha ao instalar via FTP,\naperte B para sair.",
|
||||
"Failed to init usb, press B to exit...": "Falha ao instalar via USB,\naperte B para sair.",
|
||||
"Press B to exit...": "Aperte B para sair.",
|
||||
"Connection Type: WiFi | Strength: ": "Conexão por rede Wi-Fi | Intensidade do sinal: ",
|
||||
"Connection Type: Ethernet": "Conexão por cabo (ethernet)",
|
||||
"Connection Type: None": "Sem conexão",
|
||||
"Host:": "Host:",
|
||||
"Port:": "Porta:",
|
||||
"Username:": "Nome de usuário:",
|
||||
"Password:": "Senha:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "Senha:",
|
||||
|
||||
"Installing ": "Instalando ",
|
||||
"Uninstalling ": "Desinstalando ",
|
||||
"Deleting ": "Excluindo ",
|
||||
"Deleting": "Excluindo",
|
||||
"Deleting": "Excluindo...",
|
||||
"Pasting ": "Colando ",
|
||||
"Pasting": "Colando ",
|
||||
"Removing ": "Removendo ",
|
||||
@@ -235,23 +325,26 @@
|
||||
"Loading": "Carregando",
|
||||
"Empty!": "Vazio",
|
||||
"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: ",
|
||||
"Download update: ": "Baixar autalização: ",
|
||||
"Updated to ": "Atualizado para ",
|
||||
"Press OK to restart Sphaira": "Selecione OK para reiniciar o 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?",
|
||||
"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 restore hbmenu, please re-download hbmenu": "Falha ao restaurar o hbmenu, baixe o hbmenu novamente",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira",
|
||||
"Restored hbmenu": "hbmenu restaurado",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Falha ao buscar /switch/hbmenu.nro\nUse a loja (AppStore) para reinstalar o hbmenu.",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Falha ao restaurar o hbmenu, baixe o hbmenu novamente.",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Falha ao restaurar hbmenu, usando sphaira.",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu restaurado, fechando sphaira.",
|
||||
"Restored hbmenu": "hbmenu restaurado.",
|
||||
"Delete Selected files?": "Excluir os arquivos selecionados?",
|
||||
"Completely remove ": "Remover completamente ",
|
||||
"Delete successfull!": "Exclusão foi concluída com sucesso.",
|
||||
"Delete failed!": "Exclusão falhou",
|
||||
"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?",
|
||||
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso",
|
||||
"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."
|
||||
|
||||
}
|
||||
|
||||
326
assets/romfs/i18n/uk.json
Normal file
326
assets/romfs/i18n/uk.json
Normal file
@@ -0,0 +1,326 @@
|
||||
{
|
||||
"[Applet Mode]": "[Режим Аплету]",
|
||||
"No Internet": "Без інтернету",
|
||||
"Files": "Файли",
|
||||
"Apps": "Програми",
|
||||
"Store": "Магазин",
|
||||
"Menu": "Меню",
|
||||
"Options": "Налаштування",
|
||||
"OK": "ОК",
|
||||
"Back": "Назад",
|
||||
"Select": "Вибрати",
|
||||
"Open": "Відкрити",
|
||||
"Launch": "Запустити",
|
||||
"Info": "Інфо",
|
||||
"Install": "Встановити",
|
||||
"Delete": "Видалити",
|
||||
"Restart": "Перезапустити",
|
||||
"Changelog": "Журнал змін",
|
||||
"Details": "Деталі",
|
||||
"Update": "Оновити",
|
||||
"Remove": "Видалити",
|
||||
"Restore": "Відновити",
|
||||
"Download": "Завантажити",
|
||||
"Next": "Наступний",
|
||||
"Prev": "Попередній",
|
||||
"Next Page": "Наступна сторінка",
|
||||
"Prev Page": "Попередня сторінка",
|
||||
"Unstar": "Прибрати з обраного",
|
||||
"Star": "Позначити зіркою",
|
||||
"System memory": "Пам'ять консолі",
|
||||
"microSD card": "SD-карта",
|
||||
"Sd": "SD-карта",
|
||||
"Image System memory": "Фото | Пам'ять консолі",
|
||||
"Image microSD card": "Фото | SD-карта",
|
||||
"Slow": "Повільно",
|
||||
"Normal": "Нормально",
|
||||
"Fast": "Швидко",
|
||||
"Yes": "Так",
|
||||
"No": "Ні",
|
||||
"On": "Увімк.",
|
||||
"Off": "Вимк.",
|
||||
"Enable": "Увімк.",
|
||||
"Enabled": "Увімк.",
|
||||
"Disabled": "Вимк.",
|
||||
|
||||
"Sort By": "Сортувати за",
|
||||
"Sort Options": "Опції сортування",
|
||||
"Filter": "Фільтр",
|
||||
"Sort": "Сортування",
|
||||
"Order": "Порядок",
|
||||
"Search": "Пошук",
|
||||
"Updated": "Оновлено",
|
||||
"Updated (Star)": "Оновлено (Зірка)",
|
||||
"Downloads": "Завантаження",
|
||||
"Size": "Розмір",
|
||||
"Size (Star)": "Розмір (Зірка)",
|
||||
"Alphabetical": "За алфавітом",
|
||||
"Alphabetical (Star)": "За алфавітом (Зірка)",
|
||||
"Likes": "Вподобання",
|
||||
"ID": "ID",
|
||||
"Descending": "За спаданням",
|
||||
"Descending (down)": "За спаданням (вниз)",
|
||||
"Desc": "Спад.",
|
||||
"Ascending": "За зростанням",
|
||||
"Ascending (Up)": "За зростанням (вгору)",
|
||||
"Asc": "Зрост.",
|
||||
|
||||
"Menu Options": "Опції меню",
|
||||
"Theme": "Тема",
|
||||
"Theme Options": "Опції теми",
|
||||
"Select Theme": "Вибрати тему",
|
||||
"Shuffle": "Перемішати",
|
||||
"Music": "Музика",
|
||||
"12 Hour Time": "12-годинний формат часу",
|
||||
"Download Default Music": "Завантажити музику за замовчуванням",
|
||||
"Network": "Мережа",
|
||||
"Network Options": "Опції мережі",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink підключено",
|
||||
"Nxlink Upload": "Nxlink | Завантаження",
|
||||
"Nxlink Finished": "Nxlink | Завершено",
|
||||
"Switch-Handheld!": "Switch - Портатив!",
|
||||
"Switch-Docked!": "Switch - Докований!",
|
||||
"Language": "Мова",
|
||||
"Auto": "Автоматично",
|
||||
"English": "English",
|
||||
"Japanese": "日本語",
|
||||
"French": "Français",
|
||||
"German": "Deutsch",
|
||||
"Italian": "Italiano",
|
||||
"Spanish": "Español",
|
||||
"Chinese": "中文",
|
||||
"Korean": "한국어",
|
||||
"Dutch": "Nederlands",
|
||||
"Portuguese": "Português",
|
||||
"Russian": "Русский",
|
||||
"Swedish": "Svenska",
|
||||
"Vietnamese": "Tiếng Việt",
|
||||
"Logging": "Логування",
|
||||
"Replace hbmenu on exit": "Заміна hbmenu при виході",
|
||||
"Misc": "Різне",
|
||||
"Misc Options": "Опції різного",
|
||||
"Web": "Веб",
|
||||
"Install forwarders": "Встановити форвардери",
|
||||
"Install location": "Місце встановлення",
|
||||
"Show install warning": "Попередж. при встанов.",
|
||||
"Text scroll speed": "Швидк. прокрутки",
|
||||
"Set right-side menu": "Праве меню",
|
||||
|
||||
"FileBrowser": "Файловий менеджер",
|
||||
"%zd files": "%zd файл(и)",
|
||||
"%zd dirs": "%zd тек(и)",
|
||||
"File Options": "Опції файлів",
|
||||
"Show Hidden": "Показати приховані",
|
||||
"Folders First": "Теки спочатку",
|
||||
"Hidden Last": "Приховані в кінці",
|
||||
"Cut": "Вирізати",
|
||||
"Copy": "Копіювати",
|
||||
"Paste": "Вставити",
|
||||
"Paste ": "Вставити: ",
|
||||
" file(s)?": " файл(и)?",
|
||||
"Rename": "Перейменувати",
|
||||
"Compress to zip": "Стиснути в zip",
|
||||
"Set New File Name": "Введіть нове ім'я файлу",
|
||||
"Advanced": "Додатково",
|
||||
"Advanced Options": "Додаткові опції",
|
||||
"Create File": "Створити файл",
|
||||
"Set File Name": "Введіть ім'я файлу",
|
||||
"Create Folder": "Створити теку",
|
||||
"Set Folder Name": "Введіть ім'я теки",
|
||||
"View as text (unfinished)": "Переглянути як текст (незавершено)",
|
||||
"Ignore read only": "Ігнорувати лише читання",
|
||||
"Mount": "Монтувати",
|
||||
"Empty...": "Пусто...",
|
||||
"Open with DayBreak?": "Відкрити за допомогою DayBreak?",
|
||||
"Launch ": "Запустити ",
|
||||
"Launch option for: ": "Опція запуску для: ",
|
||||
"Select launcher for: ": "Виберіть лаунчер для: ",
|
||||
|
||||
"Homebrew": "Домашні програми",
|
||||
"Homebrew Options": "Опції домашніх програм",
|
||||
"Hide Sphaira": "Приховати Sphaira",
|
||||
"Install Forwarder": "Встановити форвардер",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "УВАГА: Встановлення форвардерів може призвести до бану!",
|
||||
"Installing Forwarder": "Встановлення форвардера",
|
||||
"Creating Program": "Створення програми",
|
||||
"Creating Control": "Створення контролера",
|
||||
"Creating Meta": "Створення метаданих",
|
||||
"Writing Nca": "Запис NCA",
|
||||
"Updating ncm databse": "Оновлення бази даних NCM",
|
||||
"Pushing application record": "Запис даних програми",
|
||||
"Installed!": "Встановлено!",
|
||||
"Failed to install forwarder": "Не вдалося встановити форвардер",
|
||||
"Unstarred ": "Знято зірку з ",
|
||||
"Starred ": "Позначено зіркою ",
|
||||
|
||||
"AppStore": "Магазин програм",
|
||||
"Filter: %s | Sort: %s | Order: %s": "Фільтр: %s | Сорт.: %s | Порядок: %s",
|
||||
"AppStore Options": "Опції магазину програм",
|
||||
"All": "Всі",
|
||||
"Games": "Ігри",
|
||||
"Emulators": "Емулятори",
|
||||
"Tools": "Інструменти",
|
||||
"Themes": "Теми",
|
||||
"Legacy": "Доступні оновлення",
|
||||
"version: %s": "версія: %s",
|
||||
"updated: %s": "оновлено: %s",
|
||||
"category: %s": "категорія: %s",
|
||||
"extracted: %.2f MiB": "розмір: %.2f MiB",
|
||||
"app_dls: %s": "завантажень: %s",
|
||||
"More by Author": "Більше від автора",
|
||||
"Leave Feedback": "Залишити відгук",
|
||||
|
||||
"Game Options": "Опції ігор",
|
||||
"Launch random game": "Запустити випадкову гру",
|
||||
"List meta records": "Список метаданих записів",
|
||||
"Entries": "Записи",
|
||||
"Delete entity": "Видалити сутність",
|
||||
"Hide forwarders": "Приховати форвардери",
|
||||
|
||||
"Irs": "ІЧ-сенсор",
|
||||
"IRS (Infrared Joycon Camera)": "ІЧ (Інфрачервона камера Joycon)",
|
||||
"Ambient Noise Level: ": "Рівень навколишнього шуму: ",
|
||||
"Controller": "Контролер",
|
||||
"Pad ": "Геймпад ",
|
||||
" (Available)": " (Доступно)",
|
||||
" (Unsupported)": " (Не підтримується)",
|
||||
" (Unconnected)": " (Не підключено)",
|
||||
"HandHeld": "Портативний режим",
|
||||
"Rotation": "Обертання",
|
||||
"0 (Sideways)": "0° (Збоку)",
|
||||
"90 (Flat)": "90° (Плоско)",
|
||||
"180 (-Sideways)": "180° (-Збоку)",
|
||||
"270 (Upside down)": "270° (Догори дном)",
|
||||
"Colour": "Колір",
|
||||
"Grey": "Сірий",
|
||||
"Ironbow": "Ironbow",
|
||||
"Green": "Зелений",
|
||||
"Red": "Червоний",
|
||||
"Blue": "Синій",
|
||||
"Light Target": "Ціль освітлення",
|
||||
"All leds": "Всі світлодіоди",
|
||||
"Bright group": "Яскрава група",
|
||||
"Dim group": "Тьмяна група",
|
||||
"None": "Немає",
|
||||
"Gain": "Підсилення",
|
||||
"Negative Image": "Негативне зображення",
|
||||
"Normal image": "Нормальне зображення",
|
||||
"Trimming Format": "Формат обрізки",
|
||||
"External Light Filter": "Фільтр зовнішнього освітлення",
|
||||
"Load Default": "Завантажити типові",
|
||||
"Format": "Формат",
|
||||
"320x240": "320×240",
|
||||
"160x120": "160×120",
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Опції Themezer",
|
||||
"Nsfw": "NSFW",
|
||||
"Page": "Сторінка",
|
||||
"Page %zu / %zu": "Сторінка %zu / %zu",
|
||||
"Enter Page Number": "Введіть номер сторінки",
|
||||
"Bad Page": "Неправильна сторінка",
|
||||
"Download theme?": "Завантажити тему?",
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Завантаження JSON",
|
||||
"Select asset to download for ": "Виберіть ресурс для завантаження для ",
|
||||
|
||||
"Install Options": "Опції встановлення",
|
||||
"Install options": "Опції встановлення",
|
||||
"Boost CPU clock": "Розігнати CPU",
|
||||
"Allow downgrade": "Дозволити відкат",
|
||||
"Skip if already installed": "Пропуск, якщо встановл.",
|
||||
"Ticket only": "Тільки тікет",
|
||||
"Patch ticket": "Змінити тікет",
|
||||
"Skip base": "Пропустити базу",
|
||||
"Skip patch": "Пропустити патч",
|
||||
"Skip dlc": "Пропустити DLC",
|
||||
"Skip data patch": "Пропустити патч даних",
|
||||
"Skip ticket": "Пропустити тікет",
|
||||
"skip NCA hash verify": "Пропуск перевірку хешу NCA",
|
||||
"Skip RSA header verify": "Пропуск перевірку заголовка RSA",
|
||||
"Skip RSA NPDM verify": "Пропуск перевірку NPDM RSA",
|
||||
"Ignore distribution bit": "Ігнорувати біт розподілу",
|
||||
"Convert to standard crypto": "Конвертувати у стандартне шифрування",
|
||||
"Lower master key": "Знизити майстер-ключ",
|
||||
"Lower system version": "Знизити версію системи",
|
||||
"Install Selected files?": "Встановити вибрані файли?",
|
||||
"Installed": "Встановлено",
|
||||
"Installed ": "Встановлено ",
|
||||
"FTP Install": "Встановлення через FTP",
|
||||
"USB Install": "Встановлення через USB",
|
||||
"GameCard Install": "Встановлення з картриджа",
|
||||
|
||||
"FTP Install (EXPERIMENTAL)": "Встановлення через FTP (ЕКСПЕРИМЕНТАЛЬНО)",
|
||||
"USB": "USB",
|
||||
"GameCard": "Картридж",
|
||||
"Disable MTP for usb install": "Вимкнути MTP для встановлення через USB",
|
||||
"Re-enabled MTP": "MTP знову увімкнено",
|
||||
"Waiting for connection...": "Очікування підключення...",
|
||||
"Transferring data...": "Передача даних...",
|
||||
"Ftp install success!": "Встановлення через FTP успішно завершено.",
|
||||
"Ftp install failed!": "Встановлення через FTP не вдалося.",
|
||||
"Usb install success!": "Встановлення через USB успішно завершено.",
|
||||
"Usb install failed!": "Встановлення через USB не вдалося.",
|
||||
"Gc install success!": "Встановлення з картриджа успішно завершено.",
|
||||
"Gc install failed!": "Встановлення з картриджа не вдалося.",
|
||||
"Installed via usb": "Встановлено через USB",
|
||||
"Failed to install via FTP, press B to exit...": "Не вдалося встановити через FTP, натисніть B для виходу...",
|
||||
"Failed to init usb, press B to exit...": "Не вдалося ініціалізувати USB, натисніть B для виходу...",
|
||||
"Press B to exit...": "Натисніть B для виходу...",
|
||||
"Connection Type: WiFi | Strength:": "Тип підключення: WiFi | Сила сигналу:",
|
||||
"Connection Type: WiFi | Strength: ": "Тип підключення: WiFi | Сила сигналу: ",
|
||||
"Connection Type: Ethernet": "Тип підключення: Ethernet",
|
||||
"Connection Type: None": "Тип підключення: Немає",
|
||||
"Host:": "Хост:",
|
||||
"Port:": "Порт:",
|
||||
"Username:": "Ім'я користувача:",
|
||||
"Password:": "Пароль:",
|
||||
"SSID:": "SSID:",
|
||||
"Passphrase:": "Кодова фраза:",
|
||||
|
||||
"Installing ": "Встановлення ",
|
||||
"Uninstalling ": "Видалення ",
|
||||
"Deleting ": "Видалення ",
|
||||
"Deleting": "Видалення",
|
||||
"Pasting ": "Вставлення ",
|
||||
"Pasting": "Вставлення",
|
||||
"Removing ": "Видалення ",
|
||||
"Scanning ": "Сканування ",
|
||||
"Creating ": "Створення ",
|
||||
"Copying ": "Копіювання ",
|
||||
"Trying to load ": "Спроба завантажити ",
|
||||
"Downloading ": "Завантаження ",
|
||||
"Downloaded ": "Завантажено ",
|
||||
"Removed ": "Видалено ",
|
||||
"Checking MD5": "Перевірка MD5",
|
||||
"Loading...": "Завантаження...",
|
||||
"Loading": "Завантаження",
|
||||
"Empty!": "Пусто!",
|
||||
"Not Ready...": "Не готово...",
|
||||
"Error loading page!": "Помилка завантаження сторінки!",
|
||||
"Update avaliable: ": "Доступне оновлення: ",
|
||||
"Download update: ": "Завантажити оновлення: ",
|
||||
"Updated to ": "Оновлено до ",
|
||||
"Press OK to restart Sphaira": "Натисніть OK для перезапуску Sphaira",
|
||||
"Restart Sphaira?": "Перезапустити Sphaira?",
|
||||
"Failed to download update": "Не вдалося завантажити оновлення",
|
||||
"Restore hbmenu?": "Відновити hbmenu?",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Не вдалося знайти /switch/hbmenu.nro\nВикористовуйте Магазин програм для перевстановлення hbmenu",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Не вдалося відновити hbmenu, будь ласка, завантажте hbmenu знову",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Не вдалося відновити hbmenu, замість цього використовується Sphaira",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu відновлено, закриття Sphaira",
|
||||
"Restored hbmenu": "hbmenu відновлено",
|
||||
"Delete Selected files?": "Видалити вибрані файли?",
|
||||
"Completely remove ": "Повністю видалити ",
|
||||
"Are you sure you want to delete ": "Ви впевнені, що хочете видалити ",
|
||||
"Are you sure you wish to cancel?": "Ви впевнені, що хочете скасувати?",
|
||||
"Audio disabled due to suspended game": "Аудіо вимкнено через призупинену програму",
|
||||
"If this message appears repeatedly, please open an issue.": "Якщо це повідомлення з'являється повторно, будь ласка, повідомте про проблему."
|
||||
}
|
||||
@@ -21,6 +21,8 @@
|
||||
"Remove": "删除",
|
||||
"Restore": "恢复",
|
||||
"Download": "下载",
|
||||
"Next": "下一项",
|
||||
"Prev": "上一项",
|
||||
"Next Page": "下一页",
|
||||
"Prev Page": "上一页",
|
||||
"Unstar": "取消星标",
|
||||
@@ -66,7 +68,8 @@
|
||||
"Select Theme": "选择主题",
|
||||
"Shuffle": "随机播放",
|
||||
"Music": "音乐",
|
||||
"12 Hour Time": "",
|
||||
"12 Hour Time": "12小时制时间",
|
||||
"Download Default Music": "下载默认音乐",
|
||||
"Network": "网络",
|
||||
"Network Options": "网络选项",
|
||||
"Ftp": "FTP",
|
||||
@@ -115,6 +118,7 @@
|
||||
"Paste ": "粘贴 ",
|
||||
" file(s)?": "个文件(夹)?",
|
||||
"Rename": "重命名",
|
||||
"Compress to zip": "压缩到zip",
|
||||
"Set New File Name": "输入新命名",
|
||||
"Advanced": "高级",
|
||||
"Advanced Options": "高级选项",
|
||||
@@ -165,12 +169,19 @@
|
||||
"More by Author": "作者更多作品",
|
||||
"Leave Feedback": "留言反馈",
|
||||
|
||||
"Game Options": "游戏选项",
|
||||
"Launch random game": "开启随机游戏",
|
||||
"List meta records": "列出元数据记录",
|
||||
"Entries": "条目",
|
||||
"Delete entity": "删除整体",
|
||||
"Hide forwarders": "隐藏前端启动",
|
||||
|
||||
"Irs": "红外成像",
|
||||
"Ambient Noise Level: ": "环境噪声等级:",
|
||||
"Controller": "控制器",
|
||||
"Pad ": "手柄 ",
|
||||
" (Available)": " (可用的)",
|
||||
" (Unsupported)": "",
|
||||
" (Unsupported)": " (不支持的)",
|
||||
" (Unconnected)": " (未连接)",
|
||||
"HandHeld": "掌机模式",
|
||||
"Rotation": "旋转",
|
||||
@@ -216,6 +227,58 @@
|
||||
"Downloading json": "正在下载 json",
|
||||
"Select asset to download for ": "选择要下载的资源用于 ",
|
||||
|
||||
"Install Options": "安装选项",
|
||||
"Install options": "安装选项",
|
||||
"Boost CPU clock": "提升 CPU 频率",
|
||||
"Allow downgrade": "允许降级",
|
||||
"Skip if already installed": "若已安装则跳过",
|
||||
"Ticket only": "仅安装票据",
|
||||
"Patch ticket": "修补票据",
|
||||
"Skip base": "跳过基础部分",
|
||||
"Skip patch": "跳过补丁",
|
||||
"Skip dlc": "跳过 DLC(可下载内容)",
|
||||
"Skip data patch": "跳过数据补丁",
|
||||
"Skip ticket": "跳过票据",
|
||||
"skip NCA hash verify": "跳过 NCA 哈希验证",
|
||||
"Skip RSA header verify": "跳过 RSA 头部验证",
|
||||
"Skip RSA NPDM verify": "跳过 RSA NPDM 验证",
|
||||
"Ignore distribution bit": "忽略分布位",
|
||||
"Convert to standard crypto": "转换为标准加密方式",
|
||||
"Lower master key": "降低主密钥",
|
||||
"Lower system version": "降低系统版本",
|
||||
"Install Selected files?": "安装所选文件?",
|
||||
"Installed": "已安装",
|
||||
"FTP Install": "通过 FTP 安装",
|
||||
"USB Install": "通过 USB 安装",
|
||||
"GameCard Install": "卡带安装",
|
||||
|
||||
"FTP Install (EXPERIMENTAL)": "通过 FTP 安装(实验性)",
|
||||
"USB": "USB",
|
||||
"GameCard": "卡带",
|
||||
"Disable MTP for usb install": "暂时禁用 USB 安装的 MTP 功能",
|
||||
"Re-enabled MTP": "重新启用 MTP",
|
||||
"Waiting for connection...": "等待连接中...",
|
||||
"Transferring data...": "正在传输数据...",
|
||||
"Ftp install success!": "通过 FTP 安装成功。",
|
||||
"Ftp install failed!": "通过 FTP 安装失败。",
|
||||
"Usb install success!": "通过 USB 安装成功。",
|
||||
"Usb install failed!": "通过 USB 安装失败。",
|
||||
"Gc install success!": "游戏安装成功。",
|
||||
"Gc install failed!": "游戏安装失败。",
|
||||
"Installed via usb": "通过 USB 安装",
|
||||
"Failed to install via FTP, press B to exit...": "通过 FTP 安装失败,按 B 键退出...",
|
||||
"Failed to init usb, press B to exit...": "USB 初始化失败,按 B 键退出...",
|
||||
"Press B to exit...": "按 B 键退出...",
|
||||
"Connection Type: WiFi | Strength:": "连接类型:WiFi | 信号强度:",
|
||||
"Connection Type: Ethernet": "连接类型:以太网",
|
||||
"Connection Type: None": "连接类型:无",
|
||||
"Host:": "主机:",
|
||||
"Port:": "端口:",
|
||||
"Username:": "用户名:",
|
||||
"Password:": "密码:",
|
||||
"SSID:": "网络名称:",
|
||||
"Passphrase:": "密码:",
|
||||
|
||||
"Installing ": "正在安装 ",
|
||||
"Uninstalling ": "正在卸载 ",
|
||||
"Deleting ": "正在删除 ",
|
||||
@@ -239,7 +302,7 @@
|
||||
"Update avaliable: ": "有可用更新!",
|
||||
"Download update: ": "下载更新:",
|
||||
"Updated to ": "更新至 ",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Press OK to restart Sphaira": "按OK键以重启shphaira菜单",
|
||||
"Restart Sphaira?": "重启 Sphaira?",
|
||||
"Failed to download update": "更新下载失败",
|
||||
"Restore hbmenu?": "恢复 hbmenu?",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB |
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(sphaira_VERSION 0.6.3)
|
||||
set(sphaira_VERSION 0.10.3)
|
||||
|
||||
project(sphaira
|
||||
VERSION ${sphaira_VERSION}
|
||||
@@ -46,6 +46,11 @@ add_executable(sphaira
|
||||
source/ui/menus/menu_base.cpp
|
||||
source/ui/menus/themezer.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/menus/grid_menu_base.cpp
|
||||
|
||||
source/ui/error_box.cpp
|
||||
source/ui/notification.cpp
|
||||
@@ -57,7 +62,7 @@ add_executable(sphaira
|
||||
source/ui/sidebar.cpp
|
||||
source/ui/widget.cpp
|
||||
source/ui/list.cpp
|
||||
source/ui/bubbles.cpp
|
||||
source/ui/scrolling_text.cpp
|
||||
|
||||
source/app.cpp
|
||||
source/download.cpp
|
||||
@@ -65,20 +70,42 @@ add_executable(sphaira
|
||||
source/evman.cpp
|
||||
source/fs.cpp
|
||||
source/image.cpp
|
||||
source/location.cpp
|
||||
source/log.cpp
|
||||
source/main.cpp
|
||||
source/nro.cpp
|
||||
source/nxlink.cpp
|
||||
source/owo.cpp
|
||||
source/swkbd.cpp
|
||||
source/web.cpp
|
||||
source/i18n.cpp
|
||||
source/ftpsrv_helper.cpp
|
||||
|
||||
source/usb/base.cpp
|
||||
source/usb/usbds.cpp
|
||||
source/usb/usbhs.cpp
|
||||
source/usb/usb_uploader.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
|
||||
-DAPP_VERSION="${sphaira_VERSION}"
|
||||
-DAPP_VERSION_HASH="${sphaira_VERSION_HASH}"
|
||||
-DCURL_NO_OLDIES=1
|
||||
)
|
||||
|
||||
target_compile_options(sphaira PRIVATE
|
||||
@@ -127,13 +154,14 @@ set(FETCHCONTENT_QUIET FALSE)
|
||||
|
||||
FetchContent_Declare(ftpsrv
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/ftpsrv.git
|
||||
GIT_TAG 1.2.2
|
||||
# GIT_TAG 1.2.2
|
||||
GIT_TAG f8a30fd
|
||||
SOURCE_SUBDIR NONE
|
||||
)
|
||||
|
||||
FetchContent_Declare(libhaze
|
||||
GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git
|
||||
GIT_TAG 3244b9e
|
||||
GIT_TAG 04f1526
|
||||
)
|
||||
|
||||
FetchContent_Declare(libpulsar
|
||||
@@ -161,6 +189,24 @@ FetchContent_Declare(minIni
|
||||
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_USE_STDIO ON)
|
||||
set(MININI_USE_NX ON)
|
||||
@@ -195,6 +241,7 @@ FetchContent_MakeAvailable(
|
||||
stb
|
||||
minIni
|
||||
yyjson
|
||||
zstd
|
||||
)
|
||||
|
||||
set(FTPSRV_LIB_BUILD TRUE)
|
||||
@@ -238,31 +285,6 @@ if (USE_VFS_GC)
|
||||
)
|
||||
endif()
|
||||
|
||||
# todo: upstream cmake
|
||||
add_library(libhaze
|
||||
${libhaze_SOURCE_DIR}/source/async_usb_server.cpp
|
||||
${libhaze_SOURCE_DIR}/source/device_properties.cpp
|
||||
${libhaze_SOURCE_DIR}/source/event_reactor.cpp
|
||||
${libhaze_SOURCE_DIR}/source/haze.cpp
|
||||
${libhaze_SOURCE_DIR}/source/ptp_object_database.cpp
|
||||
${libhaze_SOURCE_DIR}/source/ptp_object_heap.cpp
|
||||
${libhaze_SOURCE_DIR}/source/ptp_responder_android_operations.cpp
|
||||
${libhaze_SOURCE_DIR}/source/ptp_responder_mtp_operations.cpp
|
||||
${libhaze_SOURCE_DIR}/source/ptp_responder_ptp_operations.cpp
|
||||
${libhaze_SOURCE_DIR}/source/ptp_responder.cpp
|
||||
${libhaze_SOURCE_DIR}/source/usb_session.cpp
|
||||
)
|
||||
target_include_directories(libhaze PUBLIC ${libhaze_SOURCE_DIR}/include)
|
||||
set_target_properties(libhaze PROPERTIES
|
||||
C_STANDARD 11
|
||||
C_EXTENSIONS ON
|
||||
CXX_STANDARD 20
|
||||
CXX_EXTENSIONS ON
|
||||
# force optimisations in debug mode as otherwise vapor errors
|
||||
# due to force_inline attribute failing...
|
||||
COMPILE_OPTIONS "$<$<CONFIG:Debug>:-Os>"
|
||||
)
|
||||
|
||||
add_library(stb INTERFACE)
|
||||
target_include_directories(stb INTERFACE ${stb_SOURCE_DIR})
|
||||
|
||||
@@ -274,6 +296,11 @@ find_package(CURL REQUIRED)
|
||||
find_path(mbedtls_inc mbedtls 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
|
||||
C_STANDARD 11
|
||||
C_EXTENSIONS ON
|
||||
@@ -296,6 +323,15 @@ target_link_libraries(sphaira PRIVATE
|
||||
${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
|
||||
include
|
||||
${minizip_inc}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "owo.hpp"
|
||||
#include "option.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
@@ -44,6 +45,8 @@ public:
|
||||
~App();
|
||||
void Loop();
|
||||
|
||||
static App* GetApp();
|
||||
|
||||
static void Exit();
|
||||
static void ExitRestart();
|
||||
static auto GetVg() -> NVGcontext*;
|
||||
@@ -62,7 +65,7 @@ public:
|
||||
static void SetTheme(s64 theme_index);
|
||||
static auto GetThemeIndex() -> s64;
|
||||
|
||||
static auto GetDefaultImage(int* w = nullptr, int* h = nullptr) -> int;
|
||||
static auto GetDefaultImage() -> int;
|
||||
|
||||
// returns argv[0]
|
||||
static auto GetExePath() -> fs::FsPath;
|
||||
@@ -75,9 +78,10 @@ public:
|
||||
static auto GetLogEnable() -> bool;
|
||||
static auto GetReplaceHbmenuEnable() -> bool;
|
||||
static auto GetInstallEnable() -> bool;
|
||||
static auto GetInstallSysmmcEnable() -> bool;
|
||||
static auto GetInstallEmummcEnable() -> bool;
|
||||
static auto GetInstallSdEnable() -> bool;
|
||||
static auto GetInstallPrompt() -> bool;
|
||||
static auto GetThemeShuffleEnable() -> bool;
|
||||
static auto GetThemeMusicEnable() -> bool;
|
||||
static auto Get12HourTimeEnable() -> bool;
|
||||
static auto GetLanguage() -> long;
|
||||
@@ -88,10 +92,10 @@ public:
|
||||
static void SetNxlinkEnable(bool enable);
|
||||
static void SetLogEnable(bool enable);
|
||||
static void SetReplaceHbmenuEnable(bool enable);
|
||||
static void SetInstallEnable(bool enable);
|
||||
static void SetInstallSysmmcEnable(bool enable);
|
||||
static void SetInstallEmummcEnable(bool enable);
|
||||
static void SetInstallSdEnable(bool enable);
|
||||
static void SetInstallPrompt(bool enable);
|
||||
static void SetThemeShuffleEnable(bool enable);
|
||||
static void SetThemeMusicEnable(bool enable);
|
||||
static void Set12HourTimeEnable(bool enable);
|
||||
static void SetLanguage(long index);
|
||||
@@ -102,6 +106,13 @@ public:
|
||||
|
||||
static void PlaySoundEffect(SoundEffect effect);
|
||||
|
||||
static void DisplayThemeOptions(bool left_side = true);
|
||||
// todo:
|
||||
static void DisplayNetworkOptions(bool left_side = true);
|
||||
static void DisplayMiscOptions(bool left_side = true);
|
||||
static void DisplayAdvancedOptions(bool left_side = true);
|
||||
static void DisplayInstallOptions(bool left_side = true);
|
||||
|
||||
void Draw();
|
||||
void Update();
|
||||
void Poll();
|
||||
@@ -116,6 +127,10 @@ public:
|
||||
void ScanThemes(const std::string& path);
|
||||
void ScanThemeEntries();
|
||||
|
||||
// helper that converts 1.2.3 to a u32 used for comparisons.
|
||||
static auto GetVersionFromString(const char* str) -> u32;
|
||||
static auto IsVersionNewer(const char* current, const char* new_version) -> u32;
|
||||
|
||||
static auto IsApplication() -> bool {
|
||||
const auto type = appletGetAppletType();
|
||||
return type == AppletType_Application || type == AppletType_SystemApplication;
|
||||
@@ -135,6 +150,21 @@ public:
|
||||
return R_SUCCEEDED(pmdmntGetApplicationProcessId(&pid));
|
||||
}
|
||||
|
||||
static auto IsEmunand() -> bool {
|
||||
alignas(0x1000) struct EmummcPaths {
|
||||
char unk[0x80];
|
||||
char nintendo[0x80];
|
||||
} paths{};
|
||||
|
||||
SecmonArgs args{};
|
||||
args.X[0] = 0xF0000404; /* smcAmsGetEmunandConfig */
|
||||
args.X[1] = 0; /* EXO_EMUMMC_MMC_NAND*/
|
||||
args.X[2] = (u64)&paths; /* out path */
|
||||
svcCallSecureMonitor(&args);
|
||||
|
||||
return (paths.unk[0] != '\0') || (paths.nintendo[0] != '\0');
|
||||
}
|
||||
|
||||
|
||||
// private:
|
||||
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
|
||||
@@ -174,13 +204,33 @@ public:
|
||||
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_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_install{INI_SECTION, "install", false};
|
||||
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", 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_sysmmc{INI_SECTION, "install_sysmmc", false};
|
||||
option::OptionBool m_install_emummc{INI_SECTION, "install_emummc", false};
|
||||
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||
option::OptionLong m_boost_mode{INI_SECTION, "boost_mode", false};
|
||||
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
|
||||
option::OptionBool m_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
|
||||
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||
|
||||
|
||||
@@ -13,10 +13,14 @@ namespace sphaira::curl {
|
||||
|
||||
enum {
|
||||
Flag_None = 0,
|
||||
|
||||
// requests to download send etag in the header.
|
||||
// the received etag is then saved on success.
|
||||
// this api is only available on downloading to file.
|
||||
Flag_Cache = 1 << 0,
|
||||
|
||||
// sets CURLOPT_NOBODY.
|
||||
Flag_NoBody = 1 << 1,
|
||||
};
|
||||
|
||||
enum class Priority {
|
||||
@@ -29,7 +33,9 @@ struct ApiResult;
|
||||
|
||||
using Path = fs::FsPath;
|
||||
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(s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow)>;
|
||||
using OnUploadCallback = std::function<size_t(void *ptr, size_t size)>;
|
||||
using OnUploadSeek = std::function<bool(s64 offset)>;
|
||||
using StopToken = std::stop_token;
|
||||
|
||||
struct Url {
|
||||
@@ -62,6 +68,55 @@ struct Flags {
|
||||
u32 m_flags{Flag_None};
|
||||
};
|
||||
|
||||
struct Port {
|
||||
Port() = default;
|
||||
Port(u16 port) : m_port{port} {}
|
||||
u16 m_port{};
|
||||
};
|
||||
|
||||
struct CustomRequest {
|
||||
CustomRequest() = default;
|
||||
CustomRequest(const std::string& str) : m_str{str} {}
|
||||
std::string m_str;
|
||||
};
|
||||
|
||||
struct UserPass {
|
||||
UserPass() = default;
|
||||
UserPass(const std::string& user) : m_user{user} {}
|
||||
UserPass(const std::string& user, const std::string& pass) : m_user{user}, m_pass{pass} {}
|
||||
std::string m_user;
|
||||
std::string m_pass;
|
||||
};
|
||||
|
||||
struct UploadInfo {
|
||||
UploadInfo() = default;
|
||||
UploadInfo(const std::string& name) : m_name{name} {}
|
||||
UploadInfo(const std::string& name, s64 size, OnUploadCallback cb) : m_name{name}, m_size{size}, m_callback{cb} {}
|
||||
UploadInfo(const std::string& name, const std::vector<u8>& data) : m_name{name}, m_data{data} {}
|
||||
std::string m_name{};
|
||||
std::vector<u8> m_data{};
|
||||
s64 m_size{};
|
||||
OnUploadCallback m_callback{};
|
||||
};
|
||||
|
||||
struct Bearer {
|
||||
Bearer() = default;
|
||||
Bearer(const std::string& str) : m_str{str} {}
|
||||
std::string m_str;
|
||||
};
|
||||
|
||||
struct PubKey {
|
||||
PubKey() = default;
|
||||
PubKey(const std::string& str) : m_str{str} {}
|
||||
std::string m_str;
|
||||
};
|
||||
|
||||
struct PrivKey {
|
||||
PrivKey() = default;
|
||||
PrivKey(const std::string& str) : m_str{str} {}
|
||||
std::string m_str;
|
||||
};
|
||||
|
||||
struct ApiResult {
|
||||
bool success;
|
||||
long code;
|
||||
@@ -76,16 +131,29 @@ struct DownloadEventData {
|
||||
StopToken stoken;
|
||||
};
|
||||
|
||||
// helper that generates the api using an location.
|
||||
#define CURL_LOCATION_TO_API(loc) \
|
||||
curl::Url{loc.url}, \
|
||||
curl::UserPass{loc.user, loc.pass}, \
|
||||
curl::Bearer{loc.bearer}, \
|
||||
curl::PubKey{loc.pub_key}, \
|
||||
curl::PrivKey{loc.priv_key}, \
|
||||
curl::Port(loc.port)
|
||||
|
||||
auto Init() -> bool;
|
||||
void Exit();
|
||||
|
||||
// sync functions
|
||||
auto ToMemory(const Api& e) -> ApiResult;
|
||||
auto ToFile(const Api& e) -> ApiResult;
|
||||
auto FromMemory(const Api& e) -> ApiResult;
|
||||
auto FromFile(const Api& e) -> ApiResult;
|
||||
|
||||
// async functions
|
||||
auto ToMemoryAsync(const Api& e) -> bool;
|
||||
auto ToFileAsync(const Api& e) -> bool;
|
||||
auto FromMemoryAsync(const Api& e) -> bool;
|
||||
auto FromFileAsync(const Api& e) -> bool;
|
||||
|
||||
// uses curl to convert string to their %XX
|
||||
auto EscapeString(const std::string& str) -> std::string;
|
||||
@@ -107,6 +175,15 @@ struct Api {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto From(Ts&&... ts) {
|
||||
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
|
||||
return FromFile(std::forward<Ts>(ts)...);
|
||||
} else {
|
||||
return FromMemory(std::forward<Ts>(ts)...);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto ToAsync(Ts&&... ts) {
|
||||
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
|
||||
@@ -116,6 +193,15 @@ struct Api {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto FromAsync(Ts&&... ts) {
|
||||
if constexpr(std::disjunction_v<std::is_same<Path, Ts>...>) {
|
||||
return FromFileAsync(std::forward<Ts>(ts)...);
|
||||
} else {
|
||||
return FromMemoryAsync(std::forward<Ts>(ts)...);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto ToMemory(Ts&&... ts) {
|
||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||
@@ -125,6 +211,16 @@ struct Api {
|
||||
return curl::ToMemory(*this);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto FromMemory(Ts&&... ts) {
|
||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo 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<OnComplete, Ts>...>, "OnComplete must not be specified");
|
||||
Api::set_option(std::forward<Ts>(ts)...);
|
||||
return curl::FromMemory(*this);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto ToFile(Ts&&... ts) {
|
||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||
@@ -134,6 +230,16 @@ struct Api {
|
||||
return curl::ToFile(*this);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto FromFile(Ts&&... ts) {
|
||||
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<UploadInfo, Ts>...>, "UploadInfo 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)...);
|
||||
return curl::FromFile(*this);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto ToMemoryAsync(Ts&&... ts) {
|
||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||
@@ -144,6 +250,17 @@ struct Api {
|
||||
return curl::ToMemoryAsync(*this);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto FromMemoryAsync(Ts&&... ts) {
|
||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||
static_assert(std::disjunction_v<std::is_same<UploadInfo, Ts>...>, "UploadInfo 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<StopToken, Ts>...>, "StopToken must be specified");
|
||||
Api::set_option(std::forward<Ts>(ts)...);
|
||||
return curl::FromMemoryAsync(*this);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
auto ToFileAsync(Ts&&... ts) {
|
||||
static_assert(std::disjunction_v<std::is_same<Url, Ts>...>, "Url must be specified");
|
||||
@@ -154,62 +271,55 @@ struct Api {
|
||||
return curl::ToFileAsync(*this);
|
||||
}
|
||||
|
||||
auto& GetUrl() const {
|
||||
return m_url.m_str;
|
||||
}
|
||||
auto& GetFields() const {
|
||||
return m_fields.m_str;
|
||||
}
|
||||
auto& GetHeader() const {
|
||||
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;
|
||||
template <typename... Ts>
|
||||
auto FromFileAsync(Ts&&... ts) {
|
||||
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<UploadInfo, Ts>...>, "UploadInfo 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)...);
|
||||
return curl::FromFileAsync(*this);
|
||||
}
|
||||
|
||||
private:
|
||||
void SetOption(Url&& v) {
|
||||
m_url = v;
|
||||
}
|
||||
void SetOption(Fields&& v) {
|
||||
m_fields = v;
|
||||
}
|
||||
void SetOption(Header&& v) {
|
||||
m_header = v;
|
||||
}
|
||||
void SetOption(Flags&& v) {
|
||||
m_flags = v;
|
||||
}
|
||||
void SetOption(Path&& v) {
|
||||
m_path = v;
|
||||
}
|
||||
void SetOption(OnComplete&& v) {
|
||||
m_on_complete = v;
|
||||
}
|
||||
void SetOption(OnProgress&& v) {
|
||||
m_on_progress = v;
|
||||
}
|
||||
void SetOption(Priority&& v) {
|
||||
m_prio = v;
|
||||
}
|
||||
void SetOption(StopToken&& v) {
|
||||
m_stoken = v;
|
||||
}
|
||||
void SetUpload(bool enable) { m_is_upload = enable; }
|
||||
|
||||
auto IsUpload() const { return m_is_upload; }
|
||||
auto& GetUrl() const { return m_url.m_str; }
|
||||
auto& GetFields() const { return m_fields.m_str; }
|
||||
auto& GetHeader() const { return m_header; }
|
||||
auto& GetFlags() const { return m_flags.m_flags; }
|
||||
auto& GetPath() const { return m_path; }
|
||||
auto& GetPort() const { return m_port.m_port; }
|
||||
auto& GetCustomRequest() const { return m_custom_request.m_str; }
|
||||
auto& GetUserPass() const { return m_userpass; }
|
||||
auto& GetBearer() const { return m_bearer.m_str; }
|
||||
auto& GetPubKey() const { return m_pub_key.m_str; }
|
||||
auto& GetPrivKey() const { return m_priv_key.m_str; }
|
||||
auto& GetUploadInfo() const { return m_info; }
|
||||
auto& GetOnComplete() const { return m_on_complete; }
|
||||
auto& GetOnProgress() const { return m_on_progress; }
|
||||
auto& GetOnUploadSeek() const { return m_on_upload_seek; }
|
||||
auto& GetPriority() const { return m_prio; }
|
||||
auto& GetToken() const { return m_stoken; }
|
||||
|
||||
void SetOption(Url&& v) { m_url = v; }
|
||||
void SetOption(Fields&& v) { m_fields = v; }
|
||||
void SetOption(Header&& v) { m_header = v; }
|
||||
void SetOption(Flags&& v) { m_flags = v; }
|
||||
void SetOption(Path&& v) { m_path = v; }
|
||||
void SetOption(Port&& v) { m_port = v; }
|
||||
void SetOption(CustomRequest&& v) { m_custom_request = v; }
|
||||
void SetOption(UserPass&& v) { m_userpass = v; }
|
||||
void SetOption(Bearer&& v) { m_bearer = v; }
|
||||
void SetOption(PubKey&& v) { m_pub_key = v; }
|
||||
void SetOption(PrivKey&& v) { m_priv_key = v; }
|
||||
void SetOption(UploadInfo&& v) { m_info = v; }
|
||||
void SetOption(OnComplete&& v) { m_on_complete = v; }
|
||||
void SetOption(OnProgress&& v) { m_on_progress = v; }
|
||||
void SetOption(OnUploadSeek&& v) { m_on_upload_seek = v; }
|
||||
void SetOption(Priority&& v) { m_prio = v; }
|
||||
void SetOption(StopToken&& v) { m_stoken = v; }
|
||||
|
||||
template <typename T>
|
||||
void set_option(T&& t) {
|
||||
@@ -223,16 +333,25 @@ private:
|
||||
}
|
||||
|
||||
private:
|
||||
Url m_url;
|
||||
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};
|
||||
Port m_port{};
|
||||
CustomRequest m_custom_request{};
|
||||
UserPass m_userpass{};
|
||||
Bearer m_bearer{};
|
||||
PubKey m_pub_key{};
|
||||
PrivKey m_priv_key{};
|
||||
UploadInfo m_info{};
|
||||
OnComplete m_on_complete{};
|
||||
OnProgress m_on_progress{};
|
||||
OnUploadSeek m_on_upload_seek{};
|
||||
Priority m_prio{Priority::High};
|
||||
std::stop_source m_stop_source{};
|
||||
StopToken m_stoken{m_stop_source.get_token()};
|
||||
bool m_is_upload{};
|
||||
};
|
||||
|
||||
} // 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
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
|
||||
bool Init();
|
||||
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
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace sphaira::i18n {
|
||||
|
||||
bool init(long index);
|
||||
void exit();
|
||||
|
||||
std::string get(const char* str);
|
||||
std::string get(std::string_view str);
|
||||
|
||||
} // namespace sphaira::i18n
|
||||
|
||||
inline namespace literals {
|
||||
|
||||
std::string operator"" _i18n(const char* str, size_t len);
|
||||
std::string operator""_i18n(const char* str, size_t len);
|
||||
|
||||
} // namespace literals
|
||||
|
||||
24
sphaira/include/location.hpp
Normal file
24
sphaira/include/location.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::location {
|
||||
|
||||
struct Entry {
|
||||
std::string name{};
|
||||
std::string url{};
|
||||
std::string user{};
|
||||
std::string pass{};
|
||||
std::string bearer{};
|
||||
std::string pub_key{};
|
||||
std::string priv_key{};
|
||||
u16 port{};
|
||||
};
|
||||
using Entries = std::vector<Entry>;
|
||||
|
||||
auto Load() -> Entries;
|
||||
void Add(const Entry& e);
|
||||
|
||||
} // namespace sphaira::location
|
||||
@@ -1,24 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define sphaira_USE_LOG 1
|
||||
|
||||
#include <cstdarg>
|
||||
#include <stdarg.h>
|
||||
|
||||
#if sphaira_USE_LOG
|
||||
auto log_file_init() -> bool;
|
||||
auto log_nxlink_init() -> bool;
|
||||
bool log_file_init();
|
||||
bool log_nxlink_init();
|
||||
void log_file_exit();
|
||||
void log_nxlink_exit();
|
||||
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
|
||||
inline auto log_file_init() -> bool {
|
||||
inline bool log_file_init() {
|
||||
return true;
|
||||
}
|
||||
inline auto log_nxlink_init() -> bool {
|
||||
inline bool log_nxlink_init() {
|
||||
return true;
|
||||
}
|
||||
#define log_file_exit()
|
||||
#define log_nxlink_exit()
|
||||
#define log_write(...)
|
||||
#define log_write_arg(...)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -14,8 +14,12 @@ struct OptionBase {
|
||||
{}
|
||||
|
||||
auto Get() -> T;
|
||||
auto GetOr(const char* name) -> T;
|
||||
void Set(T value);
|
||||
|
||||
private:
|
||||
auto GetInternal(const char* name) -> T;
|
||||
|
||||
private:
|
||||
const std::string m_section;
|
||||
const std::string m_name;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "ui/types.hpp"
|
||||
#include "ui/object.hpp"
|
||||
|
||||
namespace sphaira::ui::bubble {
|
||||
|
||||
void Init();
|
||||
void Draw(NVGcontext* vg, Theme* theme);
|
||||
void Exit();
|
||||
|
||||
} // namespace sphaira::ui::bubble
|
||||
@@ -8,15 +8,14 @@ namespace sphaira::ui {
|
||||
class ErrorBox final : public Widget {
|
||||
public:
|
||||
ErrorBox(Result code, const std::string& message);
|
||||
ErrorBox(const std::string& message);
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
private:
|
||||
Result m_code{};
|
||||
std::optional<Result> m_code{};
|
||||
std::string m_message{};
|
||||
std::string m_module_str{};
|
||||
std::string m_description_str{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
namespace sphaira::ui {
|
||||
|
||||
struct List final : Object {
|
||||
enum class Layout {
|
||||
HOME,
|
||||
GRID,
|
||||
};
|
||||
|
||||
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 = {});
|
||||
|
||||
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;
|
||||
|
||||
@@ -35,10 +40,36 @@ struct List final : Object {
|
||||
return m_v.h + m_pad.y;
|
||||
}
|
||||
|
||||
auto GetMaxX() const {
|
||||
return m_v.w + m_pad.x;
|
||||
}
|
||||
|
||||
auto GetLayout() const {
|
||||
return m_layout;
|
||||
}
|
||||
|
||||
void SetLayout(Layout layout) {
|
||||
m_layout = layout;
|
||||
}
|
||||
|
||||
auto GetRow() const {
|
||||
return m_row;
|
||||
}
|
||||
|
||||
auto GetPage() const {
|
||||
return m_page;
|
||||
}
|
||||
|
||||
private:
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override {}
|
||||
auto ClampX(float x, s64 count) const -> float;
|
||||
auto ClampY(float y, s64 count) const -> float;
|
||||
|
||||
void OnUpdateHome(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
|
||||
void OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback);
|
||||
void DrawHome(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
|
||||
void DrawGrid(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const;
|
||||
|
||||
private:
|
||||
const s64 m_row;
|
||||
const s64 m_page;
|
||||
@@ -52,6 +83,8 @@ private:
|
||||
float m_yoff{};
|
||||
// in progress y offset, used when scrolling.
|
||||
float m_y_prog{};
|
||||
|
||||
Layout m_layout{Layout::GRID};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/menus/grid_menu_base.hpp"
|
||||
#include "ui/scrollable_text.hpp"
|
||||
#include "ui/scrolling_text.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::ui::menu::appstore {
|
||||
@@ -73,6 +74,7 @@ struct EntryMenu final : MenuBase {
|
||||
EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu);
|
||||
~EntryMenu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "Entry"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
// void OnFocusGained() override;
|
||||
@@ -134,27 +136,25 @@ enum OrderType {
|
||||
OrderType_Ascending,
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu(const std::vector<NroEntry>& nro_entries);
|
||||
using LayoutType = grid::LayoutType;
|
||||
|
||||
struct Menu final : grid::Menu {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "Store"; };
|
||||
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();
|
||||
|
||||
void SetFilter(Filter filter);
|
||||
void SetSort(SortType sort);
|
||||
void SetOrder(OrderType order);
|
||||
|
||||
void SetSearch(const std::string& term);
|
||||
void SetAuthor();
|
||||
|
||||
auto GetEntry(s64 i) -> Entry& {
|
||||
return m_entries[m_entries_current[i]];
|
||||
}
|
||||
|
||||
auto GetEntry() -> Entry& {
|
||||
return m_entries[m_entries_current[m_index]];
|
||||
return GetEntry(m_index);
|
||||
}
|
||||
|
||||
auto SetDirty() {
|
||||
@@ -162,16 +162,27 @@ struct Menu final : MenuBase {
|
||||
}
|
||||
|
||||
private:
|
||||
const std::vector<NroEntry>& m_nro_entries;
|
||||
void SetIndex(s64 index);
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
void SortAndFindLastFile();
|
||||
void SetFilter();
|
||||
void SetSearch(const std::string& term);
|
||||
void OnLayoutChange();
|
||||
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "appstore";
|
||||
|
||||
std::vector<Entry> m_entries{};
|
||||
std::vector<EntryMini> m_entries_index[Filter_MAX]{};
|
||||
std::vector<EntryMini> m_entries_index_author{};
|
||||
std::vector<EntryMini> m_entries_index_search{};
|
||||
std::span<EntryMini> m_entries_current{};
|
||||
|
||||
Filter m_filter{Filter::Filter_All};
|
||||
SortType m_sort{SortType::SortType_Updated};
|
||||
OrderType m_order{OrderType::OrderType_Descending};
|
||||
option::OptionLong m_filter{INI_SECTION, "filter", Filter::Filter_All};
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
|
||||
|
||||
s64 m_index{}; // where i am in the array
|
||||
LazyImage m_default_image{};
|
||||
|
||||
@@ -10,6 +10,7 @@ struct Menu final : MenuBase {
|
||||
Menu(const fs::FsPath& path);
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "File"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
@@ -89,6 +89,19 @@ struct FileAssocEntry {
|
||||
std::string name{}; // ini name
|
||||
std::vector<std::string> ext{}; // list of ext
|
||||
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 {
|
||||
@@ -111,6 +124,7 @@ struct Menu final : MenuBase {
|
||||
Menu(const std::vector<NroEntry>& nro_entries);
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "Files"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
@@ -122,6 +136,12 @@ struct Menu final : MenuBase {
|
||||
private:
|
||||
void SetIndex(s64 index);
|
||||
void InstallForwarder();
|
||||
|
||||
void InstallFiles();
|
||||
void UnzipFiles(fs::FsPath folder);
|
||||
void ZipFiles(fs::FsPath zip_path);
|
||||
void UploadFiles();
|
||||
|
||||
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
||||
|
||||
void LoadAssocEntriesPath(const fs::FsPath& path);
|
||||
@@ -141,15 +161,15 @@ private:
|
||||
}
|
||||
|
||||
auto GetSelectedEntries() const -> std::vector<FileEntry> {
|
||||
if (!m_selected_count) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<FileEntry> out;
|
||||
|
||||
for (auto&e : m_entries) {
|
||||
if (e.IsSelected()) {
|
||||
out.emplace_back(e);
|
||||
if (!m_selected_count) {
|
||||
out.emplace_back(GetEntry());
|
||||
} else {
|
||||
for (auto&e : m_entries) {
|
||||
if (e.IsSelected()) {
|
||||
out.emplace_back(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,13 +188,6 @@ private:
|
||||
m_selected_path = m_path;
|
||||
}
|
||||
|
||||
void AddCurrentFileToSelection(SelectedType type) {
|
||||
m_selected_files.emplace_back(GetEntry());
|
||||
m_selected_count++;
|
||||
m_selected_type = type;
|
||||
m_selected_path = m_path;
|
||||
}
|
||||
|
||||
void ResetSelection() {
|
||||
m_selected_files.clear();
|
||||
m_selected_count = 0;
|
||||
|
||||
61
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
61
sphaira/include/ui/menus/ftp_menu.hpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#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{};
|
||||
bool m_was_ftp_enabled{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::ftp
|
||||
153
sphaira/include/ui/menus/game_menu.hpp
Normal file
153
sphaira/include/ui/menus/game_menu.hpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/grid_menu_base.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "option.hpp"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace sphaira::ui::menu::game {
|
||||
|
||||
enum class NacpLoadStatus {
|
||||
// not yet attempted to be loaded.
|
||||
None,
|
||||
// started loading.
|
||||
Progress,
|
||||
// loaded, ready to parse.
|
||||
Loaded,
|
||||
// failed to load, do not attempt to load again!
|
||||
Error,
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
u64 app_id{};
|
||||
char display_version[0x10]{};
|
||||
NacpLanguageEntry lang{};
|
||||
int image{};
|
||||
bool selected{};
|
||||
|
||||
std::shared_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;
|
||||
}
|
||||
};
|
||||
|
||||
struct ThreadResultData {
|
||||
u64 id{};
|
||||
std::shared_ptr<NsApplicationControlData> control{};
|
||||
u64 control_size{};
|
||||
char display_version[0x10]{};
|
||||
NacpLanguageEntry lang{};
|
||||
NacpLoadStatus status{NacpLoadStatus::None};
|
||||
};
|
||||
|
||||
struct ThreadData {
|
||||
ThreadData();
|
||||
|
||||
auto IsRunning() const -> bool;
|
||||
void Run();
|
||||
void Close();
|
||||
void Push(u64 id);
|
||||
void Push(std::span<const Entry> entries);
|
||||
void Pop(std::vector<ThreadResultData>& out);
|
||||
|
||||
private:
|
||||
UEvent m_uevent{};
|
||||
Mutex m_mutex_id{};
|
||||
Mutex m_mutex_result{};
|
||||
|
||||
// app_ids pushed to the queue, signal uevent when pushed.
|
||||
std::vector<u64> m_ids{};
|
||||
// control data pushed to the queue.
|
||||
std::vector<ThreadResultData> m_result{};
|
||||
|
||||
std::atomic_bool m_running{};
|
||||
};
|
||||
|
||||
enum SortType {
|
||||
SortType_Updated,
|
||||
};
|
||||
|
||||
enum OrderType {
|
||||
OrderType_Descending,
|
||||
OrderType_Ascending,
|
||||
};
|
||||
|
||||
using LayoutType = grid::LayoutType;
|
||||
|
||||
struct Menu final : grid::Menu {
|
||||
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(bool scan);
|
||||
void FreeEntries();
|
||||
void OnLayoutChange();
|
||||
|
||||
auto GetSelectedEntries() const {
|
||||
std::vector<Entry> out;
|
||||
for (auto& e : m_entries) {
|
||||
if (e.selected) {
|
||||
out.emplace_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_entries.empty() && out.empty()) {
|
||||
out.emplace_back(m_entries[m_index]);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void ClearSelection() {
|
||||
for (auto& e : m_entries) {
|
||||
e.selected = false;
|
||||
}
|
||||
|
||||
m_selected_count = 0;
|
||||
}
|
||||
|
||||
void DeleteGames();
|
||||
void DumpGames(u32 flags);
|
||||
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "games";
|
||||
static constexpr inline const char* INI_SECTION_DUMP = "dump";
|
||||
|
||||
std::vector<Entry> m_entries{};
|
||||
s64 m_index{}; // where i am in the array
|
||||
s64 m_selected_count{};
|
||||
std::unique_ptr<List> m_list{};
|
||||
bool m_is_reversed{};
|
||||
bool m_dirty{};
|
||||
|
||||
ThreadData m_thread_data{};
|
||||
Thread m_thread{};
|
||||
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Updated};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
|
||||
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
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
|
||||
@@ -45,6 +45,7 @@ struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "GitHub"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
35
sphaira/include/ui/menus/grid_menu_base.hpp
Normal file
35
sphaira/include/ui/menus/grid_menu_base.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/scrolling_text.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::ui::menu::grid {
|
||||
|
||||
enum LayoutType {
|
||||
LayoutType_List,
|
||||
LayoutType_Grid,
|
||||
LayoutType_GridDetail,
|
||||
};
|
||||
|
||||
struct Menu : MenuBase {
|
||||
using MenuBase::MenuBase;
|
||||
|
||||
protected:
|
||||
void OnLayoutChange(std::unique_ptr<List>& list, int layout);
|
||||
void DrawEntry(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version);
|
||||
// same as above but doesn't draw image and returns image dimension.
|
||||
Vec4 DrawEntryNoImage(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, const char* name, const char* author, const char* version);
|
||||
|
||||
private:
|
||||
Vec4 DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version);
|
||||
|
||||
private:
|
||||
ScrollingText m_scroll_name{};
|
||||
ScrollingText m_scroll_author{};
|
||||
ScrollingText m_scroll_version{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::grid
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/menus/grid_menu_base.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "nro.hpp"
|
||||
#include "fs.hpp"
|
||||
@@ -22,31 +22,37 @@ enum OrderType {
|
||||
OrderType_Ascending,
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
using LayoutType = grid::LayoutType;
|
||||
|
||||
struct Menu final : grid::Menu {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "Apps"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
void SetIndex(s64 index);
|
||||
void InstallHomebrew();
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
void SortAndFindLastFile();
|
||||
|
||||
auto GetHomebrewList() const -> const std::vector<NroEntry>& {
|
||||
return m_entries;
|
||||
}
|
||||
|
||||
static Result InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon);
|
||||
static Result InstallHomebrewFromPath(const fs::FsPath& path);
|
||||
|
||||
private:
|
||||
void SetIndex(s64 index);
|
||||
void InstallHomebrew();
|
||||
void ScanHomebrew();
|
||||
void Sort();
|
||||
void SortAndFindLastFile();
|
||||
void FreeEntries();
|
||||
void OnLayoutChange();
|
||||
|
||||
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 InstallHomebrewFromPath(const fs::FsPath& path);
|
||||
|
||||
private:
|
||||
static constexpr inline const char* INI_SECTION = "homebrew";
|
||||
|
||||
@@ -56,6 +62,7 @@ private:
|
||||
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_AlphabeticalStar};
|
||||
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending};
|
||||
option::OptionLong m_layout{INI_SECTION, "layout", LayoutType::LayoutType_GridDetail};
|
||||
option::OptionBool m_hide_sphaira{INI_SECTION, "hide_sphaira", false};
|
||||
};
|
||||
|
||||
|
||||
@@ -30,16 +30,19 @@ struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "IRS"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
private:
|
||||
void PollCameraStatus(bool statup = false);
|
||||
void LoadDefaultConfig();
|
||||
void UpdateConfig(const IrsImageTransferProcessorExConfig* config);
|
||||
void ResetImage();
|
||||
void UpdateImage();
|
||||
void updateColourArray();
|
||||
auto GetEntryName(s64 i) -> std::string;
|
||||
|
||||
private:
|
||||
Result m_init_rc{};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/menus/homebrew.hpp"
|
||||
#include "ui/menus/filebrowser.hpp"
|
||||
#include "ui/menus/appstore.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::main {
|
||||
|
||||
@@ -18,6 +17,32 @@ enum class UpdateState {
|
||||
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
|
||||
struct MainMenu final : Widget {
|
||||
MainMenu();
|
||||
@@ -39,7 +64,7 @@ private:
|
||||
private:
|
||||
std::shared_ptr<homebrew::Menu> m_homebrew_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::string m_update_url{};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/widget.hpp"
|
||||
#include "nro.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace sphaira::ui::menu {
|
||||
@@ -10,6 +9,7 @@ struct MenuBase : Widget {
|
||||
MenuBase(std::string title);
|
||||
virtual ~MenuBase();
|
||||
|
||||
virtual auto GetShortTitle() const -> const char* = 0;
|
||||
virtual void Update(Controller* controller, TouchInfo* touch);
|
||||
virtual void Draw(NVGcontext* vg, Theme* theme);
|
||||
|
||||
@@ -21,6 +21,10 @@ struct MenuBase : Widget {
|
||||
void SetTitleSubHeading(std::string sub_heading);
|
||||
void SetSubHeading(std::string sub_heading);
|
||||
|
||||
auto GetTitle() const {
|
||||
return m_title;
|
||||
}
|
||||
|
||||
private:
|
||||
void UpdateVars();
|
||||
|
||||
@@ -29,6 +33,7 @@ private:
|
||||
std::string m_title_sub_heading{};
|
||||
std::string m_sub_heading{};
|
||||
|
||||
protected:
|
||||
struct tm m_tm{};
|
||||
TimeStamp m_poll_timestamp{};
|
||||
u32 m_battery_percetange{};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "ui/scrollable_text.hpp"
|
||||
#include "ui/scrolling_text.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include "option.hpp"
|
||||
#include <span>
|
||||
@@ -132,6 +133,7 @@ struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
auto GetShortTitle() const -> const char* override { return "Themezer"; };
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
@@ -160,6 +162,9 @@ private:
|
||||
s64 m_index{}; // where i am in the array
|
||||
std::unique_ptr<List> m_list{};
|
||||
|
||||
ScrollingText m_scroll_name{};
|
||||
ScrollingText m_scroll_author{};
|
||||
|
||||
// options
|
||||
option::OptionLong m_sort{INI_SECTION, "sort", 0};
|
||||
option::OptionLong m_order{INI_SECTION, "order", 0};
|
||||
|
||||
45
sphaira/include/ui/menus/usb_menu.hpp
Normal file
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
|
||||
@@ -2,24 +2,26 @@
|
||||
|
||||
#include "nanovg.h"
|
||||
#include "ui/types.hpp"
|
||||
#include "ui/scrolling_text.hpp"
|
||||
|
||||
namespace sphaira::ui::gfx {
|
||||
|
||||
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture);
|
||||
void drawImage(NVGcontext*, const Vec4& v, int texture);
|
||||
void drawImageRounded(NVGcontext*, float x, float y, float w, float h, int texture);
|
||||
void drawImageRounded(NVGcontext*, const Vec4& v, int texture);
|
||||
void drawImage(NVGcontext*, float x, float y, float w, float h, int texture, float rounded = 0.F);
|
||||
void drawImage(NVGcontext*, const Vec4& v, int texture, float rounded = 0.F);
|
||||
|
||||
void dimBackground(NVGcontext*);
|
||||
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, bool rounded = false);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, bool rounded = false);
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, bool rounded = false);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, bool rounded = false);
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGcolor& c, float rounding = 0.F);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGcolor& c, float rounding = 0.F);
|
||||
void drawRect(NVGcontext*, float x, float y, float w, float h, const NVGpaint& p, float rounding = 0.F);
|
||||
void drawRect(NVGcontext*, const Vec4& v, const NVGpaint& p, float rounding = 0.F);
|
||||
|
||||
void drawRectOutline(NVGcontext*, const Theme*, float size, float x, float y, float w, float h);
|
||||
void drawRectOutline(NVGcontext*, const Theme*, float size, const Vec4& v);
|
||||
|
||||
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c);
|
||||
void drawTriangle(NVGcontext*, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p);
|
||||
|
||||
void drawText(NVGcontext*, float x, float y, float size, const char* str, const char* end, int align, const NVGcolor& c);
|
||||
void drawText(NVGcontext*, float x, float y, float size, const NVGcolor& c, const char* str, int align = NVG_ALIGN_LEFT | NVG_ALIGN_TOP, const char* end = nullptr);
|
||||
void drawText(NVGcontext*, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c);
|
||||
@@ -37,6 +39,8 @@ void drawScrollbar(NVGcontext*, const Theme*, float x, float y, float h, u32 ind
|
||||
void drawScrollbar2(NVGcontext*, const Theme*, float x, float y, float h, s64 index_off, s64 count, s64 row, s64 page);
|
||||
void drawScrollbar2(NVGcontext*, const Theme*, s64 index_off, s64 count, s64 row, s64 page);
|
||||
|
||||
void drawAppLable(NVGcontext* vg, const Theme*, ScrollingText& st, float x, float y, float w, const char* name);
|
||||
|
||||
void updateHighlightAnimation();
|
||||
void getHighlightAnimation(float* gradientX, float* gradientY, float* color);
|
||||
|
||||
|
||||
@@ -32,11 +32,9 @@ public:
|
||||
using Options = std::vector<Option>;
|
||||
|
||||
public:
|
||||
OptionBox(const std::string& message, const Option& a, Callback cb = [](auto){}); // 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, s64 index, Callback cb); // 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
|
||||
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, int image = 0); // yesno
|
||||
OptionBox(const std::string& message, const Option& a, const Option& b, s64 index, Callback cb, int image = 0); // yesno
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
@@ -50,6 +48,7 @@ private:
|
||||
private:
|
||||
std::string m_message{};
|
||||
Callback m_callback{};
|
||||
int m_image{};
|
||||
|
||||
Vec4 m_spacer_line{};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/widget.hpp"
|
||||
#include "ui/scrolling_text.hpp"
|
||||
#include "ui/list.hpp"
|
||||
#include <optional>
|
||||
|
||||
@@ -36,8 +37,10 @@ private:
|
||||
Items m_items{};
|
||||
Callback m_callback{};
|
||||
s64 m_index{}; // index in list array
|
||||
s64 m_starting_index{};
|
||||
|
||||
std::unique_ptr<List> m_list{};
|
||||
ScrollingText m_scroll_text{};
|
||||
|
||||
float m_yoff{};
|
||||
float m_line_top{};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "widget.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <functional>
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
@@ -12,17 +13,23 @@ using ProgressBoxDoneCallback = std::function<void(bool success)>;
|
||||
|
||||
struct ProgressBox final : Widget {
|
||||
ProgressBox(
|
||||
int image,
|
||||
const std::string& action,
|
||||
const std::string& title,
|
||||
ProgressBoxCallback callback, ProgressBoxDoneCallback done = [](bool success){},
|
||||
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*1024
|
||||
int cpuid = 1, int prio = 0x2C, int stack_size = 1024*128
|
||||
);
|
||||
~ProgressBox();
|
||||
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> 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 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();
|
||||
auto ShouldExit() -> bool;
|
||||
|
||||
@@ -31,15 +38,24 @@ struct ProgressBox final : Widget {
|
||||
void Yield();
|
||||
|
||||
auto OnDownloadProgressCallback() {
|
||||
return [this](u32 dltotal, u32 dlnow, u32 ultotal, u32 ulnow){
|
||||
return [this](s64 dltotal, s64 dlnow, s64 ultotal, s64 ulnow){
|
||||
if (this->ShouldExit()) {
|
||||
return false;
|
||||
}
|
||||
this->UpdateTransfer(dlnow, dltotal);
|
||||
|
||||
if (dltotal) {
|
||||
this->UpdateTransfer(dlnow, dltotal);
|
||||
} else {
|
||||
this->UpdateTransfer(ulnow, ultotal);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
private:
|
||||
void FreeImage();
|
||||
|
||||
public:
|
||||
struct ThreadData {
|
||||
ProgressBox* pbox{};
|
||||
@@ -51,12 +67,22 @@ private:
|
||||
Mutex m_mutex{};
|
||||
Thread m_thread{};
|
||||
ThreadData m_thread_data{};
|
||||
|
||||
ProgressBoxDoneCallback m_done{};
|
||||
|
||||
// shared data start.
|
||||
std::string m_action{};
|
||||
std::string m_title{};
|
||||
std::string m_transfer{};
|
||||
s64 m_size{};
|
||||
s64 m_offset{};
|
||||
s64 m_last_offset{};
|
||||
s64 m_speed{};
|
||||
TimeStamp m_timestamp{};
|
||||
std::vector<u8> m_image_data{};
|
||||
// shared data end.
|
||||
|
||||
int m_image{};
|
||||
bool m_own_image{};
|
||||
};
|
||||
|
||||
// this is a helper function that does many things.
|
||||
|
||||
20
sphaira/include/ui/scrolling_text.hpp
Normal file
20
sphaira/include/ui/scrolling_text.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#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)));
|
||||
void Reset(const std::string& text_entry = "");
|
||||
|
||||
private:
|
||||
std::string m_str;
|
||||
s64 m_tick;
|
||||
float m_text_xoff;
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui
|
||||
@@ -14,7 +14,7 @@ namespace sphaira {
|
||||
#define SCREEN_WIDTH 1280.f
|
||||
#define SCREEN_HEIGHT 720.f
|
||||
|
||||
struct [[nodiscard]] Vec2 {
|
||||
struct Vec2 {
|
||||
constexpr Vec2() = default;
|
||||
constexpr Vec2(float _x, float _y) : x{_x}, y{_y} {}
|
||||
|
||||
@@ -53,7 +53,7 @@ struct [[nodiscard]] Vec2 {
|
||||
float x{}, y{};
|
||||
};
|
||||
|
||||
struct [[nodiscard]] Vec4 {
|
||||
struct Vec4 {
|
||||
constexpr Vec4() = default;
|
||||
constexpr Vec4(float _x, float _y, float _w, float _h) : x{_x}, y{_y}, w{_w}, h{_h} {}
|
||||
constexpr Vec4(Vec2 vec0, Vec2 vec1) : x{vec0.x}, y{vec0.y}, w{vec1.x}, h{vec1.y} {}
|
||||
@@ -226,7 +226,6 @@ struct ThemeMeta {
|
||||
|
||||
struct Theme {
|
||||
ThemeMeta meta;
|
||||
PLSR_BFSTM music;
|
||||
ElementEntry elements[ThemeEntryID_MAX];
|
||||
|
||||
auto GetColour(ThemeEntryID id) const {
|
||||
|
||||
101
sphaira/include/usb/base.hpp
Normal file
101
sphaira/include/usb/base.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <new>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
struct Base {
|
||||
enum { USBModule = 523 };
|
||||
|
||||
enum : Result {
|
||||
Result_Cancelled = MAKERESULT(USBModule, 100),
|
||||
};
|
||||
|
||||
Base(u64 transfer_timeout);
|
||||
|
||||
// sets up usb.
|
||||
virtual Result Init() = 0;
|
||||
|
||||
// returns 0 if usb is connected to a device.
|
||||
virtual Result IsUsbConnected(u64 timeout) = 0;
|
||||
|
||||
// transfers a chunk of data, check out_size_transferred for how much was transferred.
|
||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout);
|
||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred) {
|
||||
return TransferPacketImpl(read, page, size, out_size_transferred, m_transfer_timeout);
|
||||
}
|
||||
|
||||
// transfers all data.
|
||||
Result TransferAll(bool read, void *data, u32 size, u64 timeout);
|
||||
Result TransferAll(bool read, void *data, u32 size) {
|
||||
return TransferAll(read, data, size, m_transfer_timeout);
|
||||
}
|
||||
|
||||
// returns the cancel event.
|
||||
auto GetCancelEvent() {
|
||||
return &m_uevent;
|
||||
}
|
||||
|
||||
// cancels an in progress transfer.
|
||||
void Cancel() {
|
||||
ueventSignal(GetCancelEvent());
|
||||
}
|
||||
|
||||
auto& GetTransferBuffer() {
|
||||
return m_aligned;
|
||||
}
|
||||
|
||||
auto GetTransferTimeout() const {
|
||||
return m_transfer_timeout;
|
||||
}
|
||||
|
||||
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* {
|
||||
n = (n + (Align - 1)) &~ (Align - 1);
|
||||
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);
|
||||
::operator delete[] (p, 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>>;
|
||||
|
||||
protected:
|
||||
enum UsbSessionEndpoint {
|
||||
UsbSessionEndpoint_In = 0,
|
||||
UsbSessionEndpoint_Out = 1,
|
||||
};
|
||||
|
||||
virtual Event *GetCompletionEvent(UsbSessionEndpoint ep) = 0;
|
||||
virtual Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) = 0;
|
||||
virtual Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) = 0;
|
||||
virtual Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) = 0;
|
||||
|
||||
private:
|
||||
u64 m_transfer_timeout{};
|
||||
UEvent m_uevent{};
|
||||
PageAlignedVector m_aligned{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb
|
||||
47
sphaira/include/usb/tinfoil.hpp
Normal file
47
sphaira/include/usb/tinfoil.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::usb::tinfoil {
|
||||
|
||||
enum Magic : u32 {
|
||||
Magic_List0 = 0x304C5554, // TUL0 (Tinfoil Usb List 0)
|
||||
Magic_Command0 = 0x30435554, // TUC0 (Tinfoil USB Command 0)
|
||||
};
|
||||
|
||||
enum USBCmdType : u8 {
|
||||
REQUEST = 0,
|
||||
RESPONSE = 1
|
||||
};
|
||||
|
||||
enum USBCmdId : u32 {
|
||||
EXIT = 0,
|
||||
FILE_RANGE = 1
|
||||
};
|
||||
|
||||
struct TUSHeader {
|
||||
u32 magic; // TUL0 (Tinfoil Usb List 0)
|
||||
u32 nspListSize;
|
||||
u64 padding;
|
||||
};
|
||||
|
||||
struct NX_PACKED USBCmdHeader {
|
||||
u32 magic; // TUC0 (Tinfoil USB Command 0)
|
||||
USBCmdType type;
|
||||
u8 padding[0x3];
|
||||
u32 cmdId;
|
||||
u64 dataSize;
|
||||
u8 reserved[0xC];
|
||||
};
|
||||
|
||||
struct FileRangeCmdHeader {
|
||||
u64 size;
|
||||
u64 offset;
|
||||
u64 nspNameLen;
|
||||
u64 padding;
|
||||
};
|
||||
|
||||
static_assert(sizeof(TUSHeader) == 0x10, "TUSHeader must be 0x10!");
|
||||
static_assert(sizeof(USBCmdHeader) == 0x20, "USBCmdHeader must be 0x20!");
|
||||
|
||||
} // namespace sphaira::usb::tinfoil
|
||||
47
sphaira/include/usb/usb_uploader.hpp
Normal file
47
sphaira/include/usb/usb_uploader.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "usb/usbhs.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::usb::upload {
|
||||
|
||||
struct Usb {
|
||||
enum { USBModule = 523 };
|
||||
|
||||
enum : Result {
|
||||
Result_BadMagic = MAKERESULT(USBModule, 0),
|
||||
Result_Exit = MAKERESULT(USBModule, 1),
|
||||
Result_BadCount = MAKERESULT(USBModule, 2),
|
||||
Result_BadTransferSize = MAKERESULT(USBModule, 3),
|
||||
Result_BadTotalSize = MAKERESULT(USBModule, 4),
|
||||
Result_BadCommand = MAKERESULT(USBModule, 4),
|
||||
};
|
||||
|
||||
Usb(u64 transfer_timeout);
|
||||
virtual ~Usb();
|
||||
|
||||
virtual Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
|
||||
Result IsUsbConnected(u64 timeout) {
|
||||
return m_usb->IsUsbConnected(timeout);
|
||||
}
|
||||
|
||||
// waits for connection and then sends file list.
|
||||
Result WaitForConnection(u64 timeout, std::span<const std::string> names);
|
||||
|
||||
// polls for command, executes transfer if possible.
|
||||
// will return Result_Exit if exit command is recieved.
|
||||
Result PollCommands();
|
||||
|
||||
private:
|
||||
Result FileRangeCmd(u64 data_size);
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::UsbHs> m_usb;
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb::upload
|
||||
27
sphaira/include/usb/usbds.hpp
Normal file
27
sphaira/include/usb/usbds.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
// Device Host
|
||||
struct UsbDs final : Base {
|
||||
using Base::Base;
|
||||
~UsbDs();
|
||||
|
||||
Result Init() override;
|
||||
Result IsUsbConnected(u64 timeout) override;
|
||||
Result GetSpeed(UsbDeviceSpeed* out);
|
||||
|
||||
private:
|
||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
|
||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
|
||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) override;
|
||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) override;
|
||||
|
||||
private:
|
||||
UsbDsInterface* m_interface{};
|
||||
UsbDsEndpoint* m_endpoints[2]{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb
|
||||
33
sphaira/include/usb/usbhs.hpp
Normal file
33
sphaira/include/usb/usbhs.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
|
||||
namespace sphaira::usb {
|
||||
|
||||
struct UsbHs final : Base {
|
||||
UsbHs(u8 index, const UsbHsInterfaceFilter& filter, u64 transfer_timeout);
|
||||
~UsbHs();
|
||||
|
||||
Result Init() override;
|
||||
Result IsUsbConnected(u64 timeout) override;
|
||||
|
||||
private:
|
||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) override;
|
||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) override;
|
||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_xfer_id) override;
|
||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 xfer_id, u32 *out_requested_size, u32 *out_transferred_size) override;
|
||||
|
||||
Result Connect();
|
||||
void Close();
|
||||
|
||||
private:
|
||||
u8 m_index{};
|
||||
UsbHsInterfaceFilter m_filter{};
|
||||
UsbHsInterface m_interface{};
|
||||
UsbHsClientIfSession m_s{};
|
||||
UsbHsClientEpSession m_endpoints[2]{};
|
||||
Event m_event{};
|
||||
bool m_connected{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::usb
|
||||
@@ -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
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
|
||||
17
sphaira/include/yati/container/nsp.hpp
Normal file
17
sphaira/include/yati/container/nsp.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include <switch.h>
|
||||
#include <span>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
|
||||
struct Nsp final : Base {
|
||||
using Base::Base;
|
||||
Result GetCollections(Collections& out) override;
|
||||
|
||||
// builds nsp meta data and the size of the entier nsp.
|
||||
static auto Build(std::span<CollectionEntry> collections, s64& size) -> std::vector<u8>;
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
15
sphaira/include/yati/container/xci.hpp
Normal file
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
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
|
||||
101
sphaira/include/yati/nx/es.hpp
Normal file
101
sphaira/include/yati/nx/es.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#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 Initialize();
|
||||
void Exit();
|
||||
Service* GetServiceSession();
|
||||
|
||||
// todo: find the ipc that gets personalised tickets.
|
||||
// todo: if ipc doesn't exist, manually parse es personalised save.
|
||||
// todo: add personalised -> common ticket conversion.
|
||||
// todo: make the above an option for both dump and install.
|
||||
|
||||
Result ImportTicket(const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
|
||||
Result CountCommonTicket(s32* count);
|
||||
Result CountPersonalizedTicket(s32* count);
|
||||
Result ListCommonTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count);
|
||||
Result ListPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count);
|
||||
Result ListMissingPersonalizedTicket(s32 *out_entries_written, FsRightsId* out_ids, s32 count); // untested
|
||||
Result GetCommonTicketSize(u64 *size_out, const FsRightsId* rightsId);
|
||||
Result GetCommonTicketData(u64 *size_out, void *tik_data, u64 tik_size, const FsRightsId* rightsId);
|
||||
Result GetCommonTicketAndCertificateSize(u64 *tik_size_out, u64 *cert_size_out, const FsRightsId* rightsId); // [4.0.0+]
|
||||
Result GetCommonTicketAndCertificateData(u64 *tik_size_out, u64 *cert_size_out, void* tik_buf, u64 tik_size, void* cert_buf, u64 cert_size, const FsRightsId* rightsId); // [4.0.0+]
|
||||
|
||||
// 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
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
|
||||
228
sphaira/include/yati/nx/nca.hpp
Normal file
228
sphaira/include/yati/nx/nca.hpp
Normal file
@@ -0,0 +1,228 @@
|
||||
#pragma once
|
||||
|
||||
#include "fs.hpp"
|
||||
#include "keys.hpp"
|
||||
#include "ncm.hpp"
|
||||
#include <switch.h>
|
||||
#include <vector>
|
||||
|
||||
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);
|
||||
|
||||
// helpers that parse an nca.
|
||||
Result ParseCnmt(const fs::FsPath& path, u64 program_id, ncm::PackagedContentMeta& header, std::vector<u8>& extended_header, std::vector<NcmPackagedContentInfo>& infos);
|
||||
Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out = nullptr, s64 nacp_size = 0, std::vector<u8>* icon_out = nullptr);
|
||||
|
||||
auto GetKeyGenStr(u8 key_gen) -> const char*;
|
||||
|
||||
} // namespace sphaira::nca
|
||||
47
sphaira/include/yati/nx/ncm.hpp
Normal file
47
sphaira/include/yati/nx/ncm.hpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#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 GetMetaTypeShortStr(u8 meta_type) -> const char*;
|
||||
|
||||
auto GetAppId(u8 meta_type, u64 id) -> u64;
|
||||
auto GetAppId(const NcmContentMetaKey& key) -> u64;
|
||||
auto GetAppId(const PackagedContentMeta& meta) -> u64;
|
||||
|
||||
auto GetContentIdFromStr(const char* str) -> NcmContentId;
|
||||
|
||||
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
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
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
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
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__ */
|
||||
56
sphaira/include/yati/nx/service_guard.h
Normal file
56
sphaira/include/yati/nx/service_guard.h
Normal file
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
#include <switch/types.h>
|
||||
#include <switch/result.h>
|
||||
#include <switch/kernel/mutex.h>
|
||||
#include <switch/sf/service.h>
|
||||
#include <switch/services/sm.h>
|
||||
|
||||
typedef struct ServiceGuard {
|
||||
Mutex mutex;
|
||||
u32 refCount;
|
||||
} ServiceGuard;
|
||||
|
||||
NX_INLINE bool serviceGuardBeginInit(ServiceGuard* g)
|
||||
{
|
||||
mutexLock(&g->mutex);
|
||||
return (g->refCount++) == 0;
|
||||
}
|
||||
|
||||
NX_INLINE Result serviceGuardEndInit(ServiceGuard* g, Result rc, void (*cleanupFunc)(void))
|
||||
{
|
||||
if (R_FAILED(rc)) {
|
||||
cleanupFunc();
|
||||
--g->refCount;
|
||||
}
|
||||
mutexUnlock(&g->mutex);
|
||||
return rc;
|
||||
}
|
||||
|
||||
NX_INLINE void serviceGuardExit(ServiceGuard* g, void (*cleanupFunc)(void))
|
||||
{
|
||||
mutexLock(&g->mutex);
|
||||
if (g->refCount && (--g->refCount) == 0)
|
||||
cleanupFunc();
|
||||
mutexUnlock(&g->mutex);
|
||||
}
|
||||
|
||||
#define NX_GENERATE_SERVICE_GUARD_PARAMS(name, _paramdecl, _parampass) \
|
||||
\
|
||||
static ServiceGuard g_##name##Guard; \
|
||||
NX_INLINE Result _##name##Initialize _paramdecl; \
|
||||
static void _##name##Cleanup(void); \
|
||||
\
|
||||
Result name##Initialize _paramdecl \
|
||||
{ \
|
||||
Result rc = 0; \
|
||||
if (serviceGuardBeginInit(&g_##name##Guard)) \
|
||||
rc = _##name##Initialize _parampass; \
|
||||
return serviceGuardEndInit(&g_##name##Guard, rc, _##name##Cleanup); \
|
||||
} \
|
||||
\
|
||||
void name##Exit(void) \
|
||||
{ \
|
||||
serviceGuardExit(&g_##name##Guard, _##name##Cleanup); \
|
||||
}
|
||||
|
||||
#define NX_GENERATE_SERVICE_GUARD(name) NX_GENERATE_SERVICE_GUARD_PARAMS(name, (void), ())
|
||||
29
sphaira/include/yati/source/base.hpp
Normal file
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
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
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
28
sphaira/include/yati/source/stream.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include <vector>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
// streams are for data that do not allow for random access,
|
||||
// such as FTP or MTP.
|
||||
struct Stream : Base {
|
||||
virtual ~Stream() = default;
|
||||
virtual Result ReadChunk(void* buf, s64 size, u64* bytes_read) = 0;
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
bool IsStream() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
Result m_open_result{};
|
||||
|
||||
private:
|
||||
s64 m_offset{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
22
sphaira/include/yati/source/stream_file.hpp
Normal file
22
sphaira/include/yati/source/stream_file.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
// this is used for testing that streams work, this code isn't used in normal
|
||||
// release builds as it is slower / less feature complete than normal.
|
||||
#pragma once
|
||||
|
||||
#include "stream.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct StreamFile final : Stream {
|
||||
StreamFile(FsFileSystem* fs, const fs::FsPath& path);
|
||||
~StreamFile();
|
||||
|
||||
Result ReadChunk(void* buf, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
FsFile m_file{};
|
||||
s64 m_offset{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
50
sphaira/include/yati/source/usb.hpp
Normal file
50
sphaira/include/yati/source/usb.hpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include "usb/usbds.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#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),
|
||||
};
|
||||
|
||||
Usb(u64 transfer_timeout);
|
||||
~Usb();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
Result Finished(u64 timeout);
|
||||
|
||||
Result IsUsbConnected(u64 timeout) {
|
||||
return m_usb->IsUsbConnected(timeout);
|
||||
}
|
||||
|
||||
Result WaitForConnection(u64 timeout, std::vector<std::string>& out_names);
|
||||
void SetFileNameForTranfser(const std::string& name);
|
||||
|
||||
void SignalCancel() override {
|
||||
m_usb->Cancel();
|
||||
}
|
||||
|
||||
private:
|
||||
Result SendCmdHeader(u32 cmdId, size_t dataSize, u64 timeout);
|
||||
Result SendFileRangeCmd(u64 offset, u64 size, u64 timeout);
|
||||
|
||||
private:
|
||||
std::unique_ptr<usb::UsbDs> m_usb;
|
||||
std::string m_transfer_file_name{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
138
sphaira/include/yati/yati.hpp
Normal file
138
sphaira/include/yati/yati.hpp
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 "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 = {});
|
||||
|
||||
} // namespace sphaira::yati
|
||||
@@ -1,7 +1,11 @@
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/bubbles.hpp"
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
#include "ui/option_box.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
@@ -34,8 +38,33 @@ extern "C" {
|
||||
namespace sphaira {
|
||||
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 {
|
||||
fs::FsPath music_path{"/config/sphaira/themes/default_music.bfstm"};
|
||||
fs::FsPath music_path{DEFAULT_MUSIC_PATH};
|
||||
std::string elements[ThemeEntryID_MAX]{};
|
||||
};
|
||||
|
||||
@@ -271,30 +300,33 @@ auto GetNroIcon(const std::vector<u8>& nro_icon) -> std::vector<u8> {
|
||||
auto LoadThemeMeta(const fs::FsPath& path, ThemeMeta& meta) -> bool {
|
||||
meta = {};
|
||||
|
||||
char buf[FS_MAX_PATH]{};
|
||||
int len{};
|
||||
len = ini_gets("meta", "name", "", buf, sizeof(buf) - 1, path);
|
||||
if (len <= 1) {
|
||||
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||
auto meta = static_cast<ThemeMeta*>(UserData);
|
||||
|
||||
if (!std::strcmp(Section, "meta")) {
|
||||
if (!std::strcmp(Key, "name")) {
|
||||
meta->name = Value;
|
||||
} else if (!std::strcmp(Key, "author")) {
|
||||
meta->author = Value;
|
||||
} else if (!std::strcmp(Key, "version")) {
|
||||
meta->version = Value;
|
||||
} else if (!std::strcmp(Key, "inherit")) {
|
||||
meta->inherit = Value;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (!ini_browse(cb, &meta, path)) {
|
||||
return false;
|
||||
}
|
||||
meta.name = buf;
|
||||
|
||||
len = ini_gets("meta", "author", "", buf, sizeof(buf) - 1, path);
|
||||
if (len <= 1) {
|
||||
if (meta.name.empty() || meta.author.empty() || meta.version.empty()) {
|
||||
return false;
|
||||
}
|
||||
meta.author = buf;
|
||||
|
||||
len = ini_gets("meta", "version", "", buf, sizeof(buf) - 1, path);
|
||||
if (len <= 1) {
|
||||
return false;
|
||||
}
|
||||
meta.version = buf;
|
||||
|
||||
len = ini_gets("meta", "inherit", "", buf, sizeof(buf) - 1, path);
|
||||
if (len > 1) {
|
||||
meta.inherit = buf;
|
||||
}
|
||||
|
||||
log_write("loaded meta from: %s\n", path.s);
|
||||
meta.ini_path = path;
|
||||
@@ -327,7 +359,7 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||
auto theme_data = static_cast<ThemeData*>(UserData);
|
||||
|
||||
if (!std::strcmp(Section, "theme")) {
|
||||
@@ -544,7 +576,7 @@ auto App::GetThemeIndex() -> s64 {
|
||||
return g_app->m_theme_index;
|
||||
}
|
||||
|
||||
auto App::GetDefaultImage(int* w, int* h) -> int {
|
||||
auto App::GetDefaultImage() -> int {
|
||||
return g_app->m_default_image;
|
||||
}
|
||||
|
||||
@@ -569,7 +601,19 @@ auto App::GetReplaceHbmenuEnable() -> bool {
|
||||
}
|
||||
|
||||
auto App::GetInstallEnable() -> bool {
|
||||
return g_app->m_install.Get();
|
||||
if (IsEmunand()) {
|
||||
return GetInstallEmummcEnable();
|
||||
} else {
|
||||
return GetInstallSysmmcEnable();
|
||||
}
|
||||
}
|
||||
|
||||
auto App::GetInstallSysmmcEnable() -> bool {
|
||||
return g_app->m_install_sysmmc.GetOr("install");
|
||||
}
|
||||
|
||||
auto App::GetInstallEmummcEnable() -> bool {
|
||||
return g_app->m_install_emummc.GetOr("install");
|
||||
}
|
||||
|
||||
auto App::GetInstallSdEnable() -> bool {
|
||||
@@ -580,10 +624,6 @@ auto App::GetInstallPrompt() -> bool {
|
||||
return g_app->m_install_prompt.Get();
|
||||
}
|
||||
|
||||
auto App::GetThemeShuffleEnable() -> bool {
|
||||
return g_app->m_theme_shuffle.Get();
|
||||
}
|
||||
|
||||
auto App::GetThemeMusicEnable() -> bool {
|
||||
return g_app->m_theme_music.Get();
|
||||
}
|
||||
@@ -679,7 +719,7 @@ void App::SetReplaceHbmenuEnable(bool enable) {
|
||||
}
|
||||
|
||||
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 (IsVersionNewer(sphaira_nacp.display_version, hbmenu_nacp.display_version)) {
|
||||
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.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
||||
} else {
|
||||
@@ -729,8 +769,12 @@ void App::SetReplaceHbmenuEnable(bool enable) {
|
||||
}
|
||||
}
|
||||
|
||||
void App::SetInstallEnable(bool enable) {
|
||||
g_app->m_install.Set(enable);
|
||||
void App::SetInstallSysmmcEnable(bool enable) {
|
||||
g_app->m_install_sysmmc.Set(enable);
|
||||
}
|
||||
|
||||
void App::SetInstallEmummcEnable(bool enable) {
|
||||
g_app->m_install_emummc.Set(enable);
|
||||
}
|
||||
|
||||
void App::SetInstallSdEnable(bool enable) {
|
||||
@@ -741,10 +785,6 @@ void App::SetInstallPrompt(bool 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) {
|
||||
g_app->m_theme_music.Set(enable);
|
||||
PlaySoundEffect(SoundEffect::SoundEffect_Music);
|
||||
@@ -758,7 +798,7 @@ void App::SetMtpEnable(bool enable) {
|
||||
if (App::GetMtpEnable() != enable) {
|
||||
g_app->m_mtp_enabled.Set(enable);
|
||||
if (enable) {
|
||||
hazeInitialize(haze_callback);
|
||||
hazeInitialize(haze_callback, 0x2C, 2);
|
||||
} else {
|
||||
hazeExit();
|
||||
}
|
||||
@@ -1009,13 +1049,16 @@ void App::Draw() {
|
||||
}
|
||||
|
||||
m_notif_manager.Draw(vg, &m_theme);
|
||||
ui::bubble::Draw(vg, &m_theme);
|
||||
|
||||
nvgResetTransform(vg);
|
||||
nvgEndFrame(this->vg);
|
||||
this->queue.presentImage(this->swapchain, slot);
|
||||
}
|
||||
|
||||
auto App::GetApp() -> App* {
|
||||
return g_app;
|
||||
}
|
||||
|
||||
auto App::GetVg() -> NVGcontext* {
|
||||
return g_app->vg;
|
||||
}
|
||||
@@ -1108,7 +1151,6 @@ void App::CloseTheme() {
|
||||
if (m_sound_ids[SoundEffect_Music]) {
|
||||
plsrPlayerFree(m_sound_ids[SoundEffect_Music]);
|
||||
m_sound_ids[SoundEffect_Music] = nullptr;
|
||||
plsrBFSTMClose(&m_theme.music);
|
||||
}
|
||||
|
||||
for (auto& e : m_theme.elements) {
|
||||
@@ -1138,10 +1180,12 @@ void App::LoadTheme(const ThemeMeta& meta) {
|
||||
|
||||
// load music
|
||||
if (!theme_data.music_path.empty()) {
|
||||
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &m_theme.music))) {
|
||||
if (R_SUCCEEDED(plsrPlayerLoadStream(&m_theme.music, &m_sound_ids[SoundEffect_Music]))) {
|
||||
PLSR_BFSTM music_stream;
|
||||
if (R_SUCCEEDED(plsrBFSTMOpen(theme_data.music_path, &music_stream))) {
|
||||
if (R_SUCCEEDED(plsrPlayerLoadStream(&music_stream, &m_sound_ids[SoundEffect_Music]))) {
|
||||
PlaySoundEffect(SoundEffect_Music);
|
||||
}
|
||||
plsrBFSTMClose(&music_stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1185,11 +1229,14 @@ void App::ScanThemeEntries() {
|
||||
ScanThemes("romfs:/themes/");
|
||||
romfsExit();
|
||||
}
|
||||
|
||||
// then load custom entries
|
||||
ScanThemes("/config/sphaira/themes/");
|
||||
}
|
||||
|
||||
App::App(const char* argv0) {
|
||||
TimeStamp ts;
|
||||
|
||||
g_app = this;
|
||||
m_start_timestamp = armGetSystemTick();
|
||||
if (!std::strncmp(argv0, "sdmc:/", 6)) {
|
||||
@@ -1213,10 +1260,11 @@ App::App(const char* argv0) {
|
||||
if (App::GetLogEnable()) {
|
||||
log_file_init();
|
||||
log_write("hello world\n");
|
||||
App::Notify("Warning! Logs are enabled, Sphaira will run slowly!"_i18n);
|
||||
}
|
||||
|
||||
if (App::GetMtpEnable()) {
|
||||
hazeInitialize(haze_callback);
|
||||
hazeInitialize(haze_callback, 0x2C, 2);
|
||||
}
|
||||
|
||||
if (App::GetFtpEnable()) {
|
||||
@@ -1304,16 +1352,21 @@ App::App(const char* argv0) {
|
||||
if (R_SUCCEEDED(plsrBFSAROpen("qlaunch:/sound/qlaunch.bfsar", &qlaunch_bfsar))) {
|
||||
ON_SCOPE_EXIT(plsrBFSARClose(&qlaunch_bfsar));
|
||||
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconFocus", &m_sound_ids[SoundEffect_Focus]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconScroll", &m_sound_ids[SoundEffect_Scroll]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconLimit", &m_sound_ids[SoundEffect_Limit]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeStartupMenu_game", &m_sound_ids[SoundEffect_Startup]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeGameIconAdd", &m_sound_ids[SoundEffect_Install]);
|
||||
plsrPlayerLoadSoundByName(&qlaunch_bfsar, "SeInsertError", &m_sound_ids[SoundEffect_Error]);
|
||||
const auto load_sound = [&](const char* name, u32 id) {
|
||||
if (R_FAILED(plsrPlayerLoadSoundByName(&qlaunch_bfsar, name, &m_sound_ids[id]))) {
|
||||
log_write("[PLSR] failed to load sound effect: %s\n", name);
|
||||
}
|
||||
};
|
||||
|
||||
load_sound("SeGameIconFocus", SoundEffect_Focus);
|
||||
load_sound("SeGameIconScroll", SoundEffect_Scroll);
|
||||
load_sound("SeGameIconLimit", SoundEffect_Limit);
|
||||
load_sound("StartupMenu_Game", SoundEffect_Startup);
|
||||
load_sound("SeGameIconAdd", SoundEffect_Install);
|
||||
load_sound("SeInsertError", SoundEffect_Error);
|
||||
|
||||
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Limit], 2.0f);
|
||||
plsrPlayerSetVolume(m_sound_ids[SoundEffect_Focus], 0.5f);
|
||||
PlaySoundEffect(SoundEffect_Startup);
|
||||
}
|
||||
} else {
|
||||
log_write("failed to mount romfs 0x0100000000001000\n");
|
||||
@@ -1323,11 +1376,7 @@ App::App(const char* argv0) {
|
||||
|
||||
fs::FsPath theme_path{};
|
||||
constexpr fs::FsPath default_theme_path{"romfs:/themes/abyss_theme.ini"};
|
||||
if (App::GetThemeShuffleEnable() && m_theme_meta_entries.size()) {
|
||||
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);
|
||||
}
|
||||
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
|
||||
|
||||
// try and load previous theme, default to previous version otherwise.
|
||||
ThemeMeta theme_meta;
|
||||
@@ -1358,6 +1407,10 @@ App::App(const char* argv0) {
|
||||
// padInitializeDefault(&m_pad);
|
||||
padInitializeAny(&m_pad);
|
||||
|
||||
// usbHsFsSetFileSystemMountFlags(UsbHsFsMountFlags_ReadOnly);
|
||||
// usbHsFsSetPopulateCallback();
|
||||
// usbHsFsInitialize(0);
|
||||
|
||||
m_prev_timestamp = ini_getl("paths", "timestamp", 0, App::CONFIG_PATH);
|
||||
const auto last_launch_path_size = ini_gets("paths", "last_launch_path", "", m_prev_last_launch, sizeof(m_prev_last_launch), App::CONFIG_PATH);
|
||||
fs::FsPath last_launch_path;
|
||||
@@ -1382,15 +1435,6 @@ App::App(const char* argv0) {
|
||||
const long old_launch_count = ini_getl(GetExePath(), "launch_count", 0, App::PLAYLOG_PATH);
|
||||
ini_putl(GetExePath(), "launch_count", old_launch_count + 1, App::PLAYLOG_PATH);
|
||||
|
||||
s64 sd_free_space;
|
||||
if (R_SUCCEEDED(fs.GetFreeSpace("/", &sd_free_space))) {
|
||||
log_write("sd_free_space: %zd\n", sd_free_space);
|
||||
}
|
||||
s64 sd_total_space;
|
||||
if (R_SUCCEEDED(fs.GetTotalSpace("/", &sd_total_space))) {
|
||||
log_write("sd_total_space: %zd\n", sd_total_space);
|
||||
}
|
||||
|
||||
// load default image
|
||||
if (R_SUCCEEDED(romfsInit())) {
|
||||
ON_SCOPE_EXIT(romfsExit());
|
||||
@@ -1400,11 +1444,8 @@ App::App(const char* argv0) {
|
||||
}
|
||||
}
|
||||
|
||||
// soon (tm)
|
||||
// ui::bubble::Init();
|
||||
|
||||
App::Push(std::make_shared<ui::menu::main::MainMenu>());
|
||||
log_write("finished app constructor\n");
|
||||
log_write("finished app constructor, time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
}
|
||||
|
||||
void App::PlaySoundEffect(SoundEffect effect) {
|
||||
@@ -1420,14 +1461,225 @@ void App::PlaySoundEffect(SoundEffect effect) {
|
||||
plsrPlayerPlay(id);
|
||||
}
|
||||
|
||||
void App::DisplayThemeOptions(bool left_side) {
|
||||
ui::SidebarEntryArray::Items theme_items{};
|
||||
const auto theme_meta = App::GetThemeMetaList();
|
||||
for (auto& p : theme_meta) {
|
||||
theme_items.emplace_back(p.name);
|
||||
}
|
||||
|
||||
auto options = std::make_shared<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 sysmmc"_i18n, App::GetInstallSysmmcEnable(), [](bool& enable){
|
||||
App::SetInstallSysmmcEnable(enable);
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<ui::SidebarEntryBool>("Enable emummc"_i18n, App::GetInstallEmummcEnable(), [](bool& enable){
|
||||
App::SetInstallEmummcEnable(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() {
|
||||
log_write("starting to exit\n");
|
||||
|
||||
i18n::exit();
|
||||
curl::Exit();
|
||||
|
||||
ui::bubble::Exit();
|
||||
|
||||
// this has to be called before any cleanup to ensure the lifetime of
|
||||
// nvg is still active as some widgets may need to free images.
|
||||
m_widgets.clear();
|
||||
@@ -1492,7 +1744,7 @@ App::~App() {
|
||||
|
||||
// found sphaira, now lets get compare version
|
||||
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 (IsVersionNewer(hbmenu_nacp.display_version, sphaira_nacp.display_version)) {
|
||||
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.s, rc, R_MODULE(rc), R_DESCRIPTION(rc));
|
||||
} else {
|
||||
@@ -1530,6 +1782,16 @@ App::~App() {
|
||||
ini_putl("paths", "timestamp", timestamp, App::CONFIG_PATH);
|
||||
}
|
||||
|
||||
auto App::GetVersionFromString(const char* str) -> u32 {
|
||||
u32 major{}, minor{}, macro{};
|
||||
std::sscanf(str, "%u.%u.%u", &major, &minor, ¯o);
|
||||
return MAKEHOSVERSION(major, minor, macro);
|
||||
}
|
||||
|
||||
auto App::IsVersionNewer(const char* current, const char* new_version) -> u32 {
|
||||
return GetVersionFromString(current) < GetVersionFromString(new_version);
|
||||
}
|
||||
|
||||
void App::createFramebufferResources() {
|
||||
this->swapchain = nullptr;
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
#include "defines.hpp"
|
||||
#include "evman.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
#include <switch.h>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <curl/curl.h>
|
||||
#include <yyjson.h>
|
||||
|
||||
@@ -25,7 +28,7 @@ namespace {
|
||||
log_write("curl_share_setopt(%s, %s) msg: %s\n", #opt, #v, curl_share_strerror(r)); \
|
||||
} \
|
||||
|
||||
constexpr auto API_AGENT = "ITotalJustice";
|
||||
constexpr auto API_AGENT = "TotalJustice";
|
||||
constexpr u64 CHUNK_SIZE = 1024*1024;
|
||||
constexpr auto MAX_THREADS = 4;
|
||||
constexpr int THREAD_PRIO = 0x2C;
|
||||
@@ -33,8 +36,18 @@ constexpr int THREAD_CORE = 1;
|
||||
|
||||
std::atomic_bool g_running{};
|
||||
CURLSH* g_curl_share{};
|
||||
// this is used for single threaded blocking installs.
|
||||
// avoids the needed for re-creating the handle each time.
|
||||
CURL* g_curl_single{};
|
||||
Mutex g_mutex_share[CURL_LOCK_DATA_LAST]{};
|
||||
|
||||
struct UploadStruct {
|
||||
std::span<const u8> data;
|
||||
s64 offset{};
|
||||
s64 size{};
|
||||
FsFile f{};
|
||||
};
|
||||
|
||||
struct DataStruct {
|
||||
std::vector<u8> data;
|
||||
s64 offset{};
|
||||
@@ -42,6 +55,16 @@ struct DataStruct {
|
||||
s64 file_offset{};
|
||||
};
|
||||
|
||||
struct SeekCustomData {
|
||||
OnUploadSeek cb{};
|
||||
s64 size{};
|
||||
};
|
||||
|
||||
// helper for creating webdav folders as libcurl does not have built-in
|
||||
// support for it.
|
||||
// only creates the folders if they don't exist.
|
||||
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool;
|
||||
|
||||
auto generate_key_from_path(const fs::FsPath& path) -> std::string {
|
||||
const auto key = crc32Calculate(path.s, path.size());
|
||||
return std::to_string(key);
|
||||
@@ -195,6 +218,8 @@ private:
|
||||
} else {
|
||||
const auto update_entry = [this, &hash_key](const char* tag, const std::string& value) {
|
||||
if (value.empty()) {
|
||||
// workaround for appstore accepting etags but not returning them.
|
||||
yyjson_mut_obj_remove_str(hash_key, tag);
|
||||
return true;
|
||||
} else {
|
||||
auto key = yyjson_mut_obj_get(hash_key, tag);
|
||||
@@ -300,7 +325,7 @@ struct ThreadQueue {
|
||||
threadClose(&m_thread);
|
||||
}
|
||||
|
||||
auto Add(const Api& api) -> bool {
|
||||
auto Add(const Api& api, bool is_upload = false) -> bool {
|
||||
if (api.GetUrl().empty() || api.GetPath().empty() || !api.GetOnComplete()) {
|
||||
return false;
|
||||
}
|
||||
@@ -310,10 +335,10 @@ struct ThreadQueue {
|
||||
|
||||
switch (api.GetPriority()) {
|
||||
case Priority::Normal:
|
||||
m_entries.emplace_back(api);
|
||||
m_entries.emplace_back(api).api.SetUpload(is_upload);
|
||||
break;
|
||||
case Priority::High:
|
||||
m_entries.emplace_front(api);
|
||||
m_entries.emplace_front(api).api.SetUpload(is_upload);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -364,6 +389,94 @@ auto ProgressCallbackFunc2(void *clientp, curl_off_t dltotal, curl_off_t dlnow,
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto SeekCallback(void *clientp, curl_off_t offset, int origin) -> int {
|
||||
if (!g_running) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto data_struct = static_cast<UploadStruct*>(clientp);
|
||||
|
||||
if (origin == SEEK_SET) {
|
||||
offset = offset;
|
||||
} else if (origin == SEEK_CUR) {
|
||||
offset = data_struct->offset + offset;
|
||||
} else if (origin == SEEK_END) {
|
||||
offset = data_struct->size;
|
||||
}
|
||||
|
||||
if (offset < 0 || offset > data_struct->size) {
|
||||
return CURL_SEEKFUNC_CANTSEEK;
|
||||
}
|
||||
|
||||
data_struct->offset = offset;
|
||||
return CURL_SEEKFUNC_OK;
|
||||
}
|
||||
|
||||
auto SeekCustomCallback(void *clientp, curl_off_t offset, int origin) -> int {
|
||||
if (!g_running) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto data_struct = static_cast<SeekCustomData*>(clientp);
|
||||
if (origin != SEEK_SET || offset < 0 || offset > data_struct->size) {
|
||||
return CURL_SEEKFUNC_CANTSEEK;
|
||||
}
|
||||
|
||||
if (!data_struct->cb(offset)) {
|
||||
return CURL_SEEKFUNC_CANTSEEK;
|
||||
}
|
||||
|
||||
return CURL_SEEKFUNC_OK;
|
||||
}
|
||||
|
||||
auto ReadFileCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
|
||||
if (!g_running) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto data_struct = static_cast<UploadStruct*>(userp);
|
||||
const auto realsize = size * nmemb;
|
||||
|
||||
u64 bytes_read;
|
||||
if (R_FAILED(fsFileRead(&data_struct->f, data_struct->offset, ptr, realsize, FsReadOption_None, &bytes_read))) {
|
||||
log_write("reading file error\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
data_struct->offset += bytes_read;
|
||||
svcSleepThread(YieldType_WithoutCoreMigration);
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
auto ReadMemoryCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
|
||||
if (!g_running) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto data_struct = static_cast<UploadStruct*>(userp);
|
||||
auto realsize = size * nmemb;
|
||||
realsize = std::min(realsize, data_struct->data.size() - data_struct->offset);
|
||||
|
||||
std::memcpy(ptr, data_struct->data.data(), realsize);
|
||||
data_struct->offset += realsize;
|
||||
|
||||
svcSleepThread(YieldType_WithoutCoreMigration);
|
||||
return realsize;
|
||||
}
|
||||
|
||||
auto ReadCustomCallback(char *ptr, size_t size, size_t nmemb, void *userp) -> size_t {
|
||||
if (!g_running) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto data_struct = static_cast<UploadInfo*>(userp);
|
||||
auto realsize = size * nmemb;
|
||||
const auto result = data_struct->m_callback(ptr, realsize);
|
||||
|
||||
svcSleepThread(YieldType_WithoutCoreMigration);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto WriteMemoryCallback(void *contents, size_t size, size_t num_files, void *userp) -> size_t {
|
||||
if (!g_running) {
|
||||
return 0;
|
||||
@@ -379,11 +492,9 @@ auto WriteMemoryCallback(void *contents, size_t size, size_t num_files, void *us
|
||||
|
||||
data_struct->data.resize(data_struct->offset + realsize);
|
||||
std::memcpy(data_struct->data.data() + data_struct->offset, contents, realsize);
|
||||
|
||||
data_struct->offset += realsize;
|
||||
|
||||
svcSleepThread(YieldType_WithoutCoreMigration);
|
||||
|
||||
return realsize;
|
||||
}
|
||||
|
||||
@@ -442,6 +553,121 @@ auto header_callback(char* b, size_t size, size_t nitems, void* userdata) -> siz
|
||||
return numbytes;
|
||||
}
|
||||
|
||||
auto EscapeString(CURL* curl, const std::string& str) -> std::string {
|
||||
char* s{};
|
||||
if (!curl) {
|
||||
s = curl_escape(str.data(), str.length());
|
||||
} else {
|
||||
s = curl_easy_escape(curl, str.data(), str.length());
|
||||
}
|
||||
|
||||
if (!s) {
|
||||
return str;
|
||||
}
|
||||
|
||||
const std::string result = s;
|
||||
curl_free(s);
|
||||
return result;
|
||||
}
|
||||
|
||||
auto EncodeUrl(std::string url) -> std::string {
|
||||
log_write("[CURL] encoding url\n");
|
||||
|
||||
if (url.starts_with("webdav://")) {
|
||||
log_write("[CURL] updating host\n");
|
||||
url.replace(0, std::strlen("webdav"), "https");
|
||||
log_write("[CURL] updated host: %s\n", url.c_str());
|
||||
}
|
||||
|
||||
auto clu = curl_url();
|
||||
R_UNLESS(clu, url);
|
||||
ON_SCOPE_EXIT(curl_url_cleanup(clu));
|
||||
|
||||
log_write("[CURL] setting url\n");
|
||||
CURLUcode clu_code;
|
||||
clu_code = curl_url_set(clu, CURLUPART_URL, url.c_str(), CURLU_URLENCODE);
|
||||
R_UNLESS(clu_code == CURLUE_OK, url);
|
||||
log_write("[CURL] set url success\n");
|
||||
|
||||
char* encoded_url;
|
||||
clu_code = curl_url_get(clu, CURLUPART_URL, &encoded_url, 0);
|
||||
R_UNLESS(clu_code == CURLUE_OK, url);
|
||||
|
||||
log_write("[CURL] encoded url: %s [vs]: %s\n", encoded_url, url.c_str());
|
||||
const std::string out = encoded_url;
|
||||
curl_free(encoded_url);
|
||||
return out;
|
||||
}
|
||||
|
||||
void SetCommonCurlOptions(CURL* curl, const Api& e) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERAGENT, API_AGENT);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOPROGRESS, 0L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_UPLOAD_BUFFERSIZE, 1024*512);
|
||||
|
||||
// enable all forms of compression supported by libcurl.
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_ACCEPT_ENCODING, "");
|
||||
|
||||
// for smb / ftp, try and use ssl if possible.
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_TRY);
|
||||
|
||||
// in most cases, this will use CURLAUTH_BASIC.
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPAUTH, (long)CURLAUTH_ANY);
|
||||
|
||||
// enable TE is server supports it.
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_TRANSFER_ENCODING, 1L);
|
||||
|
||||
// set flags.
|
||||
if (e.GetFlags() & Flag_NoBody) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOBODY, 1L);
|
||||
}
|
||||
|
||||
// set custom request.
|
||||
if (!e.GetCustomRequest().empty()) {
|
||||
log_write("[CURL] setting custom request: %s\n", e.GetCustomRequest().c_str());
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_CUSTOMREQUEST, e.GetCustomRequest().c_str());
|
||||
}
|
||||
|
||||
// set oath2 bearer.
|
||||
if (!e.GetBearer().empty()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XOAUTH2_BEARER, e.GetBearer().c_str());
|
||||
}
|
||||
|
||||
// set ssh pub/priv key file.
|
||||
if (!e.GetPubKey().empty()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSH_PUBLIC_KEYFILE, e.GetPubKey().c_str());
|
||||
}
|
||||
if (!e.GetPrivKey().empty()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSH_PRIVATE_KEYFILE, e.GetPrivKey().c_str());
|
||||
}
|
||||
|
||||
// set auth.
|
||||
if (!e.GetUserPass().m_user.empty()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_USERPWD, e.GetUserPass().m_user.c_str());
|
||||
}
|
||||
if (!e.GetUserPass().m_pass.empty()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_PASSWORD, e.GetUserPass().m_pass.c_str());
|
||||
}
|
||||
|
||||
// set port, if valid.
|
||||
if (e.GetPort()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_PORT, (long)e.GetPort());
|
||||
}
|
||||
|
||||
// progress calls.
|
||||
if (e.GetOnProgress()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
|
||||
} else {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
|
||||
}
|
||||
|
||||
}
|
||||
auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
// check if stop has been requested before starting download
|
||||
if (e.GetToken().stop_requested()) {
|
||||
@@ -451,6 +677,7 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
fs::FsPath tmp_buf;
|
||||
const bool has_file = !e.GetPath().empty() && e.GetPath() != "";
|
||||
const bool has_post = !e.GetFields().empty() && e.GetFields() != "";
|
||||
const auto encoded_url = EncodeUrl(e.GetUrl());
|
||||
|
||||
DataStruct chunk;
|
||||
Header header_in = e.GetHeader();
|
||||
@@ -471,7 +698,8 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (e.GetFlags() & Flag_Cache) {
|
||||
// only add etag if the dst file still exists.
|
||||
if ((e.GetFlags() & Flag_Cache) && fs::FileExists(&fs.m_fs, e.GetPath())) {
|
||||
g_cache.get(e.GetPath(), header_in);
|
||||
}
|
||||
}
|
||||
@@ -480,14 +708,9 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
chunk.data.reserve(CHUNK_SIZE);
|
||||
|
||||
curl_easy_reset(curl);
|
||||
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_FOLLOWLOCATION, 1L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FAILONERROR, 1L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SHARE, g_curl_share);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_BUFFERSIZE, 1024*512);
|
||||
SetCommonCurlOptions(curl, e);
|
||||
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, encoded_url.c_str());
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
|
||||
|
||||
@@ -521,15 +744,6 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list);
|
||||
}
|
||||
|
||||
// progress calls.
|
||||
if (e.GetOnProgress()) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFODATA, &e);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc2);
|
||||
} else {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallbackFunc1);
|
||||
}
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_NOPROGRESS, 0L);
|
||||
|
||||
// write calls.
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEFUNCTION, has_file ? WriteFileCallback : WriteMemoryCallback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEDATA, &chunk);
|
||||
@@ -558,6 +772,15 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
g_cache.set(e.GetPath(), header_out);
|
||||
}
|
||||
|
||||
// enable to log received headers.
|
||||
#if 0
|
||||
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()))) {
|
||||
@@ -573,18 +796,210 @@ auto DownloadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
}
|
||||
}
|
||||
|
||||
log_write("Downloaded %s %s\n", e.GetUrl().c_str(), curl_easy_strerror(res));
|
||||
log_write("Downloaded %s code: %ld %s\n", e.GetUrl().c_str(), http_code, curl_easy_strerror(res));
|
||||
return {success, http_code, header_out, chunk.data, e.GetPath()};
|
||||
}
|
||||
|
||||
auto DownloadInternal(const Api& e) -> ApiResult {
|
||||
auto curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
log_write("curl init failed\n");
|
||||
auto UploadInternal(CURL* curl, const Api& e) -> ApiResult {
|
||||
// check if stop has been requested before starting download
|
||||
if (e.GetToken().stop_requested()) {
|
||||
return {};
|
||||
}
|
||||
ON_SCOPE_EXIT(curl_easy_cleanup(curl));
|
||||
return DownloadInternal(curl, e);
|
||||
|
||||
if (e.GetUrl().starts_with("webdav://")) {
|
||||
if (!WebdavCreateFolder(curl, e)) {
|
||||
log_write("[CURL] failed to create webdav folder, aborting\n");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const auto& info = e.GetUploadInfo();
|
||||
const auto url = e.GetUrl() + "/" + info.m_name;
|
||||
const auto encoded_url = EncodeUrl(url);
|
||||
const bool has_file = !e.GetPath().empty() && e.GetPath() != "";
|
||||
|
||||
UploadStruct chunk{};
|
||||
DataStruct chunk_out{};
|
||||
SeekCustomData seek_data{};
|
||||
Header header_in = e.GetHeader();
|
||||
Header header_out;
|
||||
fs::FsNativeSd fs{};
|
||||
|
||||
if (has_file) {
|
||||
if (R_FAILED(fs.OpenFile(e.GetPath(), FsOpenMode_Read, &chunk.f))) {
|
||||
log_write("failed to open file: %s\n", e.GetPath().s);
|
||||
return {};
|
||||
}
|
||||
|
||||
fsFileGetSize(&chunk.f, &chunk.size);
|
||||
log_write("got chunk size: %zd\n", chunk.size);
|
||||
} else {
|
||||
if (info.m_callback) {
|
||||
chunk.size = info.m_size;
|
||||
log_write("setting upload size: %zu\n", chunk.size);
|
||||
} else {
|
||||
chunk.size = info.m_data.size();
|
||||
chunk.data = info.m_data;
|
||||
}
|
||||
}
|
||||
|
||||
if (url.starts_with("file://")) {
|
||||
const auto folder_path = fs::AppendPath("/", url.substr(std::strlen("file://")));
|
||||
log_write("creating local folder: %s\n", folder_path.s);
|
||||
// create the folder as libcurl doesn't seem to manually create it.
|
||||
fs.CreateDirectoryRecursivelyWithPath(folder_path);
|
||||
// remove the path so that libcurl can upload over it.
|
||||
fs.DeleteFile(folder_path);
|
||||
}
|
||||
|
||||
// reserve the first chunk
|
||||
chunk_out.data.reserve(CHUNK_SIZE);
|
||||
|
||||
curl_easy_reset(curl);
|
||||
SetCommonCurlOptions(curl, e);
|
||||
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_URL, encoded_url.c_str());
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERFUNCTION, header_callback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HEADERDATA, &header_out);
|
||||
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_UPLOAD, 1L);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)chunk.size);
|
||||
|
||||
// instruct libcurl to create ftp folders if they don't yet exist.
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR_RETRY);
|
||||
|
||||
struct curl_slist* list = NULL;
|
||||
ON_SCOPE_EXIT(if (list) { curl_slist_free_all(list); } );
|
||||
|
||||
for (const auto& [key, value] : header_in.m_map) {
|
||||
if (value.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// create header key value pair.
|
||||
const auto header_str = key + ": " + value;
|
||||
|
||||
// try to append header chunk.
|
||||
auto temp = curl_slist_append(list, header_str.c_str());
|
||||
if (temp) {
|
||||
log_write("adding header: %s\n", header_str.c_str());
|
||||
list = temp;
|
||||
} else {
|
||||
log_write("failed to append header\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (list) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_HTTPHEADER, list);
|
||||
}
|
||||
|
||||
// set callback for reading more data.
|
||||
if (info.m_callback) {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READFUNCTION, ReadCustomCallback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READDATA, &info);
|
||||
|
||||
if (e.GetOnUploadSeek()) {
|
||||
seek_data.cb = e.GetOnUploadSeek();
|
||||
seek_data.size = chunk.size;
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKFUNCTION, SeekCustomCallback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKDATA, &seek_data);
|
||||
}
|
||||
} else {
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READFUNCTION, has_file ? ReadFileCallback : ReadMemoryCallback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_READDATA, &chunk);
|
||||
|
||||
// allow for seeking upon uploads, may be used for ftp and http.
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKFUNCTION, SeekCallback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_SEEKDATA, &chunk);
|
||||
}
|
||||
|
||||
// write calls.
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
|
||||
CURL_EASY_SETOPT_LOG(curl, CURLOPT_WRITEDATA, &chunk_out);
|
||||
|
||||
// perform upload and cleanup after and report the result.
|
||||
const auto res = curl_easy_perform(curl);
|
||||
bool success = res == CURLE_OK;
|
||||
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
|
||||
if (has_file) {
|
||||
fsFileClose(&chunk.f);
|
||||
}
|
||||
|
||||
log_write("Uploaded %s code: %ld %s\n", url.c_str(), http_code, curl_easy_strerror(res));
|
||||
return {success, http_code, header_out, chunk_out.data};
|
||||
}
|
||||
|
||||
auto WebdavCreateFolder(CURL* curl, const Api& e) -> bool {
|
||||
// if using webdav, extract the file path and create the directories.
|
||||
// https://github.com/WebDAVDevs/webdav-request-samples/blob/master/webdav_curl.md
|
||||
if (e.GetUrl().starts_with("webdav://")) {
|
||||
log_write("[CURL] found webdav url\n");
|
||||
|
||||
const auto info = e.GetUploadInfo();
|
||||
if (info.m_name.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto& file_path = info.m_name;
|
||||
log_write("got file path: %s\n", file_path.c_str());
|
||||
|
||||
const auto file_loc = file_path.find_last_of('/');
|
||||
if (file_loc == file_path.npos) {
|
||||
log_write("failed to find last slash\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto path_view = file_path.substr(0, file_loc);
|
||||
log_write("got folder path: %s\n", path_view.c_str());
|
||||
|
||||
auto e2 = e;
|
||||
e2.SetOption(Path{});
|
||||
e2.SetOption(Url{e.GetUrl() + "/" + path_view});
|
||||
e2.SetOption(Flags{e.GetFlags() | Flag_NoBody});
|
||||
e2.SetOption(CustomRequest{"PROPFIND"});
|
||||
e2.SetOption(Header{
|
||||
{ "Depth", "0" },
|
||||
});
|
||||
|
||||
// test to see if the directory exists first.
|
||||
const auto exist_result = DownloadInternal(curl, e2);
|
||||
if (exist_result.success) {
|
||||
log_write("[CURL] folder already exist: %s\n", path_view.c_str());
|
||||
return true;
|
||||
} else {
|
||||
log_write("[CURL] folder does NOT exist, manually creating: %s\n", path_view.c_str());
|
||||
}
|
||||
|
||||
// make the request to create the folder.
|
||||
std::string folder;
|
||||
for (const auto dir : std::views::split(path_view, '/')) {
|
||||
if (dir.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
folder += "/" + std::string{dir.data(), dir.size()};
|
||||
e2.SetOption(Url{e.GetUrl() + folder});
|
||||
e2.SetOption(Header{});
|
||||
e2.SetOption(CustomRequest{"MKCOL"});
|
||||
|
||||
const auto result = DownloadInternal(curl, e2);
|
||||
if (result.code == 201) {
|
||||
log_write("[CURL] created webdav directory\n");
|
||||
} else if (result.code == 405) {
|
||||
log_write("[CURL] webdav directory already exists: %ld\n", result.code);
|
||||
} else {
|
||||
log_write("[CURL] failed to create webdav directory: %ld\n", result.code);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log_write("[CURL] not a webdav url: %s\n", e.GetUrl().c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void my_lock(CURL *handle, curl_lock_data data, curl_lock_access laccess, void *useptr) {
|
||||
@@ -608,10 +1023,12 @@ void ThreadEntry::ThreadFunc(void* p) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto result = DownloadInternal(data->m_curl, data->m_api);
|
||||
const auto result = data->m_api.IsUpload() ? UploadInternal(data->m_curl, data->m_api) : DownloadInternal(data->m_curl, data->m_api);
|
||||
if (g_running && data->m_api.GetOnComplete() && !data->m_api.GetToken().stop_requested()) {
|
||||
const DownloadEventData event_data{data->m_api.GetOnComplete(), result, data->m_api.GetToken()};
|
||||
evman::push(std::move(event_data), false);
|
||||
evman::push(
|
||||
DownloadEventData{data->m_api.GetOnComplete(), result, data->m_api.GetToken()},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
data->m_in_progress = false;
|
||||
@@ -708,6 +1125,11 @@ auto Init() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
g_curl_single = curl_easy_init();
|
||||
if (!g_curl_single) {
|
||||
log_write("failed to create g_curl_single\n");
|
||||
}
|
||||
|
||||
log_write("finished creating threads\n");
|
||||
|
||||
if (!g_cache.init()) {
|
||||
@@ -722,6 +1144,11 @@ void Exit() {
|
||||
|
||||
g_thread_queue.Close();
|
||||
|
||||
if (g_curl_single) {
|
||||
curl_easy_cleanup(g_curl_single);
|
||||
g_curl_single = nullptr;
|
||||
}
|
||||
|
||||
for (auto& entry : g_threads) {
|
||||
entry.Close();
|
||||
}
|
||||
@@ -739,14 +1166,28 @@ auto ToMemory(const Api& e) -> ApiResult {
|
||||
if (!e.GetPath().empty()) {
|
||||
return {};
|
||||
}
|
||||
return DownloadInternal(e);
|
||||
return DownloadInternal(g_curl_single, e);
|
||||
}
|
||||
|
||||
auto ToFile(const Api& e) -> ApiResult {
|
||||
if (e.GetPath().empty()) {
|
||||
return {};
|
||||
}
|
||||
return DownloadInternal(e);
|
||||
return DownloadInternal(g_curl_single, e);
|
||||
}
|
||||
|
||||
auto FromMemory(const Api& e) -> ApiResult {
|
||||
if (!e.GetPath().empty()) {
|
||||
return {};
|
||||
}
|
||||
return UploadInternal(g_curl_single, e);
|
||||
}
|
||||
|
||||
auto FromFile(const Api& e) -> ApiResult {
|
||||
if (e.GetPath().empty()) {
|
||||
return {};
|
||||
}
|
||||
return UploadInternal(g_curl_single, e);
|
||||
}
|
||||
|
||||
auto ToMemoryAsync(const Api& api) -> bool {
|
||||
@@ -757,14 +1198,16 @@ auto ToFileAsync(const Api& e) -> bool {
|
||||
return g_thread_queue.Add(e);
|
||||
}
|
||||
|
||||
auto FromMemoryAsync(const Api& api) -> bool {
|
||||
return g_thread_queue.Add(api, true);
|
||||
}
|
||||
|
||||
auto FromFileAsync(const Api& e) -> bool {
|
||||
return g_thread_queue.Add(e, true);
|
||||
}
|
||||
|
||||
auto EscapeString(const std::string& str) -> std::string {
|
||||
std::string result;
|
||||
const auto s = curl_escape(str.data(), str.length());
|
||||
if (s) {
|
||||
result = s;
|
||||
curl_free(s);
|
||||
}
|
||||
return result;
|
||||
return EscapeString(nullptr, str);
|
||||
}
|
||||
|
||||
} // namespace sphaira::curl
|
||||
|
||||
@@ -12,14 +12,30 @@
|
||||
#include <nx/vfs_nx.h>
|
||||
#include <nx/utils.h>
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
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";
|
||||
FtpSrvConfig g_ftpsrv_config = {0};
|
||||
volatile bool g_should_exit = false;
|
||||
bool g_is_running{false};
|
||||
Thread g_thread;
|
||||
std::mutex g_mutex{};
|
||||
InstallSharedData g_shared_data{};
|
||||
|
||||
void ftp_log_callback(enum FTP_API_LOG_TYPE type, const char* msg) {
|
||||
sphaira::App::NotifyFlashLed();
|
||||
@@ -29,6 +45,235 @@ void ftp_progress_callback(void) {
|
||||
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) {
|
||||
while (!g_should_exit) {
|
||||
ftpsrv_init(&g_ftpsrv_config);
|
||||
@@ -44,8 +289,6 @@ void loop(void* arg) {
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace sphaira::ftpsrv {
|
||||
|
||||
bool Init() {
|
||||
std::scoped_lock lock{g_mutex};
|
||||
if (g_is_running) {
|
||||
@@ -84,6 +327,9 @@ bool Init() {
|
||||
mount_bis = ini_getbool("Nx-App", "mount_bis", mount_bis, 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) {
|
||||
return false;
|
||||
}
|
||||
@@ -93,7 +339,13 @@ bool Init() {
|
||||
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;
|
||||
if (R_FAILED(rc = threadCreate(&g_thread, loop, nullptr, nullptr, 1024*16, 0x2C, 2))) {
|
||||
@@ -123,6 +375,40 @@ void Exit() {
|
||||
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
|
||||
|
||||
extern "C" {
|
||||
@@ -132,9 +418,9 @@ void log_file_write(const char* msg) {
|
||||
}
|
||||
|
||||
void log_file_fwrite(const char* fmt, ...) {
|
||||
std::va_list v{};
|
||||
va_list v{};
|
||||
va_start(v, fmt);
|
||||
log_write_arg(fmt, v);
|
||||
log_write_arg(fmt, &v);
|
||||
va_end(v);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ yyjson_doc* json;
|
||||
yyjson_val* root;
|
||||
std::unordered_map<std::string, std::string> g_tr_cache;
|
||||
|
||||
std::string get_internal(const char* str, size_t len) {
|
||||
const std::string kkey = {str, len};
|
||||
std::string get_internal(std::string_view str) {
|
||||
const std::string kkey = {str.data(), str.length()};
|
||||
|
||||
if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) {
|
||||
return it->second;
|
||||
@@ -28,7 +28,7 @@ std::string get_internal(const char* str, size_t len) {
|
||||
return kkey;
|
||||
}
|
||||
|
||||
auto key = yyjson_obj_getn(root, str, len);
|
||||
auto key = yyjson_obj_getn(root, str.data(), str.length());
|
||||
if (!key) {
|
||||
log_write("\tfailed to find key: [%s]\n", kkey.c_str());
|
||||
return kkey;
|
||||
@@ -78,6 +78,7 @@ bool init(long index) {
|
||||
case 11: setLanguage = SetLanguage_RU; break; // "Russian"
|
||||
case 12: lang_name = "se"; break; // "Swedish"
|
||||
case 13: lang_name = "vi"; break; // "Vietnamese"
|
||||
case 14: lang_name = "uk"; break; // "Ukrainian"
|
||||
}
|
||||
|
||||
switch (setLanguage) {
|
||||
@@ -86,7 +87,7 @@ bool init(long index) {
|
||||
case SetLanguage_DE: lang_name = "de"; break;
|
||||
case SetLanguage_IT: lang_name = "it"; break;
|
||||
case SetLanguage_ES: lang_name = "es"; break;
|
||||
case SetLanguage_ZHCN: lang_name = "zh"; break;
|
||||
case SetLanguage_ZHCN: lang_name = "zh"; break;
|
||||
case SetLanguage_KO: lang_name = "ko"; break;
|
||||
case SetLanguage_NL: lang_name = "nl"; break;
|
||||
case SetLanguage_PT: lang_name = "pt"; break;
|
||||
@@ -134,16 +135,16 @@ void exit() {
|
||||
g_i18n_data.clear();
|
||||
}
|
||||
|
||||
std::string get(const char* str) {
|
||||
return get_internal(str, std::strlen(str));
|
||||
std::string get(std::string_view str) {
|
||||
return get_internal(str);
|
||||
}
|
||||
|
||||
} // namespace sphaira::i18n
|
||||
|
||||
namespace literals {
|
||||
|
||||
std::string operator"" _i18n(const char* str, size_t len) {
|
||||
return sphaira::i18n::get_internal(str, len);
|
||||
std::string operator""_i18n(const char* str, size_t len) {
|
||||
return sphaira::i18n::get_internal({str, len});
|
||||
}
|
||||
|
||||
} // namespace literals
|
||||
|
||||
75
sphaira/source/location.cpp
Normal file
75
sphaira/source/location.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "location.hpp"
|
||||
#include "fs.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <minIni.h>
|
||||
|
||||
namespace sphaira::location {
|
||||
namespace {
|
||||
|
||||
constexpr fs::FsPath location_path{"/config/sphaira/locations.ini"};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Add(const Entry& e) {
|
||||
if (e.name.empty() || e.url.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ini_puts(e.name.c_str(), "url", e.url.c_str(), location_path);
|
||||
if (!e.user.empty()) {
|
||||
ini_puts(e.name.c_str(), "user", e.user.c_str(), location_path);
|
||||
}
|
||||
if (!e.pass.empty()) {
|
||||
ini_puts(e.name.c_str(), "pass", e.pass.c_str(), location_path);
|
||||
}
|
||||
if (!e.bearer.empty()) {
|
||||
ini_puts(e.name.c_str(), "bearer", e.bearer.c_str(), location_path);
|
||||
}
|
||||
if (!e.pub_key.empty()) {
|
||||
ini_puts(e.name.c_str(), "pub_key", e.pub_key.c_str(), location_path);
|
||||
}
|
||||
if (!e.priv_key.empty()) {
|
||||
ini_puts(e.name.c_str(), "priv_key", e.priv_key.c_str(), location_path);
|
||||
}
|
||||
if (e.port) {
|
||||
ini_putl(e.name.c_str(), "port", e.port, location_path);
|
||||
}
|
||||
}
|
||||
|
||||
auto Load() -> Entries {
|
||||
Entries out{};
|
||||
|
||||
auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||
auto e = static_cast<Entries*>(UserData);
|
||||
|
||||
// add new entry if use section changed.
|
||||
if (e->empty() || std::strcmp(Section, e->back().name.c_str())) {
|
||||
e->emplace_back(Section);
|
||||
}
|
||||
|
||||
if (!std::strcmp(Key, "url")) {
|
||||
e->back().url = Value;
|
||||
} else if (!std::strcmp(Key, "user")) {
|
||||
e->back().user = Value;
|
||||
} else if (!std::strcmp(Key, "pass")) {
|
||||
e->back().pass = Value;
|
||||
} else if (!std::strcmp(Key, "bearer")) {
|
||||
e->back().bearer = Value;
|
||||
} else if (!std::strcmp(Key, "pub_key")) {
|
||||
e->back().pub_key = Value;
|
||||
} else if (!std::strcmp(Key, "priv_key")) {
|
||||
e->back().priv_key = Value;
|
||||
} else if (!std::strcmp(Key, "port")) {
|
||||
e->back().port = std::atoi(Value);
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
ini_browse(cb, &out, location_path);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace sphaira::location
|
||||
@@ -14,18 +14,20 @@ std::FILE* file{};
|
||||
int nxlink_socket{};
|
||||
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) {
|
||||
std::vfprintf(file, s, v);
|
||||
std::vfprintf(file, s, *v);
|
||||
std::fflush(file);
|
||||
}
|
||||
if (nxlink_socket) {
|
||||
std::vprintf(s, v);
|
||||
std::vprintf(s, *v);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
auto log_file_init() -> bool {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (file) {
|
||||
@@ -70,11 +72,11 @@ void log_write(const char* s, ...) {
|
||||
|
||||
std::va_list v{};
|
||||
va_start(v, s);
|
||||
log_write_arg_internal(s, v);
|
||||
log_write_arg_internal(s, &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};
|
||||
if (!file && !nxlink_socket) {
|
||||
return;
|
||||
@@ -83,4 +85,6 @@ void log_write_arg(const char* s, std::va_list& v) {
|
||||
log_write_arg_internal(s, v);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -66,6 +66,9 @@ void userAppInit(void) {
|
||||
if (R_FAILED(rc = ncmInitialize()))
|
||||
diagAbortWithResult(rc);
|
||||
|
||||
// it doesn't matter if this fails.
|
||||
appletSetScreenShotPermission(AppletScreenShotPermission_Enable);
|
||||
|
||||
log_nxlink_init();
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ auto nro_parse_internal(fs::FsNative& fs, const fs::FsPath& path, NroEntry& entr
|
||||
|
||||
// some .nro (vgedit) have bad nacp, fake the nacp
|
||||
if (asset.magic != NROASSETHEADER_MAGIC || asset.nacp.offset == 0 || asset.nacp.size != sizeof(entry.nacp)) {
|
||||
std::memset(&asset, 0, sizeof(asset));
|
||||
std::memset(&entry.nacp, 0, sizeof(entry.nacp));
|
||||
|
||||
// get the name without the .nro
|
||||
@@ -157,6 +158,11 @@ auto nro_scan_internal(const fs::FsPath& path, std::vector<NroEntry>& nros, bool
|
||||
}
|
||||
|
||||
auto nro_get_icon_internal(FsFile* f, u64 size, u64 offset) -> std::vector<u8> {
|
||||
// protect again really messed up sizes.
|
||||
if (size > 1024 * 1024) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<u8> icon;
|
||||
u64 bytes_read{};
|
||||
icon.resize(size);
|
||||
|
||||
@@ -6,21 +6,35 @@
|
||||
namespace sphaira::option {
|
||||
|
||||
template<typename T>
|
||||
auto OptionBase<T>::Get() -> T {
|
||||
auto OptionBase<T>::GetInternal(const char* name) -> T {
|
||||
if (!m_value.has_value()) {
|
||||
if constexpr(std::is_same_v<T, bool>) {
|
||||
m_value = ini_getbool(m_section.c_str(), m_name.c_str(), m_default_value, App::CONFIG_PATH);
|
||||
m_value = ini_getbool(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, long>) {
|
||||
m_value = ini_getl(m_section.c_str(), m_name.c_str(), m_default_value, App::CONFIG_PATH);
|
||||
m_value = ini_getl(m_section.c_str(), name, m_default_value, App::CONFIG_PATH);
|
||||
} else if constexpr(std::is_same_v<T, std::string>) {
|
||||
char buf[FS_MAX_PATH];
|
||||
ini_gets(m_section.c_str(), m_name.c_str(), m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
|
||||
ini_gets(m_section.c_str(), name, m_default_value.c_str(), buf, sizeof(buf), App::CONFIG_PATH);
|
||||
m_value = buf;
|
||||
}
|
||||
}
|
||||
return m_value.value();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto OptionBase<T>::Get() -> T {
|
||||
return GetInternal(m_name.c_str());
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto OptionBase<T>::GetOr(const char* name) -> T {
|
||||
if (ini_haskey(m_section.c_str(), m_name.c_str(), App::CONFIG_PATH)) {
|
||||
return Get();
|
||||
} else {
|
||||
return GetInternal(name);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void OptionBase<T>::Set(T value) {
|
||||
m_value = value;
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
#include <string_view>
|
||||
#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 "defines.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_META_HASH_BLOCK_SIZE = 0x1000;
|
||||
constexpr u32 PFS0_PADDING_SIZE = 0x200;
|
||||
constexpr u32 NCA_SECTION_TOTAL = 0x4;
|
||||
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
||||
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
|
||||
struct BufHelper {
|
||||
BufHelper() = default;
|
||||
@@ -124,12 +89,6 @@ struct CnmtHeader {
|
||||
};
|
||||
static_assert(sizeof(CnmtHeader) == 0x20);
|
||||
|
||||
struct NcmContentStorageRecord {
|
||||
NcmContentMetaKey key;
|
||||
u8 storage_id; //
|
||||
u8 padding[0x7];
|
||||
};
|
||||
|
||||
struct NcmContentMetaData {
|
||||
NcmContentMetaHeader header;
|
||||
NcmApplicationMetaExtendedHeader extended;
|
||||
@@ -142,7 +101,7 @@ struct NcaMetaEntry {
|
||||
NcaEntry nca_entry;
|
||||
NcmContentMetaHeader content_meta_header{};
|
||||
NcmContentMetaKey content_meta_key{};
|
||||
NcmContentStorageRecord content_storage_record{};
|
||||
ncm::ContentStorageRecord content_storage_record{};
|
||||
NcmContentMetaData content_meta_data{};
|
||||
};
|
||||
|
||||
@@ -171,61 +130,6 @@ struct 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 {
|
||||
char title_name[0x10]{"Application"};
|
||||
char product_code[0x10]{};
|
||||
@@ -238,56 +142,6 @@ struct NcapPatch {
|
||||
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 {
|
||||
u32 entry_offset;
|
||||
struct romfs_dirent_ctx *parent; /* Parent node */
|
||||
@@ -317,70 +171,6 @@ typedef struct {
|
||||
u64 file_partition_size;
|
||||
} 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 {
|
||||
const u64 size = block - (off % block);
|
||||
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
|
||||
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||
NpdmMeta meta{};
|
||||
NpdmAci0 aci0{};
|
||||
NpdmAcid acid{};
|
||||
npdm::Meta meta{};
|
||||
npdm::Aci0 aci0{};
|
||||
npdm::Acid acid{};
|
||||
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
||||
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
||||
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
||||
@@ -805,7 +595,7 @@ void write_nca_padding(BufHelper& buf) {
|
||||
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{};
|
||||
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];
|
||||
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
|
||||
section.media_end_offset = end / 0x200; // Section end offset / 200
|
||||
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];
|
||||
fs_header.hash_type = NcaHashType_HierarchicalSha256;
|
||||
fs_header.fs_type = NcaFileSystemType_PFS0;
|
||||
fs_header.hash_type = nca::HashType_HierarchicalSha256;
|
||||
fs_header.fs_type = nca::FileSystemType_PFS0;
|
||||
fs_header.version = 0x2; // Always 2
|
||||
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
|
||||
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;
|
||||
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));
|
||||
}
|
||||
|
||||
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];
|
||||
fs_header.hash_type = NcaHashType_HierarchicalIntegrity;
|
||||
fs_header.fs_type = NcaFileSystemType_RomFS;
|
||||
fs_header.hash_type = nca::HashType_HierarchicalIntegrity;
|
||||
fs_header.fs_type = nca::FileSystemType_RomFS;
|
||||
fs_header.version = 0x2; // Always 2
|
||||
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.master_hash_size = SHA256_HASH_SIZE;
|
||||
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
|
||||
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_hash_table = build_pfs0_hash_table(pfs0, block_size);
|
||||
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;
|
||||
}
|
||||
|
||||
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& meta_info = fs_header.hash_data.integrity_meta_info;
|
||||
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);
|
||||
}
|
||||
|
||||
void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span<const u8> key, NcaContentType type, BufHelper& buf) {
|
||||
nca_header.magic = 0x3341434E;
|
||||
nca_header.distribution_type = NcaDistributionType_System;
|
||||
void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Keys& keys, nca::ContentType type, BufHelper& buf) {
|
||||
nca_header.magic = NCA3_MAGIC;
|
||||
nca_header.distribution_type = nca::DistributionType_System;
|
||||
nca_header.content_type = type;
|
||||
nca_header.title_id = tid;
|
||||
nca_header.program_id = tid;
|
||||
nca_header.sdk_version = 0x000C1100;
|
||||
nca_header.size = buf.tell();
|
||||
|
||||
nca_encrypt_header(&nca_header, key);
|
||||
nca_encrypt_header(&nca_header, keys.header_key);
|
||||
buf.seek(0);
|
||||
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;
|
||||
NcaHeader nca_header{};
|
||||
nca::Header nca_header{};
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
|
||||
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()) {
|
||||
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};
|
||||
}
|
||||
|
||||
auto create_control_nca(u64 tid, std::span<const u8> key, const FileEntries& romfs) -> NcaEntry{
|
||||
NcaHeader nca_header{};
|
||||
auto create_control_nca(u64 tid, const keys::Keys& keys, const FileEntries& romfs) -> NcaEntry{
|
||||
nca::Header nca_header{};
|
||||
BufHelper buf;
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
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{};
|
||||
NcmApplicationMetaExtendedHeader cnmt_extended{};
|
||||
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);
|
||||
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));
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
R_UNLESS(!config.nro_path.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());
|
||||
ON_SCOPE_EXIT(nsExit());
|
||||
|
||||
// generate header kek
|
||||
u8 header_kek[0x20];
|
||||
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));
|
||||
keys::Keys keys;
|
||||
R_TRY(keys::parse_keys(keys, false));
|
||||
|
||||
// fix args to include nro path
|
||||
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)];
|
||||
const auto hash_path = config.nro_path + config.args;
|
||||
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;
|
||||
|
||||
@@ -1124,7 +889,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
patch_npdm(exefs[1].data, npdm_patch);
|
||||
|
||||
nca_entries.emplace_back(
|
||||
create_program_nca(tid, key, exefs, romfs, logo)
|
||||
create_program_nca(tid, keys, exefs, romfs, logo)
|
||||
);
|
||||
} else {
|
||||
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);
|
||||
|
||||
nca_entries.emplace_back(
|
||||
create_control_nca(tid, key, romfs)
|
||||
create_control_nca(tid, keys, romfs)
|
||||
);
|
||||
}
|
||||
|
||||
// create meta
|
||||
NcmContentMetaHeader content_meta_header;
|
||||
NcmContentMetaKey content_meta_key;
|
||||
NcmContentStorageRecord content_storage_record;
|
||||
ncm::ContentStorageRecord content_storage_record;
|
||||
NcmContentMetaData content_meta_data;
|
||||
{
|
||||
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);
|
||||
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));
|
||||
}
|
||||
|
||||
// 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
|
||||
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_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
||||
R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
||||
|
||||
// force flush
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1241,7 +1012,7 @@ auto install_forwarder(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId st
|
||||
}
|
||||
|
||||
auto install_forwarder(OwoConfig& config, NcmStorageId storage_id) -> Result {
|
||||
App::Push(std::make_shared<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));
|
||||
}));
|
||||
R_SUCCEED();
|
||||
|
||||
@@ -42,14 +42,15 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
|
||||
} // namespace
|
||||
|
||||
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));
|
||||
out = cfg.out_text;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
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));
|
||||
out = std::atoll(cfg.out_text);
|
||||
R_SUCCEED();
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
#include "ui/types.hpp"
|
||||
#include "ui/object.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
|
||||
namespace sphaira::ui::bubble {
|
||||
namespace {
|
||||
|
||||
constexpr auto MAX_BUBBLES = 20;
|
||||
|
||||
struct Bubble {
|
||||
int start_x;
|
||||
int texture;
|
||||
int x,y,w,h;
|
||||
int y_inc;
|
||||
float sway_inc;
|
||||
float sway;
|
||||
bool sway_right_flag;
|
||||
};
|
||||
|
||||
Bubble bubbles[MAX_BUBBLES]{};
|
||||
int g_textures[3];
|
||||
bool g_is_init = false;
|
||||
|
||||
void setup_bubble(Bubble *bubble) {
|
||||
// setup normal vars.
|
||||
bubble->texture = (randomGet64() % std::size(g_textures));
|
||||
bubble->start_x = randomGet64() % (int)SCREEN_WIDTH;
|
||||
bubble->x = bubble->start_x;
|
||||
bubble->y = (int)SCREEN_HEIGHT - ( randomGet64() % 60 );
|
||||
const int size = (randomGet64() % 50) + 40;
|
||||
bubble->w = size;
|
||||
bubble->h = size;
|
||||
bubble->y_inc = (randomGet64() % 5) + 1;
|
||||
bubble->sway_inc = ((randomGet64() % 6) + 3) / 10;
|
||||
bubble->sway = 0;
|
||||
}
|
||||
|
||||
void setup_bubbles(void) {
|
||||
for (auto& bubble : bubbles) {
|
||||
setup_bubble(&bubble);
|
||||
}
|
||||
}
|
||||
|
||||
void update_bubbles(void) {
|
||||
for (auto& bubble : bubbles) {
|
||||
if (bubble.y + bubble.h < 0) {
|
||||
setup_bubble(&bubble);
|
||||
} else {
|
||||
bubble.y -= bubble.y_inc;
|
||||
|
||||
if (bubble.sway_right_flag) {
|
||||
bubble.x = bubble.start_x + (bubble.sway -= bubble.sway_inc);
|
||||
if (bubble.sway <= 0) {
|
||||
bubble.sway_right_flag = false;
|
||||
}
|
||||
} else {
|
||||
bubble.x = bubble.start_x + (bubble.sway += bubble.sway_inc);
|
||||
if (bubble.sway > 30) {
|
||||
bubble.sway_right_flag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Init() {
|
||||
if (g_is_init) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (R_SUCCEEDED(romfsInit())) {
|
||||
ON_SCOPE_EXIT(romfsExit());
|
||||
|
||||
auto vg = App::GetVg();
|
||||
g_textures[0] = nvgCreateImage(vg, "romfs:/theme/bubble1.png", 0);
|
||||
g_textures[1] = nvgCreateImage(vg, "romfs:/theme/bubble2.png", 0);
|
||||
g_textures[2] = nvgCreateImage(vg, "romfs:/theme/bubble3.png", 0);
|
||||
|
||||
setup_bubbles();
|
||||
g_is_init = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Draw(NVGcontext* vg, Theme* theme) {
|
||||
if (!g_is_init) {
|
||||
return;
|
||||
}
|
||||
|
||||
update_bubbles();
|
||||
|
||||
for (auto& bubble : bubbles) {
|
||||
gfx::drawImage(vg, bubble.x, bubble.y, bubble.w, bubble.h, g_textures[bubble.texture]);
|
||||
}
|
||||
}
|
||||
|
||||
void Exit() {
|
||||
if (!g_is_init) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto vg = App::GetVg();
|
||||
for (auto& texture : g_textures) {
|
||||
if (texture) {
|
||||
nvgDeleteImage(vg, texture);
|
||||
texture = 0;
|
||||
}
|
||||
}
|
||||
|
||||
g_is_init = false;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::bubble
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::ui {
|
||||
|
||||
@@ -14,6 +15,11 @@ List::List(s64 row, s64 page, const Vec4& pos, const Vec4& v, const Vec2& pad)
|
||||
SetScrollBarPos(SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200);
|
||||
}
|
||||
|
||||
auto List::ClampX(float x, s64 count) const -> float {
|
||||
const float x_max = count * GetMaxX();
|
||||
return std::clamp(x, 0.F, x_max);
|
||||
}
|
||||
|
||||
auto List::ClampY(float y, s64 count) const -> float {
|
||||
float y_max = 0;
|
||||
|
||||
@@ -25,17 +31,168 @@ auto List::ClampY(float y, s64 count) const -> float {
|
||||
y_max = (count - m_page) / m_row * GetMaxY();
|
||||
}
|
||||
|
||||
if (y < 0) {
|
||||
y = 0;
|
||||
} else if (y > y_max) {
|
||||
y = y_max;
|
||||
}
|
||||
|
||||
return y;
|
||||
return std::clamp(y, 0.F, y_max);
|
||||
}
|
||||
|
||||
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCallback callback) {
|
||||
if (touch->is_clicked && touch->in_range(GetPos())) {
|
||||
void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
|
||||
switch (m_layout) {
|
||||
case Layout::HOME:
|
||||
OnUpdateHome(controller, touch, index, count, callback);
|
||||
break;
|
||||
case Layout::GRID:
|
||||
OnUpdateGrid(controller, touch, index, count, callback);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
|
||||
switch (m_layout) {
|
||||
case Layout::HOME:
|
||||
DrawHome(vg, theme, count, callback);
|
||||
break;
|
||||
case Layout::GRID:
|
||||
DrawGrid(vg, theme, count, callback);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
|
||||
const auto old_index = index;
|
||||
const auto max = m_layout == Layout::GRID ? GetMaxY() : GetMaxX();
|
||||
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index + step < count) {
|
||||
index += step;
|
||||
} else {
|
||||
index = count - 1;
|
||||
}
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
s64 delta = index - old_index;
|
||||
s64 start = m_yoff / max * m_row;
|
||||
|
||||
while (index < start) {
|
||||
start -= m_row;
|
||||
m_yoff -= max;
|
||||
}
|
||||
|
||||
if (index - start >= m_page) {
|
||||
do {
|
||||
start += m_row;
|
||||
delta -= m_row;
|
||||
m_yoff += max;
|
||||
} while (delta > 0 && start + m_page < count);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
|
||||
const auto old_index = index;
|
||||
const auto max = m_layout == Layout::GRID ? GetMaxY() : GetMaxX();
|
||||
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= step) {
|
||||
index -= step;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
s64 start = m_yoff / max * m_row;
|
||||
|
||||
while (index < start) {
|
||||
start -= m_row;
|
||||
m_yoff -= max;
|
||||
}
|
||||
|
||||
while (index - start >= m_page && start + m_page < count) {
|
||||
start += m_row;
|
||||
m_yoff += max;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void List::OnUpdateHome(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
|
||||
if (controller->GotDown(Button::RIGHT)) {
|
||||
if (ScrollDown(index, m_row, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(Button::LEFT)) {
|
||||
if (ScrollUp(index, m_row, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (touch->is_clicked && touch->in_range(GetPos())) {
|
||||
auto v = m_v;
|
||||
v.x -= ClampX(m_yoff + m_y_prog, count);
|
||||
|
||||
for (s64 i = 0; i < count; i++, v.x += v.w + m_pad.x) {
|
||||
if (v.x > GetX() + GetW()) {
|
||||
break;
|
||||
}
|
||||
|
||||
Vec4 vv = v;
|
||||
// if not drawing, only return clipped v as its used for touch
|
||||
vv.w = std::min(v.x + v.w, m_pos.x + m_pos.w) - v.x;
|
||||
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
|
||||
|
||||
if (touch->in_range(vv)) {
|
||||
callback(true, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (touch->is_scroll && touch->in_range(GetPos())) {
|
||||
m_y_prog = (float)touch->initial.x - (float)touch->cur.x;
|
||||
} else if (touch->is_end) {
|
||||
m_yoff = ClampX(m_yoff + m_y_prog, count);
|
||||
m_y_prog = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void List::OnUpdateGrid(Controller* controller, TouchInfo* touch, s64 index, s64 count, TouchCallback callback) {
|
||||
const auto page_up_button = m_row == 1 ? Button::DPAD_LEFT : Button::L2;
|
||||
const auto page_down_button = m_row == 1 ? Button::DPAD_RIGHT : Button::R2;
|
||||
|
||||
if (controller->GotDown(Button::DOWN)) {
|
||||
if (ScrollDown(index, m_row, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(Button::UP)) {
|
||||
if (ScrollUp(index, m_row, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(page_down_button)) {
|
||||
if (ScrollDown(index, m_page, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (controller->GotDown(page_up_button)) {
|
||||
if (ScrollUp(index, m_page, count)) {
|
||||
callback(false, index);
|
||||
}
|
||||
} else if (m_row > 1 && controller->GotDown(Button::RIGHT)) {
|
||||
if (count && index < (count - 1) && (index + 1) % m_row != 0) {
|
||||
callback(false, index + 1);
|
||||
}
|
||||
} else if (m_row > 1 && controller->GotDown(Button::LEFT)) {
|
||||
if (count && index != 0 && (index % m_row) != 0) {
|
||||
callback(false, index - 1);
|
||||
}
|
||||
} else if (touch->is_clicked && touch->in_range(GetPos())) {
|
||||
auto v = m_v;
|
||||
v.y -= ClampY(m_yoff + m_y_prog, count);
|
||||
|
||||
@@ -63,7 +220,7 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCa
|
||||
vv.h = std::min(v.y + v.h, m_pos.y + m_pos.h) - v.y;
|
||||
|
||||
if (touch->in_range(vv)) {
|
||||
callback(i);
|
||||
callback(true, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -78,7 +235,31 @@ void List::OnUpdate(Controller* controller, TouchInfo* touch, s64 count, TouchCa
|
||||
}
|
||||
}
|
||||
|
||||
void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
|
||||
void List::DrawHome(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
|
||||
const auto yoff = ClampX(m_yoff + m_y_prog, count);
|
||||
auto v = m_v;
|
||||
v.x -= yoff;
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||
|
||||
for (s64 i = 0; i < count; i++, v.x += v.w + m_pad.x) {
|
||||
// skip anything not visible
|
||||
if (v.x + v.w < GetX()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (v.x > GetX() + GetW()) {
|
||||
break;
|
||||
}
|
||||
|
||||
callback(vg, theme, v, i);
|
||||
}
|
||||
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
void List::DrawGrid(NVGcontext* vg, Theme* theme, s64 count, Callback callback) const {
|
||||
const auto yoff = ClampY(m_yoff + m_y_prog, count);
|
||||
const s64 start = yoff / GetMaxY() * m_row;
|
||||
gfx::drawScrollbar2(vg, theme, m_scrollbar.x, m_scrollbar.y, m_scrollbar.h, start, count, m_row, m_page);
|
||||
@@ -87,7 +268,7 @@ void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) cons
|
||||
v.y -= yoff;
|
||||
|
||||
nvgSave(vg);
|
||||
nvgScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||
nvgIntersectScissor(vg, GetX(), GetY(), GetW(), GetH());
|
||||
|
||||
for (s64 i = 0; i < count; v.y += v.h + m_pad.y) {
|
||||
if (v.y > GetY() + GetH()) {
|
||||
@@ -116,74 +297,4 @@ void List::Draw(NVGcontext* vg, Theme* theme, s64 count, Callback callback) cons
|
||||
nvgRestore(vg);
|
||||
}
|
||||
|
||||
auto List::ScrollDown(s64& index, s64 step, s64 count) -> bool {
|
||||
const auto old_index = index;
|
||||
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index + step < count) {
|
||||
index += step;
|
||||
} else {
|
||||
index = count - 1;
|
||||
}
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
s64 delta = index - old_index;
|
||||
s64 start = m_yoff / GetMaxY() * m_row;
|
||||
|
||||
while (index < start) {
|
||||
start -= m_row;
|
||||
m_yoff -= GetMaxY();
|
||||
}
|
||||
|
||||
if (index - start >= m_page) {
|
||||
do {
|
||||
start += m_row;
|
||||
delta -= m_row;
|
||||
m_yoff += GetMaxY();
|
||||
} while (delta > 0 && start + m_page < count);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto List::ScrollUp(s64& index, s64 step, s64 count) -> bool {
|
||||
const auto old_index = index;
|
||||
|
||||
if (!count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index >= step) {
|
||||
index -= step;
|
||||
} else {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
if (index != old_index) {
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
s64 start = m_yoff / GetMaxY() * m_row;
|
||||
|
||||
while (index < start) {
|
||||
start -= m_row;
|
||||
m_yoff -= GetMaxY();
|
||||
}
|
||||
|
||||
while (index - start >= m_page && start + m_page < count) {
|
||||
start += m_row;
|
||||
m_yoff += GetMaxY();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "yyjson_helper.hpp"
|
||||
#include "swkbd.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "nro.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <string>
|
||||
@@ -34,8 +35,6 @@ constexpr auto URL_JSON = "https://switch.cdn.fortheusers.org/repo.json";
|
||||
constexpr auto URL_POST_FEEDBACK = "http://switchbru.com/appstore/feedback";
|
||||
constexpr auto URL_GET_FEEDACK = "http://switchbru.com/appstore/feedback";
|
||||
|
||||
constexpr const char* INI_SECTION = "appstore";
|
||||
|
||||
constexpr const char* FILTER_STR[] = {
|
||||
"All",
|
||||
"Games",
|
||||
@@ -236,18 +235,15 @@ void DrawIcon(NVGcontext* vg, const LazyImage& l, const LazyImage& d, float x, f
|
||||
bool crop = false;
|
||||
if (iw < w || ih < h) {
|
||||
rounded_image = false;
|
||||
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded);
|
||||
gfx::drawRect(vg, x, y, w, h, nvgRGB(i.first_pixel[0], i.first_pixel[1], i.first_pixel[2]), rounded ? 5 : 0);
|
||||
}
|
||||
if (iw > w || ih > h) {
|
||||
crop = true;
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x, y, w, h);
|
||||
}
|
||||
if (rounded_image) {
|
||||
gfx::drawImageRounded(vg, ix, iy, iw, ih, i.image);
|
||||
} else {
|
||||
gfx::drawImage(vg, ix, iy, iw, ih, i.image);
|
||||
}
|
||||
|
||||
gfx::drawImage(vg, ix, iy, iw, ih, i.image, rounded_image ? 5 : 0);
|
||||
if (crop) {
|
||||
nvgRestore(vg);
|
||||
}
|
||||
@@ -593,6 +589,14 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> bool {
|
||||
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
|
||||
|
||||
EntryMenu::EntryMenu(Entry& entry, const LazyImage& default_icon, Menu& menu)
|
||||
@@ -769,7 +773,7 @@ void EntryMenu::UpdateOptions() {
|
||||
};
|
||||
|
||||
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);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
@@ -782,7 +786,7 @@ void EntryMenu::UpdateOptions() {
|
||||
};
|
||||
|
||||
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);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
@@ -841,59 +845,40 @@ void EntryMenu::SetIndex(s64 index) {
|
||||
}
|
||||
}
|
||||
|
||||
auto toLower(const std::string& str) -> std::string {
|
||||
std::string lower;
|
||||
std::transform(str.cbegin(), str.cend(), std::back_inserter(lower), tolower);
|
||||
return lower;
|
||||
}
|
||||
|
||||
Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}, m_nro_entries{nro_entries} {
|
||||
Menu::Menu() : grid::Menu{"AppStore"_i18n} {
|
||||
fs::FsNativeSd fs;
|
||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/icons");
|
||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/banners");
|
||||
fs.CreateDirectoryRecursively("/switch/sphaira/cache/appstore/screens");
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
if (m_entries_current.empty()) {
|
||||
return;
|
||||
}
|
||||
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||
if (m_is_author) {
|
||||
m_is_author = false;
|
||||
if (m_is_search) {
|
||||
SetSearch(m_search_term);
|
||||
} else {
|
||||
SetFilter();
|
||||
}
|
||||
|
||||
if (m_index < (m_entries_current.size() - 1) && (m_index + 1) % 3 != 0) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved right\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_entries_current.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_index != 0 && (m_index % 3) != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved left\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 3, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 3, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 9, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 9, m_entries_current.size())) {
|
||||
SetIndex(m_index);
|
||||
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);
|
||||
}
|
||||
} else if (m_is_search) {
|
||||
m_is_search = false;
|
||||
SetFilter();
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
SetPop();
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Info"_i18n, [this](){
|
||||
@@ -927,17 +912,30 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
||||
order_items.push_back("Descending"_i18n);
|
||||
order_items.push_back("Ascending"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this, filter_items](s64& index_out){
|
||||
SetFilter((Filter)index_out);
|
||||
}, (s64)m_filter));
|
||||
SidebarEntryArray::Items layout_items;
|
||||
layout_items.push_back("List"_i18n);
|
||||
layout_items.push_back("Icon"_i18n);
|
||||
layout_items.push_back("Grid"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||
SetSort((SortType)index_out);
|
||||
}, (s64)m_sort));
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Filter"_i18n, filter_items, [this](s64& index_out){
|
||||
m_filter.Set(index_out);
|
||||
SetFilter();
|
||||
}, m_filter.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this, order_items](s64& index_out){
|
||||
SetOrder((OrderType)index_out);
|
||||
}, (s64)m_order));
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this](s64& index_out){
|
||||
m_sort.Set(index_out);
|
||||
SortAndFindLastFile();
|
||||
}, m_sort.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Order"_i18n, order_items, [this](s64& index_out){
|
||||
m_order.Set(index_out);
|
||||
SortAndFindLastFile();
|
||||
}, m_order.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Layout"_i18n, layout_items, [this](s64& index_out){
|
||||
m_layout.Set(index_out);
|
||||
OnLayoutChange();
|
||||
}, m_layout.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Search"_i18n, [this](){
|
||||
std::string out;
|
||||
@@ -967,14 +965,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"AppStore"_i18n}
|
||||
}
|
||||
});
|
||||
|
||||
m_filter = (Filter)ini_getl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
|
||||
m_sort = (SortType)ini_getl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
|
||||
m_order = (OrderType)ini_getl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
|
||||
|
||||
const Vec4 v{75, 110, 370, 155};
|
||||
const Vec2 pad{10, 10};
|
||||
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||
Sort();
|
||||
OnLayoutChange();
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
@@ -983,8 +974,8 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries_current.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
@@ -1069,43 +1060,27 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
}
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == m_index) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
} else {
|
||||
DrawElement(x, y, w, h, ThemeEntryID_GRID);
|
||||
}
|
||||
const auto selected = pos == m_index;
|
||||
const auto image_vec = DrawEntryNoImage(vg, theme, m_layout.Get(), v, selected, e.title.c_str(), e.author.c_str(), e.version.c_str());
|
||||
|
||||
constexpr double image_scale = 256.0 / 115.0;
|
||||
// const float image_size = 256 / image_scale;
|
||||
// const float image_size_h = 150 / image_scale;
|
||||
DrawIcon(vg, e.image, m_default_image, x + 20, y + 20, 115, 115, true, image_scale);
|
||||
const auto image_scale = 256.0 / image_vec.w;
|
||||
DrawIcon(vg, e.image, m_default_image, image_vec.x, image_vec.y, image_vec.w, image_vec.h, true, image_scale);
|
||||
// gfx::drawImage(vg, x + 20, y + 20, image_size, image_size_h, image.image ? image.image : m_default_image);
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, v.x, v.y, w - 30.f, h); // clip
|
||||
{
|
||||
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());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 80, 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());
|
||||
}
|
||||
nvgRestore(vg);
|
||||
|
||||
// todo: fix position on non-grid layout.
|
||||
float i_size = 22;
|
||||
switch (e.status) {
|
||||
case EntryStatus::Get:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_get.image, 20);
|
||||
break;
|
||||
case EntryStatus::Installed:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_installed.image, 20);
|
||||
break;
|
||||
case EntryStatus::Local:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_local.image, 20);
|
||||
break;
|
||||
case EntryStatus::Update:
|
||||
gfx::drawImageRounded(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image);
|
||||
gfx::drawImage(vg, x + w - 30.f, y + 110, i_size, i_size, m_update.image, 20);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -1141,12 +1116,16 @@ void Menu::OnFocusGained() {
|
||||
|
||||
for (u32 i = 0; i < m_entries_current.size(); i++) {
|
||||
if (current_entry.name == m_entries[m_entries_current[i]].name) {
|
||||
SetIndex(i);
|
||||
if (i >= 9) {
|
||||
m_list->SetYoff((((i - 9) + 3) / 3) * m_list->GetMaxY());
|
||||
const auto index = i;
|
||||
const auto row = m_list->GetRow();
|
||||
const auto page = m_list->GetPage();
|
||||
// guesstimate where the position is
|
||||
if (index >= page) {
|
||||
m_list->SetYoff((((index - page) + row) / row) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
SetIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1230,15 +1209,20 @@ void Menu::ScanHomebrew() {
|
||||
index.shrink_to_fit();
|
||||
}
|
||||
|
||||
SetFilter(Filter_All);
|
||||
SetFilter();
|
||||
SetIndex(0);
|
||||
Sort();
|
||||
}
|
||||
|
||||
void Menu::Sort() {
|
||||
// log_write("doing sort: size: %zu count: %zu\n", repo_json.size(), m_entries.size());
|
||||
|
||||
const auto sort = m_sort.Get();
|
||||
const auto order = m_order.Get();
|
||||
const auto filter = m_filter.Get();
|
||||
|
||||
// returns true if lhs should be before rhs
|
||||
const auto sorter = [this](EntryMini _lhs, EntryMini _rhs) -> bool {
|
||||
const auto sorter = [this, sort, order](EntryMini _lhs, EntryMini _rhs) -> bool {
|
||||
const auto& lhs = m_entries[_lhs];
|
||||
const auto& rhs = m_entries[_rhs];
|
||||
|
||||
@@ -1256,11 +1240,11 @@ void Menu::Sort() {
|
||||
} else if (!(lhs.status == EntryStatus::Local) && rhs.status == EntryStatus::Local) {
|
||||
return false;
|
||||
} else {
|
||||
switch (m_sort) {
|
||||
switch (sort) {
|
||||
case SortType_Updated: {
|
||||
if (lhs.updated_num == rhs.updated_num) {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||
} else if (m_order == OrderType_Descending) {
|
||||
} else if (order == OrderType_Descending) {
|
||||
return lhs.updated_num > rhs.updated_num;
|
||||
} else {
|
||||
return lhs.updated_num < rhs.updated_num;
|
||||
@@ -1269,7 +1253,7 @@ void Menu::Sort() {
|
||||
case SortType_Downloads: {
|
||||
if (lhs.app_dls == rhs.app_dls) {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||
} else if (m_order == OrderType_Descending) {
|
||||
} else if (order == OrderType_Descending) {
|
||||
return lhs.app_dls > rhs.app_dls;
|
||||
} else {
|
||||
return lhs.app_dls < rhs.app_dls;
|
||||
@@ -1278,14 +1262,14 @@ void Menu::Sort() {
|
||||
case SortType_Size: {
|
||||
if (lhs.extracted == rhs.extracted) {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||
} else if (m_order == OrderType_Descending) {
|
||||
} else if (order == OrderType_Descending) {
|
||||
return lhs.extracted > rhs.extracted;
|
||||
} else {
|
||||
return lhs.extracted < rhs.extracted;
|
||||
}
|
||||
} break;
|
||||
case SortType_Alphabetical: {
|
||||
if (m_order == OrderType_Descending) {
|
||||
if (order == OrderType_Descending) {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) < 0;
|
||||
} else {
|
||||
return strcasecmp(lhs.name.c_str(), rhs.name.c_str()) > 0;
|
||||
@@ -1299,34 +1283,43 @@ void Menu::Sort() {
|
||||
|
||||
|
||||
char subheader[128]{};
|
||||
std::snprintf(subheader, sizeof(subheader), "Filter: %s | Sort: %s | Order: %s"_i18n.c_str(), i18n::get(FILTER_STR[m_filter]).c_str(), i18n::get(SORT_STR[m_sort]).c_str(), i18n::get(ORDER_STR[m_order]).c_str());
|
||||
std::snprintf(subheader, sizeof(subheader), "Filter: %s | Sort: %s | Order: %s"_i18n.c_str(), i18n::get(FILTER_STR[filter]).c_str(), i18n::get(SORT_STR[sort]).c_str(), i18n::get(ORDER_STR[order]).c_str());
|
||||
SetTitleSubHeading(subheader);
|
||||
|
||||
std::sort(m_entries_current.begin(), m_entries_current.end(), sorter);
|
||||
}
|
||||
|
||||
void Menu::SetFilter(Filter filter) {
|
||||
void Menu::SortAndFindLastFile() {
|
||||
const auto name = GetEntry().name;
|
||||
Sort();
|
||||
SetIndex(0);
|
||||
|
||||
s64 index = -1;
|
||||
for (u64 i = 0; i < m_entries_current.size(); i++) {
|
||||
if (name == GetEntry(i).name) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
const auto row = m_list->GetRow();
|
||||
const auto page = m_list->GetPage();
|
||||
// guesstimate where the position is
|
||||
if (index >= page) {
|
||||
m_list->SetYoff((((index - page) + row) / row) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
SetIndex(index);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::SetFilter() {
|
||||
m_is_search = false;
|
||||
m_is_author = false;
|
||||
RemoveAction(Button::B);
|
||||
|
||||
m_filter = filter;
|
||||
m_entries_current = m_entries_index[m_filter];
|
||||
ini_putl(INI_SECTION, "filter", m_filter, App::CONFIG_PATH);
|
||||
SetIndex(0);
|
||||
Sort();
|
||||
}
|
||||
|
||||
void Menu::SetSort(SortType sort) {
|
||||
m_sort = sort;
|
||||
ini_putl(INI_SECTION, "sort", m_sort, App::CONFIG_PATH);
|
||||
SetIndex(0);
|
||||
Sort();
|
||||
}
|
||||
|
||||
void Menu::SetOrder(OrderType order) {
|
||||
m_order = order;
|
||||
ini_putl(INI_SECTION, "order", m_order, App::CONFIG_PATH);
|
||||
m_entries_current = m_entries_index[m_filter.Get()];
|
||||
SetIndex(0);
|
||||
Sort();
|
||||
}
|
||||
@@ -1338,27 +1331,15 @@ void Menu::SetSearch(const std::string& term) {
|
||||
|
||||
m_search_term = term;
|
||||
m_entries_index_search.clear();
|
||||
const auto query = toLower(m_search_term);
|
||||
const auto npos = std::string::npos;
|
||||
const auto query = m_search_term;
|
||||
|
||||
for (u64 i = 0; i < m_entries.size(); 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);
|
||||
}
|
||||
}
|
||||
|
||||
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_entries_current = m_entries_index_search;
|
||||
SetIndex(0);
|
||||
@@ -1372,35 +1353,26 @@ void Menu::SetAuthor() {
|
||||
|
||||
m_author_term = m_entries[m_entries_current[m_index]].author;
|
||||
m_entries_index_author.clear();
|
||||
const auto query = m_author_term;
|
||||
|
||||
for (u64 i = 0; i < m_entries.size(); 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);
|
||||
}
|
||||
}
|
||||
|
||||
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_entries_current = m_entries_index_author;
|
||||
SetIndex(0);
|
||||
Sort();
|
||||
}
|
||||
|
||||
void Menu::OnLayoutChange() {
|
||||
m_index = 0;
|
||||
grid::Menu::OnLayoutChange(m_list, m_layout.Get());
|
||||
}
|
||||
|
||||
LazyImage::~LazyImage() {
|
||||
if (image) {
|
||||
nvgDeleteImage(App::GetVg(), image);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
312
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
312
sphaira/source/ui/menus/ftp_menu.cpp
Normal file
@@ -0,0 +1,312 @@
|
||||
#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>
|
||||
#include <algorithm>
|
||||
|
||||
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);
|
||||
m_was_ftp_enabled = App::GetFtpEnable();
|
||||
if (!m_was_ftp_enabled) {
|
||||
log_write("[FTP] wasn't enabled, forcefully enabling\n");
|
||||
App::SetFtpEnable(true);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (!m_was_ftp_enabled) {
|
||||
log_write("[FTP] disabling on exit\n");
|
||||
App::SetFtpEnable(false);
|
||||
}
|
||||
|
||||
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
|
||||
1369
sphaira/source/ui/menus/game_menu.cpp
Normal file
1369
sphaira/source/ui/menus/game_menu.cpp
Normal file
File diff suppressed because it is too large
Load Diff
564
sphaira/source/ui/menus/gc_menu.cpp
Normal file
564
sphaira/source/ui/menus/gc_menu.cpp
Normal file
@@ -0,0 +1,564 @@
|
||||
#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>
|
||||
#include <algorithm>
|
||||
|
||||
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);
|
||||
|
||||
nsInitialize();
|
||||
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(nca::ParseCnmt(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()));
|
||||
|
||||
const auto id = m_entries[m_entry_index].app_id;
|
||||
|
||||
if (hosversionBefore(20,0,0)) {
|
||||
TimeStamp ts;
|
||||
auto control = std::make_unique<NsApplicationControlData>();
|
||||
u64 control_size;
|
||||
|
||||
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, control.get(), sizeof(NsApplicationControlData), &control_size))) {
|
||||
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
|
||||
NacpLanguageEntry* lang_entry{};
|
||||
nacpGetLanguageEntry(&control->nacp, &lang_entry);
|
||||
|
||||
if (lang_entry) {
|
||||
m_lang_entry = *lang_entry;
|
||||
}
|
||||
|
||||
const auto jpeg_size = control_size - sizeof(NacpStruct);
|
||||
m_icon = nvgCreateImageMem(App::GetVg(), 0, control->icon, jpeg_size);
|
||||
if (m_icon > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = id | collection.id_offset;
|
||||
if (hosversionAtLeast(17, 0, 0)) {
|
||||
fsGetProgramId(&program_id, path, FsContentAttributes_All);
|
||||
}
|
||||
|
||||
TimeStamp ts;
|
||||
if (R_SUCCEEDED(nca::ParseControl(path, program_id, &nacp, sizeof(nacp), &icon))) {
|
||||
log_write("\t\tnca::ParseControl(): %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
|
||||
|
||||
log_write("managed to parse control nca %s\n", path.s);
|
||||
NacpLanguageEntry* lang_entry{};
|
||||
nacpGetLanguageEntry(&nacp, &lang_entry);
|
||||
|
||||
if (lang_entry) {
|
||||
m_lang_entry = *lang_entry;
|
||||
}
|
||||
|
||||
m_icon = nvgCreateImageMem(App::GetVg(), 0, icon.data(), icon.size());
|
||||
if (m_icon > 0) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
log_write("\tFAILED to parse control nca %s\n", path.s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::gc
|
||||
@@ -247,27 +247,6 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
||||
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 1, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 1, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_RIGHT, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 8, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DPAD_LEFT, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 8, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
|
||||
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
||||
if (m_entries.empty()) {
|
||||
return;
|
||||
@@ -277,7 +256,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
||||
static GhApiEntry gh_entry;
|
||||
gh_entry = {};
|
||||
|
||||
App::Push(std::make_shared<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);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
@@ -325,7 +304,7 @@ Menu::Menu() : MenuBase{"GitHub"_i18n} {
|
||||
}
|
||||
|
||||
const auto func = [this, &asset_entry, ptr](){
|
||||
App::Push(std::make_shared<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);
|
||||
}, [this, ptr](bool success){
|
||||
if (success) {
|
||||
@@ -373,8 +352,8 @@ Menu::~Menu() {
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
|
||||
81
sphaira/source/ui/menus/grid_menu_base.cpp
Normal file
81
sphaira/source/ui/menus/grid_menu_base.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#include "app.hpp"
|
||||
#include "ui/menus/grid_menu_base.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::grid {
|
||||
|
||||
void Menu::DrawEntry(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version) {
|
||||
DrawEntry(vg, theme, true, layout, v, selected, image, name, author, version);
|
||||
}
|
||||
|
||||
Vec4 Menu::DrawEntryNoImage(NVGcontext* vg, Theme* theme, int layout, const Vec4& v, bool selected, const char* name, const char* author, const char* version) {
|
||||
return DrawEntry(vg, theme, false, layout, v, selected, 0, name, author, version);
|
||||
}
|
||||
|
||||
Vec4 Menu::DrawEntry(NVGcontext* vg, Theme* theme, bool draw_image, int layout, const Vec4& v, bool selected, int image, const char* name, const char* author, const char* version) {
|
||||
const auto& [x, y, w, h] = v;
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (selected) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
} else {
|
||||
DrawElement(v, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
Vec4 image_v = v;
|
||||
|
||||
if (layout == LayoutType_GridDetail) {
|
||||
image_v.x += 20;
|
||||
image_v.y += 20;
|
||||
image_v.w = 115;
|
||||
image_v.h = 115;
|
||||
|
||||
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), name);
|
||||
m_scroll_author.Draw(vg, selected, text_x, y + 80, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), author);
|
||||
m_scroll_version.Draw(vg, selected, text_x, y + 115, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), version);
|
||||
} else {
|
||||
if (selected) {
|
||||
gfx::drawAppLable(vg, theme, m_scroll_name, x, y, w, name);
|
||||
}
|
||||
}
|
||||
|
||||
if (draw_image) {
|
||||
gfx::drawImage(vg, image_v, image ?: App::GetDefaultImage(), 5);
|
||||
}
|
||||
|
||||
return image_v;
|
||||
}
|
||||
|
||||
void Menu::OnLayoutChange(std::unique_ptr<List>& list, int layout) {
|
||||
m_scroll_name.Reset();
|
||||
m_scroll_author.Reset();
|
||||
m_scroll_version.Reset();
|
||||
|
||||
switch (layout) {
|
||||
case LayoutType_List: {
|
||||
const Vec2 pad{14, 14};
|
||||
const Vec4 v{106, 194, 256, 256};
|
||||
list = std::make_unique<List>(1, 4, m_pos, v, pad);
|
||||
list->SetLayout(List::Layout::HOME);
|
||||
} break;
|
||||
|
||||
case LayoutType_Grid: {
|
||||
const Vec2 pad{10, 10};
|
||||
const Vec4 v{93, 186, 174, 174};
|
||||
list = std::make_unique<List>(6, 6*2, m_pos, v, pad);
|
||||
} break;
|
||||
|
||||
case LayoutType_GridDetail: {
|
||||
const Vec2 pad{10, 10};
|
||||
const Vec4 v{75, 110, 370, 155};
|
||||
list = std::make_unique<List>(3, 3*3, m_pos, v, pad);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::grid
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <minIni.h>
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
|
||||
namespace sphaira::ui::menu::homebrew {
|
||||
namespace {
|
||||
@@ -24,44 +25,15 @@ auto GenerateStarPath(const fs::FsPath& nro_path) -> fs::FsPath {
|
||||
return out;
|
||||
}
|
||||
|
||||
void FreeEntry(NVGcontext* vg, NroEntry& e) {
|
||||
nvgDeleteImage(vg, e.image);
|
||||
e.image = 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
Menu::Menu() : grid::Menu{"Homebrew"_i18n} {
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
if (m_index < (m_entries.size() - 1) && (m_index + 1) % 3 != 0) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved right\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index != 0 && (m_index % 3) != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved left\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 3, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 3, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
if (m_list->ScrollDown(m_index, 9, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
if (m_list->ScrollUp(m_index, 9, m_entries.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Launch"_i18n, [this](){
|
||||
nro_launch(m_entries[m_index].path);
|
||||
}}),
|
||||
@@ -86,6 +58,11 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
order_items.push_back("Descending"_i18n);
|
||||
order_items.push_back("Ascending"_i18n);
|
||||
|
||||
SidebarEntryArray::Items layout_items;
|
||||
layout_items.push_back("List"_i18n);
|
||||
layout_items.push_back("Icon"_i18n);
|
||||
layout_items.push_back("Grid"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||
m_sort.Set(index_out);
|
||||
SortAndFindLastFile();
|
||||
@@ -96,9 +73,14 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
SortAndFindLastFile();
|
||||
}, m_order.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Layout"_i18n, layout_items, [this](s64& index_out){
|
||||
m_layout.Set(index_out);
|
||||
OnLayoutChange();
|
||||
}, m_layout.Get()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Hide Sphaira"_i18n, m_hide_sphaira.Get(), [this](bool& enable){
|
||||
m_hide_sphaira.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
}));
|
||||
|
||||
#if 0
|
||||
@@ -114,11 +96,12 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
"Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
if (R_SUCCEEDED(fs::FsNativeSd().DeleteFile(m_entries[m_index].path))) {
|
||||
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));
|
||||
|
||||
@@ -131,7 +114,7 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
if (op_index && *op_index) {
|
||||
InstallHomebrew();
|
||||
}
|
||||
}
|
||||
}, m_entries[m_index].image
|
||||
));
|
||||
} else {
|
||||
InstallHomebrew();
|
||||
@@ -142,23 +125,17 @@ Menu::Menu() : MenuBase{"Homebrew"_i18n} {
|
||||
}})
|
||||
);
|
||||
|
||||
const Vec4 v{75, 110, 370, 155};
|
||||
const Vec2 pad{10, 10};
|
||||
m_list = std::make_unique<List>(3, 9, m_pos, v, pad);
|
||||
OnLayoutChange();
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
auto vg = App::GetVg();
|
||||
|
||||
for (auto&p : m_entries) {
|
||||
nvgDeleteImage(vg, p.image);
|
||||
}
|
||||
FreeEntries();
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
m_list->OnUpdate(controller, touch, m_entries.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
@@ -175,7 +152,6 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
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];
|
||||
|
||||
// lazy load image
|
||||
@@ -193,34 +169,24 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
}
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == m_index) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
} else {
|
||||
DrawElement(v, ThemeEntryID_GRID);
|
||||
}
|
||||
|
||||
const float image_size = 115;
|
||||
gfx::drawImageRounded(vg, x + 20, y + 20, image_size, image_size, e.image ? e.image : App::GetDefaultImage());
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||
{
|
||||
bool has_star = false;
|
||||
if (IsStarEnabled()) {
|
||||
if (!e.has_star.has_value()) {
|
||||
e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path));
|
||||
}
|
||||
has_star = e.has_star.value();
|
||||
bool has_star = false;
|
||||
if (IsStarEnabled()) {
|
||||
if (!e.has_star.has_value()) {
|
||||
e.has_star = fs::FsNativeSd().FileExists(GenerateStarPath(e.path));
|
||||
}
|
||||
|
||||
const float font_size = 18;
|
||||
gfx::drawTextArgs(vg, x + 148, y + 45, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s%s", has_star ? "\u2605 " : "", e.GetName());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 80, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetAuthor());
|
||||
gfx::drawTextArgs(vg, x + 148, y + 115, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.GetDisplayVersion());
|
||||
has_star = e.has_star.value();
|
||||
}
|
||||
nvgRestore(vg);
|
||||
|
||||
std::string name;
|
||||
if (has_star) {
|
||||
name = std::string("\u2605 ") + e.GetName();
|
||||
} else {
|
||||
name = e.GetName();
|
||||
}
|
||||
|
||||
const auto selected = pos == m_index;
|
||||
DrawEntry(vg, theme, m_layout.Get(), v, selected, e.image, name.c_str(), e.GetAuthor(), e.GetDisplayVersion());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -272,6 +238,7 @@ void Menu::InstallHomebrew() {
|
||||
|
||||
void Menu::ScanHomebrew() {
|
||||
TimeStamp ts;
|
||||
FreeEntries();
|
||||
nro_scan("/switch", m_entries, m_hide_sphaira.Get());
|
||||
log_write("nros found: %zu time_taken: %.2f\n", m_entries.size(), ts.GetSecondsD());
|
||||
|
||||
@@ -418,9 +385,11 @@ void Menu::SortAndFindLastFile() {
|
||||
}
|
||||
|
||||
if (index >= 0) {
|
||||
const auto row = m_list->GetRow();
|
||||
const auto page = m_list->GetPage();
|
||||
// guesstimate where the position is
|
||||
if (index >= 9) {
|
||||
m_list->SetYoff((((index - 9) + 3) / 3) * m_list->GetMaxY());
|
||||
if (index >= page) {
|
||||
m_list->SetYoff((((index - page) + row) / row) * m_list->GetMaxY());
|
||||
} else {
|
||||
m_list->SetYoff(0);
|
||||
}
|
||||
@@ -428,6 +397,21 @@ void Menu::SortAndFindLastFile() {
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::FreeEntries() {
|
||||
auto vg = App::GetVg();
|
||||
|
||||
for (auto&p : m_entries) {
|
||||
FreeEntry(vg, p);
|
||||
}
|
||||
|
||||
m_entries.clear();
|
||||
}
|
||||
|
||||
void Menu::OnLayoutChange() {
|
||||
m_index = 0;
|
||||
grid::Menu::OnLayoutChange(m_list, m_layout.Get());
|
||||
}
|
||||
|
||||
Result Menu::InstallHomebrew(const fs::FsPath& path, const NacpStruct& nacp, const std::vector<u8>& icon) {
|
||||
OwoConfig config{};
|
||||
config.nro_path = path.toString();
|
||||
|
||||
@@ -86,20 +86,7 @@ Menu::Menu() : MenuBase{"Irs"_i18n} {
|
||||
|
||||
SidebarEntryArray::Items controller_str;
|
||||
for (u32 i = 0; i < IRS_MAX_CAMERAS; i++) {
|
||||
const auto& e = m_entries[i];
|
||||
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
|
||||
switch (e.status) {
|
||||
case IrsIrCameraStatus_Available:
|
||||
text += " (Available)"_i18n;
|
||||
break;
|
||||
case IrsIrCameraStatus_Unsupported:
|
||||
text += " (Unsupported)"_i18n;
|
||||
break;
|
||||
case IrsIrCameraStatus_Unconnected:
|
||||
text += " (Unconnected)"_i18n;
|
||||
break;
|
||||
}
|
||||
controller_str.emplace_back(text);
|
||||
controller_str.emplace_back(GetEntryName(i));
|
||||
}
|
||||
|
||||
SidebarEntryArray::Items rotation_str;
|
||||
@@ -189,7 +176,7 @@ Menu::Menu() : MenuBase{"Irs"_i18n} {
|
||||
options->Add(std::make_shared<SidebarEntryBool>("External Light Filter"_i18n, m_config.is_external_light_filter_enabled, [this](bool& enable){
|
||||
m_config.is_external_light_filter_enabled = enable;
|
||||
UpdateConfig(&m_config);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Load Default"_i18n, [this](){
|
||||
@@ -233,6 +220,7 @@ Menu::~Menu() {
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
PollCameraStatus();
|
||||
SetTitleSubHeading(GetEntryName(m_index));
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
@@ -308,6 +296,20 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
|
||||
if (m_entries[m_index].status != IrsIrCameraStatus_Available) {
|
||||
// poll to get first available handle.
|
||||
PollCameraStatus(false);
|
||||
|
||||
// find the first available entry and connect to that.
|
||||
for (s64 i = 0; i < std::size(m_entries); i++) {
|
||||
if (m_entries[i].status == IrsIrCameraStatus_Available) {
|
||||
m_index = i;
|
||||
UpdateConfig(&m_config);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::PollCameraStatus(bool statup) {
|
||||
@@ -530,4 +532,21 @@ void Menu::updateColourArray() {
|
||||
UpdateImage();
|
||||
}
|
||||
|
||||
auto Menu::GetEntryName(s64 i) -> std::string {
|
||||
const auto& e = m_entries[i];
|
||||
std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i));
|
||||
switch (e.status) {
|
||||
case IrsIrCameraStatus_Available:
|
||||
text += " (Available)"_i18n;
|
||||
break;
|
||||
case IrsIrCameraStatus_Unsupported:
|
||||
text += " (Unsupported)"_i18n;
|
||||
break;
|
||||
case IrsIrCameraStatus_Unconnected:
|
||||
text += " (Unconnected)"_i18n;
|
||||
break;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::irs
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
#include "ui/menus/main_menu.hpp"
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
@@ -9,11 +6,19 @@
|
||||
#include "ui/progress_box.hpp"
|
||||
#include "ui/error_box.hpp"
|
||||
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "ui/menus/ftp_menu.hpp"
|
||||
#include "ui/menus/gc_menu.hpp"
|
||||
#include "ui/menus/game_menu.hpp"
|
||||
#include "ui/menus/appstore.hpp"
|
||||
|
||||
#include "app.hpp"
|
||||
#include "log.hpp"
|
||||
#include "download.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "web.hpp"
|
||||
#include "i18n.hpp"
|
||||
|
||||
#include <cstring>
|
||||
@@ -26,6 +31,22 @@ namespace {
|
||||
constexpr const char* GITHUB_URL{"https://api.github.com/repos/ITotalJustice/sphaira/releases/latest"};
|
||||
constexpr fs::FsPath CACHE_PATH{"/switch/sphaira/cache/sphaira_latest.json"};
|
||||
|
||||
template<typename T>
|
||||
auto MiscMenuFuncGenerator() {
|
||||
return std::make_shared<T>();
|
||||
}
|
||||
|
||||
const MiscMenuEntry MISC_MENU_ENTRIES[] = {
|
||||
{ .name = "Appstore", .title = "Appstore", .func = MiscMenuFuncGenerator<ui::menu::appstore::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "Games", .title = "Games", .func = MiscMenuFuncGenerator<ui::menu::game::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator<ui::menu::themezer::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator<ui::menu::gh::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
{ .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator<ui::menu::ftp::Menu>, .flag = MiscMenuFlag_Install },
|
||||
{ .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator<ui::menu::usb::Menu>, .flag = MiscMenuFlag_Install },
|
||||
{ .name = "GameCard", .title = "GameCard Install", .func = MiscMenuFuncGenerator<ui::menu::gc::Menu>, .flag = MiscMenuFlag_Shortcut|MiscMenuFlag_Install },
|
||||
{ .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator<ui::menu::irs::Menu>, .flag = MiscMenuFlag_Shortcut },
|
||||
};
|
||||
|
||||
auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string version) -> bool {
|
||||
static fs::FsPath zip_out{"/switch/sphaira/cache/update.zip"};
|
||||
constexpr auto chunk_size = 1024 * 512; // 512KiB
|
||||
@@ -143,8 +164,24 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v
|
||||
return true;
|
||||
}
|
||||
|
||||
auto CreateRightSideMenu() -> std::shared_ptr<MenuBase> {
|
||||
const auto name = App::GetApp()->m_right_side_menu.Get();
|
||||
|
||||
for (auto& e : GetMiscMenuEntries()) {
|
||||
if (e.name == name) {
|
||||
return e.func();
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_shared<ui::menu::appstore::Menu>();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto GetMiscMenuEntries() -> std::span<const MiscMenuEntry> {
|
||||
return MISC_MENU_ENTRIES;
|
||||
}
|
||||
|
||||
MainMenu::MainMenu() {
|
||||
curl::Api().ToFileAsync(
|
||||
curl::Url{GITHUB_URL},
|
||||
@@ -175,7 +212,7 @@ MainMenu::MainMenu() {
|
||||
|
||||
const auto version = yyjson_get_str(tag_key);
|
||||
R_UNLESS(version, false);
|
||||
if (std::strcmp(APP_VERSION, version) >= 0) {
|
||||
if (!App::IsVersionNewer(APP_VERSION, version)) {
|
||||
m_update_state = UpdateState::None;
|
||||
return true;
|
||||
}
|
||||
@@ -212,6 +249,7 @@ MainMenu::MainMenu() {
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::START, Action{App::Exit}),
|
||||
std::make_pair(Button::SELECT, Action{App::DisplayMiscOptions}),
|
||||
std::make_pair(Button::Y, Action{"Menu"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Menu Options"_i18n, "v" APP_VERSION_HASH, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
@@ -231,53 +269,31 @@ MainMenu::MainMenu() {
|
||||
language_items.push_back("Russian"_i18n);
|
||||
language_items.push_back("Swedish"_i18n);
|
||||
language_items.push_back("Vietnamese"_i18n);
|
||||
language_items.push_back("Ukrainian"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [this](){
|
||||
SidebarEntryArray::Items theme_items{};
|
||||
const auto theme_meta = App::GetThemeMetaList();
|
||||
for (auto& p : theme_meta) {
|
||||
theme_items.emplace_back(p.name);
|
||||
}
|
||||
|
||||
auto options = std::make_shared<Sidebar>("Theme Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Select Theme"_i18n, theme_items, [this, theme_items](s64& index_out){
|
||||
App::SetTheme(index_out);
|
||||
}, App::GetThemeIndex()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Shuffle"_i18n, App::GetThemeShuffleEnable(), [this](bool& enable){
|
||||
App::SetThemeShuffleEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
|
||||
App::SetThemeMusicEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [this](bool& enable){
|
||||
App::Set12HourTimeEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Theme"_i18n, [](){
|
||||
App::DisplayThemeOptions();
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Network Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [this](bool& enable){
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Ftp"_i18n, App::GetFtpEnable(), [](bool& enable){
|
||||
App::SetFtpEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [this](bool& enable){
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Mtp"_i18n, App::GetMtpEnable(), [](bool& enable){
|
||||
App::SetMtpEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [this](bool& enable){
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Nxlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){
|
||||
App::SetNxlinkEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
if (m_update_state == UpdateState::Update) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Download update: "_i18n + m_update_version, [this](){
|
||||
App::Push(std::make_shared<ProgressBox>("Downloading "_i18n + m_update_version, [this](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(0, "Downloading "_i18n, "Sphaira v" + m_update_version, [this](auto pbox){
|
||||
return InstallUpdate(pbox, m_update_url, m_update_version);
|
||||
}, [this](bool success){
|
||||
if (success) {
|
||||
@@ -296,76 +312,23 @@ MainMenu::MainMenu() {
|
||||
}
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [this](s64& index_out){
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Language"_i18n, language_items, [](s64& index_out){
|
||||
App::SetLanguage(index_out);
|
||||
}, (s64)App::GetLanguage()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Misc Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Themezer"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::themezer::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("GitHub"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::gh::Menu>());
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Irs"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::irs::Menu>());
|
||||
}));
|
||||
|
||||
if (App::IsApplication()) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Web"_i18n, [](){
|
||||
WebShow("https://lite.duckduckgo.com/lite");
|
||||
}));
|
||||
}
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Misc"_i18n, [](){
|
||||
App::DisplayMiscOptions();
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
SidebarEntryArray::Items install_items;
|
||||
install_items.push_back("System memory"_i18n);
|
||||
install_items.push_back("microSD card"_i18n);
|
||||
|
||||
SidebarEntryArray::Items text_scroll_speed_items;
|
||||
text_scroll_speed_items.push_back("Slow"_i18n);
|
||||
text_scroll_speed_items.push_back("Normal"_i18n);
|
||||
text_scroll_speed_items.push_back("Fast"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Logging"_i18n, App::GetLogEnable(), [this](bool& enable){
|
||||
App::SetLogEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [this](bool& enable){
|
||||
App::SetReplaceHbmenuEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Install forwarders"_i18n, App::GetInstallEnable(), [this](bool& enable){
|
||||
App::SetInstallEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
||||
App::SetInstallSdEnable(index_out);
|
||||
}, (s64)App::GetInstallSdEnable()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
|
||||
App::SetTextScrollSpeed(index_out);
|
||||
}, (s64)App::GetTextScrollSpeed()));
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [](){
|
||||
App::DisplayAdvancedOptions();
|
||||
}));
|
||||
}})
|
||||
);
|
||||
|
||||
m_homebrew_menu = std::make_shared<homebrew::Menu>();
|
||||
m_filebrowser_menu = std::make_shared<filebrowser::Menu>(m_homebrew_menu->GetHomebrewList());
|
||||
m_app_store_menu = std::make_shared<appstore::Menu>(m_homebrew_menu->GetHomebrewList());
|
||||
m_right_side_menu = CreateRightSideMenu();
|
||||
m_current_menu = m_homebrew_menu;
|
||||
|
||||
AddOnLRPress();
|
||||
@@ -416,16 +379,16 @@ void MainMenu::OnLRPress(std::shared_ptr<MenuBase> menu, Button b) {
|
||||
|
||||
void MainMenu::AddOnLRPress() {
|
||||
if (m_current_menu != m_filebrowser_menu) {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Files" : "Apps";
|
||||
const auto label = m_current_menu == m_homebrew_menu ? m_filebrowser_menu->GetShortTitle() : m_homebrew_menu->GetShortTitle();
|
||||
SetAction(Button::L, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_filebrowser_menu, Button::L);
|
||||
}});
|
||||
}
|
||||
|
||||
if (m_current_menu != m_app_store_menu) {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? "Store" : "Apps";
|
||||
if (m_current_menu != m_right_side_menu) {
|
||||
const auto label = m_current_menu == m_homebrew_menu ? m_right_side_menu->GetShortTitle() : m_homebrew_menu->GetShortTitle();
|
||||
SetAction(Button::R, Action{i18n::get(label), [this]{
|
||||
OnLRPress(m_app_store_menu, Button::R);
|
||||
OnLRPress(m_right_side_menu, Button::R);
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,17 +18,18 @@ MenuBase::~MenuBase() {
|
||||
|
||||
void MenuBase::Update(Controller* controller, TouchInfo* touch) {
|
||||
Widget::Update(controller, touch);
|
||||
|
||||
// update every second.
|
||||
if (m_poll_timestamp.GetSeconds() >= 1) {
|
||||
UpdateVars();
|
||||
}
|
||||
}
|
||||
|
||||
void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
DrawElement(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, ThemeEntryID_BACKGROUND);
|
||||
Widget::Draw(vg, theme);
|
||||
|
||||
// update every second, do this in Draw because Update() isn't called if it
|
||||
// doesn't have focus.
|
||||
if (m_poll_timestamp.GetSeconds() >= 1) {
|
||||
UpdateVars();
|
||||
}
|
||||
|
||||
const float start_y = 70;
|
||||
const float font_size = 22;
|
||||
const float spacing = 30;
|
||||
|
||||
@@ -306,6 +306,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> bool {
|
||||
}
|
||||
|
||||
const auto file_path = fs::AppendPath(dir_path, name);
|
||||
pbox->NewTransfer(name);
|
||||
|
||||
Result rc;
|
||||
if (R_FAILED(rc = fs.CreateFile(file_path, info.uncompressed_size, 0)) && rc != FsError_PathAlreadyExists) {
|
||||
@@ -365,49 +366,16 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this]{
|
||||
SetPop();
|
||||
// if search is valid, then we are in search mode, return back to normal.
|
||||
if (!m_search.empty()) {
|
||||
m_search.clear();
|
||||
InvalidateAllPages();
|
||||
} else {
|
||||
SetPop();
|
||||
}
|
||||
}});
|
||||
|
||||
this->SetActions(
|
||||
std::make_pair(Button::RIGHT, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_index < (page.m_packList.size() - 1) && (m_index + 1) % 3 != 0) {
|
||||
SetIndex(m_index + 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved right\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::LEFT, Action{[this](){
|
||||
if (m_index != 0 && (m_index % 3) != 0) {
|
||||
SetIndex(m_index - 1);
|
||||
App::PlaySoundEffect(SoundEffect_Scroll);
|
||||
log_write("moved left\n");
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::DOWN, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollDown(m_index, 3, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::UP, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollUp(m_index, 3, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::R2, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollDown(m_index, 6, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L2, Action{[this](){
|
||||
const auto& page = m_pages[m_page_index];
|
||||
if (m_list->ScrollUp(m_index, 6, page.m_packList.size())) {
|
||||
SetIndex(m_index);
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::A, Action{"Download"_i18n, [this](){
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Download theme?"_i18n,
|
||||
@@ -418,7 +386,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
const auto& entry = page.m_packList[m_index];
|
||||
const auto url = apiBuildUrlDownloadPack(entry);
|
||||
|
||||
App::Push(std::make_shared<ProgressBox>("Installing "_i18n + entry.details.name, [this, &entry](auto pbox){
|
||||
App::Push(std::make_shared<ProgressBox>(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox){
|
||||
return InstallTheme(pbox, entry);
|
||||
}, [this, &entry](bool success){
|
||||
if (success) {
|
||||
@@ -447,7 +415,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Nsfw"_i18n, m_nsfw.Get(), [this](bool& v_out){
|
||||
m_nsfw.Set(v_out);
|
||||
InvalidateAllPages();
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Sort"_i18n, sort_items, [this, sort_items](s64& index_out){
|
||||
if (m_sort.Get() != index_out) {
|
||||
@@ -480,10 +448,12 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
|
||||
m_search = out;
|
||||
// PackListDownload();
|
||||
InvalidateAllPages();
|
||||
}
|
||||
}));
|
||||
}}),
|
||||
std::make_pair(Button::R, Action{"Next Page"_i18n, [this](){
|
||||
std::make_pair(Button::R2, Action{"Next"_i18n, [this](){
|
||||
m_page_index++;
|
||||
if (m_page_index >= m_page_index_max) {
|
||||
m_page_index = m_page_index_max - 1;
|
||||
@@ -491,7 +461,7 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
PackListDownload();
|
||||
}
|
||||
}}),
|
||||
std::make_pair(Button::L, Action{"Prev Page"_i18n, [this](){
|
||||
std::make_pair(Button::L2, Action{"Prev"_i18n, [this](){
|
||||
if (m_page_index) {
|
||||
m_page_index--;
|
||||
PackListDownload();
|
||||
@@ -524,8 +494,8 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_list->OnUpdate(controller, touch, page.m_packList.size(), [this](auto i) {
|
||||
if (m_index == i) {
|
||||
m_list->OnUpdate(controller, touch, m_index, page.m_packList.size(), [this](bool touch, auto i) {
|
||||
if (touch && m_index == i) {
|
||||
FireAction(Button::A);
|
||||
} else {
|
||||
App::PlaySoundEffect(SoundEffect_Focus);
|
||||
@@ -567,7 +537,8 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
auto& e = page.m_packList[pos];
|
||||
|
||||
auto text_id = ThemeEntryID_TEXT;
|
||||
if (pos == m_index) {
|
||||
const auto selected = pos == m_index;
|
||||
if (selected) {
|
||||
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||
} else {
|
||||
@@ -634,16 +605,14 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
}
|
||||
}
|
||||
|
||||
gfx::drawImageRounded(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage());
|
||||
gfx::drawImage(vg, x + xoff, y, 320, 180, image.image ? image.image : App::GetDefaultImage(), 5);
|
||||
}
|
||||
|
||||
nvgSave(vg);
|
||||
nvgIntersectScissor(vg, x, y, w - 30.f, h); // clip
|
||||
{
|
||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 20, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.details.name.c_str());
|
||||
gfx::drawTextArgs(vg, x + xoff, y + 180 + 55, 18, NVG_ALIGN_LEFT, theme->GetColour(text_id), "%s", e.creator.display_name.c_str());
|
||||
}
|
||||
nvgRestore(vg);
|
||||
const auto text_x = x + xoff;
|
||||
const auto text_clip_w = w - 30.f - xoff;
|
||||
const float font_size = 18;
|
||||
m_scroll_name.Draw(vg, selected, text_x, y + 180 + 20, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.details.name.c_str());
|
||||
m_scroll_author.Draw(vg, selected, text_x, y + 180 + 55, text_clip_w, font_size, NVG_ALIGN_LEFT, theme->GetColour(text_id), e.creator.display_name.c_str());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -652,11 +621,9 @@ void Menu::OnFocusGained() {
|
||||
}
|
||||
|
||||
void Menu::InvalidateAllPages() {
|
||||
for (auto& e : m_pages) {
|
||||
e.m_packList.clear();
|
||||
e.m_ready = PageLoadState::None;
|
||||
}
|
||||
|
||||
m_pages.clear();
|
||||
m_pages.resize(1);
|
||||
m_page_index = 0;
|
||||
PackListDownload();
|
||||
}
|
||||
|
||||
|
||||
180
sphaira/source/ui/menus/usb_menu.cpp
Normal file
180
sphaira/source/ui/menus/usb_menu.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui::menu::usb {
|
||||
namespace {
|
||||
|
||||
constexpr u64 CONNECTION_TIMEOUT = UINT64_MAX;
|
||||
constexpr u64 TRANSFER_TIMEOUT = UINT64_MAX;
|
||||
constexpr u64 FINISHED_TIMEOUT = 1e+9 * 3; // 3 seconds.
|
||||
|
||||
void thread_func(void* user) {
|
||||
auto app = static_cast<Menu*>(user);
|
||||
|
||||
for (;;) {
|
||||
if (app->GetToken().stop_requested()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto rc = app->m_usb_source->IsUsbConnected(CONNECTION_TIMEOUT);
|
||||
if (rc == ::sphaira::usb::UsbDs::Result_Cancelled) {
|
||||
break;
|
||||
}
|
||||
|
||||
// set connected status
|
||||
mutexLock(&app->m_mutex);
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
app->m_state = State::Connected_WaitForFileList;
|
||||
} else {
|
||||
app->m_state = State::None;
|
||||
}
|
||||
mutexUnlock(&app->m_mutex);
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
std::vector<std::string> names;
|
||||
if (R_SUCCEEDED(app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, names))) {
|
||||
mutexLock(&app->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
||||
app->m_state = State::Connected_StartingTransfer;
|
||||
app->m_names = names;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu() : MenuBase{"USB"_i18n} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
SetAction(Button::X, Action{"Options"_i18n, [this](){
|
||||
App::DisplayInstallOptions(false);
|
||||
}});
|
||||
|
||||
// if mtp is enabled, disable it for now.
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Disable MTP for usb install"_i18n);
|
||||
App::SetMtpEnable(false);
|
||||
}
|
||||
|
||||
// 3 second timeout for transfers.
|
||||
m_usb_source = std::make_shared<yati::source::Usb>(TRANSFER_TIMEOUT);
|
||||
if (R_FAILED(m_usb_source->GetOpenResult())) {
|
||||
log_write("usb init open\n");
|
||||
m_state = State::Failed;
|
||||
}
|
||||
|
||||
mutexInit(&m_mutex);
|
||||
|
||||
if (m_state != State::Failed) {
|
||||
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, 0x2C, 1);
|
||||
threadStart(&m_thread);
|
||||
}
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
m_stop_source.request_stop();
|
||||
m_usb_source->SignalCancel();
|
||||
threadWaitForExit(&m_thread);
|
||||
threadClose(&m_thread);
|
||||
|
||||
// free usb source before re-enabling mtp.
|
||||
log_write("closing data!!!!\n");
|
||||
m_usb_source.reset();
|
||||
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Re-enabled MTP"_i18n);
|
||||
App::SetMtpEnable(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
if (m_state == State::Connected_StartingTransfer) {
|
||||
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 {
|
||||
ON_SCOPE_EXIT(m_usb_source->Finished(FINISHED_TIMEOUT));
|
||||
|
||||
log_write("inside progress box\n");
|
||||
for (const auto& file_name : m_names) {
|
||||
m_usb_source->SetFileNameForTranfser(file_name);
|
||||
|
||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source, file_name);
|
||||
if (R_FAILED(rc)) {
|
||||
m_usb_source->SignalCancel();
|
||||
log_write("exiting usb install\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
App::Notify("Installed via usb"_i18n);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool result){
|
||||
if (result) {
|
||||
App::Notify("Usb install success!"_i18n);
|
||||
m_state = State::Done;
|
||||
SetPop();
|
||||
} else {
|
||||
App::Notify("Usb install failed!"_i18n);
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
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_WaitForFileList:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, waiting for file list..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected_StartingTransfer:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Connected, starting transfer..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
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 init usb, press B to exit..."_i18n.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::usb
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "log.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdarg>
|
||||
@@ -10,6 +11,9 @@
|
||||
namespace sphaira::ui::gfx {
|
||||
namespace {
|
||||
|
||||
constexpr auto ALIGN_HOR = NVG_ALIGN_LEFT|NVG_ALIGN_CENTER|NVG_ALIGN_RIGHT;
|
||||
constexpr auto ALIGN_VER = NVG_ALIGN_TOP|NVG_ALIGN_MIDDLE|NVG_ALIGN_BOTTOM|NVG_ALIGN_BASELINE;
|
||||
|
||||
constexpr std::array buttons = {
|
||||
std::pair{Button::A, "\uE0E0"},
|
||||
std::pair{Button::B, "\uE0E1"},
|
||||
@@ -17,10 +21,8 @@ constexpr std::array buttons = {
|
||||
std::pair{Button::Y, "\uE0E3"},
|
||||
std::pair{Button::L, "\uE0E4"},
|
||||
std::pair{Button::R, "\uE0E5"},
|
||||
std::pair{Button::L, "\uE0E6"},
|
||||
std::pair{Button::R, "\uE0E7"},
|
||||
std::pair{Button::L2, "\uE0E8"},
|
||||
std::pair{Button::R2, "\uE0E9"},
|
||||
std::pair{Button::L2, "\uE0E6"},
|
||||
std::pair{Button::R2, "\uE0E7"},
|
||||
std::pair{Button::UP, "\uE0EB"},
|
||||
std::pair{Button::DOWN, "\uE0EC"},
|
||||
std::pair{Button::LEFT, "\uE0ED"},
|
||||
@@ -33,25 +35,42 @@ constexpr std::array buttons = {
|
||||
std::pair{Button::R3, "\uE105"},
|
||||
};
|
||||
|
||||
// NEW ---------------------
|
||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
|
||||
nvgBeginPath(vg);
|
||||
if (rounded) {
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||
} else {
|
||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||
// software based clipping, saves a few cpu cycles.
|
||||
bool ClipRect(float x, float y) {
|
||||
return x >= SCREEN_WIDTH || y >= SCREEN_HEIGHT;
|
||||
}
|
||||
|
||||
bool ClipText(float x, float y, int align) {
|
||||
if ((!(align & ALIGN_HOR) || (align & NVG_ALIGN_LEFT)) && x >= SCREEN_WIDTH) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!(align & ALIGN_VER) || (align & NVG_ALIGN_TOP)) && y >= SCREEN_HEIGHT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// NEW ---------------------
|
||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
|
||||
if (ClipRect(v.x, v.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
||||
nvgFillColor(vg, c);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
||||
nvgBeginPath(vg);
|
||||
if (rounded) {
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||
} else {
|
||||
nvgRect(vg, v.x, v.y, v.w, v.h);
|
||||
void drawRectIntenal(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
|
||||
if (ClipRect(v.x, v.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, rounded);
|
||||
nvgFillPaint(vg, p);
|
||||
nvgFill(vg);
|
||||
}
|
||||
@@ -128,6 +147,10 @@ void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, con
|
||||
}
|
||||
|
||||
void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, const Vec4& v, const NVGcolor& c) {
|
||||
if (ClipRect(v.x, v.y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto corner_radius = 0.5;
|
||||
drawRectOutlineInternal(vg, theme, size, v);
|
||||
nvgBeginPath(vg);
|
||||
@@ -137,6 +160,10 @@ void drawRectOutlineInternal(NVGcontext* vg, const Theme* theme, float size, con
|
||||
}
|
||||
|
||||
void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str, const char* end, int align, const NVGcolor& c) {
|
||||
if (ClipText(v.x, v.y, align)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgFontSize(vg, size);
|
||||
nvgTextAlign(vg, align);
|
||||
@@ -144,6 +171,24 @@ void drawTextIntenal(NVGcontext* vg, const Vec2& v, float size, const char* str,
|
||||
nvgText(vg, v.x, v.y, str, end);
|
||||
}
|
||||
|
||||
void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
|
||||
nvgBeginPath(vg);
|
||||
nvgMoveTo(vg, aX, aY);
|
||||
nvgLineTo(vg, bX, bY);
|
||||
nvgLineTo(vg, cX, cY);
|
||||
nvgFillColor(vg, c);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void drawTriangleInternal(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
||||
nvgBeginPath(vg);
|
||||
nvgMoveTo(vg, aX, aY);
|
||||
nvgLineTo(vg, bX, bY);
|
||||
nvgLineTo(vg, cX, cY);
|
||||
nvgFillPaint(vg, p);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* getButton(const Button want) {
|
||||
@@ -164,28 +209,20 @@ void drawTextArgs(NVGcontext* vg, float x, float y, float size, int align, const
|
||||
drawText(vg, x, y, size, buffer, nullptr, align, c);
|
||||
}
|
||||
|
||||
void drawImage(NVGcontext* vg, const Vec4& v, int texture) {
|
||||
void drawImage(NVGcontext* vg, const Vec4& v, int texture, float rounded) {
|
||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||
drawRect(vg, v, paint, false);
|
||||
drawRect(vg, v, paint, rounded);
|
||||
}
|
||||
|
||||
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture) {
|
||||
drawImage(vg, Vec4(x, y, w, h), texture);
|
||||
}
|
||||
|
||||
void drawImageRounded(NVGcontext* vg, const Vec4& v, int texture) {
|
||||
const auto paint = nvgImagePattern(vg, v.x, v.y, v.w, v.h, 0, texture, 1.f);
|
||||
nvgBeginPath(vg);
|
||||
nvgRoundedRect(vg, v.x, v.y, v.w, v.h, 15);
|
||||
nvgFillPaint(vg, paint);
|
||||
nvgFill(vg);
|
||||
}
|
||||
|
||||
void drawImageRounded(NVGcontext* vg, float x, float y, float w, float h, int texture) {
|
||||
drawImageRounded(vg, Vec4(x, y, w, h), texture);
|
||||
void drawImage(NVGcontext* vg, float x, float y, float w, float h, int texture, float rounded) {
|
||||
drawImage(vg, Vec4(x, y, w, h), texture, rounded);
|
||||
}
|
||||
|
||||
void drawTextBox(NVGcontext* vg, float x, float y, float size, float bound, const NVGcolor& c, const char* str, int align, const char* end) {
|
||||
if (ClipText(x, y, align)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nvgBeginPath(vg);
|
||||
nvgFontSize(vg, size);
|
||||
nvgTextAlign(vg, align);
|
||||
@@ -208,19 +245,19 @@ void dimBackground(NVGcontext* vg) {
|
||||
drawRectIntenal(vg, {0.f,0.f,SCREEN_WIDTH,SCREEN_HEIGHT}, nvgRGBA(0, 0, 0, 180), false);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, bool rounded) {
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGcolor& c, float rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, c, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, bool rounded) {
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGcolor& c, float rounded) {
|
||||
drawRectIntenal(vg, v, c, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, bool rounded) {
|
||||
void drawRect(NVGcontext* vg, float x, float y, float w, float h, const NVGpaint& p, float rounded) {
|
||||
drawRectIntenal(vg, {x,y,w,h}, p, rounded);
|
||||
}
|
||||
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, bool rounded) {
|
||||
void drawRect(NVGcontext* vg, const Vec4& v, const NVGpaint& p, float rounded) {
|
||||
drawRectIntenal(vg, v, p, rounded);
|
||||
}
|
||||
|
||||
@@ -290,6 +327,63 @@ void drawScrollbar2(NVGcontext* vg, const Theme* theme, s64 index_off, s64 count
|
||||
drawScrollbar2(vg, theme, SCREEN_WIDTH - 50, 100, SCREEN_HEIGHT-200, index_off, count, row, page);
|
||||
}
|
||||
|
||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGcolor& c) {
|
||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, c);
|
||||
}
|
||||
|
||||
void drawTriangle(NVGcontext* vg, float aX, float aY, float bX, float bY, float cX, float cY, const NVGpaint& p) {
|
||||
drawTriangleInternal(vg, aX, aY, bX, bY, cX, cY, p);
|
||||
}
|
||||
|
||||
void drawAppLable(NVGcontext* vg, const Theme* theme, ScrollingText& st, float x, float y, float w, const char* name) {
|
||||
// todo: no more 5am code
|
||||
const float max_box_w = 392.f;
|
||||
const float box_h = 48.f;
|
||||
// used for adjusting the position of the box.
|
||||
const float clip_pad = 25.f;
|
||||
const float clip_left = clip_pad;
|
||||
const float clip_right = 1220.f - clip_pad;
|
||||
const float text_pad = 25.f;
|
||||
const float font_size = 22.f;
|
||||
|
||||
nvgTextAlign(vg, NVG_ALIGN_LEFT);
|
||||
nvgFontSize(vg, font_size);
|
||||
float bounds[4]{};
|
||||
nvgTextBounds(vg, 0, 0, name, NULL, bounds);
|
||||
|
||||
const float trinaglex = x + (w / 2.f) - 9.f;
|
||||
const float trinagley = y - 14.f;
|
||||
const float center_x = x + (w / 2.f);
|
||||
const float y_offset = y - 62.f; // top of box
|
||||
const float text_width = bounds[2];
|
||||
float box_w = text_width + text_pad * 2;
|
||||
if (box_w > max_box_w) {
|
||||
box_w = max_box_w;
|
||||
}
|
||||
|
||||
float box_x = center_x - (box_w / 2.f);
|
||||
if (box_x < clip_left) {
|
||||
box_x = clip_left;
|
||||
}
|
||||
if ((box_x + box_w) > clip_right) {
|
||||
// box_x -= ((box_x + box_w) - clip_right) / 2;
|
||||
box_x = (clip_right - box_w);
|
||||
}
|
||||
|
||||
const float text_x = box_x + text_pad;
|
||||
const float text_y = y_offset + (box_h / 2.f);
|
||||
|
||||
drawRect(vg, {x-4, y-4, w+8, w+8}, theme->GetColour(ThemeEntryID_GRID));
|
||||
nvgBeginPath(vg);
|
||||
|
||||
nvgRoundedRect(vg, box_x, y_offset, box_w, box_h, 3.f);
|
||||
nvgFillColor(vg, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
||||
nvgFill(vg);
|
||||
|
||||
drawTriangle(vg, trinaglex, trinagley, trinaglex + 18.f, trinagley, trinaglex + 9.f, trinagley + 12.f, theme->GetColour(ThemeEntryID_SELECTED_BACKGROUND));
|
||||
st.Draw(vg, true, text_x, text_y, box_w - text_pad * 2, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_SELECTED), name);
|
||||
}
|
||||
|
||||
#define HIGHLIGHT_SPEED 350.0
|
||||
|
||||
static double highlightGradientX = 0;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user